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(7),
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![Line::from(vec![])]),
97 Row::new(vec![
98 Cell::from(
99 Line::from(vec![Span::styled(
100 " Storage Drive: ",
101 Style::default().fg(LIGHT_PERIWINKLE),
102 )])
103 .alignment(Alignment::Left),
104 ),
105 Cell::from(
106 Line::from(vec![Span::styled(
107 format!(" {} ", self.storage_drive),
108 Style::default().fg(VIVID_SKY_BLUE),
109 )])
110 .alignment(Alignment::Left),
111 ),
112 Cell::from(
113 Line::from(vec![
114 Span::styled(" Change Drive ", Style::default().fg(VERY_LIGHT_AZURE)),
115 Span::styled(" [Ctrl+D] ", Style::default().fg(GHOST_WHITE)),
116 ])
117 .alignment(Alignment::Right),
118 ),
119 ]),
120 Row::new(vec![
121 Cell::from(
122 Line::from(vec![Span::styled(
123 " Connection Mode: ",
124 Style::default().fg(LIGHT_PERIWINKLE),
125 )])
126 .alignment(Alignment::Left),
127 ),
128 Cell::from(
129 Line::from(vec![Span::styled(
130 format!(" {} ", self.connection_mode),
131 Style::default().fg(VIVID_SKY_BLUE),
132 )])
133 .alignment(Alignment::Left),
134 ),
135 Cell::from(
136 Line::from(vec![
137 Span::styled(" Change Mode ", Style::default().fg(VERY_LIGHT_AZURE)),
138 Span::styled(" [Ctrl+K] ", Style::default().fg(GHOST_WHITE)),
139 ])
140 .alignment(Alignment::Right),
141 ),
142 ]),
143 Row::new(vec![
144 Cell::from(
145 Line::from(vec![Span::styled(
146 " Port Range: ",
147 Style::default().fg(LIGHT_PERIWINKLE),
148 )])
149 .alignment(Alignment::Left),
150 ),
151 Cell::from(
152 Line::from(vec![
153 if self.connection_mode == ConnectionMode::CustomPorts {
154 Span::styled(
155 format!(
156 " {}-{} ",
157 self.port_from.unwrap_or(0),
158 self.port_to.unwrap_or(0)
159 ),
160 Style::default().fg(VIVID_SKY_BLUE),
161 )
162 } else {
163 Span::styled(" Auto ", Style::default().fg(COOL_GREY))
164 },
165 ])
166 .alignment(Alignment::Left),
167 ),
168 Cell::from(
169 Line::from(if self.connection_mode == ConnectionMode::CustomPorts {
170 vec![
171 Span::styled(port_legend, Style::default().fg(VERY_LIGHT_AZURE)),
172 Span::styled(port_key, Style::default().fg(GHOST_WHITE)),
173 ]
174 } else {
175 vec![]
176 })
177 .alignment(Alignment::Right),
178 ),
179 ]),
180 Row::new(vec![Line::from(vec![])]),
181 ],
182 &[
183 Constraint::Length(18),
184 Constraint::Fill(1),
185 Constraint::Length((port_legend.len() + port_key.len()) as u16),
186 ],
187 )
188 .block(block1)
189 .style(Style::default().fg(GHOST_WHITE));
190
191 let beta_legend = if self.rewards_address.is_empty() {
193 " Add Wallet "
194 } else {
195 " Change Wallet "
196 };
197 let beta_key = " [Ctrl+B] ";
198 let block2 = Block::default()
199 .title(" Wallet ")
200 .title_style(Style::default().bold().fg(GHOST_WHITE))
201 .style(Style::default().fg(GHOST_WHITE))
202 .borders(Borders::ALL)
203 .border_style(Style::default().fg(VERY_LIGHT_AZURE));
204 let beta_rewards = Table::new(
205 vec![Row::new(vec![
206 Cell::from(
207 Line::from(vec![Span::styled(
208 " Wallet Address: ",
209 Style::default().fg(LIGHT_PERIWINKLE),
210 )])
211 .alignment(Alignment::Left),
212 ),
213 Cell::from(
214 Line::from(vec![Span::styled(
215 format!(" {} ", self.rewards_address),
216 Style::default().fg(VIVID_SKY_BLUE),
217 )])
218 .alignment(Alignment::Left),
219 ),
220 Cell::from(
221 Line::from(vec![
222 Span::styled(beta_legend, Style::default().fg(VERY_LIGHT_AZURE)),
223 Span::styled(beta_key, Style::default().fg(GHOST_WHITE)),
224 ])
225 .alignment(Alignment::Right),
226 ),
227 ])],
228 &[
229 Constraint::Length(18),
230 Constraint::Fill(1),
231 Constraint::Length((beta_legend.len() + beta_key.len()) as u16),
232 ],
233 )
234 .block(block2)
235 .style(Style::default().fg(GHOST_WHITE));
236
237 let logs_legend = " Access Logs ";
239 let logs_key = " [Ctrl+L] ";
240 let block3 = Block::default()
241 .title(" Access Logs ")
242 .title_style(Style::default().bold().fg(GHOST_WHITE))
243 .style(Style::default().fg(GHOST_WHITE))
244 .borders(Borders::ALL)
245 .border_style(Style::default().fg(VERY_LIGHT_AZURE));
246 let logs_folder = Table::new(
247 vec![Row::new(vec![
248 Cell::from(
249 Line::from(vec![Span::styled(
250 " Open the Logs folder on this device ",
251 Style::default().fg(LIGHT_PERIWINKLE),
252 )])
253 .alignment(Alignment::Left),
254 ),
255 Cell::from(
256 Line::from(vec![
257 Span::styled(logs_legend, Style::default().fg(VERY_LIGHT_AZURE)),
258 Span::styled(logs_key, Style::default().fg(GHOST_WHITE)),
259 ])
260 .alignment(Alignment::Right),
261 ),
262 ])],
263 &[
264 Constraint::Fill(1),
265 Constraint::Length((logs_legend.len() + logs_key.len()) as u16),
266 ],
267 )
268 .block(block3)
269 .style(Style::default().fg(GHOST_WHITE));
270
271 let reset_legend = " Begin Reset ";
273 let reset_key = " [Ctrl+R] ";
274 let upgrade_legend = " Begin Upgrade ";
275 let upgrade_key = " [Ctrl+U] ";
276 let block4 = Block::default()
277 .title(" Update Nodes ")
278 .title_style(Style::default().bold().fg(GHOST_WHITE))
279 .style(Style::default().fg(GHOST_WHITE))
280 .borders(Borders::ALL)
281 .border_style(Style::default().fg(EUCALYPTUS));
282 let reset_nodes = Table::new(
283 vec![
284 Row::new(vec![
285 Cell::from(
286 Line::from(vec![Span::styled(
287 " Upgrade all Nodes ",
288 Style::default().fg(LIGHT_PERIWINKLE),
289 )])
290 .alignment(Alignment::Left),
291 ),
292 Cell::from(
293 Line::from(vec![
294 Span::styled(upgrade_legend, Style::default().fg(EUCALYPTUS)),
295 Span::styled(upgrade_key, Style::default().fg(GHOST_WHITE)),
296 ])
297 .alignment(Alignment::Right),
298 ),
299 ]),
300 Row::new(vec![
301 Cell::from(
302 Line::from(vec![Span::styled(
303 " Reset all Nodes on this device ",
304 Style::default().fg(LIGHT_PERIWINKLE),
305 )])
306 .alignment(Alignment::Left),
307 ),
308 Cell::from(
309 Line::from(vec![
310 Span::styled(reset_legend, Style::default().fg(EUCALYPTUS)),
311 Span::styled(reset_key, Style::default().fg(GHOST_WHITE)),
312 ])
313 .alignment(Alignment::Right),
314 ),
315 ]),
316 ],
317 &[
318 Constraint::Fill(1),
319 Constraint::Length(
320 (max(reset_legend.len(), upgrade_legend.len())
321 + max(reset_key.len(), upgrade_key.len())) as u16,
322 ),
323 ],
324 )
325 .block(block4)
326 .style(Style::default().fg(GHOST_WHITE));
327
328 let quit_legend = "Quit ";
330 let quit_key = "[Q] ";
331 let block5 = Block::default()
332 .style(Style::default().fg(GHOST_WHITE))
333 .borders(Borders::ALL)
334 .border_style(Style::default().fg(VIVID_SKY_BLUE));
335 let quit = Table::new(
336 vec![Row::new(vec![
337 Cell::from(
338 Line::from(vec![Span::styled(
339 " Close Launchpad (your nodes will keep running in the background) ",
340 Style::default().fg(LIGHT_PERIWINKLE),
341 )])
342 .alignment(Alignment::Left),
343 ),
344 Cell::from(
345 Line::from(vec![
346 Span::styled(quit_legend, Style::default().fg(VIVID_SKY_BLUE)),
347 Span::styled(quit_key, Style::default().fg(GHOST_WHITE)),
348 ])
349 .alignment(Alignment::Right),
350 ),
351 ])],
352 &[
353 Constraint::Fill(1),
354 Constraint::Length((quit_legend.len() + quit_key.len()) as u16),
355 ],
356 )
357 .block(block5)
358 .style(Style::default().fg(GHOST_WHITE));
359
360 f.render_widget(storage_drivename, layout[1]);
362 f.render_widget(beta_rewards, layout[2]);
363 f.render_widget(logs_folder, layout[3]);
364 f.render_widget(reset_nodes, layout[4]);
365 f.render_widget(quit, layout[5]);
366
367 Ok(())
368 }
369
370 fn update(&mut self, action: Action) -> Result<Option<Action>> {
371 match action {
372 Action::SwitchScene(scene) => match scene {
373 Scene::Options
374 | Scene::ChangeDrivePopUp
375 | Scene::ChangeConnectionModePopUp
376 | Scene::ChangePortsPopUp { .. }
377 | Scene::OptionsRewardsAddressPopUp
378 | Scene::ResetNodesPopUp
379 | Scene::UpgradeNodesPopUp => {
380 self.active = true;
381 return Ok(Some(Action::SwitchInputMode(InputMode::Navigation)));
383 }
384 _ => self.active = false,
385 },
386 Action::OptionsActions(action) => match action {
387 OptionsActions::TriggerChangeDrive => {
388 return Ok(Some(Action::SwitchScene(Scene::ChangeDrivePopUp)));
389 }
390 OptionsActions::UpdateStorageDrive(mountpoint, drive) => {
391 self.storage_mountpoint = mountpoint;
392 self.storage_drive = drive;
393 }
394 OptionsActions::TriggerChangeConnectionMode => {
395 return Ok(Some(Action::SwitchScene(Scene::ChangeConnectionModePopUp)));
396 }
397 OptionsActions::UpdateConnectionMode(mode) => {
398 self.connection_mode = mode;
399 }
400 OptionsActions::TriggerChangePortRange => {
401 return Ok(Some(Action::SwitchScene(Scene::ChangePortsPopUp {
402 connection_mode_old_value: None,
403 })));
404 }
405 OptionsActions::UpdatePortRange(from, to) => {
406 self.port_from = Some(from);
407 self.port_to = Some(to);
408 }
409 OptionsActions::TriggerRewardsAddress => {
410 return Ok(Some(Action::SwitchScene(Scene::OptionsRewardsAddressPopUp)));
411 }
412 OptionsActions::UpdateRewardsAddress(rewards_address) => {
413 self.rewards_address = rewards_address;
414 }
415 OptionsActions::TriggerAccessLogs => {
416 open_logs(None)?;
417 }
418 OptionsActions::TriggerUpdateNodes => {
419 return Ok(Some(Action::SwitchScene(Scene::UpgradeNodesPopUp)));
420 }
421 OptionsActions::TriggerResetNodes => {
422 return Ok(Some(Action::SwitchScene(Scene::ResetNodesPopUp)))
423 }
424 _ => {}
425 },
426 _ => {}
427 }
428 Ok(None)
429 }
430}