diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 85f2947..a26f712 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/package.json b/frontend/package.json index 9002825..d6c9ebb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-fs": "~2", "@tauri-apps/plugin-http": "~2", + "@tauri-apps/plugin-log": "~2", "@tauri-apps/plugin-opener": "^2", "clsx": "^2.1.1", "fuse.js": "^7.1.0", diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index ce6331d..31e14e0 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "base64 0.21.7", "chrono", "cocoa", + "log", "notify", "serde", "serde_json", diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 66abbcd..4827672 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -27,6 +27,7 @@ tokio = { version = "1.36.0", features = ["full"] } tauri-plugin-store = "2.0.0-beta.12" tauri-plugin-http = "2.0.0-beta.12" chrono = "0.4" +log = "0.4" tauri-plugin-log = "2" tauri-plugin-sharetarget = "0.1.6" tauri-plugin-fs = "2" diff --git a/frontend/src-tauri/src/commands.rs b/frontend/src-tauri/src/commands.rs index ec471e6..0b4b645 100644 --- a/frontend/src-tauri/src/commands.rs +++ b/frontend/src-tauri/src/commands.rs @@ -1,3 +1,4 @@ +use crate::screenshot::take_area_screenshot; use crate::state::SharedWatcherState; use crate::utils::process_png_file; use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; @@ -69,3 +70,8 @@ pub async fn handle_selected_folder( Ok(format!("Now watching directory: {}", path)) } + +#[tauri::command] +pub fn take_screenshot(app: AppHandle) -> Result { + take_area_screenshot(&app) +} diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 480344e..c3fec08 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -15,6 +15,7 @@ pub fn desktop() { let watcher_state = new_shared_watcher_state(); tauri::Builder::default() + .plugin(tauri_plugin_log::Builder::new().build()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_http::init()) @@ -24,6 +25,7 @@ pub fn desktop() { .manage(watcher_state) .invoke_handler(tauri::generate_handler![ commands::handle_selected_folder, + commands::take_screenshot, shortcut::change_shortcut, shortcut::unregister_shortcut, shortcut::get_current_shortcut, diff --git a/frontend/src-tauri/src/screenshot.rs b/frontend/src-tauri/src/screenshot.rs index 1b78861..a20f1e1 100644 --- a/frontend/src-tauri/src/screenshot.rs +++ b/frontend/src-tauri/src/screenshot.rs @@ -1,8 +1,55 @@ use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; -use std::fs; -use std::process::Command; +use std::process::{Command, Output}; +use std::{fs, path::PathBuf}; use tauri::{AppHandle, Emitter, Runtime}; +#[cfg(target_os = "macos")] +fn screenshot(path: &PathBuf) -> Result { + Command::new("screencapture") + .arg("-i") // interactive selection + .arg("-x") // don't play sound + .arg("-o") // don't show cursor + .arg("-r") // don't add shadow + .arg(path.to_str().unwrap()) + .output() + .map_err(|e| format!("Failed to execute screencapture: {}", e)) +} + +#[cfg(target_os = "linux")] +fn screenshot(path: &PathBuf) -> Result { + let slurp_output = Command::new("slurp") + .output() + .map_err(|e| format!("Failed to execute screencapture: {}", e))?; + + if !slurp_output.status.success() { + let stderr = String::from_utf8_lossy(&slurp_output.stderr); + if slurp_output.status.code() == Some(1) && stderr.is_empty() { + log::warn!("slurp cancelled by user."); + return Err("Screenshot cancelled by user.".to_string()); + } + return Err(format!( + "slurp failed. Status: {:?}, Stderr: {}", + slurp_output.status, stderr + )); + } + + let geometry = String::from_utf8(slurp_output.stdout) + .map_err(|e| format!("slurp output is not valid UTF-8: {}", e))? + .trim() + .to_string(); + + if geometry.is_empty() { + return Err("slurp succeeded but returned empty geometry".to_string()); + } + + Command::new("grim") + .arg("-g") + .arg(&geometry) + .arg(path.to_str().unwrap()) + .output() + .map_err(|e| format!("Failed to execute screencapture: {}", e)) +} + /// Takes a screenshot of a selected area and returns the image data as base64 pub fn take_area_screenshot(app: &AppHandle) -> Result { // Create a temporary file path @@ -10,15 +57,10 @@ pub fn take_area_screenshot(app: &AppHandle) -> Result(app: &AppHandle) -> Result { const [processingImage, setProcessingImage] = @@ -25,34 +26,41 @@ export const App = () => { }); }); - createEffect(() => { - let listener: PluginListener; - const setupListener = async () => { - listener = await listenForShareEvents( - async (intent: ShareEvent) => { - const contents = await readFile(intent.stream).catch( - (error: Error) => { - console.warn("fetching shared content failed:"); - throw error; - }, - ); - setFile( - new File([contents], intent.name ?? "no-name", { - type: intent.content_type, - }), - ); - setLogs((l) => [...l, intent.uri]); - }, - ); - }; - setupListener(); - return () => { - listener?.unregister(); - }; - }); + // createEffect(() => { + // let listener: PluginListener; + // const setupListener = async () => { + // listener = await listenForShareEvents( + // async (intent: ShareEvent) => { + // const contents = await readFile(intent.stream ?? "").catch( + // (error: Error) => { + // console.warn("fetching shared content failed:"); + // throw error; + // }, + // ); + // setFile( + // new File([contents], intent.name ?? "no-name", { + // type: intent.content_type, + // }), + // ); + // setLogs((l) => [...l, intent.uri]); + // }, + // ); + // }; + // setupListener(); + // return () => { + // listener?.unregister(); + // }; + // }); + + const onTakeScreenshot = () => { + invoke("take_screenshot"); + }; return ( <> +