1use ratatui::{
2 Frame,
3 layout::{Constraint, Layout, Rect},
4 style::{Color, Modifier, Style, Stylize},
5 text::{Line, Text},
6 widgets::{List, ListItem, Paragraph, Wrap},
7};
8
9use crate::ui::{
10 bordered_block, label, peer_lines, render_add_menu, render_confirm, render_full_tunnel_warning,
11 render_help, render_import_conflict, render_input, render_peer_config, render_peer_qr, section,
12 truncate_key,
13};
14
15use super::App;
16
17impl App {
18 pub fn draw(&mut self, frame: &mut Frame) {
19 let chunks = Layout::horizontal(if self.flags.show_details() {
20 vec![Constraint::Percentage(40), Constraint::Percentage(60)]
21 } else {
22 vec![Constraint::Percentage(100)]
23 })
24 .split(frame.area());
25
26 self.render_main(frame, chunks[0]);
27 if self.flags.show_details() && chunks.len() > 1 {
28 self.render_details(frame, chunks[1]);
29 }
30 self.render_overlays(frame);
31 }
32
33 fn render_main(&mut self, frame: &mut Frame, area: Rect) {
34 let main = Layout::vertical([
35 Constraint::Length(3),
36 Constraint::Min(0),
37 Constraint::Length(3),
38 ])
39 .split(area);
40
41 Self::render_header(frame, main[0]);
42 self.render_list(frame, main[1]);
43 self.render_status(frame, main[2]);
44 }
45
46 fn render_overlays(&mut self, frame: &mut Frame) {
47 if self.flags.show_help() {
48 render_help(frame);
49 }
50 if self.flags.confirm_delete()
51 && let Some(tunnel) = self.selected()
52 {
53 render_confirm(frame, &tunnel.name);
54 }
55 if let Some(ref pending) = self.pending_import {
56 render_import_conflict(frame, pending.conflicts);
57 }
58 if let Some(ref name) = self.confirm_full_tunnel {
59 render_full_tunnel_warning(frame, name);
60 }
61 if self.flags.show_add_menu() {
62 render_add_menu(frame);
63 }
64 self.render_path_inputs(frame);
65 self.render_wizard_input(frame);
66 self.render_peer_input(frame);
67 self.render_peer_output(frame);
68 }
69
70 fn render_path_inputs(&mut self, frame: &mut Frame) {
71 if let Some(ref path) = self.input_path {
72 let cwd = std::env::current_dir()
73 .map(|p| format!("cwd: {} (use ~/ for home)", p.display()))
74 .ok();
75 render_input(
76 frame,
77 "Import Tunnel",
78 "File path (.conf):",
79 path,
80 cwd.as_deref(),
81 );
82 }
83 if let Some(ref path) = self.input_zip {
84 let cwd = std::env::current_dir()
85 .map(|p| format!("cwd: {} (use ~/ for home)", p.display()))
86 .ok();
87 render_input(
88 frame,
89 "Import Zip",
90 "Zip path (.zip):",
91 path,
92 cwd.as_deref(),
93 );
94 }
95 if let Some(ref path) = self.export_path {
96 let hint = std::env::current_dir()
97 .map(|p| {
98 format!(
99 "{} tunnel(s) — cwd: {} (use ~/ for home)",
100 self.tunnels.len(),
101 p.display()
102 )
103 })
104 .ok();
105 render_input(
106 frame,
107 "Export All Tunnels",
108 "Destination (.zip):",
109 path,
110 hint.as_deref(),
111 );
112 }
113 }
114
115 fn render_wizard_input(&mut self, frame: &mut Frame) {
116 if let Some(ref wizard) = self.new_tunnel {
117 let (title, prompt, hint) = wizard.ui();
118 render_input(
119 frame,
120 &title,
121 prompt,
122 wizard.current_value(),
123 hint.as_deref(),
124 );
125 }
126 }
127
128 fn render_peer_input(&mut self, frame: &mut Frame) {
129 if let Some(ref endpoint) = self.peer_endpoint_input {
130 render_input(
131 frame,
132 "Peer Endpoint",
133 "Endpoint (host:port):",
134 endpoint,
135 Some("Confirm or edit the server address"),
136 );
137 }
138 if let Some(ref dns) = self.peer_dns_input {
139 render_input(
140 frame,
141 "Peer DNS",
142 "DNS (optional):",
143 dns,
144 Some("Leave empty to skip"),
145 );
146 }
147 }
148
149 fn render_peer_output(&mut self, frame: &mut Frame) {
150 if let Some(ref peer) = self.peer_config {
151 if peer.show_qr {
152 if let Some(code) = peer.qr_code.as_ref() {
153 render_peer_qr(frame, code);
154 } else {
155 render_peer_config(frame, &peer.config_text, &peer.suggested_path);
156 }
157 } else {
158 render_peer_config(frame, &peer.config_text, &peer.suggested_path);
159 }
160 }
161 if let Some(ref path) = self.peer_save_path {
162 render_input(
163 frame,
164 "Save Peer Config",
165 "Destination (.conf):",
166 path,
167 Some("Press Enter to save"),
168 );
169 }
170 }
171
172 fn render_header(f: &mut Frame, area: Rect) {
173 let title = Line::from(vec![
174 " WireGuard ".fg(Color::Cyan).bold(),
175 "TUI Manager".fg(Color::White),
176 ]);
177 f.render_widget(Paragraph::new(title).block(bordered_block(None)), area);
178 }
179
180 fn render_list(&mut self, f: &mut Frame, area: Rect) {
181 let items: Vec<ListItem> = self
182 .tunnels
183 .iter()
184 .map(|t| {
185 let (icon, color) = if t.is_active {
186 ("●", Color::Green)
187 } else {
188 ("○", Color::DarkGray)
189 };
190 ListItem::new(Line::from(vec![
191 format!(" {icon} ").fg(color),
192 t.name.clone().fg(Color::White),
193 ]))
194 })
195 .collect();
196
197 let list = List::new(items)
198 .block(bordered_block(Some(" Tunnels ")))
199 .highlight_style(
200 Style::default()
201 .bg(Color::DarkGray)
202 .add_modifier(Modifier::BOLD),
203 )
204 .highlight_symbol("▶ ");
205
206 f.render_stateful_widget(list, area, &mut self.list_state);
207 }
208
209 fn render_status(&self, f: &mut Frame, area: Rect) {
210 let content = match &self.message {
211 Some(msg) => Line::styled(format!(" {}", msg.text()), msg.style()),
212 None => Line::from(vec![
213 " j/k".fg(Color::Yellow),
214 " nav ".into(),
215 "Enter".fg(Color::Yellow),
216 " toggle ".into(),
217 "d".fg(Color::Yellow),
218 " details ".into(),
219 "?".fg(Color::Yellow),
220 " help ".into(),
221 "q".fg(Color::Yellow),
222 " quit".into(),
223 ]),
224 };
225 f.render_widget(Paragraph::new(content).block(bordered_block(None)), area);
226 }
227
228 fn render_details(&self, f: &mut Frame, area: Rect) {
229 let Some(tunnel) = self.selected() else {
230 f.render_widget(
231 Paragraph::new(" No tunnel selected")
232 .fg(Color::DarkGray)
233 .block(bordered_block(Some(" Details "))),
234 area,
235 );
236 return;
237 };
238
239 let mut lines = vec![
240 label("Name: ", &tunnel.name),
241 label("Config: ", &tunnel.config_path.display().to_string()),
242 Line::from(vec![
243 "Status: ".fg(Color::Yellow),
244 if tunnel.is_active {
245 "Active".fg(Color::Green)
246 } else {
247 "Inactive".fg(Color::Red)
248 },
249 ]),
250 Line::raw(""),
251 ];
252
253 if let Some(iface) = &tunnel.interface {
254 lines.push(section("Interface"));
255 if !iface.public_key.is_empty() {
256 lines.push(label("Public Key: ", &truncate_key(&iface.public_key)));
257 }
258 if let Some(port) = iface.listen_port {
259 lines.push(label("Listen Port: ", &port.to_string()));
260 }
261
262 for (i, peer) in iface.peers.iter().enumerate() {
263 lines.push(Line::raw(""));
264 if i == 0 {
265 lines.push(section(&format!("Peers ({})", iface.peers.len())));
266 }
267 lines.extend(peer_lines(peer));
268 }
269 }
270
271 f.render_widget(
272 Paragraph::new(Text::from(lines))
273 .block(bordered_block(Some(" Details ")))
274 .wrap(Wrap { trim: false }),
275 area,
276 );
277 }
278}