Skip to main content

openlogi_cli/
lib.rs

1//! OpenLogi CLI implementation. The `openlogi` binary is a thin wrapper that
2//! calls [`run`]; the command tree and argument parsing live here.
3
4use anyhow::Result;
5use clap::Parser;
6use tracing_subscriber::{EnvFilter, fmt};
7
8mod cmd;
9
10/// OpenLogi: a local-first companion for Logitech HID++ peripherals.
11#[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
23/// Initialise logging, parse arguments, and dispatch the chosen subcommand.
24pub 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    /// Clap's own structural validation (arg ID collisions, invalid
50    /// `conflicts_with` targets, etc.) — cheap and catches a broken derive
51    /// tree before it ever reaches a user.
52    #[test]
53    fn cli_command_tree_is_well_formed() {
54        Cli::command().debug_assert();
55    }
56
57    /// A bare `openlogi` invocation must remain valid — `run()` defaults the
58    /// missing subcommand to `list`.
59    #[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        // `--sensitivity` is a `NonZeroU8`; 0 must fail to parse rather than
81        // silently becoming "no change" downstream.
82        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}