Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f7d1c1b
style: carry reasons on remaining lint suppressions, drop section ban…
AprilNEA Jul 13, 2026
dbc54d5
refactor(gui): fail fast on an invalid embedded version
AprilNEA Jul 13, 2026
92f8d4a
refactor(agent,core): resolve the LaunchAgents path via core paths
AprilNEA Jul 13, 2026
9979160
fix(hidpp): stop dropping events that carry unknown wire values
AprilNEA Jul 13, 2026
7070ab0
fix(hid,agent): validate the pairing passkey and DPI at their boundaries
AprilNEA Jul 13, 2026
f80d558
fix(core,gui,agent,cli): validate the lighting color once as a typed Rgb
AprilNEA Jul 13, 2026
75c3589
refactor(core): persist the config through atomic-write-file
AprilNEA Jul 13, 2026
1fc9364
refactor(inject): move the platform backends into per-platform files
AprilNEA Jul 13, 2026
bb88de8
refactor(gui,cli): rename semantics-carrying mod.rs modules
AprilNEA Jul 13, 2026
57ed1b3
refactor(gui): extract the pacing module and drop tiny indirections
AprilNEA Jul 13, 2026
76e8651
test(gui): use tempfile for the asset-resolver test directories
AprilNEA Jul 13, 2026
fb1ed48
refactor(gui): move the Load/LazyDeviceData infrastructure to state/l…
AprilNEA Jul 13, 2026
69e6fc4
refactor(gui): move the asset-sync orchestration out of main.rs
AprilNEA Jul 13, 2026
6562d38
refactor(gui): split app.rs into home, detail, and shared-widget modules
AprilNEA Jul 13, 2026
1601e62
refactor(core): move the swipe-gesture machinery to binding/swipe.rs
AprilNEA Jul 13, 2026
2c2470b
test(assets): use tempfile for the cache test directories
AprilNEA Jul 13, 2026
6d78dd6
docs(core): document the remaining public items and deny missing_docs
AprilNEA Jul 13, 2026
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
3 changes: 3 additions & 0 deletions Cargo.lock

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

37 changes: 17 additions & 20 deletions crates/openlogi-agent-core/src/hardware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use std::time::Duration;

use openlogi_core::config::Lighting;
use openlogi_hid::{
CaptureChannel, DeviceRoute, DpiInfo, HidppOperation, SharedChannel, SmartShiftMode,
SmartShiftStatus, WriteError,
CaptureChannel, DeviceRoute, DpiInfo, HidppFeatureErrorKind, HidppOperation, SharedChannel,
SmartShiftMode, SmartShiftStatus, WriteError,
};
use tracing::{debug, warn};

