OpenLogi

GitHub

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

AI Prompts & Endpoints
Agent Skills View CodeWiki Knowledge Base

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.

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..9macos::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."<addr>".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 (CustomShortcutmacos::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 "<src>" | 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<WorkflowStep>) | 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 Delays. 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."<addr>".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 CGEvents; 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<WorkflowStep>) 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+<anything else>
is indistinguishable from <anything else> 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 (MouseButton6MouseButton9) 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 MouseButton6MouseButton9 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 KeyEvents. 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<KeyEvent> {
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.rsAction 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 <source>. Power-user escape hatch.
RunAppleScript(String),
/// Run a shell command via /bin/sh -c <command>. 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::<KeyTrigger>().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):

text
/ 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<str> = 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<u16> = 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::<String>());
}
// 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<HashMap<KeyTrigger, Action>> 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::KeyModifiersconfig::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:

[email protected]

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
<https://www.contributor-covenant.org/version/2/1/code_of_conduct.html>.

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
<https://www.contributor-covenant.org/faq>. Translations are available at
<https://www.contributor-covenant.org/translations>.

---

CONFIGURATION

Configuration

How OpenLogi stores its settings. For install and usage, see the
README.

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:<filename>.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.

text
/ 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.

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 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

text
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=<identity> — codesign the .app and .dmg with the
given Developer ID.
- OPENLOGI_DMG_BACKGROUND_URL=<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 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/<tag>/ 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 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 — see
the main README 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.

<h4 align="right"><a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.ja.md">日本語</a> | <strong>Deutsch</strong> | <a href="README.fr.md">Français</a> | <a href="README.ko.md">한국어</a></h4>

<p align="center">
<img src="https://assets.openlogi.org/brand/openlogi-icon.png" width="138" alt="OpenLogi"/>
</p>

<h1 align="center">OpenLogi</h1>
<p align="center"><strong>⚡️ Eine native, local-first Alternative zu Logitech Options+, geschrieben in Rust 🦀<br/>Tasten, DPI und SmartShift über HID++ neu belegen. Kein Konto, keine Telemetrie.</strong></p>


<div align="center">
<a href="https://twitter.com/AprilNEA" target="_blank">
<img alt="twitter" src="https://img.shields.io/badge/follow-AprilNEA-green?style=social&logo=Twitter"></a>
<a href="https://t.me/+VDtkR5OSAT04NzVh" target="_blank">
<img alt="telegram" src="https://img.shields.io/badge/chat-telegram-blueviolet?style=flat&logo=Telegram"></a>
<a href="https://github.com/AprilNEA/OpenLogi/releases" target="_blank">
<img alt="GitHub downloads" src="https://img.shields.io/github/downloads/AprilNEA/OpenLogi/total.svg?style=flat"></a>
<a href="https://github.com/AprilNEA/OpenLogi/commits" target="_blank">
<img alt="GitHub commit" src="https://img.shields.io/github/commit-activity/m/AprilNEA/OpenLogi?style=flat"></a>
<img alt="Hits" src="https://hits.aprilnea.com/hits?url=https://github.com/aprilnea/openlogi">
</div>

<p align="center">
<a href="https://trendshift.io/repositories/42303" target="_blank">
<img src="https://trendshift.io/api/badge/trendshift/repositories/42303/daily?language=Rust" alt="AprilNEA%2FOpenLogi | Trendshift" width="250" height="55"/></a>
</p>

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 — 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 — 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 — 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.

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 und ziehe OpenLogi.app nach /Applications.

Oder per Homebrew:

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:

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.

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 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.


Verwendung (CLI)

Siehe USAGE.md

Konfiguration

Siehe CONFIGURATION.md

Entwicklung

Siehe DEVELOPMENT.md

Danksagungen

- Windows, Kameras und i18n von @davidbudnick — der Windows-Eingabe-Hook und MSI-Updates, Logitech-Webcam-Unterstützung, Tastatur-RGB und die Crowdin-Übersetzungspipeline
- Linux-Portierung von @cserby — evdev/uinput-Hook, D-Bus-Aktionen, .deb/.rpm-Paketierung
- Solaar von @pwr — die vollständigste quelloffene HID++-Implementierung und unsere Protokollreferenz
- Mouser von @TomBadash — Vorarbeit zum selben Ziel: ein lokaler Options+-Ersatz ohne Konto

Lizenz

Doppelt lizenziert, wahlweise unter

- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT-Lizenz (LICENSE-MIT)

Code von Dritten

