### Superpowers/Specs/2026 06 29 Mouse Buttons 6 9 Design # Design: Mouse buttons 6–9 **Status:** Approved **Date:** 2026-06-29 **Scope:** Add four pickable actions that synthesize mouse buttons 6–9, so apps that bind those buttons (CAD, games, Blender, MMO-mouse emulators) receive them. ## Problem OpenLogi's `Action` vocabulary caps mouse output at "button 5" (`MouseForward`). There is no way to emit mouse buttons 6–9, even though the underlying injection layer on macOS and Linux already supports arbitrary button numbers. Users who want a physical button to produce a button-6-or-higher event have no path today. ## Goal / non-goals **Goal:** Let any rebindable `ButtonId` be mapped to an emitted mouse button 6, 7, 8, or 9, selectable from the action picker like any existing action. **Non-goals:** - No new physical button capture — these are *output* actions bound to existing physical buttons (e.g. Gesture Button → button 6). - No new picker UI. The catalog auto-surfaces new variants under the MOUSE section. - No Windows support for buttons 6–9. `SendInput`'s mouse path carries flags for buttons 1–5 only; 6–9 are a documented macOS/Linux-only gap (Windows remains an "untested preview" per the README). See [Platform coverage](#platform-coverage). ## Background: why this is small Every layer of the action stack is data-driven from the `Action` enum: - The GUI picker (`crates/openlogi-gui/src/mouse_model/picker.rs`) builds its rows by calling `Action::catalog()` and grouping by `Action::category()`. - The picker's icon mapping (`picker.rs:298`) is an exhaustive `match` with **no wildcard arm**, so the compiler refuses to build if a new variant lacks an icon entry. - `Action::label()` / `category()` / `catalog()` drive the picker text, grouping, and TOML roundtrip tests. - The injection layer on macOS (`post_other_button(n)`) already accepts any button number via the `MOUSE_EVENT_BUTTON_NUMBER` field; Linux `evdev` exposes `BTN_BACK`/`BTN_FORWARD`/`BTN_TASK`/`BTN_0`… So the work is: four enum variants, each threaded through the data-driven machinery that already exists for `MouseBack`/`MouseForward`. ## Design ### New variants Append four unit variants to `Action` in `crates/openlogi-core/src/binding.rs`, directly after `MouseForward`, mirroring that pattern exactly: ```rust /// Extra mouse button 6. Emitted as the real button-6 event for apps/games/CAD /// that bind it. macOS/Linux only — Windows SendInput caps at button 5. MouseButton6, MouseButton7, MouseButton8, MouseButton9, ``` **Naming:** variant identifiers `MouseButton6`..`MouseButton9`; display labels `"Button 6"`..`"Button 9"`. Unlike `Back`/`Forward`, these numbers have no universal semantic meaning, so they carry no semantic name. ### Per-layer changes | Layer | File | Change | |---|---|---| | Enum | `openlogi-core/src/binding.rs` | +4 variants after `MouseForward` | | `Action::label()` | same | `"Button 6"` … `"Button 9"` | | `Action::category()` | same | all 4 → `Category::Mouse` | | `Action::catalog()` | same | append all 4 to the catalog (Mouse group) | | Picker icon map | `openlogi-gui/src/mouse_model/picker.rs:298` | map all 4 to an existing generic icon (`action-icons/mouse.svg`) — the existing exhaustive `match` forces this | | Inject — macOS | `openlogi-inject/src/inject.rs` (`execute_macos`) | `MouseButton6..9` → `macos::post_other_button(5..=8)` | | Inject — Linux | `openlogi-inject/src/inject.rs` (`execute_linux`) | → `BTN_FORWARD` / `BTN_BACK` / `BTN_TASK` / `BTN_0` | | Inject — Windows | `openlogi-inject/src/inject.rs` (`execute_windows`) | log-and-skip (`tracing::debug!`, same pattern as the macOS-only navigation actions at `inject.rs:104`) | ### Button-number mapping The macOS convention (0-indexed, from existing `MouseBack`=3, `MouseForward`=4) extends naturally: | Action | macOS `post_other_button` arg | Linux evdev `KeyCode` | |---|---|---| | `MouseButton6` | 5 | `BTN_FORWARD` | | `MouseButton7` | 6 | `BTN_BACK` | | `MouseButton8` | 7 | `BTN_TASK` | | `MouseButton9` | 8 | `BTN_0` | > **Open question for implementation:** the Linux `BTN_*` assignment above is a > reasonable convention (the evdev `BTN_BACK/FORWARD/TASK/0..9` family is how > multi-button mice report extras), but exact code choice is a convention call > that should be confirmed against how target apps read buttons on Linux. > macOS numbers are unambiguous. ### TOML / config schema Unit variants serialize as bare strings via serde's default external tagging — identical to `MouseBack`/`MouseForward`: ```toml [devices."".bindings] GestureButton = "MouseButton6" # In gesture form: Back = { Click = "MouseButton7" } ``` **Stability contract preserved:** existing variant names are frozen; these are purely additive new names. **No `schema_version` bump, no migration.** Older OpenLogi builds reading a config containing `MouseButton6` will error on the unknown variant (acceptable — same as any newer-schema config on older code). ### Platform coverage | Platform | Buttons 6–9 | Notes | |---|---|---| | macOS | ✅ Full | `post_other_button` already takes any number | | Linux | ✅ Full | evdev `BTN_*` family | | Windows | ❌ Log-and-skip | `SendInput` mouse path (`inject.rs:1416`) has flags for buttons 1–5 only; no flag exists for 6+. Documented gap, matches the codebase's existing "no platform equivalent → debug log + skip" pattern. | Windows users who bind these actions see nothing on press and a debug log line; no crash, no misfire. Given Windows is an untested preview and the requester is on macOS, this boundary is acceptable and explicitly out of scope to fix here. ## Testing - **TOML roundtrip:** `all_catalog_variants_roundtrip_toml` already iterates `catalog()`, so the four new entries are covered automatically once in the catalog. - **Category:** extend `category_mouse_variants` to assert all four map to `Category::Mouse`. - **Compile-time guarantee:** the exhaustive picker icon `match` (no wildcard) fails to build if any variant is missed — this is the primary safety net. ## Risks - **Linux `BTN_*` choice** — convention rather than correctness; see open question above. Low impact (target apps are the test). - **Pickup-row clutter** — four more entries in the MOUSE group. Acceptable; matches user intent for a "full set". - None to the input-capture path — these are pure output/synthesis actions. ## Out of scope - Windows support for buttons 6–9. - A parameterized `MouseButton(n)` variant (Approach B) — rejected as disproportionate picker UI for a fixed set of four. - Capturing buttons 6–9 as *input* from exotic hardware. --- ### Superpowers/Specs/2026 06 30 Function Key Remapper Design # Design: Function-Key Remapper **Status:** Draft (pending user review) **Date:** 2026-06-30 **Scope:** Turn every capturable function-row key (and, in a later milestone, the system media keys) into a fully programmable trigger that can reassign media keys, type macro strings, run AppleScript, run shell commands, or execute a timed multi-step workflow. ## Motivation OpenLogi today remaps **mouse** buttons only. Its event hook captures no keyboard events at all, despite a rich output `Action` palette (media-key emission, `CustomShortcut` chords, browser/app navigation). Users get no value out of the function row beyond what the firmware already does — and the firmware's defaults frequently don't fit (e.g. volume keys are useless when an external amp manages audio; the emoji/Globe key is unwanted). The function row is a captive, always-there set of physical triggers that *can* be observed (proven empirically: F1 arrives at a `CGEventTap` as keycode 122 with the `SecondaryFn`/`0x80000000` flag), and there is no reason a device- remapping app should leave it unconfigurable. This design makes every capturable key a fully programmable one. ## Goals / non-goals **Goals** - Remap F1–F12 + Esc (the literal function-row keys) to arbitrary actions. - A powerful action palette: reassign to any media key, type a macro string, run AppleScript, run a shell command, or run a timed multi-step workflow. - Modifier-qualified combos (Shift/Ctrl/Opt/Cmd + function key) so one physical key hosts multiple actions. - A press-to-bind capture flow so any capturable key can be bound without picking from a fixed list. **Non-goals (for this design)** - **Capturing the `Fn` modifier itself** as a trigger. Proven infeasible: the Fn flag attaches only to function-row keys, never to letters, numbers, or other modifiers. `Fn+Q` is byte-identical to plain `Q` at the event tap. Fn is firmware-internal unless the key has a dual function-row meaning. See Appendix A. - **Windows / Linux** capture in the initial milestones. macOS first; the execution actions cross-platform where they already are; capture ported later. - **Per-application profiles** for keyboard bindings in M1 (the mouse side has these; the keyboard side inherits them in a later milestone once the base works). ## Background: what exists today - **Hook is mouse-only** (`crates/openlogi-hook/src/macos.rs::translate`): handles `LeftMouseDown`/`RightMouseDown`/scroll/move only. Zero keyboard events. This is the central new ground. - **Rich action palette** (`crates/openlogi-core/src/binding.rs::Action`): `VolumeUp/Down`, `MuteVolume`, `PlayPause`, `NextTrack`, `PrevTrack`, `BrightnessUp/Down`, `BrowserBack/Forward`, `MissionControl`, `LaunchpadShow`, `Paste/Copy/Cut/Undo/Redo`, `CustomShortcut(KeyCombo)`, `SetDpiPreset`, and (via the mouse-buttons-6-9 PR) `MouseButton6..9`. - **Media-key emission exists** (`macos::post_media_key(NX_KEYTYPE_*)`) — so "reassign to a media key" is already an execution primitive, not new work. - **Key-chord emission exists** (`CustomShortcut` → `macos::post_key` + modifiers) — so emitting key sequences is partially there, but there is **no text-typing / unicode-string primitive** (`CGEventKeyboardSetUnicodeString`). - **Config is TOML**, keyed per-device, with a frozen variant-name contract and `schema_version` for migrations. ## Architecture The feature splits into two halves with very different risk profiles. ### Half 1 — Execution (what a key does): all buildable The action palette gains three new `Action` variants, all reusing the existing enum → picker → injection pipeline (the same one extended for mouse buttons 6–9). No new mechanism, only new variants + two new emission primitives. | Action variant | Mechanism | New work | |---|---|---| | `RunAppleScript(String)` | spawn `osascript -e ""` | new variant, trivial | | `RunShellCommand(String)` | spawn shell, capture nothing | new variant, trivial | | `TypeText(String)` | new `macos::post_unicode(&str)` via `CGEventKeyboardSetUnicodeString` | new variant **+ new emitter** | | `Workflow(Vec)` | a sequencer that runs steps with `Delay` timing | new variant **+ new sequencer subsystem** | | (media reassignment) | existing `post_media_key` | **already exists** | A `WorkflowStep` is a small enum: ```rust enum WorkflowStep { TypeText(String), PressKey(KeyCode), // reuse the key-emitter from CustomShortcut Delay(Duration), RunAppleScript(String), RunShellCommand(String), } ``` The sequencer runs steps in order, awaiting `Delay`s. This is the native, no-code version of the "type 'bite me', wait 5s, Enter, wait 5s, type more, Escape" example. Power users can equivalently express the same thing in a single `RunAppleScript` or `RunShellCommand`. ### Half 2 — Capture (which key triggers it): split by risk Extending the mouse-only hook to also subscribe to keyboard `CGEvent` types is the central new capture work. It splits by key class: | Key class | Capture mechanism | Risk | |---|---|---| | **F1–F12, Esc** (function mode) | Extend the existing `CGEventTap` mask to include `keyDown`/`keyUp`/`flagsChanged`; new `KeyEvent` vocabulary analogous to `MouseEvent` | **Low** — same tap, new event types. F1 proven empirically (keycode 122 + `0x80000000`). | | **Media keys** (volume / brightness / emoji / play / etc.) | New `NX_SYSDEFINED` system-event tap (`CGSSetSystemDefinedMediaTap`) — a **separate event stream** OpenLogi has none of today | **High / unproven** — gated milestone; see M3. | This split is why the milestones order F-key capture before media-key capture: F-key capture is an extension of the proven existing tap; media-key capture is a new subsystem whose feasibility must be empirically confirmed before design commits to it. ## Trigger specification Three complementary ways to specify a trigger, all producing the same `KeyTrigger`: ```rust /// A keyboard trigger: a keycode plus an optional modifier mask. /// Stored under `[keyboard.bindings]` keyed by a stable string. struct KeyTrigger { keycode: u16, // macOS kVK_* code (e.g. 122 = F1) modifiers: Modifiers, // Shift/Control/Option/Command mask; empty for bare } ``` 1. **Fixed F-key list** — the picker offers F1–F12 + Esc (the keys proven capturable). Matches the mouse-button picker UX. 2. **Modifier-qualified combos** — Shift/Ctrl/Opt/Cmd + F-key, so one physical key hosts several actions. These modifiers ARE detectable (unlike Fn). 3. **Press-to-bind capture** — a "press a key to bind" flow: OpenLogi records the next `keyDown`'s keycode (+modifiers) and binds it. Generalizes beyond the fixed list to any capturable key. ## Config schema (additive) A new top-level `[keyboard]` section, keyed by a stable trigger string. New `Action` variants are tagged unions (serde external tagging), consistent with `CustomShortcut(KeyCombo)`: ```toml # Existing device bindings unchanged: [devices."".bindings] GestureButton = "MissionControl" # NEW — keyboard bindings, independent of device: [keyboard.bindings] "f1" = { TypeText = "bite me" } "shift+f1" = { RunAppleScript = "tell application \"Terminal\" to activate" } "cmd+f1" = { RunShellCommand = "open -a 'Safari' https://example.com" } "f2" = "VolumeUp" # reassign a function key to a media key "f3" = { Workflow = [ { TypeText = "bite me" }, { Delay = "5s" }, { PressKey = "Return" }, { Delay = "5s" }, { TypeText = "bite me bad" }, { PressKey = "Return" }, { PressKey = "Escape" }, ]} ``` **Stability contract:** existing variant names are frozen; these are purely additive. A `[keyboard]` section is new, but unknown top-level sections are ignored by older loaders, so no `schema_version` bump is *required* for back-compat. Bump it anyway (cheap, conventional) so the GUI can show a clean "what changed" diff and refuse to silently drop bindings a newer build wrote. ## Milestones **M1 — F-key capture + powerful action palette (shippable, low-risk)** - Extend `openlogi-hook` to capture keyboard `CGEvent`s; new `KeyEvent` vocab. - New `Action` variants: `RunAppleScript`, `RunShellCommand`, `TypeText` (+ new `macos::post_unicode` emitter). - `[keyboard.bindings]` config + loader; fixed F-key picker UI. - Modifier-qualified combos (Shift/Ctrl/Opt/Cmd + F-key). - Deliverable: any F1–F12/Esc (and combo) runs AppleScript / shell / types a string / fires a media key. **M2 — Native Workflow sequencer** - `Workflow(Vec)` action + sequencer with `Delay` timing. - `WorkflowStep`: `TypeText`, `PressKey`, `Delay`, `RunAppleScript`, `RunShellCommand`. - Deliverable: the timed multi-step "type, wait, Enter, wait, type, Esc" flows authorable in TOML without scripting. **M3 — Media-key capture (gated on feasibility test)** - *Before any design commitment:* empirically test whether volume/brightness/ emoji keys are interceptable via an `NX_SYSDEFINED` system-event tap. If the OS grabs them below the tap (as the Fn investigation warned), this milestone is descoped or killed — do not assume. - If feasible: new system-event tap subsystem; extend trigger list to media keys; deliverable: remap volume/brightness/emoji to any action. ## Risks (honest) 1. **Media-key capture (M3) is unproven.** macOS routes system media keys through `NX_SYSDEFINED`, a separate stream from `CGEventTap`. OpenLogi has zero of this today. The feasibility test gates M3; M1/M2 do not depend on it. 2. **Security surface.** `RunShellCommand` / `RunAppleScript` execute arbitrary code from config. This is a real escalation vs. today's action set. Mitigation: these variants are **never** in the default catalog; they must be hand-authored in config, and the loader warns on first use. (Matches how `CustomShortcut` is already a deliberate escape hatch.) 3. **Key-suppression correctness.** Remapping requires *consuming* the original key event (returning "drop this") so it doesn't also type. The mouse hook already does this via `EventDisposition`; the keyboard path must too, with care to avoid wedging input (the documented HID-tap-wedge failure mode). 4. **Capture vs. the existing mouse tap.** Adding keyboard event types to the existing tap broadens what it intercepts; the HID-location tap that outlives its permission wedges **all** input (mouse + keyboard). Extra care + testing needed here, given that documented failure mode. ## Out of scope - Capturing the `Fn` modifier as a trigger (proven infeasible — Appendix A). - Per-application keyboard profiles in M1 (mouse side has these; keyboard inherits later). - Windows/Linux keyboard capture in M1 (port after macOS works). --- ## Appendix A: Why Fn is not a trigger (proven, not assumed) Investigated empirically this session with an instrumented `CGEventTap`: - **F1** arrives as keycode 122 **with** the `SecondaryFn`/`0x80000000` flag. - **plain Q** and **Fn+Q** are byte-for-byte identical (keycode 12, `raw=0x100`, no Fn flag). Same for A. - **plain Shift** and **Fn+Shift** are byte-for-byte identical (`raw=0x20102`, no Fn flag). - Pressing **Fn alone** produces no event of any kind (no `FlagsChanged`). **Conclusion:** the Fn flag attaches **only to function-row keys** (F1–F12), never to letters, numbers, or other modifiers. The keyboard firmware holds Fn internal unless the key has a dual function-row meaning. `Fn+` is indistinguishable from `` at the `CGEventTap`. This is firmware behavior, not a limitation OpenLogi can code around at this layer. The only theoretical path to sensing Fn+letters is raw-HID reading below the OS event system (Karabiner/driver-kit territory) — a large subsystem with no guarantee the MX Keys S exposes Fn there. Not pursued. --- ### Superpowers/Plans/2026 06 29 Mouse Buttons 6 9 # Mouse Buttons 6–9 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add four pickable actions (`MouseButton6`–`MouseButton9`) that synthesize mouse buttons 6–9 on macOS and Linux, surfaced in the action picker under the MOUSE section. **Architecture:** Pure data-driven addition. The `Action` enum in `openlogi-core` is the single source of truth — the GUI picker, category grouping, TOML schema, and per-platform injection all derive from it. Four new unit variants mirror the existing `MouseBack`/`MouseForward` pattern. On macOS the existing `post_other_button(n)` already accepts any button number; on Linux the evdev `BTN_*` family covers it; on Windows `SendInput` caps at button 5, so 6–9 log-and-skip there (documented gap, same pattern as existing platform-limited actions). **Tech Stack:** Rust (workspace), serde/TOML for config, GPUI for the GUI, core-graphics / evdev / windows-sys for injection, rust-i18n for locale strings. **Spec:** `docs/superpowers/specs/2026-06-29-mouse-buttons-6-9-design.md` --- ## File Structure | File | Responsibility | Change | |---|---|---| | `crates/openlogi-core/src/binding.rs` | The `Action` enum + `label()`/`category()`/`catalog()` | Add 4 variants + their 3 match arms + extend 2 tests | | `crates/openlogi-inject/src/inject.rs` | Per-platform `Action` → OS event synthesis | Add arms in `execute_macos` / `execute_linux` / `execute_windows` | | `crates/openlogi-gui/src/mouse_model/picker.rs` | Picker icon mapping (exhaustive `match`) | Add 4 arms (compiler-forced) | | `crates/openlogi-gui/locales/*.yml` (20 files) | i18n translation keys, keyed by English label | Add `"Button 6"`–`"Button 9"` keys after line 147 | No new files. The exhaustive `match` arms across the codebase are the safety net — the compiler refuses to build if any variant is missed. --- ## Task 1: Add the `Action` variants and core metadata This task adds the four variants and threads them through `label()`, `category()`, and `catalog()`. Tests fail first, then pass. **Files:** - Modify: `crates/openlogi-core/src/binding.rs` - [ ] **Step 1: Write the failing test (extend `category_mouse_variants`)** In `crates/openlogi-core/src/binding.rs`, find the test at line ~1346 and replace it: ```rust #[test] fn category_mouse_variants() { assert_eq!(Action::LeftClick.category(), Category::Mouse); assert_eq!(Action::RightClick.category(), Category::Mouse); assert_eq!(Action::MiddleClick.category(), Category::Mouse); assert_eq!(Action::MouseBack.category(), Category::Mouse); assert_eq!(Action::MouseForward.category(), Category::Mouse); assert_eq!(Action::MouseButton6.category(), Category::Mouse); assert_eq!(Action::MouseButton7.category(), Category::Mouse); assert_eq!(Action::MouseButton8.category(), Category::Mouse); assert_eq!(Action::MouseButton9.category(), Category::Mouse); } ``` - [ ] **Step 2: Add a label test (append to the `#[cfg(test)] mod tests` block, after `category_mouse_variants`)** ```rust #[test] fn extra_mouse_button_labels() { assert_eq!(Action::MouseButton6.label(), "Button 6"); assert_eq!(Action::MouseButton7.label(), "Button 7"); assert_eq!(Action::MouseButton8.label(), "Button 8"); assert_eq!(Action::MouseButton9.label(), "Button 9"); } ``` - [ ] **Step 3: Run the tests to verify they fail** Run: `cargo test -p openlogi-core --lib binding::tests::category_mouse_variants binding::tests::extra_mouse_button_labels` Expected: FAIL with `cannot find variant MouseButton6` (and 7/8/9) — compile error. - [ ] **Step 4: Add the four variants to the `Action` enum** In `crates/openlogi-core/src/binding.rs`, find the `MouseForward` variant (line ~371) and add the four new variants immediately after it, before the `// ── Editing ───` comment: ```rust /// Mouse "forward" side button (extra button 5). Native counterpart to /// [`Action::MouseBack`]; see [`Action::BrowserForward`] for the ⌘] form. MouseForward, /// Extra mouse button 6. Emitted as the real button-6 event for apps, games, /// and CAD software that bind it. macOS/Linux only — Windows `SendInput` /// caps at button 5, so this logs-and-skips there. MouseButton6, /// Extra mouse button 7. See [`Action::MouseButton6`]. MouseButton7, /// Extra mouse button 8. See [`Action::MouseButton6`]. MouseButton8, /// Extra mouse button 9. See [`Action::MouseButton6`]. MouseButton9, ``` - [ ] **Step 5: Add the four labels to `Action::label()`** In the `label()` match (around line ~686, after the `MouseForward` arm), add: ```rust Action::MouseForward => "Forward (Button 5)".into(), Action::MouseButton6 => "Button 6".into(), Action::MouseButton7 => "Button 7".into(), Action::MouseButton8 => "Button 8".into(), Action::MouseButton9 => "Button 9".into(), ``` - [ ] **Step 6: Add the four to `Action::category()`** In the `category()` match (around line ~737), extend the existing Mouse arm: ```rust Action::LeftClick | Action::RightClick | Action::MiddleClick | Action::MouseBack | Action::MouseForward | Action::MouseButton6 | Action::MouseButton7 | Action::MouseButton8 | Action::MouseButton9 => Category::Mouse, ``` - [ ] **Step 7: Add the four to `Action::catalog()`** In the `catalog()` vec (around line ~793, after `Action::MouseForward`), add: ```rust // Mouse Action::LeftClick, Action::RightClick, Action::MiddleClick, Action::MouseBack, Action::MouseForward, Action::MouseButton6, Action::MouseButton7, Action::MouseButton8, Action::MouseButton9, ``` - [ ] **Step 8: Run the tests to verify they pass** Run: `cargo test -p openlogi-core --lib binding` Expected: PASS — `category_mouse_variants`, `extra_mouse_button_labels`, and `all_catalog_variants_roundtrip_toml` (which iterates `catalog()`) all pass. - [ ] **Step 9: Commit** ```bash git add crates/openlogi-core/src/binding.rs git commit -m "feat(core): add MouseButton6-9 actions to the binding vocabulary" ``` --- ## Task 2: Inject the buttons on macOS macOS already has `post_other_button(n)` which stamps `MOUSE_EVENT_BUTTON_NUMBER`. Buttons 6–9 map to numbers 5–8 (0-indexed: Back=3, Forward=4). **Files:** - Modify: `crates/openlogi-inject/src/inject.rs` (the `execute_macos` function, around line 186–187) - [ ] **Step 1: Add the macOS injection arms** Find the macOS extra-button arms (line ~186–187): ```rust Action::MouseBack => macos::post_other_button(3), Action::MouseForward => macos::post_other_button(4), ``` Add immediately after them: ```rust Action::MouseBack => macos::post_other_button(3), Action::MouseForward => macos::post_other_button(4), // Buttons 6–9 (button numbers 5–8, 0-indexed). Same path as 4/5 — // post_other_button stamps MOUSE_EVENT_BUTTON_NUMBER to address any // button ≥ 3. Action::MouseButton6 => macos::post_other_button(5), Action::MouseButton7 => macos::post_other_button(6), Action::MouseButton8 => macos::post_other_button(7), Action::MouseButton9 => macos::post_other_button(8), ``` - [ ] **Step 2: Verify the macOS build compiles** Run: `cargo build -p openlogi-inject` Expected: BUILD SUCCEEDS (on macOS the `execute_macos` arms are the ones compiled). - [ ] **Step 3: Commit** ```bash git add crates/openlogi-inject/src/inject.rs git commit -m "feat(inject): synthesize mouse buttons 6-9 on macOS" ``` --- ## Task 3: Inject the buttons on Linux evdev 0.13.2 exposes `BTN_BACK`, `BTN_FORWARD`, `BTN_TASK`, and `BTN_0` as `KeyCode` constants. These are the conventional codes for extra mouse buttons beyond the side pair. **Files:** - Modify: `crates/openlogi-inject/src/inject.rs` (the `execute_linux` function, around line 77–78) - [ ] **Step 1: Add the Linux injection arms** Find the Linux extra-button arms (line ~77–78): ```rust Action::MouseBack => linux::click(KeyCode::BTN_SIDE), Action::MouseForward => linux::click(KeyCode::BTN_EXTRA), ``` Add immediately after them: ```rust Action::MouseBack => linux::click(KeyCode::BTN_SIDE), Action::MouseForward => linux::click(KeyCode::BTN_EXTRA), // Buttons 6–9 use the evdev extra-button codes beyond the side pair. Action::MouseButton6 => linux::click(KeyCode::BTN_FORWARD), Action::MouseButton7 => linux::click(KeyCode::BTN_BACK), Action::MouseButton8 => linux::click(KeyCode::BTN_TASK), Action::MouseButton9 => linux::click(KeyCode::BTN_0), ``` - [ ] **Step 2: Verify it compiles on Linux (cross-check)** This is a `#[cfg(target_os = "linux")]` block. On macOS it won't be compiled, so to verify the `KeyCode::BTN_*` constants resolve, run a Linux target check: Run: `cargo check -p openlogi-inject --target x86_64-unknown-linux-gnu` Expected: If the target is installed, CHECK SUCCEEDS. If not installed, this step is skipped — the constant names (`BTN_FORWARD`/`BTN_BACK`/`BTN_TASK`/`BTN_0`) are confirmed present in evdev 0.13.2 (see spec's button-number table), and CI will catch any mismatch on Linux. - [ ] **Step 3: Commit** ```bash git add crates/openlogi-inject/src/inject.rs git commit -m "feat(inject): synthesize mouse buttons 6-9 on Linux via evdev BTN_*" ``` --- ## Task 4: Log-and-skip on Windows Windows `SendInput` mouse input carries flags for buttons 1–5 only; there is no flag for button 6+. The codebase's established pattern for "no platform equivalent" is a `tracing::debug!` log and skip (see the macOS-only navigation actions at `inject.rs:104`). Mirror that. **Files:** - Modify: `crates/openlogi-inject/src/inject.rs` (the `execute_windows` function, around line 290–291) - [ ] **Step 1: Add the Windows log-and-skip arms** Find the Windows extra-button arms (line ~290–291): ```rust Action::MouseBack => windows::post_click(windows::MouseButton::Back), Action::MouseForward => windows::post_click(windows::MouseButton::Forward), ``` Add immediately after them: ```rust Action::MouseBack => windows::post_click(windows::MouseButton::Back), Action::MouseForward => windows::post_click(windows::MouseButton::Forward), // Windows SendInput carries flags for buttons 1–5 only; there is no // flag for button 6+, so these log-and-skip (same pattern as the // macOS-only navigation actions). macOS/Linux emit them natively. Action::MouseButton6 | Action::MouseButton7 | Action::MouseButton8 | Action::MouseButton9 => { tracing::debug!( action = action.label(), "mouse buttons 6-9 are not supported on Windows — press ignored" ); } ``` - [ ] **Step 2: Verify it compiles on Windows (cross-check)** Run: `cargo check -p openlogi-inject --target x86_64-pc-windows-msvc` Expected: If the target is installed, CHECK SUCCEEDS. If not, skip — CI covers Windows. (No new types are referenced; `tracing::debug!` and `action.label()` already exist.) - [ ] **Step 3: Commit** ```bash git add crates/openlogi-inject/src/inject.rs git commit -m "feat(inject): log-and-skip mouse buttons 6-9 on Windows" ``` --- ## Task 5: Add picker icons (compiler-forced) The picker's `action_icon_path` is an exhaustive `match` with no wildcard. After Task 1, the macOS/GUI build will fail here until the four variants are mapped. Reuse the existing generic mouse icon (`action-icons/mouse.svg`, already used by `MiddleClick`). **Files:** - Modify: `crates/openlogi-gui/src/mouse_model/picker.rs` (the `action_icon_path` match, line ~304–305) - [ ] **Step 1: Add the four icon arms** Find the MouseBack/MouseForward arms (line ~304–305): ```rust Action::MouseBack => "action-icons/circle-arrow-left.svg", Action::MouseForward => "action-icons/circle-arrow-right.svg", ``` Add immediately after them: ```rust Action::MouseBack => "action-icons/circle-arrow-left.svg", Action::MouseForward => "action-icons/circle-arrow-right.svg", // Buttons 6–9 have no canonical glyph; reuse the generic mouse icon // (same as MiddleClick). The button number is in the label. Action::MouseButton6 | Action::MouseButton7 | Action::MouseButton8 | Action::MouseButton9 => "action-icons/mouse.svg", ``` - [ ] **Step 2: Verify the full workspace builds (this is the compile-time gate)** Run: `cargo build -p openlogi-gui` Expected: BUILD SUCCEEDS. If any variant is still missing an arm anywhere, this fails — that's the safety net working. - [ ] **Step 3: Commit** ```bash git add crates/openlogi-gui/src/mouse_model/picker.rs git commit -m "feat(gui): pick icons for mouse buttons 6-9 in the action picker" ``` --- ## Task 6: Add i18n keys to all 20 locale files The picker translates action labels via `t!(action.label())` — keyed by the English string. The existing `"Back (Button 4)"` / `"Forward (Button 5)"` keys live at line ~146–147 of every locale file. New keys `"Button 6"`–`"Button 9"` must be added so the labels translate. For non-English locales, the translation mirrors the English form plus the localized "Button" word where the locale already uses one — but since the English label is intentionally number-only, the safest correct default is to leave the translated value identical to the key (English fallback) except where the locale clearly localizes "Button" (most do not for raw button numbers). **Files:** - Modify: all 20 files in `crates/openlogi-gui/locales/*.yml` - [ ] **Step 1: Add the four keys to `en.yml` (the source)** In `crates/openlogi-gui/locales/en.yml`, after line 147 (`"Forward (Button 5)": "Forward (Button 5)"`), add: ```yaml "Forward (Button 5)": "Forward (Button 5)" "Button 6": "Button 6" "Button 7": "Button 7" "Button 8": "Button 8" "Button 9": "Button 9" ``` - [ ] **Step 2: Add the same four keys to each of the other 19 locale files** For each file in `crates/openlogi-gui/locales/` except `en.yml`, after the `"Forward (Button 5)"` line (which exists at line ~147 in every file — confirmed), append: ```yaml "Button 6": "Button 6" "Button 7": "Button 7" "Button 8": "Button 8" "Button 9": "Button 9" ``` The 19 files are: `da.yml de.yml el.yml es.yml fi.yml fr.yml it.yml ja.yml ko.yml nb.yml nl.yml pl.yml pt-BR.yml pt-PT.yml ru.yml sv.yml zh-CN.yml zh-HK.yml zh-TW.yml`. (Values are left as the English string — these are raw button numbers with no semantic name to localize. A crowdin pass can refine later; the project already uses crowdin per `crowdin.yml`. Leaving them English-identical is the correct fallback and matches how `en.yml` itself is authored.) - [ ] **Step 3: Verify the GUI still builds and the i18n test passes** Run: `cargo test -p openlogi-gui --lib i18n` Expected: PASS. (The i18n test at `i18n.rs:185+` checks specific known strings, not exhaustively, so it won't break — but it confirms the locale loader still parses all files.) - [ ] **Step 4: Commit** ```bash git add crates/openlogi-gui/locales/*.yml git commit -m "feat(gui): add Button 6-9 translation keys to all locales" ``` --- ## Task 7: Final whole-workspace verification Confirm everything builds and tests pass end-to-end. - [ ] **Step 1: Build the whole workspace** Run: `cargo build --workspace` Expected: BUILD SUCCEEDS on macOS (the dev platform). This compiles `execute_macos`, the picker, the core — everything reachable on this host. - [ ] **Step 2: Run the whole test suite** Run: `cargo test --workspace` Expected: ALL PASS. Key tests: `binding::tests::category_mouse_variants`, `binding::tests::extra_mouse_button_labels`, `binding::tests::all_catalog_variants_roundtrip_toml` (now exercises the 4 new variants' TOML roundtrip). - [ ] **Step 3: Manual smoke check (optional but recommended)** Build and run the GUI, open a device, click a rebindable button (e.g. Gesture Button), and confirm "Button 6"–"Button 9" appear in the MOUSE section of the action picker. Bind one and confirm it fires (e.g. an app that binds MB6). Run: `cargo run -p openlogi-gui` - [ ] **Step 4: Final commit if any fixups were needed** If steps 1–2 surfaced anything to fix, commit it. Otherwise this task produces no commit. --- ## Self-Review Notes **Spec coverage:** Every layer in the spec's "Per-layer changes" table maps to a task — enum/label/category/catalog (Task 1), macOS inject (Task 2), Linux inject (Task 3), Windows log-and-skip (Task 4), picker icons (Task 5), i18n keys (Task 6). The spec's "Testing" section is covered by the test edits in Task 1 and the full-suite run in Task 7. Platform-coverage table matches (Windows gap explicit in Task 4). **No placeholders:** Every code step shows the exact code. The two `cargo check --target` steps for Linux/Windows explicitly document the "skip if target not installed" fallback rather than hiding it. **Type consistency:** Variant names `MouseButton6`–`MouseButton9` are identical across all tasks. macOS button numbers (5–8) are internally consistent with the existing 3/4 for Back/Forward. Linux `KeyCode` constants are confirmed in evdev 0.13.2. --- ### Superpowers/Plans/2026 06 30 Function Key Remapper M1 # Function-Key Remapper — M1 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Capture F1–F12 + Esc key presses (and Shift/Ctrl/Opt/Cmd-qualified combos) and remap each to any action in the existing palette, plus three new execution actions (`TypeText`, `RunAppleScript`, `RunShellCommand`). **Architecture:** Three layers, each mirroring the mouse path. (1) `openlogi-hook` gains keyboard event types and a `KeyEvent` vocabulary alongside `MouseEvent`. (2) `openlogi-core` gains three `Action` variants and a `[keyboard.bindings]` config section keyed by keycode+modifiers. (3) `openlogi-inject` gains a `post_unicode` text-typing primitive and the new action arms. The hook callback routes keyboard events through the same `EventDisposition` (PassThrough/Suppress) the mouse side uses, so remapped keys are suppressed exactly as remapped mouse buttons are. **Tech Stack:** Rust workspace; `CGEventTap` (macOS) for capture; `CGEventKeyboardSetUnicodeString` for text typing; `std::process::Command` for AppleScript/shell; serde/TOML for config. **Spec:** `docs/superpowers/specs/2026-06-30-function-key-remapper-design.md` (M1 scope only; M2 Workflow and M3 media-key capture are separate plans). --- ## File Structure | File | Responsibility | Change | |---|---|---| | `crates/openlogi-hook/src/lib.rs` | The event vocabulary (`MouseEvent`, `EventDisposition`) | Add `KeyEvent` + `HookEvent` union; widen the callback signature | | `crates/openlogi-hook/src/macos.rs` | The `CGEventTap` capture | Add keyboard event types to the mask; `translate` keyboard events; macOS keycode table for F-keys | | `crates/openlogi-core/src/binding.rs` | The `Action` enum + `label`/`category`/`catalog` | Add `TypeText`/`RunAppleScript`/`RunShellCommand` variants (excluded from catalog — power-user escape hatch) | | `crates/openlogi-core/src/config.rs` | Config loading | Add `[keyboard]` section + `KeyTrigger` (keycode + modifiers) | | `crates/openlogi-inject/src/inject.rs` | Action → OS event synthesis | Add `post_unicode` primitive; three new `Action` arms in `execute_macos` | | `crates/openlogi-agent-core/src/hook_runtime.rs` | Dispatches hook events → actions | Route `KeyEvent` → look up keyboard binding → execute action → Suppress | No new files except where a table cell says "Add". The exhaustive `match` arms across the codebase are the safety net (the picker icon `match`, the inject `match`) — they fail to compile if a variant is missed, exactly as with mouse buttons 6–9. --- ## Task 1: Add the `KeyEvent` vocabulary and widen the hook callback This is the foundational change: the hook must be able to report keyboard events, not just mouse. We add a `KeyEvent` type and a `HookEvent` union so the existing `Hook::start` callback can receive either, then update the (single) call site. **Files:** - Modify: `crates/openlogi-hook/src/lib.rs:47` (add `KeyEvent`, `HookEvent` near `MouseEvent`) - Modify: `crates/openlogi-hook/src/lib.rs` (the `Hook::start` signature — find it via `grep -n "pub fn start" crates/openlogi-hook/src/lib.rs`) - Modify: `crates/openlogi-agent-core/src/hook_runtime.rs:115` (the single call site) - [ ] **Step 1: Read the current `MouseEvent` + `Hook::start` signature** Run: `sed -n '40,100p' crates/openlogi-hook/src/lib.rs && grep -n "pub fn start" crates/openlogi-hook/src/lib.rs` Note the `MouseEvent` enum (around line 47), `EventDisposition` (line 95), and the `start` signature's callback type `impl Fn(MouseEvent) -> EventDisposition`. - [ ] **Step 2: Add `KeyEvent` + `KeyModifiers` + `HookEvent` to `lib.rs`** Immediately above `pub enum MouseEvent {` (line 47), add: ```rust /// Which modifier keys were held when a key event fired. Mirrors the /// detectable macOS modifier flags (everything *except* Fn — see spec /// Appendix A; Fn is firmware-internal and never reported on non-function-row /// keys). #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] pub struct KeyModifiers { pub shift: bool, pub control: bool, pub option: bool, pub command: bool, } /// A keyboard event observed by the hook. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct KeyEvent { /// macOS virtual keycode (e.g. 122 = F1, 53 = Escape). pub keycode: u16, /// `true` = key down; `false` = key up. pub pressed: bool, /// Which modifiers were held. pub modifiers: KeyModifiers, } /// Anything the hook can observe. `Mouse` keeps the existing callback shape; /// `Key` is the new keyboard path. Wrapping in a union means the callback /// signature widens once (here) and stays stable as more event classes arrive. #[derive(Debug, Clone, Copy)] pub enum HookEvent { Mouse(MouseEvent), Key(KeyEvent), } ``` - [ ] **Step 3: Widen the `Hook::start` callback to `HookEvent`** Find `pub fn start(` in `lib.rs`. Change every occurrence of the callback parameter type `impl Fn(MouseEvent) -> EventDisposition + Send + Sync + 'static` to `impl Fn(HookEvent) -> EventDisposition + Send + Sync + 'static` (on all platform stubs — macOS, Linux, Windows, and the `Unsupported` fallback). - [ ] **Step 4: Update the single call site in `hook_runtime.rs:115`** The callback currently matches `MouseEvent` variants directly. Wrap the existing body to only act on `HookEvent::Mouse` and pass through keys for now: ```rust let result = Hook::start(move |event| match event { HookEvent::Mouse(mouse_event) => match mouse_event { MouseEvent::Button { id, pressed } => { // ... existing body unchanged ... } MouseEvent::Moved { delta_x, delta_y } => { // ... existing body unchanged ... } MouseEvent::CaptureInterrupted => { // ... existing body unchanged ... } MouseEvent::Scroll { .. } => EventDisposition::PassThrough, }, HookEvent::Key(_) => EventDisposition::PassThrough, // wired up in Task 6 }); ``` Add the import: `use openlogi_hook::{EventDisposition, Hook, HookEvent, MouseEvent};` - [ ] **Step 5: Build + run the full hook + agent-core tests** Run: `cargo test -p openlogi-hook -p openlogi-agent-core` Expected: PASS. The keyboard path is inert (`PassThrough`), so behavior is unchanged; this just proves the widened signature compiles and nothing regresses. - [ ] **Step 6: Commit** ```bash git add crates/openlogi-hook/src/lib.rs crates/openlogi-agent-core/src/hook_runtime.rs git commit -m "refactor(hook): widen hook callback to HookEvent (Mouse | Key) Adds KeyEvent + KeyModifiers + HookEvent vocabulary alongside MouseEvent. Hook::start's callback now receives HookEvent; hook_runtime wraps its existing MouseEvent body and passes keys through inertly. No behavior change yet — keyboard capture lands in the next task." ``` --- ## Task 2: Capture keyboard events in the macOS `CGEventTap` Extend the existing tap (currently mouse-only) to also subscribe to keyboard event types, and translate them into `KeyEvent`s. F-keys are proven to arrive here (F1 = keycode 122 + `SecondaryFn` flag); this task makes the tap see them. **Files:** - Modify: `crates/openlogi-hook/src/macos.rs:452` (the `event_types` vec) - Modify: `crates/openlogi-hook/src/macos.rs:257` (`translate` — add keyboard arms) and the callback closure at `:475` - [ ] **Step 1: Add keyboard event types to the tap mask** In `macos.rs`, the `event_types` vec (line 452) currently lists only mouse types. Append: ```rust let event_types = vec![ CGEventType::LeftMouseDown, CGEventType::LeftMouseUp, // ... existing mouse types unchanged ... CGEventType::OtherMouseDragged, // NEW — keyboard capture for the function-key remapper (M1). CGEventType::KeyDown, CGEventType::KeyUp, CGEventType::FlagsChanged, ]; ``` - [ ] **Step 2: Add a keyboard-translation helper** Above the existing `fn translate(...)` (line 257), add: ```rust /// Map the macOS modifier flags on a `CGEvent` to our [`KeyModifiers`]. /// `SecondaryFn` is deliberately ignored — it is firmware-internal and /// unreliable as a trigger (see spec Appendix A). fn modifiers_from_flags(flags: CGEventFlags) -> KeyModifiers { KeyModifiers { shift: flags.contains(CGEventFlags::MASK_SHIFT), control: flags.contains(CGEventFlags::MASK_CONTROL), option: flags.contains(CGEventFlags::MASK_ALTERNATE), command: flags.contains(CGEventFlags::MASK_COMMAND), } } /// Translate a keyboard `CGEvent` into a [`KeyEvent`]. Returns `None` for /// non-key event types (handled by the mouse path) or for `FlagsChanged` /// alone (modifier state is reported on the subsequent key event). fn translate_key(etype: CGEventType, event: &CGEvent) -> Option { let (pressed, keycode) = match etype { CGEventType::KeyDown => (true, event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE) as u16), CGEventType::KeyUp => (false, event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE) as u16), // FlagsChanged carries no keycode of interest here; modifiers ride on // the next key event via its flags. Drop it. _ => return None, }; Some(KeyEvent { keycode, pressed, modifiers: modifiers_from_flags(event.get_flags()), }) } ``` (Add `use core_graphics::event::EventField;` to the imports if not already present — check with `grep -n "use core_graphics" crates/openlogi-hook/src/macos.rs | head`.) - [ ] **Step 3: Route keyboard events through the callback** The callback closure (line 475) currently does `let Some(mouse_event) = translate(etype, event)`. Replace it to build a `HookEvent` from either path: ```rust move |_proxy: CGEventTapProxy, etype: CGEventType, event: &CGEvent| { let hook_event = if let Some(mouse_event) = translate(etype, event) { HookEvent::Mouse(mouse_event) } else if let Some(key_event) = translate_key(etype, event) { HookEvent::Key(key_event) } else { return CallbackResult::Keep; }; match cb(hook_event) { EventDisposition::PassThrough => CallbackResult::Keep, EventDisposition::Suppress => CallbackResult::Drop, } }, ``` - [ ] **Step 4: Build the hook crate** Run: `cargo build -p openlogi-hook` Expected: BUILD SUCCEEDS. The `CGEventFlags::MASK_*` constant names must match what `core-graphics` exposes; if any is named differently, the compiler error names the correct constant — fix and rebuild. - [ ] **Step 5: Commit** ```bash git add crates/openlogi-hook/src/macos.rs git commit -m "feat(hook): capture keyboard events in the macOS CGEventTap Adds KeyDown/KeyUp/FlagsChanged to the tap mask, plus translate_key() which maps a key CGEvent to our KeyEvent (keycode + press state + detectable modifiers, ignoring SecondaryFn). The callback now builds a HookEvent from either the mouse or key path. F1-F12/Esc are now observed; nothing acts on them yet." ``` --- ## Task 3: Add the three execution `Action` variants The action palette gains `TypeText`, `RunAppleScript`, `RunShellCommand`. These are power-user escape hatches (like `CustomShortcut`), so they are **excluded from the default catalog** — they must be hand-authored in config. This task only adds the variants + their `label`/`category`/TOML shape; injection lands in Task 5. **Files:** - Modify: `crates/openlogi-core/src/binding.rs` — `Action` enum (near `CustomShortcut(KeyCombo)` at line 483), `label()` (:679), `category()` (:731), `catalog()` (:787) - [ ] **Step 1: Add the failing test for the new variants' category + label** In `binding.rs`, append to the `#[cfg(test)] mod tests` block: ```rust #[test] fn power_user_action_labels_and_category() { assert_eq!(Action::TypeText("hi".into()).label(), "Type \"hi\""); assert_eq!(Action::RunAppleScript("osascript".into()).label(), "Run AppleScript"); assert_eq!(Action::RunShellCommand("echo hi".into()).label(), "Run Command"); // All three are power-user escape hatches: never in the default catalog, // but classed as Editing so a hand-authored binding has a home group. assert_eq!(Action::TypeText("x".into()).category(), Category::Editing); assert_eq!(Action::RunAppleScript("x".into()).category(), Category::Editing); assert_eq!(Action::RunShellCommand("x".into()).category(), Category::Editing); } #[test] fn power_user_actions_excluded_from_catalog() { let cat = Action::catalog(); assert!(cat.iter().all(|a| !matches!(a, Action::TypeText(_) | Action::RunAppleScript(_) | Action::RunShellCommand(_)))); } ``` - [ ] **Step 2: Run the tests to verify they fail** Run: `cargo test -p openlogi-core --lib binding::tests::power_user` Expected: FAIL with `cannot find variant TypeText` — compile error. - [ ] **Step 3: Add the three variants to the `Action` enum** After `CustomShortcut(KeyCombo),` (line 483), add: ```rust /// Type an arbitrary string by emitting unicode characters (macOS /// `CGEventKeyboardSetUnicodeString`). Used for macro text. Power-user /// escape hatch — excluded from the default catalog. TypeText(String), /// Run an AppleScript via `osascript -e `. Power-user escape hatch. RunAppleScript(String), /// Run a shell command via `/bin/sh -c `. Power-user escape hatch. RunShellCommand(String), ``` - [ ] **Step 4: Add the three labels** In `label()` (:679), in the `match` (after the `CustomShortcut` arm), add: ```rust Action::TypeText(s) => format!("Type \"{s}\"").into(), Action::RunAppleScript(_) => "Run AppleScript".into(), Action::RunShellCommand(_) => "Run Command".into(), ``` - [ ] **Step 5: Add the three category arms** In `category()` (:731), extend the existing `Editing` arm that already holds `CustomShortcut`: ```rust | Action::CustomShortcut(_) | Action::TypeText(_) | Action::RunAppleScript(_) | Action::RunShellCommand(_) => Category::Editing, ``` - [ ] **Step 6: Confirm `catalog()` excludes them** `catalog()` (:787) is an explicit list — by NOT adding the three variants to it, they are excluded. Verify the existing `catalog_excludes_custom_shortcut` test pattern and confirm no test forces them in. No code change needed here; the test in Step 1 asserts exclusion. - [ ] **Step 7: Run the tests to verify they pass + the TOML roundtrip still works** Run: `cargo test -p openlogi-core --lib binding` Expected: PASS — including the new tests and the existing `all_catalog_variants_roundtrip_toml` (the new variants aren't in the catalog, but add a manual roundtrip assertion for `TypeText` in the test block): ```rust #[test] fn power_user_actions_roundtrip_toml() { for action in [ Action::TypeText("hello".into()), Action::RunAppleScript("beep".into()), Action::RunShellCommand("date".into()), ] { let toml = toml::to_string(&action).unwrap(); let back: Action = toml::from_str(&toml).unwrap(); assert_eq!(action, back); } } ``` - [ ] **Step 8: Commit** ```bash git add crates/openlogi-core/src/binding.rs git commit -m "feat(core): add TypeText / RunAppleScript / RunShellCommand actions Three power-user escape-hatch actions (excluded from the default catalog, classed as Editing). TypeText emits a unicode string; the two Run actions spawn osascript / sh. Injection arms land in the inject task." ``` --- ## Task 4: Add the `[keyboard]` config section + `KeyTrigger` The config gains a new top-level `[keyboard]` table mapping trigger strings (`"f1"`, `"shift+f1"`) to actions. This is independent of the per-device `[devices]` bindings. **Files:** - Modify: `crates/openlogi-core/src/config.rs` — the `Config` struct (find via `grep -n "pub struct Config" crates/openlogi-core/src/config.rs`) - Create: the `KeyTrigger` type + trigger-string parser lives in `config.rs` (single file, follow existing patterns) - [ ] **Step 1: Read the current `Config` struct + a device-binding sample** Run: `sed -n '/pub struct Config/,/^}/p' crates/openlogi-core/src/config.rs` Note the existing fields (`devices`, `app_settings`, `schema_version`) and how bindings are typed. - [ ] **Step 2: Write the failing test for the trigger-string parser + config load** Append to `config.rs`'s test module: ```rust #[test] fn key_trigger_parses_bare_and_modified() { // Bare function key. let t: KeyTrigger = "f1".parse().unwrap(); assert_eq!(t.keycode, 122); assert!(t.modifiers.is_empty()); // Modifier-qualified. let t: KeyTrigger = "shift+cmd+f5".parse().unwrap(); assert_eq!(t.keycode, 96); // F5 assert!(t.modifiers.shift && t.modifiers.command); assert!(!t.modifiers.control && !t.modifiers.option); } #[test] fn keyboard_section_loads_from_toml() { let toml = r#" [keyboard.bindings] "f1" = { TypeText = "hi" } "shift+f2" = "VolumeUp" "#; let cfg: Config = toml::from_str(toml).unwrap(); assert_eq!(cfg.keyboard.bindings.len(), 2); assert!(cfg.keyboard.bindings.contains_key(&"f1".parse::().unwrap())); } ``` - [ ] **Step 3: Run the test to verify it fails** Run: `cargo test -p openlogi-core --lib config::` Expected: FAIL — `KeyTrigger` and `keyboard` field don't exist. - [ ] **Step 4: Add `KeyTrigger` + the parser + `KeyboardConfig`** In `config.rs`, add (types first, then impls): ``` /* Detailed source-code truncated for AI context efficiency. */ ``` (`KeyModifiers` lives in `openlogi-hook`; re-export or duplicate the four bools in `openlogi-core` to avoid a core→hook dependency. Prefer duplicating — core must stay leaf-level. Use a `core::KeyModifiers` with the same shape and convert at the boundary in Task 6.) Add the field to `Config`: ```rust #[serde(default)] pub keyboard: KeyboardConfig, ``` - [ ] **Step 5: Run the tests to verify they pass** Run: `cargo test -p openlogi-core --lib config::` Expected: PASS — parser + TOML load both green. - [ ] **Step 6: Commit** ```bash git add crates/openlogi-core/src/config.rs git commit -m "feat(core): add [keyboard] config section + KeyTrigger parser KeyTrigger parses '[mod+]+key' strings (f1, shift+cmd+f5, esc) into a keycode + modifier mask, using the macOS F-key virtual keycodes. The [keyboard.bindings] table maps triggers to Actions, independent of the per-device bindings." ``` --- ## Task 5: Add the `post_unicode` primitive + inject the three new actions The execution layer. `post_unicode` types a string via `CGEventKeyboardSetUnicodeString`; the three new `Action` arms call it (for `TypeText`) or spawn a process (for the two Run actions). **Files:** - Modify: `crates/openlogi-inject/src/inject.rs:516` (add `post_unicode` next to `post_key`) and the `execute_macos` match arms - [ ] **Step 1: Add the `post_unicode` primitive to the macOS mod** In the `mod macos {` block (after `post_media_key`, line 541), add: ```rust /// Type an arbitrary unicode string by emitting a single key event per /// character whose payload is set via `CGEventKeyboardSetUnicodeString`. /// This sidesteps the keyboard layout entirely — characters are injected /// as unicode, so "bite me" types verbatim regardless of layout. pub(super) fn post_unicode(text: &str) { for ch in text.chars() { let mut buf = [0u16; 2]; let s: Cow = Cow::Owned(ch.to_string()); let _ = s; // unused; the unicode string is set on the event below. let event = CGEvent::new(None); event.set_flags(CGEventFlags::empty()); // CGEventKeyboardSetUnicodeString: max 20 UTF-16 units per call; // one char at a time is simplest and always in-bounds. let units: Vec = ch.encode_utf16(&mut buf).to_vec(); unsafe { core_foundation::string::CFString::from(&*units.iter() .filter_map(|&u| char::from_u32(u as u32)) .collect::()); } // Use the core-graphics binding's keyboard-set-unicode path: event.set_string_from_utf16(&units); event.post(CGEventTapLocation::HID); } } ``` NOTE: the exact `core-graphics` API for setting a unicode string on a `CGEvent` varies by crate version — `set_string_from_utf16` is the typical name. If the compiler rejects it, run `grep -rn "KeyboardSetUnicodeString\|set_string\|unicode" ~/.cargo/registry/src/*/core-graphics-*/src/event.rs` to find the exact method name in the pinned version, and use that. The contract is: one `CGEvent` per character, unicode payload set, posted to HID. - [ ] **Step 2: Add the three `execute_macos` arms** In `execute_macos` (find the `match action {` and the `CustomShortcut` arm near line 142), add: ```rust Action::TypeText(text) => macos::post_unicode(text), Action::RunAppleScript(src) => { // Fire-and-forget; the agent must not block the event tap thread. let src = src.clone(); std::thread::spawn(move || { let _ = std::process::Command::new("osascript") .args(["-e", &src]) .output(); }); } Action::RunShellCommand(cmd) => { let cmd = cmd.clone(); std::thread::spawn(move || { let _ = std::process::Command::new("/bin/sh") .args(["-c", &cmd]) .output(); }); } ``` (The Run actions spawn off the tap thread because the tap callback must not block — posting a key while the tap is waiting on a child process wedges input. Same discipline the existing mouse actions follow.) - [ ] **Step 3: Build the inject crate** Run: `cargo build -p openlogi-inject` Expected: BUILD SUCCEEDS once the `post_unicode` API name matches the pinned core-graphics version (resolve per the NOTE in Step 1 if needed). - [ ] **Step 4: Commit** ```bash git add crates/openlogi-inject/src/inject.rs git commit -m "feat(inject): post_unicode primitive + TypeText/Run* execution post_unicode types a string one char at a time via CGEventKeyboardSetUnicodeString (layout-independent). TypeText uses it; RunAppleScript spawns osascript, RunShellCommand spawns /bin/sh, both off the tap thread so a slow script can't wedge input." ``` --- ## Task 6: Wire keyboard events → bindings → actions in `hook_runtime` The integration task. A `KeyEvent` arrives; look it up in the `[keyboard.bindings]` table (by keycode + modifiers); if matched, execute the action and `Suppress` the original key; else `PassThrough`. **Files:** - Modify: `crates/openlogi-agent-core/src/hook_runtime.rs` (the `HookEvent::Key(_)` arm from Task 1, Step 4) - [ ] **Step 1: Read how mouse bindings are looked up + executed** Run: `grep -n "bindings\|MouseEvent::Button\|inject\|execute" crates/openlogi-agent-core/src/hook_runtime.rs | head -20` Note how `MouseEvent::Button { id, pressed }` finds its action and calls into `openlogi-inject`. Mirror that for keys. - [ ] **Step 2: Replace the inert `HookEvent::Key(_)` arm with real lookup** The binding state needs access to the loaded `Config`'s `keyboard.bindings`. Capture an `Arc>` into the hook closure (same way the mouse bindings are captured — find the existing `Arc` capture pattern in `hook_runtime.rs` and mirror it). Then: ```rust HookEvent::Key(KeyEvent { keycode, pressed: true, modifiers }) => { // Only act on key-down (avoid double-fire on key-up). let trigger = KeyTrigger { keycode, modifiers: convert_modifiers(modifiers) }; match keyboard_bindings.get(&trigger) { Some(action) => { execute_action(action); // reuse the existing mouse-action executor EventDisposition::Suppress // eat the original key } None => EventDisposition::PassThrough, } } HookEvent::Key(_) => EventDisposition::PassThrough, // key-up, ignore ``` `convert_modifiers` maps `hook::KeyModifiers` → `config::KeyModifiers` (the duplicate-type boundary noted in Task 4, Step 4). Add it as a small `fn` in `hook_runtime.rs`. - [ ] **Step 3: Build + run agent-core tests** Run: `cargo test -p openlogi-agent-core` Expected: PASS. No new test here — the integration is exercised manually in Task 7 (the unit-testable seams are the parser and the action arms, both already covered). - [ ] **Step 4: Commit** ```bash git add crates/openlogi-agent-core/src/hook_runtime.rs git commit -m "feat(agent): dispatch keyboard events to [keyboard] bindings A key-down whose keycode+modifiers match a [keyboard.bindings] entry executes its action and suppresses the original key; unmatched keys pass through. Reuses the existing action executor; key-up is ignored." ``` --- ## Task 7: Manual end-to-end verification on hardware M1 is complete at this point. This task verifies it on real hardware — the critical check, per the spec's "test incrementally on hardware" note. - [ ] **Step 1: Build the dev agent** Run: `cargo build -p openlogi-agent` - [ ] **Step 2: Add a test binding to config** Append to `~/.config/openlogi/config.toml`: ```toml [keyboard.bindings] "f1" = { TypeText = "hello from F1" } ``` - [ ] **Step 3: Stop the installed agent and run the dev agent foreground** ```sh launchctl bootout gui/$(id -u)/org.openlogi.agent # also quit the GUI so it doesn't respawn the agent osascript -e 'tell application "OpenLogi" to quit' sleep 2 OPENLOGI_LOG=debug target/debug/openlogi-agent ``` - [ ] **Step 4: Press F1 in a text field** Expected: the text "hello from F1" is typed. The original F1 is suppressed (no brightness/media action fires). - [ ] **Step 5: Verify the failure modes don't wedge input** - Press an **unbound** key (e.g. `a`) — it must type normally (PassThrough works). - Hold the agent running for 30s of mixed typing — input must not freeze. If it does, the tap is wedging; revisit Task 2 (the documented HID-tap failure mode). - [ ] **Step 6: Restore the installed agent** ```sh launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.openlogi.agent.plist ``` - [ ] **Step 7: Commit any fixups + tag the milestone** If Steps 4–5 surfaced anything, fix and commit. Then: ```bash git commit --allow-empty -m "chore: M1 complete — F-key capture + action palette verified on hardware" ``` --- ## Self-Review Notes **Spec coverage (M1 scope):** Execution actions (TypeText/RunAppleScript/RunShellCommand) → Task 3 + 5. `[keyboard.bindings]` config + KeyTrigger → Task 4. F-key capture → Task 2. Modifier-qualified combos → Task 4 (parser) + Task 6 (dispatch). Press-to-bind is **deferred to a later M1.x** — it's a UI/UX flow, not a correctness gap, and adding it here would balloon the plan; flagged honestly rather than hidden. Fixed F-key list → covered by the parser's `f1..f12`/`esc` table (Task 4). Suppress-on-remap → Task 6 + verified in Task 7 Step 5. Media-key reassignment (existing `post_media_key`) is already wired and reachable as a binding target (Task 4's `Action` is the full enum). **Placeholder scan:** Task 5 Step 1 has an explicit NOTE about resolving the exact `core-graphics` unicode API name against the pinned version — this is a *known unknown with a resolution path*, not a placeholder; the grep command finds the real method name. No "TBD"/"implement later"/"add error handling" anywhere. **Type consistency:** `KeyEvent` (hook) ↔ `KeyModifiers` (hook) ↔ `KeyTrigger` (config) ↔ `KeyModifiers` (config, duplicate) — the `convert_modifiers` boundary fn in Task 6 bridges the intentional duplicate (core stays leaf-level, no core→hook dep). `Action::TypeText(String)` etc. consistent across Tasks 3/5/6. **Out of scope (separate plans):** M2 `Workflow` sequencer, M3 media-key capture, press-to-bind UI, per-app keyboard profiles, Windows/Linux capture. ## Execution Handoff Plan complete and saved to `docs/superpowers/plans/2026-06-30-function-key-remapper-m1.md`. Two execution options: **1. Subagent-Driven (recommended)** — fresh subagent per task, review between tasks, fast iteration. Best for a multi-task plan touching the input hook (where each task changes observable behavior). **2. Inline Execution** — execute tasks in this session with checkpoints for review. Which approach? --- ### CODE OF CONDUCT # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders of OpenLogi pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people. - Being respectful of differing opinions, viewpoints, and experiences. - Giving and gracefully accepting constructive feedback. - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience. - Focusing on what is best not just for us as individuals, but for the overall community. - Keeping technical discussions specific, actionable, and relevant to OpenLogi's project goals. - Respecting user privacy when sharing logs, device information, screenshots, or configuration files. Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind. - Trolling, insulting or derogatory comments, and personal or political attacks. - Public or private harassment. - Publishing others' private information, such as a physical or email address, without their explicit permission. - Pressuring maintainers or contributors for unpaid work, private support, urgent fixes, or release dates. - Encouraging unsafe behavior, including instructions that intentionally bypass operating-system protections or user consent. - Other conduct which could reasonably be considered inappropriate in a professional setting. ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, pull requests, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all OpenLogi community spaces, including GitHub issues, pull requests, discussions, comments, community chat rooms, social channels, release announcement threads, and project events. This Code of Conduct also applies when an individual is officially representing the OpenLogi community in public spaces. Examples of representing the community include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at: `conduct@openlogi.org` All complaints will be reviewed and investigated promptly and fairly. Community leaders are obligated to respect the privacy and security of the reporter of any incident. Reports may include links, screenshots, message IDs, timestamps, and a short description of what happened. If you prefer not to include identifying details, say so in the report. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning Community Impact: A violation through a single incident or series of actions. Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban Community Impact: A serious violation of community standards, including sustained inappropriate behavior. Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. Consequence: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at . Community Impact Guidelines were inspired by Mozilla's code of conduct enforcement ladder. For answers to common questions about this code of conduct, see the FAQ at . Translations are available at . --- ### CONFIGURATION # Configuration How OpenLogi stores its settings. For install and usage, see the [README](../README.md). Config is a TOML file, read on startup and written atomically on change. Before the first save in each app process, OpenLogi preserves the previous file as `config.toml.backup.1` and rotates up to `config.toml.backup.5`. - macOS & Linux: `$XDG_CONFIG_HOME/openlogi/config.toml` (default `~/.config/openlogi/config.toml`) - Windows: `%USERPROFILE%\.config\openlogi\config.toml` Most settings below are managed by the GUI (Settings window, action picker, DPI / SmartShift / lighting panels), but the file stays hand-editable; per-application overlays and custom shortcuts are currently authored there. OpenLogi reloads it on startup. Older schemas are migrated on load, including `schema_version = 1` files that split button and gesture bindings. Per-device settings are keyed by physical identity, such as `receiver:aabbccdd:slot:1` for a receiver-connected device. This keeps two mice of the same model independent: - `bindings` — one entry per rebindable button: either a single action, or a per-direction table for the gesture button. - `per_app_bindings` — overlays keyed by application id (bundle id such as `com.microsoft.VSCode` on macOS, `WM_CLASS` on Linux/X11, or a lower-cased executable path on Windows) that take precedence while that app is frontmost. Windows also accepts `exe:.exe`, for example `exe:sharex.exe`, as a stable fallback for Store and self-updating apps. An exact path entry wins when both forms exist. - `action_ring` — the enabled state, haptic-feedback preference, default eight-slot layout, and complete per-application layouts. - `dpi_presets` — the ordered list cycled by the `CycleDpiPresets` action. - `smartshift` — wheel mode, sensitivity, and permanent-ratchet state. - `invert_scroll` — reverse this device's native vertical wheel direction without changing the system trackpad direction. - `lighting` — static RGB colour, brightness (0–100), and on/off for wired RGB keyboards. - `light` — standalone-light power, normalized brightness, and temperature. Set `auto_camera = true` on macOS to turn the light on while any camera is in use and off when camera use stops; the manual power preference and the other light settings remain independent. - `gesture_owner` — which button owns the gesture role, when chosen explicitly (otherwise inferred). - `host_switch_targets` — on a compatible keyboard, physical config keys of mice that should follow its Easy-Switch channel. Both devices must already be paired on corresponding channels. The keyboard's host controls and every target must expose the HID++ features needed for host switching. Configure the link on every computer from which the keyboard may initiate a switch. - `fn_lock` — keyboards only: `true` makes the F-row send F1–F12 without holding Fn, `false` keeps the printed media/shortcut functions. Absent means the keyboard's own state is left alone. Re-applied on reconnect. The app-wide `[app_settings]` block holds `launch_at_login`, `check_for_updates`, and `auto_install_updates` (all off by default); `show_in_menu_bar` (macOS menu bar / Windows tray, ignored on Linux; on by default); `capture_mouse_events` (on by default; set to `false` to keep the agent from installing the OS-level mouse hook at all — button remapping stops working, but no input device is grabbed or intercepted; DPI, SmartShift, and the other HID++-side features keep working; takes effect on agent restart); `auto_download_assets` (on by default); `language` (absent = follow the system locale); `thumbwheel_sensitivity` (default `14`); and the `appearance` (default `"system"`), `theme_light`, `theme_dark`, and `ui_radius` presentation settings. The theme and radius overrides are absent by default. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` Action names are the catalog's variant names (`LeftClick`, `MouseBack`, `Copy`, `PlayPause`, `CycleDpiPresets`, …). `ShowActionsRing` opens the ring; a detected Haptic Sense Panel uses it by default, and pressing the trigger again while the ring is showing dismisses it. Ring slots reject `ShowActionsRing` itself to prevent recursive sessions. `OpenApplication` accepts an application, folder, filesystem path, or URL. A leading `~` is expanded when the action runs; for example: ```toml Top = { action = { OpenApplication = { path = "/Applications/Safari.app", display_name = "Safari" } } } Bottom = { action = { OpenApplication = { path = "~/Downloads", display_name = "Downloads" } } } ``` `CustomShortcut` stores a platform-neutral textual chord, for example: ```toml Top = { action = { CustomShortcut = "Cmd+Shift+P" } } ``` The GUI accepts chords such as `Cmd+Shift+P`, `Ctrl+Alt+Left`, or `F5`. It also lets each ring slot keep its action-derived icon or choose a custom icon from the built-in gallery. --- ### DECISIONS # Decision log Durable "why we did it this way" records that are not obvious from the code. Add a dated entry when a non-obvious architectural or dependency decision is made or revisited. ## 2026-08: Shared clippy lint set The workspace adopted the shared ten-lint set (`assertions_on_result_states`, `cast_possible_truncation`, `cast_possible_wrap`, `cast_sign_loss`, `error_impl_error`, `exit`, `or_fun_call`, `ptr_as_ptr`, `tests_outside_test_module`, `undocumented_unsafe_blocks`) on top of the existing `pedantic` + `unwrap_used`/`expect_used` table. - One table, inherited everywhere. `openlogi-gui`, `openlogi-camera` and `openlogi-hook` carried hand-copied duplicates of `[workspace.lints]`, so any lint added to the workspace would have silently skipped them — three of the crates holding most of the FFI. Cargo rejects `[lints] workspace = true` alongside local overrides, so `openlogi-hook` moved its `unsafe_code` opt-out into its three platform modules. `openlogi-hidpp` stays out on purpose (vendored). - `tests_outside_test_module` only recognises a literal `#[cfg(test)]`. Compound gates are written as stacked attributes (`#[cfg(test)]` then `#[cfg(unix)]`); an integration test under `tests/` carries a file-level `#![expect(…)]` because that file is already a test-only crate. Splitting the attribute also wakes `items_after_test_module`, so such a module belongs last in its file. - `exit` gets a real `ExitCode` wherever the call site can return — `openlogi list` hands status 2 back to `main` — and a reasoned `#[expect]` where it cannot (the AppKit run loop, the watchdog threads, the update handover). Clippy does not look inside `define_class!`, so the menu-bar Quit body moved out of the macro rather than escape the lint by accident. - Not adopted: the policy's `unexpected_cfgs` / `check-cfg = ['cfg(kani)']` entry. Nothing here uses Kani, and `unexpected_cfgs` already warns by default, so it would be dead configuration. ## 2026-08: Standalone raw-light boundary Standalone lights such as Litra stay outside the HID++ receiver/paired-device model and are normalized only at the shared agent and GUI device-record boundary. This keeps the existing HID++ wire and routing semantics unchanged while allowing future light drivers to share capability-driven controls. - Persist brightness as a normalized percentage and temperature as Kelvin; native units and report encoding remain driver responsibilities. - Use device serials for persistent raw-device keys. OS-node identifiers are runtime-only hints and must not silently become physical configuration keys. - Advertise optional light controls through `LightCapabilities`; the GUI gates controls from those capabilities rather than from `DeviceKind::Light`. - Serialize and coalesce per-device light writes in the agent so reconnect, camera automation, config reload, and manual commands cannot interleave at packet level. ## 2026-07: Infrastructure we keep custom instead of using a crate A dependency audit replaced most general-purpose infrastructure code with mature crates (`tempfile`, `which`, `plist`, `walkdir`, `xshell`, `sysinfo`, `fs-err`, `backon`, `opener`, `etcetera`, and others — see the git history of `FIXDRY.md` for the full list). The following stayed custom, deliberately: - `openlogi-core::single_instance`: the `single-instance` crate uses different backends (for example abstract Unix sockets on Linux) and does not preserve OpenLogi's data-dir lock-file path, per-role names, and error classification closely enough to be a safe deletion. - Agent tray Quit's `openlogi://quit` dispatch keeps `std::process::Command::output()` intentionally: it blocks until LaunchServices accepts the Apple Event, while generic opener crates only guarantee process spawn. - GUI helper launch keeps `/usr/bin/open -g -n` intentionally: it needs LaunchServices-specific flags to start the packaged agent under its own TCC identity, which generic opener crates do not expose. - Agent autostart install keeps direct `systemctl` calls because it is managing systemd user units, not merely opening or spawning an arbitrary program. - Self-restart and `disclaim` launches stay custom because they are process identity / update lifecycle boundaries, not generic command orchestration. - `openlogi-hook`: event suppression/rewriting and foreground-app lookup are OpenLogi-specific and not covered cleanly by generic input crates. - `openlogi-inject`: platform-specific action synthesis may overlap with `enigo`, but current semantics are narrower and more controlled. - `openlogi-hid` / vendored `openlogi-hidpp`: the right path is upstreaming OpenLogi-specific fixes, not replacing the fork blindly. --- ### DEVELOPMENT # Developing OpenLogi This document covers the local development workflow for OpenLogi. For end-user build instructions, see the [README](../README.md). ## Toolchain - Stable Rust (Edition 2024, MSRV 1.96) - macOS: Xcode 16+ with the optional **Metal Toolchain** component (required by GPUI's `gpui_macos` build script to compile shaders) - Linux: system libraries — on Debian/Ubuntu: `sudo apt-get install libudev-dev gcc g++ clang libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libx11-xcb-dev libssl-dev libzstd-dev pkg-config` - `create-dmg` for packaging (`brew install create-dmg`); `cargo-bundle` is installed automatically by `cargo run -p xtask -- macos bundle` ## Building from source Nix/devenv is optional. A normal Rust toolchain is enough. ### Without Nix ```sh # rustup installs the stable toolchain pinned in rust-toolchain.toml curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # macOS: full Xcode 16+ with the Metal Toolchain (not only Command Line Tools) # Linux: see system libraries under Toolchain above # optional helpers: brew install cmake create-dmg sccache git clone https://github.com/AprilNEA/OpenLogi cd OpenLogi cargo run -p openlogi --release -- list cargo run -p openlogi-gui --release ``` If you use [direnv](https://direnv.net) without devenv installed, `.envrc` prints a notice and leaves your shell alone. Install rustup/cargo yourself and keep working. ### With devenv (optional) `devenv.nix` provisions sccache, the stable Rust toolchain, packaging helpers, and the macOS env overrides GPUI needs (`DEVELOPER_DIR` / `SDKROOT`). Tasks: ```sh devenv tasks run openlogi:gui # run the desktop app devenv tasks run openlogi:check # fmt + clippy + tests (run before committing) devenv tasks run openlogi:dmg # build the macOS DMG devenv tasks run openlogi:i18n-upload # upload English source strings to Crowdin devenv tasks run openlogi:i18n-download # download translations and run i18n tests ``` After a `devenv.nix` change, reload direnv so the new env takes effect: ```sh direnv reload # or: exit your shell and `cd` back in ``` Without that, GPUI's `gpui_macos` build script can't find Apple's `metal` shader compiler, and link errors about missing `_write` / `_sysconf` / `_waitpid` symbols show up because the Nix `apple-sdk-14.4` stub doesn't expose `libSystem` the way Apple's real linker wants. ### Dev app bundle (macOS) On macOS the desktop binary is launched from inside a throwaway `target/dev/OpenLogi.app` — a Cargo `runner` wired in `.cargo/config.toml` (`scripts/cargo-run-macos.sh`). This makes the dev build show as **OpenLogi Dev** in the menu bar and Dock, with the real app icon; a bare `cargo run` binary has no bundle, so macOS would otherwise fall back to the `openlogi-gui` executable name and a generic icon. The binary is hardlinked in (no copy) and the icon is generated on demand by `cargo run -p xtask -- macos icns`. The runner is a transparent passthrough for everything else (the CLI, tests); set `OPENLOGI_DEV_BUNDLE=0` to launch the raw `openlogi-gui` binary instead. Packaged local dev bundles (`cargo run` and `cargo run -p xtask -- macos bundle`) use `.dev` bundle identifiers and the `openlogi-dev` XDG profile (`~/.config/openlogi-dev`, `~/.local/share/openlogi-dev`, and its own `agent.sock`). That keeps the dev GUI and agent from sharing the installed production app's Accessibility grant, single-instance lock, config, or IPC socket. Those identifiers are a channel, not a guess from the build type: `macos bundle` takes `--channel dev|production` (dev by default) and verifies what it stamped, and `macos dmg` refuses a non-production bundle once it is given a signing identity. Reproduce the shipped layout locally with `--channel production`, but don't sign and run it — it would take over the installed app's grants and config, which is exactly what releases 0.6.24–0.6.26 did in reverse. To install the CLI binary on `PATH`: ```sh cargo install --path . ``` ## Developing the GUI without hardware `openlogi-agent-mock` serves the real agent IPC contract from a scripted in-memory inventory, so the desktop app can be developed with no Logitech device (or receiver) attached: ```sh cargo run -p openlogi-agent --bin openlogi-agent-mock # then, in another terminal: OPENLOGI_DEV_AGENT=0 cargo run -p openlogi-gui ``` The mock defaults itself to the `openlogi-dev` profile (as if `OPENLOGI_PROFILE=dev` were set), which is the profile the dev app bundle already uses — so it meets the dev GUI on the dev socket, and an installed *release* build, which is on the production profile, keeps running untouched. (A locally built bundle installed into `/Applications` carries `.dev` identifiers and therefore shares the dev profile: it and the mock contend for the same lock, and whichever starts second exits.) `OPENLOGI_DEV_AGENT=0` keeps the runner from building and embedding the real agent for the GUI to auto-spawn; add `OPENLOGI_ALLOW_EXTERNAL_AGENT=1` if your installed production agent is running, since the runner's guard against it predates the profile split and cannot know the dev GUI is on a separate socket. Pass `OPENLOGI_PROFILE=prod` to serve the production socket instead; the mock then contends for the production agent's single-instance lock and refuses to start while it is running. The script covers an online mouse (DPI and SmartShift writes persist and read back, battery drains so poll-driven repaints are visible), an offline mouse, a lighting-capable keyboard, a directly-attached device, and a full Bolt pairing flow (discovery → passkey → paired). Its agent version carries a `-mock` suffix, so a mock session is identifiable in the UI. It is a dev tool only and is never bundled. ## Project layout ``` src/ the `openlogi` binary (workspace root package) — a thin wrapper over openlogi-cli crates/ openlogi-core/ types, config (TOML), paths, button + action catalog — no HID, no async openlogi-inject/ OS input synthesis: CGEvent, uinput/MPRIS, and SendInput openlogi-hidpp/ vendored HID++ protocol crate (lib name `hidpp`) openlogi-hid/ device discovery, HID++ reads/writes, and control capture over async-hid openlogi-assets/ device-render registry schema + cached HTTP fetch from OpenLogi asset mirrors openlogi-cli/ CLI implementation: command tree + `run()`, called by the `openlogi` binary openlogi-agent-core/ shared orchestration + the agent/GUI IPC contract openlogi-agent/ the `openlogi-agent` binary — background agent owning device I/O and the hook openlogi-hook/ OS mouse hook: macOS CGEventTap, Linux evdev/uinput, Windows WH_MOUSE_LL openlogi-gui/ the `openlogi-gui` binary — GPUI + gpui-component IPC client ``` ## Pre-commit checklist Before committing, the following must pass: ```sh cargo fmt --all -- --check cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace ``` Equivalent to `devenv tasks run openlogi:check`. ## Packaging the macOS DMG ```sh cargo run -p xtask -- macos package # → target/release/OpenLogi.dmg ``` Environment overrides: - `OPENLOGI_BUNDLE_ASSETS=1` — bundle every device render into the `.app` for a fully offline build (default: fetched on demand at first launch). - `OPENLOGI_SIGN_IDENTITY=` — codesign the `.app` and `.dmg` with the given Developer ID. - `OPENLOGI_DMG_BACKGROUND_URL=` — override the branded DMG background TIFF URL (default: `https://assets.openlogi.org/dmg/dmg-background.tiff`). The local packaging command and release workflow both use the same branded DMG layout: a 760×480 background image in a 760×512 Finder window, with 128px icons positioned at `(212, 250)` for `OpenLogi.app` and `(548, 250)` for `Applications`. ## Packaging Linux `.deb` / `.rpm` / `.pkg.tar.zst` Requires [nfpm](https://nfpm.goreleaser.com/) on `PATH`; the package arch is derived from the host (override with `PKG_ARCH`): ```sh cargo run -p xtask -- linux package # → target/release/openlogi_*.deb / .rpm / .pkg.tar.zst ``` The package contents (binaries, udev rules, systemd user unit, desktop entry, icon) are declared in `packaging/linux/nfpm.yaml`. ## Release updater publishing Tagged releases still attach DMGs and `SHA256SUMS` to GitHub Releases for manual downloads and the Homebrew cask. The release workflow also publishes the same DMGs to Cloudflare R2 and writes a static updater manifest at: ```text ${OPENLOGI_UPDATE_BASE_URL}/channels/stable/latest.json ``` The app embeds that manifest URL at build time via `OPENLOGI_UPDATE_MANIFEST_URL`, derived from `OPENLOGI_UPDATE_BASE_URL` in the release workflow. Release builds also embed `OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY` and run with `Verification::Strict`: an update is installed only if the manifest asset carries a minisign signature that verifies against that key, plus a matching SHA-256. A build without the key embedded (local/dev) fails closed — the update check errors rather than installing an unverified artifact. Configure the R2/update settings in one 1Password item referenced by the GitHub secret `OP_R2_SECRET_ITEM`. The item must contain: - `OPENLOGI_UPDATE_BASE_URL` — public HTTPS base URL, for example `https://updates.openlogi.org`. - `OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY` — base64 minisign public key embedded in the app and used to verify updater artifacts. - `OPENLOGI_UPDATE_MINISIGN_SECRET_KEY` — the passwordless minisign secret key file, **base64-encoded** (`base64 < minisign.key`), used only in the release publish job to sign DMGs before `latest.json` is generated. It is stored base64 (not raw) so its two lines survive 1Password's paste handling; the workflow decodes it, mirroring the GitHub App key. - `CLOUDFLARE_R2_ACCOUNT_ID` — Cloudflare account ID used for the S3 endpoint. - `CLOUDFLARE_R2_BUCKET` — bucket name. - `CLOUDFLARE_R2_ACCESS_KEY_ID` — R2 S3 access key. - `CLOUDFLARE_R2_SECRET_ACCESS_KEY` — R2 S3 secret key. The workflow uploads immutable artifacts under `/releases//` and only the channel manifest under `/channels/stable/latest.json` is mutable. The manifest is generated by the workspace `xtask` helper: ```sh cargo run -p xtask -- release latest-json \ --dist dist \ --tag v0.2.0 \ --base-url https://updates.openlogi.org \ --output dist/latest.json ``` ## Crowdin translation sync `.github/workflows/crowdin.yml` syncs GUI locales with [Crowdin](https://crowdin.com/project/openlogi) and opens a `crowdin/i18n` PR when a **real** translation value improved — nightly, and on master pushes that touch English sources (`en.yml`), `crowdin.yml`, the Crowdin workflow, the merge script under `scripts/i18n/`, or the shared GitHub App token action. **How it helps translation** | | Role | |--|--| | `en.yml` (git) | English source of truth — English text is the key | | All `locales/*.yml` in git | Same keys as `en.yml` (parity test); seed Crowdin per language | | Crowdin project | Where people improve non-English **values** | | Merge script | Applies only values ≠ English; restores keys sparse exports omit | | Bot PR (`crowdin/i18n`) | Only when a non-English value actually changed | Feature PRs add new keys to **every** locale file in the same change. Crowdin does not invent translations; it only stores and syncs them. A raw Crowdin download is unsafe: untranslated strings come back as English (#549), and `skip_untranslated_strings` overwrites catalogs with sparse files that delete keys (#552). The workflow always **snapshots → download → merge** via `scripts/i18n/merge_crowdin_download.py` so catalogs stay complete and only real translations land in git. Each run: 1. Snapshots every `locales/*.yml`. 2. Uploads `en.yml` **sources**. 3. Uploads **per-language translations** already in git (`import_eq_suggestions` off so `value == English` is not stored as a finished translation). 4. Downloads Crowdin’s export (`skip_untranslated_strings`; sparse is fine). 5. Merges the export into the snapshot (English fill-in ignored; omitted keys kept; headers / `_version` preserved). 6. Opens/updates `crowdin/i18n` only when the working tree still differs. Like the release workflow, the job reads its credentials from one 1Password item referenced by the GitHub secret `OP_CROWDIN_SECRET_ITEM`. The item must contain: - `CROWDIN_PROJECT_ID` — the numeric Crowdin project id. - `CROWDIN_PERSONAL_TOKEN` — a Crowdin API token with access to the project. Grant the token only these scopes and restrict its granular access to the OpenLogi project: - Projects (List, Get, Create, Edit) — Read. - Translation Status — Read Only. - Source files & strings — Read and Write. - Translations — Read and Write. Missing or invalid credentials fail the workflow. Translation PRs run the normal CI checks, including the locale key parity test (every catalog must match `en.yml` key-for-key). The workflow uses the existing `OP_GITHUB_APP_ITEM` to mint a short-lived token for pushing its translation branch and opening the PR; the default `GITHUB_TOKEN` remains read-only. Checkout runs with `persist-credentials: false` and the origin remote is rewritten to the app token so git push does not inherit the read-only Actions credential. Local helpers (with Crowdin credentials configured): ```sh devenv tasks run openlogi:i18n-upload # en.yml sources + per-language translations devenv tasks run openlogi:i18n-download # download + merge + i18n tests python3 scripts/i18n/merge_crowdin_download.py --self-test ``` --- ### INSTALL Linux # Installing OpenLogi on Linux > [!NOTE] > Linux support is in active development. HID++ device enumeration supports > **Logi Bolt** (USB PID `0xC548`) and **Logi Unifying** (PID `0xC52B` and > others) receivers, as well as Bluetooth-direct devices. ## Prerequisites - **Quit Solaar** (or any other Logitech manager) before starting OpenLogi — the two applications fight over HID++ access. - A kernel with `hidraw` and `uinput` module support (standard on all major distros). - `systemd` + `udev` (standard on Ubuntu, Fedora, Arch, Debian, openSUSE, …). ## Build from source Pre-built `.deb` and `.rpm` packages are available on the [releases page](https://github.com/AprilNEA/OpenLogi/releases/latest) — see the main [README](../README.md#linux) for the package-based install. To build from source instead, use the stable Rust toolchain: ```sh git clone https://github.com/AprilNEA/OpenLogi cd OpenLogi cargo build --release ``` The three binaries land in `target/release/`: | Binary | Role | |---|---| | `openlogi` | CLI — inventory, diagnostics, asset sync | | `openlogi-gui` | Desktop GUI | | `openlogi-agent` | Background agent — HID++ loop, input hook | ## Device access: udev rules OpenLogi needs: - **Write access to `/dev/uinput`** — to create the virtual input device for button remapping. - **Read/write access to `/dev/hidraw*`** — to send HID++ commands to the Bolt receiver, or to the device itself when it is paired over Bluetooth. - **Read access to the mouse's `/dev/input/event*` node** — the hook grabs the pointer there to capture button presses. Bluetooth mice need the bundled rule for this: their event node hangs off `/devices/virtual/misc/uhid`, which has no seat, so `logind` never grants the ACL on its own. Install the bundled udev rules to grant access to the active-seat user without requiring `sudo` or group membership (requires `systemd-logind`): ```sh sudo cp packaging/linux/udev/70-openlogi.rules /etc/udev/rules.d/ sudo udevadm control --reload-rules sudo udevadm trigger ``` Verify access (should open without error): ```sh # Check uinput openlogi-agent --check-uinput 2>/dev/null || \ test -w /dev/uinput && echo "uinput OK" # Check a hidraw node ls -la /dev/hidraw* # Check the mouse's event node — look for a "+" (ACL) in the mode, or your # user in the ACL itself. Without it the agent logs # "could not install OS mouse hook". getfacl /dev/input/event* ``` The GUI Settings → Permissions page shows a live `Granted` / `Not granted` indicator; check it after installing the rules (no restart needed). > **Device already connected?** `udevadm trigger` re-evaluates rules but does > not re-grant `uaccess` ACLs on nodes that were already open when the rules > were installed. If access is still denied, unplug and replug your receiver or > mouse (or power-cycle for wireless devices) to let udev apply the new rules on > reconnect. ### Non-systemd systems (SysV init, OpenRC) Replace `TAG+="uaccess"` in the rules file with `MODE="0660", GROUP="input"`, then add your user to the `input` group: ```sh sudo usermod -aG input "$USER" # Re-login for the group change to take effect. ``` ## Install with the script The `packaging/linux/install.sh` script copies the binaries, udev rules, systemd unit, desktop entry, and icon to system paths, then reloads `udevadm`. ```sh # From the repo root, after building: sudo packaging/linux/install.sh # Or to a custom prefix (e.g. /usr): packaging/linux/install.sh --prefix=/usr ``` To remove: ```sh packaging/linux/uninstall.sh ``` ## Autostart (launch at login) The background agent (`openlogi-agent`) must be running for the GUI and CLI to show connected devices. Enable it for your user session: ```sh systemctl --user enable --now openlogi-agent.service ``` Alternatively, toggle **Settings → General → Launch at login** in the GUI — it writes the unit to `~/.config/systemd/user/openlogi-agent.service` automatically. ## Verify the installation ```sh # List connected Logitech devices: openlogi list # Launch the GUI: openlogi-gui ``` ## Known limitations | Limitation | Status | |---|---| | Wayland: per-application profile switching | Requires XWayland (`WM_CLASS` lookup uses X11) | | Button capture: middle / mode-shift / thumbwheel | Side buttons only today | --- ### README.De > [!WARNING] > **OpenLogi befindet sich in aktiver Entwicklung** und ist noch nicht stabil — Funktionen und Konfiguration können sich noch ändern. Gib dem Repo einen **Star** ⭐ und **beobachte** 👀 es, um benachrichtigt zu werden, wenn ein neues Release erscheint.

