shellshot/
app.rs

1use clap::Parser;
2use thiserror::Error;
3
4use crate::{
5    image_generator::{self, SaveError},
6    image_renderer::{ImageRenderer, ImageRendererError},
7    os_command::{ExecutorError, PlatformExecutor},
8    screen_builder::{ScreenBuilder, ScreenBuilderError},
9    window_decoration::{create_window_decoration, WindowDecorationType},
10    Executor,
11};
12
13/// Errors that can occur in `shellshot`
14#[derive(Error, Debug)]
15pub enum ShellshotError {
16    #[error("Failed to execute command: {0}")]
17    CommandExecution(#[from] ExecutorError),
18
19    #[error("Failed to build screen from output: {0}")]
20    ScreenBuild(#[from] ScreenBuilderError),
21
22    #[error("Failed to render image: {0}")]
23    ImageRender(#[from] ImageRendererError),
24
25    #[error("Failed to save image to file: {0}")]
26    Save(#[from] SaveError),
27}
28
29/// Command-line arguments for `shellshot`
30#[derive(Parser, Debug)]
31#[command(
32    name = "shellshot",
33    about = "Transform command-line output into stunning, shareable images",
34    version,
35    long_about = None
36)]
37pub struct Args {
38    /// Command to execute
39    #[arg(trailing_var_arg = true)]
40    pub command: Vec<String>,
41
42    /// Do not draw window decorations
43    #[arg(long)]
44    pub no_decoration: bool,
45
46    /// Specify decoration style
47    #[arg(long, short = 'd', default_value = "classic")]
48    pub decoration: WindowDecorationType,
49
50    /// Specify output filename (default: out.png)
51    #[arg(long, short = 'f', default_value = "out.png")]
52    pub filename: Option<String>,
53
54    /// Save to clipboard
55    #[arg(long)]
56    pub clipboard: bool,
57}
58
59/// Main entry point for shellshot logic
60///
61/// # Errors
62///
63/// Returns an error if:
64/// - Command execution fails
65/// - Screen building fails
66/// - Image rendering fails
67/// - Saving the image fails
68pub fn run_shellshot(args: Args) -> Result<(), ShellshotError> {
69    println!("Starting shellshot v{}", env!("CARGO_PKG_VERSION"));
70
71    let command_str = args.command.join(" ");
72    let output = PlatformExecutor::execute_command(&command_str)?;
73
74    let decoration = (!args.no_decoration).then_some(args.decoration);
75    let window_decoration = create_window_decoration(decoration.as_ref());
76
77    let screen = ScreenBuilder::from_output(&output, &command_str, window_decoration.as_ref())?;
78    let image_data = ImageRenderer::render_image(&screen, window_decoration)?;
79
80    if args.clipboard {
81        image_generator::save_to_clipboard(&image_data)?;
82        println!("✅ Screenshot saved to clipboard");
83    }
84
85    let filename = args.filename.unwrap_or_else(|| "out.png".to_string());
86    image_generator::save_to_file(&image_data, &filename)?;
87    println!("✅ Screenshot saved to {filename}");
88
89    Ok(())
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_execute_command_with_file() {
98        let args = Args {
99            command: vec!["echo".into(), "hello".into()],
100            no_decoration: false,
101            decoration: WindowDecorationType::Classic,
102            filename: Some("test.png".into()),
103            clipboard: false,
104        };
105
106        let result = run_shellshot(args);
107        assert!(result.is_ok());
108    }
109}