Merge pull request #94 from appergb/codex/windows-recording-fixes
fix: stabilize Windows recording startup
This commit is contained in:
11 files changed
+209
-72
No files matched your search
Generated
+2
-2
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "openless-app",
|
||||
"private": true,
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+79
-2
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
Reference in new issue
Block a user