ezTerm

Changelog

Newest releases first. Sourced from docs/release-notes/ in the repo.

ezTerm v1.3.7

Patch: drag-and-drop in the sessions sidebar now actually moves items. PR #72 added the right context-menu hit zones and the "Move to folder…" right-click path, but the underlying drag flow was still broken — dragging a session onto a folder grayed the source row and then silently did nothing.

What changed

  • ui/components/sessions-sidebar.tsxNodeView is hoisted out of SessionsSidebar. The component-inside-component pattern gave NodeView a fresh function identity on every setState, so each dragoversetDragTarget re-render caused React to unmount + remount the whole tree. That ripped the drag-source DOM element out from under the browser, which then aborted the in-flight HTML5 drag — dragover and drop never fired again, and the move never happened.
  • NodeView now lives at module scope and takes a single ctx prop bundling the state and handlers it needs (expanded, drag, dragTarget, selectedId, connectedSessionIds, the handle* drag callbacks, slotForRow, toggleFolder, the menu openers, setConfirm, setSelectedId, openTabAction). The ctx object is rebuilt each render, but NodeView's identity is stable — React reconciles props instead of remounting.

The right-click "Move to folder…" path was unaffected by the bug and remains the same — it doesn't depend on a long-lived DOM element across re-renders, which is why it worked while drag-and-drop didn't.

Other

  • Synced version across Cargo.toml, src-tauri/tauri.conf.json, ui/package.json, and the lockfiles. Previously tauri.conf.json was pinned at 1.0.2 and ui/package.json at 1.1.0, both stale from earlier merges.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.3.4

Minor: SSH port forwarding (-L, -R, -D) is now first-class inside ezTerm sessions.

What's new

Three new forward kinds, configurable per session and runnable on a live tab:

  • Local (-L) — a port on your machine tunnels to a destination reachable from the remote (localhost:5432 → db.internal:5432). The Postgres / Redis / internal-web-UI workflow.
  • Remote (-R) — a port on the remote tunnels back to a destination reachable from your machine. Sharing a local dev server with a teammate's box, exposing a local webhook receiver.
  • Dynamic (-D) — a local SOCKS5 proxy that routes your client app's traffic through the remote. Point Chrome or curl (--socks5-hostname) at the bind port to browse from the remote's network.

Two lifetimes

  • Persistent — configured per session in the new Forwards tab of the session edit dialog. Forwards with Auto-start on come up automatically when the session connects.
  • Ad-hoc — added to a live tab from the new Forwards side pane (Network icon in the tab bar). Lives until the connection drops.

Both share the same UI and runtime.

UX details

  • Default bind address is 127.0.0.1. Any other value (including 0.0.0.0) shows a yellow LAN-reachable warning so you don't accidentally expose a tunnel to other machines on your network.
  • Ports below 1024 are allowed in the UI; the OS enforces. Bind failures show a clear message with elevation guidance.
  • Forwards have an optional name (e.g. "Postgres dev"); empty falls back to an auto-label.
  • Editing a running forward stops and restarts it in place — no separate Apply step.
  • Concurrent tabs on the same session don't dedupe — the second tab's bind hits EADDRINUSE and surfaces the error in its pane (matches OpenSSH).

How it works under the hood

  • A new session_forwards table holds per-session config. Auto-start scans run at the end of ssh::connect_impl; failures don't abort the connect — they surface as toast + side-pane error.
  • The Rust runtime lives in ssh/forwards/ with one file per kind. russh 0.60 already provides channel_open_direct_tcpip, tcpip_forward, and cancel_tcpip_forward; no new crate dependencies.
  • Inbound forwarded-tcpip channels (for -R) are routed back to the right forward task via a per-connection dispatch table keyed on (bind_addr, bind_port).
  • The SOCKS5 server is hand-rolled (no-auth + CONNECT only); the protocol is small enough to keep the dependency surface clean.
  • Status transitions emit forwards:status:{connection_id} events so the side pane stays live.

Pre-ship audit improvements

A multi-agent review of the port-forwarding branch turned up findings that were folded in before tagging:

  • Concurrencyssh_handle switched from Arc<Mutex<Handle>> to Arc<Handle> (every method we call takes &self); concurrent SSH channel opens no longer serialize behind one lock. Inbound forwarded-tcpip dispatch uses a sync std::sync::RwLock and an unbounded mpsc (backpressure is enforced by the SSH window). Dispatch entry is removed before awaiting cancel_tcpip_forward so a cancelled forward can't accept stray channels into a dropped receiver.
  • Throughputcopy_bidirectional runs with 64 KiB buffers (was 8 KiB) so a single stream can fill the SSH channel window between window-adjust round-trips.
  • Backpressure — per-forward Semaphore(256) caps in-flight pumps; accept floods shed rather than spawn unbounded tasks.
  • SOCKS5 hardening — every read_exact is wrapped in a 10 s timeout (slowloris defense), a fixed [u8; 320] stack scratch replaces per-accept Vec allocations, and destination hostnames no longer get logged (they may carry sensitive subdomain names).
  • Error surfacingAddrInUse and PermissionDenied produce friendly messages ("another ezTerm tab, or a different app on this port?"). Async error-status events fire a toast as well as marking the pane row red. The auto-start DB scan no longer swallows failures.
  • Persistent edit safetyforward_update and forward_delete now walk every live connection, stop runtimes backed by that persistent id, and (for update) restart with the new spec on the same tabs. The old behavior left running forwards on the old spec until the user manually stopped them.
  • Defense in depthbind_addr is parse-checked at save time; non-IP / non-localhost values are rejected. Remote forwards refuse duplicate (bind_addr, bind_port) registrations per connection.

