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 use cmd::diag::wheel::ResolutionArg;
49
50 #[test]
54 fn cli_command_tree_is_well_formed() {
55 Cli::command().debug_assert();
56 }
57
58 #[test]
61 fn bare_invocation_has_no_subcommand() {
62 let cli = Cli::try_parse_from(["openlogi"]).expect("bare invocation parses");
63 assert!(cli.cmd.is_none());
64 }
65
66 #[test]
67 fn smartshift_leave_flipped_conflicts_with_sensitivity() {
68 let result = Cli::try_parse_from([
69 "openlogi",
70 "diag",
71 "smartshift",
72 "--leave-flipped",
73 "--sensitivity",
74 "10",
75 ]);
76 assert!(result.is_err());
77 }
78
79 #[test]
80 fn smartshift_rejects_zero_sensitivity() {
81 let result = Cli::try_parse_from(["openlogi", "diag", "smartshift", "--sensitivity", "0"]);
84 assert!(result.is_err());
85 }
86
87 #[test]
88 fn dpi_target_and_device_flags_are_mapped() {
89 let cli = Cli::try_parse_from([
90 "openlogi",
91 "diag",
92 "dpi",
93 "--target",
94 "800",
95 "--device",
96 "MX Master",
97 ])
98 .expect("valid dpi invocation parses");
99
100 match cli.cmd.expect("subcommand present") {
101 Command::Diag(DiagCmd::Dpi(args)) => {
102 assert_eq!(args.target, Some(800));
103 assert_eq!(args.device.as_deref(), Some("MX Master"));
104 }
105 other => panic!("expected Diag(Dpi), got {other:?}"),
106 }
107 }
108
109 #[test]
110 fn lighting_color_is_positional_and_method_is_a_flag() {
111 let cli = Cli::try_parse_from([
112 "openlogi", "diag", "lighting", "ff0000", "--method", "effects",
113 ])
114 .expect("valid lighting invocation parses");
115
116 match cli.cmd.expect("subcommand present") {
117 Command::Diag(DiagCmd::Lighting(args)) => {
118 assert_eq!(args.color, "ff0000");
119 assert!(matches!(args.method, Method::Effects));
120 }
121 other => panic!("expected Diag(Lighting), got {other:?}"),
122 }
123 }
124
125 #[test]
126 fn lighting_rejects_unknown_method() {
127 let result = Cli::try_parse_from([
128 "openlogi", "diag", "lighting", "ff0000", "--method", "bogus",
129 ]);
130 assert!(result.is_err());
131 }
132
133 #[test]
134 fn wheel_resolution_and_device_flags_are_mapped() {
135 let cli = Cli::try_parse_from([
136 "openlogi",
137 "diag",
138 "wheel",
139 "--device",
140 "MX Anywhere 3S",
141 "--resolution",
142 "low",
143 ])
144 .expect("valid wheel invocation parses");
145
146 match cli.cmd.expect("subcommand present") {
147 Command::Diag(DiagCmd::Wheel(args)) => {
148 assert_eq!(args.device.as_deref(), Some("MX Anywhere 3S"));
149 assert_eq!(args.resolution, Some(ResolutionArg::Low));
150 }
151 other => panic!("expected Diag(Wheel), got {other:?}"),
152 }
153 }
154}