Skip to content

fix(windows): 修复 OpenLess 前台主窗口热键失效#86

Merged
appergb merged 1 commit into
Open-Less:developfrom
Cooper-X-Oak:codex/windows-foreground-hotkey-fallback-upstream
Apr 30, 2026
Merged

fix(windows): 修复 OpenLess 前台主窗口热键失效#86
appergb merged 1 commit into
Open-Less:developfrom
Cooper-X-Oak:codex/windows-foreground-hotkey-fallback-upstream

Conversation

@Cooper-X-Oak
Copy link
Copy Markdown
Contributor

@Cooper-X-Oak Cooper-X-Oak commented Apr 30, 2026

摘要

修复 Windows 下 OpenLess 主窗口自身处于前台时,默认 rightControl 热键不触发的问题。

变更

  • 在 Windows 主窗口 keydown/keyup 上报当前键盘事件
  • 后端按当前 hotkey.trigger 匹配主窗口内的热键事件
  • 命中后复用既有 coordinator 的 pressed/released/cancel 语义
  • 增加 held 去重,避免 repeat 或双通路重复触发
  • 保持原有全局 hook 路径不变,只补足 OpenLess 自己前台时的缺口

验证

  • powershell -ExecutionPolicy Bypass -File .\openless-all\app\scripts\windows-build-gnu.ps1 -CheckOnly -KeepMirror
  • 实体键日志确认:
    • OpenLess 前台下出现
      • [window-hotkey] pressed trigger=RightControl code=ControlRight repeat=false
      • [coord] hotkey pressed mode=Toggle phase=Idle
    • Notepad 前台下原有全局 hook 行为仍正常

Closes #85

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 30, 2026

Reviewer's Guide

Implements a Windows-specific path for handling the main window’s own hotkey keyboard events by forwarding keydown/keyup from the frontend to the Rust coordinator, matching them against the configured hotkey trigger, and reusing existing coordinator pressed/released/cancel semantics with de‑duplication, plus optional UI key event logging for debugging.

Updated class diagram for Coordinator and hotkey handling

classDiagram
    class Inner {
        +Prefs prefs
        +Mutex~Option_Recorder~~ recorder
        +Mutex~Option_HotkeyMonitor~~ hotkey
        +Mutex~HotkeyStatus~ hotkey_status
        +Mutex~bool~ window_hotkey_held
    }

    class Coordinator {
        -Arc~Inner~ inner
        +new() Coordinator
        +handle_window_hotkey_event(event_type: &str, key: &str, code: &str, repeat: bool) Result~(), String~
        +repolish(raw_text: String, mode: PolishMode) Result~String, String~
    }

    class HotkeyTrigger {
        <<enum>>
        RightControl
        LeftControl
        RightOption
        RightAlt
        LeftOption
        RightCommand
        Fn
    }

    class CommandsModule {
        <<module>>
        +is_debug_ui_key_events_enabled() bool
        +debug_log_ui_key_event(event_type: String, key: String, code: String, ctrl: bool, alt: bool, shift: bool, meta: bool, repeat: bool) void
        +handle_window_hotkey_event(coord: CoordinatorState, event_type: String, key: String, code: String, repeat: bool) Result~(), String~
    }

    class CoordinatorCoreFunctions {
        <<module>>
        +handle_window_hotkey_event(inner: &Arc~Inner~, event_type: &str, key: &str, code: &str, repeat: bool) void
        +matches_window_hotkey(trigger: HotkeyTrigger, code: &str) bool
        +handle_pressed(inner: &Arc~Inner~) void
        +handle_released(inner: &Arc~Inner~) void
        +cancel_session(inner: &Arc~Inner~) void
    }

    class App_tsx {
        +useEffect_hotkey_debug()
        +useEffect_window_hotkey()
    }

    class Ipc_ts {
        +isDebugUiKeyEventsEnabled() Promise~boolean~
        +debugLogUiKeyEvent(eventType: string, key: string, code: string, ctrl: boolean, alt: boolean, shift: boolean, meta: boolean, repeat: boolean) Promise~void~
        +handleWindowHotkeyEvent(eventType: string, key: string, code: string, repeat: boolean) Promise~void~
    }

    Coordinator --> Inner
    CoordinatorCoreFunctions --> Inner
    CoordinatorCoreFunctions --> HotkeyTrigger
    CommandsModule --> Coordinator
    App_tsx --> Ipc_ts
    Ipc_ts --> CommandsModule
Loading

File-Level Changes

Change Details Files
Add Windows main-window hotkey handling in the coordinator, reusing existing pressed/released/cancel flows and preventing duplicate triggers.
  • Extend Coordinator inner state with a window_hotkey_held mutex flag and initialize it to false.
  • Expose an async handle_window_hotkey_event method on Coordinator that delegates to a new internal handler.
  • Implement a Windows-only handle_window_hotkey_event function that matches keyboard events against the configured HotkeyTrigger, handles Escape as a cancel, and calls handle_pressed/handle_released accordingly.
  • Introduce a matches_window_hotkey helper to map HotkeyTrigger variants to DOM KeyboardEvent.code values and ignore unsupported Fn trigger.
  • Guard non-Windows builds with a no-op implementation to keep cross-platform behavior unchanged.
