//! ## Popups //! //! popups components mod chmod; mod goto; use std::time::UNIX_EPOCH; use bytesize::ByteSize; use remotefs::File; use tui_realm_stdlib::{Input, List, Paragraph, ProgressBar, Radio, Span}; use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::props::{ Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TableBuilder, TextSpan, }; use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; #[cfg(posix)] use uzers::{get_group_by_gid, get_user_by_uid}; pub use self::chmod::ChmodPopup; pub use self::goto::{ATTR_FILES, GotoPopup}; use super::super::Browser; use super::{Msg, PendingActionMsg, TransferMsg, UiMsg}; use crate::explorer::FileSorting; use crate::utils::fmt::fmt_time; #[derive(MockComponent)] pub struct CopyPopup { component: Input, } impl CopyPopup { pub fn new(color: Color) -> Self { Self { component: Input::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .input_type(InputType::Text) .placeholder( "destination", Style::default().fg(Color::Rgb(128, 128, 128)), ) .title("Copy file(s) to…", Alignment::Center), } } } impl Component for CopyPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Home, .. }) => { self.perform(Cmd::GoTo(Position::Begin)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::End, .. }) => { self.perform(Cmd::GoTo(Position::End)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Delete, .. }) => { self.perform(Cmd::Cancel); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Backspace, .. }) => { self.perform(Cmd::Delete); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Char(ch), .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => match self.state() { State::One(StateValue::String(i)) => { Some(Msg::Transfer(TransferMsg::CopyFileTo(i))) } _ => Some(Msg::None), }, Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseCopyPopup)) } _ => None, } } } #[derive(MockComponent)] pub struct FilterPopup { component: Input, } impl FilterPopup { pub fn new(color: Color) -> Self { Self { component: Input::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .input_type(InputType::Text) .placeholder( "regex or wildmatch", Style::default().fg(Color::Rgb(128, 128, 128)), ) .title( "Filter files by regex or wildmatch in the current directory", Alignment::Center, ), } } } impl Component for FilterPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Home, .. }) => { self.perform(Cmd::GoTo(Position::Begin)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::End, .. }) => { self.perform(Cmd::GoTo(Position::End)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Delete, .. }) => { self.perform(Cmd::Cancel); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Backspace, .. }) => { self.perform(Cmd::Delete); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Char(ch), .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => match self.state() { State::One(StateValue::String(filter)) => Some(Msg::Ui(UiMsg::FilterFiles(filter))), _ => Some(Msg::None), }, Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseFilterPopup)) } _ => None, } } } #[derive(MockComponent)] pub struct DeletePopup { component: Radio, } impl DeletePopup { pub fn new(color: Color) -> Self { Self { component: Radio::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .choices(["Yes", "No"]) .value(1) .title("Delete file(s)?", Alignment::Center), } } } impl Component for DeletePopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseDeletePopup)) } Event::Keyboard(KeyEvent { code: Key::Char('y'), modifiers: KeyModifiers::NONE, }) => Some(Msg::Transfer(TransferMsg::DeleteFile)), Event::Keyboard(KeyEvent { code: Key::Char('n'), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::CloseDeletePopup)), Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => { if matches!( self.perform(Cmd::Submit), CmdResult::Submit(State::One(StateValue::Usize(0))) ) { Some(Msg::Transfer(TransferMsg::DeleteFile)) } else { Some(Msg::Ui(UiMsg::CloseDeletePopup)) } } _ => None, } } } #[derive(MockComponent)] pub struct DisconnectPopup { component: Radio, } impl DisconnectPopup { pub fn new(color: Color) -> Self { Self { component: Radio::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .choices(["Yes", "No"]) .title("Are you sure you want to disconnect?", Alignment::Center), } } } impl Component for DisconnectPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseDisconnectPopup)) } Event::Keyboard(KeyEvent { code: Key::Char('y'), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::Disconnect)), Event::Keyboard(KeyEvent { code: Key::Char('n'), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::CloseDisconnectPopup)), Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => { if matches!( self.perform(Cmd::Submit), CmdResult::Submit(State::One(StateValue::Usize(0))) ) { Some(Msg::Ui(UiMsg::Disconnect)) } else { Some(Msg::Ui(UiMsg::CloseDisconnectPopup)) } } _ => None, } } } #[derive(MockComponent)] pub struct ErrorPopup { component: Paragraph, } impl ErrorPopup { pub fn new>(text: S, color: Color) -> Self { Self { component: Paragraph::default() .alignment(Alignment::Center) .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .text([TextSpan::from(text.as_ref())]) .wrap(true), } } } impl Component for ErrorPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Esc | Key::Enter, .. }) => Some(Msg::Ui(UiMsg::CloseErrorPopup)), _ => None, } } } #[derive(MockComponent)] pub struct FatalPopup { component: Paragraph, } impl FatalPopup { pub fn new>(text: S, color: Color) -> Self { Self { component: Paragraph::default() .alignment(Alignment::Center) .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .text([TextSpan::from(text.as_ref())]) .wrap(true), } } } impl Component for FatalPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Esc | Key::Enter, .. }) => Some(Msg::Ui(UiMsg::CloseFatalPopup)), _ => None, } } } #[derive(MockComponent)] pub struct FileInfoPopup { component: List, } impl FileInfoPopup { pub fn new(file: &File) -> Self { let mut texts: TableBuilder = TableBuilder::default(); // Abs path let real_path = file.metadata().symlink.as_deref(); let path: String = match real_path { Some(symlink) => format!("{} -> {}", file.path().display(), symlink.display()), None => format!("{}", file.path().display()), }; // Make texts texts .add_col(TextSpan::from("Path: ")) .add_col(TextSpan::new(path.as_str()).fg(Color::Yellow)); texts .add_row() .add_col(TextSpan::from("Name: ")) .add_col(TextSpan::new(file.name()).fg(Color::Yellow)); if let Some(filetype) = file.extension() { texts .add_row() .add_col(TextSpan::from("File type: ")) .add_col(TextSpan::new(filetype).fg(Color::LightGreen)); } let (bsize, size): (ByteSize, u64) = (ByteSize(file.metadata().size), file.metadata().size); texts .add_row() .add_col(TextSpan::from("Size: ")) .add_col(TextSpan::new(format!("{bsize} ({size})").as_str()).fg(Color::Cyan)); let atime: String = fmt_time( file.metadata().accessed.unwrap_or(UNIX_EPOCH), "%b %d %Y %H:%M:%S", ); let ctime: String = fmt_time( file.metadata().created.unwrap_or(UNIX_EPOCH), "%b %d %Y %H:%M:%S", ); let mtime: String = fmt_time( file.metadata().modified.unwrap_or(UNIX_EPOCH), "%b %d %Y %H:%M:%S", ); texts .add_row() .add_col(TextSpan::from("Creation time: ")) .add_col(TextSpan::new(ctime.as_str()).fg(Color::LightGreen)); texts .add_row() .add_col(TextSpan::from("Last modified time: ")) .add_col(TextSpan::new(mtime.as_str()).fg(Color::LightBlue)); texts .add_row() .add_col(TextSpan::from("Last access time: ")) .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); // User #[cfg(posix)] let username: String = match file.metadata().uid { Some(uid) => match get_user_by_uid(uid) { Some(user) => user.name().to_string_lossy().to_string(), None => uid.to_string(), }, None => String::from("0"), }; #[cfg(win)] let username: String = format!("{}", file.metadata().uid.unwrap_or(0)); // Group #[cfg(posix)] let group: String = match file.metadata().gid { Some(gid) => match get_group_by_gid(gid) { Some(group) => group.name().to_string_lossy().to_string(), None => gid.to_string(), }, None => String::from("0"), }; #[cfg(win)] let group: String = format!("{}", file.metadata().gid.unwrap_or(0)); texts .add_row() .add_col(TextSpan::from("User: ")) .add_col(TextSpan::new(username.as_str()).fg(Color::LightYellow)); texts .add_row() .add_col(TextSpan::from("Group: ")) .add_col(TextSpan::new(group.as_str()).fg(Color::Blue)); Self { component: List::default() .borders(Borders::default().modifiers(BorderType::Rounded)) .scroll(false) .title(file.name(), Alignment::Left) .rows(texts.build()), } } } impl Component for FileInfoPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Esc | Key::Enter, .. }) => Some(Msg::Ui(UiMsg::CloseFileInfoPopup)), _ => None, } } } #[derive(MockComponent)] pub struct KeybindingsPopup { component: List, } impl KeybindingsPopup { pub fn new(key_color: Color) -> Self { Self { component: List::default() .borders(Borders::default().modifiers(BorderType::Rounded)) .scroll(true) .step(8) .highlighted_str("? ") .title("Keybindings", Alignment::Center) .rewind(true) .rows( TableBuilder::default() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Disconnect")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Go to previous directory")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Change explorer tab")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Move up/down in list")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Enter directory")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Upload/Download file")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Switch between explorer and log window", )) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Toggle hidden files")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Change file sorting mode")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Copy")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Make directory")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Search files")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Go to path")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Show help")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Show info about selected file", )) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Create symlink pointing to the current selected entry", )) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Reload directory content")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Select file")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Create new file")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Open text file with preferred editor", )) .add_row() .add_col(TextSpan::new("

