Merge pull request #41 from veeso/open-rs

Open-rs
This commit is contained in:
Christian Visintin 2021-06-19 15:35:48 +02:00 committed by GitHub
commit 3df8ed13a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 569 additions and 91 deletions

View file

@ -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
View file

@ -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",

View file

@ -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"

View file

@ -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)

View file

@ -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 ⭐

View file

@ -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);
}
}
}
}

View file

@ -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>),

View 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();
}
}
}

View 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,
}
}
}

View file

@ -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()
}
}

View file

@ -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);

View file

@ -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

View file

@ -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();

View file

@ -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()

View file

@ -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]

View file

@ -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,