From 35ccd5659e3a437d9b636d5331f8130324810f01 Mon Sep 17 00:00:00 2001 From: NimBold Date: Tue, 30 Jun 2026 15:47:02 +0330 Subject: [PATCH] fix(windows): hide engine consoles Disable the native Windows frame at startup and render in-app window controls so packaged builds use Firelink's own titlebar placement instead of the default OS chrome. Apply CREATE_NO_WINDOW to raw child processes used by the aria2 daemon and shared engine version checks. Route ffmpeg and deno checks through the same hidden version-check helper so Settings > Engine does not flash console windows. Normalize Windows extended-length paths before returning approved download roots to the UI, while keeping canonicalization for backend safety checks. Detect yt-dlp's embedded runtime per platform so Windows onedir builds look for python*.dll instead of macOS Python.framework. Verification: npm run build; cargo check --manifest-path src-tauri/Cargo.toml; cargo test --manifest-path src-tauri/Cargo.toml --lib; node --check scripts/smoke-packaged-app.js; Ruby YAML parse for .github/workflows/release.yml; git diff --check. --- src-tauri/src/lib.rs | 135 +++++++++++++++++++----------- src-tauri/src/platform.rs | 44 ++++++++++ src/App.tsx | 2 + src/components/WindowControls.tsx | 38 +++++++++ src/index.css | 42 ++++++++-- 5 files changed, 207 insertions(+), 54 deletions(-) create mode 100644 src/components/WindowControls.tsx diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d51109d..7f327da 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1346,19 +1346,8 @@ async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result { #[tauri::command] async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result { - use tauri_plugin_shell::ShellExt; - - let binary_path = resolve_bundled_binary_path(&app_handle, "ffmpeg")?; - let output = app_handle - .shell() - .command(&binary_path) - .arg("-version") - .output() - .await - .map_err(|e| format!("Failed to execute ffmpeg: {}", e))?; - - if output.status.success() { - let text = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let (version, error, _) = run_sidecar_version(&app_handle, "ffmpeg", &["-version"]).await; + if let Some(text) = version { let first_line = text.lines().next().unwrap_or(""); let re = regex::Regex::new(r"(?i)version\s+([\d\.]+)").unwrap(); let clean = re @@ -1373,30 +1362,18 @@ async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result { .split('-') .next() .unwrap_or("") - .to_string() + .to_string() }); Ok(clean) } else { - let err = String::from_utf8_lossy(&output.stderr); - Err(format!("ffmpeg error: {}", err)) + Err(error.unwrap_or_else(|| "ffmpeg returned no version output".to_string())) } } #[tauri::command] async fn test_deno(app_handle: tauri::AppHandle) -> Result { - use tauri_plugin_shell::ShellExt; - - let binary_path = resolve_bundled_binary_path(&app_handle, "deno")?; - let output = app_handle - .shell() - .command(&binary_path) - .arg("--version") - .output() - .await - .map_err(|e| format!("Failed to execute deno: {}", e))?; - - if output.status.success() { - let text = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let (version, error, _) = run_sidecar_version(&app_handle, "deno", &["--version"]).await; + if let Some(text) = version { let re = regex::Regex::new(r"deno\s+(\d+\.\d+\.\d+)").unwrap(); let clean = re .captures(&text) @@ -1406,8 +1383,7 @@ async fn test_deno(app_handle: tauri::AppHandle) -> Result { .to_string(); Ok(clean) } else { - let err = String::from_utf8_lossy(&output.stderr); - Err(format!("deno error: {}", err)) + Err(error.unwrap_or_else(|| "deno returned no version output".to_string())) } } @@ -1492,7 +1468,7 @@ fn approve_download_root(app_handle: tauri::AppHandle, path: String) -> Result let resolved_path = resolved .as_ref() .ok() - .map(|p| p.to_string_lossy().to_string()); + .map(|p| crate::platform::display_path(p)); let (startup_err, daemon_stderr) = { let guard = app_handle.state::(); @@ -2131,19 +2108,15 @@ async fn check_ytdlp(app_handle: &tauri::AppHandle) -> EngineStatusItem { let resolved_path = resolved .as_ref() .ok() - .map(|p| p.to_string_lossy().to_string()); + .map(|p| crate::platform::display_path(p)); - let (has_internal_dir, has_python_framework) = if let Some(ref path) = resolved_path { - let parent = std::path::Path::new(path).parent().map(|p| p.to_path_buf()); + let (has_internal_dir, has_python_runtime) = if let Ok(ref path) = resolved { + let parent = path.parent().map(|p| p.to_path_buf()); if let Some(parent) = parent { - let internal = crate::engines::ytdlp_internal_dir(std::path::Path::new(path)) - .unwrap_or_else(|| parent.join("_internal")); + let internal = + crate::engines::ytdlp_internal_dir(path).unwrap_or_else(|| parent.join("_internal")); let hi = internal.is_dir(); - let hp = if hi { - internal.join("Python.framework").is_dir() || internal.join("Python").exists() - } else { - false - }; + let hp = hi && ytdlp_embedded_runtime_exists(&internal); (hi, hp) } else { (false, false) @@ -2159,8 +2132,8 @@ async fn check_ytdlp(app_handle: &tauri::AppHandle) -> EngineStatusItem { let mut error = run_error; let mut remediation_hint = None; - if error.is_none() && has_internal_dir && !has_python_framework { - error = Some("_internal/Python.framework was not found beside yt-dlp sidecar".to_string()); + if error.is_none() && has_internal_dir && !has_python_runtime { + error = Some(yt_dlp_missing_runtime_message().to_string()); remediation_hint = Some( "The yt-dlp distribution is missing its embedded Python runtime. Reinstall Firelink." .to_string(), @@ -2189,10 +2162,68 @@ async fn check_ytdlp(app_handle: &tauri::AppHandle) -> EngineStatusItem { last_stderr_tail: None, expects_internal_dir: has_internal_dir.then_some(true), has_internal_dir: has_internal_dir.then_some(true), - has_python_framework: has_internal_dir.then_some(has_python_framework), + has_python_framework: has_internal_dir.then_some(has_python_runtime), } } +fn ytdlp_embedded_runtime_exists(internal: &std::path::Path) -> bool { + #[cfg(target_os = "windows")] + { + return std::fs::read_dir(internal) + .ok() + .into_iter() + .flat_map(|entries| entries.flatten()) + .any(|entry| { + let name = entry.file_name().to_string_lossy().to_ascii_lowercase(); + name.starts_with("python") + && entry + .path() + .extension() + .is_some_and(|extension| extension.eq_ignore_ascii_case("dll")) + }); + } + + #[cfg(target_os = "linux")] + { + return std::fs::read_dir(internal) + .ok() + .into_iter() + .flat_map(|entries| entries.flatten()) + .any(|entry| { + let name = entry.file_name().to_string_lossy().to_string(); + name == "Python" || name.starts_with("libpython") && name.contains(".so") + }); + } + + #[cfg(target_os = "macos")] + { + return internal.join("Python.framework").is_dir() || internal.join("Python").exists(); + } + + #[allow(unreachable_code)] + false +} + +fn yt_dlp_missing_runtime_message() -> &'static str { + #[cfg(target_os = "windows")] + { + return "_internal/python*.dll was not found beside yt-dlp sidecar"; + } + + #[cfg(target_os = "linux")] + { + return "_internal/libpython*.so was not found beside yt-dlp sidecar"; + } + + #[cfg(target_os = "macos")] + { + return "_internal/Python.framework was not found beside yt-dlp sidecar"; + } + + #[allow(unreachable_code)] + "_internal Python runtime was not found beside yt-dlp sidecar" +} + async fn check_ffmpeg(app_handle: &tauri::AppHandle) -> EngineStatusItem { let sidecar_name = "ffmpeg"; let expected_sidecar = crate::platform::engine_binary_name(sidecar_name); @@ -2201,7 +2232,7 @@ async fn check_ffmpeg(app_handle: &tauri::AppHandle) -> EngineStatusItem { let resolved_path = resolved .as_ref() .ok() - .map(|p| p.to_string_lossy().to_string()); + .map(|p| crate::platform::display_path(p)); let (version_raw, run_error, stderr_tail) = run_sidecar_version(app_handle, sidecar_name, &["-version"]).await; @@ -2252,7 +2283,7 @@ async fn check_deno(app_handle: &tauri::AppHandle) -> EngineStatusItem { let resolved_path = resolved .as_ref() .ok() - .map(|p| p.to_string_lossy().to_string()); + .map(|p| crate::platform::display_path(p)); let (version_raw, run_error, stderr_tail) = run_sidecar_version(app_handle, sidecar_name, &["--version"]).await; @@ -4567,6 +4598,13 @@ pub fn run() { .plugin(tauri_plugin_deep_link::init()) .manage(Aria2DaemonGuard::new()) .setup(move |app| { + #[cfg(target_os = "windows")] + if let Some(window) = app.get_webview_window("main") { + window + .set_decorations(false) + .map_err(|error| format!("failed to disable Windows native frame: {error}"))?; + } + let mut sys = sysinfo::System::new_all(); sys.refresh_all(); log::info!("=== System Information ==="); @@ -4694,6 +4732,7 @@ pub fn run() { let mut success = false; for attempt_port in 6800..6900 { let mut cmd = std::process::Command::new(&binary_path); + crate::platform::hide_child_console(&mut cmd); let mut config_file = tempfile::Builder::new().prefix("aria2-").suffix(".conf").tempfile().expect("failed to create aria2 config file"); use std::io::Write; diff --git a/src-tauri/src/platform.rs b/src-tauri/src/platform.rs index 4bbdd23..3d983ef 100644 --- a/src-tauri/src/platform.rs +++ b/src-tauri/src/platform.rs @@ -39,6 +39,50 @@ pub fn engine_binary_name(engine: &str) -> String { format!("{engine}-{}{}", target_triple(), executable_suffix()) } +#[cfg(target_os = "windows")] +const CREATE_NO_WINDOW: u32 = 0x08000000; + +pub fn hide_child_console(command: &mut std::process::Command) { + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + command.creation_flags(CREATE_NO_WINDOW); + } + + #[cfg(not(target_os = "windows"))] + { + let _ = command; + } +} + +pub fn hide_tokio_child_console(command: &mut tokio::process::Command) { + #[cfg(target_os = "windows")] + { + command.creation_flags(CREATE_NO_WINDOW); + } + + #[cfg(not(target_os = "windows"))] + { + let _ = command; + } +} + +pub fn display_path(path: &Path) -> String { + let text = path.to_string_lossy(); + + #[cfg(target_os = "windows")] + { + if let Some(stripped) = text.strip_prefix(r"\\?\UNC\") { + return format!(r"\\{stripped}"); + } + if let Some(stripped) = text.strip_prefix(r"\\?\") { + return stripped.to_string(); + } + } + + text.to_string() +} + pub fn trusted_system_path() -> Result { let entries = trusted_system_path_entries(); std::env::join_paths(entries) diff --git a/src/App.tsx b/src/App.tsx index 60eaa77..0fd460d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import SchedulerView from "./components/SchedulerView"; import SpeedLimiterView from "./components/SpeedLimiterView"; import LogsView from "./components/LogsView"; import { KeychainPermissionModal } from "./components/KeychainPermissionModal"; +import { WindowControls } from "./components/WindowControls"; import { useToast } from "./contexts/ToastContext"; import { openUrl } from '@tauri-apps/plugin-opener'; import { usePlatformInfo } from './utils/platform'; @@ -576,6 +577,7 @@ function App() { return (
+ {platform.os === 'windows' && }
+ + + +
+ ); +} diff --git a/src/index.css b/src/index.css index 8edd184..a794a7f 100644 --- a/src/index.css +++ b/src/index.css @@ -1279,13 +1279,43 @@ .mac-switch:checked::after { transform: translateX(16px); } - .downloads-view { - background: hsl(var(--main-bg)); - } +.downloads-view { + background: hsl(var(--main-bg)); +} - .main-titlebar { - height: 52px; - display: flex; +.window-controls { + position: fixed; + top: 10px; + left: 12px; + z-index: 60; + display: inline-flex; + align-items: center; + gap: 7px; +} + +.window-control { + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + color: hsl(var(--text-muted)); + border-radius: 999px; +} + +.window-control:hover { + color: hsl(var(--text-primary)); + background: hsl(var(--item-hover)); +} + +.window-control.close:hover { + color: white; + background: #c42b1c; +} + +.main-titlebar { + height: 52px; + display: flex; align-items: center; padding: 0 18px; border-bottom: 1px solid hsl(var(--border-color));