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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion .github/workflows/mux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,25 @@ permissions:

jobs:
test:
runs-on: ${{ vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }}
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os == 'macos' && (vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15') || 'ubuntu-latest' }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
os: [macos, linux]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Set persist-credentials: false on checkout steps.

Static analysis flags both checkout steps for credential persistence (artipacked). The checked-out git credentials remain on disk for the rest of the job unless explicitly disabled, which is unnecessary here since no step needs to push/authenticate as the checkout token.

🔒 Proposed fix
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+        with:
+          persist-credentials: false

Apply to both the test job (line 33) and windows-experimental job (line 83).

Also applies to: 83-83

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 33-33: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/mux.yml at line 33, Add persist-credentials: false to
every actions/checkout step in the workflow, including the checkout entries in
the test and windows-experimental jobs. Update the checkout configuration itself
rather than any later step, so the Actions token is not left on disk after
checkout since no subsequent step needs git push/authenticated access.

Source: Linters/SAST tools


- name: Init ghostty submodule
run: git submodule update --init --depth 1 ghostty

- name: Install Linux build dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y clang libclang-dev pkg-config

- name: Install zig
run: ./scripts/install-zig-ci.sh

Expand Down Expand Up @@ -62,3 +73,36 @@ jobs:
- name: Detach/reattach smoke test
working-directory: mux
run: python3 scripts/smoke-attach.py

windows-experimental:
name: windows experimental (x86_64-gnu)
runs-on: windows-latest
timeout-minutes: 40
continue-on-error: true
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Init ghostty submodule
run: git submodule update --init --depth 1 ghostty

- name: Install zig
uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # v2.0.5
with:
version: 0.15.2

- name: Install Rust GNU target
shell: bash
run: |
rustup target add x86_64-pc-windows-gnu
echo "C:\\msys64\\mingw64\\bin" >> "$GITHUB_PATH"

- name: Build libghostty-vt for Windows GNU
shell: bash
working-directory: ghostty
run: |
zig build -Demit-lib-vt=true -Demit-xcframework=false -Doptimize=ReleaseFast -Dtarget=x86_64-windows-gnu --prefix "$RUNNER_TEMP/ghostty-vt-win-gnu"
zig ar t "$RUNNER_TEMP/ghostty-vt-win-gnu/lib/ghostty-vt-static.lib" | head

- name: cargo build mux-tui for Windows GNU
working-directory: mux
run: cargo build -p mux-tui --target x86_64-pc-windows-gnu --locked
Comment on lines +99 to +108

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Manual zig build step for ghostty-vt looks redundant with build.rs.

The "Build libghostty-vt for Windows GNU" step builds into $RUNNER_TEMP/ghostty-vt-win-gnu purely as a standalone check, but ghostty-vt-sys/build.rs invokes its own zig build (with the same -Dtarget=x86_64-windows-gnu cross-target logic, see build.rs lines 44-51) into OUT_DIR when cargo build -p mux-tui --target x86_64-pc-windows-gnu runs at line 108. That means the Ghostty VT static library is built twice, doubling the slowest part of this job for no functional purpose (the manually built archive at $RUNNER_TEMP is never consumed by the cargo build). If it's meant purely as an early smoke-check, consider that this experimental job already tolerates failures (continue-on-error: true), so the extra build mainly costs CI time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/mux.yml around lines 99 - 108, The standalone “Build
libghostty-vt for Windows GNU” step is redundant with the
`ghostty-vt-sys/build.rs` path used by `cargo build -p mux-tui --target
x86_64-pc-windows-gnu`. Remove the explicit `zig build`/archive inspection step
from the workflow, or fold it into a single smoke-check if needed, so the
Windows GNU `mux-tui` job relies on the existing `build.rs`-driven `ghostty-vt`
build only once.

94 changes: 91 additions & 3 deletions mux/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions mux/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ unicode-width = "0.2"
base64 = "0.22"
libc = "0.2"
tungstenite = { version = "0.24", default-features = false, features = ["handshake"] }
uds_windows = "1.2"

[profile.release]
lto = "thin"
Expand Down
17 changes: 13 additions & 4 deletions mux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ cargo test # unit + integration tests

Detach with prefix-d while attached; the headless session keeps running and `attach` reconnects with full screen state (VT replay + live stream). A local (non-attach) `cmux-mux` ends its session on quit.

## Platforms

cmux-mux supports macOS and Linux. Runtime sockets live under `$XDG_RUNTIME_DIR/cmux-mux-<uid>` when `XDG_RUNTIME_DIR` is set, then `$TMPDIR/cmux-mux-<uid>`, then `/tmp/cmux-mux-<uid>`. Config uses `CMUX_MUX_CONFIG`, then `$XDG_CONFIG_HOME/cmux/mux.json`, then `~/.config/cmux/mux.json`; Ghostty selection colors are seeded from `$XDG_CONFIG_HOME/ghostty/config`, `~/.config/ghostty/config`, and on macOS the Ghostty Application Support config. Launched Chrome profiles use the macOS Application Support path or `$XDG_DATA_HOME/cmux-mux/chrome-profile`, falling back to `~/.local/share/cmux-mux/chrome-profile`.

PTY tabs use `$SHELL`; if it is unset, Unix falls back to `/bin/bash` when present and then `/bin/sh`. Chrome discovery checks configured `browser.chrome_binary` first. macOS then checks the standard Chrome, Chromium, Brave, and Edge app bundles before PATH names; Linux checks `google-chrome`, `google-chrome-stable`, `chromium`, and `chromium-browser` from PATH, then common `/usr/bin`, `/snap/bin`, and `/opt` locations.

Windows support via ConPTY is planned for phase 2; the transport, config, and shell seams are already isolated, but Windows is not documented as supported yet.

Keys (prefix Ctrl-b, tmux-style): `c` new screen, `n`/`p` next/previous screen, `&` close screen, `,` rename screen, `t` new PTY tab, `B` new browser tab URL prompt, `Tab`/`BackTab` next/previous tab, `1`-`9` select tab, `%` split right, `"` split down, `h j k l`/arrows move focus, `x` close tab, `X` close pane, `$` rename workspace, `w`/`W` switch/create workspace, `s` toggle the workspace sidebar, PageUp/PageDown scrollback, `d` quit, `Ctrl-b` twice sends a literal Ctrl-b. Modeless Alt shortcuts are also on by default: `Alt-n` smart-splits the focused pane, `Alt-h/j/k/l` or Alt-arrows move focus, `Alt-[`/`Alt-]` switch screens, `Alt-t` opens a tab, and `Alt-=`/`Alt--` resize the focused split.

Every pane draws a border box; the active pane's border is highlighted, the pane under the mouse gets a hover shade, and the box is where flashing notifications will hook in later. The top border doubles as an always-visible tab bar: tabs are numbered (`1`, `2`, ...; the process title follows the number when reported), clicking a title switches, dragging a tab reorders it within the pane or moves it to another pane's tab bar, the trailing `+` opens a new tab, and when tabs overflow, `‹`/`›` arrows (or the wheel over the bar) scroll them while the active tab stays visible. User-assigned tab names replace the generated number/title label outright. Drag a shared pane border to resize that split live; dragging a corner moves both intersecting splits, and outer pane edges are inert. Click anywhere in a pane to focus it. The status bar shows the active workspace's screens: click an entry to switch, the trailing `+` for a new screen; it spans only the pane region (not the sidebar). Right-click a pane for rename tab / new tab / split right / split down / close tab / close pane; right-click a workspace in the sidebar for rename/close; right-click a screen in the status bar for rename/close. Context menus and prompts draw muted borders; menu items keep one-cell side padding and the hover/selection highlight spans the full inner row. Right-press, drag, and release on a row activates that row. Prompts use readline-style editing with shortcut buttons (`Clear ^C`, `Cancel esc`, `OK ⏎`); Enter commits, Esc cancels, Ctrl-C clears, and empty tab/screen names fall back to defaults. Right-clicking while the prompt is open shakes it instead of opening a menu. The sidebar reserves two lines per workspace (name, then the active pane's title) under a `workspaces` header with a blank line after it and between entries; click an entry to switch, drag entries to reorder workspaces, `+ new workspace` to create one, and drag the sidebar's right border to resize it for the current session.
Expand Down Expand Up @@ -53,7 +61,7 @@ If no reusable browser is found and no Chrome binary is found, browser tab creat

