Skip to content

Terminal copy and Ctrl keys, SSH agent forwarding, system tray - #113

Merged
timhartmann7 merged 7 commits into
mainfrom
feat/clipboard-agent-forwarding-tray
Sep 26, 2026
Merged

timhartmann7 merged 7 commits into
mainfrom
feat/clipboard-agent-forwarding-tray

Conversation

@timhartmann7

Copy link
Copy Markdown
Owner

Three changes, one branch.

Closes #96
Closes #101
Closes #102

Copy from the desktop terminal with Ctrl+Shift+C

On Windows and Linux nothing copied from the terminal but the right-click menu: Ctrl+C is the shell's interrupt and has to stay one. Ctrl+Shift+C now copies the selection, as in GNOME Terminal and Windows Terminal, and Ctrl+Shift+V keeps pasting through the webview's own binding. Ctrl+C and Ctrl+V still send ^C and ^V. The chord follows the letter on Latin layouts (Dvorak's J key stays a J) and the physical key on non-Latin ones. macOS is unchanged: Cmd+C and Cmd+V already reach xterm through the Edit menu.

Ctrl keys on a non-Latin layout (Linux)

Found while verifying the copy chord on a Russian layout. Under any non-Latin layout, WebKitGTK reports keyCode 0 for letter keys, which had two effects. xterm sent nothing for Ctrl+C, Ctrl+D, Ctrl+Z or Ctrl+[, so a running command could not be interrupted. And the webview's own Ctrl+Shift+V, bound by the V key symbol, never fired. JS navigator.clipboard.readText and execCommand('paste') are both denied in that webview.

When the key code is 0, the physical key now decides, as in GNOME Terminal:

  • a Ctrl+letter chord (and Ctrl+[, \, ]) goes in as its control character through term.input. This applies only when the key is a letter of a non-Latin script: Latin letters with diacritics (ü, å, ç) and dead keys also report 0, and they keep their meaning;
  • Ctrl+Shift+V calls terminal_paste, which runs WebKitGTK's own PasteAsPlainText editing command, so bracketed paste works as with the native binding.

Every other webview reports a Latin key code under these layouts and never takes this path.

SSH agent forwarding

A host can lend its terminals the local agent, like ssh -A. That covers sudo through pam_ssh_agent_auth, git over SSH and onward hops. It is off for every host until switched on in either app's host form, or set with ForwardAgent yes in ~/.ssh/config.

  • Only a terminal's target connection lends it: not bastions, the dashboard, SFTP, tunnels or key setup.
    • The decision is made once per terminal, and only when an agent actually answers at SSH_AUTH_SOCK.
    • The flag is fixed when the connection's handler is built, so every re-dial of the same login carries the same answer.
  • auth-agent-req@openssh.com goes out before the pty and shell, and only on unix.
  • Refusal: an agent channel a server opens on a connection that does not lend is closed, as ssh(1) refuses one.
    • russh confirms these channels before the handler runs, so this is confirm-then-close.
    • The handler never returns an error, which would end the connection.
  • A lent channel ends with EOF, never a close: in russh 0.46 a close of ours racing the server's window adjust ends the whole connection. It is also dropped when the session ends, so an agent that never answers cannot hold a task open.
  • ssh_config:
    • Only a host's own yes turns it on.
    • A no in the global section, a wildcard Host or a Match block, including inside Included files, keeps the agent from every host after it, since ssh(1) may take that value first.

Minimize or close to the system tray

Two opt-in settings (Settings → Window). The icon exists only while one is on, and the window hides into it only while it is up.

  • Linux counts a tray only when there is one to show the icon:

    • the appindicator library (probed with dlopen, since libappindicator panics on a missing one);
    • and either a StatusNotifier watcher on the session bus or, for an X11 window, an XEmbed tray.

    Stock GNOME without the AppIndicator extension therefore reports no tray instead of hiding the window into nothing.

  • Minimize-to-tray needs X11: GTK under native Wayland is never told the window was minimized, so the switch is disabled there with a note. The window hides only on the way down, so a stale event after a restore cannot send it back.

  • macOS uses a menu-bar icon. Minimizing stays with the Dock, and a Dock click brings a hidden window back (RunEvent::Reopen).

  • tauri-plugin-single-instance makes a second launch reveal the running window.

    • It is skipped when the session bus address cannot be parsed (DBUS_SESSION_BUS_ADDRESS=disabled:), where the plugin would panic at startup.
    • The render-retry re-exec releases its D-Bus name first.
  • The startup reveal is one-shot, so the 3 s fallback never pulls a window back out of the tray. A reveal requested before the page is up waits for it.

Contract

New wire surface, approved:

  • HostDto.forwardAgent and HostInputDto.forwardAgent.
  • The command set_tray_behavior { minimizeToTray, closeToTray } → TraySupportDto { available, minimize }.
  • The command terminal_paste → (), which does something on Linux only.

Packaging

With tauri's tray-icon feature, the bundler makes libayatana-appindicator3 a hard .deb/.rpm dependency and copies it into the AppImage. release.yml already installs the dev package the bundler needs. New Linux-only crate dependencies (zbus, x11-dl, gtk) were already in the tree through tauri and its plugins.

Verification

All Linux runs were in Docker (Debian trixie, WebKitGTK, release-like build) against a real OpenSSH server.

  • Terminal keys:
    • Ctrl+Shift+C copies, on a US and on a Russian layout.
    • Ctrl+C sends ^C and Ctrl+V sends ^V.
    • Ctrl+Shift+V pastes on a US layout.
    • On a Russian layout, Ctrl+С interrupts, Ctrl+D (the В key) sends EOF, Ctrl+[ sends ESC and Ctrl+Shift+V pastes.
  • Agent forwarding (desktop and terminal apps):
    • ssh-add -l, sudo through pam_ssh_agent_auth and an onward ssh hop all work.
    • A host without the flag gets no SSH_AUTH_SOCK, and neither does a stale SSH_AUTH_SOCK.
    • A ForwardAgent yes import forwards, and the editor switch persists.
  • Tray on X11:
    • With no tray, the settings say so and closing quits.
    • With a StatusNotifier watcher or an XEmbed tray, the tray is available.
    • Minimize and restore work over three cycles in a row (WM_STATE withdrawn versus iconic).
    • Close to tray, the tray's Show and Quit, and a second launch revealing the window (exit 0) all work. A terminal survives being hidden, and the setting survives a restart.
  • Tray on native Wayland (headless sway and swaybar): the tray is available, minimize is reported unsupported, and closing hides the window.
  • Tray robustness:
    • Without libayatana there is no panic, and the settings warn.
    • With no D-Bus, or with a disabled: address, the app starts.
    • A forced render-retry re-exec keeps the single-instance name.
  • Tests:
    • tests/agent_forward.rs runs an in-process SSH server that opens agent channels whether or not the client offered one. It tells a closed channel from a silent one and checks the connection survives a refusal; it was mutation-checked against a silent drop and an erroring refusal.
    • ssh_config unit tests and an Include integration test cover ForwardAgent.
    • Vitest and Playwright cover the copy chord (including Dvorak and macOS) and the non-Latin fallback. The Playwright check uses a synthetic keyCode 0 keydown and was mutation-checked.
    • They also cover the host switch (Linux and Windows) and the tray settings (available, none, Wayland, macOS).
    • Startup-contract guards pin the plugin order and the absence of prevent_exit.
  • Gates:
    • cargo fmt.
    • cargo clippy --all-targets -D warnings for the workspace and the GUI on macOS, and the GUI on Linux.
    • cargo test, svelte-check 0/0, vitest 263, Playwright 54/55 (see below).

Review

/code-review high plus an adversarial review (four lenses, each finding checked by a refuting verifier). Everything confirmed is fixed except the items below.

Waived, with reasons:

  • Tray commands reachable from the webview. Enabling tray-icon registers tauri's tray commands, and core:default already granted core:tray:default. The capability file is unchanged, and the UI has no HTML-injection sink. Tightening core:default into explicit sub-sets would change existing behaviour and is left for its own change.
  • Duplicated switch markup and the persisted-pref store pattern. This follows the existing pattern. A shared Switch and a pref factory would refactor merged code.
  • One branch for three issues. The maintainer asked for it.

Known limits (not fixed here)

  • Not run here: Windows (WebView2 clipboard, tray restore order) and the macOS runtime.
  • Agent forwarding gaps: it does not follow IdentityAgent, a ForwardAgent <socket>, or a yes under Host *. It is not available on Windows, where the agent is not used for logins either.
  • Pre-existing:
    • flaky tests/tunnel.rs on macOS (fails on main too);
    • the e2e Close web-1 · terminal label test;
    • eslint and knip are still not wired.
  • Owed: the stage-boundary /code-review ultra, which only the maintainer can start.

@timhartmann7
timhartmann7 merged commit 15169f5 into main Sep 26, 2026
6 checks passed
@timhartmann7
timhartmann7 deleted the feat/clipboard-agent-forwarding-tray branch September 26, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]Minimize to the system tray [Feature Request] Agent Forwarding Keyboard shortcuts for copy and paste not working

1 participant