Out of scope (for this release)

  • SOCKS5 authentication and BIND / UDP ASSOCIATE verbs.
  • ProxyJump / jump hosts, agent forwarding (-A), ControlMaster.
  • Per-forward bandwidth limits or byte / connection counters in the UI.
  • Sharing or deduping forwards across tabs of the same session.
  • A backend-only loopback-bind gate (the UI's yellow LAN warning + save-time parse-check are the v1 answer).

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.2.2

Minor: Local shells are now first-class on macOS and Linux, and Windows gets its own auto-seeded shells folder alongside the existing WSL one.

What's new

On unlock, ezTerm detects the shells available on your machine and seeds a new Local Shells folder with one session per shell — same pattern as the WSL folder, just for native shells.

  • macOS and Linux: the folder is populated from /etc/shells (filtered to ones that actually exist and aren't nologin / false). Typical contents: bash, zsh, fish, sh — whatever the host has.
  • Windows: the folder is populated with cmd and powershell (always present on Win10/11), plus pwsh when PowerShell 7+ is on PATH. The existing WSL autodetect is untouched, so you'll see both folders.

How it works

  • A new local_shells_autodetect_seed backend command runs once per unlock, with the same race-protected, idempotent behavior as the WSL flow. Re-unlocking doesn't create duplicates.
  • On macOS / Linux, local shells launch as a login shell (-l), so PATH and your profile env (Homebrew, asdf, nvm, etc.) populate as you'd expect from Terminal.app or a GNOME terminal. No more "command not found" surprises after launching a tab.
  • The session edit dialog's shell picker now reads from the backend's detection instead of a hardcoded list. On Linux/macOS you see your installed shells in the dropdown; on Windows you see cmd / powershell / pwsh-if-installed. Custom path is still there for shells outside the detected set.

Out of scope (for this release)

  • Per-session login vs. non-login toggle — login is always on for Unix local shells.
  • Auto-removing sessions for shells you've uninstalled — adds are idempotent, removes stay manual.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.1.5

Patch: terminal copy/paste now works reliably on Linux and macOS. Windows behavior unchanged.

What was wrong

Terminal copy and paste used navigator.clipboard.readText() / writeText() directly. Tauri uses a different webview engine per platform, and navigator.clipboard doesn't behave the same on all of them:

  • Windows uses WebView2 (Chromium) → worked fine
  • Linux uses WebKitGTK → restrictive clipboard permissions, no UI to grant them; readText() typically returned empty or failed silently
  • macOS uses WKWebView → behavior varied by macOS version

The fix

Switched to Tauri's official clipboard-manager plugin. It routes through the Rust backend → native OS clipboard APIs (NSPasteboard on macOS, xclip/wl-clipboard on Linux, OLE on Windows), bypassing webview restrictions entirely. Same single API surface across all three platforms.

The keyboard shortcuts and right-click menu paths are unchanged — Shift+Insert, Ctrl+Shift+C, Ctrl+Shift+V, and right-click → Copy / Paste all work the same way. They just go through a more reliable plumbing.

Out of scope (for this release)

  • Linux PRIMARY selection (middle-click paste) — Tauri's plugin uses CLIPBOARD only. If you want middle-click paste on Linux, that's a separate ticket.
  • Image clipboard.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.1.4

Patch: cell separators are visible in dark mode for tile / cascade view modes, and the focused terminal pops with an accent ring. Light mode is unchanged.

What was wrong

In dark mode, the 1px hairline that divides tile cells (and outlines cascade frames) used the same --border token as the rest of the app's chrome. That token is intentionally quiet for inputs and dividers — but in tile/cascade layouts it's load-bearing for spatial separation, and washed out against the surface colors. Cells blurred into each other.

The fix

Two CSS-only changes, zero added pixels:

  • New --border-strong design token. Brighter than --border in dark mode (so dividers actually divide); marginally darker than --border in light mode (light mode already worked, so the new token stays subtle there).
  • Active-cell accent ring. When you focus a tile cell, it gets a 1px accent ring on the inside (not a border — the ring lives inside the box, doesn't push siblings). Cascade's existing accent-border on the active frame is kept; inactive cascade frames now use the brighter token instead of the muted one, so spatial separation reads even before you focus a frame.

Net effect in dark mode:

  • Inactive cells: visible 1px brighter divider, no chrome
  • Active cell: brighter divider plus an inset accent ring — immediately obvious
  • Light mode: unchanged

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.1.3

Patch: in-app auto-update now works on Linux .deb installs and on macOS aarch64. Previously, deb users hit "Invalid updater binary format" when checking for updates, and macOS auto-update wasn't wired up at all.

What was wrong

The Tauri updater plugin asks the running binary which installer it was distributed as (deb / AppImage / msi / nsis / app), looks up latest.json's platforms["{os}-{arch}-{installer}"] entry, and downloads from there. Our manifest only had the bare {os}-{arch} entries pointing at AppImage URLs — so a .deb-installed binary downloaded an AppImage and tried to install it as a deb, which fails fast with "Invalid updater binary format".

macOS users had no entry at all because the release workflow built only a raw binary on macOS, no .app bundle.

The fix

The release workflow now:

  • Signs every .deb with the same minisign key Tauri uses for the AppImage and macOS sigs
  • Builds a proper .app bundle on macOS (alongside the existing portable tar.xz)
  • Emits per-installer entries in latest.json: linux-x86_64-appimage, linux-x86_64-deb, linux-aarch64-appimage, linux-aarch64-deb, darwin-aarch64
  • Keeps the bare linux-x86_64 / linux-aarch64 entries as a fallback for binaries with an unknown bundle type
  • Has a CI sanity-check that asserts every manifest URL resolves to an uploaded asset before tagging the release — so this class of drift can't reach users again

Impact on existing installs

Hit "Check for updates" in your existing v1.0.0+ binary:

  • Windows NSIS — same as before (already worked)
  • Linux AppImage — same as before (URL just moved from linux-x86_64 to linux-x86_64-appimage; both now point at the AppImage)
  • Linux .deb — now works for the first time; downloads *_amd64.deb, installs via dpkg. Note: on locked-down installs dpkg -i may need elevation; if the update fails with a permission error, install the .deb manually one time with sudo dpkg -i
  • macOS aarch64 — auto-update offered for the first time. The .app is ad-hoc-signed (no Apple Developer ID); first launch needs right-click → Open

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.1.2

Patch: switching window view modes no longer drops your SSH sessions, and the SFTP pane keeps its state across mode flips. Fixes a regression introduced in v1.1.0.

What was wrong

Cycling between Tabs / Tile / Cascade / Auto-arrange (the toolbar buttons or Ctrl+Shift+W) was tearing down and re-establishing every SSH connection on every mode switch. If you had top running, an interactive REPL open, an in-progress tail -f, or just a long shell history, all of it was lost the moment you tried a different layout. The new view modes were net-negative for any real work.

The fix

The renderer now keeps every terminal mounted across all view modes. Layout differences are pure CSS changes on a stable wrapper — React doesn't unmount the terminal, so its connection cleanup never fires due to a mode change. The same fix keeps the SFTP pane mounted across mode flips: switching to cascade and back returns you to the same directory listing without re-fetching.

The only thing that disconnects an SSH session now is closing its tab.

Other improvements bundled in

  • Cascade drag/resize is no longer a re-render storm. Each frame's geometry update only re-renders the dragged frame, not every cascade window. Smoother drags at higher tab counts.
  • Mode switches don't spam the backend with resize IPCs anymore. Each terminal coalesces its ResizeObserver callbacks at one per animation frame, so a switch to tile-grid with N visible tabs no longer fires N synchronous resize calls in the same tick.
  • SFTP pane doesn't issue listing requests while hidden. When you flip out of tabs mode, the pane stays alive (so its state is preserved) but stops hitting the remote with sftpList.
  • Cascade chrome is more robust to dev-mode hot-reload. A drag started right before HMR no longer gets stuck "in dragging" until you click again.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.1.0

Minor: mIRC-style window view modes for session tabs. Six layouts — Tabs, Tile Horizontal, Tile Vertical, Tile Grid, Cascade (full MDI), and Auto-arrange — plus a global cycle hotkey. No migrations, no new backend commands; this is a pure-frontend feature.

Window views

Older versions only had a single tab strip with the active terminal filling the workspace. v1.1.0 adds a six-button toolbar at the right end of the tab strip that switches between layout strategies, on the mIRC pattern:

  • Tabs view — current behaviour: only the focused terminal is visible, others stay alive in the background.
  • Tile Horizontal — every non-minimised tab stacked in rows, full width each.
  • Tile Vertical — every non-minimised tab side-by-side in columns, full height each.
  • Tile Gridrows × cols grid; pick the dimensions in a small modal. Surplus tabs flow into a scrollable extra row, so nothing is silently hidden when the grid is undersized.
  • Cascade — full MDI: each terminal is a draggable, resizable, overlapping floating frame with a title bar (status dot, name, minimise, maximise/restore, close). Click anywhere in a frame to bring it to the front. Double-click the title bar to maximise the frame within the cascade area; double-click again to restore.
  • Auto-arrangeTile Grid with cols = ceil(sqrt(N)), re-tiled automatically as tabs open and close.

Cascade specifics

  • 8 resize handles per frame (4 edges + 4 corners). Min size 200×120.
  • Drag and resize are throttled to the display refresh rate via requestAnimationFrame so neither the cascade map updates nor the xterm safeFit IPC fire faster than the screen can paint, even with many windows open.
  • Minimised frames condense to a row of chips along the bottom of the cascade area (mIRC's iconified strip). Click a chip to restore the frame; restored frames come back to the front.
  • Window-resize clamps frame positions for display only — your stored geometry isn't mutated by browser resizes; dragging will normalise it.

Hotkey

Ctrl+Shift+W cycles through all six modes: Tabs → Tile-H → Tile-V → Tile-Grid → Cascade → Auto → Tabs. The existing Ctrl+B (toggle sidebar), Ctrl+Shift+C/V/F (terminal copy/paste/find), and middle-click-to-close-tab shortcuts are unchanged.

What persists

  • View mode, tile-grid dimensions (rows × cols), and the set of minimised tabs are stored in localStorage and restored on next launch.
  • Cascade frame geometry (positions and sizes) is in-memory only — relaunching the app starts every cascade frame at the staircase default. Tabs themselves don't survive restart yet, so persisting geometry would have nothing to attach to.

What's intentionally out of scope

  • Drag-to-detach a tab into a floating window from Tabs mode.
  • Saved named layouts.
  • Snap-to-edge / snap-to-frame in cascade.
  • Persisted cascade geometry across restarts.
  • Aspect-ratio-aware auto-arrange.

Other changes

  • Tab IDs are now generated with crypto.randomUUID() instead of an 8-char Math.random() slice. Same shape, stronger collision resistance, not predictable from the renderer.
  • StatusDot (the connection-state pip used in the tab strip) is now a shared component in ui/components/status-dot.tsx. Three copies had crept in across the tab strip, the new MDI frames, and the minimised strip; they're a single import now.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.0.2

Patch: connection manager gets a MobaXterm-style visual refresh — bordered panel cards in the session dialog, and tinted tile icons in the sessions sidebar. No behaviour changes, no migrations.

Polish

Session dialog — panel cards + leading icons

Every tab in the Edit / New Session dialog (General, Terminal, Advanced) used to be a flat stack of fields separated only by tiny uppercase section headings. Each logical group is now wrapped in a bordered Section card with its title inline in the card header, so related fields read as one unit:

  • General — Type, Connection, Authentication (SSH only), Appearance
  • Terminal — On connect, Display, Environment variables
  • Advanced — Connection, Transport, X11 forwarding (SSH only)

Layering is token-driven so light and dark both look right: the dialog sits on --surface, each card drops to --bg for an inset feel, and inputs inside the card stay on --surface-2 so they pop on the panel. A thin --border/60 separator below the title keeps the header row contained inside the card.

Identity inputs carry small muted lucide glyphs inside the left padding — Folder, Server (SSH host), User (username / WSL user), SquareTerminal (WSL distro), Terminal (local shell), FolderOpen (starting dir). Port, numeric settings, and checkbox rows stay plain; icons there would be noise.

Sessions sidebar — tinted tile icons

The sidebar's flat line icons and rainbow-hashed folder colours looked toy-like next to the rest of the UI. Replaced with a quieter, more deliberate scheme:

  • Session rows now show an 18px rounded tile tinted per kind (SSH = --accent blue, WSL = --warning amber, Local = --success green), with a white lucide glyph on top. When the user has set a per-session colour, that wins over the kind default while idle; once the session connects, the tile flips to --success green — clear "live" signal that reinforces the existing rail pulse.
  • Folder rows lose the per-name rainbow palette. A single theme-accent folder glyph is used for every folder; text-accent when open, text-accent/70 when closed, so expansion state still reads at a glance without the toy vibe.
  • FOLDER_PALETTE / folderColor() helpers removed (no other references in the codebase).

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.0.1

Patch: fixes unreadable dropdowns in the Edit Connection dialog (and everywhere else a <select> appears) on Linux, in both dark and light modes. See #29.

Fix

<select> dropdowns unreadable on Linux

The Edit Connection dialog uses plain HTML <select> elements for Folder, Shell, Font family, and the Credential picker. All share the .input class in ui/app/globals.css. On Windows (WebView2) they rendered with our theme tokens and looked fine; on Linux (Tauri v2 → WebKitGTK) the closed selects had near-invisible text and the open popup list used clashing system GTK colors. Bug affected both dark and light modes.

Two gaps in the stylesheet:

  1. Neither :root (dark) nor .light declared a color-scheme. WebKitGTK therefore assumed a light UA palette for form widgets no matter which theme was active, and painted the closed <select>'s value with light-mode UA text color against our dark --surface-2 background.
  2. <option> / <optgroup> had no explicit color / background-color, so the opened popup list fell back to system (GTK) colors that don't follow our --fg / --surface tokens.

Fix:

  • Added color-scheme: dark to :root and color-scheme: light to .light. Form widgets now render against the correct UA palette automatically.
  • Pinned color: rgb(var(--fg)) on .input so the closed select's value always uses our foreground token instead of an inherited UA default.
  • Styled .input option, .input optgroup with --fg on --surface so the open dropdown list tracks the app theme on platforms that honor it.

All changes are token-driven and flip with .light, so dark and light modes both improve without platform branching. No behavior changes; Windows and macOS rendering is unchanged.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

1.0 — ezTerm leaves beta. This release closes out the final WSL-tab blocker (a two-layer race that left bash hanging with a blinking cursor on the first connect) and adds a per-session starting-directory control that applies to both WSL and SSH sessions.

Feature

Per-session starting directory

A new "Starting directory" field in the session dialog's Terminal pane lets you pick where the shell lands on connect.

  • WSL sessions now pass the value to wsl.exe --cd <value> and default to ~ when the field is empty, so you land in your Linux home instead of /mnt/c/Users/<name> (which is what wsl.exe inherits from the Windows cwd by default). Tilde paths like ~/projects or absolute Linux paths like /srv/app both work — wsl.exe expands them inside the distro.
  • SSH sessions now write cd <value>\n into the interactive shell right after shell_request succeeds; empty leaves the shell at whatever the remote default is (typically $HOME). Tilde-prefixed values pass through raw so bash expansion still works; bare paths are double-quoted and the shell metacharacters that matter inside double quotes (\, ", $, `) are backslash-escaped, so a path with spaces like /var/My App survives without the user having to quote it.
  • Local (cmd/pwsh) rows ignore the field — they continue to use the Host row as the Windows cwd to avoid disturbing existing local sessions.

A new migration 20260423130000_session_starting_dir.sql adds the column as nullable; existing sessions keep the same behavior they had before (empty starting dir).

Fix

First WSL tab of the session hangs with a blinking cursor

Opening a WSL (or local cmd/pwsh) session on a cold app showed the tab, claimed "connected", but never rendered a bash prompt — just a blinking cursor. Closing and reopening the tab sometimes worked; other times it stayed stuck. SSH sessions weren't affected.

Layer 1 — PTY reader outran the Tauri listener. The local PTY backend spawned its reader thread inside local_connect and the thread began emitting ssh:data:<id> events immediately. The frontend only subscribed to those events after the localConnect invoke resolved. ConPTY wrote the shell's first bytes into the PTY microseconds after spawn — faster than the frontend's listen() IPC round-trip — and Tauri events don't buffer for late subscribers, so those bytes were silently discarded.

Layer 2 — bash's DSR cursor-position query had no reply handler. Even once layer 1 was fixed, the symptom persisted: bash on WSL emits an early ESC [ 6 n (4-byte DSR cursor-position query) as part of prompt setup and waits synchronously for the terminal to reply with ESC [ <row> ; <col> R. xterm.js generates that reply inside its parser and fires onData — but onData was registered after the reader was unblocked, so the reply fell on the floor and bash hung forever waiting for cursor coordinates.

SSH has the same race structurally in both layers, but network RTT masks layer 1 and SSH shells don't DSR-query the terminal as aggressively as bash-on-ConPTY does, so only the local path exhibited the bug.

The fixes:

  • local::Connection now holds a reader_gate (std::sync::mpsc::Sender<()>) and the reader thread blocks on recv() as its first action. A new local_ready Tauri command fires the gate. The frontend calls api.localReady(connectionId) only after its ssh:data:<id> listener is installed, so the reader never emits into an unsubscribed channel.
  • terminal.tsx now registers bundle.terminal.onData(...) before calling api.localReady, so by the time the reader unblocks and bash's DSR query reaches xterm.js, the handler that pipes xterm's reply back to the PTY is already in place.

If the tab is closed before the frontend gets a chance to signal (cancel race), the Connection drops, the gate's Sender drops with it, the blocked reader thread's recv() returns Err and the thread exits cleanly without emitting anything — no spurious "closed" dot surfaces in the UI.

Minor release: terminal tabs are now draggable to reorder, and new SSH sessions default to a 60-second keepalive so long-idle connections don't get silently reaped by NAT / firewall idle timers.

Feature

Drag-to-reorder terminal tabs

Tabs in the top bar can now be dragged horizontally to rearrange them — click-and-hold on any tab, drag past the midpoint of another tab, and release to insert there. A thin accent bar on the left or right edge of the hover target previews the exact insertion point before you drop, and the tab being dragged dims to 50% opacity so it's obvious which one is moving. Dropping back onto the source tab is a no-op (and suppresses the drop indicator there so it doesn't imply a move that won't happen).

Drag interactions are handled entirely in the tab bar chrome — the SFTP toggle and close button are marked draggable="false" so clicking those still does what it always did. Middle-click-to-close continues to work as well.

Implementation uses the native HTML5 drag-and-drop API with a 1×1 transparent drag image; the browser's default "ghost tab" overlay would have clashed with the existing opacity + accent-bar feedback cues. The reorder state lives in the tabs-store (Zustand) so any future tab UI consumers get the new ordering for free.

Fix

Overnight SSH connections silently dropping

The per-session keepalive_secs control already existed but defaulted to 0 (disabled). With no keepalive traffic, an SSH channel sitting idle overnight would be reaped by corporate NATs / home routers — most of which drop idle TCP after 30 minutes to 2 hours — and the disconnect wasn't surfaced until the next keystroke hit a dead socket.

The new-session default in the session dialog is now 60 seconds (matching MobaXterm's "SSH keep-alive" default). russh passes that interval as Config::keepalive_interval, which sends a benign SSH "ignore" message on the otherwise idle channel every minute — enough to keep the flow entry alive on any sane middlebox.

Existing saved sessions get the fix too

A follow-up migration — migrations/20260423120000_keepalive_default_60.sql — runs on upgrade and bumps any saved session still holding the old 0 default up to 60. Users don't have to re-edit every session to stop getting disconnected; anyone who genuinely wants keepalives off can still set the field back to 0 by hand after the upgrade.

ezTerm v0.18.6

Patch: fixes a Linux-only splash-screen hang where the app would sit on the logo indefinitely instead of transitioning to the unlock / main screen.

Fix

Splash never closes on Linux / WebKitGTK

On Linux the main window is created with visible: false so users never see a blank webview while Next.js hydrates — Rust flips it visible once the frontend signals it's ready. The handshake lived in ui/app/page.tsx and invoked ui_ready inside a requestAnimationFrame callback, the idea being that rAF guarantees a paint before the splash disappears so the unlock screen is on screen the instant the transition happens.

WebKitGTK follows the HTML spec strictly: a window whose underlying GTK widget is not mapped has document.hidden === true, and rAF callbacks are suspended on hidden documents. So on Linux the sequence was:

  1. Splash renders (static GTK window, no rAF dependency — fine).
  2. Main window's WebView loads the Next.js bundle behind the scenes.
  3. useEffect fires, schedules invoke('ui_ready') via rAF.
  4. rAF never fires, because document.hidden is true.
  5. Rust never receives ui_ready, never calls main.show().
  6. Main stays hidden → document.hidden stays true → rAF stays queued. Deadlock.

Windows Edge WebView2 doesn't observe the same hidden-document gating for rAF, which is why the bug only manifested on Linux even though the code path was identical.

The fix drops the rAF wrapper and calls invoke('ui_ready') directly from useEffect. The 2 s SPLASH_MIN_VISIBLE floor already enforced in src-tauri/src/commands/splash.rs continues to be the anti-flash guard on fast hardware, and gives React plenty of time to render into the hidden main window before the transition — so dropping rAF doesn't introduce a blank flash on any platform.

No Rust, tauri.conf.json, or Windows-build changes — strictly a frontend one-liner.

ezTerm v0.18.5

Patch: fixes a startup panic introduced by a line-ending mismatch between the embedded sqlx::migrate! checksums and whatever was recorded in a user's SQLite DB, and hardens the repo against the root cause so this can't happen again.

Fix

Startup panic: Migrate(VersionMismatch(...))

v0.18.4 could fail to launch with thread 'main' panicked at init db: Migrate(VersionMismatch(…)) on any install whose _sqlx_migrations rows were populated by a build with different line endings than the current binary. Example path: install v0.14.1 (CI Windows build with CRLF-normalized migrations) → run a local cargo tauri dev once (picks up whatever line endings your editor saved) → mixed checksums land in the DB → next CI-built installer reads uniform CRLF and panics on the first mismatch.

db::init_pool now runs a self-heal pass before MIGRATOR.run(): for each applied migration whose stored checksum doesn't match the embedded one, it recomputes the embedded SQL's sha384 under both LF and CRLF and silently rewrites the stored checksum if either variant matches. Genuinely edited migrations still trip sqlx's VersionMismatch because their SQL differs semantically, not just at line-ending level.

Root cause: repo had no .gitattributes

Without an explicit attribute, git defers to core.autocrlf per environment — true on Windows (CRLF on checkout), false-ish on Linux (LF), and anything on a developer box. sqlx::migrate! hashes raw bytes at compile time, so the same repo produced different embedded checksums depending on who and where built it.

A new .gitattributes pins every text file to eol=lf both in the index and on checkout. Combined with git add --renormalize ., every tracked text file is now LF end-to-end, and every future build (CI or local, Windows or Linux) is deterministic with respect to line endings. Binary extensions (*.png, *.ico, *.icns) are explicitly flagged to belt-and-braces against text=auto misclassifying them.

Under the hood

  • src-tauri/src/db/mod.rs: new heal_line_ending_checksums helper runs before MIGRATOR.run(). Skips when _sqlx_migrations doesn't exist (fresh DB). Only rewrites rows where the stored checksum is demonstrably a line-ending variant of the currently embedded SQL.
  • .gitattributes: * text=auto eol=lf default, explicit binary for *.png, *.ico, *.icns.
  • Every tracked text file renormalized to LF in the same commit.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v0.18.4

Patch: status-bar now shows the real running version.

Fix

The status bar at the bottom-right displayed a hardcoded "ezTerm v0.18" label regardless of which build was installed — added during v0.18.0 prep and never wired up. It made "what version am I actually running?" unanswerable without digging into Add/Remove Programs, and gave no signal that an in-app upgrade had landed.

  • StatusBar now reads @tauri-apps/api/app#getVersion() on mount and renders ezTerm v<actual>. The fetched value reflects the binary's CARGO_PKG_VERSION, so it tracks each release exactly (including the bundle-version sync added in v0.18.3).
  • During the async read there's a brief window where just ezTerm renders without the version — deliberate, to avoid flashing a stale value.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v0.18.3

Patch: fixes the in-app auto-updater (broken since updater landed in v0.14.1), fixes installer filenames + MSI/NSIS embedded product version, and fixes a terminal focus regression.

Fixes

Auto-updater now actually works

tauri.conf.json was missing bundle.createUpdaterArtifacts: true, so the Tauri bundler silently skipped minisign signing even though TAURI_SIGNING_PRIVATE_KEY was in the CI environment. With no .sig files next to the installers, the release job skipped generating latest.json, and every client asking releases/latest/download/latest.json got a 404 — "Could not fetch a valid release JSON from the remote".

  • Enabled createUpdaterArtifacts — bundler now emits ezTerm_<version>_x64-setup.exe.sig, ...AppImage.sig, and friends alongside the installers.
  • Release job picks them up, writes latest.json, uploads it with the rest of the assets.

Anyone already on v0.14.1+ will need to install v0.18.3 manually once (the older clients have nothing to update from). From v0.18.3 onward the in-app updater chain is live.

Installer filenames + Windows Installer upgrades

tauri.conf.json and ui/package.json were pinned at 0.1.0 since initial scaffold and never bumped. Every release back to v0.14.0 shipped installers named ezTerm_0.1.0_x64-setup.exe / ezTerm_0.1.0_x64_en-US.msi — and, more importantly, the MSI embedded 0.1.0 as its product version, so Windows Installer saw every new release as "already installed" and declined to upgrade.

  • CI now has a Sync bundle version from tag / Cargo.toml step that runs right after checkout and rewrites both JSON files from the tag (or Cargo.toml on workflow_dispatch). Single source of truth stays Cargo.toml.
  • Installer filenames now match the tag: ezTerm_0.18.3_x64-setup.exe, ezTerm_0.18.3_x64_en-US.msi, etc.
  • MSI upgrades on top of older installs now work the standard way.

Terminal focus on tab switch

Clicking an already-connected tab and immediately typing did nothing — xterm's hidden textarea kept focus on whichever tab the user clicked away from, so keystrokes went nowhere until the user clicked back into the terminal body.

  • TerminalView's visibility effect now re-focuses the xterm instance after the fit, but only when the tab is connected and no blocking overlay (host-key prompt, auth-fix dialog, VcXsrv-missing dialog, find bar, font popover) is up. Those flows still own the keyboard.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v0.18.2

Patch: SSH client rewrite onto russh 0.60. No user-visible feature change — host keys, known_hosts rows, passwords, and private-key sessions stored under v0.18.1 keep working.

What changed

  • russh 0.45 → 0.60.1 — 15-minor-version jump spanning the native-async Handler trait rewrite, russh-keys fold-in (now russh::keys), and the new AuthResult / PrivateKeyWithHashAlg / Signer APIs.
  • russh-keys dep removed — the crate is gone; all key operations come from russh::keys::*.
  • russh-sftp 2 → 2.1.1 — bumped alongside russh for API compatibility.
  • Rust edition 2021 → 2024 (workspace) — required for russh 0.60's native-async Handler trait to match idiomatically.
  • Workspace resolver 2 → 3 — edition-2024 default.

Under the hood

  • client::Handler::check_server_key now takes &ssh_key::PublicKey (formerly &russh_keys::key::PublicKey). Host-key fingerprint bytes still come from .to_bytes() — byte-identical to the 0.45 public_key_bytes(), so existing trusted-host rows stay valid.
  • authenticate_password / authenticate_publickey now return AuthResult instead of Result<bool>; call sites use .success().
  • Private-key auth wraps the key in PrivateKeyWithHashAlg and pulls the server's preferred RSA hash via Handle::best_supported_rsa_hash — avoids the legacy ssh-rsa (SHA-1) fallback where the server advertises rsa-sha2-* via server-sig-algs.
  • SSH-agent auth now uses the new Signer-based authenticate_publickey_with. Concrete agent transports per platform: OpenSSH-for-Windows named pipe on Windows (\\.\pipe\openssh-ssh-agent), SSH_AUTH_SOCK on Unix. Pageant fallback is deferred — the type-erased Box<dyn AgentStream> path trips an HRTB Send check inside authenticate_publickey_with on russh 0.60.1.
  • ssh::connect now takes a new ConnectDeps (owned Arcs cloned from AppState) instead of &AppState. russh 0.60's native-async Handler composes inner futures through the trait solver in a way that can't prove &Pool<Sqlite>: Send under HRTB at the #[tauri::command] boundary; threading owned deps through sidesteps this. The returned future is also explicitly boxed (Pin<Box<dyn Future + Send>>) at the crate boundary for the same reason.
  • ClientHandler::server_key swapped from tokio::sync::Mutex to std::sync::Mutex — the handler only writes once per connect and never holds the lock across an await; the async mutex's future captured a &Mutex borrow that propagated HRTB Send failures through the russh event loop.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v0.18.1

First tagged release since v0.15. Rolls up three rounds of work:

  • Splash screen (v0.18) — native logo splash on startup
  • Per-session fonts + Ctrl+wheel polish (v0.17)
  • VcXsrv bundling, in-app VcXsrv installer, intra-folder DnD reordering, cargo majors, Node 24 CI (v0.16 backlog)

What's new

Splash screen

A borderless, centered 500 × 500 window with the ezTerm logo appears the instant the app launches — no blank webview while Next.js hydrates. A new ui_ready Tauri command closes the splash and shows the main window once React has painted, with a 2 s visible floor so the logo registers on fast hardware.

Per-session terminal fonts

  • Session dialog → Terminal tab gains a Font family picker with OS-aware presets:
    • Windows: Cascadia Mono / Code, Consolas, Courier New, Lucida Console, Fira Code, JetBrains Mono, MS Gothic
    • macOS: SF Mono, Menlo, Monaco, Andale Mono, Courier New, Fira Code, JetBrains Mono
    • Linux: DejaVu Sans Mono, Liberation Mono, Ubuntu Mono, Noto Sans Mono, Source Code Pro, Courier 10 Pitch, Fira Code, JetBrains Mono
    • "(default)" = a cross-OS stack (ui-monospace → native system mono per platform, with Cascadia / Menlo / DejaVu as fallbacks).
    • Custom… switches to a free-text input for any CSS stack.
  • Font size field still present (8 – 48 pt).
  • Right-click the terminal → Font… opens a compact popover: family dropdown, −/+ size stepper + numeric input, "Save as session default" checkbox.

Ctrl + mouse wheel zoom, without the scroll conflict

Previously the terminal viewport scrolled a line or two per wheel notch before the font update landed, because xterm's internal handler ran first. Now the wheel listener registers in the capture phase and, when Ctrl/Cmd is held, fully swallows the event (preventDefault + stopImmediatePropagation) — no scroll, no browser zoom, just the font change. Plain scrolling (no Ctrl) is untouched. Deltas accumulate against a 40 px threshold so trackpads don't overshoot.

VcXsrv bundled in MSI + NSIS installers

MSI and NSIS now ship VcXsrv at <install>\resources\vcxsrv\ via a Tauri resource declaration in the new tauri.windows.conf.json. CI stages the tree from Chocolatey's cache into src-tauri/vcxsrv/ before cargo tauri build runs. The portable .tar.xz mirrors the same layout next to ezterm.exe.

In-app "Install VcXsrv" flow + missing-VcXsrv dialog

When a session has X11 forwarding on but no VcXsrv is present, the connect failure now surfaces a dedicated dialog with three actions:

  • Install VcXsrv — downloads 1.20.14.0 via reqwest and silent-installs to %APPDATA%\zerosandones\ezterm\data\vcxsrv\. No admin required. On success the connect retries automatically.
  • Continue without X11 — reconnects with a new disable_x11 one-shot override plumbed through ssh_connect. The session's forward_x11 flag is left alone, so next connect asks for X11 again.
  • Cancel — closes the tab.

The Rust side returns a dedicated XServerMissing error code ("xserver_missing"), so the frontend routes it cleanly without string-sniffing error messages.

Intra-folder drag-and-drop reordering

The sessions sidebar finally supports reordering siblings by drag:

  • Session rows split 50 / 50 — top half drops above, bottom half below. A 2 px accent line renders at the edge that'll receive the drop.
  • Folder rows split 25 / 50 / 25 — top / bottom bands reorder the folder among its siblings; middle 50 % keeps the existing "drop INTO this folder" behaviour.
  • Cross-folder drops work in one gesture: dropping on a sibling in a different folder both moves and positions it.
  • New backend commands session_reorder / folder_reorder renumber sort = index * 10 atomically in one transaction.

Uninstaller cleanup for per-user VcXsrv

  • New NSIS PREUNINSTALL hook (src-tauri/installer-hooks.nsh) and a WiX custom action fragment (src-tauri/wix-remove-user-vcxsrv.wxs, using the WiX v3 schema Tauri v2 expects) remove the per-user VcXsrv dir on uninstall.
  • Bundled <install>\resources\vcxsrv is cleaned by the standard uninstaller automatically.
  • User data (sessions DB, vault, settings, known_hosts) is deliberately left alone — explicit user action, not a silent uninstall side effect.

Cargo patch-level majors

  • thiserror 1 → 2
  • rand 0.8 → 0.10 (OsRng / RngCoreSysRng / TryRng::try_fill_bytes)
  • directories 5 → 6
  • sha2 0.10 → 0.11
  • portable-pty 0.8 → 0.9

GitHub Actions bumps

  • actions/checkout / setup-node / upload-artifact / download-artifact → v5
  • Node 20 → 24

Fixes

  • WSL autodetect filters out internal distros (docker-desktop, rancher-desktop, podman-machine-default, …) so they don't show up as shell tabs.
  • MSI uninstall fragment now parses under WiX v3 — local cargo tauri build produces both MSI and NSIS installers cleanly.

Under the hood

New files:

  • ui/lib/fonts.ts (OS detection + per-OS presets)
  • ui/components/font-picker-popover.tsx
  • ui/components/xserver-missing-dialog.tsx
  • ui/public/splash.html, ui/public/ezterm.png
  • src-tauri/src/commands/splash.rs
  • src-tauri/tauri.windows.conf.json
  • src-tauri/installer-hooks.nsh
  • src-tauri/wix-remove-user-vcxsrv.wxs
  • migrations/20260422120000_session_font_family.sql

New dependency: reqwest (already transitive via tauri-plugin-updater) pinned with default-features = false, features = ["rustls-tls"] — no OpenSSL system dep on Windows.

Session / SessionInput grow font_family: String threaded through create / update / duplicate, MobaXterm import, and backup restore. error.rs grows a dedicated XServerMissing variant with code "xserver_missing".

Download

Same matrix as v0.15: portable .tar.xz per platform, plus MSI + NSIS (Windows) and AppImage + deb (Linux) installers with minisign signatures. Auto-update picks this up for anyone on v0.14+ via MSI / AppImage.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

What's next (v0.19+)

  • macOS code signing + notarisation so the auto-updater works there too.
  • SFTP drag-out (#28).
  • russh 0.45 → 0.60 SSH client rewrite.

ezTerm v0.17.0

Per-session terminal font customisation plus a Ctrl+wheel zoom that finally stops fighting xterm's scroll. Rolls up the v0.16.0 backlog that wasn't yet tagged — VcXsrv bundled into MSI/NSIS, in-app VcXsrv installer, intra-folder drag-and-drop reordering, cargo majors sweep, CI on Node 24, and uninstaller cleanup for per-user VcXsrv.

What's new in v0.17

Per-session font family + size

  • Session dialog → Terminal tab gains a Font family field with OS-aware presets:
    • Windows: Cascadia Mono, Cascadia Code, Consolas, Courier New, Lucida Console, Fira Code, JetBrains Mono, MS Gothic.
    • macOS: SF Mono, Menlo, Monaco, Andale Mono, Courier New, Fira Code, JetBrains Mono.
    • Linux: DejaVu Sans Mono, Liberation Mono, Ubuntu Mono, Noto Sans Mono, Source Code Pro, Courier 10 Pitch, Fira Code, JetBrains Mono.
    • "(default)" = the cross-OS stack ui-monospace, "Cascadia Mono", Menlo, "DejaVu Sans Mono", Consolas, monospace which resolves to the native system mono on each platform.
    • Custom… switches the row to a free-text input so you can type any CSS font stack (e.g. "Iosevka", monospace).
  • Font size remains an integer input, 8–48pt.
  • New DB column font_family TEXT NOT NULL DEFAULT '' (migration 20260422120000_session_font_family.sql). Empty = "use app default". Existing rows keep today's look without a data migration.

Right-click → Font… quick picker

  • Terminal context menu gains a Font… entry (shortcut hint: Ctrl+Wheel) that opens a compact popover anchored top-right of the terminal.
  • Family dropdown (same OS-aware presets; shows (custom) for values set via the session dialog's free-text input or fonts from a different OS).
  • Size: −/+ buttons + a numeric field, live-updating the terminal on every change.
  • Save as session default checkbox + a Save button to persist the current font + size back to the session row. Un-saved changes stay ephemeral for this tab only, so accidental tweaks don't rewrite your saved session.

Ctrl + mouse wheel zoom, without the scroll conflict

Previously Ctrl+wheel changed the font but xterm's internal viewport wheel handler still ran, so the buffer scrolled by a line or two each notch before the font update landed — jarring on logs.

  • Wheel listener now registers in the capture phase with { capture: true, passive: false } on the terminal container, so we see the event before xterm's canvas does.
  • When Ctrl (or Cmd on macOS) is held: preventDefault() + stopImmediatePropagation() fully swallow the event — the webview doesn't zoom, and xterm doesn't scroll.
  • Deltas accumulate against a 40 px threshold, so a mouse wheel (~100 px per detent) steps once per notch and a trackpad stops overshooting.
  • Plain scrolling (no Ctrl) is completely untouched — xterm's viewport handles it as before.

Rolled-up v0.16 backlog

Since v0.15 no tag has shipped; the following all lands in v0.17:

  • VcXsrv bundled in MSI + NSIS installers. CI stages VcXsrv from Chocolatey's cache into src-tauri/vcxsrv/ before cargo tauri build. tauri.windows.conf.json adds bundle.resources: ["vcxsrv/**/*"] so it ships at <install>\resources\vcxsrv\. The portable .tar.xz still mirrors the same tree next to the binary.
  • In-app "Install VcXsrv" flow. When a session has X11 forwarding on but no VcXsrv is present, the connect failure now surfaces a dedicated dialog with Install / Continue-without-X11 / Cancel. Install downloads VcXsrv 1.20.14.0 via reqwest and silent-installs it to %APPDATA%\zerosandones\ezterm\data\vcxsrv\ — no admin required. Continue-without-X11 reconnects with a new disable_x11 one-shot override plumbed through ssh_connect; the session's forward_x11 flag is left alone.
  • Intra-folder drag-and-drop reordering. Dragging a session row over a sibling splits 50/50 — top half places above, bottom half below — with a 2 px accent line indicator. Folder rows split 25/50/25 so you can reorder siblings or drop INTO from the same row. New backend commands session_reorder / folder_reorder renumber sort atomically (sort = index * 10, leaving gap inserts for later if needed).
  • Cargo majors. thiserror 1→2, rand 0.8→0.10 (OsRng/RngCoreSysRng/TryRng::try_fill_bytes), directories 5→6, sha2 0.10→0.11, portable-pty 0.8→0.9.
  • GitHub Actions bumps. checkout/setup-node/upload-artifact/ download-artifact → v5; Node 20 → 24.
  • Uninstaller cleanup. New NSIS hook (src-tauri/installer-hooks.nsh) plus a WiX custom action (src-tauri/wix-remove-user-vcxsrv.wxs) remove the per-user VcXsrv dir on uninstall. User data (sessions DB, vault, settings, known_hosts) is deliberately left alone.

Under the hood

  • New: ui/lib/fonts.ts (OS detection + per-OS font presets), ui/components/font-picker-popover.tsx, ui/components/xserver-missing-dialog.tsx, src-tauri/tauri.windows.conf.json, src-tauri/installer-hooks.nsh, src-tauri/wix-remove-user-vcxsrv.wxs, src-tauri/vcxsrv/.gitkeep, migrations/20260422120000_session_font_family.sql.
  • Session / SessionInput grow font_family: String; threaded through create / update / duplicate + MobaXterm import + backup restore.
  • lib/xterm.ts exports DEFAULT_FONT_STACK (cross-OS stack) and resolveFontFamily so a bare "Fira Code" gets quoted correctly.
  • error.rs grows XServerMissing (code "xserver_missing").
  • reqwest added as a direct dep (already transitive via tauri-plugin-updater) for the in-app installer download, default-features = false, features = ["rustls-tls"] — no OpenSSL system dep on Windows.

Download

Same matrix as v0.15: portable .tar.xz per platform, plus MSI + NSIS (Windows) and AppImage + deb (Linux) installers with minisign signatures. Auto-update picks this up for anyone on v0.14+ via MSI / AppImage.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

What's next (v0.18+)

  • macOS code signing + notarisation so the auto-updater works there too.
  • SFTP drag-out (#28).
  • russh 0.45 → 0.60 SSH client rewrite.

ezTerm v0.16.0

The "finish the v0.1 polish backlog" release. MSI and NSIS installers now ship VcXsrv inside the bundle, the sidebar finally supports intra-folder drag reordering, dev dependencies take a patch-level majors pass, and CI moves to Node 24 / v5 actions.

What's new in v0.16

VcXsrv ships inside the Windows installer

MSI and NSIS bundles now include the full VcXsrv tree as a Tauri resource. Previously only the portable .tar.xz build shipped VcXsrv next to ezterm.exe; the MSI/NSIS installer routes left users to install VcXsrv separately.

  • CI stages VcXsrv (from Chocolatey's cache, not SourceForge) into src-tauri/vcxsrv/ before cargo tauri build runs. Tauri picks it up via tauri.windows.conf.jsonbundle.resources: ["vcxsrv/**/*"] and ships it at <install>\resources\vcxsrv\.
  • Runtime discovery: xserver::detect_install_path checks, in order:
    1. vcxsrv/ next to the ezTerm binary (portable tar.xz layout)
    2. resources/vcxsrv/ next to the ezTerm binary (MSI / NSIS layout)
    3. Per-user install at %APPDATA%\zerosandones\ezterm\data\vcxsrv\ (populated by the in-app installer — see below)
    4. C:\Program Files\VcXsrv\ and the x86 variant (system install, handy for dev builds)
  • Uninstall cleans up properly. MSI and NSIS each remove the bundled tree automatically; a new NSIS installer hook (src-tauri/installer-hooks.nsh) plus a WiX fragment (src-tauri/wix-remove-user-vcxsrv.wxs) additionally remove the per-user VcXsrv dir on uninstall. User data (sessions DB, vault, settings, known_hosts) is deliberately left alone — an explicit "remove user data" pass can land later if users ask.

In-app VcXsrv installer + missing-VcXsrv dialog

Connecting a session that has X11 forwarding on, without VcXsrv present anywhere, used to surface as "Connection failed — VcXsrv is not installed" in the generic error card. Now there's a dedicated dialog (XServerMissingDialog) with three actions:

  • Install VcXsrv — downloads the 1.20.14.0 NSIS installer (follows SourceForge's mirror redirects via reqwest) and runs it silently with /S /D=<user_dir>. No admin required — it lands under %APPDATA%\zerosandones\ezterm\data\vcxsrv\. On success the dialog auto-retries the connect.
  • Continue without X11 — reconnects with a new disable_x11 one-shot override plumbed through ssh_connect. The session row's forward_x11 flag is untouched, so the next connect asks for X11 again — this is a "right-now" escape hatch, not a permanent setting change.
  • Cancel — closes the tab.

Surface: the Rust side now returns a dedicated XServerMissing error code ("xserver_missing") instead of the old Validation("VcXsrv is not installed…") string, so the frontend can route it cleanly without message sniffing.

Intra-folder drag-and-drop reordering

The sessions sidebar finally supports reordering siblings by drag.

  • Hovering over a session row splits the row 50/50: top half drops the dragged item above, bottom half below. A 2 px accent line renders at the edge that will receive the drop.
  • Hovering over a folder row splits 25% / 50% / 25%: top / bottom bands reorder the folder among its siblings; the middle 50% keeps the existing "drop INTO this folder" behaviour.
  • Cross-folder drops work in one gesture: dropping a session on a sibling in a different folder both moves and positions it in the destination folder's order.
  • New backend commands session_reorder / folder_reorder take the full sibling-order ID list and renumber sort in a single transaction (sort = index * 10, leaving gap-insert room if we ever need it).

Cargo patch-level majors

One sweep through the doable majors. No API redesigns — the rand 0.10 rename (rngs::OsRngrngs::SysRng, RngCore::fill_bytesTryRng::try_fill_bytes returning a Result) was the only non-trivial API delta; the rest (thiserror 1→2, directories 5→6, sha2 0.10→0.11, portable-pty 0.8→0.9) are drop-in.

  • thiserror 1 → 2
  • rand 0.8 → 0.10 (with the OsRng/RngCoreSysRng/TryRng migration in backup, vault::aead, vault::mod, xserver)
  • directories 5 → 6
  • sha2 0.10 → 0.11
  • portable-pty 0.8 → 0.9

GitHub Actions: Node 24, v5 actions

release.yml moves to the 2025-Q4 stable line:

  • actions/checkout@v4 → v5
  • actions/setup-node@v4 → v5 (node 20 → 24)
  • actions/upload-artifact@v4 → v5
  • actions/download-artifact@v4 → v5

Under the hood

  • New files: src-tauri/tauri.windows.conf.json, src-tauri/installer-hooks.nsh, src-tauri/wix-remove-user-vcxsrv.wxs, ui/components/xserver-missing-dialog.tsx.
  • reqwest added as a direct dep (already transitive via tauri-plugin-updater) for the in-app installer download. Feature set: default-features = false, features = ["rustls-tls"] — no OpenSSL system dep on Windows.
  • ssh_connect command accepts an optional disable_x11 boolean; ConnectRequest adds the matching field.
  • error.rs grows a dedicated XServerMissing variant with code "xserver_missing".
  • Uninstaller hook path: on MSI the custom action runs Before RemoveFiles with REMOVE~="ALL" so it only fires during a real uninstall, not a repair/upgrade. NSIS hooks into NSIS_HOOK_PREUNINSTALL.

Download

Same matrix as v0.15: portable .tar.xz for every platform, plus MSI

  • NSIS (Windows) and AppImage + deb (Linux) installers with minisign signatures. Auto-update picks this up for anyone on v0.14+ via MSI / AppImage.

Verify

GitHub's inline hash copy per asset, or:

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

What's next (v0.17+)

  • macOS code signing + notarisation so the auto-updater works there too.
  • SFTP drag-out (#28).
  • russh 0.45 → 0.60 SSH client rewrite.

ezTerm v0.15.0

Cloud sync phase 2 — native S3-compatible push/pull lands. Plus two bug fixes that shipped on main since v0.14.1: sidebar drag-and-drop restored under React 19, and the Updater ACL properly whitelisted so the ✨ check-for-updates button actually reaches the plugin.

What's new in v0.15

S3-compatible cloud sync

Cloud sync now has two backends; the Cloud sync… dialog gained a Target picker at the top:

  • Local folder — unchanged from v0.13.
  • S3-compatible — direct PUT/HEAD/GET against any S3 API:
    • AWS S3, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces, MinIO, SeaweedFS — anything that speaks the S3 wire protocol.
    • Path-style addressing by default (works with R2 / MinIO out of the box; AWS accepts it too).
    • Bucket + optional key prefix; ezterm-sync.json is the single object per bucket.

Config fields (Cloud sync dialog → S3-compatible tab):

  • Endpoint URL (e.g. https://s3.us-east-005.backblazeb2.com)
  • Region (auto works for R2, most providers default to their zone)
  • Bucket + optional prefix
  • Access key ID + secret access key (secret is vault-encrypted at rest)
  • Sync passphrase (wraps the backup payload, distinct from master password)

Behaviour:

  • On save, the dialog HEADs the sync object to verify credentials + endpoint before writing any config — a bad secret fails here, not later when the auto-pusher fires.
  • ETag-based conflict detection on every push: before PUT, HEAD the remote and compare to the ETag we last observed. If the remote drifted (another device pushed), the writer refuses to overwrite and surfaces the error in the status strip. User pulls to reconcile, then tries again.
  • Pull from cloud button downloads the remote object into a temp file and opens the standard Restore dialog with that path — same preview + checkbox selection + rename-on-collision flow as a manual backup file.
  • Secrets never leave the device in plaintext: the backup is wrapped under your passphrase before upload, and the passphrase + S3 secret live encrypted under the vault key in app_settings.

Fixes since v0.14.1

Sidebar drag-and-drop (regression under React 19): React 19 batches setState more aggressively than 18 did, so the drag state set in dragstart hadn't propagated by the time dragover fired — the handler gated preventDefault() on that stale null, the browser concluded the row wasn't a drop zone, and drop never dispatched. Now tracks the drag source in a useRef for synchronous access and unconditionally preventDefaults in dragover; payload-level validity still runs at drop time. Dragging sessions and folders works again.

Updater ACL (Command plugin:updater|check not allowed by ACL): capabilities/default.json only granted core:default + dialog:default, so the ✨ check-for-updates click + subsequent relaunch both hit the ACL gate. Added updater:default and process:default. The live v0.14.1 build you have installed still blocks — tagging v0.15.0 ships the fix.

Under the hood

  • New rust-s3 crate (MIT) with tokio-rustls-tls — no OpenSSL system dep on Windows.
  • New src-tauri/src/sync/s3.rs module: narrow S3Driver with put / head / get, ETag extraction that handles both single-quoted and case-variant ETag headers.
  • SyncTarget::S3 { … } variant + config commands (sync_configure_s3, sync_pull_to_temp) parallel the local-folder ones.
  • Shared build_bundle() helper so the two writer paths don't duplicate the bundle-assembly logic.
  • chrono::Utc::now().timestamp_millis used for the temp-file name so parallel pulls don't collide.

Download

Same matrix as v0.14: portable .tar.xz for every platform, plus MSI + NSIS (Windows) and AppImage + deb (Linux) installers with minisign signatures. Auto-update will pick this up for anyone on v0.14 via MSI / AppImage.

Verify

GitHub's inline hash copy per asset, or:

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

What's next (v0.16+)

  • macOS code signing + notarisation so the auto-updater works there too.
  • SFTP drag-out (#28).
  • Rust cargo majors (russh 0.45 → 0.60 SSH client rewrite is the biggest ticket remaining).

ezTerm v0.14.1

Framework refresh. No user-facing behaviour changes vs v0.14.0 — just the underlying UI stack moved to current stable releases so the app builds faster, ships smaller, and stays on supported versions.

What changed

Dependency bumps (UI)

  • Next.js 14.2 → 16.2.4 — Turbopack is now the default compiler.
  • React 18.3 → 19.2.5 — automatic runtime, concurrent features, new hooks available.
  • Tailwind CSS 3.4 → 4.2.4 — faster engine, JIT everywhere, auto content discovery.
  • TypeScript 5.4 → 5.x latest — new inference, stricter defaults surfaced a few warnings that were fixed properly.
  • ESLint 8 → 9 with flat config; next lint is gone in Next 16 so npm run lint now runs eslint directly.
  • @xterm/xterm 5 → 6 + matching addons (addon-fit, addon-search, addon-web-links).
  • Zustand 4 → 5, @types/node 20 → 25, @types/react 18 → 19.

Code touch-ups

  • Sidebar width now hydrates via lazy useState initialiser instead of an effect-then-setState, which satisfies React 19's new react-hooks/set-state-in-effect rule properly.
  • globals.css swaps Tailwind v3's three @tailwind directives for a single @import "tailwindcss" + @config pointer to the existing TS theme file.
  • tsconfig.json drops the TS 6-deprecated baseUrl; @/* path resolution still works.

Still on original versions (deferred)

The Rust stack's cargo majors (russh 0.45→0.60, thiserror 1→2, rand 0.8→0.10, directories 5→6, sha2 0.10→0.11, portable-pty 0.8→0.9) are their own focused migrations — especially russh, which needs a handler-trait rewrite — and didn't block this UI refresh.

Download

Same matrix as v0.14.0: portable .tar.xz for every platform plus MSI / NSIS (Windows) and AppImage / deb (Linux). The updater will pick this up automatically on anyone who installed v0.14.0 via MSI or AppImage.

Verify

SHA-256 sums on every archive are available via GitHub's inline hash display or the SHA256SUMS asset:

sha256sum -c SHA256SUMS    # Linux / macOS / Git Bash / WSL

Licence

GPL v3 (version 3 only). See LICENSE.

What's next (v0.15)

  • Cloud sync phase 2 — S3-compatible push/pull with ETag conflict detection.
  • Rust cargo majors (especially russh 0.60).
  • SFTP drag-out (#28).

ezTerm v0.14.0

Auto-updates land. ezTerm now checks GitHub Releases for new versions, verifies signed installers against a baked-in minisign public key, and can update itself without the user re-downloading the tarball. MSI, NSIS, AppImage and deb installers join the existing portable tar.xz archives so you can pick "install + auto-update" or "extract + run" to taste.

What's new in v0.14

In-app updates

  • Status bar → ✨ icon → opens the Updates dialog.
  • Monthly auto-check on vault unlock — if a newer version is available, the dialog pops automatically with release notes and an Install & restart button. You're always prompted; nothing installs silently.
  • Downloaded bytes are verified against the updater's public key before install — tampered downloads are rejected.
  • Cadence is enforced via localStorage; the manual check button in the status bar always works regardless.

New distribution formats

Release tarballs stay (portable, no install), and these are alongside them now:

Platform Installer Notes
Windows x86_64 ezterm_0.14.0_x64-setup.exe (NSIS) + .msi Auto-update-capable; bundles VcXsrv.
Linux x86_64 / aarch64 ezterm_0.14.0_amd64.AppImage, ezterm_0.14.0_amd64.deb AppImage is auto-update-capable; deb is one-off.
macOS aarch64 tar.xz portable only Code signing + notarisation are a later pass.

All installers (and the tarballs) are hashed into SHA256SUMS, and the installers additionally carry minisign .sig files next to them for the updater's signature check.

Download

Download from the release page. For auto-update support, grab the installer for your platform; for portable use, grab the .tar.xz.

Verify

GitHub shows each asset's SHA-256 inline; sha256sum -c SHA256SUMS covers the whole release in one command.

Full feature set (carried over)

  • SSH + SFTP + xterm.js with truecolor + per-session config
  • WSL tabs + local shells (cmd/pwsh/powershell)
  • X11 forwarding via bundled VcXsrv (Windows)
  • MobaXterm import (sessions + key files → vault)
  • Encrypted backup / restore with reauth gate
  • Cloud sync (local folder, phase 1)
  • Inline auth-fix overlay
  • Drag-and-drop session/folder management with resizable sidebar

Platform notes

  • Linux runtime still needs webkit2gtk-4.1 + libssl.
  • macOS still ships portable only; Gatekeeper prompts on first launch.
  • Updater on older versions — anyone on v0.13 or earlier needs a one-off manual upgrade to v0.14+ to pick up the public key and auto-update support.

Licence

GPL v3 (version 3 only). See LICENSE.

What's next (v0.15)

  • Phase 2 of cloud sync — direct S3-compatible push/pull with ETag conflict detection (AWS S3, Cloudflare R2, Backblaze B2, MinIO).
  • macOS code signing + notarisation + auto-update support.
  • Drag-out from the SFTP pane (#28).

ezTerm v0.13.0

Phase 1 of cloud sync — ezTerm can now keep an encrypted copy of your vault in a folder synced by your cloud provider of choice, so your sessions + credentials follow you across devices without any ezTerm-specific infrastructure.

What's new in v0.13

Cloud sync to a local folder

  • Sidebar root-click menu → Cloud sync….
  • Point at any folder on disk — typically one already synced by Dropbox, OneDrive, iCloud Drive, or Google Drive for Desktop.
  • Pick a sync passphrase (≥ 8 chars, distinct from your master password). ezTerm wraps the backup under that passphrase via Argon2id + ChaCha20-Poly1305 — the cloud provider only ever sees the encrypted file.
  • After every mutation (create/update/delete on sessions, folders, credentials; MobaXterm import; backup restore) ezTerm debounces ~3 s then atomically writes ezterm-sync.json to the configured folder.
  • Status pill in the dialog shows last-success timestamp and any error from the background writer.
  • Push now button for manual flushes; Disable button to turn sync off and clear the stored passphrase.

Who this is for

  • Single user, multiple devices — extract ezTerm on machine #2, point its sync dialog at the same folder + passphrase, done. The cloud provider's sync handles transport; ezTerm handles the crypto.
  • Backup safety net — even without multi-device sync, writing to an always-on cloud folder means a disk failure doesn't eat your vault.

What's not in this release

  • Native S3-compatible direct push (AWS S3, Cloudflare R2, Backblaze B2, MinIO, etc.) — that's Phase 2, coming next.
  • Conflict resolution — last-write-wins for now. If you edit on two devices simultaneously, the later write overwrites the earlier one (your cloud provider's file-history may save you). Per-record merge will come with the S3 driver.

Download

Platform Archive
Windows x86_64 ezterm-windows-x86_64.tar.xz (≈ 20 MB with VcXsrv)
Linux x86_64 ezterm-linux-x86_64.tar.xz
Linux aarch64 ezterm-linux-aarch64.tar.xz
macOS aarch64 ezterm-macos-aarch64.tar.xz

Extract, run ezterm / ezterm.exe. Verify with sha256sum -c SHA256SUMS or GitHub's inline hash copy button on each asset.

Full feature set (carried over)

SSH + Terminal

  • russh-backed auth (password / private key / SSH-agent), host-key TOFU, xterm.js truecolor, Ctrl+wheel zoom, per-session font/scrollback/env/keepalive.

Session manager

  • Folders + drag-and-drop, resizable sidebar, coloured tree, MobaXterm import (reads referenced private keys off disk into the vault), inline auth-fix overlay.

WSL + Local shells

  • WSL tabs (wsl.exe -d <distro>), local shells (cmd / pwsh / powershell / any absolute path), WSL autodetect on unlock.

SFTP

  • Left-docked SFTP pane, breadcrumb nav, drag-drop upload, 32 KiB streaming with progress.

X11 forwarding (Windows)

  • Bundled VcXsrv in the Windows tarball — tick "Forward X11" on an SSH session and run xeyes / gedit / JetBrains tools with zero install.

Encrypted backup / restore

  • Master-password reauth gate → passphrase-wrapped archive → selective import with ID remapping and rename-on-collision.

Security

  • Argon2id → ChaCha20-Poly1305 for every vault secret; verifier-based master-password check; exponential unlock back-off; zeroise-on-drop.
  • SECURITY.md for reporting policy.

Platform notes

  • Linux runtime needs webkit2gtk-4.1 and libssl.
  • macOS binary isn't a .app bundle — Gatekeeper prompts on first launch.
  • Non-Windows skips Windows-specific features (WSL, cmd, VcXsrv) gracefully.

Licence

GPL v3 (version 3 only). See LICENSE.

What's next (v0.14)

  • Cloud sync phase 2 — native S3-compatible push/pull (AWS S3, Cloudflare R2, Backblaze B2, MinIO), ETag-based conflict detection, pull-on-startup.
  • Code signing pipeline (Authenticode, Apple notarization).
  • Optional installer formats (MSI / NSIS) alongside the portable tarball.

ezTerm v0.12.0

Second tagged release. Two headline additions on top of the v0.11 feature set: encrypted backup / restore and a bundled VcXsrv in the Windows tarball so X11 forwarding works with no separate install.

What's new in v0.12

Encrypted backup / restore

  • Backup — sidebar root-click menu → Backup…. Two-step flow: re-enter your master password (defeats the "walk up to an unlocked laptop" exfiltration path), pick a separate backup passphrase, choose a save location. The archive is a passphrase-wrapped JSON bundle; credentials never touch disk in plaintext.
  • Restore — sidebar root-click menu → Restore…. File picker → passphrase → preview tree with per-folder / per-session / per-credential / per-known-host checkboxes and section-level All/None toggles. Selected items are re-encrypted under the target vault's key and inserted with ID remapping; name collisions get (2), (3), … suffixes and a post-import renamed list.
  • Works across vaults: export from one install, import into a freshly-initialised one with the import passphrase (not the source master password).

Bundled VcXsrv on Windows

  • The Windows release tarball now ships VcXsrv in a vcxsrv/ subfolder alongside ezterm.exe. Tick Forward X11 on an SSH session, run xeyes / gedit / JetBrains tools — no separate VcXsrv install needed.
  • ezTerm checks for the bundled copy first and falls back to %ProgramFiles%\VcXsrv\ for system installs and dev builds.
  • VcXsrv (GPLv2) stays in its own directory alongside ezTerm (GPLv3) as a mere aggregation — both licences permit this.

Download

Platform Archive
Windows x86_64 ezterm-windows-x86_64.tar.xz (≈ 20 MB with VcXsrv)
Linux x86_64 ezterm-linux-x86_64.tar.xz
Linux aarch64 ezterm-linux-aarch64.tar.xz
macOS aarch64 ezterm-macos-aarch64.tar.xz

No installer. Extract, run ezterm / ezterm.exe.

Verify the download

Each archive's SHA-256 is shown inline on this release page (hover any asset → copy button). For bulk verification download SHA256SUMS and run:

sha256sum -c SHA256SUMS       # Linux / macOS / Git Bash / WSL

On Windows PowerShell compare each archive against the matching SHA256SUMS line:

Get-FileHash ezterm-windows-x86_64.tar.xz -Algorithm SHA256

Full feature set (carried over from v0.11)

SSH + Terminal

  • russh-backed connections (password / private key / SSH-agent)
  • Passphrase-protected keys via a separate key_passphrase vault credential
  • Host-key TOFU, hard-fail on mismatch
  • xterm.js — 256-color + 24-bit truecolor, Ctrl+Shift+C/V, Shift+Insert, Find (Ctrl+Shift+F) with case-sensitive + regex
  • Ctrl + mouse wheel zoom (8–48pt, SSH channel resized every step)
  • Per-session font, scrollback, cursor, env vars, keepalive, connect timeout, initial command, compression

Session manager

  • Folders + drag-and-drop
  • Resizable sidebar (180–520px, persisted, double-click to reset)
  • Colourful tree — folder palette, connection-status rails, session-count badges
  • MobaXterm import — parses .mxtsessions / MobaXterm.ini, reads referenced private keys off disk and encrypts them into the vault
  • Inline auth-fix overlay — fix bad auth in-tab, no "close and retry" ritual

WSL + Local Shells

  • WSL tabs (wsl.exe -d <distro> [-u <user>]) — code . and other interop just works
  • Local shells — cmd, pwsh, powershell, or any absolute path, with optional starting dir
  • WSL autodetect seeds a WSL folder with one session per installed distro

SFTP

  • Per-tab SFTP pane with breadcrumb, context menu, drag-drop upload
  • 32 KiB streaming, live progress

Security

  • Argon2id → ChaCha20-Poly1305 for every vault secret
  • Verifier-based master-password check; exponential unlock back-off; zeroise-on-drop
  • Reporting policy in SECURITY.md

Platform notes

  • Linux runtime needs webkit2gtk-4.1 and libssl.
  • macOS binary isn't a .app bundle — Gatekeeper prompts on first launch.
  • Non-Windows: wsl.exe / cmd / VcXsrv features are Windows-only at runtime; SSH / SFTP / terminal / backup-restore all work.

Licence

GPL v3 (version 3 only). See LICENSE.

What's next (v0.13)

  • Code signing pipeline (Authenticode on Windows, Apple notarization on macOS)
  • Dependency major bumps (Next 16, React 19, Tailwind 4, TS 6, russh 0.55)
  • Installer options (MSI / NSIS) in addition to the portable tarball