1use anyhow::Result;
5use clap::Parser;
6use tracing_subscriber::{EnvFilter, fmt};
7
8mod cmd;
9
10#[derive(Debug, Parser)]
12#[command(
13 name = "openlogi",
14 version,
15 about = "OpenLogi: a local-first companion for Logitech HID++ peripherals.",
16 long_about = None,
17)]
18struct Cli {
19 #[command(subcommand)]
20 cmd: Option<cmd::Command>,
21}
22
23pub async fn run() -> Result<()> {
25 fmt()
26 .with_writer(std::io::stderr)
27 .with_env_filter(
28 EnvFilter::try_from_env("OPENLOGI_LOG").unwrap_or_else(|_| EnvFilter::new("info")),
29 )
30 .init();
31
32 let cli = Cli::parse();
33 let command = cli
34 .cmd
35 .unwrap_or(cmd::Command::List(cmd::list::ListArgs {}));
36 command.run().await
37}
38
39#[cfg(test)]
40#[allow(clippy::expect_used, reason = "expect/unwrap are idiomatic in tests")]
41mod tests {
42 use clap::CommandFactory;
43
44 use super::*;
45 use cmd::Command;
46 use cmd::diag::DiagCmd;
47 use cmd::diag::lighting::Method;
48
49 #[test]
53 fn cli_command_tree_is_well_formed() {
54 Cli::command().debug_assert();
55 }
56
57 #[test]
60 fn bare_invocation_has_no_subcommand() {
61 let cli = Cli::try_parse_from(["openlogi"]).expect("bare invocation parses");
62 assert!(cli.cmd.is_none());
63 }
64
65 #[test]
66 fn smartshift_leave_flipped_conflicts_with_sensitivity() {
67 let result = Cli::try_parse_from([
68 "openlogi",
69 "diag",
70 "smartshift",
71 "--leave-flipped",
72 "--sensitivity",
73 "10",
74 ]);
75 assert!(result.is_err());
76 }
77
78 #[test]
79 fn smartshift_rejects_zero_sensitivity() {
80 let result = Cli::try_parse_from(["openlogi", "diag", "smartshift", "--sensitivity", "0"]);
83 assert!(result.is_err());
84 }
85
86 #[test]
87 fn dpi_target_and_device_flags_are_mapped() {
88 let cli = Cli::try_parse_from([
89 "openlogi",
90 "diag",
91 "dpi",
92 "--target",
93 "800",
94 "--device",
95 "MX Master",
96 ])
97 .expect("valid dpi invocation parses");
98
99 match cli.cmd.expect("subcommand present") {
100 Command::Diag(DiagCmd::Dpi(args)) => {
101 assert_eq!(args.target, Some(800));
102 assert_eq!(args.device.as_deref(), Some("MX Master"));
103 }
104 other => panic!("expected Diag(Dpi), got {other:?}"),
105 }
106 }
107
108 #[test]
109 fn lighting_color_is_positional_and_method_is_a_flag() {
110 let cli = Cli::try_parse_from([
111 "openlogi", "diag", "lighting", "ff0000", "--method", "effects",
112 ])
113 .expect("valid lighting invocation parses");
114
115 match cli.cmd.expect("subcommand present") {
116 Command::Diag(DiagCmd::Lighting(args)) => {
117 assert_eq!(args.color, "ff0000");
118 assert!(matches!(args.method, Method::Effects));
119 }
120 other => panic!("expected Diag(Lighting), got {other:?}"),
121 }
122 }
123
124 #[test]
125 fn lighting_rejects_unknown_method() {
126 let result = Cli::try_parse_from([
127 "openlogi", "diag", "lighting", "ff0000", "--method", "bogus",
128 ]);
129 assert!(result.is_err());
130 }
131}