Skip to content

v0.20.0

Latest

Choose a tag to compare

@github-actions github-actions released this 13 Aug 16:41

What's Changed

Added

Sessions and tabs

  • Opening a cluster labels its tabs with a group named after the cluster — a cluster used to dissolve into anonymous tabs the moment it opened. Nothing on screen said which tabs belonged together, and the only thing that could act on the set as a whole was the Disconnect button in the Clusters dialog. Each member tab now joins a tab group named after the cluster, so it reads [cluster] host and every existing tab-group operation applies to the cluster for free: Close All in Group on any member closes the cluster, Close All Ungrouped closes everything except your open clusters, and the tab switcher (Ctrl+%) shows the cluster name beside each member. No new command, no new setting — the two features simply meet.

    The group name is the cluster's name verbatim, so renaming a cluster affects the next opening rather than tabs already open, and a cluster sharing a name with a hand-made group merges into it, which is the rule two hand-labelled tabs already follow. Per-tab overrides still work: Remove from Group or Set Group… move one member out without removing it from the cluster, so Disconnect still reaches it. Closing a cluster retires its name from the group chooser unless another tab still wears it — otherwise every cluster ever opened would accumulate there, which is a new problem created by naming groups automatically rather than by hand.

    This is also the first caller TerminalNotebook::set_tab_group and TabGroupManager::remove_group have ever had: the assignment mechanism was already written and wired to nothing, with the context-menu dialog duplicating its body inline instead.

  • Web embedded mode: auto-hide floating toolbar with reveal zone — the navigation toolbar (Back, Forward, Reload, Home, URL bar, Zoom, Menu) now floats as a semi-transparent overlay above the WebView, matching the behavior of RDP and VNC embedded sessions. The toolbar appears briefly when the page connects, then auto-hides after 2 seconds of inactivity. A small arrow indicator at the top center acts as the reveal zone — hover or click it to show the toolbar. The toolbar stays visible while the pointer is over it, any control has focus, or a menu is open. This gives the web content the full viewport height, identical to how graphical remote sessions work.

  • A connection can now switch its floating toolbar off entirely, hot zone and all (#260) — RDP, VNC and the embedded browser each gained a Session Toolbar / Navigation Toolbar switch in the connection editor, plus --vnc-toolbar and --web-toolbar on rustconn-cli add and update. Turned off, there is no toolbar, no 44×44 reveal arrow and no revealer left to intercept a click — and the split view stops adding its own corner buttons over that session too. #260 asked for this and was closed by 0.19.14's auto-hide toolbar, which the reporter then explained was not the same thing: with a full-screen browser behind the session they kept catching the barely visible arrow by accident, and the toolbar it summoned covered the tab strip they were aiming for. Auto-hide reduces how long the obstruction lasts; it cannot remove one for someone who never wanted the toolbar.

    What goes with it is stated in the switch's own subtitle rather than left to be discovered: for RDP and VNC that is Ctrl+Alt+Del, which has no keyboard route of its own, along with Fit resolution, autotype, scripts and the quick actions — Copy and Paste survive, since Ctrl+C/V reach the remote session directly. The embedded browser loses less: Alt+←/→, Ctrl+R, Ctrl+L and Ctrl+±/0 keep working, so only the URL bar itself is gone. In a split pane the panel's Remove from Split and Close session stay on the right-click menu, which lives on the pane and not on the overlay being suppressed.

    Stored as hide_floating_toolbar, phrased negatively on purpose: false is both what Default::default() produces and what a profile written before this release deserialises to, so no existing connection and nothing built by an importer, template or wizard changes behaviour. The setting travels into a split pane by itself — the split view reparents the live viewer rather than rebuilding it, so the controller and its state move with the widget, including across the layout rebuild that follows closing a pane. Quick Connect, which has no stored profile, keeps the toolbar.

Settings

  • Settings ▸ Interface ▸ Rendering — pick the GTK renderer (issue #274) — Automatic, Hardware (GPU) or Software (Cairo), applied on the next start because GTK reads GSK_RENDERER while it opens the first window. Automatic keeps GTK's GPU renderer everywhere except the two environments where it is known to be worse than software rasterisation (see the macOS guest-VM fix below and the X11 popover workaround from #85). The two explicit values exist because both of those are heuristics about the environment rather than facts about the user's hardware: Software is the escape hatch for a slow environment the probe does not recognise, and Hardware is the way back for an X11 session whose driver is fine — until now every X11 session was downgraded unconditionally. An explicit GSK_RENDERER in the environment still overrides all three.

Release and distribution

  • Signed build provenance for every release artifact — the .deb, .rpm, AppImage and Flatpak bundle attached to a GitHub release now carry a SLSA build-provenance attestation, signed through Sigstore with a short-lived certificate issued to the release workflow itself. A download can be checked against the repository and the commit it was built from with gh attestation verify <file> --repo totoshko88/RustConn, which fails if the artifact was rebuilt elsewhere or altered after the run. The attestation binds a file's digest together with its name, so it is generated after the Flatpak bundle is renamed to its versioned filename. The snap is not covered here: it is built by a separate post-release job and the Snap Store signs it on its own.

Fixed

Tabs, clusters and split view

  • An RDP, VNC, SPICE or Web member of a cluster was never registered in it, so "Disconnect all cluster sessions" could not close it — the notebook resolves a cluster membership when the member's tab appears, and that resolution was called from the terminal creation path only. Every other protocol opened its tab and stayed in the pending map forever: get_cluster_sessions never listed it, Disconnect skipped it, and the stale pending entry survived until the cluster was unregistered. The call moved to notify_tab_added, the one function all five creation paths (terminal, VNC, embedded RDP, embedded Web, external process) already go through, so a cluster of mixed protocols now behaves like a cluster of shells. Found while wiring the cluster tab groups above — the same single point that fixes this is what labels the tabs.

  • A tab returning from a split pane lost its group labelrestore_session_tab rebuilds the tab for a session leaving a split layout and set the bare connection name as the title, while the [group] prefix is only re-applied by the detach path. The tab stayed a member of its group for every operation and still showed the group in its tooltip, so the label was missing from the one place the user reads it. The prefix is now composed in a single place (tab_title) that the creation, rename, group-change and restore paths all use, with a matching strip_group_prefix for the one case that has to recover the base name from the rendered title.

The floating viewer toolbar

  • The floating viewer toolbar was revealed but inert for its first two seconds — RDP, VNC and Web alike — every state change routes through ToolbarAutoHide::show_briefly, which revealed the toolbar without making it targetable. Only the private show(), reached from the arrow handle, ever called set_can_target(true), and the viewers construct their revealer with can_target(false) so it does not swallow input while hidden. So the toolbar that appears on connect could not be clicked, and the pointer-motion controller that is supposed to hold it open while hovered never fired either — it became usable only after auto-hiding once and being re-revealed from the arrow. Targetability now follows revealed-ness in one place, which is what the comment beside can_target(false) had claimed all along. Present since the auto-hide toolbar arrived; 0.20.0 extended it to Web.

  • The toolbar overflow breakpoints had been overtaken by the touch-target rule — the three collapse thresholds were eyeballed pixel counts, and ToolbarAutoHide later began applying the GNOME HIG 44×44 minimum to every toolbar button, which the numbers predated. The Web toolbar felt it worst: nine buttons at a 44 px floor plus a URL entry need more width than the 520 px breakpoint and its 48 px hysteresis grant, so between roughly 570 and 620 px it expanded and then clipped anyway — the failure the overflow controller exists to prevent. ToolbarOverflow now measures the toolbar (WidgetExt::measure) instead of comparing against a constant, and reports the same required width whether expanded or collapsed by adding back the parked actions and subtracting the "⋯" button that replaced them. RDP_OVERFLOW_THRESHOLD_PX, SPICE_VNC_OVERFLOW_THRESHOLD_PX and WEB_OVERFLOW_THRESHOLD_PX are gone, and with them a ponytail note asking for them to be retuned whenever a toolbar gains a button.

  • Web embedded mode: the reveal handle sat on top of the page, and the toolbar ignored the local theme — the arrow that summons the hidden toolbar is a 44×44 button pinned to the top centre of the viewer, which is right for a remote desktop and wrong for a web page: the top centre is where a site puts its logo, primary navigation and search, and the button swallowed clicks there. Its position is now the caller's choice (RevealHandle::TopCentre for RDP and VNC, TopTrailing for Web, which is also where the toolbar's own menu button appears). Separately, .web-toolbar-overlay had been merged with .rdp-toolbar-overlay to remove a byte-identical duplicate, which also locked the Web toolbar to a fixed dark scrim — fine over a remote desktop drawn in someone else's theme, wrong for local chrome containing a GtkEntry, which produced a light URL bar inside a dark bar on a light desktop. The two classes now share their geometry and differ in exactly the one declaration that has to. The handle's min-width/min-height have been dropped from CSS, where they disagreed with the 44×44 size request that was silently winning.

  • Web embedded mode: the toolbar clipped instead of collapsing in a narrow split panel — making the toolbar float (see Added) replaced its own responsive rule, a tick callback that hid Home and the secondary actions below 500 px, with nothing. A floating toolbar spans the panel, so in a pane narrower than the assembled width the box overflowed its allocation and GTK clipped the last children — the secondary actions and, worst of all, the menu button, which is the only route to Copy URL, Open in System Browser, Zoom Reset and Clear Session Data. The Web toolbar now uses the same ToolbarOverflow controller as RDP and VNC: below WEB_OVERFLOW_THRESHOLD_PX (520 px, between the RDP and VNC breakpoints because this is the only toolbar carrying a text entry) Home, Autofill, Zoom In and Zoom Out are reparented into a "⋯" popover rather than hidden, so every action stays reachable at any width — which is more than the pre-0.20 rule managed, since the menu it hid them behind never contained them. Back, Forward, Reload, the URL bar and the menu stay in place. ToolbarOverflow gained attach_to_widget for this: its existing attach watches GtkDrawingArea::resize, and a WebView has no drawing area, so the new variant reads the allocated width from a tick callback and only acts when the number changes.

Embedded Web viewer

  • Web zoom shortcuts (Ctrl+/-/0) did not work in split view — keyboard shortcuts for zoom were intercepted by WebKitGTK's internal handlers before reaching the application's EventControllerKey. Fixed by attaching the key controller to the container widget with PropagationPhase::Capture, which intercepts events before they propagate to the WebView.

  • …and still only after clicking the toolbar, and then in the wrong panel — the capture-phase controller above sits on the panel's container, so it only sees a key press that GTK routes into that container, which requires something inside it to hold the keyboard focus. Clicking a toolbar button gave the focus to the button as a side effect of the press, which is why the shortcuts came alive only after using the toolbar; in a split view they kept reaching whichever panel had last been clicked, because a click on the page moved no focus at all. Two things were in the way. A click on the page never reached WebKit: SplitViewAdapter's panel gesture recognises buttons, VTE terminals and the RDP/VNC drawing surfaces as interactive and steps aside for them, but a WebKitWebView matched none of those, so the gesture was claimed on what it took for empty panel background — which also means links, form fields and text selection were being swallowed in a split panel, not just the focus. It is now recognised like its siblings. And the WebView now takes the focus itself on a press, so any click in the page area routes that panel's shortcuts to that panel's page. A set_focus_child chain was tried alongside this and has been removed: nothing calls grab_focus() on that container, gtk_widget_set_focus_child is documented as an API for widget implementations, and GTK overwrites the value during focus navigation — the gesture is what works, and it is the same thing the RDP and VNC surfaces already do.

  • Web embedded mode: a failed page load left the toolbar logic unrun, and a load timeout said nothing at allset_state grew the toolbar handling for the Error state, but nothing reached it: the only caller passing Error was a report_error helper with no callers of its own, while the two paths that actually fail — the load-failed signal and the 60-second load timeout — assigned the state field directly from closures that hold a few Rc clones rather than the widget. So the state and its presentation could disagree, which is the kind of split that quietly outlives the release that introduced it. The presentation now lives in one associated function every writer calls, report_error is gone, and the timeout fills the reconnect banner the way a load failure already did — before this it only fired the callbacks, leaving a page that never loaded with no explanation and no Reload button. On Error the toolbar is now shown briefly rather than switched off: unlike a remote desktop, an error page is something the user navigates away from, and Back, the URL bar and the menu are the way out.

Connection defaults

  • RdpConfig::default() and VncConfig::default() disagreed with their own serde defaults, and every importer used the wrong one#[derive(Default)] cannot see #[serde(default = "…")], so five RDP fields and three VNC fields had two different "defaults" depending on how the config was created. A stored profile missing the keys deserialised to clipboard_enabled: true, show_local_cursor: true, script_paste_via_clipboard: true, jiggler_interval_secs: 60 and autotype_delay_ms: 20; RdpConfig::default() handed back false, false, false, 0 and 0. That second set is what every Remmina, RoyalTS, RDM, SecureCRT, CSV, Ásbrú and libvirt import arrived with, along with models::template, sync::inventory, the connection wizard and RDP quick-connect — so an imported RDP connection had no clipboard, no local cursor, and a 0 ms inter-character autotype delay, fast enough to drop characters on a Citrix or gateway session. VNC imports lost scale-to-fit as well. Both types now have hand-written Default impls that call the same default_true() / default_jiggler_interval() / default_autotype_delay() functions the serde attributes name, which is what SpiceConfig already did. A new test compares whole structs — T::default() against deserialising {} — for all four protocol configs, so the next field added with a non-Default serde default fails the suite instead of quietly repeating this. Surfaced while adding hide_floating_toolbar above: that field is negative precisely so this trap could not catch it, and then the trap was worth closing.

Startup and platform

  • A non-system interface language cost macOS users the tray icon (issue #158) — applying a configured language re-executed the process with LANGUAGE set in the child, and that re-exec was not platform-gated. On macOS replacing the process image destroys the LaunchServices scene registration NSStatusItem needs, which is the exact defect the renderer fallback was moved off exec() to avoid this release — diagnosed for GSK_RENDERER, missed for LANGUAGE, in the same file tree. The variable now goes through rustconn-env-sys::set_startup_var like the renderer does, and the setlocale call that follows is what makes gettext re-read it. The _RUSTCONN_LANG_SET loop sentinel is gone with the re-exec, and startup spawns two processes fewer than before 0.20.0 rather than one.

  • macOS inside a virtual machine: input lag, late frames and stuttering scroll (#274) — Apple's paravirtualised GPU gives a macOS guest Metal but no accelerated OpenGL, and Homebrew builds gtk4 with -Dvulkan=disabled, so GSK has only its GL renderer and Cairo to choose from and the GL one lands on a software path inside a guest: slow, and busy enough to keep a core occupied. The reporter had found GSK_RENDERER=cairo themselves; RustConn now reaches the same conclusion on its own, asking sysctl -n kern.hv_vmm_present — Darwin's own "am I a guest" answer — and selecting Cairo when the answer is 1. A probe that cannot be answered counts as "no hypervisor", never as "guest", so a failed sysctl on bare metal cannot cost anyone their GPU renderer. The chosen renderer and the reason for it are logged at info level. Two details worth recording for the next report: GDK_SCALE, which the same workaround set, is an X11-only variable and does nothing on the macOS backend (which takes its scale from NSWindow.backingScaleFactor); and the fix had to work for /opt/homebrew/bin/rustconn launched directly, not just the .app, so it could not live in a bundle wrapper script.

Accessibility

  • The header bar's busy spinner lost its accessible name — the gtk4::Spinneradw::Spinner swap dropped the update_property(Property::Label) call, because libadwaita 0.9 does not implement IsA<gtk::Accessible> for adw::Spinner and the call does not compile on the concrete type. Every GtkWidget is a GtkAccessible in C, so crate::spinner::set_accessible_label sets it through a GtkWidget upcast and both build paths keep the label a screen reader needs.

Localisation

  • POTFILES.in did not list the two modules extracted from terminal/mod.rsscripts/check-potfiles.sh is a CI gate and it fails on an unlisted source that calls i18n(), so the terminal split (see Improved) would have turned the release commit's CI red. Extraction itself was never affected — po/update-pot.sh globs rustconn/src rather than reading the manifest — so no msgid was lost and no catalogue changed; the manifest was simply out of step with the sources it is there to describe. scripts/release.sh now runs this check plus check-i18n-escapes.sh and check-po-complete.sh, the same three the CI i18n job runs, so the next drift is caught before a tag instead of after one.

  • Seven translatable strings were in the source but in no catalogue — regenerating po/rustconn.pot for this release surfaced them: the KeePass unlock dialog's four strings, the Backspace/Delete hint, and the two Automatic (^?) / Automatic (\e[3~) erase-mode labels (the latter in 15 locales; Ukrainian had them). They rendered as English in every locale while scripts/check-po-complete.sh reported all 16 catalogues at 100%, because that gate reads the committed .po files and never regenerates the POT to compare against — the same class of silent rot check-potfiles.sh was written for, in the one direction it does not cover. Now translated in all 16 locales. Nothing yet checks that the committed POT matches the sources; that gap is real and outlives this fix.

  • Six strings in the Web connection panel were in no catalogue, for the same reason as the seven above and a different mechanismSwitchRowBuilder::build() calls i18n() on a variable, so xgettext finds nothing at the call site: "JavaScript", "Private / Incognito Mode" and their subtitles rendered through gettext at runtime with msgids that had never been extracted, which is to say they rendered in English in all 16 locales. check-potfiles.sh cannot catch this — the file is listed, it is the string that is invisible. They now go through the _i18n_markers() function the same file already keeps for dialog_header() labels, and are translated in all 16 locales. The Web panel is the builder's only caller.

Changed

  • The 60-second Web load timeout now reports itself in the reconnect banner — the visible half of the state-machine fix above, called out separately because it changes what the user sees: a page that never finishes loading now shows "Connection timed out. Check that the host is reachable." with a Reload button, where before the tab simply sat there.

Improved

Lints, toolchain and CI gates

  • The three crates allowed to write unsafe were the only three with no lints at all[workspace.lints.rust] set unsafe_code = "forbid", and forbid cannot be overridden at any level, so rustconn-pty-sys, rustconn-locale-sys and rustconn-env-sys could not inherit the workspace lint table and each declared its own [lints.rust] unsafe_code = "allow" instead. A crate-local [lints] table replaces the inherited one rather than adding to it, so the effect — visible in what cargo actually passes to clippy-driver — was that the FFI crates compiled with --allow=unsafe_code and nothing else: no clippy::all, no pedantic, no nursery, no unwrap_used, no dbg_macro. The workspace lint is now deny, each helper writes [lints] workspace = true and re-opens the one lint with a crate-level #![expect(unsafe_code, reason = "…")], and expect rather than allow so a -sys crate that loses its last unsafe block says so instead of keeping a stale exemption. One step weaker on paper, considerably stronger in practice; introducing unsafe elsewhere is still a hard error, and the pre-write hook still blocks it before the compiler is reached. Turning the lints on found three real things in rustconn-pty-sys, all fixed: two clippy::borrow_as_ptr sites where &ws as *const _ and an implicit &mut coercion created a reference and immediately discarded it — asserting to the compiler an aliasing guarantee that ioctl(2) and poll(2) never agreed to — now &raw const and &raw mut, and one redundant import in the test module.

  • rustfmt.toml stopped configuring two options it could not applyimports_granularity = "Module" and group_imports = "StdExternalCrate" are nightly-only, so on the pinned stable toolchain they did nothing except print a warning apiece per crate: 22 lines of noise on every cargo fmt and on the CI format gate, for rules that never ran. Pinning the toolchain to stable 1.97.1 this release made "run it under nightly occasionally" stop being a plan at all. They are gone, replaced by the exact command to apply them deliberately (cargo +nightly fmt --all -- --config …). Same class of problem as the typos.toml that had no runner, in the other direction.

  • Two configured-but-unexecuted quality tools became CI gates, and the toolchain is pinnedtypos.toml had been in the repository fully configured, down to ignore patterns for UUIDs and commit hashes, with nothing ever running it; a config with no runner is decoration. Its first run produced 73 findings and every one was a false positive: HashiCorp, the flate2 and writeable crate names, Ásbrú's own Parrallels wire-format spelling, the bottons field vnc-rs misspells upstream, a deliberate prodction in the CLI docs demonstrating the "Did you mean" suggestion, and base64 certificate fixtures. The vocabulary was therefore recorded with a reason per entry instead of "correcting" the code — several of those corrections would have introduced bugs. cargo machete joined it and found one genuinely dead dependency (see Dependencies). Both now run in a new toolchain-free hygiene job together with a gate asserting that the copies of the pinned toolchain version agree across rust-toolchain.toml and both workflows. The toolchain is pinned in rust-toolchain.toml, so a new stable release can no longer turn CI red with no change to this repository — the project already carried an escape hatch for that exact failure, which is the evidence it had happened. MSRV is untouched and still 1.95: the msrv job, the MSRV-built RPM and the snap each override the pin with RUSTUP_TOOLCHAIN and now assert the compiler they actually got, because a pin that silently overrides a deliberately older toolchain is worse than no pin at all.

Startup

  • The X11 Cairo fallback no longer re-execs the process — the #85 workaround used to set GSK_RENDERER=cairo by replacing the process image with a copy of itself carrying the variable, because std::env::set_var is unsafe in edition 2024. That is unavailable on macOS, where an exec() destroys the LaunchServices scene registration NSStatusItem needs and the tray icon disappears — which is why the guest-VM case above had no fix until the write moved in-process. The environment write now lives in a new rustconn-env-sys, the third sanctioned FFI crate, guarded exactly like rustconn-locale-sys: set_startup_var refuses to run from any thread but the one main() started on, refuses once the process has spawned a thread of its own (counted from /proc/self/task where the OS offers it), and refuses after seal_env() closes the startup window. Both platforms now share one renderer decision in rustconn/src/renderer.rs, and startup spawns one process fewer than it did. The crate is an unconditional workspace member rather than a macOS-only dependency on purpose: no CI job builds macOS, so gating it would leave the workspace's newest unsafe block compiled by nothing.

  • The single-key config.toml scan has one implementation instead of two — the language and the renderer are both needed before GTK exists, long before the application's settings are loaded, and each was reading its own key out of the file with its own hand-rolled scan. They now share rustconn/src/startup_config.rs, which is tested rather than assumed: it no longer confuses a longer key with the same prefix (renderer_debug for renderer), it stops a value at its closing quote so a hand-added trailing comment is not read as part of the setting, and a value that is not quoted at all reads as unset rather than aborting the scan.

Code structure

  • terminal/mod.rs split into three modules, shrinking it by 30% — the 4365-line file has been divided along lifecycle lines. Note what this is and is not: the file was split, not the type. TerminalNotebook still has 156 methods, now spread over three files with the moved ones widened from private to pub(super), so coupling did not go down — it became visible. The god object is the per-tab state the methods share, and splitting it out is the next step, recorded as a // ponytail: note at the top of mod.rs.

    • tab_lifecycle.rs (889 lines): welcome tab creation, terminal/VNC/RDP/Web tab creation, tab parking for split view, tab restore, widget reparenting
    • session_lifecycle.rs (500 lines): reconnect preparation, VTE reset with history preservation, disconnect/connect status indicators, reconnect banner UI, poll cancellation, font refresh after fontconfig changes
    • mod.rs reduced to 3052 lines (−1313 lines, −30%)

    Every method moved verbatim; only the visibility widened from private to pub(super). No public API and no behaviour changed.

  • adw::Spinner where the runtime has it, gtk4::Spinner where it does not — decided in one place — libadwaita 1.6 introduced AdwSpinner, and the six construction sites each carried their own #[cfg(feature = "adw-1-6")] pair to choose between it and GtkSpinner. The choice now lives in crate::spinner, which hands out a Spinner type alias, so no call site mentions the feature. Nothing outside that module mentions spinning either: AdwSpinner animates whenever it is mapped and cannot be stopped, so the fallback hands out a GtkSpinner that is already spinning, and GTK advances a CSS animation only for a mapped widget — which makes "show it" the way to start a spinner and "hide it" the way to stop one on both paths.

    The adw-1-6 feature stays opt-in and out of default, so the workspace baseline remains libadwaita 1.5. Ubuntu 24.04 ships 1.5.0 and the snap's core24 gnome-46-2404 platform ships 1.5, and libadwaita-sys turns a version feature into a hard system-deps failure in the build script rather than an unsatisfied dependency — so raising the baseline breaks those builds with an error that names pkg-config, not RustConn. Flatpak and the OBS tiers that have 1.6 or newer already pass adw-1-7/adw-1-8, which now imply adw-1-6 and therefore get AdwSpinner. Retire the feature once no supported target is below 1.6.

  • One CSS rule for the floating viewer toolbar instead of two identical copies.web-toolbar-overlay was a byte-for-byte duplicate of .rdp-toolbar-overlay, which meant a change to one silently stopped applying to the other. They now share one selector for their geometry and differ in the single declaration that has to (see Fixed: the shared background was wrong for Web).

Packaging

  • packaging/obs/rustconn.dsc said less than the file OBS actually builds from — it named neither libasound2-dev nor gettext, and until this release named no libadwaita at all. It is also not read by anything: scripts/obs-publish.sh rewrites debian.dsc only. Its Build-Depends is now identical to debian.dsc's, and which of the two is live is written down in packaging/obs/README.md — a .dsc is strict deb822 with no comment syntax, so the note cannot live in the file that needs it.

  • Build dependencies state the versions the crate features actually requirelibadwaita-1-dev (>= 1.5) and libvte-2.91-gtk4-dev (>= 0.76) in debian/control, packaging/obs/debian.control, debian.dsc and rustconn.dsc, and the matching pkgconfig() floors in rustconn.spec. rustconn.dsc did not name libadwaita at all. These mirror the v1_5 and v0_76 feature selections, so a too-old system now fails in the dependency solver with a legible message instead of inside a build script.

Credential handling

  • The password dialog stopped making an extra copy of the password to zeroize — 0.20.0 development wrapped the entry's text in Zeroizing before handing it to SecretString, which allocated a second plaintext copy and then scrubbed the copy it had just made: SecretString already owns its Box<str> and zeroizes it on drop, and the one plaintext that does escape unscrubbed is the GString the entry returns, which GTK frees without zeroing and which no Rust-side wrapper can reach under unsafe_code = "forbid". The intermediate is gone, so the dialog holds one copy where it briefly held two.

Documentation

  • How to verify a downloaded release artifact is now written down — the build-provenance attestation described above existed only in this file, which meant nobody downloading a .deb or an AppImage would learn that gh attestation verify applies to it. docs/INSTALL.md gains a section with the command, the offline variant via gh attestation download --bundle, and what is not covered (the snap, which the Snap Store signs itself, and anything installed from Flathub, OBS, AUR, nixpkgs or Homebrew, which those repositories sign). SECURITY.md states what a pass proves and, more usefully, what it does not: provenance answers "who built this, from what source", never "is this source trustworthy". README.md points at both.

  • docs/ARCHITECTURE.md stopped describing a guard rule that 0.19.21 removed — three places, plus the crate table in steering, still said the setlocale guard "refuses once the process has a second thread". That is precisely the rule that aborted RustConn at startup on Fedora 44 (#271) and was replaced by baseline-growth, and 0.19.21's notes claimed the documentation had been corrected — it had been, in deny.toml, .cargo/audit.toml and the crate docs, but not here. Worse, the new rustconn-env-sys paragraph in this release restated it for a crate that never behaved that way. Both paragraphs now describe the baseline rule, say that the thread count is a Linux-only check, and say plainly that this one clause is a judgement rather than a proof. The dependency diagram is also redrawn: after rustconn-env-sys was added to it, the arrow into the -sys block appeared to come from rustconn-cli, contradicting the caption directly beneath it.

  • The floating toolbar is documented, including how to switch it off and what that costsdocs/USER_GUIDE.md gains the Session Toolbar switch under RDP (with the list of actions that have no keyboard route, Ctrl+Alt+Del first), a pointer to it from VNC, the Navigation Toolbar switch under the embedded browser next to the keyboard routes that survive it, and a note in Split View that the panel's corner indicator goes with it while the right-click menu does not. docs/CLI_REFERENCE.md gains --vnc-toolbar and --web-toolbar. The Web section's "secondary buttons are hidden below 500px" was also corrected: they are reparented into the "⋯" menu rather than hidden, and the threshold has been measured rather than fixed since the overflow rewrite in this release.

  • The unsafe policy is documented as what it now isunsafe_code = "deny" plus a crate-level expect in each helper, with the reason forbid was given up written down so it is not "tightened" back. Updated in docs/ARCHITECTURE.md, AGENTS.md, both steering files, the power's reference table and the crate-boundary guard hook, whose description had also never been updated to mention rustconn-env-sys.

Dependencies

  • Updated: http-body-util 0.1.4→0.1.5. The GTK stack is already at the newest published bindings (gtk4 0.11.4, libadwaita 0.9.2, vte4 0.10.0, webkit6 0.6.1), and their version features cannot rise while Ubuntu 24.04 (libadwaita 1.5.0, VTE 0.76.0) and snap core24 are supported targets.

  • Removed: tracing-subscriber as a direct dependency of rustconn-core. It had no reference anywhere in that crate — the subscriber is installed by the application entry points, which declare it themselves, and rustconn-core/src/tracing/mod.rs said as much in its own doc comment. Found by cargo machete on its first run. A library pulling in a subscriber is also an invitation to end up with two in one process, so it is documented as "do not re-add" rather than merely deleted. md-5, vnc-rs, gettext-rs and native-tls are declared as machete exceptions in the same pass: the first three are used under an import path that differs from the package name, and native-tls is present only to pin a version away from 0.2.17's Tlsv13 compile bug.

  • Updated: http-body-util 0.1.4→0.1.5, clap_mangen 0.3.2→0.3.3 (rustconn-cli's man-page generator, build-time only), font-types 0.12.2→0.12.3 (transitively, through read-fonts for harfrust and skrifa). The GTK stack is already at the newest published bindings (gtk4 0.11.4, libadwaita 0.9.2, vte4 0.10.0, webkit6 0.6.1), and their version features cannot rise while Ubuntu 24.04 (libadwaita 1.5.0, VTE 0.76.0) and snap core24 are supported targets.

    Both Flatpak and Flathub cargo-sources.json manifests were regenerated for the two patch bumps. That is not optional bookkeeping: those files list every crate by exact version and digest, so a Cargo.lock ahead of them makes flatpak-builder vendor crates the build then cannot find. 0.19.22 had to catch up on a resvg bump that landed without them, which is the same omission one release earlier.

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.20.0_amd64.deb
sudo apt-get install -f  # Install dependencies if needed

Fedora (.rpm from this release)

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

AppImage

chmod +x RustConn-0.20.0-x86_64.AppImage
./RustConn-0.20.0-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