## Configuration

`~/.config/cmux/mux.json` (override with `CMUX_MUX_CONFIG`); every key is optional:
`CMUX_MUX_CONFIG`, `$XDG_CONFIG_HOME/cmux/mux.json`, or `~/.config/cmux/mux.json`; every key is optional:

```json
{
Expand Down Expand Up @@ -107,14 +115,15 @@ If no reusable browser is found and no Chrome binary is found, browser tab creat
}
```

Colors are `#rrggbb`, `#rgb`, or an xterm-256 index. The selection colors default to the user's Ghostty config (`selection-background`/`selection-foreground` from `~/.config/ghostty/config`), falling back to a dark grey. `sidebar_rail` controls the active workspace rail, `sidebar_active_bg` its two-row background, `tab_rail` the active tab chip rail, `tab_bg` inactive solid tab chips, and `tab_active_bg` overrides the focused/unfocused active tab chip backgrounds when set. Tabs are numbered `1 2 3…` by default; recognized agent programs (the `agents` list) surface after the number, `show_titles` restores full process titles, and a user-assigned tab name overrides both. `sidebar.max_width` defaults to `0` for unlimited, while live drag still leaves at least 40 columns for panes. `scrollbar.position` is `"column"` by default or `"border"` for the old right-border overlay. Browser config is optional: `chrome_binary` overrides binary discovery, `cdp_url` accepts `ws://...` or `http://host:port`, `discover` defaults to true, `discover_ports` defaults to `[9222]`, `user_data_dir` overrides the launched profile path, and `ephemeral` restores temporary-profile behavior. When `ephemeral` is true it takes precedence over `user_data_dir`: cmux creates and later deletes a fresh temp profile and never deletes the configured directory. Every prefix/modeless binding is remappable via `keys` (formats: `"c"`, `"%"`, `"ctrl+b"`, `"alt+enter"`, `"tab"`, `"backtab"`, `"pageup"`); values may be a string, an array of strings, or `"none"` to unbind. Set `"alt_shortcuts": false` to remove default Alt chords without blocking user-configured Alt chords. `1`-`9` stay fixed to tab selection. The old key name `"rename-pane"` is still accepted as an alias for `"rename-tab"`.
Colors are `#rrggbb`, `#rgb`, or an xterm-256 index. The selection colors default to the user's Ghostty config (`selection-background`/`selection-foreground` from the platform paths above), falling back to a dark grey. `sidebar_rail` controls the active workspace rail, `sidebar_active_bg` its two-row background, `tab_rail` the active tab chip rail, `tab_bg` inactive solid tab chips, and `tab_active_bg` overrides the focused/unfocused active tab chip backgrounds when set. Tabs are numbered `1 2 3…` by default; recognized agent programs (the `agents` list) surface after the number, `show_titles` restores full process titles, and a user-assigned tab name overrides both. `sidebar.max_width` defaults to `0` for unlimited, while live drag still leaves at least 40 columns for panes. `scrollbar.position` is `"column"` by default or `"border"` for the old right-border overlay. Browser config is optional: `chrome_binary` overrides binary discovery, `cdp_url` accepts `ws://...` or `http://host:port`, `discover` defaults to true, `discover_ports` defaults to `[9222]`, `user_data_dir` overrides the launched profile path, and `ephemeral` restores temporary-profile behavior. When `ephemeral` is true it takes precedence over `user_data_dir`: cmux creates and later deletes a fresh temp profile and never deletes the configured directory. Every prefix/modeless binding is remappable via `keys` (formats: `"c"`, `"%"`, `"ctrl+b"`, `"alt+enter"`, `"tab"`, `"backtab"`, `"pageup"`); values may be a string, an array of strings, or `"none"` to unbind. Set `"alt_shortcuts": false` to remove default Alt chords without blocking user-configured Alt chords. `1`-`9` stay fixed to tab selection. The old key name `"rename-pane"` is still accepted as an alias for `"rename-tab"`.

