diff --git a/Cargo.lock b/Cargo.lock index 9b1b68fc..6f1cf65f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4837,6 +4837,7 @@ dependencies = [ name = "openlogi-agent" version = "0.6.19" dependencies = [ + "embed-resource", "futures", "interprocess", "objc2", @@ -4923,6 +4924,7 @@ dependencies = [ "anyhow", "backon", "disclaim", + "embed-resource", "fluent-langneg", "gpui", "gpui-component", diff --git a/README.md b/README.md index 8d293dbf..be53455c 100644 --- a/README.md +++ b/README.md @@ -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+ @@ -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. @@ -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 diff --git a/crates/openlogi-agent/Cargo.toml b/crates/openlogi-agent/Cargo.toml index c5d4387b..82ac4729 100644 --- a/crates/openlogi-agent/Cargo.toml +++ b/crates/openlogi-agent/Cargo.toml @@ -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-owned AppKit bindings (no #99-style leaks); versions match # the GUI's so the resolve doesn't move the gpui Cargo.lock pin. diff --git a/crates/openlogi-agent/build.rs b/crates/openlogi-agent/build.rs new file mode 100644 index 00000000..32bde880 --- /dev/null +++ b/crates/openlogi-agent/build.rs @@ -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"); +} diff --git a/crates/openlogi-gui/Cargo.toml b/crates/openlogi-gui/Cargo.toml index 0c6575f6..e89fc8b4 100644 --- a/crates/openlogi-gui/Cargo.toml +++ b/crates/openlogi-gui/Cargo.toml @@ -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`-owned AppKit bindings so the status-item code can't leak (#99). [target.'cfg(target_os = "macos")'.dependencies] diff --git a/crates/openlogi-gui/build.rs b/crates/openlogi-gui/build.rs index 1479b669..c3e7a1c9 100644 --- a/crates/openlogi-gui/build.rs +++ b/crates/openlogi-gui/build.rs @@ -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 @@ -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"); @@ -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 { diff --git a/crates/openlogi-gui/locales/da.yml b/crates/openlogi-gui/locales/da.yml index 2e2f041f..872b8c83 100644 --- a/crates/openlogi-gui/locales/da.yml +++ b/crates/openlogi-gui/locales/da.yml @@ -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." diff --git a/crates/openlogi-gui/locales/de.yml b/crates/openlogi-gui/locales/de.yml index 99480ebe..16febd4f 100644 --- a/crates/openlogi-gui/locales/de.yml +++ b/crates/openlogi-gui/locales/de.yml @@ -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." diff --git a/crates/openlogi-gui/locales/el.yml b/crates/openlogi-gui/locales/el.yml index 3b96ae1c..82b1d95c 100644 --- a/crates/openlogi-gui/locales/el.yml +++ b/crates/openlogi-gui/locales/el.yml @@ -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 δεν πραγματοποιεί αιτήματα δικτύου για πόρους· οι ενσωματωμένες εικόνες και η σιλουέτα εξακολουθούν να εμφανίζονται." diff --git a/crates/openlogi-gui/locales/en.yml b/crates/openlogi-gui/locales/en.yml index 7b567aa7..5124f43a 100644 --- a/crates/openlogi-gui/locales/en.yml +++ b/crates/openlogi-gui/locales/en.yml @@ -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." diff --git a/crates/openlogi-gui/locales/es.yml b/crates/openlogi-gui/locales/es.yml index 9374362f..7365d9cc 100644 --- a/crates/openlogi-gui/locales/es.yml +++ b/crates/openlogi-gui/locales/es.yml @@ -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." diff --git a/crates/openlogi-gui/locales/fi.yml b/crates/openlogi-gui/locales/fi.yml index 44fe3b8c..7b90c6f6 100644 --- a/crates/openlogi-gui/locales/fi.yml +++ b/crates/openlogi-gui/locales/fi.yml @@ -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." diff --git a/crates/openlogi-gui/locales/fr.yml b/crates/openlogi-gui/locales/fr.yml index 1317cd65..50fbb7b3 100644 --- a/crates/openlogi-gui/locales/fr.yml +++ b/crates/openlogi-gui/locales/fr.yml @@ -106,6 +106,9 @@ _version: 1 "Check once per launch for a new version (query only — no automatic download).": "Vérifier une nouvelle version une fois par lancement (vérification seulement — aucun téléchargement automatique)." "Show in menu bar": "Afficher dans la barre des menus" "Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Conserver l'icône d'OpenLogi dans la barre des menus. Si désactivé, elle reste dans le Dock." +"Show in the notification area": "Afficher dans la zone de notification" +"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Garde l'icône d'OpenLogi dans la zone de notification de la barre des tâches. Prend effet au prochain démarrage de l'agent en arrière-plan." +"Download from GitHub": "Télécharger depuis GitHub" "Assets": "Ressources" "Automatically download device images": "Télécharger automatiquement les images des appareils" "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.": "Récupérer les images des appareils depuis assets.openlogi.org lorsqu'un appareil se connecte. Si désactivé, OpenLogi n'effectue aucune requête réseau pour les ressources ; les images intégrées et la silhouette restent affichées." diff --git a/crates/openlogi-gui/locales/it.yml b/crates/openlogi-gui/locales/it.yml index 08b290b7..05a67c49 100644 --- a/crates/openlogi-gui/locales/it.yml +++ b/crates/openlogi-gui/locales/it.yml @@ -106,6 +106,9 @@ _version: 1 "Check once per launch for a new version (query only — no automatic download).": "Controlla la presenza di una nuova versione ad ogni avvio (solo verifica — nessun download automatico)." "Show in menu bar": "Mostra nella barra dei menu" "Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Mantieni l'icona di OpenLogi nella barra dei menu. Quando è disattivata, rimarrà invece nel Dock." +"Show in the notification area": "Mostra nell'area di notifica" +"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Mantiene l'icona di OpenLogi nell'area di notifica della barra delle applicazioni. Ha effetto al prossimo avvio dell'agente in background." +"Download from GitHub": "Scarica da GitHub" "Assets": "Risorse" "Automatically download device images": "Scarica automaticamente le immagini dei dispositivi" "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.": "Recupera le immagini dei dispositivi da assets.openlogi.org quando un dispositivo si connette. Se disattivato, OpenLogi non effettua alcuna richiesta di rete per le risorse; le immagini incluse e la sagoma vengono comunque mostrate." diff --git a/crates/openlogi-gui/locales/ja.yml b/crates/openlogi-gui/locales/ja.yml index 61c9d9d0..04d59d87 100644 --- a/crates/openlogi-gui/locales/ja.yml +++ b/crates/openlogi-gui/locales/ja.yml @@ -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 はアセットのネットワーク要求を一切行わず、バンドルされた画像とシルエットのみが表示されます。" diff --git a/crates/openlogi-gui/locales/ko.yml b/crates/openlogi-gui/locales/ko.yml index c4504c0e..ed0d8cb9 100644 --- a/crates/openlogi-gui/locales/ko.yml +++ b/crates/openlogi-gui/locales/ko.yml @@ -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는 리소스 네트워크 요청을 전혀 하지 않으며, 내장 이미지와 실루엣은 계속 표시됩니다." diff --git a/crates/openlogi-gui/locales/nb.yml b/crates/openlogi-gui/locales/nb.yml index 2f0e55c4..9a2d65b9 100644 --- a/crates/openlogi-gui/locales/nb.yml +++ b/crates/openlogi-gui/locales/nb.yml @@ -106,6 +106,9 @@ _version: 1 "Check once per launch for a new version (query only — no automatic download).": "Sjekk én gang per oppstart om det finnes en ny versjon (kun forespørsel — ingen automatisk nedlasting)." "Show in menu bar": "Vis i menylinjen" "Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Behold OpenLogi-ikonet i menylinjen. Når av forblir det i Dock-en i stedet." +"Show in the notification area": "Vis i varslingsområdet" +"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Beholder OpenLogi-ikonet i varslingsområdet på oppgavelinjen. Trer i kraft neste gang bakgrunnsagenten starter." +"Download from GitHub": "Last ned fra GitHub" "Assets": "Ressurser" "Automatically download device images": "Last ned enhetsbilder 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 enhetsbilder fra assets.openlogi.org når en enhet kobles til. Når dette er av, sender OpenLogi ingen nettverksforespørsler etter ressurser; medfølgende bilder og silhuetten vises fortsatt." diff --git a/crates/openlogi-gui/locales/nl.yml b/crates/openlogi-gui/locales/nl.yml index 51f32483..95c25a83 100644 --- a/crates/openlogi-gui/locales/nl.yml +++ b/crates/openlogi-gui/locales/nl.yml @@ -106,6 +106,9 @@ _version: 1 "Check once per launch for a new version (query only — no automatic download).": "Controleer eenmaal per start op een nieuwe versie (alleen opvragen — geen automatische download)." "Show in menu bar": "Weergeven in menubalk" "Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Houd het OpenLogi-pictogram in de menubalk. Indien uit, blijft het in het Dock." +"Show in the notification area": "Weergeven in het systeemvak" +"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Houdt het OpenLogi-pictogram in het systeemvak van de taakbalk. Wordt van kracht wanneer de achtergrondagent opnieuw start." +"Download from GitHub": "Downloaden van GitHub" "Assets": "Bronnen" "Automatically download device images": "Apparaatafbeeldingen automatisch downloaden" "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.": "Haal apparaatafbeeldingen op van assets.openlogi.org wanneer een apparaat verbinding maakt. Indien uitgeschakeld doet OpenLogi geen enkel netwerkverzoek voor bronnen; meegeleverde afbeeldingen en het silhouet worden nog steeds getoond." diff --git a/crates/openlogi-gui/locales/pl.yml b/crates/openlogi-gui/locales/pl.yml index fbec6b45..50bf6bed 100644 --- a/crates/openlogi-gui/locales/pl.yml +++ b/crates/openlogi-gui/locales/pl.yml @@ -106,6 +106,9 @@ _version: 1 "Check once per launch for a new version (query only — no automatic download).": "Sprawdzaj raz przy każdym uruchomieniu, czy jest nowa wersja (tylko zapytanie — bez automatycznego pobierania)." "Show in menu bar": "Pokaż na pasku menu" "Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Zachowaj ikonę OpenLogi na pasku menu. Po wyłączeniu pozostanie w Docku." +"Show in the notification area": "Pokaż w obszarze powiadomień" +"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Utrzymuje ikonę OpenLogi w obszarze powiadomień paska zadań. Zacznie obowiązywać przy następnym uruchomieniu agenta w tle." +"Download from GitHub": "Pobierz z GitHuba" "Assets": "Zasoby" "Automatically download device images": "Automatycznie pobieraj obrazy urządzeń" "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.": "Pobieraj obrazy urządzeń z assets.openlogi.org po podłączeniu urządzenia. Gdy wyłączone, OpenLogi nie wysyła żadnych żądań sieciowych po zasoby; dołączone obrazy i sylwetka nadal są wyświetlane." diff --git a/crates/openlogi-gui/locales/pt-BR.yml b/crates/openlogi-gui/locales/pt-BR.yml index 4508e5ae..0d301b30 100644 --- a/crates/openlogi-gui/locales/pt-BR.yml +++ b/crates/openlogi-gui/locales/pt-BR.yml @@ -106,6 +106,9 @@ _version: 1 "Check once per launch for a new version (query only — no automatic download).": "Verificar uma vez por inicialização se há uma nova versão (apenas consulta — sem download automático)." "Show in menu bar": "Mostrar na barra de menus" "Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Manter o ícone do OpenLogi na barra de menus. Quando desativado, permanece no Dock." +"Show in the notification area": "Mostrar na área de notificação" +"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Mantém o ícone do OpenLogi na área de notificação da barra de tarefas. Entra em vigor na próxima vez que o agente em segundo plano iniciar." +"Download from GitHub": "Baixar do GitHub" "Assets": "Recursos" "Automatically download device images": "Baixar imagens dos dispositivos automaticamente" "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.": "Buscar as imagens dos dispositivos em assets.openlogi.org quando um dispositivo se conecta. Quando desativado, o OpenLogi não faz nenhuma solicitação de rede de recursos; as imagens incluídas e a silhueta continuam sendo exibidas." diff --git a/crates/openlogi-gui/locales/pt-PT.yml b/crates/openlogi-gui/locales/pt-PT.yml index 0a13f08c..8083e2fb 100644 --- a/crates/openlogi-gui/locales/pt-PT.yml +++ b/crates/openlogi-gui/locales/pt-PT.yml @@ -106,6 +106,9 @@ _version: 1 "Check once per launch for a new version (query only — no automatic download).": "Procurar uma nova versão uma vez por arranque (apenas consulta — sem transferência automática)." "Show in menu bar": "Mostrar na barra de menus" "Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Manter o ícone do OpenLogi na barra de menus. Quando desativado, permanece no Dock." +"Show in the notification area": "Mostrar na área de notificação" +"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Mantém o ícone do OpenLogi na área de notificação da barra de tarefas. Produz efeito no próximo arranque do agente em segundo plano." +"Download from GitHub": "Transferir do GitHub" "Assets": "Recursos" "Automatically download device images": "Transferir imagens dos dispositivos automaticamente" "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.": "Obter as imagens dos dispositivos de assets.openlogi.org quando um dispositivo se liga. Quando desativado, o OpenLogi não faz quaisquer pedidos de rede de recursos; as imagens incluídas e a silhueta continuam a ser apresentadas." diff --git a/crates/openlogi-gui/locales/ru.yml b/crates/openlogi-gui/locales/ru.yml index 32e1be75..fc924df5 100644 --- a/crates/openlogi-gui/locales/ru.yml +++ b/crates/openlogi-gui/locales/ru.yml @@ -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 не делает сетевых запросов за ресурсами; по-прежнему показываются встроенные изображения и силуэт." diff --git a/crates/openlogi-gui/locales/sv.yml b/crates/openlogi-gui/locales/sv.yml index cc6cf27f..209c4b84 100644 --- a/crates/openlogi-gui/locales/sv.yml +++ b/crates/openlogi-gui/locales/sv.yml @@ -106,6 +106,9 @@ _version: 1 "Check once per launch for a new version (query only — no automatic download).": "Sök efter en ny version en gång per start (endast förfrågan — ingen automatisk hämtning)." "Show in menu bar": "Visa i menyraden" "Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.": "Behåll OpenLogis ikon i menyraden. När av stannar den i Dock i stället." +"Show in the notification area": "Visa i meddelandefältet" +"Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts.": "Behåller OpenLogis ikon i aktivitetsfältets meddelandefält. Träder i kraft nästa gång bakgrundsagenten startar." +"Download from GitHub": "Ladda ner från GitHub" "Assets": "Resurser" "Automatically download device images": "Hämta enhetsbilder automatiskt" "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.": "Hämta enhetsbilder från assets.openlogi.org när en enhet ansluts. När det är avstängt gör OpenLogi inga nätverksanrop efter resurser; medföljande bilder och siluetten visas ändå." diff --git a/crates/openlogi-gui/locales/zh-CN.yml b/crates/openlogi-gui/locales/zh-CN.yml index aa217b46..077e72a1 100644 --- a/crates/openlogi-gui/locales/zh-CN.yml +++ b/crates/openlogi-gui/locales/zh-CN.yml @@ -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 图标;关闭后改为停留在程序坞中。" +"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 不发起任何资源网络请求,仍会显示内置图片与剪影。" diff --git a/crates/openlogi-gui/locales/zh-HK.yml b/crates/openlogi-gui/locales/zh-HK.yml index e11ad21a..f4e7ef66 100644 --- a/crates/openlogi-gui/locales/zh-HK.yml +++ b/crates/openlogi-gui/locales/zh-HK.yml @@ -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 不會發出任何資源網路請求,仍會顯示內建圖片與剪影。" diff --git a/crates/openlogi-gui/locales/zh-TW.yml b/crates/openlogi-gui/locales/zh-TW.yml index 5d51239b..f8a84ea2 100644 --- a/crates/openlogi-gui/locales/zh-TW.yml +++ b/crates/openlogi-gui/locales/zh-TW.yml @@ -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 不會發出任何資源網路請求,仍會顯示內建圖片與剪影。" diff --git a/crates/openlogi-gui/src/asset/mod.rs b/crates/openlogi-gui/src/asset/mod.rs index 633468c6..d801d565 100644 --- a/crates/openlogi-gui/src/asset/mod.rs +++ b/crates/openlogi-gui/src/asset/mod.rs @@ -91,19 +91,15 @@ pub fn reveal_cache_in_file_manager() { open_in_file_manager(&root); } -/// Open `path` in the platform file manager. Only macOS has a reveal command -/// wired up today; elsewhere this is a no-op. (Split out so the early return -/// above isn't the function's last statement on non-macOS — `needless_return`.) -#[cfg(target_os = "macos")] +/// Open `path` in the platform file manager. `opener` dispatches per OS +/// (Finder / Explorer / xdg-open), so no `#[cfg]` split — the old macOS-only +/// gating left the Settings → Assets "Open" button silently dead elsewhere. fn open_in_file_manager(path: &Path) { if let Err(e) = opener::open(path) { - warn!(error = %e, "could not open cache dir in Finder"); + warn!(error = %e, "could not open cache dir in the file manager"); } } -#[cfg(not(target_os = "macos"))] -fn open_in_file_manager(_path: &Path) {} - #[derive(Debug, Clone)] pub struct ResolvedAsset { #[allow( diff --git a/crates/openlogi-gui/src/platform/updater.rs b/crates/openlogi-gui/src/platform/updater.rs index 6a1f0375..e38442b2 100644 --- a/crates/openlogi-gui/src/platform/updater.rs +++ b/crates/openlogi-gui/src/platform/updater.rs @@ -74,6 +74,15 @@ fn minisign_public_key() -> Option<&'static str> { .filter(|key| !key.is_empty()) } +/// Whether this platform's install flow is wired end to end. gpui-updater's +/// Windows strategy is rename-in-place for a bare `.exe`; handing it the MSI +/// the manifest serves would clobber `OpenLogi.exe` with installer bytes. +/// Until an msiexec flow lands upstream, Windows checks are notify-only: a +/// check still resolves and surfaces "update available", but nothing +/// downloads or installs — the Updates page routes the user to the release +/// instead. +pub const INSTALL_SUPPORTED: bool = !cfg!(target_os = "windows"); + fn release_arch() -> &'static str { match std::env::consts::ARCH { "aarch64" => "arm64", @@ -84,7 +93,11 @@ fn release_arch() -> &'static str { fn release_format() -> &'static str { match std::env::consts::OS { "macos" => "dmg", - "windows" => "exe", + // Matches the `format` field `xtask release latest-json` emits for the + // per-arch MSIs. No bare exe ships anymore, so "exe" could never + // match; with [`INSTALL_SUPPORTED`] false the format only has to let a + // *check* resolve. + "windows" => "msi", _ => "tar.gz", } } @@ -100,9 +113,10 @@ pub fn install(cx: &mut App, settings: &AppSettings) { // later manual check — is honoured. Installed unconditionally; it's inert // until both the flag is on and a check resolves to `Available`. let auto_install = cx.observe(&updater, |updater, cx| { - let opted_in = cx - .try_global::() - .is_some_and(|s| s.app_settings().auto_install_updates); + let opted_in = INSTALL_SUPPORTED + && cx + .try_global::() + .is_some_and(|s| s.app_settings().auto_install_updates); if opted_in && matches!(updater.read(cx).status(), UpdateStatus::Available(_)) { updater.update(cx, Updater::download_and_install); } diff --git a/crates/openlogi-gui/src/windows/mod.rs b/crates/openlogi-gui/src/windows/mod.rs index 9d6053cb..ea6f4a60 100644 --- a/crates/openlogi-gui/src/windows/mod.rs +++ b/crates/openlogi-gui/src/windows/mod.rs @@ -72,6 +72,10 @@ pub fn open_or_focus( let bounds = Bounds::centered(None, size, cx); let options = WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), + // Aux windows are fixed-content dialogs authored for `size`; that + // makes it the floor too, the way the main window sets one in + // `main_window_options` — below it rows clip their trailing controls. + window_min_size: Some(size), app_id: Some("openlogi".to_string()), titlebar: Some(TitlebarOptions { title: Some(title.clone()), diff --git a/crates/openlogi-gui/src/windows/settings.rs b/crates/openlogi-gui/src/windows/settings.rs index 37b42964..bfcc3114 100644 --- a/crates/openlogi-gui/src/windows/settings.rs +++ b/crates/openlogi-gui/src/windows/settings.rs @@ -54,6 +54,10 @@ mod appearance; mod assets; mod general; mod language; +// Windows needs no privacy grants — the WH_MOUSE_LL hook and raw HID access +// work without one — so there the page would render empty; register it only +// where it has content. `SettingsPage::index` tracks the shift. +#[cfg(any(target_os = "macos", target_os = "linux"))] mod permissions; mod updates; @@ -73,7 +77,15 @@ impl SettingsPage { match self { Self::General => 0, Self::Updates => 1, - Self::About => 5, + // One lower on Windows: the Permissions page isn't registered + // there (see the `mod permissions` cfg). + Self::About => { + if cfg!(any(target_os = "macos", target_os = "linux")) { + 5 + } else { + 4 + } + } } } } @@ -248,7 +260,10 @@ pub fn open_at(page: SettingsPage, cx: &mut App) { windows::open_or_focus( |reg| &mut reg.settings, "Settings", - Size::new(px(840.), px(600.)), + // Wide enough that the pages' custom rows keep slack under fonts wider + // than the macOS system font (Segoe UI tipped the old 840 into + // clipping the hero rows' trailing buttons on Windows). + Size::new(px(920.), px(640.)), move |window, cx| SettingsView::new(page, window, cx), cx, ); @@ -271,25 +286,35 @@ impl Render for SettingsView { // Outline group boxes give every page bordered cards (depth / // definition that the flat Fill variant lacked); the hero / // source / config blocks are custom rows inside them. - Settings::new("settings") - .with_group_variant(GroupBoxVariant::Outline) - .sidebar_width(px(210.)) - .default_selected_index(SelectIndex { - page_ix: self.initial_page.index(), - group_ix: None, - }) - .page(general::general_page(self.sensitivity_slider.clone())) - .page(updates::updates_page(self.updater.clone(), pal)) - .page(permissions::permissions_page(pal)) - .page(appearance::appearance_page( - view.clone(), - self.theme_filter, - self.theme_search.clone(), - self.language_select.clone(), - pal, - )) - .page(assets::assets_page(pal, self.asset_cache_desc.clone())) - .page(about::about_page(view, self.copied, pal)), + { + let settings = Settings::new("settings") + .with_group_variant(GroupBoxVariant::Outline) + .sidebar_width(px(210.)) + .default_selected_index(SelectIndex { + page_ix: self.initial_page.index(), + group_ix: None, + }) + .page(general::general_page(self.sensitivity_slider.clone())) + .page(updates::updates_page(self.updater.clone(), pal)); + // Registered only where grants exist to manage — see the + // `mod permissions` cfg for why Windows skips it. + #[cfg(any(target_os = "macos", target_os = "linux"))] + let settings = settings.page(permissions::permissions_page(pal)); + settings + .page(appearance::appearance_page( + view.clone(), + self.theme_filter, + self.theme_search.clone(), + self.language_select.clone(), + pal, + )) + .page(assets::assets_page( + view.clone(), + pal, + self.asset_cache_desc.clone(), + )) + .page(about::about_page(view, self.copied, pal)) + }, ) } } diff --git a/crates/openlogi-gui/src/windows/settings/about.rs b/crates/openlogi-gui/src/windows/settings/about.rs index c621a2d4..aeed617e 100644 --- a/crates/openlogi-gui/src/windows/settings/about.rs +++ b/crates/openlogi-gui/src/windows/settings/about.rs @@ -148,22 +148,35 @@ fn about_config(pal: Palette) -> AnyElement { .justify_between() .gap_3() .child( + // The absolute path can be arbitrarily long (deep home dirs, + // Windows profiles) — ellipsize it rather than letting it shove + // the reveal button past the window edge. v_flex() .gap_1() + .flex_1() + .min_w_0() .child(div().font_weight(FontWeight::MEDIUM).child("config.toml")) - .child(div().text_xs().text_color(pal.text_muted).child(path)), + .child( + div() + .text_xs() + .text_color(pal.text_muted) + .truncate() + .child(path), + ), ) .child( - Button::new("about-reveal-config") - .outline() - .label(tr!("Show in file manager")) - .on_click(|_, _, cx| { - if let Ok(dir) = openlogi_core::paths::config_dir() - && let Ok(url) = url::Url::from_file_path(&dir) - { - cx.open_url(url.as_str()); - } - }), + div().flex_shrink_0().child( + Button::new("about-reveal-config") + .outline() + .label(tr!("Show in file manager")) + .on_click(|_, _, cx| { + if let Ok(dir) = openlogi_core::paths::config_dir() + && let Ok(url) = url::Url::from_file_path(&dir) + { + cx.open_url(url.as_str()); + } + }), + ), ) .into_any_element() } diff --git a/crates/openlogi-gui/src/windows/settings/appearance.rs b/crates/openlogi-gui/src/windows/settings/appearance.rs index a4d5766b..9e26162a 100644 --- a/crates/openlogi-gui/src/windows/settings/appearance.rs +++ b/crates/openlogi-gui/src/windows/settings/appearance.rs @@ -368,8 +368,14 @@ fn theme_picker( .justify_between() .gap_3() .child( + // Translated chip labels vary in width; let the chip row + // yield (wrapping onto a second line if it must) so the + // fixed-width search input is never pushed out of view. h_flex() .gap_2() + .flex_1() + .min_w_0() + .flex_wrap() .child(filter_chip( view, "filter-all", @@ -396,7 +402,7 @@ fn theme_picker( )), ) .child( - div().w(px(200.)).child( + div().w(px(200.)).flex_shrink_0().child( Input::new(theme_search) .small() .cleanable(true) diff --git a/crates/openlogi-gui/src/windows/settings/assets.rs b/crates/openlogi-gui/src/windows/settings/assets.rs index 8df1866c..e62cdd13 100644 --- a/crates/openlogi-gui/src/windows/settings/assets.rs +++ b/crates/openlogi-gui/src/windows/settings/assets.rs @@ -1,12 +1,19 @@ //! Assets (device-image cache) settings page. +use std::time::Duration; + use super::{ - App, AppState, AssetCommand, AssetControl, BorrowAppContext, IconName, InteractiveElement, - IntoElement, Palette, ParentElement, SettingField, SettingGroup, SettingItem, SettingPage, - SharedString, StatefulInteractiveElement, Styled, div, + App, AppState, AssetCommand, AssetControl, BorrowAppContext, Entity, IconName, + InteractiveElement, IntoElement, Palette, ParentElement, SettingField, SettingGroup, + SettingItem, SettingPage, SettingsView, SharedString, StatefulInteractiveElement, Styled, div, }; -pub(super) fn assets_page(pal: Palette, cache_desc: SharedString) -> SettingPage { +pub(super) fn assets_page( + view: Entity, + pal: Palette, + cache_desc: SharedString, +) -> SettingPage { + let refresh_view = view.clone(); let group = SettingGroup::new() .item( SettingItem::new( @@ -37,8 +44,14 @@ pub(super) fn assets_page(pal: Palette, cache_desc: SharedString) -> SettingPage SettingItem::new( tr!("Refresh assets"), SettingField::render(move |_, _, _| { - action_button("assets-refresh", tr!("Refresh"), pal, |cx| { + let view = refresh_view.clone(); + action_button("assets-refresh", tr!("Refresh"), pal, move |cx| { send_asset_command(cx, AssetCommand::Refresh); + // Give the spawned sync a moment to land small fetches, + // then re-quote the size row so the click visibly did + // something. Best-effort — a longer sync is caught by + // the next action or window reopen. + refresh_cache_desc_after(&view, Duration::from_secs(2), cx); }) }), ) @@ -48,9 +61,15 @@ pub(super) fn assets_page(pal: Palette, cache_desc: SharedString) -> SettingPage SettingItem::new( tr!("Clear cache"), SettingField::render(move |_, _, _| { - action_button("assets-clear", tr!("Clear"), pal, |cx| { + let view = view.clone(); + action_button("assets-clear", tr!("Clear"), pal, move |cx| { send_asset_command(cx, AssetCommand::ClearCache); cx.refresh_windows(); + // The wipe runs on the main loop's channel arm, not + // synchronously here — without a recompute the row + // keeps quoting the pre-Clear size until the window + // reopens, which reads as the button doing nothing. + refresh_cache_desc_after(&view, Duration::from_millis(750), cx); }) }), ) @@ -74,6 +93,24 @@ pub(super) fn assets_page(pal: Palette, cache_desc: SharedString) -> SettingPage .group(group) } +/// Re-walk the cache and swap the size blurb into the view after `delay`. The +/// manual actions run on the main loop's channel arm, not synchronously in the +/// click handler, so an immediate recompute would race the wipe/fetch. +fn refresh_cache_desc_after(view: &Entity, delay: Duration, cx: &mut App) { + // Weak: the window can close before the timer fires; a strong handle + // would keep the dead view alive just to update it. + let view = view.downgrade(); + cx.spawn(async move |cx| { + cx.background_executor().timer(delay).await; + view.update(cx, |this, cx| { + this.asset_cache_desc = cache_size_description(); + cx.notify(); + }) + .ok(); + }) + .detach(); +} + /// Human-readable size of the on-disk asset cache, for the "Clear cache" row. /// Computed once when the Settings window opens (`asset_cache_desc`), not per /// render. diff --git a/crates/openlogi-gui/src/windows/settings/general.rs b/crates/openlogi-gui/src/windows/settings/general.rs index 7b6adc78..3d2094d3 100644 --- a/crates/openlogi-gui/src/windows/settings/general.rs +++ b/crates/openlogi-gui/src/windows/settings/general.rs @@ -40,10 +40,18 @@ pub(super) fn general_page(sensitivity_slider: Entity) -> SettingPa )), ); - #[cfg(target_os = "macos")] + // The same `show_in_menu_bar` setting drives the macOS status item and + // the Windows notification-area icon (the agent honors it on both; next + // launch, see tray.rs / tray_windows.rs) — so both platforms get the + // switch, with platform-fitting wording. Linux has no tray; no switch. + #[cfg(any(target_os = "macos", target_os = "windows"))] let group = group.item( SettingItem::new( - tr!("Show in menu bar"), + if cfg!(target_os = "macos") { + tr!("Show in menu bar") + } else { + tr!("Show in the notification area") + }, SettingField::switch( |cx| { cx.try_global::() @@ -57,9 +65,13 @@ pub(super) fn general_page(sensitivity_slider: Entity) -> SettingPa }, ), ) - .description(tr!( - "Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead." - )), + .description(if cfg!(target_os = "macos") { + tr!("Keep OpenLogi's icon in the menu bar. When off, it stays in the Dock instead.") + } else { + tr!( + "Keep OpenLogi's icon in the taskbar notification area. Takes effect the next time the background agent starts." + ) + }), ); SettingPage::new(tr!("General")) diff --git a/crates/openlogi-gui/src/windows/settings/updates.rs b/crates/openlogi-gui/src/windows/settings/updates.rs index 4318aa8a..e8ce1683 100644 --- a/crates/openlogi-gui/src/windows/settings/updates.rs +++ b/crates/openlogi-gui/src/windows/settings/updates.rs @@ -15,28 +15,31 @@ pub(super) fn updates_page(updater: Entity, pal: Palette) -> SettingPag update_hero(&updater, pal, cx) })); - let toggles = SettingGroup::new() - .item( - SettingItem::new( - tr!("Check for updates"), - SettingField::switch( - |cx| { - cx.try_global::() - .is_some_and(|s| s.app_settings().check_for_updates) - }, - |enabled, cx| { - cx.update_global::(move |s, _| { - s.set_check_for_updates(enabled); - }); - cx.refresh_windows(); - }, - ), - ) - .description(tr!( - "Check once per launch for a new version (query only — no automatic download)." - )), + let toggles = SettingGroup::new().item( + SettingItem::new( + tr!("Check for updates"), + SettingField::switch( + |cx| { + cx.try_global::() + .is_some_and(|s| s.app_settings().check_for_updates) + }, + |enabled, cx| { + cx.update_global::(move |s, _| { + s.set_check_for_updates(enabled); + }); + cx.refresh_windows(); + }, + ), ) - .item( + .description(tr!( + "Check once per launch for a new version (query only — no automatic download)." + )), + ); + // Offering the auto-install switch on a platform whose install flow isn't + // wired (Windows, today) would be a control that silently does nothing — + // hide it instead; checks there are notify-only. + let toggles = if crate::platform::updater::INSTALL_SUPPORTED { + toggles.item( SettingItem::new( tr!("Automatically download and install"), SettingField::switch( @@ -55,7 +58,10 @@ pub(super) fn updates_page(updater: Entity, pal: Palette) -> SettingPag .description(tr!( "Download updates in the background and apply them the next time OpenLogi restarts." )), - ); + ) + } else { + toggles + }; let source = SettingGroup::new().item(SettingItem::render(move |_, _, _| update_source(pal))); @@ -108,6 +114,15 @@ fn update_hero(updater: &Entity, pal: Palette, cx: &mut App) -> AnyElem let action = { let u = updater.clone(); match &status { + // No wired install flow (Windows): a found update routes to the + // GitHub release for a manual download instead of feeding + // gpui-updater an artifact its installer can't apply. + UpdateStatus::Available(_) if !crate::platform::updater::INSTALL_SUPPORTED => { + Button::new("update-download") + .outline() + .label(tr!("Download from GitHub")) + .on_click(|_, _, cx| cx.open_url(RELEASES_URL)) + } UpdateStatus::Available(_) => Button::new("update-install") .outline() .label(tr!("Download & Install")) @@ -135,13 +150,21 @@ fn update_hero(updater: &Entity, pal: Palette, cx: &mut App) -> AnyElem .justify_between() .gap_4() .child( + // The left block yields and ellipsizes; the action button never + // shrinks — mirrors the library's own SettingItem rows, which + // otherwise protect themselves the same way. Without this a long + // status line (or a wide UI font) shoves the button past the + // window edge. h_flex() .items_center() .gap_3() + .flex_1() + .min_w_0() .child(img(crate::app_assets::LOGO).w(px(52.)).h(px(52.))) .child( v_flex() .gap_1() + .min_w_0() .child( h_flex() .items_center() @@ -157,11 +180,12 @@ fn update_hero(updater: &Entity, pal: Palette, cx: &mut App) -> AnyElem div() .text_xs() .text_color(pal.text_muted) + .truncate() .child(message.unwrap_or_else(|| tr!("Stable channel"))), ), ), ) - .child(action.disabled(busy)) + .child(div().flex_shrink_0().child(action.disabled(busy))) .into_any_element() } @@ -177,8 +201,12 @@ fn update_source(pal: Palette) -> AnyElement { .justify_between() .gap_3() .child( + // Shrink-safe like the hero row above: the text yields, + // the button stays whole. v_flex() .gap_1() + .flex_1() + .min_w_0() .child( div() .font_weight(FontWeight::MEDIUM) @@ -188,15 +216,18 @@ fn update_source(pal: Palette) -> AnyElement { div() .text_xs() .text_color(pal.text_muted) + .truncate() .child("github.com/AprilNEA/OpenLogi/releases"), ), ) .child( - Button::new("update-changelog") - .ghost() - .icon(IconName::ExternalLink) - .label(tr!("View changelog")) - .on_click(|_, _, cx| cx.open_url(RELEASES_URL)), + div().flex_shrink_0().child( + Button::new("update-changelog") + .ghost() + .icon(IconName::ExternalLink) + .label(tr!("View changelog")) + .on_click(|_, _, cx| cx.open_url(RELEASES_URL)), + ), ), ) .child( diff --git a/crates/openlogi-hook/src/tests.rs b/crates/openlogi-hook/src/tests.rs index fe6efa6a..faa8b925 100644 --- a/crates/openlogi-hook/src/tests.rs +++ b/crates/openlogi-hook/src/tests.rs @@ -53,23 +53,27 @@ fn event_disposition_equality() { assert_ne!(EventDisposition::PassThrough, EventDisposition::Suppress); } -/// On unsupported targets (not macOS, not Linux), `Hook::start` returns `Unsupported`. -#[cfg(not(any(target_os = "macos", target_os = "linux")))] +/// On unsupported targets (not macOS, Linux, or Windows), `Hook::start` +/// returns `Unsupported`. The cfg predates the Windows port (#167) — Windows +/// belongs with the supported targets below, where this stale form made +/// `cargo test` fail on every real Windows box. +#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] #[test] fn unsupported_start_returns_unsupported() { let result = Hook::start(|_| EventDisposition::PassThrough); assert!(matches!(result, Err(HookError::Unsupported))); } -/// On Linux, `Hook::start` never returns `Unsupported` — it either succeeds or -/// returns a Linux-specific error (e.g. `NoDeviceFound` in a headless CI env). -#[cfg(target_os = "linux")] +/// On Linux and Windows, `Hook::start` never returns `Unsupported` — it either +/// succeeds (`WH_MOUSE_LL` needs no grant on Windows) or returns a +/// platform-specific error (e.g. `NoDeviceFound` in a headless Linux CI env). +#[cfg(any(target_os = "linux", target_os = "windows"))] #[test] -fn linux_start_does_not_return_unsupported() { +fn supported_start_does_not_return_unsupported() { let result = Hook::start(|_| EventDisposition::PassThrough); assert!( !matches!(result, Err(HookError::Unsupported)), - "Hook::start returned Unsupported on Linux" + "Hook::start returned Unsupported on a supported platform" ); // Clean up if a hook was actually installed. if let Ok(hook) = result { diff --git a/design/icon/openlogi.ico b/design/icon/openlogi.ico new file mode 100644 index 00000000..a604ce21 Binary files /dev/null and b/design/icon/openlogi.ico differ diff --git a/packaging/windows/OpenLogi.wxs b/packaging/windows/OpenLogi.wxs index f5286f65..4f20150e 100644 --- a/packaging/windows/OpenLogi.wxs +++ b/packaging/windows/OpenLogi.wxs @@ -12,6 +12,9 @@ Version numeric x.y.z ProductVersion (0.0.0 on non-tag dispatches) ExeFile path to the signed openlogi-gui exe to package AgentExeFile path to the signed openlogi-agent exe to package + IconFile optional override for the app .ico (Add/Remove Programs + entry); defaults to the committed design/icon/openlogi.ico + below, so the release workflow needs no extra define - Requires WixToolset.Util.wixext (util:CloseApplication below); the windows-msi job installs it pinned to the same version as wix itself. @@ -28,6 +31,12 @@ + + + + + + + + diff --git a/xtask/src/commands/release/latest_json.rs b/xtask/src/commands/release/latest_json.rs index 31b67031..7fb4dad4 100644 --- a/xtask/src/commands/release/latest_json.rs +++ b/xtask/src/commands/release/latest_json.rs @@ -9,7 +9,10 @@ use time::{OffsetDateTime, format_description::well_known::Rfc3339}; const APP_ID: &str = "org.openlogi.openlogi"; const CHANNEL: &str = "stable"; -const MINIMUM_OS_VERSION: &str = "13.0"; +const MACOS_MINIMUM_OS_VERSION: &str = "13.0"; +/// Windows 10+. Informational — the client updater doesn't gate on it today, +/// and everything that can run OpenLogi reports at least 10.0. +const WINDOWS_MINIMUM_OS_VERSION: &str = "10.0"; #[derive(Parser)] pub(crate) struct Args { @@ -25,6 +28,12 @@ pub(crate) struct Args { /// Public update base URL, for example `https://updates.openlogi.org`. #[arg(long, env = "OPENLOGI_UPDATE_BASE_URL")] base_url: String, + /// Also emit the per-arch Windows `.msi`/`.zip` entries. Off by default so + /// the manifest can never reference objects the release workflow's R2 + /// upload step doesn't ship: flip this in the same workflow change that + /// stops excluding the zip/msi from the `releases/` prefix (#347 PR 4). + #[arg(long)] + include_windows: bool, } #[derive(Serialize)] @@ -53,6 +62,17 @@ struct Asset { minimum_os_version: &'static str, } +/// The per-OS constants of an updater-relevant artifact, derived from its file +/// name. The Linux packages (`.deb`/`.rpm`) are deliberately absent: those +/// installs update through the distro package manager, not the in-app updater. +struct Classified { + os: &'static str, + arch: String, + format: &'static str, + content_type: &'static str, + minimum_os_version: &'static str, +} + pub(crate) fn run(args: &Args) -> Result<()> { let version = args.tag.strip_prefix('v').unwrap_or(&args.tag).to_string(); let release_base = format!( @@ -60,8 +80,11 @@ pub(crate) fn run(args: &Args) -> Result<()> { args.base_url.trim_end_matches('/'), args.tag ); - let assets = collect_assets(&args.dist, &release_base)?; - if assets.is_empty() { + let assets = collect_assets(&args.dist, &release_base, args.include_windows)?; + // The DMGs are the publish gate's guaranteed artifact set; the Windows + // legs are best-effort per arch (a failed leg publishes without them), so + // their absence must not sink the whole manifest. + if !assets.iter().any(|asset| asset.os == "macos") { bail!("no architecture-specific DMG assets found for manifest"); } @@ -92,21 +115,23 @@ pub(crate) fn run(args: &Args) -> Result<()> { .with_context(|| format!("could not write manifest to {}", args.output.display())) } -fn collect_assets(dist: &Path, release_base: &str) -> Result> { +fn collect_assets(dist: &Path, release_base: &str, include_windows: bool) -> Result> { let mut assets = Vec::new(); for entry in fs_err::read_dir(dist) .with_context(|| format!("could not read artifact directory {}", dist.display()))? { let path = entry?.path(); - if path.extension().and_then(|ext| ext.to_str()) != Some("dmg") { - continue; - } let Some(name) = path.file_name().and_then(|name| name.to_str()) else { continue; }; - let Some(arch) = dmg_arch(name) else { + let Some(classified) = classify(name) else { continue; }; + // Gated so the manifest and the R2 upload step can never disagree + // about the Windows artifacts — see the `include_windows` arg doc. + if classified.os == "windows" && !include_windows { + continue; + } let signature_name = format!("{name}.minisig"); let signature_path = dist.join(&signature_name); if !signature_path.is_file() { @@ -120,10 +145,10 @@ fn collect_assets(dist: &Path, release_base: &str) -> Result> { name: name.to_string(), url: format!("{release_base}/{name}"), signature_url: format!("{release_base}/{signature_name}"), - os: "macos", - arch: arch.to_string(), - format: "dmg", - content_type: "application/x-apple-diskimage", + os: classified.os, + arch: classified.arch, + format: classified.format, + content_type: classified.content_type, size: path .metadata() .with_context(|| format!("could not stat {}", path.display()))? @@ -131,17 +156,52 @@ fn collect_assets(dist: &Path, release_base: &str) -> Result> { sha256: path .sha256() .with_context(|| format!("could not hash artifact {}", path.display()))?, - minimum_os_version: MINIMUM_OS_VERSION, + minimum_os_version: classified.minimum_os_version, }); } assets.sort_by(|a, b| a.name.cmp(&b.name)); Ok(assets) } -fn dmg_arch(name: &str) -> Option<&str> { - let stem = name.strip_suffix(".dmg")?; - let (_, arch) = stem.rsplit_once("-macos-")?; - matches!(arch, "arm64" | "x86_64").then_some(arch) +/// Map an artifact file name onto its manifest constants; `None` for anything +/// the updater can't consume (SHA256SUMS, the Linux packages, the minisigs +/// themselves). +fn classify(name: &str) -> Option { + if let Some(stem) = name.strip_suffix(".dmg") { + return Some(Classified { + os: "macos", + arch: platform_arch(stem, "-macos-")?, + format: "dmg", + content_type: "application/x-apple-diskimage", + minimum_os_version: MACOS_MINIMUM_OS_VERSION, + }); + } + if let Some(stem) = name.strip_suffix(".msi") { + return Some(Classified { + os: "windows", + arch: platform_arch(stem, "-windows-")?, + format: "msi", + content_type: "application/x-msi", + minimum_os_version: WINDOWS_MINIMUM_OS_VERSION, + }); + } + if let Some(stem) = name.strip_suffix(".zip") { + return Some(Classified { + os: "windows", + arch: platform_arch(stem, "-windows-")?, + format: "zip", + content_type: "application/zip", + minimum_os_version: WINDOWS_MINIMUM_OS_VERSION, + }); + } + None +} + +/// The `arm64`/`x86_64` suffix after the `--` marker, or `None` when the +/// stem doesn't carry one (which also filters out non-artifact archives). +fn platform_arch(stem: &str, marker: &str) -> Option { + let (_, arch) = stem.rsplit_once(marker)?; + matches!(arch, "arm64" | "x86_64").then(|| arch.to_string()) } #[cfg(test)] @@ -154,7 +214,14 @@ mod tests { let dist = tempfile::tempdir().unwrap(); fs_err::write(dist.path().join("OpenLogi-v1.2.3-macos-arm64.dmg"), b"dmg").unwrap(); - assert!(collect_assets(dist.path(), "https://updates.example/releases/v1.2.3").is_err()); + assert!( + collect_assets( + dist.path(), + "https://updates.example/releases/v1.2.3", + false + ) + .is_err() + ); } #[test] @@ -167,12 +234,88 @@ mod tests { ) .unwrap(); - let assets = - collect_assets(dist.path(), "https://updates.example/releases/v1.2.3").unwrap(); + let assets = collect_assets( + dist.path(), + "https://updates.example/releases/v1.2.3", + false, + ) + .unwrap(); assert_eq!( assets[0].signature_url, "https://updates.example/releases/v1.2.3/OpenLogi-v1.2.3-macos-arm64.dmg.minisig" ); } + + #[test] + fn collect_assets_skips_windows_artifacts_unless_opted_in() { + // Off by default: the manifest must never reference Windows objects + // the release workflow's R2 upload step doesn't ship. + let dist = tempfile::tempdir().unwrap(); + for name in [ + "OpenLogi-v1.2.3-windows-x86_64.msi", + "OpenLogi-v1.2.3-windows-x86_64.zip", + ] { + fs_err::write(dist.path().join(name), b"artifact").unwrap(); + fs_err::write(dist.path().join(format!("{name}.minisig")), b"signature").unwrap(); + } + + let assets = collect_assets( + dist.path(), + "https://updates.example/releases/v1.2.3", + false, + ) + .unwrap(); + + assert!(assets.is_empty()); + } + + #[test] + fn collect_assets_includes_windows_msi_and_zip_per_arch() { + let dist = tempfile::tempdir().unwrap(); + for name in [ + "OpenLogi-v1.2.3-windows-x86_64.msi", + "OpenLogi-v1.2.3-windows-arm64.msi", + "OpenLogi-v1.2.3-windows-x86_64.zip", + ] { + fs_err::write(dist.path().join(name), b"artifact").unwrap(); + fs_err::write(dist.path().join(format!("{name}.minisig")), b"signature").unwrap(); + } + + let assets = + collect_assets(dist.path(), "https://updates.example/releases/v1.2.3", true).unwrap(); + + assert_eq!(assets.len(), 3); + assert!(assets.iter().all(|a| a.os == "windows")); + let msi = assets + .iter() + .find(|a| a.name.ends_with("x86_64.msi")) + .unwrap(); + assert_eq!((msi.arch.as_str(), msi.format), ("x86_64", "msi")); + let zip = assets + .iter() + .find(|a| a.name.ends_with("x86_64.zip")) + .unwrap(); + assert_eq!((zip.arch.as_str(), zip.format), ("x86_64", "zip")); + assert!(assets.iter().any(|a| a.arch == "arm64")); + } + + #[test] + fn collect_assets_skips_linux_packages_and_foreign_archives() { + let dist = tempfile::tempdir().unwrap(); + for name in [ + "openlogi-v1.2.3-linux-amd64.deb", + "openlogi-v1.2.3-linux-amd64.rpm", + "not-an-artifact.zip", + "SHA256SUMS", + ] { + fs_err::write(dist.path().join(name), b"artifact").unwrap(); + fs_err::write(dist.path().join(format!("{name}.minisig")), b"signature").unwrap(); + } + + let assets = + collect_assets(dist.path(), "https://updates.example/releases/v1.2.3", true).unwrap(); + + assert!(assets.is_empty()); + } }