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.
431 lines
13 KiB
Rust
431 lines
13 KiB
Rust
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(())
|
|
}
|