diff --git a/data/app.driftwood.Driftwood.gschema.xml b/data/app.driftwood.Driftwood.gschema.xml index 70656db..7c692f1 100644 --- a/data/app.driftwood.Driftwood.gschema.xml +++ b/data/app.driftwood.Driftwood.gschema.xml @@ -32,7 +32,7 @@ Application color scheme: default (follow system), force-light, or force-dark. - false + true Auto scan on startup Whether to automatically scan for AppImages when the application starts. diff --git a/data/resources/style.css b/data/resources/style.css index c44d14b..6e3e64f 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -66,7 +66,29 @@ color: @window_fg_color; } +/* ===== Drop Overlay ===== */ +.drop-overlay-scrim { + background: alpha(@window_bg_color, 0.75); +} + +.drop-zone-card { + background: @card_bg_color; + border-radius: 16px; + padding: 48px 40px; + border: 2px dashed alpha(@accent_bg_color, 0.5); + box-shadow: 0 8px 32px alpha(black, 0.15); +} + +.drop-zone-icon { + color: @accent_bg_color; + opacity: 0.7; +} + /* ===== Card View (using libadwaita .card) ===== */ +.card { + padding: 24px 20px; +} + flowboxchild:focus-visible .card { outline: 2px solid @accent_bg_color; outline-offset: 3px; diff --git a/src/core/database.rs b/src/core/database.rs index 867e90c..ba05303 100644 --- a/src/core/database.rs +++ b/src/core/database.rs @@ -703,7 +703,17 @@ impl Database { last_scanned = datetime('now')", params![path, filename, appimage_type, size_bytes, is_executable, file_modified], )?; - Ok(self.conn.last_insert_rowid()) + // 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) } pub fn update_metadata( diff --git a/src/core/inspector.rs b/src/core/inspector.rs index 2ef70e2..9e08e2f 100644 --- a/src/core/inspector.rs +++ b/src/core/inspector.rs @@ -74,13 +74,70 @@ fn has_unsquashfs() -> bool { .is_ok() } +/// Find the squashfs offset by scanning for a valid superblock in the binary. +/// This avoids executing the AppImage, which can hang for apps with custom AppRun scripts. +fn find_squashfs_offset(path: &Path) -> Result { + let data = fs::read(path)?; + let magic = b"hsqs"; + // Search for squashfs magic after the ELF header (skip first 4KB to avoid false matches) + let start = 4096.min(data.len()); + for i in start..data.len().saturating_sub(96) { + if &data[i..i + 4] == magic { + // Validate: check squashfs superblock version at offset +28 (major) and +30 (minor) + let major = u16::from_le_bytes([data[i + 28], data[i + 29]]); + let minor = u16::from_le_bytes([data[i + 30], data[i + 31]]); + // Valid squashfs 4.0 + if major == 4 && minor == 0 { + // Also check block_size at offset +12 is a power of 2 and reasonable (4KB-1MB) + let block_size = u32::from_le_bytes([data[i + 12], data[i + 13], data[i + 14], data[i + 15]]); + if block_size.is_power_of_two() && block_size >= 4096 && block_size <= 1_048_576 { + return Ok(i as u64); + } + } + } + } + Err(InspectorError::NoOffset) +} + /// Get the squashfs offset from the AppImage by running it with --appimage-offset. +/// Falls back to binary scanning if execution times out or fails. fn get_squashfs_offset(path: &Path) -> Result { - let output = Command::new(path) + // First try the fast binary scan approach (no execution needed) + if let Ok(offset) = find_squashfs_offset(path) { + return Ok(offset); + } + + // Fallback: run the AppImage with a timeout + let child = Command::new(path) .arg("--appimage-offset") .env("APPIMAGE_EXTRACT_AND_RUN", "0") - .output()?; + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()) + .spawn(); + let mut child = match child { + Ok(c) => c, + Err(e) => return Err(InspectorError::IoError(e)), + }; + + // Wait up to 5 seconds + let start = std::time::Instant::now(); + loop { + match child.try_wait() { + Ok(Some(_)) => break, + Ok(None) => { + if start.elapsed() > std::time::Duration::from_secs(5) { + let _ = child.kill(); + let _ = child.wait(); + return Err(InspectorError::NoOffset); + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Err(e) => return Err(InspectorError::IoError(e)), + } + } + + let output = child.wait_with_output()?; let stdout = String::from_utf8_lossy(&output.stdout); stdout .trim() @@ -103,7 +160,10 @@ fn extract_metadata_files( .arg(dest) .arg(appimage_path) .arg("*.desktop") + .arg("usr/share/applications/*.desktop") .arg(".DirIcon") + .arg("*.png") + .arg("*.svg") .arg("usr/share/icons/*") .arg("usr/share/metainfo/*.xml") .arg("usr/share/appdata/*.xml") @@ -135,7 +195,10 @@ fn extract_metadata_files_direct( .arg(dest) .arg(appimage_path) .arg("*.desktop") + .arg("usr/share/applications/*.desktop") .arg(".DirIcon") + .arg("*.png") + .arg("*.svg") .arg("usr/share/icons/*") .arg("usr/share/metainfo/*.xml") .arg("usr/share/appdata/*.xml") @@ -152,8 +215,10 @@ fn extract_metadata_files_direct( } } -/// Find the first .desktop file in a directory. +/// Find the first .desktop file in the extract directory. +/// Checks root level first, then usr/share/applications/. fn find_desktop_file(dir: &Path) -> Option { + // Check root of extract dir if let Ok(entries) = fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); @@ -162,6 +227,16 @@ fn find_desktop_file(dir: &Path) -> Option { } } } + // Check usr/share/applications/ + let apps_dir = dir.join("usr/share/applications"); + if let Ok(entries) = fs::read_dir(&apps_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) == Some("desktop") { + return Some(path); + } + } + } None } @@ -266,9 +341,9 @@ fn detect_architecture(path: &Path) -> Option { /// Find an icon file in the extracted squashfs directory. fn find_icon(extract_dir: &Path, icon_name: Option<&str>) -> Option { - // First try .DirIcon + // First try .DirIcon (skip if it's a broken symlink) let dir_icon = extract_dir.join(".DirIcon"); - if dir_icon.exists() { + if dir_icon.exists() && dir_icon.metadata().is_ok() { return Some(dir_icon); } @@ -282,7 +357,7 @@ fn find_icon(extract_dir: &Path, icon_name: Option<&str>) -> Option { } } - // Check usr/share/icons recursively + // Check usr/share/icons recursively (prefer largest resolution) let icons_dir = extract_dir.join("usr/share/icons"); if icons_dir.exists() { if let Some(found) = find_icon_recursive(&icons_dir, name) { @@ -291,6 +366,19 @@ fn find_icon(extract_dir: &Path, icon_name: Option<&str>) -> Option { } } + // Fallback: grab any .png or .svg at the root level + if let Ok(entries) = fs::read_dir(extract_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + if ext == "png" || ext == "svg" { + return Some(path); + } + } + } + } + None } diff --git a/src/window.rs b/src/window.rs index 363cb2c..ca8f335 100644 --- a/src/window.rs +++ b/src/window.rs @@ -784,11 +784,8 @@ impl DriftwoodWindow { } } - // Auto-scan on startup if enabled - let settings = self.settings(); - if settings.boolean("auto-scan-on-startup") { - self.trigger_scan(); - } + // Always scan on startup to discover new AppImages and complete pending analyses + self.trigger_scan(); // Check for orphaned desktop entries in the background let toast_overlay = self.imp().toast_overlay.get().unwrap().clone(); @@ -878,7 +875,8 @@ impl DriftwoodWindow { let size_unchanged = ex.size_bytes == d.size_bytes as i64; let mtime_unchanged = modified.as_deref() == ex.file_modified.as_deref(); let analysis_done = ex.analysis_status.as_deref() == Some("complete"); - if size_unchanged && mtime_unchanged && analysis_done { + let has_icon = ex.icon_path.is_some(); + if size_unchanged && mtime_unchanged && analysis_done && has_icon { skipped_count += 1; continue; }