T24: the icon, Ely holding a terminal, drawn in SVG: on macOS's rounded square, and alone and flat for Windows and Linux; scripts/mac-app.sh builds ELYTerminal.app, signed ad hoc, and its disk image, its icon rendered by examples/icons.rs through resvg
CI / platforms (windows-latest) (push) Successful in 2m40s
CI / platforms (ubuntu-24.04) (push) Successful in 3m41s
CI / macos (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-15 15:10:17 -04:00
1 parent c0a9eb3c98
commit 67fe91249a
11 files changed
+253

No files matched your search

+4
View File
@@ -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.
Generated
+1
View File
@@ -1767,6 +1767,7 @@ dependencies = [
"objc",
"polling",
"raw-window-handle",
"resvg",
"rustix-openpty",
"serde",
"serde_json",
+3
View File
@@ -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"
+6
View File
@@ -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.
+2
View File
@@ -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
+4
View File
@@ -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.
+85
View File
@@ -0,0 +1,85 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<defs>
<linearGradient id="plate" x1="0" y1="100" x2="0" y2="924" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3E3066"/><stop offset="1" stop-color="#1B1530"/>
</linearGradient>
<radialGradient id="glow" cx="512" cy="400" r="380" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#F7A6C9" stop-opacity=".45"/><stop offset="1" stop-color="#F7A6C9" stop-opacity="0"/>
</radialGradient>
<linearGradient id="gloss" x1="0" y1="100" x2="0" y2="512" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity=".16"/><stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
</linearGradient>
<clipPath id="plateClip"><rect x="100" y="100" width="824" height="824" rx="185"/></clipPath>
<filter id="plateShadow" x="-10%" y="-10%" width="120%" height="125%"><feDropShadow dx="0" dy="12" stdDeviation="14" flood-color="#000000" flood-opacity=".35"/></filter>
<filter id="grain" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".85" numOctaves="2" seed="7" stitchTiles="stitch"/><feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 .14 0 0 0 0"/></filter>
<filter id="figureShadow" x="-20%" y="-20%" width="140%" height="140%"><feDropShadow dx="0" dy="10" stdDeviation="12" flood-color="#0B0716" flood-opacity=".5"/></filter>
</defs>
<rect x="100" y="100" width="824" height="824" rx="185" fill="url(#plate)" filter="url(#plateShadow)"/>
<g clip-path="url(#plateClip)">
<rect x="100" y="100" width="824" height="824" fill="url(#glow)"/>
<rect x="100" y="100" width="824" height="824" fill="#000000" filter="url(#grain)"/>
<path fill="#FFFFFF" opacity="0.8" d="M232 228Q235.96 246.04 254 250Q235.96 253.96 232 272Q228.04 253.96 210 250Q228.04 246.04 232 228Z"/><path fill="#FFFFFF" opacity="0.6" d="M800 284Q802.88 297.12 816 300Q802.88 302.88 800 316Q797.12 302.88 784 300Q797.12 297.12 800 284Z"/><path fill="#FFFFFF" opacity="0.7" d="M770 740Q773.6 756.4 790 760Q773.6 763.6 770 780Q766.4 763.6 750 760Q766.4 756.4 770 740Z"/><path fill="#FFFFFF" opacity="0.5" d="M250 708Q252.16 717.84 262 720Q252.16 722.16 250 732Q247.84 722.16 238 720Q247.84 717.84 250 708Z"/><path fill="#FFFFFF" opacity="0.5" d="M300 161Q301.62 168.38 309 170Q301.62 171.62 300 179Q298.38 171.62 291 170Q298.38 168.38 300 161Z"/>
<g filter="url(#figureShadow)"><svg x="112" y="112" width="800" height="800" viewBox="0 0 512 512">
<defs>
<clipPath id="face">
<path d="M178 164C178 116 212 94 256 94C300 94 334 116 334 164C334 202 312 228 284 238C274 242 264 244 256 244C248 244 238 242 228 238C200 228 178 202 178 164Z"/>
</clipPath>
</defs>
<!-- back hair -->
<path fill="#EC86B5" d="M256 46C192 46 142 90 136 158C131 214 126 262 118 306C110 350 104 398 110 444C112 460 106 474 94 486C114 488 126 480 134 470C136 482 144 492 158 498C158 484 162 468 170 452L342 452C350 468 354 484 354 498C368 492 376 482 378 470C386 480 398 488 418 486C406 474 400 460 402 444C408 398 402 350 394 306C386 262 381 214 376 158C370 90 320 46 256 46Z"/>
<path fill="#D9659A" d="M194 200C198 238 214 262 230 276L282 276C298 262 314 238 318 200Z"/>
<!-- neck, collar, bow -->
<path fill="#F7CDBE" d="M236 222L276 222L276 266C268 270 244 270 236 266Z"/>
<path fill="#FFFFFF" d="M198 284C204 268 222 258 238 256L256 272L274 256C290 258 308 268 314 284Z"/>
<path fill="#F05A97" d="M256 264C244 250 226 248 224 258C222 270 236 278 256 264Z"/>
<path fill="#F05A97" d="M256 264C268 250 286 248 288 258C290 270 276 278 256 264Z"/>
<circle cx="256" cy="264" r="8" fill="#D94683"/>
<!-- face -->
<path fill="#FFE7DC" d="M178 164C178 116 212 94 256 94C300 94 334 116 334 164C334 202 312 228 284 238C274 242 264 244 256 244C248 244 238 242 228 238C200 228 178 202 178 164Z"/>
<g clip-path="url(#face)">
<path fill="#F9D8CB" transform="translate(0 12)" d="M170 176C162 110 204 62 256 62C308 62 350 110 342 176Q328 152 318 134Q316 156 304 172Q298 150 286 130Q286 156 274 178Q268 150 256 128Q244 150 238 178Q226 156 226 130Q214 150 208 172Q196 156 194 134Q184 152 170 176Z"/>
</g>
<!-- eyes -->
<ellipse cx="222" cy="188" rx="18" ry="23" fill="#A24FB9"/>
<ellipse cx="222" cy="198" rx="13.5" ry="11.5" fill="#EE84C4"/>
<ellipse cx="222" cy="190" rx="7.5" ry="10.5" fill="#5A2368"/>
<circle cx="215" cy="178" r="7" fill="#FFFFFF"/>
<circle cx="229" cy="199" r="3.2" fill="#FFFFFF"/>
<path fill="#45233F" d="M194 182C195 168 208 159 224 159C234 159 241 164 244 172L240 173C236 168 231 165 224 165C214 165 207 172 204 183Z"/>
<path fill="none" stroke="#C2506F" stroke-width="2" stroke-linecap="round" opacity=".35" d="M215 212Q222 214 229 212"/>
<ellipse cx="290" cy="188" rx="18" ry="23" fill="#A24FB9"/>
<ellipse cx="290" cy="198" rx="13.5" ry="11.5" fill="#EE84C4"/>
<ellipse cx="290" cy="190" rx="7.5" ry="10.5" fill="#5A2368"/>
<circle cx="283" cy="178" r="7" fill="#FFFFFF"/>
<circle cx="297" cy="199" r="3.2" fill="#FFFFFF"/>
<path fill="#45233F" d="M318 182C317 168 304 159 288 159C278 159 271 164 268 172L272 173C276 168 281 165 288 165C298 165 305 172 308 183Z"/>
<path fill="none" stroke="#C2506F" stroke-width="2" stroke-linecap="round" opacity=".35" d="M283 212Q290 214 297 212"/>
<!-- blush and mouth -->
<ellipse cx="198" cy="216" rx="15" ry="8" fill="#FF8DAA" opacity=".55"/>
<ellipse cx="314" cy="216" rx="15" ry="8" fill="#FF8DAA" opacity=".55"/>
<path fill="none" stroke="#C2506F" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" d="M244 222Q250 229 256 223Q262 229 268 222"/>
<!-- side locks -->
<path fill="#F7A6C9" d="M184 118C164 150 156 204 160 258C162 282 156 300 148 316C164 312 178 300 186 286C190 256 192 214 198 176C200 152 196 132 184 118Z"/>
<path fill="#F7A6C9" d="M328 118C348 150 356 204 352 258C350 282 356 300 364 316C348 312 334 300 326 286C322 256 320 214 314 176C312 152 316 132 328 118Z"/>
<!-- bangs -->
<path fill="#F7A6C9" d="M170 176C162 110 204 62 256 62C308 62 350 110 342 176Q328 152 318 134Q316 156 304 172Q298 150 286 130Q286 156 274 178Q268 150 256 128Q244 150 238 178Q226 156 226 130Q214 150 208 172Q196 156 194 134Q184 152 170 176Z"/>
<path fill="#FFD1E4" d="M196 108C220 84 292 84 316 108C292 92 220 92 196 108Z"/>
<path fill="#F7A6C9" d="M250 66C238 40 252 18 280 20C264 28 258 44 262 64Z"/>
<path fill="#FFE48A" d="M318 86Q320 98 332 100Q320 102 318 114Q316 102 304 100Q316 98 318 86Z"/>
<!-- terminal -->
<rect x="128" y="278" width="256" height="180" rx="26" fill="#3D3654"/>
<path fill="#FFFFFF" opacity=".15" d="M154 278H358C372 278 384 290 384 304V308C384 294 372 282 358 282H154C140 282 128 294 128 308V304C128 290 140 278 154 278Z"/>
<rect x="144" y="300" width="224" height="142" rx="14" fill="#1D1A2C"/>
<path fill="none" stroke="#FF9ECF" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" d="M172 344L200 366L172 388"/>
<rect x="212" y="378" width="46" height="12" rx="6" fill="#FFD1E4"/>
<!-- fingers over the top edge -->
<rect x="150" y="294" width="44" height="7" rx="3.5" fill="#000000" opacity=".3"/>
<rect x="318" y="294" width="44" height="7" rx="3.5" fill="#000000" opacity=".3"/>
<path fill="#FFE7DC" d="M150 296L150 280C150 272 161 272 161 280C161 270 172 270 172 279C172 270 183 270 183 280C183 272 194 272 194 281L194 296C194 300 150 300 150 296Z"/>
<path fill="#FFE7DC" d="M318 296L318 281C318 272 329 272 329 280C329 270 340 270 340 279C340 270 351 270 351 280C351 272 362 272 362 280L362 296C362 300 318 300 318 296Z"/>
<path fill="none" stroke="#F2BBA8" stroke-width="2" stroke-linecap="round" d="M161 282V294M172 281V294M183 282V294M329 282V294M340 281V294M351 282V294"/>
</svg></g>
<rect x="100" y="100" width="824" height="824" fill="url(#gloss)"/>
</g>
<rect x="101" y="101" width="822" height="822" rx="184" fill="none" stroke="#FFFFFF" stroke-opacity=".12" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

+60
View File
@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<defs>
<clipPath id="face">
<path d="M178 164C178 116 212 94 256 94C300 94 334 116 334 164C334 202 312 228 284 238C274 242 264 244 256 244C248 244 238 242 228 238C200 228 178 202 178 164Z"/>
</clipPath>
</defs>
<!-- back hair -->
<path fill="#EC86B5" d="M256 46C192 46 142 90 136 158C131 214 126 262 118 306C110 350 104 398 110 444C112 460 106 474 94 486C114 488 126 480 134 470C136 482 144 492 158 498C158 484 162 468 170 452L342 452C350 468 354 484 354 498C368 492 376 482 378 470C386 480 398 488 418 486C406 474 400 460 402 444C408 398 402 350 394 306C386 262 381 214 376 158C370 90 320 46 256 46Z"/>
<path fill="#D9659A" d="M194 200C198 238 214 262 230 276L282 276C298 262 314 238 318 200Z"/>
<!-- neck, collar, bow -->
<path fill="#F7CDBE" d="M236 222L276 222L276 266C268 270 244 270 236 266Z"/>
<path fill="#FFFFFF" d="M198 284C204 268 222 258 238 256L256 272L274 256C290 258 308 268 314 284Z"/>
<path fill="#F05A97" d="M256 264C244 250 226 248 224 258C222 270 236 278 256 264Z"/>
<path fill="#F05A97" d="M256 264C268 250 286 248 288 258C290 270 276 278 256 264Z"/>
<circle cx="256" cy="264" r="8" fill="#D94683"/>
<!-- face -->
<path fill="#FFE7DC" d="M178 164C178 116 212 94 256 94C300 94 334 116 334 164C334 202 312 228 284 238C274 242 264 244 256 244C248 244 238 242 228 238C200 228 178 202 178 164Z"/>
<g clip-path="url(#face)">
<path fill="#F9D8CB" transform="translate(0 12)" d="M170 176C162 110 204 62 256 62C308 62 350 110 342 176Q328 152 318 134Q316 156 304 172Q298 150 286 130Q286 156 274 178Q268 150 256 128Q244 150 238 178Q226 156 226 130Q214 150 208 172Q196 156 194 134Q184 152 170 176Z"/>
</g>
<!-- eyes -->
<ellipse cx="222" cy="188" rx="18" ry="23" fill="#A24FB9"/>
<ellipse cx="222" cy="198" rx="13.5" ry="11.5" fill="#EE84C4"/>
<ellipse cx="222" cy="190" rx="7.5" ry="10.5" fill="#5A2368"/>
<circle cx="215" cy="178" r="7" fill="#FFFFFF"/>
<circle cx="229" cy="199" r="3.2" fill="#FFFFFF"/>
<path fill="#45233F" d="M194 182C195 168 208 159 224 159C234 159 241 164 244 172L240 173C236 168 231 165 224 165C214 165 207 172 204 183Z"/>
<path fill="none" stroke="#C2506F" stroke-width="2" stroke-linecap="round" opacity=".35" d="M215 212Q222 214 229 212"/>
<ellipse cx="290" cy="188" rx="18" ry="23" fill="#A24FB9"/>
<ellipse cx="290" cy="198" rx="13.5" ry="11.5" fill="#EE84C4"/>
<ellipse cx="290" cy="190" rx="7.5" ry="10.5" fill="#5A2368"/>
<circle cx="283" cy="178" r="7" fill="#FFFFFF"/>
<circle cx="297" cy="199" r="3.2" fill="#FFFFFF"/>
<path fill="#45233F" d="M318 182C317 168 304 159 288 159C278 159 271 164 268 172L272 173C276 168 281 165 288 165C298 165 305 172 308 183Z"/>
<path fill="none" stroke="#C2506F" stroke-width="2" stroke-linecap="round" opacity=".35" d="M283 212Q290 214 297 212"/>
<!-- blush and mouth -->
<ellipse cx="198" cy="216" rx="15" ry="8" fill="#FF8DAA" opacity=".55"/>
<ellipse cx="314" cy="216" rx="15" ry="8" fill="#FF8DAA" opacity=".55"/>
<path fill="none" stroke="#C2506F" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" d="M244 222Q250 229 256 223Q262 229 268 222"/>
<!-- side locks -->
<path fill="#F7A6C9" d="M184 118C164 150 156 204 160 258C162 282 156 300 148 316C164 312 178 300 186 286C190 256 192 214 198 176C200 152 196 132 184 118Z"/>
<path fill="#F7A6C9" d="M328 118C348 150 356 204 352 258C350 282 356 300 364 316C348 312 334 300 326 286C322 256 320 214 314 176C312 152 316 132 328 118Z"/>
<!-- bangs -->
<path fill="#F7A6C9" d="M170 176C162 110 204 62 256 62C308 62 350 110 342 176Q328 152 318 134Q316 156 304 172Q298 150 286 130Q286 156 274 178Q268 150 256 128Q244 150 238 178Q226 156 226 130Q214 150 208 172Q196 156 194 134Q184 152 170 176Z"/>
<path fill="#FFD1E4" d="M196 108C220 84 292 84 316 108C292 92 220 92 196 108Z"/>
<path fill="#F7A6C9" d="M250 66C238 40 252 18 280 20C264 28 258 44 262 64Z"/>
<path fill="#FFE48A" d="M318 86Q320 98 332 100Q320 102 318 114Q316 102 304 100Q316 98 318 86Z"/>
<!-- terminal -->
<rect x="128" y="278" width="256" height="180" rx="26" fill="#3D3654"/>
<path fill="#FFFFFF" opacity=".15" d="M154 278H358C372 278 384 290 384 304V308C384 294 372 282 358 282H154C140 282 128 294 128 308V304C128 290 140 278 154 278Z"/>
<rect x="144" y="300" width="224" height="142" rx="14" fill="#1D1A2C"/>
<path fill="none" stroke="#FF9ECF" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" d="M172 344L200 366L172 388"/>
<rect x="212" y="378" width="46" height="12" rx="6" fill="#FFD1E4"/>
<!-- fingers over the top edge -->
<rect x="150" y="294" width="44" height="7" rx="3.5" fill="#000000" opacity=".3"/>
<rect x="318" y="294" width="44" height="7" rx="3.5" fill="#000000" opacity=".3"/>
<path fill="#FFE7DC" d="M150 296L150 280C150 272 161 272 161 280C161 270 172 270 172 279C172 270 183 270 183 280C183 272 194 272 194 281L194 296C194 300 150 300 150 296Z"/>
<path fill="#FFE7DC" d="M318 296L318 281C318 272 329 272 329 280C329 270 340 270 340 279C340 270 351 270 351 280C351 272 362 272 362 280L362 296C362 300 318 300 318 296Z"/>
<path fill="none" stroke="#F2BBA8" stroke-width="2" stroke-linecap="round" d="M161 282V294M172 281V294M183 282V294M329 282V294M340 281V294M351 282V294"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

+33
View File
@@ -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(())
}
+47
View File
@@ -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" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key><string>en</string>
<key>CFBundleDisplayName</key><string>ELYTerminal</string>
<key>CFBundleExecutable</key><string>elyterminal</string>
<key>CFBundleIconFile</key><string>ELYTerminal</string>
<key>CFBundleIdentifier</key><string>com.zacharyzhang.elyterminal</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleName</key><string>ELYTerminal</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>$version</string>
<key>CFBundleVersion</key><string>$version</string>
<key>LSApplicationCategoryType</key><string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key><string>11.0</string>
<key>NSHighResolutionCapable</key><true/>
</dict>
</plist>
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"
+8
View File
@@ -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: