Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"private": true,
"version": "0.2.5",
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
Expand Down Expand Up @@ -47,6 +50,7 @@
"@vitejs/plugin-react": "^4.7.0",
"@vitest/coverage-v8": "^4.0.17",
"autoprefixer": "^10.4.23",
"happy-dom": "^20.6.1",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

happy-dom@20.x requires Node >= 20 (per its engines). Might be worth double-checking CI/dev tooling is pinned to Node 20+ so installs/tests don’t start failing unexpectedly.

"jsdom": "^27.4.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
Expand Down
48 changes: 43 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capabilities for Better Shot windows",
"windows": ["main", "quick-overlay"],
"windows": [
"main",
"quick-overlay",
"region-selector"
],
"permissions": [
"core:default",
"core:window:default",
"core:window:allow-set-fullscreen",
"core:window:allow-set-always-on-top",
"core:window:allow-set-decorations",
"core:window:allow-set-resizable",
"core:window:allow-set-size",
"core:window:allow-set-position",
"core:window:allow-set-focus",
Expand Down Expand Up @@ -46,4 +51,4 @@
"autostart:allow-disable",
"autostart:allow-is-enabled"
]
}
}
106 changes: 105 additions & 1 deletion src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::Mutex;
use tauri::{AppHandle, Manager};
use tauri::{AppHandle, Emitter, Manager};

#[cfg(target_os = "macos")]
use objc2::msg_send;
Expand Down Expand Up @@ -512,3 +512,107 @@ pub async fn native_capture_ocr_region(save_dir: String) -> Result<String, Strin

Ok(recognized_text)
}

/// Open region selector window with captured screenshots
/// Captures all monitors and opens a fullscreen region selector window
#[tauri::command]
pub async fn open_region_selector(
app_handle: AppHandle,
save_dir: String,
) -> Result<(), String> {
// Capture all monitors
let monitor_shots = capture_monitors(&save_dir)?;

// Create the region selector window if it doesn't exist
let window_label = "region-selector";

if let Some(existing_window) = app_handle.get_webview_window(window_label) {
// Close existing window if any
existing_window.close().ok();
}

// Create new fullscreen window for region selection
let window = tauri::WebviewWindowBuilder::new(
&app_handle,
window_label,
tauri::WebviewUrl::App("index.html?region-selector=1".into()),
)
.title("Region Selector")
.fullscreen(true)
.decorations(false)
.resizable(false)
.always_on_top(true)
.visible(false) // Start hidden, will show after setup
.build()
.map_err(|e| format!("Failed to create region selector window: {}", e))?;

// Show the window first
window
.show()
.map_err(|e| format!("Failed to show region selector window: {}", e))?;

// Give the React component time to mount and set up event listeners
// Increased from 100ms to 800ms to ensure reliable event delivery
// Use async sleep to avoid blocking the runtime thread
tauri::async_runtime::sleep(std::time::Duration::from_millis(800)).await;

// Emit event with screenshot data to the window
window
.emit(
"region-selector-show",
serde_json::json!({
"screenshotPath": monitor_shots.first().map(|m| m.path.clone()).unwrap_or_default(),
"monitorShots": monitor_shots,
}),
)
.map_err(|e| format!("Failed to emit region selector event: {}", e))?;

Ok(())
}

/// Emit capture complete event to main window
#[tauri::command]
pub async fn emit_capture_complete(app_handle: AppHandle, path: String) -> Result<(), String> {
if let Some(main_window) = app_handle.get_webview_window("main") {
main_window
.emit("capture-complete", serde_json::json!({ "path": path }))
.map_err(|e| format!("Failed to emit capture complete: {}", e))?;

// Restore and focus main window
main_window.show().ok();
main_window.set_focus().ok();
}
Ok(())
}

/// Clean up a temporary file safely
#[tauri::command]
pub async fn cleanup_temp_file(path: String) -> Result<(), String> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleanup_temp_file currently lets the frontend delete any path on disk. At minimum, consider restricting deletes to the OS temp dir (after canonicalization) to avoid accidentally removing user files.

Suggested change
pub async fn cleanup_temp_file(path: String) -> Result<(), String> {
pub async fn cleanup_temp_file(path: String) -> Result<(), String> {
let temp_dir = std::env::temp_dir().canonicalize().unwrap_or_else(|_| std::env::temp_dir());
let canonical = std::path::PathBuf::from(&path)
.canonicalize()
.map_err(|e| format!("Failed to resolve temp file path: {}", e))?;
if !canonical.starts_with(&temp_dir) {
return Err("Refusing to delete non-temp file".to_string());
}
std::fs::remove_file(&canonical).map_err(|e| format!("Failed to remove temp file: {}", e))
}

// Security check: ensure the file is within the system temp directory
let temp_dir = std::env::temp_dir();
let canonical_temp = temp_dir.canonicalize().unwrap_or(temp_dir);

let path_buf = PathBuf::from(&path);
let canonical_path = path_buf.canonicalize().map_err(|e| format!("Invalid path: {}", e))?;

if !canonical_path.starts_with(&canonical_temp) {
return Err("Security violation: Attempted to delete file outside of temp directory".to_string());
}

std::fs::remove_file(&canonical_path)
.map_err(|e| format!("Failed to remove temp file: {}", e))
}

/// Restore the main window
#[tauri::command]
pub async fn restore_main_window(app_handle: AppHandle) -> Result<(), String> {
if let Some(main_window) = app_handle.get_webview_window("main") {
main_window
.show()
.map_err(|e| format!("Failed to show main window: {}", e))?;
main_window
.set_focus()
.map_err(|e| format!("Failed to focus main window: {}", e))?;
}
Ok(())
}
18 changes: 12 additions & 6 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ mod screenshot;
mod utils;

use commands::{
capture_all_monitors, capture_once, capture_region, copy_image_file_to_clipboard,
get_desktop_directory, get_mouse_position, get_temp_directory, move_window_to_active_space,
native_capture_fullscreen, native_capture_interactive, native_capture_window,
native_capture_ocr_region, play_screenshot_sound, render_image_with_effects_rust, save_edited_image,
capture_all_monitors, capture_region, capture_once, cleanup_temp_file,
copy_image_file_to_clipboard, emit_capture_complete, get_desktop_directory,
get_mouse_position, get_temp_directory, move_window_to_active_space,
native_capture_fullscreen, native_capture_interactive, native_capture_ocr_region,
native_capture_window, open_region_selector, play_screenshot_sound,
render_image_with_effects_rust, restore_main_window, save_edited_image,
};

use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
Expand Down Expand Up @@ -102,7 +104,7 @@ pub fn run() {
.center()
.resizable(true)
.decorations(true)
.visible(false) // Start hidden
.visible(cfg!(debug_assertions)) // Show on startup for development only
.build()?;

// Handle close request - hide instead of quit
Expand Down Expand Up @@ -256,7 +258,11 @@ pub fn run() {
play_screenshot_sound,
get_mouse_position,
move_window_to_active_space,
copy_image_file_to_clipboard
copy_image_file_to_clipboard,
open_region_selector,
emit_capture_complete,
cleanup_temp_file,
restore_main_window
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
Loading