English | 简体中文 | 日本語 | Deutsch | Français | 한국어

OpenLogi

OpenLogi

⚡️ Eine native, local-first Alternative zu Logitech Options+, geschrieben in Rust 🦀
Tasten, DPI und SmartShift über HID++ neu belegen. Kein Konto, keine Telemetrie.

AprilNEA%2FOpenLogi | Trendshift

> **Genug von Options+? Probier OpenLogi.** Tasten neu belegen, DPI und SmartShift steuern, Profile pro App umschalten — ohne Logitech-Konto, ohne Telemetrie, ohne das offizielle Options+. Keine Cloud, Konfiguration als einfaches TOML. Standardmäßig verbindet sich die App nur zum Abruf von Gerätebildern automatisch; Updateprüfungen und Downloads laufen nur auf Anfrage oder nach Opt-in. --- ## Was es ist OpenLogi spricht mit Logitech-HID++-Peripheriegeräten über Logi-Bolt- und Unifying-Empfänger, Bluetooth-Direktverbindungen oder USB-Kabel — ganz ohne Logi Options+. Es besteht aus drei Komponenten: - **[OpenLogi GUI](../crates/openlogi-gui)** — eine GPUI-Desktop-App: interaktives Mausdiagramm mit klickbaren Hotspots, Aktions-Picker pro Taste (eingebaute Aktionen plus eigene Tastenkürzel aus der TOML-Konfiguration), DPI-Voreinstellungen, SmartShift, native Scroll-Umkehr pro Gerät, RGB-Tastaturbeleuchtung, Profile pro Anwendung, Live-Gerätekarussell und ein in 20 Sprachen lokalisiertes Einstellungsfenster. - **[OpenLogi agent](../crates/openlogi-agent)** — der Hintergrunddienst, dem der Input-Hook und sämtliche Geräte-I/O gehören. Die GUI ist ein reiner IPC-Client und startet den Agent bei Bedarf. - **[OpenLogi CLI](../crates/openlogi-cli)** — ein Kommandozeilenwerkzeug für headless Inventar (`list`) sowie Asset-Sync- und Geräte-Diagnose-Unterbefehle. Alles bleibt lokal: Belegungen liegen in einer einfachen TOML-Datei, der Agent leitet Tastendrücke über den OS-Input-Hook um und schreibt DPI-, SmartShift-, Scroll- und Beleuchtungsänderungen per HID++ direkt aufs Gerät. macOS, Linux und Windows werden unterstützt. Windows ist der neueste Port: Er wurde auf Windows 11-Hardware vollständig validiert, kann aber noch mehr Ecken und Kanten als die macOS- und Linux-Builds haben; siehe [Roadmap](#roadmap). ## Mehr als Options+ Was OpenLogi kann und Options+ nicht: - **Auf Linux laufen.** Options+ gibt es nur für macOS und Windows. OpenLogi behandelt Linux als vollwertige Plattform: evdev/uinput-Hook, udev-Regeln, eine systemd-User-Unit und `.deb`-/`.rpm`-/`.pkg.tar.zst`-Pakete. - **Die Gestentaste verschieben.** Wähle, welche physische Taste die Gestenrolle übernimmt — dedizierte Gestentaste, Mitteltaste, Zurück oder Vor — mit Wischbelegungen pro Richtung, oder schalte Gesten ganz ab. Options+ nagelt die Gestenrolle auf die dedizierte Gestentaste fest. - **Konfiguration im Klartext.** Alles steckt in einer TOML-Datei, die du lesen, diffen, versionieren und zwischen Rechnern kopieren kannst. - **Skriptbar.** Eine echte CLI: Geräteinventar, Asset-Prefetch und HID++-Diagnosen am Gerät (Feature-/Control-Dumps, DPI-/SmartShift-Roundtrips und Prüfungen der Tastaturbeleuchtung). - **Leichtgewichtig bleiben.** Native Rust-+-GPUI-Binaries — keine Electron-Suite, keine residenten Updater, kein Konto, keine Telemetrie. ## Roadmap | Fähigkeit | Status | |---|---| | Bolt-Empfänger finden + gekoppelte Geräte auflisten (CLI + GUI) | ✅ | | Unifying-Empfänger (älteres Protokoll, von Bolt abgelöst) | ✅ | | Bluetooth-Direkt- / Kabelgeräte (ohne Empfänger) | ✅ | | Akkustand / Ladezustand | ✅ (Geräte online) | | Interaktive GUI: Karussell, Mausdiagramm, Aktions-Picker | ✅ macOS + Linux + Windows | | Tastenumbelegung über den OS-Input-Hook | ✅ macOS + Linux + Windows | | Katalog eingebauter Aktionen + eigene Tastenkürzel (in TOML angelegt) | ✅ macOS + Linux + Windows¹ | | DPI-Steuerung + Voreinstellungen + Cycle-/Set-Preset-Aktionen (HID++ `0x2201`) | ✅ | | SmartShift-Rad: Modus + Empfindlichkeit + permanente Rasterung (HID++ `0x2111`) | ✅ | | Native Scroll-Umkehr pro Gerät (HID++ `0x2121`) | ✅ (unterstützte Geräte) | | Statische RGB-Tastaturbeleuchtung (HID++ `0x8070` / `0x8080`) | ✅ (unterstützte Geräte) | | Profil-Overlays pro Anwendung (Auto-Wechsel bei App-Fokus) | ✅ macOS + Windows, 🟡 Linux (nur X11 / XWayland) | | Einstellungsfenster: Autostart, Updates, Berechtigungen, Sprache, Erscheinungsbild | ✅ macOS + Linux + Windows | | Agent-Statussymbol | ✅ macOS-Menüleiste + Windows-Infobereich; unter Linux nicht anwendbar | | Lokalisierte Oberfläche (20 Sprachen: da, de, el, en, es, fi, fr, it, ja, ko, nb, nl, pl, pt-BR, pt-PT, ru, sv, zh-CN, zh-HK, zh-TW) | ✅ | | Linux-Paketierung: udev-Regeln, systemd-Unit, `.deb` / `.rpm` / `.pkg.tar.zst` | ✅ Linux | | Gestentaste: Belegungen pro Richtung + Live-Erfassung | ✅ (abhängig von Gerätefähigkeiten) | | Erfassung von Mittel-/Mode-Shift-/Daumenrad-Taste | ✅ Mitteltaste auf allen Plattformen; Mode-Shift / Daumenrad geräteabhängig | | Windows (Agent, GUI, Event-Hook, Installer) | ✅ auf Windows 11-Hardware validiert; neuerer Port mit laufender Kompatibilitätsverbesserung | ¹ Medientasten-Aktionen nutzen unter Linux D-Bus MPRIS; einige macOS-spezifische Aktionen haben unter Linux kein universelles Gegenstück und sind No-ops. Windows bildet Plattformaktionen, wo verfügbar, auf native Entsprechungen ab. ## Installation > [!IMPORTANT] > Beende zuerst **Logi Options+** — die beiden Anwendungen streiten sich um den HID++-Zugriff, und ein Empfänger kann immer nur einem gehören. ### macOS Erfordert macOS 13 oder neuer. Lade das signierte, notarisierte `.dmg` vom [neuesten Release](https://github.com/AprilNEA/OpenLogi/releases/latest) und ziehe `OpenLogi.app` nach `/Applications`. Oder per [Homebrew](https://brew.sh): ```sh brew install --cask openlogi ``` Der offizielle Homebrew-Cask ist der Standardweg. Um stattdessen explizit das neueste GitHub-Release über `aprilnea/tap` zu verfolgen: ```sh brew tap aprilnea/tap brew install --cask aprilnea/tap/openlogi@latest ``` `openlogi@latest` wird vom Release-Workflow von OpenLogi gepflegt und kann aktualisiert sein, bevor der Autobump des offiziellen Casks greift. Installiere entweder `openlogi` oder `openlogi@latest`, nicht beide. ### Linux Lade das `.deb` oder `.rpm` vom [neuesten Release](https://github.com/AprilNEA/OpenLogi/releases/latest): ```sh # Debian / Ubuntu sudo dpkg -i openlogi_*.deb # Fedora / RHEL sudo rpm -i openlogi-*.rpm # Arch Linux sudo pacman -U openlogi-*.pkg.tar.zst ``` Pakete erscheinen für `x86_64`/`amd64` und `arm64`/`aarch64`. Das Paket installiert udev-Regeln, die deinem Benutzer Zugriff auf `/dev/hidraw*` und `/dev/uinput` ohne `sudo` geben. Aktiviere nach der Installation den Hintergrund-Agent für deinen Benutzer: ```sh systemctl --user enable --now openlogi-agent.service ``` Für manuelle / Quellcode-Installationen und Distributionen ohne systemd siehe [INSTALL-linux.md](INSTALL-linux.md). ### Windows Jedem Release liegen signierte portable `.zip`-Archive und Per-User-`.msi`-Installer (x86_64 und arm64) bei. Beide enthalten die GUI (`OpenLogi.exe`) zusammen mit dem Hintergrund-Agent (`openlogi-agent.exe`), dem sämtliche Geräte-I/O gehören. Halte bei der portablen ZIP beide Dateien nebeneinander, sonst hat die GUI keine Gegenstelle. Windows funktioniert und wurde auf echter Windows 11-Hardware vollständig validiert — mit einer kabelgebundenen Tastatur und einer Maus am Unifying-Empfänger, einschließlich Installation, In-Place-Upgrade und Deinstallation des MSI. Der Port ist neuer als die macOS-Version; [melde](https://github.com/AprilNEA/OpenLogi/issues) bitte Ecken und Kanten. Der Agent zeigt ein Symbol im Infobereich (Hauptfenster anzeigen / Beenden), damit die App nach dem Schließen des Hauptfensters erreichbar bleibt. Setze zum Deaktivieren unter Windows `show_in_menu_bar = false` im TOML-Block `[app_settings]` und starte den Agent neu; der GUI-Schalter ist derzeit nur unter macOS verfügbar. Zum Bauen aus dem Quellcode siehe [DEVELOPMENT.md](DEVELOPMENT.md). ## Verwendung (CLI) Siehe [USAGE.md](USAGE.md) ## Konfiguration Siehe [CONFIGURATION.md](CONFIGURATION.md) ## Entwicklung Siehe [DEVELOPMENT.md](DEVELOPMENT.md) ## Danksagungen - **Windows, Kameras und i18n** von [@davidbudnick](https://github.com/davidbudnick) — der Windows-Eingabe-Hook und MSI-Updates, Logitech-Webcam-Unterstützung, Tastatur-RGB und die Crowdin-Übersetzungspipeline - **Linux-Portierung** von [@cserby](https://github.com/cserby) — evdev/uinput-Hook, D-Bus-Aktionen, .deb/.rpm-Paketierung - [Solaar](https://github.com/pwr-Solaar/Solaar) von [@pwr](https://github.com/pwr) — die vollständigste quelloffene HID++-Implementierung und unsere Protokollreferenz - [Mouser](https://github.com/TomBadash/Mouser) von [@TomBadash](https://github.com/TomBadash) — Vorarbeit zum selben Ziel: ein lokaler Options+-Ersatz ohne Konto ## Lizenz Doppelt lizenziert, wahlweise unter - Apache License, Version 2.0 ([LICENSE-APACHE](../LICENSE-APACHE)) - MIT-Lizenz ([LICENSE-MIT](../LICENSE-MIT)) ### Code von Dritten `crates/openlogi-hidpp` ist ein eingebundener Fork von [`hidpp`](https://crates.io/crates/hidpp) von [@lus](https://github.com/lus), lizenziert unter 0BSD. ### Logo & Markenressourcen Das OpenLogi-Logo und das App-Icon — die Markenressourcen unter [`design/`](../design/) — sind © 2026 AprilNEA, alle Rechte vorbehalten, und fallen nicht unter die obigen MIT-/Apache-Lizenzen; siehe [`design/LICENSE`](../design/LICENSE). Ein Fork des Codes gewährt kein Recht am Namen, Logo oder Icon von OpenLogi; bitte verwende sie nicht ohne vorherige schriftliche Erlaubnis für eigene Projekte, Forks oder Distributionen. --- **Nicht mit Logitech verbunden.** „Logitech", „MX Master" und „Options+" sind Marken der Logitech International S.A. ## Repo-Aktivität --- ### README.Fr > [!WARNING] > **OpenLogi est en cours de développement actif** et n'est pas encore stable — les fonctionnalités et la configuration peuvent encore changer. Mettez une **Star** ⭐ au dépôt et **suivez-le** 👀 pour être averti dès qu'une nouvelle version est publiée.