").bold().fg(key_color)) .add_col(TextSpan::from(" Toggle bottom panel")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Quit termscp")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Rename file")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Save file as")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Watch/unwatch file changes")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Go to parent directory")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Open file with default application for file type", )) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Open file with specified application", )) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Execute shell command")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Toggle synchronized browsing", )) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Change file permissions")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Filter files")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Delete selected file")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Select all files")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Deselect all files")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Interrupt file transfer")) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Get total path size of selected files", )) .add_row() .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Show watched paths")) .build(), ), } } } impl Component for KeybindingsPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Esc | Key::Enter, .. }) => Some(Msg::Ui(UiMsg::CloseKeybindingsPopup)), Event::Keyboard(KeyEvent { code: Key::Down, .. }) => { self.perform(Cmd::Move(Direction::Down)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { self.perform(Cmd::Move(Direction::Up)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::PageDown, .. }) => { self.perform(Cmd::Scroll(Direction::Down)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::PageUp, .. }) => { self.perform(Cmd::Scroll(Direction::Up)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Home, .. }) => { self.perform(Cmd::GoTo(Position::Begin)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::End, .. }) => { self.perform(Cmd::GoTo(Position::End)); Some(Msg::None) } _ => None, } } } #[derive(MockComponent)] pub struct MkdirPopup { component: Input, } impl MkdirPopup { pub fn new(color: Color) -> Self { Self { component: Input::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .input_type(InputType::Text) .placeholder( "New directory name", Style::default().fg(Color::Rgb(128, 128, 128)), ) .title("directory-name", Alignment::Center), } } } impl Component for MkdirPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Home, .. }) => { self.perform(Cmd::GoTo(Position::Begin)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::End, .. }) => { self.perform(Cmd::GoTo(Position::End)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Delete, .. }) => { self.perform(Cmd::Cancel); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Backspace, .. }) => { self.perform(Cmd::Delete); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Char(ch), .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => match self.state() { State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::Mkdir(i))), _ => Some(Msg::None), }, Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseMkdirPopup)) } _ => None, } } } #[derive(MockComponent)] pub struct NewfilePopup { component: Input, } impl NewfilePopup { pub fn new(color: Color) -> Self { Self { component: Input::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .input_type(InputType::Text) .placeholder( "New file name", Style::default().fg(Color::Rgb(128, 128, 128)), ) .title("file.txt", Alignment::Center), } } } impl Component for NewfilePopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Home, .. }) => { self.perform(Cmd::GoTo(Position::Begin)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::End, .. }) => { self.perform(Cmd::GoTo(Position::End)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Delete, .. }) => { self.perform(Cmd::Cancel); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Backspace, .. }) => { self.perform(Cmd::Delete); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Char(ch), .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => match self.state() { State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::NewFile(i))), _ => Some(Msg::None), }, Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseNewFilePopup)) } _ => None, } } } #[derive(MockComponent)] pub struct OpenWithPopup { component: Input, } impl OpenWithPopup { pub fn new(color: Color) -> Self { Self { component: Input::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .input_type(InputType::Text) .placeholder( "Open file with…", Style::default().fg(Color::Rgb(128, 128, 128)), ) .title("Type the program to open the file with", Alignment::Center), } } } impl Component for OpenWithPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Home, .. }) => { self.perform(Cmd::GoTo(Position::Begin)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::End, .. }) => { self.perform(Cmd::GoTo(Position::End)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Delete, .. }) => { self.perform(Cmd::Cancel); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Backspace, .. }) => { self.perform(Cmd::Delete); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Char(ch), .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => match self.state() { State::One(StateValue::String(i)) => { Some(Msg::Transfer(TransferMsg::OpenFileWith(i))) } _ => Some(Msg::None), }, Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseOpenWithPopup)) } _ => None, } } } #[derive(MockComponent)] pub struct ProgressBarFull { component: ProgressBar, } impl ProgressBarFull { pub fn new>(prog: f64, label: S, title: S, color: Color) -> Self { Self { component: ProgressBar::default() .borders( Borders::default() .modifiers(BorderType::Rounded) .sides(BorderSides::TOP | BorderSides::LEFT | BorderSides::RIGHT), ) .foreground(color) .label(label) .progress(prog) .title(title, Alignment::Center), } } } impl Component for ProgressBarFull { fn on(&mut self, ev: Event) -> Option { if matches!( ev, Event::Keyboard(KeyEvent { code: Key::Char('c'), modifiers: KeyModifiers::CONTROL }) ) { Some(Msg::Transfer(TransferMsg::AbortTransfer)) } else { None } } } #[derive(MockComponent)] pub struct ProgressBarPartial { component: ProgressBar, } impl ProgressBarPartial { pub fn new>(prog: f64, label: S, title: S, color: Color) -> Self { Self { component: ProgressBar::default() .borders( Borders::default() .modifiers(BorderType::Rounded) .sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT), ) .foreground(color) .label(label) .progress(prog) .title(title, Alignment::Center), } } } impl Component for ProgressBarPartial { fn on(&mut self, ev: Event) -> Option { if matches!( ev, Event::Keyboard(KeyEvent { code: Key::Char('c'), modifiers: KeyModifiers::CONTROL }) ) { Some(Msg::Transfer(TransferMsg::AbortTransfer)) } else { None } } } #[derive(MockComponent)] pub struct QuitPopup { component: Radio, } impl QuitPopup { pub fn new(color: Color) -> Self { Self { component: Radio::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .choices(["Yes", "No"]) .title("Are you sure you want to quit termscp?", Alignment::Center), } } } impl Component for QuitPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseQuitPopup)) } Event::Keyboard(KeyEvent { code: Key::Char('y'), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::Quit)), Event::Keyboard(KeyEvent { code: Key::Char('n'), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::CloseQuitPopup)), Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => { if matches!( self.perform(Cmd::Submit), CmdResult::Submit(State::One(StateValue::Usize(0))) ) { Some(Msg::Ui(UiMsg::Quit)) } else { Some(Msg::Ui(UiMsg::CloseQuitPopup)) } } _ => None, } } } #[derive(MockComponent)] pub struct RenamePopup { component: Input, } impl RenamePopup { pub fn new(color: Color) -> Self { Self { component: Input::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .input_type(InputType::Text) .placeholder( "/foo/bar/buzz.txt", Style::default().fg(Color::Rgb(128, 128, 128)), ) .title("Move file(s) to…", Alignment::Center), } } } impl Component for RenamePopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Home, .. }) => { self.perform(Cmd::GoTo(Position::Begin)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::End, .. }) => { self.perform(Cmd::GoTo(Position::End)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Delete, .. }) => { self.perform(Cmd::Cancel); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Backspace, .. }) => { self.perform(Cmd::Delete); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Char(ch), .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => match self.state() { State::One(StateValue::String(i)) => { Some(Msg::Transfer(TransferMsg::RenameFile(i))) } _ => Some(Msg::None), }, Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseRenamePopup)) } _ => None, } } } #[derive(MockComponent)] pub struct ReplacePopup { component: Radio, } impl ReplacePopup { pub fn new(filename: Option<&str>, color: Color) -> Self { let text = match filename { Some(f) => format!(r#"File "{f}" already exists. Overwrite file?"#), None => "Overwrite files?".to_string(), }; Self { component: Radio::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .choices(["Replace", "Skip", "Replace All", "Skip All", "Cancel"]) .title(text, Alignment::Center), } } } impl Component for ReplacePopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel)) } Event::Keyboard(KeyEvent { code: Key::Char('y'), modifiers: KeyModifiers::NONE, }) => Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)), Event::Keyboard(KeyEvent { code: Key::Char('n'), modifiers: KeyModifiers::NONE, }) => Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)), Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => match self.perform(Cmd::Submit) { CmdResult::Submit(State::One(StateValue::Usize(0))) => { Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)) } CmdResult::Submit(State::One(StateValue::Usize(1))) => { Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)) } CmdResult::Submit(State::One(StateValue::Usize(2))) => { Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll)) } CmdResult::Submit(State::One(StateValue::Usize(3))) => { Some(Msg::PendingAction(PendingActionMsg::ReplaceSkipAll)) } CmdResult::Submit(State::One(StateValue::Usize(4))) => { Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel)) } _ => Some(Msg::None), }, _ => None, } } } #[derive(MockComponent)] pub struct SaveAsPopup { component: Input, } impl SaveAsPopup { pub fn new(color: Color) -> Self { Self { component: Input::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .input_type(InputType::Text) .placeholder( "/foo/bar/buzz.txt", Style::default().fg(Color::Rgb(128, 128, 128)), ) .title("Save as…", Alignment::Center), } } } impl Component for SaveAsPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Home, .. }) => { self.perform(Cmd::GoTo(Position::Begin)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::End, .. }) => { self.perform(Cmd::GoTo(Position::End)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Delete, .. }) => { self.perform(Cmd::Cancel); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Backspace, .. }) => { self.perform(Cmd::Delete); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Char(ch), .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => match self.state() { State::One(StateValue::String(i)) => { Some(Msg::Transfer(TransferMsg::SaveFileAs(i))) } _ => Some(Msg::None), }, Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseSaveAsPopup)) } _ => None, } } } #[derive(MockComponent)] pub struct SortingPopup { component: Radio, } impl SortingPopup { pub fn new(value: FileSorting, color: Color) -> Self { Self { component: Radio::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .choices(["Name", "Modify time", "Creation time", "Size"]) .title("Sort files by…", Alignment::Center) .value(match value { FileSorting::CreationTime => 2, FileSorting::ModifyTime => 1, FileSorting::Name => 0, FileSorting::Size => 3, FileSorting::None => 0, }), } } } impl Component for SortingPopup { fn on(&mut self, ev: Event) -> Option { let result = match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => self.perform(Cmd::Move(Direction::Left)), Event::Keyboard(KeyEvent { code: Key::Right, .. }) => self.perform(Cmd::Move(Direction::Right)), Event::Keyboard(KeyEvent { code: Key::Esc | Key::Enter, .. }) => return Some(Msg::Ui(UiMsg::CloseFileSortingPopup)), _ => return None, }; if let CmdResult::Changed(State::One(StateValue::Usize(i))) = result { Some(Msg::Ui(UiMsg::ChangeFileSorting(match i { 0 => FileSorting::Name, 1 => FileSorting::ModifyTime, 2 => FileSorting::CreationTime, 3 => FileSorting::Size, _ => FileSorting::Name, }))) } else { Some(Msg::None) } } } #[derive(MockComponent)] pub struct StatusBarLocal { component: Span, } impl StatusBarLocal { pub fn new(browser: &Browser, sorting_color: Color, hidden_color: Color) -> Self { let file_sorting = file_sorting_label(browser.host_bridge().file_sorting); let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible()); Self { component: Span::default().spans([ TextSpan::new("File sorting: ").fg(sorting_color), TextSpan::new(file_sorting).fg(sorting_color).reversed(), TextSpan::new(" Hidden files: ").fg(hidden_color), TextSpan::new(hidden_files).fg(hidden_color).reversed(), ]), } } } impl Component for StatusBarLocal { fn on(&mut self, _ev: Event) -> Option { None } } #[derive(MockComponent)] pub struct StatusBarRemote { component: Span, } impl StatusBarRemote { pub fn new( browser: &Browser, sorting_color: Color, hidden_color: Color, sync_color: Color, ) -> Self { let file_sorting = file_sorting_label(browser.remote().file_sorting); let hidden_files = hidden_files_label(browser.remote().hidden_files_visible()); let sync_browsing = match browser.sync_browsing { true => "ON ", false => "OFF", }; Self { component: Span::default().spans([ TextSpan::new("File sorting: ").fg(sorting_color), TextSpan::new(file_sorting).fg(sorting_color).reversed(), TextSpan::new(" Hidden files: ").fg(hidden_color), TextSpan::new(hidden_files).fg(hidden_color).reversed(), TextSpan::new(" Sync browsing: ").fg(sync_color), TextSpan::new(sync_browsing).fg(sync_color).reversed(), ]), } } } impl Component for StatusBarRemote { fn on(&mut self, _ev: Event) -> Option { None } } fn file_sorting_label(sorting: FileSorting) -> &'static str { match sorting { FileSorting::CreationTime => "By creation time", FileSorting::ModifyTime => "By modify time", FileSorting::Name => "By name", FileSorting::Size => "By size", FileSorting::None => "", } } fn hidden_files_label(visible: bool) -> &'static str { match visible { true => "Show", false => "Hide", } } #[derive(MockComponent)] pub struct SymlinkPopup { component: Input, } impl SymlinkPopup { pub fn new(color: Color) -> Self { Self { component: Input::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .input_type(InputType::Text) .placeholder( "Symlink name", Style::default().fg(Color::Rgb(128, 128, 128)), ) .title( "Create a symlink pointing to the selected entry", Alignment::Center, ), } } } impl Component for SymlinkPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Home, .. }) => { self.perform(Cmd::GoTo(Position::Begin)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::End, .. }) => { self.perform(Cmd::GoTo(Position::End)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Delete, .. }) => { self.perform(Cmd::Cancel); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Backspace, .. }) => { self.perform(Cmd::Delete); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Char(ch), .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => match self.state() { State::One(StateValue::String(i)) => { Some(Msg::Transfer(TransferMsg::CreateSymlink(i))) } _ => Some(Msg::None), }, Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseSymlinkPopup)) } _ => None, } } } #[derive(MockComponent)] pub struct SyncBrowsingMkdirPopup { component: Radio, } impl SyncBrowsingMkdirPopup { pub fn new(color: Color, dir_name: &str) -> Self { Self { component: Radio::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .choices(["Yes", "No"]) .title( format!( r#"Sync browsing: directory "{dir_name}" doesn't exist. Do you want to create it?"# ), Alignment::Center, ), } } } impl Component for SyncBrowsingMkdirPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::PendingAction( PendingActionMsg::CloseSyncBrowsingMkdirPopup, )), Event::Keyboard(KeyEvent { code: Key::Char('y'), modifiers: KeyModifiers::NONE, }) => Some(Msg::PendingAction(PendingActionMsg::MakePendingDirectory)), Event::Keyboard(KeyEvent { code: Key::Char('n'), modifiers: KeyModifiers::NONE, }) => Some(Msg::PendingAction( PendingActionMsg::CloseSyncBrowsingMkdirPopup, )), Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => { if matches!( self.perform(Cmd::Submit), CmdResult::Submit(State::One(StateValue::Usize(0))) ) { Some(Msg::PendingAction(PendingActionMsg::MakePendingDirectory)) } else { Some(Msg::PendingAction( PendingActionMsg::CloseSyncBrowsingMkdirPopup, )) } } _ => None, } } } #[derive(MockComponent)] pub struct WaitPopup { component: Paragraph, } impl WaitPopup { pub fn new>(text: S, color: Color) -> Self { Self { component: Paragraph::default() .alignment(Alignment::Center) .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .text([TextSpan::from(text.as_ref())]) .wrap(true), } } } impl Component for WaitPopup { fn on(&mut self, _ev: Event) -> Option { None } } #[derive(MockComponent)] pub struct WalkdirWaitPopup { component: Paragraph, } impl WalkdirWaitPopup { pub fn new>(text: S, color: Color) -> Self { Self { component: Paragraph::default() .alignment(Alignment::Center) .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .text([ TextSpan::from(text.as_ref()), TextSpan::from("Press 'CTRL+C' to abort"), ]) .wrap(true), } } } impl Component for WalkdirWaitPopup { fn on(&mut self, ev: Event) -> Option { if matches!( ev, Event::Keyboard(KeyEvent { code: Key::Char('c'), modifiers: KeyModifiers::CONTROL }) ) { Some(Msg::Transfer(TransferMsg::AbortWalkdir)) } else { None } } } #[derive(MockComponent)] pub struct WatchedPathsList { component: List, } impl WatchedPathsList { pub fn new(paths: &[std::path::PathBuf], color: Color) -> Self { Self { component: List::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .rewind(true) .scroll(true) .step(4) .highlighted_color(color) .highlighted_str("➤ ") .title( "These files are currently synched with the remote host", Alignment::Center, ) .rows( paths .iter() .map(|x| vec![TextSpan::from(x.to_string_lossy().to_string())]) .collect(), ), } } } impl Component for WatchedPathsList { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseWatchedPathsList)) } Event::Keyboard(KeyEvent { code: Key::Down, .. }) => { self.perform(Cmd::Move(Direction::Down)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { self.perform(Cmd::Move(Direction::Up)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::PageDown, .. }) => { self.perform(Cmd::Scroll(Direction::Down)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::PageUp, .. }) => { self.perform(Cmd::Scroll(Direction::Up)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Home, .. }) => { self.perform(Cmd::GoTo(Position::Begin)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::End, .. }) => { self.perform(Cmd::GoTo(Position::End)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => { // get state if let State::One(StateValue::Usize(idx)) = self.component.state() { Some(Msg::Transfer(TransferMsg::ToggleWatchFor(idx))) } else { Some(Msg::None) } } _ => None, } } } #[derive(MockComponent)] pub struct WatcherPopup { component: Radio, } impl WatcherPopup { pub fn new(watched: bool, local: &str, remote: &str, color: Color) -> Self { let text = match watched { false => format!(r#"Synchronize changes from "{local}" to "{remote}"?"#), true => format!(r#"Stop synchronizing changes at "{local}"?"#), }; Self { component: Radio::default() .borders( Borders::default() .color(color) .modifiers(BorderType::Rounded), ) .foreground(color) .choices(["Yes", "No"]) .title(text, Alignment::Center), } } } impl Component for WatcherPopup { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Some(Msg::Ui(UiMsg::CloseWatcherPopup)) } Event::Keyboard(KeyEvent { code: Key::Char('y'), modifiers: KeyModifiers::NONE, }) => Some(Msg::Transfer(TransferMsg::ToggleWatch)), Event::Keyboard(KeyEvent { code: Key::Char('n'), modifiers: KeyModifiers::NONE, }) => Some(Msg::Ui(UiMsg::CloseWatcherPopup)), Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => { if matches!( self.perform(Cmd::Submit), CmdResult::Submit(State::One(StateValue::Usize(0))) ) { Some(Msg::Transfer(TransferMsg::ToggleWatch)) } else { Some(Msg::Ui(UiMsg::CloseWatcherPopup)) } } _ => None, } } }