Skip to main content

killswitch/cli/commands/
mod.rs

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