diff --git a/src/config.rs b/src/config.rs index 1542154..fde811d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,7 @@ pub struct Config { pub capture: Capture, pub preview: Preview, pub zoom: Zoom, + pub editor: Editor, } #[derive(Debug, Clone, PartialEq, Deserialize)] @@ -51,6 +52,21 @@ impl Default for Zoom { } } +#[derive(Debug, Clone, PartialEq, Deserialize, Default)] +#[serde(default)] +pub struct Editor { + pub backend: Backend, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum Backend { + /// `valw edit`. + #[default] + Builtin, + Satty, +} + impl Default for Preview { fn default() -> Self { Self { @@ -157,12 +173,12 @@ mod tests { #[test] fn unknown_keys_are_reported_not_rejected() { - let text = "[editor]\nbackend = \"satty\"\n\n[save]\nfolder = \"x\"\n"; + let text = "[toolbar]\nposition = \"top\"\n\n[save]\nfolder = \"x\"\n"; let (config, unknown) = parse(text).unwrap(); assert_eq!(config, Config::default()); assert_eq!( unknown, - vec!["editor".to_string(), "save.folder".to_string()] + vec!["save.folder".to_string(), "toolbar".to_string()] ); } @@ -220,6 +236,14 @@ mod tests { ); } + #[test] + fn editor_backend_default_and_values() { + assert_eq!(Config::default().editor.backend, Backend::Builtin); + let (config, _) = parse("[editor]\nbackend = \"satty\"\n").unwrap(); + assert_eq!(config.editor.backend, Backend::Satty); + assert!(parse("[editor]\nbackend = \"gimp\"\n").is_err()); + } + #[test] fn missing_file_gives_defaults() { let dir = tempfile::tempdir().unwrap(); diff --git a/src/host.rs b/src/host.rs index 86be708..dd27a80 100644 --- a/src/host.rs +++ b/src/host.rs @@ -24,6 +24,7 @@ use wayland_client::{Connection, QueueHandle, protocol::wl_surface::WlSurface}; use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape; use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; +use crate::config::{self, Backend}; use crate::dnd::{self, Outcome}; use crate::ipc::{self, Reply, Request}; use crate::lock::Lock; @@ -500,26 +501,50 @@ fn add(state: &mut State, path: &Path, output_name: &str) -> Result<()> { Ok(()) } -/// Opens the screenshot in Satty; saving there overwrites the file. +/// The command that opens `path` in the configured editor; both save over +/// the file. +fn editor_command(backend: Backend, path: &Path, exe: &Path) -> std::process::Command { + let mut command = match backend { + Backend::Builtin => { + let mut c = std::process::Command::new(exe); + c.arg("edit").arg(path); + c + } + Backend::Satty => { + let mut c = std::process::Command::new("satty"); + c.arg("--filename") + .arg(path) + .arg("--output-filename") + .arg(path); + c + } + }; + command + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()); + command +} + +/// Opens the screenshot in the configured editor. fn open_editor(path: &Path) { use std::os::unix::process::CommandExt; - use std::process::{Command, Stdio}; - let mut command = Command::new("satty"); - command - .arg("--filename") - .arg(path) - .arg("--output-filename") - .arg(path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); + let backend = match config::load(&config::default_path()) { + Ok(c) => c.editor.backend, + Err(e) => { + tracing::warn!("using the built-in editor: {e:#}"); + Backend::Builtin + } + }; + let exe = std::env::current_exe().unwrap_or_else(|_| "valw".into()); + let mut command = editor_command(backend, path, &exe); // SAFETY: setsid is async-signal-safe. unsafe { command.pre_exec(|| rustix::process::setsid().map(drop).map_err(Into::into)); } if let Err(e) = command.spawn() { - tracing::warn!("could not start satty: {e}"); + tracing::warn!("could not start the editor ({backend:?}): {e}"); } } @@ -527,6 +552,29 @@ fn open_editor(path: &Path) { mod tests { use super::*; + #[test] + fn editor_command_per_backend() { + let path = Path::new("/p/Shot 1.png"); + let exe = Path::new("/nix/store/x/bin/valw"); + let builtin = editor_command(Backend::Builtin, path, exe); + assert_eq!(builtin.get_program(), exe); + assert_eq!( + builtin.get_args().collect::>(), + ["edit", "/p/Shot 1.png"] + ); + let satty = editor_command(Backend::Satty, path, exe); + assert_eq!(satty.get_program(), "satty"); + assert_eq!( + satty.get_args().collect::>(), + [ + "--filename", + "/p/Shot 1.png", + "--output-filename", + "/p/Shot 1.png" + ] + ); + } + #[test] fn hidden_until_every_hider_leaves() { let mut core = Core::new(Instant::now());