Technical Documentation: AprilNEA/OpenLogi
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
[!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.
OpenLogi
Fed up with Options+? Try OpenLogi.
Runs on macOS, Linux, and Windows.
Beyond Options+
Things OpenLogi does that Options+ won't:
- Stay light. Native Rust + GPUI.
- Run on Linux. Linux is a first-class platform in OpenLogi.
- Gestures on supported buttons. Assign gesture actions to supported controls — or turn gestures off entirely.
- Plain-text config. Everything is one TOML file you can sync between machines however you like.
- Script it. A real CLI alongside the GUI: a JSON automation API for DPI, SmartShift, Fn-lock, and report rate, plus hardware diagnostics.
Features
Devices
- Logi Bolt and Unifying receivers, Bluetooth, and wired connections, with battery level and charge state
- Low-battery notices at the bottom of the screen, once per discharge, with no OS notification permission
- An on-screen indicator when a keyboard turns Caps Lock on or off, plus Num Lock and Scroll Lock on Windows and Linux
| Easy-SwitchSee which computer is paired on each channel, with its OS and connectionRename a channel on devices that allow it |
|---|
Buttons and actions
| Remap any buttonA built-in action catalog, custom shortcuts, typed text, or multi-step workflowsSeparate tap and hold actions, and hold-to-talk chords that release with the buttonGestures on Middle Click, Back/Forward, DPI/ModeShift, the gesture button, and the haptic panel | |
|---|---|
| Actions RingEight actions in a ring around the cursorIts own layout per application | |
| Keyboard keysBind media, emoji, screenshot, and other dedicated keys like mouse buttonsRestore one key to its device default, or reset the whole row |
Per-application profiles switch with the focused app. An [apps] alias lets one profile follow an application across platforms.
Pointer and scrolling
| PointerDPI presets with cycle and set-preset actionsPointer speed and report ratePrecision Mode: one button switches the mouse to its slowest speed and back | |
|---|---|
| SmartShiftWheel mode and sensitivityScrolling force on SmartShift Enhanced miceA permanent-ratchet switch | |
| ScrollingScroll inversion per device, natively or in softwareWheel resolution and smooth wheel scrollingHold Back or Forward and roll the wheel to scroll sideways on mice without a thumb wheel |
Keyboard and lighting
| KeyboardFn Lock, and multi-platform keyboards that stay on the host OS layoutBacklight level, automatic brightness, and battery savingThe backlight level shown on screen as it changesStatic RGB on supported keyboards | |
|---|---|
| Litra lightsPower, brightness, and color temperatureTurns on with the camera if you wantKeeps a change made on the light itself |
Webcams
- Any Logitech UVC webcam (Brio, StreamCam, the C920 series, …), plug and play
- Live preview that holds the camera only while you watch; leave it and the LED goes off
- Zoom, pan, tilt, focus, exposure, white balance, and every other UVC control, written to the hardware so Meet, Zoom, and OBS see the change
- Built-in Default / Streaming / Video call profiles plus your own snapshots, kept per camera
Feature coverage depends on what each device reports; the app shows only the controls a device supports.
Platform support
| Feature | macOS | Windows | Linux |
|---|---|---|---|
| Mouse button remapping and gestures | ✓ | ✓ | ✓ |
| Keyboard F-key remapping | ✓ | ✓ | — |
| Per-application profiles | ✓ | ✓ | X11, wlroots (sway, Hyprland), GNOME with the companion extension |
| Mouse profile follows the window under the pointer | ✓ | ✓ | X11 only |
| Media keys | Native | Native | D-Bus MPRIS |
| Window-manager actions (desktops, Mission Control, …) | Native | Native equivalents | GNOME, KDE (KWin), Hyprland; some macOS-only actions do nothing |
| Menu-bar / tray icon | ✓ | ✓ | — |
| In-app updates | Sparkle | Built-in | Built-in |
Install
[!IMPORTANT]
Quit Logi Options+ (and Solaar on Linux) first. Only one application can own a receiver at a time.
| Platform | System | Architecture |
|---|---|---|
| macOS | macOS 13 or later | Apple silicon, Intel |
| Linux | GLIBC 2.35 or newer | x86_64, arm64 |
| Windows | Tested on Windows 11 | x86_64, arm64 |
Every download carries a minisign signature and an entry in SHA256SUMS. To build from source, see DEVELOPMENT.md.
macOS
[](https://openlogi.org/download/mac-arm64)
[](https://openlogi.org/download/mac-x64)Or with Homebrew:
brew install --cask openlogiOpen OpenLogi and grant Accessibility and Input Monitoring when macOS asks. OpenLogi then updates itself through Sparkle.
Linux
[](https://openlogi.org/download/linux)
[](https://openlogi.org/download/linux-rpm)
[](https://openlogi.org/download/linux-arch)Or install the right package for your distribution with one command. It verifies the minisign signature and starts the agent; install minisign first:
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | shTo read the script before running it, and for Flatpak, NixOS, and distributions without systemd, see INSTALL-linux.md.
Windows
[](https://openlogi.org/download/windows)
[](https://openlogi.org/download/windows-zip)Usage (CLI)
See USAGE.md
Configuration
Settings use plain TOML; saves preserve symlinked config files. See
CONFIGURATION.md.
Developing
See DEVELOPMENT.md, including macOS input-hook safety.
Acknowledgments
- Windows, cameras, and i18n by @davidbudnick — keyboard RGB, Windows support, Logitech webcam support
- Linux port by @cserby — Linux support
- Solaar by @pwr — open-source HID++ implementation
- Mouser by @TomBadash — a local, account-free Options+ replacement
License
The code in this repository is 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
Thanks to @kubai087 for designing the OpenLogi
logo. The OpenLogi logo and app icon (the brand assets underdesign/) 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
Overview (`fixtures/README.md`)
OpenLogi fixture corpus
This directory is the repository-level home for reviewed, sanitized
captures produced by openlogi fixture record. One physical specimen owns one
directory:
fixtures/devices/<synthetic-specimen-id>/
manifest.json
profile.json
cases/
<operation>.jsonOnly privacy-verified fixture assets belong here. Never commit native recorder
output, host paths, original hardware identities, passkeys, or unsanitized
temporary files. Run openlogi fixture verify <fixture-directory> before
review. cargo test -p openlogi-cli fixture::verify also discovers and strictly
verifies every fixture directory in this corpus and the packaged synthetic
corpus, including newly added specimens.
Automatic verification
The repository tests discover every specimen through openlogi_fixture::fs::repository_corpus. Missing files, extra files, symlinks, invalid identities, and an absent or empty repository corpus fail the suite. Published packages without the repository workspace report that the contributed corpus is unavailable; their embedded synthetic fixture remains testable.
Run the complete recorded-data chain on macOS:
cargo test -p openlogi-cli -p openlogi-agent-core -p openlogi-agent -p openlogi-desktop contributed_corpus
cargo test -p openlogi-cli fixture::verify
cargo test -p openlogi-fixture --features fs fs::tests| Boundary | Automated contract |
|---|---|
| CLI / device operations | Every declared case executes its production read and consumes every required cassette exchange. Settings match the profile. Feature tables, control tables, and battery diagnostics match fixed reviewed expectations. |
| Agent hardware | All recorded DPI, SmartShift, wheel, and backlight reads use the injected backend, exact-route registry, receiver lease, and device-I/O gate. An unpublished route and a suspended gate refuse the operation. Resuming permits the recorded read on one channel. |
| Mock Agent / IPC | Every profile supplies the real mock RPC handlers. Tests check setting support, DPI and SmartShift write/readback, snapshots, and frozen-time observations. Unix tests run the production handshake and bincode socket transport in an isolated process. |
| Desktop | Every profile enters AppState::apply_agent_snapshot. Tests check identity, routes, battery, measured capabilities, expected detail tabs, and stable identity across offline/online snapshots. |
The operation expectation file is corpus_diagnostics.json. Each feature tuple is [feature_id, version, type_bits]; each control tuple is [control_id, task_id, capability_bits]. The initial tables were transcribed from reviewed response payloads, independently of the production decoder. Tests never regenerate expectations. When adding a specimen, review and add its exact case set, diagnostics, and desktop tab expectations. Unknown operations, missing expectations, deleted reviewed specimens, and changed case coverage fail.
All suites run under the existing workspace test jobs. CI selects those jobs when source, fixtures, test resources, dependencies, toolchain settings, or CI configuration change. Pure Markdown and unrelated design assets skip the test runners. The path filter lives in the fmt job of ci.yml; local cargo test commands always run the requested suites. Linux and Windows run device and Agent tests; macOS also runs desktop tests. Unix socket isolation is unavailable on the fixed Windows pipe, so Windows runs the in-memory RPC suite instead.
These contributions contain individual read operations, not full discovery sessions. Agent tests publish an opened recorded route as fixture setup; separate synthetic enumeration tests cover discovery and reconnect topology. The corpus replay helper rejects multi-device and raw-HID profiles instead of guessing an operation target. Firmware cases remain excluded pending privacy classification. Mock writes, offline transitions, and replay do not establish physical write behavior, gestures, haptics, radio reconnects, or OS permissions.
Contribute a device fixture
Use the contribution wizard instead of writing manifest.json, synthetic
identities, case relationships, or occurrence counts by hand:
openlogi fixture contribute \
--id mx-master-3s-001 \
--name "MX Master 3S" \
--device "MX Master 3S" \
--output fixtures/devices/mx-master-3s-001The first run talks only to the running Agent and writes a privacy-safe semantic
profile plus resumable state. It then asks you to stop the Agent and rerun the
same command. The second run uses the CLI's own HID permission to capture all
eight supported read-only operations, self-replays them, generates the exact
identity ledger and case relationships, and runs strict on-disk verification.
Nothing is uploaded automatically.
Before selecting a target, direct discovery may enable wireless notifications
and request arrival reports on connected receivers. Notification flags are not
restored. The CLI warns before discovery; captured operations do not change
device settings or pairings.
Use --profile-only when direct HID access is unavailable. Standalone raw-HID
devices automatically produce profile-only fixtures because the cassette
format covers HID++, not raw device writes. A profile-only contribution is
still useful for the mock Agent and desktop tests.
Keep the same physical device connected between both runs. On macOS the Agent
and CLI are separate Input Monitoring identities, so permission granted to one
does not grant it to the other. The wizard deliberately does not add raw
recording to Agent IPC.
The small built-in synthetic profile is packaging data rather than captured
corpus. It stays undercrates/openlogi-fixture/fixtures/devices/openlogi-canonical-synthetic-001/ so
the published openlogi-fixture crate and mock agent remain self-contained.
It intentionally declares no recorded cases or hardware provenance.
MX Master 4 capture
devices/mx-master-4-001 was captured on macOS on 2026-10-10 through a Bolt receiver (046d:c548, slot 2). The mouse reports model ID 0xb042. The semantic profile came from CLI and Agent 0.8.11; the HID++ cassettes came from the repository CLI 0.8.13. Receiver and device identities are synthetic.
The fixture contains seven cases: feature table, reprogrammable controls, raw battery, DPI, SmartShift, wheel mode, and unsupported backlight state. The DPI capture reports 2000 DPI with a supported range of 200–8000 in steps of 50. The control table has nine entries, including separate gesture (0x00c3) and haptic panel (0x01a0) controls. Regression tests replay DPI and control reads through the production operations and require complete cassette consumption.
The firmware-entities case is excluded because nonzero extra_version bytes and entity flags beyond the active bit remain unclassified. The generated manifest retains the seven accepted cases and their existing identity occurrences. This capture does not verify setting writes, haptic output, gestures, or physical wheel behavior. The Agent and Overlay were restored after capture.
MX Anywhere 3S capture
devices/mx-anywhere-3s-001 was captured on macOS 26.4 on 2026-10-10 through Bluetooth LE (046d:b037). The semantic profile came from CLI and Agent 0.8.11; the HID++ cassettes came from the repository CLI 0.8.13. Device identities are synthetic.
The fixture contains seven cases: feature table, reprogrammable controls, raw battery, DPI, SmartShift, wheel mode, and unsupported backlight state. All 65 exchanges use 20-byte long reports. The DPI capture reports current and default values of 1000 DPI, with a supported range of 200–8000 in steps of 50. The control table has seven entries, including a physical control (0x00c4) with raw XY and analytics flags and a virtual control (0x00d7) with forced raw XY. Regression tests replay DPI and control reads through the production operations, exercise Bluetooth short-to-long request widening, and require complete cassette consumption.
The firmware-entities case is excluded because nonzero extra_version bytes remain unclassified. The generated manifest retains the seven accepted cases and removes the excluded case's unit-ID occurrence. This capture does not verify setting writes, gestures, physical wheel behavior, or reconnects. The Agent and Overlay were restored after capture.
MX Master 3S capture
devices/mx-master-3s-001 was captured on macOS 26.7.1 (25G241) on 2026-10-10 through Bluetooth LE (046d:b034). The semantic profile came from CLI and Agent 0.8.11; the HID++ cassettes came from the repository CLI 0.8.13. Device identities are synthetic.
The fixture contains seven cases: feature table, reprogrammable controls, raw battery, DPI, SmartShift, wheel mode, and unsupported backlight state. All 68 exchanges use 20-byte long reports. The DPI capture reports current and default values of 1000 DPI, with a supported range of 200–8000 in steps of 50. The control table has eight entries, including a gesture control (0x00c3, task 0x00a9) with raw XY and analytics flags and a virtual control (0x00d7) with forced raw XY.
This specimen exposes legacy SmartShift (0x2110): ratchet mode, an auto-disengage threshold of 10, and no tunable torque. Regression tests replay SmartShift and control reads through the production operations and require complete cassette consumption. The SmartShift case covers two missing-0x2111 probes followed by the 0x2110 read.
The firmware-entities case is excluded because two entities contain nonzero extra_version bytes that remain unclassified. The generated manifest retains the seven accepted cases and removes the excluded case's unit-ID occurrence. This capture does not verify setting writes, gestures, physical wheel behavior, reconnects, or alternate transports. The GUI, Agent, and Overlay were restored after capture.
Overview (`xtask/README.md`)
OpenLogi xtask
xtask is the repository-level entry point for development tasks that need Rust
or cross-language orchestration. Run it from the repository root:
devenv shell -- cargo xtask <command>
# or, without the cargo alias:
devenv shell -- cargo run -p xtask -- <command>Commands
ci [--list] [--dry-run] [JOB…]— reproduce theci.ymljobs this host can
run; a job it cannot is skipped with a reason, never passed.macos icon— compiledesign/icon/openlogi.iconinto theAppIcon.icnsandAssets.carundercrates/openlogi-desktop/icon/.macos bundle [--channel dev|production]— buildOpenLogi.appand embed the
agent and overlay helpers.macos dev-bundle --binary— wrap a freshly built desktop binary intarget/dev/OpenLogi.app. Driven by the Cargo runner, not run by hand.macos dmg— package an existing app bundle into the branded DMG.macos package— build the app bundle, embed Sparkle when given its public key,
optionally sign it, then create the branded DMG.linux package— build release binaries and package.deb,.rpm, and.pkg.tar.zstartifacts with nfpm.lint sweep-allows [--out ]— remove each#[allow]in turn, run
clippy, and record which of its lints fire on this host.lint merge-allows <reports…>— join the per-host reports and fail on a lint
that fires on none of them.stale-allows.ymlruns both weekly.release changelog— write the next workspace version's section intoCHANGELOG.mdwith git-cliff.release check-publish— verify that every crates.io package has a publishable,
versioned workspace dependency closure.release checkout-version-bump— pin a release job to the commit that
introduced the current workspace version.release latest-json— write a release's static updater manifest into its
feed record.release appcast— sign the macOS DMGs and their Sparkle deltas with
Sparkle's EdDSA key and write one single-item appcast per architecture into
the release's feed record.release channels— compose every channel'slatest.jsonand appcasts
from the release records on the update host, and print what each channel
offers as GitHub Actions outputs.release next-beta --release-ref <commit>— name the beta tag to cut onHEAD: the version that<commit>, the open release PR, prepares, and the
count of commits since the stable release before it, checked asrelease versionchecks the pushed tag. It reads committed files, not the
working tree, and needs the whole history and everyv*tag; it refuses a
shallow repository. The prerelease workflow runs it and creates the tag.release sparkle-deltas— make verified Sparkle deltas from the newest
earlier stable releases to a stable release's stapled DMG, with the metadatarelease appcastreads. A beta gets none.release stamp-version— write a version into the workspace manifests and
the workspace crates inCargo.lockbefore a build, uncommitted, so a beta
builds at its own version.release version— print a release tag's channel, version, build version,
the version its build stamps (empty for a stable tag) and the previous tag
its release notes compare with as GitHub Actions outputs, after checking the
tagged commit's workspace version: the tag's for a stable tag, and the
stable release it follows for a beta.
Bundle identity
macOS keys TCC grants to a bundle's code identity, and OpenLogi keys its config
profile to the identifier's suffix — so the identity decides whose permission
grants and whose settings a build inherits. Every bundle therefore gets it
written explicitly and read back:
| channel | app | agent helper | overlay helper |
|---|---|---|---|
production |
org.openlogi.openlogi / OpenLogi |
org.openlogi.agent / OpenLogi Agent |
org.openlogi.overlay / OpenLogi Overlay |
dev |
the same, suffixed -dev / Dev |
macos bundle defaults to --channel dev, so a local build can never claim the
installed app's grants, and signs the result with OPENLOGI_LOCAL_CODESIGN_IDENTITY
or the first Apple Development identity it finds (OPENLOGI_LOCAL_CODESIGN=0
leaves it unsigned). macos package always builds the production channel and
signs with OPENLOGI_SIGN_IDENTITY / --sign-identity; macos dmg refuses to
package anything but a production bundle once it is given a signing identity, so
a dev build cannot reach users. That check is what releases 0.6.24–0.6.26 lacked
when the release workflow shipped .dev identifiers.
macos package --agent-provisioning-profile (OPENLOGI_AGENT_PROVISIONING_PROFILE)
takes the agent's Developer ID provisioning profile, which grantscom.apple.developer.hid.virtual.device to org.openlogi.agent. It needs--sign-identity. bundle/provisioning.rs checks the profile before the
build, embeds it as the agent's Contents/embedded.provisionprofile, signs the
agent with exactly the entitlements the virtual HID keyboard needs, and then
checks that the profile includes the signing certificate. Without a profile,
the agent claims no entitlements: AMFI kills a process that claims a
restricted entitlement without its profile. The release workflow reads the
profile from 1Password (APPLE_AGENT_PROVISIONING_PROFILE, base64).
The profile lists the Developer ID Application certificate that signs the
release. provisioning.rs refuses a profile that has expired or that does not
list the signing certificate, so a stale profile fails the build. The current
profile expires on 2044-10-15, and its certificate expires on 2031-01-20. When
either is renewed:
- In the Apple Developer portal, create a Developer ID profile for the
org.openlogi.agentApp ID with the current Developer ID Application
certificate. The App ID must keep the HID Virtual Device capability. - Put the profile's base64 in the
APPLE_AGENT_PROVISIONING_PROFILEfield of
the 1Password item thatOP_APPLE_SECRET_ITEMnames. - If the certificate was renewed, update the same item too, because CI
imports the certificate separately:APPLE_CERTIFICATE(the base64 of the
new certificate and private key, exported as PKCS#12),APPLE_CERTIFICATE_PASSWORD, andAPPLE_SIGNING_IDENTITYif the identity
name changed. - Before the next release, run a signed build on a branch
(gh workflow run build.yml --ref -f sign=true) and check that its
macOS jobs pass.
macos bundle asks cargo-bundle for the base .app only, then embeds and signs
the helpers. Use macos package when you need the final DMG. The package command
also accepts --target aarch64-apple-darwin or --target x86_64-apple-darwin so
CI can cross-compile either distribution architecture.
Sparkle
Given OPENLOGI_SPARKLE_PUBLIC_ED_KEY (or --sparkle-public-key), macos package
embeds the pinned Sparkle release (bundle/sparkle.rs: version and archive
SHA-256, cached under target/sparkle/) as Contents/Frameworks/Sparkle.framework,
drops its XPC services (OpenLogi is not sandboxed), and stamps the SU* keys
into Info.plist. The feed URL comes from support/update_feed.rs: one appcast
per architecture under OPENLOGI_UPDATE_BASE_URL, because each architecture
ships its own DMG. release appcast and release channels write those feeds
from the same module, so the URL a bundle reads and the file a release
uploads cannot drift apart. The module also owns where a release's files and
feed record live on the update host.
Sparkle orders releases by CFBundleVersion. When supplied, --build-version
(from release version) sets CFBundleVersion; otherwise it uses the crate
version. CFBundleShortVersionString always uses the crate version. A stable
release's build version is its version; support/release_tag.rs owns the
encoding.
Signing re-signs Sparkle's Autoupdate, Updater.app and the framework, inside
out, before the app seals them. Without the key the bundle has no Sparkle and
the app updates through its built-in engine; PR builds and unsigned
dispatches are built that way.
The dev bundle
macos dev-bundle assembles target/dev/OpenLogi.app around the binary Cargo
just built, so cargo run -p openlogi-desktop gets an app name, a Dock icon, anopenlogi:// registration and a stable signed identity. It reuses everythingmacos bundle uses — the identity table, the helper table, the Info.plist
templates — which is the point: the two were separate implementations until
they drifted far enough that the dev overlay shipped without an icon and the-dev rename had to be made twice.
It also stops the dev agent and overlay an earlier run left behind. Those are
launched through LaunchServices for their own TCC identity, so they are not
children of the GUI and survive both closing its window and Ctrl-C.
../.cargo/run-macos.sh stays a shell script — Cargo execs it for every binary
of every cargo run/test/bench, so the passthrough must not pay for an
interpreter start — but it now does nothing except that passthrough and calling
this command.
Layout
/* Detailed source-code truncated for AI context efficiency. */Unit tests are a sibling file throughout this crate: foo.rs declares#[cfg(test)] mod tests; and the tests live in foo/tests.rs. That keeps a
module's source to what it does, and the #[cfg(test)] on the declaration is
what carries the Clippy unwrap/expect test exemption into the file — a test
helper outside any #[test] fn still gets it. Note that include_str! in such
a file resolves relative to foo/, one level deeper than the module it came
from.
A test that reads a file from the repository has one more constraint: the Nix
package builds from a source derivation that deliberately omits documentation
and CI metadata — editing a workflow must not rebuild the application — and it
runs cargo test inside that sandbox. Either add the file to the fileset inpackaging/linux/package.nix, the way nfpm.yaml is there for the packaged-bins
test, or let the test skip when the file's whole directory is absent, the way
the ci.yml drift tests do.
Keep command modules aligned with the CLI hierarchy. A platform action belongs
under its platform (macos bundle, linux package); release metadata belongs
under release; shared helpers belong in support only when they are reused by
multiple commands or handle real error/resource boundaries.
Maintenance rules
- Use
xshellfor short-lived external tools such ascargo,create-dmg,codesign, andnfpm. - Use
std::process::Commandonly when a command needs explicit process
lifetime, streaming, or stdout/stderr control. - Use crates for structured data and platform-neutral formats:
serde_jsonfor
JSON,plistfor plist files,timefor timestamps, hashing crates for
digests, andtempfilefor temporary directories. - Do not shell out just to avoid a small, appropriate Rust dependency.
- Do not reintroduce thin wrappers around tasks already owned by a dedicated
package script, Cargo subcommand, or external tool. - Inline single-use helpers unless the name captures a durable domain concept,
hides meaningful resource handling, or reduces repeated complexity.
Overview (`crates/openlogi-hid/README.md`)
openlogi-hid
HID++ device discovery and control helpers for OpenLogi.
This crate is the OpenLogi-oriented HID layer built on top ofasync-hid and the workspace's vendoredopenlogi-hidpp protocol crate. It
owns device enumeration, receiver routing, shared-channel transport setup, and a
small set of typed operations used by the CLI, agent, and GUI.
Use this crate when OpenLogi needs to talk to Logitech HID++ devices through the
host HID stack. Use openlogi-hidpp directly when implementing protocol-level
feature support that is independent of OpenLogi's discovery and transport
policy.
Public entry points include:
enumeratefor a one-shot inventory of receivers and paired devices.list_pairing_receivers,run_pairing, andunpairfor receiver pairing.get_dpi,set_dpi, SmartShift, high-resolution wheel, thumbwheel, and
reprogrammable-control helpers for supported HID++ features.set_keyboard_color/set_keyboard_color_withfor solid keyboard RGB —
preferring the typedColorLedEffects(0x8070) wrapper and falling back to
thePerKeyLighting(0x8080) stream.dump_featuresanddump_reprog_controlsfor diagnostics.
Protocol-level feature support lives in openlogi-hidpp; this crate adds the
discovery, routing, fallback, and error-classification policy on top.
Overview (`crates/openlogi-hidpp/README.md`)
openlogi-hidpp
OpenLogi's vendored fork of the hidpp crate —
an implementation of the Logitech HID++ protocol.
- Upstream: https://github.com/lus/logy (crate
hidpp) - Forked at: commit
135c5600807845c269b5d5bfa1f33032281fbd86(upstream v0.3.0, 2025-12-26) - License: 0BSD © Lukas Schulte Pelkum (see
LICENSE)
The library target is named hidpp, so dependents use hidpp::… unchanged.
OpenLogi-specific changes live here; the source is otherwise kept close to
upstream to ease future syncs.
The crate is versioned with the OpenLogi workspace (unified versioning), not
upstream's 0.3.0 — that number is provenance, recorded above.
Feature coverage
Beyond upstream, this fork adds typed wrappers for a broad set of HID++ 2.0
features. Each is registered in feature::registry and obtained viadevice.get_feature::<…>(); implemented areas include:
- Device & power — Root, FeatureSet, DeviceInformation, DeviceTypeAndName,
DeviceFriendlyName, UnifiedBattery, WirelessDeviceStatus. - Hosts & platform — HostsInfo, ChangeHost, MultiPlatform, DualPlatform.
- Pointer & wheel — MousePointer, AdjustableDpi, ExtendedAdjustableDpi,
VerticalScrolling, HiResWheel, Thumbwheel, SmartShift (and enhanced). - Controls & remapping — ReprogControls (
0x1b04, with named control-id and
task-id constants), PersistentRemappableAction. - Keyboard — Fn inversion (legacy and multi-host), DisableKeys,
DisableKeysByUsage, ModeStatus. - Lighting — Backlight, Illumination, BrightnessControl, ColorLedEffects,
RgbEffects, PerKeyLighting. - Audio — Sidetone, Equalizer.
- Report rate — AdjustableReportRate, ExtendedAdjustableReportRate.
- Touch, crown & misc — Crown, TouchpadRawXy, TouchMouseRaw,
SolarKeyboardDashboard.
Each wrapper encodes/decodes the official wire format, models domain values with
enums/bitflags/newtypes, returns Hidpp20Error::UnsupportedResponse for unknown
wire values rather than guessing, and is unit-tested against the published spec.
Disable Keys integration
DisableKeys (0x4521) is used beyond this protocol wrapper: OpenLogi's device
layer reads the supported mask, preserves advertised unknown disabled bits, and
confirms writes by reading them back. The agent serializes manual and lifecycle
writes on the inventory-owned channel. The desktop's capability-gated Device
card saves only confirmed known keys. See Disabled keys configuration and
recovery for supported controls,
persistence, and reconnect behavior. DisableKeysByUsage is a separate feature;
this card does not provide arbitrary key remapping.
Overview (`crates/openlogi-hook/gnome-shell-extension/README.md`)
OpenLogi Frontmost Window — GNOME Shell extension
GNOME (Mutter) does not let ordinary clients see which window is focused on
Wayland, and it implements neither wlr-foreign-toplevel nor a focused-window
portal. This minimal extension bridges that gap: it exports the WM_CLASS of the
focused window over D-Bus and emits every native focus change so OpenLogi'sgnome-shell backend can drive per-app mouse-profile switching without
polling.
It reads only global.display.focus_window.get_wm_class(). No titles, no window
contents, no input, no UI.
D-Bus surface
- name:
org.openlogi.Frontmost - path:
/org/openlogi/Frontmost - method:
GetFocusedWmClass() -> s(empty string when nothing is focused) - signal:
FocusedWmClassChanged(s)(emitted on each GNOME focus change)
Install
[email protected]
DEST="$HOME/.local/share/gnome-shell/extensions/$UUID"
mkdir -p "$DEST"
cp metadata.json extension.js "$DEST"/On Wayland the shell cannot be reloaded in place, so log out and back in to
let GNOME pick up the newly added extension, then enable it:
gnome-extensions enable "$UUID"
gnome-extensions info "$UUID" # State should be ACTIVEVerify
# Introspect the service:
busctl --user introspect org.openlogi.Frontmost /org/openlogi/Frontmost
# Focus a window, then query it:
gdbus call --session \
-d org.openlogi.Frontmost \
-o /org/openlogi/Frontmost \
-m org.openlogi.Frontmost.GetFocusedWmClass
# Watch focus changes:
gdbus monitor --session \
-d org.openlogi.Frontmost \
-o /org/openlogi/FrontmostIf the call prints the focused window's WM_CLASS and the monitor reportsFocusedWmClassChanged while switching windows, OpenLogi's GNOME backend will
pick it up automatically the next time the hook starts.
Notes
- The
shell-versionlist inmetadata.jsoncovers GNOME 45–50. Newer GNOME
releases may need an added entry; the API used here (Gio.DBusExportedObject,global.display.focus_window,Meta.Window.get_wm_class) has been stable
across these versions. - The extension name/UUID and the D-Bus name (
org.openlogi.*) are placeholders
that should track the project's namespace; if they change, update the matching
constants incrates/openlogi-hook/src/linux/gnome_shell.rs.
CODE OF CONDUCT (`docs/CODE_OF_CONDUCT.md`)
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:
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 (`docs/CONFIGURATION.md`)
Configuration
OpenLogi stores settings as plain TOML. The GUI and agent read the same file:
- macOS and Linux:
$XDG_CONFIG_HOME/openlogi/config.toml(normally~/.config/openlogi/config.toml) - Windows:
%USERPROFILE%\.config\openlogi\config.toml
The complete, tested example is config.example.toml.
Copy only the sections you need and replace its example physical device keys
with keys already written by OpenLogi for your devices.
Editing and recovery
The GUI writes atomically and keeps config.toml.backup.1 throughconfig.toml.backup.5. Existing comments and formatting are retained when the
GUI updates known fields.
When config.toml is a symbolic link, saves update its target and preserve
the link. A missing target and its parent directories are created. Backups stay
beside the configured config.toml, while a persistent .lock file stays beside
the target so different links to the same file share the writer lock.
The schema is strict: misspelled, obsolete, and out-of-range fields stop the
config from loading instead of silently selecting a default or disappearing on
the next save. The GUI then opens in read-only mode and shows the exact TOML
error. Fix the file and relaunch OpenLogi.
If the file changes in an editor while the GUI is open, the next GUI save is
refused rather than overwriting the external edit. Relaunch to load that
revision. Opening the GUI also tells the resident agent to reload the current
file, so hand edits and runtime behavior converge immediately.
Schema versions newer than the running build are rejected before their fields
are parsed. v1 binding maps and the v2–v3 gesture-owner layout migrate on load.
The v3 physical-device-key transition cannot safely assign older model-scoped
device settings when two identical devices exist, so v2 model-key entries must
be copied manually to the generated physical keys. Pre-v7 thumb-wheel scroll
pairs on the old defaults are migrated in both device and per-application
profiles so they keep their native direction. v9 adds the [apps] alias table;
v8 files load unchanged.
v10 adds the per-device disabled_keys policy. Older readers reject a v10
header before parsing its fields. A v9 file without the policy still loads.
Shape
schema_version is required and currently 10. selected_device is an
optional physical device key.
[app_settings] contains application-wide preferences:
- startup, update, menu-bar / tray, input-capture, and asset-download toggles
asset_source:automatic,openlogi,cloudflare, orfastlylanguage,appearance,device_view_mode(grid,list, orcarousel),
optional theme names, and optional UI radiussmooth_scrolltoggles finite animation for traditional mouse-wheel input. With it
off, a diverted horizontal thumb wheel still scrolls as a phased gesture (ended after
120 ms idle) so apps that reveal content on horizontal swipes, like Messages, react;
vertical wheel output stays a plain wheel. A mouse whosesmooth_wheelis on
eases its main wheel either wayvertical_scroll_sensitivity, from1through100(14is 1×);
continuous trackpad input remains nativethumbwheel_sensitivity, from1through100(14is 1×)mouse_profile_target:pointer(default, including existing configs that
omit this preference) orfocused. Mouse button profiles follow the window
under the pointer; the desktop uses the global bindings. Keyboard profiles
continue to follow the focused application. Pointer targeting is supported
on macOS, Windows, and X11; unsupported sessions such as Wayland use the
focused application, and so does a pointer over a surface OpenLogi cannot
identify, such as a system overlay, or a lookup that fails. An unidentified
surface uses the focused application's profile rather than the desktop's;
with no focused application there is no such profile and the global bindings
apply, as in focused mode. OpenLogi never activates a background window to
send a shortcut: over an identified window or the desktop, mouse
bindings that produce keystrokes or run workflows are skipped unless the
hovered window is focused. Global actions such as desktop switching can run
without changing application focus.low_battery_notifications(defaulttrue): the agent shows a notice at the
bottom of the screen when a connected device's battery runs low. Low means
20% or less while the device is not charging, or a low or critical level from
a device that reports no percentage. Each discharge is announced once. The
device is announced again only after it charges, reads 30% or more, or
reports a good or full level. A notice closes after 8 seconds or when it is
clicked. With the setting off the agent posts no notice, but it still notes
when a device charges, so the next discharge is announced once the setting
is back on. A notice already on screen still closes on its own.lock_key_notifications(defaulttrue): when a keyboard with the HID++LockKeyStatefeature (0x4220) reports Caps Lock turning on or off, the
agent shows an indicator in the centre of the screen for 1.5 seconds. On
Windows and Linux it does the same for Num Lock and Scroll Lock. macOS gives
those two keys no meaning, so they show nothing there. A change replaces the
one on screen. The state a keyboard reports when it connects is not
announced. With the setting off the agent shows nothing, and an indicator
already on screen still closes on its own. Fn Lock is not shown.animate_desktop_switch(defaulttrue, macOS only): Previous Desktop and
Next Desktop slide to the neighbouring Space. The agent types the "Move left
a space" or "Move right a space" shortcut from System Settings → Keyboard →
Keyboard Shortcuts → Mission Control, so the Dock plays the slide it plays
for the keyboard. When that shortcut is off, the agent posts the slide the
way a three-finger swipe does: 20 frames, one every 16.6 ms, and each moving
frame covers 15% of the distance left; the Dock finishes the slide. That
action returns when the last frame is posted, about 315 ms later. With the
setting off, the agent posts two frames and the Dock changes Space at once.
A change applies from the next switch. Windows and Linux ignore the setting.
[devices.""] contains per-device state. Receiver keys look likereceiver:<receiver-id>:slot:<number>; direct, raw-HID, and camera devices use
other generated keys. Do not substitute a model id such as 2b042.
A camera without a USB serial has no unique port-stable identity. Itscustom_name key therefore follows the OS capture id so two same-model cameras
remain distinguishable; moving it to another USB port may require naming it
again.
Common device fields are:
custom_name,enabled,dpi,dpi_presets, thumb-wheel sensitivity,
scroll inversion, and scroll resolutionsmooth_wheelfor mice with a HID++0x2121wheel:truesmooths that
mouse's main wheel in software, as Logi Options+ "Smooth scrolling" does,
whateversmooth_scrollsays. The agent switches the wheel to diverted
reporting and posts eased scrolling for each report, applying the mouse's
scroll inversion andvertical_scroll_sensitivity. It needs the input access
the mouse hook needs (Accessibility on macOS) andcapture_mouse_eventson;
without them the wheel stays native. The agent hands the wheel back to native
reporting when the switch goes off, the mouse is disabled, or the agent
exits. If the agent crashes first, the wheel does not scroll until the agent
runs again (the launch-at-login service restarts it) or the mouse reconnects,
which drops the diversion. The desktop's Scrolling card offers it as
Smooth wheel when the current connection has0x2121; a smooth wheel
turned on over another connection can still be turned off there. Absent orfalseleaves the wheel nativebindings: a button maps to one action, an independent short/long action
pair, or a gesture-direction map.Thumbwheelis the thumb wheel's capacitive tap — it has no GUI control and
stays inert unless bound here, because the wheel reports taps from incidental
thumb contact as well as from deliberate ones.
Keyboard keys are named by their HID++0x1b04control: a catalog name such
asKeyScreenCapture,KeyCalculatororKeyLock, orcontrol:0x<cid>for
any controlopenlogi diag controlsreports as divertable. A bound key is
diverted to OpenLogi while the agent runs; an unbound key keeps its firmware
function, so bindingNoneis the same as removing the entry. Naming one
control under both spellings in the same table is an error, not a mergeper_app_bindings: sparse action overlays keyed by macOS bundle id (or the
exact executable path for applications without a bundle id), Linux application
id, exact lower-cased Windows executable path, orexe:<filename>.exe. macOS
paths retain their original spelling and case. The Buttons panel edits these
under its Profile selector. After bringing an application to the foreground,
choose it from Recent applications to use the identifier the agent observed.
Identifiers differ between platforms, so a profile keyed by one of them does
not match on another. Key the profileapp:<alias>instead to share it
across platforms (see Application aliases). An
overlay holds one action per button; gesture-direction maps live inbindingsaction_ring: default and complete per-application eight-slot layouts;action_ring.per_apptakes the same selectors asper_app_bindings,
including the Windowsexe:<filename>fallback andapp:<alias>keyslighting,smartshift, and camera controls / profiles- standalone
light: the agent writes it when the light connects, when a save
changes it, and, withauto_cameraon, when camera use starts or stops.
Saving another setting leaves a change made with the light's own buttons in
place. On a light the agent reaches over HID++0x1990, a change made with
its buttons or by another program also replaces the saved value while the
agent runs, so a reconnect writes the new value back, as in Logi Options+.
The agent publishes the change to clients and keeps it until a save or a
write from OpenLogi sets that control; a write that fails leaves it, unless
a reload during the write replaced it. The desktop app saves the change
as the light's setting. While one of its own
writes to that light is in flight, it waits for the write to settle and
saves only what the agent still reports. It writes each control it changes
before it saves it, so its save of a value the light reported earlier leaves
a newer change made on the light in place. A control set without a write, as
on an offline light, replaces the change. A hand edit of this file, or an
edit with the CLI, is a new choice, even when it matches an earlier change
on the light. That holds as well for a hand edit made after a save of the
app and before the agent reloads that save: the agent finds the file
changed since the save. Withauto_cameraon, a power change on the light
is a manual choice until the next camera transition, like the power switch
in the settings app host_switch_targetsfor keyboards with Easy-Switch keys (HID++0x1814):
the config keys of the mice and other devices that move with the keyboard
when it switches hosts. A linked device moves to its slot whose host name
(HID++0x1815) matches the keyboard's new slot, and otherwise to the slot
with the same number. The agent also writes the linked device's0x1814
cookies, so that Logi Options+ on the keyboard's other hosts moves the same
devices. It does not replace a cookie that names another keyboard. Removing
a link clears the keyboard's cookie. When the device is offline or the clear
fails, the agent clears it when the device is next online, unless the agent
restarts first. A disabled device gets no cookie writes. When it is enabled
again, the agent writes the cookie changes that it skipped. The agent
remembers each keyboard's cookie while it runs, so it can also write or
clear a link when the keyboard is asleep or its receiver is unplugged. After
a restart, the agent knows the cookie again once the keyboard reports itfn_lockfor compatible keyboardsdisabled_keysfor keyboards with HID++0x4521: the keys the keyboard
suppresses in hardware (see Disabled keys). Absent leaves
the setting unmanaged; an explicit empty array enables all supported known
keyskeep_os_layoutfor keyboards whose key layout software can set (HID++0x4531): absent ortruekeeps the layout on the host OS, as Logi Options+
does;falseleaves the keyboard's layout to its own shortcutsreport_ratefor mice with an adjustable report rate (HID++0x8060or0x8061), in hertz. The agent writes it when the mouse reconnects over a
connection that offers that rate; absent leaves the mouse's own ratepointer_speedfor mice that scale their own pointer motion instead of
adjustable DPI (HID++0x2205, for example the MX Ergo and the M590), in
1/256 steps:256is 1×, and the value must be in32..=768(0.125× to
3×), the range Logi Options+ writes. The agent writes it when the mouse
connects; absent leaves the mouse's own speed. The desktop app sets it with
the Pointer speed slider, where 50 % is 1×force_sensingfor devices with a force-sensing button whose press force
software can change (HID++0x19c0, the MX Master 4's Haptic Sense Panel),
as a raw device value with no unit: lower is a lighter press. The range
differs from unit to unit (one MX Master 4 reports4071..=6768with a
default of5089), so the agent clamps the value into the range the device
reports, then writes it when the device connects; absent leaves the device's
own force, and the agent never writes one. The desktop app sets it with the
Press force slider in the Haptic Sense Panel's inspector on the mouse page,
which saves the force the device reads backidentityanddisabled_gestures, which are application-managed metadata
[keyboard.bindings] contains global key triggers such as f1 orshift+command+f5. Supported trigger modifiers are shift, control,option, and command; aliases such as ctrl, alt, and cmd are accepted.
Disabled keys
The Device tab shows a Disabled keys card only when the device advertises
HID++ 0x4521. It reads the supported mask from the keyboard and offers only
the supported keys: Caps Lock, Num Lock, Scroll Lock, Insert, and Windows /
Command (labelled for the host OS). A switch on disables that key. This is
a firmware setting, not key remapping: disabling Caps Lock also suppresses a
host-side action assigned to it, such as switching the input language.
For example, under an existing physical-device entry:
disabled_keys = ["caps_lock", "windows_command"]The other accepted names are num_lock, scroll_lock, and insert. Removing
the field stops managing this setting; disabled_keys = [] instead requests
that every supported known key be enabled. OpenLogi preserves advertised
disabled bits it does not recognise, rather than clearing firmware settings
outside these five controls.
Controls require an online device with a persistent identity, a writable
configuration, and a successful read. A change is written to the keyboard and
read back before the app saves the confirmed known keys and asks the agent to
reload. An error or timeout is not proof that the keyboard stayed unchanged:
the app discards its old value, reads again, and prevents further edits until
that read succeeds. Retry retries an exhausted read. An unconfirmed request
is never saved as the device's setting. Save Retry and Reload Retry
recover their respective persistence steps without repeating the hardware
write. Save Retry rereads the configuration to preserve external edits and
refreshes the app's settings and binding displays from that revision, even if
the subsequent save must be retried again.
Reload acceptance is tied to the saved file revision of each change, even when
several requests share one agent reload. Acceptance of a newer save does not
confirm an older one. If the file changed externally, the app refreshes it
without overwriting it, requests acceptance of that exact revision, and then
reads the keyboard again before editing resumes. This also covers another edit
between the agent's reload and the app's refresh. It applies to every keyboard,
including when Save Retry or a save of another setting finds the external edit.
A failed refresh or agent reload keeps editing blocked; Reload Retry resumes
this reconciliation. A device read alone cannot
resolve an unconfirmed file revision.
Recovery stays inside the device card, using the same status and retry controls
as other device settings. Messages follow the selected interface language;
technical read, write, and configuration errors are recorded in the app logs.
Switches and recovery actions retain identity per physical device, not per
translated label or connection route. Each switch's accessible name states the
disable action and the key.
The agent reapplies a managed device's configured policy on initial discovery,
reconnect, and wake, with bounded delayed confirmation attempts. If an
unconfirmed manual write may have changed hardware, it schedules restoration
of the latest confirmed choice (including a choice not yet saved by the app),
without overtaking a newer pending request. Once a restoring write is queued,
the agent moves the keyboard's published settings revision, so an open app reads
the keyboard again after that write. New configuration or a retired
connection invalidates obsolete work. Failed inventory scans do not count as
successful confirmation: they use bounded delayed retries, retaining pending
confirmation for the normal recovery scan after those retries are exhausted.
An unreachable keyboard cannot be guaranteed to accept a setting; reconnect
or wake starts a new lifecycle attempt.
Manual writes retain their setting turn, receiver lease, and transport turn
until each submitted native report completes, including discovery and readback
reports. Cancellation is checked before submitting the next report. A completed
write becomes the recovery baseline only when the requester consumes its
successful result; an unread result is still unconfirmed. Reads of this setting
queue behind already scheduled policy writes, including workers that have not
started running yet. Ordinary standalone reads still use bounded query timeouts.
A native driver operation that never completes can therefore keep subsequent
writes waiting; a requester timeout does not make it safe to release that turn.
A request cancelled before its first report leaves the queue at once and
releases the turns and lease it was waiting for.
When a receiver slot or a directly connected keyboard reports a different known
unit ID or serial, the agent retires the former device's requests and starts a
fresh discovery retry budget. Learning previously missing identity information
for the same slot, or a serial for a direct keyboard with the same nonzero unit
ID, instead preserves active work and its remaining budget. Replacement cannot
be detected from a slot alone when neither snapshot provides distinguishing
identity, and a direct keyboard without a nonzero unit ID counts as new when its
identity changes.
Application aliases
[apps.<alias>] names one application on every platform, so one per-app
profile can follow it from macOS to Linux and Windows. An alias name uses only
ASCII letters, digits, -, and _. Its fields are:
name(optional): what the GUI calls the application. Without it, the GUI
uses the name the agent reports for the application, then the alias name.macos,linux, andwindows(each optional): the identifiers that
platform reports for the application, as one string or an array. They take
the same forms asper_app_bindingskeys. Windows identifiers are
lower-case, andexe:<filename>.exematches every install path.
[apps.chrome]
name = "Google Chrome"
macos = "com.google.Chrome"
linux = ["google-chrome", "Google-chrome"]
windows = "exe:chrome.exe"
[devices."<physical-key>".per_app_bindings."app:chrome"]
Back = "PrevTab"A profile keyed app:<alias> in per_app_bindings or action_ring.per_app
applies when the application in front has one of the alias's identifiers for
the running platform. On a platform the alias lists no identifier for, the
profile stays in the file and does not apply.
The most specific key wins, in this order:
- the exact identifier
- the Windows
exe:<filename>key - the alias that lists the exact identifier
- the alias that lists
exe:<filename>
A key in the platform's own namespace therefore wins over an alias profile for
the same application.
The config stops loading when an app: key names an alias that [apps] does
not define. It also stops loading when two aliases list the same identifier
for one platform, or when an identifier is empty, starts with app:, or is a
Windows identifier with an upper-case letter.
The desktop app shows an alias profile under the alias's name and the icon of
its first identifier on this platform. It does not offer an application in
Recent applications or All applications when that application already runs
under a profile. The desktop app does not create aliases; write them here.
Actions
Action names are the serialized Rust variant names, including Copy,BrowserBack, PlayPause, CycleDpiPresets, and ShowActionsRing.
TogglePrecisionMode switches a mouse between its saved speed and its
slowest speed, as in Logi Options+:
| Mouse | Slowest speed | Saved speed | When none is saved |
|---|---|---|---|
Has 0x2205 and no adjustable DPI, such as the MX Ergo |
0.125× | pointer_speed |
1× |
Has adjustable DPI (0x2201 / 0x2202) |
The lowest DPI the sensor lists | dpi |
The sensor's default DPI |
CycleDpiPresets also switches precision mode on a 0x2205 mouse, because
that mouse has no adjustable DPI; DPI presets saved for it are ignored. This
makes the MX Ergo's precision button work with its default binding. On a mouse
with adjustable DPI, CycleDpiPresets keeps cycling the DPI presets.
A mouse with a DPI LED (HID++ 0x1300), such as the MX Ergo, shows the mode
on that LED, as in Logi Options+. The LED lights when precision mode starts
and goes dark when it ends. OpenLogi takes control of the LED from the
firmware the first time precision mode starts, and keeps it. A mouse that
never uses precision mode keeps the LED behavior of its firmware.
Precision mode stays on when the mouse reconnects. It ends at the next press,
at a speed or DPI change from the desktop app or config.toml, and at a
binding change for that mouse. On a mouse with adjustable DPI, a DPI preset
action also ends it. A change made while the mouse is offline also ends it,
and the mouse reconnects at its saved speed, or at the speed for none saved.
After an agent restart, precision mode is off. A mouse with a saved speed gets
that speed when the agent connects to it. A mouse without one keeps the
slowest speed until precision mode is switched on and off again.
Payload actions use a one-key inline table:
Back = { CustomShortcut = "Cmd+Shift+P" }
Forward = { HoldShortcut = "Ctrl+Space" }
GestureButton = { TapKeyHoldingModifiers = "Cmd+Tab" }
MiddleClick = { OpenApplication = { path = "~/Downloads", display_name = "Downloads" } }
DpiToggle = { short = "ShowDesktop", long = "MissionControl" }Chord modifiers are Cmd, Ctrl, Alt, Shift, and Super. Cmd is the
cross-platform primary modifier: Command on macOS, Control on Linux and Windows,
so Cmd+C copies everywhere. Super (aliases Win, Meta) always presses the
platform's logo key — Command on macOS, the Windows key, KEY_LEFTMETA on Linux —
so Win+L locks Windows and Super+End reaches GNOME and KDE shortcuts bound to
Super.
CustomShortcut emits an immediate key-down/key-up pair. HoldShortcut keeps
the chord down until the originating physical button is released, and also
releases it if capture is interrupted, the binding becomes invalid, or the
agent shuts down. Use it for push-to-talk and other hold-to-activate controls.
TapKeyHoldingModifiers splits the chord: the modifiers are held for the same
lifetime as HoldShortcut, while the ordinary key is tapped exactly once when
the press starts. That is what an application switcher needs. Cmd+Tab steps
one window forward and leaves the switcher on screen until the button is
released, where HoldShortcut would hold Tab down too and let key repeat walk
the switcher to its last window, and CustomShortcut would release Cmd at once
and dismiss it. The same shape covers Cmd+` , Alt+Tab, and an IDE'sCtrl+Tab. A chord with no modifier degrades to a plain tap of its key. If
another button is already holding the same key through HoldShortcut, the tap
is skipped and only the modifiers go down, so that button keeps its key held.
A { short = ..., long = ... } binding waits for the button's outcome instead
of firing on press. Releasing before 500 ms fires short; keeping the button
down for 500 ms fires long exactly once, and the later release does not also
fire short. If capture is interrupted, the binding changes, or the agent
shuts down before either outcome, neither action fires. A source that can only
report an instantaneous button pulse falls back to short. long may itself
be a HoldShortcut, in which case its chord stays down from the 500 ms
threshold until the physical release.
Long-press pairs currently apply to global device bindings and are authored
in TOML. The GUI presents their short action; changing that button in the GUI
replaces the whole pair with the selected single action. per_app_bindings andkeyboard.bindings remain single-action maps.
A wheel-hold binding on Back or Forward turns the main scroll wheel into a
virtual thumb wheel while the button is held, as Logi Options+ offers on mice
without a thumb wheel such as the MX Anywhere family:
Back = { click = "BrowserBack", wheel = "HorizontalScroll" }Turning the wheel away from you scrolls left and toward you scrolls right,
reversed when the device's invert_scroll is on. Releasing the button without
turning the wheel fires click; a press that scrolled fires nothing else. It is
off unless you bind it.
The held scroll needs raw-wheel reporting on that button, which only some
firmware offers, and the mouse hook, so OpenLogi can post the scroll (on macOS,
the Accessibility permission). Without either, OpenLogi captures the button forclick alone, as it does a long-press pair, and the wheel scrolls as usual.
With mouse capture turned off the button keeps its native function, like every
other mouse binding. While the button is diverted the wheel scrolls only through
OpenLogi, so OpenLogi hands both back to the firmware when the binding changes,
the device disconnects, or the agent exits. It never sets the firmware's
persistent flag, so the diversion does not outlive a device power cycle.
In the desktop app, select Back or Forward in the default profile and turn
on Hold to scroll sideways. The switch appears only on mice that report raw
wheel on that button; a binding carried over to another mouse still shows it,
so it can be turned off. Picking a different action for the button keeps the
wheel hold and changes click. A button in gesture mode offers the switch too:
turning it on leaves gesture mode with the gesture map's Click as the press,
and turning gestures back on restores the saved directions. The setting is per
device, not per application: an app profile that gives the button its own
action replaces the wheel hold in that application.
An Actions Ring entry wraps the action and may add an icon or literal label:
Top = { action = { CustomShortcut = "Cmd+Shift+P" }, icon = "Keyboard", label = "Command Palette" }ShowActionsRing is rejected inside a ring slot to prevent recursive rings.
DECISIONS (`docs/DECISIONS.md`)
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-10: Backlight writes invalidate other clients' device readings
A successful CLI backlight write could leave the already-open GUI showing the
previous setting: the write's readback went only to its requester, and firmware
does not guarantee a settings broadcast for that write.
The agent uses the existing orchestrator-owned settings_revision epoch to
invalidate client readings after a backlight transaction settles. It announces
errors too, because a write may have reached hardware before confirmation failed.
No returned value is broadcast as truth and no configuration file is changed.
An agent-owned task finishes the transaction and publication if its requesting
client disconnects. Admission reserves the existing per-route write order before
task scheduling; hardware execution keeps its existing timeout, excluding queue
wait. The orchestrator lock is never held over hardware I/O.
Desktop invalidates its existing backlight cache rather than joining a read
started before the revision. If a revision arrives while local writes are still
pending, it reads again after their replies settle: a delayed local reply may
predate a newer write from another client. This reconciliation issues no write,
preserves later local intent, and is discarded with the old route's read state.
The notification uses the existing snapshot contract, not a new wire field or
polling timer.
2026-10: Channel files are composed from per-release records
Beta releases share the update host with stable ones. A beta publish must
change what beta offers and leave what stable offers as it is. A stable
hotfix published after a newer beta must not take that beta off the beta
channel. So each release uploads an immutable feed record toreleases/<tag>/feed/: its own latest.json and one single-item appcast per
architecture. Every publish then composes all channel files again from all
records (cargo xtask release channels).
- Composed again, not edited in place. Reading the current channel file,
adding an item and writing it back would make each file depend on the order
in which tags published. A failed publish would leave it wrong until someone
repaired it by hand. Composing from the records gives the same files in any
order. - Newest by build version, not by tag version. A beta's build version is
the stable release it was cut from, plus its beta number, so build order
follows the commits. Sparkle and Windows Installer compare build versions.
The built-in update engine compares release versions, and never offers a
lower one. The two orders disagree whenever a beta is numbered past a
release cut after it, which happens when the release PR's version goes
down in between. The later release can be stable (0.9.0-beta.5, then0.8.19) or another beta (0.9.0-beta.3, then0.8.19-beta.10). The
channels offer the release cut last, which has the earlier one's changes.
Sparkle moves a Mac to it; the built-in engine keeps the higher-numbered
beta until a release numbered past it. We accept this:release channels
warns whenever a beta is numbered past what the beta channel offers, so
that a release numbered past the beta can follow. - Items copied byte for byte. The composer never signs. The EdDSA
signatures stay the onesrelease appcastmade, and the private key stays
in that one step. - One appcast per architecture for both channels. This is Sparkle's own
channel model: the beta item carries<sparkle:channel>beta</sparkle:channel>, and Sparkle offers it only to an
app that allows the beta channel. - The bucket lists the releases, not GitHub Releases. The updater can offer
only what is on R2, and reading R2 needs no GitHub token. Releases up tov0.8.17have no record. Until a stable release has one, the current stable
channel files stand in for it; after that they are not read. - A record counts once its manifest is up. A record's appcasts upload
first and itslatest.jsonlast. A record directory withoutlatest.json
is an upload that stopped part way, and the composer skips it, so it cannot
block later releases. A record with its manifest but a file missing or
wrong is an error.
2026-10: The mock agent's console drives the agent's own indicator state
openlogi-agent-mock reads one command per stdin line while the GUI and
overlay stay connected. A developer can then see a low battery, an offline or
failing device, a lock-key or backlight indicator, or an agent restart with no
hardware. Logi Options+ 2.8 has comparable hooks in its agent: a simulated
device on its HID write path and a fake battery feature. OpenLogi puts the
same kind of control in its separate mock binary, so the agent ships none.
- The agent's state machines, not copies. A battery reading goes through
LowBatteryWatch::take_reading, a lock key throughLockKeyIndicator::announce, and a backlight level throughBacklightOverlayManager. The orchestrator uses the same calls, so the
user's settings, the once-per-discharge rule, and the 1.5-second lifetime
behave as they do in the agent.take_readingandannouncereplace the
separate public calls that the orchestrator used to sequence by itself, so
no caller can skip the settings check. - Words for people, values for tests. Tests build
Commandvalues and
callMockAgent::applywith explicitAppSettings. No test reads a config
file or depends on the command words. - A thread of its own for stdin. Tokio's
stdinreads on the blocking
pool and cannot cancel the read, so it can hold up runtime shutdown. A std
thread reads the lines and one task applies them in order. When stdin
closes, the console stops and the mock keeps serving. - Semantic state only. The console changes what the IPC contract already
exposes. It does not model HID channels, reports, or barriers. Those stay
the Phase 4 replay scenarios in
MOCK_DEVICE_TESTING.md. disconnectcloses connections, not the process. Each connection
waits on a shared signal beside its requests. The overlay's agent-lost path
and the GUI's reconnect can then be tried without losing the mock's state.
Verified with unit tests of every command and an in-memory connection thatdisconnect closes, and by piping commands into the built binary on macOS.
Nobody has watched the GUI or the overlay follow the console yet.
2026-10: An animated desktop switch types the user's Space shortcut first
0.8.16 shipped the timed DockSwipe slide of the next entry. Users on macOS
26.6.1 and 27.0.1 report that it is slower than a native Ctrl+←/→ switch
(#1722) and that it stutters in the middle (#1972). The Dock plays its own
slide for that shortcut. An animated switch therefore types the user's "Move
left a space" or "Move right a space" shortcut (symbolic hotkeys 79 and 81),
as #1847 proposed. This revisits the "Not the shortcut" point below.
- Typed as a keyboard types it. Each modifier goes down as a key of its
own, and an arrow key carries the numeric-pad flag. WindowServer ignores a
synthesized hotkey in any other shape (#1653). - Through the virtual HID keyboard when it is installed. Each edge goes
through the device, as a held chord's edges do. An edge the device does not
carry goes out as itsCGEvent. EveryCGEventis built before the first
edge goes out. - Held chords stay intact. The keystroke is typed under the held-chord
lock. A held modifier the shortcut does not use is lifted for the keystroke
and pressed again after it. A held modifier the shortcut uses stays down. - The slide stays as the fallback. Its frames are built before the gate.
They are posted when the shortcut is off or cannot be read, and when a held
chord holds the shortcut's key down. A rebound shortcut is read from its
record, so it still switches with the Dock's slide. - Instant stays a DockSwipe. With
animate_desktop_switchoff, the switch
posts the two-frame DockSwipe, because the shortcut always animates.
Verified: unit tests check the order, flags and routing key of every edge.
#1847's author switched both ways with the Dock's slide on macOS 27.0.1. That
run used the CGEvent edges, because the test process had no virtual
keyboard. On 0.8.16, #1653's reporter switched Spaces with the slide through
a Ctrl+←/→ binding on the virtual keyboard. That binding sends the same four
reports as this keystroke. Not verified on hardware: this keystroke through
the virtual keyboard, and the fallback slide.
2026-10: Desktop switches slide like an Options+ three-finger swipe, and can switch at once
Previous Desktop and Next Desktop on macOS posted a DockSwipe of two frames,
Began and Ended, so the Dock changed Space with no slide (#1673). Two pull
requests proposed a slide. #1847 posts the user's "Move left a space" and
"Move right a space" shortcuts (symbolic hotkeys 79 and 81) and falls back to
the DockSwipe when they are off. #1672 posts DockSwipe frames that follow a
live drag. OpenLogi posts timed DockSwipe frames, as Logi Options+ does for
its three-finger swipe, and keeps the two-frame switch as an option.
- The Options+ timing. Logi Options+ 2.8 posts one DockSwipe frame per
tick of a 16.6 ms timer (injectionPeriodInMsingesture_handler_3_finger_horz_swipe), and each tick consumes 15% of the
motion still pending. OpenLogi has no hand to follow, so it starts with one
desktop width pending. It posts 19 moving frames, 16.6 ms apart, until less
than 5% remains (progress 0.954). An Ended frame with an exit velocity then
lets the Dock finish. The progress is past the Options+ completion threshold
(0.3), so the Dock completes the switch. These values come from static
analysis of the Options+ binary, not from a trace of its events. - Not the shortcut. Options+ also has a Ctrl+Arrow keystroke for a
one-shot move (+[injector InjectMoveToLeftSpace]), which is close to
#1847. The static analysis did not find which actions call it. The shortcut
can be off or rebound, and then #1847 switches with no slide again. The
DockSwipe needs no user setting, and the Space confirmation per display
stays as it is. - Frame fields from #1672. Up to macOS 26 a slide frame carries the
fields #1672 found, with the progress in field 124. macOS 27 reads an
attachedIOHIDEventinstead (#1703). There each frame carries the same
progress in its DockSwipe progress field, and the Ended frame adds the
velocity child, as Mac Mouse Fix posts its swipes on macOS 27. OneLazyLockowns the choice of layout, and an ast-grep rule keeps it the
only one. - On by default, with an off switch.
animate_desktop_switchis on,
because the Options+ swipe animates. Off restores the two-frame switch for
users who prefer no slide. Only macOS reads it, and the settings page shows
it only on macOS. - The action waits for the slide to be posted. The action worker posts
every frame before it takes the next action, about 315 ms, so two switches
in a row do not mix their frames. A switch that starts while another is
under way is skipped, as before.
Verified on macOS 26.7 with a screen recording: the slide shows about eight
intermediate frames at 30 fps, and the two-frame switch jumps in one frame.
The macOS 27 slide is verified only as event construction in unit tests;
nobody has run it on macOS 27 yet.
2026-10: macOS key synthesis types through a virtual HID keyboard when entitled
A key that CGEventPost injects enters the window server as software input.
Some system consumers treat it differently from a key typed on a keyboard:
issues #1653 and #744 report shortcuts that a real keyboard triggers and a
posted event does not. An IOHIDUserDevice keyboard reports its keys into the
HID event system, where a USB keyboard's keys enter. The agent therefore
creates one at arming and sends shortcut keys through it.
- The entitlement decides, at run time. Creating the device needs the
restrictedcom.apple.developer.hid.virtual.deviceentitlement. Apple grants
it only through a provisioning profile, and AMFI kills a process that claims
it without one (an ad-hoc signed probe exited withSIGKILLon macOS 26.7).
Release builds embed a Developer ID profile fororg.openlogi.agentthat
grants it. Local and dev builds carry none, so there creation fails and
every key stays onCGEventPost. - The claim and its profile ship together, or neither does.
xtask/src/commands/macos/bundle/provisioning.rsembeds the profile and
writes the agent's entitlements from it, so no entitlements file can claim
the grant alone. Before the build starts, it rejects a profile that would get
the agent killed on a user's Mac: one limited to registered devices, one for
another App ID, one without the grant, or an expired one. After signing, it
checks that the profile includes the signing certificate. The release
workflow requires the profile, so a signed release cannot fall back toCGEventPostwithout anyone noticing. - Logi Options+ creates the same kind of device, but sends nothing through
it. Its 2.8.981479 agent holds the entitlement and a Developer ID profile.gestures::create_virtual_hid_keyboardpasses only aReportDescriptor(a
65-byte boot keyboard with five key slots and an LED output report). The
binary imports noIOHIDUserDeviceHandleReport*function, and the gesture
then posts F14 or F15 asCGEvents. OpenLogi's use of input reports has no
Options+ precedent to copy. - A boot keyboard without outputs. The descriptor has the modifier bitmap,
a reserved byte and six key slots: an eight-byte input report without a
report ID. It declares no LED report, because a device without a dispatch
queue receives no output reports. The properties set only the descriptor
andProduct. Without a vendor ID,openlogi-hid's Logitech filter never
opens the device. - A key is released on the path that pressed it. A key without a HID usage,
a press beyond six keys, and an up edge whose down edge the device did not
carry all go out asCGEvents. A tap reports its release before it returns,
so the device holds keys only for held chords. A rejected report removes the
device and reports every key released; key synthesis then postsCGEvents
for the rest of the process. The keys that held chords still own go down
again asCGEvents, so each chord's release finds its keys down. - The hook recognises the device by name. Its key events carry no
SYNTHETIC_EVENT_USER_DATA, because the HID event system creates them. The
hook resolves each key event's sender to the device'sProductand skips
OpenLogi's own, as it skips a taggedCGEvent. - The agent owns it. Only the armed agent creates the device. The exit path
removes it after the input workers stop, so a chord released during shutdown
still reaches it. The CLI and the GUI keep postingCGEvents.
Verified with unit tests of the report state, the routing and the descriptor,
and by running an unentitled build: creation fails and keys keep theCGEvent path. Nobody has run an entitled build, so the report path and its
effect on #1653 and #744 are not verified.
2026-10: One application on every platform is an `[apps]` alias
A per-app profile was keyed by the identifier one platform reports: a macOS
bundle id, a Linux application id, or a Windows executable path. The same
application has a different identifier on each platform, so a profile written
on one did not apply on another. Logi Options+ 2.8 solves this with oneapplicationId and a list of detection rules per platform
(applications.json). OpenLogi adds the same layer as a table inconfig.toml, and accepts the config format change (schema 9).
- A separate table, referenced by key.
[apps.<alias>]lists the
identifiers of one application per platform. A profile references it asapp:<alias>, the same way the existingexe:<filename>key names an
executable. Profiles keyed by a platform identifier keep working unchanged,
so no existing file needs to move. Later features that select an
application, such as scene templates, can take the sameapp:key. - The most specific key wins. The matcher tries the exact identifier, then
exe:<filename>, then the alias that lists the exact identifier, then the
alias that listsexe:<filename>. A profile keyed by the platform's own
identifier therefore wins over an alias profile. That lets one platform
differ from a shared profile. - One alias per identifier. Two aliases that list the same identifier for
one platform stop the load, because the matcher could not choose between
them. An identifier can repeat across platforms. - An undefined alias stops the load. A misspelled
app:key would match
nothing and silently do nothing, so loading rejects it, as the strict schema
rejects other mistakes. Windows identifiers must be lower-case for the same
reason: Windows reports lower-case paths. - One matcher.
openlogi_core::appowns both theexe:fallback and the
alias lookup for button profiles and Actions Ring layouts, and an ast-grep
rule keeps a second copy out. - The desktop app reads aliases but does not write them yet. It shows an
alias profile under the alias name and stops offering an application that
already runs under a profile. Sharing a profile across platforms from the
GUI, and a hint for an alias with no identifier on this platform, need new
strings in every locale. A later change adds them.
Verified with unit tests, which CI runs on macOS, Linux, and Windows. Nobody
has used one config on two platforms with a real application yet.
2026-10: On Wayland, the ring opens a layer-shell surface on each display
A Wayland client cannot read the global cursor position or place its own
windows. Since #1004 the overlay has opened one ring window per display, and
each window draws the ring where the pointer enters it. On GNOME 50 with two
monitors, both rings appeared on one monitor (#1901). An ordinary window
cannot name its output, Mutter put both windows on one display, and each
window that heard the pointer drew a ring of its own.
- Layer-shell surfaces first. A
wlr-layer-shellsurface names its
output. Each display gets one on the overlay layer, anchored to all four
edges and without keyboard focus. The surface has no size of its own, so
the compositor sizes it to the display. GPUI derives display bounds from an
integer scale, which is wrong under fractional scaling, so the overlay does
not use them here. - Ordinary windows as the fallback. GNOME does not offer
wlr-layer-shell, so there the overlay opens the maximized windows it
opened before. The overlay remembers the first refusal for the life of the
process, for rings and notices alike. GPUI binds the compositor's globals
once, and each refused request leaves behind thewl_surfaceGPUI created
for it. - One window draws the ring. The windows of one ring share one record of
which window heard the pointer last, and only that window draws the ring.
When the windows are stacked on one display, the last to hear the pointer
is the one on top, the only one a click can reach. A window that loses the
ring also drops its hovered slot.
Verified with unit tests and a GPUI test that opens two ring windows for one
ring. Nobody has run the Wayland paths on screen yet. CI builds and tests
them on Linux.
2026-10: The lock-key indicator follows Logi Options+ 2.8, without Fn Lock
Logi Options+ 2.8 shows lock-key changes in its own on-screen indicator. Its0x4220 handler compares each broadcast bitmask with the last one and posts
one indicator per changed bit. Its osd_resources.json shows Caps Lock on
every platform and Num Lock and Scroll Lock on Windows only. OpenLogi follows
both rules.
- A baseline before the first change. The agent reads the keyboard's lock
state (GetLockState) when it learns the device, and announces only bits
that change after that. Num Lock is usually on under Windows, so a keyboard
that connects must not announce it. A keyboard that goes offline, or that
reconnects between two scans, loses its baseline and is read again. Options+
probably reads a baseline at start too; that part of the disassembly is not
verified. - The agent sees every change; the indicator shows the latest. The device
layer hands changes to the agent in a small queue, not a latest-value cell,
so a change the platform rule drops (Num Lock on macOS) cannot hide one it
keeps (Caps Lock) reported with it. The indicator shows only the latest
change, so presses in quick succession can skip the states between them. - The agent owns the indicator. It shows a change for 1.5 seconds, the
backlight indicator's time, and a newer change replaces it. The overlay
hides the indicator when the connection to the agent drops, as it does for
notices. - On by default. One global switch,
lock_key_notifications, turns it
off. The Options+ default for its setting was not found in the static
analysis. - No Fn Lock yet. Options+ also shows "Media Keys" and "F-Keys". OpenLogi
writes the Fn Lock state back to a keyboard when it reconnects, and the
keyboard reports that write like a key press, so the indicator would show
on every reconnect. That needs its own decision.
Verified with unit tests. Nobody has seen the indicator on screen with a real
keyboard yet.
2026-10: Low-battery notices are drawn by the overlay, not sent as OS notifications
Logi Options+ 2.8 shows a low battery in its own on-screen toast
(logi::overlay::battery_notification, the low_battery entry of itsnotification_resources.json), on by default. It sends OS notifications only
for macros and integrations. OpenLogi follows the same split.
- No notification permission. An OS notification needs authorization on
macOS, a registered app identity on Windows, and a notification daemon on
Linux. The overlay runs whenever the agent is armed, with or without the
settings app, and already places windows per display. - One rule for low.
BatteryInfo::needs_attentioninopenlogi-core
decides it for the agent's notice and the settings app's battery colour, and
an ast-grep rule keeps a second copy out. - The agent owns a notice. It decides when to post one and when it ends
(8 seconds), so an overlay that starts or reconnects sees only the notices
that are current. Notices travel in their own observation cell
(observe_notices), not in the agent snapshot, so a notice does not wake
every state observer. When the connection to the agent drops, the overlay
clears the cards at once, because nothing else would ever end them. The
backlight indicator follows the same rule. - Once per discharge. A device is announced when it becomes low, and
again only after it charges or reads 30% or more, so a reading that wobbles
around 20% does not repeat the notice. - Not click-through. The card sits at the bottom centre of the usable area
of the display under the cursor (macOS visible frame, Windows work area,
Wayland layer-shell surface anchored to the bottom edge, X11 display). It
opens without taking focus, and a click closes it.
Verified with unit tests, and on screen on macOS against the mock agent's
demo: the card appears at the bottom centre above the Dock and closes after 8
seconds. A click to close is covered by unit tests only. The Windows and Linux
windows are checked only by CI builds; nobody has seen them on screen yet.
2026-10: Litra is reached over HID++ `0x1990`, with the raw report as fallback
The "raw" Litra report [0x11, 0xff, 0x04, …] was always a HID++ 2.0 long
report: device 0xff, feature index 4, the 0x1990 Illumination setters.
Hard-coding the index sent writes to the wrong feature on any firmware that
lists 0x1990 elsewhere, and nothing was ever read back. Logi Options+ 2.8
resolves the index through the root feature, reads the light's ranges and
state, and writes a value only when it differs from the one it read.
- HID++ first. A command opens the light's own node as HID++, pings it,
looks0x1990up through the root feature and reads the light's ranges and
state. It writes only a changed value, reads the written control back, and
checks values against the ranges the light reports rather than the static
per-model ones. - The effective maximum is gated.
getBrightnessEffectiveMaxis read only
for feature version 1 with the dynamic-maximum flag set, as the
specification and Options+ gate it. - The raw report stays as the fallback. It is the path every earlier
release shipped and the only one proven on hardware. A light that opens but
does not answer the ping, list0x1990or have its state read within one
second gets it instead; without the deadline it would hold the command for
the channel's five-second request timeout. That answer is remembered per
route and OS node for the life of the process, so a light that comes back as
a different OS node is asked again. A refused open says nothing about the
light, so it falls back for that command only. - No raw retry after a HID++ write. Once the command has gone out over
HID++, a failure is returned rather than repeated as a raw report. - One link per light. The agent keeps the light's HID++ channel open and
listens on it, so commands and notifications share one channel and the
feature lookup runs once. A channel that stops answering is replaced on the
next command, and the listener follows the new one. - A change on the light is the new setting. Options+ broadcasts the state
a0x1990notification reports and stores it as the light's settings. The
agent keeps a value changed with the light's buttons or by another program
in place of the saved one, so a reconnect writes it back instead of undoing
it, and publishes it so the settings app can save it. A save of that control
or a write from OpenLogi replaces it; the power of a camera-linked light is
a manual choice until the next camera transition instead. The desktop app
saves each published change, except on a light with one of its own writes in
flight. The agent takes the controls a write sets before it writes and gives
them back only if the write fails, so a state published during the write
never offers the old change, and the app cannot save it over the value the
user just chose. The agent holds what a write took until the write settles,
so a reload during the write replaces it as it replaces any change, and a
failed write gives back only what is left. The app reloads its own saves
withreload_saved_config.
It writes each control it changes before it saves it, and the write takes
the change, so a saved value that differs from the one before is the app's
copy of an earlier report. That save leaves a newer change on the light in
place, so a press that reaches the agent after the app saved the previous
one is not written over. The save names the controls the user set without a
write, as on an offline light (ChosenLight): those, and the values the
save removed, replace the change. The save also names the revision of the
file it wrote (ConfigRevision, a digest of the text). A file the agent
reads at another revision was edited after the save, so it loads as a plain
reload. A plainreload_config, after a hand edit ofconfig.toml, a CLI
edit or at startup, replaces the change wherever the saved value differs,
even when it matches an earlier report. - OpenLogi's own writes are not changes on the light. The light echoes
every setter as a notification. A notification that matches the value the
agent just wrote is the echo. A brightness the light's effective maximum
explains is the light's limit, not the user's choice, even when another
notification is read first: lowered to a limit below the old brightness or
the old limit, or raised from the limit it sat at once that limit rose. A
press read together with a limit it stays under is still the user's. Nor is
a brightness a request was clamped to: the light sends the clamp when a
request asks for more than the limit, and holds the limit, which nobody
chose. A clamp the light's button made still follows a press on it. A clamp
stops explaining the brightness once the button makes a clamp, or once a
later brightness differs from the clamped one. A notification that arrives
while the light is read makes the watcher read it again, at most three times
per notification; a light that keeps notifying is read again on the next
call without waiting. The published state still follows the write: the
watcher reports the state a write read back once, whether or not the light
echoes it, unless a later read replaced it.
Verified against scripted lights only; the repository has no Litra capture yet.
2026-10: RGB software control is held per session and handed back on exit
A volatile 0x8071 effect shows only while the host holds software control,
so a successful write keeps it. The agent used to keep it forever, which left
a keyboard frozen on the last colour after OpenLogi quit, with no other host
able to take it until a power cycle.
- Hand it back on a final exit. Tray quit, uninstall, and SIGTERM return
cluster control to the firmware before the HID++ fleet stops, so lighting
goes back to the onboard effect. G HUB behaves this way, and Solaar's RGB
control restores firmware control on close to match it. It is also the rule
this agent already follows for diversion: leave the device in its native
state. Logi Options+ is no reference here: it builds the0x8070/0x8071
wrappers but never drives them, and claims only0x1300for the
precision-mode DPI LED. - Not on a handover or replacement. The successor repaints on arrival, so
releasing would only flash the onboard effect, and the successor's own
final exit still hands control back. A disconnect or crash cannot send
anything; the firmware drops volatile state on power loss. - Clear only the bit that was set. The release clears
ALL_CLUSTERSand
keeps the other control and event flags from before the session's first
claim. Restoring that snapshot verbatim would leave control held forever
after an agent that never released it, which every earlier build was. - The standalone CLI keeps the colour. Leaving a colour set is the point
ofopenlogi diag lighting, so only the agent's exit consumes the leases. - Lighting off is not a release. It writes black under the same control;
releasing there would light the onboard effect instead. - The exit waits for writes already running. It refuses new lighting
writes first, then gives running ones a bounded wait to record their
leases, so a first write that claims while the agent quits is still handed
back. A write still running at the deadline keeps its control, and the
exit logs it. - A lease belongs to one device. It is bound to the connection the claim
went through and released only through that connection while inventory
still publishes it, which covers a replug. A receiver slot keeps the
receiver's connection when another device is paired into it, so every
write also records the unit ID the device reports (0x0003), including
one that finds control already held: the replacement may be held by this
session's earlier claim or by another host. A write with another unit ID
replaces the lease, and the release re-reads the unit ID and leaves a
different device alone. Inventory snapshots do not void leases: they trail
the channel a write goes through, so dropping a route's lease when a
snapshot shows its device gone could erase a claim already taken on the
replacement. A device without0x0003is identified by having none. 0x8081per-key frames go under the same claim. While the firmware
owns a cluster it draws its own effect over host frames. On a device with an0x8071engine a per-key write therefore claims (with rollback), sets
Static as the base on every cluster that offers it, paints, and replays the
first frame after a fresh claim, as Solaar does. A device with only0x8081
has no engine to take over and is written as before.
Verified against scripted devices only; the repository has no 0x8071 or0x8081 capture yet.
2026-10: macOS updates go through Sparkle, which never polls
The built-in engine replaces the running bundle in place, so a standard user
whose copy sits in an /Applications they cannot write to gets a permission
error instead of an update, and there is no install on quit. Sparkle asks for
administrator rights there and installs on quit. The signed macOS bundle now
embeds Sparkle 2 (through gpui-updater-sparkle, loaded at run time) and the
engine stays the fallback everywhere else.
- The privacy promise outranks Sparkle's defaults. Updates are opt-in and
OpenLogi does not poll. The bundle shipsSUEnableAutomaticChecks = NOand
the app switches the schedule off should the user defaults turn it on; the
one launch check is OpenLogi's own, behind Check for updates, and opens
no window.SUAllowsAutomaticUpdates = YESis what lets Automatic updates
work without the schedule: Sparkle's settings otherwise tie automatic
downloads to automatic checks (SPUUpdaterSettings.m). The config owns the
preference and Sparkle'sautomaticallyDownloadsUpdatesmirrors it; a change
made with the checkbox in Sparkle's update window is adopted back. - One feed per architecture, not a universal build. The release ships an
arm64 and an x86_64 DMG from a build matrix;SUFeedURLpoints each bundle
atappcast-<arch>.xml, which keeps the matrix and the DMGs as they are. A
universal bundle would allow one feed but doubles every binary in the
download and changes what the release builds. - Signed on Linux, in Rust.
release appcastsigns in the publish job
throughgpui-updater-release, checked against signatures Sparkle's ownsign_updatemade, instead of running Sparkle's macOS-only tools there. It
refuses a private key that does not match the public key the bundles embed. - Deltas from the three newest Sparkle releases, made on macOS. Sparkle
offers a delta only to an app at exactly its base version, so three bases
cover most installs and keep the macOS leg's time bounded; everyone else
takes the full DMG.BinaryDeltais macOS-only, so the build leg makes the
deltas from the stapled DMG and the Linux publish job only signs and lists
them. A delta is kept only after it rebuilds the new app exactly, and one
that cannot be made never blocks the release. For 0.8.14 → 0.8.15 on arm64
the delta is 3.7 MB against a 21.2 MB DMG. CFBundleVersionis the crate version. Sparkle compares the appcast'ssparkle:versionagainst it, and cargo-bundle writes the build's UTC
timestamp there, which would rank above every release. xtask stamps the
crate version, as it already did for the helpers, and refuses a Sparkle
bundle without it. Bundles from before Sparkle keep their timestamps, but
they update through the engine, which compares the version compiled into
the binary.- Kept:
latest.jsonand the DMG minisigs. Bundles from before Sparkle read
only the manifest, and they reach a Sparkle bundle through it. - Rejected: Sparkle's XPC services. OpenLogi is not sandboxed, so they never
run; xtask removes them rather than sign and ship them.
2026-09: Grant probes run on notification, with a heartbeat backstop
The hook re-checked the Accessibility grant between every 500 ms run-loop
slice — AXIsProcessTrusted plus a throwaway CGEventTapCreate — and the
agent polled both privacy grants every 1.2 s for the GUI. Each probe is a
WindowServer round trip that takes seconds around a sleep transition, which is
where the probe-budget work (#952, #1282) came from. macOS posts two
undocumented notifications when the list is edited, and theaxwatch crate observes both. Measured
against tccd's event log on macOS 26: adding a row, switching it off and
switching it on post both; removing the row posts only the Darwin one.
- Probe on cue.
grant::ProbeCueraises a flag from the watch's handler,
and the tap thread probes only on a raised flag; the two agent-core grant
watchers read on the same wakes. Detection latency for an announced edit is
unchanged (one slice); an unannounced one is bounded by the heartbeat. - Keep the heartbeat and the probe. The notifications are undocumented, and
a missing run loop, a code-signing rule or a future macOS can swallow them;
the reconciled probe remains the authority, at 5 s instead of 500 ms. A watch
that cannot start falls back to the old cadence with a warning, never
silently. - Rejected:
AXIsProcessTrustedalone on wake. It staystrueafter the
row is removed — the very edit only the Darwin notification announces — so
the read cannot replace the probe. - Not changed:
TAP_PROBE_BUDGET. Rarer probes make a collision with a
sleep transition rarer, not impossible; shrinking the budget waits for a
measured revoke→teardown on hardware.
2026-09: A crash respawn re-arms by login session, not by exit code
With launch_at_login off, the macOS dormancy gate read every launchd start as
a login the user opted out of, and left 60 s later with the exit(0) launchd
never respawns. A crash respawn takes the same path — the plist has one trigger
(SuccessfulExit implies RunAtLoad) — so one hook-watchdog exit(78) on a
lid close silently ended remapping until the user opened the GUI (#952).
- The gate asks the session, not launchd. Arming records the login session
(kernel boot session UUID + audit session id) in the runtime dir; every finalexit(0)— tray Quit, uninstall, SIGTERM — erases it; a handover to a
scheduled successor (binary update, Input Monitoring relaunch) keeps it. A
start that finds its own session recorded re-arms at once; a login finds a
stale session and stays dormant. The boot half is required: audit ids repeat
across boots. - Rejected: a second, crash-only plist (
KeepAlive = {Crashed: true}, noRunAtLoad).Crasheddoes not coverexit(78)or a panic'sexit(101),
which are exactly the exits that need recovery, and it would reintroduce the
two-label registration the 2026-08 lifecycle work removed. - SIGTERM is final. launchd's logout and dev tooling mean "stop". The
stale-agent takeover (takeover.rs) sends it too, and there the successor
would rather find the record; it makes no difference, because the only
starts that reach the takeover are GUI kickstarts, and that GUI's
declaration arms the successor anyway.
2026-09: The macOS device-I/O gate follows powerd, through an SPI, and fails open
The gate that keeps HID access off while the Mac is dark (#656) was first built
on NSWorkspace notifications (#1142): WillSleep / ScreensDidSleep /SessionDidResignActive closed it, ScreensDidWake / SessionDidBecomeActive
reopened it. Those are edges, and macOS does not guarantee the wake edge —
Apple DTS (developer forums thread 796109) confirms ScreensDidWake is skipped
on some MacBooks for a lid-close/open cycle and that the system may not even
run the app's run loop to deliver it. One dropped edge paused device I/O until
the agent was restarted (#1281). The screen-sleep source was never a
requirement in its own right: it stood in for "not a DarkWake", the state
AppKit cannot name (NSWorkspaceDidWakeNotification fires for both).
The gate now reads the thing it is actually about. activity_macos subscribes
to powerd through IOPMConnection, which reports every Sleep / DarkWake /
FullWake transition as the complete capability set of the new state and reads
the current set on demand, so every input is a level: nothing pairs, a missed
event is corrected by the next, no reconciler timer exists. The console level
(fast user switching) comes from CGSessionCopyCurrentDictionary, with the
AppKit session edges forwarded as hints and the level re-read on every power
transition. A launch reads both levels before releasing its hold, so a
watchdog relaunch during a sleep transition no longer guesses "awake" (#952).
Three things decided the API:
- Only
IOPMConnectionnames a DarkWake. DTS states plainly that the
public API does not expose DarkWake at all:IORegisterForSystemPower
deliverskIOMessageSystemHasPoweredOnfor a DarkWake as for a full wake,
so a gate on it cannot tell the one-second DarkWake blip #1281 was reported
with from the full wake that followed it. The undocumentedIOPMrootDomain
"System Capabilities" registry key carries the same level but no events,
which is what forced #1283 into polling. - It is an SPI, and that is accepted. The functions are exported by IOKit
since 10.6 and prototyped only in Apple's open-sourceIOKitUser; the
shippingpmsetis built on them. The hand-writtenexternis confined toactivity_macos.rsand inventoried in.agents/rules/objc-ffi.md. OpenLogi
is not App Store distributed. - Losing the SPI fails open. If the subscription cannot be made the gate
is told "permanent full wake" and follows the console alone, at error level.
That is pre-0.8.2 behaviour (hardware usable, #656 unprotected); failing
closed would leave an agent that can never touch hardware, which is the
outage #1281 describes.
2026-08: The agent stays one process; a crossing edge gets a wire, not an event layer
The 2026-06 daemon split (#165) put the resident input machinery inopenlogi-agent and left the GUI an on-demand IPC client. Its two
cross-process abstractions have been production infrastructure since:openlogi-ipc is the versioned, append-only wire, and succession owns the
one-process-per-role lifetime pattern (the agent negotiates as a peer, the
overlay serves one run as a subordinate, the GUI only reports mismatches).
This entry records the deliberate end of that road: the agent itself does not
split further, and no transport-agnostic event abstraction is built inside it.
Two facts decide it:
- Every candidate cut crosses the hot path. The agent's two
permission-bearing roles — the CGEventTap hook (Accessibility) and HID++
device I/O (Input Monitoring) — are not adjacent subsystems but two ends of
one input loop: the hook feeds the action dispatcher, which performs HID++
writes (DPI cycle) and OS injection, and HID++ gesture capture feeds the
same dispatcher in the other direction. A hook/device process boundary puts
IPC latency and a new failure mode inside button-press handling — the path
the product exists to keep fast. The GUI/agent split worked precisely
because its crossing edges are cold (config saves, snapshots, long-polls);
no cold cut of the agent exists. - The isolation benefits already arrived by other means. Crash recovery is
launchd supervision (KeepAlive = {SuccessfulExit: false}); prompt
containment is the dormancy gate (nothing user-visible before arming);
orphan lifetime issuccession. What a split would still add — the event
tap surviving the seconds of a device-stack respawn — does not pay for a
second TCC identity: bundle identity is the TCC primary key, so a new
helper process is a new identity with zero existing grants, a mass
re-grant event of the 0.6.24 class.
The standing doctrine for any edge that does someday cross a process boundary:
give it the treatment the overlay got — a versioned method on theopenlogi-ipc contract (append-only, golden-tested, PROTOCOL_VERSION
bumped) plus a succession role for the new process's lifetime — and cut
along the named seams (startup::StateWatchers is the message list of a
watcher-side wire, lifecycle::Armed is the consumer-side state list). What
is deliberately not built is an in-process event bus or transport-agnostic
event-source layer: it would tax every in-process edge with serialization
bounds, fallibility, and reconnection semantics that shared memory does not
have, in preparation for a boundary this analysis says should not exist.
Revisit if a platform forces a privilege split (a packaging world where
uinput access requires a system service), if sandboxing the device stack
becomes a requirement, or if a genuinely cold cut appears. The first move is
still a wire, not an abstraction.
2026-08: Device settings are keyed by identity, not by transport
A device's config key was derived from how it was reached, so the same mouse
on a receiver and on a cable became two devices: two entries, two carousel
cards, settings left behind on whichever link set them. The correlating value
was already read on both paths and thrown away — DeviceStableId::from_parts
took serial and unit_id and discarded both for receiver routes.
- Keys are now
unit:<hex>orserial:<s>, with a persistedlinkstable
naming the routes the device has been seen on. That table is also the index
that identifies a sleeping device when only its route is known, which is why
it is stored rather than recomputed. It fixes a second bug in passing:
unpairing mouse A from a slot and pairing B into it no longer hands B all of
A's bindings, because the key is no longer the slot. Not retroactively,
though — a pre-upgrade config records nothing about which device wrote a slot
entry, so the first sighting after the upgrade still folds it into whatever
now occupies the slot. Only from that point on is the entry keyed by unit and
the slot empty. - A device whose unit id reads all-zero keeps its route key and never
correlates — degradation, not an assumption about hardware we cannot sample. - Capabilities are measured per link, because they genuinely differ per link:
a G502 LIGHTSPEED publishes0x2121 HiResWheelover its receiver and0x00c2 DfuControlSignedover USB, same firmware image either way (#660).
The probe was never wrong; one device simply could not own both readings.
Each link's capabilities are rewritten from the sighting that reached it, so
the table describes the hardware rather than whatever a migration happened to
leave behind. Settings that disagree between links become per-link overrides
rather than one link overwriting the other. - Migration is two-phase because a v4 direct key carries the unit id in the key
string while a receiver key says nothing about the device: schema 4 → 5
renames direct keys mechanically at load, and receiver entries fold into
their canonical entry on the next online sighting. Until that sighting the
legacy entry stays orphaned. Closing that window at load time by matching
entries on identicalmodel_idswas rejected — model ids are model-scoped,
so two identical mice, one on a receiver and one cabled, would merge into one
device. That is the property slot-keying protected, and it must not regress.
2026-08: Suppressions are `expect`, and tests are exempt by config
A sweep of every lint suppression in the tree (207 attributes, 247 lints) found
two systemic problems, both now fixed.
- Tests restated the same exemption 78 times.
unwrap_used/expect_used
stay at warn so product code has to state its panics, but every test module
had copied#[allow(…, reason = "idiomatic in tests")]to opt out — about
40% of all suppressions in the workspace, carrying no information. A rootclippy.tomlwithallow-unwrap-in-tests/allow-expect-in-testsreplaces
all of them. Clippy's exemption covers#[cfg(test)]modules and#[test]
functions; a free helper in atests/integration file is the one shape it
cannot see, soopenlogi-ipc's wire-format test keeps a file-level
suppression. Build scripts are not tests and keep theirs too. allowrots silently. 20 suppressions no longer suppressed anything,
including three module-widedead_codeblankets inopenlogi-assetsthat
had been inert since those modules becamepub mod— and would have hidden
real dead code the moment they went private again. Suppressions are now#[expect]by default, which fails the build once it stops being needed.allowsurvives only whereexpectcannot work: a lint that fires under
somecfgbut not others, one whose fulfilment differs between a crate's
lib and test targets, and one raised inside a macro expansion (rustc does not
credit an expectation with those, so it both suppresses the warning and
reports itself unfulfilled). Each such site carries a comment saying which.- Both rules are now machine-checked.
allow_attributesandallow_attributes_without_reasonjoin the lint table, costing three
annotated exceptions and nothing else — the sweep had already left the tree
compliant. One blind spot to remember:allow_attributesonly sees outer#[allow], so a module-wide#![allow(…)]— precisely the shape that rotted
inopenlogi-assets— still passes it. Adding them also disproved the
first-pass rule that acfg_attr-wrapped suppression always needsallow:
two of the four turned out to work fine asexpect.
Related: the seven file-wide cast_* blankets were narrowed to the functions
that need them, and most of their sites turned out not to need a suppression at
all — cast_signed/cast_unsigned, &raw const/&raw mut, to_le_bytes, or
a shared conversion helper. Linux CI showed capture_linux's file-level
blanket was two-thirds dead (cast_possible_truncation /cast_possible_wrap); the remaining cast_sign_loss sits on clamp_u8.
Supersedes the "openlogi-hidpp stays out on purpose (vendored)" note in the
shared-lint-set entry below: the hard-fork ruling retired that, and the crate
inherits [lints] workspace = true like every other.
Sweeping for rot is mechanical and worth repeating: rewrite every
non-cfg_attr allow( to expect(, run clippy, and each "lint expectation is
unfulfilled" is a suppression to delete. Run it on all three lanes — native,--target x86_64-pc-windows-gnu, --target aarch64-unknown-linux-musl —
because CI has no macOS clippy job and a platform-gated suppression is only
evaluated on its own platform.
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-desktop,openlogi-cameraandopenlogi-hookcarried 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, soopenlogi-hookmoved itsunsafe_codeopt-out
into its three platform modules.openlogi-hidppstays out on purpose
(vendored). tests_outside_test_moduleonly recognises a literal#[cfg(test)]. Compound
gates are written as stacked attributes (#[cfg(test)]then#[cfg(unix)]);
an integration test undertests/carries a file-level#![expect(…)]
because that file is already a test-only crate. Splitting the attribute also
wakesitems_after_test_module, so such a module belongs last in its file.exitgets a realExitCodewherever the call site can return —openlogi listhands status 2 back tomain— and a reasoned#[expect]where it
cannot (the AppKit run loop, the watchdog threads, the update handover).
Clippy does not look insidedefine_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, andunexpected_cfgsalready 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 fromDeviceKind::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 ofFIXDRY.md for the full list). The following stayed custom, deliberately:
openlogi-core::single_instance: thesingle-instancecrate 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://quitdispatch keepsstd::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 -nintentionally: it needs
LaunchServices-specific flags to start the packaged agent under its own TCC
identity, which generic opener crates do not expose. (Since 2026-08 it is
the fallback: a registered login item is kickstarted throughlaunchctl
instead, which supervises the agent as well —openremains for
unregistered installs and stays custom for the same reason.) - Agent autostart install keeps direct
systemctlcalls because it is managing
systemd user units, not merely opening or spawning an arbitrary program. - Self-restart and
disclaimlaunches 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 withenigo, but current semantics are narrower and more controlled.openlogi-hid/ vendoredopenlogi-hidpp: the right path is upstreaming
OpenLogi-specific fixes, not replacing the fork blindly.
2026-06: Fn is not a capturable trigger (macOS)
An instrumented CGEventTap probe during the function-key remapper design
(the spec lived in docs/superpowers/ until 2026-08; full text at
91fe5d80)
settled why the Fn modifier cannot join the keyboard-remap trigger vocabulary:
- F1 arrives as keycode 122 with the
SecondaryFnflag, but plainQandFn+Qare byte-for-byte identical, as are plain Shift andFn+Shift;
pressing Fn alone produces no event at all (not evenFlagsChanged). - The keyboard firmware holds Fn internal unless the key has a dual
function-row meaning, so the flag attaches only to F1–F12.Fn+<anything else>is indistinguishable at the tap — firmware behavior,
not something OpenLogi can code around at this layer. - The only theoretical path is raw-HID reading below the OS event system
(Karabiner/DriverKit territory) — a large subsystem with no guarantee a
given keyboard exposes Fn there. Not pursued.
DEVELOPMENT (`docs/DEVELOPMENT.md`)
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.99 — the floor tracks current stable)
- macOS: Xcode 26+ with the optional Metal Toolchain component. The Metal
Toolchain is what GPUI'sgpui_macosbuild script compiles shaders with; the
version floor isactool, which packaging uses to compile the app icon from
its Icon Composer document.OPENLOGI_DEVELOPER_DIRoverrides which Xcode is
used when several are installed. - 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-dmgfor packaging (brew install create-dmg);cargo-bundleis
installed automatically bycargo run -p xtask -- macos bundle
Building from source
Nix/devenv is optional. A normal Rust toolchain is enough.
Without Nix
# rustup installs the stable toolchain pinned in rust-toolchain.toml
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# macOS: full Xcode 26+ 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-desktop --releaseIf 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, platform libraries,
nfpm on Linux, and the macOS packaging/env helpers GPUI needs
(create-dmg, DEVELOPER_DIR, and SDKROOT). Tasks:
devenv tasks run openlogi:gui # run the desktop app
devenv tasks run openlogi:check # host-OS gate: fmt + clippy + tests + rustdoc
devenv tasks run openlogi:ci # every GitHub Actions CI job this host can reproduce
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 testsAfter a devenv.nix change, reload direnv so the new env takes effect:
direnv reload # or: exit your shell and `cd` back inWithout 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.
Nix package
The Flake in packaging/ exposes native x86_64-linux and aarch64-linux
packages plus the NixOS module. It is separate from the devenv shell. Nix only
looks for a Flake in the working directory and its parents, so name it from the
repository root:
nix flake check ./packaging --all-systems --no-build # evaluate every output
nix build ./packaging#openlogi # build + test this host's package
nix run ./packaging#openlogi -- list # run the packaged CLIThe package expression and NixOS module live beside the other Linux packaging
inputs in packaging/linux/. nix fmt formats all Nix expressions through the
Flake's pinned formatter. It takes no Flake argument, so run it frompackaging/.
The root flake.nix is a deprecated alias for existinggithub:AprilNEA/OpenLogi inputs. It has no committed lock file, prints a
warning on every evaluation, and will be removed in a later release.
Dev app bundle (macOS)
On macOS the desktop binary is launched from inside a throwawaytarget/dev/OpenLogi.app — a Cargo runner wired in .cargo/config.toml
(.cargo/run-macos.sh) that hands the build to xtask macos dev-bundle. 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-desktop executable name and a generic icon. The
binary is hardlinked in (no copy) unless the bundle is being signed, and the
icon is generated on demand. The runner is a transparent passthrough for
everything else (the CLI, tests); set OPENLOGI_DEV_BUNDLE=0 to launch the rawopenlogi-desktop binary instead.
Each run also stops the dev agent and overlay left behind by the previous one,
then starts the freshly built agent and waits for its IPC socket before the
GUI launches — so the window connects immediately instead of sitting on its
connecting frame while the GUI's production fallback re-spawns the agent.
The helpers are launched through LaunchServices so they get their own TCC
identity, which also means they are not children of the GUI: closing its
window or pressing Ctrl-C ends only the GUI, and a surviving dev agent
relaunches itself ~20 s later once its watcher notices the rewritten binary.
Set OPENLOGI_DEV_AGENT=0 to run against an agent you started yourself —
nothing is stopped, built, embedded, or started then.
Packaged local dev bundles (cargo run andcargo run -p xtask -- macos bundle) use -dev bundle identifiers and theopenlogi-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:
cargo install --path crates/openlogimacOS input-hook safety
The agent's input-hook watchdog gives CoreGraphics permission probes a 10-second budget. After a probe returns, tap servicing and teardown use a fresh 1.5-second budget. Teardown keeps that short budget when Accessibility is revoked or the re-arm limit is reached. The watchdog exits the agent if cleanup stalls, so macOS releases the process-owned tap.
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:
cargo run -p openlogi-agent --bin openlogi-agent-mock # then, in another terminal:
OPENLOGI_DEV_AGENT=0 cargo run -p openlogi-desktopThe 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.
While the mock runs, type a command in its terminal and press Enter to change
what it reports. help lists the commands, and devices numbers the devices
that the other commands address:
devices # 1 is the online Bolt mouse in the built-in script
battery 1 5 # it reads 5% and the overlay shows a low-battery notice
offline 1 # it goes offline; reads and writes fail
fail 3 error # the keyboard stays online, but every request fails
lock caps on # the overlay shows the Caps Lock indicator
backlight 2 4 # the overlay shows backlight level 2 of 4
disconnect # every client loses its connection and reconnectsThe low-battery notice and the lock-key indicator follow the notification
switches in Settings, as they do with the real agent. advance <seconds> moves
time only in a --fixture run.
The proposed architecture for recorded device profiles, raw HID++ replay, and
deterministic hardware scenarios is documented in
Mock device and hardware record/replay architecture.
Component gallery
Use the debug-only component gallery to review shared controls across light and
dark themes and every supported interface scale without config, IPC, or hardware:
OPENLOGI_COMPONENT_GALLERY=1 cargo run -p openlogi-desktopGallery mode opens one isolated window and bypasses the normal single-instance,
config, agent, asset-sync, and updater startup paths. The environment variable is
ignored by release builds.
Project layout
crates/
openlogi/ the `openlogi` binary — a thin wrapper over openlogi-cli
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-ui/ presentation shared by the two GPUI processes: ring geometry/icons,
the GPUI asset source, locale negotiation — gpui, no gpui-component
openlogi-desktop/ the `openlogi-desktop` binary — GPUI + gpui-component IPC client
openlogi-overlay/ the `openlogi-overlay` binary — the cursor-centred Actions RingAgent guidance
Shared rules have one tracked source: .agents/rules/.
Edit and link those .md files, not a client-specific copy. The tracked.claude/rules symlink points to ../.agents/rules inside the checkout.
Existing references to .claude/rules/<name>.md still resolve through that alias.
Crate-specific contracts stay in each crate's AGENTS.md; task workflows stay
in .agents/skills/. No rule generator or install step is required.
The root AGENTS.md holds global instructions and the rule index..claude/CLAUDE.md imports only that entrypoint. Claude Code discovers .md
rules through .claude/rules and uses their paths metadata for conditional
loading.
Other clients must follow the index; .agents/rules/ is not a universal
automatic discovery path. Keep the index as ordinary Markdown links: importing
every rule from the root would load unrelated guidance into Claude's context.
Check the checkout and client loading
From the repository root, in a POSIX shell or Git Bash:
git ls-files --stage -- .claude/rules .agents/rules
test -L .claude/rules && test -f .claude/rules/rust.md
readlink .claude/rulesExpect regular rule files under .agents/rules/ and one 120000 entry
for .claude/rules, whose link target is ../.agents/rules. The file checks
must succeed too: the index mode alone does not prove a working symlink.
On Windows, enable Developer Mode or obtain symlink creation permission before
cloning with git clone -c core.symlinks=true <repository-url> <new-directory>.
With core.symlinks=false, Git writes a text file containing the target instead
of a directory link; Claude cannot discover the rules through it. Changing the
config alone does not repair an existing checkout. Preserve local changes and
use a fresh symlink-enabled checkout. Until then, read the canonical rules via
the root index; do not replace the alias with independently maintained copies.
Verify loading in the client, not only the filesystem. In a fresh Claude Code
session, use /context or an InstructionsLoaded hook to inspect loaded files:
- Read
README.md: Rust and GUI path rules should not load solely from that read. - Read
crates/openlogi-core/src/lib.rs: the Rust rule should load, but not GUI. - Read
crates/openlogi-desktop/src/app.rs: the GUI rule should now load too.
Follow Claude's loading diagnostics
if those results differ. Filesystem checks and unchanged paths metadata are
not evidence that a particular client version loaded the rules correctly.
Local CI
The PR test pipeline is .github/workflows/ci.yml. To run every job this
machine can reproduce — including typos, the ast-grep guards, MSRV, cargo-deny,
and the Windows cross-lint the host-OS gate does not run:
cargo xtask ci
cargo xtask ci --list # job → command table
devenv tasks run openlogi:ci # same, from devenvThe runner sets RUSTFLAGS=-D warnings the way CI does. Jobs that need another
OS are reported as skipped; a skip is not a pass. The full job map (and which
diff requires which job) is .agents/rules/ci.md.
For structural searches and guard changes, use the ast-grep skill. The guard workflow defines placement beside the owner, root discovery, and verification.
Pre-push gate
Before pushing, read the local gate and push checklist.
That file owns tier selection, exact commands, and additional checks required by
the diff. devenv tasks run openlogi:check runs the full host-OS tier, not the
whole CI pipeline. A Rust-bearing rebase or conflict resolution requires the
full tier. Non-Rust changes use the applicable non-Rust checks.
GitHub workflow
Read this section before preparing, adopting, reviewing, or merging a PR.
These procedures do not authorize remote writes: obtain approval before pushing,
opening or merging PRs, publishing, or approving/rerunning workflows.
Preparing and merging PRs
- Always
git fetch upstream master(or origin) immediately before a rebase. Rebase
onto the refreshed tip, not a stale localmaster. - Merging PRs: squash by default with a hand-written subject
type(scope): description (#N)(release-plz parses it; merge commits are disabled).
Rebase-merge only when every commit on the branch is already release-quality
conventional. Wait for the Greptile review check and CI before merging — findings get
fixed, replied to, and resolved, not ignored. - PR bodies:
## Summary,## Changes(per-crate bullets),## Testinglisting the
exact commands run plus hardware-verification status (say "not runtime-tested on
hardware" when true — real-hardware verification is the maintainer's job, so every
fix PR states how to test it), and a closingFixes #Nline. Screenshots for UI
changes. - Issues use the bug/feature/device forms and the
type:/area:/platform:/needs:/status:label families. Deferred or out-of-scope work becomes a linked issue, not a
TODO comment.
Adopting contributor PRs
Contributor PRs are adopted, not rejected: check maintainerCanModify, rebase onto
fresh master in a worktree, fix review findings, run the applicable local gate
on the rebased tip (a Rust-bearing rebase takes the full tier), then push to the
fork branch; preserve authorship (Co-authored-by when re-homing work).
Squash-then-rebase is fine when the PR is far behind and commit-by-commit conflicts
thrash.
CI / Actions when adopting PRs
- CI concurrency is per branch (
ci-${{ workflow }}-${{ ref }}withcancel-in-progress: true). Approving or re-running an old SHA on the same
branch cancels the current-head run. Only approve / re-run workflows whosehead_shaequals the PR's current head. - After a force-push, wait for the new runs; do not re-approve stale
action_requiredjobs from earlier commits on that branch. - First-time-fork PRs may sit in
action_requireduntil a maintainer approves the
workflow run — that is fine; still do not push until the local gate is green.
Packaging the macOS DMG
cargo run -p xtask -- macos package # → target/release/OpenLogi.dmg
# Cross-compile a distribution DMG (aarch64 or x86_64):
cargo run -p xtask -- macos package --target x86_64-apple-darwinEnvironment overrides:
OPENLOGI_BUNDLE_ASSETS=1— bundle every device render into the.appfor a
fully offline build (default: fetched on demand at first launch).OPENLOGI_SIGN_IDENTITY=— codesign the.appand.dmgwith the
given Developer ID.OPENLOGI_SPARKLE_PUBLIC_ED_KEY=— embed Sparkle.framework and update
through it (see Release updater publishing). Without it the bundle updates
through the built-in engine.OPENLOGI_UPDATE_BASE_URL=— update host of the embedded Sparkle feed;
required withOPENLOGI_SPARKLE_PUBLIC_ED_KEY.OPENLOGI_DMG_BACKGROUND_URL=— override the branded DMG background
TIFF URL (default:https://assets.openlogi.org/dmg/dmg-background.tiff).
The local packaging command and release workflow both use the same branded DMG
layout: a 760×480 background image in a 760×512 Finder window, with 128px icons
positioned at (212, 250) for OpenLogi.app and (548, 250) forApplications.
Packaging Linux `.deb` / `.rpm` / `.pkg.tar.zst`
Requires nfpm on PATH; the package arch is
derived from the host (override with PKG_ARCH):
cargo run -p xtask -- linux package
# → target/release/*.deb / *.rpm / *.pkg.tar.zstThe package contents (binaries, udev rules, systemd user unit, desktop entry,
icon) are declared in packaging/linux/nfpm.yaml.
packaging/linux/install.sh is a POSIX /bin/sh frontend for those release
packages, not a second package lifecycle. Its online mode maps the host and
package manager to the release naming contractopenlogi-v<version>-linux-<amd64|arm64>.<format>, authenticates that file's
detached signature against its embedded minisign public key, verifies only that
file's SHA256SUMS entry, and then delegates installation to apt, dnf, yum,
zypper, rpm, or pacman. The embedded public key must stay in sync withOPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY. The nFPM post-install script remains the
owner of udev and desktop/icon cache reloads. The explicit --from-source mode
preserves the checkout installer for the four local target/release binaries
and shared resources.
Run the mocked online/source smoke suite directly with dash; it exercises
latest and pinned versions, both architectures, every package-manager mapping,
signature and checksum rejection before sudo, dry-run, and the source resource
set without network or system writes:
dash -n packaging/linux/install.sh
dash packaging/linux/tests/install-smoke.shThe Nix package uses the same shared resources and is declared inpackaging/linux/package.nix; see the Nix package section above for its build
commands.
Installation-source detection
The desktop app probes once in the background and publishes the typedplatform::installation::Installation global: Detecting, thenDetected(InstallationSource). It also logs detected installation source.
Settings → Updates displays the result as Installation source, separately
from the update download source. An open window refreshes when detection
completes or the interface language changes.
This is an ownership snapshot, not download provenance or an update policy;
the updater does not yet change behavior based on it.
- Homebrew: matches the installed receipt and Caskroom app back-link to
the running bundle, distinguishingopenlogifromopenlogi@latest. It
checks both standard prefixes,HOMEBREW_PREFIX, and prefixes discoverable
fromPATH, without executing brew. An undiscoverable custom prefix cannot
be recognized. Other macOS bundles reportMacAppBundle, not "DMG". - Linux: recognizes a resolved
/nix/store/executable, or queries dpkg,
rpm, and pacman for ownership of the exact executable byopenlogi.
Package queries are read-only, with a two-second timeout per command. - Windows: the MSI writes its
InstallLocationunderHKCU\Software\OpenLogi; only a matching executable isWindowsMsi.
The ZIP carriesopenlogi-installation.jsonnext toOpenLogi.exeand isWindowsPortable. A matching MSI registration takes precedence. - Unknown: unmarked Windows releases predating these markers, bare
source/manual installs, or otherwise inconclusive ownership. Missing
metadata never implies a portable ZIP or a DMG.
Release updater publishing
Tagged releases attach artifacts and SHA256SUMS to GitHub Releases for manual
downloads. When both Linux build legs succeed, the release workflow copiespackaging/linux/install.sh to dist/install.sh; that exact file is listed inSHA256SUMS, signed as install.sh.minisig, and attached as a release asset.
Partial releases without a complete Linux package set omit the installer. The
workflow also publishes the artifacts to Cloudflare R2 and writes a static
updater manifest at:
${OPENLOGI_UPDATE_BASE_URL}/channels/stable/latest.jsonThe app embeds the update host at build time via OPENLOGI_UPDATE_BASE_URL
(unset or empty means https://updates.openlogi.org) and derives the manifest
URL from it with openlogi_core::update_feed, the module that owns the
channel layout on the host. 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.
The app follows one update channel per launch, whichUpdateChannel::followed decides. A channel picked in Settings → Updates
(update_channel in config.toml) wins. Without one, a pre-release such as0.8.17-beta.12 follows beta and a final release follows stable. The
engine reads ${OPENLOGI_UPDATE_BASE_URL}/channels/<channel>/latest.json.
Sparkle keeps reading the stable appcasts below and, on beta, also offers
their items tagged <sparkle:channel>beta</sparkle:channel>. The beta
channel's latest.json offers the newest beta when it is newer than the
newest stable release, and that stable release otherwise. So an app on beta
gets every stable release too, with one exception. The engine compares
release versions and never offers a lower one, so an app on a beta numbered
past a release cut after it stays on that beta until a release numbered past
it. This happens when the release PR's version goes down after the beta,
whether the later release is stable or another beta. Sparkle compares build
versions and moves the app to the later release. release channels warns
whenever a beta is numbered past what the beta channel offers.
macOS: Sparkle
The signed macOS release bundle embeds
Sparkle and updates through it instead
(platform::updater in openlogi-desktop). Each architecture ships its own
DMG, so each reads its own feed:
${OPENLOGI_UPDATE_BASE_URL}/channels/stable/appcast-arm64.xml
${OPENLOGI_UPDATE_BASE_URL}/channels/stable/appcast-x86_64.xmlEach feed carries the newest stable release's item. When a beta is newer, the
beta's item comes before it, tagged <sparkle:channel>beta</sparkle:channel>.
An item is that architecture's notarized, stapled DMG with an EdDSA
(sparkle:edSignature) signature over the exact file, and the release notes
as Markdown. The bundle trusts only the key in its SUPublicEDKey, andSUVerifyUpdateBeforeExtraction makes Sparkle check that signature before it
unpacks anything.
Updates stay opt-in. The bundle ships SUEnableAutomaticChecks = NO, so
Sparkle never schedules a check or asks to; the app runs the one launch check
itself when Check for updates is on, without a window. With Automatic
updates also on, a found update downloads in the background and installs on
quit or through Restart to Update. "Check for Updates…" opens Sparkle's
update window. A bundle without Sparkle (Windows, Linux, local builds) and a
Sparkle that fails to load fall back to the engine and latest.json.
latest.json and the DMG minisigs keep being published: bundles from before
Sparkle read only that manifest, and they update into a Sparkle bundle through
it.
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 examplehttps://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 release artifacts beforelatest.jsonis 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.OPENLOGI_SPARKLE_PUBLIC_ED_KEY— Sparkle's base64 EdDSA public key, stamped
into the macOS bundle asSUPublicEDKey.OPENLOGI_SPARKLE_PRIVATE_ED_KEY— the matching base64 private key (the
32-byte seed), used only in the publish job to sign the DMGs into the
appcasts.release appcastrefuses a private key that does not belong to the
public key above.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 files under /releases/<tag>/: the artifacts
first, then the release's feed record in /releases/<tag>/feed/. The record is
the release's own latest.json and one single-item appcast per architecture.
Only the channel files are mutable: latest.json and the two appcasts under/channels/stable/, and latest.json under /channels/beta/. After the
record is up, cargo xtask release channels composes them from every record
on R2, and they go up last. Each channel offers its newest release in
Sparkle's order of build versions, whatever order the tags published in.
Items are copied from the records byte for byte, so their signatures are the
ones release appcast made. A record's appcasts go up before itslatest.json, and a record counts only once its latest.json is up, so an
upload that stops part way is skipped rather than blocking later releases.
Records began after v0.8.17; until a stable release has one, the current
stable channel files stand in for it, and after that they are not read.
Publish jobs run one at a time (the release-publish
concurrency group), so two tags never interleave these writes. Up to 100
waiting publish jobs queue (queue: max) instead of replacing each other, and
start in the order they began to wait. The queue only keeps writes apart: it
does not order releases by version, and it does not make a re-run safe. Never
re-run publish for a tag whose release has published, even partly. The release
is immutable, so the re-run fails, after it has already overwritten that tag's
R2 files and composed the channel files again. A missing artifact ships with
the next tag.
Generate the Sparkle key pair once, with the generate_keys tool from the
Sparkle release xtask
embeds:
./bin/generate_keys --account openlogi # prints the public key
./bin/generate_keys --account openlogi -x sparkle-private.key
# store the file's contents as OPENLOGI_SPARKLE_PRIVATE_ED_KEY, then delete itThe key cannot be replaced casually: every installed bundle trusts only the one
it shipped with. Rotating it means shipping a bundle with the new key that is
still signed with the old one, or relying on Sparkle's fallback to the DMG's
Developer ID signature.
After a complete Linux stable release is published, the workflow dispatchespublish-openlogi-installer with the tag to openlogi-org/get. That repository
downloads install.sh, install.sh.minisig, and SHA256SUMS from the published
GitHub Release, verifies both the exact checksum and minisign signature, and
commits immutable versioned files plus the latest stable aliases served by
Cloudflare Pages at get.openlogi.org. It mirrors no native package binaries.
The dispatch uses the same 1Password-backed GitHub App as the Homebrew update;
the App must be installed for the openlogi-org/get repository.
The workspace xtask helper writes the record into a stage laid out as on
R2. First the manifest:
cargo run -p xtask -- release latest-json \
--dist dist \
--tag v0.2.0 \
--base-url https://updates.openlogi.org \
--stage .release/stagethen the appcasts, after the DMGs are stapled (stapling rewrites them). The
private key is read only from the environment:
OPENLOGI_SPARKLE_PRIVATE_ED_KEY=… cargo run -p xtask -- release appcast \
--dist dist \
--tag v0.2.0 \
--build-version 0.2.0 \
--base-url https://updates.openlogi.org \
--public-key "$OPENLOGI_SPARKLE_PUBLIC_ED_KEY" \
--release-notes .release/RELEASE_NOTES.md \
--stage .release/stageAn item's sparkle:version is the release's build version (see
Release versions), and its sparkle:shortVersionString
and title are the tag's version. Sparkle's version comparator stops at the
first -, so it would read 1.2.0-beta.3 as 1.2.0.
The channel files come from a copy of R2's records and channel files, with
this release's record in it:
cargo run -p xtask -- release channels \
--tag v0.2.0 \
--mirror .release/bucket \
--output-dir .release/channels \
--base-url https://updates.openlogi.orgA stable release's item also offers Sparkle deltas from the three newest
earlier stable releases that embed Sparkle (0.8.14 and later). A beta's item
offers none, so a beta update downloads the full DMG. The macOS build leg
makes the deltas right after it staples the DMG, because Sparkle'sBinaryDelta runs only on macOS:
GH_TOKEN=… cargo run -p xtask -- release sparkle-deltas \
--dmg dist/OpenLogi-v0.2.0-macos-arm64.dmg \
--tag v0.2.0It downloads each earlier DMG from the GitHub Release, applies the new delta
to that app, and keeps the delta only when the result matches the new app file
for file. It writes <dmg>.from-<version>.sparkle.delta and a .json next to
it; release appcast reads that metadata and signs the delta. A delta that
cannot be made is a warning, and the feed still offers the full DMG. The deltas
go to /releases/<tag>/ with the DMGs; their metadata stays out of R2.
Sparkle applies a download as a delta only when its file name ends in.delta. With any other name it cannot unarchive the file and downloads the
full DMG instead. 0.8.16 and 0.8.17 shipped .sparkle-delta names, so no
client used those deltas.
Release versions
Every tag run starts with cargo xtask release version. It reads the tag and
checks the workspace version of the tagged commit. A stable tag is on
release-plz's version bump, so the workspace version must be the tag's
version. A beta tags master between releases without a version bump, so the
workspace version must be the stable release the beta follows. The command
prints these values for the build:
| Output | Stable v0.8.17 |
Beta v0.8.17-beta.12 |
|---|---|---|
channel |
stable |
beta |
version |
0.8.17 |
0.8.17-beta.12 |
build_version |
0.8.17 |
0.8.16.12 |
stamp_version |
empty | 0.8.17-beta.12 |
A beta builds at its own version. Each build leg that compiles OpenLogi
first runs cargo xtask release stamp-version --version <stamp_version>
(windows-msi compiles nothing and has no such step). It writes
the version into [workspace.package], into the version of every path
dependency and [patch] entry that points at a workspace crate, and into the
workspace crates in Cargo.lock, and commits nothing. A dependency belongs
to the workspace by its path, so a registry or git crate with a workspace
crate's name keeps its requirement. The command fails on a requirement it
would not write itself, such as 0.8, before it writes anything. After
writing, the command requires full cargo metadata --locked resolution and
the requested version on every workspace crate. On failure, it tries to
restore every file it may have written and names each file it could not
restore. That run compiles xtask at the old version, but
everything the leg builds after it, xtask included, sees the beta'sCARGO_PKG_VERSION: the update engine's current version, the About page,
diagnostics, CFBundleShortVersionString, the Linux packages and the
AppStream release. nfpm writes a pre-release as 0.8.17~beta.12 in the deb
and rpm. For the Arch package, linux package spells it 0.8.17beta.12,
because nfpm 2.46.3 leaves a semver pre-release out of the Arch pkgver;
it reads .PKGINFO back to check. Each sorts below 0.8.17. The Windows
executables' FILEVERSION keeps only the first three fields (0.8.17.0). A
stable tag's stamp_version is empty, and its legs skip the step.
To try a beta's build before its tag exists, dispatch the Build workflow on
the branch with stamp_version set. It builds unsigned and publishes
nothing. It is not quite a tag's build: without a build_version, the macOS
bundles' CFBundleVersion is the stamped version and the MSI'sProductVersion is 0.0.0. Sparkle would read 0.8.18-beta.1 as 0.8.18,
so a signed stamp without a build version fails before anything builds.
Sparkle and Windows Installer order releases by build_version. The macOS
bundles carry it as CFBundleVersion, and the MSI as its ProductVersion. A
beta's build version is the stable release before it plus a fourth field, the
beta number. Sparkle ranks it above the stable release before it and below
the next stable release. Windows Installer ignores the fourth field, so it
treats the beta and that stable release as the same version, and the MSI
allows same-version upgrades.
The stable release before a beta is the newest stable tag reachable from the
beta tag, and the beta's version must be above it. The beta number is the
count of commits from that stable release to the beta
(git rev-list --count), so it is at least 1 and grows with master.release version fails if the tag's number is a different count.
Only vX.Y.Z and vX.Y.Z-beta.N are release tags.xtask/src/support/release_tag.rs owns the syntax and the encoding.
The publish side follows the channel. A beta becomes a GitHub prerelease and
never the latest release. It leaves the installer site and openlogi@latest
as they are. Publishing recomposes the channel files in build-version order.
The beta channel offers the beta with the highest build version if it is
above the newest stable release's; otherwise it offers that stable release.
The appcasts contain the stable item and any beta item the beta channel
offers, and only apps on beta receive the beta item. The stable channel'slatest.json keeps its bytes.
The Homebrew tap gets one event per channel. A stable release sendsupdate-openlogi for openlogi@latest. A publish that changes what the beta
channel offers sends update-openlogi-beta for openlogi@beta, with the tag
the beta channel now serves: the new beta, or a stable release newer than
every beta. A tap workflow that listens only for update-openlogi ignoresupdate-openlogi-beta, so a beta event cannot change openlogi@latest
through it.
A stable release's notes compare with the previous stable tag, so they cover
its betas too. A beta's notes compare with the highest-version release tag
below it that its commit can reach, betas included.cargo xtask release version picks that tag and prints it asprevious_tag; publish passes it to GitHub's release-notes API.
Cutting a beta
Run the Prerelease workflow (.github/workflows/prerelease.yml) frommaster in the Actions tab. It tags the master commit the run was
dispatched on, and does not move to a newer commit when master advances.
Nobody tags a beta by hand. It finds the open release-plz PR whose head
contains that commit, the way release-plz.yml finds it, and stops if there
is none. Then it runscargo xtask release next-beta --release-ref . The beta's
version is that PR's workspace version, the next stable release. Its number
is the count of commits since the stable release before it. With the release
PR at 0.8.18 and master three commits after v0.8.17, the tag isv0.8.18-beta.3, and its build version is 0.8.17.3.
release next-beta refuses to name a beta when:
- the release PR's version is not above the stable release before the beta;
mastercarries a version that has no stable tag yet;- the commit is that stable release, or already has a beta tag;
- the tag exists, is not above the newest beta, or the newest beta is not on
the commit's history; - the build version does not fit an MSI
ProductVersion.
release version checks the first, second and last of these again on the
pushed tag. They check the tag's version only, so a build, signature or
publish can still fail. The others keep betas rising, one per commit.
release next-beta reads the HEAD commit and the tags, not the working
tree, so an uncommitted edit cannot change its answer. It needs the whole
history and every v* tag, and refuses a shallow repository, because a
shallow history can hide commits from the count. To run it outside the
workflow, run git fetch --tags in a full clone first.
The workflow creates the tag with the GitHub App's token, because a tag thatGITHUB_TOKEN pushes starts no workflow. The tag push runs release.yml. A
dry run prints the tag and creates nothing. Only one cut runs at a time, and
a newer pending run replaces an older pending one.
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.toml), .config/crowdin.yml, the Crowdin workflow,
the merge script under .github/scripts/i18n/, or the shared GitHub App token
action.
How it helps translation
| Role | |
|---|---|
en.toml (git) |
English source of truth; stable semantic keys grouped by product-domain tables |
All locales/*.toml in git |
Same keys as en.toml (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 |
Call sites use stable keys such as device.connected. Feature PRs add
new keys to every locale file in the same change. English wording can change
without renaming the key or updating call sites. Crowdin does not invent
translations; it only stores and syncs them. A raw Crowdin download is unsafe:
untranslated strings come back as English (#549), andskip_untranslated_strings overwrites catalogs with sparse files that delete
keys (#552). The workflow always snapshots → download → merge via.github/scripts/i18n/merge_crowdin_download.py so catalogs stay complete and only real
translations land in git.
Each run:
- Snapshots every
locales/*.toml. - Uploads
en.tomlsources. - Uploads per-language translations already in git (
import_eq_suggestions
off sovalue == Englishis not stored as a finished translation). - Downloads Crowdin’s export (
skip_untranslated_strings; sparse is fine). - Merges the export into the snapshot (English fill-in ignored; omitted keys
kept; headers /_versionpreserved). - Opens/updates
crowdin/i18nonly 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 matchen.toml 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 withpersist-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):
devenv tasks run openlogi:i18n-upload # en.toml sources + per-language translations
devenv tasks run openlogi:i18n-download # download + merge + i18n tests
python3 .github/scripts/i18n/merge_crowdin_download.py --self-testINSTALL Linux (`docs/INSTALL-linux.md`)
Installing OpenLogi on Linux
[!NOTE]
Linux support is in active development. HID++ device enumeration supports
Logi Bolt (USB PID0xC548) and Logi Unifying (PID0xC52Band
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
hidrawanduinputmodule support (standard on all major
distros). systemd+udev(standard on Ubuntu, Fedora, Arch, Debian, openSUSE, …).- GLIBC 2.35 or newer for the pre-built packages (Ubuntu 22.04 baseline).
minisign, installed through your distribution's package manager, for
authenticating release packages.
Install a release package
Install minisign through your distribution, then run the installer as your
normal user (not with sudo):
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | shThe script resolves the latest GitHub release, detects x86_64/aarch64 and
apt, dnf, yum, zypper, rpm, or pacman, then downloads the exact matching.deb, .rpm, or .pkg.tar.zst. It authenticates that package's detached
signature against the minisign public key embedded in the script, verifies the
package's one entry in the release SHA256SUMS, and only then runs the
package-manager command with sudo. nFPM's package scripts reload udev and the
desktop/icon caches. The installer refuses to run as root.
Review the installer first
The one-line command trusts the install.sh attached to the release. To read
and verify the script before it runs, download it with its signature instead:
base=https://github.com/AprilNEA/OpenLogi/releases/latest/download
curl --proto '=https' --tlsv1.2 -fsSLO "$base/install.sh"
curl --proto '=https' --tlsv1.2 -fsSLO "$base/install.sh.minisig"
minisign -Vm install.sh -P RWRRkFtw+rqkvTlCTGKUszSE5dX9CK1teaQD45jO4P9rYlWLO4/nHVUF
less install.sh
sh install.shThe public key is the same one the script embeds for package signatures.
Useful options:
# Pin a release (an optional leading v is accepted):
sh install.sh --version "$VERSION"
# Pass options through the one-line command with `sh -s --`:
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | sh -s -- --dry-run
# Override detection, verify without installing, or leave the agent stopped:
sh install.sh --package-manager zypper
sh install.sh --dry-run
sh install.sh --no-startThe installer enables and starts openlogi-agent.service for the current user
when systemd is available; failure to reach the user service manager does not
roll back an otherwise successful package installation.
NixOS
The Flake in the repository's packaging/ directory provides a package and a
NixOS module for x86_64 and aarch64 Linux. Importing the module is preferred
over adding the package to environment.systemPackages by itself: the module
also registers the udev rules required for device access and manages the
agent's user service.
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs.openlogi = {
url = "github:AprilNEA/OpenLogi?dir=packaging";
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { nixpkgs, openlogi, ... }: {
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; # or aarch64-linux
modules = [
openlogi.nixosModules.default
{
programs.openlogi = {
enable = true;
# Starts openlogi-agent with graphical-session.target by default.
launchAtLogin = true;
};
}
];
};
};
}Set programs.openlogi.launchAtLogin = false to install the package and udev
rules without automatically starting the agent. It remains available assystemctl --user start openlogi-agent.service.
For a build without installing the module:
nix build 'github:AprilNEA/OpenLogi?dir=packaging#openlogi'Earlier versions of this guide used github:AprilNEA/OpenLogi without?dir=packaging. That URL now points to a deprecated alias: it needs Nix 2.26
or newer, prints a warning on every evaluation, and will be removed in a later
release. Add ?dir=packaging to the input URL to move off it.
Flatpak
Each release carries a .flatpak bundle for amd64 and arm64, on the
releases page:
flatpak install --user ./openlogi-*.flatpakIt is a one-time install. There is no remote behind a bundle, so flatpak update has nothing to check and a new version means downloading the next one.
[!IMPORTANT]
A Flatpak cannot write to/etc, so the udev rules have to be installed on
the host separately. Until you do, no devices are detected at all: the
sandbox is allowed to reach/dev/hidraw*,/dev/uinputand/dev/input/event*, but the kernel still refuses your user access to them,
so the app opens to an empty device list.
The same rules the packages install ship inside the app, so this needs no
checkout and no extra download:
flatpak run --command=cat org.openlogi.OpenLogi \
/app/share/openlogi/udev/70-openlogi.rules |
sudo tee /etc/udev/rules.d/70-openlogi.rules >/dev/null
sudo udevadm control --reload-rules
sudo udevadm triggerNothing extra is needed for /dev/uinput: the rules create its node at boot
with static_node=uinput, and opening it loads the module. See
Device access: udev rules for what the rules grant
and how to verify them.
The agent installs its input hook once at startup, so restart the app after
granting access:
flatpak kill org.openlogi.OpenLogiThen launch it again. There is no user service to enable — unlike the packages,
the application launches its own agent.
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:
git clone https://github.com/AprilNEA/OpenLogi
cd OpenLogi
cargo build --release \
-p openlogi -p openlogi-desktop -p openlogi-overlay -p openlogi-agentFour production executables land in target/release/:
| Binary | Role |
|---|---|
openlogi |
CLI — inventory, diagnostics, asset sync |
openlogi-desktop |
Desktop GUI |
openlogi-overlay |
Actions Ring overlay helper |
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, sologindnever 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):
sudo cp packaging/linux/udev/70-openlogi.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm triggerVerify access (should open without error):
# 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 triggerre-evaluates rules but does
not re-grantuaccessACLs 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:
sudo usermod -aG input "$USER"
# Re-login for the group change to take effect.Install from source with the script
From a repository checkout, --from-source copies the four localtarget/release binaries, udev rules, systemd unit, desktop entry, and icon to
system paths, then reloads udev and desktop/icon caches. The script requestssudo only for system files and lifecycle commands; do not run the whole
installer with sudo. --prefix applies only to this mode.
# From the repo root, after building:
packaging/linux/install.sh --from-source
# Or to a custom prefix (e.g. /usr):
packaging/linux/install.sh --from-source --prefix=/usrTo remove a source installation (release packages should be removed through
their package manager):
packaging/linux/uninstall.shAutostart (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:
systemctl --user enable --now openlogi-agent.serviceAlternatively, toggle Settings → General → Launch at login in the GUI. When
a packaged unit is already installed it simply enables that one. Otherwise — a
build from source, or an install under a custom prefix — it generates a unit at~/.local/share/systemd/user/openlogi-agent.service pointing at the running
binary.
Either way ~/.config/systemd/user/openlogi-agent.service stays yours: systemd
ranks it above both locations, so a unit you write there overrides whatever
OpenLogi does. Use systemctl --user edit openlogi-agent.service for a drop-in
that survives package upgrades.
OpenLogi never overwrites or deletes a unit it did not generate, at either
location, and it only turns off an autostart it turned on. Enabling the service
yourself with the command above keeps working regardless of the GUI toggle.
Verify the installation
# List connected Logitech devices:
openlogi list
# Launch the GUI:
openlogi-desktopHyprland and Omarchy
When the agent runs inside a Hyprland session, native actions use the
Hyprland/Omarchy-compatible commands below:
| OpenLogi action | Linux behavior |
|---|---|
LaunchpadShow |
omarchy-menu toggle |
PreviousDesktop |
hyprctl dispatch workspace e-1 |
NextDesktop |
hyprctl dispatch workspace e+1 |
LockScreen |
omarchy-system-lock |
The lock action never sends Super+L on Hyprland because Omarchy uses that
shortcut to change the workspace layout. If an Omarchy helper is unavailable
or exits unsuccessfully, OpenLogi falls back to the corresponding compositor
shortcut; the lock fallback is Super+Ctrl+L.
The agent selects this behavior when HYPRLAND_INSTANCE_SIGNATURE is present.
Because the agent normally runs as a systemd user service, verify that its
manager environment contains both that variable and XDG_RUNTIME_DIR:
systemctl --user show-environment | \
grep -E '^(HYPRLAND_INSTANCE_SIGNATURE|XDG_RUNTIME_DIR)='If either variable is missing, configure the session to import it into the
systemd user manager before restarting openlogi-agent.service. The helper
executables are resolved from /usr/bin and /usr/local/bin; missing helpers
use the keyboard fallbacks above.
Known limitations
| Limitation | Status |
|---|---|
| Wayland: per-application profile switching | Native on wlroots compositors (sway, Hyprland, …); GNOME needs the companion extension; KDE falls back to XWayland. wlroots reports the xdg-shell app_id while X11 and GNOME report WM_CLASS, so a profile keyed under one does not match under the other |
| Button capture: middle / mode-shift / thumbwheel | Side buttons only today |
MOCK DEVICE TESTING (`docs/MOCK_DEVICE_TESTING.md`)
Mock device and hardware record/replay architecture
Status: implemented architecture for fixture contracts, replay, recording,
CLI capture/verification, semantic mock profiles, and injected agent hardware.
The root corpus contains reviewed captures with automatic device, Agent, IPC, and desktop regression suites. Fixture schema v1 remains unreleased and is not yet a stable public format. See current guarantees and commands.
Why this needs more than one mock
OpenLogi already has two useful but separate hardware-free paths:
openlogi-agent-mockimplements the agent IPC service from a hard-coded,
mutable inventory. It is good for manual GUI development, but it bypasses
device enumeration and HID++ parsing.openlogi-device::replaysupplies cassette-backed channels and mutable
topology below production device code. Focused unit tests still use concise
scripted responders where a persisted cassette would obscure the contract.
The missing piece is not one more fake device. It is a small test platform that
can retain facts learned from real devices, replay them below production device
logic, and add deterministic behavior that cannot be learned from one capture.
Hardware complexity makes the distinction important:
- One OS node may be a Bolt or Unifying receiver serving several device slots,
while a Bluetooth or wired device is addressed directly. - Node presence, successful open, channel connectivity, receiver pairing, and
paired-device online state are independent. Collapsing them into oneonlineflag cannot reproduce recovery bugs. - HID++ 1.0 register traffic and HID++ 2.0 feature traffic have different
correlation rules. HID++ 2.0 feature indexes are assigned by each device at
runtime and its software ID can differ between channel opens. - A channel has a dedicated reader and can carry concurrent requests,
responses, late responses, and unsolicited lifecycle/input notifications. - Some output uses request/response HID++ reports, some lighting uses 64-byte
0x12reports, and standalone Litra devices are tried over HID++0x1990
before falling back to raw writes with no response. - Enumeration is event-first and stateful. It reuses channels, caches immutable
probe data, refreshes volatile battery state, replays last-good inventory
through transient failures, and eventually retires and reopens unhealthy
channels. - Gesture capture and pairing are asynchronous state machines with cleanup
obligations. A successful byte transcript does not describe what should
happen after cancellation, sleep, disconnect, or a missing response. - The native backend includes behavior a portable replay cannot prove: macOS
TCC identity and exclusive opens, Windows composite short/long collections,
Linux receiver-child filtering, and host HID callback failures.
The framework therefore has three source artifacts with different contracts:
semantic reads
┌──────────────┐ ───────────────────▶ Device Profile
│ Real device │ │
│ and receiver │ ├──▶ IPC mock / desktop tests
└──────┬───────┘ └──▶ human-reviewed expectations
│
│ observed HID reports
└──────────────────────────────────▶ HID Cassette
│
└──▶ device-layer replay
Hand-authored deterministic transitions ─────▶ Scenario
│
└──▶ lifecycle/fault/state testsGoals
- Turn a real but otherwise unavailable Logitech device into a reviewable,
sanitized regression fixture. - Exercise production HID++ parsing, feature probing, routing, reads, and
writes without hardware in CI. - Feed realistic device inventories and initial settings to the existing IPC
mock without making the GUI know it is using a fixture. - Reproduce lifecycle and failure paths deterministically, without real-time
sleeps or flaky global ordering assumptions. - Make an incomplete or stale fixture fail loudly: unmatched requests,
unconsumed required exchanges, unsupported schema versions, and sensitive
identifiers are errors. - Keep platform-specific hardware verification explicit. A replay pass is not
evidence that macOS TCC, Linux hidraw access, Windows composite opening, or
a physical write works.
Non-goals
- Emulating arbitrary Logitech firmware.
- Recording all traffic from a normal desktop session and treating it as a
deterministic test. - Replacing focused hand-written protocol and state-machine unit tests.
- Running real hardware in mandatory CI.
- Adding mock-control methods to the append-only production agent IPC service.
- Making fixture data part of the agent/GUI wire format or product config.
- Proving that a value generated by the current parser is correct merely by
recording it as a golden value.
Testing layers
The layers intentionally overlap. A failure should name the lowest broken
contract rather than surface only as a GUI mismatch.
| Layer | Production code exercised | Data source | Primary purpose |
|---|---|---|---|
| Protocol codec | openlogi-hidpp message/feature decoders |
Hand-written bytes | Byte layout and unknown-value handling |
| Device operation | openlogi-device over a replay raw channel |
HID cassette | Feature lookup, reads, writes, fallback |
| Discovery/recovery | Enumerator over a replay HidBackend |
Profile + cassette + scenario | Topology, cache, ledger, channel lifecycle |
| Agent semantics | MockAgent initially; real agent-core later |
Device profile + scenario | Snapshot and command contract |
| Desktop projection | Desktop state merge/render seams | Agent snapshot derived from profile | Panels, routes, online/offline presentation |
| Hardware-in-loop | Native backend and physical device | Opt-in local run | Host permissions, opens, transport, actual effects |
The first implementation should stop at the device and IPC-mock layers. Full
real-agent replay requires coherent hardware dependency injection and is a
later phase, not a prerequisite for useful fixtures.
Architectural invariants
- Production device logic never branches on “mock”. It sees a
HidBackend,RawHidChannel, orRawWriterwith the existing contracts. - Physical topology and device state stay separate. OS node presence,
open result, channel connection, receiver slot pairing, and link liveness
are distinct values. - Identity stays separate from transport. A specimen can have Bolt,
Bluetooth, and USB links. Captures must not merge two physical devices just
because their model IDs match, and must not turn an OS node ID into a stable
identity. - Capabilities come from observed feature data, not device kind. Fixtures
preserve the same rule as production inventory. - Raw evidence and semantic expectations are separate. Raw reports can be
replayed after parser changes; expected inventory/settings are independently
reviewable assertions. - Recorded order is local, not global. Concurrent receiver slots may
interleave differently. Matching is per logical channel and normalized
request; unsolicited events are explicit. - Fixture time is logical. Tests advance state or release a barrier. They
do not wait for the production 30-second recovery scan, 90-second pairing
deadline, or mock battery timers. - Recording is read-only by default. A write case needs explicit consent,
a known restoration plan, and a failed restoration prevents fixture output. - No sensitive raw data reaches disk by default. Sanitization happens in
memory before an atomic write. - Strict replay is the default. Unknown outgoing requests fail
immediately, and every required exchange must be consumed by the end of a
case.
Ownership
Fixture contracts and device replay have different dependency boundaries. The
schema is a published, host-free crate consumed by capture, verification, and
semantic mocks; replay stays beside the HidBackend contract it implements.
`openlogi-fixture`
Owns the persisted contract and its pure verification:
- Versioned semantic profile, cassette, and manifest schemas.
- Synthetic identity generation and protocol-aware identity extraction.
- Exact relationship, privacy, occurrence-count, and framing validation.
- The packaged canonical synthetic profile used by the no-argument mock.
It performs no file I/O, host access, or async work and depends only onopenlogi-core plus serialization/error support. This keeps the published CLI
dependency closure valid and lets schema checks run without pulling in HID++
transport.
`openlogi-device`
The replay module owns runtime behavior over fixture cassettes:
ReplayRawHidChannel,ReplayRawWriter, andReplayBackend.- A mutable virtual topology and explicit event injection handles.
- Response barriers, request matching, and completion diagnostics.
Reusable transport plumbing lives here rather than in the test-onlychannel::scripted module. Concise responder helpers remain for hand-written
unit tests; a fixture should not make small tests harder to read.
This remains portable: it knows HID++ and HidBackend, not async-hid, paths,
files, clocks, or a host OS.
`openlogi-hid`
Own native recording because this is where async-hid channels are turned
into HidppChannels and raw writers:
- A recording backend variant wraps native channels before they are shared.
- An optional channel observer records actual post-normalization reports and
request outcomes. - Raw
0x12/Litra writers are observed separately because they bypass the
request/response channel. - Native
NodeInfo, supported report widths, open failures, and channel IDs
are reported to the recorder without becoming persisted physical identity.
The process-wide native HID manager remains shared. A recorder may own a
separate backend facade and handle cache, but it must not create a second host
HID manager per operation.
`openlogi-cli`
Own user interaction and file I/O:
openlogi fixture contributeopenlogi fixture record profileopenlogi fixture record caseopenlogi fixture verify <fixture-directory>- Strict target selection, safety warnings, sanitization report, case
self-replay, atomic publication, and strict corpus loading.
snapshot already means camera capture, so fixture commands must not reuse
that name.
`openlogi-agent-mock`
Load a semantic profile and construct its own current AgentStatus, generation,
camera, foreground-app, and pairing state. A captured AgentSnapshot is not the
fixture format: it contains host status and protocol-version concerns that do
not describe a device.
The built-in scripted inventory remains the no-argument default. Manual demo
mode can retain moving battery/app/camera behavior. Test mode freezes those
inputs and changes them only through a scenario or the console's advance
command.
While it runs, the mock reads one console command per stdin line (help lists
them). The console changes only semantic state that the IPC contract already
exposes:
- a paired device's battery reading;
- a device's online flag;
- a fault that makes every read and write to an online device fail;
- a lock-key or backlight indicator change;
- frozen test time;
- every open client connection, closed as an agent restart closes them.
Battery readings and indicators go through the same agent-core state machines
that the orchestrator uses. Tests build Command values and callMockAgent::apply directly; the command words are for people only. The console
does not replace the scenario controls below: it models no channel, report, or
barrier.
`openlogi-agent-core` (later)
When full-agent replay has a concrete test that needs it, introduce one
coherent hardware runtime value rather than adding backend parameters one call
at a time. It must supply the same backend to:
- the persistent enumerator and hotplug watcher;
- the channel pool and registry;
- pairing;
- standalone enumeration and Litra writes;
- the device I/O gate and probe-cache store.
Production constructs a native runtime; tests construct a replay runtime with
an in-memory cache store. This is dependency injection, not a new event bus:
hotplug and HID lifecycle input continue through their existing streams. No IPC
change is needed.
Virtual hardware model
The replay backend models four orthogonal levels.
Topology
VirtualHardware
nodes[]
NodeInfo (sanitized)
presence: absent | present
open: hidpp | not_hidpp | denied | disconnected
report_support: short_and_long | long_only
raw_writer: optional
logical_channel_idTopology snapshots are returned by enumerate/enumerate_hidpp. A scenario
updates the snapshot and emits a HotplugEvent; the event never secretly
changes topology by itself. Tests can therefore reproduce a missed event or a
changed snapshot independently.
Channel
A replay channel owns:
- pending cassette exchanges keyed by normalized requests;
- an incoming report queue;
- an explicit connected flag;
- unsolicited event injection;
- all observed writes;
- a completion report naming unmatched and unconsumed exchanges.
Several routes behind one receiver share one logical channel. Direct devices
use separate channels. Reopening a node creates a new channel lifetime so tests
can distinguish a repaired connection from continued use of a stale one.
Device state
The semantic mock starts with a small typed state, not a generic property bag:
- DPI current value and capabilities;
- SmartShift status;
- scroll mode;
- backlight state;
- accepted lighting/light operations;
- battery snapshot;
- online/offline status.
Only add a state field when production exposes a read-after-write contract for
it. HID cassettes can assert exact writes without pretending to model the
resulting firmware state. This prevents the framework from growing into an
incomplete firmware implementation.
Scenario control
Scenarios perform explicit transitions such as:
- connect or remove a node;
- fail the next open;
- disconnect an open channel;
- put one receiver slot to sleep or wake it;
- enqueue an unsolicited HID++ notification;
- answer a request with a feature error or malformed report;
- release a blocked request after the test reaches a barrier;
- begin, advance, cancel, or fail pairing.
Start with hand-authored Rust scenarios. A serialized scenario language should
be added only after several tests repeat the same transition vocabulary. Static
device facts and captures belong in data; race orchestration is clearer in code.
Fixture corpus
Store one directory per sanitized physical specimen, with one or more transport
links:
fixtures/devices/
mx-master-3s-001/
manifest.json
profile.json
cases/
bolt-probe.json
dpi-read.json
smartshift-read.jsonThe built-in profile-only synthetic fixture is packaging data rather than
captured corpus. It lives undercrates/openlogi-fixture/fixtures/devices/openlogi-canonical-synthetic-001/
so crates.io packages remain self-contained. It must not be copied into the
root corpus or presented as physical-hardware evidence.
The numeric suffix is a synthetic specimen identity, not a device serial. Two
captures are linked as the same specimen only when the recorder/operator knows
they came from the same physical unit. Matching model IDs is insufficient.
`manifest.json`
Contains the exact privacy and relationship contract, not capture provenance or
semantic expectations:
- fixture schema version;
- synthetic specimen ID and related profile ID;
- named case, logical channel, and receiver/device relationship;
- fixture-local receiver and device principals with typed profile routes;
- canonical synthetic identity representations;
- exact nonzero occurrence counts at typed profile/cassette locations.
Do not store contributor identity, local paths, OS node IDs, capture timestamps
that create churn, hashed hardware identifiers, original values, or asanitized boolean. Verification re-extracts identity evidence from the profile
and every cassette rather than trusting capture metadata.
`profile.json`
Contains semantic initial state and expected domain values:
- receiver/direct/raw-HID shape;
- paired device records and capabilities;
- standalone device capabilities;
- supported setting reads and their initial results;
- stable synthetic identities preserving relationships within the fixture.
Use the existing domain types where their JSON shape is appropriate, wrapped in
an independently versioned profile schema. Do not serialize bincode IPC goldens
or make profile schema changes bump PROTOCOL_VERSION.
`cases/*.json`
Each file represents one named operation or coherent probe, not an ambient
session:
{
"schema_version": 1,
"name": "dpi-read",
"channel": "bolt-receiver-1",
"report_support": "short_and_long",
"exchanges": [
{
"request_match": "hidpp20",
"request": "1101001100000000000000000000000000000000",
"response": "1101001104000000000000000000000000000000",
"required": true
}
]
}The example illustrates shape only; captured values must come from the actual
operation. Hex is lowercase and includes the report ID. The cassette contains
raw evidence only; independently reviewed semantic expectations belong in the
profile or an operation-contract test and are not silently regenerated from
replay output.
Split cases because a full inventory probe can contain hundreds of exchanges
and changes more often than a focused DPI read. A regression should identify
the operation that no longer matches.
Run openlogi fixture verify <fixture-directory> before review. The loader
accepts only a real, non-symlink directory whose name equals the manifest ID,
requires exactly manifest.json, profile.json, and the complete declaredcases/*.json set, and rejects unknown files, unsafe case names, symlinks, and
unknown JSON fields. It reports schema, relationship, privacy, and replay
validation separately. This command performs no hardware access and does not
establish semantic or physical correctness.
The CLI corpus tests discover and strictly verify every directory under bothfixtures/devices/ and the packaged synthetic corpus. The root corpus may be
absent until the first contribution; once present, every entry must be a valid
fixture directory. Run cargo test -p openlogi-cli fixture::verify locally.
Recording model
Two capture modes
Semantic profile capture should prefer the running agent's existing IPC
snapshot and read methods. The agent is the normal owner of device permission
and already holds stable channels. This mode records no raw reports and needs no
new IPC method.
Raw case capture is a controlled direct diagnostic operation. It uses the
same ownership model as the existing openlogi diag commands: discover devices,
select one target, open it, run one named read operation, and exit. Discovery
uses production enumeration, which may write receiver notification flags
(register 0x00) and request arrival reports (register 0x02) on connected
receivers before target selection. Those flags are not restored. The CLI warns
before discovery; use semantic profile capture to avoid this direct access.
These setup writes are outside the cassette and are not accepted as read-only
exchanges by the sanitizer. Capture does not change device settings or pairings
and is not an ambient sniffer.
Before enumerating hardware, raw fixture capture acquires the same profile'sagent.lock used by agent startup, then checks that the IPC endpoint is
unreachable. The selected target holds the lock for the entire capture, including
all eight cases in the contribution wizard. This excludes agent relaunches and
other fixture captures until completion; errors, cancellation, and process exit
release the lock. It does not exclude Options+ or other direct diagnostic tools.
Contribution wizard
openlogi fixture contribute combines the safe capture modes without changing
their ownership boundaries:
openlogi fixture contribute \
--id mx-master-3s-001 \
--name "MX Master 3S" \
--device "MX Master 3S" \
--output fixtures/devices/mx-master-3s-001Run it once while the Agent is available. The CLI captures and validates one
semantic profile, then stores only that sanitized profile and a hidden
resumption file containing synthetic fixture metadata and a sanitized route.
It never stores the selector, original receiver identity, node path, hashes,
timestamps, or contributor identity.
For HID++ devices, stop the Agent and rerun the same command. The CLI confirms
the same transport kind and receiver slot (or direct VID/PID), records all eight
supported read-only operation cases, self-replays every candidate, and generatesmanifest.json from the finalized profile and cassettes. Manifest generation
re-extracts exact identity occurrences; it does not trust sanitizer audit
counts. A profile-derived synthetic identity plan keeps cassette receiver and
device identities related to the profile even when the selected receiver slot
is not the first retained device.
The wizard captures all cassettes in memory before publishing any case. A
capture or verification failure leaves the resumable state in place and writes
no partial case set. The state file is removed only after cases and manifest
have been written; strict on-disk verification then rejects any unexpected
entry or relationship. No upload, branch, commit, or pull request is created.
--profile-only ends after the Agent phase. Raw-HID standalone devices use this
mode automatically because fixture schema v1 has no safe replay contract for
raw writes. The lower-level fixture record profile and fixture record case
commands remain available for focused diagnostics, but contributors should not
hand-author the manifest or identity occurrence ledger.
On macOS these are different TCC identities. A packaged or development agent's
Input Monitoring grant does not authorize the CLI, and a GUI grant authorizes
neither. The raw recorder must never prompt indirectly or claim the agent's
grant applies to it. It should:
- detect and refuse conflicting agent/Options+ ownership where possible;
- identify itself as direct CLI hardware access;
- report an open failure using the existing permission/exclusive-open
classification; - leave semantic profile capture available through the agent when direct
capture is not authorized.
Do not add raw-recording methods to production IPC. They would permanently
expand an append-only wire with a developer-only, sensitive-data interface.
Reliable request association
Raw write then read order is insufficient: receiver slots probe concurrently
and unsolicited reports can appear between a request and its response. The
recording path needs an optional no-op-by-default observer at theHidppChannel boundary, where pending request IDs and response matching are
already known.
The observer records:
- outgoing request ID and actual post-normalization report;
- matched incoming report and request ID;
- unmatched incoming report;
- timeout, write failure, or cancellation outcome;
- fire-and-forget and raw writes without a response ID.
Incoming bytes should be observed before parse rejection as well, so an
explicit malformed-report case can retain its evidence. The native backend
adds node/channel metadata; it does not reimplement correlation.
Capture plans
Version capture plans by name. The initial read-only plan should contain:
- target identity and transport metadata;
- production inventory/probe for the selected node;
- feature and firmware tables;
- capability-gated reads for battery, DPI, SmartShift, wheel, controls, and
backlight; - semantic expectations produced as a review candidate;
- local self-replay before output is accepted.
Unsupported features and typed device errors are valid recorded outcomes. Host
open errors and interrupted captures are capture failures, not device fixtures.
Target selection must reject ambiguity. The current diagnostic convenience of
choosing the first case-insensitive name match is not safe when a corpus must
claim which physical route it represents.
Writes
The default plan records only reads. Its pre-selection receiver discovery has
the notification/arrival setup side effects described above; it is not a
zero-write hardware session. Setting-write cases are not implemented. A future
named write case requires --allow-writes and must define:
- the initial read;
- a bounded alternate value supported by that device;
- verification when the protocol exposes it;
- restoration of the exact initial value;
- an error path that attempts restoration and refuses to write a fixture if
restoration is not confirmed.
This still cannot protect against process or host failure between write and
restore, so the CLI must state the risk before it starts. Pairing/unpairing is
excluded from automated recording in the first versions because it is
destructive, interactive, and carries addresses/passkeys.
Output transaction
The recorder performs the following transaction entirely before the final path
appears:
- capture into memory;
- classify and sanitize identifiers;
- validate schema and invariants;
- replay through production device APIs;
- compare replay output with the candidate semantic result;
- run the privacy validator;
- write a temporary file and atomically rename it.
Self-replay proves completeness and determinism, not semantic correctness. A
reviewer still confirms model, transport, capabilities, and expected values.
Sanitization
Values requiring replacement include at least:
- receiver unique ID/serial;
- device serial number and unit ID;
- raw-HID serial identity and host node identity;
- Bluetooth device address;
- pairing passkey;
- any local path or foreground-application data accidentally included by a
broader capture.
Replacement is relation-preserving and protocol-aware. For example, every
occurrence of one receiver ID maps to the same synthetic receiver ID in the
profile and raw response. Use generated synthetic values, not hashes of the
original: a hash remains a persistent fingerprint and can be reversible for a
small identifier space.
Raw payload replacement must preserve report length and field validity. The
sanitizer tracks feature-index mappings learned during the capture so it can
recognize known identity functions even though their runtime indexes differ by
device. Receiver registers use their own protocol-specific rules.
Unknown raw data is not automatically safe. If the sanitizer cannot classify a
report that may carry identity or pairing data, it may retain it only in an
explicit local/private capture; it must refuse to mark the fixture committable.
Passkeys are never persisted, even in a sanitized pairing transcript.
The repository test walks every fixture and reruns the privacy validator. This
guards later manual edits as well as the recorder.
Replay matching
HID++ 2.0 normalization
Match the actual report ID, length, device index, feature index, function, and
payload. Normalize only the request software-ID low nibble. On response:
- a normal response receives the current request software ID in its header;
- a feature-error response receives it in the echoed original function/software
byte; - all other bytes remain exact.
Arbitrary user-authored masks are rejected. They make fixtures pass after
meaningful payload changes and hide recorder bugs.
HID++ 1.0
Do not apply the HID++ 2.0 software-ID rule. Register address and payload are
matched using HID++ 1.0 correlation semantics. The HID++ 2.0 ping used to
detect a 1.0 device is represented explicitly as that cross-version exchange.
Repetition and concurrency
Store a FIFO queue per normalized request key. This supports repeated reads
whose response changes while allowing independent receiver-slot requests to
interleave. Do not assert one global write order unless the production contract
itself requires serialization, such as ordered lighting frames.
Unsolicited input
Notifications are scenario events, even if they were observed during capture.
A cassette may retain an unassociated report as evidence, but replay injects it
only at an explicit step/barrier. Otherwise a scheduler difference would turn
recorded timing into a flaky test.
Completion
A replay case reports:
- unmatched outgoing requests with normalized and actual hex;
- required exchanges not consumed;
- optional exchanges consumed or unused;
- unexpected raw writes;
- channel open/reopen counts;
- final connection/topology state.
Optional exchanges are reserved for documented best-effort production reads.
They are not a general escape hatch for fixture drift.
Determinism and failure testing
Use immediate, controlled outcomes for most failures:
- HID++ feature error;
- malformed response;
- backend open error;
- transport write error;
- explicit channel disconnect;
- dropped node or sleeping slot;
- late response released after a barrier.
A “no response” test otherwise waits for the production five-second channel
deadline. Keep timeout mechanics in focused openlogi-hidpp tests that call the
configurable timeout with a small budget. Higher-layer fixture tests should use
an equivalent immediate failure unless the deadline itself is their subject.
Inventory schedule tests use a logical/manual clock or existing scheduling
seams. They should advance the 200/400 ms settle windows, two-second repair
window, and 30-second recovery interval without sleeping. The IPC mock has two
clock modes:
Demo: current wall/monotonic behavior for visually changing battery,
foreground application, and camera state.Test: frozen time advanced only by the scenario.
Race tests should synchronize on explicit barriers such as “request written” or
“channel retirement started,” never millisecond sleeps.
Test suites generated from the corpus
The current implementation discovers all four contributed specimens and replays all 28 declared reads. openlogi-fixture owns strict filesystem loading behind its optional fs feature; the default schema and privacy APIs remain host-free. ReplayTopology::for_device owns capture and corpus topology. ReplayBackend::from_profile supports the single-device HID++ contributions currently recorded and rejects unsupported target shapes.
CLI operation expectations, Agent lifecycle gates, mock RPC/socket behavior, and desktop projection are mandatory workspace tests. The following sections also describe extensions that require additional captures: firmware, complete enumeration sessions, physical writes, and hardware-in-loop validation are not covered by the current read corpus. See the corpus contract for the exact boundary of each executed suite.
Corpus integrity
One test discovers every fixture directory and verifies:
- known schema version and no unknown fields;
- unique synthetic identities and valid route relationships;
- no sensitive identifiers;
- capabilities and declared operation results are consistent;
- every case names a profile link and logical channel that exist;
- every hex report has a valid declared length.
Device operation contracts
For each relevant case, construct a replay channel/backend and invoke the same
public openlogi-device operation production uses. Assert the independently
reviewed semantic result, exact required writes, and complete cassette
consumption.
Initial case families:
- direct and receiver-backed probe;
- feature/firmware/control dump;
- battery read across unified, legacy, and voltage features;
- DPI and SmartShift read;
- wheel and backlight read;
- long-only direct-device request widening;
- raw lighting/Litra encoding expectations.
Enumeration and recovery scenarios
Combine a profile/cassette with hand-authored topology transitions to test:
- several slots sharing one receiver channel;
- offline slots surfacing without feature probes;
- transient open/probe failure replaying last-good inventory;
- channel retirement and later reopen;
- a disappeared OS node being forgotten independently of a sleepy slot;
- hotplug and HID lifecycle event coalescing;
- probe-cache warm start and volatile battery refresh.
These tests should assert inventory plus channel/open/cache effects. Inventory
alone can be correct while a stale channel leak remains.
Stateful command scenarios
The semantic mock verifies:
- setting read/write round trips;
- nearest supported DPI selection;
- unsupported-feature errors;
- writes to unknown or offline routes;
- console battery, link, fault, indicator, time, and disconnect transitions;
- generation changes only when observable state changes;
- pairing cancel/restart isolation;
- frozen-time event progression.
The byte-layer suite separately verifies what reports production writes. The
semantic mock does not claim its state transition proves physical firmware
behavior.
Desktop and IPC
Derive an agent observation from a profile and feed it through the desktop's
normal state merge boundary. Test device identity, route-specific capabilities,
panel availability, online/offline updates, and setting failures there.
Keep process/socket tests small: one current-version handshake and snapshot,
one state-changing command/readback, and one observation generation update are
enough to prove the mock server wiring. The IPC wire itself remains protected
by its existing bincode goldens.
Hardware-in-loop
Provide an opt-in command that runs the same named capture/read plan against
hardware and compares its semantic result to a fixture. It reports rather than
gates normal CI. The report distinguishes:
- not run on hardware;
- node not enumerated;
- open/permission/exclusive-access failure;
- protocol mismatch;
- semantic mismatch;
- write not runtime-verified.
This is the only layer that can support claims about platform HID access and
physical effects.
Incremental delivery
Each phase is independently useful and should land with its own tests.
Phase 1: replay foundation
- Define schema v1 and corpus validator.
- Generalize the existing scripted raw channel into strict cassette replay.
- Add replay raw writer and mutable replay backend topology.
- Check in one synthetic direct-device and one receiver-backed fixture built
from existing responders. - Run production DPI/probe reads against them.
Success criterion: Linux CI exercises direct and receiver device code with no
hardware, and an altered request fails with a useful unmatched-report message.
Phase 2: semantic profiles and IPC mock
- Extract the hard-coded mock inventory into the profile/state model.
- Keep it as the built-in default.
- Add
openlogi-agent-mock --fixtureand frozen test time. - Add profile validation and setting round-trip tests.
- Add the desktop state-injection seam needed to consume a profile-derived
observation.
Success criterion: one profile drives both mock-agent behavior and desktop
state tests without duplicating the inventory in Rust.
Phase 3: read-only recorder
- Add observed-channel recording and raw-writer capture.
- Add strict, unambiguous target selection.
- Implement
fixture record profile,fixture record case, andfixture verify. - Implement known identity sanitizers and refuse unclassified committable raw
output. - Record and review the first real direct and receiver-backed devices.
Success criterion: a fresh clone can replay a fixture captured on another host,
obtain the reviewed semantic output, and pass privacy validation.
Phase 4: lifecycle and fault scenarios
- Add explicit event/barrier controls.
- Cover channel failure/repair, sleeping slots, malformed reports, late
responses, gesture restoration, and pairing cancellation. - Serialize a scenario format only if repeated authored scenarios demonstrate a
stable vocabulary.
Success criterion: tests reproduce recovery sequences without real sleeps and
assert channel/cache effects as well as final inventory.
Phase 5: full agent-core replay
- Introduce the coherent hardware runtime dependency.
- Route inventory, pool, pairing, standalone devices, lights, gate, and cache
through it. - Add a small agent-core integration suite over
ReplayBackend.
Success criterion: the real orchestration publishes and mutates a replayed
device without any native backend call. This phase must not replace the faster
IPC mock or device-level suites.
Phase 6: explicit write capture and hardware comparison
- Add individually reviewed, restorable write plans.
- Add opt-in hardware comparison reports.
- Keep pairing/unpairing and any non-restorable operation manual unless a safe
device-specific protocol is established.
Success criterion: every supported write case states whether it was byte-tested,
semantic-readback-tested, and runtime-tested on physical hardware.
First vertical slice
The smallest implementation that validates the architecture is:
- schema and validator for one profile plus named cassettes;
- strict replay of a direct mouse channel and a Bolt receiver channel;
- production
get_dpi_infoand inventory probe assertions; - the current mock mouse represented as a profile and loaded by
openlogi-agent-mock; - no recording, writes, pairing, serialized scenario language, or full-agent
dependency injection yet.
This proves that one semantic profile can serve UI development while raw
cassettes independently protect device parsing. Only then is native recording
worth adding.
Rejected shortcuts
- One linear transcript per device: breaks under concurrent receiver slots,
retries, optional reads, and unsolicited reports. - IPC-only recording: useful for screenshots, but cannot catch parser,
feature-index, routing, or write regressions. - Raw-only firmware emulator: a capture cannot infer state transitions,
cancellation cleanup, or unobserved failures. - Persisting
AgentSnapshot: couples fixtures to host status and the
append-only wire instead of device facts. - Adding fixture control to
Agent: permanently expands production IPC for
developer-only behavior. - Global timing replay: reproduces the recorder's scheduler, not the device
contract, and becomes flaky across hosts. - Arbitrary byte masks: lets meaningful request changes match stale
fixtures. - Automatically blessing semantic output: records current bugs as expected
behavior without independent review. - Refactoring the whole agent before the first fixture: delays the useful
device-layer and IPC-mock tests and creates an unproven abstraction.
PAIRING (`docs/PAIRING.md`)
Pair a wireless device
Open Add Device, put your device in pairing mode, and choose the receiver
you want to use. Use a Bolt receiver for a Bolt device and a Unifying receiver
for a Unifying device. OpenLogi shows the receiver name and the last four
characters of its identifier so you can distinguish connected receivers.
For Bolt, select the discovered device and follow the authentication instructions.
Unifying waits for a compatible device to connect after you choose the receiver.
Try again uses the same receiver, including after closing and reopening Add
Device. If it was unplugged, reconnect it or choose Change receiver. The
receiver list becomes available after the previous session finishes cancelling.
OpenLogi never redirects a retry to another connected receiver. If a receiver
cannot be identified yet, OpenLogi automatically retries the identity read and
enables its row when identification succeeds. Reconnect it if the problem persists.
Bluetooth pairing is managed by the operating system, outside this receiver flow.
README.De (`docs/README.de.md`)
[!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.
OpenLogi
Genug von Options+? Probier OpenLogi.
Läuft auf macOS, Linux und Windows.
Mehr als Options+
Was OpenLogi kann und Options+ nicht:
- Leichtgewichtig bleiben. Natives Rust + GPUI.
- Auf Linux laufen. Linux ist in OpenLogi eine vollwertige Plattform.
- Gesten auf unterstützten Tasten. Weise unterstützten Bedienelementen Gestenaktionen zu — oder schalte Gesten ganz ab.
- Konfiguration im Klartext. Alles steckt in einer TOML-Datei, die sich beliebig zwischen Rechnern synchronisieren lässt.
- Skriptbar. Neben der GUI gibt es eine echte CLI: eine JSON-Automatisierungs-API für DPI, SmartShift, Fn-Sperre und Abfragerate, dazu Hardware-Diagnosen.
Funktionen
Geräte
- Logi-Bolt- und Unifying-Empfänger, Bluetooth und Kabelverbindungen, mit Akkustand und Ladezustand
- Hinweise bei niedrigem Akkustand am unteren Bildschirmrand, einmal pro Entladung, ohne Berechtigung für Systembenachrichtigungen
- Eine Bildschirmanzeige, wenn eine Tastatur Caps Lock ein- oder ausschaltet, unter Windows und Linux auch für Num Lock und Scroll Lock
| Easy-SwitchSieh, welcher Computer auf welchem Kanal gekoppelt ist, samt Betriebssystem und VerbindungBenenne einen Kanal um, sofern das Gerät es erlaubt |
|---|
Tasten und Aktionen
| Jede Taste umbelegenEin eingebauter Aktionskatalog, eigene Tastenkürzel, Texteingabe oder mehrstufige WorkflowsGetrennte Aktionen für Tippen und Halten sowie Hold-to-Talk-Kombinationen, die mit der Taste losgelassen werdenGesten auf Mittelklick, Zurück/Vorwärts, DPI/ModeShift, der Gestentaste und dem Haptik-Panel | |
|---|---|
| Actions RingAcht Aktionen in einem Ring um den CursorEin eigenes Layout pro Anwendung | |
| TastaturtastenMedien-, Emoji-, Screenshot- und andere Sondertasten wie Maustasten belegenEine einzelne Taste auf die Gerätevorgabe zurücksetzen oder die ganze Reihe |
Profile pro Anwendung wechseln mit der App im Fokus. Ein [apps]-Alias lässt ein Profil einer Anwendung plattformübergreifend folgen.
Zeiger und Scrollen
| ZeigerDPI-Voreinstellungen mit Cycle- und Set-Preset-AktionenZeigergeschwindigkeit und AbfrageratePräzisionsmodus: Eine Taste schaltet die Maus auf ihre langsamste Geschwindigkeit und wieder zurück | |
|---|---|
| SmartShiftRadmodus und EmpfindlichkeitScrollkraft bei Mäusen mit SmartShift EnhancedEin Schalter für dauerhafte Rasterung | |
| ScrollenScroll-Umkehr pro Gerät, nativ oder per SoftwareMausradauflösung und weiches Scrollen mit dem MausradZurück oder Vorwärts halten und das Rad drehen, um auf Mäusen ohne Daumenrad seitwärts zu scrollen |
Tastatur und Beleuchtung
| TastaturFn Lock sowie Multi-Plattform-Tastaturen, die beim Layout des Host-Betriebssystems bleibenStufe der Hintergrundbeleuchtung, automatische Helligkeit und EnergiesparenBildschirmanzeige der Beleuchtungsstufe, sobald sie sich ändertStatisches RGB auf unterstützten Tastaturen | |
|---|---|
| Litra-LeuchtenEin/Aus, Helligkeit und FarbtemperaturSchaltet sich auf Wunsch mit der Kamera einÜbernimmt Änderungen, die direkt an der Leuchte vorgenommen werden |
Webcams
- Jede Logitech-UVC-Webcam (Brio, StreamCam, die C920-Serie, …), Plug and Play
- Live-Vorschau, die die Kamera nur belegt, solange du hinschaust; verlässt du sie, erlischt die LED
- Zoom, Schwenken, Neigen, Fokus, Belichtung, Weißabgleich und alle anderen UVC-Regler, direkt in die Hardware geschrieben, sodass Meet, Zoom und OBS die Änderung sehen
- Eingebaute Profile Standard / Streaming / Videoanruf plus eigene Schnappschüsse, pro Kamera gespeichert
Der Funktionsumfang hängt davon ab, was das jeweilige Gerät meldet; die App zeigt nur die Bedienelemente, die ein Gerät unterstützt.
Plattformunterstützung
| Funktion | macOS | Windows | Linux |
|---|---|---|---|
| Umbelegung von Maustasten und Gesten | ✓ | ✓ | ✓ |
| Umbelegung der F-Tasten | ✓ | ✓ | — |
| Profile pro Anwendung | ✓ | ✓ | X11, wlroots (sway, Hyprland), GNOME mit der Begleiterweiterung |
| Mausprofil folgt dem Fenster unter dem Zeiger | ✓ | ✓ | Nur X11 |
| Medientasten | Nativ | Nativ | D-Bus MPRIS |
| Fenstermanager-Aktionen (Schreibtische, Mission Control, …) | Nativ | Native Entsprechungen | GNOME, KDE (KWin), Hyprland; einige reine macOS-Aktionen bewirken nichts |
| Symbol in Menüleiste / Infobereich | ✓ | ✓ | — |
| Updates in der App | Sparkle | Integriert | Integriert |
Installation
[!IMPORTANT]
Beende zuerst Logi Options+ (und unter Linux Solaar). Ein Empfänger kann immer nur einer Anwendung gleichzeitig gehören.
| Plattform | System | Architektur |
|---|---|---|
| macOS | macOS 13 oder neuer | Apple Silicon, Intel |
| Linux | GLIBC 2.35 oder neuer | x86_64, arm64 |
| Windows | Getestet unter Windows 11 | x86_64, arm64 |
Jeder Download hat eine minisign-Signatur und einen Eintrag in SHA256SUMS. Zum Bauen aus dem Quellcode siehe DEVELOPMENT.md.
macOS
[](https://openlogi.org/download/mac-arm64)
[](https://openlogi.org/download/mac-x64)Oder per Homebrew:
brew install --cask openlogiÖffne OpenLogi und erteile Bedienungshilfen und Eingabeüberwachung, wenn macOS danach fragt. Danach aktualisiert sich OpenLogi selbst über Sparkle.
Linux
[](https://openlogi.org/download/linux)
[](https://openlogi.org/download/linux-rpm)
[](https://openlogi.org/download/linux-arch)Oder installiere mit einem Befehl das passende Paket für deine Distribution. Das Skript prüft die minisign-Signatur und startet den Agent; installiere vorher minisign:
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | shWie du das Skript vor dem Ausführen prüfst, sowie Hinweise zu Flatpak, NixOS und Distributionen ohne systemd findest du in INSTALL-linux.md.
Windows
[](https://openlogi.org/download/windows)
[](https://openlogi.org/download/windows-zip)Verwendung (CLI)
Siehe USAGE.md
Konfiguration
Einstellungen werden als reines TOML gespeichert; beim Speichern bleiben per Symlink eingebundene Konfigurationsdateien erhalten. Siehe CONFIGURATION.md.
Entwicklung
Siehe DEVELOPMENT.md, einschließlich Sicherheit des macOS-Input-Hooks.
Danksagungen
- Windows, Kameras und i18n von @davidbudnick — Tastatur-RGB, Windows-Unterstützung, Logitech-Webcam-Unterstützung
- Linux-Portierung von @cserby — Linux-Unterstützung
- Solaar von @pwr — quelloffene HID++-Implementierung
- Mouser von @TomBadash — ein lokaler Options+-Ersatz ohne Konto
Lizenz
Der Code in diesem Repository ist 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
Danke an @kubai087 für das Design des OpenLogi-Logos. 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.Es (`docs/README.es.md`)
[!WARNING]
OpenLogi está en desarrollo activo y todavía no es estable: las funciones y la configuración aún pueden cambiar. Dale una Star ⭐ al repositorio y ponlo en Watch 👀 para enterarte en cuanto salga una versión nueva.
OpenLogi
¿Harto de Options+? Prueba OpenLogi.
Funciona en macOS, Linux y Windows.
Más allá de Options+
Cosas que hace OpenLogi y Options+ no:
- No pesar. Rust nativo + GPUI.
- Funcionar en Linux. En OpenLogi, Linux es una plataforma de primera.
- Gestos en los botones compatibles. Asigna acciones de gestos a los controles compatibles, o desactiva los gestos por completo.
- Configuración en texto plano. Todo cabe en un único fichero TOML que puedes sincronizar entre máquinas como te convenga.
- Automatizar. Una CLI de verdad junto a la interfaz gráfica: una API de automatización en JSON para DPI, SmartShift, Fn-lock y frecuencia de sondeo, además de diagnósticos de hardware.
Funciones
Dispositivos
- Receptores Logi Bolt y Unifying, Bluetooth y conexiones por cable, con nivel de batería y estado de carga
- Avisos de batería baja en la parte inferior de la pantalla, una vez por descarga y sin permiso de notificaciones del sistema
- Un indicador en pantalla cuando un teclado activa o desactiva Caps Lock, y también Num Lock y Scroll Lock en Windows y Linux
| Easy-SwitchQué ordenador está emparejado en cada canal, con su sistema operativo y su conexiónCambia el nombre de un canal en los dispositivos que lo permiten |
|---|
Botones y acciones
| Reasigna cualquier botónUn catálogo de acciones integrado, atajos personalizados, texto escrito o flujos de varios pasosAcciones distintas para pulsar y mantener, y acordes de push-to-talk que se sueltan con el botónGestos en el clic central, Atrás/Adelante, DPI/ModeShift, el botón de gestos y el panel háptico | |
|---|---|
| Actions RingOcho acciones en un anillo alrededor del cursorSu propia distribución para cada aplicación | |
| Teclas del tecladoAsigna las teclas multimedia, de emoji, de captura de pantalla y otras teclas dedicadas igual que los botones del ratónDevuelve una tecla a su valor predeterminado del dispositivo o restablece toda la fila |
Los perfiles por aplicación cambian con la app que tengas en primer plano. Un alias en [apps] permite que un mismo perfil siga a una aplicación en distintas plataformas.
Puntero y desplazamiento
| PunteroPreajustes de DPI con acciones para recorrerlos y fijar unoVelocidad del puntero y frecuencia de sondeoModo de precisión: un botón pasa el ratón a su velocidad más lenta y lo devuelve | |
|---|---|
| SmartShiftModo de la rueda y sensibilidadFuerza de desplazamiento en ratones con SmartShift EnhancedUn interruptor de trinquete permanente | |
| DesplazamientoInversión del desplazamiento por dispositivo, de forma nativa o por softwareResolución de la rueda y desplazamiento suave con la ruedaMantén Atrás o Adelante y gira la rueda para desplazarte en horizontal en ratones sin rueda lateral |
Teclado e iluminación
| TecladoFn Lock, y teclados multiplataforma que mantienen la distribución del sistema anfitriónNivel de retroiluminación, brillo automático y ahorro de bateríaEl nivel de retroiluminación se muestra en pantalla mientras cambiaRGB estático en los teclados compatibles | |
|---|---|
| Luces LitraEncendido, brillo y temperatura de colorSe encienden con la cámara si quieresRespetan los cambios hechos en la propia luz |
Webcams
- Cualquier webcam UVC de Logitech (Brio, StreamCam, la serie C920, …), sin configurar nada
- Vista previa en directo que solo ocupa la cámara mientras la miras; al salir, el LED se apaga
- Zoom, panorámica, inclinación, enfoque, exposición, balance de blancos y el resto de controles UVC, escritos en el hardware para que Meet, Zoom y OBS vean el cambio
- Perfiles integrados Predeterminado / Transmisión / Videollamada, más instantáneas propias, guardados por cámara
Las funciones disponibles dependen de lo que informe cada dispositivo; la aplicación solo muestra los controles que el dispositivo admite.
Compatibilidad por plataforma
| Función | macOS | Windows | Linux |
|---|---|---|---|
| Reasignación de botones del ratón y gestos | ✓ | ✓ | ✓ |
| Reasignación de las teclas F del teclado | ✓ | ✓ | — |
| Perfiles por aplicación | ✓ | ✓ | X11, wlroots (sway, Hyprland), GNOME con la extensión complementaria |
| El perfil del ratón sigue a la ventana bajo el puntero | ✓ | ✓ | Solo X11 |
| Teclas multimedia | Nativo | Nativo | D-Bus MPRIS |
| Acciones del gestor de ventanas (escritorios, Mission Control, …) | Nativo | Equivalentes nativos | GNOME, KDE (KWin), Hyprland; algunas acciones exclusivas de macOS no hacen nada |
| Icono en la barra de menús / área de notificación | ✓ | ✓ | — |
| Actualizaciones desde la aplicación | Sparkle | Integradas | Integradas |
Instalación
[!IMPORTANT]
Cierra antes Logi Options+ (y Solaar en Linux). Un receptor solo puede pertenecer a una aplicación a la vez.
| Plataforma | Sistema | Arquitectura |
|---|---|---|
| macOS | macOS 13 o posterior | Apple silicon, Intel |
| Linux | GLIBC 2.35 o superior | x86_64, arm64 |
| Windows | Probado en Windows 11 | x86_64, arm64 |
Cada descarga lleva una firma minisign y una entrada en SHA256SUMS. Para compilar desde el código fuente, consulta DEVELOPMENT.md.
macOS
[](https://openlogi.org/download/mac-arm64)
[](https://openlogi.org/download/mac-x64)O con Homebrew:
brew install --cask openlogiAbre OpenLogi y concede Accesibilidad y Monitorización de los dispositivos de entrada cuando macOS lo pida. A partir de ahí, OpenLogi se actualiza solo mediante Sparkle.
Linux
[](https://openlogi.org/download/linux)
[](https://openlogi.org/download/linux-rpm)
[](https://openlogi.org/download/linux-arch)O instala el paquete adecuado para tu distribución con un solo comando. Verifica la firma minisign y arranca el agente; instala antes minisign:
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | shPara revisar el script antes de ejecutarlo, y para Flatpak, NixOS y distribuciones sin systemd, consulta INSTALL-linux.md.
Windows
[](https://openlogi.org/download/windows)
[](https://openlogi.org/download/windows-zip)Uso (CLI)
Consulta USAGE.md
Configuración
Los ajustes se guardan en TOML plano; al guardar se respetan los ficheros de configuración enlazados mediante enlaces simbólicos. Consulta CONFIGURATION.md.
Desarrollo
Consulta DEVELOPMENT.md, incluida la seguridad del hook de entrada en macOS.
Agradecimientos
- Windows, cámaras e i18n por @davidbudnick — RGB del teclado, compatibilidad con Windows, compatibilidad con las webcams de Logitech
- Port a Linux por @cserby — compatibilidad con Linux
- Solaar de @pwr — implementación de HID++ de código abierto
- Mouser de @TomBadash — un sustituto de Options+ local y sin cuentas
Licencia
El código de este repositorio tiene licencia doble, a tu elección:
- Apache License, versión 2.0 (LICENSE-APACHE)
- Licencia MIT (LICENSE-MIT)
Código de terceros
crates/openlogi-hidpp es un fork integrado de hidpp
de @lus, con licencia 0BSD.
Logo y recursos de marca
Gracias a @kubai087 por diseñar el logo de
OpenLogi. El logo y el icono de la aplicación (los recursos de marca que hay endesign/) son © 2026 AprilNEA, todos los derechos reservados, y no
están cubiertos por las licencias MIT/Apache anteriores; consultadesign/LICENSE. Hacer un fork del código no da ningún
derecho sobre el nombre, el logo ni el icono de OpenLogi; por favor, no los uses
para representar tus propios proyectos, forks o distribuciones sin permiso
previo por escrito.
Sin afiliación con Logitech. «Logitech», «MX Master» y «Options+» son marcas comerciales de Logitech International S.A.
Actividad del repositorio
README.Fr (`docs/README.fr.md`)
[!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.
OpenLogi
Assez d'Options+ ? Essayez OpenLogi.
Fonctionne sous macOS, Linux et Windows.
Au-delà d'Options+
Ce qu'OpenLogi fait et qu'Options+ ne fait pas :
- Rester léger. Du Rust natif + GPUI.
- Tourner sous Linux. Linux est une plateforme de premier rang pour OpenLogi.
- Des gestes sur les boutons pris en charge. Affectez des actions gestuelles aux commandes prises en charge — ou désactivez complètement les gestes.
- Une configuration en texte brut. Tout tient dans un fichier TOML, synchronisable entre machines comme vous voulez.
- Scriptable. Une vraie CLI en plus de la GUI : une API d'automatisation JSON pour le DPI, SmartShift, le verrouillage Fn et le taux de rapport, plus des diagnostics matériels.
Fonctionnalités
Appareils
- Récepteurs Logi Bolt et Unifying, Bluetooth et connexions filaires, avec niveau de batterie et état de charge
- Alertes de batterie faible en bas de l'écran, une fois par décharge, sans autorisation de notifications système
- Un indicateur à l'écran quand un clavier active ou désactive Caps Lock, ainsi que Num Lock et Scroll Lock sous Windows et Linux
| Easy-SwitchVoyez quel ordinateur est appairé sur chaque canal, avec son système et sa connexionRenommez un canal sur les appareils qui le permettent |
|---|
Boutons et actions
| Remappez n'importe quel boutonUn catalogue d'actions intégré, des raccourcis personnalisés, du texte saisi ou des workflows multi-étapesDes actions distinctes pour l'appui court et l'appui long, et des combinaisons push-to-talk relâchées avec le boutonDes gestes sur le clic du milieu, Précédent/Suivant, DPI/ModeShift, le bouton de gestes et le panneau haptique | |
|---|---|
| Actions RingHuit actions en anneau autour du curseurUne disposition propre à chaque application | |
| Touches du clavierAffectez les touches multimédia, emoji, capture d'écran et autres touches dédiées comme des boutons de sourisRétablissez une touche à sa valeur par défaut de l'appareil, ou réinitialisez toute la rangée |
Les profils par application basculent avec l'application au premier plan. Un alias [apps] permet à un profil de suivre une application d'une plateforme à l'autre.
Pointeur et défilement
| PointeurPréréglages DPI avec actions Cycle et Set-presetVitesse du pointeur et taux de rapportMode Précision : un bouton fait passer la souris à sa vitesse la plus lente, puis la rétablit | |
|---|---|
| SmartShiftMode de molette et sensibilitéForce de défilement sur les souris SmartShift EnhancedUn interrupteur de crantage permanent | |
| DéfilementInversion du défilement par appareil, nativement ou par logicielRésolution de la molette et défilement fluide à la moletteMaintenez Précédent ou Suivant et tournez la molette pour défiler horizontalement sur les souris sans molette latérale |
Clavier et éclairage
| ClavierFn Lock, et claviers multiplateformes qui restent sur la disposition du système hôteNiveau de rétroéclairage, luminosité automatique et économie de batterieLe niveau de rétroéclairage affiché à l'écran quand il changeRGB statique sur les claviers compatibles | |
|---|---|
| Lampes LitraAlimentation, luminosité et température de couleurS'allume avec la caméra si vous le souhaitezConserve un réglage effectué sur la lampe elle-même |
Webcams
- N'importe quelle webcam UVC Logitech (Brio, StreamCam, série C920, …), prête à l'emploi
- Aperçu en direct qui n'occupe la caméra que pendant que vous regardez ; quittez-le et la LED s'éteint
- Zoom, panoramique, inclinaison, mise au point, exposition, balance des blancs et toutes les autres commandes UVC, écrits dans le matériel pour que Meet, Zoom et OBS voient le changement
- Profils intégrés Par défaut / Streaming / Appel vidéo, plus vos propres instantanés, conservés par caméra
Les fonctionnalités disponibles dépendent de ce que chaque appareil signale ; l'application n'affiche que les commandes qu'un appareil prend en charge.
Plateformes prises en charge
| Fonctionnalité | macOS | Windows | Linux |
|---|---|---|---|
| Remappage des boutons de souris et gestes | ✓ | ✓ | ✓ |
| Remappage des touches F du clavier | ✓ | ✓ | — |
| Profils par application | ✓ | ✓ | X11, wlroots (sway, Hyprland), GNOME avec l'extension compagnon |
| Le profil de la souris suit la fenêtre sous le pointeur | ✓ | ✓ | X11 uniquement |
| Touches multimédia | Natif | Natif | D-Bus MPRIS |
| Actions du gestionnaire de fenêtres (bureaux, Mission Control, …) | Natif | Équivalents natifs | GNOME, KDE (KWin), Hyprland ; certaines actions propres à macOS sont sans effet |
| Icône de barre des menus / zone de notification | ✓ | ✓ | — |
| Mises à jour intégrées | Sparkle | Intégré | Intégré |
Installation
[!IMPORTANT]
Quittez d'abord Logi Options+ (et Solaar sous Linux). Un récepteur ne peut appartenir qu'à une seule application à la fois.
| Plateforme | Système | Architecture |
|---|---|---|
| macOS | macOS 13 ou ultérieur | Apple silicon, Intel |
| Linux | GLIBC 2.35 ou plus récent | x86_64, arm64 |
| Windows | Testé sous Windows 11 | x86_64, arm64 |
Chaque téléchargement est accompagné d'une signature minisign et d'une entrée dans SHA256SUMS. Pour compiler depuis les sources, voir DEVELOPMENT.md.
macOS
[](https://openlogi.org/download/mac-arm64)
[](https://openlogi.org/download/mac-x64)Ou avec Homebrew :
brew install --cask openlogiOuvrez OpenLogi et accordez Accessibilité et Surveillance de la saisie lorsque macOS le demande. OpenLogi se met ensuite à jour lui-même via Sparkle.
Linux
[](https://openlogi.org/download/linux)
[](https://openlogi.org/download/linux-rpm)
[](https://openlogi.org/download/linux-arch)Ou installez le paquet adapté à votre distribution en une seule commande. Le script vérifie la signature minisign et démarre l'agent ; installez d'abord minisign :
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | shPour examiner le script avant de l'exécuter, ainsi que pour Flatpak, NixOS et les distributions sans systemd, voir INSTALL-linux.md.
Windows
[](https://openlogi.org/download/windows)
[](https://openlogi.org/download/windows-zip)Utilisation (CLI)
Voir USAGE.md
Configuration
Les réglages sont stockés en TOML brut ; l'enregistrement préserve les fichiers de configuration liés par lien symbolique. Voir CONFIGURATION.md.
Développement
Voir DEVELOPMENT.md, notamment la sécurité du hook d'entrée macOS.
Remerciements
- Windows, caméras et i18n par @davidbudnick — RGB clavier, prise en charge de Windows, prise en charge des webcams Logitech
- Portage Linux par @cserby — prise en charge de Linux
- Solaar par @pwr — implémentation open source de HID++
- Mouser par @TomBadash — un remplacement d'Options+ local et sans compte
Licence
Le code de ce dépôt est 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
Merci à @kubai087 pour la conception du logo OpenLogi. 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 (`docs/README.ja.md`)
[!WARNING]
OpenLogi は現在活発に開発中であり、まだ安定していません —— 機能や設定は今後も変わる可能性があります。リポジトリに Star ⭐ と Watch 👀 を付けて、新しいリリースの通知を受け取りましょう。
OpenLogi
Options+ にうんざり?OpenLogi をどうぞ。
macOS、Linux、Windows に対応。
Options+ を超えて
OpenLogi にできて Options+ にできないこと:
- 軽量なまま。 ネイティブ Rust + GPUI。
- Linux で動く。 Linux は OpenLogi のファーストクラスプラットフォームです。
- 対応ボタンでジェスチャー。 対応するコントロールにジェスチャー操作を割り当てられ、ジェスチャーを完全にオフにもできます。
- 設定がプレーンテキスト。 すべてが 1 つの TOML ファイルに収まり、好きな方法でマシン間を同期できます。
- スクリプトで叩ける。 GUI に加えて本物の CLI も。DPI、SmartShift、Fn Lock、レポートレートを操作する JSON の自動化 API と、ハードウェア診断を備えています。
機能一覧
デバイス
- Logi Bolt / Unifying レシーバー、Bluetooth、有線接続に対応し、バッテリー残量と充電状態を表示
- バッテリー低下を画面下部に通知。放電ごとに 1 回だけで、OS の通知権限は不要
- キーボードで Caps Lock をオン / オフすると画面上にインジケーターを表示。Windows と Linux では Num Lock と Scroll Lock にも対応
| Easy-Switch各チャンネルにペアリングされたコンピューターを、OS と接続方式つきで確認対応デバイスではチャンネル名を変更可能 |
|---|
ボタンとアクション
| あらゆるボタンを再マッピング組み込みアクションカタログ、カスタムショートカット、テキスト入力、複数ステップのワークフロータップと長押しで別々のアクション、ボタンを離すと解除される長押しトーク用のキーコンボミドルクリック、戻る / 進む、DPI / モードシフト、ジェスチャーボタン、触覚パネルでのジェスチャー | |
|---|---|
| Actions Ringカーソルの周りにリング状に並ぶ 8 つのアクションアプリごとに独自のレイアウト | |
| キーボードのキーメディア、絵文字、スクリーンショットなどの専用キーをマウスボタンと同じように割り当てキーを 1 つだけデバイスの既定値に戻すことも、列全体をリセットすることも可能 |
アプリごとのプロファイルは、フォーカス中のアプリに合わせて切り替わります。[apps] エイリアスを使えば、1 つのプロファイルをプラットフォームをまたいで同じアプリに適用できます。
ポインターとスクロール
| ポインターDPI プリセットと、サイクル / プリセット指定アクションポインタ速度とレポートレート精密モード:ボタン 1 つでマウスを最低速度に切り替え、もう一度押すと元に戻る | |
|---|---|
| SmartShiftホイールモードと切り替え感度SmartShift Enhanced 対応マウスではスクロールの重さも調整常時ラチェットのスイッチ | |
| スクロールデバイスごとのスクロール反転(ネイティブまたはソフトウェア)ホイール解像度とスムーズホイールスクロールサムホイールのないマウスでも、戻る / 進むを押しながらホイールを回すと横スクロール |
キーボードとライト
| キーボードFn Lock と、ホスト OS のレイアウトを維持するマルチプラットフォームキーボードバックライトの明るさ、明るさの自動調整、省電力バックライトの明るさを変更時に画面に表示対応キーボードでの静的 RGB | |
|---|---|
| Litra ライト電源、明るさ、色温度必要ならカメラの使用に連動して点灯ライト本体で行った変更を保持 |
ウェブカメラ
- あらゆる Logitech UVC ウェブカメラ(Brio、StreamCam、C920 シリーズなど)にプラグアンドプレイで対応
- ライブプレビューは見ているあいだだけカメラを使い、離れると LED も消灯
- ズーム、パン、チルト、フォーカス、露出、ホワイトバランスなどあらゆる UVC コントロールをハードウェアに書き込むため、Meet、Zoom、OBS にも変更が反映
- 組み込みの「デフォルト / 配信 / ビデオ通話」プロファイルに加えて独自のスナップショットを保存でき、カメラごとに保持
対応機能は各デバイスが報告する内容によって異なります。アプリには、そのデバイスが対応するコントロールだけが表示されます。
プラットフォーム対応状況
| 機能 | macOS | Windows | Linux |
|---|---|---|---|
| マウスボタンの再マッピングとジェスチャー | ✓ | ✓ | ✓ |
| キーボードの F キー再マッピング | ✓ | ✓ | — |
| アプリごとのプロファイル | ✓ | ✓ | X11、wlroots(sway、Hyprland)、GNOME(コンパニオン拡張機能が必要) |
| マウスのプロファイルがポインター下のウィンドウに追従 | ✓ | ✓ | X11 のみ |
| メディアキー | ネイティブ | ネイティブ | D-Bus MPRIS |
| ウィンドウマネージャー操作(デスクトップ、Mission Control など) | ネイティブ | ネイティブの同等機能 | GNOME、KDE (KWin)、Hyprland。macOS 専用の一部操作は何もしない |
| メニューバー / トレイアイコン | ✓ | ✓ | — |
| アプリ内アップデート | Sparkle | 組み込み | 組み込み |
インストール
[!IMPORTANT]
先に Logi Options+(Linux では Solaar も)を終了してください。1 つのレシーバーを同時に所有できるアプリケーションは 1 つだけです。
| プラットフォーム | システム | アーキテクチャ |
|---|---|---|
| macOS | macOS 13 以降 | Apple シリコン、Intel |
| Linux | GLIBC 2.35 以降 | x86_64, arm64 |
| Windows | Windows 11 で動作確認済み | x86_64, arm64 |
すべてのダウンロードには minisign 署名と SHA256SUMS のエントリが付属します。ソースからのビルドは DEVELOPMENT.md を参照してください。
macOS
[](https://openlogi.org/download/mac-arm64)
[](https://openlogi.org/download/mac-x64)または Homebrew で:
brew install --cask openlogiOpenLogi を開き、macOS に求められたら アクセシビリティ と 入力監視 を許可してください。以降、OpenLogi は Sparkle で自動的に更新されます。
Linux
[](https://openlogi.org/download/linux)
[](https://openlogi.org/download/linux-rpm)
[](https://openlogi.org/download/linux-arch)または、1 つのコマンドでディストリビューションに合ったパッケージをインストールできます。スクリプトは minisign 署名を検証し、agent を起動します。事前に minisign をインストールしてください:
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | sh実行前にスクリプトを確認する方法や、Flatpak、NixOS、systemd のないディストリビューションについては INSTALL-linux.md を参照してください。
Windows
[](https://openlogi.org/download/windows)
[](https://openlogi.org/download/windows-zip)使い方(CLI)
USAGE.md を参照
設定
設定はプレーンな TOML を使い、保存時もシンボリックリンクされた設定ファイルを維持します。CONFIGURATION.md を参照。
開発
DEVELOPMENT.md を参照。macOS 入力フックの安全性についても記載しています。
謝辞
- Windows・カメラ・i18n: @davidbudnick —— キーボード RGB 対応、Windows 対応、Logitech ウェブカメラ対応
- Linux 移植: @cserby —— Linux 対応
- Solaar by @pwr —— オープンソースの HID++ 実装
- Mouser by @TomBadash —— ローカル完結・アカウント不要の Options+ 代替
ライセンス
本リポジトリのコードは、以下のいずれかのライセンスを選択できます:
- Apache License 2.0(LICENSE-APACHE)
- MIT ライセンス(LICENSE-MIT)
サードパーティコード
crates/openlogi-hidpp は hidpp(作者 @lus)の vendored fork で、0BSD ライセンスです。
ロゴとブランドアセット
OpenLogi のロゴをデザインしてくれた @kubai087 に感謝します。OpenLogi のロゴとアプリアイコン —— design/ 配下のブランドアセット —— は © 2026 AprilNEA が全権利を留保しており、上記の MIT/Apache ライセンスの対象外です。design/LICENSE を参照してください。コードをフォークしても OpenLogi の名称・ロゴ・アイコンの使用権は付与されません。事前の書面による許可なく、ご自身のプロジェクト、フォーク、配布物を表すために使用しないでください。
Logitech とは無関係です。 「Logitech」「MX Master」「Options+」は Logitech International S.A. の商標です。
リポジトリの活動
README.Ko (`docs/README.ko.md`)
[!WARNING]
OpenLogi는 활발히 개발 중이며 아직 안정 단계가 아닙니다 — 기능과 설정이 변경될 수 있습니다. 저장소에 Star ⭐ 와 Watch 👀 를 눌러 두면 새 릴리스가 나올 때 알림을 받을 수 있습니다.
OpenLogi
Options+가 지긋지긋하다면? OpenLogi를 써 보세요.
macOS, Linux, Windows를 지원합니다.
Options+ 그 너머
OpenLogi는 되고 Options+는 안 되는 것들:
- 가볍게 유지. 네이티브 Rust + GPUI.
- Linux 지원. Linux는 OpenLogi의 일급 플랫폼입니다.
- 지원되는 버튼에서 제스처 사용. 지원되는 컨트롤에 제스처 동작을 지정하거나, 제스처를 아예 끌 수 있습니다.
- 순수 텍스트 설정. 모든 설정이 TOML 파일 하나에 담겨 원하는 방법으로 기기 간 동기화할 수 있습니다.
- 스크립트 가능. GUI 외에 진짜 CLI도 제공합니다. DPI, SmartShift, Fn Lock, 보고율을 다루는 JSON 자동화 API와 하드웨어 진단 기능을 갖추고 있습니다.
기능 목록
기기
- Logi Bolt 수신기, Unifying 수신기, Bluetooth, 유선 연결을 지원하며 배터리 잔량과 충전 상태 표시
- 배터리 부족 알림을 화면 하단에 방전당 한 번만 표시하며, OS 알림 권한은 필요 없음
- 키보드에서 Caps Lock을 켜거나 끄면 화면에 표시기를 띄움. Windows와 Linux에서는 Num Lock과 Scroll Lock도 지원
| Easy-Switch채널마다 페어링된 컴퓨터를 OS, 연결 방식과 함께 확인지원 기기에서는 채널 이름 변경 가능 |
|---|
버튼과 액션
| 모든 버튼 리매핑내장 액션 카탈로그, 사용자 지정 단축키, 텍스트 입력, 다단계 워크플로탭과 길게 누르기에 각각 다른 액션, 버튼을 놓으면 해제되는 누르고 말하기용 키 조합가운데 클릭, 뒤로/앞으로, DPI/모드 시프트, 제스처 버튼, 햅틱 패널에서 제스처 사용 | |
|---|---|
| Actions Ring커서 주위에 링 형태로 배치되는 액션 8개앱마다 별도 레이아웃 | |
| 키보드 키미디어, 이모지, 스크린샷 등 전용 키를 마우스 버튼처럼 지정키 하나만 기기 기본값으로 되돌리거나 행 전체를 재설정 |
앱별 프로필은 포커스된 앱에 따라 전환됩니다. [apps] 별칭을 쓰면 프로필 하나가 여러 플랫폼에서 같은 앱을 따라갑니다.
포인터와 스크롤
| 포인터DPI 프리셋과 사이클 / 프리셋 지정 액션포인터 속도와 보고율정밀 모드: 버튼 하나로 마우스를 가장 느린 속도로 바꾸고 다시 원래대로 복귀 | |
|---|---|
| SmartShift휠 모드와 민감도SmartShift Enhanced 마우스의 스크롤 압력래칫 고정 스위치 | |
| 스크롤기기별 스크롤 반전(네이티브 또는 소프트웨어)휠 해상도와 부드러운 휠 스크롤썸휠이 없는 마우스에서도 뒤로 또는 앞으로 버튼을 누른 채 휠을 굴리면 가로 스크롤 |
키보드와 조명
| 키보드Fn Lock, 그리고 호스트 OS 레이아웃을 유지하는 멀티 플랫폼 키보드백라이트 밝기, 자동 밝기, 배터리 절약백라이트 밝기를 바꿀 때 화면에 표시지원 키보드의 정적 RGB | |
|---|---|
| Litra 조명전원, 밝기, 색온도원하면 카메라 사용에 맞춰 자동으로 켜기조명 본체에서 바꾼 설정을 유지 |
웹캠
- 모든 Logitech UVC 웹캠(Brio, StreamCam, C920 시리즈 등) 플러그 앤 플레이 지원
- 실시간 미리보기는 보고 있는 동안에만 카메라를 사용하며, 벗어나면 LED도 꺼집니다
- 줌, 팬, 틸트, 초점, 노출, 화이트 밸런스 등 모든 UVC 컨트롤을 하드웨어에 기록하므로 Meet, Zoom, OBS에도 변경 사항이 적용됩니다
- 기본값 / 스트리밍 / 영상 통화 내장 프로필과 사용자 스냅숏을 카메라별로 저장
지원 기능은 각 기기가 보고하는 내용에 따라 다르며, 앱은 기기가 지원하는 컨트롤만 표시합니다.
플랫폼 지원
| 기능 | macOS | Windows | Linux |
|---|---|---|---|
| 마우스 버튼 리매핑과 제스처 | ✓ | ✓ | ✓ |
| 키보드 F 키 리매핑 | ✓ | ✓ | — |
| 앱별 프로필 | ✓ | ✓ | X11, wlroots(sway, Hyprland), GNOME(동반 확장 필요) |
| 포인터 아래 창을 따라가는 마우스 프로필 | ✓ | ✓ | X11 전용 |
| 미디어 키 | 네이티브 | 네이티브 | D-Bus MPRIS |
| 창 관리자 액션(데스크톱, Mission Control 등) | 네이티브 | 네이티브 대응 기능 | GNOME, KDE (KWin), Hyprland; 일부 macOS 전용 액션은 동작하지 않음 |
| 메뉴 막대 / 트레이 아이콘 | ✓ | ✓ | — |
| 앱 내 업데이트 | Sparkle | 내장 | 내장 |
설치
[!IMPORTANT]
먼저 Logi Options+(Linux에서는 Solaar도)를 종료하세요. 하나의 수신기는 한 번에 하나의 애플리케이션만 소유할 수 있습니다.
| 플랫폼 | 시스템 | 아키텍처 |
|---|---|---|
| macOS | macOS 13 이상 | Apple 실리콘, Intel |
| Linux | GLIBC 2.35 이상 | x86_64, arm64 |
| Windows | Windows 11에서 테스트됨 | x86_64, arm64 |
모든 다운로드에는 minisign 서명과 SHA256SUMS 항목이 함께 제공됩니다. 소스에서 빌드하려면 DEVELOPMENT.md를 참고하세요.
macOS
[](https://openlogi.org/download/mac-arm64)
[](https://openlogi.org/download/mac-x64)또는 Homebrew로 설치:
brew install --cask openlogiOpenLogi를 열고 macOS가 요청하면 손쉬운 사용과 입력 모니터링 권한을 허용하세요. 이후 OpenLogi는 Sparkle로 스스로 업데이트합니다.
Linux
[](https://openlogi.org/download/linux)
[](https://openlogi.org/download/linux-rpm)
[](https://openlogi.org/download/linux-arch)또는 명령 하나로 배포판에 맞는 패키지를 설치할 수 있습니다. 스크립트는 minisign 서명을 검증하고 agent를 시작합니다. 먼저 minisign을 설치하세요:
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | sh실행 전에 스크립트를 확인하는 방법과 Flatpak, NixOS, systemd가 없는 배포판에 대해서는 INSTALL-linux.md를 참고하세요.
Windows
[](https://openlogi.org/download/windows)
[](https://openlogi.org/download/windows-zip)사용법 (CLI)
USAGE.md 참고
설정
설정은 순수 TOML을 사용하며, 저장할 때 심볼릭 링크된 설정 파일을 그대로 유지합니다. CONFIGURATION.md 참고.
개발
macOS 입력 훅 안전성을 포함해 DEVELOPMENT.md 참고.
감사의 말
- Windows·카메라·i18n — @davidbudnick: 키보드 RGB 지원, Windows 지원, Logitech 웹캠 지원
- Linux 포팅 — @cserby: Linux 지원
- Solaar — @pwr: 오픈소스 HID++ 구현
- Mouser — @TomBadash: 로컬에서 동작하는 계정 없는 Options+ 대체제
라이선스
이 저장소의 코드는 다음 중 하나를 선택해 사용할 수 있습니다:
- Apache License 2.0 (LICENSE-APACHE)
- MIT 라이선스 (LICENSE-MIT)
서드파티 코드
crates/openlogi-hidpp는 hidpp(@lus 제작)의 vendored fork이며, 0BSD 라이선스를 따릅니다.
로고 및 브랜드 자산
OpenLogi 로고를 디자인해 준 @kubai087에게 감사드립니다. OpenLogi 로고와 앱 아이콘 — design/ 아래의 브랜드 자산 — 은 © 2026 AprilNEA가 모든 권리를 보유하며, 위 MIT/Apache 라이선스의 적용을 받지 않습니다. design/LICENSE를 참고하세요. 코드를 포크해도 OpenLogi 이름·로고·아이콘에 대한 권리는 부여되지 않습니다. 사전 서면 허가 없이 자신의 프로젝트, 포크, 배포판을 나타내는 데 사용하지 마세요.
Logitech과 무관합니다. "Logitech", "MX Master", "Options+"는 Logitech International S.A.의 상표입니다.
저장소 활동
README.Pt BR (`docs/README.pt-BR.md`)
[!WARNING]
OpenLogi está em desenvolvimento ativo e ainda não é estável: recursos e configuração podem mudar. Dê uma estrela ⭐ e acompanhe 👀 o repositório para saber quando um novo lançamento sair.
OpenLogi
Cansado do Options+? Experimente o OpenLogi.
Roda em macOS, Linux e Windows.
Além do Options+
O que o OpenLogi faz e o Options+ não faz:
- Fica leve. Rust nativo + GPUI.
- Roda em Linux. Linux é uma plataforma de primeira classe no OpenLogi.
- Gestos nos botões compatíveis. Atribua ações de gesto aos controles compatíveis, ou desligue os gestos por completo.
- Configuração em texto puro. Tudo em um único arquivo TOML, sincronizável entre máquinas do jeito que você preferir.
- Programável. Uma CLI de verdade ao lado da GUI: uma API de automação em JSON para DPI, SmartShift, Fn-lock e taxa de relatório, além de diagnósticos de hardware.
Recursos
Dispositivos
- Receptores Logi Bolt e Unifying, Bluetooth e conexões com fio, com nível de bateria e estado de carga
- Avisos de bateria fraca na parte de baixo da tela, uma vez por descarga, sem precisar de permissão de notificação do sistema
- Um indicador na tela quando um teclado liga ou desliga o Caps Lock, além de Num Lock e Scroll Lock no Windows e no Linux
| Easy-SwitchVeja qual computador está pareado em cada canal, com o sistema operacional e a conexãoRenomeie um canal nos dispositivos que permitem |
|---|
Botões e ações
| Remapeie qualquer botãoUm catálogo de ações embutido, atalhos personalizados, texto digitado ou fluxos de várias etapasAções separadas para toque e pressão longa, e combinações de push-to-talk que soltam junto com o botãoGestos no clique do meio, Voltar/Avançar, DPI/ModeShift, no botão de gesto e no painel háptico | |
|---|---|
| Actions RingOito ações em um anel ao redor do cursorUm layout próprio para cada aplicativo | |
| Teclas do tecladoVincule teclas de mídia, emoji, captura de tela e outras teclas dedicadas como se fossem botões do mouseRestaure uma tecla ao padrão do dispositivo ou redefina a linha inteira |
Os perfis por aplicativo trocam junto com o app em foco. Um alias em [apps] permite que um perfil acompanhe um aplicativo em todas as plataformas.
Ponteiro e rolagem
| PonteiroPredefinições de DPI com ações de ciclar e definir predefiniçãoVelocidade do ponteiro e taxa de relatórioModo de precisão: um botão leva o mouse à velocidade mais lenta e de volta | |
|---|---|
| SmartShiftModo da roda e sensibilidadeForça de rolagem em mouses com SmartShift EnhancedUma chave de catraca permanente | |
| RolagemInversão de rolagem por dispositivo, nativa ou por softwareResolução da roda e rolagem suave da rodaSegure Voltar ou Avançar e gire a roda para rolar na horizontal em mouses sem roda lateral |
Teclado e iluminação
| TecladoFn Lock, e teclados multiplataforma que seguem o layout do sistema operacional do hostNível da luz de fundo, brilho automático e economia de bateriaO nível da luz de fundo mostrado na tela enquanto mudaRGB estático nos teclados compatíveis | |
|---|---|
| Luzes LitraLiga/desliga, brilho e temperatura de corLiga junto com a câmera, se você quiserMantém um ajuste feito na própria luz |
Webcams
- Qualquer webcam UVC da Logitech (Brio, StreamCam, a série C920, …), plug and play
- Pré-visualização ao vivo que só ocupa a câmera enquanto você está olhando; saia dela e o LED apaga
- Zoom, pan, tilt, foco, exposição, balanço de branco e todos os outros controles UVC, gravados no hardware para que o Meet, o Zoom e o OBS vejam a mudança
- Perfis Padrão / Streaming / Chamada de vídeo embutidos mais seus próprios instantâneos, guardados por câmera
A cobertura de recursos depende do que cada dispositivo informa; o app mostra só os controles que o dispositivo suporta.
Suporte por plataforma
| Recurso | macOS | Windows | Linux |
|---|---|---|---|
| Remapeamento de botões do mouse e gestos | ✓ | ✓ | ✓ |
| Remapeamento das teclas F do teclado | ✓ | ✓ | — |
| Perfis por aplicativo | ✓ | ✓ | X11, wlroots (sway, Hyprland), GNOME com a extensão complementar |
| Perfil do mouse segue a janela sob o ponteiro | ✓ | ✓ | Só X11 |
| Teclas de mídia | Nativo | Nativo | D-Bus MPRIS |
| Ações do gerenciador de janelas (áreas de trabalho, Mission Control, …) | Nativo | Equivalentes nativos | GNOME, KDE (KWin), Hyprland; algumas ações exclusivas do macOS não fazem nada |
| Ícone na barra de menus / bandeja | ✓ | ✓ | — |
| Atualizações no app | Sparkle | Embutido | Embutido |
Instalação
[!IMPORTANT]
Feche o Logi Options+ (e o Solaar no Linux) primeiro. Só um aplicativo pode controlar um receptor por vez.
| Plataforma | Sistema | Arquitetura |
|---|---|---|
| macOS | macOS 13 ou mais recente | Apple silicon, Intel |
| Linux | GLIBC 2.35 ou mais recente | x86_64, arm64 |
| Windows | Testado no Windows 11 | x86_64, arm64 |
Todo download traz uma assinatura minisign e uma entrada em SHA256SUMS. Para compilar a partir do código-fonte, veja DEVELOPMENT.md.
macOS
[](https://openlogi.org/download/mac-arm64)
[](https://openlogi.org/download/mac-x64)Ou pelo Homebrew:
brew install --cask openlogiAbra o OpenLogi e conceda Acessibilidade e Monitoramento de entrada quando o macOS pedir. Depois disso, o OpenLogi se atualiza sozinho pelo Sparkle.
Linux
[](https://openlogi.org/download/linux)
[](https://openlogi.org/download/linux-rpm)
[](https://openlogi.org/download/linux-arch)Ou instale o pacote certo para a sua distribuição com um único comando. Ele verifica a assinatura minisign e inicia o agente; instale o minisign antes:
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | shPara ler o script antes de executá-lo, e para Flatpak, NixOS e distribuições sem systemd, veja INSTALL-linux.md.
Windows
[](https://openlogi.org/download/windows)
[](https://openlogi.org/download/windows-zip)Uso (CLI)
Veja USAGE.md
Configuração
As configurações usam TOML puro; salvar preserva arquivos de configuração que são links simbólicos. Veja CONFIGURATION.md.
Desenvolvimento
Veja DEVELOPMENT.md, incluindo segurança do hook de entrada no macOS.
Agradecimentos
- Windows, câmeras e i18n por @davidbudnick: RGB do teclado, suporte a Windows, suporte às webcams Logitech
- Port para Linux por @cserby: suporte a Linux
- Solaar, de @pwr: implementação de HID++ de código aberto
- Mouser, de @TomBadash: alternativa local ao Options+, sem conta
Licença
O código deste repositório é licenciado, à sua escolha, sob qualquer uma das seguintes licenças:
- Apache License, Version 2.0 (LICENSE-APACHE)
- Licença MIT (LICENSE-MIT)
Código de terceiros
crates/openlogi-hidpp é um fork vendorizado do hidpp,
de @lus, licenciado sob 0BSD.
Logo e ativos de marca
Obrigado a @kubai087 por desenhar o logo do OpenLogi. O
logo e o ícone do app (os ativos de marca em design/) são © 2026 AprilNEA,
todos os direitos reservados, e não são cobertos pelas licenças MIT/Apache acima; vejadesign/LICENSE. Fazer fork do código não dá direito ao nome, logo
ou ícone do OpenLogi; por favor, não os use para representar seus próprios projetos,
forks ou distribuições sem permissão prévia por escrito.
Sem vínculo com a Logitech. "Logitech", "MX Master" e "Options+" são marcas registradas da Logitech International S.A.
Atividade do repositório
README.Ru (`docs/README.ru.md`)
[!WARNING]
OpenLogi активно разрабатывается и ещё не стабилен — функции и конфигурация могут меняться. Поставьте репозиторию Star ⭐ и Watch 👀, чтобы узнать о новом релизе.
OpenLogi
Надоел Options+? Попробуйте OpenLogi.
Работает на macOS, Linux и Windows.
Больше, чем Options+
То, что OpenLogi умеет, а Options+ — нет:
- Лёгкий. Нативный Rust + GPUI.
- Linux. В OpenLogi Linux — платформа первого класса.
- Жесты на поддерживаемых кнопках. Назначайте действия жестам на поддерживаемых элементах управления — или отключите жесты совсем.
- Конфиг обычным текстом. Всё в одном TOML-файле, который можно синхронизировать между машинами как угодно.
- Скрипты. Настоящий CLI рядом с GUI: JSON-API автоматизации для DPI, SmartShift, Fn-lock и частоты опроса, плюс диагностика железа.
Возможности
Устройства
- Приёмники Logi Bolt и Unifying, Bluetooth и провод, с уровнем заряда батареи и состоянием зарядки
- Предупреждение о низком заряде внизу экрана — один раз за разряд и без системного разрешения на уведомления
- Экранный индикатор, когда клавиатура включает или выключает Caps Lock, а на Windows и Linux — ещё Num Lock и Scroll Lock
| Easy-SwitchКакой компьютер сопряжён на каждом канале, с его ОС и типом подключенияПереименование канала на устройствах, которые это позволяют |
|---|
Кнопки и действия
| Переназначение любой кнопкиВстроенный каталог действий, свои сочетания клавиш, набор текста или многошаговые сценарииОтдельные действия на нажатие и удержание, а также аккорды push-to-talk, которые отпускаются вместе с кнопкойЖесты на средней кнопке, Назад/Вперёд, DPI/ModeShift, кнопке жестов и тактильной панели | |
|---|---|
| Actions RingВосемь действий кольцом вокруг курсораСвоя раскладка для каждого приложения | |
| Клавиши клавиатурыМедиаклавиши, эмодзи, снимок экрана и другие выделенные клавиши назначаются так же, как кнопки мышиСброс одной клавиши к значению устройства по умолчанию или всего ряда разом |
Профили по приложениям переключаются вместе с приложением в фокусе. Псевдоним в [apps] позволяет одному профилю следовать за приложением на разных платформах.
Указатель и прокрутка
| УказательПресеты DPI и действия Cycle / Set-presetСкорость указателя и частота опросаРежим точности: одна кнопка переключает мышь на самую низкую скорость и обратно | |
|---|---|
| SmartShiftРежим колеса и чувствительностьУсилие прокрутки на мышах с SmartShift EnhancedПереключатель постоянной трещотки | |
| ПрокруткаИнверсия прокрутки на устройство — нативно или программноРазрешение колеса и плавная прокрутка колесомУдерживайте Назад или Вперёд и вращайте колесо, чтобы прокручивать по горизонтали на мышах без колеса под большой палец |
Клавиатура и подсветка
| КлавиатураFn Lock и мультиплатформенные клавиатуры, которые остаются на раскладке ОС хостаУровень подсветки, автояркость и экономия зарядаУровень подсветки показывается на экране при измененииСтатическая RGB-подсветка на поддерживаемых клавиатурах | |
|---|---|
| Светильники LitraПитание, яркость и цветовая температураПри желании включаются вместе с камеройИзменения, сделанные на самом светильнике, сохраняются |
Веб-камеры
- Любая Logitech UVC-веб-камера (Brio, StreamCam, серия C920, …), plug and play
- Живой превью держит камеру только пока вы смотрите; закройте его — и LED погаснет
- Масштаб, панорама, наклон, фокус, экспозиция, баланс белого и все остальные UVC-параметры пишутся в железо, поэтому изменения видят Meet, Zoom и OBS
- Встроенные профили Default / Streaming / Video call плюс свои снимки, отдельно для каждой камеры
Набор функций зависит от того, что сообщает каждое устройство; приложение показывает только те элементы управления, которые устройство поддерживает.
Поддержка платформ
| Функция | macOS | Windows | Linux |
|---|---|---|---|
| Переназначение кнопок мыши и жесты | ✓ | ✓ | ✓ |
| Переназначение F-клавиш клавиатуры | ✓ | ✓ | — |
| Профили по приложениям | ✓ | ✓ | X11, wlroots (sway, Hyprland), GNOME с расширением-компаньоном |
| Профиль мыши следует за окном под указателем | ✓ | ✓ | Только X11 |
| Медиаклавиши | Нативно | Нативно | D-Bus MPRIS |
| Действия оконного менеджера (рабочие столы, Mission Control, …) | Нативно | Нативные эквиваленты | GNOME, KDE (KWin), Hyprland; часть действий только для macOS ничего не делает |
| Иконка в строке меню / трее | ✓ | ✓ | — |
| Обновления в приложении | Sparkle | Встроенные | Встроенные |
Установка
[!IMPORTANT]
Сначала закройте Logi Options+ (а в Linux — и Solaar). Приёмник может принадлежать только одному приложению одновременно.
| Платформа | Система | Архитектура |
|---|---|---|
| macOS | macOS 13 или новее | Apple silicon, Intel |
| Linux | GLIBC 2.35 или новее | x86_64, arm64 |
| Windows | Проверено на Windows 11 | x86_64, arm64 |
Каждый файл подписан minisign и указан в SHA256SUMS. Сборка из исходников: DEVELOPMENT.md.
macOS
[](https://openlogi.org/download/mac-arm64)
[](https://openlogi.org/download/mac-x64)Или через Homebrew:
brew install --cask openlogiОткройте OpenLogi и выдайте разрешения Универсальный доступ и Мониторинг ввода, когда macOS их запросит. Дальше OpenLogi обновляется сам через Sparkle.
Linux
[](https://openlogi.org/download/linux)
[](https://openlogi.org/download/linux-rpm)
[](https://openlogi.org/download/linux-arch)Или установите подходящий для вашего дистрибутива пакет одной командой. Скрипт проверяет подпись minisign и запускает агент; сначала установите minisign:
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | shКак просмотреть скрипт перед запуском, а также Flatpak, NixOS и дистрибутивы без systemd — в INSTALL-linux.md.
Windows
[](https://openlogi.org/download/windows)
[](https://openlogi.org/download/windows-zip)Использование (CLI)
См. USAGE.md
Конфигурация
Настройки хранятся в обычном TOML; при сохранении конфиги-симлинки остаются симлинками. См. CONFIGURATION.md.
Разработка
См. DEVELOPMENT.md, включая безопасность input hook на macOS.
Благодарности
- Windows, камеры и i18n — @davidbudnick: RGB клавиатуры, поддержка Windows, веб-камеры Logitech
- Порт на Linux — @cserby
- Solaar от @pwr — открытая реализация HID++
- Mouser от @TomBadash — локальная замена Options+ без аккаунта
Лицензия
Код в этом репозитории распространяется по двойной лицензии — на выбор:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
Сторонний код
crates/openlogi-hidpp — вендорский форк hidpp
от @lus, лицензия 0BSD.
Логотип и бренд
Спасибо @kubai087 за дизайн логотипа OpenLogi.
Логотип и иконка приложения (бренд-ассеты вdesign/) © 2026 AprilNEA, все права защищены, и не покрываются
лицензиями MIT/Apache выше; см. design/LICENSE. Форк кода
не даёт прав на имя, логотип или иконку OpenLogi; не используйте их для своих
проектов, форков и дистрибутивов без предварительного письменного разрешения.
Не связан с Logitech. «Logitech», «MX Master» и «Options+» — товарные знаки Logitech International S.A.
Активность репозитория
README.Tr (`docs/README.tr.md`)
[!WARNING]
OpenLogi hâlâ aktif geliştirme aşamasında ve henüz kararlı değil — özellikler ve ayarlar değişebilir. Yeni bir sürüm çıktığında haberdar olmak için depoya Star ⭐ verin ve Watch 👀 edin.
OpenLogi
Options+'tan bıktınız mı? OpenLogi'yi deneyin.
macOS, Linux ve Windows'ta çalışır.
Options+'ın Ötesinde
OpenLogi'nin Options+'ta bulamayacağınız özellikleri:
- Hafif kalır. Native Rust + GPUI.
- Linux'ta çalışır. Linux, OpenLogi'de birinci sınıf bir platformdur.
- Desteklenen tuşlarda jestler. Desteklenen kontrollere jest eylemleri atayın — isterseniz jestleri tamamen kapatın.
- Düz metin ayar dosyası. Her şey tek bir TOML dosyasında; makineler arasında istediğiniz şekilde senkronize edin.
- Betiklenebilir. Arayüzün yanında gerçek bir CLI: DPI, SmartShift, Fn-lock ve raporlama hızı için JSON tabanlı bir otomasyon API'si, ayrıca donanım tanılama araçları.
Özellikler
Cihazlar
- Logi Bolt ve Unifying alıcıları, Bluetooth ve kablolu bağlantılar; pil seviyesi ve şarj durumu dahil
- Ekranın altında düşük pil uyarıları; her deşarjda bir kez gösterilir ve işletim sisteminden bildirim izni gerektirmez
- Bir klavye Caps Lock'u açıp kapattığında ekranda bir gösterge; Windows ve Linux'ta Num Lock ve Scroll Lock için de
| Easy-SwitchHer kanalda hangi bilgisayarın eşleştirildiğini, işletim sistemi ve bağlantısıyla birlikte görünİzin veren cihazlarda bir kanalı yeniden adlandırın |
|---|
Tuşlar ve eylemler
| Herhangi bir tuşu yeniden atayınHazır eylem kataloğu, özel kısayollar, yazılı metin veya çok adımlı iş akışlarıAyrı dokunma ve basılı tutma eylemleri; tuş bırakılınca serbest kalan bas-konuş kombinasyonlarıOrta Tık, Geri/İleri, DPI/ModeShift, jest tuşu ve dokunsal panelde jestler | |
|---|---|
| Actions Ringİmlecin etrafında halka şeklinde sekiz eylemHer uygulama için ayrı bir düzen | |
| Klavye tuşlarıMedya, emoji, ekran görüntüsü ve diğer özel tuşları fare tuşları gibi atayınTek bir tuşu cihaz varsayılanına döndürün veya tüm satırı sıfırlayın |
Uygulama başına profiller, odaktaki uygulamayla birlikte değişir. Bir [apps] takma adı, tek bir profilin bir uygulamayı tüm platformlarda takip etmesini sağlar.
İşaretçi ve kaydırma
| İşaretçiDöngü ve Önayar-Belirle eylemleriyle DPI önayarlarıİşaretçi hızı ve raporlama hızıHassas Mod: tek bir tuş fareyi en yavaş hızına alır ve geri döndürür | |
|---|---|
| SmartShiftTekerlek modu ve hassasiyetSmartShift Enhanced farelerde kaydırma kuvvetiKalıcı kademeli mod anahtarı | |
| KaydırmaCihaz başına kaydırma yönü ters çevirme, yerel olarak veya yazılımlaTekerlek çözünürlüğü ve yumuşak tekerlek kaydırmaParmak tekerleği olmayan farelerde Geri veya İleri'yi basılı tutup tekerleği çevirerek yatay kaydırma |
Klavye ve aydınlatma
| KlavyeFn Lock ve ana bilgisayarın işletim sistemi düzeninde kalan çok platformlu klavyelerArka aydınlatma seviyesi, otomatik parlaklık ve pil tasarrufuArka aydınlatma seviyesi değiştikçe ekranda gösterilirDesteklenen klavyelerde statik RGB | |
|---|---|
| Litra ışıklarıGüç, parlaklık ve renk sıcaklığıİsterseniz kamerayla birlikte açılırIşığın kendisi üzerinde yapılan değişikliği korur |
Web kameraları
- Herhangi bir Logitech UVC web kamerası (Brio, StreamCam, C920 serisi, …), tak-çalıştır
- Kamerayı yalnızca siz izlerken kullanan canlı önizleme; ekrandan çıktığınızda LED söner
- Yakınlaştırma, kaydırma, eğme, odak, pozlama, beyaz dengesi ve diğer tüm UVC kontrolleri; donanıma yazılır, böylece Meet, Zoom ve OBS değişikliği görür
- Yerleşik Varsayılan / Yayın / Görüntülü arama profilleri ile kendi anlık görüntüleriniz; kamera başına saklanır
Özellik kapsamı her cihazın bildirdiklerine bağlıdır; uygulama yalnızca cihazın desteklediği kontrolleri gösterir.
Platform desteği
| Özellik | macOS | Windows | Linux |
|---|---|---|---|
| Fare tuşu yeniden atama ve jestler | ✓ | ✓ | ✓ |
| Klavye F-tuşu yeniden atama | ✓ | ✓ | — |
| Uygulama başına profiller | ✓ | ✓ | X11, wlroots (sway, Hyprland), yardımcı eklentiyle GNOME |
| Fare profili işaretçinin altındaki pencereyi takip eder | ✓ | ✓ | Yalnızca X11 |
| Medya tuşları | Yerel | Yerel | D-Bus MPRIS |
| Pencere yöneticisi eylemleri (masaüstleri, Mission Control, …) | Yerel | Yerel karşılıklar | GNOME, KDE (KWin), Hyprland; macOS'a özel bazı eylemler hiçbir şey yapmaz |
| Menü çubuğu / sistem tepsisi simgesi | ✓ | ✓ | — |
| Uygulama içi güncellemeler | Sparkle | Yerleşik | Yerleşik |
Kurulum
[!IMPORTANT]
Önce Logi Options+'tan (Linux'ta Solaar'dan da) çıkın. Bir alıcıyı aynı anda yalnızca bir uygulama kullanabilir.
| Platform | Sistem | Mimari |
|---|---|---|
| macOS | macOS 13 veya üzeri | Apple silicon, Intel |
| Linux | GLIBC 2.35 veya üzeri | x86_64, arm64 |
| Windows | Windows 11'de test edildi | x86_64, arm64 |
Her indirmenin bir minisign imzası ve SHA256SUMS içinde bir girdisi vardır. Kaynaktan derlemek için DEVELOPMENT.md'ye bakın.
macOS
[](https://openlogi.org/download/mac-arm64)
[](https://openlogi.org/download/mac-x64)Veya Homebrew ile:
brew install --cask openlogiOpenLogi'yi açın ve macOS sorduğunda Erişilebilirlik ve Girdi İzleme izinlerini verin. Ardından OpenLogi kendini Sparkle ile günceller.
Linux
[](https://openlogi.org/download/linux)
[](https://openlogi.org/download/linux-rpm)
[](https://openlogi.org/download/linux-arch)Veya dağıtımınıza uygun paketi tek bir komutla kurun. Komut minisign imzasını doğrular ve ajanı başlatır; önce minisign'ı kurun:
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | shBetiği çalıştırmadan önce okumak ve Flatpak, NixOS ile systemd olmayan dağıtımlar için INSTALL-linux.md'ye bakın.
Windows
[](https://openlogi.org/download/windows)
[](https://openlogi.org/download/windows-zip)Kullanım (CLI)
USAGE.md'ye bakın
Yapılandırma
Ayarlar düz TOML kullanır; kaydetme işlemleri sembolik bağlı (symlink) ayar dosyalarını korur. CONFIGURATION.md'ye bakın.
Geliştirme
DEVELOPMENT.md'ye bakın, macOS giriş-kancası güvenliği dahil.
Teşekkürler
- Windows, kameralar ve i18n: @davidbudnick — klavye RGB, Windows desteği, Logitech web kamerası desteği
- Linux portu: @cserby — Linux desteği
- Solaar (@pwr) — açık kaynak HID++ uygulaması
- Mouser (@TomBadash) — hesap gerektirmeyen, yerel bir Options+ alternatifi
Lisans
Bu depodaki kod, aşağıdakilerden birinin seçimine bağlı olarak çift lisanslıdır:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT lisansı (LICENSE-MIT)
Üçüncü taraf kod
crates/openlogi-hidpp, @lus tarafından yazılanhidpp paketinin 0BSD lisanslı, depoya
gömülü bir çatallanmasıdır (fork).
Logo ve marka varlıkları
OpenLogi logosunu tasarlayan @kubai087'e
teşekkürler. OpenLogi logosu ve uygulama simgesi (design/
altındaki marka varlıkları) © 2026 AprilNEA'ya aittir, tüm hakları saklıdır,
ve yukarıdaki MIT/Apache lisansları kapsamında değildir; bakınızdesign/LICENSE. Kodu çatallamak, OpenLogi adı, logosu
veya simgesi üzerinde hak vermez; lütfen bunları kendi projelerinizi,
çatallarınızı veya dağıtımlarınızı temsil etmek için önceden yazılı izin
almadan kullanmayın.
Logitech ile bağlantılı değildir. "Logitech", "MX Master" ve "Options+", Logitech International S.A.'nın ticari markalarıdır.
Depo etkinliği
README.Zh CN (`docs/README.zh-CN.md`)
[!WARNING]
OpenLogi 仍在积极开发中,尚未稳定 —— 功能与配置仍可能变动。点个 Star ⭐ 并 Watch 👀 本仓库,在新版本发布时获得通知。
OpenLogi
被 Options+ 折腾够了?试试 OpenLogi。
支持 macOS、Linux 和 Windows。
超越 Options+
OpenLogi 能做、而 Options+ 做不到的事:
- 轻量:原生 Rust + GPUI。
- 支持 Linux:Linux 是 OpenLogi 的一等平台。
- 在受支持的按键上使用手势:给受支持的控件分配手势动作,也可以完全关闭手势。
- 纯文本配置:所有设置都在一个 TOML 文件里,用什么方式在多台机器之间同步都可以。
- 可脚本化:GUI 之外还有完整的 CLI,提供 JSON 自动化 API(DPI、SmartShift、Fn 锁定、回报率)和硬件诊断。
功能
设备
- 支持 Logi Bolt 和 Unifying 接收器、蓝牙以及有线连接,显示电池电量和充电状态
- 电量低时在屏幕底部提示,每次放电只提示一次,不需要系统通知权限
- 键盘切换 Caps Lock 时在屏幕上显示提示;Windows 和 Linux 上还包括 Num Lock 和 Scroll Lock
| Easy-Switch查看每个通道配对的电脑、系统和连接方式设备允许时可以重命名通道 |
|---|
按键与动作
| 重新映射任意按键内置动作、自定义快捷键、文本输入或多步工作流单击和长按可以分别设置动作;按住型组合键在松开按键时释放,适合按键说话在中键、后退和前进键、DPI 或模式切换键、手势键和触觉面板上使用手势 | |
|---|---|
| Actions Ring光标周围一圈八个动作每个应用可以有自己的布局 | |
| 键盘按键像鼠标按键一样绑定媒体、表情、截屏等专用键可以把单个按键恢复为设备默认,或重置整排按键 |
按应用配置随前台应用自动切换。用 [apps] 别名可以让同一份配置跨平台跟随一个应用。
指针与滚动
| 指针DPI 预设,支持循环切换和切到指定预设指针速度和回报率精准模式:一个按键把鼠标切到最低速度,再按一次切回 | |
|---|---|
| SmartShift滚轮模式和灵敏度SmartShift Enhanced 鼠标可调滚动力度永久棘轮开关 | |
| 滚动按设备反转滚动方向,设备支持时原生实现,否则由软件实现滚轮分辨率和平滑滚轮在没有拇指滚轮的鼠标上,按住后退或前进键再滚动主滚轮即可横向滚动 |
键盘与灯光
| 键盘Fn Lock;多平台键盘保持当前系统的布局背光档位、自动亮度和省电模式调整背光时在屏幕上显示档位受支持键盘的静态 RGB 灯光 | |
|---|---|
| Litra 补光灯开关、亮度和色温可以跟随摄像头自动开灯在灯上直接做的调整也会保存 |
摄像头
- 支持任何 Logitech UVC 摄像头(Brio、StreamCam、C920 系列等),即插即用
- 实时预览只在查看时占用摄像头,离开预览即释放,指示灯随之熄灭
- 变焦、平移、俯仰、对焦、曝光、白平衡等全部 UVC 控制直接写入硬件,Meet、Zoom、OBS 都能看到变化
- 内置「默认」「直播」「视频通话」三个配置档,另可保存自定义快照,按摄像头分别保存
能用哪些功能取决于设备报告的能力,应用只显示设备支持的控件。
平台支持
| 功能 | macOS | Windows | Linux |
|---|---|---|---|
| 鼠标按键映射和手势 | ✓ | ✓ | ✓ |
| 键盘 F 键映射 | ✓ | ✓ | — |
| 按应用配置 | ✓ | ✓ | X11、wlroots(sway、Hyprland),以及装有配套扩展的 GNOME |
| 鼠标配置跟随指针所在窗口 | ✓ | ✓ | 仅 X11 |
| 媒体键 | 原生 | 原生 | D-Bus MPRIS |
| 窗口管理动作(切换桌面、调度中心等) | 原生 | 原生对应功能 | GNOME、KDE(KWin)、Hyprland;部分 macOS 专属动作无效果 |
| 菜单栏或托盘图标 | ✓ | ✓ | — |
| 应用内更新 | Sparkle | 内置 | 内置 |
安装
[!IMPORTANT]
先退出 Logi Options+(Linux 上还有 Solaar)。同一个接收器同时只能由一个应用占用。
| 平台 | 系统 | 架构 |
|---|---|---|
| macOS | macOS 13 或更高版本 | Apple 芯片、Intel |
| Linux | GLIBC 2.35 或更高版本 | x86_64, arm64 |
| Windows | 已在 Windows 11 上测试 | x86_64, arm64 |
每个下载文件都带 minisign 签名,并在 SHA256SUMS 中有对应条目。从源码构建见 DEVELOPMENT.md。
macOS
[](https://openlogi.org/download/mac-arm64)
[](https://openlogi.org/download/mac-x64)也可以用 Homebrew 安装:
brew install --cask openlogi打开 OpenLogi,在 macOS 提示时授予辅助功能和输入监控权限。之后 OpenLogi 会通过 Sparkle 自动更新。
Linux
[](https://openlogi.org/download/linux)
[](https://openlogi.org/download/linux-rpm)
[](https://openlogi.org/download/linux-arch)也可以用一条命令安装适合当前发行版的安装包。脚本会校验 minisign 签名并启动 agent;先装好 minisign:
curl -fsSL https://github.com/AprilNEA/OpenLogi/releases/latest/download/install.sh | sh想先审阅脚本再运行,或使用 Flatpak、NixOS、无 systemd 的发行版,见 INSTALL-linux.md。
Windows
[](https://openlogi.org/download/windows)
[](https://openlogi.org/download/windows-zip)使用(CLI)
见 USAGE.md。
配置
设置保存为纯文本 TOML;保存时会保留以符号链接方式管理的配置文件。见 CONFIGURATION.md。
开发
见 DEVELOPMENT.md,其中包括 macOS 输入钩子安全。
致谢
- Windows、摄像头与 i18n:@davidbudnick —— 键盘 RGB 支持、Windows 支持、Logitech 摄像头支持
- Linux 移植:@cserby —— Linux 支持
- Solaar,作者 @pwr —— 开源 HID++ 实现
- Mouser,作者 @TomBadash —— 本地、无需账号的 Options+ 替代品
许可证
本仓库代码可选择以下任一许可证:
- Apache License 2.0(LICENSE-APACHE)
- MIT 许可证(LICENSE-MIT)
第三方代码
crates/openlogi-hidpp 是 hidpp(作者 @lus)的 vendored fork,采用 0BSD 许可证。
Logo 与品牌资产
感谢 @kubai087 为 OpenLogi 设计的 Logo,该 Logo —— 即 design/ 下的品牌资产 —— © 2026 AprilNEA 保留所有权利,不在上述 MIT/Apache 许可范围内,许可证详见 design/LICENSE。
Fork 代码并不授予 OpenLogi 名称、Logo 或图标的使用权,未经事先书面许可,请勿用它们代表你自己的项目、Fork 或分发版本。
与 Logitech 无关联。 「Logitech」、「MX Master」与「Options+」是 Logitech International S.A. 的商标。
仓库活跃度
SECURITY (`docs/SECURITY.md`)
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:
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 (`docs/USAGE.md`)
Usage (CLI)
The openlogi command-line tool. For install and configuration, see the
README.
For the desktop app's Add Device flow, see wireless pairing.
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 pointer-speed # test 0x2205 pointer scaling and attempt to restore it
openlogi diag smartshift # toggle SmartShift and restore (smoke test)
openlogi diag smartshift --torque 60 # set scrolling force / feedback intensity (1-100%)
openlogi diag lighting ff0000 # solid colour for a wired RGB keyboard (any RRGGBB hex)Running openlogi with no subcommand defaults to list. SetOPENLOGI_LOG=debug for verbose tracing in the CLI, GUI, or agent.
openlogi diag pointer-speed temporarily changes the selected device's pointer scaling. The command attempts to restore and verify the original scaling even when the test write fails. --device NAME selects a device; --target RAW sets the temporary 8.8 fixed-point value (256 is 1×). By default, the command halves the current value, or doubles it when the raw value is 1. Firmware may accept extreme values without clipping. A restoration error means the original speed could not be confirmed.
Keyboard backlight
openlogi backlight reads and changes the white HID++ Backlight2 (0x1982)
backlight, for example on MX Keys. It requires a running, protocol-compatible
OpenLogi agent. Start OpenLogi normally and use the same OPENLOGI_PROFILE for
the app and CLI. The command uses the agent's inventory and existing channel;
it never opens HID directly, starts the agent, or falls back to diagnostics.
It does not arm a dormant agent's input stack.
openlogi backlight # status, no write
openlogi backlight --device "MX Keys" status
openlogi backlight --device "MX Keys" off
openlogi backlight --device "MX Keys" on
openlogi backlight --device "MX Keys" auto on
openlogi backlight --device "MX Keys" level 3
openlogi backlight --device "MX Keys" battery-saving on--device NAME is a case-insensitive name substring, not the automation API's
opaque route ID. It must match exactly one online HID++ device. Without it,
automatic selection requires exactly one online device with measured backlight
support. Unknown capabilities require an explicit name and an agent read;
known unsupported devices are refused. An incomplete inventory, missing route,
or ambiguous selection fails before any backlight operation. Identical direct
devices sharing a route cannot be disambiguated by name.
Compatibility change: earlier versions could open hardware without an agent
and choose the first matching device. This command now fails instead. This does
not change list fallback, hardware diagnostics, or the JSON API's selection.
Bare invocation and status only read. Changes are saved in the keyboard's
non-volatile memory, not config.toml, and survive reconnects, host switches,
and power cycles. The agent confirms writes; the CLI prints that returned state
as read-back and only then reports success. status lists which additional
controls the firmware offers. The agent rejects unsupported modes or levels.
This command does not add timer or effect settings.
With the matching agent and GUI build, an already-open Backlight panel refreshes
after a CLI write; reopening the device or restarting the app is unnecessary.
The agent announces a device-settings revision and the GUI reads the keyboard
again. A failed write can also trigger a refresh because the keyboard may have
changed before confirmation failed; the refresh is not a success report.
Failures exit nonzero. If the agent times out or disconnects during a write,
the outcome is unconfirmed: the keyboard may already have applied it.
The command does not automatically retry. Reconnect to the matching agent and
read status before deciding whether another write is needed. Firmware's
temporary-manual mode cannot be restored by software; the command warns when
an otherwise mode-preserving change would return it to automatic mode.
Automation API
openlogi api is the JSON interface for launchers such as Raycast and local
scripts. It requires a running, protocol-compatible OpenLogi agent. It never
opens hardware directly, starts the agent, or falls back to diagnostic commands.
Start OpenLogi normally before using it. Existing human-readable commands are
unchanged.
openlogi api status
openlogi api devices
# Copy an exact, non-null id from api devices; quote it as one argument.
openlogi api dpi --device "$DEVICE_ID"
openlogi api dpi --device "$DEVICE_ID" --set 1200
openlogi api dpi --device "$DEVICE_ID" --set 1200 --save
openlogi api smartshift --device "$DEVICE_ID"
openlogi api smartshift --device "$DEVICE_ID" --mode free
openlogi api smartshift --device "$DEVICE_ID" --mode ratchet --auto-disengage 255
openlogi api fn-lock --device "$DEVICE_ID"
openlogi api fn-lock --device "$DEVICE_ID" --set on
openlogi api os-layout --device "$DEVICE_ID" --set off --save
openlogi api report-rate --device "$DEVICE_ID"
openlogi api report-rate --device "$DEVICE_ID" --set 1000 --save
openlogi api easy-switch --device "$DEVICE_ID"
openlogi api easy-switch --device "$DEVICE_ID" --channel 2 --name "Work laptop"Output and compatibility
After arguments parse, stdout contains exactly one JSON object and a newline.
Success exits with status 0; runtime failure exits with status 1:
{"schema_version":1,"ok":true,"data":{"inventory":"ready","devices":[]}}{"schema_version":1,"ok":false,"error":{"code":"agent_unavailable","message":"…"}}Consumers must check schema_version and ok, ignore unknown object fields,
and handle unknown error codes as failures. Breaking changes to this JSON
contract require a schema version change; it is not the internal IPC version.
Error messages are explanatory text, not stable values to parse. CLI syntax
errors retain standard clap behavior (exit 2, diagnostics on stderr);--help and --version are text. Logging also goes to stderr. Failure to write
stdout cannot provide a JSON envelope.
| Command | data fields |
|---|---|
status |
agent_version, inventory, accessibility_granted, input_monitoring_granted, hook_installed, hid_open_failures, launch_at_login |
devices |
inventory, devices: array of id, name, kind, online, battery, capabilities, light_capabilities |
dpi |
device, persistence, current (integer), supported (array of integers) |
smartshift |
device, persistence, mode (free or ratchet), auto_disengage (integer), tunable_torque (1–100, percent of the wheel's maximum force, or null) |
fn-lock |
device, persistence, fn_lock, default_fn_lock (boolean, or null when the keyboard reports no firmware default) |
os-layout |
device, persistence, keep_os_layout (boolean) |
report-rate |
device, persistence, current (integer, hertz), supported (array of integers in hertz, for the current connection) |
easy-switch |
device, persistence, current_channel (integer from 1, or null), can_rename (boolean), channels: array of channel (integer from 1), paired (boolean), connection (unknown, unifying, usb, bluetooth, or bolt), name (string or null), name_max_len (integer, UTF-8 bytes), os (string such as macOS 15.2, or null) |
inventory is scanning, ready, or unavailable. Only ready with an empty
device array means enumeration completed and found no peripherals. Device
operations are refused until inventory is ready. Status queries do not arm a
dormant agent's input stack; opening the desktop app follows the normal lifecycle.
Device IDs are opaque current-route identifiers, not persistent physical-device
keys. Refresh them after reconnects or updates; do not construct IDs, parse them,
or use array positions or names to address devices. Missing routes have a null
ID. Duplicate IDs are returned without deduplication, and operations against
them fail with ambiguous_device. In particular, the current transport cannot
distinguish identical direct-attached devices that share one route. IDs can
contain hardware identity information: keep them local and redact before sharing.
name, battery, and measured capabilities may be null; null does not mean
zero charge or an unsupported feature. Battery objects contain percentage,level, and status (for example charging or discharging). percentage
is null when the device has no state-of-charge gauge and reports only a level,
or when the reading carries no charge figure; read level in that case.
Capabilities are the agent's measured feature flags, not guesses based on
device kind.
Not every readable setting has an inventory flag; a setting read can returnunsupported_feature. Standalone lights are listed with their light capability
descriptor and null battery. Cameras are not part of this agent inventory API.
The response excludes pairing passkeys, application history, and model serials.
Immediate control and saved preferences
The DPI, SmartShift, Fn-lock, and report-rate commands read only unless a setting flag is supplied. Without--save, writes return persistence: "not_saved": they do not modifyconfig.toml or request a config reload. Saved preferences may be reapplied on
reconnect, wake, or later configuration changes; firmware may retain some values.
Add --save to an explicit DPI, SmartShift, Fn-lock, or report-rate change to save the verified
setting and request an agent config reload. Success returns persistence: "saved".
The device must have a probed physical identity. Existing legacy configuration
entries are respected; CLI route IDs are never used directly as config keys.
Saving edits only the selected setting, preserving unrelated preferences and
comments. A per-link override of that setting is refused with link_override
rather than silently saving a default that the override would supersede.
There is no profile-switch API yet.
os-layout changes an agent setting rather than the device, so it requires
both --set and --save; without --save it fails with invalid_value. The
agent applies the saved value after the reload. on, the default for keyboards
that allow it, sets the keyboard's key layout (HID++ 0x4531) to the host OS
when the keyboard connects, when the setting turns on, and after the keyboard
reports that a layout shortcut switched it. off leaves the keyboard's layout
as it is; it does not restore an earlier layout. A keyboard whose measured
capabilities lack os_layout returns unsupported_feature.
report-rate --set accepts only a rate in supported. That list belongs to
the device's current connection — a gaming mouse can offer more rates over its
receiver or a cable than over Bluetooth — so any other value fails withinvalid_value before anything is written. A saved rate is re-applied on
reconnect only over a connection that lists it. A device without HID++0x8060 or 0x8061 returns unsupported_feature.
easy-switch lists the device's Easy-Switch channels (HID++ 0x1815) and the
name and OS each paired computer reported. --channel and --name together
rename the computer on that channel; the device stores the name itself, so
there is no --save (it fails with invalid_value) and the result is alwayspersistence: "not_saved". Surrounding whitespace is trimmed. A name that is
empty, contains NUL, or is longer than the channel's name_max_len bytes, or a
channel with no paired computer, fails with invalid_value before anything is
written. A device that does not let software rename hosts (can_rename is
false) returns unsupported_feature, and a name the device does not read back
returns readback_mismatch. A device without 0x1815 returnsunsupported_feature.
The desktop app shows the same channels in an Easy-Switch card on the Device
tab and renames a paired channel under the same name rules. Neither the CLI
nor the app renames a channel on its own: unlike Logi Options+, OpenLogi does
not write this computer's name or OS onto the current channel when it starts.
Hardware writes, file persistence, and reload are separate steps, not an atomic
transaction. The config revision is checked under a writer lock; concurrent edits
are not overwritten. A lock conflict is reported immediately, without waiting.
The persistent config.toml.lock sidecar must not be deleted while writers run.
External editors that do not honor this lock can still race with a save.
If saving fails after verified hardware readback, the JSON error includespersistence: "not_saved" with config_conflict, config_busy, orconfig_save_failed. The live hardware has already changed; no rollback or
automatic retry is attempted. If saving succeeds but reload fails, the error
includes persistence: "saved": config_reload_failed means rejection;config_reload_unknown means timeout/disconnection with an unknown reload
outcome. Neither is reported as success. Other errors occur before persistence;
hardware outcomes after a failed write/readback may still be uncertain.
DPI must be in the device-reported supported list; values are never silently
rounded. SmartShift preserves unspecified fields, including wheel torque.auto_disengage is 1–254 in firmware units of 0.25 turn/s; 255 means permanent
ratchet, and 0 is rejected. --mode ratchet alone does not disable automatic
release: use --auto-disengage 255 as well. Fn lock on means bare function keys
send F1–F12; off means the printed media functions.
With --save, SmartShift must use the configuration-supported range 8–255;
this also validates a threshold retained from the device when only mode changes.
DPI and SmartShift are read back after writes; Fn lock returns the firmware
echo. Reads and writes are not a transaction against simultaneous GUI changes.
A timeout, disconnect, or readback error after a write means its outcome may
be uncertain: read the setting again rather than blindly retrying the write.
Stable error codes: agent_unavailable, handshake_failed, version_mismatch,timeout, disconnected, inventory_not_ready, device_not_found,device_offline, ambiguous_device, unsupported_feature, invalid_value,readback_mismatch, device_error, identity_unavailable, link_override,config_error, config_conflict, config_busy, config_save_failed,config_reload_failed, and config_reload_unknown. A protocol mismatch requires matching
OpenLogi CLI and agent builds; it never triggers direct-hardware fallback.
Asset synchronization
Asset synchronization probes assets.openlogi.org, the versioned Cloudflare
Pages release alias, and the pinned jsDelivr npm release concurrently. The first
mirror with a valid catalog supplies every file for that synchronization run.
Set OPENLOGI_ASSETS or pass openlogi assets sync --base to use one
uniform asset origin instead of automatic mirror selection.
--- METRICS ---
- Files Extracted: 24
- Estimated Token Budget: ~95284 tokens
- Canonical Reference: https://codewiki.google/github.com/AprilNEA/OpenLogi