fix(linux): fcitx5 auxDown persistent + hotkey mode save + hide clipboard/capsule settings

- coordinator.rs: terminal state (Done/Error/Cancelled) auto-clears after 3s;
  fix RETRY_GEN comparison (fetch_add returns old value, use latest_gen > gen+1);
  remove broken 300ms retry
- HotkeySettingsContext.tsx: fix queueSetSettings never calling setSettings()
  (latestPrefsRef was already updated to the new object before microtask ran)
- openless.cpp: add PostInputMethod event hook to restore auxDown after
  fcitx5 processes key events
- RecordingInputSection.tsx: hide clipboard/paste/capsule settings on Linux;
  streaming insert shown as standalone card instead of inside clipboard group
- ShortcutsSection.tsx: hide "capsule confirm" shortcut hint on Linux

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
aeoformandClaude Opus 4.7 committed 2026-05-29 20:53:32 +08:00
1 parent f517912068
commit eb05bc1ca1
5 files changed
+50 -38

No files matched your search

+10 -25
View File
@@ -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();
@@ -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<boolean> {
const { invoke } = await import('@tauri-apps/api/core');
@@ -35,6 +36,7 @@ async function autostartDisable(): Promise<void> {
export function RecordingInputSection() {
const { t } = useTranslation();
const os = detectOS();
const { prefs, capability, updatePrefs: savePrefs } = useHotkeySettings();
const [microphoneDevices, setMicrophoneDevices] = useState<MicrophoneDevice[]>([]);
const [microphoneDevicesLoaded, setMicrophoneDevicesLoaded] = useState(false);
@@ -205,15 +207,18 @@ export function RecordingInputSection() {
)}
</div>
</SettingRow>
{os !== 'linux' && (
<SettingRow label={t('settings.recording.capsuleLabel')}>
<Toggle on={prefs.showCapsule} onToggle={onShowCapsuleChange} />
</SettingRow>
)}
<SettingRow label={t('settings.recording.muteDuringRecordingLabel')}>
<Toggle on={prefs.muteDuringRecording} onToggle={onMuteDuringRecordingChange} />
</SettingRow>
</Card>
{/* ─── 插入与剪贴板(折叠) ──────────────────────────────────── */}
{/* ─── 插入与剪贴板(折叠,仅 macOS / Windows) ──────────────── */}
{os !== 'linux' && (
<Collapsible title={t('settings.recording.insertGroupTitle')}>
<SettingRow label={t('settings.recording.restoreClipboardLabel')}>
<Toggle on={prefs.restoreClipboardAfterPaste} onToggle={onRestoreClipboardChange} />
@@ -256,6 +261,18 @@ export function RecordingInputSection() {
/>
</SettingRow>
</Collapsible>
)}
{/* ─── 流式输入(Linux) ────────────────────────────────────────── */}
{os === 'linux' && (
<Card>
<SettingRow label={t('settings.advanced.streamingInsertLabel')}>
<Toggle
on={!!prefs.streamingInsert}
onToggle={(next) => void savePrefs({ ...prefs, streamingInsert: next })}
/>
</SettingRow>
</Card>
)}
{/* ─── 启动(折叠) ──────────────────────────────────────────── */}
<Collapsible title={t('settings.recording.startupGroupTitle')}>
@@ -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 (
<Card>
@@ -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],
)
@@ -222,6 +222,21 @@ public:
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<KeyEvent &>(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";
}