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;
}