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::backlight::BacklightAction;
47 use cmd::diag::DiagCmd;
48 use cmd::diag::lighting::Method;
49 use cmd::diag::wheel::ResolutionArg;
50
51 #[test]
55 fn cli_command_tree_is_well_formed() {
56 Cli::command().debug_assert();
57 }
58
59 #[test]
62 fn bare_invocation_has_no_subcommand() {
63 let cli = Cli::try_parse_from(["openlogi"]).expect("bare invocation parses");
64 assert!(cli.cmd.is_none());
65 }
66
67 #[test]
70 fn backlight_defaults_to_status_and_accepts_a_device_filter() {
71 let cli = Cli::try_parse_from(["openlogi", "backlight", "--device", "MX KEYS S"])
72 .expect("bare backlight invocation parses");
73
74 match cli.cmd.expect("subcommand present") {
75 Command::Backlight(args) => {
76 assert_eq!(args.device.as_deref(), Some("MX KEYS S"));
77 assert!(args.action.is_none());
78 }
79 other => panic!("expected Backlight, got {other:?}"),
80 }
81 }
82
83 #[test]
84 fn backlight_off_is_parsed_as_its_own_action() {
85 let cli =
86 Cli::try_parse_from(["openlogi", "backlight", "off"]).expect("backlight off parses");
87
88 match cli.cmd.expect("subcommand present") {
89 Command::Backlight(args) => {
90 assert!(matches!(args.action, Some(BacklightAction::Off)));
91 }
92 other => panic!("expected Backlight, got {other:?}"),
93 }
94 }
95
96 #[test]
97 fn backlight_rejects_an_unknown_action() {
98 let result = Cli::try_parse_from(["openlogi", "backlight", "dim"]);
99 assert!(result.is_err());
100 }
101
102 #[test]
103 fn smartshift_leave_flipped_conflicts_with_sensitivity() {
104 let result = Cli::try_parse_from([
105 "openlogi",
106 "diag",
107 "smartshift",
108 "--leave-flipped",
109 "--sensitivity",
110 "10",
111 ]);
112 assert!(result.is_err());
113 }
114
115 #[test]
116 fn smartshift_rejects_zero_sensitivity() {
117 let result = Cli::try_parse_from(["openlogi", "diag", "smartshift", "--sensitivity", "0"]);
120 assert!(result.is_err());
121 }
122
123 #[test]
124 fn dpi_target_and_device_flags_are_mapped() {
125 let cli = Cli::try_parse_from([
126 "openlogi",
127 "diag",
128 "dpi",
129 "--target",
130 "800",
131 "--device",
132 "MX Master",
133 ])
134 .expect("valid dpi invocation parses");
135
136 match cli.cmd.expect("subcommand present") {
137 Command::Diag(DiagCmd::Dpi(args)) => {
138 assert_eq!(args.target, Some(800));
139 assert_eq!(args.device.as_deref(), Some("MX Master"));
140 }
141 other => panic!("expected Diag(Dpi), got {other:?}"),
142 }
143 }
144
145 #[test]
146 fn lighting_color_is_positional_and_method_is_a_flag() {
147 let cli = Cli::try_parse_from([
148 "openlogi", "diag", "lighting", "ff0000", "--method", "effects",
149 ])
150 .expect("valid lighting invocation parses");
151
152 match cli.cmd.expect("subcommand present") {
153 Command::Diag(DiagCmd::Lighting(args)) => {
154 assert_eq!(args.color, "ff0000");
155 assert!(matches!(args.method, Method::Effects));
156 }
157 other => panic!("expected Diag(Lighting), got {other:?}"),
158 }
159 }
160
161 #[test]
162 fn lighting_rejects_unknown_method() {
163 let result = Cli::try_parse_from([
164 "openlogi", "diag", "lighting", "ff0000", "--method", "bogus",
165 ]);
166 assert!(result.is_err());
167 }
168
169 #[test]
170 fn wheel_resolution_and_device_flags_are_mapped() {
171 let cli = Cli::try_parse_from([
172 "openlogi",
173 "diag",
174 "wheel",
175 "--device",
176 "MX Anywhere 3S",
177 "--resolution",
178 "low",
179 ])
180 .expect("valid wheel invocation parses");
181
182 match cli.cmd.expect("subcommand present") {
183 Command::Diag(DiagCmd::Wheel(args)) => {
184 assert_eq!(args.device.as_deref(), Some("MX Anywhere 3S"));
185 assert_eq!(args.resolution, Some(ResolutionArg::Low));
186 }
187 other => panic!("expected Diag(Wheel), got {other:?}"),
188 }
189 }
190}