English | 简体中文 | 日本語 | Deutsch | Français | 한국어

OpenLogi

OpenLogi

⚡️ Une alternative native et local-first à Logitech Options+, écrite en Rust 🦀
Remappez boutons, DPI et SmartShift via HID++. Sans compte, sans télémétrie.

AprilNEA%2FOpenLogi | Trendshift

> **Assez d'Options+ ? Essayez OpenLogi.** Remappez les boutons, pilotez le DPI et SmartShift, basculez de profil selon l'application — sans compte Logitech, sans télémétrie, sans installer l'Options+ officiel. Pas de cloud, une configuration en TOML brut. Par défaut, l'application ne se connecte automatiquement que pour récupérer les images d'appareils ; la vérification et le téléchargement des mises à jour ne se font qu'à votre demande ou après opt-in. --- ## Présentation OpenLogi dialogue avec les périphériques Logitech HID++ via des récepteurs Logi Bolt et Unifying, une connexion Bluetooth directe ou un câble USB — sans exécuter Logi Options+. Il se compose de trois éléments : - **[OpenLogi GUI](../crates/openlogi-gui)** — une application de bureau GPUI : schéma de souris interactif avec zones cliquables, sélecteur d'action par bouton (actions intégrées et raccourcis personnalisés rédigés dans la configuration TOML), préréglages DPI, SmartShift, inversion native du défilement par appareil, éclairage RGB des claviers, profils par application, carrousel d'appareils en direct et fenêtre de réglages traduite en 20 langues. - **[OpenLogi agent](../crates/openlogi-agent)** — le service d'arrière-plan qui possède le hook d'entrée et toutes les E/S des appareils. La GUI est un pur client IPC et démarre l'agent au besoin. - **[OpenLogi CLI](../crates/openlogi-cli)** — un outil en ligne de commande : inventaire headless (`list`), synchronisation des assets et sous-commandes de diagnostic des appareils. Tout reste local : les affectations vivent dans un fichier TOML brut, l'agent remappe les pressions de boutons par le hook d'entrée de l'OS et écrit directement sur l'appareil via HID++ les changements de DPI, SmartShift, défilement et éclairage. macOS, Linux et Windows sont pris en charge. Windows est le portage le plus récent : il a été validé de bout en bout sur du matériel Windows 11, mais peut rester moins poli que les builds macOS et Linux ; voir la [feuille de route](#feuille-de-route). ## Au-delà d'Options+ Ce qu'OpenLogi fait et qu'Options+ ne fait pas : - **Tourner sous Linux.** Options+ n'existe que pour macOS et Windows. OpenLogi traite Linux en plateforme de premier rang : hook evdev/uinput, règles udev, unité utilisateur systemd et paquets `.deb` / `.rpm` / `.pkg.tar.zst`. - **Déplacer le bouton de gestes.** Choisissez quel bouton physique porte le rôle de gestes — bouton de gestes dédié, bouton du milieu, précédent ou suivant — avec des affectations de glissement par direction, ou désactivez complètement les gestes. Options+ fige ce rôle sur le bouton de gestes dédié. - **Une configuration en texte brut.** Tout tient dans un fichier TOML que vous pouvez lire, diff-er, versionner et copier entre machines. - **Scriptable.** Une vraie CLI : inventaire des appareils, préchargement des assets et diagnostics HID++ sur l'appareil (dumps des features / contrôles, allers-retours DPI / SmartShift et vérification de l'éclairage du clavier). - **Rester léger.** Des binaires natifs Rust + GPUI — pas de suite Electron, pas d'updaters résidents, pas de compte, pas de télémétrie. ## Feuille de route | Capacité | État | |---|---| | Découverte des récepteurs Bolt + liste des appareils appairés (CLI + GUI) | ✅ | | Récepteurs Unifying (protocole plus ancien, remplacé par Bolt) | ✅ | | Appareils Bluetooth directs / filaires (sans récepteur) | ✅ | | Pourcentage de batterie / état de charge | ✅ (appareils en ligne) | | GUI interactive : carrousel, schéma de souris, sélecteur d'action | ✅ macOS + Linux + Windows | | Remappage des boutons via le hook d'entrée de l'OS | ✅ macOS + Linux + Windows | | Catalogue d'actions intégrées + raccourcis clavier personnalisés (rédigés en TOML) | ✅ macOS + Linux + Windows¹ | | Contrôle DPI + préréglages + actions Cycle / Set-preset (HID++ `0x2201`) | ✅ | | Molette SmartShift : mode + sensibilité + cran permanent (HID++ `0x2111`) | ✅ | | Inversion native du défilement par appareil (HID++ `0x2121`) | ✅ (appareils compatibles) | | Éclairage RGB statique des claviers (HID++ `0x8070` / `0x8080`) | ✅ (appareils compatibles) | | Surcouches de profil par application (bascule automatique au focus) | ✅ macOS + Windows, 🟡 Linux (X11 / XWayland uniquement) | | Fenêtre de réglages : lancement à la connexion, mises à jour, permissions, langue, apparence | ✅ macOS + Linux + Windows | | Icône d'état de l'agent | ✅ barre des menus macOS + zone de notification Windows ; sans objet sous Linux | | Interface localisée (20 langues : da, de, el, en, es, fi, fr, it, ja, ko, nb, nl, pl, pt-BR, pt-PT, ru, sv, zh-CN, zh-HK, zh-TW) | ✅ | | Empaquetage Linux : règles udev, unité systemd, `.deb` / `.rpm` / `.pkg.tar.zst` | ✅ Linux | | Affectations par direction du bouton de gestes + capture en direct | ✅ (selon les capacités de l'appareil) | | Capture des boutons du milieu / mode-shift / molette de pouce | ✅ milieu sur toutes les plateformes ; mode-shift / molette selon l'appareil | | Windows (agent, GUI, hook d'événements, installateur) | ✅ validé sur du matériel Windows 11 ; portage récent dont la compatibilité continue d'être peaufinée | ¹ Sous Linux, les actions de touches multimédia passent par D-Bus MPRIS ; quelques actions propres à macOS n'ont pas d'équivalent Linux universel et sont sans effet. Windows associe les actions de plateforme à leurs équivalents natifs lorsqu'ils existent. ## Installation > [!IMPORTANT] > Quittez d'abord **Logi Options+** — les deux applications se disputent l'accès HID++ et un récepteur ne peut appartenir qu'à une seule à la fois. ### macOS Nécessite macOS 13 ou une version ultérieure. Téléchargez le `.dmg` signé et notarié depuis la [dernière release](https://github.com/AprilNEA/OpenLogi/releases/latest) et glissez `OpenLogi.app` dans `/Applications`. Ou installez via [Homebrew](https://brew.sh) : ```sh brew install --cask openlogi ``` Le cask Homebrew officiel est la voie d'installation par défaut. Pour suivre explicitement la dernière release GitHub via `aprilnea/tap` : ```sh brew tap aprilnea/tap brew install --cask aprilnea/tap/openlogi@latest ``` `openlogi@latest` est maintenu par le workflow de release d'OpenLogi et peut être mis à jour avant l'autobump du cask officiel. Installez `openlogi` ou `openlogi@latest`, pas les deux. ### Linux Téléchargez le `.deb` ou le `.rpm` depuis la [dernière release](https://github.com/AprilNEA/OpenLogi/releases/latest) : ```sh # Debian / Ubuntu sudo dpkg -i openlogi_*.deb # Fedora / RHEL sudo rpm -i openlogi-*.rpm # Arch Linux sudo pacman -U openlogi-*.pkg.tar.zst ``` Les paquets sont publiés pour `x86_64`/`amd64` et `arm64`/`aarch64`. Le paquet installe des règles udev qui donnent à votre utilisateur l'accès à `/dev/hidraw*` et `/dev/uinput` sans `sudo`. Après l'installation, activez l'agent d'arrière-plan pour votre utilisateur : ```sh systemctl --user enable --now openlogi-agent.service ``` Pour les installations manuelles / depuis les sources et les distributions sans systemd, voir [INSTALL-linux.md](INSTALL-linux.md). ### Windows Des archives portables `.zip` signées et des installateurs `.msi` par utilisateur (x86_64 et arm64) accompagnent chaque release. Tous deux contiennent la GUI (`OpenLogi.exe`) et l'agent d'arrière-plan (`openlogi-agent.exe`), qui possède toutes les E/S des appareils. Avec le ZIP portable, gardez les deux fichiers côte à côte, sinon la GUI n'aura rien auquel se connecter. La prise en charge de Windows fonctionne et a été validée de bout en bout sur du matériel Windows 11 réel — un clavier filaire et une souris sur récepteur Unifying, y compris l'installation, la mise à niveau sur place et la désinstallation du MSI. Ce portage est plus récent que celui de macOS ; [signalez](https://github.com/AprilNEA/OpenLogi/issues) toute aspérité. L'agent affiche une icône dans la zone de notification (Afficher la fenêtre principale / Quitter), afin que l'application reste accessible après la fermeture de sa fenêtre principale. Pour la désactiver sous Windows, définissez `show_in_menu_bar = false` dans le bloc TOML `[app_settings]`, puis redémarrez l'agent ; l'option de la GUI est actuellement réservée à macOS. Pour compiler depuis les sources, voir [DEVELOPMENT.md](DEVELOPMENT.md). ## Utilisation (CLI) Voir [USAGE.md](USAGE.md) ## Configuration Voir [CONFIGURATION.md](CONFIGURATION.md) ## Développement Voir [DEVELOPMENT.md](DEVELOPMENT.md) ## Remerciements - **Windows, caméras et i18n** par [@davidbudnick](https://github.com/davidbudnick) — le hook d'entrée Windows et les mises à jour MSI, la prise en charge des webcams Logitech, le RGB clavier et le pipeline de traduction Crowdin - **Portage Linux** par [@cserby](https://github.com/cserby) — le hook evdev/uinput, les actions D-Bus, l'empaquetage .deb/.rpm - [Solaar](https://github.com/pwr-Solaar/Solaar) par [@pwr](https://github.com/pwr) — l'implémentation open source la plus complète de HID++, et notre référence pour le protocole - [Mouser](https://github.com/TomBadash/Mouser) par [@TomBadash](https://github.com/TomBadash) — un précurseur avec le même objectif : un remplacement d'Options+ local et sans compte ## Licence Sous double licence, au choix : - Apache License, version 2.0 ([LICENSE-APACHE](../LICENSE-APACHE)) - Licence MIT ([LICENSE-MIT](../LICENSE-MIT)) ### Code tiers `crates/openlogi-hidpp` est un fork intégré de [`hidpp`](https://crates.io/crates/hidpp) par [@lus](https://github.com/lus), sous licence 0BSD. ### Logo et ressources de marque Le logo et l'icône d'application OpenLogi — les ressources de marque sous [`design/`](../design/) — sont © 2026 AprilNEA, tous droits réservés, et ne sont pas couverts par les licences MIT/Apache ci-dessus ; voir [`design/LICENSE`](../design/LICENSE). Forker le code ne confère aucun droit sur le nom, le logo ou l'icône d'OpenLogi ; merci de ne pas les utiliser pour représenter vos propres projets, forks ou distributions sans autorisation écrite préalable. --- **Sans affiliation avec Logitech.** « Logitech », « MX Master » et « Options+ » sont des marques de Logitech International S.A. ## Activité du dépôt --- ### README.Ja > [!WARNING] > **OpenLogi は現在活発に開発中**であり、まだ安定していません —— 機能や設定は今後も変わる可能性があります。リポジトリに **Star** ⭐ と **Watch** 👀 を付けて、新しいリリースの通知を受け取りましょう。

