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(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        // ==== 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![
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        // Beta Rewards Program
190        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        // Access Logs
236        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        // Update Nodes
270        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        // Quit
327        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        // Render the tables in their respective sections
359        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                    // make sure we're in navigation mode
380                    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}