crates/openlogi-hidpp ist ein eingebundener Fork von hidpp
von @lus, lizenziert unter 0BSD.

Logo & Markenressourcen

Das OpenLogi-Logo und das App-Icon — die Markenressourcen unter design/ — sind © 2026 AprilNEA, alle Rechte vorbehalten, und fallen nicht unter die obigen MIT-/Apache-Lizenzen; siehe 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.

<h4 align="right"><a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.ja.md">日本語</a> | <a href="README.de.md">Deutsch</a> | <strong>Français</strong> | <a href="README.ko.md">한국어</a></h4>

<p align="center">
<img src="https://assets.openlogi.org/brand/openlogi-icon.png" width="138" alt="OpenLogi"/>
</p>

<h1 align="center">OpenLogi</h1>
<p align="center"><strong>⚡️ Une alternative native et local-first à Logitech Options+, écrite en Rust 🦀<br/>Remappez boutons, DPI et SmartShift via HID++. Sans compte, sans télémétrie.</strong></p>


<div align="center">
<a href="https://twitter.com/AprilNEA" target="_blank">
<img alt="twitter" src="https://img.shields.io/badge/follow-AprilNEA-green?style=social&logo=Twitter"></a>
<a href="https://t.me/+VDtkR5OSAT04NzVh" target="_blank">
<img alt="telegram" src="https://img.shields.io/badge/chat-telegram-blueviolet?style=flat&logo=Telegram"></a>
<a href="https://github.com/AprilNEA/OpenLogi/releases" target="_blank">
<img alt="GitHub downloads" src="https://img.shields.io/github/downloads/AprilNEA/OpenLogi/total.svg?style=flat"></a>
<a href="https://github.com/AprilNEA/OpenLogi/commits" target="_blank">
<img alt="GitHub commit" src="https://img.shields.io/github/commit-activity/m/AprilNEA/OpenLogi?style=flat"></a>
<img alt="Hits" src="https://hits.aprilnea.com/hits?url=https://github.com/aprilnea/openlogi">
</div>

<p align="center">
<a href="https://trendshift.io/repositories/42303" target="_blank">
<img src="https://trendshift.io/api/badge/trendshift/repositories/42303/daily?language=Rust" alt="AprilNEA%2FOpenLogi | Trendshift" width="250" height="55"/></a>
</p>

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 — 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 — 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 — 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.

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 et glissez OpenLogi.app dans /Applications.

Ou installez via Homebrew :

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 :

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.

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 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.


Utilisation (CLI)

Voir USAGE.md

Configuration

Voir CONFIGURATION.md

Développement

Voir DEVELOPMENT.md

Remerciements

- Windows, caméras et i18n par @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 — le hook evdev/uinput, les actions D-Bus, l'empaquetage .deb/.rpm
- Solaar par @pwr — l'implémentation open source la plus complète de HID++, et notre référence pour le protocole
- Mouser par @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)
- Licence MIT (LICENSE-MIT)

Code tiers

crates/openlogi-hidpp est un fork intégré de hidpp
par @lus, sous licence 0BSD.

Logo et ressources de marque

Le logo et l'icône d'application OpenLogi — les ressources de marque sous design/ — sont © 2026 AprilNEA, tous droits réservés, et ne sont pas couverts par les licences MIT/Apache ci-dessus ; voir 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 👀 を付けて、新しいリリースの通知を受け取りましょう。

<h4 align="right"><a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <strong>日本語</strong> | <a href="README.de.md">Deutsch</a> | <a href="README.fr.md">Français</a> | <a href="README.ko.md">한국어</a></h4>

<p align="center">
<img src="https://assets.openlogi.org/brand/openlogi-icon.png" width="138" alt="OpenLogi"/>
</p>

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


<div align="center">
<a href="https://twitter.com/AprilNEA" target="_blank">
<img alt="twitter" src="https://img.shields.io/badge/follow-AprilNEA-green?style=social&logo=Twitter"></a>
<a href="https://t.me/+VDtkR5OSAT04NzVh" target="_blank">
<img alt="telegram" src="https://img.shields.io/badge/chat-telegram-blueviolet?style=flat&logo=Telegram"></a>
<a href="https://github.com/AprilNEA/OpenLogi/releases" target="_blank">
<img alt="GitHub downloads" src="https://img.shields.io/github/downloads/AprilNEA/OpenLogi/total.svg?style=flat"></a>
<a href="https://github.com/AprilNEA/OpenLogi/commits" target="_blank">
<img alt="GitHub commit" src="https://img.shields.io/github/commit-activity/m/AprilNEA/OpenLogi?style=flat"></a>
<img alt="Hits" src="https://hits.aprilnea.com/hits?url=https://github.com/aprilnea/openlogi">
</div>

