6 files changed
+103
-5
No files matched your search
@@ -89,6 +89,18 @@ Generated GNU artifacts:
|
||||
- `%TEMP%\openless-windows-gnu\src-tauri\target\x86_64-pc-windows-gnu\release\bundle\msi\OpenLess_*_x64_en-US.msi`
|
||||
- `%TEMP%\openless-windows-gnu\src-tauri\target\x86_64-pc-windows-gnu\release\bundle\nsis\OpenLess_*_x64-setup.exe`
|
||||
|
||||
### Hotkey Injection Gate
|
||||
|
||||
Use this gate before/after Windows hotkey changes when a physical keyboard
|
||||
regression is unavailable. It injects a dev/test-only hotkey click through the
|
||||
coordinator `handle_pressed` / `handle_released` path, asserts the log contains
|
||||
`[coord] hotkey pressed`, and cancels the dry-run session automatically.
|
||||
|
||||
```powershell
|
||||
cd app
|
||||
npm run check:hotkey-injection
|
||||
```
|
||||
|
||||
### Windows Runtime Notes
|
||||
|
||||
- Windows does not need the macOS Accessibility permission. Use Settings ->
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
"tauri": "tauri",
|
||||
"check:hotkey-injection": "node scripts/check-hotkey-injection.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const result = spawnSync(
|
||||
'cargo',
|
||||
['test', '--manifest-path', 'src-tauri/Cargo.toml', 'hotkey_injection_gate_logs_pressed_and_cancels', '--', '--nocapture'],
|
||||
{
|
||||
env: { ...process.env, OPENLESS_HOTKEY_INJECTION_DRY_RUN: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
);
|
||||
|
||||
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
||||
process.stdout.write(result.stdout ?? '');
|
||||
process.stderr.write(result.stderr ?? '');
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
|
||||
if (!output.includes('[coord] hotkey pressed')) {
|
||||
console.error("Hotkey injection gate did not emit '[coord] hotkey pressed'.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Hotkey injection gate passed.');
|
||||
@@ -163,6 +163,12 @@ pub fn cancel_dictation(coord: CoordinatorState<'_>) {
|
||||
coord.cancel_dictation();
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[tauri::command]
|
||||
pub async fn inject_hotkey_click_for_dev(coord: CoordinatorState<'_>) -> Result<(), String> {
|
||||
coord.inject_hotkey_click_for_dev().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn repolish(
|
||||
coord: CoordinatorState<'_>,
|
||||
|
||||
@@ -154,6 +154,15 @@ impl Coordinator {
|
||||
cancel_session(&self.inner);
|
||||
}
|
||||
|
||||
#[cfg(any(debug_assertions, test))]
|
||||
pub async fn inject_hotkey_click_for_dev(&self) -> Result<(), String> {
|
||||
log::info!("[coord] dev hotkey injection started");
|
||||
handle_pressed(&self.inner).await;
|
||||
handle_released(&self.inner).await;
|
||||
cancel_session(&self.inner);
|
||||
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)
|
||||
@@ -241,6 +250,7 @@ fn hotkey_bridge_loop(inner: Arc<Inner>, rx: mpsc::Receiver<HotkeyEvent>) {
|
||||
async fn handle_pressed(inner: &Arc<Inner>) {
|
||||
let mode = inner.prefs.get().hotkey.mode;
|
||||
let phase = inner.state.lock().phase;
|
||||
log::info!("[coord] hotkey pressed (mode={mode:?}, phase={phase:?})");
|
||||
match (mode, phase) {
|
||||
(HotkeyMode::Toggle, SessionPhase::Idle) => {
|
||||
let _ = begin_session(inner).await;
|
||||
@@ -257,8 +267,9 @@ async fn handle_pressed(inner: &Arc<Inner>) {
|
||||
|
||||
async fn handle_released(inner: &Arc<Inner>) {
|
||||
let mode = inner.prefs.get().hotkey.mode;
|
||||
let phase = inner.state.lock().phase;
|
||||
log::info!("[coord] hotkey released (mode={mode:?}, phase={phase:?})");
|
||||
if mode == HotkeyMode::Hold {
|
||||
let phase = inner.state.lock().phase;
|
||||
if phase == SessionPhase::Listening {
|
||||
let _ = end_session(inner).await;
|
||||
}
|
||||
@@ -277,9 +288,24 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
state.started_at = Instant::now();
|
||||
}
|
||||
|
||||
#[cfg(any(debug_assertions, test))]
|
||||
if hotkey_injection_dry_run_enabled() {
|
||||
emit_capsule(inner, CapsuleState::Recording, 0.0, 0, None, None);
|
||||
inner.state.lock().phase = SessionPhase::Listening;
|
||||
log::info!("[coord] session started (hotkey-injection dry-run)");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(message) = ensure_microphone_permission(inner) {
|
||||
log::warn!("[coord] microphone permission gate failed: {message}");
|
||||
emit_capsule(inner, CapsuleState::Error, 0.0, 0, Some(message.clone()), None);
|
||||
emit_capsule(
|
||||
inner,
|
||||
CapsuleState::Error,
|
||||
0.0,
|
||||
0,
|
||||
Some(message.clone()),
|
||||
None,
|
||||
);
|
||||
inner.state.lock().phase = SessionPhase::Idle;
|
||||
return Err(message);
|
||||
}
|
||||
@@ -311,8 +337,9 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
inner.state.lock().phase = SessionPhase::Idle;
|
||||
return Err(e.to_string());
|
||||
}
|
||||
let c: Arc<dyn crate::recorder::AudioConsumer> =
|
||||
Arc::new(AsrBridge { asr: Arc::clone(&asr) });
|
||||
let c: Arc<dyn crate::recorder::AudioConsumer> = Arc::new(AsrBridge {
|
||||
asr: Arc::clone(&asr),
|
||||
});
|
||||
*inner.asr.lock() = Some(ActiveAsr::Volcengine(asr));
|
||||
c
|
||||
};
|
||||
@@ -511,6 +538,11 @@ fn cancel_session(inner: &Arc<Inner>) {
|
||||
|
||||
// ─────────────────────────── helpers ───────────────────────────
|
||||
|
||||
#[cfg(any(debug_assertions, test))]
|
||||
fn hotkey_injection_dry_run_enabled() -> bool {
|
||||
std::env::var_os("OPENLESS_HOTKEY_INJECTION_DRY_RUN").is_some()
|
||||
}
|
||||
|
||||
fn ensure_microphone_permission(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
use crate::permissions::{self, PermissionStatus};
|
||||
|
||||
@@ -626,6 +658,26 @@ fn enabled_hotwords(inner: &Arc<Inner>) -> Vec<DictionaryHotword> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn hotkey_injection_gate_logs_pressed_and_cancels() {
|
||||
let _ = env_logger::builder()
|
||||
.filter_level(log::LevelFilter::Info)
|
||||
.is_test(false)
|
||||
.try_init();
|
||||
std::env::set_var("OPENLESS_HOTKEY_INJECTION_DRY_RUN", "1");
|
||||
|
||||
let coordinator = Coordinator::new();
|
||||
coordinator.inject_hotkey_click_for_dev().await.unwrap();
|
||||
|
||||
assert_eq!(coordinator.inner.state.lock().phase, SessionPhase::Idle);
|
||||
std::env::remove_var("OPENLESS_HOTKEY_INJECTION_DRY_RUN");
|
||||
}
|
||||
}
|
||||
|
||||
fn enabled_phrases(inner: &Arc<Inner>) -> Vec<String> {
|
||||
inner
|
||||
.vocab
|
||||
|
||||
@@ -139,6 +139,8 @@ pub fn run() {
|
||||
commands::start_dictation,
|
||||
commands::stop_dictation,
|
||||
commands::cancel_dictation,
|
||||
#[cfg(debug_assertions)]
|
||||
commands::inject_hotkey_click_for_dev,
|
||||
commands::repolish,
|
||||
commands::set_default_polish_mode,
|
||||
commands::set_style_enabled,
|
||||
|
||||
Reference in new issue
Block a user