Add real pause, desktop notifications, undo, accessibility, CLI watch

This commit is contained in:
2026-03-06 12:38:16 +02:00
parent b21b9edb36
commit 9efcbd082e
5 changed files with 753 additions and 26 deletions

View File

@@ -93,11 +93,45 @@ enum Commands {
action: PresetAction,
},
/// Manage watch folders
Watch {
#[command(subcommand)]
action: WatchAction,
},
/// View processing history
History,
/// Undo last batch operation
Undo,
/// Undo last batch operation (moves output files to trash)
Undo {
/// Undo the last N batches (default 1)
#[arg(long, default_value = "1")]
last: usize,
},
}
#[derive(Subcommand)]
enum WatchAction {
/// Add a watch folder with a linked preset
Add {
/// Folder path to watch
path: String,
/// Preset name to apply
#[arg(long)]
preset: String,
/// Watch subdirectories recursively
#[arg(short, long)]
recursive: bool,
},
/// List configured watch folders
List,
/// Remove a watch folder
Remove {
/// Folder path to remove
path: String,
},
/// Start watching configured folders (blocks until Ctrl+C)
Start,
}
#[derive(Subcommand)]
@@ -160,8 +194,18 @@ fn main() {
PresetAction::Export { name, output } => cmd_preset_export(&name, &output),
PresetAction::Import { path } => cmd_preset_import(&path),
},
Commands::Watch { action } => match action {
WatchAction::Add {
path,
preset,
recursive,
} => cmd_watch_add(&path, &preset, recursive),
WatchAction::List => cmd_watch_list(),
WatchAction::Remove { path } => cmd_watch_remove(&path),
WatchAction::Start => cmd_watch_start(),
},
Commands::History => cmd_history(),
Commands::Undo => cmd_undo(),
Commands::Undo { last } => cmd_undo(last),
}
}
@@ -420,17 +464,49 @@ fn cmd_history() {
}
}
fn cmd_undo() {
fn cmd_undo(count: usize) {
let history = HistoryStore::new();
match history.list() {
Ok(entries) => {
if let Some(last) = entries.last() {
println!("Last operation: {} images from {}", last.total, last.input_dir);
println!("Output files would be moved to trash.");
println!("(Undo with file deletion not yet implemented - requires gio trash support)");
} else {
if entries.is_empty() {
println!("No processing history to undo.");
return;
}
let to_undo = entries.iter().rev().take(count);
let mut total_trashed = 0;
for entry in to_undo {
if entry.output_files.is_empty() {
println!(
"Batch from {} has no recorded output files - cannot undo",
entry.timestamp
);
continue;
}
println!(
"Undoing batch: {} images from {}",
entry.total, entry.input_dir
);
for file_path in &entry.output_files {
let path = PathBuf::from(file_path);
if path.exists() {
// Move to OS trash using the trash crate
match trash::delete(&path) {
Ok(()) => {
total_trashed += 1;
}
Err(e) => {
eprintln!(" Failed to trash {}: {}", path.display(), e);
}
}
}
}
}
println!("{} files moved to trash", total_trashed);
}
Err(e) => {
eprintln!("Failed to read history: {}", e);
@@ -439,6 +515,183 @@ fn cmd_undo() {
}
}
fn cmd_watch_add(path: &str, preset_name: &str, recursive: bool) {
// Verify the preset exists
let _preset = find_preset(preset_name);
let watch_path = PathBuf::from(path);
if !watch_path.exists() {
eprintln!("Watch folder does not exist: {}", path);
std::process::exit(1);
}
// Save watch folder config
let watch = pixstrip_core::watcher::WatchFolder {
path: watch_path,
preset_name: preset_name.to_string(),
recursive,
active: true,
};
// Store in config
let config_dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("~/.config"))
.join("pixstrip");
let watches_path = config_dir.join("watches.json");
let mut watches: Vec<pixstrip_core::watcher::WatchFolder> = if watches_path.exists() {
std::fs::read_to_string(&watches_path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
} else {
Vec::new()
};
// Don't add duplicate paths
if watches.iter().any(|w| w.path == watch.path) {
println!("Watch folder already configured: {}", path);
return;
}
watches.push(watch);
let _ = std::fs::create_dir_all(&config_dir);
let _ = std::fs::write(&watches_path, serde_json::to_string_pretty(&watches).unwrap());
println!("Added watch: {} -> preset '{}'", path, preset_name);
}
fn cmd_watch_list() {
let config_dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("~/.config"))
.join("pixstrip");
let watches_path = config_dir.join("watches.json");
let watches: Vec<pixstrip_core::watcher::WatchFolder> = if watches_path.exists() {
std::fs::read_to_string(&watches_path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
} else {
Vec::new()
};
if watches.is_empty() {
println!("No watch folders configured.");
println!("Use 'pixstrip watch add <path> --preset <name>' to add one.");
return;
}
println!("Configured watch folders:");
for watch in &watches {
let recursive_str = if watch.recursive { " (recursive)" } else { "" };
let status = if watch.active { "active" } else { "inactive" };
println!(
" {} -> '{}' [{}]{}",
watch.path.display(),
watch.preset_name,
status,
recursive_str
);
}
}
fn cmd_watch_remove(path: &str) {
let config_dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("~/.config"))
.join("pixstrip");
let watches_path = config_dir.join("watches.json");
let mut watches: Vec<pixstrip_core::watcher::WatchFolder> = if watches_path.exists() {
std::fs::read_to_string(&watches_path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
} else {
Vec::new()
};
let original_len = watches.len();
let target = PathBuf::from(path);
watches.retain(|w| w.path != target);
if watches.len() == original_len {
println!("Watch folder not found: {}", path);
return;
}
let _ = std::fs::write(&watches_path, serde_json::to_string_pretty(&watches).unwrap());
println!("Removed watch folder: {}", path);
}
fn cmd_watch_start() {
let config_dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("~/.config"))
.join("pixstrip");
let watches_path = config_dir.join("watches.json");
let watches: Vec<pixstrip_core::watcher::WatchFolder> = if watches_path.exists() {
std::fs::read_to_string(&watches_path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
} else {
Vec::new()
};
let active: Vec<_> = watches.iter().filter(|w| w.active).collect();
if active.is_empty() {
println!("No active watch folders. Use 'pixstrip watch add' first.");
return;
}
println!("Starting watch on {} folder(s)...", active.len());
for w in &active {
println!(" {} -> '{}'", w.path.display(), w.preset_name);
}
println!("Press Ctrl+C to stop.");
let (tx, rx) = std::sync::mpsc::channel();
let mut watchers = Vec::new();
for watch in &active {
let watcher = pixstrip_core::watcher::FolderWatcher::new();
if let Err(e) = watcher.start(watch, tx.clone()) {
eprintln!("Failed to start watching {}: {}", watch.path.display(), e);
continue;
}
watchers.push((watcher, watch.preset_name.clone()));
}
// Process incoming files
for event in &rx {
match event {
pixstrip_core::watcher::WatchEvent::NewImage(path) => {
println!("New image: {}", path.display());
// Find which watcher this came from and use its preset
if let Some((_, preset_name)) = watchers.first() {
let preset = find_preset(preset_name);
let input_dir = path.parent().unwrap_or_else(|| std::path::Path::new(".")).to_path_buf();
let output_dir = input_dir.join("processed");
let mut job = preset.to_job(&input_dir, &output_dir);
job.add_source(&path);
let executor = PipelineExecutor::new();
match executor.execute(&job, |_| {}) {
Ok(r) => println!(" Processed: {} -> {}", format_bytes(r.total_input_bytes), format_bytes(r.total_output_bytes)),
Err(e) => eprintln!(" Failed: {}", e),
}
}
}
pixstrip_core::watcher::WatchEvent::Error(err) => {
eprintln!("Watch error: {}", err);
}
}
}
for (w, _) in &watchers {
w.stop();
}
}
// --- Helpers ---
fn find_preset(name: &str) -> Preset {