## Control socket

Every instance serves a JSON-lines protocol on a unix socket (default `$TMPDIR/cmux-mux-<uid>/<session>.sock`, also exported to children as `CMUX_MUX_SOCKET`). One request per line:
Every instance serves a JSON-lines protocol on a unix socket (default under the platform runtime directory, also exported to children as `CMUX_MUX_SOCKET`). One request per line:

```bash
SOCK=${TMPDIR:-/tmp}/cmux-mux-$(id -u)/main.sock
SESSION=main
SOCK=${CMUX_MUX_SOCKET:-${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/cmux-mux-$(id -u)/${SESSION}.sock}
printf '%s\n' '{"id":1,"cmd":"identify"}' | nc -U "$SOCK"
printf '%s\n' '{"id":2,"cmd":"list-workspaces"}' | nc -U "$SOCK"
printf '%s\n' '{"id":3,"cmd":"send","surface":1,"text":"ls\r"}' | nc -U "$SOCK"
Expand Down
36 changes: 27 additions & 9 deletions mux/crates/ghostty-vt-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,33 @@ fn main() {
// is an order of magnitude slower.
let zig = env::var("ZIG").unwrap_or_else(|_| "zig".to_string());
let prefix = out_dir.join("ghostty-vt");
let status = Command::new(&zig)
let target = env::var("TARGET").unwrap();
let host = env::var("HOST").unwrap();
let mut command = Command::new(&zig);
command
.current_dir(&ghostty_dir)
.arg("build")
.arg("-Demit-lib-vt=true")
.arg("-Demit-xcframework=false")
.arg("-Doptimize=ReleaseFast")
.arg("--prefix")
.arg(&prefix)
.status()
.unwrap_or_else(|e| {
panic!("failed to run `{zig} build` in {}: {e}", ghostty_dir.display())
});
.arg("-Doptimize=ReleaseFast");
if target != host {
if let Some(zig_target) = zig_target_for_rust_target(&target) {
command.arg(format!("-Dtarget={zig_target}"));
}
}
Comment on lines +44 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Silent no-op when cross-compiling to an unmapped target.

When target != host and zig_target_for_rust_target returns None (any target other than the three Windows triples), the code silently skips -Dtarget=..., so zig build proceeds using the host's native target instead of the requested cross target. The resulting libghostty-vt archive would then be built for the wrong architecture/OS, and linking it into the cross-compiled Rust binary would likely fail at link time — or worse, produce a corrupt binary if ABI-compatible enough to link but not run. This should fail loudly rather than silently mis-target the build.

🛠️ Proposed fix
     if target != host {
-        if let Some(zig_target) = zig_target_for_rust_target(&target) {
-            command.arg(format!("-Dtarget={zig_target}"));
-        }
+        match zig_target_for_rust_target(&target) {
+            Some(zig_target) => {
+                command.arg(format!("-Dtarget={zig_target}"));
+            }
+            None => {
+                panic!(
+                    "cross-compiling to unsupported target {target} from host {host}: \
+                     add a mapping in zig_target_for_rust_target"
+                );
+            }
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if target != host {
if let Some(zig_target) = zig_target_for_rust_target(&target) {
command.arg(format!("-Dtarget={zig_target}"));
}
}
if target != host {
match zig_target_for_rust_target(&target) {
Some(zig_target) => {
command.arg(format!("-Dtarget={zig_target}"));
}
None => {
panic!(
"cross-compiling to unsupported target {target} from host {host}: \
add a mapping in zig_target_for_rust_target"
);
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mux/crates/ghostty-vt-sys/build.rs` around lines 44 - 48, The
cross-compilation path in build.rs silently falls back to the host target when
zig_target_for_rust_target(&target) returns None, which can misbuild
libghostty-vt for the wrong platform. Update the target handling in the build
command setup so the non-host branch explicitly errors out when no Zig mapping
exists instead of skipping -Dtarget; use the existing zig_target_for_rust_target
and the command construction around target != host to fail loudly for unmapped
targets.

