Skip to content
Merged
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
5 changes: 3 additions & 2 deletions crates/openlogi-core/src/brand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ pub enum DeeplinkCommand {
Show,
/// Open the Settings window.
OpenSettings,
/// Open the About window.
/// Open Settings on the About page.
OpenAbout,
/// Run a manual update check (and show where its status is rendered).
/// Run a manual update check and open Settings on the Updates page, where
/// its status is rendered.
CheckForUpdates,
/// Quit the GUI.
Quit,
Expand Down
58 changes: 58 additions & 0 deletions crates/openlogi-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ impl Default for Config {
}
}

/// Light/dark appearance preference. `System` follows the OS appearance (the
/// historical behaviour); `Light` / `Dark` force a mode regardless of the OS.
/// Platform-free so the core crate stays GUI-agnostic — the GUI maps this onto
/// gpui-component's `ThemeMode`.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Appearance {
/// Follow the operating system's light/dark setting.
#[default]
System,
/// Always use the light variant of the selected theme.
Light,
/// Always use the dark variant of the selected theme.
Dark,
}

/// App-wide preferences not tied to any particular device.
///
/// All fields are `#[serde(default)]` so adding a new one is backward
Expand All @@ -85,6 +101,14 @@ pub struct AppSettings {
/// available — no automatic download.
#[serde(default)]
pub check_for_updates: bool,
/// Opt-in automatic install. When true *and* [`Self::check_for_updates`]
/// surfaces a newer version, the GUI downloads and stages it in the
/// background; the update is applied on the next restart (never mid-session,
/// and never auto-relaunched). **Off by default** — it only acts after a
/// check the user already opted into, and stays inert in unsigned dev builds
/// where verification fails closed.
#[serde(default)]
pub auto_install_updates: bool,
/// True once the first-run "check for updates?" prompt has been answered
/// (either way), so it is never shown again. The prompt is how a
/// privacy-conscious default of `check_for_updates = false` still lets a
Expand Down Expand Up @@ -119,6 +143,22 @@ pub struct AppSettings {
/// diverted from native scrolling once this leaves the default.
#[serde(default = "default_thumbwheel_sensitivity")]
pub thumbwheel_sensitivity: i32,
/// Light/dark appearance preference. Defaults to following the OS.
#[serde(default)]
pub appearance: Appearance,
/// Name of the theme used in light mode (a [`crate`]-agnostic string
/// matching a gpui-component theme, e.g. `"OpenLogi Light"`). `None` uses
/// the OpenLogi brand light theme.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theme_light: Option<String>,
/// Name of the theme used in dark mode. `None` uses the OpenLogi brand dark
/// theme.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theme_dark: Option<String>,
/// Corner-radius override for the UI, in pixels (the Appearance page offers
/// `0` / `6` / `12`). `None` keeps each theme's own radius.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ui_radius: Option<u8>,
}

/// Out-of-the-box [`AppSettings::thumbwheel_sensitivity`]. At this value the
Expand All @@ -144,11 +184,16 @@ impl Default for AppSettings {
Self {
launch_at_login: false,
check_for_updates: false,
auto_install_updates: false,
update_prompt_seen: false,
show_in_menu_bar: true,
auto_download_assets: true,
language: None,
thumbwheel_sensitivity: DEFAULT_THUMBWHEEL_SENSITIVITY,
appearance: Appearance::System,
theme_light: None,
theme_dark: None,
ui_radius: None,
}
}
}
Expand Down Expand Up @@ -841,6 +886,19 @@ impl Config {
.identity = Some(identity);
}

/// Whether `device_key` has a non-empty per-app binding overlay for the
/// foreground app `app` (bundle id). Drives the menu-bar popover's "override
/// active" badge — when the current app has its own bindings for this
/// device, the global bindings are (partly) overridden.
#[must_use]
pub fn has_app_override(&self, device_key: &str, app: &str) -> bool {
self.devices.get(device_key).is_some_and(|d| {
d.per_app_bindings
.get(app)
.is_some_and(|overlay| !overlay.is_empty())
})
}

/// Iterate every device we've recorded an identity for, as
/// `(config_key, identity)`. Used to seed offline placeholder cards so a
/// known device stays visible (with its panels) before any live probe.
Expand Down
2 changes: 1 addition & 1 deletion crates/openlogi-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ opener = "0.8.5"
# `Retained<T>`-owned AppKit bindings so the status-item code can't leak (#99).
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.4"
objc2-app-kit = { version = "0.3.2", features = ["NSStatusBar", "NSStatusItem", "NSStatusBarButton", "NSButton", "NSControl", "NSResponder", "NSView", "NSMenu", "NSMenuItem", "NSImage", "NSApplication"] }
objc2-app-kit = { version = "0.3.2", features = ["NSStatusBar", "NSStatusItem", "NSStatusBarButton", "NSButton", "NSControl", "NSResponder", "NSView", "NSMenu", "NSMenuItem", "NSImage", "NSApplication", "NSAppearance"] }
objc2-foundation = { version = "0.3.2", features = ["NSString", "NSProcessInfo"] }

