Add file manager integration install/uninstall logic
Implements actual extension file creation for Nautilus (Python extension), Nemo (.nemo_action files), Thunar (custom actions XML), and Dolphin (KDE service menu .desktop). Each extension creates a "Process with Pixstrip" submenu with all presets listed. Toggle switches in welcome wizard and settings now call install/uninstall.
This commit is contained in:
430
pixstrip-core/src/fm_integration.rs
Normal file
430
pixstrip-core/src/fm_integration.rs
Normal file
@@ -0,0 +1,430 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::preset::Preset;
|
||||
use crate::storage::PresetStore;
|
||||
|
||||
/// Supported file managers for right-click integration.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileManager {
|
||||
Nautilus,
|
||||
Nemo,
|
||||
Thunar,
|
||||
Dolphin,
|
||||
}
|
||||
|
||||
impl FileManager {
|
||||
/// Human-readable name.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Nautilus => "Nautilus",
|
||||
Self::Nemo => "Nemo",
|
||||
Self::Thunar => "Thunar",
|
||||
Self::Dolphin => "Dolphin",
|
||||
}
|
||||
}
|
||||
|
||||
/// Desktop file ID used to detect if the FM is installed.
|
||||
pub fn desktop_id(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Nautilus => "org.gnome.Nautilus",
|
||||
Self::Nemo => "org.nemo.Nemo",
|
||||
Self::Thunar => "thunar",
|
||||
Self::Dolphin => "org.kde.dolphin",
|
||||
}
|
||||
}
|
||||
|
||||
/// Install the integration extension for this file manager.
|
||||
pub fn install(&self) -> Result<()> {
|
||||
match self {
|
||||
Self::Nautilus => install_nautilus(),
|
||||
Self::Nemo => install_nemo(),
|
||||
Self::Thunar => install_thunar(),
|
||||
Self::Dolphin => install_dolphin(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the integration extension for this file manager.
|
||||
pub fn uninstall(&self) -> Result<()> {
|
||||
match self {
|
||||
Self::Nautilus => uninstall_nautilus(),
|
||||
Self::Nemo => uninstall_nemo(),
|
||||
Self::Thunar => uninstall_thunar(),
|
||||
Self::Dolphin => uninstall_dolphin(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the integration is currently installed.
|
||||
pub fn is_installed(&self) -> bool {
|
||||
match self {
|
||||
Self::Nautilus => nautilus_extension_path().exists(),
|
||||
Self::Nemo => nemo_action_path().exists(),
|
||||
Self::Thunar => thunar_action_path().exists(),
|
||||
Self::Dolphin => dolphin_service_path().exists(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Regenerate the extension files for all installed file managers.
|
||||
/// Call this after presets change so the submenu stays up to date.
|
||||
pub fn regenerate_all() -> Result<()> {
|
||||
for fm in &[FileManager::Nautilus, FileManager::Nemo, FileManager::Thunar, FileManager::Dolphin] {
|
||||
if fm.is_installed() {
|
||||
fm.install()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pixstrip_bin() -> String {
|
||||
// Try to find the pixstrip binary path
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
// If running from the GTK app, find the CLI sibling
|
||||
let dir = exe.parent().unwrap_or(Path::new("/usr/bin"));
|
||||
let cli_path = dir.join("pixstrip");
|
||||
if cli_path.exists() {
|
||||
return cli_path.display().to_string();
|
||||
}
|
||||
let gtk_path = dir.join("pixstrip-gtk");
|
||||
if gtk_path.exists() {
|
||||
return gtk_path.display().to_string();
|
||||
}
|
||||
return exe.display().to_string();
|
||||
}
|
||||
"pixstrip-gtk".into()
|
||||
}
|
||||
|
||||
fn get_preset_names() -> Vec<String> {
|
||||
let mut names: Vec<String> = Preset::all_builtins()
|
||||
.into_iter()
|
||||
.map(|p| p.name)
|
||||
.collect();
|
||||
|
||||
let store = PresetStore::new();
|
||||
if let Ok(user_presets) = store.list() {
|
||||
for p in user_presets {
|
||||
if p.is_custom && !names.contains(&p.name) {
|
||||
names.push(p.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
// --- Nautilus (Python extension) ---
|
||||
|
||||
fn nautilus_extension_dir() -> PathBuf {
|
||||
let data = std::env::var("XDG_DATA_HOME")
|
||||
.unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "~".into());
|
||||
format!("{}/.local/share", home)
|
||||
});
|
||||
PathBuf::from(data).join("nautilus-python").join("extensions")
|
||||
}
|
||||
|
||||
fn nautilus_extension_path() -> PathBuf {
|
||||
nautilus_extension_dir().join("pixstrip-integration.py")
|
||||
}
|
||||
|
||||
fn install_nautilus() -> Result<()> {
|
||||
let dir = nautilus_extension_dir();
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
|
||||
let bin = pixstrip_bin();
|
||||
let presets = get_preset_names();
|
||||
|
||||
let mut preset_items = String::new();
|
||||
for name in &presets {
|
||||
preset_items.push_str(&format!(
|
||||
" item = Nautilus.MenuItem(\n\
|
||||
\x20 name='Pixstrip::Preset::{}',\n\
|
||||
\x20 label='{}',\n\
|
||||
\x20 )\n\
|
||||
\x20 item.connect('activate', self._on_preset, '{}', files)\n\
|
||||
\x20 submenu.append_item(item)\n\n",
|
||||
name.replace(' ', "_"),
|
||||
name,
|
||||
name,
|
||||
));
|
||||
}
|
||||
|
||||
let script = format!(
|
||||
r#"import subprocess
|
||||
from gi.repository import Nautilus, GObject
|
||||
|
||||
class PixstripExtension(GObject.GObject, Nautilus.MenuProvider):
|
||||
def get_file_items(self, files):
|
||||
if not files:
|
||||
return []
|
||||
|
||||
# Only show for image files
|
||||
image_mimes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif',
|
||||
'image/tiff', 'image/avif', 'image/bmp']
|
||||
valid = [f for f in files if f.get_mime_type() in image_mimes or f.is_directory()]
|
||||
if not valid:
|
||||
return []
|
||||
|
||||
top = Nautilus.MenuItem(
|
||||
name='Pixstrip::Menu',
|
||||
label='Process with Pixstrip',
|
||||
icon='applications-graphics-symbolic',
|
||||
)
|
||||
|
||||
submenu = Nautilus.Menu()
|
||||
top.set_submenu(submenu)
|
||||
|
||||
# Open in Pixstrip (wizard)
|
||||
open_item = Nautilus.MenuItem(
|
||||
name='Pixstrip::Open',
|
||||
label='Open in Pixstrip...',
|
||||
)
|
||||
open_item.connect('activate', self._on_open, files)
|
||||
submenu.append_item(open_item)
|
||||
|
||||
# Separator via disabled item
|
||||
sep = Nautilus.MenuItem(
|
||||
name='Pixstrip::Sep',
|
||||
label='---',
|
||||
sensitive=False,
|
||||
)
|
||||
submenu.append_item(sep)
|
||||
|
||||
# Preset items
|
||||
{preset_items}
|
||||
return [top]
|
||||
|
||||
def _on_open(self, menu, files):
|
||||
paths = [f.get_location().get_path() for f in files if f.get_location()]
|
||||
subprocess.Popen(['{bin}', '--files'] + paths)
|
||||
|
||||
def _on_preset(self, menu, preset_name, files):
|
||||
paths = [f.get_location().get_path() for f in files if f.get_location()]
|
||||
subprocess.Popen(['{bin}', '--preset', preset_name, '--files'] + paths)
|
||||
"#,
|
||||
preset_items = preset_items,
|
||||
bin = bin,
|
||||
);
|
||||
|
||||
std::fs::write(nautilus_extension_path(), script)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall_nautilus() -> Result<()> {
|
||||
let path = nautilus_extension_path();
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Nemo (.nemo_action files) ---
|
||||
|
||||
fn nemo_action_dir() -> PathBuf {
|
||||
let data = std::env::var("XDG_DATA_HOME")
|
||||
.unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "~".into());
|
||||
format!("{}/.local/share", home)
|
||||
});
|
||||
PathBuf::from(data).join("nemo").join("actions")
|
||||
}
|
||||
|
||||
fn nemo_action_path() -> PathBuf {
|
||||
nemo_action_dir().join("pixstrip-open.nemo_action")
|
||||
}
|
||||
|
||||
fn install_nemo() -> Result<()> {
|
||||
let dir = nemo_action_dir();
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
|
||||
let bin = pixstrip_bin();
|
||||
|
||||
// Main "Open in Pixstrip" action
|
||||
let open_action = format!(
|
||||
"[Nemo Action]\n\
|
||||
Name=Open in Pixstrip...\n\
|
||||
Comment=Process images with Pixstrip\n\
|
||||
Exec={bin} --files %F\n\
|
||||
Icon-Name=applications-graphics-symbolic\n\
|
||||
Selection=Any\n\
|
||||
Extensions=jpg;jpeg;png;webp;gif;tiff;tif;avif;bmp;\n\
|
||||
Mimetypes=image/*;\n",
|
||||
bin = bin,
|
||||
);
|
||||
std::fs::write(nemo_action_path(), open_action)?;
|
||||
|
||||
// Per-preset actions
|
||||
let presets = get_preset_names();
|
||||
for name in &presets {
|
||||
let safe_name = name.replace(' ', "-").to_lowercase();
|
||||
let action_path = dir.join(format!("pixstrip-preset-{}.nemo_action", safe_name));
|
||||
let action = format!(
|
||||
"[Nemo Action]\n\
|
||||
Name=Pixstrip: {name}\n\
|
||||
Comment=Process with {name} preset\n\
|
||||
Exec={bin} --preset \"{name}\" --files %F\n\
|
||||
Icon-Name=applications-graphics-symbolic\n\
|
||||
Selection=Any\n\
|
||||
Extensions=jpg;jpeg;png;webp;gif;tiff;tif;avif;bmp;\n\
|
||||
Mimetypes=image/*;\n",
|
||||
name = name,
|
||||
bin = bin,
|
||||
);
|
||||
std::fs::write(action_path, action)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall_nemo() -> Result<()> {
|
||||
let dir = nemo_action_dir();
|
||||
// Remove main action
|
||||
let main_path = nemo_action_path();
|
||||
if main_path.exists() {
|
||||
std::fs::remove_file(main_path)?;
|
||||
}
|
||||
// Remove preset actions
|
||||
if dir.exists() {
|
||||
for entry in std::fs::read_dir(&dir)?.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
if name_str.starts_with("pixstrip-preset-") && name_str.ends_with(".nemo_action") {
|
||||
let _ = std::fs::remove_file(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Thunar (Custom Actions via uca.xml) ---
|
||||
|
||||
fn thunar_action_dir() -> PathBuf {
|
||||
let config = std::env::var("XDG_CONFIG_HOME")
|
||||
.unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "~".into());
|
||||
format!("{}/.config", home)
|
||||
});
|
||||
PathBuf::from(config).join("Thunar")
|
||||
}
|
||||
|
||||
fn thunar_action_path() -> PathBuf {
|
||||
thunar_action_dir().join("pixstrip-actions.xml")
|
||||
}
|
||||
|
||||
fn install_thunar() -> Result<()> {
|
||||
let dir = thunar_action_dir();
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
|
||||
let bin = pixstrip_bin();
|
||||
let presets = get_preset_names();
|
||||
|
||||
let mut actions = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<actions>\n");
|
||||
|
||||
// Open in Pixstrip
|
||||
actions.push_str(&format!(
|
||||
" <action>\n\
|
||||
\x20 <icon>applications-graphics-symbolic</icon>\n\
|
||||
\x20 <name>Open in Pixstrip...</name>\n\
|
||||
\x20 <command>{bin} --files %F</command>\n\
|
||||
\x20 <description>Process images with Pixstrip</description>\n\
|
||||
\x20 <patterns>*.jpg;*.jpeg;*.png;*.webp;*.gif;*.tiff;*.tif;*.avif;*.bmp</patterns>\n\
|
||||
\x20 <image-files/>\n\
|
||||
\x20 <directories/>\n\
|
||||
</action>\n",
|
||||
bin = bin,
|
||||
));
|
||||
|
||||
for name in &presets {
|
||||
actions.push_str(&format!(
|
||||
" <action>\n\
|
||||
\x20 <icon>applications-graphics-symbolic</icon>\n\
|
||||
\x20 <name>Pixstrip: {name}</name>\n\
|
||||
\x20 <command>{bin} --preset \"{name}\" --files %F</command>\n\
|
||||
\x20 <description>Process with {name} preset</description>\n\
|
||||
\x20 <patterns>*.jpg;*.jpeg;*.png;*.webp;*.gif;*.tiff;*.tif;*.avif;*.bmp</patterns>\n\
|
||||
\x20 <image-files/>\n\
|
||||
\x20 <directories/>\n\
|
||||
</action>\n",
|
||||
name = name,
|
||||
bin = bin,
|
||||
));
|
||||
}
|
||||
|
||||
actions.push_str("</actions>\n");
|
||||
std::fs::write(thunar_action_path(), actions)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall_thunar() -> Result<()> {
|
||||
let path = thunar_action_path();
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Dolphin (KDE Service Menu) ---
|
||||
|
||||
fn dolphin_service_dir() -> PathBuf {
|
||||
let data = std::env::var("XDG_DATA_HOME")
|
||||
.unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "~".into());
|
||||
format!("{}/.local/share", home)
|
||||
});
|
||||
PathBuf::from(data).join("kio").join("servicemenus")
|
||||
}
|
||||
|
||||
fn dolphin_service_path() -> PathBuf {
|
||||
dolphin_service_dir().join("pixstrip.desktop")
|
||||
}
|
||||
|
||||
fn install_dolphin() -> Result<()> {
|
||||
let dir = dolphin_service_dir();
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
|
||||
let bin = pixstrip_bin();
|
||||
let presets = get_preset_names();
|
||||
|
||||
let mut desktop = format!(
|
||||
"[Desktop Entry]\n\
|
||||
Type=Service\n\
|
||||
X-KDE-ServiceTypes=KonqPopupMenu/Plugin\n\
|
||||
MimeType=image/jpeg;image/png;image/webp;image/gif;image/tiff;image/avif;image/bmp;inode/directory;\n\
|
||||
Actions=Open;{preset_actions}\n\
|
||||
X-KDE-Submenu=Process with Pixstrip\n\
|
||||
Icon=applications-graphics-symbolic\n\n\
|
||||
[Desktop Action Open]\n\
|
||||
Name=Open in Pixstrip...\n\
|
||||
Icon=applications-graphics-symbolic\n\
|
||||
Exec={bin} --files %F\n\n",
|
||||
bin = bin,
|
||||
preset_actions = presets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, _)| format!("Preset{}", i))
|
||||
.collect::<Vec<_>>()
|
||||
.join(";"),
|
||||
);
|
||||
|
||||
for (i, name) in presets.iter().enumerate() {
|
||||
desktop.push_str(&format!(
|
||||
"[Desktop Action Preset{i}]\n\
|
||||
Name={name}\n\
|
||||
Icon=applications-graphics-symbolic\n\
|
||||
Exec={bin} --preset \"{name}\" --files %F\n\n",
|
||||
i = i,
|
||||
name = name,
|
||||
bin = bin,
|
||||
));
|
||||
}
|
||||
|
||||
std::fs::write(dolphin_service_path(), desktop)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall_dolphin() -> Result<()> {
|
||||
let path = dolphin_service_path();
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,6 +3,7 @@ pub mod discovery;
|
||||
pub mod encoder;
|
||||
pub mod error;
|
||||
pub mod executor;
|
||||
pub mod fm_integration;
|
||||
pub mod loader;
|
||||
pub mod operations;
|
||||
pub mod pipeline;
|
||||
|
||||
Reference in New Issue
Block a user