ezTerm

Changelog

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

ezTerm v1.11.1

Port forwarding is now a first-class, reliable feature — create, edit, and manage Local (-L), Remote (-R), and Dynamic / SOCKS5 (-D) tunnels per session, with clear live state and no surprising errors. This release folds in the forward-persistence groundwork from the never-published v1.9.0 draft, adds a full reliability pass, and refreshes dependencies to clear outstanding security advisories.

Port forwarding

  • Editable everywhere — edit a saved forward from the session editor or the per-tab Forwards pane, and right-click any forward row for an Edit / Start-Stop / Delete menu. (Previously the pane's Edit button silently saved-and-closed the session dialog, and saved forwards couldn't be edited from the pane at all.)
  • Reliable auto-start — forwards set to auto-start on connect now show as running immediately instead of appearing idle, so you no longer hit "port already bound" clicking Start on a tunnel that's already up.
  • Safe edit-in-place — editing a running forward waits for the old listener to release its bind before re-binding, so same-port edits no longer race into "address in use."
  • Graceful conflicts across tabs & windows — opening the same session in a second tab, or running a second ezTerm, no longer throws a red error when a port is already forwarded. The row shows a neutral amber "in use elsewhere" state with Start still available to take it over.
  • Clearer pane — green Start / red Stop, live per-forward status dots, and error/conflict messages that stay put instead of vanishing.
  • Forwards created from a live tab persist to the session (survive reconnects) and auto-restore on the next connect.

Fixes

  • Right-clicking empty areas no longer opens the webview's native menu, whose Reload wiped every open tab and connection — the native context menu is suppressed app-wide, leaving only ezTerm's own menus.