let status = command.arg("--prefix").arg(&prefix).status().unwrap_or_else(|e| {
panic!("failed to run `{zig} build` in {}: {e}", ghostty_dir.display())
});
if !status.success() {
panic!("zig build of libghostty-vt failed with {status}");
}

println!("cargo:rustc-link-search=native={}", prefix.join("lib").display());
println!("cargo:rustc-link-lib=static=ghostty-vt");
if target.contains("windows") {
println!("cargo:rustc-link-lib=static=ghostty-vt-static");
} else {
println!("cargo:rustc-link-lib=static=ghostty-vt");
}

// Generate bindings from the public C header.
let include_dir = ghostty_dir.join("include");
Expand All @@ -66,3 +75,12 @@ fn main() {
.expect("bindgen failed for ghostty/vt.h");
bindings.write_to_file(out_dir.join("bindings.rs")).expect("failed to write bindings.rs");
}

fn zig_target_for_rust_target(target: &str) -> Option<&'static str> {
match target {
"x86_64-pc-windows-gnu" => Some("x86_64-windows-gnu"),
"x86_64-pc-windows-msvc" => Some("x86_64-windows-msvc"),
"aarch64-pc-windows-msvc" => Some("aarch64-windows-msvc"),
_ => None,
}
}
Loading
Loading