Files
termscp/src/ui/activities/filetransfer/components/popups.rs
Christian Visintin a0b357cf8c
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
feat: Added <CTRL+S> keybinding to get the total size of selected paths. (#367)
* feat: Added `<CTRL+S>` keybinding to get the total size of selected paths.

closes #297
2025-11-09 21:14:42 +01:00

1869 lines
62 KiB
Rust

//! ## 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<Msg, NoUserEvent> for CopyPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for FilterPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for DeletePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for DisconnectPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<S: AsRef<str>>(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<Msg, NoUserEvent> for ErrorPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<S: AsRef<str>>(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<Msg, NoUserEvent> for FatalPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for FileInfoPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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("<ESC>").bold().fg(key_color))
.add_col(TextSpan::from(" Disconnect"))
.add_row()
.add_col(TextSpan::new("<BACKSPACE>").bold().fg(key_color))
.add_col(TextSpan::from(" Go to previous directory"))
.add_row()
.add_col(TextSpan::new("<TAB|RIGHT|LEFT>").bold().fg(key_color))
.add_col(TextSpan::from(" Change explorer tab"))
.add_row()
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(key_color))
.add_col(TextSpan::from(" Move up/down in list"))
.add_row()
.add_col(TextSpan::new("<ENTER>").bold().fg(key_color))
.add_col(TextSpan::from(" Enter directory"))
.add_row()
.add_col(TextSpan::new("<SPACE>").bold().fg(key_color))
.add_col(TextSpan::from(" Upload/Download file"))
.add_row()
.add_col(TextSpan::new("<BACKTAB>").bold().fg(key_color))
.add_col(TextSpan::from(
" Switch between explorer and log window",
))
.add_row()
.add_col(TextSpan::new("<A>").bold().fg(key_color))
.add_col(TextSpan::from(" Toggle hidden files"))
.add_row()
.add_col(TextSpan::new("<B>").bold().fg(key_color))
.add_col(TextSpan::from(" Change file sorting mode"))
.add_row()
.add_col(TextSpan::new("<C|F5>").bold().fg(key_color))
.add_col(TextSpan::from(" Copy"))
.add_row()
.add_col(TextSpan::new("<D|F7>").bold().fg(key_color))
.add_col(TextSpan::from(" Make directory"))
.add_row()
.add_col(TextSpan::new("<F>").bold().fg(key_color))
.add_col(TextSpan::from(" Search files"))
.add_row()
.add_col(TextSpan::new("<G>").bold().fg(key_color))
.add_col(TextSpan::from(" Go to path"))
.add_row()
.add_col(TextSpan::new("<H|F1>").bold().fg(key_color))
.add_col(TextSpan::from(" Show help"))
.add_row()
.add_col(TextSpan::new("<I>").bold().fg(key_color))
.add_col(TextSpan::from(
" Show info about selected file",
))
.add_row()
.add_col(TextSpan::new("<K>").bold().fg(key_color))
.add_col(TextSpan::from(
" Create symlink pointing to the current selected entry",
))
.add_row()
.add_col(TextSpan::new("<L>").bold().fg(key_color))
.add_col(TextSpan::from(" Reload directory content"))
.add_row()
.add_col(TextSpan::new("<M>").bold().fg(key_color))
.add_col(TextSpan::from(" Select file"))
.add_row()
.add_col(TextSpan::new("<N>").bold().fg(key_color))
.add_col(TextSpan::from(" Create new file"))
.add_row()
.add_col(TextSpan::new("<O|F4>").bold().fg(key_color))
.add_col(TextSpan::from(
" Open text file with preferred editor",
))
.add_row()
.add_col(TextSpan::new("<P>").bold().fg(key_color))
.add_col(TextSpan::from(" Toggle bottom panel"))
.add_row()
.add_col(TextSpan::new("<Q|F10>").bold().fg(key_color))
.add_col(TextSpan::from(" Quit termscp"))
.add_row()
.add_col(TextSpan::new("<R|F6>").bold().fg(key_color))
.add_col(TextSpan::from(" Rename file"))
.add_row()
.add_col(TextSpan::new("<S|F2>").bold().fg(key_color))
.add_col(TextSpan::from(" Save file as"))
.add_row()
.add_col(TextSpan::new("<T>").bold().fg(key_color))
.add_col(TextSpan::from(" Watch/unwatch file changes"))
.add_row()
.add_col(TextSpan::new("<U>").bold().fg(key_color))
.add_col(TextSpan::from(" Go to parent directory"))
.add_row()
.add_col(TextSpan::new("<V|F3>").bold().fg(key_color))
.add_col(TextSpan::from(
" Open file with default application for file type",
))
.add_row()
.add_col(TextSpan::new("<W>").bold().fg(key_color))
.add_col(TextSpan::from(
" Open file with specified application",
))
.add_row()
.add_col(TextSpan::new("<X>").bold().fg(key_color))
.add_col(TextSpan::from(" Execute shell command"))
.add_row()
.add_col(TextSpan::new("<Y>").bold().fg(key_color))
.add_col(TextSpan::from(
" Toggle synchronized browsing",
))
.add_row()
.add_col(TextSpan::new("<Z>").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("<DEL|F8|E>").bold().fg(key_color))
.add_col(TextSpan::from(" Delete selected file"))
.add_row()
.add_col(TextSpan::new("<CTRL+A>").bold().fg(key_color))
.add_col(TextSpan::from(" Select all files"))
.add_row()
.add_col(TextSpan::new("<ALT+A>").bold().fg(key_color))
.add_col(TextSpan::from(" Deselect all files"))
.add_row()
.add_col(TextSpan::new("<CTRL+C>").bold().fg(key_color))
.add_col(TextSpan::from(" Interrupt file transfer"))
.add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(key_color))
.add_col(TextSpan::from(
" Get total path size of selected files",
))
.add_row()
.add_col(TextSpan::new("<CTRL+T>").bold().fg(key_color))
.add_col(TextSpan::from(" Show watched paths"))
.build(),
),
}
}
}
impl Component<Msg, NoUserEvent> for KeybindingsPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for MkdirPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for NewfilePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for OpenWithPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<S: Into<String>>(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<Msg, NoUserEvent> for ProgressBarFull {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<S: Into<String>>(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<Msg, NoUserEvent> for ProgressBarPartial {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for QuitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for RenamePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for ReplacePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for SaveAsPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for SortingPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for StatusBarLocal {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for StatusBarRemote {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for SymlinkPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for SyncBrowsingMkdirPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<S: AsRef<str>>(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<Msg, NoUserEvent> for WaitPopup {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
None
}
}
#[derive(MockComponent)]
pub struct WalkdirWaitPopup {
component: Paragraph,
}
impl WalkdirWaitPopup {
pub fn new<S: AsRef<str>>(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<Msg, NoUserEvent> for WalkdirWaitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for WatchedPathsList {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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<Msg, NoUserEvent> for WatcherPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
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,
}
}
}