1use std::{cmp::max, path::PathBuf};
2
3use color_eyre::eyre::Result;
4use ratatui::{
5 layout::{Alignment, Constraint, Direction, Layout, Rect},
6 style::{Style, Stylize},
7 text::{Line, Span},
8 widgets::{Block, Borders, Cell, Row, Table},
9 Frame,
10};
11use tokio::sync::mpsc::UnboundedSender;
12
13use super::{header::SelectedMenuItem, utils::open_logs, Component};
14use crate::{
15 action::{Action, OptionsActions},
16 components::header::Header,
17 connection_mode::ConnectionMode,
18 mode::{InputMode, Scene},
19 style::{
20 COOL_GREY, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE, VIVID_SKY_BLUE,
21 },
22};
23
24#[derive(Clone)]
25pub struct Options {
26 pub storage_mountpoint: PathBuf,
27 pub storage_drive: String,
28 pub rewards_address: String,
29 pub connection_mode: ConnectionMode,
30 pub port_edit: bool,
31 pub port_from: Option<u32>,
32 pub port_to: Option<u32>,
33 pub active: bool,
34 pub action_tx: Option<UnboundedSender<Action>>,
35}
36
37impl Options {
38 pub async fn new(
39 storage_mountpoint: PathBuf,
40 storage_drive: String,
41 rewards_address: String,
42 connection_mode: ConnectionMode,
43 port_from: Option<u32>,
44 port_to: Option<u32>,
45 ) -> Result<Self> {
46 Ok(Self {
47 storage_mountpoint,
48 storage_drive,
49 rewards_address,
50 connection_mode,
51 port_edit: false,
52 port_from,
53 port_to,
54 active: false,
55 action_tx: None,
56 })
57 }
58}
59
60impl Component for Options {
61 fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
62 if !self.active {
63 return Ok(());
64 }
65 let layout = Layout::default()
67 .direction(Direction::Vertical)
68 .constraints(
69 [
70 Constraint::Length(1),
71 Constraint::Length(5),
72 Constraint::Length(3),
73 Constraint::Length(3),
74 Constraint::Length(4),
75 Constraint::Length(3),
76 ]
77 .as_ref(),
78 )
79 .split(area);
80
81 let header = Header::new();
83 f.render_stateful_widget(header, layout[0], &mut SelectedMenuItem::Options);
84
85 let port_legend = " Edit Port Range ";
87 let port_key = " [Ctrl+P] ";
88 let block1 = Block::default()
89 .title(" Device Options ")
90 .title_style(Style::default().bold().fg(GHOST_WHITE))
91 .style(Style::default().fg(GHOST_WHITE))
92 .borders(Borders::ALL)
93 .border_style(Style::default().fg(VERY_LIGHT_AZURE));
94 let storage_drivename = Table::new(
95 vec![
96 Row::new(vec![
97 Cell::from(
98 Line::from(vec![Span::styled(
99 " Storage Drive: ",
100 Style::default().fg(LIGHT_PERIWINKLE),
101 )])
102 .alignment(Alignment::Left),
103 ),
104 Cell::from(
105 Line::from(vec![Span::styled(
106 format!(" {} ", self.storage_drive),
107 Style::default().fg(VIVID_SKY_BLUE),
108 )])
109 .alignment(Alignment::Left),
110 ),
111 Cell::from(
112 Line::from(vec![
113 Span::styled(" Change Drive ", Style::default().fg(VERY_LIGHT_AZURE)),
114 Span::styled(" [Ctrl+D] ", Style::default().fg(GHOST_WHITE)),
115 ])
116 .alignment(Alignment::Right),
117 ),
118 ]),
119 Row::new(vec![
120 Cell::from(
121 Line::from(vec![Span::styled(
122 " Connection Mode: ",
123 Style::default().fg(LIGHT_PERIWINKLE),
124 )])
125 .alignment(Alignment::Left),
126 ),
127 Cell::from(
128 Line::from(vec![Span::styled(
129 format!(" {} ", self.connection_mode),
130 Style::default().fg(VIVID_SKY_BLUE),
131 )])
132 .alignment(Alignment::Left),
133 ),
134 Cell::from(
135 Line::from(vec![
136 Span::styled(" Change Mode ", Style::default().fg(VERY_LIGHT_AZURE)),
137 Span::styled(" [Ctrl+K] ", Style::default().fg(GHOST_WHITE)),
138 ])
139 .alignment(Alignment::Right),
140 ),
141 ]),
142 Row::new(vec![
143 Cell::from(
144 Line::from(vec![Span::styled(
145 " Port Range: ",
146 Style::default().fg(LIGHT_PERIWINKLE),
147 )])
148 .alignment(Alignment::Left),
149 ),
150 Cell::from(
151 Line::from(vec![
152 if self.connection_mode == ConnectionMode::CustomPorts {
153 Span::styled(
154 format!(
155 " {}-{} ",
156 self.port_from.unwrap_or(0),
157 self.port_to.unwrap_or(0)
158 ),
159 Style::default().fg(VIVID_SKY_BLUE),
160 )
161 } else {
162 Span::styled(" Auto ", Style::default().fg(COOL_GREY))
163 },
164 ])
165 .alignment(Alignment::Left),
166 ),
167 Cell::from(
168 Line::from(if self.connection_mode == ConnectionMode::CustomPorts {
169 vec![
170 Span::styled(port_legend, Style::default().fg(VERY_LIGHT_AZURE)),
171 Span::styled(port_key, Style::default().fg(GHOST_WHITE)),
172 ]
173 } else {
174 vec![]
175 })
176 .alignment(Alignment::Right),
177 ),
178 ]),
179 ],
180 &[
181 Constraint::Length(18),
182 Constraint::Fill(1),
183 Constraint::Length((port_legend.len() + port_key.len()) as u16),
184 ],
185 )
186 .block(block1)
187 .style(Style::default().fg(GHOST_WHITE));
188
189 let beta_legend = if self.rewards_address.is_empty() {
191 " Add Wallet "
192 } else {
193 " Change Wallet "
194 };
195 let beta_key = " [Ctrl+B] ";
196 let block2 = Block::default()
197 .title(" Wallet ")
198 .title_style(Style::default().bold().fg(GHOST_WHITE))
199 .style(Style::default().fg(GHOST_WHITE))
200 .borders(Borders::ALL)
201 .border_style(Style::default().fg(VERY_LIGHT_AZURE));
202 let beta_rewards = Table::new(
203 vec![Row::new(vec![
204 Cell::from(
205 Line::from(vec![Span::styled(
206 " Wallet Address: ",
207 Style::default().fg(LIGHT_PERIWINKLE),
208 )])
209 .alignment(Alignment::Left),
210 ),
211 Cell::from(
212 Line::from(vec![Span::styled(
213 format!(" {} ", self.rewards_address),
214 Style::default().fg(VIVID_SKY_BLUE),
215 )])
216 .alignment(Alignment::Left),
217 ),
218 Cell::from(
219 Line::from(vec![
220 Span::styled(beta_legend, Style::default().fg(VERY_LIGHT_AZURE)),
221 Span::styled(beta_key, Style::default().fg(GHOST_WHITE)),
222 ])
223 .alignment(Alignment::Right),
224 ),
225 ])],
226 &[
227 Constraint::Length(18),
228 Constraint::Fill(1),
229 Constraint::Length((beta_legend.len() + beta_key.len()) as u16),
230 ],
231 )
232 .block(block2)
233 .style(Style::default().fg(GHOST_WHITE));
234
235 let logs_legend = " Access Logs ";
237 let logs_key = " [Ctrl+L] ";
238 let block3 = Block::default()
239 .title(" Access Logs ")
240 .title_style(Style::default().bold().fg(GHOST_WHITE))
241 .style(Style::default().fg(GHOST_WHITE))
242 .borders(Borders::ALL)
243 .border_style(Style::default().fg(VERY_LIGHT_AZURE));
244 let logs_folder = Table::new(
245 vec![Row::new(vec![
246 Cell::from(
247 Line::from(vec![Span::styled(
248 " Open the Logs folder on this device ",
249 Style::default().fg(LIGHT_PERIWINKLE),
250 )])
251 .alignment(Alignment::Left),
252 ),
253 Cell::from(
254 Line::from(vec![
255 Span::styled(logs_legend, Style::default().fg(VERY_LIGHT_AZURE)),
256 Span::styled(logs_key, Style::default().fg(GHOST_WHITE)),
257 ])
258 .alignment(Alignment::Right),
259 ),
260 ])],
261 &[
262 Constraint::Fill(1),
263 Constraint::Length((logs_legend.len() + logs_key.len()) as u16),
264 ],
265 )
266 .block(block3)
267 .style(Style::default().fg(GHOST_WHITE));
268
269 let reset_legend = " Begin Reset ";
271 let reset_key = " [Ctrl+R] ";
272 let upgrade_legend = " Begin Upgrade ";
273 let upgrade_key = " [Ctrl+U] ";
274 let block4 = Block::default()
275 .title(" Update Nodes ")
276 .title_style(Style::default().bold().fg(GHOST_WHITE))
277 .style(Style::default().fg(GHOST_WHITE))
278 .borders(Borders::ALL)
279 .border_style(Style::default().fg(EUCALYPTUS));
280 let reset_nodes = Table::new(
281 vec![
282 Row::new(vec![
283 Cell::from(
284 Line::from(vec![Span::styled(
285 " Upgrade all Nodes ",
286 Style::default().fg(LIGHT_PERIWINKLE),
287 )])
288 .alignment(Alignment::Left),
289 ),
290 Cell::from(
291 Line::from(vec![
292 Span::styled(upgrade_legend, Style::default().fg(EUCALYPTUS)),
293 Span::styled(upgrade_key, Style::default().fg(GHOST_WHITE)),
294 ])
295 .alignment(Alignment::Right),
296 ),
297 ]),
298 Row::new(vec![
299 Cell::from(
300 Line::from(vec![Span::styled(
301 " Reset all Nodes on this device ",
302 Style::default().fg(LIGHT_PERIWINKLE),
303 )])
304 .alignment(Alignment::Left),
305 ),
306 Cell::from(
307 Line::from(vec![
308 Span::styled(reset_legend, Style::default().fg(EUCALYPTUS)),
309 Span::styled(reset_key, Style::default().fg(GHOST_WHITE)),
310 ])
311 .alignment(Alignment::Right),
312 ),
313 ]),
314 ],
315 &[
316 Constraint::Fill(1),
317 Constraint::Length(
318 (max(reset_legend.len(), upgrade_legend.len())
319 + max(reset_key.len(), upgrade_key.len())) as u16,
320 ),
321 ],
322 )
323 .block(block4)
324 .style(Style::default().fg(GHOST_WHITE));
325
326 let quit_legend = "Quit ";
328 let quit_key = "[Q] ";
329 let block5 = Block::default()
330 .style(Style::default().fg(GHOST_WHITE))
331 .borders(Borders::ALL)
332 .border_style(Style::default().fg(VIVID_SKY_BLUE));
333 let quit = Table::new(
334 vec![Row::new(vec![
335 Cell::from(
336 Line::from(vec![Span::styled(
337 " Close Launchpad (your nodes will keep running in the background) ",
338 Style::default().fg(LIGHT_PERIWINKLE),
339 )])
340 .alignment(Alignment::Left),
341 ),
342 Cell::from(
343 Line::from(vec![
344 Span::styled(quit_legend, Style::default().fg(VIVID_SKY_BLUE)),
345 Span::styled(quit_key, Style::default().fg(GHOST_WHITE)),
346 ])
347 .alignment(Alignment::Right),
348 ),
349 ])],
350 &[
351 Constraint::Fill(1),
352 Constraint::Length((quit_legend.len() + quit_key.len()) as u16),
353 ],
354 )
355 .block(block5)
356 .style(Style::default().fg(GHOST_WHITE));
357
358 f.render_widget(storage_drivename, layout[1]);
360 f.render_widget(beta_rewards, layout[2]);
361 f.render_widget(logs_folder, layout[3]);
362 f.render_widget(reset_nodes, layout[4]);
363 f.render_widget(quit, layout[5]);
364
365 Ok(())
366 }
367
368 fn update(&mut self, action: Action) -> Result<Option<Action>> {
369 match action {
370 Action::SwitchScene(scene) => match scene {
371 Scene::Options
372 | Scene::ChangeDrivePopUp
373 | Scene::ChangeConnectionModePopUp
374 | Scene::ChangePortsPopUp { .. }
375 | Scene::OptionsRewardsAddressPopUp
376 | Scene::ResetNodesPopUp
377 | Scene::UpgradeNodesPopUp => {
378 self.active = true;
379 return Ok(Some(Action::SwitchInputMode(InputMode::Navigation)));
381 }
382 _ => self.active = false,
383 },
384 Action::OptionsActions(action) => match action {
385 OptionsActions::TriggerChangeDrive => {
386 return Ok(Some(Action::SwitchScene(Scene::ChangeDrivePopUp)));
387 }
388 OptionsActions::UpdateStorageDrive(mountpoint, drive) => {
389 self.storage_mountpoint = mountpoint;
390 self.storage_drive = drive;
391 }
392 OptionsActions::TriggerChangeConnectionMode => {
393 return Ok(Some(Action::SwitchScene(Scene::ChangeConnectionModePopUp)));
394 }
395 OptionsActions::UpdateConnectionMode(mode) => {
396 self.connection_mode = mode;
397 }
398 OptionsActions::TriggerChangePortRange => {
399 return Ok(Some(Action::SwitchScene(Scene::ChangePortsPopUp {
400 connection_mode_old_value: None,
401 })));
402 }
403 OptionsActions::UpdatePortRange(from, to) => {
404 self.port_from = Some(from);
405 self.port_to = Some(to);
406 }
407 OptionsActions::TriggerRewardsAddress => {
408 return Ok(Some(Action::SwitchScene(Scene::OptionsRewardsAddressPopUp)));
409 }
410 OptionsActions::UpdateRewardsAddress(rewards_address) => {
411 self.rewards_address = rewards_address;
412 }
413 OptionsActions::TriggerAccessLogs => {
414 open_logs(None)?;
415 }
416 OptionsActions::TriggerUpdateNodes => {
417 return Ok(Some(Action::SwitchScene(Scene::UpgradeNodesPopUp)));
418 }
419 OptionsActions::TriggerResetNodes => {
420 return Ok(Some(Action::SwitchScene(Scene::ResetNodesPopUp)))
421 }
422 _ => {}
423 },
424 _ => {}
425 }
426 Ok(None)
427 }
428}