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:
1 parent
f517912068
commit
eb05bc1ca1
5 files changed
+50
-38
No files matched your search
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user