Skip to content

v0.19.20

Choose a tag to compare

@github-actions github-actions released this 11 Aug 18:31
· 6 commits to main since this release

What's Changed

Added

  • SSH connections can choose what Backspace and Delete send (issue #271) — the reporter has devices that do not recognise the codes RustConn sends and need Ctrl+H for Backspace, and asked for the switch the Telnet panel already has. The SSH panel of the connection editor gains the same Keyboard group: Backspace and Delete can each send Automatic, Backspace (^H) or Delete (^?) — the labels name the control character, and the byte each one puts on the wire (0x08, 0x7F) is documented on the enum variants rather than shown in the dropdown. It is a property of the terminal rather than of the ssh command, so it is applied to the session's VTE widget after the tab's terminal settings — which is also why it is re-applied on reconnect, where the same terminal is reused. Automatic is the previous behaviour (^? for Backspace, the VT220 \e[3~ for Delete), so stored connections are unaffected and the field defaults on configurations written by earlier releases; neither choice ever hands the decision back to VTE, which would abort the process for want of a PTY to read VERASE from (issue #247). The two option types are shared with Telnet rather than duplicated — TelnetBackspaceSends/TelnetDeleteSends are now BackspaceSends/DeleteSends, since the remote side's disagreement about the erase byte is not specific to either protocol.

  • MOSH connections get the same Keyboard group (issue #271) — MOSH draws into the same VTE widget as SSH and Telnet, so a host that wants ^H wants it over MOSH too. MoshConfig gains backspace_sends/delete_sends with the same Automatic default, applied both when the session starts and when it reconnects in place. The three protocols now read their pair through one ProtocolConfig::erase_modes(), so the terminal side has a single place to ask rather than a match arm per protocol at every call site.

Fixed

  • Saving Preferences threw away the Backspace/Delete choice on a live session (issue #271) — the setting is installed on the session's VTE widget, and TerminalNotebook::apply_settings reinstalls the global erase bindings over every open terminal when the Terminal page is saved. So opening Preferences and clicking Save on an unrelated setting silently put the session back to ^? until the next reconnect — the same shape as the per-connection theme override that issue #99 fixed, and it now gets the same treatment: reapply_erase_modes runs right after apply_settings and restores each tab's pair from its connection.

  • SFTP connections offered a Keyboard group that did nothing and could overwrite the stored value (issue #271) — SFTP reuses the SSH options tab, so it inherited the new group, but an SFTP session opens a file manager or mc rather than a terminal that applies erase modes. Worse, the shared dropdowns sit at whatever the last-shown protocol left them at, so saving an SFTP connection could write another protocol's choice into it. The group is now hidden for SFTP by apply_general_field_visibility, and build_sftp_config writes the value the connection was loaded with, kept aside in sftp_erase_modes, instead of reading the widgets.

  • RDM JSON import still aborted on an integer field, and one bad entry cost the whole file (issue #234) — a third report of invalid type: integer 25, expected a string. 0.19.8 and 0.19.13 made ConnectionType, Port and every other string field tolerant, but Name was left as a plain String on both connection and folder entries, so an entry named after a number was the one remaining field that could still produce exactly that message; ViewOnly had the same problem with 0/1 under a different message. Chasing fields one at a time cannot end, because RDM's add-on architecture lets any add-on contribute entry fields and there is no schema that covers every export — so the structural cause is fixed too: entries are now decoded one by one instead of the whole document in a single serde_json::from_str, and an entry that still cannot be read is reported in the skipped list while the rest of the file imports. It is labelled by its Name when it has one, and by its position (Connection #3) when it does not — which is in practice the only remaining case, since every field now has a tolerant deserializer and the one thing left that can fail to decode is an array element that is not an object at all, and such an element has no name to report. Name and ViewOnly accept any scalar form, and a Credentials value that is not an object no longer maps onto the credential fields by position. Port is now parsed by one parse_port_text for both the quoted and the unquoted form: it accepts the 3389.0 that .NET serializers write for an integral value, which previously failed to deserialize and — because the document was decoded in one serde_json::from_str — took the whole file down with it. An out-of-range value was never that destructive: the old u16::try_from(i64) path already yielded the protocol default. What changed for it is that the rejected value is now visible as a warning naming the connection and the raw text, instead of a silent fallback. A bare array of entries and a single entry from RDM's Clipboard > Copy are accepted alongside the usual {"Connections": [...]} export, and a JSON file that is not an RDM export now says so rather than reporting a successful import of nothing.

  • Royal TS Telnet sessions were imported as SSH on port 22 (issue #234) — Royal TS has no Telnet object: its Terminal object is RoyalSSHConnection for Telnet, SSH, RAW, rlogin and serial alike, and its own ConnectionType field (telnet;Telnet, ssh;SSH) picks the protocol. That field was never read, so every Telnet session became an SSH connection on the SSH port. Telnet sessions now import as Telnet with a default port of 23; RAW, rlogin and serial have no RustConn equivalent and stay on the object's default. Object element names are also matched case insensitively, and a connection whose target host is under ComputerName, HostName or Host rather than URI is imported instead of being skipped as "Missing host".

  • Royal TS import gave no hint why every connection came in without a password (issue #234) — passwords genuinely cannot be imported, and that is a property of the format rather than a gap in RustConn: Royal TS never stores a password in clear text, encrypting it under the document's encryption password when one is set and under a key built into the application when it is not. The importer already marked such connections "prompt for password" but said nothing, leaving the user to discover the empty password on the first connect. The import result now carries a warning naming the limitation, the import dialog renders warnings at all (ImportResult::warnings was populated by some importers and never displayed), and docs/USER_GUIDE.md explains the limitation and the two ways around it.

  • Import warnings were permanently English, and the list mixed translated and untranslated text (issue #234)ImportResult::warnings was a Vec<String> filled in by importers in rustconn-core, which po/update-pot.sh does not scan, so every warning reached the dialog as a finished English sentence no locale could touch. Rendering them next to the dialog's own translated headings produced a result page in two languages at once. The warnings are now a typed ImportWarning enum carrying the reason and its arguments; ImportDialog::format_warning matches on the variant and calls i18n_f with a literal xgettext can extract, while ImportWarning::message returns that same literal so the two cannot drift and Display still renders English for the CLI, logs and tests.

  • An import that produced nothing claimed it had "Successfully imported 0 connection(s)" (issue #234) — with warnings displayed for the first time, the summary line above them turned out to be wrong in exactly the case that now matters: nothing imported and nothing failed is neither a success nor an error, yet the success wording was printed anyway, directly above a list explaining why nothing arrived. That case now says "Nothing was imported. See the {} warning(s) below."

  • Royal TS entries with their own encrypted password were left with neither a password nor a prompt (issue #234) — the prompt fallback keyed off the assigned credential, so an object that carried its own <Password> element and no credential kept PasswordSource::None. Since the ciphertext cannot be decrypted from outside Royal TS, that connection had no password and never asked for one. Such objects now get PasswordSource::Prompt like the rest and are counted in the "passwords cannot be imported" warning.

  • Several RDM shapes were guessed at instead of reported (issue #234) — an empty ConnectionType was silently turned into RDP, which is RDM's documented default only for an absent field; a blank one names no protocol and is now listed as skipped, while an absent one still means RDP. Port 0 and "0" are rejected alongside out-of-range values, because zero means "any free port" to a listener and nothing at all to a client. And looks_like_entry now requires ConnectionType plus one of ID/Name before treating a top-level object as a single copied entry: {"Name": …, "Host": …} is the shape of countless unrelated inventory files, and reading one as a one-connection import was worse than saying the file is not an RDM export.

  • The KeePass database password was never written to the system keyring (issue #272) — with KeePassXC configured and its unlock password set to "System keyring" storage, the password never reached the keyring: Seahorse showed no entry and every start logged KeePass password not found in system keyring. The keyring key rename in 0.19.18 was not the cause — storing and reading both use rustconn/kdbx-password, and legacy entries still migrate on first retrieval. The break came from 0.19.17, which moved the keyring write out of the settings-collect phase into a deferred save_pending_keyring_credentials() that runs after the dialog closes. That function reads the password from the collected SecretSettings, but collect_secret_settings() returns kdbx_password: None for keyring storage by design — it must not write an encrypted blob to disk in that mode — so the deferred save found nothing to store and silently did nothing. The collect step now carries the typed password as the runtime-only SecretString it already is (#[serde(skip)], so still never serialized) and continues to leave the encrypted blob empty; AppState::update_settings correspondingly stops encrypting that password to disk when keyring storage is selected (through SecretSettings::apply_storage_persistence, see Improved), so the secret is never duplicated against the user's explicit choice. The password also reaches the secret manager immediately, so the database unlocks without waiting for a restart.

  • Re-entering a vault password in Settings was discarded as "no change" (issue #272) — the second half of the same report: on an already-configured backend, typing the password and closing the dialog logged Settings unchanged — skipping save and did nothing. The dirty check added in 0.19.17 compares the collected settings against a snapshot taken when the dialog opened, and PartialEq for SecretSettings deliberately ignores the six runtime-only SecretString fields because they are not persisted. A freshly typed password is invisible to that comparison, yet for a keyring-backed backend the save path is the only thing that hands it to the keyring — so the one case where the password mattered most was the one that skipped the save. The check now also asks SecretSettings::has_new_runtime_secret, which reports a runtime secret that is newly present or different. A field left None never counts, so an untouched open/close round trip stays the no-op it was meant to be.

  • Bitwarden, 1Password and Passbolt had the same keyring hole as KeePass (issue #272) — the report named KeePass, but the deferred keyring save reads whatever collect_secret_settings put in SecretSettings, and the CredentialStorage::SystemKeyring arm of all four backends returned nothing for the same reason. So a Bitwarden master password, a 1Password service-account token or a Passbolt GPG passphrase set to keyring storage was collected as None and never stored either. Each of those arms now carries the runtime-only SecretString as well, so the fix covers the backend the reporter used and the three that would have produced the next three reports.

  • 1Password and Passbolt wrote a placeholder literal to disk instead of the encrypted secret (issue #272) — with encrypted-file storage selected, the collect step fell back to the strings encrypted_token_placeholder and encrypted_passphrase_placeholder when no ciphertext was at hand, and those literals were then persisted as if they were the secret. The next read decrypted a placeholder, so the backend behaved as though it had never been configured while the config file looked populated. Found while auditing the four SystemKeyring arms above.

  • Moving a secret from an encrypted file to the system keyring destroyed it (issue #272) — switching the storage of an already-configured backend without retyping the password left the new keyring entry unwritten and cleared the encrypted blob, because the collected settings had neither the ciphertext (storage changed) nor a freshly typed secret. SecretSettings::carry_over_runtime_secrets now brings the in-memory secret forward from the previous settings, so the switch re-homes the existing password instead of losing it.

  • Turning a backend off, or moving its secret elsewhere, left the old keyring entry behind (issue #272)delete_kdbx_password_from_keyring and its three siblings existed in rustconn-core::secret but had no caller anywhere in the GUI, so a password stayed in GNOME Keyring or KWallet after the user disabled the backend or moved its secret to an encrypted file — the opposite of what changing that setting is for. SecretSettings::keyring_revocations compares the saved settings against the previous ones and reports which entries are now stale; revoke_stale_keyring_credentials deletes them as part of the same deferred save that writes the new ones.

  • A failed keyring write was a log line under a "Settings saved" toast (issue #272) — when the deferred save could not reach the keyring, it emitted tracing::warn! and nothing else, so the user saw a success toast for a credential that had not been stored. With keyring storage there is no encrypted blob on disk to fall back on, so that credential exists in memory only and is gone after a restart: losing the message loses data, which the GNOME HIG error-feedback rule puts in a modal dialog. The failure now raises alert::show_error explaining that the credential is session-only and what to check, and KeyringGaps tracks which backends are still missing an entry so the user can retry the save once the keyring is unlocked instead of having to guess which one failed.

Changed

  • RUSTSEC-2026-0244 is fixed rather than accepted: setlocale moved into a new rustconn-locale-sys crate (issue #267) — the advisory reports gettext-rs setlocale as unsound in a multi-threaded program, and 0.19.19 answered it by removing the one call site that ran after threads existed and recording the analysis in deny.toml. That left the fixed gettext-rs 0.8.0 unusable, because its fix is to mark setlocale unsafe and every main crate sets unsafe_code = "forbid". The call now lives in rustconn-locale-sys, the second sanctioned FFI crate alongside rustconn-pty-sys, so gettext-rs is on 0.8 and the advisory is off the ignore lists in both deny.toml and .cargo/audit.tomlcargo deny check passes without suppressing it.

    The new crate does more than relocate the unsafe: it turns the precondition into something checked. init_locale refuses to call setlocale if the process already has a second thread (counted from /proc/self/task on Linux), if an earlier call came from a different thread, or after seal_locale() has closed the startup window — and i18n::apply_language_from_config() seals it on every path that returns. So the invariant the previous release could only document and hand-verify by reading main() is now enforced: a setlocale call added to a running application panics with an explanation on the first run instead of corrupting locale state. The guard is a testable type, since the FFI call itself is deliberately unreachable from the multi-threaded test harness. User-visible behaviour is unchanged, including the Flatpak LANGUAGE/LC_MESSAGES handling from issue #158.

  • A JSON file that is not an RDM export is a parse error, not an empty import (issue #234) — the visible behaviour change from the import rework, called out separately because it is the one that can turn a previously "successful" import into a failure. {}, 42, {"servers": …} and {"Name": …, "Host": …} now return ImportError::ParseError naming what RDM export was expected, where before they were read as a document with no connections and reported as a successful import of nothing. An export with an empty list — {"Connections": []} — is still valid and still imports zero connections, because there the file really is an RDM export that happens to be empty.

Removed

  • locale_is_sealed() (issue #267) — a public predicate on rustconn-locale-sys with no caller. Whether the startup window is still open is init_locale's business, and it already refuses and says so; exposing the flag only invited a caller to check it and then race it.

  • ImportResult::record_warning (issue #234) — took an impl Into<String>, which is exactly what made untranslatable warnings easy to add. ImportResult::add_warning takes an ImportWarning and is the only way in, so a warning cannot be expressed as free text. (ImportStatistics::record_warning, a different type, is untouched.)

Improved

  • One derivation of the erase-mode pair instead of one per call site (issue #271)ProtocolConfig::erase_modes() returns the (BackspaceSends, DeleteSends) pair for SSH, Telnet and MOSH and Automatic for everything else, so the connect path, the in-place reconnect path and the Preferences re-apply path all ask the same question of the same function. Adding a fourth terminal protocol is a match arm in one place rather than three copies to keep in step.

  • One rule for what is written to disk, instead of a per-backend block in AppState::update_settings (issue #272) — the "do not persist an encrypted blob when the keyring holds it" logic had grown asymmetrically: KeePass had it, the other three did not, which is how the placeholder-literal bug above survived. SecretSettings::apply_storage_persistence states the rule once and applies it to all four backends, and update_settings calls it instead of reimplementing part of it.

  • init_locale is #[must_use], and its platform limits are written down (issue #267) — a discarded None meant the locale had quietly not applied, so the return value now has to be handled or explicitly ignored. Two things the guard cannot do are stated in the SAFETY comment and the crate docs rather than left to be inferred: the thread count comes from /proc/self/task and no equivalent is implemented for macOS even though RustConn ships macOS builds, and the advisory's "no POSIX signal handlers" clause is not checkable at all — the Rust runtime installs handlers before main, so the condition is already false in every Rust process.

  • crate-boundary-guard.sh no longer depends on the checkout being called RustConn (issue #267) — the hook normalised paths by stripping */RustConn/, so a clone named rustconn/ or RustConn-fork/, or a cwd outside the repo, left the path absolute: the GUI-import invariant was skipped entirely and the unsafe invariant blocked legitimate edits to the sanctioned FFI crates. The greedy strip also cut the crate prefix off if /RustConn/ appeared again below the root, disabling both. It now asks git rev-parse --show-toplevel from the nearest existing ancestor of the target — the file usually does not exist yet, since the hook runs before the write — and falls back to the cwd prefix, keeping the fail-open contract. The unsafe pattern is the rustconn-*-sys crate-name shape rather than a hardcoded crate list, and the keyword list covers edition-2024 unsafe extern.

  • deny.toml and .cargo/audit.toml explain the removal without naming an unreleased version (issue #267) — the notes left where RUSTSEC-2026-0244 used to be ignored said it was dropped in 0.19.20, a version that does not exist yet and that the entry would outlive anyway. They now say the ignore was removed because the advisory was resolved rather than at a release boundary, and that re-adding it means something regressed.

Documentation

  • docs/ARCHITECTURE.md covers the five-crate workspace (issue #267) — the crate list, dependency graph and boundary table still described four crates and named rustconn-pty-sys as the place unsafe lives. They now include rustconn-locale-sys and describe the exception as the rustconn-*-sys shape, which is what the guard hook enforces.

  • docs/AI_DEVELOPMENT.md hook table matches the guard (issue #267) — the crate-boundary-guard row said unsafe outside rustconn-pty-sys; it now says outside a rustconn-*-sys crate.

  • docs/USER_GUIDE.md documents the Keyboard group, the Royal TS password limitation, RDM's tolerant parsing and keyring write failures (issues #271, #234, #272) — a new Backspace and Delete Behavior section with the three options and which host each suits, noting that MOSH has the same group and SFTP does not; MOSH and SSH rows in the protocol options table; why Royal TS passwords cannot be imported and the two ways around it; which scalar shapes the RDM importer accepts and that a file which is not an RDM export is now rejected outright; and, on the Secrets page, what happens when the keyring refuses a credential and when RustConn removes an entry it previously wrote.

Dependencies

  • Updated: gettext-rs 0.7.7→0.8.0 (see above; gettext-sys stays at 0.26.0, so no new system build dependency). The Flatpak and Flathub cargo-sources.json manifests were regenerated and also caught up with the resvg 0.47.0→0.48.1 bump that landed without them (adding base64 0.23.1, bytemuck_derive 1.12.0, font-types, harfrust, read-fonts, skrifa and dropping core_maths, rustybuzz, ttf-parser and the standalone unicode-* crates).

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

Fedora (.rpm from this release)

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

AppImage

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