node_launchpad/components/
options.rs

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        // Define the layout to split the area into four sections
66        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        // ==== Header =====
82        let header = Header::new();
83        f.render_stateful_widget(header, layout[0], &mut SelectedMenuItem::Options);
84
85        // Storage Drive
86        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        // Beta Rewards Program
192        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        // Access Logs
238        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        // Update Nodes
272        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        // Quit
329        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        // Render the tables in their respective sections
361        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                    // make sure we're in navigation mode
382                    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}