Three things that were parked, and the reasons they were parked turned out to be the useful part of doing them. The Dock can sit on the left or the right as well as the bottom. Everything that assumed the bottom edge is now asked which edge it is on: the anchors, the axis that gets an implicit size, the sliver of input region that survives hiding, the direction the body slides away in, and which side a tooltip opens towards. The body was a Row and is a Grid, because one declaration then serves both orientations -- Row and Column would each need their own children, and the cross-axis anchors that centre items in a Row are the wrong axis in a Column. Bottom is unchanged in every particular, and the settings default to it, so a hot reload in the middle of this work left the running dock exactly where it was. One bug worth recording because static review would never have found it: a dock spans the edge it lives on, which means anchoring BOTH ends of that edge. The first side dock anchored top and left only, was free to collapse to its implicit height, and came out one pixel tall. It parsed, it loaded, and it rendered nothing. The contract measures the geometry rather than reading the source for that reason, and was verified by putting the single-ended anchor back. Per-screen is a list of names where empty means every screen, because a list is what goes stale when a display is unplugged and "all" should not be spelled as one. Turning off the last screen collapses to "all" rather than leaving no dock anywhere and no obvious way back. Pins can be dragged by a grip. The objection this file recorded for a long time was real -- dragging inside a Flickable inside a scrolling page fails in a way that reads as breakage -- and the answer is preventStealing on the grip, so the page cannot claim a gesture that started there. The arrow buttons stay: they are the keyboard-reachable path and a grip is not. The order is held locally during the drag and written once on release, rather than rewriting settings.json for every slot crossed. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
156 lines
6.2 KiB
Bash
Executable File
156 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The Dock on an edge other than the bottom, and on chosen screens.
|
|
#
|
|
# The rules:
|
|
#
|
|
# 1. A dock spans the edge it lives on, which means anchoring BOTH ends of
|
|
# that edge. Anchoring one leaves the surface free to collapse to its
|
|
# implicit size on that axis -- the first side dock written here came out
|
|
# one pixel tall, looked like nothing had rendered, and passed every static
|
|
# check. So the geometry is measured, not read.
|
|
# 2. Bottom is unchanged. Somebody who never touches the setting must get the
|
|
# dock they already had, byte for byte in behaviour.
|
|
# 3. Only one axis gets an implicit size. Setting both fights the anchors.
|
|
# 4. An empty screen list means every screen. A list of names goes stale the
|
|
# moment a display is unplugged, so "all" must not be spelled as one.
|
|
# 5. Reordering pins never loses or duplicates an entry.
|
|
# 6. The drag grip sets preventStealing. Without it the settings page's own
|
|
# Flickable claims the vertical gesture and the row never moves -- which is
|
|
# the exact objection this feature was refused over for a long time.
|
|
#
|
|
# The geometry checks launch isolated shells against a temporary config. The
|
|
# real settings are read to build them and never written.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
shell_dir="$repo_dir/config/dot/quickshell"
|
|
dock="$shell_dir/modules/dock/Dock.qml"
|
|
body="$shell_dir/modules/dock/DockBody.qml"
|
|
editor="$shell_dir/modules/settings/DockPinsEditor.qml"
|
|
|
|
fail() {
|
|
printf 'dock position contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
for path in "$dock" "$body" "$editor"; do
|
|
[[ -r "$path" ]] || fail "missing $path"
|
|
done
|
|
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work" "$shell_dir/dock-position-probe.qml"' EXIT
|
|
|
|
# ── 3, 4, 6. What can be read ───────────────────────────────────────────────
|
|
|
|
grep -q 'implicitHeight: root.vertical ? 0 :' "$dock" \
|
|
|| fail 'the dock sets an implicit height on both orientations, which fights the anchors'
|
|
grep -q 'implicitWidth: root.vertical ? ' "$dock" \
|
|
|| fail 'the dock has no implicit width for a side position'
|
|
|
|
grep -q 'wanted.length === 0' "$dock" \
|
|
|| fail 'an empty screen list is not treated as every screen'
|
|
|
|
grep -q 'preventStealing: true' "$editor" \
|
|
|| fail 'the drag grip does not set preventStealing, so the page will scroll instead of reordering'
|
|
# The arrows are the keyboard-reachable path and predate the grip. A grip is not
|
|
# a replacement for them.
|
|
grep -q 'text: "↑"' "$editor" \
|
|
|| fail 'the move-up button was removed, leaving no keyboard-reachable reorder'
|
|
|
|
# ── 5. Reordering keeps every entry, exactly once ───────────────────────────
|
|
|
|
python3 - <<'PY' || fail 'reordering loses or duplicates a pinned application'
|
|
def drag_to(working, dragging, target):
|
|
if dragging < 0 or target == dragging:
|
|
return working, dragging
|
|
if target < 0 or target >= len(working):
|
|
return working, dragging
|
|
nxt = working[:]
|
|
nxt.insert(target, nxt.pop(dragging))
|
|
return nxt, target
|
|
|
|
base = ["a", "b", "c", "d", "e"]
|
|
for start, targets in [(0, [1, 2, 3, 4]), (4, [3, 2, 1, 0]), (2, [3]), (2, [1]),
|
|
(0, [3, 3, 3]), (1, [-1]), (3, [99])]:
|
|
working, dragging = base[:], start
|
|
for target in targets:
|
|
working, dragging = drag_to(working, dragging, target)
|
|
if sorted(working) != sorted(base) or len(working) != len(base):
|
|
raise SystemExit(f'starting at {start} through {targets} produced {working}')
|
|
PY
|
|
|
|
# ── 1, 2. Geometry, measured ────────────────────────────────────────────────
|
|
|
|
cat >"$shell_dir/dock-position-probe.qml" <<'QML'
|
|
import Quickshell
|
|
import QtQuick
|
|
import qs.modules.dock
|
|
|
|
ShellRoot {
|
|
Dock { id: probe }
|
|
Timer {
|
|
interval: 1200; running: true
|
|
onTriggered: {
|
|
console.warn("DOCKGEOM " + probe.position
|
|
+ " vertical=" + probe.vertical
|
|
+ " w=" + Math.round(probe.width)
|
|
+ " h=" + Math.round(probe.height));
|
|
Qt.quit();
|
|
}
|
|
}
|
|
}
|
|
QML
|
|
|
|
settings_source="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json"
|
|
[[ -r "$settings_source" ]] || fail 'no settings to build a probe configuration from'
|
|
|
|
measure() {
|
|
local position="$1"
|
|
python3 - "$settings_source" "$work/panama/settings.json" "$position" <<'PY'
|
|
import json, pathlib, sys
|
|
data = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
|
data["dockPosition"] = sys.argv[3]
|
|
out = pathlib.Path(sys.argv[2])
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
out.write_text(json.dumps(data))
|
|
PY
|
|
( cd "$shell_dir" && XDG_CONFIG_HOME="$work" timeout 40 qs -p ./dock-position-probe.qml 2>&1 ) \
|
|
| grep -o 'DOCKGEOM .*' | head -1
|
|
}
|
|
|
|
bottom="$(measure bottom)"
|
|
[[ -n "$bottom" ]] || fail 'the bottom dock produced no geometry at all'
|
|
left="$(measure left)"
|
|
[[ -n "$left" ]] || fail 'the left dock produced no geometry at all'
|
|
|
|
python3 - "$bottom" "$left" <<'PY' || fail 'a dock does not span the edge it lives on'
|
|
import re, sys
|
|
|
|
def read(line):
|
|
m = re.search(r'DOCKGEOM (\w+) vertical=(\w+) w=(\d+) h=(\d+)', line)
|
|
if not m:
|
|
raise SystemExit(f'unreadable probe output: {line!r}')
|
|
return m.group(1), m.group(2) == 'true', int(m.group(3)), int(m.group(4))
|
|
|
|
_, bottom_vertical, bottom_w, bottom_h = read(sys.argv[1])
|
|
_, left_vertical, left_w, left_h = read(sys.argv[2])
|
|
|
|
if bottom_vertical:
|
|
raise SystemExit('the bottom dock reports itself as vertical')
|
|
if not left_vertical:
|
|
raise SystemExit('the left dock does not report itself as vertical')
|
|
|
|
# The bug this exists for: a side dock anchored at one end only collapses to a
|
|
# sliver on the axis it should span.
|
|
if left_h <= bottom_h:
|
|
raise SystemExit(f'the left dock is {left_h}px tall and does not span the screen')
|
|
if bottom_w <= left_w:
|
|
raise SystemExit(f'the bottom dock is {bottom_w}px wide and does not span the screen')
|
|
if left_w >= bottom_w or bottom_h >= left_h:
|
|
raise SystemExit('the two orientations are not thin on opposite axes')
|
|
PY
|
|
|
|
printf 'dock position contract: ok\n'
|