Expand Down Expand Up @@ -228,9 +228,12 @@ pub fn write_dpi_in_background(
return;
}
};
// All device-supported DPI values fit in HID++'s u16 wire field. The
// saturating fallback exists only for type-system exhaustiveness.
let dpi_u16 = u16::try_from(dpi).unwrap_or(u16::MAX);
// All device-supported DPI values fit in HID++'s u16 wire field; a
// larger value is a caller bug and must not be clamped onto the device.
let Ok(dpi_u16) = u16::try_from(dpi) else {
warn!(dpi, "DPI exceeds the HID++ u16 wire field; write skipped");
return;
};
let result = rt.block_on(async {
tokio::time::timeout(WRITE_BUDGET, async {
match &shared {
Expand Down Expand Up @@ -342,43 +345,37 @@ pub fn set_lighting_in_background(target: Option<DeviceRoute>, lighting: &Lighti
});
}

/// Parse `"RRGGBB"` (optionally `#`-prefixed) into an `(r, g, b)` triple.
fn parse_hex(hex: &str) -> (u8, u8, u8) {
let v = u32::from_str_radix(hex.trim_start_matches('#'), 16).unwrap_or(0);
(
u8::try_from((v >> 16) & 0xff).unwrap_or(0),
u8::try_from((v >> 8) & 0xff).unwrap_or(0),
u8::try_from(v & 0xff).unwrap_or(0),
)
}

/// Resolve a [`Lighting`] config to an `(r, g, b)` triple: the configured hex
/// Resolve a [`Lighting`] config to an `(r, g, b)` triple: the configured
/// colour scaled by brightness, or black when lighting is off.
fn lighting_rgb(lighting: &Lighting) -> (u8, u8, u8) {
if !lighting.enabled {
return (0, 0, 0);
}
let (r, g, b) = parse_hex(&lighting.color);
let (r, g, b) = lighting.color.components();
let scale =
|c: u8| u8::try_from(u16::from(c) * u16::from(lighting.brightness) / 100).unwrap_or(c);
(scale(r), scale(g), scale(b))
}

// ---------------------------------------------------------------------------
// Async, awaitable variants used by the IPC server (the GUI routes "apply now"
// / "read" device commands through the agent, which awaits and reports the
// result). Writes reuse the capture session's open channel when it targets the
// same device, exactly like the fire-and-forget `*_in_background` helpers, so
// the daemon never opens a second channel to a device it already holds.
// ---------------------------------------------------------------------------

/// Apply `dpi` to `route`, reusing the capture session's channel when possible.
pub async fn apply_dpi(
capture: &CaptureChannel,
route: &DeviceRoute,
dpi: u32,
) -> Result<(), WriteError> {
let dpi = u16::try_from(dpi).unwrap_or(u16::MAX);
// Reject a DPI beyond the HID++ u16 wire field the same way the device
// itself would reject an out-of-range argument.
let dpi = u16::try_from(dpi).map_err(|_| WriteError::HidppFeature {
operation: HidppOperation::WriteDpi,
feature_hex: 0x2201,
kind: HidppFeatureErrorKind::OutOfRange,
})?;
let shared = reusable_channel(Some(capture), route);
timed(HidppOperation::WriteDpi, async {
match &shared {
Expand Down
5 changes: 5 additions & 0 deletions crates/openlogi-agent-core/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ impl From<PairingError> for PairingFailure {
PairingError::Timeout => Self::Timeout,
PairingError::Device(code) => Self::Device { code },
PairingError::Cancelled => Self::Cancelled,
// Carried as the generic transport-failure message so the wire
// format stays unchanged (PairingFailure variants are append-only).
PairingError::MalformedNotification(what) => Self::Hid {
message: format!("malformed pairing notification ({what})"),
},
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion crates/openlogi-agent-core/tests/wire_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,12 @@ fn device_settings_payloads() {
});
assert_wire(&smartshift, "0001103c");

// `Rgb` serializes as the same hex string the field used to hold raw, so
// the pinned bytes are identical to the pre-newtype encoding.
assert_wire(
&Lighting {
enabled: true,
color: "8000ff".into(),
color: "8000ff".parse().expect("valid hex"),
brightness: 80,
},
"010638303030666650",
Expand Down
6 changes: 3 additions & 3 deletions crates/openlogi-agent/src/launch_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ fn remove_legacy() {

#[cfg(target_os = "macos")]
fn plist_path(label: &str) -> io::Result<PathBuf> {
let home = std::env::var_os("HOME")
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "$HOME not set"))?;
Ok(PathBuf::from(home)
let home =
openlogi_core::paths::home_dir().map_err(|e| io::Error::new(io::ErrorKind::NotFound, e))?;
Ok(home
.join("Library")
.join("LaunchAgents")
.join(format!("{label}.plist")))
Expand Down
3 changes: 3 additions & 0 deletions crates/openlogi-assets/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ atomic-write-file = "0.3.0"

[lints]
workspace = true

[dev-dependencies]
tempfile = "3.27.0"
15 changes: 5 additions & 10 deletions crates/openlogi-assets/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,27 +274,23 @@ mod tests {
#[test]
#[allow(clippy::expect_used, reason = "expect/unwrap are idiomatic in tests")]
fn write_replace_overwrites_in_place() {
let dir = std::env::temp_dir().join(format!("openlogi-http-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create temp dir");
let dst = dir.join("a.png");
let dir = tempfile::tempdir().expect("create temp dir");
let dst = dir.path().join("a.png");

write_replace(&dst, b"one").expect("first write");
write_replace(&dst, b"two").expect("replace");

assert_eq!(std::fs::read(&dst).expect("read back"), b"two");
let _ = std::fs::remove_dir_all(&dir);
}

#[cfg(unix)]
#[test]
#[allow(clippy::expect_used, reason = "expect/unwrap are idiomatic in tests")]
fn write_replace_replaces_a_planted_symlink_instead_of_following_it() {
let dir =
std::env::temp_dir().join(format!("openlogi-http-symlink-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create temp dir");
let victim = dir.join("victim.txt");
let dir = tempfile::tempdir().expect("create temp dir");
let victim = dir.path().join("victim.txt");
std::fs::write(&victim, b"untouched").expect("seed victim");
let dst = dir.join("b.png");
let dst = dir.path().join("b.png");
std::os::unix::fs::symlink(&victim, &dst).expect("plant symlink");

write_replace(&dst, b"payload").expect("write through planted link");
Expand All @@ -305,7 +301,6 @@ mod tests {
let meta = std::fs::symlink_metadata(&dst).expect("stat dst");
assert!(meta.file_type().is_file());
assert_eq!(std::fs::read(&dst).expect("read dst"), b"payload");
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
Expand Down
12 changes: 3 additions & 9 deletions crates/openlogi-cli/src/cmd/diag/lighting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use anyhow::{Result, anyhow};
use clap::{Args, ValueEnum};
use openlogi_core::color::Rgb;
use openlogi_hid::{DeviceRoute, LightingMethod};

#[derive(Debug, Clone, Copy, ValueEnum)]
Expand Down Expand Up @@ -44,15 +45,8 @@ pub struct LightingArgs {
}

pub async fn run(args: LightingArgs) -> Result<()> {
let hex = args.color.trim_start_matches('#');
if hex.len() != 6 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(anyhow!("color must be exactly 6 hex digits, e.g. ff0000"));
}
let rgb = u32::from_str_radix(hex, 16)
.map_err(|_| anyhow!("color must be 6 hex digits, e.g. ff0000"))?;
let r = ((rgb >> 16) & 0xff) as u8;
let g = ((rgb >> 8) & 0xff) as u8;
let b = (rgb & 0xff) as u8;
let color: Rgb = args.color.trim_start_matches('#').parse()?;
let (r, g, b) = color.components();

let device_query = args.device;
let needle = device_query.as_deref().map(str::to_lowercase);
Expand Down
1 change: 1 addition & 0 deletions crates/openlogi-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ thiserror = { workspace = true }
tracing = { workspace = true }
fs4 = { version = "1.1.0", features = ["sync"] }
etcetera = "0.11.0"
atomic-write-file = "0.3.0"

[dev-dependencies]
tempfile = "3"
Expand Down
Loading
Loading