use std::time::Duration;
use clap::{ArgGroup, Parser};
use thiserror::Error;
use tracing::info;
use crate::{
image_generator::{self, SaveError},
image_renderer::{ImageRenderer, ImageRendererError},
pty_executor::{PtyExecutor, PtyExecutorError, PtyOptions, dimension::Dimension},
terminal_builder::TerminalBuilderError,
theme::{Theme, ThemeError},
window_decoration::{WindowDecorationType, create_window_decoration},
};
#[derive(Error, Debug)]
pub enum ShellshotError {
#[error("Failed to execute command: {0}")]
CommandExecution(#[from] PtyExecutorError),
#[error("Failed to load theme: {0}")]
ThemeError(#[from] ThemeError),
#[error("Failed to build terminal from output: {0}")]
TerminalBuild(#[from] TerminalBuilderError),
#[error("Failed to render image: {0}")]
ImageRender(#[from] ImageRendererError),
#[error("Failed to save image to file: {0}")]
Save(#[from] SaveError),
}
#[derive(Parser, Debug)]
#[command(
name = "shellshot",
about = "Transform your command-line output into clean, shareable images with a single command.",
version,
long_about = None,
group(ArgGroup::new("output_mode")
.required(true)
.args(&["output", "clipboard"])
)
)]
pub struct Args {
#[arg(trailing_var_arg = true, required = true)]
pub command: Vec<String>,
#[arg(long, short = 'q')]
pub quiet: bool,
#[arg(long, conflicts_with = "decoration")]
pub no_decoration: bool,
#[arg(
long,
short = 'd',
default_value = "classic",
conflicts_with = "no_decoration"
)]
pub decoration: WindowDecorationType,
#[arg(long)]
pub theme: Option<String>,
#[arg(long, short = 'o', conflicts_with = "clipboard")]
pub output: Option<String>,
#[arg(long, conflicts_with = "output")]
pub clipboard: bool,
#[arg(long, short = 'W', default_value = "auto")]
pub width: Dimension,
#[arg(long, short = 'H', default_value = "auto")]
pub height: Dimension,
#[arg(long, short = 't')]
pub timeout: Option<u64>,
#[arg(long)]
pub shell: bool,
}
pub fn run_shellshot(args: Args) -> Result<(), ShellshotError> {
let pty_options = PtyOptions {
cols: args.width,
rows: args.height,
timeout: args.timeout.map(Duration::from_secs),
shell: args.shell,
quiet: args.quiet,
};
let decoration = (!args.no_decoration).then_some(args.decoration);
let window_decoration = create_window_decoration(decoration.as_ref());
let theme = if let Some(theme_source) = args.theme {
Theme::load(&theme_source)?
} else {
Theme::default()
};
let screen = PtyExecutor::run_command(&pty_options, &args.command)?;
let image_data = ImageRenderer::render_image(&args.command, &screen, window_decoration, theme)?;
if args.clipboard {
image_generator::save_to_clipboard(&image_data)?;
info!("✅ Screenshot saved to clipboard");
}
if let Some(output) = args.output {
image_generator::save_to_file(&image_data, &output)?;
info!("✅ Screenshot saved to {output}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
#[test]
fn test_execute_command_with_file() {
let base_command = vec!["echo".to_string(), "hello".to_string()];
let command: Vec<String> = if cfg!(windows) && base_command[0] == "echo" {
vec!["cmd".into(), "/C".into(), base_command[1..].join(" ")]
} else {
base_command
};
let tmp = tempdir().unwrap();
let nested = tmp.path().join("nested/folder/test.png");
let args = Args {
command,
quiet: true,
no_decoration: false,
decoration: WindowDecorationType::Classic,
theme: None,
output: Some(nested.to_str().unwrap().to_string()),
clipboard: false,
width: Dimension::Auto,
height: Dimension::Auto,
timeout: None,
shell: false,
};
let result = run_shellshot(args);
assert!(result.is_ok());
assert!(nested.exists());
}
}