What Is the Purpose of the openlogi-core Crate in OpenLogi?
The openlogi-core crate defines the pure data model and TOML-serializable configuration structures for Logitech HID++ devices, containing absolutely no hardware I/O or async runtime code to enable testing and reuse across the entire OpenLogi application stack.
The openlogi-core crate serves as the foundational data layer for AprilNEA/OpenLogi, a native, local-first alternative to Logitech Options+ written in Rust. It establishes the single source of truth for device capabilities, button bindings, and global settings that power the CLI, background agent, and desktop GUI components.
Core Responsibilities of the openlogi-core Crate
Pure Types and Zero I/O Design
At its heart, openlogi-core is a collection of serde-serializable Rust structs and enums that describe every aspect of Logitech device configuration. According to the crate's documentation, it contains strictly no platform-specific I/O—it never talks to HID devices, never spawns async tasks, and never accesses the file system except through explicit helper functions provided by consuming crates.
This I/O-free architecture means the crate focuses exclusively on:
- Device descriptors (vendor/product IDs, DPI ranges, LED capabilities, button layouts)
- Configuration schemas (global defaults and per-device overrides in TOML format)
- Action definitions (click, scroll, key-combo, swipe, DPI step, and macro actions)
- Binding catalogs (mappings between physical buttons and logical actions, including per-application overrides)
Configuration Management via the Settings Struct
The central configuration type is the Settings struct defined in src/config/settings.rs. This struct captures global defaults that apply to all connected devices unless explicitly overridden:
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Settings {
#[serde(default = "default_smart_shift")]
pub smart_shift: bool,
#[serde(default = "default_step")]
pub step: u16,
#[serde(default = "default_colors")]
pub colors: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assets_dir: Option<PathBuf>,
}
The smart_shift field controls automatic DPI shifting behavior, step defines the DPI adjustment increment (defaulting to 100), and colors stores the global LED palette as hex RGB strings. Because these types implement serde traits, they serialize cleanly to human-readable TOML configuration files.
Device Model and Action Catalog
Beyond global settings, openlogi-core houses the complete device model in src/device.rs and the action catalog in src/binding.rs. These modules define:
- Device identification using vendor/product ID pairs (e.g.,
0x046dfor Logitech) - Capability matrices describing available buttons, LED zones, and sensor resolutions
- Action kinds ranging from simple key combinations to complex multi-step macros
Architecture Benefits of the I/O-Free Design
Testability Without Hardware
Because openlogi-core contains no hardware interaction logic, developers can run comprehensive unit tests on the data model without any physical Logitech devices attached. The separation allows validation of configuration parsing, binding resolution, and device capability matching in pure Rust test suites.
Shared Foundation Across Binaries
The crate's clean separation enables reuse across OpenLogi's multi-binary architecture:
openlogi-cliuses the core for diagnostic output and configuration file validationopenlogi-agentlayers the runtime HID++ I/O on top of core typesopenlogi-desktopandopenlogi-overlaypresent the core data model through the GPUI interface
This ensures that changes to the configuration format or action definitions only require modifications in openlogi-core, automatically propagating consistency to all consuming components.
Practical Usage Examples
Loading a TOML Configuration File
The load_from_path function in src/config/file.rs provides a synchronous interface for deserializing configuration files into the Settings struct:
use openlogi_core::config::file::load_from_path;
fn main() -> anyhow::Result<()> {
// Load from a standard TOML configuration file
let settings = load_from_path("docs/config.example.toml")?;
println!("SmartShift enabled? {}", settings.smart_shift);
println!("Global DPI step: {}", settings.step);
Ok(())
}
Creating Settings Programmatically
You can construct configuration objects directly and serialize them back to TOML for persistence:
use openlogi_core::config::Settings;
fn main() -> anyhow::Result<()> {
let custom = Settings {
smart_shift: false,
step: 200,
colors: vec!["#ff0000".into(), "#00ff00".into()],
assets_dir: None,
};
let toml_str = toml::to_string_pretty(&custom)?;
println!("{}", toml_str);
Ok(())
}
Defining Button Bindings
The binding system maps physical buttons to actions using the types defined in src/binding.rs:
use openlogi_core::binding::{Binding, Action, ActionKind};
let binding = Binding {
device: "G502".into(),
button: "thumb_button".into(),
action: Action {
kind: ActionKind::KeyCombo,
keys: vec!["Ctrl".into(), "Alt".into(), "Delete".into()],
..Default::default()
},
};
Key Source Files and API Surface
| File Path | Responsibility |
|---|---|
src/lib.rs |
Public crate entry point and type re-exports |
src/config/settings.rs |
Global Settings struct with default value functions |
src/config/file.rs |
load_from_path() function for TOML deserialization |
src/device.rs |
Device model definitions and capability descriptors |
src/binding.rs |
Action types, binding structures, and the action catalog |
Summary
- openlogi-core provides the pure data model for the entire OpenLogi ecosystem, ensuring type safety across CLI, agent, and GUI components.
- The crate maintains a strict zero-I/O policy, containing no hardware communication, async runtimes, or platform-specific system calls.
- All configuration types implement serde traits for seamless TOML serialization and deserialization.
- The
Settingsstruct insrc/config/settings.rsmanages global defaults for SmartShift behavior, DPI steps, and LED color palettes. - By centralizing device definitions and action catalogs, the crate creates a single source of truth that prevents configuration drift between application layers.
Frequently Asked Questions
Does openlogi-core handle hardware communication?
No. The crate intentionally contains no HID++ protocol implementation, no async I/O, and no hardware abstraction layers. Device communication is handled by separate crates such as openlogi-hid and openlogi-hidpp, which consume the pure types defined in openlogi-core but never expose I/O operations within the core itself.
How does openlogi-core handle configuration file parsing?
The crate uses the toml crate combined with serde derives for deserialization. The load_from_path function in src/config/file.rs provides a thin wrapper around std::fs::read_to_string and toml::from_str, returning a strongly-typed Settings struct or a descriptive error through anyhow.
Can I use openlogi-core in my own Rust project?
Yes, though it is primarily designed for OpenLogi's internal architecture. The crate exposes public types for Logitech device configuration that could theoretically be reused by other Rust applications needing structured HID++ device descriptors, provided they adhere to the same TOML configuration schema.
What serialization format does openlogi-core use?
TOML is the primary configuration format. All core types implement Serialize and Deserialize from the serde ecosystem, enabling human-readable configuration files with comments (as seen in docs/config.example.toml) while maintaining strict type safety at compile time.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →