diff --git a/AGENTS.md b/AGENTS.md
index 5cad3ef..7cc3968 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -113,3 +113,21 @@ Full contrast and rationale: `docs/LEARNINGS.md` §17.
Ground UI changes in `README.md` and `docs/LEARNINGS.md` (real screenshots and
hard-won GPUI lessons) — not remembered descriptions of how things look.
+
+**Close the visual loop — `just screenshot` instead of guessing.** After a UI edit,
+capture the result and *look* at the image (this is the OODA loop: observe the real
+pixels, don't assume). It launches the app, optionally drives it to a view, captures
+the front window, and quits — also how you refresh the README shots. macOS only; a
+human grants your terminal **Screen Recording** + **Accessibility** once (System
+Settings → Privacy & Security), so a sandboxed session can't do the first run unaided.
+
+```
+just screenshot # welcome → docs/screenshot.png
+just screenshot docs/screenshot-settings.png "" cmd+, # settings page
+just screenshot docs/screenshot-palette.png "" cmd+k # command palette
+just screenshot docs/overlay.png overlay # overlay rail + pill (alpha, leak-proof)
+SHOT_BACKDROP=zed just screenshot docs/tray.png tray # menu-bar tray icon + its menu
+```
+
+Overlay/tray capture is `--features`-driven (see the recipe). Hover states still need a
+cursor tool (e.g. `cliclick`) — out of scope.
diff --git a/CLAUDE.md b/CLAUDE.md
index ab31749..fdb1f48 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -113,3 +113,21 @@ Full contrast and rationale: `docs/LEARNINGS.md` §17.
Ground UI changes in `README.md` and `docs/LEARNINGS.md` (real screenshots and
hard-won GPUI lessons) — not remembered descriptions of how things look.
+
+**Close the visual loop — `just screenshot` instead of guessing.** After a UI edit,
+capture the result and *look* at the image (this is the OODA loop: observe the real
+pixels, don't assume). It launches the app, optionally drives it to a view, captures
+the front window, and quits — also how you refresh the README shots. macOS only; a
+human grants your terminal **Screen Recording** + **Accessibility** once (System
+Settings → Privacy & Security), so a sandboxed session can't do the first run unaided.
+
+```
+just screenshot # welcome → docs/screenshot.png
+just screenshot docs/screenshot-settings.png "" cmd+, # settings page
+just screenshot docs/screenshot-palette.png "" cmd+k # command palette
+just screenshot docs/overlay.png overlay # overlay rail + pill (alpha, leak-proof)
+SHOT_BACKDROP=zed just screenshot docs/tray.png tray # menu-bar tray icon + its menu
+```
+
+Overlay/tray capture is `--features`-driven (see the recipe). Hover states still need a
+cursor tool (e.g. `cliclick`) — out of scope.
diff --git a/NOTICE b/NOTICE
index 63e56bc..7833bf9 100644
--- a/NOTICE
+++ b/NOTICE
@@ -31,17 +31,32 @@ and are all Apache-2.0:
https://github.com/longbridge/gpui-component (pinned at commit dadfca97).
Apache-2.0, © Longbridge.
-GPL-3.0-or-later obligation (please read before distributing a binary)
-......................................................................
+GPL-3.0-or-later components (matters mainly for closed-source binaries)
+.......................................................................
Zed's logging/tracing crates — zlog, ztracing, ztracing_macro — are
-GPL-3.0-or-later, and the git gpui stack links them statically into the binary
-via the chain `gpui -> sum_tree -> ztracing -> zlog`. Deck did not add them;
-they are inherent to building on Zed's git gpui stack. Because this code is
-compiled into a distributed Deck binary, that binary carries a GPL-3.0-or-later
-copyleft (offer-of-source) obligation: if you ship a binary, you must make the
-corresponding source available under GPL-3.0-or-later. This applies only to the
-binary — Deck's own source remains 0BSD. See deny.toml for the scoped license
-exceptions covering exactly these three crates.
+GPL-3.0-or-later, and the git gpui stack links them into the binary via the
+chain `gpui -> sum_tree -> ztracing -> zlog`. sum_tree itself is Apache-2.0; it
+is simply the carrier whose unconditional `ztracing` dependency drags the three
+GPL crates in. Deck did not add them — Zed wired ztracing into sum_tree in Dec
+2025 (PR #44147), so they are present on Deck's pinned git commit and on current
+gpui main, but NOT in the Oct-2025 crates.io 0.2.2 snapshot. At Deck's default
+build they are compile-time no-ops (dormant Tracy instrumentation, active only
+behind an env-var-gated `--cfg ztracing`).
+
+GPL obligations attach to DISTRIBUTION, not to building or running:
+
+ - Local / internal-only use: no obligation, on any channel.
+ - Open-source fork (source already public): GPL-3.0's source-availability
+ requirement is satisfied trivially; nothing further to do.
+ - Closed-source / proprietary binary: you cannot statically link GPL-3.0 code
+ and keep the combined work proprietary. Build on the permissive crates.io
+ pair (`gpui = "0.2"`, `gpui-component = "0.5"`), whose `gpui_sum_tree`
+ carries none of Zed's git tracing crates, so the binary is free of the
+ GPL-3.0 chain.
+
+Deck's own source is 0BSD regardless. See deny.toml for the scoped license
+exceptions covering exactly these three crates, and zed-industries/zed#55470 for
+the upstream tracking issue. This is a summary, not legal advice.
tray-icon (optional, `--features tray`)
---------------------------------------
diff --git a/README.md b/README.md
index 82e60ab..70cdaa4 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ mode. Then delete the welcome screen and build your app — or wire in your own
framework; [gpui-component](https://github.com/longbridge/gpui-component) adds a shadcn-style
component kit on top. Deck is the boilerplate you'd otherwise rewrite for every project — native
window, menu bar, shortcuts, a non-harsh theme, saved settings, an app icon, a shippable bundle —
-done once, opinionated, and kept small: ~700 lines across a few files, fresh git-pinned GPUI
+done once and kept small: ~700 lines across a few files, fresh git-pinned GPUI
(reproducible via `Cargo.lock`), no submodules, no vendoring, no `node`.
## Quick start
@@ -77,7 +77,7 @@ optional. Plus:
| ⌨️ | **Keyboard shortcuts** → actions → menu items | `main.rs`, `shell.rs` |
| 📋 | Native **menu bar** (App / File / Edit / View) | `main.rs` |
| 🟣 | Optional **menu-bar / tray mode**, no dock icon — `--features tray` | `tray.rs` |
-| 🫧 | Optional **floating overlay** — transparent always-on-top window — `--features overlay` | `overlay/` |
+| 🫧 | Optional **floating overlay** — transparent always-on-top window (macOS; Linux no-op) — `--features overlay` | `overlay/` |
| 🔣 | **Lucide** icon set (ISC licensed, bundled) | `gpui-component` |
| 🖼️ | **App icon** pipeline (image → squircle → icns) + `cargo bundle` config | `scripts/`, `assets/`, `Cargo.toml` |
@@ -134,6 +134,8 @@ custom overlay over a dialog, how commands are run via the list's event, and the
cargo run --features tray
```
+
+
This turns Deck into a menu-bar app with no dock icon. The tray icon is a *native* status item — an
image plus a native menu, so there's **no second rendering system** and your windows stay GPUI — and
it recolors to match your accent. Menu clicks are bridged back into GPUI on its own executor.
@@ -146,6 +148,8 @@ is macOS-only and cfg-gated. Architecture in [LEARNINGS §8](docs/LEARNINGS.md#t
cargo run --features overlay
```
+
+
This adds a transparent, always-on-top surface that floats over other apps — the seam for a HUD,
a quick-capture bar, or an ambient agent panel. It's a real GPUI window (no second renderer); the
panel is hardened via the shared `objc2` stack so it sits above full-screen spaces. macOS-only in
@@ -259,7 +263,9 @@ it's developed against Zed's gpui **HEAD** — so the matched git pair is the on
with the component kit. Why not crates.io? Zed publishes `gpui` there only rarely (the `0.2.x` line
shipped Oct 2025 and nothing since), so the published `gpui-component` is pinned to an ~8-month-old gpui
snapshot. Deck takes the fresh path and bumps on a cadence (`just bump-gpui`, ~monthly); the plain-stable
-crates.io pair (`gpui = "0.2"` + `gpui-component = "0.5"`) is documented as a zero-git **fallback**. Full
+crates.io pair (`gpui = "0.2"` + `gpui-component = "0.5"`) is documented as a zero-git **fallback** —
+which also sidesteps the GPL-3.0 binary obligation noted under [license](#credits--license), since the
+crates.io `gpui` pulls none of Zed's git-only logging crates. Full
rationale + bump/fallback procedures in [LEARNINGS §2](docs/LEARNINGS.md#dependencies) and
[UPGRADING.md](docs/UPGRADING.md).
@@ -270,7 +276,21 @@ Built on [Zed](https://github.com/zed-industries/zed) (GPUI) and
[Lucide](https://lucide.dev) (ISC). Deck itself is 0BSD — zero-attribution, do whatever you want.
See [NOTICE](NOTICE) for third-party attributions.
-Note: distributed Deck binaries statically link GPL-3.0-or-later components (`zlog` / `ztracing` /
-`ztracing_macro`, pulled in transitively via `gpui`), so shipping a **binary** carries a GPL-3.0
-source-offer obligation; Deck's
-own source stays 0BSD. See [NOTICE](NOTICE) for details.
+**On the GPL crates — read this only if you ship a _closed-source_ binary.** Deck's default (git)
+channel links three of Zed's crates that are `GPL-3.0-or-later` — `zlog`, `ztracing`, `ztracing_macro`
+— via the chain `gpui → sum_tree → ztracing → zlog`. Deck didn't add them; Zed wired them into
+`sum_tree` ([PR #44147](https://github.com/zed-industries/zed/pull/44147), Dec 2025), and at Deck's
+default build they're inert no-ops (dormant Tracy instrumentation). GPL obligations attach only when
+you **distribute a binary**, so what this means depends on you:
+
+- **Build / run locally, or internal-only use** — nothing to do, on any channel.
+- **Open-source fork** — nothing to do: if your source is public, GPL-3.0's source-availability
+ requirement is met trivially (0BSD lets you fold your code into the combined work).
+- **Closed-source / proprietary binary** — you can't statically link GPL-3.0 code and keep the
+ result proprietary. Build on the permissive **crates.io pair** (`gpui = "0.2"`,
+ `gpui-component = "0.5"`) instead: its `gpui_sum_tree` carries none of Zed's git tracing crates, so
+ the binary is free of the GPL-3.0 chain. See [the dependency story](#tech-stack--the-dependency-story)
+ and [UPGRADING.md](docs/UPGRADING.md).
+
+Tracked upstream at [zed-industries/zed#55470](https://github.com/zed-industries/zed/issues/55470)
+(likely to resolve at the source). This is a summary, not legal advice — details in [NOTICE](NOTICE).
diff --git a/deny.toml b/deny.toml
index 35c4812..bc724fb 100644
--- a/deny.toml
+++ b/deny.toml
@@ -56,10 +56,12 @@ allow = [
# ⚠️ COPYLEFT IN THE TREE — read this before shipping a closed-source fork. Zed's logging/tracing
# crates (zlog, ztracing, ztracing_macro) are GPL-3.0-or-later and are LINKED into the binary as a
# normal runtime dep: gpui -> sum_tree -> ztracing -> zlog. This is inherent to building on Zed's
-# git gpui stack — Deck did not add it. The exceptions below allow GPL ONLY for those three named
-# crates (so the gate can go green); they do NOT open the whole tree to copyleft. If your product
-# cannot take a GPL-3.0 obligation, raise it with upstream gpui — don't just delete these lines and
-# ship. Re-verify the crate list after every `just bump-gpui`.
+# git gpui stack — Deck did not add it (Zed wired ztracing into sum_tree in PR #44147, Dec 2025).
+# The exceptions below allow GPL ONLY for those three named crates (so the gate can go green); they
+# do NOT open the whole tree to copyleft. If your product cannot take a GPL-3.0 obligation, the clean
+# fix is to build on the permissive crates.io pair (gpui = "0.2", gpui-component = "0.5"), whose
+# gpui_sum_tree carries none of these crates — not to delete these exceptions and ship the git stack
+# anyway. Tracked upstream: zed-industries/zed#55470. Re-verify the crate list after every `just bump-gpui`.
exceptions = [
{ allow = ["GPL-3.0-or-later"], crate = "zlog" },
{ allow = ["GPL-3.0-or-later"], crate = "ztracing" },
diff --git a/docs/screenshot-overlay.png b/docs/screenshot-overlay.png
new file mode 100644
index 0000000..e4dbd56
Binary files /dev/null and b/docs/screenshot-overlay.png differ
diff --git a/docs/screenshot-tray.png b/docs/screenshot-tray.png
new file mode 100644
index 0000000..283b45d
Binary files /dev/null and b/docs/screenshot-tray.png differ
diff --git a/justfile b/justfile
index 7d42cef..0c97c55 100644
--- a/justfile
+++ b/justfile
@@ -21,6 +21,19 @@ run-tray:
run-overlay:
cargo run --features overlay
+# Screenshot the running app → an image. macOS only; grant your terminal Screen
+# Recording + Accessibility once (System Settings → Privacy & Security). Launches the
+# app, captures, quits. What's captured depends on the feature you pass (arg 2):
+# just screenshot # welcome → docs/screenshot.png
+# just screenshot docs/screenshot-settings.png "" cmd+, # settings page (window)
+# just screenshot docs/screenshot-palette.png "" cmd+k # command palette (window)
+# just screenshot docs/overlay.png overlay # rail + pill → -rail/-pill (alpha)
+# SHOT_BACKDROP=zed just screenshot docs/tray.png tray # menu-bar status item + menu
+# Overlay panels capture with transparency (leak-proof). Hover states still need a cursor
+# tool (e.g. `cliclick`) and are out of scope.
+screenshot out="docs/screenshot.png" features="" keys="":
+ bash scripts/screenshot.sh "{{out}}" "{{features}}" "{{keys}}"
+
# Format the code.
fmt:
cargo fmt
diff --git a/scripts/screenshot.sh b/scripts/screenshot.sh
new file mode 100755
index 0000000..89af0fd
--- /dev/null
+++ b/scripts/screenshot.sh
@@ -0,0 +1,132 @@
+#!/usr/bin/env bash
+# Screenshot the app this repo builds. macOS only (built-in `screencapture` + `osascript`).
+#
+# One-time setup (the OS guards screen capture + UI scripting behind permissions):
+# System Settings -> Privacy & Security -> Screen Recording -> enable your terminal
+# System Settings -> Privacy & Security -> Accessibility -> enable your terminal
+# A sandboxed/headless agent session usually lacks both and can't self-grant them.
+#
+# Usage: scripts/screenshot.sh [OUT] [FEATURES] [KEYS]
+# OUT output image path (default: docs/screenshot.png)
+# FEATURES cargo features to build/run (e.g. overlay, tray; default: none)
+# KEYS shortcut to drive the app first, window mode only
+# (e.g. cmd+k for the palette, cmd+, for settings)
+#
+# What gets captured is decided by FEATURES (each handled independently, so
+# `tray,overlay` captures both):
+# (none) the front app window, after optional KEYS -> OUT
+# contains overlay the floating rail + pill panels, alpha (transparent) -> OUT -rail/-pill
+# contains tray the menu-bar status item with its menu open -> OUT
+#
+# Overlay capture is by window id with the alpha channel (no background), so it can't
+# leak whatever is behind the panels and needs no clean backdrop. The tray menu is
+# opaque, but its region capture may include whatever is behind it — pass SHOT_BACKDROP
+# (a blank editor) or crop the result.
+#
+# Env: SHOT_DELAY seconds to wait for first paint (default 3)
+# SHOT_BACKDROP app to bring to front first, for a clean backdrop behind the tray
+# menu (e.g. "zed"); optional, tray mode only.
+set -euo pipefail
+
+OUT="${1:-docs/screenshot.png}"
+FEATURES="${2:-}"
+KEYS="${3:-}"
+DELAY="${SHOT_DELAY:-3}"
+BACKDROP="${SHOT_BACKDROP:-}"
+
+[ "$(uname)" = "Darwin" ] || { echo "screenshot: macOS only (needs screencapture)"; exit 1; }
+mkdir -p "$(dirname "$OUT")"
+BASE="${OUT%.*}"; EXT="${OUT##*.}"
+
+# Optional clean backdrop (e.g. a blank editor) behind floating panels / the tray menu.
+[ -n "$BACKDROP" ] && { osascript -e "tell application \"$BACKDROP\" to activate" 2>/dev/null || true; sleep 1; }
+
+# Launch the app (debug build is fine; reuses the cargo cache).
+run_args=(run)
+[ -n "$FEATURES" ] && run_args+=(--features "$FEATURES")
+cargo "${run_args[@]}" >/dev/null 2>&1 &
+APP_PID=$!
+disown "$APP_PID" 2>/dev/null || true # keep the shell from printing "Terminated" on cleanup
+cleanup() { pkill -P "$APP_PID" 2>/dev/null || true; kill "$APP_PID" 2>/dev/null || true; }
+trap cleanup EXIT
+sleep "$DELAY"
+
+# Identify the app process by its binary name (== cargo package name). Do NOT use
+# "frontmost": a `tray` build is a menu-bar accessory and never becomes frontmost.
+PROC=$(cargo metadata --no-deps --format-version 1 2>/dev/null \
+ | python3 -c 'import sys,json; print(json.load(sys.stdin)["packages"][0]["name"])' 2>/dev/null)
+[ -n "$PROC" ] || PROC=$(osascript -e 'tell application "System Events" to get name of (first process whose frontmost is true)' 2>/dev/null)
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+shot() { screencapture -x -o -R"$1" "$2" && echo "-> $2"; }
+
+capture_window() {
+ if [ -n "$KEYS" ]; then # optionally drive to a view (cmd+k, cmd+,, ...)
+ local key="${KEYS##*+}" mods="" parts m
+ IFS='+' read -ra parts <<< "$KEYS"
+ for m in "${parts[@]:0:${#parts[@]}-1}"; do
+ case "$m" in
+ cmd|command) mods+="command down, " ;;
+ shift) mods+="shift down, " ;;
+ ctrl|control) mods+="control down, " ;;
+ opt|option|alt) mods+="option down, " ;;
+ esac
+ done
+ if [ -n "$mods" ]; then
+ osascript -e "tell application \"System Events\" to keystroke \"$key\" using {${mods%, }}"
+ else
+ osascript -e "tell application \"System Events\" to keystroke \"$key\""
+ fi
+ sleep 0.7
+ fi
+ local b
+ b=$(osascript -e "tell application \"System Events\" to tell process \"$PROC\" to get {position, size} of front window" 2>/dev/null | tr -d ' ')
+ [ -n "$b" ] || { echo "screenshot: no front window found for process $PROC"; return 1; }
+ shot "$b" "$OUT"
+}
+
+capture_overlay() {
+ # Capture each floating panel by its window id WITH its alpha channel
+ # (`screencapture -l`), so the shot has a transparent background and can't leak
+ # whatever is behind it — no clean backdrop needed. Needs `swift` + winid.swift.
+ # rail = portrait (h>w), pill = landscape (w>h); the big main window is skipped.
+ local helper="$SCRIPT_DIR/winid.swift" got=0 id w h tag f
+ { command -v swift >/dev/null && [ -f "$helper" ]; } || { echo "screenshot(overlay): needs swift + scripts/winid.swift"; return 1; }
+ while read -r id w h; do
+ { [ "$w" -lt 500 ] && [ "$h" -lt 500 ]; } || continue # skip the big main window
+ tag=$([ "$h" -gt "$w" ] && echo rail || echo pill)
+ f="${BASE}-${tag}.${EXT}"
+ screencapture -x -o -l"$id" "$f" || continue
+ # Trim the transparent margins down to the panel (no-op if Pillow isn't installed).
+ python3 - "$f" <<'PY' 2>/dev/null || true
+import sys
+from PIL import Image
+im = Image.open(sys.argv[1]).convert("RGBA"); b = im.getbbox()
+if b: im.crop(b).save(sys.argv[1])
+PY
+ echo "-> $f"; got=1
+ done < <(swift "$helper" "$PROC" 2>/dev/null)
+ [ "$got" = 1 ] || { echo "screenshot(overlay): no panels found for $PROC"; return 1; }
+}
+
+capture_tray() {
+ # Menu-bar status item: find it, click to open its native menu, capture the corner.
+ local pos sx sy rx
+ pos=$(osascript -e "tell application \"System Events\" to tell process \"$PROC\" to get position of menu bar item 1 of menu bar 2" 2>/dev/null | tr -d ' ')
+ [ -n "$pos" ] || { echo "screenshot(tray): no status item found for process $PROC"; return 1; }
+ IFS=',' read -r sx sy <<< "$pos"
+ # NB: do NOT re-activate the backdrop here — bringing another app to the front
+ # right before the click suppresses the status-item menu. Rely on the backdrop
+ # already being frontmost (the overlay pass, or SHOT_BACKDROP before launch).
+ osascript -e "tell application \"System Events\" to tell process \"$PROC\" to click menu bar item 1 of menu bar 2" >/dev/null 2>&1 &
+ local click=$!; sleep 1.2
+ rx=$(( sx - 110 < 0 ? 0 : sx - 110 ))
+ shot "${rx},0,300,170" "$OUT"
+ osascript -e 'tell application "System Events" to key code 53' 2>/dev/null || true # Esc closes the menu
+ kill "$click" 2>/dev/null || true
+}
+
+did=0
+case "$FEATURES" in *overlay*) capture_overlay && did=1 ;; esac
+case "$FEATURES" in *tray*) capture_tray && did=1 ;; esac
+[ "$did" = 0 ] && capture_window
diff --git a/scripts/winid.swift b/scripts/winid.swift
new file mode 100644
index 0000000..68f34d2
--- /dev/null
+++ b/scripts/winid.swift
@@ -0,0 +1,21 @@
+// Print the on-screen windows owned by a given app: " "
+// (one per line). Used by scripts/screenshot.sh to capture the floating overlay
+// panels by window id (`screencapture -l`), which yields the panel WITH its
+// alpha channel — a transparent background, so the shot can't leak whatever is
+// behind it. Run: `swift scripts/winid.swift ` (macOS only).
+import CoreGraphics
+import Foundation
+
+let owner = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : ""
+guard !owner.isEmpty,
+ let list = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]]
+else { exit(1) }
+
+for info in list {
+ guard let o = info[kCGWindowOwnerName as String] as? String, o == owner,
+ let num = info[kCGWindowNumber as String] as? Int,
+ let b = info[kCGWindowBounds as String] as? [String: Any],
+ let w = b["Width"] as? CGFloat, let h = b["Height"] as? CGFloat
+ else { continue }
+ print("\(num) \(Int(w)) \(Int(h))")
+}
diff --git a/src/overlay/harden.rs b/src/overlay/harden.rs
index 51f1f8a..eb3145c 100644
--- a/src/overlay/harden.rs
+++ b/src/overlay/harden.rs
@@ -58,8 +58,10 @@ fn harden_panel_macos(window: &gpui::Window, hide_shadow: bool) {
// window a near-opaque backing so the OS still draws a shadow (gpui_macos
// window.rs: "avoid broken shadow"), but that shadow is a rounded *rectangle*
// the size of the window — it shows as a mismatched frame behind a `rounded_full`
- // pill. Killing it lets the pill's own rendered `shadow_lg` be the only depth cue.
- // The rail passes `false` and keeps its window shadow.
+ // pill. Killing it lets the panel's own rendered `shadow_lg` be the only depth cue.
+ // Both the pill and the rail pass `true` — each is a content-sized panel centered in
+ // a larger transparent canvas, so the window-sized shadow would just be a mismatched
+ // frame around the empty canvas.
panel.setHasShadow(false);
}
}
diff --git a/src/overlay/mod.rs b/src/overlay/mod.rs
index bdf9d22..f5e8502 100644
--- a/src/overlay/mod.rs
+++ b/src/overlay/mod.rs
@@ -177,9 +177,11 @@ fn install_macos(cx: &mut gpui::App, main_window: gpui::WindowHandle