chmod popup (#183)

* feat: chmod popup

* fix: windows shall not allows chmod on localhost
This commit is contained in:
Christian Visintin
2023-05-10 17:04:24 +02:00
committed by GitHub
parent b4fa50a666
commit 79dd9e2303
17 changed files with 502 additions and 7 deletions

View File

@@ -35,6 +35,8 @@
Released on ??
- **Change file permissions**: you can now change file permissions easily with the permissions popup pressing `Z` in the explorer.
- [Issue 172](https://github.com/veeso/termscp/issues/172)
- [Issue 153](https://github.com/veeso/termscp/issues/153): show a loading message when loading directory's content
- [Issue 176](https://github.com/veeso/termscp/issues/176): debug log is now written to CACHE_DIR
- [Issue 173](https://github.com/veeso/termscp/issues/173): allow unknown fields in ssh2 configuration file

View File

@@ -214,6 +214,7 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
| `<W>` | Open file with provided program | With |
| `<X>` | Execute a command | eXecute |
| `<Y>` | Toggle synchronized browsing | sYnc |
| `<Z>` | Change file mode | |
| `<CTRL+A>` | Select all files | |
| `<CTRL+C>` | Abort file transfer process | |
| `<CTRL+T>` | Show all synchronized paths | Track |

View File

@@ -214,6 +214,7 @@ Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador
| `<W>` | Abrir archivo con el programa proporcionado | With |
| `<X>` | Ejecutar un comando | eXecute |
| `<Y>` | Alternar navegación sincronizada | sYnc |
| `<Z>` | Cambiar ppermisos de archivo | |
| `<CTRL+A>` | Seleccionar todos los archivos | |
| `<CTRL+C>` | Abortar el proceso de transferencia de archivos | |
| `<CTRL+T>` | Mostrar todas las rutas sincronizadas | Track |

View File

@@ -212,6 +212,7 @@ Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de
| `<W>` | Ouvrir le fichier avec le programme spécifié | With |
| `<X>` | Exécuter une commande | eXecute |
| `<Y>` | Basculer la navigation synchronisée | sYnc |
| `<Z>` | Changer permissions de fichier | |
| `<CTRL+A>` | Sélectionner tous les fichiers | |
| `<CTRL+C>` | Abandonner le processus de transfert de fichiers | |
| `<CTRL+T>` | Afficher tous les chemins synchronisés | Track |

View File

@@ -208,6 +208,7 @@ Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pan
| `<W>` | Apri il file con il programma specificato | With |
| `<X>` | Esegui comando shell | eXecute |
| `<Y>` | Abilita/disabilita Sync-Browsing | sYnc |
| `<Z>` | Modifica permessi file | |
| `<CTRL+A>` | Seleziona tutti i file | |
| `<CTRL+C>` | Annulla trasferimento file | |
| `<CTRL+T>` | Visualizza tutti i percorsi sincronizzati | Track |

View File

@@ -212,6 +212,7 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
| `<W>` | Open file with provided program | With |
| `<X>` | Execute a command | eXecute |
| `<Y>` | Toggle synchronized browsing | sYnc |
| `<Z>` | Change file mode | |
| `<CTRL+A>` | Select all files | |
| `<CTRL+C>` | Abort file transfer process | |
| `<CTRL+T>` | Show all synchronized paths | Track |

View File

@@ -209,6 +209,7 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
| `<W>` | 使用指定程序打开文件 | With |
| `<X>` | 运行命令 | eXecute |
| `<Y>` | 是否开启同步浏览 | sYnc |
| `<Z>` | 更改文件权限 | |
| `<CTRL+A>` | 选中所有文件 | |
| `<CTRL+C>` | 终止文件传输 | |
| `<CTRL+T>` | 显示所有同步路径 | Track |

View File

@@ -4,7 +4,8 @@
// locals
// ext
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tuirealm::tui::style::Color;
use crate::utils::fmt::fmt_color;

View File

@@ -0,0 +1,101 @@
use remotefs::fs::UnixPex;
use super::{FileTransferActivity, LogLevel};
impl FileTransferActivity {
#[cfg(target_family = "unix")]
pub fn action_local_chmod(&mut self, mode: UnixPex) {
let files = self.get_local_selected_entries().get_files();
for file in files {
if let Err(err) = self.host.chmod(file.path(), mode) {
self.log_and_alert(
LogLevel::Error,
format!(
"could not change mode for {}: {}",
file.path().display(),
err
),
);
return;
}
self.log(
LogLevel::Info,
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
);
}
}
pub fn action_remote_chmod(&mut self, mode: UnixPex) {
let files = self.get_remote_selected_entries().get_files();
for file in files {
let mut metadata = file.metadata.clone();
metadata.mode = Some(mode);
if let Err(err) = self.client.setstat(file.path(), metadata) {
self.log_and_alert(
LogLevel::Error,
format!(
"could not change mode for {}: {}",
file.path().display(),
err
),
);
return;
}
self.log(
LogLevel::Info,
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
);
}
}
#[cfg(target_family = "unix")]
pub fn action_find_local_chmod(&mut self, mode: UnixPex) {
let files = self.get_found_selected_entries().get_files();
for file in files {
if let Err(err) = self.host.chmod(file.path(), mode) {
self.log_and_alert(
LogLevel::Error,
format!(
"could not change mode for {}: {}",
file.path().display(),
err
),
);
return;
}
self.log(
LogLevel::Info,
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
);
}
}
pub fn action_find_remote_chmod(&mut self, mode: UnixPex) {
let files = self.get_found_selected_entries().get_files();
for file in files {
let mut metadata = file.metadata.clone();
metadata.mode = Some(mode);
if let Err(err) = self.client.setstat(file.path(), metadata) {
self.log_and_alert(
LogLevel::Error,
format!(
"could not change mode for {}: {}",
file.path().display(),
err
),
);
return;
}
self.log(
LogLevel::Info,
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
);
}
}
}

View File

@@ -2,6 +2,7 @@
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
use remotefs::fs::UnixPex;
pub(self) use remotefs::File;
use tuirealm::{State, StateValue};
@@ -13,6 +14,7 @@ pub(self) use super::{
// actions
pub(crate) mod change_dir;
pub(crate) mod chmod;
pub(crate) mod copy;
pub(crate) mod delete;
pub(crate) mod edit;
@@ -35,6 +37,27 @@ pub(crate) enum SelectedFile {
None,
}
impl SelectedFile {
/// Get file mode for `SelectedFile`
/// In case is `Many` the first item mode is returned
pub fn unix_pex(&self) -> Option<UnixPex> {
match self {
Self::Many(files) => files.iter().next().and_then(|file| file.metadata().mode),
Self::One(file) => file.metadata().mode,
Self::None => None,
}
}
/// Get files as vec
pub fn get_files(self) -> Vec<File> {
match self {
Self::One(file) => vec![file],
Self::Many(files) => files,
Self::None => vec![],
}
}
}
#[derive(Debug)]
enum SelectedFileIndex {
One(usize),

View File

@@ -16,8 +16,8 @@ mod transfer;
pub use misc::FooterBar;
pub use popups::{
CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup, FileInfoPopup,
FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup,
ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup,
FileInfoPopup, FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup,
ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WatchedPathsList, WatcherPopup,

View File

@@ -21,6 +21,10 @@ use super::{Msg, PendingActionMsg, TransferMsg, UiMsg};
use crate::explorer::FileSorting;
use crate::utils::fmt::fmt_time;
mod chmod;
pub use chmod::ChmodPopup;
#[derive(MockComponent)]
pub struct CopyPopup {
component: Input,
@@ -746,6 +750,8 @@ impl KeybindingsPopup {
.add_col(TextSpan::from(
" Open text file with preferred editor",
))
.add_col(TextSpan::new("<P>").bold().fg(key_color))
.add_col(TextSpan::from(" Toggle log panel"))
.add_row()
.add_col(TextSpan::new("<Q|F10>").bold().fg(key_color))
.add_col(TextSpan::from(" Quit termscp"))
@@ -779,6 +785,8 @@ impl KeybindingsPopup {
.add_col(TextSpan::from(
" Toggle synchronized browsing",
))
.add_col(TextSpan::new("<Z>").bold().fg(key_color))
.add_col(TextSpan::from(" Change file permissions"))
.add_row()
.add_col(TextSpan::new("<DEL|F8|E>").bold().fg(key_color))
.add_col(TextSpan::from(" Delete selected file"))

View File

@@ -0,0 +1,267 @@
use remotefs::fs::{UnixPex, UnixPexClass};
use tui_realm_stdlib::Checkbox;
use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, AttrValue, Attribute, BorderSides, Borders, Color};
use tuirealm::tui::layout::{Constraint, Direction as LayoutDirection, Layout};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue};
use super::{Msg, TransferMsg, UiMsg};
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum Item {
#[default]
User,
Group,
Others,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct States {
focus: Item,
}
/// Permissions popup for chmod command
pub struct ChmodPopup {
props: Props,
states: States,
title: String,
color: Color,
user: Checkbox,
group: Checkbox,
others: Checkbox,
}
/// Make checkbox values from unix pex class
fn make_pex_values(mode: UnixPexClass) -> Vec<usize> {
let mut values = Vec::with_capacity(3);
if mode.read() {
values.push(0);
}
if mode.write() {
values.push(1);
}
if mode.execute() {
values.push(2);
}
values
}
impl ChmodPopup {
pub fn new(pex: UnixPex, color: Color, title: String) -> Self {
Self {
props: Props::default(),
color,
title,
states: States {
focus: Item::default(),
},
user: Checkbox::default()
.foreground(color)
.choices(&["Read", "Write", "Execute"])
.title("User", Alignment::Left)
.borders(Borders::default().sides(BorderSides::NONE))
.values(&make_pex_values(pex.user()))
.rewind(true),
group: Checkbox::default()
.foreground(color)
.choices(&["Read", "Write", "Execute"])
.title("Group", Alignment::Left)
.borders(Borders::default().sides(BorderSides::NONE))
.values(&make_pex_values(pex.group()))
.rewind(true),
others: Checkbox::default()
.foreground(color)
.choices(&["Read", "Write", "Execute"])
.title("Others", Alignment::Left)
.borders(Borders::default().sides(BorderSides::NONE))
.values(&make_pex_values(pex.others()))
.rewind(true),
}
}
fn get_active_checkbox(&mut self) -> &'_ mut Checkbox {
match self.states.focus {
Item::Group => &mut self.group,
Item::Others => &mut self.others,
Item::User => &mut self.user,
}
}
fn toggle_checkbox_focus(&mut self, value: bool) {
match self.states.focus {
Item::User => self.user.attr(Attribute::Focus, AttrValue::Flag(value)),
Item::Group => self.group.attr(Attribute::Focus, AttrValue::Flag(value)),
Item::Others => self.others.attr(Attribute::Focus, AttrValue::Flag(value)),
}
}
fn active_checkbox_up(&mut self) {
self.toggle_checkbox_focus(false);
let next = match self.states.focus {
Item::User => Item::Others,
Item::Group => Item::User,
Item::Others => Item::Group,
};
self.states.focus = next;
self.toggle_checkbox_focus(true);
}
fn active_checkbox_down(&mut self) {
self.toggle_checkbox_focus(false);
let next = match self.states.focus {
Item::User => Item::Group,
Item::Group => Item::Others,
Item::Others => Item::User,
};
self.states.focus = next;
self.toggle_checkbox_focus(true);
}
fn checkbox_state_to_pex_class(state: State) -> UnixPexClass {
let values: Vec<usize> = state
.unwrap_vec()
.into_iter()
.map(|x| x.unwrap_usize())
.collect();
UnixPexClass::new(
values.contains(&0),
values.contains(&1),
values.contains(&2),
)
}
fn get_mode(&self) -> UnixPex {
UnixPex::new(
Self::checkbox_state_to_pex_class(self.user.state()),
Self::checkbox_state_to_pex_class(self.group.state()),
Self::checkbox_state_to_pex_class(self.others.state()),
)
}
}
impl MockComponent for ChmodPopup {
fn attr(&mut self, attr: tuirealm::Attribute, value: AttrValue) {
self.props.set(attr, value.clone());
if attr == Attribute::Focus {
self.get_active_checkbox().attr(attr, value);
} else {
self.user.attr(attr, value.clone());
self.group.attr(attr, value.clone());
self.others.attr(attr, value);
}
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd {
Cmd::Move(Direction::Left) | Cmd::Move(Direction::Right) => {
self.get_active_checkbox().perform(cmd)
}
Cmd::Move(Direction::Up) => {
self.active_checkbox_up();
CmdResult::None
}
Cmd::Move(Direction::Down) => {
self.active_checkbox_down();
CmdResult::None
}
Cmd::Toggle => self.get_active_checkbox().perform(cmd),
Cmd::Submit => CmdResult::Submit(self.state()),
_ => CmdResult::None,
}
}
fn query(&self, attr: tuirealm::Attribute) -> Option<AttrValue> {
self.props.get(attr)
}
fn state(&self) -> State {
State::One(StateValue::U32(self.get_mode().into()))
}
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) {
if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) != AttrValue::Flag(true) {
return;
}
let chunks = Layout::default()
.direction(LayoutDirection::Vertical)
.margin(1)
.constraints(
[
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
]
.as_ref(),
)
.split(area);
let focus = self
.props
.get_or(Attribute::Focus, AttrValue::Flag(false))
.unwrap_flag();
let div = tui_realm_stdlib::utils::get_block(
Borders::default().color(self.color),
Some((self.title.clone(), Alignment::Center)),
focus,
None,
);
frame.render_widget(div, area);
self.user.view(frame, chunks[0]);
self.group.view(frame, chunks[1]);
self.others.view(frame, chunks[2]);
}
}
impl Component<Msg, NoUserEvent> for ChmodPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseChmodPopup))
}
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(' '),
..
}) => {
self.perform(Cmd::Toggle);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Transfer(TransferMsg::Chmod(self.get_mode()))),
_ => None,
}
}
}

