diff --git a/AGENTS.md b/AGENTS.md
index 945b39b..3729a10 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -9,6 +9,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- `alacritty_terminal` 0.26: grid, parser, pseudo-terminal, event loop.
- `serde`, `serde_json`: settings. `dirs`: the config folder. `log`, `env_logger`: logs to stderr. `unicode-normalization`, on macOS: Git's marks in composed form.
- On Linux, `wayland-client`, `wayland-backend` and `wayland-protocols` (staging) ask the compositor for `ext_background_effect_v1` on gpui's own connection; `wayland-server` hosts a compositor in the tests. `x11rb` sets `_KDE_NET_WM_BLUR_BEHIND_REGION`; its test runs Xvfb.
+- `resvg`, a dev dependency at the version gpui uses, renders the icon's SVG for the macOS app.
## Commands
@@ -21,6 +22,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- mouse gestures handed to gpui's view, at points from the content's top-left, modifiers as for keys: `click@x,y`, `click2@`, `click3@`, `rclick@`, `mclick@`, `drag@x,y@x,y`, `move@x,y`, the steps `ldown@`, `lup@`, `ldrag@`, `rdown@`, `rup@`, `mdown@`, `mup@`, and `scrollup@x,y@lines`, `scrolldown@x,y@lines`.
- `scripts/check.sh`: fmt, clippy `-D warnings`, tests, the 500-line rule.
- `scripts/linux.sh ARGS` runs `cargo ARGS` on the working tree in a Linux container, the build kept in Docker volumes; `scripts/linux.sh test --locked` runs the tests as a user, not root, as CI does. The tree goes in as a tar stream, every file dated now: Colima's shared folder can show the VM a file as it was before an edit, and cargo judges by date, so a tree older than the last build would reuse that build. It needs a Docker daemon: on macOS, Colima (`colima start`).
+- `scripts/mac-app.sh` builds `target/release/ELYTerminal.app` from a release build, signed ad hoc, and `ELYTerminal-VERSION.dmg` beside it, the app next to a link to Applications. Its icon: `examples/icons.rs` renders `assets/icons/elyterminal-macos.svg` into an iconset, which `iconutil` packs.
## Layout
@@ -31,6 +33,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- `assets/shell-integration`: ELYTerminal's own scripts (not Ghostty's, which are GPL): zsh through `ZDOTDIR`, bash through `--rcfile`, fish through `XDG_DATA_DIRS`, PowerShell through `-NoExit -Command`. Each prompt sends OSC 7 and, except in fish, whose `fish_title` stays, a title.
- `assets/fonts`: JetBrains Mono 2.304's bold and italic faces (OFL; Ely bundles regular), and Nerd Fonts 3.5.1's JetBrains Mono NF Mono Regular, which draws icons a font lacks.
- `assets/themes`: Ghostty theme files copied verbatim from iTerm2-Color-Schemes (MIT, its `LICENSE` beside them).
+- `assets/icons`: the icon, an original figure, Ely, holding a terminal: `elyterminal-macos.svg` on macOS's rounded square, `elyterminal.svg` the figure alone, for Windows and Linux.
## Rules
@@ -77,3 +80,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 2026-10-15: the macOS menu bar is the PRD's: ELYTerminal (Settingsā¦, Quit), Shell (New Session, Close Session), Edit (Copy, Paste, Select All, Find), View (Toggle Sessions, Toggle Right Panel, Bigger, Smaller, Reset Size), Window (Minimize, Zoom, then the windows macOS lists). An item shows the key bound to its action, so the menus are set after the bindings; Copy, Paste and Select All go through AppKit's `copy:`, `paste:` and `selectAll:`, as gpui's `OsAction` maps them. They are Ely's text input actions, which `forms` exports for this, and the terminal handles them too: gpui sends a menu's action to what holds focus and enables the item only where a handler waits, so one action serves the terminal, find and every field. The new keys are Ghostty's: Cmd-Q, Cmd-A, Cmd-= and Cmd-+, Cmd-minus, Cmd-0, Cmd-M. Off macOS Quit and Select All take Ctrl-Shift, since Ctrl with a letter types a control byte; the font's size takes plain Ctrl with =, +, minus and 0, which type none, as in Ghostty, Alacritty and Windows Terminal; Minimize has no key there. Bigger and Smaller move every session's font a point, inside the Size range, and Reset Size goes back to the setting; nothing saves the change, as in Ghostty. Select All runs from the first character in the history to the last on the screen, the blanks around them left out, as Ghostty's, though a space under a combining mark shows a character and counts as one; copy on select takes it, and a blank grid selects nothing.
- 2026-10-15: CI runs on self-hosted Gitea runners, each job with what its runner must hold. macOS runs on the host, with fish and PowerShell installed, or the two shell tests fail as they should. Windows runs on the host, with rustup's `cargo` and `rustc` proxies beside `rustup` and Visual Studio's C++ tools. Ubuntu (`ubuntu-24.04`) runs as root in `docker.gitea.com/runner-images:ubuntu-24.04`, installs the libraries gpui links, and runs the tests as a user it makes, since root ignores the file modes they check; its runner serves this repository alone, in a VM of its own, so a build cannot starve other containers. The Linux and Windows jobs build without debuginfo, as `scripts/linux.sh` does: with it, linking gpui's test binaries outgrew that VM's 8 GB. A run that waits on a missing runner stays queued: cancel stale ones before a new runner registers, or it runs every one. The shell tests type a line only once the line editor reads raw keys, canonical mode off: on CI, a line typed before PowerShell's first read was echoed by the terminal and never run.
- 2026-10-15: on macOS 27 gpui's `Blurred` gives its NSVisualEffectView the `Selection` material, which builds no backdrop layer, so the desktop showed through sharp. `blur/macos` sets the view to `underWindowBackground` whenever the window turns Blurred: a gaussian blur of radius 30, the radius the owner's Ghostty uses; gpui's `updateLayer` still strips the material's tint and saturation. The view's presence proved nothing, so `shot` reads the radius off its backdrop layer.
+- 2026-10-15: the icon is drawn by hand in SVG, flat, so one figure serves both forms: on macOS it stands on a night-purple rounded square on Apple's grid (824 of 1024, radius 185) with sparkles, the gradient grained so it does not band; on Windows and Linux it stands alone, as Papirus draws app icons. The app is `com.zacharyzhang.elyterminal`, after the owner's domain, signed ad hoc: it opens on the Mac that built it, and Gatekeeper asks once on any other.
diff --git a/Cargo.lock b/Cargo.lock
index 1b708dc..82558d8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1767,6 +1767,7 @@ dependencies = [
"objc",
"polling",
"raw-window-handle",
+ "resvg",
"rustix-openpty",
"serde",
"serde_json",
diff --git a/Cargo.toml b/Cargo.toml
index 25d3d14..4ebbbda 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,6 +24,9 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_path_to_error = "0.1.20"
+[dev-dependencies]
+resvg = { version = "0.46", default-features = false }
+
[target.'cfg(unix)'.dependencies]
libc = "0.2"
rustix-openpty = "0.2"
diff --git a/PRD.md b/PRD.md
index d2e7ca6..a0350c1 100644
--- a/PRD.md
+++ b/PRD.md
@@ -123,6 +123,12 @@ Cmd-, opens it. Categories on the left, rows on the right, Ely's settings compon
- `scripts/check.sh` passes: fmt, clippy with `-D warnings`, tests, no file over 500 lines.
- CI green on macOS, Ubuntu and Windows.
+## 14. Icon and the macOS app
+
+- The icon is Ely, a girl with long pink hair, holding a terminal in front of her.
+- macOS: the icon on the rounded square macOS gives app icons. Windows and Linux: the figure alone, no ground, flat, as Papirus draws.
+- `scripts/mac-app.sh` builds `ELYTerminal.app` from a release build with its icon, signed ad hoc, and a disk image holding it beside a link to Applications.
+
## Deferred
Splits, tabs inside a session, profiles, key rebinding, kitty graphics, sixel, OSC 133 prompt marks, session restore, push and pull, quick terminal.
diff --git a/README.md b/README.md
index d5d10f8..b7635b3 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@ Rust 1.95.
cargo run --release
```
+On macOS, `scripts/mac-app.sh` builds `ELYTerminal.app`, with its icon, and a disk image of it in `target/release`.
+
On Linux, install the libraries gpui links first. On Debian and Ubuntu:
```sh
diff --git a/TASKS.md b/TASKS.md
index 9542115..33215f1 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -60,3 +60,7 @@ Progress truth. Top to bottom, one at a time. A task is ticked only after its co
## 13. Acceptance
- [x] T23 End-to-end checks, README, acceptance table, CI green on three platforms.
+
+## 14. Icon and the macOS app
+
+- [ ] T24 Icon: Ely holding a terminal, on macOS's rounded square and alone for Windows and Linux; `scripts/mac-app.sh` builds `ELYTerminal.app` and its disk image.
diff --git a/assets/icons/elyterminal-macos.svg b/assets/icons/elyterminal-macos.svg
new file mode 100644
index 0000000..980c277
--- /dev/null
+++ b/assets/icons/elyterminal-macos.svg
@@ -0,0 +1,85 @@
+
diff --git a/assets/icons/elyterminal.svg b/assets/icons/elyterminal.svg
new file mode 100644
index 0000000..13c18bb
--- /dev/null
+++ b/assets/icons/elyterminal.svg
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/icons.rs b/examples/icons.rs
new file mode 100644
index 0000000..d40726f
--- /dev/null
+++ b/examples/icons.rs
@@ -0,0 +1,33 @@
+//! Renders an SVG into a macOS iconset: `icons SVG FOLDER`.
+
+use std::path::Path;
+
+use anyhow::Context as _;
+use resvg::{tiny_skia, usvg};
+
+fn main() -> anyhow::Result<()> {
+ let mut args = std::env::args().skip(1);
+ let svg = args.next().context("icons: name an SVG to render")?;
+ let out = args
+ .next()
+ .context("icons: name an iconset folder to write")?;
+ let data = std::fs::read(&svg).with_context(|| format!("icons: cannot read {svg}"))?;
+ let tree = usvg::Tree::from_data(&data, &usvg::Options::default())
+ .with_context(|| format!("icons: {svg} is not an SVG"))?;
+ std::fs::create_dir_all(&out).with_context(|| format!("icons: cannot make {out}"))?;
+ for points in [16, 32, 128, 256, 512] {
+ for (scale, suffix) in [(1, ""), (2, "@2x")] {
+ let side = points * scale;
+ let mut pixmap = tiny_skia::Pixmap::new(side, side).context("icons: no pixmap")?;
+ let fit = side as f32 / tree.size().width();
+ let transform = tiny_skia::Transform::from_scale(fit, fit);
+ resvg::render(&tree, transform, &mut pixmap.as_mut());
+ let png = Path::new(&out).join(format!("icon_{points}x{points}{suffix}.png"));
+ pixmap
+ .save_png(&png)
+ .with_context(|| format!("icons: cannot write {}", png.display()))?;
+ }
+ }
+ println!("icons: {svg} drawn into {out}");
+ Ok(())
+}
diff --git a/scripts/mac-app.sh b/scripts/mac-app.sh
new file mode 100755
index 0000000..212d850
--- /dev/null
+++ b/scripts/mac-app.sh
@@ -0,0 +1,47 @@
+#!/bin/sh
+# mac-app.sh: target/release/ELYTerminal.app, signed ad hoc, and its disk image.
+set -eu
+cd "$(dirname "$0")/.."
+cargo build --release --locked
+version=$(cargo pkgid)
+version=${version##*@}
+out=target/release
+app=$out/ELYTerminal.app
+work=$(mktemp -d)
+trap 'rm -rf "$work"' EXIT
+cargo run --release --locked --quiet --example icons -- \
+ assets/icons/elyterminal-macos.svg "$work/ELYTerminal.iconset"
+rm -rf "$app"
+mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources"
+cp "$out/elyterminal" "$app/Contents/MacOS/"
+iconutil -c icns "$work/ELYTerminal.iconset" -o "$app/Contents/Resources/ELYTerminal.icns"
+cat > "$app/Contents/Info.plist" <
+
+
+
+ CFBundleDevelopmentRegionen
+ CFBundleDisplayNameELYTerminal
+ CFBundleExecutableelyterminal
+ CFBundleIconFileELYTerminal
+ CFBundleIdentifiercom.zacharyzhang.elyterminal
+ CFBundleInfoDictionaryVersion6.0
+ CFBundleNameELYTerminal
+ CFBundlePackageTypeAPPL
+ CFBundleShortVersionString$version
+ CFBundleVersion$version
+ LSApplicationCategoryTypepublic.app-category.developer-tools
+ LSMinimumSystemVersion11.0
+ NSHighResolutionCapable
+
+
+EOF
+plutil -lint -s "$app/Contents/Info.plist"
+codesign --force --sign - "$app"
+mkdir "$work/disk"
+ditto "$app" "$work/disk/ELYTerminal.app"
+ln -s /Applications "$work/disk/Applications"
+dmg=$out/ELYTerminal-$version.dmg
+hdiutil create -quiet -volname ELYTerminal -srcfolder "$work/disk" -ov -format UDZO "$dmg"
+echo "mac-app: $app"
+echo "mac-app: $dmg"
diff --git a/tasks/acceptance.md b/tasks/acceptance.md
index 0251b88..e8cfc8a 100644
--- a/tasks/acceptance.md
+++ b/tasks/acceptance.md
@@ -134,6 +134,14 @@ What no check reached: no one has looked at the window on Windows, which CI buil
| `scripts/check.sh` passes | macOS: 135 tests, clippy, fmt, 500 lines; Linux in Colima: 138 tests, clippy | Pass |
| CI green on macOS, Ubuntu and Windows | run 919 on `09930c2`: all three jobs pass; see the CI notes below | Pass |
+## 14. Icon and the macOS app
+
+| PRD line | Task | Evidence | Status |
+|---|---|---|---|
+| The icon: Ely, a girl with long pink hair, holding a terminal | T24 | `assets/icons`, drawn at 16 to 1024 px and legible at 32; the owner saw it | Pass |
+| macOS: the rounded square; Windows and Linux: the figure alone, flat | T24 | `elyterminal-macos.svg` on Apple's grid; `elyterminal.svg`, which no Windows or Linux build carries yet | Pass |
+| `scripts/mac-app.sh` builds the app, signed ad hoc, and its disk image | T24 | its run: `codesign --verify --strict` passes on the app and on its copy in the image; the `.icns` holds 16 to 1024 px; the image verifies and mounts with the app beside Applications; LaunchServices opens the app as `com.zacharyzhang.elyterminal` | Pass |
+
## CI notes
On 2026-10-15 every job first failed on its runner, not on the code. With the owner's leave, each runner was put right: