Skip to main content

wg_tui/
app.rs

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}