# unsafe_code stays denied; the status-item/tray modules opt in locally with
Expand Down
23 changes: 23 additions & 0 deletions crates/openlogi-gui/action-icons/bug.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions crates/openlogi-gui/action-icons/scroll-text.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
135 changes: 135 additions & 0 deletions crates/openlogi-gui/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,138 @@
//! Build script for openlogi-gui.
//!
//! Besides the existing update-manifest env hook, this embeds the upstream
//! gpui-component themes *without* vendoring copies into this repo. Those theme
//! files live only in the gpui-component git checkout (the compiled crate
//! doesn't ship them), so we ask `cargo metadata` where gpui-component's source
//! actually resides — which is correct across the local git cache, CI, and
//! Nix's vendored `source-git` tree alike — and copy the wanted theme files
//! into `OUT_DIR`, emitting an include list that `theme.rs` pulls in.
//!
//! `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.

// 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::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};

/// Upstream theme file stems we embed (everything except our own openlogi.json,
/// which stays a committed, in-repo theme).
const UPSTREAM: &[&str] = &[
"ayu",
"catppuccin",
"tokyonight",
"gruvbox",
"solarized",
"everforest",
"flexoki",
"molokai",
"spaceduck",
"matrix",
"adventure",
"alduin",
"asciinema",
"fahrenheit",
"harper",
"hybrid",
"jellybeans",
"kibble",
"macos-classic",
"mellifluous",
"twilight",
];

fn main() {
println!("cargo:rerun-if-env-changed=OPENLOGI_UPDATE_MANIFEST_URL");
println!("cargo:rerun-if-env-changed=OPENLOGI_THEMES_DIR");

let out = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let src_dir = locate_themes_dir();
let dest = out.join("themes");
fs::create_dir_all(&dest).expect("create OUT_DIR/themes");

let mut generated = String::from(
"// @generated by build.rs — upstream gpui-component themes embedded at build time.\n\
static UPSTREAM_THEME_JSON: &[&str] = &[\n",
);
for stem in UPSTREAM {
let src = src_dir.join(format!("{stem}.json"));
let json = fs::read_to_string(&src)
.unwrap_or_else(|e| panic!("cannot read theme `{stem}` at {}: {e}", src.display()));
fs::write(dest.join(format!("{stem}.json")), json).expect("write theme into OUT_DIR");
writeln!(
generated,
" include_str!(concat!(env!(\"OUT_DIR\"), \"/themes/{stem}.json\")),"
)
.expect("writing to a String cannot fail");
println!("cargo:rerun-if-changed={}", src.display());
}
generated.push_str("];\n");
fs::write(out.join("builtin_themes.rs"), generated).expect("write builtin_themes.rs");
}

/// 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 {
if let Some(dir) = env::var_os("OPENLOGI_THEMES_DIR") {
return PathBuf::from(dir);
}

let metadata = cargo_metadata();
// The themes live at the repo root next to (not inside) the gpui-component
// crate, so walk up from its manifest until a populated `themes/` appears.
// `gpui-component-assets` shares the same checkout root, so either match
// converges on the same directory.
for manifest in manifest_paths(&metadata).filter(|p| p.contains("gpui-component")) {
for ancestor in Path::new(manifest).ancestors() {
let themes = ancestor.join("themes");
if themes.join("catppuccin.json").is_file() {
return themes;
}
}
}

panic!(
"could not locate the gpui-component themes dir via `cargo metadata`; \
set OPENLOGI_THEMES_DIR to the gpui-component `themes/` directory"
);
}