View File

@@ -127,6 +127,10 @@ impl Component<Msg, NoUserEvent> for ExplorerFind {
code: Key::Char('w'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('z'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowChmodPopup)),
_ => None,
}
}
@@ -308,6 +312,10 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
code: Key::Char('w'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('z'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowChmodPopup)),
_ => None,
}
}
@@ -489,6 +497,10 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
code: Key::Char('w'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('z'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowChmodPopup)),
_ => None,
}
}

View File

@@ -38,6 +38,7 @@ use crate::system::watcher::FsWatcher;
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum Id {
ChmodPopup,
CopyPopup,
DeletePopup,
DisconnectPopup,
@@ -93,6 +94,7 @@ enum PendingActionMsg {
#[derive(Debug, PartialEq)]
enum TransferMsg {
AbortTransfer,
Chmod(remotefs::fs::UnixPex),
CopyFileTo(String),
CreateSymlink(String),
DeleteFile,
@@ -119,6 +121,7 @@ enum TransferMsg {
enum UiMsg {
ChangeFileSorting(FileSorting),
ChangeTransferWindow,
CloseChmodPopup,
CloseCopyPopup,
CloseDeletePopup,
CloseDisconnectPopup,
@@ -144,6 +147,7 @@ enum UiMsg {
LogBackTabbed,
Quit,
ReplacePopupTabbed,
ShowChmodPopup,
ShowCopyPopup,
ShowDeletePopup,
ShowDisconnectPopup,

View File

@@ -32,6 +32,22 @@ impl FileTransferActivity {
TransferMsg::AbortTransfer => {
self.transfer.abort();
}
TransferMsg::Chmod(mode) => {
self.umount_chmod();
self.mount_blocking_wait("Applying new file mode…");
match self.browser.tab() {
#[cfg(target_family = "unix")]
FileExplorerTab::Local => self.action_local_chmod(mode),
#[cfg(target_family = "unix")]
FileExplorerTab::FindLocal => self.action_find_local_chmod(mode),
FileExplorerTab::Remote => self.action_remote_chmod(mode),
FileExplorerTab::FindRemote => self.action_find_remote_chmod(mode),
#[cfg(target_family = "windows")]
FileExplorerTab::Local | FileExplorerTab::FindLocal => {}
}
self.umount_wait();
self.update_browser_file_list();
}
TransferMsg::CopyFileTo(dest) => {
self.umount_copy();
self.mount_blocking_wait("Copying file(s)…");
@@ -336,6 +352,7 @@ impl FileTransferActivity {
fn update_ui(&mut self, msg: UiMsg) -> Option<Msg> {
match msg {
UiMsg::CloseChmodPopup => self.umount_chmod(),
UiMsg::ChangeFileSorting(sorting) => {
match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => {
@@ -422,6 +439,32 @@ impl FileTransferActivity {
assert!(self.app.active(&Id::ReplacePopup).is_ok());
}
}
UiMsg::ShowChmodPopup => {
let selected_file = match self.browser.tab() {
#[cfg(target_family = "unix")]
FileExplorerTab::Local => self.get_local_selected_entries(),
#[cfg(target_family = "unix")]
FileExplorerTab::FindLocal => self.get_found_selected_entries(),
FileExplorerTab::Remote => self.get_remote_selected_entries(),
FileExplorerTab::FindRemote => self.get_found_selected_entries(),
#[cfg(target_family = "windows")]
FileExplorerTab::Local | FileExplorerTab::FindLocal => SelectedFile::None,
};
if let Some(mode) = selected_file.unix_pex() {
self.mount_chmod(
mode,
match selected_file {
SelectedFile::Many(files) => {
format!("changing mode for {} files…", files.len())
}
SelectedFile::One(file) => {
format!("changing mode for {}", file.name())
}
SelectedFile::None => "".to_string(),
},
);
}
}
UiMsg::ShowCopyPopup => self.mount_copy(),
UiMsg::ShowDeletePopup => self.mount_radio_delete(),
UiMsg::ShowDisconnectPopup => self.mount_disconnect(),

View File

@@ -4,7 +4,7 @@
// locals
// Ext
use remotefs::fs::File;
use remotefs::fs::{File, UnixPex};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::tui::widgets::Clear;
@@ -158,6 +158,11 @@ impl FileTransferActivity {
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::CopyPopup, f, popup);
} else if self.app.mounted(&Id::ChmodPopup) {
let popup = Popup(Size::Percentage(50), Size::Unit(12)).draw_in(f.size());
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::ChmodPopup, f, popup);
} else if self.app.mounted(&Id::FindPopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size());
f.render_widget(Clear, popup);
@@ -435,6 +440,24 @@ impl FileTransferActivity {
let _ = self.app.umount(&Id::DisconnectPopup);
}
pub(super) fn mount_chmod(&mut self, mode: UnixPex, title: String) {
// Mount
let color = self.theme().misc_input_dialog;
assert!(self
.app
.remount(
Id::ChmodPopup,
Box::new(components::ChmodPopup::new(mode, color, title)),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::ChmodPopup).is_ok());
}
pub(super) fn umount_chmod(&mut self) {
let _ = self.app.umount(&Id::ChmodPopup);
}
pub(super) fn mount_copy(&mut self) {
let input_color = self.theme().misc_input_dialog;
assert!(self
@@ -1068,9 +1091,14 @@ impl FileTransferActivity {
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WatchedPathsList,
)))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WaitPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::ChmodPopup,
)))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WaitPopup,
)))),
)),
)),
)),
)),