Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Versions follow [Semantic Versioning](https://semver.org/).
- **Watch a host by a port check instead of an SSH login.** Firewalls, switches and other appliances answer SSH but have no shell to read `top` or `free` from, so monitoring could only ever fail on them — and log in again every cycle to find that out. A host can now be set to **TCP port check**: OmnySSH opens a connection to the port and closes it, with no login and no commands. Its card shows reachable / unreachable in place of the metric tiles, rather than tiles for numbers nobody collected. Set it in the host form — `tcp` (the host's SSH port) or `tcp:PORT` in the terminal app, a dropdown in the desktop app. Existing hosts are untouched and stay on SSH monitoring. ICMP is not offered yet: unprivileged ping is unavailable on the Linux packaging most people use.

### Bug Fixes
- **The desktop app reopens at the size and position you left it.** Every launch reset the window to 1100x720 wherever the system chose to put it, so a window sized to your screen — or moved to a second monitor — had to be set up again each time. Size and position now persist. Visibility deliberately does not: the window still starts hidden and appears once the interface has painted, so restoring geometry never brings back the blank frame at launch.
- **macOS: the window buttons no longer sit across the edge of a collapsed sidebar.** The red/amber/green cluster is positioned by the system against the window, not by the app's layout, and it is wider than the collapsed sidebar was — so the sidebar's edge fell on the green button and left it over the content area while the other two stayed on the sidebar. The collapsed sidebar is now wide enough to hold the whole cluster on macOS. Windows and Linux draw their own title bar and keep the narrower one.
- **Hosts split across `Include` files are imported.** A relative pattern — `Include conf.d/*.conf`, the form nearly every split-config guide prints — was looked for in whatever directory the app happened to be launched from, which for the desktop app started from Finder or the application menu is `/`. Nothing matched, so every host defined in `~/.ssh/conf.d` was missing and had to be added by hand. Those patterns now resolve against `~/.ssh`, the way `ssh` itself resolves them. Four things in the same code path were fixed alongside it: full glob patterns work (`?`, `[abc]`, a wildcard in a directory name, and more than one `*` in a file name), several pathnames on one `Include` line are all read instead of none, a quoted path keeps its spaces, and an `Include` written inside a `Host` block no longer swallows that host's remaining settings. An `Include` that matches nothing is written to the log rather than passing in silence.
- **CPU no longer reads ~91–100 % on an idle server whose system language isn't English.** Monitoring reads the idle percentage from `top` and shows the rest as used. On a server set to a language with a comma decimal separator, `top` prints `99,1 id`, and the parser split that line on commas — reading the idle value as `1` and reporting 99 % used on a machine doing nothing. The same servers could show RAM and Disk as N/A, because `free` and `df` translate the labels being looked for, and the process list could come back empty. The monitoring commands now run in a fixed locale, and the CPU parsers understand a decimal comma on their own for hosts where that cannot be set. Process names keep the server's own character set.
- **A host that answers SSH but not shell commands is no longer re-logged-in every 30 seconds.** Network appliances — firewalls, switches — authenticate fine but cannot run `top` or `free`, and a round of failed metric commands was treated as a dead connection. The retry delay was reset on every successful login, so it never grew past its first step: one login every 30 seconds, indefinitely, and as often as every 10 seconds in the desktop app, whose refresh timer also cut the retry delay short. The delay now escalates properly and is left alone by the refresh timer — the trade-off being that a manual refresh no longer cuts a retry delay short, so a host that has been unreachable for a while retries on its own schedule (at most five minutes). Separately, a device that ignores SSH keepalives was being disconnected after 30 seconds even while its commands still worked; connections are now held open, with liveness still bounded by the keepalive limit.
Expand Down
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/omnyssh-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ tauri-plugin-updater = "2"
# Opens external URLs (the GitHub repo + Telegram channel from the support dialog) in
# the user's default browser; the webview itself must never navigate away from the app.
tauri-plugin-opener = "2"
# Restores the window's size and position between launches. Registered Rust-side only,
# so none of its commands are granted to the frontend.
tauri-plugin-window-state = "2"
# `generate_context!` embeds the `plugins.updater` config and expands to serde_json at
# runtime; also asserts the wire form of DTOs in tests (e.g. HostDto never carries a password).
serde_json = "1"
Expand Down
30 changes: 30 additions & 0 deletions crates/omnyssh-gui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use omnyssh_core::ssh::pty::PtyManager;
use state::GuiState;
use tauri::webview::PageLoadEvent;
use tauri::Manager;
use tauri_plugin_window_state::StateFlags;
use tauri_specta::{collect_commands, collect_events, Builder};

// Absolute at build time, so the export target is independent of the run CWD.
Expand Down Expand Up @@ -57,6 +58,28 @@ const _: () = assert!(RENDER_HEAL_DEADLINE.as_secs() > REVEAL_FALLBACK.as_secs()
#[cfg(target_os = "linux")]
const RETRY_MARKER: &str = "OMNYSSH_SOFTWARE_RENDER_RETRY";

/// What the window-state plugin is allowed to restore. Geometry only: it applies every
/// flag from `on_window_ready`, before the page exists, and only sizing and moving leave
/// a hidden window hidden. `VISIBLE` shows it outright, undoing the reveal that keeps the
/// blank webview off screen; `MAXIMIZED` reaches `ShowWindow(SW_MAXIMIZE)` on Windows,
/// which carries no visibility guard of its own; `FULLSCREEN` touches the same hidden
/// window; `DECORATIONS` re-derives the macOS style mask the overlay title bar needs.
///
/// Dropping `MAXIMIZED` costs a maximised window its position: the plugin records a move
/// without checking for one, so the maximised origin becomes the saved position, and the
/// pre-maximise origin it keeps alongside is read back only for a window it also restores
/// maximised. Such a window reopens at its own size in the corner of the display.
const WINDOW_STATE_FLAGS: StateFlags = StateFlags::SIZE.union(StateFlags::POSITION);

// The reveal is the only path to a visible window — the app has no tray icon — so the
// exclusions above are too load-bearing to live in prose alone.
const _: () = assert!(!WINDOW_STATE_FLAGS.intersects(
StateFlags::VISIBLE
.union(StateFlags::MAXIMIZED)
.union(StateFlags::FULLSCREEN)
.union(StateFlags::DECORATIONS)
));

/// The single definition of the IPC surface. Shared by `main` (dev export +
/// wiring) and the drift test so they can never disagree.
fn specta_builder() -> Builder<tauri::Wry> {
Expand Down Expand Up @@ -195,6 +218,13 @@ fn main() {
.plugin(tauri_plugin_updater::Builder::new().build())
// Opens the support dialog's GitHub/Telegram links in the default browser.
.plugin(tauri_plugin_opener::init())
// Restores the window's size and position between launches; the flags keep it
// away from everything that would touch the window itself (WINDOW_STATE_FLAGS).
.plugin(
tauri_plugin_window_state::Builder::default()
.with_state_flags(WINDOW_STATE_FLAGS)
.build(),
)
.invoke_handler(builder.invoke_handler())
// The window is created hidden (tauri.conf.json `visible: false`) so the
// launch never shows the webview's blank base colour; reveal it once the
Expand Down
14 changes: 14 additions & 0 deletions crates/omnyssh-gui/tests/startup_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ fn a_loaded_page_is_recorded_for_the_render_retry() {
);
}

/// Window geometry is restored by a plugin whose default flag set includes `VISIBLE`,
/// which it applies from `on_window_ready` — before the page exists. Fall back to those
/// defaults and every launch shows the window over a blank webview again, undoing the
/// reveal. `WINDOW_STATE_FLAGS` carries a compile-time guard of its own; this catches
/// the other half, a call site that stops using it.
#[test]
fn window_state_restores_geometry_but_never_visibility() {
assert!(
MAIN_RS.contains(".with_state_flags(WINDOW_STATE_FLAGS)"),
"the window-state plugin no longer takes its flags from WINDOW_STATE_FLAGS — \
the default set includes VISIBLE and would reveal the window before it painted"
);
}

/// The rpm bundler writes `Requires:` from this list and nothing else — it never scans
/// the binary — so an empty list ships a package that installs onto a system with no
/// webview and then dies at launch. Sonames, not package names: the package providing
Expand Down
6 changes: 6 additions & 0 deletions crates/omnyssh-gui/ui/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,15 @@
app.html pre-paint script so there is no layout shift. */
:root {
--titlebar-h: 0px;
/* Collapsed sidebar width. The macOS traffic lights are anchored to the window, not
to the layout, and the cluster runs to x=69px — past a 3.5rem rail, whose right
border lands on the green button's left edge and leaves it over the content pane.
5rem holds the whole cluster; the other platforms have no lights to clear. */
--rail-w: 3.5rem;
}
:root[data-os='macos'] {
--titlebar-h: 1.75rem;
--rail-w: 5rem;
}

html,
Expand Down
2 changes: 1 addition & 1 deletion crates/omnyssh-gui/ui/src/lib/components/AppShell.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

<div
class="relative grid h-screen grid-rows-[1fr_auto] overflow-hidden bg-bg text-fg transition-[grid-template-columns] duration-200 ease-out {$sidebarCollapsed
? 'grid-cols-[3.5rem_1fr]'
? 'grid-cols-[var(--rail-w)_1fr]'
: 'grid-cols-[15rem_1fr]'}"
>
<!-- Draggable strip under the macOS overlay traffic lights; zero-height elsewhere
Expand Down
4 changes: 3 additions & 1 deletion crates/omnyssh-gui/ui/src/lib/components/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@
>
<button
type="button"
class="flex min-w-0 flex-1 items-center gap-2.5 rounded text-left {focusRing}"
class="flex min-w-0 items-center gap-2.5 rounded text-left {focusRing} {$sidebarCollapsed
? ''
: 'flex-1'}"
title={sessionTitle(s)}
aria-label={sessionTitle(s)}
aria-current={active ? 'true' : undefined}
Expand Down
Loading