From 626b4f8cc5baf5b16494cf57001c69e597d120ec Mon Sep 17 00:00:00 2001
From: Awiteb
Date: Fri, 27 Dec 2024 21:47:15 +0000
Subject: [PATCH] feat: Support creating new entries
Signed-off-by: Awiteb
---
README.md | 5 ++-
src/controller/key_event_matcher/mod.rs | 13 +++++-
src/controller/key_event_matcher/modes.rs | 10 ++++-
src/controller/key_event_matcher/new_entry.rs | 44 +++++++++++++++++++
src/controller/key_event_matcher/remove.rs | 6 +--
src/controller/key_event_matcher/rename.rs | 4 +-
src/model/config/keybinding.rs | 6 +++
src/model/config/mod.rs | 3 ++
src/view/mod.rs | 23 +++++++++-
src/view/mode.rs | 2 +
src/view/update.rs | 10 ++++-
twilight-tree.toml | 1 +
12 files changed, 113 insertions(+), 14 deletions(-)
create mode 100644 src/controller/key_event_matcher/new_entry.rs
diff --git a/README.md b/README.md
index e9177e2..09140b9 100644
--- a/README.md
+++ b/README.md
@@ -22,8 +22,8 @@ cargo install --path . # this will install the binary in $HOME/.cargo/bin
`../new_name.rs` will move the file to the parent directory and rename it to
`new_name.rs`.
- You can delete a file/directory with `ctrl+d` keybinding
-
-
+- You can create a new file with `n+f` keybinding
+- You can create a new directory with `n+d` keybinding
## Implemented features
@@ -58,6 +58,7 @@ The key bindings are configurable. For the set of configurable keys and key comb
| `left arrow` | `--keybinding.collapse_dir=left` | collapse an entry directory or jump to parent if not collapsable |
| `right arrow` | `--keybinding.expand_dir=left` | expand an entry directory |
| `r` | `--keybinding.rename_mode=r` | Enter rename mode (To exit rename mode press `quit` keybinding) |
+| `n` | `--keybinding.new_entry=n` | Enter create mode (To exit rename mode press `quit` keybinding) |
| `ctrl+r` | `--keybinding.reload=r` | collapse all directories and reload root directory |
| `ctrl+d` | `--keybinding.remove=ctrl+d` | remove a file/directory |
| `return` | `--keybinding.file_action=return` | perform configured file action |
diff --git a/src/controller/key_event_matcher/mod.rs b/src/controller/key_event_matcher/mod.rs
index 29d7421..7f23571 100644
--- a/src/controller/key_event_matcher/mod.rs
+++ b/src/controller/key_event_matcher/mod.rs
@@ -15,6 +15,7 @@ mod entry_up;
mod expand_dir;
mod file_action;
mod modes;
+mod new_entry;
mod quit;
mod reload;
mod remove;
@@ -33,7 +34,17 @@ impl EventQueue {
(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::Normal, k) if k == Key::from(&ck.new_entry) => self.do_enter_create_mode(),
(Mode::Normal, k) if k == Key::from(&ck.remove) => self.do_remove_entry(),
+ (Mode::CreateEntry, k) if k == Key::from(&ck.quit) => self.do_enter_normal_mode(),
+ (Mode::CreateEntry, k) if k == Key::from("f") => {
+ self.do_create_file();
+ self.do_enter_normal_mode()
+ }
+ (Mode::CreateEntry, k) if k == Key::from("d") => {
+ self.do_create_dir();
+ self.do_enter_normal_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) {
@@ -47,7 +58,7 @@ impl EventQueue {
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);
+ self.do_reload();
}
Some(())
}
diff --git a/src/controller/key_event_matcher/modes.rs b/src/controller/key_event_matcher/modes.rs
index 975e0ef..0991698 100644
--- a/src/controller/key_event_matcher/modes.rs
+++ b/src/controller/key_event_matcher/modes.rs
@@ -8,19 +8,25 @@ 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);
+ self.do_reload();
Some(())
}
pub fn do_enter_rename_mode(&mut self) -> Option<()> {
self.pager.mode = Mode::Rename(
self.pager
.current_entry
+ .path
.file_name()
.unwrap()
.to_string_lossy()
.to_string(),
);
- self.update_pager(0);
+ self.do_reload();
+ Some(())
+ }
+ pub fn do_enter_create_mode(&mut self) -> Option<()> {
+ self.pager.mode = Mode::CreateEntry;
+ self.do_reload();
Some(())
}
}
diff --git a/src/controller/key_event_matcher/new_entry.rs b/src/controller/key_event_matcher/new_entry.rs
new file mode 100644
index 0000000..17e5379
--- /dev/null
+++ b/src/controller/key_event_matcher/new_entry.rs
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2024 Awiteb
+
+use std::{fs, io::Write};
+
+use crate::controller::EventQueue;
+
+/// Default name for new files, if the name already exists, a number will be appended
+const NEW_FILE_NAME: &str = "new_file";
+/// Default name for new directories, if the name already exists, a number will be appended
+const NEW_DIR_NAME: &str = "new_dir";
+
+impl EventQueue {
+ /// Create a new file in the current directory
+ pub fn do_create_file(&mut self) -> Option<()> {
+ let mut counter = 1;
+ let mut new_file_path = if self.pager.current_entry.is_expanded {
+ self.pager.current_entry.path.join(NEW_FILE_NAME)
+ } else {
+ self.pager.current_entry.path.with_file_name(NEW_FILE_NAME)
+ };
+ while new_file_path.exists() {
+ new_file_path = new_file_path.with_file_name(format!("{NEW_FILE_NAME}{counter}"));
+ counter += 1
+ }
+ let _ = fs::File::create(new_file_path);
+ Some(())
+ }
+ /// Create a new directory in the current directory
+ pub fn do_create_dir(&mut self) -> Option<()> {
+ let mut counter = 1;
+ let mut new_dir_path = if self.pager.current_entry.is_expanded {
+ self.pager.current_entry.path.join(NEW_DIR_NAME)
+ } else {
+ self.pager.current_entry.path.with_file_name(NEW_DIR_NAME)
+ };
+ while new_dir_path.exists() {
+ new_dir_path = new_dir_path.with_file_name(format!("{NEW_DIR_NAME}{counter}"));
+ counter += 1
+ }
+ let _ = fs::create_dir(new_dir_path);
+ Some(())
+ }
+}
diff --git a/src/controller/key_event_matcher/remove.rs b/src/controller/key_event_matcher/remove.rs
index 9f455b6..6c47518 100644
--- a/src/controller/key_event_matcher/remove.rs
+++ b/src/controller/key_event_matcher/remove.rs
@@ -8,10 +8,10 @@ use crate::controller::EventQueue;
impl EventQueue {
/// Remove the current entry
pub fn do_remove_entry(&mut self) -> Option<()> {
- if self.pager.current_entry.is_dir() {
- let _ = fs::remove_dir_all(&self.pager.current_entry);
+ if self.pager.current_entry.path.is_dir() {
+ let _ = fs::remove_dir_all(&self.pager.current_entry.path);
} else {
- let _ = fs::remove_file(&self.pager.current_entry);
+ let _ = fs::remove_file(&self.pager.current_entry.path);
}
self.do_reload();
Some(())
diff --git a/src/controller/key_event_matcher/rename.rs b/src/controller/key_event_matcher/rename.rs
index 425465d..0a055e8 100644
--- a/src/controller/key_event_matcher/rename.rs
+++ b/src/controller/key_event_matcher/rename.rs
@@ -28,9 +28,9 @@ impl EventQueue {
/// 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);
+ let new_path = self.pager.current_entry.path.with_file_name(new_name);
if !new_path.exists() {
- let _ = fs::rename(&self.pager.current_entry, new_path);
+ let _ = fs::rename(&self.pager.current_entry.path, new_path);
};
Some(())
}
diff --git a/src/model/config/keybinding.rs b/src/model/config/keybinding.rs
index 17972bf..1878e7a 100644
--- a/src/model/config/keybinding.rs
+++ b/src/model/config/keybinding.rs
@@ -24,6 +24,8 @@ pub struct Keybinding {
pub rename_mode: String,
#[serde(default = "Keybinding::default_remove")]
pub remove: String,
+ #[serde(default = "Keybinding::default_new_entry")]
+ pub new_entry: String,
}
impl Default for Keybinding {
@@ -38,6 +40,7 @@ impl Default for Keybinding {
reload: Self::default_reload(),
rename_mode: Self::default_rename_mode(),
remove: Self::default_remove(),
+ new_entry: Self::default_new_entry(),
}
}
}
@@ -70,4 +73,7 @@ impl Keybinding {
fn default_remove() -> String {
String::from("ctrl+d")
}
+ fn default_new_entry() -> String {
+ String::from("n")
+ }
}
diff --git a/src/model/config/mod.rs b/src/model/config/mod.rs
index a0b77bf..895e942 100644
--- a/src/model/config/mod.rs
+++ b/src/model/config/mod.rs
@@ -80,6 +80,9 @@ impl Config {
"--keybinding.entry_up" => config.keybinding.entry_up = Self::parse_value((key, value)),
"--keybinding.expand_dir" => config.keybinding.expand_dir = Self::parse_value((key, value)),
"--keybinding.file_action" => config.keybinding.file_action = Self::parse_value((key, value)),
+ "--keybinding.rename_mode" => config.keybinding.rename_mode = Self::parse_value((key, value)),
+ "--keybinding.new_entry" => config.keybinding.new_entry = Self::parse_value((key, value)),
+ "--keybinding.remove" => config.keybinding.remove = Self::parse_value((key, value)),
"--keybinding.quit" => config.keybinding.quit = Self::parse_value((key, value)),
"--keybinding.reload" => config.keybinding.reload = Self::parse_value((key, value)),
"--setup.working_dir" => config.setup.working_dir = Self::parse_value((key, value)),
diff --git a/src/view/mod.rs b/src/view/mod.rs
index 83c3afa..ce9d776 100644
--- a/src/view/mod.rs
+++ b/src/view/mod.rs
@@ -15,17 +15,36 @@ mod print;
mod scroll;
mod update;
+pub struct CurrentEntry {
+ pub path: PathBuf,
+ pub is_expanded: bool,
+}
+
pub struct Pager {
config: Config,
pub mode: Mode,
pub cursor_row: i32,
- pub current_entry: PathBuf,
+ pub current_entry: CurrentEntry,
out: W,
terminal_cols: i32,
terminal_rows: i32,
text_row: i32,
}
+impl CurrentEntry {
+ fn new(path: impl AsRef) -> Self {
+ Self {
+ path: path.as_ref().to_path_buf(),
+ is_expanded: false,
+ }
+ }
+
+ fn expand(mut self, yes: bool) -> Self {
+ self.is_expanded = yes;
+ self
+ }
+}
+
impl Pager {
pub fn new(config: Config, mut out: W) -> Self {
info!("initializing pager");
@@ -40,7 +59,7 @@ impl Pager {
.unwrap();
Self {
- current_entry: Path::new(&config.setup.working_dir).to_path_buf(),
+ current_entry: CurrentEntry::new(&config.setup.working_dir),
config,
cursor_row: 0,
out,
diff --git a/src/view/mode.rs b/src/view/mode.rs
index 037eed1..ac885b0 100644
--- a/src/view/mode.rs
+++ b/src/view/mode.rs
@@ -8,6 +8,8 @@ pub enum Mode {
Normal,
/// Rename mode. Renaming a file.
Rename(String),
+ /// Create entry mode. Creating a new file and directory.
+ CreateEntry,
}
impl Mode {
diff --git a/src/view/update.rs b/src/view/update.rs
index 1bc3c6c..755fdb8 100644
--- a/src/view/update.rs
+++ b/src/view/update.rs
@@ -6,6 +6,8 @@ use crate::{controller::Entrie, view::Pager};
use std::{io::Write, path};
use termion::terminal_size;
+use super::CurrentEntry;
+
impl Pager {
fn update_terminal_size(&mut self) {
let (terminal_cols_raw, terminal_rows_raw) = terminal_size().unwrap();
@@ -59,8 +61,12 @@ impl Pager {
let entry = &entries[index as usize];
if index == self.cursor_row {
- self.current_entry =
- path::absolute(&entry.path).unwrap_or_else(|_| entry.path.clone());
+ self.current_entry = CurrentEntry::new(
+ path::absolute(&entry.path).unwrap_or_else(|_| entry.path.clone()),
+ )
+ .expand(
+ entry.path.is_dir() && entry.display_text.trim().starts_with(['▼', 'v']),
+ );
let rename_name = self.mode.new_name().map(|new_name| {
let filename = entry.path.file_name().unwrap().to_str().unwrap();
diff --git a/twilight-tree.toml b/twilight-tree.toml
index 07307a0..9682a87 100644
--- a/twilight-tree.toml
+++ b/twilight-tree.toml
@@ -52,6 +52,7 @@ reload = "ctrl+r"
skip_up = "ctrl+up"
skip_down = "ctrl+down"
remove = "d"
+new_entry = "n"
[setup]
# the working directory used when starting