T25: release builds: scripts/windows-zip.ps1 zips elyterminal.exe, the icon inside as resource 1 (build.rs, embed-resource), a GUI program carrying its C runtime, git started without a console; scripts/linux-tar.sh builds on Debian 12, glibc 2.35 for this build, with a desktop entry and the icon; every window names itself com.zacharyzhang.elyterminal; rust-toolchain.toml pins 1.95.0, and CI installs it without rustup default
CI / platforms (windows-latest) (push) Successful in 1m35s
CI / platforms (ubuntu-24.04) (push) Successful in 3m14s
CI / macos (push) Successful in 9m49s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-15 16:50:22 -04:00
1 parent 2b8ae5d026
commit 0075667ab1
21 files changed
+211 -39

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# The C runtime goes inside, so Windows needs no Visual C++ redistributable.
[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "target-feature=+crt-static"]
+9 -7
View File
@@ -10,10 +10,9 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.95"
components: clippy, rustfmt
# The runner's PATH lacks the host's rustup.
- run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- run: rustup toolchain install --no-self-update
- run: scripts/check.sh
- run: cargo build --locked
@@ -28,9 +27,12 @@ jobs:
CARGO_PROFILE_DEV_DEBUG: 0
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.95"
# The job container comes without rustup.
- if: runner.os == 'Linux'
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain none
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- run: rustup toolchain install --no-self-update
# On a host runner its post step deletes the host's rustup.
- if: runner.os == 'Linux'
uses: Swatinem/rust-cache@v2
+10 -4
View File
@@ -4,12 +4,12 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
## Stack
- Rust 1.95, edition 2024. One binary crate, `elyterminal`.
- Rust 1.95.0, as `rust-toolchain.toml` pins it, edition 2024. One binary crate, `elyterminal`.
- Ely (`ely-gpui-component`) from git.zacharyzhang.com, pinned by rev. gpui and gpui_platform from crates.io's `gpui-pre`, the exact version Ely pins, under their crate names through `package`.
- `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.
- `resvg`, a dev dependency at the version gpui uses, renders the icon's SVG for the macOS app and the Windows `.ico`. `embed-resource`, a build dependency at the version gpui uses, puts the icon into the Windows executable.
## Commands
@@ -23,9 +23,12 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- `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.
- `scripts/windows-zip.ps1`, on Windows with Visual Studio's C++ tools, builds `elyterminal.exe` with its icon and zips it as `ELYTerminal-VERSION-windows-ARCH.zip` in `target/release`. After an edit to the figure, `cargo run --example icons -- ico assets/icons/elyterminal.svg assets/icons/elyterminal.ico` renders the icon again.
- `scripts/linux-tar.sh` builds the release in Docker on `rust:1.95.0-bookworm` and packs it with `com.zacharyzhang.elyterminal.desktop` and the icon as `ELYTerminal-VERSION-linux-ARCH.tar.gz` in `target/release`.
## Layout
- `build.rs`: on Windows, the icon into the executable as resource 1, through `assets/icons/elyterminal.rc`. `.cargo/config.toml`: Windows builds carry the C runtime. `rust-toolchain.toml`: the toolchain rustup installs.
- `src/main.rs`: start. `src/lib.rs`: the modules, public so tests and examples reach them. `src/app.rs`: fonts, settings, integration, then the main window. `src/menus.rs`: the menu bar, the keys of Quit, the font's size and Minimize, and what they do. `src/blur.rs`: what the window shows of the desktop, from the opacity and blur settings, set as they change; `blur/macos` gives gpui's NSVisualEffectView a material that blurs; `blur/wayland` asks a Wayland compositor for blur behind the window's surface through `ext_background_effect_v1`; `blur/x11` names the window's pane in `_KDE_NET_WM_BLUR_BEHIND_REGION`. `src/workspace.rs`: the window's content, Ely's `ColumnShell` with the sessions, terminal and right columns, their toggles, the right column's Git and Files switch, the title, and the sessions' lifecycle. `src/sessions.rs`: a session and its row. `src/files.rs`: the Files panel, the shown session's folder as Ely's `Tree` with Git's marks. `src/git.rs`: git through its command line: Files' marks, and the Git panel's status, stage, unstage, discard, commit and log; `src/git/diff.rs` reads git's diff into Ely's stretches. `src/git/panel.rs`: the Git panel's state and actions; `src/git/panel/view.rs`: how it draws.
- `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`; `window` is the settings window, `window/appearance` its Appearance page, `window/terminal` its Terminal page. `tests/fonts.rs` brings its own main: gpui builds its Mac platform only on the main thread.
- `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance.
@@ -33,7 +36,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; the macOS file holds a copy of the figure's markup, so an edit to the figure is made in both.
- `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; the macOS file holds a copy of the figure's markup, so an edit to the figure is made in both. `elyterminal.ico` is the figure at 16 to 256 px for Windows, and `elyterminal.rc` names it resource 1.
## Rules
@@ -78,6 +81,9 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 2026-10-15: on Wayland the window asks for `ext_background_effect_v1` beside gpui's `org_kde_kwin_blur`. A guest connection on gpui's own display binds it, takes the effect for the window's surface once, and blurs the window's pane: the surface less Ely's resize margin on each free edge of a client-drawn window, worked out each frame and sent only when it changes; it goes and comes back with Blurred and lands at the surface's next commit, which every frame makes. The guest connection reads the display only while the window opens, before gpui's loop runs, and is never dropped: a drop destroys its proxies through the display, which gpui may close first. The log names what the compositor offers, so which way blur goes: `ext_background_effect_v1`, gpui's `org_kde_kwin_blur`, or none, the desktop clear. Its test runs a `wayland-server` compositor on a socket pair.
- 2026-10-15: on a client-drawn window, so on Linux, Ely's `ResizeBorder` paints its pane solid and shadowed under the content, which kept the desktop from showing at all. `ResizeBorder::glass`, added to Ely for this, leaves the pane to the content while the window is see-through: no ground, nor the shadow gpui draws under the box, which would tint it. So the blur covers the pane alone and the resize margin stays clear. On X11 the window names its pane in `_KDE_NET_WM_BLUR_BEHIND_REGION` (CARDINAL, 32: x, y, width, height in the window's pixels), as Ghostty names its own, and drops the property when Blurred goes; KWin reads it, other compositors pass it by. It goes over a connection of ELYTerminal's own to `$DISPLAY`, which gpui opens too: a property belongs to the window, whoever sets it. gpui's own `org_kde_kwin_blur` on KDE's Wayland still covers the whole surface, margin included, beyond ELYTerminal's reach.
- 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. The macOS and Windows jobs run on the owner's own machines, so they skip `Swatinem/rust-cache`: its post step cleans `$CARGO_HOME`, which there is the owner's own, and after every job it deleted `rustup` from `~/.cargo/bin`, leaving its proxies pointing at nothing, and pruned the shared registry; the Linux job keeps it, inside its container. 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: 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. The macOS and Windows jobs run on the owner's own machines, so they skip `Swatinem/rust-cache`: its post step cleans `$CARGO_HOME`, which there is the owner's own, and after every job it deleted `rustup` from `~/.cargo/bin`, leaving its proxies pointing at nothing, and pruned the shared registry; the Linux job keeps it, inside its container. 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. Every job installs the toolchain `rust-toolchain.toml` names with `rustup toolchain install --no-self-update` and never runs `rustup default`, so the owner's machines keep their own default toolchain and rustup; the macOS job puts the host's `~/.cargo/bin` on the runner's PATH, and the Linux container installs rustup first. A toolchain file's components install only with the toolchain: one already there without them makes each cargo call try to download them, so `scripts/linux.sh` and `scripts/linux-tar.sh` add them to their images.
- 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.
- 2026-10-15: a release is a Gitea release tagged `vVERSION` with three builds, each from its script: the macOS disk image for Apple silicon, a Windows zip and a Linux tarball for x86_64. Each window names itself `com.zacharyzhang.elyterminal` through `Window::set_app_id` as it opens: on Linux that is Wayland's app id and X11's `WM_CLASS`, which the desktop entry's name and `StartupWMClass` match, so a dock pairs the window with the icon; gpui's X11 windows read no app id from `WindowOptions`.
- 2026-10-15: on Windows a release build is a GUI program, so no console window opens beside it; a debug build keeps its console for the log. git then starts with `CREATE_NO_WINDOW`, or each call would open a console. Windows builds link the C runtime statically, so the executable needs no Visual C++ redistributable. gpui's build script embeds its manifest, PerMonitorV2 DPI awareness among it; `build.rs` adds the icon as resource 1, which gpui loads for each window, and stops the build when no resource compiler is found.
- 2026-10-15: the Linux tarball builds on Debian 12, in `rust:1.95.0-bookworm`: a binary asks for the glibc symbol versions it was linked against, so it is linked against an old glibc, not the builder's. `scripts/linux-tar.sh` prints the glibc its build asks for, and the README names it: 2.35 for v0.1.0, which runs on Ubuntu 22.04.
Generated
+1
View File
@@ -1755,6 +1755,7 @@ dependencies = [
"core-graphics 0.24.0",
"dirs 7.0.0",
"ely-gpui-component",
"embed-resource",
"env_logger",
"foreign-types",
"futures",
+3
View File
@@ -24,6 +24,9 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_path_to_error = "0.1.20"
[build-dependencies]
embed-resource = "3"
[dev-dependencies]
resvg = { version = "0.46", default-features = false }
+6
View File
@@ -129,6 +129,12 @@ Cmd-, opens it. Categories on the left, rows on the right, Ely's settings compon
- 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.
## 15. Release
- A release on Gitea, tagged `vVERSION`, carries three builds: the macOS disk image (Apple silicon), a Windows zip (x86_64) and a Linux tarball (x86_64).
- The Windows executable carries the icon. The Linux tarball carries the icon and a desktop entry, and the window names itself after it, `com.zacharyzhang.elyterminal`, as the macOS bundle does.
- Each build comes from its platform's script: `scripts/mac-app.sh`, `scripts/windows-zip.ps1`, `scripts/linux-tar.sh`.
## Deferred
Splits, tabs inside a session, profiles, key rebinding, kitty graphics, sixel, OSC 133 prompt marks, session restore, push and pull, quick terminal.
+21 -3
View File
@@ -2,15 +2,33 @@
A terminal for macOS, Windows and Linux, in Rust on GPUI and Ely components. Sessions sit on the left, the terminal in the middle, Git or Files on the right. Every setting has a control in the settings window.
## Install
A release on Gitea carries three builds.
- macOS 11 or later, Apple silicon: open `ELYTerminal-VERSION.dmg` and drag ELYTerminal into Applications. The app is signed ad hoc, not notarized, so macOS stops its first open: in System Settings, Privacy & Security, press Open Anyway.
- Windows 10 or later, x86_64: unzip `ELYTerminal-VERSION-windows-x86_64.zip` and run `elyterminal.exe`. It is unsigned; if SmartScreen stops it, press More info, then Run anyway.
- Linux, x86_64, glibc 2.35 or later, with libxkbcommon, libxkbcommon-x11 and libxcb:
```sh
tar -xzf ELYTerminal-VERSION-linux-x86_64.tar.gz
cd ELYTerminal-VERSION-linux-x86_64
install -Dm755 elyterminal -t ~/.local/bin
install -Dm644 com.zacharyzhang.elyterminal.desktop -t ~/.local/share/applications
install -Dm644 com.zacharyzhang.elyterminal.svg -t ~/.local/share/icons/hicolor/scalable/apps
```
The desktop entry runs `elyterminal`, so `~/.local/bin` must be on `PATH`.
## Build and run
Rust 1.95.
Rust 1.95.0, which `rust-toolchain.toml` pins; rustup installs it on the first build.
```sh
cargo run --release
```
On macOS, `scripts/mac-app.sh` builds `ELYTerminal.app`, with its icon, and a disk image of it in `target/release`.
Each platform's script builds its release in `target/release`: on macOS, `scripts/mac-app.sh` builds `ELYTerminal.app`, with its icon, and a disk image of it; on Windows, `scripts/windows-zip.ps1` zips `elyterminal.exe`, its icon inside; on Linux, `scripts/linux-tar.sh` builds in Docker and packs the binary with its desktop entry and icon.
On Linux, install the libraries gpui links first. On Debian and Ubuntu:
@@ -19,7 +37,7 @@ sudo apt-get install pkg-config libwayland-dev libxkbcommon-dev libxkbcommon-x11
libxcb1-dev libx11-xcb-dev libfontconfig-dev libfreetype-dev
```
The app writes its log to stderr. `RUST_LOG=elyterminal=debug` writes more; `trace` logs each paint.
The app writes its log to stderr. `RUST_LOG=elyterminal=debug` writes more; `trace` logs each paint. A Windows release build opens no console: `elyterminal.exe 2> log.txt` keeps its log.
## What it does
+4
View File
@@ -64,3 +64,7 @@ Progress truth. Top to bottom, one at a time. A task is ticked only after its co
## 14. Icon and the macOS app
- [x] 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.
## 15. Release
- [ ] T25 Windows zip with the icon in the executable, Linux tarball with the icon and a desktop entry, the app id on Linux windows, their scripts; release v0.1.0 on Gitea with the three builds.
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+1
View File
@@ -0,0 +1 @@
1 ICON "elyterminal.ico"
+11
View File
@@ -0,0 +1,11 @@
//! On Windows, the icon goes into the executable as resource 1, which gpui loads.
fn main() {
println!("cargo:rerun-if-changed=assets/icons");
embed_resource::compile(
"assets/icons/elyterminal.rc",
embed_resource::ParamsIncludeDirs(["assets/icons"]),
)
.manifest_required()
.expect("build: the icon did not go into the Windows executable");
}
+56 -18
View File
@@ -1,33 +1,71 @@
//! Renders an SVG into a macOS iconset: `icons SVG FOLDER`.
//! Renders the icon: `icons iconset SVG FOLDER` or `icons ico SVG FILE`.
use std::path::Path;
use anyhow::Context as _;
use anyhow::{Context as _, bail};
use resvg::{tiny_skia, usvg};
fn main() -> anyhow::Result<()> {
let mut args = std::env::args().skip(1);
let form = args.next().context("icons: name a form, iconset or ico")?;
let svg = args.next().context("icons: name an SVG to render")?;
let out = args
.next()
.context("icons: name an iconset folder to write")?;
let out = args.next().context("icons: name where 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()))?;
}
match form.as_str() {
"iconset" => iconset(&tree, &out)?,
"ico" => ico(&tree, &out)?,
other => bail!("icons: no form {other}; iconset or ico"),
}
println!("icons: {svg} drawn into {out}");
Ok(())
}
/// A macOS iconset: each size at 1x and 2x.
fn iconset(tree: &usvg::Tree, out: &str) -> anyhow::Result<()> {
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 png = Path::new(out).join(format!("icon_{points}x{points}{suffix}.png"));
std::fs::write(&png, draw(tree, points * scale)?)
.with_context(|| format!("icons: cannot write {}", png.display()))?;
}
}
Ok(())
}
/// A Windows icon: one PNG per size, as Windows reads since Vista.
fn ico(tree: &usvg::Tree, out: &str) -> anyhow::Result<()> {
let sides = [16, 24, 32, 48, 64, 128, 256];
let images = sides
.iter()
.map(|&side| draw(tree, side))
.collect::<anyhow::Result<Vec<_>>>()?;
let mut file = vec![0, 0, 1, 0];
file.extend((images.len() as u16).to_le_bytes());
let mut offset = 6 + 16 * images.len();
for (side, png) in sides.iter().zip(&images) {
// The format writes 256 as 0.
let edge = (side % 256) as u8;
file.extend([edge, edge, 0, 0]);
file.extend(1u16.to_le_bytes());
file.extend(32u16.to_le_bytes());
file.extend((png.len() as u32).to_le_bytes());
file.extend((offset as u32).to_le_bytes());
offset += png.len();
}
for png in &images {
file.extend(png);
}
std::fs::write(out, file).with_context(|| format!("icons: cannot write {out}"))
}
/// The SVG drawn into a square PNG `side` pixels wide.
fn draw(tree: &usvg::Tree, side: u32) -> anyhow::Result<Vec<u8>> {
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());
pixmap.encode_png().context("icons: cannot encode a PNG")
}
+4
View File
@@ -0,0 +1,4 @@
[toolchain]
channel = "1.95.0"
components = ["clippy", "rustfmt"]
profile = "minimal"
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
# linux-tar.sh: the release build on Debian 12's glibc 2.36, in a tarball with its desktop entry and icon.
set -eu
cd "$(dirname "$0")/.."
docker build -q -t elyterminal-release - >/dev/null <<'EOF'
FROM rust:1.95.0-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends pkg-config libwayland-dev libxkbcommon-dev \
libxkbcommon-x11-dev libxcb1-dev libx11-xcb-dev libfontconfig-dev libfreetype-dev \
&& rm -rf /var/lib/apt/lists/* \
&& rustup component add clippy rustfmt
EOF
run() { docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/src" -w /src elyterminal-release "$@"; }
run cargo build --release --locked
version=$(run cargo pkgid)
version=${version##*[#@]}
case $version in [0-9]*) ;; *) echo "linux-tar: cargo pkgid names no version" >&2; exit 1 ;; esac
out=target/release
id=com.zacharyzhang.elyterminal
name=ELYTerminal-$version-linux-$(uname -m)
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir "$work/$name"
cp "$out/elyterminal" "$work/$name/"
cp assets/icons/elyterminal.svg "$work/$name/$id.svg"
cat > "$work/$name/$id.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=ELYTerminal
Exec=elyterminal
Icon=$id
Terminal=false
Categories=System;TerminalEmulator;
StartupWMClass=$id
EOF
tar -C "$work" -czf "$out/$name.tar.gz" "$name"
glibc=$(run sh -c "objdump -T $out/elyterminal | grep -o 'GLIBC_[0-9.]*' | sort -u -V | tail -n 1")
echo "linux-tar: $out/$name.tar.gz, glibc ${glibc#GLIBC_} or later"
+3 -3
View File
@@ -1,17 +1,17 @@
#!/bin/sh
# mac-app.sh: target/release/ELYTerminal.app, signed ad hoc, and its disk image.
# mac-app.sh: ELYTerminal.app in target/release, signed ad hoc, and its disk image.
set -eu
cd "$(dirname "$0")/.."
cargo build --release --locked
version=$(cargo pkgid)
version=${version##*[#@]}
case $version in [0-9]*) ;; *) echo "mac-app: cargo pkgid names no version" >&2; exit 1 ;; esac
out=target/release
out=${CARGO_TARGET_DIR:-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"
iconset 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/"
+13
View File
@@ -0,0 +1,13 @@
# windows-zip.ps1: the release build, its icon inside, in a zip.
Set-Location (Join-Path $PSScriptRoot '..') -ErrorAction Stop
cargo build --release --locked
if ($LASTEXITCODE -ne 0) { throw "windows-zip: cargo build failed" }
$version = ((cargo pkgid) -split '[#@]')[-1]
if ($version -notmatch '^[0-9]') { throw "windows-zip: cargo pkgid names no version" }
$arch = @{ AMD64 = 'x86_64'; ARM64 = 'aarch64' }[$env:PROCESSOR_ARCHITECTURE]
if (-not $arch) { throw "windows-zip: no name for $env:PROCESSOR_ARCHITECTURE" }
$target = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { 'target' }
$out = Join-Path $target 'release'
$zip = Join-Path $out "ELYTerminal-$version-windows-$arch.zip"
Compress-Archive -Force -Path (Join-Path $out 'elyterminal.exe') -DestinationPath $zip -ErrorAction Stop
Write-Output "windows-zip: $zip"
+4
View File
@@ -17,6 +17,9 @@ use crate::{
workspace::{self, TITLE, Workspace},
};
/// The id the macOS bundle and Linux desktop entry share.
pub const APP_ID: &str = "com.zacharyzhang.elyterminal";
/// Fonts, settings, integration scripts and the chosen font, each checked before a window opens.
pub fn start(cx: &mut App) -> anyhow::Result<()> {
cx.text_system()
@@ -96,6 +99,7 @@ pub fn open(launch: Launch, cx: &mut App) -> anyhow::Result<WindowHandle<Workspa
};
let (pty, events) = spawn(&launch, (80, 24), (8, 16), config(SettingsStore::get(cx)))?;
let window = cx.open_window(options, |window, cx| {
window.set_app_id(APP_ID);
window
.observe_window_appearance(|_, cx| themes::apply(cx))
.detach();
+8 -1
View File
@@ -25,10 +25,17 @@ fn line<A: AsRef<OsStr>>(args: &[A]) -> String {
args.join(" ")
}
/// Keeps Windows from opening a console window for git.
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
/// Runs git in `folder`: English messages, literal paths, no lock the user's own git could meet; `input` feeds stdin.
fn git<A: AsRef<OsStr>>(folder: &Path, args: &[A], input: &[u8]) -> anyhow::Result<Output> {
let named = || format!("git: `git {}` in {}", line(args), folder.display());
let mut child = Command::new("git")
let mut command = Command::new("git");
#[cfg(windows)]
std::os::windows::process::CommandExt::creation_flags(&mut command, CREATE_NO_WINDOW);
let mut child = command
.arg("-C")
.arg(folder)
.args(["--no-optional-locks", "--literal-pathspecs"])
+3
View File
@@ -1,3 +1,6 @@
// A release build on Windows opens no console window.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use ely_gpui_component::Assets;
use elyterminal::{
app,
+2 -1
View File
@@ -17,7 +17,7 @@ use gpui::{
};
use super::{Settings, SettingsStore, StartIn};
use crate::{menus, term::BLINK};
use crate::{app::APP_ID, menus, term::BLINK};
mod appearance;
mod terminal;
@@ -75,6 +75,7 @@ fn open_window(cx: &mut App) {
..Default::default()
};
match cx.open_window(options, |window, cx| {
window.set_app_id(APP_ID);
cx.new(|cx| SettingsWindow::new(window, cx))
}) {
Ok(_) => log::info!("settings: the window opens"),
+11 -2
View File
@@ -131,7 +131,7 @@ What no check reached: no one has looked at the window on Windows, which CI buil
| PRD line | Evidence | Status |
|---|---|---|
| Each task ticked with a passing review | each task is ticked only after its review, as `tasks/reviews.md` records; T04 and T10 reached the three-round cap without a passing round, their last fixes standing on red-green tests | Partial |
| `scripts/check.sh` passes | macOS: 135 tests, clippy, fmt, 500 lines; Linux in Colima: 138 tests, clippy | Pass |
| `scripts/check.sh` passes | macOS: 135 tests, clippy, fmt, 500 lines; Linux through `scripts/linux.sh`: 138 tests, clippy, on aarch64 in Colima and on x86_64 in WSL's Docker | Pass |
| CI green on macOS, Ubuntu and Windows | run 926 on `65bb4d3`: all three jobs pass; see the CI notes below | Pass |
## 14. Icon and the macOS app
@@ -139,9 +139,18 @@ What no check reached: no one has looked at the window on Windows, which CI buil
| 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 |
| macOS: the rounded square; Windows and Linux: the figure alone, flat | T24 | `elyterminal-macos.svg` on Apple's grid; `elyterminal.svg`, which the Linux tarball carries and the Windows `.ico` renders | 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 |
## 15. Release
| PRD line | Task | Evidence | Status |
|---|---|---|---|
| A Gitea release, `vVERSION`, with the macOS disk image, a Windows zip and a Linux tarball | T25 | published after T25's review | Open |
| The Windows executable carries the icon | T25 | its resources: icon group 1 with the seven images of `elyterminal.ico`, 16 to 256 px, and gpui's manifest (PerMonitorV2); a GUI program, importing no C runtime DLL; not yet seen on a Windows screen | Pass |
| The Linux tarball carries the icon and a desktop entry; the window names itself `com.zacharyzhang.elyterminal` | T25 | on Ubuntu 22.04: `desktop-file-validate` reports nothing, every library resolves, glibc 2.35; under Xvfb `WM_CLASS` reads `com.zacharyzhang.elyterminal` twice, and under headless sway the window's `app_id` reads the same | Pass |
| Each build comes from its script | T25 | `scripts/mac-app.sh` on macOS, `scripts/windows-zip.ps1` on the owner's Windows PC, `scripts/linux-tar.sh` in the owner's WSL | 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: