Skip to content
Merged
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
15 changes: 14 additions & 1 deletion src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,13 +775,26 @@ pub async fn hide_main_window(app: tauri::AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("main") {
// hide() removes the window in ~1 frame; minimize() plays a ~250ms
// macOS genie animation that has to be waited out before capturing.
// Instant hide is what lets the screenshot flow feel snappy.
// Instant hide is what lets the screenshot flow feel snappy. Used where
// the window is restored automatically a moment later (screenshots).
window.hide().map_err(|e: tauri::Error| e.to_string())?;
println!("[Window] Main window hidden");
}
Ok(())
}

/// Minimize (not hide) the main window. Used for recording, where the window
/// stays out of view for the whole session: a minimized window can be brought
/// back from the dock, but a fully-hidden one can't — which locked users out.
#[tauri::command]
pub async fn minimize_main_window(app: tauri::AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("main") {
window.minimize().map_err(|e: tauri::Error| e.to_string())?;
println!("[Window] Main window minimized");
}
Ok(())
}

#[tauri::command]
pub async fn show_main_window(app: tauri::AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("main") {
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ fn main() {
commands::show_countdown,
commands::close_countdown,
commands::hide_main_window,
commands::minimize_main_window,
commands::show_main_window,
commands::get_cursor_position,
commands::check_permissions,
Expand Down
69 changes: 36 additions & 33 deletions src/islands/media/ScreenRecorder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export default function ScreenRecorder() {
const extRef = useRef('webm');
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const windowHiddenRef = useRef(false);
// Always holds the latest toggle logic so the hotkey callback never goes stale.
const toggleRecordingRef = useRef<() => void>(() => {});

useEffect(() => {
// Check if recording is supported (browser or Tauri)
Expand Down Expand Up @@ -105,41 +107,33 @@ export default function ScreenRecorder() {

useEffect(() => () => { if (resultUrl) URL.revokeObjectURL(resultUrl); }, [resultUrl]);

// Register global hotkey for screen recording (Tauri only)
// Register the global recording hotkey ONCE (Tauri only). Registering it in an
// effect keyed on [recording, stopping] re-registered on every start/stop; the
// async unregister/register raced and could hit "already registered", silently
// killing the shortcut. A single stable registration that reads the ref fixes it.
useEffect(() => {
if (!inTauriApp) return;

let hotkeyId: string | null = null;

const registerHotkey = async () => {
try {
hotkeyId = await hotkeyService.register(
'CommandOrControl+Shift+R',
() => {
console.log('[ScreenRecorder] Global hotkey triggered');
// Toggle recording: start if not recording, stop if recording
if (recording) {
stop();
} else if (!stopping) {
start();
}
},
'Toggle screen recording'
);
console.log('[ScreenRecorder] Registered global hotkey:', hotkeyId);
} catch (err) {
console.warn('[ScreenRecorder] Failed to register hotkey:', err);
}
};

registerHotkey();
hotkeyService
.register(
'CommandOrControl+Shift+R',
() => {
console.log('[ScreenRecorder] Recording hotkey triggered');
toggleRecordingRef.current();
},
'Toggle screen recording',
)
.then((id) => {
hotkeyId = id;
console.log('[ScreenRecorder] Registered recording hotkey:', id);
})
.catch((err) => console.warn('[ScreenRecorder] Failed to register hotkey:', err));

return () => {
if (hotkeyId) {
hotkeyService.unregister(hotkeyId).catch(console.warn);
}
if (hotkeyId) hotkeyService.unregister(hotkeyId).catch(console.warn);
};
}, [recording, stopping]);
}, [inTauriApp]);

const start = async () => {
setError('');
Expand All @@ -156,13 +150,16 @@ export default function ScreenRecorder() {
if (inTauriApp) {
const { invoke } = await import('@tauri-apps/api/core');

// Hide window if user enabled the option
// Hide window if user enabled the option.
// Use MINIMIZE (not hide) for recording: the window stays hidden for the
// whole session, and a minimized window can be restored from the dock —
// a fully-hidden one can't, which locked the user out mid-recording.
if (hideWindow) {
console.log('[ScreenRecorder] Hiding window for cleaner capture');
await invoke('hide_main_window');
console.log('[ScreenRecorder] Minimizing window for cleaner capture');
await invoke('minimize_main_window');
windowHiddenRef.current = true;
// hide() is instant; a couple of frames is enough for the compositor.
await new Promise(resolve => setTimeout(resolve, 60));
// Let the minimize animation settle before capture begins.
await new Promise(resolve => setTimeout(resolve, 250));
} else {
console.log('[ScreenRecorder] Keeping window visible (user preference)');
windowHiddenRef.current = false;
Expand Down Expand Up @@ -359,6 +356,12 @@ export default function ScreenRecorder() {
downloadService.download(result, `screen-recording.${extRef.current}`);
};

// Keep the hotkey's toggle current with live state (runs every render).
toggleRecordingRef.current = () => {
if (recording) stop();
else if (!stopping) start();
};

const mmss = `${String(Math.floor(elapsed / 60)).padStart(2, '0')}:${String(elapsed % 60).padStart(2, '0')}`;

if (!supported) {
Expand Down
Loading