<p align="center">
<a href="https://trendshift.io/repositories/42303" target="_blank">
<img src="https://trendshift.io/api/badge/trendshift/repositories/42303/daily?language=Rust" alt="AprilNEA%2FOpenLogi | Trendshift" width="250" height="55"/></a>
</p>

Options+ にうんざり?OpenLogi をどうぞ。

Logitech アカウントもテレメトリも公式 Options+ のインストールも不要で、ボタンの再マッピング、DPI と SmartShift の制御、アプリごとのプロファイル切り替えができます。クラウドなし、設定はプレーンな TOML ファイル。デフォルトでは自動接続はデバイス画像の取得だけで、更新の確認とダウンロードは要求時またはオプトイン時にのみ実行されます。

---

概要

OpenLogi は Logi Bolt および Unifying レシーバー、Bluetooth 直結、USB ケーブル経由で Logitech の HID++ 周辺機器と通信します。Logi Options+ を動かす必要はありません。3 つのコンポーネントで構成されます:

- OpenLogi GUI —— GPUI 製デスクトップアプリ:クリック可能なホットスポット付きのインタラクティブなマウス図、ボタンごとのアクションピッカー(組み込みアクション + TOML 設定で作成するカスタムショートカット)、DPI プリセット、SmartShift、デバイスごとのスクロール反転、RGB キーボード照明、アプリごとのプロファイル、ライブデバイスカルーセル、20 言語にローカライズされた設定ウィンドウ。
- OpenLogi agent —— 入力フックとすべてのデバイス I/O を所有するバックグラウンドサービス。GUI は純粋な IPC クライアントで、必要時に agent を起動します。
- 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 以降が必要です。

最新リリースから署名・公証済みの .dmg をダウンロードし、OpenLogi.app/Applications にドラッグします。

または Homebrew で:

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 より先に更新されることがあります。openlogiopenlogi@latest のどちらか一方だけをインストールしてください。

Linux

最新リリースから .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/amd64arm64/aarch64 の両方で公開されています。

パッケージは udev ルールをインストールし、sudo なしで /dev/hidraw*/dev/uinput にアクセスできるようにします。インストール後、ユーザーのバックグラウンドエージェントを有効化してください:

sh
systemctl --user enable --now openlogi-agent.service

手動 / ソースからのインストールや systemd のないディストリビューションは 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 版より新しいため、問題があれば報告してください。agent はシステムトレイアイコン(メインウィンドウを表示 / 終了)を表示し、メインウィンドウを閉じてもアプリを開けます。Windows で無効にするには TOML の [app_settings] ブロックで show_in_menu_bar = false を設定し、agent を再起動してください。GUI の切り替えは現在 macOS 専用です。

ソースからのビルドは DEVELOPMENT.md を参照。


使い方(CLI)

USAGE.md を参照

設定

CONFIGURATION.md を参照

開発

DEVELOPMENT.md を参照

謝辞

- Windows・カメラ・i18n: @davidbudnick —— Windows の入力フックと MSI アップデート、Logitech ウェブカメラ対応、キーボード RGB、Crowdin 翻訳パイプライン
- Linux 移植: @cserby —— evdev/uinput フック、D-Bus アクション、.deb/.rpm パッケージング
- Solaar by @pwr —— 最も網羅的なオープンソースの HID++ 実装であり、本プロジェクトのプロトコル参照元
- Mouser by @TomBadash —— 同じ目標の先行プロジェクト:ローカル完結・アカウント不要の Options+ 代替

ライセンス

以下のいずれかを選択できます:

