diff --git a/data/app.driftwood.Driftwood.desktop b/data/app.driftwood.Driftwood.desktop index 3f3de0c..d31b663 100644 --- a/data/app.driftwood.Driftwood.desktop +++ b/data/app.driftwood.Driftwood.desktop @@ -8,3 +8,4 @@ Type=Application Categories=System;PackageManager;GTK; Keywords=AppImage;Application;Manager;Package; StartupNotify=true +SingleMainWindow=true diff --git a/src/cli.rs b/src/cli.rs index 755aa61..444a010 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -609,48 +609,36 @@ fn cmd_launch(db: &Database, path: &str) -> ExitCode { // Try to find in database for tracking let record = db.get_appimage_by_path(path).ok().flatten(); - if let Some(ref record) = record { - match launcher::launch_appimage(db, record.id, file_path, "cli", &[], &[]) { - launcher::LaunchResult::Started { method, .. } => { - println!( - "Launched {} ({})", - record.app_name.as_deref().unwrap_or(&record.filename), - method.as_str(), - ); - ExitCode::SUCCESS - } - launcher::LaunchResult::Crashed { stderr, exit_code, .. } => { - eprintln!( - "App crashed immediately (exit code: {})\n{}", - exit_code.map(|c| c.to_string()).unwrap_or_else(|| "unknown".into()), - stderr, - ); - ExitCode::FAILURE - } - launcher::LaunchResult::Failed(msg) => { - eprintln!("Error: {}", msg); - ExitCode::FAILURE - } - } + let launch_result = if let Some(ref record) = record { + launcher::launch_appimage(db, record.id, file_path, "cli", &[], &[]) } else { - // Not in database - launch without tracking - match launcher::launch_appimage_simple(file_path, &[]) { - launcher::LaunchResult::Started { method, .. } => { - println!("Launched {} ({})", path, method.as_str()); - ExitCode::SUCCESS - } - launcher::LaunchResult::Crashed { stderr, exit_code, .. } => { - eprintln!( - "App crashed immediately (exit code: {})\n{}", - exit_code.map(|c| c.to_string()).unwrap_or_else(|| "unknown".into()), - stderr, - ); - ExitCode::FAILURE - } - launcher::LaunchResult::Failed(msg) => { - eprintln!("Error: {}", msg); - ExitCode::FAILURE - } + launcher::launch_appimage_simple(file_path, &[]) + }; + + match launch_result { + launcher::LaunchResult::Started { pid, method } => { + let name = record.as_ref() + .and_then(|r| r.app_name.as_deref()) + .unwrap_or(path); + println!("Launched {} (PID: {}, {})", name, pid, method.as_str()); + ExitCode::SUCCESS + } + launcher::LaunchResult::Crashed { exit_code, stderr, method } => { + let name = record.as_ref() + .and_then(|r| r.app_name.as_deref()) + .unwrap_or(path); + eprintln!( + "{} crashed on launch (exit code: {}, method: {})\n{}", + name, + exit_code.map(|c: i32| c.to_string()).unwrap_or_else(|| "unknown".into()), + method.as_str(), + stderr, + ); + ExitCode::FAILURE + } + launcher::LaunchResult::Failed(msg) => { + eprintln!("Error: {}", msg); + ExitCode::FAILURE } } } diff --git a/src/config.rs b/src/config.rs index 5cdb55b..233caf2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,25 @@ +use std::path::PathBuf; + pub const APP_ID: &str = "app.driftwood.Driftwood"; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const GSETTINGS_SCHEMA_DIR: &str = env!("GSETTINGS_SCHEMA_DIR"); pub const SYSTEM_APPIMAGE_DIR: &str = "/opt/appimages"; + +/// Return the XDG data directory with a proper $HOME-based fallback. +/// Unlike `PathBuf::from("~/.local/share")`, this actually resolves to the +/// user's home directory instead of creating a literal `~` path. +pub fn data_dir_fallback() -> PathBuf { + dirs::data_dir().unwrap_or_else(|| home_dir().join(".local/share")) +} + +/// Return the XDG config directory with a proper $HOME-based fallback. +#[allow(dead_code)] +pub fn config_dir_fallback() -> PathBuf { + dirs::config_dir().unwrap_or_else(|| home_dir().join(".config")) +} + +fn home_dir() -> PathBuf { + dirs::home_dir() + .or_else(|| std::env::var("HOME").ok().map(PathBuf::from)) + .unwrap_or_else(|| PathBuf::from("/tmp")) +} diff --git a/src/core/analysis.rs b/src/core/analysis.rs index 63b35e6..d55876d 100644 --- a/src/core/analysis.rs +++ b/src/core/analysis.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use std::sync::{Condvar, Mutex}; use std::sync::atomic::{AtomicUsize, Ordering}; use crate::core::database::Database; @@ -14,17 +15,21 @@ const MAX_CONCURRENT_ANALYSES: usize = 2; /// Counter for currently running analyses. static RUNNING_ANALYSES: AtomicUsize = AtomicUsize::new(0); +/// Condvar to efficiently wait for a slot instead of busy-polling. +static SLOT_AVAILABLE: (Mutex<()>, Condvar) = (Mutex::new(()), Condvar::new()); + /// Returns the number of currently running background analyses. pub fn running_count() -> usize { RUNNING_ANALYSES.load(Ordering::Relaxed) } -/// RAII guard that decrements the analysis counter on drop. +/// RAII guard that decrements the analysis counter on drop and notifies waiters. struct AnalysisGuard; impl Drop for AnalysisGuard { fn drop(&mut self) { RUNNING_ANALYSES.fetch_sub(1, Ordering::Release); + SLOT_AVAILABLE.1.notify_one(); } } @@ -36,7 +41,7 @@ impl Drop for AnalysisGuard { /// /// Blocks until a slot is available if the concurrency limit is reached. pub fn run_background_analysis(id: i64, path: PathBuf, appimage_type: AppImageType, integrate: bool) { - // Wait for a slot to become available + // Wait for a slot to become available using condvar instead of busy-polling loop { let current = RUNNING_ANALYSES.load(Ordering::Acquire); if current < MAX_CONCURRENT_ANALYSES { @@ -44,7 +49,8 @@ pub fn run_background_analysis(id: i64, path: PathBuf, appimage_type: AppImageTy break; } } else { - std::thread::sleep(std::time::Duration::from_millis(200)); + let lock = SLOT_AVAILABLE.0.lock().unwrap(); + let _ = SLOT_AVAILABLE.1.wait_timeout(lock, std::time::Duration::from_secs(1)); } } let _guard = AnalysisGuard; diff --git a/src/core/appstream.rs b/src/core/appstream.rs index d15fdc4..2b9cd5c 100644 --- a/src/core/appstream.rs +++ b/src/core/appstream.rs @@ -446,14 +446,14 @@ pub fn generate_catalog(db: &Database) -> Result { xml.push_str(" \n"); } - // Provide hint about source - xml.push_str(" \n"); - xml.push_str(" driftwood\n"); + // Provide hint about source (AppStream spec uses for key-value data) + xml.push_str(" \n"); + xml.push_str(" driftwood\n"); xml.push_str(&format!( - " {}\n", + " {}\n", xml_escape(&record.path), )); - xml.push_str(" \n"); + xml.push_str(" \n"); xml.push_str(" \n"); } @@ -467,8 +467,7 @@ pub fn generate_catalog(db: &Database) -> Result { pub fn install_catalog(db: &Database) -> Result { let catalog_xml = generate_catalog(db)?; - let catalog_dir = dirs::data_dir() - .unwrap_or_else(|| PathBuf::from("~/.local/share")) + let catalog_dir = crate::config::data_dir_fallback() .join("swcatalog") .join("xml"); @@ -484,8 +483,7 @@ pub fn install_catalog(db: &Database) -> Result { /// Remove the AppStream catalog from the local swcatalog directory. pub fn uninstall_catalog() -> Result<(), AppStreamError> { - let catalog_path = dirs::data_dir() - .unwrap_or_else(|| PathBuf::from("~/.local/share")) + let catalog_path = crate::config::data_dir_fallback() .join("swcatalog") .join("xml") .join("driftwood.xml"); @@ -500,8 +498,7 @@ pub fn uninstall_catalog() -> Result<(), AppStreamError> { /// Check if the AppStream catalog is currently installed. pub fn is_catalog_installed() -> bool { - let catalog_path = dirs::data_dir() - .unwrap_or_else(|| PathBuf::from("~/.local/share")) + let catalog_path = crate::config::data_dir_fallback() .join("swcatalog") .join("xml") .join("driftwood.xml"); diff --git a/src/core/backup.rs b/src/core/backup.rs index a17d2ed..e682e7b 100644 --- a/src/core/backup.rs +++ b/src/core/backup.rs @@ -25,8 +25,7 @@ pub struct BackupPathEntry { } fn backups_dir() -> PathBuf { - let dir = dirs::data_dir() - .unwrap_or_else(|| PathBuf::from("~/.local/share")) + let dir = crate::config::data_dir_fallback() .join("driftwood") .join("backups"); fs::create_dir_all(&dir).ok(); @@ -123,17 +122,17 @@ pub fn create_backup(db: &Database, appimage_id: i64) -> Result Result let extracted = if let Ok(rel) = source.strip_prefix(&home_dir) { temp_dir.path().join(rel) } else { - let source_name = source.file_name().unwrap_or_default(); - temp_dir.path().join(source_name) + // Non-home paths are archived with full path (leading / stripped) + let abs_rel = source.strip_prefix("/").unwrap_or(source); + temp_dir.path().join(abs_rel) }; let target = Path::new(&entry.original_path); diff --git a/src/core/database.rs b/src/core/database.rs index 8c1d6b6..e0e646d 100644 --- a/src/core/database.rs +++ b/src/core/database.rs @@ -198,8 +198,7 @@ pub struct SandboxProfileRecord { } fn db_path() -> PathBuf { - let data_dir = dirs::data_dir() - .unwrap_or_else(|| PathBuf::from("~/.local/share")) + let data_dir = crate::config::data_dir_fallback() .join("driftwood"); std::fs::create_dir_all(&data_dir).ok(); data_dir.join("driftwood.db") @@ -753,7 +752,7 @@ impl Database { is_executable: bool, file_modified: Option<&str>, ) -> SqlResult { - self.conn.execute( + let id: i64 = self.conn.query_row( "INSERT INTO appimages (path, filename, appimage_type, size_bytes, is_executable, file_modified) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(path) DO UPDATE SET @@ -762,17 +761,9 @@ impl Database { size_bytes = excluded.size_bytes, is_executable = excluded.is_executable, file_modified = excluded.file_modified, - last_scanned = datetime('now')", + last_scanned = datetime('now') + RETURNING id", params![path, filename, appimage_type, size_bytes, is_executable, file_modified], - )?; - // last_insert_rowid() returns 0 for ON CONFLICT UPDATE, so query the actual id - let id = self.conn.last_insert_rowid(); - if id != 0 { - return Ok(id); - } - let id: i64 = self.conn.query_row( - "SELECT id FROM appimages WHERE path = ?1", - params![path], |row| row.get(0), )?; Ok(id) diff --git a/src/core/duplicates.rs b/src/core/duplicates.rs index e9a8e8e..fed8c76 100644 --- a/src/core/duplicates.rs +++ b/src/core/duplicates.rs @@ -329,6 +329,8 @@ fn build_name_group(name: &str, records: &[&AppImageRecord]) -> DuplicateGroup { } /// Compare two version strings for ordering. +/// Falls back to lexicographic comparison of cleaned versions to guarantee +/// the total ordering contract (antisymmetry) required by sort_by. fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering { use super::updater::{clean_version, version_is_newer}; @@ -339,8 +341,11 @@ fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering { std::cmp::Ordering::Equal } else if version_is_newer(a, b) { std::cmp::Ordering::Greater - } else { + } else if version_is_newer(b, a) { std::cmp::Ordering::Less + } else { + // Neither is newer (unparseable components) - use lexicographic fallback + ca.cmp(&cb) } } diff --git a/src/core/footprint.rs b/src/core/footprint.rs index ff76c4e..af26941 100644 --- a/src/core/footprint.rs +++ b/src/core/footprint.rs @@ -396,8 +396,14 @@ pub fn dir_size_pub(path: &Path) -> u64 { } fn dir_size(path: &Path) -> u64 { - if path.is_file() { - return path.metadata().map(|m| m.len()).unwrap_or(0); + // Use symlink_metadata to avoid following symlinks outside the tree + if let Ok(meta) = path.symlink_metadata() { + if meta.is_file() { + return meta.len(); + } + if meta.is_symlink() { + return 0; + } } let mut total = 0u64; if let Ok(entries) = std::fs::read_dir(path) { @@ -406,6 +412,10 @@ fn dir_size(path: &Path) -> u64 { Ok(ft) => ft, Err(_) => continue, }; + // Skip symlinks to avoid counting external files or recursing out of tree + if ft.is_symlink() { + continue; + } if ft.is_file() { total += entry.metadata().map(|m| m.len()).unwrap_or(0); } else if ft.is_dir() { diff --git a/src/core/inspector.rs b/src/core/inspector.rs index 4a232bd..cc12fb6 100644 --- a/src/core/inspector.rs +++ b/src/core/inspector.rs @@ -80,8 +80,7 @@ struct DesktopEntryFields { } fn icons_cache_dir() -> PathBuf { - let dir = dirs::data_dir() - .unwrap_or_else(|| PathBuf::from("~/.local/share")) + let dir = crate::config::data_dir_fallback() .join("driftwood") .join("icons"); fs::create_dir_all(&dir).ok(); diff --git a/src/core/integrator.rs b/src/core/integrator.rs index 4c11e68..433b205 100644 --- a/src/core/integrator.rs +++ b/src/core/integrator.rs @@ -25,20 +25,34 @@ impl From for IntegrationError { } } +/// Escape a string for use inside a double-quoted Exec argument in a .desktop file. +/// Per the Desktop Entry spec, `\`, `"`, `` ` ``, and `$` must be escaped with `\`. +fn escape_exec_arg(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '\\' | '"' | '`' | '$' => { + out.push('\\'); + out.push(c); + } + _ => out.push(c), + } + } + out +} + pub struct IntegrationResult { pub desktop_file_path: PathBuf, pub icon_install_path: Option, } fn applications_dir() -> PathBuf { - dirs::data_dir() - .unwrap_or_else(|| PathBuf::from("~/.local/share")) + crate::config::data_dir_fallback() .join("applications") } fn icons_dir() -> PathBuf { - dirs::data_dir() - .unwrap_or_else(|| PathBuf::from("~/.local/share")) + crate::config::data_dir_fallback() .join("icons/hicolor") } @@ -90,21 +104,22 @@ pub fn integrate(record: &AppImageRecord) -> Result, stderr: String, method: LaunchMethod, }, - /// Failed to launch. + /// Failed to launch (binary not found, permission denied, etc.). Failed(String), } @@ -183,34 +183,21 @@ fn execute_appimage( match cmd.spawn() { Ok(mut child) => { - // Give the process a brief moment to fail on immediate errors - // (missing libs, exec format errors, Qt plugin failures, etc.) - std::thread::sleep(std::time::Duration::from_millis(150)); - - match child.try_wait() { - Ok(Some(status)) => { - // Already exited - immediate crash. Read stderr for details. - let stderr_text = child.stderr.take().map(|mut pipe| { - let mut buf = String::new(); - use std::io::Read; - // Read with a size cap to avoid huge allocations - let mut limited = (&mut pipe).take(64 * 1024); - let _ = limited.read_to_string(&mut buf); - buf - }).unwrap_or_default(); + let pid = child.id(); + // Monitor for early crash (2s window). This blocks the current + // thread, so callers should run this inside gio::spawn_blocking. + match check_early_crash(&mut child, std::time::Duration::from_secs(2)) { + Some((exit_code, stderr)) => { LaunchResult::Crashed { - exit_code: status.code(), - stderr: stderr_text, + exit_code, + stderr, method: method.clone(), } } - _ => { - // Still running after 150ms - drop the stderr pipe so the - // child process won't block if it fills the pipe buffer. - drop(child.stderr.take()); + None => { LaunchResult::Started { - child, + pid, method: method.clone(), } } @@ -220,7 +207,44 @@ fn execute_appimage( } } -/// Parse a launch_args string from the database into a Vec of individual arguments. +/// Check if a recently-launched child process crashed during startup. +/// Waits up to `timeout` for the process to exit. If it exits within that window, +/// reads stderr and returns a Crashed result. If still running, drops the stderr +/// pipe (to prevent pipe buffer deadlock) and returns None. +/// +/// Call this from a background thread after spawning the process. +pub fn check_early_crash( + child: &mut Child, + timeout: std::time::Duration, +) -> Option<(Option, String)> { + let start = std::time::Instant::now(); + loop { + match child.try_wait() { + Ok(Some(status)) => { + // Process exited - read stderr for crash details + let stderr_text = child.stderr.take().map(|mut pipe| { + let mut buf = String::new(); + use std::io::Read; + let mut limited = (&mut pipe).take(64 * 1024); + let _ = limited.read_to_string(&mut buf); + buf + }).unwrap_or_default(); + + return Some((status.code(), stderr_text)); + } + Ok(None) => { + if start.elapsed() >= timeout { + // Still running - drop stderr pipe to avoid deadlock + drop(child.stderr.take()); + return None; + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Err(_) => return None, + } + } +} + /// Parse launch arguments with basic quote support. /// Splits on whitespace, respecting double-quoted strings. /// Returns an empty Vec if the input is None or empty. diff --git a/src/core/orphan.rs b/src/core/orphan.rs index 5e42505..0d84c78 100644 --- a/src/core/orphan.rs +++ b/src/core/orphan.rs @@ -14,14 +14,12 @@ pub struct CleanupSummary { } fn applications_dir() -> PathBuf { - dirs::data_dir() - .unwrap_or_else(|| PathBuf::from("~/.local/share")) + crate::config::data_dir_fallback() .join("applications") } fn icons_dir() -> PathBuf { - dirs::data_dir() - .unwrap_or_else(|| PathBuf::from("~/.local/share")) + crate::config::data_dir_fallback() .join("icons/hicolor") } diff --git a/src/core/report.rs b/src/core/report.rs index 49157c6..adc03b7 100644 --- a/src/core/report.rs +++ b/src/core/report.rs @@ -229,7 +229,7 @@ pub fn render_html(report: &SecurityReport) -> String { html.push_str("\n\n"); for f in &app.findings { - let sev_class = f.severity.to_lowercase(); + let sev_class = html_escape(&f.severity.to_lowercase()); html.push_str(&format!( "\n", html_escape(&f.cve_id), diff --git a/src/core/sandbox.rs b/src/core/sandbox.rs index 86f8c66..e08d50b 100644 --- a/src/core/sandbox.rs +++ b/src/core/sandbox.rs @@ -44,8 +44,7 @@ impl ProfileSource { /// Directory where local sandbox profiles are stored. fn profiles_dir() -> PathBuf { - let dir = dirs::config_dir() - .unwrap_or_else(|| PathBuf::from("~/.config")) + let dir = crate::config::config_dir_fallback() .join("driftwood") .join("sandbox"); fs::create_dir_all(&dir).ok(); diff --git a/src/core/updater.rs b/src/core/updater.rs index 27c7290..005c02d 100644 --- a/src/core/updater.rs +++ b/src/core/updater.rs @@ -762,8 +762,12 @@ pub fn version_is_newer(latest: &str, current: &str) -> bool { } } - // If all compared parts are equal, longer version wins (1.2.3 > 1.2) - latest_parts.len() > current_parts.len() + // If all compared parts are equal, only consider newer if extra parts are non-zero + // (e.g., 1.2.1 > 1.2, but 1.2.0 == 1.2) + if latest_parts.len() > current_parts.len() { + return latest_parts[current_parts.len()..].iter().any(|&p| p > 0); + } + false } /// Parse a version string into numeric parts. @@ -781,13 +785,13 @@ fn parse_version_parts(version: &str) -> Vec { /// Check if AppImageUpdate tool is available on the system. pub fn has_appimage_update_tool() -> bool { + // Check that the binary exists and can be spawned (--help may return non-zero) std::process::Command::new("AppImageUpdate") .arg("--help") .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .status() - .map(|s| s.success()) - .unwrap_or(false) + .is_ok() } /// Batch check: read update info from an AppImage and check for updates. @@ -935,7 +939,8 @@ pub fn download_and_apply_update( // Atomic rename temp -> target if let Err(e) = fs::rename(&temp_path, appimage_path) { - // Try to restore backup on failure + // Clean up temp file and restore backup on failure + fs::remove_file(&temp_path).ok(); if let Some(ref backup) = backup_path { fs::rename(backup, appimage_path).ok(); } diff --git a/src/main.rs b/src/main.rs index b9bd5de..cafc935 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,8 +14,9 @@ use application::DriftwoodApplication; use config::{APP_ID, GSETTINGS_SCHEMA_DIR}; fn main() -> ExitCode { - // Point GSettings at our compiled schema directory (dev builds) - std::env::set_var("GSETTINGS_SCHEMA_DIR", GSETTINGS_SCHEMA_DIR); + // Point GSettings at our compiled schema directory (dev builds). + // SAFETY: Called before any threads are spawned, at program start. + unsafe { std::env::set_var("GSETTINGS_SCHEMA_DIR", GSETTINGS_SCHEMA_DIR); } // Parse CLI arguments let parsed = cli::Cli::parse(); diff --git a/src/ui/detail_view.rs b/src/ui/detail_view.rs index d944f45..4105e2c 100644 --- a/src/ui/detail_view.rs +++ b/src/ui/detail_view.rs @@ -24,7 +24,7 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc) -> adw::Nav // Toast overlay for copy actions let toast_overlay = adw::ToastOverlay::new(); - // ViewStack for tabbed content with crossfade transitions. + // ViewStack for tabbed content (transitions disabled for instant switching). // vhomogeneous=false so the stack sizes to the visible child only, // preventing shorter tabs from having excess scrollable empty space. let view_stack = adw::ViewStack::new(); @@ -124,10 +124,10 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc) -> adw::Nav btn_ref.set_sensitive(true); match result { - Ok(launcher::LaunchResult::Started { child, method }) => { - let pid = child.id(); + Ok(launcher::LaunchResult::Started { pid, method }) => { log::info!("Launched: {} (PID: {}, method: {})", path, pid, method.as_str()); + // App survived startup - do Wayland analysis after a delay let db_wayland = db_launch.clone(); let path_clone = path.clone(); glib::spawn_future_local(async move { @@ -488,12 +488,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { spinner_ref.set_visible(false); if let Ok(Some(data)) = result { let gbytes = glib::Bytes::from(&data); - let stream = gio::MemoryInputStream::from_bytes(&gbytes); - if let Ok(pixbuf) = gtk::gdk_pixbuf::Pixbuf::from_stream( - &stream, - None::<&gio::Cancellable>, - ) { - let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf); + if let Ok(texture) = gtk::gdk::Texture::from_bytes(&gbytes) { picture_ref.set_paintable(Some(&texture)); if let Some(slot) = textures_load.borrow_mut().get_mut(idx) { *slot = Some(texture); @@ -708,8 +703,11 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { .margin_start(12) .margin_end(12) .build(); - let label_row = adw::ActionRow::new(); - label_row.set_child(Some(&label)); + let label_row = gtk::ListBoxRow::builder() + .activatable(false) + .selectable(false) + .child(&label) + .build(); row.add_row(&label_row); release_group.add(&row); @@ -2080,12 +2078,7 @@ fn fetch_favicon_async(url: &str, image: >k::Image) { if let Ok(Some(data)) = result { let gbytes = glib::Bytes::from(&data); - let stream = gio::MemoryInputStream::from_bytes(&gbytes); - if let Ok(pixbuf) = gtk::gdk_pixbuf::Pixbuf::from_stream( - &stream, - None::<&gio::Cancellable>, - ) { - let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf); + if let Ok(texture) = gtk::gdk::Texture::from_bytes(&gbytes) { image_ref.set_paintable(Some(&texture)); } } diff --git a/src/ui/update_dialog.rs b/src/ui/update_dialog.rs index f502980..0506de0 100644 --- a/src/ui/update_dialog.rs +++ b/src/ui/update_dialog.rs @@ -90,17 +90,15 @@ pub fn show_update_dialog( let db_update = db_ref.clone(); let record_path = record_clone.path.clone(); let new_version = check_result.latest_version.clone(); - dialog_ref.connect_response(None, move |dlg, response| { - if response == "update" { - start_update( - dlg, - &record_path, - &download_url, - record_id, - new_version.as_deref(), - &db_update, - ); - } + dialog_ref.connect_response(Some("update"), move |dlg, _response| { + start_update( + dlg, + &record_path, + &download_url, + record_id, + new_version.as_deref(), + &db_update, + ); }); } } else { @@ -239,11 +237,9 @@ fn handle_old_version_cleanup(dialog: &adw::AlertDialog, old_path: PathBuf) { dialog.set_response_appearance("remove-old", adw::ResponseAppearance::Destructive); let path = old_path.clone(); - dialog.connect_response(None, move |_dlg, response| { - if response == "remove-old" { - if path.exists() { - std::fs::remove_file(&path).ok(); - } + dialog.connect_response(Some("remove-old"), move |_dlg, _response| { + if path.exists() { + std::fs::remove_file(&path).ok(); } }); } diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index 14e476f..f583f13 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -182,12 +182,10 @@ pub fn copy_button(text_to_copy: &str, toast_overlay: Option<&adw::ToastOverlay> let text = text_to_copy.to_string(); let toast = toast_overlay.cloned(); btn.connect_clicked(move |button| { - if let Some(display) = button.display().into() { - let clipboard = gtk::gdk::Display::clipboard(&display); - clipboard.set_text(&text); - if let Some(ref overlay) = toast { - overlay.add_toast(adw::Toast::new("Copied to clipboard")); - } + let clipboard = button.display().clipboard(); + clipboard.set_text(&text); + if let Some(ref overlay) = toast { + overlay.add_toast(adw::Toast::new("Copied to clipboard")); } }); btn diff --git a/src/window.rs b/src/window.rs index f3efcab..7fb9d42 100644 --- a/src/window.rs +++ b/src/window.rs @@ -614,8 +614,8 @@ impl DriftwoodWindow { launcher::launch_appimage(&bg_db, record_id, appimage_path, "gui_context", &launch_args, &[]) }).await; match result { - Ok(launcher::LaunchResult::Started { child, method }) => { - log::info!("Launched: {} (PID: {}, method: {})", path_str, child.id(), method.as_str()); + Ok(launcher::LaunchResult::Started { pid, method }) => { + log::info!("Launched: {} (PID: {}, method: {})", path_str, pid, method.as_str()); } Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method }) => { log::error!("App crashed (exit {}, method: {}): {}", exit_code.unwrap_or(-1), method.as_str(), stderr);
CVESeverityCVSSLibraryFixed InSummary
{}{}{}{} {}{}{}