openless-all/app/src-tauri/src/coordinator.rs
Wire frontend keydown/keyup events from the main window into Tauri IPC for both debugging and hotkey handling on Windows.
  • Import new IPC helpers (debugLogUiKeyEvent, handleWindowHotkeyEvent, isDebugUiKeyEventsEnabled) into the React App component.
  • Add an effect to optionally log all UI keyboard events when OPENLESS_DEBUG_UI_KEY_EVENTS is enabled via IPC, including key, code, modifiers, and repeat.
  • Add a Windows-only effect that attaches capturing keydown/keyup listeners on window and forwards minimal event data to the handle_window_hotkey_event IPC command.
  • Ensure cleanup of event listeners on unmount or when OS changes.
openless-all/app/src/App.tsx
Extend IPC surface with commands and helpers for UI key-event debugging and main-window hotkey bridging.
  • Add TypeScript IPC wrappers isDebugUiKeyEventsEnabled, debugLogUiKeyEvent, and handleWindowHotkeyEvent that invoke corresponding Tauri commands with the required payloads.
  • Implement Rust Tauri commands is_debug_ui_key_events_enabled and debug_log_ui_key_event to gate and log UI key events based on the OPENLESS_DEBUG_UI_KEY_EVENTS environment variable.
  • Implement an async Tauri command handle_window_hotkey_event that forwards events to Coordinator::handle_window_hotkey_event.
  • Register the new commands with the Tauri builder so they are callable from the frontend.
openless-all/app/src/lib/ipc.ts
openless-all/app/src-tauri/src/commands.rs
openless-all/app/src-tauri/src/lib.rs
Improve main window event logging on focus events to assist debugging.
  • Extend the Tauri RunEvent::WindowEvent handling for the main window to log focus changes via tauri::WindowEvent::Focused while preserving existing CloseRequested behavior.
openless-all/app/src-tauri/src/lib.rs

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="openless-all/app/src-tauri/src/commands.rs" line_range="322-303" />
<code_context>
+}
+
+#[tauri::command]
+pub async fn handle_window_hotkey_event(
+    coord: CoordinatorState<'_>,
+    event_type: String,
+    key: String,
+    code: String,
+    repeat: bool,
+) -> Result<(), String> {
+    coord.handle_window_hotkey_event(&event_type, &key, &code, repeat)
</code_context>
<issue_to_address>
**issue (bug_risk):** The Tauri command argument names don't match the keys used in the frontend invoke payload, which will likely break deserialization.

On the TS side, `handleWindowHotkeyEvent` is invoked with `{ eventType, key, code, repeat }`, so Tauri expects arguments with those exact names. The Rust command uses `event_type: String`, which won’t match `eventType`, so deserialization will fail at runtime. Please either align the Rust parameter names with the JS keys, use a struct with `#[serde(rename = "eventType")]` (and similar for others), or update the frontend payload to use snake_case. The same mismatch exists for `debug_log_ui_key_event` (`eventType` vs `event_type`).
</issue_to_address>

### Comment 2
<location path="openless-all/app/src-tauri/src/lib.rs" line_range="182-183" />
<code_context>
-                        api.prevent_close();
-                        hide_main_window(app);
+                    match event {
+                        tauri::WindowEvent::Focused(focused) => {
+                            log::info!("[window] main focused={focused}");
+                        }
+                        tauri::WindowEvent::CloseRequested { api, .. } => {
</code_context>
<issue_to_address>
**issue (bug_risk):** Losing window focus while the hotkey is held can leave `window_hotkey_held` stuck in the "pressed" state.

Because `window_hotkey_held` is only cleared on `keyup`, losing focus while the hotkey is pressed can leave it stuck `true`, making later hotkey presses no-op until restart. Add logic to reset this flag when the main window loses focus (e.g., in a `WindowEvent::Focused(false)` branch).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

alt: bool,
shift: bool,
meta: bool,
repeat: bool,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The Tauri command argument names don't match the keys used in the frontend invoke payload, which will likely break deserialization.

On the TS side, handleWindowHotkeyEvent is invoked with { eventType, key, code, repeat }, so Tauri expects arguments with those exact names. The Rust command uses event_type: String, which won’t match eventType, so deserialization will fail at runtime. Please either align the Rust parameter names with the JS keys, use a struct with #[serde(rename = "eventType")] (and similar for others), or update the frontend payload to use snake_case. The same mismatch exists for debug_log_ui_key_event (eventType vs event_type).

Comment on lines +182 to +183
tauri::WindowEvent::Focused(focused) => {
log::info!("[window] main focused={focused}");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Losing window focus while the hotkey is held can leave window_hotkey_held stuck in the "pressed" state.

Because window_hotkey_held is only cleared on keyup, losing focus while the hotkey is pressed can leave it stuck true, making later hotkey presses no-op until restart. Add logic to reset this flag when the main window loses focus (e.g., in a WindowEvent::Focused(false) branch).

@H-Chris233
Copy link
Copy Markdown
Collaborator

不幸的是,我们似乎进行了相同的工作... #90
还有,AI review给出了两个issue
请修复issues并重新触发review,然后我会合并分支

@appergb appergb merged commit e86edd1 into Open-Less:develop Apr 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants