feat(desktop): summon shortcut toggles -- press while focused hides (#13803)

This commit is contained in:
Justin Tahara authored and GitHub committed 2026-08-07 19:54:06 +00:00
1 parent 955b039ed0
commit 330246dbf0
3 files changed
+97 -14

No files matched your search

+1 -13
View File
@@ -1,7 +1,7 @@
use crate::config::ConfigState;
use crate::debug_log::{log_backend_error, MENU_OPEN_DEBUG_LOG_ID, MENU_TOGGLE_DEVTOOLS_ID};
use crate::shortcuts::SummonShortcut;
use crate::window::{focus_main_window, open_chat_window};
use crate::window::{focus_main_window, focused_webview_window, open_chat_window};
use tauri::image::Image;
#[cfg(not(target_os = "macos"))]
use tauri::menu::AboutMetadataBuilder;
@@ -131,18 +131,6 @@ fn build_view_menu(app: &AppHandle, menu: &Menu<Wry>) -> tauri::Result<()> {
Ok(())
}
/// The window a View-menu action should apply to: whichever one is focused
/// (menu accelerators only fire while the app is active, but the app can have
/// several windows).
fn focused_webview_window(app: &AppHandle) -> Option<tauri::WebviewWindow> {
app.webview_windows().into_values().find(|window| {
window.is_focused().unwrap_or_else(|e| {
log_backend_error(app, &format!("Failed to query window focus: {e}"));
false
})
})
}
pub fn handle_reload(app: &AppHandle) {
if let Some(window) = focused_webview_window(app) {
crate::commands::reload_page(window);
+84 -1
View File
@@ -1,6 +1,7 @@
use crate::config::ConfigState;
use crate::debug_log::{log_backend_debug, log_backend_error};
use crate::window::{focus_main_window, open_chat_window};
use crate::window::{focus_main_window, focused_webview_window, open_chat_window};
use std::sync::{Mutex, PoisonError};
use tauri::{AppHandle, Manager};
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
@@ -11,6 +12,11 @@ pub struct SummonShortcut {
pub chord: String,
}
/// Labels of the windows the last chord dismissal hid. Auxiliary windows
/// have no other path back once hidden (the tray and summon paths only
/// manage `main`), so the next summon restores this exact set.
struct DismissedWindows(Mutex<Vec<String>>);
/// Register the configured summon shortcut. This is deliberately the only
/// global (system-wide) shortcut the app registers: summoning Onyx is the one
/// action that must work while another app is focused. Every other shortcut
@@ -30,12 +36,24 @@ pub fn setup_global_shortcuts(app: &AppHandle) {
}
};
app.manage(DismissedWindows(Mutex::new(Vec::new())));
let result = app
.global_shortcut()
.on_shortcut(shortcut, |app, _shortcut, event| {
if event.state() != ShortcutState::Pressed {
return;
}
// Toggle contract: the chord that summons Onyx also dismisses it,
// returning focus to whatever app was active before. Dismissal
// hides every visible Onyx window -- hiding only the focused one
// would just hand focus to the next Onyx window instead of the
// previous application.
if focused_webview_window(app).is_some() {
dismiss_all_windows(app);
return;
}
restore_dismissed_windows(app);
let opens_new_chat = app.state::<ConfigState>().config().summon_opens_new_chat;
log_backend_debug(
app,
@@ -65,6 +83,71 @@ pub fn setup_global_shortcuts(app: &AppHandle) {
}
}
fn dismiss_all_windows(app: &AppHandle) {
let mut hidden = Vec::new();
for (label, window) in app.webview_windows() {
let visible = window.is_visible().unwrap_or_else(|e| {
log_backend_error(app, &format!("Failed to query window visibility: {e}"));
false
});
if !visible {
continue;
}
if let Err(e) = window.hide() {
log_backend_error(app, &format!("Failed to hide window \"{label}\": {e}"));
} else {
hidden.push(label);
}
}
log_backend_debug(
app,
&format!(
"Summon shortcut fired (action=dismiss, windows={})",
hidden.len()
),
);
// Merge rather than replace: the queue may still hold labels whose
// restore failed earlier, and overwriting would strand those windows.
if let Some(state) = app.try_state::<DismissedWindows>() {
let mut queued = state.0.lock().unwrap_or_else(PoisonError::into_inner);
for label in hidden {
if !queued.contains(&label) {
queued.push(label);
}
}
}
}
/// Re-show the windows the last dismissal hid. `main` is skipped here; the
/// summon action that follows shows and focuses it. A window whose `show`
/// fails stays queued so a later summon retries it -- dropping the label
/// would strand the window hidden with no path back.
fn restore_dismissed_windows(app: &AppHandle) {
let Some(state) = app.try_state::<DismissedWindows>() else {
return;
};
let labels = std::mem::take(&mut *state.0.lock().unwrap_or_else(PoisonError::into_inner));
let mut failed = Vec::new();
for label in labels {
if label == "main" {
continue;
}
if let Some(window) = app.get_webview_window(&label) {
if let Err(e) = window.show() {
log_backend_error(app, &format!("Failed to restore window \"{label}\": {e}"));
failed.push(label);
}
}
}
if !failed.is_empty() {
state
.0
.lock()
.unwrap_or_else(PoisonError::into_inner)
.extend(failed);
}
}
#[cfg(test)]
mod tests {
use super::*;
+12
View File
@@ -14,6 +14,18 @@ use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
const TITLEBAR_SCRIPT: &str = include_str!("../../src/titlebar.js");
const CHAT_LINK_INTERCEPT_SCRIPT: &str = include_str!("scripts/chat_link_intercept.js");
/// The window a shortcut or menu action should apply to: whichever one is
/// focused (the app can have several windows). Focus-query failures are
/// logged and treated as unfocused.
pub fn focused_webview_window(app: &AppHandle) -> Option<WebviewWindow> {
app.webview_windows().into_values().find(|window| {
window.is_focused().unwrap_or_else(|e| {
log_backend_error(app, &format!("Failed to query window focus: {e}"));
false
})
})
}
pub fn focus_main_window(app: &AppHandle) {
if let Some(window) = app.get_webview_window("main") {
if let Err(e) = window.unminimize() {