/// Run `cargo metadata` (locked, so it never mutates `Cargo.lock`) and return
/// its JSON stdout.
fn cargo_metadata() -> String {
println!("cargo:rerun-if-changed=../../Cargo.lock");
let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
let output = Command::new(cargo)
.args(["metadata", "--format-version=1", "--locked"])
.current_dir(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"))
.output()
.expect("run `cargo metadata`");
assert!(
output.status.success(),
"`cargo metadata` failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).expect("`cargo metadata` produced non-UTF-8 output")
}

/// Yield every `manifest_path` string value in the metadata JSON. Paths on the
/// platforms that build the GUI (macOS/Linux) contain no characters that JSON
/// would escape, so a direct scan is enough and avoids a serde build-dep.
fn manifest_paths(json: &str) -> impl Iterator<Item = &str> {
const KEY: &str = "\"manifest_path\":\"";
json.match_indices(KEY).filter_map(|(i, _)| {
let rest = &json[i + KEY.len()..];
rest.find('"').map(|end| &rest[..end])
})
}
34 changes: 34 additions & 0 deletions crates/openlogi-gui/locales/da.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,37 @@ _version: 1
"Scrolling": "Rulning"
"Invert scroll direction": "Vend rulleretning om"
"Reverse this mouse's scroll wheel. Your trackpad keeps the system scroll direction.": "Vend musens rullehjul om. Din pegeplade beholder systemets rulleretning."
"About": "Om"
"Updates": "Opdateringer"
"Off by default — checking for updates is OpenLogi's only optional outbound network request.": "Off by default — checking for updates is OpenLogi's only optional outbound network request."
"Up to date": "Up to date"
"Update available": "Update available"
"Update ready": "Update ready"
"Update failed": "Update failed"
"Automatically download and install": "Automatically download and install"
"Download updates in the background and apply them the next time OpenLogi restarts.": "Download updates in the background and apply them the next time OpenLogi restarts."
"Update source": "Update source"
"View changelog": "View changelog"
"No background updater — OpenLogi only connects when you turn on automatic checks or click Check for Updates.": "No background updater — OpenLogi only connects when you turn on automatic checks or click Check for Updates."
"Stable channel": "Stable channel"
"A native, local-first alternative to Logitech Options+.": "A native, local-first alternative to Logitech Options+."
"Changelog": "Changelog"
"Documentation": "Documentation"
"Report an issue": "Report an issue"
"Show in file manager": "Show in file manager"
"Not affiliated with Logitech. \"Logitech\", \"MX Master\", and \"Options+\" are trademarks of Logitech International S.A.": "Not affiliated with Logitech. \"Logitech\", \"MX Master\", and \"Options+\" are trademarks of Logitech International S.A."
"Appearance": "Appearance"
"Appearance mode": "Appearance mode"
"Light and dark use the matching theme; Follow system tracks the OS setting.": "Light and dark use the matching theme; Follow system tracks the OS setting."
"Light": "Light"
"Dark": "Dark"
"Theme": "Theme"
"Corner radius": "Corner radius"
"Roundness of buttons, cards, and controls.": "Roundness of buttons, cards, and controls."
"Sharp": "Sharp"
"Round": "Round"
"All": "All"
"Filter themes…": "Filter themes…"
"No themes match “%{query}”.": "No themes match “%{query}”."
"Color theme": "Color theme"
"Interface language": "Interface language"
34 changes: 34 additions & 0 deletions crates/openlogi-gui/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,37 @@ _version: 1
"Scrolling": "Scrollen"
"Invert scroll direction": "Scrollrichtung umkehren"
"Reverse this mouse's scroll wheel. Your trackpad keeps the system scroll direction.": "Kehrt das Scrollrad dieser Maus um. Dein Trackpad behält die Scrollrichtung des Systems."
"About": "Über"
"Updates": "Updates"
"Off by default — checking for updates is OpenLogi's only optional outbound network request.": "Off by default — checking for updates is OpenLogi's only optional outbound network request."
"Up to date": "Up to date"
"Update available": "Update available"
"Update ready": "Update ready"
"Update failed": "Update failed"
"Automatically download and install": "Automatically download and install"
"Download updates in the background and apply them the next time OpenLogi restarts.": "Download updates in the background and apply them the next time OpenLogi restarts."
"Update source": "Update source"
"View changelog": "View changelog"
"No background updater — OpenLogi only connects when you turn on automatic checks or click Check for Updates.": "No background updater — OpenLogi only connects when you turn on automatic checks or click Check for Updates."
"Stable channel": "Stable channel"
"A native, local-first alternative to Logitech Options+.": "A native, local-first alternative to Logitech Options+."
"Changelog": "Changelog"
"Documentation": "Documentation"
"Report an issue": "Report an issue"
"Show in file manager": "Show in file manager"
"Not affiliated with Logitech. \"Logitech\", \"MX Master\", and \"Options+\" are trademarks of Logitech International S.A.": "Not affiliated with Logitech. \"Logitech\", \"MX Master\", and \"Options+\" are trademarks of Logitech International S.A."
"Appearance": "Appearance"
"Appearance mode": "Appearance mode"
"Light and dark use the matching theme; Follow system tracks the OS setting.": "Light and dark use the matching theme; Follow system tracks the OS setting."
"Light": "Light"
"Dark": "Dark"
"Theme": "Theme"
"Corner radius": "Corner radius"
"Roundness of buttons, cards, and controls.": "Roundness of buttons, cards, and controls."
"Sharp": "Sharp"
"Round": "Round"
"All": "All"
"Filter themes…": "Filter themes…"
"No themes match “%{query}”.": "No themes match “%{query}”."
"Color theme": "Color theme"
"Interface language": "Interface language"
Loading
Loading