diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 2bafe7e7..b7869133 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -4608,21 +4608,17 @@ fn emit_capsule( let mut last = LAST_AUX.lock().unwrap(); if aux != last.as_deref() { - let was_none = last.is_none(); *last = aux.map(String::from); // 代数计数器:每次状态变化 +1,retry 线程只在自己代数仍为最新时生效。 // 避免 Recording→Idle→Recording 快速切换时多个 retry 重复触发。 static RETRY_GEN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + // fetch_add 返回旧值,所以 latest_gen > gen+1 才表示"在我之后又发生了变更"。 let gen = RETRY_GEN.fetch_add(1, std::sync::atomic::Ordering::SeqCst); match aux { Some(t) => { log::info!("[capsule] set_aux_down: {t} gen={gen}"); - // 把 DBus I/O 移到独立线程:emit_capsule 会被音频回调线程 - // (cpal) 调用,同步阻塞可能导致录音卡顿或可闻杂音。 let text = t.to_string(); std::thread::spawn(move || { - // 状态检查:发送前确认 LAST_AUX 未变,避免在快速状态切换时 - // 旧 set_aux_down 跑到 clear_aux_down 后面,旧文字覆盖新状态。 let current = LAST_AUX.lock().unwrap().clone(); if current.as_deref() != Some(&text) { log::info!("[capsule] set_aux_down skipped: state changed to {current:?}"); @@ -4632,42 +4628,31 @@ fn emit_capsule( log::warn!("[capsule] set_aux_down failed: {e}"); } }); - // 首次设置(从 None 转为有值)时,fcitx5 可能还在处理触发 - // 快捷键的按键事件(press/release),这些事件可能覆盖 auxDown。 - // 延迟 300ms 重设一次确保状态不被竞态覆盖。 - // retry 使用 gen 去重:状态已变则不再发送旧文字。 - if was_none { + // 终态(Done/Cancelled/Error)3 秒后自动清除,避免一直跟随焦点。 + if matches!(state, CapsuleState::Done | CapsuleState::Cancelled | CapsuleState::Error) { let text = t.to_string(); std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_millis(300)); - // 检查代数:新的状态变化已发生则跳过。 + std::thread::sleep(std::time::Duration::from_secs(3)); let latest_gen = RETRY_GEN.load(std::sync::atomic::Ordering::SeqCst); - if gen != latest_gen { - log::info!("[capsule] set_aux_down retry skipped: gen {gen} < {latest_gen}"); + if latest_gen > gen + 1 { return; } let current = LAST_AUX.lock().unwrap().clone(); if current.as_deref() != Some(&text) { - log::info!("[capsule] set_aux_down retry skipped: state changed to {current:?}"); return; } - log::info!("[capsule] set_aux_down retry: {text}"); - if let Err(e) = crate::linux_fcitx::set_aux_down(&text) { - log::warn!("[capsule] set_aux_down retry failed: {e}"); - } + log::info!("[capsule] auto-clear terminal state: {text}"); + let _ = crate::linux_fcitx::set_aux_down(""); + *LAST_AUX.lock().unwrap() = None; }); } } None => { log::info!("[capsule] clear_aux_down gen={gen}"); - // 同样从音频线程挪走,避免阻塞。 - // 状态守卫:发送前确认 LAST_AUX 仍是 None,避免快速状态切换时 - // 旧 clear_aux_down 跑到新 set_aux_down 后面,把新文字清掉。 std::thread::spawn(move || { - // 检查代数:新的状态变化已发生则跳过。 let latest_gen = RETRY_GEN.load(std::sync::atomic::Ordering::SeqCst); - if gen != latest_gen { - log::info!("[capsule] clear_aux_down skipped: gen {gen} < {latest_gen}"); + if latest_gen > gen + 1 { + log::info!("[capsule] clear_aux_down skipped: gen {gen}, latest {latest_gen}"); return; } let current = LAST_AUX.lock().unwrap().clone(); diff --git a/openless-all/app/src/pages/settings/RecordingInputSection.tsx b/openless-all/app/src/pages/settings/RecordingInputSection.tsx index 6a1fc11c..d397a137 100644 --- a/openless-all/app/src/pages/settings/RecordingInputSection.tsx +++ b/openless-all/app/src/pages/settings/RecordingInputSection.tsx @@ -17,6 +17,7 @@ import { SelectLite } from '../../components/ui/SelectLite'; import { Card, Collapsible } from '../_atoms'; import { SettingRow, Toggle, inputStyle } from './shared'; import { MicrophoneSelect } from './MicrophoneSelect'; +import { detectOS } from '../../components/WindowChrome'; async function autostartIsEnabled(): Promise { const { invoke } = await import('@tauri-apps/api/core'); @@ -35,6 +36,7 @@ async function autostartDisable(): Promise { export function RecordingInputSection() { const { t } = useTranslation(); + const os = detectOS(); const { prefs, capability, updatePrefs: savePrefs } = useHotkeySettings(); const [microphoneDevices, setMicrophoneDevices] = useState([]); const [microphoneDevicesLoaded, setMicrophoneDevicesLoaded] = useState(false); @@ -205,15 +207,26 @@ export function RecordingInputSection() { )} + {os !== 'linux' && ( + )} + {os === 'linux' && ( + + void savePrefs({ ...prefs, streamingInsert: next })} + /> + + )} - {/* ─── 插入与剪贴板(折叠) ──────────────────────────────────── */} + {/* ─── 插入与剪贴板(折叠,仅 macOS / Windows) ──────────────── */} + {os !== 'linux' && ( @@ -256,7 +269,7 @@ export function RecordingInputSection() { /> - + )} {/* ─── 启动(折叠) ──────────────────────────────────────────── */} diff --git a/openless-all/app/src/pages/settings/ShortcutsSection.tsx b/openless-all/app/src/pages/settings/ShortcutsSection.tsx index 1e0cab69..e26b3f54 100644 --- a/openless-all/app/src/pages/settings/ShortcutsSection.tsx +++ b/openless-all/app/src/pages/settings/ShortcutsSection.tsx @@ -13,9 +13,11 @@ import { import { useHotkeySettings } from '../../state/HotkeySettingsContext'; import { Card } from '../_atoms'; import { SettingRow } from './shared'; +import { detectOS } from '../../components/WindowChrome'; export function ShortcutsSection() { const { t } = useTranslation(); + const os = detectOS(); const { prefs, hotkey, capability, updatePrefs: savePrefs } = useHotkeySettings(); if (!prefs || !hotkey || !capability) { @@ -28,7 +30,7 @@ export function ShortcutsSection() { const readonlyRows: Array<[string, string]> = [ [t('settings.shortcuts.cancel'), 'Esc'], - [t('settings.shortcuts.confirm'), t('settings.shortcuts.confirmHint')], + ...(os !== 'linux' ? [[t('settings.shortcuts.confirm'), t('settings.shortcuts.confirmHint')]] as Array<[string, string]> : []), ]; return ( diff --git a/openless-all/app/src/state/HotkeySettingsContext.tsx b/openless-all/app/src/state/HotkeySettingsContext.tsx index 7282482f..cb69b49a 100644 --- a/openless-all/app/src/state/HotkeySettingsContext.tsx +++ b/openless-all/app/src/state/HotkeySettingsContext.tsx @@ -88,15 +88,11 @@ export function HotkeySettingsProvider({ children }: { children: ReactNode }) { }, []) const queueSetSettings = useCallback( - (resolveNext: (current: UserPreferences) => UserPreferences) => { + (resolved: UserPreferences) => { const task = persistQueueRef.current .catch(() => undefined) .then(async () => { - const current = latestPrefsRef.current - if (!current) return - const next = resolveNext(current) - if (next === current) return - await setSettings(next) + await setSettings(resolved) }) persistQueueRef.current = task return task @@ -176,10 +172,7 @@ export function HotkeySettingsProvider({ children }: { children: ReactNode }) { const merged = { ...currentPrefs, ...nextLocalePrefs } latestPrefsRef.current = merged setPrefs(merged) - void queueSetSettings((current) => ({ - ...current, - ...nextLocalePrefs, - })).catch((error) => { + void queueSetSettings(merged).catch((error) => { console.warn( "[settings] sync locale output preferences failed", error, @@ -199,7 +192,7 @@ export function HotkeySettingsProvider({ children }: { children: ReactNode }) { if (resolved === current) return setPrefs(resolved) latestPrefsRef.current = resolved - await queueSetSettings(() => resolved) + await queueSetSettings(resolved) }, [queueSetSettings], ) diff --git a/openless-all/scripts/linux-fcitx5-plugin/openless.cpp b/openless-all/scripts/linux-fcitx5-plugin/openless.cpp index 8d05f5ac..cbb4ab24 100644 --- a/openless-all/scripts/linux-fcitx5-plugin/openless.cpp +++ b/openless-all/scripts/linux-fcitx5-plugin/openless.cpp @@ -222,6 +222,21 @@ class OpenLess final : public AddonInstance, instance_->flushUI(); })); + // 6. PostInputMethod 阶段恢复 lastAuxText_:fcitx5 在处理按键后可能 + // 清掉 auxDown(如 enter/backspace 触发内联模式),此钩子自动补回。 + eventHandlers_.push_back( + instance_->watchEvent( + EventType::InputContextKeyEvent, + EventWatcherPhase::PostInputMethod, + [this](Event &event) { + if (lastAuxText_.empty()) return; + auto &keyEvent = static_cast(event); + auto *ic = keyEvent.inputContext(); + if (!ic) return; + ic->inputPanel().setAuxDown(Text(lastAuxText_)); + ic->updateUserInterface(UserInterfaceComponent::InputPanel, true); + })); + FCITX_LOGC(openless, Info) << "OpenLess plugin loaded"; }