Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
18 changes: 18 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
35 changes: 25 additions & 10 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
---------------------------------------
Expand Down
34 changes: 27 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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` |

Expand Down Expand Up @@ -134,6 +134,8 @@ custom overlay over a dialog, how commands are run via the list's event, and the
cargo run --features tray
```

<img src="docs/screenshot-tray.png" width="300" alt="Deck tray menu — accent-colored status item with its native menu open">

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.
Expand All @@ -146,6 +148,8 @@ is macOS-only and cfg-gated. Architecture in [LEARNINGS §8](docs/LEARNINGS.md#t
cargo run --features overlay
```

<img src="docs/screenshot-overlay.png" width="520" alt="Deck overlay surfaces — a top-right job-status rail and a bottom-center recording pill">

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
Expand Down Expand Up @@ -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).

Expand All @@ -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).
10 changes: 6 additions & 4 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
Binary file added docs/screenshot-overlay.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshot-tray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
132 changes: 132 additions & 0 deletions scripts/screenshot.sh
Original file line number Diff line number Diff line change
@@ -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
Loading