From cfb52b0cfe52a147d6f7ef6338d7d3f342f2babc Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Tue, 28 Jul 2026 19:28:05 +0400 Subject: [PATCH 1/6] fix(gui): link release builds as a Windows GUI binary Without the subsystem attribute Windows allocates a console for the app and keeps it on screen next to the window for the whole session. --- crates/omnyssh-gui/src/main.rs | 4 ++++ crates/omnyssh-gui/tests/startup_contract.rs | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 crates/omnyssh-gui/tests/startup_contract.rs diff --git a/crates/omnyssh-gui/src/main.rs b/crates/omnyssh-gui/src/main.rs index 9b24c42..8ed3f87 100644 --- a/crates/omnyssh-gui/src/main.rs +++ b/crates/omnyssh-gui/src/main.rs @@ -1,3 +1,7 @@ +// Release builds link as a Windows GUI binary, so launching the app never opens a +// console window alongside it. Debug builds keep the console for cargo output. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + //! OmnySSH Desktop entry point. Wires the tauri-specta IPC boundary (commands + //! events), regenerates the TypeScript bindings in dev, spawns the core-event //! bridge, and boots the window (tech-gui.md §3.3–§3.4). diff --git a/crates/omnyssh-gui/tests/startup_contract.rs b/crates/omnyssh-gui/tests/startup_contract.rs new file mode 100644 index 0000000..77f11c7 --- /dev/null +++ b/crates/omnyssh-gui/tests/startup_contract.rs @@ -0,0 +1,16 @@ +//! Launch-time contract of the desktop app. These are link-time or native-window +//! settings that no runtime assertion can reach from a test binary (`cargo test` +//! always builds with `debug_assertions`), so they are guarded at their source. + +const MAIN_RS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/main.rs")); + +/// Without this attribute the binary links as a console app and Windows opens a +/// terminal next to the window for the whole session. +#[test] +fn release_builds_link_as_a_windows_gui_binary() { + assert!( + MAIN_RS.contains(r#"windows_subsystem = "windows""#), + "src/main.rs no longer declares windows_subsystem — release builds would \ + open a console window alongside the app on Windows" + ); +} From ac51250e05c512fddf0baaae2a300fb8e84897db Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Tue, 28 Jul 2026 19:29:15 +0400 Subject: [PATCH 2/6] fix(core): keep ssh-keygen from opening a console on Windows A GUI-subsystem parent has no console to lend the child, so key setup would pop one for the length of the run. --- crates/omnyssh-core/src/ssh/key_setup.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/omnyssh-core/src/ssh/key_setup.rs b/crates/omnyssh-core/src/ssh/key_setup.rs index c1692d5..ae412d0 100644 --- a/crates/omnyssh-core/src/ssh/key_setup.rs +++ b/crates/omnyssh-core/src/ssh/key_setup.rs @@ -288,6 +288,11 @@ pub async fn generate_key_pair(host_name: &str, key_type: KeyType) -> Result<(Pa .arg("-C") .arg(format!("omnyssh-{}", host_name)); // Comment + // CREATE_NO_WINDOW: the GUI has no console to lend the child, so without this + // Windows opens one for it. Output is piped either way, so nothing is lost. + #[cfg(windows)] + keygen_cmd.creation_flags(0x0800_0000); + let keygen_output = keygen_cmd .output() .await From 8b9f08c849fe9c349df7fcb48ce52d83bf7349a4 Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Tue, 28 Jul 2026 19:33:39 +0400 Subject: [PATCH 3/6] fix(gui): reveal the window only once the page has loaded The window used to appear before the webview had painted, so the first frame was its blank base colour. A fallback shows it anyway if the page stalls. --- crates/omnyssh-gui/src/main.rs | 25 +++++++++++ crates/omnyssh-gui/tauri.conf.json | 1 + crates/omnyssh-gui/tests/startup_contract.rs | 46 ++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/crates/omnyssh-gui/src/main.rs b/crates/omnyssh-gui/src/main.rs index 8ed3f87..31ea77c 100644 --- a/crates/omnyssh-gui/src/main.rs +++ b/crates/omnyssh-gui/src/main.rs @@ -25,12 +25,18 @@ use commands::update::{check_update, install_update, load_update_config, save_up use omnyssh_core::event::{CoreEvent, SessionId}; use omnyssh_core::ssh::pty::PtyManager; use state::GuiState; +use tauri::webview::PageLoadEvent; use tauri::Manager; use tauri_specta::{collect_commands, collect_events, Builder}; // Absolute at build time, so the export target is independent of the run CWD. const BINDINGS_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/ui/src/lib/bindings.ts"); +/// How long the hidden window may wait for the page before it is revealed anyway. +/// The app has no tray icon, so a frontend that never loads must not leave a +/// running process the user cannot see or reach. +const REVEAL_FALLBACK: std::time::Duration = std::time::Duration::from_secs(3); + /// 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 { @@ -117,9 +123,28 @@ fn main() { // Opens the support dialog's GitHub/Telegram links in the default browser. .plugin(tauri_plugin_opener::init()) .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 + // document — stylesheet included — is up. + .on_page_load(|webview, payload| { + if matches!(payload.event(), PageLoadEvent::Finished) { + let window = webview.window(); + let _ = window.show(); + // A window shown after build does not become key on its own everywhere. + let _ = window.set_focus(); + } + }) .setup(move |app| { builder.mount_events(app); + let reveal = app.handle().clone(); + tauri::async_runtime::spawn(async move { + tokio::time::sleep(REVEAL_FALLBACK).await; + if let Some(window) = reveal.get_webview_window("main") { + let _ = window.show(); + } + }); + let (engine_tx, engine_rx) = tokio::sync::mpsc::channel::(256); // The additive PTY raw-byte tap (§3.6): the manager mirrors each session's // bytes into `raw_tx`; a forwarder demuxes them into per-tab channels. diff --git a/crates/omnyssh-gui/tauri.conf.json b/crates/omnyssh-gui/tauri.conf.json index 90ed6de..b4cc089 100644 --- a/crates/omnyssh-gui/tauri.conf.json +++ b/crates/omnyssh-gui/tauri.conf.json @@ -18,6 +18,7 @@ "minWidth": 880, "minHeight": 560, "backgroundColor": "#171717", + "visible": false, "theme": "Dark", "titleBarStyle": "Overlay", "hiddenTitle": true diff --git a/crates/omnyssh-gui/tests/startup_contract.rs b/crates/omnyssh-gui/tests/startup_contract.rs index 77f11c7..0cec58b 100644 --- a/crates/omnyssh-gui/tests/startup_contract.rs +++ b/crates/omnyssh-gui/tests/startup_contract.rs @@ -2,6 +2,9 @@ //! settings that no runtime assertion can reach from a test binary (`cargo test` //! always builds with `debug_assertions`), so they are guarded at their source. +use std::path::Path; + +const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); const MAIN_RS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/main.rs")); /// Without this attribute the binary links as a console app and Windows opens a @@ -14,3 +17,46 @@ fn release_builds_link_as_a_windows_gui_binary() { open a console window alongside the app on Windows" ); } + +/// The window is revealed only once the page is up, so the launch never shows the +/// webview's blank base colour. The background is what the fallback reveal paints +/// when the frontend is slow, so it has to match the app's own dark surface. +#[test] +fn the_window_starts_hidden_on_the_dark_background() { + let config: serde_json::Value = + serde_json::from_str(&read(Path::new(MANIFEST_DIR).join("tauri.conf.json"))) + .expect("tauri.conf.json is valid JSON"); + let window = &config["app"]["windows"][0]; + + assert_eq!( + window["visible"], + serde_json::json!(false), + "the main window must be created hidden and revealed on page load" + ); + assert_eq!( + window["backgroundColor"].as_str().map(str::to_lowercase), + Some(dark_background_token()), + "the native window background drifted from the dark --bg token" + ); +} + +/// `--bg` of the dark theme — the single source of truth for the app's backdrop. +fn dark_background_token() -> String { + let css = read(Path::new(MANIFEST_DIR).join("ui/src/app.css")); + css.split_once(":root[data-theme='dark']") + .expect("app.css declares a dark theme block") + .1 + .split_once("--bg:") + .expect("the dark theme block declares --bg") + .1 + .split(';') + .next() + .expect("--bg is terminated") + .trim() + .to_lowercase() +} + +fn read(path: impl AsRef) -> String { + let path = path.as_ref(); + std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())) +} From a6334d146e6917a0ae858b51bb5844f88a512a27 Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Tue, 28 Jul 2026 19:35:16 +0400 Subject: [PATCH 4/6] chore(release): 1.1.1 --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 6 +++--- Cargo.toml | 2 +- crates/omnyssh/Cargo.toml | 2 +- doc/omny.1 | 4 ++-- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9c3676..4f761bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ Versions follow [Semantic Versioning](https://semver.org/). --- +## 1.1.1 — 2026-07-28 + +### Bug Fixes +- **Windows: the desktop app no longer opens a console window next to itself.** The GUI was linked as a console application, so Windows gave it a terminal and kept it on screen for the whole session. Release builds now link as a GUI binary. The same fix would have made one-click SSH key setup flash a console of its own while `ssh-keygen` runs, so that is suppressed too. macOS and Linux were never affected. +- **No more white flash when the desktop app launches.** The window used to appear before the webview had painted anything, so the first frame was blank white before the dark (or light) interface took over. The window is now created hidden and revealed once the page is up, with a fallback that shows it anyway if the interface is slow to load. + +--- + ## 1.1.0 — 2026-07-24 ### Features diff --git a/Cargo.lock b/Cargo.lock index 4bb9597..af0fc8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3172,7 +3172,7 @@ dependencies = [ [[package]] name = "omnyssh" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "chrono", @@ -3191,7 +3191,7 @@ dependencies = [ [[package]] name = "omnyssh-core" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "async-trait", @@ -3216,7 +3216,7 @@ dependencies = [ [[package]] name = "omnyssh-gui" -version = "1.1.0" +version = "1.1.1" dependencies = [ "chrono", "omnyssh-core", diff --git a/Cargo.toml b/Cargo.toml index 65f7b70..aa44d6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = ["crates/omnyssh-core", "crates/omnyssh", "crates/omnyssh-gui"] default-members = ["crates/omnyssh-core", "crates/omnyssh"] [workspace.package] -version = "1.1.0" +version = "1.1.1" edition = "2021" license = "Apache-2.0" repository = "https://github.com/timhartmann7/omnyssh" diff --git a/crates/omnyssh/Cargo.toml b/crates/omnyssh/Cargo.toml index 7be9875..7dd0458 100644 --- a/crates/omnyssh/Cargo.toml +++ b/crates/omnyssh/Cargo.toml @@ -17,7 +17,7 @@ name = "omny" path = "src/main.rs" [dependencies] -omnyssh-core = { path = "../omnyssh-core", version = "1.1.0" } +omnyssh-core = { path = "../omnyssh-core", version = "1.1.1" } # TUI ratatui = "0.29" diff --git a/doc/omny.1 b/doc/omny.1 index b941baf..e61e9ce 100644 --- a/doc/omny.1 +++ b/doc/omny.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH omny 1 "omny 1.1.0" +.TH omny 1 "omny 1.1.1" .SH NAME omny \- TUI SSH dashboard & server manager .SH SYNOPSIS @@ -28,4 +28,4 @@ Print help (see a summary with \*(Aq\-h\*(Aq) \fB\-V\fR, \fB\-\-version\fR Print version .SH VERSION -v1.1.0 +v1.1.1 From aaea1c39ed7c574396004830b464583ae4b62511 Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Tue, 28 Jul 2026 19:53:22 +0400 Subject: [PATCH 5/6] fix(gui): make the window reveal idempotent Showing an already-visible window raises and refocuses it on macOS, so the fallback and a later page load could pull the app over what the user moved to. --- crates/omnyssh-gui/src/main.rs | 17 +++++++++++++---- crates/omnyssh-gui/tests/startup_contract.rs | 12 +++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/crates/omnyssh-gui/src/main.rs b/crates/omnyssh-gui/src/main.rs index 31ea77c..63a6baf 100644 --- a/crates/omnyssh-gui/src/main.rs +++ b/crates/omnyssh-gui/src/main.rs @@ -129,9 +129,13 @@ fn main() { .on_page_load(|webview, payload| { if matches!(payload.event(), PageLoadEvent::Finished) { let window = webview.window(); - let _ = window.show(); - // A window shown after build does not become key on its own everywhere. - let _ = window.set_focus(); + // Reveal once: a later page load must not raise the window over + // whatever the user is doing. + if !window.is_visible().unwrap_or(false) { + let _ = window.show(); + // A window shown after build does not become key on its own everywhere. + let _ = window.set_focus(); + } } }) .setup(move |app| { @@ -141,7 +145,12 @@ fn main() { tauri::async_runtime::spawn(async move { tokio::time::sleep(REVEAL_FALLBACK).await; if let Some(window) = reveal.get_webview_window("main") { - let _ = window.show(); + // Only when the page never got there: on macOS showing an + // already-visible window raises it over whatever the user + // switched to meanwhile. + if !window.is_visible().unwrap_or(false) { + let _ = window.show(); + } } }); diff --git a/crates/omnyssh-gui/tests/startup_contract.rs b/crates/omnyssh-gui/tests/startup_contract.rs index 0cec58b..dc13821 100644 --- a/crates/omnyssh-gui/tests/startup_contract.rs +++ b/crates/omnyssh-gui/tests/startup_contract.rs @@ -12,9 +12,9 @@ const MAIN_RS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/mai #[test] fn release_builds_link_as_a_windows_gui_binary() { assert!( - MAIN_RS.contains(r#"windows_subsystem = "windows""#), - "src/main.rs no longer declares windows_subsystem — release builds would \ - open a console window alongside the app on Windows" + MAIN_RS.contains(r#"#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]"#), + "src/main.rs no longer declares the windows_subsystem attribute — release \ + builds would open a console window alongside the app on Windows" ); } @@ -43,9 +43,11 @@ fn the_window_starts_hidden_on_the_dark_background() { /// `--bg` of the dark theme — the single source of truth for the app's backdrop. fn dark_background_token() -> String { let css = read(Path::new(MANIFEST_DIR).join("ui/src/app.css")); - css.split_once(":root[data-theme='dark']") + let block = css + .split_once(":root[data-theme='dark']") .expect("app.css declares a dark theme block") - .1 + .1; + block[..block.find('}').expect("the dark theme block is closed")] .split_once("--bg:") .expect("the dark theme block declares --bg") .1 From 6427c9c5cc80568fe2e159f45887866ef17b7c11 Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Tue, 28 Jul 2026 19:54:39 +0400 Subject: [PATCH 6/6] test(gui): guard both halves of the hidden-window launch --- crates/omnyssh-gui/tests/startup_contract.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/omnyssh-gui/tests/startup_contract.rs b/crates/omnyssh-gui/tests/startup_contract.rs index dc13821..ce4acb6 100644 --- a/crates/omnyssh-gui/tests/startup_contract.rs +++ b/crates/omnyssh-gui/tests/startup_contract.rs @@ -40,6 +40,20 @@ fn the_window_starts_hidden_on_the_dark_background() { ); } +/// The other half of `visible: false`: drop either reveal and the app becomes a +/// running process with no window and no way to reach it. +#[test] +fn a_hidden_window_is_always_revealed() { + assert!( + MAIN_RS.contains(".on_page_load("), + "the page-load reveal is gone — the window would stay hidden until the fallback" + ); + assert!( + MAIN_RS.contains("REVEAL_FALLBACK"), + "the fallback reveal is gone — a frontend that never loads would leave no window" + ); +} + /// `--bg` of the dark theme — the single source of truth for the app's backdrop. fn dark_background_token() -> String { let css = read(Path::new(MANIFEST_DIR).join("ui/src/app.css"));