Skip to main content

killswitch/cli/commands/
mod.rs

1use clap::{
2    Arg, ArgAction, ColorChoice, Command,
3    builder::styling::{AnsiColor, Effects, Styles},
4};
5use std::sync::OnceLock;
6
7pub mod built_info {
8    #![allow(clippy::doc_markdown)]
9    include!(concat!(env!("OUT_DIR"), "/built.rs"));
10}
11
12static LONG_VERSION: OnceLock<String> = OnceLock::new();
13
14#[must_use]
15pub fn new() -> Command {
16    let styles = Styles::styled()
17        .header(AnsiColor::Yellow.on_default() | Effects::BOLD)
18        .usage(AnsiColor::Green.on_default() | Effects::BOLD)
19        .literal(AnsiColor::Blue.on_default() | Effects::BOLD)
20        .placeholder(AnsiColor::Green.on_default());
21
22    let long_version: &str = LONG_VERSION.get_or_init(|| {
23        let git_hash = built_info::GIT_COMMIT_HASH.unwrap_or("unknown");
24        format!("{} - {git_hash}", env!("CARGO_PKG_VERSION"))
25    });
26
27    Command::new(env!("CARGO_PKG_NAME"))
28        .about(env!("CARGO_PKG_DESCRIPTION"))
29        .version(env!("CARGO_PKG_VERSION"))
30        .author(env!("CARGO_PKG_AUTHORS"))
31        .color(ColorChoice::Auto)
32        .long_version(long_version)
33        .styles(styles)
34        .arg(
35            Arg::new("enable")
36                .short('e')
37                .long("enable")
38                .help("Enable the VPN kill switch")
39                .action(ArgAction::SetTrue)
40                .conflicts_with("disable"),
41        )
42        .arg(
43            Arg::new("disable")
44                .short('d')
45                .long("disable")
46                .help("Disable the VPN kill switch")
47                .action(ArgAction::SetTrue)
48                .conflicts_with("enable"),
49        )
50        .arg(
51            Arg::new("status")
52                .short('s')
53                .long("status")
54                .help("Show kill switch status")
55                .action(ArgAction::SetTrue)
56                .conflicts_with_all(["enable", "disable"]),
57        )
58        .arg(
59            Arg::new("ipv4")
60                .long("ipv4")
61                .help("VPN peer IPv4 address (auto-detected if not specified)")
62                .value_name("IP")
63                .conflicts_with_all(["disable", "status"]),
64        )
65        .arg(
66            Arg::new("leak")
67                .long("leak")
68                .help("Allow ICMP (ping) and DNS requests outside the VPN")
69                .action(ArgAction::SetTrue)
70                .conflicts_with_all(["disable", "status"]),
71        )
72        .arg(
73            Arg::new("local")
74                .long("local")
75                .help("Allow local network traffic")
76                .action(ArgAction::SetTrue)
77                .conflicts_with_all(["disable", "status"]),
78        )
79        .arg(
80            Arg::new("print")
81                .short('p')
82                .long("print")
83                .help("Print the pf firewall rules without applying them")
84                .action(ArgAction::SetTrue)
85                .conflicts_with_all(["disable", "status"]),
86        )
87        .arg(
88            Arg::new("verbose")
89                .short('v')
90                .long("verbose")
91                .help("Increase output verbosity (-v: verbose, -vv: debug)")
92                .action(ArgAction::Count),
93        )
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn verify_command() {
102        new().debug_assert();
103    }
104
105    #[allow(clippy::unwrap_used)]
106    #[test]
107    fn test_command_metadata() {
108        let cmd = new();
109        assert_eq!(cmd.get_name(), env!("CARGO_PKG_NAME"));
110        assert_eq!(cmd.get_version().unwrap(), env!("CARGO_PKG_VERSION"));
111    }
112
113    #[test]
114    fn test_enable_flag() {
115        let matches = new().get_matches_from(vec!["killswitch", "--enable"]);
116        assert!(matches.get_flag("enable"));
117    }
118
119    #[test]
120    fn test_disable_flag() {
121        let matches = new().get_matches_from(vec!["killswitch", "-d"]);
122        assert!(matches.get_flag("disable"));
123    }
124
125    #[test]
126    fn test_verbose_count() {
127        let matches = new().get_matches_from(vec!["killswitch", "-vvv"]);
128        assert_eq!(matches.get_count("verbose"), 3);
129    }
130}