killswitch/cli/commands/
mod.rs1use 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}