diff --git a/src/ui/activities/setup_activity/input.rs b/src/ui/activities/setup_activity/input.rs index 6d42e41..11dbd9e 100644 --- a/src/ui/activities/setup_activity/input.rs +++ b/src/ui/activities/setup_activity/input.rs @@ -385,13 +385,11 @@ impl SetupActivity { } KeyCode::Char(ch) => { // Get current input - let input: &mut String = - self.user_input.get_mut(self.user_input_ptr).unwrap(); + let input: &mut String = self.user_input.get_mut(self.user_input_ptr).unwrap(); input.push(ch); } KeyCode::Backspace => { - let input: &mut String = - self.user_input.get_mut(self.user_input_ptr).unwrap(); + let input: &mut String = self.user_input.get_mut(self.user_input_ptr).unwrap(); input.pop(); } _ => { /* Nothing to do */ } @@ -416,6 +414,8 @@ impl SetupActivity { QuitDialogOption::DontSave => self.quit = true, // Just quit QuitDialogOption::Save => self.callback_save_config_and_quit(), // Save and quit } + // Reset choice + self.quit_opt = QuitDialogOption::Save; } KeyCode::Right => { // Change option diff --git a/src/ui/activities/setup_activity/layout.rs b/src/ui/activities/setup_activity/layout.rs index 3349179..4ef84c2 100644 --- a/src/ui/activities/setup_activity/layout.rs +++ b/src/ui/activities/setup_activity/layout.rs @@ -24,7 +24,11 @@ * */ -use super::{Context, Popup, QuitDialogOption, SetupActivity, SetupTab}; +use super::{ + Context, Popup, QuitDialogOption, SetupActivity, SetupTab, UserInterfaceInputField, + YesNoDialogOption, +}; +use crate::filetransfer::FileTransferProtocol; use crate::utils::fmt::align_text_center; use tui::{ @@ -42,8 +46,551 @@ impl SetupActivity { pub(super) fn draw(&mut self) { let mut ctx: Context = self.context.take().unwrap(); let _ = ctx.terminal.draw(|f| { - // TODO: prepare layout + // Prepare main chunks + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Length(3), // Current tab + Constraint::Percentage(90), // Main body + Constraint::Length(3), // Help footer + ] + .as_ref(), + ) + .split(f.size()); + // Prepare selected tab + f.render_widget(self.draw_selected_tab(), chunks[0]); + // Draw main layout + match &self.tab { + SetupTab::SshConfig => { + // Draw ssh config + // Create explorer chunks + let sshcfg_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(100)].as_ref()) + .split(chunks[1]); + if let Some(ssh_key_tab) = self.draw_ssh_keys_list() { + // Create ssh list state + let mut ssh_key_state: ListState = ListState::default(); + ssh_key_state.select(Some(self.ssh_key_idx)); + // Render ssh keys + f.render_stateful_widget(ssh_key_tab, sshcfg_chunks[0], &mut ssh_key_state); + } + } + SetupTab::UserInterface(form_field) => { + // Create chunks + let ui_cfg_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(1), + ] + .as_ref(), + ) + .split(chunks[1]); + // Render input forms + if let Some(field) = self.draw_text_editor_input() { + f.render_widget(field, ui_cfg_chunks[0]); + } + if let Some(tab) = self.draw_default_protocol_tab() { + f.render_widget(tab, ui_cfg_chunks[1]); + } + // Set cursor + if let Some(cli) = &self.config_cli { + if matches!(form_field, UserInterfaceInputField::TextEditor) { + let editor_text: String = + String::from(cli.get_text_editor().as_path().to_string_lossy()); + f.set_cursor( + ui_cfg_chunks[0].x + editor_text.width() as u16 + 1, + ui_cfg_chunks[0].y + 1, + ) + } + } + } + } + // Draw footer + f.render_widget(self.draw_footer(), chunks[2]); + // Draw popup + if let Some(popup) = &self.popup { + // Calculate popup size + let (width, height): (u16, u16) = match popup { + Popup::Alert(_, _) | Popup::Fatal(_) => (50, 10), + Popup::Help => (50, 70), + Popup::NewSshKey => (50, 20), + Popup::Quit => (40, 10), + Popup::YesNo(_, _, _) => (30, 10), + }; + let popup_area: Rect = self.draw_popup_area(f.size(), width, height); + f.render_widget(Clear, popup_area); //this clears out the background + match popup { + Popup::Alert(color, txt) => f.render_widget( + self.draw_popup_alert(*color, txt.clone(), popup_area.width), + popup_area, + ), + Popup::Fatal(txt) => f.render_widget( + self.draw_popup_fatal(txt.clone(), popup_area.width), + popup_area, + ), + Popup::Help => f.render_widget(self.draw_popup_help(), popup_area), + Popup::NewSshKey => { + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(3), // Address form + Constraint::Length(3), // Username form + ] + .as_ref(), + ) + .split(popup_area); + let (address_form, username_form): (Paragraph, Paragraph) = + self.draw_popup_new_ssh_key(); + // Render parts + f.render_widget(address_form, popup_chunks[0]); + f.render_widget(username_form, popup_chunks[1]); + // Set cursor to popup form + if self.user_input_ptr < 2 { + if let Some(selected_text) = self.user_input.get(self.user_input_ptr) { + // Set cursor + f.set_cursor( + popup_chunks[self.user_input_ptr].x + + selected_text.width() as u16 + + 1, + popup_chunks[self.user_input_ptr].y + 1, + ) + } + } + } + Popup::Quit => f.render_widget(self.draw_popup_quit(), popup_area), + Popup::YesNo(txt, _, _) => { + f.render_widget(self.draw_popup_yesno(txt.clone()), popup_area) + } + } + } }); self.context = Some(ctx); } + + /// ### draw_selecte_tab + /// + /// Draw selected tab tab + fn draw_selected_tab(&self) -> Tabs { + let choices: Vec = vec![Spans::from("User Interface"), Spans::from("SSH Keys")]; + let index: usize = match self.tab { + SetupTab::UserInterface(_) => 0, + SetupTab::SshConfig => 1, + }; + Tabs::new(choices) + .block(Block::default().borders(Borders::BOTTOM).title("Setup")) + .select(index) + .style(Style::default()) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::Yellow), + ) + } + + /// ### draw_footer + /// + /// Draw authentication page footer + fn draw_footer(&self) -> Paragraph { + // Write header + let (footer, h_style) = ( + vec![ + Span::raw("Press "), + Span::styled( + "", + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::Cyan), + ), + Span::raw(" to show keybindings"), + ], + Style::default().add_modifier(Modifier::BOLD), + ); + let mut footer_text = Text::from(Spans::from(footer)); + footer_text.patch_style(h_style); + Paragraph::new(footer_text) + } + + /// ### draw_default_protocol_tab + /// + /// Draw default protocol input tab + fn draw_default_protocol_tab(&self) -> Option { + // Check if config client is some + match &self.config_cli { + Some(cli) => { + let choices: Vec = vec![ + Spans::from("SFTP"), + Spans::from("SCP"), + Spans::from("FTP"), + Spans::from("FTPS"), + ]; + let index: usize = match cli.get_default_protocol() { + FileTransferProtocol::Sftp => 0, + FileTransferProtocol::Scp => 1, + FileTransferProtocol::Ftp(secure) => match secure { + false => 2, + true => 3, + }, + }; + let (bg, fg, block_fg): (Color, Color, Color) = match &self.tab { + SetupTab::UserInterface(field) => match field { + UserInterfaceInputField::DefaultProtocol => { + (Color::Cyan, Color::Black, Color::Cyan) + } + _ => (Color::Reset, Color::Cyan, Color::Reset), + }, + _ => (Color::Reset, Color::Reset, Color::Reset), + }; + Some( + Tabs::new(choices) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .style(Style::default().fg(block_fg)) + .title("Default File Transfer Protocol"), + ) + .select(index) + .style(Style::default()) + .highlight_style( + Style::default().add_modifier(Modifier::BOLD).fg(fg).bg(bg), + ), + ) + } + None => None, + } + } + + /// ### draw_text_editor_input + /// + /// Draw input text field for text editor parameter + fn draw_text_editor_input(&self) -> Option { + match &self.config_cli { + Some(cli) => Some( + Paragraph::new(String::from( + cli.get_text_editor().as_path().to_string_lossy(), + )) + .style(Style::default().fg(match &self.tab { + SetupTab::SshConfig => Color::White, + SetupTab::UserInterface(field) => match field { + UserInterfaceInputField::TextEditor => Color::LightGreen, + _ => Color::White, + }, + })) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title("Text Editor"), + ), + ), + None => None, + } + } + + /// ### draw_ssh_keys_list + /// + /// Draw ssh keys list + fn draw_ssh_keys_list(&self) -> Option { + // Check if config client is some + match &self.config_cli { + Some(cli) => { + // Iterate over ssh keys + let mut ssh_keys: Vec = Vec::with_capacity(cli.iter_ssh_keys().count()); + for key in cli.iter_ssh_keys() { + if let Ok(host) = cli.get_ssh_key(key) { + if let Some((addr, username, _)) = host { + ssh_keys.push(ListItem::new(Span::from(format!( + "{} at {}", + username, addr, + )))); + } else { + continue; + } + } else { + continue; + } + } + // Return list + Some( + List::new(ssh_keys) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::LightGreen)) + .title("SSH Keys"), + ) + .start_corner(Corner::TopLeft) + .highlight_style( + Style::default() + .fg(Color::Black) + .bg(Color::LightGreen) + .add_modifier(Modifier::BOLD), + ), + ) + } + None => None, + } + } + + /// ### draw_popup_area + /// + /// Draw popup area + fn draw_popup_area(&self, area: Rect, width: u16, height: u16) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage((100 - height) / 2), + Constraint::Percentage(height), + Constraint::Percentage((100 - height) / 2), + ] + .as_ref(), + ) + .split(area); + Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage((100 - width) / 2), + Constraint::Percentage(width), + Constraint::Percentage((100 - width) / 2), + ] + .as_ref(), + ) + .split(popup_layout[1])[1] + } + + /// ### draw_popup_alert + /// + /// Draw alert popup + fn draw_popup_alert(&self, color: Color, text: String, width: u16) -> List { + // Wraps texts + let message_rows = textwrap::wrap(text.as_str(), width as usize); + let mut lines: Vec = Vec::new(); + for msg in message_rows.iter() { + lines.push(ListItem::new(Spans::from(align_text_center(msg, width)))); + } + List::new(lines) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(color)) + .border_type(BorderType::Rounded) + .title("Alert"), + ) + .start_corner(Corner::TopLeft) + .style(Style::default().fg(color)) + } + + /// ### draw_popup_fatal + /// + /// Draw fatal error popup + fn draw_popup_fatal(&self, text: String, width: u16) -> List { + self.draw_popup_alert(Color::Red, text, width) + } + + /// ### draw_popup_new_ssh_key + /// + /// Draw new ssh key form popup + fn draw_popup_new_ssh_key(&self) -> (Paragraph, Paragraph) { + let address: Paragraph = Paragraph::new(self.user_input.get(0).unwrap().as_str()) + .style(Style::default().fg(match self.user_input_ptr { + 0 => Color::LightCyan, + _ => Color::White, + })) + .block( + Block::default() + .borders(Borders::TOP | Borders::RIGHT | Borders::LEFT) + .border_type(BorderType::Rounded) + .title("Host name or address"), + ); + let username: Paragraph = Paragraph::new(self.user_input.get(1).unwrap().as_str()) + .style(Style::default().fg(match self.user_input_ptr { + 1 => Color::LightMagenta, + _ => Color::White, + })) + .block( + Block::default() + .borders(Borders::TOP | Borders::RIGHT | Borders::LEFT) + .border_type(BorderType::Rounded) + .title("Username"), + ); + (address, username) + } + + /// ### draw_popup_quit + /// + /// Draw quit select popup + fn draw_popup_quit(&self) -> Tabs { + let choices: Vec = vec![ + Spans::from("Save"), + Spans::from("Don't save"), + Spans::from("Cancel"), + ]; + let index: usize = match self.quit_opt { + QuitDialogOption::Save => 0, + QuitDialogOption::DontSave => 1, + QuitDialogOption::Cancel => 2, + }; + Tabs::new(choices) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title("Exit setup?"), + ) + .select(index) + .style(Style::default()) + .highlight_style(Style::default().add_modifier(Modifier::BOLD).fg(Color::Red)) + } + + /// ### draw_popup_yesno + /// + /// Draw yes/no select popup + fn draw_popup_yesno(&self, text: String) -> Tabs { + let choices: Vec = vec![Spans::from("Yes"), Spans::from("No")]; + let index: usize = match self.yesno_opt { + YesNoDialogOption::Yes => 0, + YesNoDialogOption::No => 1, + }; + Tabs::new(choices) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title(text), + ) + .select(index) + .style(Style::default()) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::Yellow), + ) + } + + /// ### draw_popup_help + /// + /// Draw authentication page help popup + pub(super) fn draw_popup_help(&self) -> List { + // Write header + let cmds: Vec = vec![ + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("Exit setup"), + ])), + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("Change setup page"), + ])), + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("Change selected element in tab"), + ])), + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("Change input field"), + ])), + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("Submit / Dismiss popup"), + ])), + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("Delete entry"), + ])), + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("Show help"), + ])), + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("New SSH key"), + ])), + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("Revert changes"), + ])), + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("Save configuration"), + ])), + ]; + List::new(cmds) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default()) + .border_type(BorderType::Rounded) + .title("Help"), + ) + .start_corner(Corner::TopLeft) + } } diff --git a/src/ui/activities/setup_activity/mod.rs b/src/ui/activities/setup_activity/mod.rs index 5e9401a..1d085f4 100644 --- a/src/ui/activities/setup_activity/mod.rs +++ b/src/ui/activities/setup_activity/mod.rs @@ -110,6 +110,7 @@ pub struct SetupActivity { quit_opt: QuitDialogOption, // Popup::Quit selected option yesno_opt: YesNoDialogOption, // Popup::YesNo selected option ssh_key_idx: usize, // Index of selected ssh key in list + redraw: bool, // Redraw ui? } impl Default for SetupActivity { @@ -130,6 +131,7 @@ impl Default for SetupActivity { quit_opt: QuitDialogOption::Save, yesno_opt: YesNoDialogOption::Yes, ssh_key_idx: 0, + redraw: true, // Draw at first `on_draw` } } } @@ -162,20 +164,21 @@ impl Activity for SetupActivity { if self.context.is_none() { return; } - let mut redraw: bool = false; // Read one event if let Ok(event) = self.context.as_ref().unwrap().input_hnd.read_event() { if let Some(event) = event { // Set redraw to true - redraw = true; + self.redraw = true; // Handle event self.handle_input_event(&event); } } // Redraw if necessary - if redraw { + if self.redraw { // Draw self.draw(); + // Redraw back to false + self.redraw = false; } }