What That Claude DO?! Screenshot tooling. Also there is a visual bug when creating a screenshot when moving

This commit is contained in:
2026-05-03 23:58:55 +02:00
parent 01057c1c38
commit 7fe112293e
12 changed files with 5939 additions and 1 deletions
+180
View File
@@ -0,0 +1,180 @@
//! rs-pictures — Wayland screenshot tool
//!
//! Flow:
//! 1. Load config from ~/.config/rs-pictures/config.toml
//! 2. Sleep briefly so the user can switch away from the terminal that
//! launched us, and the compositor has time to repaint.
//! 3. Capture all outputs → frozen desktop snapshot for overlay background.
//! 4. Open fullscreen selection overlay (drag / two-click rubber-band).
//! 5. Sleep 120 ms so compositor repaints after overlay closes.
//! 6. Capture the selected region with libwayshot.
//! 7a. auto_save/auto_copy set → apply effects, act silently, exit.
//! 7b. Otherwise → open review window (effects applied interactively there).
mod capture;
mod config;
mod effects;
mod hyprland;
mod overlay;
mod review;
use anyhow::{Context, Result};
use arboard::{Clipboard, ImageData};
use eframe::egui;
use overlay::{SelectionOverlay, SelectionResult};
use review::ReviewWindow;
fn main() -> Result<()> {
// ── 1. Load config ────────────────────────────────────────────────────────
let config = config::Config::load().context("Failed to load config")?;
// ── 2. Query Hyprland metadata (no window open yet) ───────────────────────
// Scale is only needed for window-rect conversion in the overlay.
let scale = hyprland::active_monitor_scale();
// ── 3. Pre-capture delay ──────────────────────────────────────────────────
// Give the compositor time to unmap the terminal/launcher that started us
// and repaint the desktop before we freeze it.
// Configurable via capture_delay_ms in config.toml (default 800ms).
std::thread::sleep(std::time::Duration::from_millis(config.capture_delay_ms));
// ── 4. Capture full desktop BEFORE opening any window ────────────────────
// This must happen before eframe::run_native is called. eframe maps its
// window immediately on entry — before our first update() frame — causing
// a white rectangle to appear in the snapshot if we capture any later.
let background_snapshot =
capture::capture_all_outputs().context("Failed to capture desktop snapshot")?;
// ── 5. Run the selection overlay ─────────────────────────────────────────
let selection_result = {
use std::sync::{Arc, Mutex};
let shared: Arc<Mutex<Option<SelectionResult>>> = Arc::new(Mutex::new(None));
let shared_clone = Arc::clone(&shared);
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_fullscreen(true)
.with_decorations(false)
.with_transparent(true)
.with_always_on_top()
.with_active(false)
.with_resizable(false),
..Default::default()
};
let bg_clone = background_snapshot.clone();
eframe::run_native(
"rs-pictures — Select Region",
native_options,
Box::new(move |cc| {
let app = SelectionOverlay::new(cc, bg_clone, scale);
Ok(Box::new(OverlayWrapper {
inner: app,
result_sink: shared_clone,
}) as Box<dyn eframe::App>)
}),
)
.map_err(|e| anyhow::anyhow!("Overlay window error: {e}"))?;
Arc::try_unwrap(shared)
.ok()
.and_then(|m| m.into_inner().ok())
.flatten()
};
// ── 6. Act on the selection ───────────────────────────────────────────────
let region = match selection_result {
Some(SelectionResult::Selected(r)) => r,
Some(SelectionResult::Cancelled) | None => {
eprintln!("Selection cancelled.");
return Ok(());
}
};
// ── 7. Crop the selected region from the original snapshot ────────────────
let raw_image = {
let px = (region.x.max(0) as u32).min(background_snapshot.width().saturating_sub(1));
let py = (region.y.max(0) as u32).min(background_snapshot.height().saturating_sub(1));
let pw = region.width.min(background_snapshot.width() - px);
let ph = region.height.min(background_snapshot.height() - py);
image::imageops::crop_imm(&background_snapshot, px, py, pw, ph).to_image()
};
// ── 8a. Auto-mode — no review window ─────────────────────────────────────
if config.auto_save || config.auto_copy {
let final_image = effects::apply_effects(raw_image, &config.effects);
if config.auto_save {
let path = config.output_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Cannot create dir {}", parent.display()))?;
}
final_image.save(&path)
.with_context(|| format!("Failed to save to {}", path.display()))?;
eprintln!("Saved to {}", path.display());
}
if config.auto_copy {
clipboard_copy(&final_image)?;
eprintln!("Copied to clipboard.");
}
return Ok(());
}
// ── 8b. Review window ─────────────────────────────────────────────────────
// The review window applies effects internally on a background thread, so
// we hand it the raw (un-effected) image.
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_title("rs-pictures — Review")
.with_inner_size([900.0, 700.0])
.with_min_inner_size([400.0, 300.0]),
..Default::default()
};
eframe::run_native(
"rs-pictures — Review",
native_options,
Box::new(move |cc| {
Ok(Box::new(ReviewWindow::new(cc, raw_image, config)) as Box<dyn eframe::App>)
}),
)
.map_err(|e| anyhow::anyhow!("Review window error: {e}"))?;
Ok(())
}
// ─── Clipboard helper (shared with review.rs logic) ───────────────────────────
pub fn clipboard_copy(img: &image::RgbaImage) -> Result<()> {
let (w, h) = img.dimensions();
let bytes = img.as_raw().clone();
let mut cb = Clipboard::new().context("Could not open clipboard")?;
cb.set_image(ImageData {
width: w as usize,
height: h as usize,
bytes: bytes.into(),
})
.context("Failed to write image to clipboard")
}
// ─── Overlay wrapper ──────────────────────────────────────────────────────────
use std::sync::{Arc, Mutex};
struct OverlayWrapper {
inner: SelectionOverlay,
result_sink: Arc<Mutex<Option<SelectionResult>>>,
}
impl eframe::App for OverlayWrapper {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.inner.update(ctx, frame);
if let Some(result) = self.inner.take_result() {
*self.result_sink.lock().unwrap() = Some(result);
}
}
fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
self.inner.clear_color(visuals)
}
}