1use 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 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 let header = Header::new();
90 f.render_stateful_widget(header, layout[0], &mut SelectedMenuItem::Options);
91
92 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 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 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 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 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 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 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}