#!/bin/bash # omarchy:summary=Toggle a scratch workspace (scratchpad, claude, fastmail, beeper, home, lmstudio), starting its resident apps when any is missing. set -uo pipefail # Usage: omarchy-toggle-scratch [scratchpad|claude|fastmail|beeper|home|lmstudio] # # 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. The scrolling layout gives each # resident its own column, in the order they appear. # # HOME_MONITOR lists connector names or EDID descriptions, separated by |; # the first one connected wins (descriptions, as in the workspace rules, # survive a different DP connector after re-docking). When it is empty or # none is connected, the workspace opens on the focused monitor. EXTERNAL is # the main external display: the Studio Display, else the Samsung ultrawide # (same order as hypr/monitors.lua). EXTERNAL="Apple Computer Inc StudioDisplay 0x984262A7|Samsung Electric Company S34CG50 HNTY501103" HOME_MONITOR="" case ${1:-scratchpad} in scratchpad) NAME="scratchpad" HOME_MONITOR=$EXTERNAL MEMBERS=("so.stencil.tern|tern") ;; claude) NAME="claude" HOME_MONITOR=$EXTERNAL MEMBERS=("com.anthropic.Claude|claude-desktop") ;; fastmail) NAME="fastmail" HOME_MONITOR="eDP-1" MEMBERS=("com.fastmail.Fastmail|gtk-launch com.fastmail.Fastmail") ;; beeper) NAME="beeper" HOME_MONITOR="eDP-1" MEMBERS=("Beeper|gtk-launch beeper") ;; home) NAME="home" MEMBERS=("brave-home\.google\.com__.*|gtk-launch 'Google Home'") ;; lmstudio) NAME="lmstudio" MEMBERS=("ai\.elementlabs\.lmstudio|gtk-launch ai.elementlabs.lmstudio") ;; *) echo "usage: ${0##*/} [scratchpad|claude|fastmail|beeper|home|lmstudio]" >&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 first connected home monitor, else whatever is focused. target_monitor() { hyprctl monitors -j | jq -r --arg m "$HOME_MONITOR" ' . as $monitors | first(($m | split("|"))[] as $want | $monitors[] | select(.name == $want or .description == $want)) // first($monitors[] | 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 # eval so a launch command can quote a desktop id with spaces. eval "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)"