- Apache License 2.0(LICENSE-APACHE
- MIT ライセンス(LICENSE-MIT

サードパーティコード

crates/openlogi-hidpphidpp(作者 @lus)の vendored fork で、0BSD ライセンスです。

ロゴとブランドアセット

OpenLogi のロゴとアプリアイコン —— design/ 配下のブランドアセット —— は © 2026 AprilNEA が全権利を留保しており、上記の MIT/Apache ライセンスの対象外です。design/LICENSE を参照してください。コードをフォークしても OpenLogi の名称・ロゴ・アイコンの使用権は付与されません。事前の書面による許可なく、ご自身のプロジェクト、フォーク、配布物を表すために使用しないでください。

---

Logitech とは無関係です。 「Logitech」「MX Master」「Options+」は Logitech International S.A. の商標です。

リポジトリの活動

---

README.Ko

WARNING

OpenLogi는 활발히 개발 중이며 아직 안정 단계가 아닙니다 — 기능과 설정이 변경될 수 있습니다. 저장소에 Star ⭐ 와 Watch 👀 를 눌러 두면 새 릴리스가 나올 때 알림을 받을 수 있습니다.

<h4 align="right"><a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.ja.md">日本語</a> | <a href="README.de.md">Deutsch</a> | <a href="README.fr.md">Français</a> | <strong>한국어</strong></h4>

<p align="center">
<img src="https://assets.openlogi.org/brand/openlogi-icon.png" width="138" alt="OpenLogi"/>
</p>

<h1 align="center">OpenLogi</h1>
<p align="center"><strong>⚡️ Rust로 작성된 네이티브 로컬 우선 Logitech Options+ 대안 🦀<br/>HID++로 버튼·DPI·SmartShift를 리매핑하세요. 계정도, 텔레메트리도 없습니다.</strong></p>


<div align="center">
<a href="https://twitter.com/AprilNEA" target="_blank">
<img alt="twitter" src="https://img.shields.io/badge/follow-AprilNEA-green?style=social&logo=Twitter"></a>
<a href="https://t.me/+VDtkR5OSAT04NzVh" target="_blank">
<img alt="telegram" src="https://img.shields.io/badge/chat-telegram-blueviolet?style=flat&logo=Telegram"></a>
<a href="https://github.com/AprilNEA/OpenLogi/releases" target="_blank">
<img alt="GitHub downloads" src="https://img.shields.io/github/downloads/AprilNEA/OpenLogi/total.svg?style=flat"></a>
<a href="https://github.com/AprilNEA/OpenLogi/commits" target="_blank">
<img alt="GitHub commit" src="https://img.shields.io/github/commit-activity/m/AprilNEA/OpenLogi?style=flat"></a>
<img alt="Hits" src="https://hits.aprilnea.com/hits?url=https://github.com/aprilnea/openlogi">
</div>

<p align="center">
<a href="https://trendshift.io/repositories/42303" target="_blank">
<img src="https://trendshift.io/api/badge/trendshift/repositories/42303/daily?language=Rust" alt="AprilNEA%2FOpenLogi | Trendshift" width="250" height="55"/></a>
</p>

Options+가 지긋지긋하다면? OpenLogi를 써 보세요.

Logitech 계정도, 텔레메트리도, 공식 Options+ 설치도 없이 버튼을 리매핑하고 DPI와 SmartShift를 제어하며 앱별 프로필을 전환할 수 있습니다. 클라우드 없이 순수 TOML 설정 파일만 사용합니다. 기본 설정에서는 기기 이미지를 가져올 때만 자동으로 연결하며, 업데이트 확인과 다운로드는 요청하거나 옵트인한 경우에만 실행됩니다.

---

소개

OpenLogi는 Logi Bolt 및 Unifying 수신기, Bluetooth 직접 연결 또는 USB 케이블을 통해 Logitech HID++ 주변기기와 통신하며, Logi Options+를 실행할 필요가 없습니다. 세 가지 구성 요소로 이루어집니다:

- OpenLogi GUI — GPUI 데스크톱 앱: 클릭 가능한 핫스팟이 있는 인터랙티브 마우스 다이어그램, 버튼별 액션 선택기(내장 액션 + TOML 설정에서 작성하는 사용자 지정 단축키), DPI 프리셋, SmartShift, 기기별 스크롤 반전, RGB 키보드 조명, 앱별 프로필, 실시간 기기 캐러셀, 20개 언어로 현지화된 설정 창.
- OpenLogi agent — 입력 훅과 모든 기기 I/O를 소유하는 백그라운드 서비스. GUI는 순수 IPC 클라이언트이며 필요할 때 agent를 시작합니다.
- 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 이상이 필요합니다.

최신 릴리스에서 서명·공증된 .dmg를 내려받아 OpenLogi.app/Applications로 드래그하세요.

또는 Homebrew로 설치:

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보다 먼저 갱신될 수 있습니다. openlogiopenlogi@latest 중 하나만 설치하세요.

Linux

최신 릴리스에서 .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/amd64arm64/aarch64 두 아키텍처로 제공됩니다.

패키지는 sudo 없이 /dev/hidraw*/dev/uinput에 접근할 수 있게 해 주는 udev 규칙을 설치합니다. 설치 후 사용자용 백그라운드 에이전트를 활성화하세요:

sh
systemctl --user enable --now openlogi-agent.service

수동 / 소스 설치와 systemd가 없는 배포판은 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 포트보다 최신이므로 문제가 있으면 제보해 주세요. agent는 시스템 트레이 아이콘(메인 창 표시 / 종료)을 표시하므로 메인 창을 닫은 뒤에도 앱에 접근할 수 있습니다. Windows에서 비활성화하려면 TOML [app_settings] 블록에 show_in_menu_bar = false를 설정하고 agent를 다시 시작하세요. GUI 토글은 현재 macOS 전용입니다.

소스에서 빌드하려면 DEVELOPMENT.md를 참고하세요.


사용법 (CLI)

USAGE.md 참고

설정

CONFIGURATION.md 참고

개발

DEVELOPMENT.md 참고

감사의 말

- Windows·카메라·i18n@davidbudnick: Windows 입력 훅과 MSI 업데이트, Logitech 웹캠 지원, 키보드 RGB, Crowdin 번역 파이프라인
- Linux 포팅@cserby: evdev/uinput 훅, D-Bus 액션, .deb/.rpm 패키징
- Solaar@pwr가 만든, 가장 완성도 높은 오픈소스 HID++ 구현이자 이 프로젝트의 프로토콜 참고 자료
- Mouser@TomBadash가 만든, 같은 목표의 선행 프로젝트: 로컬에서 동작하는 계정 없는 Options+ 대체제

라이선스

다음 중 하나를 선택해 사용할 수 있습니다:

- Apache License 2.0 (LICENSE-APACHE)
- MIT 라이선스 (LICENSE-MIT)

서드파티 코드

crates/openlogi-hidpphidpp(@lus 제작)의 vendored fork이며, 0BSD 라이선스를 따릅니다.

로고 및 브랜드 자산

OpenLogi 로고와 앱 아이콘 — design/ 아래의 브랜드 자산 — 은 © 2026 AprilNEA가 모든 권리를 보유하며, 위 MIT/Apache 라이선스의 적용을 받지 않습니다. design/LICENSE를 참고하세요. 코드를 포크해도 OpenLogi 이름·로고·아이콘에 대한 권리는 부여되지 않습니다. 사전 서면 허가 없이 자신의 프로젝트, 포크, 배포판을 나타내는 데 사용하지 마세요.

---

Logitech과 무관합니다. "Logitech", "MX Master", "Options+"는 Logitech International S.A.의 상표입니다.

저장소 활동

---

README.Zh CN

WARNING

OpenLogi 仍在积极开发中,尚未稳定 —— 功能与配置仍可能变动。点个 Star ⭐ 并 Watch 👀 本仓库,在新版本发布时获得通知。

<h4 align="right"><a href="../README.md">English</a> | <strong>简体中文</strong> | <a href="README.ja.md">日本語</a> | <a href="README.de.md">Deutsch</a> | <a href="README.fr.md">Français</a> | <a href="README.ko.md">한국어</a></h4>

<p align="center">
<img src="https://assets.openlogi.org/brand/openlogi-icon.png" width="138" alt="OpenLogi"/>
</p>

<h1 align="center">OpenLogi</h1>
<p align="center"><strong>⚡️ 原生、本地优先的 Logitech Options+ 替代品,用 Rust 编写 🦀<br/>通过 HID++ 重映射按键、调节 DPI 与 SmartShift。无账号、无遥测。</strong></p>


<div align="center">
<a href="https://twitter.com/AprilNEA" target="_blank">
<img alt="twitter" src="https://img.shields.io/badge/follow-AprilNEA-green?style=social&logo=Twitter"></a>
<a href="https://t.me/+VDtkR5OSAT04NzVh" target="_blank">
<img alt="telegram" src="https://img.shields.io/badge/chat-telegram-blueviolet?style=flat&logo=Telegram"></a>
<a href="https://github.com/AprilNEA/OpenLogi/releases" target="_blank">
<img alt="GitHub downloads" src="https://img.shields.io/github/downloads/AprilNEA/OpenLogi/total.svg?style=flat"></a>
<a href="https://github.com/AprilNEA/OpenLogi/commits" target="_blank">
<img alt="GitHub commit" src="https://img.shields.io/github/commit-activity/m/AprilNEA/OpenLogi?style=flat"></a>
<img alt="Hits" src="https://hits.aprilnea.com/hits?url=https://github.com/aprilnea/openlogi">
</div>

<p align="center">
<a href="https://trendshift.io/repositories/42303" target="_blank">
<img src="https://trendshift.io/api/badge/trendshift/repositories/42303/daily?language=Rust" alt="AprilNEA%2FOpenLogi | Trendshift" width="250" height="55"/></a>
</p>

被 Options+ 折腾够了?试试 OpenLogi。

无需 Logitech 账号、无遥测、无需安装官方 Options+,即可重映射按键、调节 DPI 与 SmartShift、按应用自动切换配置。没有云端,配置是纯 TOML 文件。默认情况下,唯一的自动联网行为是获取设备图片;只有在你主动请求或选择启用时,才会检查并下载更新。

---

这是什么

OpenLogi 通过 Logi Bolt 和 Unifying 接收器、蓝牙直连或 USB 线缆与 Logitech HID++ 外设通信,完全不需要运行 Logi Options+。它由三个组件组成:

- OpenLogi GUI —— 基于 GPUI 的桌面应用:可点击热区的交互式鼠标示意图、逐按键动作选择器(内置动作 + 在 TOML 配置中编写的自定义快捷键)、DPI 预设、SmartShift、按设备原生滚动反转、RGB 键盘灯光、按应用的配置叠加层、实时设备轮播,以及界面已本地化为 20 种语言的设置窗口。
- OpenLogi agent —— 拥有输入钩子和全部设备 I/O 的后台服务。GUI 是纯 IPC 客户端,并在需要时启动 agent。
- 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 下载已签名、已公证的 .dmg,把 OpenLogi.app 拖入 /Applications

或通过 Homebrew 安装:

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 的自动更新先一步。openlogiopenlogi@latest 二选一安装,不要同时装。

Linux

最新 release 下载适用于你的发行版的安装包:

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/amd64arm64/aarch64 两种架构。

安装包会写入 udev 规则,让你的用户无需 sudo 即可访问 /dev/hidraw*/dev/uinput。装完后为当前用户启用后台 agent:

sh
systemctl --user enable --now openlogi-agent.service

手动 / 源码安装以及无 systemd 的发行版,见 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 版本更新,如遇到粗糙之处,请反馈问题。agent 会显示系统托盘图标(「显示主窗口」/「退出」),因此关闭主窗口后仍可打开应用。如需在 Windows 上禁用该图标,请在 TOML 的 [app_settings] 块中设置 show_in_menu_bar = false,然后重启 agent;GUI 开关目前仅适用于 macOS。

从源码构建见 DEVELOPMENT.md


使用(CLI)

USAGE.md

配置

CONFIGURATION.md

开发

DEVELOPMENT.md

致谢

- Windows、摄像头与 i18n@davidbudnick —— Windows 输入钩子与 MSI 更新、Logitech 摄像头支持、键盘 RGB、Crowdin 翻译流水线
- Linux 移植@cserby —— evdev/uinput 钩子、D-Bus 动作、.deb/.rpm 打包
- Solaar,作者 @pwr —— 目前最完整的开源 HID++ 实现,也是本项目的协议参考
- Mouser,作者 @TomBadash —— 同一目标的先行项目:本地、无需账号的 Options+ 替代品

许可证

以下两种许可证任选其一:

- Apache License 2.0(LICENSE-APACHE
- MIT 许可证(LICENSE-MIT

第三方代码

crates/openlogi-hidpphidpp(作者 @lus)的 vendored fork,采用 0BSD 许可证。

OpenLogi 的 Logo 与应用图标 —— 即 design/ 下的品牌资产 —— © 2026 AprilNEA 保留所有权利,不在上述 MIT/Apache 许可范围内;见 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:

[email protected]

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.

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 <URL> 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,
and this project adheres to Semantic Versioning.

[Unreleased]

[0.6.27] - 2026-08-13

Added

- (gui) dismiss the Actions Ring on a click outside it (#591)
- (agent) pressing the ring trigger again dismisses the Actions Ring (#592)
- (agent) add a hardware-free mock agent for GUI development (#568)
- per-slot custom labels for the Actions Ring (#584)
- (core) support stable Windows app selectors (#572)
- (gui) add capability-driven actions ring (#528)
- (hid) persist the immutable probe cache across restarts (#564)
- (gui) back navigation via the mouse's back button and Alt+Left (#563)
- capture the MX Master 4 haptic panel as a first-class control (#565)
- (hid) recognise Lightspeed receiver 046d:c547 (G915, G502 X) (#574)
- (hid,hidpp) read battery over BatteryVoltage (0x1001) (#575)

Fixed

- (gui) open the Actions Ring on the display containing the cursor (#588)
- (hid) detect and recover dead-delivery HID channels (#589)
- Actions Ring haptic reliability — coalescing, feature cache, firmware arming, deadlock guards (#590)
- (agent) implement the Actions Ring IPC surface in the mock agent (#587)
- (gui) redraw the Actions Ring on hover changes (#585)
- (hid) widen the Bolt per-slot probe budget for high-latency USB paths (#562)
- (macos) prevent corrupted small app icons (#570)
- (hook) release macOS tap after accessibility revocation (#578)
- (ui) prevent middle and thumb wheel popover flicker (#559)

[0.6.26] - 2026-08-10

Fixed

- (macos) add camera hardened-runtime entitlement (#557)

[0.6.25] - 2026-08-10

Added

- per-device capture with plan-driven sessions (#419)
- (hid) recognise Lightspeed nano receivers (G-series, e.g. G305) (#388)
- add MX Master 2S (3S) thumb wheel bindings (#525)

Fixed

- (i18n) add camera permission locale keys (#554)
- (hook) capture keyboard events on windows (#548)
- (gui,camera,xtask) make Camera permission grantable on macOS (#550)
- (ci) merge Crowdin downloads into locale catalogs (#553)
- (i18n) skip Crowdin English fill-in and restore locale parity (#551)
- (hidpp) retry lost feature-table reads during enumeration (#469)
- (gui,assets) fit the Keys tab to legacy keyboard assets (G513) (#544)

[0.6.24] - 2026-08-10

Added

- (hid) recognize Lightspeed receiver (046d:c539) as Unifying-compatible (#510)
- add function key remapper (#344)
- (hid) add standalone Litra light support (#513)
- keyboard F-row key remapping and fn-lock over HID++ (#395)
- (hook) Wayland frontmost-window backends (wlroots + GNOME Shell) (#191)
- (camera) add Logitech webcam support (#531)
- (backlight) support HID++ 0x1982 (#470)
- (battery) support legacy 0x1000 BatteryStatus and its charging quirk (#312)

Fixed

- (agent-core) retry volatile DPI re-apply on cold boot (#449)
- (agent) prefer online device for input capture (#453)
- (hidpp) keep events when a field carries an unknown enum value (#432)
- (agent) rearm control capture after device reconnect (#450)
- (linux) grant uaccess on Logitech input event nodes (#530)
- (agent) reapply volatile settings after macOS resume (#506)
- (hook) never wedge system pointer input (#534)
- (agent) route hardware operations through inventory channels (#532)
- (agent) reuse inventory channels for input capture (#522)
- (i18n) complete Crowdin synchronization (#508)

0.6.23 - 2026-08-02

Fixed

- (hook) grab only relative pointer devices, never touchpads or pointing sticks (#401)

0.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 - 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)

Other

- (core) add hires_wheel to the inventory equality test helper (#417)

0.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)

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 - 2026-07-04

Added

- (windows) notification-area tray icon for the agent (#347)
- (windows) bundle and package the background agent (#347)

0.6.18 - 2026-06-29

Added

- (hidpp) add typed reprog controls support

Other

- Clarify MX Master 4 gesture control semantics (#325)

0.6.17 - 2026-06-24

Added

- add Capture Region to Clipboard button action (#296)

0.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 - 2026-06-21

Added

- (scroll) per-device inverted scrolling (#126)

0.6.14 - 2026-06-15

Fixed

- (hid) solid keyboard colour via 0x8070 effect (#205)

Other

- (core) extract the OS input-injection layer into openlogi-inject (#240)

0.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 - 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 - 2026-06-13

Fixed

- (hid) replay a node's last inventory through transient probe failures (#222)

0.6.10 - 2026-06-13

Added

- (config) add auto_download_assets app setting

Fixed

- (gui) keep the diagnostics report truthful across agent restarts (#230)

0.6.9 - 2026-06-12

Added

- (gui) add a Copy Diagnostics button to the About window (#206)

0.6.8 - 2026-06-12

Added

- (linux) launch_at_login + input device access permission check (#172)
- add mouse button 4 and 5 options (#96)

0.6.7 - 2026-06-12

Fixed

- (core) post macOS volume and media keys as NX system-defined events (#184)

Other

- (ipc) pin the wire format with golden bytes and mark wire types

0.6.6 - 2026-06-10

Fixed

- (hidpp) bound device-controlled name lengths in Bolt parsing (#200)

0.6.5 - 2026-06-10

Other

- collapse nested ifs flagged by current stable clippy (#197)

0.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)
- (gui) glow keyboard card in lighting colour (#185)

0.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 - 2026-06-08

Added

- (i18n) integrate Crowdin localization workflow (#174)

Other

- switch release notes generation to Codex (#177)
- add code of conduct

0.6.1 - 2026-06-08

Fixed

- (cli) diag selects a device that exposes the feature under test (#150)

0.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 - 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 - 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)

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)
- (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 - 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 - 2026-06-05

Added

- add wired G-series keyboard RGB control (#29)

0.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)
- add GitHub issue form templates (#102)
- configure release-plz branch prefix

0.4.0 - 2026-06-02

Added

- (i18n) add zh-TW (Traditional Chinese, Taiwan) locale (#57)

0.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 - 2026-06-01

Fixed

- (assets) match devices by displayName when no PID lookup hits

0.3.2 - 2026-06-01

Other

- simplify format

0.3.1 - 2026-06-01

Added

- (updater) use static R2 manifest (#43)

0.3.0 - 2026-06-01

Added

- (openlogi-gui) add Russian localization and language select (#38)

Fixed

- (gui) stabilize device tab ordering (#37)

0.2.0 - 2026-05-31

Added

- (openlogi-hid) route HID++ writes to directly-attached devices (#5)

0.1.4 - 2026-05-31

Other

- update workflow actions for Node 24
- (release-plz) fail loudly when a release silently stalls

0.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 - 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 - 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.

<h4 align="right"><strong>English</strong> | <a href="docs/README.zh-CN.md">简体中文</a> | <a href="docs/README.ja.md">日本語</a> | <a href="docs/README.de.md">Deutsch</a> | <a href="docs/README.fr.md">Français</a> | <a href="docs/README.ko.md">한국어</a></h4>

<p align="center">
<img src="https://assets.openlogi.org/brand/openlogi-icon.png" width="138" alt="OpenLogi"/>
</p>

<h1 align="center">OpenLogi</h1>
<p align="center"><strong>⚡️ A native, local-first alternative to Logitech Options+, written in Rust 🦀<br/>Remap buttons, DPI, and SmartShift over HID++. No account, no telemetry.</strong></p>


<div align="center">
<a href="https://twitter.com/AprilNEA" target="_blank">
<img alt="twitter" src="https://img.shields.io/badge/follow-AprilNEA-green?style=social&logo=Twitter"></a>
<a href="https://t.me/+VDtkR5OSAT04NzVh" target="_blank">
<img alt="telegram" src="https://img.shields.io/badge/chat-telegram-blueviolet?style=flat&logo=Telegram"></a>
<a href="https://github.com/AprilNEA/OpenLogi/releases" target="_blank">
<img alt="GitHub downloads" src="https://img.shields.io/github/downloads/AprilNEA/OpenLogi/total.svg?style=flat"></a>
<a href="https://github.com/AprilNEA/OpenLogi/commits" target="_blank">
<img alt="GitHub commit" src="https://img.shields.io/github/commit-activity/m/AprilNEA/OpenLogi?style=flat"></a>
<img alt="Hits" src="https://hits.aprilnea.com/hits?url=https://github.com/aprilnea/openlogi">
</div>

<p align="center">
<a href="https://trendshift.io/repositories/42303" target="_blank">
<img src="https://trendshift.io/api/badge/trendshift/repositories/42303/daily?language=Rust" alt="AprilNEA%2FOpenLogi | Trendshift" width="250" height="55"/></a>
</p>

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 — 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 — 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 — 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.

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.

¹ 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 and drag OpenLogi.app to /Applications.

Or install via Homebrew:

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:

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 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. 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.


Usage (CLI)

See USAGE.md

Configuration

See CONFIGURATION.md

Developing

See DEVELOPMENT.md

Acknowledgments

- Windows, cameras, and i18n by @davidbudnick — the Windows input hook and MSI updates, Logitech webcam support, keyboard RGB, and the Crowdin translation pipeline
- Linux port by @cserby — the evdev/uinput hook, D-Bus actions, .deb/.rpm packaging
- Solaar by @pwr — the most complete open-source HID++ implementation, and our protocol reference
- Mouser by @TomBadash — prior art for a local, account-free Options+ replacement

License

Dual-licensed under either of

- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)

at your option.

Third-party code

crates/openlogi-hidpp is a vendored fork of hidpp
by @lus, licensed 0BSD.

Logo & brand assets

The OpenLogi logo and app icon — the brand assets under design/
are © 2026 AprilNEA, all rights reserved, and are not covered by the MIT/Apache
licenses above; see 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

---