Skip to content

v0.22.1

Choose a tag to compare

@github-actions github-actions released this 19 Sep 14:30
· 30 commits to main since this release

What's Changed

Added

  • Broadcast keystrokes to a group of tabs (issue #329) — keystroke broadcast previously worked only across the panes of one split tab. A new group broadcast mirrors typing across sessions that each live on their own tab. Membership is an explicit per-session opt-in (right-click a tab → "Add to Broadcast"), following the iTerm2 model rather than deriving the target set from a tab-group label — the set is exactly what you chose, never inferred. A member tab shows a ⇄ marker; once two or more tabs are members the "Group Broadcast" toggle appears in the header bar while a member tab is active, and the mode also toggles with Ctrl+Shift+A. Typing in a member tab mirrors to the other members; a non-member tab types only into itself even while the broadcast is on (the asymmetric rule). Because a group broadcast reaches tabs that are not all on screen, its active state is shown by a persistent banner (not a transient toast), and enabling it on more than five sessions asks for confirmation first. Only terminal sessions participate; embedded RDP/VNC/SPICE tabs are skipped. It is fully independent of the split-view broadcast — either can be on without affecting the other. Delivery reuses the existing per-session VTE commit → send_text_to_session path, which writes to a target's PTY regardless of whether its tab is visible.
  • Reconnect any pane in a split, not only the split owner (issue #328) — when a tab was split into several panes, the "Reconnect" affordance appeared only for the session that owned the tab; a disconnected guest pane had no way back short of closing it. The reconnect engine was already keyed by session, so only the affordance was missing — a guest pane's widget lives inside the owner's layout and has no tab page of its own, so the banner's anchor lookup returned nothing for it. Reconnect is now reachable four ways: a "Reconnect" item in the tab's own right-click menu (shown for any disconnected tab), a "Reconnect" item in a split pane's right-click menu, the "Session disconnected" banner now shown inside the disconnected pane itself, and a configurable "Reconnect Session" keyboard shortcut (default Ctrl+Shift+Y) that reconnects the focused pane, or the active tab's session when it is not split. All route through the same existing in-place reconnect, so tab position and scrollback are preserved exactly as for a non-split reconnect.
  • rustconn-cli add --tunnel-via <NAME|UUID> for Web connections — a Web connection can now be created from the CLI so it browses through an existing SSH connection's host via an auto-raised dynamic SOCKS proxy, the same tunnel_via the connection editor writes. This is what makes a host-only web UI (a Kubernetes dashboard, Cockpit, a router page reachable only past a bastion) openable from the CLI-created entry. The flag resolves the SSH connection by name or UUID and is rejected for non-Web protocols.

Fixed

  • "Select Tab" in a split pane did not open the tab picker, most stubbornly in the right pane of a vertical split (issue #328) — clicking "Select Tab" in an empty split pane failed to show the picker: it flashed and vanished next to an embedded RDP/web pane, or never appeared at all in the right pane of a vertical split, while the left pane and the bottom pane of a horizontal split worked. Three separate causes were untangled. The picker was originally parented to the panel widget or, when that was not resolvable, to the shared window root: an embedded pane repaints continuously (rebuilding the Vulkan swapchain each frame) and an autohide popover parented into that surface loses its input grab and dismisses itself within milliseconds; and because nothing tore the popover down between clicks, a later click called set_parent on a parent that still held the previous popover, which GTK rejected (gtk_popover_get_autohide: GTK_IS_POPOVER failed) while leftover popovers piled up on the shared root. The last cause explained the right-vertical-pane holdout specifically: the "Select Tab" button lives inside an Overlay set to Overflow::Hidden (so the placeholder cannot force the split past 50 %), and a popover parented anywhere in that clipped subtree is clipped away — it maps and pops up but paints nothing. In the right pane of a vertical split the clip is on the width, which is exactly the dimension the popover needs, so it vanished there; the bottom pane of a horizontal split is clipped on height with full width, so it still showed. The picker is now parented to the unclipped split-panel container (the same widget the working context menu uses) and pointed at the button's position, routed through the shared single-active-popover mechanism so each open first tears down any previous one. A final cause survived the reparenting and explained why the right vertical pane alone still failed: its button sits against the window's right edge, and a popover opening to the side with the full session list's natural width had nowhere to be placed, so GTK dropped it without an error while the bottom pane of a horizontal split — clipped on height but with full width below — still had room. The popover now opens downward (PositionType::Bottom) and its list is wrapped in a size-capped ScrolledWindow, so it always fits the space GTK has to place it. autohide behaves correctly — click-outside and Escape both dismiss it — and it also dismisses on selecting a tab.

  • duplicate child name in GtkStack warning from the tab menu on a multi-monitor setup — right-clicking a tab (repeatedly, or after splitting) logged Gtk-WARNING: While adding page: duplicate child name in GtkStack: Move to New Window on. The tab context menu keeps one long-lived gio::Menu bound to the AdwTabView (replacing the model object would crash on rapid right-clicks), and repopulates it in place on every setup-menu. The "Move to New Window on…" monitor picker was a submenu, which GTK materialises as a page in the reused PopoverMenu's internal GtkStack; the model's remove_all() did not clear that stack, so each rebuild re-added the page under the same name. The monitor targets are now flat "Move to New Window on " items in the detach section instead of a submenu — no stack page, no collision — and each monitor keeps its own tab.detach-to-monitor entry. Only appears with two or more monitors, since the picker is omitted for one.

  • Monitoring bar opened for a jump-host session that never connected — the host-metrics bar could appear, and even show live data, for a connection whose SSH session had failed at a jump host. The deferred monitoring start fired as soon as the terminal cursor passed row 2, but a jump chain pushes the cursor past that with a bastion banner or an SSH error even when the final host is unreachable. The sibling "connected" detection already guarded against this with contains_ssh_failure, but the monitoring start did not — and because the monitoring probe uses StrictHostKeyChecking=accept-new with its own ControlMaster fallback, it could reach the target through the jump and paint real metrics for a session the user watched fail (e.g. an interactive host-key rejection the probe auto-accepts). The monitoring start (both initial connect and in-place reconnect) now applies the same contains_ssh_failure guard for jump-host connections, and re-checks on later output so a genuinely successful prompt still starts it. The monitoring probe already targets the final host through the jump chain, not the bastion — that part was correct.

  • Multi-hop SSH with a key failed on every hop past the first — a connection through two or more jump hosts authenticated by an identity key (no password) closed with Permission denied / Connection closed by UNKNOWN port 65535 on the second bastion. RustConn built a nested ProxyCommand that placed the client's local -i <path> on each deep hop, but a -W hop past the first runs ssh on the previous hop, where that local key file does not exist. Identity-only multi-hop chains now use OpenSSH -J instead, and the identity is loaded into the ssh-agent first — -J does not pass -i to the ssh processes it spawns per hop, so the agent is what carries one client key across the whole chain. Verified end-to-end against a genuine laptop→bastion1→bastion2→private-target topology. Single-hop, password, Flatpak, and PKCS#11 cases keep the existing explicit ProxyCommand path (a nested command runs locally for one hop, so a local -i is valid there).

  • rustconn-cli add --protocol web was rejected despite being advertised — add --help listed web as a valid protocol and the Web construction path existed, but the protocol-string parser had no branch for it, so Unknown protocol: web came back and a Web connection could not be created from the CLI at all. The parser now accepts web, http and https, matching the documented surface; the existing Web option flags (--browser-mode, --accept-invalid-certs, --web-toolbar, --zoom-level, …) already applied once the connection was built.

  • rustconn-cli add --aws-profile help described the wrong field — for --provider aws_ssm the help read "AWS SSM instance ID (uses --host as target if not set)", but the flag actually sets the AWS profile (the instance ID always comes from --host). A user following the help put the instance ID in --profile, producing aws ssm start-session --target i-… --profile i-… --region … and The config profile (i-…) could not be found. The help now matches the update command's correct wording and the code's behaviour (profile, defaults to default).

  • Reconnect sometimes did not send the vault password (issue #330) — after a session ended, hitting Reconnect sometimes launched without the credentials that a fresh connection always populated, forcing the user to type them by hand. The initial connect always re-resolves credentials from the vault and caches them, but the in-place reconnect read the target password from that session cache alone, which expires after five minutes; a reconnect past the TTL launched with no password, so it worked only while the cache was still warm. Reconnect now falls back to the same blocking vault resolve the bastion-hop credentials already use, through a shared AppState::ensure_connection_password (cache-first, vault on a miss, then re-cache). Key- and agent-based connections (PasswordSource::None) and interactively prompted ones (Prompt) skip the lookup entirely, so they add no latency. Covers the SSH reconnect path and the Zero Trust Custom Command reconnect that expands ${password}.

  • Rapidly opening RDP connections could pop a password prompt for a saved credential — opening several RDP connections in quick succession sometimes showed the password dialog even though the password was stored in the secret backend. The connect path resolves credentials on a background thread, so the concurrent launches raced on the one backend underneath (a second bw/op/pass invocation, a second keepassxc-cli open, a second Secret Service round trip); the loser stalled past its timeout, a timed-out resolve collapsed to "no credentials", and the connection fell through to the prompt. Every vault backend operation is now serialised process-wide by a VAULT_OP_LOCK in dispatch_vault_op_for, so a second lookup waits for the first instead of colliding with it (these calls never run on the GTK thread, so the wait does not freeze the window). Additionally, the RDP path now caches the vault-resolved credentials the way the SSH, VNC and Web paths already do, so a second rapid connect takes the warm-cache fast path before any backend lookup is attempted.

  • RDP password dialog had no visible close button (GNOME HIG) — the shared dialog header hid both the start- and end-side window controls, so the credential prompt (and the snippet Execute dialog) offered no on-screen way to dismiss it — only the undiscoverable Escape shortcut. The header now keeps the native close (✕) button on the start side, restoring the standard close affordance HIG expects while leaving the primary action as the only button on the end side.

Changed

  • Embedded RDP status text no longer names the underlying client — the connecting/connected labels read "Connecting via IronRDP…", "Starting FreeRDP…" and "RDP session running in FreeRDP window", exposing an implementation detail the user did not choose and that can silently switch mid-connect when the embedded client falls back. They now read "Starting RDP…" (both embedded and external), "RDP session running in external window", and "Switch to the external window to interact with the session".

Dependencies

  • Updated: cc 1.4.6→1.4.7
  • Unchanged, checked: ./scripts/check-cli-versions.sh is clean — every CLI download endpoint is reachable and current (TigerVNC pinned at 1.16.2; kubectl, Tailscale, Teleport, Boundary, Hoop, Bitwarden, 1Password resolve latest at runtime). The bundled Flatpak sources and the GNOME runtime 50 pin are unchanged from 0.22.0.

Installation

Flatpak (Recommended)

flatpak install flathub io.github.totoshko88.RustConn

Snap

sudo snap install rustconn

Debian/Ubuntu (.deb from this release)

sudo dpkg -i rustconn_0.22.1_amd64.deb
sudo apt-get install -f  # Install dependencies if needed

Fedora (.rpm from this release)

sudo dnf install rustconn-0.22.1-1.fc44.x86_64.rpm

AppImage

chmod +x RustConn-0.22.1-x86_64.AppImage
./RustConn-0.22.1-x86_64.AppImage

macOS (Homebrew)

brew tap totoshko88/rustconn
brew install rustconn
open $(brew --prefix)/opt/rustconn/RustConn.app

All dependencies (GTK4, libadwaita, VTE, Adwaita icons) are installed automatically.
Requires macOS 13 (Ventura) or later.

OBS Repositories

Packages available at: https://build.opensuse.org/package/show/home:totoshko88:rustconn/rustconn

# Debian 13 (Trixie)
echo 'deb http://download.opensuse.org/repositories/home:/totoshko88:/rustconn/Debian_13/ /' \
  | sudo tee /etc/apt/sources.list.d/rustconn.list
curl -fsSL https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/Debian_13/Release.key \
  | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/rustconn.gpg > /dev/null
sudo apt update && sudo apt install rustconn

# Ubuntu 24.04 LTS (Noble)
echo 'deb http://download.opensuse.org/repositories/home:/totoshko88:/rustconn/xUbuntu_24.04/ /' \
  | sudo tee /etc/apt/sources.list.d/rustconn.list
curl -fsSL https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/xUbuntu_24.04/Release.key \
  | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/rustconn.gpg > /dev/null
sudo apt update && sudo apt install rustconn

# Ubuntu 26.04 LTS (Resolute)
echo 'deb http://download.opensuse.org/repositories/home:/totoshko88:/rustconn/xUbuntu_26.04/ /' \
  | sudo tee /etc/apt/sources.list.d/rustconn.list
curl -fsSL https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/xUbuntu_26.04/Release.key \
  | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/rustconn.gpg > /dev/null
sudo apt update && sudo apt install rustconn

# Fedora 44
sudo dnf config-manager addrepo --from-repofile=https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/Fedora_44/home:totoshko88:rustconn.repo
sudo dnf install rustconn

# Fedora 43
sudo dnf config-manager addrepo --from-repofile=https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/Fedora_43/home:totoshko88:rustconn.repo
sudo dnf install rustconn

# openSUSE Tumbleweed
sudo zypper ar https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/openSUSE_Tumbleweed/ rustconn
sudo zypper ref && sudo zypper in rustconn

# openSUSE Leap 16.0
sudo zypper ar https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/openSUSE_Leap_16.0/ rustconn
sudo zypper ref && sudo zypper in rustconn

Arch Linux (AUR)

yay -S rustconn

FreeBSD (Ports)

pkg install rustconn

Full installation guide: https://github.com/totoshko88/RustConn/blob/main/docs/INSTALL.md