use anyhow::Result;
use clap::Parser;
use tracing_subscriber::{EnvFilter, fmt};
mod cmd;
#[derive(Debug, Parser)]
#[command(
name = "openlogi",
version,
about = "OpenLogi: a local-first companion for Logitech HID++ peripherals.",
long_about = None,
)]
struct Cli {
#[command(subcommand)]
cmd: Option<cmd::Command>,
}
pub async fn run() -> Result<()> {
fmt()
.with_writer(std::io::stderr)
.with_env_filter(
EnvFilter::try_from_env("OPENLOGI_LOG").unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
let cli = Cli::parse();
let command = cli
.cmd
.unwrap_or(cmd::Command::List(cmd::list::ListArgs {}));
command.run().await
}
#[cfg(test)]
#[allow(clippy::expect_used, reason = "expect/unwrap are idiomatic in tests")]
mod tests {
use clap::CommandFactory;
use super::*;
use cmd::Command;
use cmd::backlight::BacklightAction;
use cmd::diag::DiagCmd;
use cmd::diag::lighting::Method;
use cmd::diag::wheel::ResolutionArg;
#[test]
fn cli_command_tree_is_well_formed() {
Cli::command().debug_assert();
}
#[test]
fn bare_invocation_has_no_subcommand() {
let cli = Cli::try_parse_from(["openlogi"]).expect("bare invocation parses");
assert!(cli.cmd.is_none());
}
#[test]
fn backlight_defaults_to_status_and_accepts_a_device_filter() {
let cli = Cli::try_parse_from(["openlogi", "backlight", "--device", "MX KEYS S"])
.expect("bare backlight invocation parses");
match cli.cmd.expect("subcommand present") {
Command::Backlight(args) => {
assert_eq!(args.device.as_deref(), Some("MX KEYS S"));
assert!(args.action.is_none());
}
other => panic!("expected Backlight, got {other:?}"),
}
}
#[test]
fn backlight_off_is_parsed_as_its_own_action() {
let cli =
Cli::try_parse_from(["openlogi", "backlight", "off"]).expect("backlight off parses");
match cli.cmd.expect("subcommand present") {
Command::Backlight(args) => {
assert!(matches!(args.action, Some(BacklightAction::Off)));
}
other => panic!("expected Backlight, got {other:?}"),
}
}
#[test]
fn backlight_rejects_an_unknown_action() {
let result = Cli::try_parse_from(["openlogi", "backlight", "dim"]);
assert!(result.is_err());
}
#[test]
fn smartshift_leave_flipped_conflicts_with_sensitivity() {
let result = Cli::try_parse_from([
"openlogi",
"diag",
"smartshift",
"--leave-flipped",
"--sensitivity",
"10",
]);
assert!(result.is_err());
}
#[test]
fn smartshift_rejects_zero_sensitivity() {
let result = Cli::try_parse_from(["openlogi", "diag", "smartshift", "--sensitivity", "0"]);
assert!(result.is_err());
}
#[test]
fn dpi_target_and_device_flags_are_mapped() {
let cli = Cli::try_parse_from([
"openlogi",
"diag",
"dpi",
"--target",
"800",
"--device",
"MX Master",
])
.expect("valid dpi invocation parses");
match cli.cmd.expect("subcommand present") {
Command::Diag(DiagCmd::Dpi(args)) => {
assert_eq!(args.target, Some(800));
assert_eq!(args.device.as_deref(), Some("MX Master"));
}
other => panic!("expected Diag(Dpi), got {other:?}"),
}
}
#[test]
fn lighting_color_is_positional_and_method_is_a_flag() {
let cli = Cli::try_parse_from([
"openlogi", "diag", "lighting", "ff0000", "--method", "effects",
])
.expect("valid lighting invocation parses");
match cli.cmd.expect("subcommand present") {
Command::Diag(DiagCmd::Lighting(args)) => {
assert_eq!(args.color, "ff0000");
assert!(matches!(args.method, Method::Effects));
}
other => panic!("expected Diag(Lighting), got {other:?}"),
}
}
#[test]
fn lighting_rejects_unknown_method() {
let result = Cli::try_parse_from([
"openlogi", "diag", "lighting", "ff0000", "--method", "bogus",
]);
assert!(result.is_err());
}
#[test]
fn wheel_resolution_and_device_flags_are_mapped() {
let cli = Cli::try_parse_from([
"openlogi",
"diag",
"wheel",
"--device",
"MX Anywhere 3S",
"--resolution",
"low",
])
.expect("valid wheel invocation parses");
match cli.cmd.expect("subcommand present") {
Command::Diag(DiagCmd::Wheel(args)) => {
assert_eq!(args.device.as_deref(), Some("MX Anywhere 3S"));
assert_eq!(args.resolution, Some(ResolutionArg::Low));
}
other => panic!("expected Diag(Wheel), got {other:?}"),
}
}
}