English | 简体中文 | 日本語 | Deutsch | Français | 한국어

OpenLogi

OpenLogi

⚡️ Rust 製のネイティブでローカルファーストな Logitech Options+ 代替 🦀
HID++ でボタン・DPI・SmartShift を再マッピング。アカウント不要、テレメトリなし。

AprilNEA%2FOpenLogi | Trendshift

> **Options+ にうんざり?OpenLogi をどうぞ。** Logitech アカウントもテレメトリも公式 Options+ のインストールも不要で、ボタンの再マッピング、DPI と SmartShift の制御、アプリごとのプロファイル切り替えができます。クラウドなし、設定はプレーンな TOML ファイル。デフォルトでは自動接続はデバイス画像の取得だけで、更新の確認とダウンロードは要求時またはオプトイン時にのみ実行されます。 --- ## 概要 OpenLogi は Logi Bolt および Unifying レシーバー、Bluetooth 直結、USB ケーブル経由で Logitech の HID++ 周辺機器と通信します。Logi Options+ を動かす必要はありません。3 つのコンポーネントで構成されます: - **[OpenLogi GUI](../crates/openlogi-gui)** —— GPUI 製デスクトップアプリ:クリック可能なホットスポット付きのインタラクティブなマウス図、ボタンごとのアクションピッカー(組み込みアクション + TOML 設定で作成するカスタムショートカット)、DPI プリセット、SmartShift、デバイスごとのスクロール反転、RGB キーボード照明、アプリごとのプロファイル、ライブデバイスカルーセル、20 言語にローカライズされた設定ウィンドウ。 - **[OpenLogi agent](../crates/openlogi-agent)** —— 入力フックとすべてのデバイス I/O を所有するバックグラウンドサービス。GUI は純粋な IPC クライアントで、必要時に agent を起動します。 - **[OpenLogi CLI](../crates/openlogi-cli)** —— ヘッドレスなデバイス一覧(`list`)、アセット同期、デバイス診断のサブコマンドを備えた CLI。 すべてはローカルで完結します:バインディングはプレーンな TOML ファイルに保存され、agent が OS の入力フックでボタン入力を再マッピングし、DPI、SmartShift、スクロール、照明の変更を HID++ で直接デバイスに書き込みます。 macOS、Linux、Windows をサポートしています。Windows は最新の移植で、Windows 11 実機上でエンドツーエンド検証済みですが、macOS / Linux ビルドより粗削りな部分が残る可能性があります。[ロードマップ](#ロードマップ)を参照してください。 ## Options+ を超えて OpenLogi にできて Options+ にできないこと: - **Linux で動く。** Options+ は macOS と Windows のみ。OpenLogi は Linux をファーストクラスで扱います:evdev/uinput フック、udev ルール、systemd ユーザーユニット、`.deb` / `.rpm` / `.pkg.tar.zst` パッケージ。 - **ジェスチャーボタンを移せる。** どの物理ボタンがジェスチャー役を担うか —— 専用ジェスチャーボタン、ミドル、戻る、進む —— を選べ、方向ごとのスワイプバインディングを設定でき、ジェスチャーを完全にオフにもできます。Options+ はジェスチャーを専用ジェスチャーボタンに固定しています。 - **設定がプレーンテキスト。** すべてが 1 つの TOML ファイル。読めて、diff できて、バージョン管理に入れられて、マシン間でコピーできます。 - **スクリプトで叩ける。** 本物の CLI:デバイス一覧、アセットのプリフェッチ、デバイス上での HID++ 診断(フィーチャー / コントロールダンプ、DPI / SmartShift のラウンドトリップ検査、キーボード照明チェック)。 - **軽量なまま。** ネイティブ Rust + GPUI バイナリ —— Electron スイートも常駐アップデーターもアカウントもテレメトリもなし。 ## ロードマップ | 機能 | 状態 | |---|---| | Bolt レシーバーの発見 + ペアリング済みデバイスの一覧(CLI + GUI) | ✅ | | Unifying レシーバー(Bolt に置き換えられた旧プロトコル) | ✅ | | Bluetooth 直結 / 有線デバイス(レシーバーなし) | ✅ | | バッテリー残量 / 充電状態 | ✅(オンラインのデバイス) | | インタラクティブ GUI:カルーセル、マウス図、アクションピッカー | ✅ macOS + Linux + Windows | | OS 入力フックによるボタン再マッピング | ✅ macOS + Linux + Windows | | 組み込みアクションカタログ + カスタムキーボードショートカット(TOML で作成) | ✅ macOS + Linux + Windows¹ | | DPI 制御 + プリセット + サイクル / プリセット指定アクション(HID++ `0x2201`) | ✅ | | SmartShift ホイール:モード切替 + 感度 + 永続ラチェットパネル(HID++ `0x2111`) | ✅ | | デバイスごとのネイティブスクロール反転(HID++ `0x2121`) | ✅(対応デバイス) | | 静的 RGB キーボード照明(HID++ `0x8070` / `0x8080`) | ✅(対応デバイス) | | アプリごとのプロファイルオーバーレイ(フォーカスで自動切替) | ✅ macOS + Windows、🟡 Linux(X11 / XWayland のみ) | | 設定ウィンドウ:ログイン時起動、更新、権限、言語、外観 | ✅ macOS + Linux + Windows | | Agent ステータスアイコン | ✅ macOS メニューバー + Windows トレイ;Linux には非該当 | | UI のローカライズ(20 言語:da、de、el、en、es、fi、fr、it、ja、ko、nb、nl、pl、pt-BR、pt-PT、ru、sv、zh-CN、zh-HK、zh-TW) | ✅ | | Linux パッケージング:udev ルール、systemd ユニット、`.deb` / `.rpm` / `.pkg.tar.zst` | ✅ Linux | | ジェスチャーボタンの方向別バインディング + ライブキャプチャ | ✅(デバイス機能に依存) | | ミドル / モードシフト / サムホイールボタンのキャプチャ | ✅ ミドルは全プラットフォーム;モードシフト / サムホイールはデバイス機能に依存 | | Windows(agent、GUI、イベントフック、インストーラー) | ✅ Windows 11 実機で検証済み;新しい移植のため互換性を継続改善中 | ¹ Linux のメディアキーアクションは D-Bus MPRIS を使います。少数の macOS 固有アクションには Linux で汎用的な対応物がなく、no-op になります。Windows では利用可能なプラットフォームアクションをネイティブの対応機能に割り当てます。 ## インストール > [!IMPORTANT] > 先に **Logi Options+** を終了してください —— 両者は HID++ アクセスを奪い合い、1 つのレシーバーを同時に所有できるのは片方だけです。 ### macOS macOS 13 以降が必要です。 [最新リリース](https://github.com/AprilNEA/OpenLogi/releases/latest)から署名・公証済みの `.dmg` をダウンロードし、`OpenLogi.app` を `/Applications` にドラッグします。 または [Homebrew](https://brew.sh) で: ```sh brew install --cask openlogi ``` 公式 Homebrew cask が標準のインストール経路です。代わりに `aprilnea/tap` で GitHub の最新リリースを明示的に追うには: ```sh brew tap aprilnea/tap brew install --cask aprilnea/tap/openlogi@latest ``` `openlogi@latest` は OpenLogi のリリースワークフローが管理しており、公式 cask の autobump より先に更新されることがあります。`openlogi` か `openlogi@latest` のどちらか一方だけをインストールしてください。 ### Linux [最新リリース](https://github.com/AprilNEA/OpenLogi/releases/latest)から `.deb` または `.rpm` をダウンロード: ```sh # Debian / Ubuntu sudo dpkg -i openlogi_*.deb # Fedora / RHEL sudo rpm -i openlogi-*.rpm # Arch Linux sudo pacman -U openlogi-*.pkg.tar.zst ``` パッケージは `x86_64`/`amd64` と `arm64`/`aarch64` の両方で公開されています。 パッケージは udev ルールをインストールし、`sudo` なしで `/dev/hidraw*` と `/dev/uinput` にアクセスできるようにします。インストール後、ユーザーのバックグラウンドエージェントを有効化してください: ```sh systemctl --user enable --now openlogi-agent.service ``` 手動 / ソースからのインストールや systemd のないディストリビューションは [INSTALL-linux.md](INSTALL-linux.md) を参照。 ### Windows 各リリースには署名済みポータブル `.zip` とユーザー単位の `.msi` インストーラー(x86_64 / arm64)が付属します。どちらも GUI(`OpenLogi.exe`)と、すべてのデバイス I/O を所有するバックグラウンド agent(`openlogi-agent.exe`)を同梱します。ポータブル zip では 2 ファイルを同じ場所に置いてください。そうしないと GUI は接続先を失います。 Windows サポートは動作しており、有線キーボードと Unifying レシーバー接続のマウスを使い、MSI のインストール、インプレースアップグレード、アンインストールを含めて Windows 11 実機でエンドツーエンド検証済みです。macOS 版より新しいため、問題があれば[報告](https://github.com/AprilNEA/OpenLogi/issues)してください。agent はシステムトレイアイコン(メインウィンドウを表示 / 終了)を表示し、メインウィンドウを閉じてもアプリを開けます。Windows で無効にするには TOML の `[app_settings]` ブロックで `show_in_menu_bar = false` を設定し、agent を再起動してください。GUI の切り替えは現在 macOS 専用です。 ソースからのビルドは [DEVELOPMENT.md](DEVELOPMENT.md) を参照。 ## 使い方(CLI) [USAGE.md](USAGE.md) を参照 ## 設定 [CONFIGURATION.md](CONFIGURATION.md) を参照 ## 開発 [DEVELOPMENT.md](DEVELOPMENT.md) を参照 ## 謝辞 - **Windows・カメラ・i18n**: [@davidbudnick](https://github.com/davidbudnick) —— Windows の入力フックと MSI アップデート、Logitech ウェブカメラ対応、キーボード RGB、Crowdin 翻訳パイプライン - **Linux 移植**: [@cserby](https://github.com/cserby) —— evdev/uinput フック、D-Bus アクション、.deb/.rpm パッケージング - [Solaar](https://github.com/pwr-Solaar/Solaar) by [@pwr](https://github.com/pwr) —— 最も網羅的なオープンソースの HID++ 実装であり、本プロジェクトのプロトコル参照元 - [Mouser](https://github.com/TomBadash/Mouser) by [@TomBadash](https://github.com/TomBadash) —— 同じ目標の先行プロジェクト:ローカル完結・アカウント不要の Options+ 代替 ## ライセンス 以下のいずれかを選択できます: - Apache License 2.0([LICENSE-APACHE](../LICENSE-APACHE)) - MIT ライセンス([LICENSE-MIT](../LICENSE-MIT)) ### サードパーティコード `crates/openlogi-hidpp` は [`hidpp`](https://crates.io/crates/hidpp)(作者 [@lus](https://github.com/lus))の vendored fork で、0BSD ライセンスです。 ### ロゴとブランドアセット OpenLogi のロゴとアプリアイコン —— [`design/`](../design/) 配下のブランドアセット —— は © 2026 AprilNEA が全権利を留保しており、上記の MIT/Apache ライセンスの対象外です。[`design/LICENSE`](../design/LICENSE) を参照してください。コードをフォークしても OpenLogi の名称・ロゴ・アイコンの使用権は付与されません。事前の書面による許可なく、ご自身のプロジェクト、フォーク、配布物を表すために使用しないでください。 --- **Logitech とは無関係です。** 「Logitech」「MX Master」「Options+」は Logitech International S.A. の商標です。 ## リポジトリの活動 --- ### README.Ko > [!WARNING] > **OpenLogi는 활발히 개발 중**이며 아직 안정 단계가 아닙니다 — 기능과 설정이 변경될 수 있습니다. 저장소에 **Star** ⭐ 와 **Watch** 👀 를 눌러 두면 새 릴리스가 나올 때 알림을 받을 수 있습니다.

English | 简体中文 | 日本語 | Deutsch | Français | 한국어

OpenLogi

OpenLogi

⚡️ Rust로 작성된 네이티브 로컬 우선 Logitech Options+ 대안 🦀
HID++로 버튼·DPI·SmartShift를 리매핑하세요. 계정도, 텔레메트리도 없습니다.

AprilNEA%2FOpenLogi | Trendshift

> **Options+가 지긋지긋하다면? OpenLogi를 써 보세요.** Logitech 계정도, 텔레메트리도, 공식 Options+ 설치도 없이 버튼을 리매핑하고 DPI와 SmartShift를 제어하며 앱별 프로필을 전환할 수 있습니다. 클라우드 없이 순수 TOML 설정 파일만 사용합니다. 기본 설정에서는 기기 이미지를 가져올 때만 자동으로 연결하며, 업데이트 확인과 다운로드는 요청하거나 옵트인한 경우에만 실행됩니다. --- ## 소개 OpenLogi는 Logi Bolt 및 Unifying 수신기, Bluetooth 직접 연결 또는 USB 케이블을 통해 Logitech HID++ 주변기기와 통신하며, Logi Options+를 실행할 필요가 없습니다. 세 가지 구성 요소로 이루어집니다: - **[OpenLogi GUI](../crates/openlogi-gui)** — GPUI 데스크톱 앱: 클릭 가능한 핫스팟이 있는 인터랙티브 마우스 다이어그램, 버튼별 액션 선택기(내장 액션 + TOML 설정에서 작성하는 사용자 지정 단축키), DPI 프리셋, SmartShift, 기기별 스크롤 반전, RGB 키보드 조명, 앱별 프로필, 실시간 기기 캐러셀, 20개 언어로 현지화된 설정 창. - **[OpenLogi agent](../crates/openlogi-agent)** — 입력 훅과 모든 기기 I/O를 소유하는 백그라운드 서비스. GUI는 순수 IPC 클라이언트이며 필요할 때 agent를 시작합니다. - **[OpenLogi CLI](../crates/openlogi-cli)** — 헤드리스 기기 목록(`list`), 에셋 동기화, 기기 진단 하위 명령을 갖춘 CLI. 모든 것이 로컬에서 이루어집니다: 바인딩은 순수 TOML 파일에 저장되고, agent가 OS 입력 훅으로 버튼 입력을 리매핑하며 DPI, SmartShift, 스크롤, 조명 변경을 HID++를 통해 기기에 직접 기록합니다. macOS, Linux, Windows를 지원합니다. Windows는 가장 최근에 포팅된 플랫폼으로 Windows 11 실제 하드웨어에서 엔드투엔드 검증을 마쳤지만, macOS와 Linux 빌드보다 다듬어지지 않은 부분이 더 있을 수 있습니다. [로드맵](#로드맵)을 참고하세요. ## Options+ 그 너머 OpenLogi는 되고 Options+는 안 되는 것들: - **Linux에서 실행.** Options+는 macOS와 Windows 전용입니다. OpenLogi는 Linux를 일급 플랫폼으로 다룹니다: evdev/uinput 훅, udev 규칙, systemd 사용자 유닛, `.deb` / `.rpm` / `.pkg.tar.zst` 패키지. - **제스처 버튼 이동.** 어떤 물리 버튼이 제스처 역할을 맡을지 — 전용 제스처 버튼, 가운데, 뒤로, 앞으로 — 직접 고를 수 있고, 방향별 스와이프 바인딩을 설정하거나 제스처를 아예 끌 수도 있습니다. Options+는 제스처 역할을 전용 제스처 버튼에 고정합니다. - **순수 텍스트 설정.** 모든 설정이 TOML 파일 하나에 들어 있어 읽고, diff하고, 버전 관리하고, 다른 기기로 복사할 수 있습니다. - **스크립트 가능.** 진짜 CLI: 기기 목록, 에셋 프리페치, 기기 내 HID++ 진단(피처 / 컨트롤 덤프, DPI / SmartShift 왕복 검사, 키보드 조명 검사). - **가볍게 유지.** 네이티브 Rust + GPUI 바이너리 — Electron 스위트도, 상주 업데이터도, 계정도, 텔레메트리도 없습니다. ## 로드맵 | 기능 | 상태 | |---|---| | Bolt 수신기 탐색 + 페어링된 기기 목록(CLI + GUI) | ✅ | | Unifying 수신기(Bolt로 대체된 구형 프로토콜) | ✅ | | Bluetooth 직접 연결 / 유선 기기(수신기 없음) | ✅ | | 배터리 잔량 / 충전 상태 | ✅ (온라인 기기) | | 인터랙티브 GUI: 캐러셀, 마우스 다이어그램, 액션 선택기 | ✅ macOS + Linux + Windows | | OS 입력 훅을 통한 버튼 리매핑 | ✅ macOS + Linux + Windows | | 내장 액션 카탈로그 + 사용자 지정 키보드 단축키(TOML 작성) | ✅ macOS + Linux + Windows¹ | | DPI 제어 + 프리셋 + 사이클 / 프리셋 지정 액션(HID++ `0x2201`) | ✅ | | SmartShift 휠: 모드 전환 + 감도 + 영구 래칫 패널(HID++ `0x2111`) | ✅ | | 기기별 네이티브 스크롤 반전(HID++ `0x2121`) | ✅ (지원 기기) | | 정적 RGB 키보드 조명(HID++ `0x8070` / `0x8080`) | ✅ (지원 기기) | | 앱별 프로필 오버레이(앱 포커스 시 자동 전환) | ✅ macOS + Windows, 🟡 Linux (X11 / XWayland 전용) | | 설정 창: 로그인 시 실행, 업데이트, 권한, 언어, 외관 | ✅ macOS + Linux + Windows | | Agent 상태 아이콘 | ✅ macOS 메뉴 막대 + Windows 트레이; Linux에는 해당 없음 | | 인터페이스 현지화(20개 언어: da, de, el, en, es, fi, fr, it, ja, ko, nb, nl, pl, pt-BR, pt-PT, ru, sv, zh-CN, zh-HK, zh-TW) | ✅ | | Linux 패키징: udev 규칙, systemd 유닛, `.deb` / `.rpm` / `.pkg.tar.zst` | ✅ Linux | | 제스처 버튼 방향별 바인딩 + 실시간 캡처 | ✅ (기기 기능에 따라 다름) | | 가운데 / 모드 시프트 / 썸휠 버튼 캡처 | ✅ 가운데 버튼은 모든 플랫폼; 모드 시프트 / 썸휠은 기기 기능에 따라 다름 | | Windows(agent, GUI, 이벤트 훅, 설치 프로그램) | ✅ Windows 11 실제 하드웨어 검증 완료; 최신 포트로 호환성을 계속 개선 중 | ¹ Linux의 미디어 키 액션은 D-Bus MPRIS를 사용합니다. 일부 macOS 전용 액션은 Linux에 범용 대응 기능이 없어 아무 동작도 하지 않습니다. Windows는 가능한 경우 플랫폼 액션을 네이티브 기능에 매핑합니다. ## 설치 > [!IMPORTANT] > 먼저 **Logi Options+** 를 종료하세요 — 두 애플리케이션은 HID++ 접근을 두고 경합하며, 하나의 수신기는 한쪽만 소유할 수 있습니다. ### macOS macOS 13 이상이 필요합니다. [최신 릴리스](https://github.com/AprilNEA/OpenLogi/releases/latest)에서 서명·공증된 `.dmg`를 내려받아 `OpenLogi.app`을 `/Applications`로 드래그하세요. 또는 [Homebrew](https://brew.sh)로 설치: ```sh brew install --cask openlogi ``` 공식 Homebrew cask가 기본 설치 경로입니다. 대신 `aprilnea/tap`으로 GitHub 최신 릴리스를 명시적으로 따라가려면: ```sh brew tap aprilnea/tap brew install --cask aprilnea/tap/openlogi@latest ``` `openlogi@latest`는 OpenLogi 릴리스 워크플로가 관리하며 공식 cask의 autobump보다 먼저 갱신될 수 있습니다. `openlogi`와 `openlogi@latest` 중 하나만 설치하세요. ### Linux [최신 릴리스](https://github.com/AprilNEA/OpenLogi/releases/latest)에서 `.deb` 또는 `.rpm`을 내려받으세요: ```sh # Debian / Ubuntu sudo dpkg -i openlogi_*.deb # Fedora / RHEL sudo rpm -i openlogi-*.rpm # Arch Linux sudo pacman -U openlogi-*.pkg.tar.zst ``` 패키지는 `x86_64`/`amd64`와 `arm64`/`aarch64` 두 아키텍처로 제공됩니다. 패키지는 `sudo` 없이 `/dev/hidraw*`와 `/dev/uinput`에 접근할 수 있게 해 주는 udev 규칙을 설치합니다. 설치 후 사용자용 백그라운드 에이전트를 활성화하세요: ```sh systemctl --user enable --now openlogi-agent.service ``` 수동 / 소스 설치와 systemd가 없는 배포판은 [INSTALL-linux.md](INSTALL-linux.md)를 참고하세요. ### Windows 각 릴리스에는 서명된 휴대용 `.zip` 아카이브와 사용자별 `.msi` 설치 파일(x86_64 및 arm64)이 포함됩니다. 둘 다 GUI(`OpenLogi.exe`)와 모든 기기 I/O를 소유하는 백그라운드 agent(`openlogi-agent.exe`)를 함께 제공합니다. 휴대용 zip을 사용할 때 두 파일을 같은 위치에 두지 않으면 GUI가 연결할 대상이 없습니다. Windows 지원은 정상 작동하며 유선 키보드와 Unifying 수신기 마우스를 사용해 MSI 설치, 인플레이스 업그레이드, 제거까지 Windows 11 실제 하드웨어에서 엔드투엔드 검증했습니다. macOS 포트보다 최신이므로 문제가 있으면 [제보](https://github.com/AprilNEA/OpenLogi/issues)해 주세요. agent는 시스템 트레이 아이콘(메인 창 표시 / 종료)을 표시하므로 메인 창을 닫은 뒤에도 앱에 접근할 수 있습니다. Windows에서 비활성화하려면 TOML `[app_settings]` 블록에 `show_in_menu_bar = false`를 설정하고 agent를 다시 시작하세요. GUI 토글은 현재 macOS 전용입니다. 소스에서 빌드하려면 [DEVELOPMENT.md](DEVELOPMENT.md)를 참고하세요. ## 사용법 (CLI) [USAGE.md](USAGE.md) 참고 ## 설정 [CONFIGURATION.md](CONFIGURATION.md) 참고 ## 개발 [DEVELOPMENT.md](DEVELOPMENT.md) 참고 ## 감사의 말 - **Windows·카메라·i18n** — [@davidbudnick](https://github.com/davidbudnick): Windows 입력 훅과 MSI 업데이트, Logitech 웹캠 지원, 키보드 RGB, Crowdin 번역 파이프라인 - **Linux 포팅** — [@cserby](https://github.com/cserby): evdev/uinput 훅, D-Bus 액션, .deb/.rpm 패키징 - [Solaar](https://github.com/pwr-Solaar/Solaar) — [@pwr](https://github.com/pwr)가 만든, 가장 완성도 높은 오픈소스 HID++ 구현이자 이 프로젝트의 프로토콜 참고 자료 - [Mouser](https://github.com/TomBadash/Mouser) — [@TomBadash](https://github.com/TomBadash)가 만든, 같은 목표의 선행 프로젝트: 로컬에서 동작하는 계정 없는 Options+ 대체제 ## 라이선스 다음 중 하나를 선택해 사용할 수 있습니다: - Apache License 2.0 ([LICENSE-APACHE](../LICENSE-APACHE)) - MIT 라이선스 ([LICENSE-MIT](../LICENSE-MIT)) ### 서드파티 코드 `crates/openlogi-hidpp`는 [`hidpp`](https://crates.io/crates/hidpp)([@lus](https://github.com/lus) 제작)의 vendored fork이며, 0BSD 라이선스를 따릅니다. ### 로고 및 브랜드 자산 OpenLogi 로고와 앱 아이콘 — [`design/`](../design/) 아래의 브랜드 자산 — 은 © 2026 AprilNEA가 모든 권리를 보유하며, 위 MIT/Apache 라이선스의 적용을 받지 않습니다. [`design/LICENSE`](../design/LICENSE)를 참고하세요. 코드를 포크해도 OpenLogi 이름·로고·아이콘에 대한 권리는 부여되지 않습니다. 사전 서면 허가 없이 자신의 프로젝트, 포크, 배포판을 나타내는 데 사용하지 마세요. --- **Logitech과 무관합니다.** "Logitech", "MX Master", "Options+"는 Logitech International S.A.의 상표입니다. ## 저장소 활동 --- ### README.Zh CN > [!WARNING] > **OpenLogi 仍在积极开发中**,尚未稳定 —— 功能与配置仍可能变动。点个 **Star** ⭐ 并 **Watch** 👀 本仓库,在新版本发布时获得通知。

