mirror of
https://github.com/veeso/termscp.git
synced 2026-09-26 14:01:17 -07:00
feat: Replaced the Exec popup with a fully functional terminal emulator (#348)
* feat: Replaced the `Exec` popup with a fully functional terminal emulator closes #340 * fix: colors and fmt for terminal * feat: Handle exit and cd on terminal * fix: Fmt pah
This commit is contained in:
+6
-1
@@ -45,8 +45,13 @@
|
|||||||
|
|
||||||
Released on 31/07/2025
|
Released on 31/07/2025
|
||||||
|
|
||||||
- **Updated dependencies** and updated the Rust edition to `2024`
|
- 🐚 An **Embedded shell for termscp**:
|
||||||
|
- [Issue 340](https://github.com/veeso/termscp/issues/340): Replaced the `Exec` popup with a **fully functional terminal emulator** embedded thanks to [A-Kenji's tui-term](https://github.com/a-kenji/tui-term).
|
||||||
|
- Command History
|
||||||
|
- Support for `cd` and `exit` commands as well.
|
||||||
|
- Exit just closes the terminal emulator.
|
||||||
- [Issue 345](https://github.com/veeso/termscp/issues/345): Default keys are used from `~/.ssh` directory if no keys are resolved for the host.
|
- [Issue 345](https://github.com/veeso/termscp/issues/345): Default keys are used from `~/.ssh` directory if no keys are resolved for the host.
|
||||||
|
- **Updated dependencies** and updated the Rust edition to `2024`
|
||||||
|
|
||||||
## 0.17.0
|
## 0.17.0
|
||||||
|
|
||||||
|
|||||||
Generated
+63
-7
@@ -105,6 +105,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -3641,9 +3647,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remotefs-ssh"
|
name = "remotefs-ssh"
|
||||||
version = "0.6.1"
|
version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47fb1d79906574c1edc5b288a6bb47af8b2d50e99a7176fa731615c496d3334e"
|
checksum = "0835109bdc3eeeb4b77f40b6146e0442155372cd9eb741a49e7479e97ea5c746"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"lazy-regex",
|
"lazy-regex",
|
||||||
@@ -4648,6 +4654,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"tui-realm-stdlib",
|
"tui-realm-stdlib",
|
||||||
|
"tui-term",
|
||||||
"tuirealm",
|
"tuirealm",
|
||||||
"unicode-width 0.2.0",
|
"unicode-width 0.2.0",
|
||||||
"uzers",
|
"uzers",
|
||||||
@@ -5030,10 +5037,20 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tuirealm"
|
name = "tui-term"
|
||||||
version = "3.0.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "069af2605553265d277bc559bc0e924596e82cb8b71f222314f5626cf33745d3"
|
checksum = "72af159125ce32b02ceaced6cffae6394b0e6b6dfd4dc164a6c59a2db9b3c0b0"
|
||||||
|
dependencies = [
|
||||||
|
"ratatui",
|
||||||
|
"vt100",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tuirealm"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29607ee819f733f15c6bbad6aa760ed3d52b345588130003399bd792bfc6b0a8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"crossterm 0.29.0",
|
"crossterm 0.29.0",
|
||||||
@@ -5045,9 +5062,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tuirealm_derive"
|
name = "tuirealm_derive"
|
||||||
version = "2.0.0"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "caa8b0560f4245acc0bbe0e1d76e1f6a308145dd6e107befce4cf29e7fe32662"
|
checksum = "bad3c151090da5a036321776209b3d026df637d7f52c0984ff8d45927326ab4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5166,6 +5183,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@@ -5253,6 +5276,39 @@ version = "0.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
|
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vt100"
|
||||||
|
version = "0.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"log",
|
||||||
|
"unicode-width 0.1.14",
|
||||||
|
"vte",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vte"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"utf8parse",
|
||||||
|
"vte_generate_state_changes",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vte_generate_state_changes"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ tokio = { version = "1.44", features = ["rt"] }
|
|||||||
toml = "^0.8"
|
toml = "^0.8"
|
||||||
tui-realm-stdlib = "3"
|
tui-realm-stdlib = "3"
|
||||||
tuirealm = "3"
|
tuirealm = "3"
|
||||||
|
tui-term = "0.2"
|
||||||
unicode-width = "^0.2"
|
unicode-width = "^0.2"
|
||||||
version-compare = "^0.2"
|
version-compare = "^0.2"
|
||||||
whoami = "^1.6"
|
whoami = "^1.6"
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
|
|||||||
- 📝 View and edit files with your favourite applications
|
- 📝 View and edit files with your favourite applications
|
||||||
- 💁 SFTP/SCP authentication with SSH keys and username/password
|
- 💁 SFTP/SCP authentication with SSH keys and username/password
|
||||||
- 🐧 Compatible with Windows, Linux, FreeBSD, NetBSD and MacOS
|
- 🐧 Compatible with Windows, Linux, FreeBSD, NetBSD and MacOS
|
||||||
|
- 🐚 Embedded terminal for executing commands on the system.
|
||||||
- 🎨 Make it yours!
|
- 🎨 Make it yours!
|
||||||
- Themes
|
- Themes
|
||||||
- Custom file explorer format
|
- Custom file explorer format
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ pub struct FileExplorer {
|
|||||||
pub(crate) opts: ExplorerOpts,
|
pub(crate) opts: ExplorerOpts,
|
||||||
/// Formatter for file entries
|
/// Formatter for file entries
|
||||||
pub(crate) fmt: Formatter,
|
pub(crate) fmt: Formatter,
|
||||||
|
/// Is terminal open for this explorer?
|
||||||
|
terminal: bool,
|
||||||
/// Files in directory
|
/// Files in directory
|
||||||
files: Vec<File>,
|
files: Vec<File>,
|
||||||
/// files enqueued for transfer. Map between source and destination
|
/// files enqueued for transfer. Map between source and destination
|
||||||
@@ -73,6 +75,7 @@ impl Default for FileExplorer {
|
|||||||
opts: ExplorerOpts::empty(),
|
opts: ExplorerOpts::empty(),
|
||||||
fmt: Formatter::default(),
|
fmt: Formatter::default(),
|
||||||
files: Vec::new(),
|
files: Vec::new(),
|
||||||
|
terminal: false,
|
||||||
transfer_queue: HashMap::new(),
|
transfer_queue: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,6 +182,15 @@ impl FileExplorer {
|
|||||||
self.transfer_queue.clear();
|
self.transfer_queue.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle terminal state
|
||||||
|
pub fn toggle_terminal(&mut self, terminal: bool) {
|
||||||
|
self.terminal = terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn terminal_open(&self) -> bool {
|
||||||
|
self.terminal
|
||||||
|
}
|
||||||
|
|
||||||
// Formatting
|
// Formatting
|
||||||
|
|
||||||
/// Format a file entry
|
/// Format a file entry
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ impl HostBridgeParams {
|
|||||||
HostBridgeParams::Remote(_, params) => params,
|
HostBridgeParams::Remote(_, params) => params,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the host name for the bridge params
|
||||||
|
pub fn username(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
HostBridgeParams::Localhost(_) => Some(whoami::username()),
|
||||||
|
HostBridgeParams::Remote(_, params) => {
|
||||||
|
params.generic_params().and_then(|p| p.username.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Holds connection parameters for file transfers
|
/// Holds connection parameters for file transfers
|
||||||
@@ -42,6 +52,15 @@ pub struct FileTransferParams {
|
|||||||
pub local_path: Option<PathBuf>,
|
pub local_path: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FileTransferParams {
|
||||||
|
/// Returns the remote path if set, otherwise returns the local path
|
||||||
|
pub fn username(&self) -> Option<String> {
|
||||||
|
self.params
|
||||||
|
.generic_params()
|
||||||
|
.and_then(|p| p.username.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Container for protocol params
|
/// Container for protocol params
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ProtocolParams {
|
pub enum ProtocolParams {
|
||||||
|
|||||||
@@ -2,41 +2,127 @@
|
|||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
/// Terminal command
|
||||||
pub(crate) fn action_local_exec(&mut self, input: String) {
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
match self.host_bridge.exec(input.as_str()) {
|
enum Command {
|
||||||
Ok(output) => {
|
Cd(String),
|
||||||
// Reload files
|
Exec(String),
|
||||||
self.log(LogLevel::Info, format!("\"{input}\": {output}"));
|
Exit,
|
||||||
}
|
}
|
||||||
Err(err) => {
|
|
||||||
// Report err
|
impl FromStr for Command {
|
||||||
self.log_and_alert(
|
type Err = String;
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not execute command \"{input}\": {err}"),
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
);
|
let mut parts = s.split_whitespace();
|
||||||
|
match parts.next() {
|
||||||
|
Some("cd") => {
|
||||||
|
if let Some(path) = parts.next() {
|
||||||
|
Ok(Command::Cd(path.to_string()))
|
||||||
|
} else {
|
||||||
|
Err("cd command requires a path".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("exit") | Some("logout") => Ok(Command::Exit),
|
||||||
|
Some(cmd) => Ok(Command::Exec(cmd.to_string())),
|
||||||
|
None => Err("".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(crate) fn action_local_exec(&mut self, input: String) {
|
||||||
|
self.action_exec(false, input);
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_exec(&mut self, input: String) {
|
pub(crate) fn action_remote_exec(&mut self, input: String) {
|
||||||
match self.client.as_mut().exec(input.as_str()) {
|
self.action_exec(true, input);
|
||||||
Ok((rc, output)) => {
|
}
|
||||||
// Reload files
|
|
||||||
self.log(
|
fn action_exec(&mut self, remote: bool, cmd: String) {
|
||||||
LogLevel::Info,
|
if cmd.is_empty() {
|
||||||
format!("\"{input}\" (exitcode: {rc}): {output}"),
|
self.print_terminal("".to_string());
|
||||||
);
|
}
|
||||||
|
|
||||||
|
let cmd = match Command::from_str(&cmd) {
|
||||||
|
Ok(cmd) => cmd,
|
||||||
|
Err(err) => {
|
||||||
|
self.log(LogLevel::Error, format!("Invalid command: {err}"));
|
||||||
|
self.print_terminal(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match cmd {
|
||||||
|
Command::Cd(path) => {
|
||||||
|
self.action_exec_cd(remote, path);
|
||||||
|
}
|
||||||
|
Command::Exec(executable) => {
|
||||||
|
self.action_exec_executable(remote, executable);
|
||||||
|
}
|
||||||
|
Command::Exit => {
|
||||||
|
self.action_exec_exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_exec_exit(&mut self) {
|
||||||
|
self.browser.toggle_terminal(false);
|
||||||
|
self.umount_exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_exec_cd(&mut self, remote: bool, input: String) {
|
||||||
|
let new_dir = if remote {
|
||||||
|
let dir_path: PathBuf =
|
||||||
|
self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||||
|
self.remote_changedir(dir_path.as_path(), true);
|
||||||
|
|
||||||
|
dir_path
|
||||||
|
} else {
|
||||||
|
let dir_path: PathBuf =
|
||||||
|
self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||||
|
self.host_bridge_changedir(dir_path.as_path(), true);
|
||||||
|
|
||||||
|
dir_path
|
||||||
|
};
|
||||||
|
|
||||||
|
self.update_browser_file_list();
|
||||||
|
|
||||||
|
// update prompt and print the new directory
|
||||||
|
self.update_terminal_prompt();
|
||||||
|
self.print_terminal(new_dir.display().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a [`Command::Exec`] command
|
||||||
|
fn action_exec_executable(&mut self, remote: bool, cmd: String) {
|
||||||
|
let res = if remote {
|
||||||
|
self.client
|
||||||
|
.as_mut()
|
||||||
|
.exec(cmd.as_str())
|
||||||
|
.map(|(_, output)| output)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
} else {
|
||||||
|
self.host_bridge
|
||||||
|
.exec(cmd.as_str())
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(output) => {
|
||||||
|
self.print_terminal(output);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Report err
|
self.log(
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!("Could not execute command \"{input}\": {err}"),
|
format!("Could not execute command \"{cmd}\": {err}"),
|
||||||
);
|
);
|
||||||
|
self.print_terminal(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ mod log;
|
|||||||
mod misc;
|
mod misc;
|
||||||
mod popups;
|
mod popups;
|
||||||
mod selected_files;
|
mod selected_files;
|
||||||
|
mod terminal;
|
||||||
mod transfer;
|
mod transfer;
|
||||||
|
|
||||||
pub use misc::FooterBar;
|
pub use misc::FooterBar;
|
||||||
pub use popups::{
|
pub use popups::{
|
||||||
ATTR_FILES, ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup,
|
ATTR_FILES, ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, FatalPopup,
|
||||||
FatalPopup, FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
|
FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
|
||||||
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
||||||
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
|
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
|
||||||
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList,
|
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList,
|
||||||
@@ -28,6 +29,7 @@ pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};
|
|||||||
|
|
||||||
pub use self::log::Log;
|
pub use self::log::Log;
|
||||||
pub use self::selected_files::SelectedFilesList;
|
pub use self::selected_files::SelectedFilesList;
|
||||||
|
pub use self::terminal::Terminal;
|
||||||
|
|
||||||
#[derive(Default, MockComponent)]
|
#[derive(Default, MockComponent)]
|
||||||
pub struct GlobalListener {
|
pub struct GlobalListener {
|
||||||
|
|||||||
@@ -362,89 +362,6 @@ impl Component<Msg, NoUserEvent> for ErrorPopup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
|
||||||
pub struct ExecPopup {
|
|
||||||
component: Input,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExecPopup {
|
|
||||||
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("ps a", Style::default().fg(Color::Rgb(128, 128, 128)))
|
|
||||||
.title("Execute command", Alignment::Center),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for ExecPopup {
|
|
||||||
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::ExecuteCmd(i)))
|
|
||||||
}
|
|
||||||
_ => Some(Msg::None),
|
|
||||||
},
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
|
||||||
Some(Msg::Ui(UiMsg::CloseExecPopup))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(MockComponent)]
|
||||||
pub struct FatalPopup {
|
pub struct FatalPopup {
|
||||||
component: Paragraph,
|
component: Paragraph,
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
mod component;
|
||||||
|
mod history;
|
||||||
|
mod line;
|
||||||
|
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::Color;
|
||||||
|
use tuirealm::{AttrValue, Attribute, Component, Event, MockComponent, NoUserEvent};
|
||||||
|
|
||||||
|
use self::component::TerminalComponent;
|
||||||
|
use self::line::Line;
|
||||||
|
use super::Msg;
|
||||||
|
use crate::ui::activities::filetransfer::{TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent, Default)]
|
||||||
|
pub struct Terminal {
|
||||||
|
component: TerminalComponent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminal {
|
||||||
|
/// Construct a new [`Terminal`] component with the given prompt line.
|
||||||
|
pub fn prompt(mut self, prompt: impl ToString) -> Self {
|
||||||
|
self.component = self.component.prompt(prompt);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a new [`Terminal`] component with the given title.
|
||||||
|
pub fn title(mut self, title: impl ToString) -> Self {
|
||||||
|
self.component
|
||||||
|
.attr(Attribute::Title, AttrValue::String(title.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_color(mut self, color: Color) -> Self {
|
||||||
|
self.component
|
||||||
|
.attr(Attribute::Borders, AttrValue::Color(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a new [`Terminal`] component with the foreground color
|
||||||
|
pub fn foreground(mut self, color: Color) -> Self {
|
||||||
|
self.component
|
||||||
|
.attr(Attribute::Foreground, AttrValue::Color(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for Terminal {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseExecPopup))
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => match self.component.perform(Cmd::Submit) {
|
||||||
|
CmdResult::Submit(state) => {
|
||||||
|
let cmd = state.unwrap_one().unwrap_string();
|
||||||
|
Some(Msg::Transfer(TransferMsg::ExecuteCmd(cmd)))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.component.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Backspace,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Cancel);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Delete, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Delete);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||||
|
self.component.perform(Cmd::Move(Direction::Up));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Down, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Move(Direction::Down));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Insert, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Toggle);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::PageDown,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Scroll(Direction::Down));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::PageUp, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Scroll(Direction::Up));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(c), ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Type(c));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
use tui_term::vt100::Parser;
|
||||||
|
use tui_term::widget::PseudoTerminal;
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||||
|
use tuirealm::props::{BorderSides, BorderType, Style};
|
||||||
|
use tuirealm::ratatui::layout::Rect;
|
||||||
|
use tuirealm::ratatui::widgets::Block;
|
||||||
|
use tuirealm::{AttrValue, Attribute, MockComponent, Props, State, StateValue};
|
||||||
|
|
||||||
|
use super::Line;
|
||||||
|
use super::history::History;
|
||||||
|
|
||||||
|
const DEFAULT_HISTORY_SIZE: usize = 128;
|
||||||
|
|
||||||
|
pub struct TerminalComponent {
|
||||||
|
pub parser: Parser,
|
||||||
|
history: History,
|
||||||
|
line: Line,
|
||||||
|
props: Props,
|
||||||
|
scroll: usize,
|
||||||
|
size: (u16, u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TerminalComponent {
|
||||||
|
fn default() -> Self {
|
||||||
|
let props = Props::default();
|
||||||
|
let parser = Parser::new(40, 220, 2048);
|
||||||
|
|
||||||
|
TerminalComponent {
|
||||||
|
parser,
|
||||||
|
history: History::new(DEFAULT_HISTORY_SIZE),
|
||||||
|
line: Line::default(),
|
||||||
|
props,
|
||||||
|
scroll: 0,
|
||||||
|
size: (40, 220),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TerminalComponent {
|
||||||
|
/// Set prompt line for the terminal
|
||||||
|
pub fn prompt(mut self, prompt: impl ToString) -> Self {
|
||||||
|
self.attr(Attribute::Content, AttrValue::String(prompt.to_string()));
|
||||||
|
self.write_prompt();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_prompt(&mut self) {
|
||||||
|
if let Some(value) = self.query(Attribute::Content) {
|
||||||
|
let prompt = value.unwrap_string();
|
||||||
|
self.parser.process(prompt.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set current line to the previous command in the [`History`]
|
||||||
|
fn history_prev(&mut self) {
|
||||||
|
if let Some(cmd) = self.history.previous() {
|
||||||
|
self.write_line(cmd.as_bytes());
|
||||||
|
self.line.set(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set current line to the next command in the [`History`]
|
||||||
|
fn history_next(&mut self) {
|
||||||
|
if let Some(cmd) = self.history.next() {
|
||||||
|
self.write_line(cmd.as_bytes());
|
||||||
|
self.line.set(cmd);
|
||||||
|
} else {
|
||||||
|
// If there is no next command, clear the line
|
||||||
|
self.line.set(String::new());
|
||||||
|
self.write_line(&[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a line to the terminal, processing it through the parser
|
||||||
|
fn write_line(&mut self, data: &[u8]) {
|
||||||
|
self.parser.process(b"\r");
|
||||||
|
// blank the line
|
||||||
|
self.write_prompt();
|
||||||
|
self.parser.process(&[b' '; 15]);
|
||||||
|
self.parser.process(b"\r");
|
||||||
|
self.write_prompt();
|
||||||
|
self.parser.process(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockComponent for TerminalComponent {
|
||||||
|
fn attr(&mut self, attr: tuirealm::Attribute, value: AttrValue) {
|
||||||
|
if attr == Attribute::Text {
|
||||||
|
if let tuirealm::AttrValue::String(s) = value {
|
||||||
|
self.parser.process(b"\r");
|
||||||
|
self.parser.process(s.as_bytes());
|
||||||
|
self.parser.process(b"\r");
|
||||||
|
self.write_prompt();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.props.set(attr, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||||
|
match cmd {
|
||||||
|
Cmd::Type(s) => {
|
||||||
|
if !s.is_ascii() || self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Ignore non-ASCII characters or if scrolled
|
||||||
|
}
|
||||||
|
self.parser.process(&[s as u8]);
|
||||||
|
self.line.push(s);
|
||||||
|
CmdResult::Changed(self.state())
|
||||||
|
}
|
||||||
|
Cmd::Move(Direction::Down) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move down if not scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
self.history_next();
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Move(Direction::Left) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move up if not scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.line.left() {
|
||||||
|
self.parser.process(&[27, 91, 68]);
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Move(Direction::Right) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move up if not scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.line.right() {
|
||||||
|
self.parser.process(&[27, 91, 67]);
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Move(Direction::Up) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move up if not scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
self.history_prev();
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Cancel => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move to the beginning if scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.line.is_empty() {
|
||||||
|
self.line.backspace();
|
||||||
|
self.parser.process(&[8]); // Backspace character
|
||||||
|
// delete the last character from the line
|
||||||
|
// write one empty character to the terminal
|
||||||
|
self.parser.process(&[32]); // Space character
|
||||||
|
self.parser.process(&[8]); // Backspace character
|
||||||
|
}
|
||||||
|
CmdResult::Changed(self.state())
|
||||||
|
}
|
||||||
|
Cmd::Delete => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move to the beginning if scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.line.is_empty() {
|
||||||
|
self.line.delete();
|
||||||
|
self.parser.process(&[27, 91, 51, 126]); // Delete character
|
||||||
|
// write one empty character to the terminal
|
||||||
|
self.parser.process(&[32]); // Space character
|
||||||
|
self.parser.process(&[8]); // Backspace character
|
||||||
|
}
|
||||||
|
CmdResult::Changed(self.state())
|
||||||
|
}
|
||||||
|
Cmd::Scroll(Direction::Down) => {
|
||||||
|
self.scroll = self.scroll.saturating_sub(8);
|
||||||
|
self.parser.set_scrollback(self.scroll);
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Scroll(Direction::Up) => {
|
||||||
|
self.parser.set_scrollback(self.scroll.saturating_add(8));
|
||||||
|
let scrollback = self.parser.screen().scrollback();
|
||||||
|
self.scroll = scrollback;
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Toggle => {
|
||||||
|
// insert
|
||||||
|
self.parser.process(&[27, 91, 50, 126]); // Toggle insert mode
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::GoTo(Position::Begin) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move to the beginning if scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..self.line.begin() {
|
||||||
|
self.parser.process(&[27, 91, 68]); // Move cursor to the left
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::GoTo(Position::End) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move to the beginning if scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..self.line.end() {
|
||||||
|
self.parser.process(&[27, 91, 67]); // Move cursor to the right
|
||||||
|
}
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Submit => {
|
||||||
|
self.scroll = 0; // Reset scroll on submit
|
||||||
|
self.parser.set_scrollback(self.scroll);
|
||||||
|
|
||||||
|
if cfg!(target_family = "unix") {
|
||||||
|
self.parser.process(b"\n");
|
||||||
|
} else {
|
||||||
|
self.parser.process(b"\r\n\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = self.line.take();
|
||||||
|
if !line.is_empty() {
|
||||||
|
self.history.push(&line);
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdResult::Submit(State::One(StateValue::String(line)))
|
||||||
|
}
|
||||||
|
_ => CmdResult::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query(&self, attr: tuirealm::Attribute) -> Option<tuirealm::AttrValue> {
|
||||||
|
self.props.get(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::One(StateValue::String(self.line.content().to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&mut self, frame: &mut tuirealm::Frame, area: Rect) {
|
||||||
|
let width = area.width.saturating_sub(2);
|
||||||
|
let height = area.height.saturating_sub(2);
|
||||||
|
|
||||||
|
// update the terminal size if it has changed
|
||||||
|
if self.size != (width, height) {
|
||||||
|
self.size = (width, height);
|
||||||
|
self.parser.set_size(height, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = self
|
||||||
|
.query(Attribute::Title)
|
||||||
|
.map(|value| value.unwrap_string())
|
||||||
|
.unwrap_or_else(|| "Terminal".to_string());
|
||||||
|
|
||||||
|
let fg = self
|
||||||
|
.query(Attribute::Foreground)
|
||||||
|
.map(|value| value.unwrap_color())
|
||||||
|
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
||||||
|
|
||||||
|
let bg = self
|
||||||
|
.query(Attribute::Background)
|
||||||
|
.map(|value| value.unwrap_color())
|
||||||
|
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
||||||
|
|
||||||
|
let border_color = self
|
||||||
|
.query(Attribute::Borders)
|
||||||
|
.map(|value| value.unwrap_color())
|
||||||
|
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
||||||
|
|
||||||
|
let terminal = PseudoTerminal::new(self.parser.screen())
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(title)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(border_color))
|
||||||
|
.borders(BorderSides::ALL)
|
||||||
|
.style(Style::default().fg(fg).bg(bg)),
|
||||||
|
)
|
||||||
|
.style(Style::default().fg(fg).bg(bg));
|
||||||
|
|
||||||
|
frame.render_widget(terminal, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
/// Shell history management module.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct History {
|
||||||
|
/// Entries in the history.
|
||||||
|
entries: VecDeque<String>,
|
||||||
|
/// Maximum size of the history.
|
||||||
|
max_size: usize,
|
||||||
|
/// Current index in the history for navigation.
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl History {
|
||||||
|
/// Create a new [`History`] with a specified maximum size.
|
||||||
|
pub fn new(max_size: usize) -> Self {
|
||||||
|
History {
|
||||||
|
entries: VecDeque::with_capacity(max_size),
|
||||||
|
max_size,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a new command into the history.
|
||||||
|
pub fn push(&mut self, cmd: &str) {
|
||||||
|
if self.entries.len() == self.max_size {
|
||||||
|
self.entries.pop_front();
|
||||||
|
}
|
||||||
|
self.entries.push_back(cmd.to_string());
|
||||||
|
self.index = self.entries.len(); // Reset index to the end after adding a new command
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the previous command in the history.
|
||||||
|
///
|
||||||
|
/// Set also the index to the last command if it exists.
|
||||||
|
pub fn previous(&mut self) -> Option<String> {
|
||||||
|
if self.index > 0 {
|
||||||
|
self.index -= 1;
|
||||||
|
self.entries.get(self.index).cloned()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the next command in the history.
|
||||||
|
///
|
||||||
|
/// Set also the index to the next command if it exists.
|
||||||
|
pub fn next(&mut self) -> Option<String> {
|
||||||
|
if self.index < self.entries.len() {
|
||||||
|
let cmd = self.entries.get(self.index).cloned();
|
||||||
|
self.index += 1;
|
||||||
|
cmd
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::History;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_history() {
|
||||||
|
let mut history = History::new(5);
|
||||||
|
history.push("first");
|
||||||
|
history.push("second");
|
||||||
|
history.push("third");
|
||||||
|
|
||||||
|
assert_eq!(history.previous(), Some("third".to_string()));
|
||||||
|
assert_eq!(history.previous(), Some("second".to_string()));
|
||||||
|
assert_eq!(history.previous(), Some("first".to_string()));
|
||||||
|
assert_eq!(history.previous(), None); // No more previous commands
|
||||||
|
assert_eq!(history.next(), Some("first".to_string()));
|
||||||
|
assert_eq!(history.next(), Some("second".to_string()));
|
||||||
|
assert_eq!(history.next(), Some("third".to_string()));
|
||||||
|
assert_eq!(history.next(), None); // No more next commands
|
||||||
|
history.push("fourth");
|
||||||
|
assert_eq!(history.previous(), Some("fourth".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
/// A simple line for the shell, which keeps track of the current
|
||||||
|
/// content and the cursor position.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Line {
|
||||||
|
content: String,
|
||||||
|
cursor: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Line {
|
||||||
|
/// Set the content of the line and reset the cursor to the end.
|
||||||
|
pub fn set(&mut self, content: String) {
|
||||||
|
self.cursor = content.len();
|
||||||
|
self.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push a character to the line at the current cursor position.
|
||||||
|
pub fn push(&mut self, c: char) {
|
||||||
|
self.content.insert(self.cursor, c);
|
||||||
|
self.cursor += c.len_utf8();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take the current line content and reset the cursor.
|
||||||
|
pub fn take(&mut self) -> String {
|
||||||
|
self.cursor = 0;
|
||||||
|
std::mem::take(&mut self.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the current line content.
|
||||||
|
pub fn content(&self) -> &str {
|
||||||
|
&self.content
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the left, if possible.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the cursor was moved, `false` if it was already at the beginning.
|
||||||
|
pub fn left(&mut self) -> bool {
|
||||||
|
if self.cursor > 0 {
|
||||||
|
// get the previous character length
|
||||||
|
let prev_char_len = self
|
||||||
|
.content
|
||||||
|
.chars()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, c)| {
|
||||||
|
if i < self.cursor {
|
||||||
|
Some(c.len_utf8())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.last()
|
||||||
|
.unwrap();
|
||||||
|
self.cursor -= prev_char_len;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the right, if possible.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the cursor was moved, `false` if it was already at the end.
|
||||||
|
pub fn right(&mut self) -> bool {
|
||||||
|
if self.cursor < self.content.len() {
|
||||||
|
// get the next character length
|
||||||
|
let next_char_len = self.content[self.cursor..]
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.len_utf8();
|
||||||
|
self.cursor += next_char_len;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the beginning of the line.
|
||||||
|
///
|
||||||
|
/// Returns the previous cursor position.
|
||||||
|
pub fn begin(&mut self) -> usize {
|
||||||
|
std::mem::take(&mut self.cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the end of the line.
|
||||||
|
///
|
||||||
|
/// Returns the difference between the previous cursor position and the new position.
|
||||||
|
pub fn end(&mut self) -> usize {
|
||||||
|
let diff = self.content.len() - self.cursor;
|
||||||
|
self.cursor = self.content.len();
|
||||||
|
|
||||||
|
diff
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the previous character from the line at the current cursor position.
|
||||||
|
pub fn backspace(&mut self) {
|
||||||
|
if self.cursor > 0 {
|
||||||
|
let prev_char_len = self
|
||||||
|
.content
|
||||||
|
.chars()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, c)| {
|
||||||
|
if i < self.cursor {
|
||||||
|
Some(c.len_utf8())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.last()
|
||||||
|
.unwrap();
|
||||||
|
self.content.remove(self.cursor - prev_char_len);
|
||||||
|
self.cursor -= prev_char_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes the character at the current cursor position.
|
||||||
|
pub fn delete(&mut self) {
|
||||||
|
if self.cursor < self.content.len() {
|
||||||
|
self.content.remove(self.cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the line is empty.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.content.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_line() {
|
||||||
|
let mut line = Line::default();
|
||||||
|
assert!(line.is_empty());
|
||||||
|
|
||||||
|
line.push('H');
|
||||||
|
line.push('e');
|
||||||
|
line.push('l');
|
||||||
|
line.push('l');
|
||||||
|
line.push('o');
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
|
||||||
|
line.left();
|
||||||
|
line.left();
|
||||||
|
line.push(' ');
|
||||||
|
assert_eq!(line.content(), "Hel lo");
|
||||||
|
|
||||||
|
line.begin();
|
||||||
|
line.push('W');
|
||||||
|
assert_eq!(line.content(), "WHel lo");
|
||||||
|
|
||||||
|
line.end();
|
||||||
|
line.push('!');
|
||||||
|
assert_eq!(line.content(), "WHel lo!");
|
||||||
|
|
||||||
|
let taken = line.take();
|
||||||
|
assert_eq!(taken, "WHel lo!");
|
||||||
|
assert!(line.is_empty());
|
||||||
|
|
||||||
|
line.set("New Line".to_string());
|
||||||
|
assert_eq!(line.content(), "New Line");
|
||||||
|
|
||||||
|
line.backspace();
|
||||||
|
assert_eq!(line.content(), "New Lin");
|
||||||
|
line.left();
|
||||||
|
line.delete();
|
||||||
|
assert_eq!(line.content(), "New Li");
|
||||||
|
line.left();
|
||||||
|
line.left();
|
||||||
|
line.right();
|
||||||
|
assert_eq!(line.content(), "New Li");
|
||||||
|
line.end();
|
||||||
|
assert_eq!(line.content(), "New Li");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_return_whether_the_cursor_was_moved() {
|
||||||
|
let mut line = Line::default();
|
||||||
|
line.set("Hello".to_string());
|
||||||
|
|
||||||
|
assert!(line.left());
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
assert_eq!(line.cursor, 4);
|
||||||
|
|
||||||
|
assert!(line.left());
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
assert_eq!(line.cursor, 3);
|
||||||
|
|
||||||
|
assert!(line.right());
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
assert_eq!(line.cursor, 4);
|
||||||
|
assert!(line.right());
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
assert!(!line.right());
|
||||||
|
assert_eq!(line.cursor, 5);
|
||||||
|
assert!(!line.right());
|
||||||
|
|
||||||
|
line.end();
|
||||||
|
assert!(!line.right());
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
assert_eq!(line.cursor, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_allow_utf8_cursors() {
|
||||||
|
let mut line = Line::default();
|
||||||
|
line.set("Hello, 世界".to_string());
|
||||||
|
assert_eq!(line.content(), "Hello, 世界");
|
||||||
|
assert_eq!(line.cursor, 13); // "Hello, " is 7 bytes, "世界" is 6 bytes
|
||||||
|
|
||||||
|
assert!(line.left());
|
||||||
|
assert_eq!(line.content(), "Hello, 世界");
|
||||||
|
assert_eq!(line.cursor, 10); // Move left to '世'
|
||||||
|
assert!(line.left());
|
||||||
|
assert_eq!(line.content(), "Hello, 世界");
|
||||||
|
assert_eq!(line.cursor, 7); // Move left to ','
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -547,7 +547,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
|
|||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('x'),
|
code: Key::Char('x'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::Ui(UiMsg::ShowExecPopup)),
|
}) => Some(Msg::Ui(UiMsg::ShowTerminal)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('y'),
|
code: Key::Char('y'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
@@ -761,7 +761,7 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
|
|||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('x'),
|
code: Key::Char('x'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::Ui(UiMsg::ShowExecPopup)),
|
}) => Some(Msg::Ui(UiMsg::ShowTerminal)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('y'),
|
code: Key::Char('y'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
|
|||||||
@@ -148,6 +148,25 @@ impl Browser {
|
|||||||
self.sync_browsing = !self.sync_browsing;
|
self.sync_browsing = !self.sync_browsing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle terminal for the current tab
|
||||||
|
pub fn toggle_terminal(&mut self, terminal: bool) {
|
||||||
|
if self.tab == FileExplorerTab::HostBridge {
|
||||||
|
self.host_bridge.toggle_terminal(terminal);
|
||||||
|
} else if self.tab == FileExplorerTab::Remote {
|
||||||
|
self.remote.toggle_terminal(terminal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if terminal is open for the host bridge tab
|
||||||
|
pub fn is_terminal_open_host_bridge(&self) -> bool {
|
||||||
|
self.tab == FileExplorerTab::HostBridge && self.host_bridge.terminal_open()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if terminal is open for the remote tab
|
||||||
|
pub fn is_terminal_open_remote(&self) -> bool {
|
||||||
|
self.tab == FileExplorerTab::Remote && self.remote.terminal_open()
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a file explorer with local host setup
|
/// Build a file explorer with local host setup
|
||||||
pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer {
|
pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer {
|
||||||
let mut builder = Self::build_explorer(cli);
|
let mut builder = Self::build_explorer(cli);
|
||||||
|
|||||||
@@ -304,6 +304,70 @@ impl FileTransferActivity {
|
|||||||
self.reload_remote_filelist();
|
self.reload_remote_filelist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_tab_hostname(&self) -> String {
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||||
|
self.get_hostbridge_hostname()
|
||||||
|
}
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.get_remote_hostname(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn terminal_prompt(&self) -> String {
|
||||||
|
const TERM_CYAN: &str = "\x1b[36m";
|
||||||
|
const TERM_GREEN: &str = "\x1b[32m";
|
||||||
|
const TERM_YELLOW: &str = "\x1b[33m";
|
||||||
|
const TERM_RESET: &str = "\x1b[0m";
|
||||||
|
|
||||||
|
let panel = self.browser.tab();
|
||||||
|
match panel {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||||
|
let username = self
|
||||||
|
.context()
|
||||||
|
.host_bridge_params()
|
||||||
|
.and_then(|params| {
|
||||||
|
params
|
||||||
|
.username()
|
||||||
|
.map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@"))
|
||||||
|
})
|
||||||
|
.unwrap_or("".to_string());
|
||||||
|
let hostname = self.get_hostbridge_hostname();
|
||||||
|
format!(
|
||||||
|
"{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{}{TERM_RESET}$ ",
|
||||||
|
fmt_path_elide_ex(
|
||||||
|
self.host_bridge().wrkdir.as_path(),
|
||||||
|
0,
|
||||||
|
hostname.len() + 3 // 3 because of '/…/'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => {
|
||||||
|
let username = self
|
||||||
|
.context()
|
||||||
|
.remote_params()
|
||||||
|
.and_then(|params| {
|
||||||
|
params
|
||||||
|
.username()
|
||||||
|
.map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@"))
|
||||||
|
})
|
||||||
|
.unwrap_or("".to_string());
|
||||||
|
let hostname = self.get_remote_hostname();
|
||||||
|
let fmt_path = fmt_path_elide_ex(
|
||||||
|
self.remote().wrkdir.as_path(),
|
||||||
|
0,
|
||||||
|
hostname.len() + 3, // 3 because of '/…/'
|
||||||
|
);
|
||||||
|
let fmt_path = if fmt_path.starts_with('/') {
|
||||||
|
fmt_path
|
||||||
|
} else {
|
||||||
|
format!("/{}", fmt_path)
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{fmt_path}{TERM_RESET}$ ",)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn reload_remote_filelist(&mut self) {
|
pub(super) fn reload_remote_filelist(&mut self) {
|
||||||
let width = self
|
let width = self
|
||||||
.context_mut()
|
.context_mut()
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ enum Id {
|
|||||||
DeletePopup,
|
DeletePopup,
|
||||||
DisconnectPopup,
|
DisconnectPopup,
|
||||||
ErrorPopup,
|
ErrorPopup,
|
||||||
ExecPopup,
|
|
||||||
ExplorerFind,
|
ExplorerFind,
|
||||||
ExplorerHostBridge,
|
ExplorerHostBridge,
|
||||||
ExplorerRemote,
|
ExplorerRemote,
|
||||||
@@ -80,6 +79,8 @@ enum Id {
|
|||||||
StatusBarRemote,
|
StatusBarRemote,
|
||||||
SymlinkPopup,
|
SymlinkPopup,
|
||||||
SyncBrowsingMkdirPopup,
|
SyncBrowsingMkdirPopup,
|
||||||
|
TerminalHostBridge,
|
||||||
|
TerminalRemote,
|
||||||
TransferQueueHostBridge,
|
TransferQueueHostBridge,
|
||||||
TransferQueueRemote,
|
TransferQueueRemote,
|
||||||
WaitPopup,
|
WaitPopup,
|
||||||
@@ -176,7 +177,7 @@ enum UiMsg {
|
|||||||
ShowCopyPopup,
|
ShowCopyPopup,
|
||||||
ShowDeletePopup,
|
ShowDeletePopup,
|
||||||
ShowDisconnectPopup,
|
ShowDisconnectPopup,
|
||||||
ShowExecPopup,
|
ShowTerminal,
|
||||||
ShowFileInfoPopup,
|
ShowFileInfoPopup,
|
||||||
ShowFileSortingPopup,
|
ShowFileSortingPopup,
|
||||||
ShowFilterPopup,
|
ShowFilterPopup,
|
||||||
|
|||||||
@@ -146,17 +146,12 @@ impl FileTransferActivity {
|
|||||||
self.update_browser_file_list()
|
self.update_browser_file_list()
|
||||||
}
|
}
|
||||||
TransferMsg::ExecuteCmd(cmd) => {
|
TransferMsg::ExecuteCmd(cmd) => {
|
||||||
// Exex command
|
// Exec command
|
||||||
self.umount_exec();
|
|
||||||
self.mount_blocking_wait(format!("Executing '{cmd}'…").as_str());
|
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::HostBridge => self.action_local_exec(cmd),
|
FileExplorerTab::HostBridge => self.action_local_exec(cmd),
|
||||||
FileExplorerTab::Remote => self.action_remote_exec(cmd),
|
FileExplorerTab::Remote => self.action_remote_exec(cmd),
|
||||||
_ => panic!("Found tab doesn't support EXEC"),
|
_ => panic!("Found tab doesn't support EXEC"),
|
||||||
}
|
};
|
||||||
self.umount_wait();
|
|
||||||
// Reload files
|
|
||||||
self.update_browser_file_list()
|
|
||||||
}
|
}
|
||||||
TransferMsg::GoTo(dir) => {
|
TransferMsg::GoTo(dir) => {
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
@@ -417,7 +412,10 @@ impl FileTransferActivity {
|
|||||||
UiMsg::CloseDeletePopup => self.umount_radio_delete(),
|
UiMsg::CloseDeletePopup => self.umount_radio_delete(),
|
||||||
UiMsg::CloseDisconnectPopup => self.umount_disconnect(),
|
UiMsg::CloseDisconnectPopup => self.umount_disconnect(),
|
||||||
UiMsg::CloseErrorPopup => self.umount_error(),
|
UiMsg::CloseErrorPopup => self.umount_error(),
|
||||||
UiMsg::CloseExecPopup => self.umount_exec(),
|
UiMsg::CloseExecPopup => {
|
||||||
|
self.browser.toggle_terminal(false);
|
||||||
|
self.umount_exec();
|
||||||
|
}
|
||||||
UiMsg::CloseFatalPopup => {
|
UiMsg::CloseFatalPopup => {
|
||||||
self.umount_fatal();
|
self.umount_fatal();
|
||||||
self.exit_reason = Some(ExitReason::Disconnect);
|
self.exit_reason = Some(ExitReason::Disconnect);
|
||||||
@@ -546,7 +544,10 @@ impl FileTransferActivity {
|
|||||||
UiMsg::ShowCopyPopup => self.mount_copy(),
|
UiMsg::ShowCopyPopup => self.mount_copy(),
|
||||||
UiMsg::ShowDeletePopup => self.mount_radio_delete(),
|
UiMsg::ShowDeletePopup => self.mount_radio_delete(),
|
||||||
UiMsg::ShowDisconnectPopup => self.mount_disconnect(),
|
UiMsg::ShowDisconnectPopup => self.mount_disconnect(),
|
||||||
UiMsg::ShowExecPopup => self.mount_exec(),
|
UiMsg::ShowTerminal => {
|
||||||
|
self.browser.toggle_terminal(true);
|
||||||
|
self.mount_exec()
|
||||||
|
}
|
||||||
UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::HostBridge => {
|
UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::HostBridge => {
|
||||||
if let SelectedFile::One(file) = self.get_local_selected_entries() {
|
if let SelectedFile::One(file) = self.get_local_selected_entries() {
|
||||||
self.mount_file_info(&file);
|
self.mount_file_info(&file);
|
||||||
|
|||||||
@@ -158,12 +158,16 @@ impl FileTransferActivity {
|
|||||||
// @! Local explorer (Find or default)
|
// @! Local explorer (Find or default)
|
||||||
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) {
|
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) {
|
||||||
self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]);
|
self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]);
|
||||||
|
} else if self.browser.is_terminal_open_host_bridge() {
|
||||||
|
self.app.view(&Id::TerminalHostBridge, f, tabs_chunks[0]);
|
||||||
} else {
|
} else {
|
||||||
self.app.view(&Id::ExplorerHostBridge, f, tabs_chunks[0]);
|
self.app.view(&Id::ExplorerHostBridge, f, tabs_chunks[0]);
|
||||||
}
|
}
|
||||||
// @! Remote explorer (Find or default)
|
// @! Remote explorer (Find or default)
|
||||||
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) {
|
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) {
|
||||||
self.app.view(&Id::ExplorerFind, f, tabs_chunks[1]);
|
self.app.view(&Id::ExplorerFind, f, tabs_chunks[1]);
|
||||||
|
} else if self.browser.is_terminal_open_remote() {
|
||||||
|
self.app.view(&Id::TerminalRemote, f, tabs_chunks[1]);
|
||||||
} else {
|
} else {
|
||||||
self.app.view(&Id::ExplorerRemote, f, tabs_chunks[1]);
|
self.app.view(&Id::ExplorerRemote, f, tabs_chunks[1]);
|
||||||
}
|
}
|
||||||
@@ -238,11 +242,6 @@ impl FileTransferActivity {
|
|||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.app.view(&Id::SymlinkPopup, f, popup);
|
self.app.view(&Id::SymlinkPopup, f, popup);
|
||||||
} else if self.app.mounted(&Id::ExecPopup) {
|
|
||||||
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area());
|
|
||||||
f.render_widget(Clear, popup);
|
|
||||||
// make popup
|
|
||||||
self.app.view(&Id::ExecPopup, f, popup);
|
|
||||||
} else if self.app.mounted(&Id::FileInfoPopup) {
|
} else if self.app.mounted(&Id::FileInfoPopup) {
|
||||||
let popup = Popup(Size::Percentage(50), Size::Percentage(50)).draw_in(f.area());
|
let popup = Popup(Size::Percentage(50), Size::Percentage(50)).draw_in(f.area());
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
@@ -570,21 +569,69 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn mount_exec(&mut self) {
|
pub(super) fn mount_exec(&mut self) {
|
||||||
|
let tab = self.browser.tab();
|
||||||
|
let id = match tab {
|
||||||
|
FileExplorerTab::HostBridge => Id::TerminalHostBridge,
|
||||||
|
FileExplorerTab::Remote => Id::TerminalRemote,
|
||||||
|
_ => panic!("Cannot mount terminal on this tab"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let border = match tab {
|
||||||
|
FileExplorerTab::HostBridge => self.theme().transfer_local_explorer_highlighted,
|
||||||
|
FileExplorerTab::Remote => self.theme().transfer_remote_explorer_highlighted,
|
||||||
|
_ => panic!("Cannot mount terminal on this tab"),
|
||||||
|
};
|
||||||
|
|
||||||
let input_color = self.theme().misc_input_dialog;
|
let input_color = self.theme().misc_input_dialog;
|
||||||
assert!(
|
assert!(
|
||||||
self.app
|
self.app
|
||||||
.remount(
|
.remount(
|
||||||
Id::ExecPopup,
|
id.clone(),
|
||||||
Box::new(components::ExecPopup::new(input_color)),
|
Box::new(
|
||||||
|
components::Terminal::default()
|
||||||
|
.foreground(input_color)
|
||||||
|
.prompt(self.terminal_prompt())
|
||||||
|
.title(format!("Terminal - {}", self.get_tab_hostname()))
|
||||||
|
.border_color(border)
|
||||||
|
),
|
||||||
vec![],
|
vec![],
|
||||||
)
|
)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
);
|
);
|
||||||
assert!(self.app.active(&Id::ExecPopup).is_ok());
|
assert!(self.app.active(&id).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the terminal prompt based on the current directory
|
||||||
|
pub(super) fn update_terminal_prompt(&mut self) {
|
||||||
|
let prompt = self.terminal_prompt();
|
||||||
|
let id = match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge => Id::TerminalHostBridge,
|
||||||
|
FileExplorerTab::Remote => Id::TerminalRemote,
|
||||||
|
_ => panic!("Cannot update terminal prompt on this tab"),
|
||||||
|
};
|
||||||
|
let _ = self
|
||||||
|
.app
|
||||||
|
.attr(&id, Attribute::Content, AttrValue::String(prompt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print output to terminal
|
||||||
|
pub(super) fn print_terminal(&mut self, text: String) {
|
||||||
|
// get id
|
||||||
|
let focus = self.app.focus().unwrap().clone();
|
||||||
|
|
||||||
|
// replace all \n with \r\n
|
||||||
|
let mut text = text.replace('\n', "\r\n");
|
||||||
|
if !text.ends_with("\r\n") && !text.is_empty() {
|
||||||
|
text.push_str("\r\n");
|
||||||
|
}
|
||||||
|
let _ = self
|
||||||
|
.app
|
||||||
|
.attr(&focus, Attribute::Text, AttrValue::String(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn umount_exec(&mut self) {
|
pub(super) fn umount_exec(&mut self) {
|
||||||
let _ = self.app.umount(&Id::ExecPopup);
|
let focus = self.app.focus().unwrap().clone();
|
||||||
|
let _ = self.app.umount(&focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn mount_find(&mut self, msg: impl ToString, fuzzy_search: bool) {
|
pub(super) fn mount_find(&mut self, msg: impl ToString, fuzzy_search: bool) {
|
||||||
@@ -1102,6 +1149,10 @@ impl FileTransferActivity {
|
|||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
|
if x.as_payload().is_none() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
x.unwrap_payload()
|
x.unwrap_payload()
|
||||||
.unwrap_vec()
|
.unwrap_vec()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -1179,7 +1230,8 @@ impl FileTransferActivity {
|
|||||||
Id::DeletePopup,
|
Id::DeletePopup,
|
||||||
Id::DisconnectPopup,
|
Id::DisconnectPopup,
|
||||||
Id::ErrorPopup,
|
Id::ErrorPopup,
|
||||||
Id::ExecPopup,
|
Id::TerminalHostBridge,
|
||||||
|
Id::TerminalRemote,
|
||||||
Id::FatalPopup,
|
Id::FatalPopup,
|
||||||
Id::FileInfoPopup,
|
Id::FileInfoPopup,
|
||||||
Id::GotoPopup,
|
Id::GotoPopup,
|
||||||
|
|||||||
Reference in New Issue
Block a user