merge: integrate PR 86 windows foreground hotkey
This commit is contained in:
commit
e86edd13c2
5 files changed
+263
-5
No files matched your search
@@ -283,5 +283,52 @@ pub fn trigger_microphone_prompt(app: AppHandle) -> Result<(), String> {
|
||||
|
||||
// ─────────────────────────── unused but exported (silences dead_code) ───────────────────────────
|
||||
|
||||
#[tauri::command]
|
||||
pub fn is_debug_ui_key_events_enabled() -> bool {
|
||||
std::env::var("OPENLESS_DEBUG_UI_KEY_EVENTS")
|
||||
.ok()
|
||||
.as_deref()
|
||||
== Some("1")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn debug_log_ui_key_event(
|
||||
event_type: String,
|
||||
key: String,
|
||||
code: String,
|
||||
ctrl: bool,
|
||||
alt: bool,
|
||||
shift: bool,
|
||||
meta: bool,
|
||||
repeat: bool,
|
||||
) {
|
||||
if !is_debug_ui_key_events_enabled() {
|
||||
return;
|
||||
}
|
||||
log::info!(
|
||||
"[ui-key] type={} key={} code={} ctrl={} alt={} shift={} meta={} repeat={}",
|
||||
event_type,
|
||||
key.replace(' ', "_"),
|
||||
code.replace(' ', "_"),
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
meta,
|
||||
repeat
|
||||
);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn handle_window_hotkey_event(
|
||||
coord: CoordinatorState<'_>,
|
||||
event_type: String,
|
||||
key: String,
|
||||
code: String,
|
||||
repeat: bool,
|
||||
) -> Result<(), String> {
|
||||
coord.handle_window_hotkey_event(&event_type, &key, &code, repeat)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn _ensure_snapshot_used(_: CredentialsSnapshot) {}
|
||||
@@ -28,7 +28,7 @@ use crate::polish::{OpenAICompatibleConfig, OpenAICompatibleLLMProvider};
|
||||
use crate::recorder::Recorder;
|
||||
use crate::types::{
|
||||
CapsulePayload, CapsuleState, DictationSession, HotkeyCapability, HotkeyMode, HotkeyStatus,
|
||||
HotkeyStatusState, InsertStatus, PolishMode,
|
||||
HotkeyStatusState, HotkeyTrigger, InsertStatus, PolishMode,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -86,6 +86,7 @@ struct Inner {
|
||||
recorder: Mutex<Option<Recorder>>,
|
||||
hotkey: Mutex<Option<HotkeyMonitor>>,
|
||||
hotkey_status: Mutex<HotkeyStatus>,
|
||||
window_hotkey_held: Mutex<bool>,
|
||||
}
|
||||
|
||||
impl Coordinator {
|
||||
@@ -109,6 +110,7 @@ impl Coordinator {
|
||||
recorder: Mutex::new(None),
|
||||
hotkey: Mutex::new(None),
|
||||
hotkey_status: Mutex::new(HotkeyStatus::default()),
|
||||
window_hotkey_held: Mutex::new(false),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -176,6 +178,17 @@ impl Coordinator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_window_hotkey_event(
|
||||
&self,
|
||||
event_type: &str,
|
||||
key: &str,
|
||||
code: &str,
|
||||
repeat: bool,
|
||||
) -> Result<(), String> {
|
||||
handle_window_hotkey_event(&self.inner, event_type, key, code, repeat).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn repolish(&self, raw_text: String, mode: PolishMode) -> Result<String, String> {
|
||||
let hotwords = enabled_phrases(&self.inner);
|
||||
polish_text(&raw_text, mode, &hotwords)
|
||||
@@ -260,6 +273,87 @@ fn hotkey_bridge_loop(inner: Arc<Inner>, rx: mpsc::Receiver<HotkeyEvent>) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_window_hotkey_event(
|
||||
inner: &Arc<Inner>,
|
||||
event_type: &str,
|
||||
key: &str,
|
||||
code: &str,
|
||||
repeat: bool,
|
||||
) {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = (inner, event_type, key, code, repeat);
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if event_type == "keydown" && key == "Escape" {
|
||||
log::info!("[window-hotkey] escape cancel from main window");
|
||||
cancel_session(inner);
|
||||
return;
|
||||
}
|
||||
|
||||
let binding = inner.prefs.get().hotkey;
|
||||
if !matches_window_hotkey(binding.trigger, code) {
|
||||
return;
|
||||
}
|
||||
|
||||
if event_type == "keydown" {
|
||||
if repeat {
|
||||
return;
|
||||
}
|
||||
let should_press = {
|
||||
let mut held = inner.window_hotkey_held.lock();
|
||||
if *held {
|
||||
false
|
||||
} else {
|
||||
*held = true;
|
||||
true
|
||||
}
|
||||
};
|
||||
if !should_press {
|
||||
return;
|
||||
}
|
||||
log::info!(
|
||||
"[window-hotkey] pressed trigger={:?} code={} repeat={}",
|
||||
binding.trigger, code, repeat
|
||||
);
|
||||
handle_pressed(inner).await;
|
||||
} else if event_type == "keyup" {
|
||||
let should_release = {
|
||||
let mut held = inner.window_hotkey_held.lock();
|
||||
if !*held {
|
||||
false
|
||||
} else {
|
||||
*held = false;
|
||||
true
|
||||
}
|
||||
};
|
||||
if !should_release {
|
||||
return;
|
||||
}
|
||||
log::info!(
|
||||
"[window-hotkey] released trigger={:?} code={} repeat={}",
|
||||
binding.trigger, code, repeat
|
||||
);
|
||||
handle_released(inner).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn matches_window_hotkey(trigger: HotkeyTrigger, code: &str) -> bool {
|
||||
match trigger {
|
||||
HotkeyTrigger::RightControl => code == "ControlRight",
|
||||
HotkeyTrigger::LeftControl => code == "ControlLeft",
|
||||
HotkeyTrigger::RightOption | HotkeyTrigger::RightAlt => code == "AltRight",
|
||||
HotkeyTrigger::LeftOption => code == "AltLeft",
|
||||
HotkeyTrigger::RightCommand => code == "MetaRight",
|
||||
HotkeyTrigger::Fn => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_pressed(inner: &Arc<Inner>) {
|
||||
let mode = inner.prefs.get().hotkey.mode;
|
||||
let phase = inner.state.lock().phase;
|
||||
|
||||
@@ -166,6 +166,9 @@ pub fn run() {
|
||||
commands::read_credential,
|
||||
commands::set_active_asr_provider,
|
||||
commands::set_active_llm_provider,
|
||||
commands::is_debug_ui_key_events_enabled,
|
||||
commands::debug_log_ui_key_event,
|
||||
commands::handle_window_hotkey_event,
|
||||
restart_app,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
@@ -175,9 +178,15 @@ pub fn run() {
|
||||
RunEvent::Reopen { .. } => show_main_window(app),
|
||||
RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label == "main" {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
hide_main_window(app);
|
||||
match event {
|
||||
tauri::WindowEvent::Focused(focused) => {
|
||||
log::info!("[window] main focused={focused}");
|
||||
}
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
api.prevent_close();
|
||||
hide_main_window(app);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,14 @@ import { Capsule } from './components/Capsule';
|
||||
import { FloatingShell } from './components/FloatingShell';
|
||||
import { Onboarding } from './components/Onboarding';
|
||||
import { detectOS } from './components/WindowChrome';
|
||||
import { checkAccessibilityPermission, checkMicrophonePermission, isTauri } from './lib/ipc';
|
||||
import {
|
||||
checkAccessibilityPermission,
|
||||
checkMicrophonePermission,
|
||||
debugLogUiKeyEvent,
|
||||
handleWindowHotkeyEvent,
|
||||
isDebugUiKeyEventsEnabled,
|
||||
isTauri,
|
||||
} from './lib/ipc';
|
||||
import { HotkeySettingsProvider } from './state/HotkeySettingsContext';
|
||||
|
||||
interface AppProps {
|
||||
@@ -53,6 +60,59 @@ export function App({ isCapsule }: AppProps) {
|
||||
};
|
||||
}, [os]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauri) return;
|
||||
let disposed = false;
|
||||
let detach: (() => void) | null = null;
|
||||
|
||||
void isDebugUiKeyEventsEnabled().then(enabled => {
|
||||
if (!enabled || disposed) return;
|
||||
const onKeyboardEvent = (event: KeyboardEvent) => {
|
||||
void debugLogUiKeyEvent({
|
||||
eventType: event.type,
|
||||
key: event.key,
|
||||
code: event.code,
|
||||
ctrl: event.ctrlKey,
|
||||
alt: event.altKey,
|
||||
shift: event.shiftKey,
|
||||
meta: event.metaKey,
|
||||
repeat: event.repeat,
|
||||
});
|
||||
};
|
||||
window.addEventListener('keydown', onKeyboardEvent, true);
|
||||
window.addEventListener('keyup', onKeyboardEvent, true);
|
||||
detach = () => {
|
||||
window.removeEventListener('keydown', onKeyboardEvent, true);
|
||||
window.removeEventListener('keyup', onKeyboardEvent, true);
|
||||
};
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
detach?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauri || os !== 'win') return;
|
||||
|
||||
const onKeyboardEvent = (event: KeyboardEvent) => {
|
||||
void handleWindowHotkeyEvent({
|
||||
eventType: event.type,
|
||||
key: event.key,
|
||||
code: event.code,
|
||||
repeat: event.repeat,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', onKeyboardEvent, true);
|
||||
window.addEventListener('keyup', onKeyboardEvent, true);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyboardEvent, true);
|
||||
window.removeEventListener('keyup', onKeyboardEvent, true);
|
||||
};
|
||||
}, [os]);
|
||||
|
||||
if (gate === 'checking') {
|
||||
return <StartupShell />;
|
||||
}
|
||||
|
||||
@@ -220,6 +220,54 @@ export function restartApp(): Promise<void> {
|
||||
return invokeOrMock('restart_app', undefined, () => undefined);
|
||||
}
|
||||
|
||||
export function isDebugUiKeyEventsEnabled(): Promise<boolean> {
|
||||
return invokeOrMock('is_debug_ui_key_events_enabled', undefined, () => false);
|
||||
}
|
||||
|
||||
export function debugLogUiKeyEvent(payload: {
|
||||
eventType: string;
|
||||
key: string;
|
||||
code: string;
|
||||
ctrl: boolean;
|
||||
alt: boolean;
|
||||
shift: boolean;
|
||||
meta: boolean;
|
||||
repeat: boolean;
|
||||
}): Promise<void> {
|
||||
return invokeOrMock(
|
||||
'debug_log_ui_key_event',
|
||||
{
|
||||
eventType: payload.eventType,
|
||||
key: payload.key,
|
||||
code: payload.code,
|
||||
ctrl: payload.ctrl,
|
||||
alt: payload.alt,
|
||||
shift: payload.shift,
|
||||
meta: payload.meta,
|
||||
repeat: payload.repeat,
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
}
|
||||
|
||||
export function handleWindowHotkeyEvent(payload: {
|
||||
eventType: string;
|
||||
key: string;
|
||||
code: string;
|
||||
repeat: boolean;
|
||||
}): Promise<void> {
|
||||
return invokeOrMock(
|
||||
'handle_window_hotkey_event',
|
||||
{
|
||||
eventType: payload.eventType,
|
||||
key: payload.key,
|
||||
code: payload.code,
|
||||
repeat: payload.repeat,
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
}
|
||||
|
||||
export async function openExternal(url: string): Promise<void> {
|
||||
if (!isTauri) {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
|
||||
Reference in new issue
Block a user