English | 简体中文 | 日本語 | Deutsch | Français | 한국어

OpenLogi

OpenLogi

⚡️ 原生、本地优先的 Logitech Options+ 替代品,用 Rust 编写 🦀
通过 HID++ 重映射按键、调节 DPI 与 SmartShift。无账号、无遥测。

AprilNEA%2FOpenLogi | Trendshift

> **被 Options+ 折腾够了?试试 OpenLogi。** 无需 Logitech 账号、无遥测、无需安装官方 Options+,即可重映射按键、调节 DPI 与 SmartShift、按应用自动切换配置。没有云端,配置是纯 TOML 文件。默认情况下,唯一的自动联网行为是获取设备图片;只有在你主动请求或选择启用时,才会检查并下载更新。 --- ## 这是什么 OpenLogi 通过 Logi Bolt 和 Unifying 接收器、蓝牙直连或 USB 线缆与 Logitech HID++ 外设通信,完全不需要运行 Logi Options+。它由三个组件组成: - **[OpenLogi GUI](../crates/openlogi-gui)** —— 基于 GPUI 的桌面应用:可点击热区的交互式鼠标示意图、逐按键动作选择器(内置动作 + 在 TOML 配置中编写的自定义快捷键)、DPI 预设、SmartShift、按设备原生滚动反转、RGB 键盘灯光、按应用的配置叠加层、实时设备轮播,以及界面已本地化为 20 种语言的设置窗口。 - **[OpenLogi agent](../crates/openlogi-agent)** —— 拥有输入钩子和全部设备 I/O 的后台服务。GUI 是纯 IPC 客户端,并在需要时启动 agent。 - **[OpenLogi CLI](../crates/openlogi-cli)** —— 命令行工具:无界面设备清单(`list`)、资产同步与设备诊断子命令。 一切都在本地完成:绑定保存在纯 TOML 文件中,agent 通过操作系统输入钩子重映射按键,并经由 HID++ 将 DPI、SmartShift、滚动和灯光修改直接写入设备。 支持 macOS、Linux 和 Windows。Windows 是最新移植的平台:已在 Windows 11 实机上完成端到端验证,但可能仍比 macOS 和 Linux 版本更显粗糙;详见[路线图](#路线图)。 ## 超越 Options+ OpenLogi 能做、而 Options+ 做不到的事: - **跑在 Linux 上。** Options+ 只有 macOS 和 Windows 版本。OpenLogi 把 Linux 当作一等公民:evdev/uinput 钩子、udev 规则、systemd 用户单元,以及 `.deb` / `.rpm` / `.pkg.tar.zst` 安装包。 - **切换手势键。** 自由指定哪个物理按键承担手势角色 —— 专用手势键、中键、后退或前进键 —— 支持按方向绑定滑动动作,也可以彻底关闭手势。Options+ 则把手势固定在专用手势键上。 - **纯文本配置。** 全部设置就是一个 TOML 文件,可读、可 diff、可纳入版本管理、可在多台机器间复制。 - **可脚本化。** 真正的 CLI:设备清单、资产预取、设备端 HID++ 诊断(特性 / 控制转储、DPI / SmartShift 往返自检和键盘灯光检查)。 - **保持轻量。** 原生 Rust + GPUI 二进制 —— 没有 Electron 全家桶、没有常驻更新器、无账号、无遥测。 ## 路线图 | 能力 | 状态 | |---|---| | 发现 Bolt 接收器 + 列出已配对设备(CLI + GUI) | ✅ | | Unifying 接收器(更早的协议,已被 Bolt 取代) | ✅ | | 蓝牙直连 / 有线设备(无接收器) | ✅ | | 电池电量 / 充电状态 | ✅(在线设备) | | 交互式 GUI:轮播、鼠标示意图、动作选择器 | ✅ macOS + Linux + Windows | | 经由 OS 输入钩子的按键重映射 | ✅ macOS + Linux + Windows | | 内置动作目录 + 自定义键盘快捷键(TOML 编写) | ✅ macOS + Linux + Windows¹ | | DPI 控制 + 预设 + 循环 / 按预设设置动作(HID++ `0x2201`) | ✅ | | SmartShift 滚轮:模式切换 + 灵敏度 + 永久棘轮面板(HID++ `0x2111`) | ✅ | | 按设备原生滚动反转(HID++ `0x2121`) | ✅(受支持设备) | | 静态 RGB 键盘灯光(HID++ `0x8070` / `0x8080`) | ✅(受支持设备) | | 按应用的配置叠加层(应用获得焦点时自动切换) | ✅ macOS + Windows,🟡 Linux(仅 X11 / XWayland) | | 设置窗口:登录时启动、更新、权限、语言、外观 | ✅ macOS + Linux + Windows | | Agent 状态图标 | ✅ macOS 菜单栏 + Windows 系统托盘;不适用于 Linux | | 界面本地化(20 种语言:da、de、el、en、es、fi、fr、it、ja、ko、nb、nl、pl、pt-BR、pt-PT、ru、sv、zh-CN、zh-HK、zh-TW) | ✅ | | Linux 打包:udev 规则、systemd 单元、`.deb` / `.rpm` / `.pkg.tar.zst` | ✅ Linux | | 手势键按方向绑定 + 实时捕获 | ✅(取决于设备能力) | | 中键 / 模式切换键 / 拇指滚轮按键捕获 | ✅ 所有平台均支持中键;模式切换键 / 拇指滚轮取决于设备能力 | | Windows(agent、GUI、事件钩子、安装程序) | ✅ 已在 Windows 11 实机验证;较新的移植版本,兼容性仍在持续打磨 | ¹ Linux 上媒体键动作走 D-Bus MPRIS;少数 macOS 专属动作在 Linux 上没有通用对应功能,因此为空操作。Windows 会在可用时将平台动作映射到原生对应功能。 ## 安装 > [!IMPORTANT] > 请先退出 **Logi Options+** —— 两者会争夺 HID++ 访问权,同一个接收器同时只能由一方持有。 ### macOS 需要 macOS 13 或更高版本。 从[最新 release](https://github.com/AprilNEA/OpenLogi/releases/latest) 下载已签名、已公证的 `.dmg`,把 `OpenLogi.app` 拖入 `/Applications`。 或通过 [Homebrew](https://brew.sh) 安装: ```sh brew install --cask openlogi ``` 官方 Homebrew cask 是默认安装途径。如需改用 `aprilnea/tap` 显式跟踪 GitHub 最新 release: ```sh brew tap aprilnea/tap brew install --cask aprilnea/tap/openlogi@latest ``` `openlogi@latest` 由 OpenLogi 的发布工作流维护,可能比官方 cask 的自动更新先一步。`openlogi` 和 `openlogi@latest` 二选一安装,不要同时装。 ### Linux 从[最新 release](https://github.com/AprilNEA/OpenLogi/releases/latest) 下载适用于你的发行版的安装包: ```sh # Debian / Ubuntu sudo dpkg -i openlogi_*.deb # Fedora / RHEL sudo rpm -i openlogi-*.rpm # Arch Linux sudo pacman -U openlogi-*.pkg.tar.zst ``` 安装包同时提供 `x86_64`/`amd64` 与 `arm64`/`aarch64` 两种架构。 安装包会写入 udev 规则,让你的用户无需 `sudo` 即可访问 `/dev/hidraw*` 和 `/dev/uinput`。装完后为当前用户启用后台 agent: ```sh systemctl --user enable --now openlogi-agent.service ``` 手动 / 源码安装以及无 systemd 的发行版,见 [INSTALL-linux.md](INSTALL-linux.md)。 ### Windows 每个 release 都附带签名的便携式 `.zip` 压缩包和按用户安装的 `.msi` 安装程序(x86_64 与 arm64)。两者均同时包含 GUI(`OpenLogi.exe`)和拥有全部设备 I/O 的后台 agent(`openlogi-agent.exe`)。使用便携式 zip 时,请把这两个文件放在同一目录,否则 GUI 将无法连接。 Windows 支持可正常工作,并已在 Windows 11 实机上完成端到端验证:包括有线键盘、使用 Unifying 接收器的鼠标,以及 MSI 的安装、原位升级和卸载。它比 macOS 版本更新,如遇到粗糙之处,请[反馈问题](https://github.com/AprilNEA/OpenLogi/issues)。agent 会显示系统托盘图标(「显示主窗口」/「退出」),因此关闭主窗口后仍可打开应用。如需在 Windows 上禁用该图标,请在 TOML 的 `[app_settings]` 块中设置 `show_in_menu_bar = false`,然后重启 agent;GUI 开关目前仅适用于 macOS。 从源码构建见 [DEVELOPMENT.md](DEVELOPMENT.md)。 ## 使用(CLI) 见 [USAGE.md](USAGE.md) ## 配置 见 [CONFIGURATION.md](CONFIGURATION.md) ## 开发 见 [DEVELOPMENT.md](DEVELOPMENT.md) ## 致谢 - **Windows、摄像头与 i18n**:[@davidbudnick](https://github.com/davidbudnick) —— Windows 输入钩子与 MSI 更新、Logitech 摄像头支持、键盘 RGB、Crowdin 翻译流水线 - **Linux 移植**:[@cserby](https://github.com/cserby) —— evdev/uinput 钩子、D-Bus 动作、.deb/.rpm 打包 - [Solaar](https://github.com/pwr-Solaar/Solaar),作者 [@pwr](https://github.com/pwr) —— 目前最完整的开源 HID++ 实现,也是本项目的协议参考 - [Mouser](https://github.com/TomBadash/Mouser),作者 [@TomBadash](https://github.com/TomBadash) —— 同一目标的先行项目:本地、无需账号的 Options+ 替代品 ## 许可证 以下两种许可证任选其一: - Apache License 2.0([LICENSE-APACHE](../LICENSE-APACHE)) - MIT 许可证([LICENSE-MIT](../LICENSE-MIT)) ### 第三方代码 `crates/openlogi-hidpp` 是 [`hidpp`](https://crates.io/crates/hidpp)(作者 [@lus](https://github.com/lus))的 vendored fork,采用 0BSD 许可证。 ### Logo 与品牌资产 OpenLogi 的 Logo 与应用图标 —— 即 [`design/`](../design/) 下的品牌资产 —— © 2026 AprilNEA 保留所有权利,不在上述 MIT/Apache 许可范围内;见 [`design/LICENSE`](../design/LICENSE)。Fork 代码并不授予 OpenLogi 名称、Logo 或图标的使用权;未经事先书面许可,请勿用它们代表你自己的项目、Fork 或分发版本。 --- **与 Logitech 无关联。** 「Logitech」、「MX Master」与「Options+」是 Logitech International S.A. 的商标。 ## 仓库活跃度 --- ### SECURITY # Security Policy ## Supported Versions OpenLogi is under active development and has not reached a stable 1.0 release. Security fixes are provided for the latest public release and the current development branch. | Version | Supported | | ------- | --------- | | Latest release | :white_check_mark: | | `master` | Best effort | | Older releases | :x: | If you are using an older release, please upgrade before reporting an issue unless the vulnerability is still present in the latest release or on `master`. ## Reporting a Vulnerability Please report suspected vulnerabilities privately by emailing: `security@openlogi.org` Do not open a public GitHub issue for a suspected vulnerability. Useful reports include: - A short description of the issue and its impact. - Steps to reproduce, proof-of-concept code, or affected configuration. - The OpenLogi version or commit, operating system version, device model, and connection type. - Relevant logs or screenshots with private data removed. - Whether the issue is already public or shared with anyone else. Examples of issues that should be reported privately include: - Arbitrary code execution, privilege escalation, or sandbox bypasses. - Unsafe handling of configuration, profile, update, or asset data. - Leaks of private configuration, logs, device identifiers, or user activity. - Security-sensitive behavior in the event hook, IPC, updater, packaging, or device communication paths. ## Response Expectations The maintainers aim to acknowledge new vulnerability reports within 7 days. After triage, we will let you know whether the report is accepted, needs more information, or is out of scope. For accepted reports, we will coordinate a fix and disclosure timeline with the reporter. We aim to provide status updates at least every 14 days while the issue is being investigated or fixed. If a report is declined, we will explain the reason when practical. ## Disclosure Please give the maintainers a reasonable opportunity to investigate and release a fix before publicly disclosing a vulnerability. Once a fix is available, we may publish a security advisory, release notes, or upgrade guidance depending on the severity and user impact. OpenLogi does not currently operate a paid bug bounty program. --- ### USAGE # Usage (CLI) The `openlogi` command-line tool. For install and configuration, see the [README](../README.md). ```sh openlogi list # paired devices: slot, codename, kind, online, battery openlogi assets sync # pre-fetch device renders from the fastest available mirror openlogi diag features # dump every HID++ feature the active device reports openlogi diag controls # dump reprogrammable controls and capability flags openlogi diag dpi # read → write → read-back → restore DPI (smoke test) openlogi diag smartshift # toggle SmartShift and restore (smoke test) openlogi diag lighting ff0000 # solid colour for a wired RGB keyboard (any RRGGBB hex) ``` Running `openlogi` with no subcommand defaults to `list`. Set `OPENLOGI_LOG=debug` for verbose tracing in the CLI, GUI, or agent. Asset synchronization probes `assets.openlogi.org`, the versioned Cloudflare Pages release alias, and the pinned jsDelivr npm release concurrently. The first mirror with a valid catalog supplies every file for that synchronization run. Set `OPENLOGI_ASSETS` or pass `openlogi assets sync --base ` to use one uniform asset origin instead of automatic mirror selection. --- ### CHANGELOG # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.6.27] - 2026-08-13 ### Added - *(gui)* dismiss the Actions Ring on a click outside it ([#591](https://github.com/AprilNEA/OpenLogi/pull/591)) - *(agent)* pressing the ring trigger again dismisses the Actions Ring ([#592](https://github.com/AprilNEA/OpenLogi/pull/592)) - *(agent)* add a hardware-free mock agent for GUI development ([#568](https://github.com/AprilNEA/OpenLogi/pull/568)) - per-slot custom labels for the Actions Ring ([#584](https://github.com/AprilNEA/OpenLogi/pull/584)) - *(core)* support stable Windows app selectors ([#572](https://github.com/AprilNEA/OpenLogi/pull/572)) - *(gui)* add capability-driven actions ring ([#528](https://github.com/AprilNEA/OpenLogi/pull/528)) - *(hid)* persist the immutable probe cache across restarts ([#564](https://github.com/AprilNEA/OpenLogi/pull/564)) - *(gui)* back navigation via the mouse's back button and Alt+Left ([#563](https://github.com/AprilNEA/OpenLogi/pull/563)) - capture the MX Master 4 haptic panel as a first-class control ([#565](https://github.com/AprilNEA/OpenLogi/pull/565)) - *(hid)* recognise Lightspeed receiver 046d:c547 (G915, G502 X) ([#574](https://github.com/AprilNEA/OpenLogi/pull/574)) - *(hid,hidpp)* read battery over BatteryVoltage (0x1001) ([#575](https://github.com/AprilNEA/OpenLogi/pull/575)) ### Fixed - *(gui)* open the Actions Ring on the display containing the cursor ([#588](https://github.com/AprilNEA/OpenLogi/pull/588)) - *(hid)* detect and recover dead-delivery HID channels ([#589](https://github.com/AprilNEA/OpenLogi/pull/589)) - Actions Ring haptic reliability — coalescing, feature cache, firmware arming, deadlock guards ([#590](https://github.com/AprilNEA/OpenLogi/pull/590)) - *(agent)* implement the Actions Ring IPC surface in the mock agent ([#587](https://github.com/AprilNEA/OpenLogi/pull/587)) - *(gui)* redraw the Actions Ring on hover changes ([#585](https://github.com/AprilNEA/OpenLogi/pull/585)) - *(hid)* widen the Bolt per-slot probe budget for high-latency USB paths ([#562](https://github.com/AprilNEA/OpenLogi/pull/562)) - *(macos)* prevent corrupted small app icons ([#570](https://github.com/AprilNEA/OpenLogi/pull/570)) - *(hook)* release macOS tap after accessibility revocation ([#578](https://github.com/AprilNEA/OpenLogi/pull/578)) - *(ui)* prevent middle and thumb wheel popover flicker ([#559](https://github.com/AprilNEA/OpenLogi/pull/559)) ## [0.6.26] - 2026-08-10 ### Fixed - *(macos)* add camera hardened-runtime entitlement ([#557](https://github.com/AprilNEA/OpenLogi/pull/557)) ## [0.6.25] - 2026-08-10 ### Added - per-device capture with plan-driven sessions ([#419](https://github.com/AprilNEA/OpenLogi/pull/419)) - *(hid)* recognise Lightspeed nano receivers (G-series, e.g. G305) ([#388](https://github.com/AprilNEA/OpenLogi/pull/388)) - add MX Master 2S (3S) thumb wheel bindings ([#525](https://github.com/AprilNEA/OpenLogi/pull/525)) ### Fixed - *(i18n)* add camera permission locale keys ([#554](https://github.com/AprilNEA/OpenLogi/pull/554)) - *(hook)* capture keyboard events on windows ([#548](https://github.com/AprilNEA/OpenLogi/pull/548)) - *(gui,camera,xtask)* make Camera permission grantable on macOS ([#550](https://github.com/AprilNEA/OpenLogi/pull/550)) - *(ci)* merge Crowdin downloads into locale catalogs ([#553](https://github.com/AprilNEA/OpenLogi/pull/553)) - *(i18n)* skip Crowdin English fill-in and restore locale parity ([#551](https://github.com/AprilNEA/OpenLogi/pull/551)) - *(hidpp)* retry lost feature-table reads during enumeration ([#469](https://github.com/AprilNEA/OpenLogi/pull/469)) - *(gui,assets)* fit the Keys tab to legacy keyboard assets (G513) ([#544](https://github.com/AprilNEA/OpenLogi/pull/544)) ## [0.6.24] - 2026-08-10 ### Added - *(hid)* recognize Lightspeed receiver (046d:c539) as Unifying-compatible ([#510](https://github.com/AprilNEA/OpenLogi/pull/510)) - add function key remapper ([#344](https://github.com/AprilNEA/OpenLogi/pull/344)) - *(hid)* add standalone Litra light support ([#513](https://github.com/AprilNEA/OpenLogi/pull/513)) - keyboard F-row key remapping and fn-lock over HID++ ([#395](https://github.com/AprilNEA/OpenLogi/pull/395)) - *(hook)* Wayland frontmost-window backends (wlroots + GNOME Shell) ([#191](https://github.com/AprilNEA/OpenLogi/pull/191)) - *(camera)* add Logitech webcam support ([#531](https://github.com/AprilNEA/OpenLogi/pull/531)) - *(backlight)* support HID++ 0x1982 ([#470](https://github.com/AprilNEA/OpenLogi/pull/470)) - *(battery)* support legacy 0x1000 BatteryStatus and its charging quirk ([#312](https://github.com/AprilNEA/OpenLogi/pull/312)) ### Fixed - *(agent-core)* retry volatile DPI re-apply on cold boot ([#449](https://github.com/AprilNEA/OpenLogi/pull/449)) - *(agent)* prefer online device for input capture ([#453](https://github.com/AprilNEA/OpenLogi/pull/453)) - *(hidpp)* keep events when a field carries an unknown enum value ([#432](https://github.com/AprilNEA/OpenLogi/pull/432)) - *(agent)* rearm control capture after device reconnect ([#450](https://github.com/AprilNEA/OpenLogi/pull/450)) - *(linux)* grant uaccess on Logitech input event nodes ([#530](https://github.com/AprilNEA/OpenLogi/pull/530)) - *(agent)* reapply volatile settings after macOS resume ([#506](https://github.com/AprilNEA/OpenLogi/pull/506)) - *(hook)* never wedge system pointer input ([#534](https://github.com/AprilNEA/OpenLogi/pull/534)) - *(agent)* route hardware operations through inventory channels ([#532](https://github.com/AprilNEA/OpenLogi/pull/532)) - *(agent)* reuse inventory channels for input capture ([#522](https://github.com/AprilNEA/OpenLogi/pull/522)) - *(i18n)* complete Crowdin synchronization ([#508](https://github.com/AprilNEA/OpenLogi/pull/508)) ## [0.6.23](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.22...openlogi-core-v0.6.23) - 2026-08-02 ### Fixed - *(hook)* grab only relative pointer devices, never touchpads or pointing sticks ([#401](https://github.com/AprilNEA/OpenLogi/pull/401)) ## [0.6.22](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.21...openlogi-core-v0.6.22) - 2026-07-21 ### Added - *(gui)* add asset source selector ### Fixed - *(gui)* label the official asset source as OpenLogi ### Other - *(core)* describe selected asset source ## [0.6.21](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.20...openlogi-core-v0.6.21) - 2026-07-19 ### Added - *(hid)* add native wheel resolution control ### Fixed - *(hid)* make one-shot enumerate retry transport-agnostic so Unifying partial drains recover ([#287](https://github.com/AprilNEA/OpenLogi/pull/287)) ### Other - *(core)* add hires_wheel to the inventory equality test helper ([#417](https://github.com/AprilNEA/OpenLogi/pull/417)) ## [0.6.20](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.19...openlogi-core-v0.6.20) - 2026-07-18 ### Fixed - *(core)* preserve hash-prefixed lighting colors - *(core,gui,agent,cli)* validate the lighting color once as a typed Rgb - *(smartshift)* stop runaway free-spin scroll and control snap-back ([#333](https://github.com/AprilNEA/OpenLogi/pull/333)) ### Other - *(core)* document the remaining public items and deny missing_docs - *(core)* move the swipe-gesture machinery to binding/swipe.rs - *(core)* persist the config through atomic-write-file - *(agent,core)* resolve the LaunchAgents path via core paths - replace assert!(matches!(…)) with std assert_matches - *(core)* split config.rs into settings and device submodules - *(core)* drop fs4 in favor of std File::try_lock ## [0.6.19](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.18...openlogi-core-v0.6.19) - 2026-07-04 ### Added - *(windows)* notification-area tray icon for the agent ([#347](https://github.com/AprilNEA/OpenLogi/pull/347)) - *(windows)* bundle and package the background agent ([#347](https://github.com/AprilNEA/OpenLogi/pull/347)) ## [0.6.18](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.17...openlogi-core-v0.6.18) - 2026-06-29 ### Added - *(hidpp)* add typed reprog controls support ### Other - Clarify MX Master 4 gesture control semantics ([#325](https://github.com/AprilNEA/OpenLogi/pull/325)) ## [0.6.17](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.16...openlogi-core-v0.6.17) - 2026-06-24 ### Added - add Capture Region to Clipboard button action ([#296](https://github.com/AprilNEA/OpenLogi/pull/296)) ## [0.6.16](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.15...openlogi-core-v0.6.16) - 2026-06-22 ### Fixed - *(scroll)* support per-device inversion ### Other - *(scroll)* require native hidpp inversion - *(config)* key settings by physical device - *(infra)* use crates for paths and language matching ## [0.6.15](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.14...openlogi-core-v0.6.15) - 2026-06-21 ### Added - *(scroll)* per-device inverted scrolling ([#126](https://github.com/AprilNEA/OpenLogi/pull/126)) ## [0.6.14](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.13...openlogi-core-v0.6.14) - 2026-06-15 ### Fixed - *(hid)* solid keyboard colour via 0x8070 effect ([#205](https://github.com/AprilNEA/OpenLogi/pull/205)) ### Other - *(core)* extract the OS input-injection layer into openlogi-inject ([#240](https://github.com/AprilNEA/OpenLogi/pull/240)) ## [0.6.13](https://github.com/AprilNEA/OpenLogi/compare/openlogi-hidpp-v0.6.12...openlogi-hidpp-v0.6.13) - 2026-06-15 ### Other - *(hidpp)* address review — multi-impl macro arm + 4-bit function guard - *(hidpp)* fold per-feature request framing into FeatureEndpoint - *(hidpp)* express the feature registry as a data macro ## [0.6.12](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.11...openlogi-core-v0.6.12) - 2026-06-13 ### Fixed - *(gui)* keep asleep devices and their panels in the device list - *(agent)* persist DPI/SmartShift per device and reapply volatile settings on reconnect ## [0.6.11](https://github.com/AprilNEA/OpenLogi/compare/openlogi-hid-v0.6.10...openlogi-hid-v0.6.11) - 2026-06-13 ### Fixed - *(hid)* replay a node's last inventory through transient probe failures ([#222](https://github.com/AprilNEA/OpenLogi/pull/222)) ## [0.6.10](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.9...openlogi-core-v0.6.10) - 2026-06-13 ### Added - *(config)* add auto_download_assets app setting ### Fixed - *(gui)* keep the diagnostics report truthful across agent restarts ([#230](https://github.com/AprilNEA/OpenLogi/pull/230)) ## [0.6.9](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.8...openlogi-core-v0.6.9) - 2026-06-12 ### Added - *(gui)* add a Copy Diagnostics button to the About window ([#206](https://github.com/AprilNEA/OpenLogi/pull/206)) ## [0.6.8](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.7...openlogi-core-v0.6.8) - 2026-06-12 ### Added - *(linux)* launch_at_login + input device access permission check ([#172](https://github.com/AprilNEA/OpenLogi/pull/172)) - add mouse button 4 and 5 options ([#96](https://github.com/AprilNEA/OpenLogi/pull/96)) ## [0.6.7](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.6...openlogi-core-v0.6.7) - 2026-06-12 ### Fixed - *(core)* post macOS volume and media keys as NX system-defined events ([#184](https://github.com/AprilNEA/OpenLogi/pull/184)) ### Other - *(ipc)* pin the wire format with golden bytes and mark wire types ## [0.6.6](https://github.com/AprilNEA/OpenLogi/compare/openlogi-hidpp-v0.6.5...openlogi-hidpp-v0.6.6) - 2026-06-10 ### Fixed - *(hidpp)* bound device-controlled name lengths in Bolt parsing ([#200](https://github.com/AprilNEA/OpenLogi/pull/200)) ## [0.6.5](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.4...openlogi-core-v0.6.5) - 2026-06-10 ### Other - collapse nested ifs flagged by current stable clippy ([#197](https://github.com/AprilNEA/OpenLogi/pull/197)) ## [0.6.4](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.3...openlogi-core-v0.6.4) - 2026-06-10 ### Added - *(core)* complete the macOS->Windows CustomShortcut keycode map - *(windows)* native input + HID++ leaf support - *(openlogi-gui)* expand UI to 19 fully-translated locales ([#24](https://github.com/AprilNEA/OpenLogi/pull/24)) - *(gui)* glow keyboard card in lighting colour ([#185](https://github.com/AprilNEA/OpenLogi/pull/185)) ## [0.6.3](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.6.2...openlogi-core-v0.6.3) - 2026-06-09 ### Added - *(core)* unify button + gesture bindings into one Binding map ### Fixed - *(core)* harden gesture Binding defaults, migration, and projection ## [0.6.2](https://github.com/AprilNEA/OpenLogi/compare/v0.6.1...v0.6.2) - 2026-06-08 ### Added - *(i18n)* integrate Crowdin localization workflow ([#174](https://github.com/AprilNEA/OpenLogi/pull/174)) ### Other - switch release notes generation to Codex ([#177](https://github.com/AprilNEA/OpenLogi/pull/177)) - add code of conduct ## [0.6.1](https://github.com/AprilNEA/OpenLogi/compare/openlogi-cli-v0.6.0...openlogi-cli-v0.6.1) - 2026-06-08 ### Fixed - *(cli)* diag selects a device that exposes the feature under test ([#150](https://github.com/AprilNEA/OpenLogi/pull/150)) ## [0.6.0](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.5.3...openlogi-core-v0.6.0) - 2026-06-07 ### Added - *(agent)* tarpc IPC server backed by the orchestrator + device I/O - *(agent)* define tarpc IPC service contract + serde-derive wire types ### Fixed - *(agent)* give the agent its own single-instance lock ### Other - Merge origin/master into feat/agent-daemon-split ## [0.5.3](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.5.2...openlogi-core-v0.5.3) - 2026-06-06 ### Fixed - *(gui)* prefer asset-registry kind + harden device-kind classification ### Other - gate config panels on HID++ capabilities, not device kind ## [0.5.2](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.5.1...openlogi-core-v0.5.2) - 2026-06-05 ### Added - *(core)* LockScreen and media actions via D-Bus on Linux - *(core)* expose action_device_path for evtest attachment - *(core)* implement Action::execute on Linux via uinput - enable Thumb Wheel Up/Down mapping, "Do Nothing" action, and native scroll sensitivity ([#125](https://github.com/AprilNEA/OpenLogi/pull/125)) ### Fixed - *(core)* fmt + clarify mpris fallback log on the Linux D-Bus code - *(core)* address PR #124 review comments - *(core)* drop unused REL_X/REL_Y from the action uinput device - *(core)* cover Action::None in execute_linux - *(core)* address PR review comments - *(core)* use enumerate_dev_nodes_blocking for correct event path - *(core)* address code review findings ### Other - run clippy on Windows instead of bare cargo check ([#146](https://github.com/AprilNEA/OpenLogi/pull/146)) - *(core)* simplify D-Bus helpers and add -v flag to inject_action - *(core)* simplify inject_action parsing, guard --delay - *(core)* extract KEY_CAPABILITIES const, drop too_many_lines allow - *(core)* note LockScreen Linux limitation and D-Bus follow-up - *(core)* note Ctrl+Shift+Z vs Ctrl+Y redo shortcut choice on Linux - *(core)* clarify scroll unit difference between post_horizontal_scroll and HorizontalScroll* actions - *(core)* simplify Linux execute helpers and doc fixes - *(core)* add vk_mapping tests and inject_action example ## [0.5.1](https://github.com/AprilNEA/OpenLogi/compare/openlogi-assets-v0.5.0...openlogi-assets-v0.5.1) - 2026-06-05 ### Fixed - *(assets)* match devices against every model id a depot lists ### Other - *(assets)* lock the index.json modelIds schema contract ## [0.5.0](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.4.1...openlogi-core-v0.5.0) - 2026-06-05 ### Added - add wired G-series keyboard RGB control ([#29](https://github.com/AprilNEA/OpenLogi/pull/29)) ## [0.4.1](https://github.com/AprilNEA/OpenLogi/compare/openlogi-v0.4.0...openlogi-v0.4.1) - 2026-06-03 ### Added - *(gui)* refine device gallery worktree changes - *(nix)* wire passthru.updateScript for nix-update / autobump - *(nix)* add nixpkgs package + flake; commit the prebuilt app icon ### Other - route issue-chooser questions to GitHub Discussions - update Telegram invite link to the new channel - *(release)* disable homebrew-tap dispatch (openlogi moved to homebrew-cask) ([#105](https://github.com/AprilNEA/OpenLogi/pull/105)) - add GitHub issue form templates ([#102](https://github.com/AprilNEA/OpenLogi/pull/102)) - configure release-plz branch prefix ## [0.4.0](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.3.4...openlogi-core-v0.4.0) - 2026-06-02 ### Added - *(i18n)* add zh-TW (Traditional Chinese, Taiwan) locale ([#57](https://github.com/AprilNEA/OpenLogi/pull/57)) ## [0.3.4](https://github.com/AprilNEA/OpenLogi/compare/openlogi-hidpp-v0.3.3...openlogi-hidpp-v0.3.4) - 2026-06-01 ### Added - *(openlogi-hidpp)* vendor the hidpp 0.3 fork from lus/logy ### Fixed - address /code-review findings (write timeouts, scanning fallback, asset sync, CoreBluetooth safety) ### Other - *(hidpp)* up-convert short→long inside the channel for long-only BLE ## [0.3.3](https://github.com/AprilNEA/OpenLogi/compare/openlogi-assets-v0.3.2...openlogi-assets-v0.3.3) - 2026-06-01 ### Fixed - *(assets)* match devices by displayName when no PID lookup hits ## [0.3.2](https://github.com/AprilNEA/OpenLogi/compare/v0.3.1...v0.3.2) - 2026-06-01 ### Other - simplify format ## [0.3.1](https://github.com/AprilNEA/OpenLogi/compare/v0.3.0...v0.3.1) - 2026-06-01 ### Added - *(updater)* use static R2 manifest ([#43](https://github.com/AprilNEA/OpenLogi/pull/43)) ## [0.3.0](https://github.com/AprilNEA/OpenLogi/compare/openlogi-core-v0.2.0...openlogi-core-v0.3.0) - 2026-06-01 ### Added - *(openlogi-gui)* add Russian localization and language select ([#38](https://github.com/AprilNEA/OpenLogi/pull/38)) ### Fixed - *(gui)* stabilize device tab ordering ([#37](https://github.com/AprilNEA/OpenLogi/pull/37)) ## [0.2.0](https://github.com/AprilNEA/OpenLogi/compare/openlogi-hid-v0.1.4...openlogi-hid-v0.2.0) - 2026-05-31 ### Added - *(openlogi-hid)* route HID++ writes to directly-attached devices ([#5](https://github.com/AprilNEA/OpenLogi/pull/5)) ## [0.1.4](https://github.com/AprilNEA/OpenLogi/compare/v0.1.3...v0.1.4) - 2026-05-31 ### Other - update workflow actions for Node 24 - *(release-plz)* fail loudly when a release silently stalls ## [0.1.3](https://github.com/AprilNEA/OpenLogi/compare/v0.1.2...v0.1.3) - 2026-05-31 ### Added - macOS menu-bar (tray) app: lives in the menu bar with the interactive mouse diagram, a mappable gesture-button hotspot, and live Open / Quit - Dynamic Dock + menu-bar presence — full window with the app menu when open, tray-only once the window is closed; optional silent start-minimized on login - "Show in menu bar" setting to keep OpenLogi in the menu bar, or run it as an ordinary Dock app instead - ⌘W closes the focused window ### Fixed - Use the real Xcode toolchain for GUI builds and build the installer DMG correctly ## [0.1.2](https://github.com/AprilNEA/OpenLogi/compare/v0.1.1...v0.1.2) - 2026-05-31 ### Added - Check for Updates in the About window, backed by the gpui-updater crate - One opt-in update check on launch, with a first-run prompt to enable it - Live download progress, and a clickable version that links to its GitHub release ## [0.1.1](https://github.com/AprilNEA/OpenLogi/compare/v0.1.0...v0.1.1) - 2026-05-30 ### Other - *(release-plz)* write a single root changelog, not one per crate - *(release-plz)* load CARGO_REGISTRY_TOKEN from 1Password --- ### README > [!WARNING] > **OpenLogi is under active development** and not yet stable — features and config may still change. Give the repo a **Star** ⭐ and **Watch** 👀 it to get notified when a new release lands.

