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