Skip to content

Commit 22e2ea7

Browse files
committed
Linux: native file icons via freedesktop-icons
- Replace file_icon_provider (GTK, main-thread-only) with freedesktop-icons + mime_guess on Linux (pure Rust, thread-safe) - ext → MIME type → XDG icon name → theme lookup (Adwaita, Yaru, etc.) - file_icon_provider moved to macOS-only dependency - SVGs skipped (no renderer); falls back through generic icon names
1 parent 944085f commit 22e2ea7

8 files changed

Lines changed: 237 additions & 15 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ There are two MCP servers available to you:
155155
- When getting oriented, consider the docs: `docs` folder and `CLAUDE.md` files in each directory.
156156
- When coming up with a plan for a development, save it to `docs/specs/{feature}-plan.md` in this repo (we clean out old
157157
plans every few weeks/months, git history remembers them).
158+
- Don't enter "Plan mode" unless specifically asked to.
158159
- When writing a plan, always capture the INTENTION behind the plan, not just the steps. That way, the implementing
159160
agent or human will know the "why"s behind the decisions and can adapt dynamically if it makes an unexpected discovery
160161
during implementation.

Cargo.lock

Lines changed: 87 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ serde_json = "1"
3939
notify = "8"
4040
dirs = "6"
4141
uzers = "0.12.2"
42-
file_icon_provider = "0.4.0"
4342
image = "0.25.9"
4443
base64 = "0.22.1"
4544
rayon = "1.11.0"
@@ -91,6 +90,10 @@ keyring = "3"
9190
cocoon = "0.4"
9291
# D-Bus client for XDG Desktop Portal (accent color, appearance settings)
9392
zbus = "5"
93+
# Native file icons: XDG icon theme lookup (pure Rust, no GTK dependency)
94+
freedesktop-icons = "0.4"
95+
# MIME type guessing from file extensions (for icon name resolution)
96+
mime_guess = "2"
9497

9598
[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies]
9699
# MTP (Android device) support via pure Rust implementation
@@ -105,6 +108,7 @@ smb = "0.11.1"
105108
smb-rpc = "=0.11.1"
106109

107110
[target.'cfg(target_os = "macos")'.dependencies]
111+
file_icon_provider = "0.4.0"
108112
core-foundation = "0.10.1"
109113
core-services = "1.0.0"
110114
icns = "0.3.1"

apps/desktop/src-tauri/src/icons.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66
77
use crate::config::ICON_SIZE;
88
use base64::Engine;
9-
use file_icon_provider::get_file_icon;
109
use image::{DynamicImage, ImageFormat, imageops::FilterType};
1110
use rayon::prelude::*;
1211
use std::collections::HashMap;
1312
use std::io::Cursor;
1413
use std::path::{Path, PathBuf};
1514
use std::sync::RwLock;
1615

16+
// file_icon_provider uses GTK on Linux which requires main-thread access and
17+
// fails silently from rayon/tokio threads. On Linux we use freedesktop-icons instead.
18+
#[cfg(target_os = "macos")]
19+
use file_icon_provider::get_file_icon;
20+
1721
/// Cache for generated icons (icon_id -> base64 WebP data URL)
1822
static ICON_CACHE: RwLock<Option<HashMap<String, String>>> = RwLock::new(None);
1923

@@ -82,7 +86,8 @@ fn image_to_data_url(img: &DynamicImage) -> Option<String> {
8286
Some(format!("data:image/webp;base64,{}", base64))
8387
}
8488

85-
/// Fetches icon for a specific file path.
89+
/// Fetches icon for a specific file path via the OS icon provider (macOS).
90+
#[cfg(target_os = "macos")]
8691
fn fetch_icon_for_path(path: &Path) -> Option<String> {
8792
// Get icon from OS (size is u16)
8893
let icon = get_file_icon(path, ICON_SIZE as u16).ok()?;
@@ -94,6 +99,19 @@ fn fetch_icon_for_path(path: &Path) -> Option<String> {
9499
image_to_data_url(&dynamic_img)
95100
}
96101

102+
/// Fetches icon for a specific file path via XDG icon theme lookup.
103+
#[cfg(target_os = "linux")]
104+
fn fetch_icon_for_path(path: &Path) -> Option<String> {
105+
let icon_id = if path.is_dir() {
106+
"dir".to_string()
107+
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
108+
format!("ext:{}", ext.to_lowercase())
109+
} else {
110+
"file".to_string()
111+
};
112+
crate::linux_icons::get_icon_for_id(&icon_id, ICON_SIZE as u16).and_then(|img| image_to_data_url(&img))
113+
}
114+
97115
/// Gets icon for a path as base64 data URL.
98116
/// Public API for use by volumes module.
99117
pub fn get_icon_for_path(path: &str) -> Option<String> {
@@ -157,7 +175,17 @@ pub fn get_icons(icon_ids: Vec<String>, use_app_icons_for_documents: bool) -> Ha
157175
#[cfg(not(target_os = "macos"))]
158176
let _ = use_app_icons_for_documents;
159177

160-
// Default path: use sample file approach
178+
// Linux: look up directly from XDG icon theme (no temp files needed)
179+
#[cfg(target_os = "linux")]
180+
if let Some(img) = crate::linux_icons::get_icon_for_id(&icon_id, ICON_SIZE as u16)
181+
&& let Some(data_url) = image_to_data_url(&img)
182+
{
183+
cache_icon(icon_id.clone(), data_url.clone());
184+
result.insert(icon_id, data_url);
185+
continue;
186+
}
187+
188+
// macOS/Windows: use sample file approach (Launch Services / Shell)
161189
if let Some(sample_path) = get_sample_path_for_icon_id(&icon_id)
162190
&& let Some(data_url) = fetch_icon_for_path(&sample_path)
163191
{

apps/desktop/src-tauri/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ mod indexing;
8181
pub mod licensing;
8282
#[cfg(target_os = "linux")]
8383
pub(crate) mod linux_distro;
84+
#[cfg(target_os = "linux")]
85+
mod linux_icons;
8486
#[cfg(target_os = "macos")]
8587
mod macos_icons;
8688
mod mcp;

0 commit comments

Comments
 (0)