file manager
git clone https://git.lucas.co/cce-files.git
Implement XDG MIME association support for file identification and launching
src/main.rs | 43 ++++++++++++++++++--------
src/services/fs.rs | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 119 insertions(+), 14 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 12d201d..8fec0a3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -680,7 +680,7 @@ impl Application for FilesystemApp {
*needs_rebuild = true;
self.needs_rebuild = true;
- if is_file_double_click && self.select_mode {
+ if is_file_double_click {
self.update(Message::SelectOpen, needs_rebuild, _exit);
}
}
@@ -711,8 +711,14 @@ impl Application for FilesystemApp {
if path.is_dir() && !is_project_dir(&path) {
self.fs_service.send(services::fs::FsRequest::ReadDirectory(path));
} else {
- println!("{}", path.display());
- std::process::exit(0);
+ if self.select_mode {
+ println!("{}", path.display());
+ std::process::exit(0);
+ } else {
+ let _ = std::process::Command::new("xdg-open")
+ .arg(&path)
+ .spawn();
+ }
}
} else {
let selected_path = if let Some(idx) = self.browse.selected {
@@ -724,18 +730,34 @@ impl Application for FilesystemApp {
if path.is_dir() && !is_project_dir(&path) {
self.fs_service.send(services::fs::FsRequest::ReadDirectory(path));
} else {
- println!("{}", path.display());
- std::process::exit(0);
+ if self.select_mode {
+ println!("{}", path.display());
+ std::process::exit(0);
+ } else {
+ let _ = std::process::Command::new("xdg-open")
+ .arg(&path)
+ .spawn();
+ }
}
}
}
+
}
}
Message::SelectCancel => {
std::process::exit(1);
}
Message::PromptOpenWith(path) => {
- let mut tb = cce_ui::widget::TextBox::new(String::new())
+ let default_cmd = if let Some(mime) = crate::services::fs::get_mime_type(&path) {
+ if let Some((_, cmd)) = crate::services::fs::get_default_application(&mime) {
+ cmd
+ } else {
+ String::new()
+ }
+ } else {
+ String::new()
+ };
+ let mut tb = cce_ui::widget::TextBox::new(default_cmd)
.with_max_width(None)
.with_placeholder("Program/Command");
tb.focus();
@@ -1470,13 +1492,8 @@ impl Application for FilesystemApp {
if let Some(entry) = self.browse.entries.get(idx) {
if entry.is_dir && !is_project_dir(&entry.path) {
return Some(Message::Browse(pages::browse::BrowseMessage::NavigateTo(idx)));
- } else if self.select_mode && !self.select_directory {
- if self.save_mode {
- return Some(Message::SelectOpen);
- } else {
- println!("{}", entry.path.display());
- std::process::exit(0);
- }
+ } else {
+ return Some(Message::SelectOpen);
}
}
} else if self.save_mode {
diff --git a/src/services/fs.rs b/src/services/fs.rs
index 5bacbcb..3fc240b 100644
--- a/src/services/fs.rs
+++ b/src/services/fs.rs
@@ -206,7 +206,17 @@ fn load_preview_data_internal(path: &Path) -> PreviewData {
})
.unwrap_or_else(|| "—".to_string());
- let file_type = infer_file_type(&name, is_dir);
+ let mut file_type = infer_file_type(&name, is_dir);
+ if !is_dir {
+ if let Some(mime) = get_mime_type(path) {
+ if let Some((app_name, _)) = get_default_application(&mime) {
+ file_type = format!("{} ({}) [Open with: {}]", file_type, mime, app_name);
+ } else {
+ file_type = format!("{} ({})", file_type, mime);
+ }
+ }
+ }
+
// Check symlink target
let target = if meta.as_ref().map(|m| m.file_type().is_symlink()).unwrap_or(false) {
@@ -375,3 +385,81 @@ pub fn save_last_dir_internal(dir: &Path) {
let _ = fs::write(path, dir.to_string_lossy().as_bytes());
}
}
+
+pub fn get_mime_type(path: &Path) -> Option<String> {
+ let output = std::process::Command::new("xdg-mime")
+ .args(&["query", "filetype"])
+ .arg(path)
+ .output()
+ .ok()?;
+ if output.status.success() {
+ let mime = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if !mime.is_empty() {
+ return Some(mime);
+ }
+ }
+ None
+}
+
+pub fn get_default_application(mime: &str) -> Option<(String, String)> {
+ // Query default handler desktop file name
+ let output = std::process::Command::new("xdg-mime")
+ .args(&["query", "default", mime])
+ .output()
+ .ok()?;
+ if !output.status.success() {
+ return None;
+ }
+ let desktop_filename = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if desktop_filename.is_empty() {
+ return None;
+ }
+
+ // Search for .desktop file in common directories
+ let home = std::env::var("HOME").ok().unwrap_or_default();
+ let search_paths = vec![
+ PathBuf::from(&home).join(".local/share/applications"),
+ PathBuf::from("/usr/share/applications"),
+ PathBuf::from("/usr/local/share/applications"),
+ ];
+
+ let mut desktop_path = None;
+ for dir in search_paths {
+ let path = dir.join(&desktop_filename);
+ if path.exists() {
+ desktop_path = Some(path);
+ break;
+ }
+ }
+
+ let path = desktop_path?;
+ let content = fs::read_to_string(path).ok()?;
+
+ let mut name = None;
+ let mut exec = None;
+
+ for line in content.lines() {
+ let line = line.trim();
+ if line.starts_with("Name=") && name.is_none() {
+ name = Some(line["Name=".len()..].trim().to_string());
+ } else if line.starts_with("Exec=") && exec.is_none() {
+ let mut cmd = line["Exec=".len()..].trim().to_string();
+ // Strip standard desktop entry field codes (placeholders)
+ let placeholders = ["%f", "%F", "%u", "%U", "%d", "%D", "%n", "%N", "%i", "%c", "%k", "%v"];
+ for placeholder in &placeholders {
+ cmd = cmd.replace(placeholder, "");
+ }
+ exec = Some(cmd.trim().to_string());
+ }
+ }
+
+ match (name, exec) {
+ (Some(n), Some(e)) => Some((n, e)),
+ (None, Some(e)) => {
+ let n_fallback = desktop_filename.strip_suffix(".desktop").unwrap_or(&desktop_filename).to_string();
+ Some((n_fallback, e))
+ }
+ _ => None,
+ }
+}
+