#!/bin/bash # omarchy:summary=Toggle a scratch workspace (scratchpad, claude, fastmail, beeper), starting its resident apps when any is missing. set -uo pipefail # Usage: omarchy-toggle-scratch [scratchpad|claude|fastmail|beeper] # # 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=("so.stencil.tern|tern") ;; claude) NAME="claude" HOME_MONITOR="Apple Computer Inc StudioDisplay 0x984262A7" MEMBERS=("com.anthropic.Claude|claude-desktop") ;; fastmail) NAME="fastmail" MEMBERS=("com.fastmail.Fastmail|gtk-launch fastmail") ;; beeper) NAME="beeper" MEMBERS=("Beeper|gtk-launch beeper") ;; *) echo "usage: ${0##*/} [scratchpad|claude|fastmail|beeper]" >&2 exit 2 ;; esac WORKSPACE="special:$NAME" # 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' } scratch_visible() { [[ -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). 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 } # 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 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 if ((launched)); then # Reveal once the first newcomer lands (Claude takes a few seconds); any # later ones 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)"