1mod input;
2mod render;
3mod wizard;
4
5use ratatui::widgets::ListState;
6
7use crate::{
8 types::{Message, Tunnel},
9 wireguard::{discover_tunnels, get_interface_info, is_interface_active},
10};
11
12use wizard::{NewTunnelWizard, PeerConfigState, PendingImport, PendingPeerConfig};
13
14pub struct App {
15 pub(crate) tunnels: Vec<Tunnel>,
16 pub(crate) list_state: ListState,
17 pub(crate) flags: AppFlags,
18 pub(crate) confirm_full_tunnel: Option<String>,
19 pub(crate) input_path: Option<String>,
20 pub(crate) input_zip: Option<String>,
21 pub(crate) export_path: Option<String>,
22 pub(crate) pending_import: Option<PendingImport>,
23 pub(crate) new_tunnel: Option<NewTunnelWizard>,
24 pub(crate) pending_peer: Option<PendingPeerConfig>,
25 pub(crate) peer_endpoint_input: Option<String>,
26 pub(crate) peer_dns_input: Option<String>,
27 pub(crate) peer_config: Option<PeerConfigState>,
28 pub(crate) peer_save_path: Option<String>,
29 pub(crate) message: Option<Message>,
30}
31
32#[derive(Debug, Clone, Copy, Default)]
33pub(crate) struct AppFlags {
34 bits: u8,
35}
36
37impl AppFlags {
38 const SHOW_DETAILS: u8 = 1 << 0;
39 const SHOW_HELP: u8 = 1 << 1;
40 const CONFIRM_DELETE: u8 = 1 << 2;
41 const SHOW_ADD_MENU: u8 = 1 << 3;
42 const SHOULD_QUIT: u8 = 1 << 4;
43
44 pub fn show_details(self) -> bool {
45 self.is_set(Self::SHOW_DETAILS)
46 }
47
48 pub fn show_help(self) -> bool {
49 self.is_set(Self::SHOW_HELP)
50 }
51
52 pub fn confirm_delete(self) -> bool {
53 self.is_set(Self::CONFIRM_DELETE)
54 }
55
56 pub fn show_add_menu(self) -> bool {
57 self.is_set(Self::SHOW_ADD_MENU)
58 }
59
60 pub fn should_quit(self) -> bool {
61 self.is_set(Self::SHOULD_QUIT)
62 }
63
64 pub fn set_show_help(&mut self, value: bool) {
65 self.set(Self::SHOW_HELP, value);
66 }
67
68 pub fn set_confirm_delete(&mut self, value: bool) {
69 self.set(Self::CONFIRM_DELETE, value);
70 }
71
72 pub fn set_show_add_menu(&mut self, value: bool) {
73 self.set(Self::SHOW_ADD_MENU, value);
74 }
75
76 pub fn set_should_quit(&mut self, value: bool) {
77 self.set(Self::SHOULD_QUIT, value);
78 }
79
80 pub fn toggle_show_details(&mut self) {
81 self.toggle(Self::SHOW_DETAILS);
82 }
83
84 fn set(&mut self, flag: u8, value: bool) {
85 if value {
86 self.bits |= flag;
87 } else {
88 self.bits &= !flag;
89 }
90 }
91
92 fn toggle(&mut self, flag: u8) {
93 self.bits ^= flag;
94 }
95
96 fn is_set(self, flag: u8) -> bool {
97 self.bits & flag != 0
98 }
99}
100
101impl Default for App {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107impl App {
108 #[must_use]
109 pub fn new() -> Self {
110 let mut app = Self {
111 tunnels: Vec::new(),
112 list_state: ListState::default(),
113 flags: AppFlags::default(),
114 confirm_full_tunnel: None,
115 input_path: None,
116 input_zip: None,
117 export_path: None,
118 pending_import: None,
119 new_tunnel: None,
120 pending_peer: None,
121 peer_endpoint_input: None,
122 peer_dns_input: None,
123 peer_config: None,
124 peer_save_path: None,
125 message: None,
126 };
127 app.refresh_tunnels();
128 if !app.tunnels.is_empty() {
129 app.list_state.select(Some(0));
130 }
131 app
132 }
133
134 pub fn refresh_tunnels(&mut self) {
135 self.tunnels = discover_tunnels();
136 for t in &mut self.tunnels {
137 t.is_active = is_interface_active(&t.name);
138 if t.is_active {
139 t.interface = get_interface_info(&t.name);
140 }
141 }
142 self.clamp_selection();
143 }
144
145 #[must_use]
146 pub fn should_quit(&self) -> bool {
147 self.flags.should_quit()
148 }
149
150 pub(crate) fn clamp_selection(&mut self) {
151 let selected = match (self.list_state.selected(), self.tunnels.len()) {
152 (_, 0) => None,
153 (None | Some(0), _) => Some(0),
154 (Some(i), len) => Some(i.min(len - 1)),
155 };
156 self.list_state.select(selected);
157 }
158
159 pub(crate) fn selected(&self) -> Option<&Tunnel> {
160 self.list_state.selected().and_then(|i| self.tunnels.get(i))
161 }
162
163 pub(crate) fn move_selection(&mut self, delta: isize) {
164 let Some(i) = self.list_state.selected() else {
165 return;
166 };
167 let max_index = self.tunnels.len().saturating_sub(1);
168 let max_index = isize::try_from(max_index).unwrap_or(0);
169 let current = isize::try_from(i).unwrap_or(0);
170 let new = (current + delta).clamp(0, max_index);
171 let new = usize::try_from(new).unwrap_or(0);
172 self.list_state.select(Some(new));
173 }
174
175 pub(crate) fn default_tunnel_name(&self) -> String {
176 for i in 0..1000u32 {
177 let name = format!("wg{i}");
178 if !self.tunnels.iter().any(|t| t.name == name) {
179 return name;
180 }
181 }
182 "wg0".into()
183 }
184}