commit
3df8ed13a4
|
@ -24,9 +24,13 @@ Released on FIXME: ??
|
|||
|
||||
> 🏄 Summer update 2021🌴
|
||||
|
||||
- **Open any file** in explorer:
|
||||
- Open file with default program for file type with `<V>`
|
||||
- Open file with a specific program with `<W>`
|
||||
- Bugfix:
|
||||
- Fixed broken input cursor when typing UTF8 characters (tui-realm 0.3.2)
|
||||
- Dependencies:
|
||||
- Added `open 1.7.0`
|
||||
- Updated `textwrap` to `0.14.0`
|
||||
- Updated `tui-realm` to `0.4.1`
|
||||
|
||||
|
|
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -757,6 +757,16 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1711eb4b31ce4ad35b0f316d8dfba4fe5c7ad601c448446d84aae7a896627b20"
|
||||
dependencies = [
|
||||
"which",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.34"
|
||||
|
@ -1329,6 +1339,7 @@ dependencies = [
|
|||
"lazy_static",
|
||||
"log",
|
||||
"magic-crypt",
|
||||
"open",
|
||||
"path-slash",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.3",
|
||||
|
|
|
@ -39,6 +39,7 @@ hostname = "0.3.1"
|
|||
lazy_static = "1.4.0"
|
||||
log = "0.4.14"
|
||||
magic-crypt = "3.1.7"
|
||||
open = "1.7.0"
|
||||
rand = "0.8.3"
|
||||
regex = "1.5.4"
|
||||
rpassword = "5.0.1"
|
||||
|
|
15
README.md
15
README.md
|
@ -69,6 +69,20 @@ while if you're a Windows user, you can install termscp with [Chocolatey](https:
|
|||
|
||||
For more information or other platforms, please visit [veeso.github.io](https://veeso.github.io/termscp/#get-started) to view all installation methods.
|
||||
|
||||
### Soft Requirements ✔️
|
||||
|
||||
These requirements are not forcely required to run termscp, but to enjoy all of its features
|
||||
|
||||
- **Linux** users
|
||||
- To **open** files via `V` (at least one of these)
|
||||
- *xdg-open*
|
||||
- *gio*
|
||||
- *gnome-open*
|
||||
- *kde-open*
|
||||
- **WSL** users
|
||||
- To **open** files via `V` (at least one of these)
|
||||
- [wslu](https://github.com/wslutilities/wslu)
|
||||
|
||||
---
|
||||
|
||||
## Buy me a coffee ☕
|
||||
|
@ -134,6 +148,7 @@ termscp is powered by these aweseome projects:
|
|||
- [crossterm](https://github.com/crossterm-rs/crossterm)
|
||||
- [edit](https://github.com/milkey-mouse/edit)
|
||||
- [keyring-rs](https://github.com/hwchen/keyring-rs)
|
||||
- [open-rs](https://github.com/Byron/open-rs)
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-ftp](https://github.com/mattnenterprise/rust-ftp)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
|
|
19
docs/man.md
19
docs/man.md
|
@ -105,6 +105,8 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
|
|||
| `<R>` | Rename file | Rename |
|
||||
| `<S>` | Save file as... | Save |
|
||||
| `<U>` | Go to parent directory | Upper |
|
||||
| `<V>` | Open file with default program for filetype | View |
|
||||
| `<W>` | Open file with provided program | With |
|
||||
| `<X>` | Execute a command | eXecute |
|
||||
| `<Y>` | Toggle synchronized browsing | sYnc |
|
||||
| `<DEL>` | Delete file | |
|
||||
|
@ -130,6 +132,23 @@ This means that whenever you'll change the working directory on one panel, the s
|
|||
|
||||
*Warning*: at the moment, whenever you try to access an unexisting directory, you won't be prompted to create it. This might change in a future update.
|
||||
|
||||
### Open and Open With 🚪
|
||||
|
||||
Open and open with commands are powered by [open-rs](https://docs.rs/crate/open/1.7.0).
|
||||
When opening files with View command (`<V>`), the system default application for the file type will be used. To do so, the default operting system service will be used, so be sure to have at least one of these installed on your system:
|
||||
|
||||
- **Windows** users: you don't have to worry about it, since the crate will use the `start` command.
|
||||
- **MacOS** users: you don't have to worry either, since the crate will use `open`, which is already installed on your system.
|
||||
- **Linux** users: one of these should be installed
|
||||
- *xdg-open*
|
||||
- *gio*
|
||||
- *gnome-open*
|
||||
- *kde-open*
|
||||
- **WSL** users: *wslview* is required, you must install [wslu](https://github.com/wslutilities/wslu).
|
||||
|
||||
> Q: Can I edit remote files using the view command?
|
||||
> A: No, at least not directly from the "remote panel". You have to download it to a local directory first, that's due to the fact that when you open a remote file, the file is downloaded into a temporary directory, but there's no way to create a watcher for the file to check when the program you used to open it was closed, so termscp is not able to know when you're done editing the file.
|
||||
|
||||
---
|
||||
|
||||
## Bookmarks ⭐
|
||||
|
|
|
@ -140,4 +140,49 @@ impl FileTransferActivity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn action_find_open(&mut self) {
|
||||
match self.get_found_selected_entries() {
|
||||
SelectedEntry::One(entry) => {
|
||||
// Open file
|
||||
self.open_found_file(&entry, None);
|
||||
}
|
||||
SelectedEntry::Many(entries) => {
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
// Open file
|
||||
self.open_found_file(entry, None);
|
||||
}
|
||||
}
|
||||
SelectedEntry::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn action_find_open_with(&mut self, with: &str) {
|
||||
match self.get_found_selected_entries() {
|
||||
SelectedEntry::One(entry) => {
|
||||
// Open file
|
||||
self.open_found_file(&entry, Some(with));
|
||||
}
|
||||
SelectedEntry::Many(entries) => {
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
// Open file
|
||||
self.open_found_file(entry, Some(with));
|
||||
}
|
||||
}
|
||||
SelectedEntry::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_found_file(&mut self, entry: &FsEntry, with: Option<&str>) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||
self.action_open_local_file(entry, with);
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
self.action_open_remote_file(entry, with);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,15 +37,19 @@ pub(crate) mod exec;
|
|||
pub(crate) mod find;
|
||||
pub(crate) mod mkdir;
|
||||
pub(crate) mod newfile;
|
||||
pub(crate) mod open;
|
||||
pub(crate) mod rename;
|
||||
pub(crate) mod save;
|
||||
pub(crate) mod submit;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SelectedEntry {
|
||||
One(FsEntry),
|
||||
Many(Vec<FsEntry>),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SelectedEntryIndex {
|
||||
One(usize),
|
||||
Many(Vec<usize>),
|
||||
|
|
155
src/ui/activities/filetransfer/actions/open.rs
Normal file
155
src/ui/activities/filetransfer/actions/open.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// deps
|
||||
extern crate open;
|
||||
// locals
|
||||
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
||||
// ext
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### action_open_local
|
||||
///
|
||||
/// Open local file
|
||||
pub(crate) fn action_open_local(&mut self) {
|
||||
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
|
||||
SelectedEntry::One(entry) => vec![entry],
|
||||
SelectedEntry::Many(entries) => entries,
|
||||
SelectedEntry::None => vec![],
|
||||
};
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_local_file(x, None));
|
||||
}
|
||||
|
||||
/// ### action_open_remote
|
||||
///
|
||||
/// Open local file
|
||||
pub(crate) fn action_open_remote(&mut self) {
|
||||
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
|
||||
SelectedEntry::One(entry) => vec![entry],
|
||||
SelectedEntry::Many(entries) => entries,
|
||||
SelectedEntry::None => vec![],
|
||||
};
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_remote_file(x, None));
|
||||
}
|
||||
|
||||
/// ### action_open_local_file
|
||||
///
|
||||
/// Perform open lopcal file
|
||||
pub(crate) fn action_open_local_file(&mut self, entry: &FsEntry, open_with: Option<&str>) {
|
||||
let entry: FsEntry = entry.get_realfile();
|
||||
self.open_path_with(entry.get_abs_path().as_path(), open_with);
|
||||
}
|
||||
|
||||
/// ### action_open_local
|
||||
///
|
||||
/// Open remote file. The file is first downloaded to a temporary directory on localhost
|
||||
pub(crate) fn action_open_remote_file(&mut self, entry: &FsEntry, open_with: Option<&str>) {
|
||||
let entry: FsEntry = entry.get_realfile();
|
||||
// Download file
|
||||
let tmpfile: String =
|
||||
match self.get_cache_tmp_name(entry.get_name(), entry.get_ftype().as_deref()) {
|
||||
None => {
|
||||
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
||||
return;
|
||||
}
|
||||
Some(p) => p,
|
||||
};
|
||||
let cache: PathBuf = match self.cache.as_ref() {
|
||||
None => {
|
||||
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
||||
return;
|
||||
}
|
||||
Some(p) => p.path().to_path_buf(),
|
||||
};
|
||||
self.filetransfer_recv(&entry, cache.as_path(), Some(tmpfile.clone()));
|
||||
// Make file and open if file exists
|
||||
let mut tmp: PathBuf = cache;
|
||||
tmp.push(tmpfile.as_str());
|
||||
if tmp.exists() {
|
||||
self.open_path_with(tmp.as_path(), open_with);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### action_local_open_with
|
||||
///
|
||||
/// Open selected file with provided application
|
||||
pub(crate) fn action_local_open_with(&mut self, with: &str) {
|
||||
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
|
||||
SelectedEntry::One(entry) => vec![entry],
|
||||
SelectedEntry::Many(entries) => entries,
|
||||
SelectedEntry::None => vec![],
|
||||
};
|
||||
// Open all entries
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_local_file(x, Some(with)));
|
||||
}
|
||||
|
||||
/// ### action_remote_open_with
|
||||
///
|
||||
/// Open selected file with provided application
|
||||
pub(crate) fn action_remote_open_with(&mut self, with: &str) {
|
||||
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
|
||||
SelectedEntry::One(entry) => vec![entry],
|
||||
SelectedEntry::Many(entries) => entries,
|
||||
SelectedEntry::None => vec![],
|
||||
};
|
||||
// Open all entries
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_remote_file(x, Some(with)));
|
||||
}
|
||||
|
||||
/// ### open_path_with
|
||||
///
|
||||
/// Common function which opens a path with default or specified program.
|
||||
fn open_path_with(&mut self, p: &Path, with: Option<&str>) {
|
||||
// Open file
|
||||
let result = match with {
|
||||
None => open::that(p),
|
||||
Some(with) => open::with(p, with),
|
||||
};
|
||||
// Log result
|
||||
match result {
|
||||
Ok(_) => self.log(LogLevel::Info, format!("Opened file `{}`", p.display())),
|
||||
Err(err) => self.log(
|
||||
LogLevel::Error,
|
||||
format!("Failed to open filoe `{}`: {}", p.display(), err),
|
||||
),
|
||||
}
|
||||
// NOTE: clear screen in order to prevent crap on stderr
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Clear screen
|
||||
ctx.clear_screen();
|
||||
}
|
||||
}
|
||||
}
|
88
src/ui/activities/filetransfer/actions/submit.rs
Normal file
88
src/ui/activities/filetransfer/actions/submit.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use super::{FileTransferActivity, FsEntry};
|
||||
|
||||
enum SubmitAction {
|
||||
ChangeDir,
|
||||
None,
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### action_submit_local
|
||||
///
|
||||
/// Decides which action to perform on submit for local explorer
|
||||
/// Return true whether the directory changed
|
||||
pub(crate) fn action_submit_local(&mut self, entry: FsEntry) -> bool {
|
||||
let action: SubmitAction = match &entry {
|
||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
||||
FsEntry::File(file) => {
|
||||
match &file.symlink {
|
||||
Some(symlink_entry) => {
|
||||
// If symlink and is directory, point to symlink
|
||||
match &**symlink_entry {
|
||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
||||
_ => SubmitAction::None,
|
||||
}
|
||||
}
|
||||
None => SubmitAction::None,
|
||||
}
|
||||
}
|
||||
};
|
||||
match action {
|
||||
SubmitAction::ChangeDir => self.action_enter_local_dir(entry, false),
|
||||
SubmitAction::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### action_submit_remote
|
||||
///
|
||||
/// Decides which action to perform on submit for remote explorer
|
||||
/// Return true whether the directory changed
|
||||
pub(crate) fn action_submit_remote(&mut self, entry: FsEntry) -> bool {
|
||||
let action: SubmitAction = match &entry {
|
||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
||||
FsEntry::File(file) => {
|
||||
match &file.symlink {
|
||||
Some(symlink_entry) => {
|
||||
// If symlink and is directory, point to symlink
|
||||
match &**symlink_entry {
|
||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
||||
_ => SubmitAction::None,
|
||||
}
|
||||
}
|
||||
None => SubmitAction::None,
|
||||
}
|
||||
}
|
||||
};
|
||||
match action {
|
||||
SubmitAction::ChangeDir => self.action_enter_remote_dir(entry, false),
|
||||
SubmitAction::None => false,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -171,7 +171,7 @@ impl Browser {
|
|||
.with_group_dirs(Some(GroupDirs::First))
|
||||
.with_hidden_files(true)
|
||||
.with_stack_size(0)
|
||||
.with_formatter(Some("{NAME} {SYMLINK}"))
|
||||
.with_formatter(Some("{NAME:32} {SYMLINK}"))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ use lib::transfer::TransferStates;
|
|||
use chrono::{DateTime, Local};
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
use tuirealm::View;
|
||||
|
||||
// -- Storage keys
|
||||
|
@ -82,6 +82,7 @@ const COMPONENT_INPUT_FIND: &str = "INPUT_FIND";
|
|||
const COMPONENT_INPUT_GOTO: &str = "INPUT_GOTO";
|
||||
const COMPONENT_INPUT_MKDIR: &str = "INPUT_MKDIR";
|
||||
const COMPONENT_INPUT_NEWFILE: &str = "INPUT_NEWFILE";
|
||||
const COMPONENT_INPUT_OPEN_WITH: &str = "INPUT_OPEN_WITH";
|
||||
const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME";
|
||||
const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS";
|
||||
const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
|
||||
|
@ -134,6 +135,7 @@ pub struct FileTransferActivity {
|
|||
browser: Browser, // Browser
|
||||
log_records: VecDeque<LogRecord>, // Log records
|
||||
transfer: TransferStates, // Transfer states
|
||||
cache: Option<TempDir>, // Temporary directory where to store stuff
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
|
@ -160,6 +162,10 @@ impl FileTransferActivity {
|
|||
browser: Browser::new(config_client.as_ref()),
|
||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||
transfer: TransferStates::default(),
|
||||
cache: match TempDir::new() {
|
||||
Ok(d) => Some(d),
|
||||
Err(_) => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,6 +192,26 @@ impl FileTransferActivity {
|
|||
pub(crate) fn found_mut(&mut self) -> Option<&mut FileExplorer> {
|
||||
self.browser.found_mut()
|
||||
}
|
||||
|
||||
/// ### get_cache_tmp_name
|
||||
///
|
||||
/// Get file name for a file in cache
|
||||
pub(crate) fn get_cache_tmp_name(&self, name: &str, file_type: Option<&str>) -> Option<String> {
|
||||
self.cache.as_ref().map(|_| {
|
||||
let base: String = format!(
|
||||
"{}-{}",
|
||||
name,
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis()
|
||||
);
|
||||
match file_type {
|
||||
None => base,
|
||||
Some(file_type) => format!("{}.{}", base, file_type),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -209,11 +235,8 @@ impl Activity for FileTransferActivity {
|
|||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Set working directory
|
||||
let pwd: PathBuf = self.host.pwd();
|
||||
// Get files at current wd
|
||||
self.local_scan(pwd.as_path());
|
||||
self.local_mut().wrkdir = pwd;
|
||||
// Get files at current pwd
|
||||
self.reload_local_dir();
|
||||
debug!("Read working directory");
|
||||
// Configure text editor
|
||||
self.setup_text_editor();
|
||||
|
@ -279,6 +302,12 @@ impl Activity for FileTransferActivity {
|
|||
/// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity.
|
||||
/// This function must be called once before terminating the activity.
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Destroy cache
|
||||
if let Some(cache) = self.cache.take() {
|
||||
if let Err(err) = cache.close() {
|
||||
error!("Failed to delete cache: {}", err);
|
||||
}
|
||||
}
|
||||
// Disable raw mode
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
|
|
|
@ -135,19 +135,59 @@ impl FileTransferActivity {
|
|||
|
||||
/// ### reload_remote_dir
|
||||
///
|
||||
/// Reload remote directory entries
|
||||
/// Reload remote directory entries and update browser
|
||||
pub(super) fn reload_remote_dir(&mut self) {
|
||||
// Get current entries
|
||||
if let Ok(pwd) = self.client.pwd() {
|
||||
self.remote_scan(pwd.as_path());
|
||||
if let Ok(wrkdir) = self.client.pwd() {
|
||||
self.remote_scan(wrkdir.as_path());
|
||||
// Set wrkdir
|
||||
self.remote_mut().wrkdir = pwd;
|
||||
self.remote_mut().wrkdir = wrkdir;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### reload_local_dir
|
||||
///
|
||||
/// Reload local directory entries and update browser
|
||||
pub(super) fn reload_local_dir(&mut self) {
|
||||
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
||||
let wrkdir: PathBuf = self.host.pwd();
|
||||
self.local_scan(wrkdir.as_path());
|
||||
self.local_mut().wrkdir = wrkdir;
|
||||
}
|
||||
|
||||
/// ### local_scan
|
||||
///
|
||||
/// Scan current local directory
|
||||
fn local_scan(&mut self, path: &Path) {
|
||||
match self.host.scan_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.local_mut().set_files(files);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current directory: {}", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### remote_scan
|
||||
///
|
||||
/// Scan current remote directory
|
||||
fn remote_scan(&mut self, path: &Path) {
|
||||
match self.client.list_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.remote_mut().set_files(files);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current directory: {}", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### filetransfer_send
|
||||
|
@ -559,7 +599,7 @@ impl FileTransferActivity {
|
|||
}
|
||||
}
|
||||
// Reload directory on local
|
||||
self.local_scan(local_path);
|
||||
self.reload_local_dir();
|
||||
// if aborted; show alert
|
||||
if self.transfer.aborted() {
|
||||
// Log abort
|
||||
|
@ -688,42 +728,6 @@ impl FileTransferActivity {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// ### local_scan
|
||||
///
|
||||
/// Scan current local directory
|
||||
pub(super) fn local_scan(&mut self, path: &Path) {
|
||||
match self.host.scan_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.local_mut().set_files(files);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current directory: {}", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### remote_scan
|
||||
///
|
||||
/// Scan current remote directory
|
||||
pub(super) fn remote_scan(&mut self, path: &Path) {
|
||||
match self.client.list_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.remote_mut().set_files(files);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current directory: {}", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### local_changedir
|
||||
///
|
||||
/// Change directory for local
|
||||
|
@ -738,9 +742,7 @@ impl FileTransferActivity {
|
|||
format!("Changed directory on local: {}", path.display()),
|
||||
);
|
||||
// Reload files
|
||||
self.local_scan(path);
|
||||
// Set wrkdir
|
||||
self.local_mut().wrkdir = PathBuf::from(path);
|
||||
self.reload_local_dir();
|
||||
// Push prev_dir to stack
|
||||
if push {
|
||||
self.local_mut().pushd(prev_dir.as_path())
|
||||
|
@ -767,9 +769,7 @@ impl FileTransferActivity {
|
|||
format!("Changed directory on remote: {}", path.display()),
|
||||
);
|
||||
// Update files
|
||||
self.remote_scan(path);
|
||||
// Set wrkdir
|
||||
self.remote_mut().wrkdir = PathBuf::from(path);
|
||||
self.reload_remote_dir();
|
||||
// Push prev_dir to stack
|
||||
if push {
|
||||
self.remote_mut().pushd(prev_dir.as_path())
|
||||
|
@ -809,6 +809,7 @@ impl FileTransferActivity {
|
|||
return Err(format!("Could not read file: {}", err));
|
||||
}
|
||||
}
|
||||
debug!("Ok, file {} is textual; opening file...", path.display());
|
||||
// Put input mode back to normal
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
|
@ -844,38 +845,32 @@ impl FileTransferActivity {
|
|||
/// Edit file on remote host
|
||||
pub(super) fn edit_remote_file(&mut self, file: &FsFile) -> Result<(), String> {
|
||||
// Create temp file
|
||||
let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
return Err(format!("Could not create temporary file: {}", err));
|
||||
}
|
||||
let tmpfile: PathBuf = match self.download_file_as_temp(file) {
|
||||
Ok(p) => p,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
// Download file
|
||||
if let Err(err) = self.filetransfer_recv_file(tmpfile.path(), file, file.name.clone()) {
|
||||
return Err(format!("Could not open file {}: {}", file.name, err));
|
||||
}
|
||||
// Get current file modification time
|
||||
let prev_mtime: SystemTime = match self.host.stat(tmpfile.path()) {
|
||||
let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) {
|
||||
Ok(e) => e.get_last_change_time(),
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Could not stat \"{}\": {}",
|
||||
tmpfile.path().display(),
|
||||
tmpfile.as_path().display(),
|
||||
err
|
||||
))
|
||||
}
|
||||
};
|
||||
// Edit file
|
||||
if let Err(err) = self.edit_local_file(tmpfile.path()) {
|
||||
if let Err(err) = self.edit_local_file(tmpfile.as_path()) {
|
||||
return Err(err);
|
||||
}
|
||||
// Get local fs entry
|
||||
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.path()) {
|
||||
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.as_path()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Could not stat \"{}\": {}",
|
||||
tmpfile.path().display(),
|
||||
tmpfile.as_path().display(),
|
||||
err
|
||||
))
|
||||
}
|
||||
|
@ -891,12 +886,12 @@ impl FileTransferActivity {
|
|||
),
|
||||
);
|
||||
// Get local fs entry
|
||||
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.path()) {
|
||||
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.as_path()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Could not stat \"{}\": {}",
|
||||
tmpfile.path().display(),
|
||||
tmpfile.as_path().display(),
|
||||
err
|
||||
))
|
||||
}
|
||||
|
@ -1035,6 +1030,33 @@ impl FileTransferActivity {
|
|||
}
|
||||
}
|
||||
|
||||
/// ### download_file_as_temp
|
||||
///
|
||||
/// Download provided file as a temporary file
|
||||
pub(super) fn download_file_as_temp(&mut self, file: &FsFile) -> Result<PathBuf, String> {
|
||||
let tmpfile: PathBuf = match self.cache.as_ref() {
|
||||
Some(cache) => {
|
||||
let mut p: PathBuf = cache.path().to_path_buf();
|
||||
p.push(file.name.as_str());
|
||||
p
|
||||
}
|
||||
None => {
|
||||
return Err(String::from(
|
||||
"Could not create tempfile: cache not available",
|
||||
))
|
||||
}
|
||||
};
|
||||
// Download file
|
||||
match self.filetransfer_recv_file(tmpfile.as_path(), file, file.name.clone()) {
|
||||
Err(err) => Err(format!(
|
||||
"Could not download {} to temporary file: {}",
|
||||
file.abs_path.display(),
|
||||
err
|
||||
)),
|
||||
Ok(()) => Ok(tmpfile),
|
||||
}
|
||||
}
|
||||
|
||||
// -- transfer sizes
|
||||
|
||||
/// ### get_total_transfer_size_local
|
||||
|
|
|
@ -32,11 +32,11 @@ use super::{
|
|||
actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel,
|
||||
COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE,
|
||||
COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO,
|
||||
COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS,
|
||||
COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR_FULL,
|
||||
COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE, COMPONENT_RADIO_DISCONNECT,
|
||||
COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL,
|
||||
COMPONENT_TEXT_HELP,
|
||||
COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH,
|
||||
COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX,
|
||||
COMPONENT_PROGRESS_BAR_FULL, COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE,
|
||||
COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING,
|
||||
COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
|
||||
};
|
||||
use crate::fs::explorer::FileSorting;
|
||||
use crate::fs::FsEntry;
|
||||
|
@ -87,7 +87,7 @@ impl Update for FileTransferActivity {
|
|||
entry = Some(e.clone());
|
||||
}
|
||||
if let Some(entry) = entry {
|
||||
if self.action_enter_local_dir(entry, false) {
|
||||
if self.action_submit_local(entry) {
|
||||
// Update file list if sync
|
||||
if self.browser.sync_browsing {
|
||||
let _ = self.update_remote_filelist();
|
||||
|
@ -118,8 +118,7 @@ impl Update for FileTransferActivity {
|
|||
}
|
||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_L) => {
|
||||
// Reload directory
|
||||
let pwd: PathBuf = self.local().wrkdir.clone();
|
||||
self.local_scan(pwd.as_path());
|
||||
self.reload_local_dir();
|
||||
// Reload file list component
|
||||
self.update_local_filelist()
|
||||
}
|
||||
|
@ -150,7 +149,7 @@ impl Update for FileTransferActivity {
|
|||
entry = Some(e.clone());
|
||||
}
|
||||
if let Some(entry) = entry {
|
||||
if self.action_enter_remote_dir(entry, false) {
|
||||
if self.action_submit_remote(entry) {
|
||||
// Update file list if sync
|
||||
if self.browser.sync_browsing {
|
||||
let _ = self.update_local_filelist();
|
||||
|
@ -191,8 +190,7 @@ impl Update for FileTransferActivity {
|
|||
}
|
||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_L) => {
|
||||
// Reload directory
|
||||
let pwd: PathBuf = self.remote().wrkdir.clone();
|
||||
self.remote_scan(pwd.as_path());
|
||||
self.reload_remote_dir();
|
||||
// Reload file list component
|
||||
self.update_remote_filelist()
|
||||
}
|
||||
|
@ -266,6 +264,26 @@ impl Update for FileTransferActivity {
|
|||
self.mount_saveas();
|
||||
None
|
||||
}
|
||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_V)
|
||||
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_V)
|
||||
| (COMPONENT_EXPLORER_FIND, &MSG_KEY_CHAR_V) => {
|
||||
// View
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_open_local(),
|
||||
FileExplorerTab::Remote => self.action_open_remote(),
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
self.action_find_open()
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_W)
|
||||
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_W)
|
||||
| (COMPONENT_EXPLORER_FIND, &MSG_KEY_CHAR_W) => {
|
||||
// Open with
|
||||
self.mount_openwith();
|
||||
None
|
||||
}
|
||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_X)
|
||||
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_X) => {
|
||||
// Mount exec
|
||||
|
@ -484,6 +502,22 @@ impl Update for FileTransferActivity {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
// -- open with
|
||||
(COMPONENT_INPUT_OPEN_WITH, &MSG_KEY_ESC) => {
|
||||
self.umount_openwith();
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_OPEN_WITH, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_local_open_with(input),
|
||||
FileExplorerTab::Remote => self.action_remote_open_with(input),
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
self.action_find_open_with(input)
|
||||
}
|
||||
}
|
||||
self.umount_openwith();
|
||||
None
|
||||
}
|
||||
// -- rename
|
||||
(COMPONENT_INPUT_RENAME, &MSG_KEY_ESC) => {
|
||||
self.umount_rename();
|
||||
|
|
|
@ -215,6 +215,14 @@ impl FileTransferActivity {
|
|||
self.view.render(super::COMPONENT_INPUT_NEWFILE, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_OPEN_WITH) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_INPUT_OPEN_WITH, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_RENAME) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
|
@ -593,6 +601,23 @@ impl FileTransferActivity {
|
|||
self.view.umount(super::COMPONENT_INPUT_NEWFILE);
|
||||
}
|
||||
|
||||
pub(super) fn mount_openwith(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_OPEN_WITH,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_label(String::from("Open file with..."))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.active(super::COMPONENT_INPUT_OPEN_WITH);
|
||||
}
|
||||
|
||||
pub(super) fn umount_openwith(&mut self) {
|
||||
self.view.umount(super::COMPONENT_INPUT_OPEN_WITH);
|
||||
}
|
||||
|
||||
pub(super) fn mount_rename(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_RENAME,
|
||||
|
@ -1030,7 +1055,9 @@ impl FileTransferActivity {
|
|||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Open text file"))
|
||||
.add_col(TextSpan::from(
|
||||
" Open text file with preferred editor",
|
||||
))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<Q>")
|
||||
|
@ -1064,6 +1091,26 @@ impl FileTransferActivity {
|
|||
)
|
||||
.add_col(TextSpan::from(" Go to parent directory"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<V>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(
|
||||
" Open file with default application for file type",
|
||||
))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<W>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(
|
||||
" Open file with specified application",
|
||||
))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<X>")
|
||||
.bold()
|
||||
|
|
|
@ -393,10 +393,7 @@ impl Component for FileList {
|
|||
self.states.toggle_file(self.states.list_index());
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Report event
|
||||
Msg::OnSubmit(self.get_state())
|
||||
}
|
||||
KeyCode::Enter => Msg::OnSubmit(self.get_state()),
|
||||
_ => {
|
||||
// Return key event to activity
|
||||
Msg::OnKey(key)
|
||||
|
@ -449,7 +446,7 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tuirealm::event::KeyEvent;
|
||||
use tuirealm::event::{KeyEvent, KeyModifiers};
|
||||
|
||||
#[test]
|
||||
fn test_ui_components_file_list_states() {
|
||||
|
@ -626,6 +623,15 @@ mod tests {
|
|||
component.on(Event::Key(KeyEvent::from(KeyCode::Char('a')))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Char('a')))
|
||||
);
|
||||
// Ctrl + a
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL
|
||||
))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.states.selected.len(), component.states.list_len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -124,7 +124,7 @@ pub const MSG_KEY_CHAR_L: Msg = Msg::OnKey(KeyEvent {
|
|||
modifiers: KeyModifiers::NONE,
|
||||
});
|
||||
/*
|
||||
pub const MSG_KEY_CHAR_M: Msg = Msg::OnKey(KeyEvent {
|
||||
pub const MSG_KEY_CHAR_M: Msg = Msg::OnKey(KeyEvent { NOTE: used for mark
|
||||
code: KeyCode::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
});
|
||||
|
@ -165,7 +165,6 @@ pub const MSG_KEY_CHAR_U: Msg = Msg::OnKey(KeyEvent {
|
|||
code: KeyCode::Char('u'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
});
|
||||
/*
|
||||
pub const MSG_KEY_CHAR_V: Msg = Msg::OnKey(KeyEvent {
|
||||
code: KeyCode::Char('v'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
|
@ -174,7 +173,6 @@ pub const MSG_KEY_CHAR_W: Msg = Msg::OnKey(KeyEvent {
|
|||
code: KeyCode::Char('w'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
});
|
||||
*/
|
||||
pub const MSG_KEY_CHAR_X: Msg = Msg::OnKey(KeyEvent {
|
||||
code: KeyCode::Char('x'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
|
|
Loading…
Reference in a new issue