node_launchpad/components/
options.rs

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