Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 10 additions & 25 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}");
Expand All @@ -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();
Expand Down
17 changes: 15 additions & 2 deletions openless-all/app/src/pages/settings/RecordingInputSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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);
Expand Down Expand Up @@ -205,15 +207,26 @@ 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>
{os === 'linux' && (
<SettingRow label={t('settings.advanced.streamingInsertLabel')}>
<Toggle
on={!!prefs.streamingInsert}
onToggle={(next) => void savePrefs({ ...prefs, streamingInsert: next })}
/>
</SettingRow>
)}
</Card>

{/* ─── 插入与剪贴板(折叠) ──────────────────────────────────── */}
{/* ─── 插入与剪贴板(折叠,仅 macOS / Windows) ──────────────── */}
{os !== 'linux' && (
<Collapsible title={t('settings.recording.insertGroupTitle')}>
<SettingRow label={t('settings.recording.restoreClipboardLabel')}>
<Toggle on={prefs.restoreClipboardAfterPaste} onToggle={onRestoreClipboardChange} />
Expand Down Expand Up @@ -256,7 +269,7 @@ export function RecordingInputSection() {
/>
</SettingRow>
</Collapsible>

)}
{/* ─── 启动(折叠) ──────────────────────────────────────────── */}
<Collapsible title={t('settings.recording.startupGroupTitle')}>
<AutostartRow />
Expand Down
4 changes: 3 additions & 1 deletion openless-all/app/src/pages/settings/ShortcutsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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>
Expand Down
15 changes: 4 additions & 11 deletions openless-all/app/src/state/HotkeySettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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],
)
Expand Down
15 changes: 15 additions & 0 deletions openless-all/scripts/linux-fcitx5-plugin/openless.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<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";
}

Expand Down
Loading