mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
@@ -16,6 +16,10 @@
|
||||
|
||||
FIXME: Released on
|
||||
|
||||
- **Explorer Formatter**:
|
||||
- Added possibility to customize the format when listing files in the explorers (Read more on README)
|
||||
- Added `file_fmt` key to configuration (if missing, default will be used).
|
||||
- Added the text input to the Settings view to set the value for `file_fmt`.
|
||||
- Bugfix:
|
||||
- Solved file index in explorer files at start of termscp, in case the first entry is an hidden file
|
||||
- SCP File transfer: when listing directory entries, check if a symlink points to a directory or to a file
|
||||
|
||||
28
README.md
28
README.md
@@ -30,6 +30,7 @@ FIXME: Current version: 0.3.2 (18/01/2021)
|
||||
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
|
||||
- [Configuration ⚙️](#configuration-️)
|
||||
- [SSH Key Storage 🔐](#ssh-key-storage-)
|
||||
- [File Explorer Format](#file-explorer-format)
|
||||
- [Keybindings ⌨](#keybindings-)
|
||||
- [Documentation 📚](#documentation-)
|
||||
- [Known issues 🧻](#known-issues-)
|
||||
@@ -60,12 +61,16 @@ It happens quite often to me, when using SCP at work to forget the path of a fil
|
||||
- SFTP
|
||||
- SCP
|
||||
- FTP and FTPS
|
||||
- Compatible with Windows, Linux, BSD and MacOS
|
||||
- Practical user interface to explore and operate on the remote and on the local machine file system
|
||||
- Bookmarks and recent connections can be saved to access quickly to your favourite hosts
|
||||
- Supports text editors to view and edit text files
|
||||
- Supports both SFTP/SCP authentication through SSH keys and username/password
|
||||
- User customization directly from the user interface
|
||||
- Compatible with Windows, Linux, BSD and MacOS
|
||||
- Customizations:
|
||||
- Custom file explorer format
|
||||
- Customizable text editor
|
||||
- Customizable file sorting
|
||||
- SSH key storage
|
||||
- Written in Rust
|
||||
- Easy to extend with new file transfers protocols
|
||||
- Developed keeping an eye on performance
|
||||
@@ -288,6 +293,25 @@ You can access the SSH key storage, from configuration moving to the `SSH Keys`
|
||||
> Q: Wait, my private key is protected with password, can I use it?
|
||||
> A: Of course you can. The password provided for authentication in termscp, is valid both for username/password authentication and for RSA key authentication.
|
||||
|
||||
### File Explorer Format
|
||||
|
||||
It is possible through configuration to define a custom format for the file explorer. This field, with name `File formatter syntax` will define how the files will be displayed in the file explorer.
|
||||
The syntax for the formatter is the following `{KEY1}... {KEY2}... {KEYn}...`.
|
||||
Each key in bracket will be replaced with the related attribute, while everything outside brackets will be left unchanged.
|
||||
These are the keys supported by the formatter:
|
||||
|
||||
- `ATIME`: Last access time (with syntax `%b %d %Y %H:%M`)
|
||||
- `CTIME`: Creation time (with syntax `%b %d %Y %H:%M`)
|
||||
- `GROUP`: Owner group
|
||||
- `MTIME`: Last change time (with syntax `%b %d %Y %H:%M`)
|
||||
- `NAME`: File name (Elided if longer than 24)
|
||||
- `PEX`: File permissions (UNIX format)
|
||||
- `SIZE`: File size (omitted for directories)
|
||||
- `SYMLINK`: Symlink (if any `-> {FILE_PATH}`)
|
||||
- `USER`: Owner user
|
||||
|
||||
If left empty, the default formatter syntax will be used: `{NAME} {PEX} {USER} {SIZE} {MTIME}`
|
||||
|
||||
---
|
||||
|
||||
## Keybindings ⌨
|
||||
|
||||
@@ -56,6 +56,7 @@ pub struct UserInterfaceConfig {
|
||||
pub default_protocol: String,
|
||||
pub show_hidden_files: bool,
|
||||
pub group_dirs: Option<String>,
|
||||
pub file_fmt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
@@ -85,6 +86,7 @@ impl Default for UserInterfaceConfig {
|
||||
default_protocol: FileTransferProtocol::Sftp.to_string(),
|
||||
show_hidden_files: false,
|
||||
group_dirs: None,
|
||||
file_fmt: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,6 +173,7 @@ mod tests {
|
||||
text_editor: PathBuf::from("nano"),
|
||||
show_hidden_files: true,
|
||||
group_dirs: Some(String::from("first")),
|
||||
file_fmt: Some(String::from("{NAME}")),
|
||||
};
|
||||
let cfg: UserConfig = UserConfig {
|
||||
user_interface: ui,
|
||||
@@ -187,6 +190,7 @@ mod tests {
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
|
||||
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -108,6 +108,7 @@ mod tests {
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
|
||||
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME} {PEX}")));
|
||||
// Verify keys
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
@@ -143,6 +144,7 @@ mod tests {
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.group_dirs, None);
|
||||
assert_eq!(cfg.user_interface.file_fmt, None);
|
||||
// Verify keys
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
@@ -199,6 +201,7 @@ mod tests {
|
||||
text_editor = "vim"
|
||||
show_hidden_files = true
|
||||
group_dirs = "last"
|
||||
file_fmt = "{NAME} {PEX}"
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::formatter::Formatter;
|
||||
use super::{ExplorerOpts, FileExplorer, FileSorting, GroupDirs};
|
||||
// Ext
|
||||
use std::collections::VecDeque;
|
||||
@@ -95,6 +96,18 @@ impl FileExplorerBuilder {
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_formatter
|
||||
///
|
||||
/// Set formatter for FileExplorer
|
||||
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
|
||||
if let Some(e) = self.explorer.as_mut() {
|
||||
if let Some(fmt_str) = fmt_str {
|
||||
e.fmt = Formatter::new(fmt_str);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -119,6 +132,7 @@ mod tests {
|
||||
.with_group_dirs(Some(GroupDirs::First))
|
||||
.with_hidden_files(true)
|
||||
.with_stack_size(24)
|
||||
.with_formatter(Some("{NAME}"))
|
||||
.build();
|
||||
// Verify
|
||||
assert!(explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES));
|
||||
|
||||
718
src/fs/explorer/formatter.rs
Normal file
718
src/fs/explorer/formatter.rs
Normal file
@@ -0,0 +1,718 @@
|
||||
//! ## Formatter
|
||||
//!
|
||||
//! `formatter` is the module which provides formatting utilities for `FileExplorer`
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate bytesize;
|
||||
extern crate regex;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
extern crate users;
|
||||
// Locals
|
||||
use super::FsEntry;
|
||||
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use regex::Regex;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
use users::{get_group_by_gid, get_user_by_uid};
|
||||
// Types
|
||||
type FmtCallback = fn(&Formatter, &FsEntry, &str, &str) -> String;
|
||||
|
||||
// Keys
|
||||
const FMT_KEY_ATIME: &str = "{ATIME}";
|
||||
const FMT_KEY_CTIME: &str = "{CTIME}";
|
||||
const FMT_KEY_GROUP: &str = "{GROUP}";
|
||||
const FMT_KEY_MTIME: &str = "{MTIME}";
|
||||
const FMT_KEY_NAME: &str = "{NAME}";
|
||||
const FMT_KEY_PEX: &str = "{PEX}";
|
||||
const FMT_KEY_SIZE: &str = "{SIZE}";
|
||||
const FMT_KEY_SYMLINK: &str = "{SYMLINK}";
|
||||
const FMT_KEY_USER: &str = "{USER}";
|
||||
// Default
|
||||
const FMT_DEFAULT_STX: &str = "{NAME} {PEX} {USER} {SIZE} {MTIME}";
|
||||
// Regex
|
||||
lazy_static! {
|
||||
static ref FMT_KEY_REGEX: Regex = Regex::new(r"\{(.*?)\}").ok().unwrap();
|
||||
}
|
||||
|
||||
/// ## CallChainBlock
|
||||
///
|
||||
/// Call Chain block is a block in a chain of functions which are called in order to format the FsEntry.
|
||||
/// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found
|
||||
/// a chain of function is made using the Formatters method.
|
||||
/// This method provides an extremely fast way to format fs entries
|
||||
struct CallChainBlock {
|
||||
func: FmtCallback,
|
||||
prefix: String,
|
||||
next_block: Option<Box<CallChainBlock>>,
|
||||
}
|
||||
|
||||
impl CallChainBlock {
|
||||
/// ### new
|
||||
///
|
||||
/// Create a new `CallChainBlock`
|
||||
pub fn new(func: FmtCallback, prefix: String) -> Self {
|
||||
CallChainBlock {
|
||||
func,
|
||||
prefix,
|
||||
next_block: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### next
|
||||
///
|
||||
/// Call next callback in the CallChain
|
||||
pub fn next(&self, fmt: &Formatter, fsentry: &FsEntry, cur_str: &str) -> String {
|
||||
// Call func
|
||||
let new_str: String = (self.func)(fmt, fsentry, cur_str, self.prefix.as_str());
|
||||
// If next is some, call next, otherwise (END OF CHAIN) return new_str
|
||||
match &self.next_block {
|
||||
Some(block) => block.next(fmt, fsentry, new_str.as_str()),
|
||||
None => new_str,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### push
|
||||
///
|
||||
/// Push func to the last element in the Call chain
|
||||
pub fn push(&mut self, func: FmtCallback, prefix: String) {
|
||||
// Call recursively until an element with next_block equal to None is found
|
||||
match &mut self.next_block {
|
||||
None => self.next_block = Some(Box::new(CallChainBlock::new(func, prefix))),
|
||||
Some(block) => block.push(func, prefix),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Formatter
|
||||
///
|
||||
/// Formatter takes care of formatting FsEntries according to the provided keys.
|
||||
/// Formatting is performed using the `CallChainBlock`, which composed makes a Call Chain. This method is extremely fast compared to match the format groups
|
||||
/// at each fmt call.
|
||||
pub struct Formatter {
|
||||
call_chain: CallChainBlock,
|
||||
}
|
||||
|
||||
impl Default for Formatter {
|
||||
/// ### default
|
||||
///
|
||||
/// Instantiates a Formatter with the default fmt syntax
|
||||
fn default() -> Self {
|
||||
Formatter {
|
||||
call_chain: Self::make_callchain(FMT_DEFAULT_STX),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Formatter {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new `Formatter` with the provided format string
|
||||
pub fn new(fmt_str: &str) -> Self {
|
||||
Formatter {
|
||||
call_chain: Self::make_callchain(fmt_str),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### fmt
|
||||
///
|
||||
/// Format fsentry
|
||||
pub fn fmt(&self, fsentry: &FsEntry) -> String {
|
||||
// Execute callchain blocks
|
||||
self.call_chain.next(self, fsentry, "")
|
||||
}
|
||||
|
||||
// Fmt methods
|
||||
|
||||
/// ### fmt_atime
|
||||
///
|
||||
/// Format last access time
|
||||
fn fmt_atime(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
|
||||
// Get date
|
||||
let datetime: String = fmt_time(fsentry.get_last_access_time(), "%b %d %Y %H:%M");
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:17}", cur_str, prefix, datetime)
|
||||
}
|
||||
|
||||
/// ### fmt_ctime
|
||||
///
|
||||
/// Format creation time
|
||||
fn fmt_ctime(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
|
||||
// Get date
|
||||
let datetime: String = fmt_time(fsentry.get_creation_time(), "%b %d %Y %H:%M");
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:17}", cur_str, prefix, datetime)
|
||||
}
|
||||
|
||||
/// ### fmt_group
|
||||
///
|
||||
/// Format owner group
|
||||
fn fmt_group(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
|
||||
// Get username
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
let group: String = match fsentry.get_group() {
|
||||
Some(gid) => match get_group_by_gid(gid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
None => gid.to_string(),
|
||||
},
|
||||
None => 0.to_string(),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let group: String = match fsentry.get_group() {
|
||||
Some(gid) => gid.to_string(),
|
||||
None => 0.to_string(),
|
||||
};
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:12}", cur_str, prefix, group)
|
||||
}
|
||||
|
||||
/// ### fmt_mtime
|
||||
///
|
||||
/// Format last change time
|
||||
fn fmt_mtime(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
|
||||
// Get date
|
||||
let datetime: String = fmt_time(fsentry.get_last_change_time(), "%b %d %Y %H:%M");
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:17}", cur_str, prefix, datetime)
|
||||
}
|
||||
|
||||
/// ### fmt_name
|
||||
///
|
||||
/// Format file name
|
||||
fn fmt_name(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
|
||||
// Get file name (or elide if too long)
|
||||
let name: &str = fsentry.get_name();
|
||||
let last_idx: usize = match fsentry.is_dir() {
|
||||
// NOTE: For directories is 19, since we push '/' to name
|
||||
true => 19,
|
||||
false => 20,
|
||||
};
|
||||
let mut name: String = match name.len() >= 24 {
|
||||
false => name.to_string(),
|
||||
true => format!("{}...", &name[0..last_idx]),
|
||||
};
|
||||
if fsentry.is_dir() {
|
||||
name.push('/');
|
||||
}
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:24}", cur_str, prefix, name)
|
||||
}
|
||||
|
||||
/// ### fmt_pex
|
||||
///
|
||||
/// Format file permissions
|
||||
fn fmt_pex(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
|
||||
// Create mode string
|
||||
let mut pex: String = String::with_capacity(10);
|
||||
let file_type: char = match fsentry.is_symlink() {
|
||||
true => 'l',
|
||||
false => match fsentry.is_dir() {
|
||||
true => 'd',
|
||||
false => '-',
|
||||
},
|
||||
};
|
||||
pex.push(file_type);
|
||||
match fsentry.get_unix_pex() {
|
||||
None => pex.push_str("?????????"),
|
||||
Some((owner, group, others)) => pex.push_str(fmt_pex(owner, group, others).as_str()),
|
||||
}
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:10}", cur_str, prefix, pex)
|
||||
}
|
||||
|
||||
/// ### fmt_size
|
||||
///
|
||||
/// Format file size
|
||||
fn fmt_size(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
|
||||
if fsentry.is_file() {
|
||||
// Get byte size
|
||||
let size: ByteSize = ByteSize(fsentry.get_size() as u64);
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:10}", cur_str, prefix, size.to_string())
|
||||
} else {
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{} ", cur_str, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
/// ### fmt_symlink
|
||||
///
|
||||
/// Format file symlink (if any)
|
||||
fn fmt_symlink(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
|
||||
// Get file name (or elide if too long)
|
||||
// Replace `FMT_KEY_NAME` with name
|
||||
match fsentry.is_symlink() {
|
||||
false => format!("{}{} ", cur_str, prefix),
|
||||
true => format!(
|
||||
"{}{}-> {:21}",
|
||||
cur_str,
|
||||
prefix,
|
||||
fmt_path_elide(fsentry.get_realfile().get_abs_path().as_path(), 20)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### fmt_user
|
||||
///
|
||||
/// Format owner user
|
||||
fn fmt_user(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
|
||||
// Get username
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
let username: String = match fsentry.get_user() {
|
||||
Some(uid) => match get_user_by_uid(uid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
None => uid.to_string(),
|
||||
},
|
||||
None => 0.to_string(),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let username: String = match fsentry.get_user() {
|
||||
Some(uid) => uid.to_string(),
|
||||
None => 0.to_string(),
|
||||
};
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:12}", cur_str, prefix, username)
|
||||
}
|
||||
|
||||
/// ### fmt_fallback
|
||||
///
|
||||
/// Fallback function in case the format key is unknown
|
||||
/// It does nothing, just returns cur_str
|
||||
fn fmt_fallback(&self, _fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
|
||||
// Add to cur str and prefix
|
||||
format!("{}{}", cur_str, prefix)
|
||||
}
|
||||
|
||||
// Static
|
||||
|
||||
/// ### make_callchain
|
||||
///
|
||||
/// Make a callchain starting from the fmt str
|
||||
fn make_callchain(fmt_str: &str) -> CallChainBlock {
|
||||
// Init chain block
|
||||
let mut callchain: Option<CallChainBlock> = None;
|
||||
// Track index of the last match found, to get the prefix for each token
|
||||
let mut last_index: usize = 0;
|
||||
// Match fmt str against regex
|
||||
for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
|
||||
// Get match index (unwrap is safe, since always exists)
|
||||
let index: usize = fmt_str.find(®ex_match[0]).unwrap();
|
||||
// Get prefix
|
||||
let prefix: String = String::from(&fmt_str[last_index..index]);
|
||||
// Increment last index (sum prefix lenght and the length of the key)
|
||||
last_index += prefix.len() + regex_match[0].len();
|
||||
// Match the match (I guess...)
|
||||
let callback: FmtCallback = match ®ex_match[0] {
|
||||
FMT_KEY_ATIME => Self::fmt_atime,
|
||||
FMT_KEY_CTIME => Self::fmt_ctime,
|
||||
FMT_KEY_GROUP => Self::fmt_group,
|
||||
FMT_KEY_MTIME => Self::fmt_mtime,
|
||||
FMT_KEY_NAME => Self::fmt_name,
|
||||
FMT_KEY_PEX => Self::fmt_pex,
|
||||
FMT_KEY_SIZE => Self::fmt_size,
|
||||
FMT_KEY_SYMLINK => Self::fmt_symlink,
|
||||
FMT_KEY_USER => Self::fmt_user,
|
||||
_ => Self::fmt_fallback,
|
||||
};
|
||||
// Create a callchain or push new element to its back
|
||||
match callchain.as_mut() {
|
||||
None => callchain = Some(CallChainBlock::new(callback, prefix)),
|
||||
Some(chain_block) => chain_block.push(callback, prefix),
|
||||
}
|
||||
}
|
||||
// Finalize and return
|
||||
match callchain {
|
||||
Some(callchain) => callchain,
|
||||
None => CallChainBlock::new(Self::fmt_fallback, String::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::fs::{FsDirectory, FsFile};
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_callchain() {
|
||||
// Make a dummy formatter
|
||||
let dummy_formatter: Formatter = Formatter::new("");
|
||||
// Make a dummy entry
|
||||
let t_now: SystemTime = SystemTime::now();
|
||||
let dummy_entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
let prefix: String = String::from("h");
|
||||
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix);
|
||||
assert!(callchain.next_block.is_none());
|
||||
assert_eq!(callchain.prefix, String::from("h"));
|
||||
// Execute
|
||||
assert_eq!(
|
||||
callchain.next(&dummy_formatter, &dummy_entry, ""),
|
||||
String::from("hA")
|
||||
);
|
||||
// Push 4 new blocks
|
||||
callchain.push(dummy_fmt, String::from("h"));
|
||||
callchain.push(dummy_fmt, String::from("h"));
|
||||
callchain.push(dummy_fmt, String::from("h"));
|
||||
callchain.push(dummy_fmt, String::from("h"));
|
||||
// Verify
|
||||
assert_eq!(
|
||||
callchain.next(&dummy_formatter, &dummy_entry, ""),
|
||||
String::from("hAhAhAhAhA")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_format_files() {
|
||||
// Make default
|
||||
let formatter: Formatter = Formatter::default();
|
||||
// Experiments :D
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// Elide name
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("piroparoporoperoperupupu.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperu... -rw-r--r-- root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperu... -rw-r--r-- 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No pex
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No user
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_format_dirs() {
|
||||
// Make default
|
||||
let formatter: Formatter = Formatter::default();
|
||||
// Experiments :D
|
||||
let t_now: SystemTime = SystemTime::now();
|
||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
||||
name: String::from("projects"),
|
||||
abs_path: PathBuf::from("/home/cvisintin/projects"),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ drwxr-xr-x root {}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ drwxr-xr-x 0 {}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No pex, no user
|
||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
||||
name: String::from("projects"),
|
||||
abs_path: PathBuf::from("/home/cvisintin/projects"),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ d????????? 0 {}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ d????????? 0 {}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_all_together_now() {
|
||||
let formatter: Formatter =
|
||||
Formatter::new("{NAME} {SYMLINK} {GROUP} {USER} {PEX} {SIZE} {ATIME} {CTIME} {MTIME}");
|
||||
// Directory (with symlink)
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let pointer: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("project.info"),
|
||||
abs_path: PathBuf::from("/project.info"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
||||
name: String::from("projects"),
|
||||
abs_path: PathBuf::from("/home/cvisintin/project"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
readonly: false,
|
||||
symlink: Some(Box::new(pointer)), // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"projects/ -> /project.info 0 0 lrwxr-xr-x {} {} {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
));
|
||||
// Directory without symlink
|
||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
||||
name: String::from("projects"),
|
||||
abs_path: PathBuf::from("/home/cvisintin/project"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"projects/ 0 0 drwxr-xr-x {} {} {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
));
|
||||
// File with symlink
|
||||
let pointer: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("project.info"),
|
||||
abs_path: PathBuf::from("/project.info"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: Some(Box::new(pointer)), // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"bar.txt -> /project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
));
|
||||
// File without symlink
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
fmt_time(t, "%b %d %Y %H:%M"),
|
||||
));
|
||||
}
|
||||
|
||||
/// ### dummy_fmt
|
||||
///
|
||||
/// Dummy formatter, just yelds an 'A' at the end of the current string
|
||||
fn dummy_fmt(_fmt: &Formatter, _entry: &FsEntry, cur_str: &str, prefix: &str) -> String {
|
||||
format!("{}{}A", cur_str, prefix)
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,12 @@
|
||||
|
||||
// Mods
|
||||
pub(crate) mod builder;
|
||||
mod formatter;
|
||||
// Deps
|
||||
extern crate bitflags;
|
||||
// Locals
|
||||
use super::FsEntry;
|
||||
use formatter::Formatter;
|
||||
// Ext
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::VecDeque;
|
||||
@@ -75,6 +77,7 @@ pub struct FileExplorer {
|
||||
pub(crate) file_sorting: FileSorting, // File sorting criteria
|
||||
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
|
||||
pub(crate) opts: ExplorerOpts, // Explorer options
|
||||
pub(crate) fmt: Formatter, // FsEntry formatter
|
||||
index: usize, // Selected file
|
||||
files: Vec<FsEntry>, // Files in directory
|
||||
}
|
||||
@@ -88,6 +91,7 @@ impl Default for FileExplorer {
|
||||
file_sorting: FileSorting::ByName,
|
||||
group_dirs: None,
|
||||
opts: ExplorerOpts::empty(),
|
||||
fmt: Formatter::default(),
|
||||
index: 0,
|
||||
files: Vec::new(),
|
||||
}
|
||||
@@ -166,6 +170,15 @@ impl FileExplorer {
|
||||
self.files.get(self.index)
|
||||
}
|
||||
|
||||
// Formatting
|
||||
|
||||
/// ### fmt_file
|
||||
///
|
||||
/// Format a file entry
|
||||
pub fn fmt_file(&self, entry: &FsEntry) -> String {
|
||||
self.fmt.fmt(entry)
|
||||
}
|
||||
|
||||
// Sorting
|
||||
|
||||
/// ### sort_by
|
||||
@@ -239,7 +252,8 @@ impl FileExplorer {
|
||||
///
|
||||
/// Sort files by creation time; the newest comes first
|
||||
fn sort_files_by_creation_time(&mut self) {
|
||||
self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time()));
|
||||
self.files
|
||||
.sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time()));
|
||||
}
|
||||
|
||||
/// ### sort_files_by_size
|
||||
@@ -507,6 +521,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::fs::{FsDirectory, FsFile};
|
||||
use crate::utils::fmt::fmt_time;
|
||||
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, SystemTime};
|
||||
@@ -854,6 +869,43 @@ mod tests {
|
||||
assert_eq!(explorer.files.get(7).unwrap().get_name(), "README.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_fmt() {
|
||||
let explorer: FileExplorer = FileExplorer::default();
|
||||
// Create fs entry
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
explorer.fmt_file(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
explorer.fmt_file(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_to_string_from_str_traits() {
|
||||
// File Sorting
|
||||
|
||||
270
src/fs/mod.rs
270
src/fs/mod.rs
@@ -25,19 +25,9 @@
|
||||
|
||||
// Mod
|
||||
pub mod explorer;
|
||||
|
||||
// Deps
|
||||
extern crate bytesize;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
extern crate users;
|
||||
// Locals
|
||||
use crate::utils::fmt::{fmt_pex, fmt_time};
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
use users::get_user_by_uid;
|
||||
|
||||
/// ## FsEntry
|
||||
///
|
||||
@@ -236,76 +226,6 @@ impl FsEntry {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FsEntry {
|
||||
/// ### fmt_ls
|
||||
///
|
||||
/// Format File Entry as `ls` does
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
// Create mode string
|
||||
let mut mode: String = String::with_capacity(10);
|
||||
let file_type: char = match self.is_symlink() {
|
||||
true => 'l',
|
||||
false => match self.is_dir() {
|
||||
true => 'd',
|
||||
false => '-',
|
||||
},
|
||||
};
|
||||
mode.push(file_type);
|
||||
match self.get_unix_pex() {
|
||||
None => mode.push_str("?????????"),
|
||||
Some((owner, group, others)) => mode.push_str(fmt_pex(owner, group, others).as_str()),
|
||||
}
|
||||
// Get username
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
let username: String = match self.get_user() {
|
||||
Some(uid) => match get_user_by_uid(uid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
None => uid.to_string(),
|
||||
},
|
||||
None => 0.to_string(),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let username: String = match self.get_user() {
|
||||
Some(uid) => uid.to_string(),
|
||||
None => 0.to_string(),
|
||||
};
|
||||
// Get group
|
||||
/*
|
||||
let group: String = match self.get_group() {
|
||||
Some(gid) => match get_group_by_gid(gid) {
|
||||
Some(group) => group.name().to_string_lossy().to_string(),
|
||||
None => gid.to_string(),
|
||||
},
|
||||
None => String::from("0"),
|
||||
};
|
||||
*/
|
||||
// Get byte size
|
||||
let size: ByteSize = ByteSize(self.get_size() as u64);
|
||||
// Get date
|
||||
let datetime: String = fmt_time(self.get_last_change_time(), "%b %d %Y %H:%M");
|
||||
// Set file name (or elide if too long)
|
||||
let name: &str = self.get_name();
|
||||
let last_idx: usize = match self.is_dir() {
|
||||
// NOTE: For directories is 19, since we push '/' to name
|
||||
true => 19,
|
||||
false => 20,
|
||||
};
|
||||
let mut name: String = match name.len() >= 24 {
|
||||
false => name.to_string(),
|
||||
true => format!("{}...", &name[0..last_idx]),
|
||||
};
|
||||
// If is directory, append '/'
|
||||
if self.is_dir() {
|
||||
name.push('/');
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
"{:24}\t{:12}\t{:12}\t{:10}\t{:17}",
|
||||
name, mode, username, size, datetime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -512,194 +432,4 @@ mod tests {
|
||||
PathBuf::from("/home/cvisintin/projects")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_fmt_file() {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-rw-r--r-- \troot \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-rw-r--r-- \t0 \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// Elide name
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("piroparoporoperoperupupu.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"piroparoporoperoperu... \t-rw-r--r-- \troot \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"piroparoporoperoperu... \t-rw-r--r-- \t0 \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No pex
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-????????? \troot \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-????????? \t0 \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No user
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-????????? \t0 \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-????????? \t0 \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_fmt_dir() {
|
||||
let t_now: SystemTime = SystemTime::now();
|
||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
||||
name: String::from("projects"),
|
||||
abs_path: PathBuf::from("/home/cvisintin/projects"),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects/ \tdrwxr-xr-x \troot \t4.1 KB \t{}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects/ \tdrwxr-xr-x \t0 \t4.1 KB \t{}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No pex, no user
|
||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
||||
name: String::from("projects"),
|
||||
abs_path: PathBuf::from("/home/cvisintin/projects"),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects/ \td????????? \t0 \t4.1 KB \t{}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects/ \td????????? \t0 \t4.1 KB \t{}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,23 @@ impl ConfigClient {
|
||||
};
|
||||
}
|
||||
|
||||
/// ### get_file_fmt
|
||||
///
|
||||
/// Get current file fmt
|
||||
pub fn get_file_fmt(&self) -> Option<String> {
|
||||
self.config.user_interface.file_fmt.clone()
|
||||
}
|
||||
|
||||
/// ### set_file_fmt
|
||||
///
|
||||
/// Set file fmt parameter
|
||||
pub fn set_file_fmt(&mut self, s: String) {
|
||||
self.config.user_interface.file_fmt = match s.is_empty() {
|
||||
true => None,
|
||||
false => Some(s),
|
||||
};
|
||||
}
|
||||
|
||||
// SSH Keys
|
||||
|
||||
/// ### save_ssh_key
|
||||
@@ -451,6 +468,21 @@ mod tests {
|
||||
assert_eq!(client.get_group_dirs(), None,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_file_fmt() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(client.get_file_fmt(), None);
|
||||
client.set_file_fmt(String::from("{NAME}"));
|
||||
assert_eq!(client.get_file_fmt().unwrap(), String::from("{NAME}"));
|
||||
// Delete
|
||||
client.set_file_fmt(String::from(""));
|
||||
assert_eq!(client.get_file_fmt(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_ssh_keys() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
|
||||
@@ -169,7 +169,7 @@ impl FileTransferActivity {
|
||||
let files: Vec<ListItem> = self
|
||||
.local
|
||||
.iter_files()
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(self.local.fmt_file(entry))))
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.tab {
|
||||
@@ -209,7 +209,7 @@ impl FileTransferActivity {
|
||||
let files: Vec<ListItem> = self
|
||||
.remote
|
||||
.iter_files()
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(self.remote.fmt_file(entry))))
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.tab {
|
||||
|
||||
@@ -133,6 +133,7 @@ impl FileTransferActivity {
|
||||
.with_group_dirs(cli.get_group_dirs())
|
||||
.with_hidden_files(cli.get_show_hidden_files())
|
||||
.with_stack_size(16)
|
||||
.with_formatter(cli.get_file_fmt().as_deref())
|
||||
.build(),
|
||||
None => FileExplorerBuilder::new() // Build default
|
||||
.with_file_sorting(FileSorting::ByName)
|
||||
|
||||
@@ -181,16 +181,28 @@ impl SetupActivity {
|
||||
KeyCode::Backspace => {
|
||||
// Pop character from selected input
|
||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
||||
// NOTE: replace with match if other text fields are added
|
||||
if matches!(field, UserInterfaceInputField::TextEditor) {
|
||||
// Pop from text editor
|
||||
let mut input: String = String::from(
|
||||
config_cli.get_text_editor().as_path().to_string_lossy(),
|
||||
);
|
||||
input.pop();
|
||||
// Update text editor value
|
||||
config_cli.set_text_editor(PathBuf::from(input.as_str()));
|
||||
match field {
|
||||
UserInterfaceInputField::TextEditor => {
|
||||
// Pop from text editor
|
||||
let mut input: String = String::from(
|
||||
config_cli.get_text_editor().as_path().to_string_lossy(),
|
||||
);
|
||||
input.pop();
|
||||
// Update text editor value
|
||||
config_cli.set_text_editor(PathBuf::from(input.as_str()));
|
||||
}
|
||||
UserInterfaceInputField::FileFmt => {
|
||||
// Push char to current file fmt
|
||||
let mut file_fmt = config_cli.get_file_fmt().unwrap_or_default();
|
||||
// Pop from file fmt
|
||||
file_fmt.pop();
|
||||
// If len is 0, will become None
|
||||
config_cli.set_file_fmt(file_fmt);
|
||||
}
|
||||
_ => { /* Not a text field */ }
|
||||
}
|
||||
// NOTE: replace with match if other text fields are added
|
||||
if matches!(field, UserInterfaceInputField::TextEditor) {}
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
@@ -270,6 +282,7 @@ impl SetupActivity {
|
||||
KeyCode::Up => {
|
||||
// Change selected field
|
||||
self.tab = SetupTab::UserInterface(match field {
|
||||
UserInterfaceInputField::FileFmt => UserInterfaceInputField::GroupDirs,
|
||||
UserInterfaceInputField::GroupDirs => {
|
||||
UserInterfaceInputField::ShowHiddenFiles
|
||||
}
|
||||
@@ -279,7 +292,7 @@ impl SetupActivity {
|
||||
UserInterfaceInputField::DefaultProtocol => {
|
||||
UserInterfaceInputField::TextEditor
|
||||
}
|
||||
UserInterfaceInputField::TextEditor => UserInterfaceInputField::GroupDirs, // Wrap
|
||||
UserInterfaceInputField::TextEditor => UserInterfaceInputField::FileFmt, // Wrap
|
||||
});
|
||||
}
|
||||
KeyCode::Down => {
|
||||
@@ -294,7 +307,8 @@ impl SetupActivity {
|
||||
UserInterfaceInputField::ShowHiddenFiles => {
|
||||
UserInterfaceInputField::GroupDirs
|
||||
}
|
||||
UserInterfaceInputField::GroupDirs => UserInterfaceInputField::TextEditor, // Wrap
|
||||
UserInterfaceInputField::GroupDirs => UserInterfaceInputField::FileFmt,
|
||||
UserInterfaceInputField::FileFmt => UserInterfaceInputField::TextEditor, // Wrap
|
||||
});
|
||||
}
|
||||
KeyCode::Char(ch) => {
|
||||
@@ -328,14 +342,24 @@ impl SetupActivity {
|
||||
// Push character to input field
|
||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
||||
// NOTE: change to match if other fields are added
|
||||
if matches!(field, UserInterfaceInputField::TextEditor) {
|
||||
// Get current text editor and push character
|
||||
let mut input: String = String::from(
|
||||
config_cli.get_text_editor().as_path().to_string_lossy(),
|
||||
);
|
||||
input.push(ch);
|
||||
// Update text editor value
|
||||
config_cli.set_text_editor(PathBuf::from(input.as_str()));
|
||||
match field {
|
||||
UserInterfaceInputField::TextEditor => {
|
||||
// Get current text editor and push character
|
||||
let mut input: String = String::from(
|
||||
config_cli.get_text_editor().as_path().to_string_lossy(),
|
||||
);
|
||||
input.push(ch);
|
||||
// Update text editor value
|
||||
config_cli.set_text_editor(PathBuf::from(input.as_str()));
|
||||
}
|
||||
UserInterfaceInputField::FileFmt => {
|
||||
// Push char to current file fmt
|
||||
let mut file_fmt = config_cli.get_file_fmt().unwrap_or_default();
|
||||
file_fmt.push(ch);
|
||||
// update value
|
||||
config_cli.set_file_fmt(file_fmt);
|
||||
}
|
||||
_ => { /* Not a text field */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ impl SetupActivity {
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
]
|
||||
.as_ref(),
|
||||
@@ -107,15 +108,28 @@ impl SetupActivity {
|
||||
if let Some(tab) = self.draw_default_group_dirs_tab() {
|
||||
f.render_widget(tab, ui_cfg_chunks[3]);
|
||||
}
|
||||
if let Some(tab) = self.draw_file_fmt_input() {
|
||||
f.render_widget(tab, ui_cfg_chunks[4]);
|
||||
}
|
||||
// Set cursor
|
||||
if let Some(cli) = &self.config_cli {
|
||||
if matches!(form_field, UserInterfaceInputField::TextEditor) {
|
||||
let editor_text: String =
|
||||
String::from(cli.get_text_editor().as_path().to_string_lossy());
|
||||
f.set_cursor(
|
||||
ui_cfg_chunks[0].x + editor_text.width() as u16 + 1,
|
||||
ui_cfg_chunks[0].y + 1,
|
||||
)
|
||||
match form_field {
|
||||
UserInterfaceInputField::TextEditor => {
|
||||
let editor_text: String =
|
||||
String::from(cli.get_text_editor().as_path().to_string_lossy());
|
||||
f.set_cursor(
|
||||
ui_cfg_chunks[0].x + editor_text.width() as u16 + 1,
|
||||
ui_cfg_chunks[0].y + 1,
|
||||
);
|
||||
}
|
||||
UserInterfaceInputField::FileFmt => {
|
||||
let file_fmt: String = cli.get_file_fmt().unwrap_or_default();
|
||||
f.set_cursor(
|
||||
ui_cfg_chunks[4].x + file_fmt.width() as u16 + 1,
|
||||
ui_cfg_chunks[4].y + 1,
|
||||
);
|
||||
}
|
||||
_ => { /* Not a text field */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -392,6 +406,31 @@ impl SetupActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### draw_file_fmt_input
|
||||
///
|
||||
/// Draw input text field for file fmt
|
||||
fn draw_file_fmt_input(&self) -> Option<Paragraph> {
|
||||
match &self.config_cli {
|
||||
Some(cli) => Some(
|
||||
Paragraph::new(cli.get_file_fmt().unwrap_or_default())
|
||||
.style(Style::default().fg(match &self.tab {
|
||||
SetupTab::SshConfig => Color::White,
|
||||
SetupTab::UserInterface(field) => match field {
|
||||
UserInterfaceInputField::FileFmt => Color::LightCyan,
|
||||
_ => Color::White,
|
||||
},
|
||||
}))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("File formatter syntax"),
|
||||
),
|
||||
),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### draw_ssh_keys_list
|
||||
///
|
||||
/// Draw ssh keys list
|
||||
|
||||
@@ -55,6 +55,7 @@ enum UserInterfaceInputField {
|
||||
TextEditor,
|
||||
ShowHiddenFiles,
|
||||
GroupDirs,
|
||||
FileFmt,
|
||||
}
|
||||
|
||||
/// ### SetupTab
|
||||
|
||||
@@ -27,6 +27,7 @@ extern crate chrono;
|
||||
extern crate textwrap;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
/// ### fmt_pex
|
||||
@@ -116,6 +117,39 @@ pub fn align_text_center(text: &str, width: u16) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// ### elide_path
|
||||
///
|
||||
/// Elide a path if longer than width
|
||||
/// In this case, the path is formatted to {ANCESTOR[0]}/.../{PARENT[0]}/{BASENAME}
|
||||
pub fn fmt_path_elide(p: &Path, width: usize) -> String {
|
||||
let fmt_path: String = format!("{}", p.display());
|
||||
match fmt_path.len() > width as usize {
|
||||
false => fmt_path,
|
||||
true => {
|
||||
// Elide
|
||||
let ancestors_len: usize = p.ancestors().count();
|
||||
let mut ancestors = p.ancestors();
|
||||
let mut elided_path: PathBuf = PathBuf::new();
|
||||
// If ancestors_len's size is bigger than 2, push count - 2
|
||||
if ancestors_len > 2 {
|
||||
elided_path.push(ancestors.nth(ancestors_len - 2).unwrap());
|
||||
}
|
||||
// If ancestors_len is bigger than 3, push '...' and parent too
|
||||
if ancestors_len > 3 {
|
||||
elided_path.push("...");
|
||||
if let Some(parent) = p.ancestors().nth(1) {
|
||||
elided_path.push(parent.file_name().unwrap());
|
||||
}
|
||||
}
|
||||
// Push file_name
|
||||
if let Some(name) = p.file_name() {
|
||||
elided_path.push(name);
|
||||
}
|
||||
format!("{}", elided_path.display())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -169,4 +203,16 @@ mod tests {
|
||||
String::from("18.192")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(target_os = "unix", target_os = "linux", target_os = "macos"))]
|
||||
fn test_utils_fmt_path_elide() {
|
||||
let p: &Path = &Path::new("/develop/pippo");
|
||||
// Under max size
|
||||
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/pippo"));
|
||||
// Above max size, only one ancestor
|
||||
assert_eq!(fmt_path_elide(p, 8), String::from("/develop/pippo"));
|
||||
let p: &Path = &Path::new("/develop/pippo/foo/bar");
|
||||
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/.../foo/bar"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user