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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ two binaries:

Everything is local: bindings live in a plain TOML file, button presses are remapped through the OS event tap, and DPI / SmartShift changes are written straight to the device over HID++.

macOS and Linux are supported. Windows is an early, untested preview — signed
builds ship with each release; see [Roadmap](#roadmap).
macOS, Linux, and Windows are supported. Windows is the newest port —
validated end-to-end on Windows 11 with real hardware; signed builds ship with
each release. See [Roadmap](#roadmap).

## Beyond Options+

Expand Down Expand Up @@ -85,7 +86,7 @@ Things OpenLogi does that Options+ won't:
| Linux packaging: udev rules, systemd unit, `.deb` / `.rpm` | ✅ Linux |
| Gesture-button per-direction bindings | 🟡 configurable; hardware capture pending |
| Middle / mode-shift / thumbwheel button capture | 🟡 configurable; hook owns side buttons only |
| Windows (agent, GUI, event hook) | 🟡 untested preview — signed `.exe` / `.msi` ship per release |
| Windows (agent, GUI, event hook, tray) | ✅ validated end-to-end on Windows 11 — signed `.zip` / `.msi` ship per release |

¹ Media key actions use D-Bus MPRIS on Linux; a handful of macOS-specific actions (e.g. Launchpad) have no Linux equivalent and are no-ops.

Expand Down Expand Up @@ -143,11 +144,18 @@ and distros without systemd.

### Windows

Signed portable `.zip` archives and per-user `.msi` installers (x86_64 and
arm64) are attached to each release. Both ship the GUI (`OpenLogi.exe`)
together with the background agent (`openlogi-agent.exe`), which owns all
device I/O — keep the two files side by side when using the portable zip, or
the GUI has nothing to connect to.
Each release attaches two signed Windows artifacts per architecture (x86_64
and arm64), carrying the same two binaries — the GUI (`OpenLogi.exe`) and the
background agent (`openlogi-agent.exe`), which owns all device I/O:

- **`.msi` (recommended)** — a per-user installer: no admin prompt, a Start
Menu shortcut, a clean entry in *Installed apps*, and in-place upgrades that
close the running app/agent safely. If you're unsure which to grab, grab
this one.
- **`.zip` (portable)** — unzip and run, no install; suited to machines where
you can't (or don't want to) install software. Keep the two exes side by
side — the GUI spawns its sibling agent, and without it there's nothing to
connect to — and update by replacing the folder yourself.

Windows support works and has been validated end-to-end on Windows 11 with
real hardware — a wired keyboard and a Unifying-receiver mouse, including
Expand Down
6 changes: 6 additions & 0 deletions crates/openlogi-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ tracing-subscriber = { workspace = true }
sysinfo = { version = "0.38.4", default-features = false, features = ["system"] }
opener = "0.8.5"

# Windows exe resources (app icon + VERSIONINFO) — see build.rs. The exact
# version already in Cargo.lock as gpui's own build-dependency, so the resolve
# adds an edge, not a crate, and the pinned gpui rev cannot move.
[build-dependencies]
embed-resource = "3.0.9"

# Menu-bar status item (NSStatusItem) — the agent hosts the tray now. objc2
# gives Retained<T>-owned AppKit bindings (no #99-style leaks); versions match
# the GUI's so the resolve doesn't move the gpui Cargo.lock pin.
Expand Down
77 changes: 77 additions & 0 deletions crates/openlogi-agent/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! Build script for openlogi-agent: embeds the Windows exe resources — the
//! app icon and a VERSIONINFO block — so Task Manager and Explorer identify
//! the background agent instead of showing a generic blank binary.
//!
//! Kept in sync with the twin in `crates/openlogi-gui/build.rs` — only the
//! description/filename strings differ. `embed-resource` is pinned to the
//! exact version already in Cargo.lock as gpui's own build-dependency, so it
//! adds an edge, not a crate, and cannot move the pinned gpui rev.

// A build script fails by panicking, so `expect` (with a message that surfaces
// in the build log) is the idiomatic error path here — exempt it from the
// workspace's strict runtime lints.
#![allow(clippy::expect_used)]

use std::path::PathBuf;
use std::{env, fs};

fn main() {
if env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
// rc.exe treats `\` in string literals as escapes; forward slashes are
// accepted by every resource compiler embed-resource can drive.
let icon = manifest_dir
.join("../../design/icon/openlogi.ico")
.display()
.to_string()
.replace('\\', "/");
println!("cargo:rerun-if-changed={icon}");

let (major, minor, patch) = (
env::var("CARGO_PKG_VERSION_MAJOR").expect("CARGO_PKG_VERSION_MAJOR"),
env::var("CARGO_PKG_VERSION_MINOR").expect("CARGO_PKG_VERSION_MINOR"),
env::var("CARGO_PKG_VERSION_PATCH").expect("CARGO_PKG_VERSION_PATCH"),
);
let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION");
let rc = format!(
r#"1 ICON "{icon}"

1 VERSIONINFO
FILEVERSION {major},{minor},{patch},0
PRODUCTVERSION {major},{minor},{patch},0
FILEOS 0x40004L
FILETYPE 0x1L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "AprilNEA"
VALUE "FileDescription", "OpenLogi Background Agent"
VALUE "FileVersion", "{version}"
VALUE "InternalName", "openlogi-agent"
VALUE "OriginalFilename", "openlogi-agent.exe"
VALUE "ProductName", "OpenLogi"
VALUE "ProductVersion", "{version}"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
"#
);
let out = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let rc_path = out.join("openlogi-agent.rc");
fs::write(&rc_path, rc).expect("write generated .rc into OUT_DIR");
// manifest_optional: a missing resource compiler downgrades to a cargo
// warning (icon-less but working exe) instead of failing dev builds on
// machines without the Windows SDK; release builds run on CI runners that
// always carry rc.exe.
embed_resource::compile(&rc_path, embed_resource::NONE)
.manifest_optional()
.expect("compile Windows resources");
}
6 changes: 6 additions & 0 deletions crates/openlogi-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ fluent-langneg = "0.14.2"
backon.workspace = true
opener = "0.8.5"

# Windows exe resources (app icon + VERSIONINFO) — see build.rs. The exact
# version already in Cargo.lock as gpui's own build-dependency, so the resolve
# adds an edge, not a crate, and the pinned gpui rev cannot move.
[build-dependencies]
embed-resource = "3.0.9"

# Menu-bar (NSStatusItem) FFI — GPUI has no status-bar API. objc2 gives typed,
# `Retained<T>`-owned AppKit bindings so the status-item code can't leak (#99).
[target.'cfg(target_os = "macos")'.dependencies]
Expand Down
82 changes: 78 additions & 4 deletions crates/openlogi-gui/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
//! `OPENLOGI_THEMES_DIR` overrides the lookup with an explicit path to the
//! gpui-component `themes/` directory, as an escape hatch.
//!
//! Uses only `std` on purpose: adding a build-dependency would re-resolve the
//! lockfile and bump the precisely-pinned (Cargo.lock-only) gpui rev — so the
//! `cargo metadata` JSON is scanned for `manifest_path` values directly rather
//! than parsed with serde.
//! Uses only `std` plus `embed-resource` on purpose: a build-dependency the
//! resolver hasn't seen would re-resolve the lockfile and bump the
//! precisely-pinned (Cargo.lock-only) gpui rev — so the `cargo metadata` JSON
//! is scanned for `manifest_path` values directly rather than parsed with
//! serde, and `embed-resource` (for [`embed_windows_resources`]) is pinned to
//! the exact version already in the lock as gpui's own build-dependency, which
//! adds an edge, not a crate.

// A build script fails by panicking, so `expect` (with a message that surfaces
// in the build log) is the idiomatic error path here — exempt it from the
Expand Down Expand Up @@ -56,6 +59,8 @@ fn main() {
println!("cargo:rerun-if-env-changed=OPENLOGI_UPDATE_MANIFEST_URL");
println!("cargo:rerun-if-env-changed=OPENLOGI_THEMES_DIR");

embed_windows_resources();

let out = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let src_dir = locate_themes_dir();
let dest = out.join("themes");
Expand All @@ -81,6 +86,75 @@ fn main() {
fs::write(out.join("builtin_themes.rs"), generated).expect("write builtin_themes.rs");
}

/// Embed the Windows exe resources — the app icon and a VERSIONINFO block —
/// so Explorer, the taskbar, and Task Manager stop showing the generic blank
/// binary. The `.rc` is generated here rather than committed so the version
/// block tracks `CARGO_PKG_VERSION` (a literal would go stale the moment
/// release-plz bumps). No-op off Windows targets.
///
/// Kept in sync with the twin in `crates/openlogi-agent/build.rs` — only the
/// description/filename strings differ.
fn embed_windows_resources() {
if env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
// rc.exe treats `\` in string literals as escapes; forward slashes are
// accepted by every resource compiler embed-resource can drive.
let icon = manifest_dir
.join("../../design/icon/openlogi.ico")
.display()
.to_string()
.replace('\\', "/");
println!("cargo:rerun-if-changed={icon}");

let (major, minor, patch) = (
env::var("CARGO_PKG_VERSION_MAJOR").expect("CARGO_PKG_VERSION_MAJOR"),
env::var("CARGO_PKG_VERSION_MINOR").expect("CARGO_PKG_VERSION_MINOR"),
env::var("CARGO_PKG_VERSION_PATCH").expect("CARGO_PKG_VERSION_PATCH"),
);
let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION");
let rc = format!(
r#"1 ICON "{icon}"

1 VERSIONINFO
FILEVERSION {major},{minor},{patch},0
PRODUCTVERSION {major},{minor},{patch},0
FILEOS 0x40004L
FILETYPE 0x1L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "AprilNEA"
VALUE "FileDescription", "OpenLogi"
VALUE "FileVersion", "{version}"
VALUE "InternalName", "openlogi-gui"
VALUE "OriginalFilename", "OpenLogi.exe"
VALUE "ProductName", "OpenLogi"
VALUE "ProductVersion", "{version}"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
"#
);
let out = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let rc_path = out.join("openlogi-gui.rc");
fs::write(&rc_path, rc).expect("write generated .rc into OUT_DIR");
// manifest_optional: a missing resource compiler downgrades to a cargo
// warning (icon-less but working exe) instead of failing dev builds on
// machines without the Windows SDK; release builds run on CI runners that
// always carry rc.exe.
embed_resource::compile(&rc_path, embed_resource::NONE)
.manifest_optional()
.expect("compile Windows resources");
}

/// Find the gpui-component `themes/` directory: an explicit override first, else
/// the dependency's real source location as reported by `cargo metadata`.
fn locate_themes_dir() -> PathBuf {
Expand Down
3 changes: 3 additions & 0 deletions crates/openlogi-gui/locales/da.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ _version: 1
"Check once per launch for a new version (query only — no automatic download).": "Søg én gang per start efter en ny version (kun forespørgsel — ingen automatisk download)."
"Show in menu bar": "Vis i menulinjen"
"Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Behold OpenLogis ikon i menulinjen. Når den er slået fra, forbliver det i Dock'en."
"Show in the notification area": "Vis i meddelelsesområdet"
"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Behold OpenLogis ikon i proceslinjens meddelelsesområde. Træder i kraft, næste gang baggrundsagenten starter."
"Download from GitHub": "Download fra GitHub"
"Assets": "Ressourcer"
"Automatically download device images": "Download enhedsbilleder automatisk"
"Fetch device renders from assets.openlogi.org when a device connects. When off, OpenLogi makes no asset network requests; bundled art and the silhouette still show.": "Hent enhedsbilleder fra assets.openlogi.org, når en enhed forbindes. Når det er slået fra, sender OpenLogi ingen netværksanmodninger efter ressourcer; medfølgende billeder og silhuetten vises stadig."
Expand Down
3 changes: 3 additions & 0 deletions crates/openlogi-gui/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ _version: 1
"Check once per launch for a new version (query only — no automatic download).": "Bei jedem Start einmal auf eine neue Version prüfen (nur Abfrage – kein automatischer Download)."
"Show in menu bar": "In der Menüleiste anzeigen"
"Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "OpenLogi-Symbol in der Menüleiste behalten. Wenn deaktiviert, bleibt es stattdessen im Dock."
"Show in the notification area": "Im Infobereich anzeigen"
"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "OpenLogi-Symbol im Infobereich der Taskleiste anzeigen. Wird beim nächsten Start des Hintergrund-Agenten wirksam."
"Download from GitHub": "Von GitHub herunterladen"
"Assets": "Ressourcen"
"Automatically download device images": "Gerätebilder automatisch herunterladen"
"Fetch device renders from assets.openlogi.org when a device connects. When off, OpenLogi makes no asset network requests; bundled art and the silhouette still show.": "Gerätebilder beim Verbinden eines Geräts von assets.openlogi.org laden. Wenn deaktiviert, stellt OpenLogi keine Netzwerkanfragen für Ressourcen; mitgelieferte Bilder und die Silhouette werden weiterhin angezeigt."
Expand Down
3 changes: 3 additions & 0 deletions crates/openlogi-gui/locales/el.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ _version: 1
"Check once per launch for a new version (query only — no automatic download).": "Έλεγχος μία φορά ανά εκκίνηση για νέα έκδοση (μόνο αναζήτηση — χωρίς αυτόματη λήψη)."
"Show in menu bar": "Εμφάνιση στη γραμμή μενού"
"Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Διατήρηση του εικονιδίου του OpenLogi στη γραμμή μενού. Όταν είναι ανενεργό, παραμένει στο Dock."
"Show in the notification area": "Εμφάνιση στην περιοχή ειδοποιήσεων"
"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Διατηρεί το εικονίδιο του OpenLogi στην περιοχή ειδοποιήσεων της γραμμής εργασιών. Ισχύει από την επόμενη εκκίνηση της υπηρεσίας παρασκηνίου."
"Download from GitHub": "Λήψη από το GitHub"
"Assets": "Πόροι"
"Automatically download device images": "Αυτόματη λήψη εικόνων συσκευών"
"Fetch device renders from assets.openlogi.org when a device connects. When off, OpenLogi makes no asset network requests; bundled art and the silhouette still show.": "Λήψη εικόνων συσκευών από το assets.openlogi.org όταν συνδέεται μια συσκευή. Όταν είναι απενεργοποιημένο, το OpenLogi δεν πραγματοποιεί αιτήματα δικτύου για πόρους· οι ενσωματωμένες εικόνες και η σιλουέτα εξακολουθούν να εμφανίζονται."
Expand Down
3 changes: 3 additions & 0 deletions crates/openlogi-gui/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ _version: 1
"Check once per launch for a new version (query only — no automatic download).": "Check once per launch for a new version (query only — no automatic download)."
"Show in menu bar": "Show in menu bar"
"Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead."
"Show in the notification area": "Show in the notification area"
"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts."
"Download from GitHub": "Download from GitHub"
"Assets": "Assets"
"Automatically download device images": "Automatically download device images"
"Fetch device renders from assets.openlogi.org when a device connects. When off, OpenLogi makes no asset network requests; bundled art and the silhouette still show.": "Fetch device renders from assets.openlogi.org when a device connects. When off, OpenLogi makes no asset network requests; bundled art and the silhouette still show."
Expand Down
3 changes: 3 additions & 0 deletions crates/openlogi-gui/locales/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ _version: 1
"Check once per launch for a new version (query only — no automatic download).": "Busca una versión nueva una vez por inicio (solo consulta: sin descarga automática)."
"Show in menu bar": "Mostrar en la barra de menús"
"Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Mantén el icono de OpenLogi en la barra de menús. Si se desactiva, permanece en el Dock."
"Show in the notification area": "Mostrar en el área de notificación"
"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Mantiene el icono de OpenLogi en el área de notificación de la barra de tareas. Surte efecto la próxima vez que se inicie el agente en segundo plano."
"Download from GitHub": "Descargar desde GitHub"
"Assets": "Recursos"
"Automatically download device images": "Descargar imágenes de dispositivos automáticamente"
"Fetch device renders from assets.openlogi.org when a device connects. When off, OpenLogi makes no asset network requests; bundled art and the silhouette still show.": "Obtén las imágenes de los dispositivos desde assets.openlogi.org cuando se conecta un dispositivo. Si está desactivado, OpenLogi no hace ninguna solicitud de red de recursos; las imágenes incluidas y la silueta se siguen mostrando."
Expand Down
3 changes: 3 additions & 0 deletions crates/openlogi-gui/locales/fi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ _version: 1
"Check once per launch for a new version (query only — no automatic download).": "Tarkista uusi versio kerran käynnistyksen yhteydessä (vain tarkistus — ei automaattista lataamista)."
"Show in menu bar": "Näytä valikkorivillä"
"Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Pidä OpenLogin kuvake valikkorivillä. Kun pois päältä, se pysyy Dockissa."
"Show in the notification area": "Näytä ilmoitusalueella"
"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Pitää OpenLogin kuvakkeen tehtäväpalkin ilmoitusalueella. Tulee voimaan, kun taustaprosessi käynnistyy seuraavan kerran."
"Download from GitHub": "Lataa GitHubista"
"Assets": "Resurssit"
"Automatically download device images": "Lataa laitekuvat automaattisesti"
"Fetch device renders from assets.openlogi.org when a device connects. When off, OpenLogi makes no asset network requests; bundled art and the silhouette still show.": "Hae laitteiden kuvat osoitteesta assets.openlogi.org, kun laite yhdistetään. Kun tämä on pois päältä, OpenLogi ei tee lainkaan resurssien verkkopyyntöjä; mukana toimitetut kuvat ja siluetti näkyvät silti."
Expand Down
Loading
Loading