Dependencies & security

  • Cleared all outstanding high- and moderate-severity advisories: docs site (astro, vite, undici, dompurify, js-yaml) and the app UI (Next's bundled postcss). Rust cmov updated for its RUSTSEC fix.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.9.0

Port forwards created from a tab's Forwards pane now show up immediately and can be saved to the session so they come back on the next connect. Previously a forward added from the pane was tab-only — it could stay invisible in the list until a reconnect, and there was no way to promote it into the session's saved config from the pane.

What changed

Per-tab forwards — instant display + persist (#118)

  • Shows immediately — a forward started or created from the per-tab pane is folded into the list right away (and flips to running) instead of only appearing after the next status event or a reconnect. The runtime layer deliberately doesn't emit an initial "Running" event; the pane now upserts the awaited summary by runtime_id itself.
  • Save to this session — the pane's Add forward dialog gains a “Save to this session (persists across reconnects)” option. When ticked, the forward is written to the session's stored forward config and started on the current tab in one step, so it's live now and auto-restored on the next connect. Ticking it also reveals the auto-start on connect toggle.
  • Dynamic (SOCKS) forwards round-trip correctly end to end — the internally-tagged start target and the nested forward spec (both carrying a kind field) deserialize cleanly for ephemeral and persistent dynamic forwards alike.

Editing and deleting saved forwards continues to live in the session editor's Forwards tab; deleting a tab forward is in the pane.

Tests

  • DB-layer coverage that local, remote, and dynamic forwards persist and list back, against an in-memory SQLite pool seeded with a real session row.
  • Serde tests pinning the exact JSON the forward dialog sends — ephemeral dynamic (SOCKS), persistent-by-id, and a persistent dynamic ForwardInput — so a frontend/back-end shape drift fails the build instead of silently breaking a forward at runtime.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.8.1

SSH keys, key passphrases, and passwords are now fully manageable: a new Credentials manager lists everything in the vault and supports rename, secret rotation, and delete — previously credentials could only be added, from inside the session editor. v1.8.1 also folds in the post-merge review hardening pass on that feature (v1.8.0 was never tagged; this release carries both).

What changed

Credential manager — feature (#111, #112)

  • ui/components/credentials-dialog.tsx (new) — a vault credential manager grouped by kind (SSH keys, key passphrases, passwords). Each row shows the label, creation date, and which sessions use the credential (names on hover), with rename, replace-secret, and delete actions plus per-group add.
  • Delete warns about usage — deleting a credential that sessions still reference lists those sessions in the confirm dialog; they'll need a new credential before their next connect (the schema's ON DELETE SET NULL behavior is unchanged, just no longer silent).
  • Entry points — right-click the sidebar background → Credentials…, or the manage button next to any credential picker in the session editor.
  • Backend — new credential_list_detailed (adds created_at + per-credential session usage) and credential_update Tauri commands; renames and secret rotations apply to every session referencing the credential, with no re-entry of unchanged fields.

Review hardening — fixes (#113)

  • Dangling credential references can't be saved — every credential picker now re-syncs and clamps its selection whenever any credentials manager mutates the vault (a new in-app broadcast), so deleting a credential selected in a sibling picker no longer leads to a FOREIGN KEY constraint failed on session save. The sidebar also refreshes its session list when the manager closes.
  • Nested-dialog key handling — the manager swallows Ctrl/Cmd+Enter while open (it previously leaked through to the session editor's save-and-close), and Escape unwinds one layer at a time even mid-mutation instead of going dead app-wide.
  • Renders as a true modal everywhere — the dialog is portaled to <body>, fixing a clipped, non-modal render when opened from the auth-retry overlay (whose backdrop-filter re-anchored fixed descendants) and removing the implicit-form-submit hazard inside the session editor.
  • Rotation can't revert a rename — label and secret updates are independent in credential_update down to the SQL, so replacing a secret no longer echoes back a possibly-stale label.
  • Plaintext zeroized on every pathcredential_create/credential_update wrap incoming secrets in Zeroizing immediately, closing a leak where a failed encrypt (e.g. vault auto-locked mid-operation) skipped the manual zeroize.
  • Mutation correctness — concurrent mutations from rapid Enter presses are dropped by a re-entrancy guard; a failed delete keeps the confirm dialog open instead of looking successful; Enter saves in the add/replace forms like it already did in the rename row.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.7.4

Patch on top of v1.7.2: re-arranging tabs (or sidebar sessions) while connections are live no longer freezes the app on Linux, and drag-reorder got an across-the-board hardening pass.

What changed

Tab / sidebar drag no longer freezes the app on Linux — fix (#109)

  • ui/lib/pointer-drag.ts (new) — the tab bar and the sessions tree used native HTML5 drag-and-drop, which on Linux runs through WebKitGTK's drag implementation. That implementation can take the pointer grab and never release it (WebKit bug 32840): drag-end never fires and the whole window stops responding. Both surfaces now use a shared pointer-event drag helper that stays entirely inside the webview's event loop — identical behavior on Windows, macOS, and Linux.
  • ui/components/main-shell.tsx — anchors and selected text are natively draggable in WebKit even with no draggable attribute, so a window-level dragstart guard now blocks all in-page native drag initiation. Dropping files from your OS into the SFTP pane is unaffected (external drags never fire an in-page dragstart).
  • Drop-slot semantics are unchanged: same midpoint left/right tab indicators, same 50/50 session and 25/50/25 folder splits in the tree, same backend calls.

Drag-reorder hardening

  • Abort gestures work: moving the pointer away from the tab strip clears the drop indicator and releasing there cancels the drag (previously any release reordered); Escape cancels mid-drag without the subsequent mouse release clicking whatever it lands on.
  • No phantom drops: a release outside the window can no longer leave a drag armed where the next click would commit it.
  • Self-drops are no-ops: wiggling a session a few pixels on its own row no longer silently jumps it to the top of its folder.
  • Stable under mutation: the dragged tab is tracked by id, not index, so a tab closing mid-drag can't make the wrong tab move; sidebar drops are computed from drop-time data, not a stale snapshot.
  • Cheaper while terminals stream: drag hit-testing is coalesced to one pass per frame and the terminal area no longer re-renders on drop-indicator changes — less jank when dragging while SSH output is pouring in.

Housekeeping

  • src-tauri/src/xserver/mod.rs — the Windows-only VcXsrv installer URL constant is now cfg-gated, clearing a dead_code warning on non-Windows builds.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.7.2

Patch on top of v1.7.1: local shells now load your shell environment, and the SFTP pane handles multi-file transfers in both directions.

What changed

Local shells load your environment — fix (#104)

  • src-tauri/src/local/mod.rs — local Unix shells were spawned as a login shell (bash -l). A login shell reads /etc/profile and ~/.profile / ~/.bash_profile but skips ~/.bashrc, which is where most Linux users keep PATH and exports. The result: env vars that worked over SSH appeared not to load in a local-shell tab.
  • Local shells now spawn interactive (bash -i / zsh -i / fish -i), matching gnome-terminal / Konsole / iTerm. ~/.bashrc (and the equivalent rc file) loads on every new tab, so your PATH, aliases, and exports are present.
  • Note: a missing MOTD banner is expected, not a bug — pam_motd only fires on a real PAM login (sshd, login), never on a terminal-emulator-spawned shell.

SFTP multi-file download — fix

  • ui/components/sftp-pane.tsx — right-clicking a file and choosing Download only ever downloaded the right-clicked entry, ignoring any active multi-selection.
  • When the clicked file is part of a multi-selection, the context menu now reads "Download N files…", prompts once for a destination folder, and downloads every selected file into it. Each file gets its own row in the transfer-progress strip. Directories in the selection are skipped (the download path is file-only). Single-file right-click is unchanged.

SFTP multi-file upload — fix

  • ui/components/sftp-pane.tsx — the toolbar Upload button's file picker was single-select. It now allows picking multiple files; each is uploaded and tracked as its own transfer, and a per-file failure no longer aborts the rest of the batch. Drag-and-drop into the pane already supported many files (and recursive directories) — unchanged.

Dependencies

  • russh 0.60.3 → 0.61.1 (#108)
  • tar 0.4.45 → 0.4.46 (#107)

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.7.1

Patch on top of v1.7.0: fixes terminal paste mangling text copied from Windows apps (Claude Code GUI, Slack, browsers, anything that puts \r\n line endings on the clipboard).

What changed

Terminal paste — fix

  • ui/components/terminal.tsxdoPaste used to UTF-8 encode the clipboard text and ship the raw bytes to the PTY. Two things this missed:
    • Line-ending normalisation. Windows clipboards carry \r\n. Terminals treat Enter as a single \r byte. Sending the raw \r\n submitted every line twice — once for the \r, once for the \n — which inserted a blank prompt between every pasted line and made indentation snowball under shells that auto-indent.
    • Bracketed paste. Modern shells (bash, zsh, fish), editors (vim, nano), and TUIs (less, claude-code's CLI) advertise DEC mode 2004 so the terminal can wrap pasted text in \e[200~ … \e[201~. The shell then knows the bytes are pasted and disables auto-indent / history expansion / inline command execution. Writing raw bytes skipped that, so the shell processed each character as if the user had typed it.
  • Paste now flows through xterm.js's own term.paste(text), which normalises \r\n / \n / lone \r all to a single \r and wraps the payload in bracketed-paste sequences whenever the remote side has the mode on. The pasted bytes then ride through xterm's onData listener that already pipes to the PTY — no second code path, no separate IPC.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.7.0

Minor release: the file-browser pane now works for WSL tabs, matching MobaXterm's behaviour. SSH tabs continue to use a real SFTP subsystem; WSL tabs use the Plan 9 bridge Windows already exposes at \\wsl.localhost\<distro>\… and shell out to the distro's coreutils for POSIX-aware listing and chmod. Plain local shells (cmd, pwsh) deliberately remain pane-less — same as MobaXterm.

What changed

File pane for WSL — new

  • src-tauri/src/wslfs/ (new module) — WSL filesystem adapter that mirrors the SSH SFTP surface so the existing sftp_* Tauri commands and SftpPane UI dispatch transparently for both kinds.
    • handle.rsWslFsHandle { distro, user, home }. Linux → UNC path translation (/foo/bar\\wsl.localhost\<distro>\foo\bar) and the wsl.exe -d <distro> [-u <user>] -e env LANG=C LC_ALL=C … runner used for POSIX-aware ops. $HOME is resolved once on first realpath and cached for the lifetime of the connection.
    • list.rs — Directory listing via ls -la --time-style=+%s, parsed token-by-token to preserve filenames containing internal spaces. Symbolic mode string (drwxr-xr-x, lrwxrwxrwx, …) is translated back into the POSIX mode word, including the high-nibble file-type bits, so the frontend's Mode column matches between SSH and WSL panes.
    • ops.rsmkdir / rmdir / remove / rename go through the UNC path via tokio::fs for speed; chmod and realpath shell out so setuid/setgid/sticky bits survive (the Plan 9 bridge maps perms to Windows ACLs and can't represent them).
    • transfer.rs — Chunked upload / download mirroring sftp/transfer.rs byte-for-byte on the event side (same sftp:transfer:{id} channel + TransferProgress payload), so the TransferStatus strip in the pane needs no kind-aware branch.
  • src-tauri/src/sftp/registry.rsSftpRegistry now stores FileBrowser { Sftp(SftpHandle) | Wsl(WslFsHandle) }. One registry, one set of commands, dispatch on the enum.
  • src-tauri/src/sftp/upload_stream.rs — Streaming-upload writer now dispatches by browser kind (sftp_writer against russh-sftp; wsl_writer against a tokio::fs::File on the UNC path). Same begin/chunk/finish/abort lifecycle either way.
  • src-tauri/src/commands/sftp.rs — All sftp_* commands now match on FileBrowser. sftp_open resolves SSH connections from state.ssh first, falls back to state.local for WSL tabs (rejecting plain local shells with a clear message).
  • src-tauri/src/local/{mod,registry}.rslocal::Connection carries an optional WslMeta { distro, user }, populated at spawn time when the session kind is wsl. That's what sftp_open reads to construct a WslFsHandle.
  • src-tauri/src/commands/local.rslocal_disconnect now drops the file-browser registry entry before tearing down the PTY, keeping cleanup symmetric with the SSH path.
  • ui/components/tab-slot.tsx, ui/components/tabs-shell.tsx — File-pane toggle + mount now lights up for both ssh and wsl session kinds. Tooltip text generalised from "SFTP pane" to "file pane".

Notes on coverage

  • WSL2 with GNU coreutils (Ubuntu, Debian, Kali, openSUSE, Oracle, etc.): full feature parity with the SSH pane — list, mkdir, rmdir, remove, rename, chmod, upload (drag-drop + file dialog + streaming), download.
  • Alpine / busybox-only distros: ls -la --time-style=+%s doesn't exist in busybox, so the listing call fails with a clear error. apk add coreutils fixes it; first-class busybox support is a follow-up.
  • WSL1: untested (the UNC bridge is WSL2-era). May or may not work depending on host Windows build.

Terminal — fixes #103 (window shrink not re-fitting)

  • ui/components/terminal.tsx — The xterm resize wiring used to live inside runConnect, so the ResizeObserver only existed after a successful auth handshake. Any resize during the handshake was dropped, and on WebView2 the observer doesn't always fire on a window-shrink that comes from a manual drag (it fires reliably on growth but not on certain shrink paths). When that happened, xterm kept its old cols/rows, Claude / vim / less never received SIGWINCH, and the rendered text extended past the visible viewport.
  • Resize wiring now lives in the mount-time effect alongside terminal.open() so it's live the instant xterm renders, regardless of connect state. Both ResizeObserver and window.addEventListener('resize', …) feed the same rAF-coalesced handler — the window event covers the WebView2 shrink case where the observer doesn't fire, the observer covers in-app layout shifts (SFTP pane toggle, view-mode switch) where there's no window event, and the rAF gate still ensures one IPC per animation frame.

Version alignment

  • Cargo.toml (workspace), src-tauri/tauri.conf.json, ui/package.json all now report 1.7.0 so the installed app and the auto-updater agree on what's running.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.6.9

Patch on top of v1.6.0: sidebar polish, fixes the SFTP drop-in upload path that Chromium quietly broke, bumps the Tauri / windows-rs stack, and removes the OS-drag-out implementation that never worked through WebView2. Also re-aligns the workspace, Tauri config, and UI bundle version numbers so the installed app and the auto-updater agree on what's running.

What changed

Sidebar polish

  • ui/components/sessions-sidebar.tsx — Dropped the green pulsing rail/tile that highlighted "auto-detected" sessions; the visual noise outweighed the signal. The new look is the same flat row treatment used for everything else.
  • migrations/20260518120000_clear_autodetect_color.sql — One-off cleanup migration that clears the auto-set #34d399 colour from existing WSL / Local rows so they pick up the default styling instead of carrying the now-removed accent.

SFTP drop-in upload — fix (regression from a Chromium change)

  • ui/components/sftp-pane.tsx — Two changes against the same Chromium quirk:
    • The drop handler now snapshots every DataTransferItem entry synchronously, before any await. Recent Chromium versions invalidate the items collection as soon as the drop handler yields, so an await mid-handler used to leave the upload code holding a stale list and silently dropping files.
    • dragover now sets dropEffect = 'copy'. WebView2 won't fire drop for OS-sourced files unless a copy effect is advertised — the old version got drops from in-app sources but ignored OS file-manager drops.

Drag-out — removed

  • src-tauri/src/sftp/drag/ (deleted) — The OLE IDataObject / IDropSource / DoDragDrop implementation for streaming SFTP drag-out is gone, along with the sftp_drag Tauri command and the dragTestFile test stub.
    • DoDragDrop returned E_FAIL from every path we tried: worker thread, Tauri main thread via run_on_main_thread, a fresh top-level Win32 window in the same process, a separate sidecar process with foreground + active + mouse held, and inside a real WndProc triggered by SendMessage WM_LBUTTONDOWN.
    • HTML5 dragstart + DownloadURL produces no OS drag in WebView2 either.
    • Conclusion: Tauri / WebView2 on Windows has no working drag-source path today. Right-click → Download… (already wired) is the supported UX.
    • src-tauri/src/splash.rs doc comment updated to reflect the limitation.

Dependencies

  • tauri 2.11.1 → 2.11.2 (and the matching @tauri-apps/api line)
  • windows-rs / windows-core 0.58 → 0.62BOOL moved into windows::core, and IDataObject::DAdvise / IStream::CopyTo now take Ref<T> instead of Option<&T>. (These no longer compile against any of the sftp/drag/ code, but that's deleted in this release.)
  • @types/node patch bump.
  • ESLint pinned to 9.xeslint-config-next still pulls in an eslint-plugin-react that's incompatible with ESLint 10.

Version alignment

  • Cargo.toml (workspace), src-tauri/tauri.conf.json, ui/package.json, and ui/package-lock.json all now report 1.6.9. Earlier releases bumped only the workspace Cargo.toml, leaving tauri.conf.json and ui/package.json stuck at 1.3.7. That caused the installed app's auto-updater to compare a stale version against latest.json and re-trigger passive reinstalls on every launch.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.6.0

Minor: completes the in-process SFTP drag-and-drop story for Windows. v1.5.0 shipped per-folder drop targets, directory uploads, in-pane progress, and the OLE drag-source scaffold. This release lifts the 256 MB upload cap, ships real streaming drag-out from the SFTP listing, and adds multi-select on the SFTP pane so a user can drag a group of files at once. macOS and Linux drag-out remain TODO (issue #28 phases B5/B6).

What's new

A3 — streaming uploads (no cap)

  • src-tauri/src/sftp/upload_stream.rs (new) — Each upload registers a writer task that holds the SFTP session lock for its lifetime and consumes chunks from a bounded mpsc channel. Per-chunk acks let the frontend stop slicing on the first remote write error (disk full, permission denied) rather than after the last byte ships.
  • Tauri commands: sftp_upload_begin / sftp_upload_chunk / sftp_upload_finish / sftp_upload_abort. Backpressure: the writer channel is bounded so chunk awaits if the SFTP link is slower than the frontend.
  • ui/components/sftp-pane.tsxuploadOneFile now slices the dropped File into 1 MB chunks via File.slice().arrayBuffer() and pushes them through sftpUploadBegin/Chunk/Finish. The 256 MB cap is replaced with a 16 GB sanity guardrail.
  • New StreamingUploadStatus strip in the pane shows live per-file progress driven by chunk acks; finished entries self-clear after a moment, errors stick for 4 seconds so the user can see what failed.

B2 + B3 — streaming SFTP drag-out (Windows)

  • src-tauri/src/sftp/drag/win.rs — New SftpStream (IStream) backed by a tokio::sync::mpsc::Receiver. A reader task on the tokio runtime opens the remote file, pushes 64 KiB chunks into the channel; IStream::Read (called by Explorer's file-copy worker) drains the channel via Receiver::blocking_recv. The OS file-copy dialog sees a real STATSTG.cbSize (from a pre-drag stat round-trip) so its progress bar and ETA work. Drag cancel propagates: dropping the IStream closes the channel, the reader task gets a send error and exits.
  • Tauri command: sftp_drag(connection_id, remote_paths) — runs the blocking DoDragDrop on spawn_blocking to avoid starving the tokio runtime.
  • ui/components/sftp-file-row.tsx — Mousedown-to-drag hysteresis (5 px) on non-folder rows. Crossing the threshold fires the OS-native drag; releasing without moving just selects.

B4 — multi-file drag-out + selection UI

  • SftpFileData in win.rs now stores Vec<SftpFile> and builds a FILEGROUPDESCRIPTORW sized for N entries; GetData(CFSTR_FILECONTENTS, lindex=N) routes to the Nth file's IStream. Each file gets its own reader task and channel; they queue on the SFTP session mutex and run in lindex order (which matches the order Explorer requests them).
  • ui/components/sftp-pane.tsx — Standard file-manager selection idioms: plain click replaces, Ctrl/Cmd-click toggles, Shift-click extends a range from the last anchor. Selection clears on cwd change. Dragging from a selected row carries the entire selection (folders filtered out — directory drag-out is a separate phase since it needs recursive remote listing).
  • Selected rows render with an accent tint so the user can see what's about to come along on a drag.

Cross-platform stance

Drag-out is Windows-only in this release. On macOS / Linux the sftp_drag command returns a clear not yet implemented on this platform error and the UI suppresses that as a no-op (no scary toast). Issue #28 phases B5 (NSFilePromiseProvider) and B6 (XDS / wl_data_source) track the remaining native work; the React side won't need changes when they land.

Deferred / not yet implemented

  • B5 — macOS drag-out (NSFilePromiseProvider).
  • B6 — Linux drag-out (X11 XDS, Wayland wl_data_source).
  • B7 — polish: custom drag cursor with progress, bandwidth limit, mid-drag SFTP disconnect recovery.
  • Recursive directory drag-out — needs a remote walk to materialise a FILEGROUPDESCRIPTORW with one entry per descendant. Tracked separately from the file-selection multi-file path (#28 has a note).

Build

  • Same windows = 0.58 Windows-only dep as v1.5.0; no new external dependencies.
  • Cross-compile still validated via cargo check --target x86_64-pc-windows-gnu.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.5.0

Minor: SFTP drag-and-drop. First slice of issue #28 — per-folder drop targets, full directory uploads, in-pane progress for tree uploads, and the Windows OLE drag-source scaffold for drag-out (validated via __ezterm.dragTestFile from devtools; SFTP integration arrives in v1.6.0 / phase B2).

Drag-in (uploads)

  • ui/components/sftp-file-row.tsx — Each folder row is now a drop target. Drop OS files onto a folder and they upload INTO it instead of landing in the current cwd. The row highlights with the accent colour while a drag is over it. Non-folder rows still pass the drag through to the pane-wide handler.
  • ui/components/sftp-pane.tsx — Refactored the drop handler into a shared uploadDataTransfer(targetDir, dt) so per-folder and pane-wide drops share the same code path. Directory uploads via webkitGetAsEntry() walk the dropped tree, sftpMkdir each remote folder (errors swallowed so re-uploads work over existing trees), and ship each leaf file through sftp_upload_bytes with the existing 256 MB per-file cap. A DirectoryJobs strip above the per-file TransferStatus shows a live N / M count while the walk runs, then a one-line summary the user can dismiss.

Drag-out scaffold (Windows)

  • src-tauri/src/sftp/drag/ (new module) — Cross-platform dispatcher (mod.rs) with a Windows-only OLE implementation in win.rs. Implements IDataObject exposing CFSTR_FILEDESCRIPTORW + CFSTR_FILECONTENTS, an IEnumFORMATETC, an IDropSource handling escape/release, and a MemoryStream (IStream) backed by Vec<u8>. The dedicated drag thread runs OleInitialize(COINIT_APARTMENTTHREADED) and calls DoDragDrop, blocking until the user drops or cancels. Non-Windows platforms surface a clear "unsupported" error pending phases B5 (macOS NSFilePromiseProvider) and B6 (Linux XDS / Wayland).
  • src-tauri/src/commands/sftp.rs — New drag_test_file(name, body) Tauri command. Runs the drag on tokio::task::spawn_blocking so the message loop pump doesn't starve the runtime. Returns 'dropped' or 'cancelled'.
  • ui/lib/tauri.tsapi.dragTestFile wrapper plus exposeDevApi() that pins api to window.__ezterm when NODE_ENV !== 'production' (or localStorage.ezterm_dev === '1'). Validate the drag end-to-end from devtools:
    await window.__ezterm.dragTestFile('hello.txt', 'hi from ezTerm');
    // then drag from the cursor onto Explorer
    

What's deferred

  • Phase B2 (SFTP integration with pre-buffered downloads up to 256 MB) — next PR.
  • Phase B3 (streaming IStream for arbitrary sizes), B4 (multi-file / directory drag-out), B5 (macOS), B6 (Linux), B7 (polish) — see issue #28 for the roadmap.
  • A3 (streaming uploads to lift the 256 MB cap) — bundled with B3 in a later PR.

Build / dev notes

  • Added windows crate (0.58, features implement, Win32_Graphics_Gdi, Win32_System_Ole, Win32_System_Com, Win32_System_Com_StructuredStorage, Win32_UI_Shell, …) as a [target.'cfg(windows)'.dependencies] entry — no impact on Linux/macOS builds.
  • Cross-compile verified via cargo check --target x86_64-pc-windows-gnu against mingw.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.4.0

Minor: vault master-password management. The vault now supports changing the master password from inside the app, generating a one-time recovery code, unlocking with that recovery code, and a "forgot password" reset that wipes the encrypted blobs and starts fresh.

After the initial implementation we ran four parallel review agents (performance, security, completeness, code quality) and addressed every Critical, High, and Medium finding before this release. The notable hardenings:

  • vault_reset requires a server-validated confirmation token (DELETE-MY-VAULT). The UI's "type DELETE" gate is no longer the only barrier; any rogue invoke from inside the webview is rejected at the Rust boundary.
  • Re-key is now race-free. vault_change_password holds state.vault.write() for the entire operation and pulls every credential / sync-blob read inside a BEGIN IMMEDIATE transaction. A concurrent credential_create, sync writer, or SSH connect can no longer observe a torn rekey or wedge a row encrypted under the old key into the rotated DB.
  • Recovery codes are single-use. A successful vault_unlock_with_recovery clears the wrap on disk, so a leaked code is no longer a permanent backdoor. Generating a new code requires master-password re-verify and explicit confirmation when overwriting an existing wrap. The dialog also takes a snapshot before regenerating, and never auto-generates on mount.
  • vault_unlock_with_recovery is constant-time-ish. "No recovery code provisioned", "invalid character", "wrong code", and "AEAD mismatch" all run a full Argon2id derive and all surface as BadPassword. No timing or error-class oracle for the recovery state.
  • Cooldown applies to every Argon2 path. vault_unlock, vault_verify_password, vault_change_password (old-password verify), and vault_unlock_with_recovery share the same failure counter and exponential lockout. Bypassing the cooldown by routing through a different command no longer works.
  • Vault rotation tears down active connections + sync target. After a successful change_password or reset, every SSH/SFTP/local connection is closed and the in-memory sync target is cleared. Old credentials don't leak into post-rotation operations, and the sync writer doesn't keep PUTting to the previous remote with the old passphrase.
  • KDF parameter floor on unlock. Stored m_cost_kib below 32 MiB or t_cost below 2 are treated as DB tampering and refused. Prevents an attacker with DB-write access from downgrading parameters and offline-cracking the verifier.
  • Snapshots are tightened. Retention is now both count-capped (5) and time-capped (30 days). On unix the snapshot file is chmod 0600 immediately after VACUUM INTO. The "snapshot before destructive op" is also taken on recovery-code regeneration, not just on password change.
  • Recovery code material is Zeroizing. The decoded bytes, the canonical re-encoded form, and the generated code itself live in zeroizing wrappers instead of bare Vec<u8> / String.
  • Backend strength gate. New master passwords must be ≥ 12 characters AND mix at least two character classes. The UI strength meter still nudges; the backend now also rejects trivial choices.
  • reset nulls both credential columns explicitly. No longer relies on the ON DELETE SET NULL FK cascade.

What changed

  • migrations/20260515154320_vault_recovery.sql — Adds three optional columns to vault_meta (recovery_salt, recovery_kdf_params, recovery_wrapped_key) used to store the recovery wrap. NULL means "no recovery code provisioned."
  • src-tauri/src/vault/mod.rs — Two new operations:
    • change_password re-derives a fresh key from the new password under the current default KdfParams (so a password change is also a security-parameter refresh) and re-encrypts every credentials row and every sync.*_blob setting in a single sqlx transaction. The verifier and salt are rewritten under the new key, the recovery wrap is invalidated, and the caller is expected to lock the vault.
    • reset wipes vault_meta, the credentials table, and the sync.*_blob settings; sessions stay but lose their credential_id reference. Single transaction so a partial wipe is impossible.
  • src-tauri/src/vault/recovery.rs (new) — Recovery-code generation and unlock. The code is a random 24-character base32 string (120 bits of entropy, hyphen-grouped for readability). At generation we Argon2id-derive a recovery key from the code and AEAD-wrap the current password-derived key under it; at unlock we re-derive, unwrap, and verify by decrypting the existing verifier. Transcription tolerances (lowercase, spaces, hyphens; common 0↔O, 1↔I, 8↔B substitutions) are baked into the decoder.
  • src-tauri/src/vault/snapshot.rs (new) — Pre-change DB snapshots via SQLite VACUUM INTO. Snapshots land at <data_local>/backups/vault-<label>-<timestamp>.sqlite; the five most recent are kept, older ones are pruned best-effort.
  • src-tauri/src/commands/vault.rs — Five new Tauri commands: vault_change_password, vault_recovery_status, vault_generate_recovery_code, vault_unlock_with_recovery, vault_reset. The recovery-unlock path shares the unlock-failure cooldown with the password path.

UI

  • ui/components/change-password-dialog.tsx — Old / new / confirm fields with a live strength meter. On success shows the snapshot path and forces the user back through the unlock screen with the new password.
  • ui/components/recovery-code-dialog.tsx — Generates a fresh code on open (regenerating invalidates any prior code), shows it exactly once with copy-to-clipboard.
  • ui/components/recovery-unlock-dialog.tsx + ui/components/reset-vault-dialog.tsx — Reachable from the unlock screen via "Use recovery code" (only shown when one is provisioned) and "Forgot password?" (always shown). The reset path requires typing DELETE as a confirmation.
  • ui/components/unlock-screen.tsx — Wires the two new escape hatches into the lock screen, with a probe of vault_recovery_status so the recovery link only appears when it'd actually work.
  • ui/components/sessions-sidebar.tsx — Adds "Change master password…" and "Recovery code…" entries to the root context menu (alongside Backup / Cloud sync).
  • ui/lib/password-strength.ts (new) — Hoists the strength heuristic out of unlock-screen.tsx so the change-password dialog can reuse it.

Notes

  • Recovery codes are invalidated by password changes. The wrap is made over the password-derived key, so changing that key would orphan the wrap. The dialog warns the user to regenerate after a password change.
  • The reset path does not require any password — premise is the user has lost theirs. Anyone with local file access already had read access to the encrypted blobs (they just couldn't decrypt them).

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

ezTerm v1.3.8

Patch: right-click → Paste now actually focuses ezTerm when the app is in the background.

What changed

  • ui/components/terminal.tsx — Both the terminal's contextmenu handler and doPaste now call getCurrentWindow().setFocus(), and doPaste also re-focuses the xterm instance after writing the pasted bytes. Previously, copying from another app, right-clicking ezTerm, and choosing Paste would paste the text but leave the window/terminal unfocused — the user had to left-click in the terminal before they could keep typing. Now the window comes forward on right-click, and after Paste the cursor is live for immediate input.

The Shift+Insert and Ctrl+Shift+V paste paths were unaffected (they only fire when ezTerm is already focused) and continue to work as before.

Verify

sha256sum -c SHA256SUMS

Licence

GPL v3 (version 3 only). See LICENSE.

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