@@ -11,6 +11,7 @@ use anyhow::Context; |
| 11 | 11 | use clap::{Parser, Subcommand}; |
| 12 | 12 | use tracing_subscriber::EnvFilter; |
| 13 | 13 | |
| 14 | +use garshot::annotate::{AnnotationOverlay, AnnotationResult}; |
| 14 | 15 | use garshot::capture::{ |
| 15 | 16 | blend_cursor, capture_full_screen, capture_region, get_cursor_image, Region, |
| 16 | 17 | }; |
@@ -61,6 +62,10 @@ enum Command { |
| 61 | 62 | /// Show desktop notification on save. |
| 62 | 63 | #[arg(long)] |
| 63 | 64 | notify: bool, |
| 65 | + |
| 66 | + /// Open annotation editor after capture. |
| 67 | + #[arg(short, long)] |
| 68 | + annotate: bool, |
| 64 | 69 | }, |
| 65 | 70 | |
| 66 | 71 | /// Capture a region by geometry. |
@@ -92,6 +97,10 @@ enum Command { |
| 92 | 97 | /// Show desktop notification on save. |
| 93 | 98 | #[arg(long)] |
| 94 | 99 | notify: bool, |
| 100 | + |
| 101 | + /// Open annotation editor after capture. |
| 102 | + #[arg(short, long)] |
| 103 | + annotate: bool, |
| 95 | 104 | }, |
| 96 | 105 | |
| 97 | 106 | /// Capture a window. |
@@ -127,6 +136,10 @@ enum Command { |
| 127 | 136 | /// Show desktop notification on save. |
| 128 | 137 | #[arg(long)] |
| 129 | 138 | notify: bool, |
| 139 | + |
| 140 | + /// Open annotation editor after capture. |
| 141 | + #[arg(short, long)] |
| 142 | + annotate: bool, |
| 130 | 143 | }, |
| 131 | 144 | |
| 132 | 145 | /// Interactive region selection with blur overlay. |
@@ -154,6 +167,24 @@ enum Command { |
| 154 | 167 | /// Show desktop notification on save. |
| 155 | 168 | #[arg(long)] |
| 156 | 169 | notify: bool, |
| 170 | + |
| 171 | + /// Open annotation editor after capture. |
| 172 | + #[arg(short, long)] |
| 173 | + annotate: bool, |
| 174 | + }, |
| 175 | + |
| 176 | + /// Annotate an existing image file. |
| 177 | + Annotate { |
| 178 | + /// Input image file to annotate. |
| 179 | + file: PathBuf, |
| 180 | + |
| 181 | + /// Output file path (default: overwrites input, use "-" for stdout). |
| 182 | + #[arg(short, long)] |
| 183 | + output: Option<PathBuf>, |
| 184 | + |
| 185 | + /// Output format (defaults to input format). |
| 186 | + #[arg(short, long)] |
| 187 | + format: Option<String>, |
| 157 | 188 | }, |
| 158 | 189 | |
| 159 | 190 | /// List available monitors. |
@@ -192,6 +223,7 @@ fn main() -> anyhow::Result<()> { |
| 192 | 223 | no_clipboard, |
| 193 | 224 | delay, |
| 194 | 225 | notify, |
| 226 | + annotate, |
| 195 | 227 | }) => { |
| 196 | 228 | if let Some(secs) = delay { |
| 197 | 229 | sleep_with_countdown(secs); |
@@ -202,7 +234,23 @@ fn main() -> anyhow::Result<()> { |
| 202 | 234 | } else { |
| 203 | 235 | Some(output.unwrap_or_else(|| default_output(&config, &format))) |
| 204 | 236 | }; |
| 205 | | - let (data, width, height) = capture_screen_raw(&format, monitor.as_deref(), cursor)?; |
| 237 | + let (mut data, mut width, mut height) = capture_screen_raw(&format, monitor.as_deref(), cursor)?; |
| 238 | + |
| 239 | + // Open annotation editor if requested |
| 240 | + if annotate { |
| 241 | + match run_annotation(&data, width, height)? { |
| 242 | + Some((new_data, new_w, new_h)) => { |
| 243 | + data = new_data; |
| 244 | + width = new_w; |
| 245 | + height = new_h; |
| 246 | + } |
| 247 | + None => { |
| 248 | + tracing::info!("Annotation cancelled"); |
| 249 | + return Ok(()); |
| 250 | + } |
| 251 | + } |
| 252 | + } |
| 253 | + |
| 206 | 254 | if let Some(ref path) = output_path { |
| 207 | 255 | save_image(&data, width, height, path, &format)?; |
| 208 | 256 | if !no_clipboard { |
@@ -225,6 +273,7 @@ fn main() -> anyhow::Result<()> { |
| 225 | 273 | no_clipboard, |
| 226 | 274 | delay, |
| 227 | 275 | notify, |
| 276 | + annotate, |
| 228 | 277 | }) => { |
| 229 | 278 | if let Some(secs) = delay { |
| 230 | 279 | sleep_with_countdown(secs); |
@@ -235,7 +284,23 @@ fn main() -> anyhow::Result<()> { |
| 235 | 284 | } else { |
| 236 | 285 | Some(output.unwrap_or_else(|| default_output(&config, &format))) |
| 237 | 286 | }; |
| 238 | | - let (data, width, height) = capture_region_raw(&geometry, cursor)?; |
| 287 | + let (mut data, mut width, mut height) = capture_region_raw(&geometry, cursor)?; |
| 288 | + |
| 289 | + // Open annotation editor if requested |
| 290 | + if annotate { |
| 291 | + match run_annotation(&data, width, height)? { |
| 292 | + Some((new_data, new_w, new_h)) => { |
| 293 | + data = new_data; |
| 294 | + width = new_w; |
| 295 | + height = new_h; |
| 296 | + } |
| 297 | + None => { |
| 298 | + tracing::info!("Annotation cancelled"); |
| 299 | + return Ok(()); |
| 300 | + } |
| 301 | + } |
| 302 | + } |
| 303 | + |
| 239 | 304 | if let Some(ref path) = output_path { |
| 240 | 305 | save_image(&data, width, height, path, &format)?; |
| 241 | 306 | if !no_clipboard { |
@@ -259,6 +324,7 @@ fn main() -> anyhow::Result<()> { |
| 259 | 324 | no_clipboard, |
| 260 | 325 | delay, |
| 261 | 326 | notify, |
| 327 | + annotate, |
| 262 | 328 | }) => { |
| 263 | 329 | if let Some(secs) = delay { |
| 264 | 330 | sleep_with_countdown(secs); |
@@ -269,7 +335,23 @@ fn main() -> anyhow::Result<()> { |
| 269 | 335 | } else { |
| 270 | 336 | Some(output.unwrap_or_else(|| default_output(&config, &format))) |
| 271 | 337 | }; |
| 272 | | - let (data, width, height) = capture_window_raw(id.as_deref(), decorations, cursor)?; |
| 338 | + let (mut data, mut width, mut height) = capture_window_raw(id.as_deref(), decorations, cursor)?; |
| 339 | + |
| 340 | + // Open annotation editor if requested |
| 341 | + if annotate { |
| 342 | + match run_annotation(&data, width, height)? { |
| 343 | + Some((new_data, new_w, new_h)) => { |
| 344 | + data = new_data; |
| 345 | + width = new_w; |
| 346 | + height = new_h; |
| 347 | + } |
| 348 | + None => { |
| 349 | + tracing::info!("Annotation cancelled"); |
| 350 | + return Ok(()); |
| 351 | + } |
| 352 | + } |
| 353 | + } |
| 354 | + |
| 273 | 355 | if let Some(ref path) = output_path { |
| 274 | 356 | save_image(&data, width, height, path, &format)?; |
| 275 | 357 | if !no_clipboard { |
@@ -291,6 +373,7 @@ fn main() -> anyhow::Result<()> { |
| 291 | 373 | blur, |
| 292 | 374 | no_clipboard, |
| 293 | 375 | notify, |
| 376 | + annotate, |
| 294 | 377 | }) => { |
| 295 | 378 | let is_stdout = output.as_ref().map(|p| p.as_os_str() == "-").unwrap_or(false); |
| 296 | 379 | let output_path = if is_stdout { |
@@ -298,7 +381,22 @@ fn main() -> anyhow::Result<()> { |
| 298 | 381 | } else { |
| 299 | 382 | Some(output.unwrap_or_else(|| default_output(&config, &format))) |
| 300 | 383 | }; |
| 301 | | - if let Some((data, width, height)) = capture_select_raw(cursor, blur)? { |
| 384 | + if let Some((mut data, mut width, mut height)) = capture_select_raw(cursor, blur)? { |
| 385 | + // Open annotation editor if requested |
| 386 | + if annotate { |
| 387 | + match run_annotation(&data, width, height)? { |
| 388 | + Some((new_data, new_w, new_h)) => { |
| 389 | + data = new_data; |
| 390 | + width = new_w; |
| 391 | + height = new_h; |
| 392 | + } |
| 393 | + None => { |
| 394 | + tracing::info!("Annotation cancelled"); |
| 395 | + return Ok(()); |
| 396 | + } |
| 397 | + } |
| 398 | + } |
| 399 | + |
| 302 | 400 | if let Some(ref path) = output_path { |
| 303 | 401 | save_image(&data, width, height, path, &format)?; |
| 304 | 402 | if !no_clipboard { |
@@ -330,6 +428,42 @@ fn main() -> anyhow::Result<()> { |
| 330 | 428 | } |
| 331 | 429 | } |
| 332 | 430 | |
| 431 | + Some(Command::Annotate { file, output, format }) => { |
| 432 | + // Load image file |
| 433 | + let img = image::open(&file).context("Failed to open image file")?; |
| 434 | + let rgba = img.to_rgba8(); |
| 435 | + let width = rgba.width(); |
| 436 | + let height = rgba.height(); |
| 437 | + let data = rgba.into_raw(); |
| 438 | + |
| 439 | + tracing::info!("Opening annotation editor for {}", file.display()); |
| 440 | + |
| 441 | + // Run annotation |
| 442 | + match run_annotation(&data, width, height)? { |
| 443 | + Some((annotated_data, w, h)) => { |
| 444 | + // Determine output path and format |
| 445 | + let out_path = output.unwrap_or_else(|| file.clone()); |
| 446 | + let is_stdout = out_path.as_os_str() == "-"; |
| 447 | + let out_format = format.unwrap_or_else(|| { |
| 448 | + file.extension() |
| 449 | + .and_then(|e| e.to_str()) |
| 450 | + .unwrap_or("png") |
| 451 | + .to_string() |
| 452 | + }); |
| 453 | + |
| 454 | + if is_stdout { |
| 455 | + write_stdout(&annotated_data, w, h, &out_format)?; |
| 456 | + } else { |
| 457 | + save_image(&annotated_data, w, h, &out_path, &out_format)?; |
| 458 | + println!("{}", out_path.display()); |
| 459 | + } |
| 460 | + } |
| 461 | + None => { |
| 462 | + tracing::info!("Annotation cancelled"); |
| 463 | + } |
| 464 | + } |
| 465 | + } |
| 466 | + |
| 333 | 467 | Some(Command::Daemon) => { |
| 334 | 468 | run_daemon()?; |
| 335 | 469 | } |
@@ -670,3 +804,21 @@ fn run_daemon() -> anyhow::Result<()> { |
| 670 | 804 | tracing::info!("Daemon stopped"); |
| 671 | 805 | Ok(()) |
| 672 | 806 | } |
| 807 | + |
| 808 | +/// Run the annotation overlay and return the annotated image data. |
| 809 | +/// Returns None if the user cancelled. |
| 810 | +fn run_annotation( |
| 811 | + data: &[u8], |
| 812 | + width: u32, |
| 813 | + height: u32, |
| 814 | +) -> anyhow::Result<Option<(Vec<u8>, u32, u32)>> { |
| 815 | + let overlay = AnnotationOverlay::new(data, width, height) |
| 816 | + .context("Failed to create annotation overlay")?; |
| 817 | + |
| 818 | + match overlay.run().context("Annotation overlay failed")? { |
| 819 | + AnnotationResult::Save { data, width, height } => { |
| 820 | + Ok(Some((data, width, height))) |
| 821 | + } |
| 822 | + AnnotationResult::Cancel => Ok(None), |
| 823 | + } |
| 824 | +} |