diff --git a/dot_local/bin/executable_omarchy-toggle-scratch b/dot_local/bin/executable_omarchy-toggle-scratch index 05da444..abfce71 100755 --- a/dot_local/bin/executable_omarchy-toggle-scratch +++ b/dot_local/bin/executable_omarchy-toggle-scratch @@ -1,45 +1,123 @@ #!/bin/bash -# omarchy:summary=Toggle the scratchpad, starting the kitty/herdr scratch terminal when it holds no windows. +# omarchy:summary=Toggle a scratch workspace (scratchpad, fastmail, beeper), starting its resident apps when any is missing. set -uo pipefail -NAME="scratchpad" +# Usage: omarchy-toggle-scratch [scratchpad|fastmail|beeper|youtube] +# +# Each profile is a special workspace, the monitor it opens on, and its +# resident apps (class pattern -> launch command; the pattern is an anchored +# ERE, so a plain class name matches itself). hypr/hyprland.lua pins every +# class to its workspace, so only the launch and the reveal are left here. A +# resident is launched only when no window on the workspace matches it, which +# is what keeps a launcher that always opens a fresh window (the Chromium +# web apps) from piling up duplicates. On first start the master layout gives +# the master slot to whichever window appears first (a terminal beats an +# Electron app); swap them afterwards if the arrangement matters. +# +# HOME_MONITOR is a connector name or an EDID description (the latter, as in +# the workspace rules, survives a different DP connector after re-docking). +# When it is empty or that monitor is absent, the workspace opens on the +# focused one. +HOME_MONITOR="" +case ${1:-scratchpad} in +scratchpad) + NAME="scratchpad" + HOME_MONITOR="Apple Computer Inc StudioDisplay 0x984262A7" + MEMBERS=( + "com.anthropic.Claude|claude-desktop" + "kitty-herdr|kitty --class=kitty-herdr --config $HOME/.config/kitty/herdr.conf" + ) + ;; +fastmail) + NAME="fastmail" + MEMBERS=("com.fastmail.Fastmail|gtk-launch fastmail") + ;; +beeper) + NAME="beeper" + MEMBERS=("Beeper|gtk-launch beeper") + ;; +youtube) + NAME="youtube" + # Chromium derives the class from the app URL, and the same web app has + # shown up under more than one host spelling (youtube.com vs + # www.youtube.com) once a video is playing, so match the family. + MEMBERS=("brave-(www\\.)?youtube\\.com__.*|gtk-launch YouTube") + ;; +*) + echo "usage: ${0##*/} [scratchpad|fastmail|beeper|youtube]" >&2 + exit 2 + ;; +esac WORKSPACE="special:$NAME" -CLASS="kitty-herdr" -LAUNCH="kitty --class=$CLASS --config $HOME/.config/kitty/herdr.conf" -# Windows living on the scratchpad, whether it is shown or hidden. -scratch_windows() { - hyprctl clients -j | jq --arg ws "$WORKSPACE" '[.[] | select(.workspace.name == $ws)] | length' +# Classes of the windows living on the scratchpad, shown or hidden. +scratch_classes() { + hyprctl clients -j | jq -r --arg ws "$WORKSPACE" '.[] | select(.workspace.name == $ws) | .class' +} + +scratch_windows() { + scratch_classes | wc -l +} + +# Name of the monitor currently displaying the scratchpad, if any. +scratch_monitor() { + hyprctl monitors -j | jq -r --arg ws "$WORKSPACE" '.[] | select(.specialWorkspace.name == $ws) | .name' } -# True when some monitor currently displays the scratchpad. scratch_visible() { - hyprctl monitors -j | jq -e --arg ws "$WORKSPACE" 'any(.[]; .specialWorkspace.name == $ws)' >/dev/null + [[ -n $(scratch_monitor) ]] } # Hyprland's lua config takes lua expressions on `hyprctl dispatch`; the # classic dispatcher is the pre-lua fallback (same shape as omarchy-cycle). -toggle() { +focus_monitor() { + hyprctl dispatch "hl.dsp.focus({ monitor = \"$1\" })" >/dev/null 2>&1 || + hyprctl dispatch focusmonitor "$1" >/dev/null +} + +# togglespecialworkspace acts on the focused monitor, so focus decides where +# the scratchpad appears (or which copy hides). +toggle_on() { + focus_monitor "$1" hyprctl dispatch "hl.dsp.workspace.toggle_special(\"$NAME\")" >/dev/null 2>&1 || hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null } -if (($(scratch_windows) > 0)); then - toggle +# The profile's home monitor when connected, else whatever is focused. +target_monitor() { + hyprctl monitors -j | jq -r --arg m "$HOME_MONITOR" \ + 'first(.[] | select(.name == $m or .description == $m)) // first(.[] | select(.focused)) | .name' +} + +# Showing: just hide, even if a member is gone. Missing members come back on +# the next reveal. +shown=$(scratch_monitor) +if [[ -n $shown ]]; then + toggle_on "$shown" exit 0 fi -# Empty scratchpad: there is nothing to toggle into view, so start the herdr -# terminal instead. hypr/hyprland.lua pins kitty-herdr to the scratchpad, so -# only the reveal is left here — and deciding it after the window lands keeps -# this correct whether or not the rule already showed the workspace. -setsid uwsm-app -- $LAUNCH >/dev/null 2>&1 & - -for _ in {1..40}; do - sleep 0.1 - (($(scratch_windows) > 0)) && break +present=$(scratch_classes) +before=$(grep -c . <<<"$present") +launched=0 +for member in "${MEMBERS[@]}"; do + pattern=${member%%|*} + grep -qE "^(${pattern})$" <<<"$present" && continue + setsid uwsm-app -- ${member#*|} >/dev/null 2>&1 & + launched=1 done -scratch_visible || toggle +if ((launched)); then + # Reveal once the first newcomer lands; later ones (Claude takes a few + # seconds) drop straight onto the now-visible scratchpad. Deciding the + # reveal after the window lands keeps this correct whether or not the rule + # already showed the workspace. + for _ in {1..80}; do + sleep 0.1 + (($(scratch_windows) > before)) && break + done +fi + +scratch_visible || toggle_on "$(target_monitor)"