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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions crates/omnyssh-core/src/ssh/key_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions crates/omnyssh-gui/src/main.rs
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -21,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<tauri::Wry> {
Expand Down Expand Up @@ -113,9 +123,37 @@ 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();
// 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| {
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") {
// 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();
}
}
});

let (engine_tx, engine_rx) = tokio::sync::mpsc::channel::<CoreEvent>(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.
Expand Down
1 change: 1 addition & 0 deletions crates/omnyssh-gui/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"minWidth": 880,
"minHeight": 560,
"backgroundColor": "#171717",
"visible": false,
"theme": "Dark",
"titleBarStyle": "Overlay",
"hiddenTitle": true
Expand Down
78 changes: 78 additions & 0 deletions crates/omnyssh-gui/tests/startup_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! 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.

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
/// terminal next to the window for the whole session.
#[test]
fn release_builds_link_as_a_windows_gui_binary() {
assert!(
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"
);
}

/// 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"
);
}

/// 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"));
let block = css
.split_once(":root[data-theme='dark']")
.expect("app.css declares a dark theme block")
.1;
block[..block.find('}').expect("the dark theme block is closed")]
.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<Path>) -> String {
let path = path.as_ref();
std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()))
}
2 changes: 1 addition & 1 deletion crates/omnyssh/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions doc/omny.1
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Loading