Desktop & Dock becomes Shell — Bar, Dock, Control Center, Tiling, Workspaces — the home for everything Quickshell draws. The settings- management cluster moves to System as Sync & Backup, Appearance's Shell tab dissolves, and 24-hour time finally lives on Date & Time, which always owned it. The bar gets what it never had: a way to survive the wallpaper. A second neutral text family (follow theme, or forced light or dark), a one-layer shadow under every glyph, and a gradient scrim for wallpapers nothing else survives — all off by default, pixel-identical until asked. Widgets earn toggles (weather, media, clipboard, calendar countdown), the vitals cluster stops leaving a dead pill behind, and Control Center's sections learn to step aside. The dock graduates from MVP: a context menu with window rows, pin, unpin, quit and new-window; scroll an icon to cycle its windows; drag to reorder on the dock itself; hover previews with one-shot captures; and "Add App to Dock" in the launcher. Three real bugs died en route — menus that slid away with the autohide, a readonly-property crash on every menu open, and a drag that drifted half a slot per icon on side docks. The pinned-apps editor in Settings becomes a drag strip. 166 contracts; the full suite is green except two live display and switcher tests that cannot run behind a locked session — re-verified on unlock. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
174 lines
8.1 KiB
Bash
Executable File
174 lines
8.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Project sessions: the windows you open together, opened together.
|
|
#
|
|
# Three things here are easy to get wrong and were, each caught by running it
|
|
# rather than reading it:
|
|
#
|
|
# * A terminal's directory is not the terminal's own working directory. That
|
|
# is where the terminal was launched from; the shell inside it is what has
|
|
# been cd'd. Reading the wrong one appeared correct for exactly as long as
|
|
# the test terminals happened to have been started from the right place.
|
|
# * gtk-launch cannot place a window. It activates over D-Bus, so the process
|
|
# Hyprland started exits and a [workspace N silent] rule has no child to
|
|
# apply to -- the window lands wherever you were looking.
|
|
# * DBusActivatable applications do the same thing even when launched
|
|
# directly, so the window has to be found and moved afterwards.
|
|
#
|
|
# The save/open round trip is exercised for real, against a fake HOME and a
|
|
# stubbed hyprctl, so no window is opened on the machine running the tests.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
helper="$repo_dir/config/dot/quickshell/scripts/panama-project"
|
|
workspaces_page="$repo_dir/config/dot/quickshell/modules/settings/WorkspacesPage.qml"
|
|
service="$repo_dir/config/dot/quickshell/services/Projects.qml"
|
|
|
|
findings=()
|
|
note() { findings+=("$1"); }
|
|
|
|
[[ -x "$helper" ]] || { printf 'projects contract: helper is missing or not executable\n' >&2; exit 1; }
|
|
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
mkdir -p "$work/bin" "$work/home"
|
|
|
|
# A compositor that reports two windows on two workspaces and accepts every
|
|
# dispatch, recording what it was asked to do.
|
|
cat >"$work/bin/hyprctl" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
if [[ "$1" == "-j" && "$2" == "clients" ]]; then
|
|
cat "$HYPRCTL_CLIENTS"
|
|
exit 0
|
|
fi
|
|
if [[ "$1" == "eval" ]]; then
|
|
printf '%s\n' "$2" >>"$HYPRCTL_LOG"
|
|
echo ok
|
|
exit 0
|
|
fi
|
|
echo "{}"
|
|
STUB
|
|
chmod +x "$work/bin/hyprctl"
|
|
|
|
cat >"$work/clients.json" <<'JSON'
|
|
[
|
|
{"address":"0x1","class":"kitty","workspace":{"name":"2"},"floating":false,"pid":1},
|
|
{"address":"0x2","class":"org.gnome.Nautilus","workspace":{"name":"3"},"floating":false,"pid":2},
|
|
{"address":"0x3","class":"bitwarden","workspace":{"name":"special:scratch"},"floating":true,"pid":3}
|
|
]
|
|
JSON
|
|
|
|
export HYPRCTL_CLIENTS="$work/clients.json"
|
|
export HYPRCTL_LOG="$work/dispatch.log"
|
|
: >"$HYPRCTL_LOG"
|
|
|
|
run() { PATH="$work/bin:$PATH" HOME="$work/home" XDG_DATA_HOME="$work/home/.local/share" "$helper" "$@"; }
|
|
|
|
# ── Saving ───────────────────────────────────────────────────────────────────
|
|
|
|
saved="$(run save demo)"
|
|
[[ "$(jq -r '.ok' <<<"$saved")" == "true" ]] || note "saving a layout failed: $saved"
|
|
|
|
# Two workspaces, not three: the scratchpad is somewhere things are put aside,
|
|
# not part of a layout, and a floating window is not a tiled arrangement.
|
|
[[ "$(jq -r '.workspaces' <<<"$saved")" == "2" ]] \
|
|
|| note "a layout with 2 tiled workspaces and a scratchpad saved $(jq -r '.workspaces' <<<"$saved") workspaces"
|
|
[[ "$(jq -r '.windows' <<<"$saved")" == "2" ]] \
|
|
|| note 'the scratchpad or a floating window was recorded as part of the layout'
|
|
|
|
stored="$work/home/.local/share/panama/projects/demo.json"
|
|
[[ -f "$stored" ]] || note 'saving produced no project file'
|
|
|
|
if [[ -f "$stored" ]]; then
|
|
# Positions, not numbers. A project saved from 2 and 3 must not insist on 2
|
|
# and 3, or opening it lands on top of whatever is there now.
|
|
jq -e '.workspaces | type == "array"' "$stored" >/dev/null 2>&1 \
|
|
|| note 'workspaces are not stored as an ordered list of positions'
|
|
jq -e '[.workspaces[].windows[] | has("index")] | any' "$stored" >/dev/null 2>&1 \
|
|
&& note 'a workspace number was recorded, so the project would demand specific workspaces'
|
|
|
|
# gtk-launch cannot place a window; the Exec line can.
|
|
jq -r '.workspaces[].windows[].exec // ""' "$stored" | grep -q 'gtk-launch' \
|
|
&& note 'projects launch through gtk-launch, which activates over D-Bus and cannot be placed on a workspace'
|
|
fi
|
|
|
|
# ── Opening ──────────────────────────────────────────────────────────────────
|
|
|
|
: >"$HYPRCTL_LOG"
|
|
opened="$(run open demo)"
|
|
[[ "$(jq -r '.ok' <<<"$opened")" == "true" ]] || note "opening a project failed: $opened"
|
|
|
|
# Free workspaces. 2 and 3 are occupied by the stub's own windows, so a project
|
|
# saved from them must be laid out somewhere else entirely.
|
|
targets="$(jq -r '.workspaces | join(",")' <<<"$opened")"
|
|
[[ "$targets" == "1,4" ]] \
|
|
|| note "a project opened onto [$targets] when 2 and 3 were occupied; it should claim free workspaces"
|
|
|
|
grep -q 'workspace 1 silent' "$HYPRCTL_LOG" \
|
|
|| note 'windows are not launched with a workspace rule'
|
|
grep -q 'exec_cmd' "$HYPRCTL_LOG" \
|
|
|| note 'the Lua dispatch form is not used, and the plain hyprctl dispatch form is a syntax error on this config'
|
|
|
|
# ── Terminal directories ─────────────────────────────────────────────────────
|
|
|
|
# Exercised against a real process tree rather than grepped for. Checking that
|
|
# the source mentions a shell name proves only that somebody wrote the word: an
|
|
# earlier version of this contract passed against a helper that had been changed
|
|
# back to reading the wrong process, because the constant was still there.
|
|
#
|
|
# The shape below is the shape that matters -- a parent sitting in one directory
|
|
# with a shell child in another, which is exactly a terminal you have cd'd
|
|
# inside. The answer must be the child's directory.
|
|
mkdir -p "$work/parent" "$work/child"
|
|
# exec, so the subshell is replaced by python and $! is python's own pid rather
|
|
# than a shell that has already gone.
|
|
( cd "$work/parent" && exec python3 -c "
|
|
import subprocess, time
|
|
subprocess.Popen(['bash', '-c', 'sleep 30; :'], cwd='$work/child')
|
|
time.sleep(30)
|
|
" ) >/dev/null 2>&1 &
|
|
parent_pid=$!
|
|
sleep 2
|
|
if [[ -n "$parent_pid" ]]; then
|
|
answer="$(PATH="$work/bin:$PATH" HOME="$work/home" python3 - "$helper" "$parent_pid" <<'PROBE'
|
|
import importlib.machinery, importlib.util, sys
|
|
spec = importlib.util.spec_from_loader("pp",
|
|
importlib.machinery.SourceFileLoader("pp", sys.argv[1]))
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
print(module.working_directory(int(sys.argv[2])) or "")
|
|
PROBE
|
|
)"
|
|
[[ "$answer" == "$work/child" ]] \
|
|
|| note "a terminal's directory is read as '$answer' rather than the shell's '$work/child' -- the terminal own working directory is where it was launched from, not where you have changed to"
|
|
kill "$parent_pid" 2>/dev/null || true
|
|
fi
|
|
|
|
# -d, not --directory with a space, which kitty accepts and silently ignores.
|
|
grep -qE 'TERMINAL_DIRECTORY_FLAG = "-d"' "$helper" \
|
|
|| note 'the terminal directory flag is not -d; the space-separated long form is ignored without an error'
|
|
|
|
# ── Placement has to be verified, not assumed ────────────────────────────────
|
|
|
|
grep -q 'hl.dsp.window.move' "$helper" \
|
|
|| note 'a window that lands on the wrong workspace is never moved, so DBusActivatable applications stay where they appeared'
|
|
|
|
# ── The page ─────────────────────────────────────────────────────────────────
|
|
|
|
grep -q 'Projects.projects' "$workspaces_page" \
|
|
|| note 'the Workspaces page does not list saved projects'
|
|
grep -q 'confirmingProject' "$workspaces_page" \
|
|
|| note 'a project can be deleted without confirming'
|
|
grep -q '"list"' "$service" \
|
|
|| note 'the settings service never reads what is saved'
|
|
|
|
if (( ${#findings[@]} > 0 )); then
|
|
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
|
|
printf 'projects contract: %d finding(s)\n' "${#findings[@]}" >&2
|
|
printf ' - %s\n' "${findings[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'projects contract: PASS\n'
|