From 298001dc15a48c1370127495401c1783a0d43ab7 Mon Sep 17 00:00:00 2001
From: Awiteb
Date: Wed, 25 Dec 2024 12:14:55 +0000
Subject: [PATCH] feat: Support entry renaming
Signed-off-by: Awiteb
---
.../key_event_matcher/collapse_dir.rs | 2 +-
.../key_event_matcher/expand_dir.rs | 2 +-
src/controller/key_event_matcher/mod.rs | 46 ++++++++++++++-----
src/controller/key_event_matcher/modes.rs | 26 +++++++++++
src/controller/key_event_matcher/reload.rs | 2 +-
src/controller/key_event_matcher/rename.rs | 46 +++++++++++++++++++
src/controller/mod.rs | 28 +++++++++--
src/model/config/keybinding.rs | 21 ++++-----
src/model/event.rs | 16 +++++++
src/model/path_node/debug.rs | 2 +-
src/model/path_node/mod.rs | 12 +++--
src/utils.rs | 8 ++--
src/view/composer.rs | 25 +++++-----
src/view/mod.rs | 8 ++++
src/view/mode.rs | 21 +++++++++
src/view/update.rs | 36 ++++++++++-----
16 files changed, 235 insertions(+), 66 deletions(-)
create mode 100644 src/controller/key_event_matcher/modes.rs
create mode 100644 src/controller/key_event_matcher/rename.rs
create mode 100644 src/view/mode.rs
diff --git a/src/controller/key_event_matcher/collapse_dir.rs b/src/controller/key_event_matcher/collapse_dir.rs
index 6ca1996..70b11a3 100644
--- a/src/controller/key_event_matcher/collapse_dir.rs
+++ b/src/controller/key_event_matcher/collapse_dir.rs
@@ -17,7 +17,7 @@ impl EventQueue {
self.path_node_root.collapse_dir(&tree_index);
}
- self.text_entries = self.composer.compose_path_node(&self.path_node_root);
+ self.entries = self.composer.compose_path_node(&self.path_node_root);
self.update_pager(cursor_delta);
Some(())
diff --git a/src/controller/key_event_matcher/expand_dir.rs b/src/controller/key_event_matcher/expand_dir.rs
index ed8d1ac..0293632 100644
--- a/src/controller/key_event_matcher/expand_dir.rs
+++ b/src/controller/key_event_matcher/expand_dir.rs
@@ -11,7 +11,7 @@ impl EventQueue {
.flat_index_to_tree_index(self.pager.cursor_row as usize);
self.path_node_root
.expand_dir(&tree_index, self.path_node_compare);
- self.text_entries = self.composer.compose_path_node(&self.path_node_root);
+ self.entries = self.composer.compose_path_node(&self.path_node_root);
self.update_pager(0);
Some(())
diff --git a/src/controller/key_event_matcher/mod.rs b/src/controller/key_event_matcher/mod.rs
index 4892ea8..70bcae2 100644
--- a/src/controller/key_event_matcher/mod.rs
+++ b/src/controller/key_event_matcher/mod.rs
@@ -4,6 +4,9 @@
use crate::controller::EventQueue;
use crate::model::event::Key;
+use crate::view::Mode;
+
+use std::borrow::Cow;
use std::io::Write;
mod collapse_dir;
@@ -11,28 +14,49 @@ mod entry_down;
mod entry_up;
mod expand_dir;
mod file_action;
+mod modes;
mod quit;
mod reload;
+mod rename;
impl EventQueue {
- #[rustfmt::skip]
pub fn match_key_event(&mut self, key: Key) -> Option<()> {
- let ck = self.config.keybinding.clone();
+ let ck = &self.config.keybinding;
- if key == Key::from(ck.collapse_dir) { self.do_collapse_dir() }
- else if key == Key::from(ck.entry_down) { self.do_entry_down() }
- else if key == Key::from(ck.entry_up) { self.do_entry_up() }
- else if key == Key::from(ck.expand_dir) { self.do_expand_dir() }
- else if key == Key::from(ck.file_action) { self.do_file_action() }
- else if key == Key::from(ck.quit) { self.do_quit() }
- else if key == Key::from(ck.reload) { self.do_reload() }
- else { Some(()) }
+ match (self.pager.mode.clone(), key) {
+ (Mode::Normal, k) if k == Key::from(&ck.collapse_dir) => self.do_collapse_dir(),
+ (Mode::Normal, k) if k == Key::from(&ck.entry_down) => self.do_entry_down(),
+ (Mode::Normal, k) if k == Key::from(&ck.entry_up) => self.do_entry_up(),
+ (Mode::Normal, k) if k == Key::from(&ck.expand_dir) => self.do_expand_dir(),
+ (Mode::Normal, k) if k == Key::from(&ck.file_action) => self.do_file_action(),
+ (Mode::Normal, k) if k == Key::from(&ck.quit) => self.do_quit(),
+ (Mode::Normal, k) if k == Key::from(&ck.reload) => self.do_reload(),
+ (Mode::Normal, k) if k == Key::from(&ck.rename_mode) => self.do_enter_rename_mode(),
+ (Mode::Rename(..), k) if k == Key::from(&ck.quit) => self.do_enter_normal_mode(),
+ (Mode::Rename(new_name), k) if k == Key::from("return") => {
+ if !new_name.chars().all(char::is_whitespace) {
+ self.do_rename_current_file(&new_name);
+ self.do_reload();
+ }
+ self.do_enter_normal_mode();
+ Some(())
+ }
+ (Mode::Rename(new_name), k) => {
+ if let Cow::Owned(updated_name) = self.do_handle_rename_input(&new_name, k.inner())
+ {
+ self.pager.mode = Mode::Rename(updated_name);
+ self.update_pager(0);
+ }
+ Some(())
+ }
+ _ => Some(()),
+ }
}
fn update_pager(&mut self, cursor_delta: i32) {
self.pager.update(
cursor_delta,
- &self.text_entries,
+ &self.entries,
self.config
.setup
.with_cwd_header
diff --git a/src/controller/key_event_matcher/modes.rs b/src/controller/key_event_matcher/modes.rs
new file mode 100644
index 0000000..975e0ef
--- /dev/null
+++ b/src/controller/key_event_matcher/modes.rs
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2024 Awiteb
+
+use std::io::Write;
+
+use crate::{controller::EventQueue, view::Mode};
+
+impl EventQueue {
+ pub fn do_enter_normal_mode(&mut self) -> Option<()> {
+ self.pager.mode = Mode::Normal;
+ self.update_pager(0);
+ Some(())
+ }
+ pub fn do_enter_rename_mode(&mut self) -> Option<()> {
+ self.pager.mode = Mode::Rename(
+ self.pager
+ .current_entry
+ .file_name()
+ .unwrap()
+ .to_string_lossy()
+ .to_string(),
+ );
+ self.update_pager(0);
+ Some(())
+ }
+}
diff --git a/src/controller/key_event_matcher/reload.rs b/src/controller/key_event_matcher/reload.rs
index ed2ccc6..55017ab 100644
--- a/src/controller/key_event_matcher/reload.rs
+++ b/src/controller/key_event_matcher/reload.rs
@@ -11,7 +11,7 @@ impl EventQueue {
pub fn do_reload(&mut self) -> Option<()> {
self.reload_openend_dirs();
- self.text_entries = self.composer.compose_path_node(&self.path_node_root);
+ self.entries = self.composer.compose_path_node(&self.path_node_root);
self.update_pager(0);
diff --git a/src/controller/key_event_matcher/rename.rs b/src/controller/key_event_matcher/rename.rs
new file mode 100644
index 0000000..425465d
--- /dev/null
+++ b/src/controller/key_event_matcher/rename.rs
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2024 Awiteb
+
+use std::{borrow::Cow, fs, io::Write};
+
+use termion::event::Key as TKey;
+
+use crate::controller::EventQueue;
+
+impl EventQueue {
+ /// Remove the last character from the string.
+ fn pop_char(name: &str) -> Cow<'_, str> {
+ if name.is_empty() {
+ return Cow::Borrowed(name);
+ }
+ let mut chars = name.chars();
+ chars.next_back();
+ Cow::Owned(chars.collect())
+ }
+
+ /// Add a character to the string, if it is not a control character.
+ fn push_char<'a>(name: &'a str, chr: &char) -> Cow<'a, str> {
+ if chr.is_control() {
+ return Cow::Borrowed(name);
+ }
+ Cow::Owned(format!("{name}{chr}"))
+ }
+
+ /// Rename the current file to the new name.
+ pub fn do_rename_current_file(&mut self, new_name: &str) -> Option<()> {
+ let new_path = self.pager.current_entry.with_file_name(new_name);
+ if !new_path.exists() {
+ let _ = fs::rename(&self.pager.current_entry, new_path);
+ };
+ Some(())
+ }
+
+ /// Handle the rename input, and return the new name.
+ pub fn do_handle_rename_input<'a>(&mut self, new_name: &'a str, key: &TKey) -> Cow<'a, str> {
+ match key {
+ TKey::Backspace => return Self::pop_char(new_name),
+ TKey::Char(chr) => Self::push_char(new_name, chr),
+ _ => Cow::Borrowed(new_name),
+ }
+ }
+}
diff --git a/src/controller/mod.rs b/src/controller/mod.rs
index 29cb165..1f0ccdd 100644
--- a/src/controller/mod.rs
+++ b/src/controller/mod.rs
@@ -12,6 +12,8 @@ use crate::view::composer::Composer;
use crate::view::Pager;
use log::info;
use std::io::Write;
+use std::path::Path;
+use std::path::PathBuf;
use std::sync::mpsc::sync_channel;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::SyncSender;
@@ -21,6 +23,12 @@ mod key_event_handler;
mod key_event_matcher;
mod resize_event_handler;
+/// Entrie, a struct that represents a file or directory.
+pub struct Entrie {
+ pub path: PathBuf,
+ pub display_text: String,
+}
+
pub struct EventQueue {
config: Config,
composer: Composer,
@@ -31,7 +39,7 @@ pub struct EventQueue {
queue_sender: SyncSender,
// TODO: should be part of the view?
- text_entries: Vec,
+ entries: Vec,
command_to_run_on_exit: Option,
}
@@ -47,10 +55,10 @@ impl EventQueue {
let (queue_sender, queue_receiver): (SyncSender, Receiver) =
sync_channel(1024);
let path_node_compare = PathNode::get_path_node_compare(&config);
- let text_entries = composer.compose_path_node(&path_node_root);
+ let entries = composer.compose_path_node(&path_node_root);
pager.update(
0,
- &text_entries,
+ &entries,
config
.setup
.with_cwd_header
@@ -66,7 +74,7 @@ impl EventQueue {
path_node_compare,
queue_receiver,
queue_sender,
- text_entries,
+ entries,
command_to_run_on_exit,
}
}
@@ -94,7 +102,7 @@ impl EventQueue {
Event::Resize => {
self.pager.update(
0,
- &self.text_entries,
+ &self.entries,
self.config
.setup
.with_cwd_header
@@ -105,3 +113,13 @@ impl EventQueue {
}
}
}
+
+impl Entrie {
+ /// Create a new `Entrie`.
+ pub fn new(path: impl AsRef, display_text: impl Into) -> Self {
+ Self {
+ path: path.as_ref().to_path_buf(),
+ display_text: display_text.into(),
+ }
+ }
+}
diff --git a/src/model/config/keybinding.rs b/src/model/config/keybinding.rs
index bfbbff3..bc0a90d 100644
--- a/src/model/config/keybinding.rs
+++ b/src/model/config/keybinding.rs
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
+// Copyright (c) 2024 Awiteb
use serde::Deserialize;
@@ -7,24 +8,20 @@ use serde::Deserialize;
pub struct Keybinding {
#[serde(default = "Keybinding::default_quit")]
pub quit: String,
-
#[serde(default = "Keybinding::default_entry_up")]
pub entry_up: String,
-
#[serde(default = "Keybinding::default_entry_down")]
pub entry_down: String,
-
#[serde(default = "Keybinding::default_expand_dir")]
pub expand_dir: String,
-
#[serde(default = "Keybinding::default_collapse_dir")]
pub collapse_dir: String,
-
#[serde(default = "Keybinding::default_file_action")]
pub file_action: String,
-
#[serde(default = "Keybinding::default_reload")]
pub reload: String,
+ #[serde(default = "Keybinding::default_rename_mode")]
+ pub rename_mode: String,
}
impl Default for Keybinding {
@@ -37,36 +34,34 @@ impl Default for Keybinding {
collapse_dir: Self::default_collapse_dir(),
file_action: Self::default_file_action(),
reload: Self::default_reload(),
+ rename_mode: Self::default_rename_mode(),
}
}
}
impl Keybinding {
fn default_quit() -> String {
- String::from("q")
+ String::from("ctrl+q")
}
-
fn default_entry_up() -> String {
String::from("up")
}
-
fn default_entry_down() -> String {
String::from("down")
}
-
fn default_expand_dir() -> String {
String::from("right")
}
-
fn default_collapse_dir() -> String {
String::from("left")
}
-
fn default_file_action() -> String {
String::from("return")
}
-
fn default_reload() -> String {
+ String::from("ctrl+r")
+ }
+ fn default_rename_mode() -> String {
String::from("r")
}
}
diff --git a/src/model/event.rs b/src/model/event.rs
index c26c281..26a7dd1 100644
--- a/src/model/event.rs
+++ b/src/model/event.rs
@@ -33,6 +33,22 @@ impl From for Key {
}
}
+impl From<&String> for Key {
+ fn from(s: &String) -> Key {
+ Key::from(convert_str_to_termion_event(s))
+ }
+}
+
+impl Key {
+ /// Returns the inner termion key event.
+ pub fn inner(&self) -> &TKey {
+ match &self.inner {
+ termion::event::Event::Key(tkey) => tkey,
+ _ => unreachable!(),
+ }
+ }
+}
+
fn convert_str_to_termion_event(s: &str) -> TEvent {
if s.chars().count() == 1 {
return TEvent::Key(TKey::Char(s.chars().last().unwrap()));
diff --git a/src/model/path_node/debug.rs b/src/model/path_node/debug.rs
index c81ed9d..c8ae2c8 100644
--- a/src/model/path_node/debug.rs
+++ b/src/model/path_node/debug.rs
@@ -15,7 +15,7 @@ impl Debug for PathNode {
let entries = composer.compose_path_node(self);
for (index, entry) in entries.iter().enumerate() {
- writeln!(f, "{:4}|{}", index, entry)?;
+ writeln!(f, "{:4}|{}", index, entry.display_text)?;
}
Ok(())
diff --git a/src/model/path_node/mod.rs b/src/model/path_node/mod.rs
index fd07898..dd0c631 100644
--- a/src/model/path_node/mod.rs
+++ b/src/model/path_node/mod.rs
@@ -20,7 +20,7 @@ pub struct PathNode {
pub is_err: bool,
pub is_expanded: bool,
pub path: PathBuf,
- pub gitignore: Vec,
+ pub gitignore: Vec,
}
impl From for PathNode
@@ -28,9 +28,11 @@ where
T: AsRef,
{
fn from(working_dir: T) -> Self {
- let mut gitignore =
- utils::parse_gitignore(Path::new(working_dir.as_ref()).join(".gitignore"));
- gitignore.push(working_dir.as_ref().join(".git").display().to_string());
+ let mut gitignore = utils::parse_gitignore(
+ working_dir.as_ref(),
+ Path::new(working_dir.as_ref()).join(".gitignore"),
+ );
+ gitignore.push(working_dir.as_ref().join(".git"));
Self {
children: Vec::new(),
display_text: working_dir.as_ref().display().to_string(),
@@ -72,7 +74,7 @@ impl PathNode {
.filter_map(|dir_entry| {
let dir_entry = dir_entry.unwrap();
let entry_name = dir_entry.file_name().into_string().unwrap();
- (!self.gitignore.contains(&entry_name)).then(|| PathNode {
+ (!self.gitignore.contains(&dir_entry.path())).then(|| PathNode {
children: Vec::new(),
display_text: entry_name,
is_dir: dir_entry.path().is_dir(),
diff --git a/src/utils.rs b/src/utils.rs
index 6d8463a..5bca27f 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -5,7 +5,7 @@
use log::info;
use std::fs;
use std::panic::set_hook;
-use std::path::Path;
+use std::path::{Path, PathBuf};
use std::process::exit;
pub fn print_help() {
@@ -63,13 +63,13 @@ pub fn get_config_dir() -> std::io::Result {
}
}
-pub fn parse_gitignore(gitignore_path: impl AsRef) -> Vec {
- if Path::exists(gitignore_path.as_ref()) {
+pub fn parse_gitignore(root: impl AsRef, gitignore_path: impl AsRef) -> Vec {
+ if gitignore_path.as_ref().exists() {
fs::read_to_string(&gitignore_path)
.map(|content| {
content
.split('\n')
- .map(|path| path.trim_matches('/').to_owned())
+ .map(|path| root.as_ref().join(path.trim_matches('/')))
.collect()
})
.unwrap_or_default()
diff --git a/src/view/composer.rs b/src/view/composer.rs
index 134edcd..38e4e3e 100644
--- a/src/view/composer.rs
+++ b/src/view/composer.rs
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
+// Copyright (c) 2024 Awiteb
+use crate::controller::Entrie;
use crate::model::config::Config;
use crate::model::path_node::PathNode;
use log::info;
@@ -34,7 +36,7 @@ impl Composer {
format!("{}~", truncated)
}
- pub fn compose_path_node(&self, path_node: &PathNode) -> Vec {
+ pub fn compose_path_node(&self, path_node: &PathNode) -> Vec {
let mut result = Vec::new();
self.compose_path_node_recursive(path_node, &mut result, 0);
@@ -45,7 +47,7 @@ impl Composer {
fn compose_path_node_recursive(
&self,
path_node: &PathNode,
- texts: &mut Vec,
+ entries: &mut Vec,
depth: usize,
) {
for child in &path_node.children {
@@ -53,15 +55,12 @@ impl Composer {
let dir_suffix = self.get_dir_suffix(child);
let indent = self.get_indent(depth);
- let text = format!(
- "{}{}{}{}",
- indent,
- dir_prefix,
- child.display_text.clone(),
- dir_suffix,
+ let display_text = format!(
+ "{indent} {dir_prefix} {name}{dir_suffix}",
+ name = child.display_text
);
- texts.push(text);
- self.compose_path_node_recursive(child, texts, depth + 1);
+ entries.push(Entrie::new(&child.path, display_text));
+ self.compose_path_node_recursive(child, entries, depth + 1);
}
}
@@ -96,16 +95,16 @@ impl Composer {
}
fn get_indent(&self, depth: usize) -> String {
- let indent_char = if !self.config.composition.show_indent {
+ let indent = " ".repeat(self.config.composition.indent as usize - 1);
+ let char_indent = if !self.config.composition.show_indent {
' '
} else if self.config.composition.use_utf8 {
'ยท'
} else {
'-'
};
- let indent = " ".repeat(self.config.composition.indent as usize - 1);
- format!("{}{}", indent_char, indent).repeat(depth)
+ format!("{char_indent}{indent}").repeat(depth)
}
}
diff --git a/src/view/mod.rs b/src/view/mod.rs
index f5cd69b..83c3afa 100644
--- a/src/view/mod.rs
+++ b/src/view/mod.rs
@@ -1,19 +1,25 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
+// Copyright (c) 2024 Awiteb
use crate::model::config::Config;
use crate::view::composer::Composer;
use log::info;
+pub use mode::Mode;
use std::io::Write;
+use std::path::{Path, PathBuf};
pub mod composer;
+mod mode;
mod print;
mod scroll;
mod update;
pub struct Pager {
config: Config,
+ pub mode: Mode,
pub cursor_row: i32,
+ pub current_entry: PathBuf,
out: W,
terminal_cols: i32,
terminal_rows: i32,
@@ -34,12 +40,14 @@ impl Pager {
.unwrap();
Self {
+ current_entry: Path::new(&config.setup.working_dir).to_path_buf(),
config,
cursor_row: 0,
out,
terminal_cols: 0,
terminal_rows: 0,
text_row: 0,
+ mode: Mode::Normal,
}
}
}
diff --git a/src/view/mode.rs b/src/view/mode.rs
new file mode 100644
index 0000000..037eed1
--- /dev/null
+++ b/src/view/mode.rs
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2024 Awiteb
+
+/// Mode of the pager.
+#[derive(Clone)]
+pub enum Mode {
+ /// Normal mode. Exploring the file system.
+ Normal,
+ /// Rename mode. Renaming a file.
+ Rename(String),
+}
+
+impl Mode {
+ /// Returns the new name of the file, if in rename mode.
+ pub fn new_name(&self) -> Option<&str> {
+ match &self {
+ Mode::Rename(new_name) => Some(new_name),
+ _ => None,
+ }
+ }
+}
diff --git a/src/view/update.rs b/src/view/update.rs
index 1933e53..1bc3c6c 100644
--- a/src/view/update.rs
+++ b/src/view/update.rs
@@ -2,8 +2,8 @@
// Copyright (c) 2019-2022 golmman
// Copyright (c) 2024 Awiteb
-use crate::view::Pager;
-use std::io::Write;
+use crate::{controller::Entrie, view::Pager};
+use std::{io::Write, path};
use termion::terminal_size;
impl Pager {
@@ -26,7 +26,7 @@ impl Pager {
pub fn update(
&mut self,
cursor_row_delta: i32,
- text_entries: &[String],
+ entries: &[Entrie],
header_text: Option,
) {
self.update_terminal_size();
@@ -34,12 +34,12 @@ impl Pager {
let spacing_bot = self.config.debug.spacing_bot;
let spacing_top = self.config.debug.spacing_top;
- let text_entries_len = text_entries.len() as i32;
+ let entries_len = entries.len() as i32;
- self.update_cursor_row(cursor_row_delta, text_entries_len);
+ self.update_cursor_row(cursor_row_delta, entries_len);
self.text_row = match self.config.behavior.scrolling.as_str() {
- "center" => self.scroll_like_center(cursor_row_delta, text_entries_len),
+ "center" => self.scroll_like_center(cursor_row_delta, entries_len),
"editor" => self.scroll_like_editor(),
_ => 0,
};
@@ -55,18 +55,32 @@ impl Pager {
for i in 0..displayable_rows {
let index = first_index + i;
- if index >= 0 && index < text_entries.len() as i32 {
- let text_entry = &text_entries[index as usize];
+ if index >= 0 && index < entries.len() as i32 {
+ let entry = &entries[index as usize];
if index == self.cursor_row {
- self.print_text_entry_emphasized(text_entry, 1 + spacing_top + i)
+ self.current_entry =
+ path::absolute(&entry.path).unwrap_or_else(|_| entry.path.clone());
+
+ let rename_name = self.mode.new_name().map(|new_name| {
+ let filename = entry.path.file_name().unwrap().to_str().unwrap();
+ let mut new_display_text = entry.display_text.chars();
+ for _ in 0..(filename.chars().count() + entry.path.is_dir() as usize) {
+ new_display_text.next_back();
+ }
+ format!("{}{new_name}", new_display_text.collect::())
+ });
+ self.print_text_entry_emphasized(
+ rename_name.as_ref().unwrap_or(&entry.display_text),
+ 1 + spacing_top + i,
+ )
} else {
- self.print_text_entry(text_entry, 1 + spacing_top + i);
+ self.print_text_entry(&entry.display_text, 1 + spacing_top + i);
}
}
}
- let footer_text = format!("[{}/{}]", self.cursor_row + 1, text_entries_len);
+ let footer_text = format!("[{}/{}]", self.cursor_row + 1, entries_len);
if let Some(header_text) = header_text.as_ref() {
self.print_header(header_text);