diff --git a/dotfiles/.config/ml4w/scripts/ml4w-autostart b/dotfiles/.config/ml4w/scripts/ml4w-autostart index b7038904e..a19f0a951 100755 --- a/dotfiles/.config/ml4w/scripts/ml4w-autostart +++ b/dotfiles/.config/ml4w/scripts/ml4w-autostart @@ -4,6 +4,7 @@ CACHE_FOLDER="$HOME/.cache/ml4w/hyprland-dotfiles" CACHE_FILE="$CACHE_FOLDER/current_wallpaper" DEFAULT_WALLPAPER="$HOME/.config/ml4w/wallpapers/default.jpg" +AWWW_CACHE_FOLDER="$HOME/.cache/awww" # Colors for UI RED='\033[0;31m' @@ -19,7 +20,7 @@ error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } # Ensure that cache folder exists mkdir -p $HOME/.cache -# Set Wallpaper +# Set ML4W Wallpaper Cache mkdir -p $CACHE_FOLDER if [ ! -f $CACHE_FILE ]; then touch $CACHE_FILE @@ -27,7 +28,16 @@ if [ ! -f $CACHE_FILE ]; then info "Cache file created" fi -~/.config/ml4w/scripts/ml4w-wallpaper "$(cat $CACHE_FILE)" & +# Start awww-daemon +if command -v awww-daemon &> /dev/null; then + pgrep -x "awww-daemon" || awww-daemon & sleep 0.5 +fi + +# Check to see if awww cache exists; if not, call wallpaper script explicitly +[ -d "$AWWW_CACHE_FOLDER" ] && [ "$(ls -A "$AWWW_CACHE_FOLDER")" ] && HAS_AWWW_CACHE=true || HAS_AWWW_CACHE=false +if ! $HAS_AWWW_CACHE; then + ~/.config/ml4w/scripts/ml4w-wallpaper "$(cat $CACHE_FILE)" & +fi # Start Quickshell killall qs diff --git a/dotfiles/.config/ml4w/scripts/ml4w-wallpaper b/dotfiles/.config/ml4w/scripts/ml4w-wallpaper index 403acca32..0a74e1714 100755 --- a/dotfiles/.config/ml4w/scripts/ml4w-wallpaper +++ b/dotfiles/.config/ml4w/scripts/ml4w-wallpaper @@ -4,7 +4,10 @@ IMAGE_PATH="" EFFECT="" NOTIFICATIONS=false -SKIP=false +SKIP_WALLPAPER=false +SKIP_THEMING=false +MONITOR_OUTPUT="" +AWWW_CROP_GRAVITY="center" CACHE_FOLDER="$HOME/.cache/ml4w/hyprland-dotfiles" CACHE_FILE="$CACHE_FOLDER/current_wallpaper" DEFAULT_WALLPAPER="$HOME/.config/ml4w/wallpapers/default.jpg" @@ -46,9 +49,13 @@ show_help() { echo "Optional Parameters:" echo " --effect NAME Apply a specific wallpaper effect." echo " --random FOLDER Select a random image from FOLDER." - echo " --notifications false Disable desktop notifications." + echo " --notifications BOOL Show or suppress desktop notifications (default false)." echo " --help Show this help message and exit." - echo " --skip Will skip setting the wallpaper with awww." + echo " --skip-wallpaper Will skip setting the wallpaper with awww." + echo " --skip-theming Will skip updating OS themes based on new wallpaper." + echo " --skip Alias for \"--skip-wallpaper\"." + echo " --monitor Specify which input to apply wallpaper change (default applies to all outputs)" + echo " --crop-gravity Specify where to anchor wallpaper image in case cropping is needed (values from 'awww img --crop-gravity; default is center)" echo "" } @@ -73,8 +80,12 @@ fi while [[ $# -gt 0 ]]; do case "$1" in - --skip) - SKIP=true + --skip|--skip-wallpaper) + SKIP_WALLPAPER=true + shift 1 + ;; + --skip-theming) + SKIP_THEMING=true shift 1 ;; --effect) @@ -111,9 +122,22 @@ while [[ $# -gt 0 ]]; do --notifications) if [[ "$2" == "false" ]]; then NOTIFICATIONS=false + elif [[ "$2" == "true" ]]; then + NOTIFICATIONS=true + else + echo "Error: --notification accpeted values are true or false" + exit 1 fi shift 2 ;; + --monitor) + MONITOR_OUTPUT="$2" + shift 2 + ;; + --crop-gravity) + AWWW_CROP_GRAVITY="$2" + shift 2 + ;; --help) show_help exit 0 @@ -159,16 +183,6 @@ if [ ! -d $CACHE_FOLDER ]; then mkdir -p $CACHE_FOLDER fi -# Ensure awww-daemon is running -if ! pgrep -x "awww-daemon" > /dev/null; then - mkdir -p "$HOME/.cache/awww" - info "Starting awww-daemon..." - awww-daemon & - - # Give the daemon a brief moment to initialize before sending commands - sleep 1 -fi - # Write image path to cache file if [ ! -f "$CACHE_FILE" ]; then touch "$CACHE_FILE" @@ -193,17 +207,49 @@ if [ -f "$SETTINGS_WALLPAPER_EFFECT" ]; then fi # Set the Wallpaper -if [ "$SKIP" = true ]; then +if [ "$SKIP_WALLPAPER" = true ]; then info "Setting wallpaper skipped" else - info "Setting wallpaper: $IMAGE_PATH" + # Ensure awww-daemon is running + if ! pgrep -x "awww-daemon" > /dev/null; then + mkdir -p "$HOME/.cache/awww" + info "Starting awww-daemon..." + awww-daemon & + + # Give the daemon a brief moment to initialize before sending commands + sleep 1 + fi + + # Crop gravity with awww is only supported on 0.12.1 and newer versions + read AWWW_MINOR AWWW_PATCH <<< $(awww --version | awk -F. '{print $2 " " $3}') + (( $AWWW_MINOR > 12 || $AWWW_MINOR == 12 && $AWWW_PATCH > 0 )) \ + && AWWW_CROP_GRAVITY_ARGS="--resize crop --crop-gravity $AWWW_CROP_GRAVITY" \ + || AWWW_CROP_GRAVITY_ARGS="" + + # Update wallpaper + info "Setting wallpaper: $IMAGE_PATH${MONITOR_OUTPUT:+ on monitor $MONITOR_OUTPUT}" info "Using wallpaper effect $SETTINGS_TRANSITION_EFFECT" - awww img "$IMAGE_PATH" --transition-type $SETTINGS_TRANSITION_EFFECT + awww img "$IMAGE_PATH" \ + ${MONITOR_OUTPUT:+-o $MONITOR_OUTPUT} \ + $AWWW_CROP_GRAVITY_ARGS \ + --transition-type $SETTINGS_TRANSITION_EFFECT + info "Wallpaper Updated: Successfully set $(basename "$IMAGE_PATH")${MONITOR_OUTPUT:+ on monitor $MONITOR_OUTPUT}" +fi + +# If skipping theming, notify then exit +if "$SKIP_THEMING"; then + info "Updating theming skipped" + if [ "$SKIP_WALLPAPER" = false ]; then + send_notification \ + "Wallpaper Update Completed" \ + "Successfully updated wallpaper $(basename "$IMAGE_PATH")${MONITOR_OUTPUT:+ on output $MONITOR_OUTPUT}" + fi + exit 0 fi # Detect Theme SETTINGS_FILE="$HOME/.config/gtk-3.0/settings.ini" -THEME_PREF=$(grep -E '^gtk-application-prefer-dark-theme=' "$SETTINGS_FILE" | awk -F'=' '{print $2}') +THEME_PREF=$(grep -E '^gtk-application-prefer-dark-theme=' "$SETTINGS_FILE" | awk -F'=' '{print ($2 == 1 ? "dark" : "light")}') # Determine matugen binary path if [ -f $HOME/.cargo/bin/matugen ]; then @@ -215,11 +261,7 @@ else fi # Execute matugen -if [ "$THEME_PREF" -eq 1 ]; then - $MATUGEN_BIN image "$IMAGE_PATH" --source-color-index 0 -m "dark" -else - $MATUGEN_BIN image "$IMAGE_PATH" --source-color-index 0 -m "light" -fi +$MATUGEN_BIN image "$IMAGE_PATH" --source-color-index 0 -m "$THEME_PREF" info "Matugen updated" # Update Quickshell theme @@ -271,9 +313,6 @@ magick "$IMAGE_PATH" -gravity Center -extent 1:1 "$SQUARE_WALLPAPER" info "$(basename "$SQUARE_WALLPAPER") created" # Create rasi file -if [ ! -f $RASI_FILE ]; then - touch $RASI_FILE -fi echo "* { current-image: url(\"$BLURRED_WALLPAPER\", height); }" > "$RASI_FILE" info "$(basename "$RASI_FILE") written with $(basename "$BLURRED_WALLPAPER")" @@ -284,6 +323,7 @@ info "$(basename "$RASI_FILE") written with $(basename "$BLURRED_WALLPAPER")" # fi # Notification -send_notification "Wallpaper Update Completed" "Successfully set and processed $(basename "$IMAGE_PATH")" -info "Wallpaper Updated: Successfully set $(basename "$IMAGE_PATH")" -exit 0 \ No newline at end of file +send_notification \ + "Wallpaper Update Completed" \ + "Successfully set and processed $(basename "$IMAGE_PATH")${MONITOR_OUTPUT:+ on monitor $MONITOR_OUTPUT}" +exit 0 diff --git a/dotfiles/.config/quickshell/WallpaperApp/WallpaperWindow.qml b/dotfiles/.config/quickshell/WallpaperApp/WallpaperWindow.qml index f8528442e..5b8b04c7f 100644 --- a/dotfiles/.config/quickshell/WallpaperApp/WallpaperWindow.qml +++ b/dotfiles/.config/quickshell/WallpaperApp/WallpaperWindow.qml @@ -182,6 +182,121 @@ PanelWindow { } } + component ML4WComboBox: ComboBox { + id: ml4wComboBox + delegate: ItemDelegate { + id: itemDelegate + width: ml4wComboBox.width + contentItem: Text { + text: ml4wComboBox.textRole ? (modelData[ml4wComboBox.textRole] ?? "") : modelData + color: itemDelegate.highlighted ? Theme.background : Theme.primary + font.family: Theme.fontFamily + font.pixelSize: 14 + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + background: Rectangle { + color: itemDelegate.highlighted ? Theme.primary : "transparent" + radius: 4 + } + highlighted: ml4wComboBox.highlightedIndex === index + } + contentItem: Text { + leftPadding: 12 + rightPadding: ml4wComboBox.indicator.width + 12 + text: ml4wComboBox.displayText + font.family: Theme.fontFamily + font.pixelSize: 14 + color: Theme.primary + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + indicator: Canvas { + id: canvas + x: ml4wComboBox.width - width - 12 + y: (ml4wComboBox.height - height) / 2 + width: 12 + height: 8 + contextType: "2d" + + onPaint: { + var ctx = getContext("2d") + ctx.reset(); + ctx.moveTo(0, 0); + ctx.lineTo(width, 0); + ctx.lineTo(width / 2, height); + ctx.closePath(); + ctx.fillStyle = Theme.primary; + ctx.fill(); + } + + Connections { + target: Theme + function onPrimaryChanged() { canvas.requestPaint(); } + } + } + background: Rectangle { + implicitHeight: 36 + color: Theme.background + border.color: Theme.primary + border.width: 1 + radius: 10 + } + popup: Popup { + y: ml4wComboBox.height + 2 + width: ml4wComboBox.width + implicitHeight: contentItem.contentHeight > 250 ? 250 : contentItem.contentHeight + padding: 4 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: ml4wComboBox.popup.visible ? ml4wComboBox.delegateModel : null + currentIndex: ml4wComboBox.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator { } + } + background: Rectangle { + color: Theme.background + border.color: Theme.primary + border.width: 1 + radius: 8 + } + } + } + + component ML4WCheckBox : CheckBox { + id: ml4wCheckBox + spacing: 10 + indicator: Rectangle { + implicitWidth: 18 + implicitHeight: 18 + x: ml4wCheckBox.leftPadding + y: (ml4wCheckBox.height - height) / 2 + radius: 4 + border.color: Theme.primary + border.width: 1 + color: "transparent" + Rectangle { + width: 8 + height: 8 + anchors.centerIn: parent + color: Theme.primary + visible: ml4wCheckBox.checked + } + } + contentItem: Text { + text: ml4wCheckBox.text + font.family: Theme.fontFamily + color: Theme.primary + font.pixelSize: 14 + verticalAlignment: Text.AlignVCenter + leftPadding: ml4wCheckBox.indicator.width + ml4wCheckBox.spacing + } + } + Item { anchors.fill: parent anchors.margins: 20 @@ -289,9 +404,13 @@ PanelWindow { text: advancedSettingsLabel() onClicked: { advancedOptions.visible = !advancedOptions.visible + if (!advancedOptions.visible) { + outputMonitorSelector.currentIndex = 0 + wallpaperPositioningSelector.currentIndex = 0 + shouldUpdateTheming.checked = true + } } } - } } } @@ -355,112 +474,96 @@ PanelWindow { id: transitionEffectInputLabel color: Theme.primary font.family: Theme.fontFamily - text: "Transition Effect" - Accessible.name: text Accessible.role: Accessible.StaticText } - ComboBox { + ML4WComboBox { id: transitionEffectComboBox model: root.transitionEffects currentIndex: root.transitionEffects.indexOf(root.transitionEffect) Layout.fillWidth: true - onActivated: { const selectedEffect = root.transitionEffects[index]; console.log("Updating wallpaper transition effect to \"" + selectedEffect + "\"") transitionEffectSettingFileHandler.setText(selectedEffect) } + } + } - delegate: ItemDelegate { - id: itemDelegate - width: transitionEffectComboBox.width - contentItem: Text { - text: modelData - color: itemDelegate.highlighted ? Theme.background : Theme.primary - font.family: Theme.fontFamily - font.pixelSize: 14 - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - background: Rectangle { - color: itemDelegate.highlighted ? Theme.primary : "transparent" - radius: 4 - } - highlighted: transitionEffectComboBox.highlightedIndex === index + // --- OUTPUT SETTINGS --- + RowLayout { + ColumnLayout { + Label { + id: outputSettingsLabel + color: Theme.primary + font.family: Theme.fontFamily + + text: "Output Monitor" + + Accessible.name: text + Accessible.role: Accessible.StaticText } - indicator: Canvas { - id: canvas - x: transitionEffectComboBox.width - width - 12 - y: (transitionEffectComboBox.height - height) / 2 - width: 12 - height: 8 - contextType: "2d" - - onPaint: { - context.reset(); - context.moveTo(0, 0); - context.lineTo(width, 0); - context.lineTo(width / 2, height); - context.closePath(); - context.fillStyle = Theme.primary; - context.fill(); - } + ML4WComboBox { + id: outputMonitorSelector + model: monitorModel + textRole: "name" + Layout.fillWidth: true - Connections { - target: transitionEffectComboBox - function onPressedChanged() { canvas.requestPaint(); } + property var monitorModel: { + let list = [{"name": "All", "isSingleOutput": false}] + for (let i = 0; i < Hyprland.monitors.values.length; i++) { + list.push({"name": Hyprland.monitors.values[i].name, "isSingleOutput": true}) + } + return list } - } - contentItem: Text { - leftPadding: 12 - rightPadding: transitionEffectComboBox.indicator.width + 12 - text: transitionEffectComboBox.displayText - font.family: Theme.fontFamily - font.pixelSize: 14 - color: Theme.primary - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - } + Accessible.name: wallpaperDirInputLabel.text + Accessible.description: qsTr("Select which output to change the wallpaper (or change for all outputs)") + Accessible.role: Accessible.ComboBox - background: Rectangle { - implicitHeight: 36 - color: Theme.background - border.color: Theme.primary - border.width: 1 - radius: 10 } + } - popup: Popup { - y: transitionEffectComboBox.height + 2 - width: transitionEffectComboBox.width - implicitHeight: contentItem.contentHeight > 250 ? 250 : contentItem.contentHeight - padding: 4 + ColumnLayout { + Label { + id: wallpaperPositioningLabel + color: Theme.primary + font.family: Theme.fontFamily - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: transitionEffectComboBox.popup.visible ? transitionEffectComboBox.delegateModel : null - currentIndex: transitionEffectComboBox.highlightedIndex + text: "Wallpaper Positioning" - ScrollIndicator.vertical: ScrollIndicator { } - } + Accessible.name: text + Accessible.role: Accessible.StaticText + } - background: Rectangle { - color: Theme.background - border.color: Theme.primary - border.width: 1 - radius: 8 - } + ML4WComboBox { + id: wallpaperPositioningSelector + model: [ + "center", + "top-left", "top", "top-right", + "left", "right", + "bottom-left", "bottom", "bottom-right", + ] + Layout.fillWidth: true + Accessible.name: wallpaperPositioningLabel.text + Accessible.description: qsTr("How to align wallpaper when setting (default is centered)") + Accessible.role: Accessible.ComboBox } } } + + ML4WCheckBox { + id: shouldUpdateTheming + checked: true + text: "Update theming from wallpaper" + + Accessible.name: text + Accessible.description: qsTr("Choose whether to update theming based on the new wallpaper selector") + Accessible.role: Accessible.CheckBox + } } Rectangle { @@ -484,6 +587,7 @@ PanelWindow { text: "Wallpaper folder is either empty or invalid." } + // --- IMAGE GRID --- GridView { id: grid @@ -613,7 +717,23 @@ PanelWindow { onClicked: { let scriptPath = Quickshell.env("HOME") + "/.config/ml4w/scripts/ml4w-wallpaper"; - Quickshell.execDetached(["bash", "-c", scriptPath + " '" + model.filePath + "'"]); + let options = "" + if (advancedOptions.visible) { + const outputSelection = outputMonitorSelector.currentValue + const outputParams = outputSelection.isSingleOutput + ? " --monitor " + outputSelection.name + : "" + const positioningParams = " --crop-gravity " + wallpaperPositioningSelector.currentText + const themingParams = shouldUpdateTheming.checked + ? "" + : " --skip-theming" + options = `${outputParams}${positioningParams}${themingParams}` + } + Quickshell.execDetached([ + "bash", + "-c", + scriptPath + " '" + model.filePath + "'" + options + ]); } } }