Add real pause, desktop notifications, undo, accessibility, CLI watch
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user