diff --git a/.gitignore b/.gitignore index 5a4f6354..2622eacf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ target *.gz *.out .direnv +*.jpg +*.jpeg +*.png +*.ppm +*.qoi diff --git a/wayshot/src/cli.rs b/wayshot/src/cli.rs index 182737b2..a076af5c 100644 --- a/wayshot/src/cli.rs +++ b/wayshot/src/cli.rs @@ -1,43 +1,45 @@ +use std::path::PathBuf; + use clap::arg; use clap::Parser; +use crate::utils::EncodingFormat; +use clap::builder::TypedValueParser; + #[derive(Parser)] #[command(version, about)] pub struct Cli { - /// Enable debug mode - #[arg(short, long, action = clap::ArgAction::SetTrue)] - pub debug: bool, + /// Where to save the screenshot, "-" for stdout. Defaults to "$UNIX_TIMESTAMP-wayshot.$EXTENSION". + #[arg(value_name = "OUTPUT")] + pub file: Option, + + /// Log level to be used for printing to stderr + #[arg(long, default_value = "info", value_parser = clap::builder::PossibleValuesParser::new(["trace", "debug", "info", "warn", "error"]).map(|s| -> tracing::Level{ s.parse().unwrap()}))] + pub log_level: tracing::Level, /// Arguments to call slurp with for selecting a region #[arg(short, long, value_name = "SLURP_ARGS")] pub slurp: Option, - /// Mention a custom file path - #[arg(short, long, conflicts_with = "stdout", value_name = "FILE_PATH")] - pub file: Option, - - /// Output the image data to standard out - #[arg(long, conflicts_with = "file", action = clap::ArgAction::SetTrue)] - pub stdout: bool, - /// Enable cursor in screenshots - #[arg(short, long, action = clap::ArgAction::SetTrue)] + #[arg(short, long)] pub cursor: bool, - /// Set image encoder (Png is default) - #[arg(short, long, value_name = "FILE_EXTENSION")] - pub extension: Option, + /// Set image encoder, by default uses the file extension from the OUTPUT + /// positional argument. Otherwise defaults to png. + #[arg(long, visible_aliases = ["extension", "format", "output-format"], value_name = "FILE_EXTENSION")] + pub encoding: Option, /// List all valid outputs - #[arg(short, long, alias = "listoutputs", action = clap::ArgAction::SetTrue)] + #[arg(short, long, alias = "listoutputs")] pub list_outputs: bool, - /// Choose a particular display to screenshot + /// Choose a particular output/display to screenshot #[arg(short, long, conflicts_with = "slurp")] pub output: Option, - /// Present a fuzzy selector for outputs - #[arg(long, alias = "chooseoutput", conflicts_with_all = ["slurp", "output"], action = clap::ArgAction::SetTrue)] + /// Present a fuzzy selector for output/display selection + #[arg(long, alias = "chooseoutput", conflicts_with_all = ["slurp", "output"])] pub choose_output: bool, } diff --git a/wayshot/src/utils.rs b/wayshot/src/utils.rs index cea4217d..1e394452 100644 --- a/wayshot/src/utils.rs +++ b/wayshot/src/utils.rs @@ -1,6 +1,11 @@ -use eyre::{ContextCompat, Result}; +use clap::ValueEnum; +use eyre::{bail, ContextCompat, Error, Result}; + use std::{ + fmt::Display, + path::PathBuf, process::exit, + str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; @@ -47,18 +52,24 @@ pub fn parse_geometry(g: &str) -> Result { } /// Supported image encoding formats. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] pub enum EncodingFormat { - /// Jpeg / jpg encoder. + /// JPG/JPEG encoder. Jpg, - /// Png encoder. + /// PNG encoder. Png, - /// Ppm encoder. + /// PPM encoder. Ppm, - /// Qoi encoder. + /// Qut encoder. Qoi, } +impl Default for EncodingFormat { + fn default() -> Self { + Self::Png + } +} + impl From for image::ImageOutputFormat { fn from(format: EncodingFormat) -> Self { match format { @@ -70,6 +81,33 @@ impl From for image::ImageOutputFormat { } } +impl TryFrom<&PathBuf> for EncodingFormat { + type Error = Error; + + fn try_from(value: &PathBuf) -> std::result::Result { + value + .extension() + .wrap_err_with(|| { + format!( + "no extension in {} to deduce encoding format", + value.display() + ) + }) + .and_then(|ext| { + ext.to_str().wrap_err_with(|| { + format!("extension in {} is not valid unicode", value.display()) + }) + }) + .and_then(|ext| ext.parse()) + } +} + +impl Display for EncodingFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", Into::<&str>::into(*self)) + } +} + impl From for &str { fn from(format: EncodingFormat) -> Self { match format { @@ -81,7 +119,21 @@ impl From for &str { } } -pub fn get_default_file_name(extension: EncodingFormat) -> String { +impl FromStr for EncodingFormat { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + Ok(match s { + "jpg" | "jpeg" => Self::Jpg, + "png" => Self::Png, + "ppm" => Self::Ppm, + "qoi" => Self::Qoi, + _ => bail!("unsupported extension '{s}'"), + }) + } +} + +pub fn get_default_file_name(extension: EncodingFormat) -> PathBuf { let time = match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(n) => n.as_secs().to_string(), Err(_) => { @@ -90,5 +142,5 @@ pub fn get_default_file_name(extension: EncodingFormat) -> String { } }; - time + "-wayshot." + extension.into() + (time + "-wayshot." + extension.into()).into() } diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index 8e3bc8af..b5632caf 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -11,9 +11,7 @@ mod cli; mod utils; use dialoguer::{theme::ColorfulTheme, FuzzySelect}; -use tracing::Level; - -use crate::utils::EncodingFormat; +use utils::EncodingFormat; fn select_ouput(ouputs: &[T]) -> Option where @@ -32,41 +30,39 @@ where fn main() -> Result<()> { let cli = cli::Cli::parse(); - let level = if cli.debug { Level::TRACE } else { Level::INFO }; tracing_subscriber::fmt() - .with_max_level(level) + .with_max_level(cli.log_level) .with_writer(std::io::stderr) .init(); - let extension = if let Some(extension) = cli.extension { - let ext = extension.trim().to_lowercase(); - tracing::debug!("Using custom extension: {:#?}", ext); - - match ext.as_str() { - "jpeg" | "jpg" => EncodingFormat::Jpg, - "png" => EncodingFormat::Png, - "ppm" => EncodingFormat::Ppm, - "qoi" => EncodingFormat::Qoi, - _ => { - tracing::error!("Invalid extension provided.\nValid extensions:\n1) jpeg\n2) jpg\n3) png\n4) ppm\n5) qoi"); - exit(1); + let input_encoding = cli + .file + .as_ref() + .and_then(|pathbuf| pathbuf.try_into().ok()); + let requested_encoding = cli + .encoding + .or(input_encoding) + .unwrap_or(EncodingFormat::default()); + + if let Some(input_encoding) = input_encoding { + if input_encoding != requested_encoding { + tracing::warn!( + "The encoding requested '{requested_encoding}' does not match the output file's encoding '{input_encoding}'. Still using the requested encoding however.", + ); + } + } + + let file = match cli.file { + Some(pathbuf) => { + if pathbuf.to_string_lossy() == "-" { + None + } else { + Some(pathbuf) } } - } else { - EncodingFormat::Png + None => Some(utils::get_default_file_name(requested_encoding)), }; - let mut file_is_stdout: bool = false; - let mut file_path: Option = None; - - if cli.stdout { - file_is_stdout = true; - } else if let Some(filepath) = cli.file { - file_path = Some(filepath.trim().to_string()); - } else { - file_path = Some(utils::get_default_file_name(extension)); - } - let wayshot_conn = WayshotConnection::new()?; if cli.list_outputs { @@ -117,16 +113,16 @@ fn main() -> Result<()> { wayshot_conn.screenshot_all(cli.cursor)? }; - if file_is_stdout { + if let Some(file) = file { + image_buffer.save(file)?; + } else { let stdout = stdout(); let mut buffer = Cursor::new(Vec::new()); let mut writer = BufWriter::new(stdout.lock()); - image_buffer.write_to(&mut buffer, extension)?; + image_buffer.write_to(&mut buffer, requested_encoding)?; writer.write_all(buffer.get_ref())?; - } else { - image_buffer.save(file_path.unwrap())?; } Ok(())