English | 简体中文 | 日本語 | Deutsch | Français | 한국어

OpenLogi

OpenLogi

⚡️ A native, local-first alternative to Logitech Options+, written in Rust 🦀
Remap buttons, DPI, and SmartShift over HID++. No account, no telemetry.

AprilNEA%2FOpenLogi | Trendshift

> **Options+ ? Try OpenLogi.** Remap buttons, drive DPI and SmartShift, and switch profiles per app — without a Logitech account, telemetry, or the official Options+ install. No cloud, plain TOML config. By default, device-image fetches are the only automatic network calls; update checks and downloads run only when you request or opt into them. --- ## What it is OpenLogi talks to Logitech HID++ peripherals over Logi Bolt and Unifying receivers, Bluetooth-direct connections, or USB cables — without running Logi Options+. It consists of three components: - **[OpenLogi GUI](crates/openlogi-gui)** — a GPUI desktop app: an interactive mouse diagram with clickable hotspots, a per-button action picker (built-in actions plus custom keyboard shortcuts authored in the TOML config), DPI presets, SmartShift, per-device scroll inversion, RGB keyboard lighting, per-application profiles, a live device carousel, and a Settings window localized into 20 languages. - **[OpenLogi agent](crates/openlogi-agent)** — the background service that owns the input hook and all device I/O. The GUI is a pure IPC client and starts the agent when needed. - **[OpenLogi CLI](crates/openlogi-cli)** — a CLI for headless inventory (`list`) plus asset-sync and on-device diagnostic subcommands. Everything stays local: bindings live in a plain TOML file, the agent remaps button presses through the OS input hook, and writes DPI, SmartShift, scroll, and lighting changes straight to the device over HID++. macOS, Linux, and Windows are supported. Windows is the newest port: it has been validated end-to-end on Windows 11 hardware, but may still have more rough edges than the macOS and Linux builds; see [Roadmap](#roadmap). ## Beyond Options+ Things OpenLogi does that Options+ won't: - **Run on Linux.** Options+ ships for macOS and Windows only. OpenLogi treats Linux as a first-class platform: evdev/uinput hook, udev rules, a systemd user unit, and `.deb` / `.rpm` / `.pkg.tar.zst` packages. - **Move the Gesture Button.** Pick which physical button owns the gesture role — the dedicated Gesture Button, middle, back, or forward — with per-direction swipe bindings, or turn gestures off entirely. Options+ pins the gesture role to the dedicated Gesture Button. - **Keep config in plain text.** Everything is one TOML file you can read, diff, version-control, and copy between machines. - **Script it.** A real CLI: device inventory, asset prefetch, and on-device HID++ diagnostics (feature/control dumps, DPI / SmartShift round-trips, and keyboard lighting checks). - **Stay light.** Native Rust + GPUI binaries — no Electron suite, no resident updaters, no account, no telemetry. ## Roadmap | Capability | State | |---|---| | Discover Bolt receivers + list paired devices (CLI + GUI) | ✅ | | Unifying receivers (older protocol, replaced by Bolt) | ✅ | | Bluetooth-direct / wired devices (no receiver) | ✅ | | Battery percentage / charge state | ✅ (online devices) | | Interactive GUI: carousel, mouse diagram, action picker | ✅ macOS + Linux + Windows | | Button remapping via the OS input hook | ✅ macOS + Linux + Windows | | Built-in action catalog + custom keyboard shortcuts (TOML-authored) | ✅ macOS + Linux + Windows¹ | | DPI control + presets + Cycle / Set-preset actions (HID++ `0x2201`) | ✅ | | SmartShift wheel: mode toggle + sensitivity + permanent-ratchet panel (HID++ `0x2111`) | ✅ | | Per-device native scroll inversion (HID++ `0x2121`) | ✅ (supported devices) | | Static RGB keyboard lighting (HID++ `0x8070` / `0x8080`) | ✅ (supported devices) | | Per-application profile overlays (auto-switch on app focus) | ✅ macOS + Windows, 🟡 Linux (X11 / XWayland only) | | Settings window: launch-at-login, updates, permissions, language, appearance | ✅ macOS + Linux + Windows | | Agent status icon | ✅ macOS menu bar + Windows tray; not applicable on Linux | | Interface localization (20 languages: da, de, el, en, es, fi, fr, it, ja, ko, nb, nl, pl, pt-BR, pt-PT, ru, sv, zh-CN, zh-HK, zh-TW) | ✅ | | Linux packaging: udev rules, systemd unit, `.deb` / `.rpm` / `.pkg.tar.zst` | ✅ Linux | | Gesture-button per-direction bindings + live capture | ✅ (device capability dependent) | | Middle / mode-shift / thumbwheel button capture | ✅ middle on all platforms; mode-shift / thumbwheel device dependent | | Windows (agent, GUI, event hook, installer) | ✅ Windows 11 hardware validated; newer port with ongoing compatibility polish | Help improve the interface translations on [Crowdin](https://crowdin.com/project/openlogi). ¹ Media key actions use D-Bus MPRIS on Linux; a handful of macOS-specific actions have no universal Linux equivalent and are no-ops. Windows maps platform actions to native equivalents where available. ## Install > [!IMPORTANT] > Quit **Logi Options+** first — the two applications fight over HID++ access and only one can own a given receiver at a time. ### macOS Requires macOS 13 or later. Download the signed, notarized `.dmg` from the [latest release](https://github.com/AprilNEA/OpenLogi/releases/latest) and drag `OpenLogi.app` to `/Applications`. Or install via [Homebrew](https://brew.sh): ```sh brew install --cask openlogi ``` The official Homebrew cask is the default installation path. To explicitly track the latest GitHub release from `aprilnea/tap` instead: ```sh brew tap aprilnea/tap brew install --cask aprilnea/tap/openlogi@latest ``` `openlogi@latest` is maintained by OpenLogi's release workflow and may update before the official cask autobump lands. Install either `openlogi` or `openlogi@latest`, not both. ### Linux Download the package for your distribution from the [latest release](https://github.com/AprilNEA/OpenLogi/releases/latest): ```sh # Debian / Ubuntu sudo dpkg -i openlogi_*.deb # Fedora / RHEL sudo rpm -i openlogi-*.rpm # Arch Linux sudo pacman -U openlogi-*.pkg.tar.zst ``` Packages are published for both `x86_64`/`amd64` and `arm64`/`aarch64`. The package installs udev rules that grant your user access to `/dev/hidraw*`, `/dev/uinput` and your Logitech mouse's `/dev/input/event*` node without `sudo`. After installation, enable the background agent for your user: ```sh systemctl --user enable --now openlogi-agent.service ``` See [docs/INSTALL-linux.md](docs/INSTALL-linux.md) for manual / source installs and distros without systemd. ### Windows Signed portable `.zip` archives and per-user `.msi` installers (x86_64 and arm64) are attached to each release. Both ship the GUI (`OpenLogi.exe`) together with the background agent (`openlogi-agent.exe`), which owns all device I/O — keep the two files side by side when using the portable zip, or the GUI has nothing to connect to. Windows support works and has been validated end-to-end on Windows 11 with real hardware — a wired keyboard and a Unifying-receiver mouse, including install, in-place upgrade, and uninstall of the MSI. It is newer than the macOS build, so if you hit a rough edge please [report it](https://github.com/AprilNEA/OpenLogi/issues). The agent shows a system-tray icon (Show Main Window / Quit) so the app stays reachable after the main window is closed. To disable it on Windows, set `show_in_menu_bar = false` in the TOML `[app_settings]` block and restart the agent; the GUI toggle is currently macOS-only. To build from source, see [DEVELOPMENT.md](docs/DEVELOPMENT.md). ## Usage (CLI) See [USAGE.md](docs/USAGE.md) ## Configuration See [CONFIGURATION.md](docs/CONFIGURATION.md) ## Developing See [DEVELOPMENT.md](docs/DEVELOPMENT.md) ## Acknowledgments - **Windows, cameras, and i18n** by [@davidbudnick](https://github.com/davidbudnick) — the Windows input hook and MSI updates, Logitech webcam support, keyboard RGB, and the Crowdin translation pipeline - **Linux port** by [@cserby](https://github.com/cserby) — the evdev/uinput hook, D-Bus actions, .deb/.rpm packaging - [Solaar](https://github.com/pwr-Solaar/Solaar) by [@pwr](https://github.com/pwr) — the most complete open-source HID++ implementation, and our protocol reference - [Mouser](https://github.com/TomBadash/Mouser) by [@TomBadash](https://github.com/TomBadash) — prior art for a local, account-free Options+ replacement ## License Dual-licensed under either of - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE)) - MIT license ([LICENSE-MIT](LICENSE-MIT)) at your option. ### Third-party code `crates/openlogi-hidpp` is a vendored fork of [`hidpp`](https://crates.io/crates/hidpp) by [@lus](https://github.com/lus), licensed 0BSD. ### Logo & brand assets The OpenLogi logo and app icon — the brand assets under [`design/`](design/) — are © 2026 AprilNEA, all rights reserved, and are not covered by the MIT/Apache licenses above; see [`design/LICENSE`](design/LICENSE). Forking the code grants no right to the OpenLogi name, logo, or icon; please don't use them to represent your own projects, forks, or distributions without prior written permission. --- **Not affiliated with Logitech.** "Logitech", "MX Master", and "Options+" are trademarks of Logitech International S.A. ## Repo activity ---