Merge pull request #94 from appergb/codex/windows-recording-fixes

fix: stabilize Windows recording startup
This commit is contained in:
TRIP authored and GitHub committed 2026-04-30 21:38:36 +08:00
commit 884455a99a
11 files changed
+209 -72

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "openless-app",
"version": "1.2.5",
"version": "1.2.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openless-app",
"version": "1.2.5",
"version": "1.2.6",
"dependencies": {
"@tauri-apps/api": "^2.1.1",
"@tauri-apps/plugin-shell": "^2.0.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "openless-app",
"private": true,
"version": "1.2.5",
"version": "1.2.6",
"type": "module",
"scripts": {
"dev": "vite",
+79 -2
View File
@@ -1225,7 +1225,7 @@ dependencies = [
"rustc_version",
"toml 1.1.2+spec-1.1.0",
"vswhom",
"winreg",
"winreg 0.55.0",
]
[[package]]
@@ -3250,7 +3250,7 @@ dependencies = [
[[package]]
name = "openless"
version = "1.2.5"
version = "1.2.6"
dependencies = [
"anyhow",
"arboard",
@@ -3287,6 +3287,7 @@ dependencies = [
"uuid",
"window-vibrancy 0.7.1",
"windows 0.58.0",
"winreg 0.52.0",
]
[[package]]
@@ -6595,6 +6596,15 @@ dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -6646,6 +6656,21 @@ dependencies = [
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -6703,6 +6728,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -6721,6 +6752,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -6739,6 +6776,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -6769,6 +6812,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -6787,6 +6836,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -6805,6 +6860,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -6823,6 +6884,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@@ -6859,6 +6926,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "winreg"
version = "0.55.0"
+3 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "openless"
version = "1.2.5"
version = "1.2.6"
description = "OpenLess — local voice input that types where your cursor is"
authors = ["OpenLess"]
edition = "2021"
@@ -54,10 +54,12 @@ objc2-app-kit = "0.2"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.58", features = [
"Win32_Foundation",
"Win32_UI_Shell",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
"Win32_System_Threading",
] }
winreg = "0.52"
# 跨平台磨砂层(macOS NSVisualEffectView / Windows Mica)。
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
+33 -6
View File
@@ -262,19 +262,46 @@ pub fn open_system_settings(pane: String) -> Result<(), String> {
.map(|_| ())
.map_err(|e| e.to_string())
}
#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "windows")]
{
use windows::core::PCWSTR;
use windows::Win32::UI::Shell::ShellExecuteW;
use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
fn wide_null(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
let uri = match pane.as_str() {
"microphone" => "ms-settings:privacy-microphone",
"sound" => "ms-settings:sound",
"accessibility" => "ms-settings:easeofaccess",
_ => "ms-settings:",
};
std::process::Command::new("cmd")
.args(["/C", "start", "", uri])
.spawn()
.map(|_| ())
.map_err(|e| e.to_string())
let operation = wide_null("open");
let target = wide_null(uri);
let result = unsafe {
ShellExecuteW(
None,
PCWSTR(operation.as_ptr()),
PCWSTR(target.as_ptr()),
PCWSTR::null(),
PCWSTR::null(),
SW_SHOWNORMAL,
)
};
if result.0 as isize <= 32 {
Err(format!("ShellExecuteW failed: {}", result.0 as isize))
} else {
Ok(())
}
}
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
{
let _ = pane;
Err("open_system_settings is only supported on macOS and Windows".to_string())
}
}
+42 -1
View File
@@ -794,7 +794,10 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
}
};
if !proceed_to_insert {
log::info!("[coord] cancel detected before insert — discarding output (chars={})", polished.chars().count());
log::info!(
"[coord] cancel detected before insert — discarding output (chars={})",
polished.chars().count()
);
return Ok(());
}
@@ -916,6 +919,15 @@ fn hotkey_injection_dry_run_enabled() -> bool {
fn ensure_microphone_permission(inner: &Arc<Inner>) -> Result<(), String> {
use crate::permissions::{self, PermissionStatus};
#[cfg(target_os = "windows")]
{
let _ = inner;
if permissions::windows_microphone_access_explicitly_denied() {
return Err("需要麦克风权限,当前状态: Denied".to_string());
}
return Ok(());
}
let status = permissions::check_microphone();
if matches!(
status,
@@ -1144,6 +1156,35 @@ mod tests {
assert_eq!(state.phase, SessionPhase::Starting);
assert!(state.pending_stop);
}
#[tokio::test]
async fn repeated_pressed_edge_during_hold_session_does_not_restart() {
let coordinator = Coordinator::new();
coordinator
.inner
.prefs
.set(crate::types::UserPreferences {
hotkey: crate::types::HotkeyBinding {
trigger: HotkeyTrigger::RightControl,
mode: HotkeyMode::Hold,
},
..Default::default()
})
.unwrap();
coordinator.inner.state.lock().phase = SessionPhase::Listening;
coordinator
.inner
.hotkey_trigger_held
.store(true, Ordering::SeqCst);
handle_pressed_edge(&coordinator.inner).await;
assert_eq!(
coordinator.inner.state.lock().phase,
SessionPhase::Listening
);
assert!(coordinator.inner.hotkey_trigger_held.load(Ordering::SeqCst));
}
}
fn enabled_phrases(inner: &Arc<Inner>) -> Vec<String> {
+3 -1
View File
@@ -42,7 +42,9 @@ pub fn run() {
// 否则两份 OpenLess(如 /Applications/ + dev build)会各自抓全局热键,
// 导致按一次键、两个进程同时跑流水线、文本被插入两遍。见 issue #50。
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
log::info!("[single-instance] another instance launched, focusing existing main window");
log::info!(
"[single-instance] another instance launched, focusing existing main window"
);
show_main_window(app);
}))
.plugin(tauri_plugin_shell::init())
+45 -13
View File
@@ -251,6 +251,10 @@ mod platform {
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{SampleFormat, StreamConfig};
use std::time::Duration;
#[cfg(target_os = "windows")]
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
#[cfg(target_os = "windows")]
use winreg::RegKey;
/// Windows / Linux 不存在 macOS 那种 Accessibility 概念;rdev 直接监听键盘。
pub fn check_accessibility() -> PermissionStatus {
@@ -290,6 +294,18 @@ mod platform {
check_microphone()
}
pub fn windows_microphone_access_explicitly_denied() -> bool {
#[cfg(target_os = "windows")]
{
windows_microphone_registry_denied()
}
#[cfg(not(target_os = "windows"))]
{
false
}
}
fn classify_audio_probe_error(message: String) -> PermissionStatus {
let lower = message.to_lowercase();
log::warn!("[mic] input probe failed: {message}");
@@ -365,24 +381,32 @@ mod platform {
}
fn registry_value_is_deny(path: &str) -> bool {
let output = match std::process::Command::new("reg")
.args(["query", path, "/v", "Value"])
.output()
#[cfg(target_os = "windows")]
{
Ok(output) => output,
Err(err) => {
log::warn!("[mic] reg query failed for {path}: {err}");
let Some((root, subkey)) = path.split_once('\\') else {
return false;
}
};
};
if !output.status.success() {
return false;
let hive = match root {
"HKCU" => RegKey::predef(HKEY_CURRENT_USER),
"HKLM" => RegKey::predef(HKEY_LOCAL_MACHINE),
_ => return false,
};
match hive.open_subkey(subkey) {
Ok(key) => match key.get_value::<String, _>("Value") {
Ok(value) => value.eq_ignore_ascii_case("Deny"),
Err(_) => false,
},
Err(_) => false,
}
}
String::from_utf8_lossy(&output.stdout)
.lines()
.any(|line| line.contains("REG_SZ") && line.split_whitespace().any(|part| part == "Deny"))
#[cfg(not(target_os = "windows"))]
{
let _ = path;
false
}
}
}
@@ -390,6 +414,14 @@ pub use platform::{
check_accessibility, check_microphone, request_accessibility, request_microphone,
};
#[cfg(target_os = "windows")]
pub use platform::windows_microphone_access_explicitly_denied;
#[cfg(not(target_os = "windows"))]
pub fn windows_microphone_access_explicitly_denied() -> bool {
false
}
/// 兼容老调用:startup 时主动弹 Accessibility 框。
pub fn request_accessibility_with_prompt(_prompt: bool) -> bool {
matches!(request_accessibility(), PermissionStatus::Granted)
@@ -20,8 +20,6 @@ use cpal::{SampleFormat, StreamConfig};
use parking_lot::Mutex;
use thiserror::Error;
use crate::permissions::{self, PermissionStatus};
/// 目标采样率(与 Swift 端常量一致;不要改)。
const TARGET_SAMPLE_RATE: u32 = 16_000;
/// 每多少个回调打一次诊断日志。
@@ -60,24 +58,6 @@ impl Recorder {
consumer: Arc<dyn AudioConsumer>,
level_handler: Arc<dyn Fn(f32) + Send + Sync>,
) -> Result<Self, RecorderError> {
let status = permissions::check_microphone();
if !matches!(
status,
PermissionStatus::Granted | PermissionStatus::NotApplicable
) {
let requested = permissions::request_microphone();
if !matches!(
requested,
PermissionStatus::Granted | PermissionStatus::NotApplicable
) {
log::warn!(
"[recorder] microphone permission not granted: {:?}",
requested
);
return Err(RecorderError::PermissionDenied);
}
}
// 启动信号:子线程构造 Stream 完成后通过 startup_tx 报告结果。
let (startup_tx, startup_rx) = channel::<Result<(), RecorderError>>();
let stop_flag = Arc::new(AtomicBool::new(false));
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenLess",
"version": "1.2.5",
"version": "1.2.6",
"identifier": "com.openless.app",
"build": {
"beforeDevCommand": "npm run dev",
-24
View File
@@ -60,14 +60,8 @@ export function App({ isCapsule }: AppProps) {
useEffect(() => {
if (!isTauri || os !== 'win') return;
const pressedKeys = new Map<string, string>();
const forwardKey = (event: KeyboardEvent) => {
if (!isWindowHotkeyCandidate(event)) return;
if (event.type === 'keydown' && event.key !== 'Escape') {
pressedKeys.set(event.code, event.key);
} else if (event.type === 'keyup') {
pressedKeys.delete(event.code);
}
void handleWindowHotkeyEvent(
event.type as 'keydown' | 'keyup',
event.key,
@@ -75,29 +69,11 @@ export function App({ isCapsule }: AppProps) {
event.repeat,
).catch(error => console.warn('[window-hotkey] forward failed', error));
};
const releasePressedKeys = () => {
if (pressedKeys.size === 0) return;
const pending = Array.from(pressedKeys.entries());
pressedKeys.clear();
for (const [code, key] of pending) {
void handleWindowHotkeyEvent('keyup', key, code, false).catch(error =>
console.warn('[window-hotkey] release fallback failed', error),
);
}
};
const releaseOnHidden = () => {
if (document.visibilityState === 'hidden') releasePressedKeys();
};
window.addEventListener('keydown', forwardKey, true);
window.addEventListener('keyup', forwardKey, true);
window.addEventListener('blur', releasePressedKeys);
document.addEventListener('visibilitychange', releaseOnHidden);
return () => {
window.removeEventListener('keydown', forwardKey, true);
window.removeEventListener('keyup', forwardKey, true);
window.removeEventListener('blur', releasePressedKeys);
document.removeEventListener('visibilitychange', releaseOnHidden);
releasePressedKeys();
};
}, [os]);