feat: Support entry renaming
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
parent
ffafb7de6a
commit
298001dc15
16 changed files with 235 additions and 66 deletions
|
@ -17,7 +17,7 @@ impl<W: Write> EventQueue<W> {
|
||||||
self.path_node_root.collapse_dir(&tree_index);
|
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);
|
self.update_pager(cursor_delta);
|
||||||
Some(())
|
Some(())
|
||||||
|
|
|
@ -11,7 +11,7 @@ impl<W: Write> EventQueue<W> {
|
||||||
.flat_index_to_tree_index(self.pager.cursor_row as usize);
|
.flat_index_to_tree_index(self.pager.cursor_row as usize);
|
||||||
self.path_node_root
|
self.path_node_root
|
||||||
.expand_dir(&tree_index, self.path_node_compare);
|
.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);
|
self.update_pager(0);
|
||||||
Some(())
|
Some(())
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
|
|
||||||
use crate::controller::EventQueue;
|
use crate::controller::EventQueue;
|
||||||
use crate::model::event::Key;
|
use crate::model::event::Key;
|
||||||
|
use crate::view::Mode;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
mod collapse_dir;
|
mod collapse_dir;
|
||||||
|
@ -11,28 +14,49 @@ mod entry_down;
|
||||||
mod entry_up;
|
mod entry_up;
|
||||||
mod expand_dir;
|
mod expand_dir;
|
||||||
mod file_action;
|
mod file_action;
|
||||||
|
mod modes;
|
||||||
mod quit;
|
mod quit;
|
||||||
mod reload;
|
mod reload;
|
||||||
|
mod rename;
|
||||||
|
|
||||||
impl<W: Write> EventQueue<W> {
|
impl<W: Write> EventQueue<W> {
|
||||||
#[rustfmt::skip]
|
|
||||||
pub fn match_key_event(&mut self, key: Key) -> Option<()> {
|
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() }
|
match (self.pager.mode.clone(), key) {
|
||||||
else if key == Key::from(ck.entry_down) { self.do_entry_down() }
|
(Mode::Normal, k) if k == Key::from(&ck.collapse_dir) => self.do_collapse_dir(),
|
||||||
else if key == Key::from(ck.entry_up) { self.do_entry_up() }
|
(Mode::Normal, k) if k == Key::from(&ck.entry_down) => self.do_entry_down(),
|
||||||
else if key == Key::from(ck.expand_dir) { self.do_expand_dir() }
|
(Mode::Normal, k) if k == Key::from(&ck.entry_up) => self.do_entry_up(),
|
||||||
else if key == Key::from(ck.file_action) { self.do_file_action() }
|
(Mode::Normal, k) if k == Key::from(&ck.expand_dir) => self.do_expand_dir(),
|
||||||
else if key == Key::from(ck.quit) { self.do_quit() }
|
(Mode::Normal, k) if k == Key::from(&ck.file_action) => self.do_file_action(),
|
||||||
else if key == Key::from(ck.reload) { self.do_reload() }
|
(Mode::Normal, k) if k == Key::from(&ck.quit) => self.do_quit(),
|
||||||
else { Some(()) }
|
(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) {
|
fn update_pager(&mut self, cursor_delta: i32) {
|
||||||
self.pager.update(
|
self.pager.update(
|
||||||
cursor_delta,
|
cursor_delta,
|
||||||
&self.text_entries,
|
&self.entries,
|
||||||
self.config
|
self.config
|
||||||
.setup
|
.setup
|
||||||
.with_cwd_header
|
.with_cwd_header
|
||||||
|
|
26
src/controller/key_event_matcher/modes.rs
Normal file
26
src/controller/key_event_matcher/modes.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::{controller::EventQueue, view::Mode};
|
||||||
|
|
||||||
|
impl<W: Write> EventQueue<W> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ impl<W: Write> EventQueue<W> {
|
||||||
pub fn do_reload(&mut self) -> Option<()> {
|
pub fn do_reload(&mut self) -> Option<()> {
|
||||||
self.reload_openend_dirs();
|
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);
|
self.update_pager(0);
|
||||||
|
|
||||||
|
|
46
src/controller/key_event_matcher/rename.rs
Normal file
46
src/controller/key_event_matcher/rename.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
||||||
|
|
||||||
|
use std::{borrow::Cow, fs, io::Write};
|
||||||
|
|
||||||
|
use termion::event::Key as TKey;
|
||||||
|
|
||||||
|
use crate::controller::EventQueue;
|
||||||
|
|
||||||
|
impl<W: Write> EventQueue<W> {
|
||||||
|
/// 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,8 @@ use crate::view::composer::Composer;
|
||||||
use crate::view::Pager;
|
use crate::view::Pager;
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::mpsc::sync_channel;
|
use std::sync::mpsc::sync_channel;
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::mpsc::Receiver;
|
||||||
use std::sync::mpsc::SyncSender;
|
use std::sync::mpsc::SyncSender;
|
||||||
|
@ -21,6 +23,12 @@ mod key_event_handler;
|
||||||
mod key_event_matcher;
|
mod key_event_matcher;
|
||||||
mod resize_event_handler;
|
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<W: Write> {
|
pub struct EventQueue<W: Write> {
|
||||||
config: Config,
|
config: Config,
|
||||||
composer: Composer,
|
composer: Composer,
|
||||||
|
@ -31,7 +39,7 @@ pub struct EventQueue<W: Write> {
|
||||||
queue_sender: SyncSender<Event>,
|
queue_sender: SyncSender<Event>,
|
||||||
|
|
||||||
// TODO: should be part of the view?
|
// TODO: should be part of the view?
|
||||||
text_entries: Vec<String>,
|
entries: Vec<Entrie>,
|
||||||
command_to_run_on_exit: Option<String>,
|
command_to_run_on_exit: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,10 +55,10 @@ impl<W: Write> EventQueue<W> {
|
||||||
let (queue_sender, queue_receiver): (SyncSender<Event>, Receiver<Event>) =
|
let (queue_sender, queue_receiver): (SyncSender<Event>, Receiver<Event>) =
|
||||||
sync_channel(1024);
|
sync_channel(1024);
|
||||||
let path_node_compare = PathNode::get_path_node_compare(&config);
|
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(
|
pager.update(
|
||||||
0,
|
0,
|
||||||
&text_entries,
|
&entries,
|
||||||
config
|
config
|
||||||
.setup
|
.setup
|
||||||
.with_cwd_header
|
.with_cwd_header
|
||||||
|
@ -66,7 +74,7 @@ impl<W: Write> EventQueue<W> {
|
||||||
path_node_compare,
|
path_node_compare,
|
||||||
queue_receiver,
|
queue_receiver,
|
||||||
queue_sender,
|
queue_sender,
|
||||||
text_entries,
|
entries,
|
||||||
command_to_run_on_exit,
|
command_to_run_on_exit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,7 +102,7 @@ impl<W: Write> EventQueue<W> {
|
||||||
Event::Resize => {
|
Event::Resize => {
|
||||||
self.pager.update(
|
self.pager.update(
|
||||||
0,
|
0,
|
||||||
&self.text_entries,
|
&self.entries,
|
||||||
self.config
|
self.config
|
||||||
.setup
|
.setup
|
||||||
.with_cwd_header
|
.with_cwd_header
|
||||||
|
@ -105,3 +113,13 @@ impl<W: Write> EventQueue<W> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Entrie {
|
||||||
|
/// Create a new `Entrie`.
|
||||||
|
pub fn new(path: impl AsRef<Path>, display_text: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
path: path.as_ref().to_path_buf(),
|
||||||
|
display_text: display_text.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright (c) 2019-2022 golmman
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
@ -7,24 +8,20 @@ use serde::Deserialize;
|
||||||
pub struct Keybinding {
|
pub struct Keybinding {
|
||||||
#[serde(default = "Keybinding::default_quit")]
|
#[serde(default = "Keybinding::default_quit")]
|
||||||
pub quit: String,
|
pub quit: String,
|
||||||
|
|
||||||
#[serde(default = "Keybinding::default_entry_up")]
|
#[serde(default = "Keybinding::default_entry_up")]
|
||||||
pub entry_up: String,
|
pub entry_up: String,
|
||||||
|
|
||||||
#[serde(default = "Keybinding::default_entry_down")]
|
#[serde(default = "Keybinding::default_entry_down")]
|
||||||
pub entry_down: String,
|
pub entry_down: String,
|
||||||
|
|
||||||
#[serde(default = "Keybinding::default_expand_dir")]
|
#[serde(default = "Keybinding::default_expand_dir")]
|
||||||
pub expand_dir: String,
|
pub expand_dir: String,
|
||||||
|
|
||||||
#[serde(default = "Keybinding::default_collapse_dir")]
|
#[serde(default = "Keybinding::default_collapse_dir")]
|
||||||
pub collapse_dir: String,
|
pub collapse_dir: String,
|
||||||
|
|
||||||
#[serde(default = "Keybinding::default_file_action")]
|
#[serde(default = "Keybinding::default_file_action")]
|
||||||
pub file_action: String,
|
pub file_action: String,
|
||||||
|
|
||||||
#[serde(default = "Keybinding::default_reload")]
|
#[serde(default = "Keybinding::default_reload")]
|
||||||
pub reload: String,
|
pub reload: String,
|
||||||
|
#[serde(default = "Keybinding::default_rename_mode")]
|
||||||
|
pub rename_mode: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Keybinding {
|
impl Default for Keybinding {
|
||||||
|
@ -37,36 +34,34 @@ impl Default for Keybinding {
|
||||||
collapse_dir: Self::default_collapse_dir(),
|
collapse_dir: Self::default_collapse_dir(),
|
||||||
file_action: Self::default_file_action(),
|
file_action: Self::default_file_action(),
|
||||||
reload: Self::default_reload(),
|
reload: Self::default_reload(),
|
||||||
|
rename_mode: Self::default_rename_mode(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Keybinding {
|
impl Keybinding {
|
||||||
fn default_quit() -> String {
|
fn default_quit() -> String {
|
||||||
String::from("q")
|
String::from("ctrl+q")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_entry_up() -> String {
|
fn default_entry_up() -> String {
|
||||||
String::from("up")
|
String::from("up")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_entry_down() -> String {
|
fn default_entry_down() -> String {
|
||||||
String::from("down")
|
String::from("down")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_expand_dir() -> String {
|
fn default_expand_dir() -> String {
|
||||||
String::from("right")
|
String::from("right")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_collapse_dir() -> String {
|
fn default_collapse_dir() -> String {
|
||||||
String::from("left")
|
String::from("left")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_file_action() -> String {
|
fn default_file_action() -> String {
|
||||||
String::from("return")
|
String::from("return")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_reload() -> String {
|
fn default_reload() -> String {
|
||||||
|
String::from("ctrl+r")
|
||||||
|
}
|
||||||
|
fn default_rename_mode() -> String {
|
||||||
String::from("r")
|
String::from("r")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,22 @@ impl From<String> 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 {
|
fn convert_str_to_termion_event(s: &str) -> TEvent {
|
||||||
if s.chars().count() == 1 {
|
if s.chars().count() == 1 {
|
||||||
return TEvent::Key(TKey::Char(s.chars().last().unwrap()));
|
return TEvent::Key(TKey::Char(s.chars().last().unwrap()));
|
||||||
|
|
|
@ -15,7 +15,7 @@ impl Debug for PathNode {
|
||||||
let entries = composer.compose_path_node(self);
|
let entries = composer.compose_path_node(self);
|
||||||
|
|
||||||
for (index, entry) in entries.iter().enumerate() {
|
for (index, entry) in entries.iter().enumerate() {
|
||||||
writeln!(f, "{:4}|{}", index, entry)?;
|
writeln!(f, "{:4}|{}", index, entry.display_text)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -20,7 +20,7 @@ pub struct PathNode {
|
||||||
pub is_err: bool,
|
pub is_err: bool,
|
||||||
pub is_expanded: bool,
|
pub is_expanded: bool,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub gitignore: Vec<String>,
|
pub gitignore: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<T> for PathNode
|
impl<T> From<T> for PathNode
|
||||||
|
@ -28,9 +28,11 @@ where
|
||||||
T: AsRef<Path>,
|
T: AsRef<Path>,
|
||||||
{
|
{
|
||||||
fn from(working_dir: T) -> Self {
|
fn from(working_dir: T) -> Self {
|
||||||
let mut gitignore =
|
let mut gitignore = utils::parse_gitignore(
|
||||||
utils::parse_gitignore(Path::new(working_dir.as_ref()).join(".gitignore"));
|
working_dir.as_ref(),
|
||||||
gitignore.push(working_dir.as_ref().join(".git").display().to_string());
|
Path::new(working_dir.as_ref()).join(".gitignore"),
|
||||||
|
);
|
||||||
|
gitignore.push(working_dir.as_ref().join(".git"));
|
||||||
Self {
|
Self {
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
display_text: working_dir.as_ref().display().to_string(),
|
display_text: working_dir.as_ref().display().to_string(),
|
||||||
|
@ -72,7 +74,7 @@ impl PathNode {
|
||||||
.filter_map(|dir_entry| {
|
.filter_map(|dir_entry| {
|
||||||
let dir_entry = dir_entry.unwrap();
|
let dir_entry = dir_entry.unwrap();
|
||||||
let entry_name = dir_entry.file_name().into_string().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(),
|
children: Vec::new(),
|
||||||
display_text: entry_name,
|
display_text: entry_name,
|
||||||
is_dir: dir_entry.path().is_dir(),
|
is_dir: dir_entry.path().is_dir(),
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::panic::set_hook;
|
use std::panic::set_hook;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
|
||||||
pub fn print_help() {
|
pub fn print_help() {
|
||||||
|
@ -63,13 +63,13 @@ pub fn get_config_dir() -> std::io::Result<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_gitignore(gitignore_path: impl AsRef<Path>) -> Vec<String> {
|
pub fn parse_gitignore(root: impl AsRef<Path>, gitignore_path: impl AsRef<Path>) -> Vec<PathBuf> {
|
||||||
if Path::exists(gitignore_path.as_ref()) {
|
if gitignore_path.as_ref().exists() {
|
||||||
fs::read_to_string(&gitignore_path)
|
fs::read_to_string(&gitignore_path)
|
||||||
.map(|content| {
|
.map(|content| {
|
||||||
content
|
content
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(|path| path.trim_matches('/').to_owned())
|
.map(|path| root.as_ref().join(path.trim_matches('/')))
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright (c) 2019-2022 golmman
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
||||||
|
|
||||||
|
use crate::controller::Entrie;
|
||||||
use crate::model::config::Config;
|
use crate::model::config::Config;
|
||||||
use crate::model::path_node::PathNode;
|
use crate::model::path_node::PathNode;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
@ -34,7 +36,7 @@ impl Composer {
|
||||||
format!("{}~", truncated)
|
format!("{}~", truncated)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compose_path_node(&self, path_node: &PathNode) -> Vec<String> {
|
pub fn compose_path_node(&self, path_node: &PathNode) -> Vec<Entrie> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
self.compose_path_node_recursive(path_node, &mut result, 0);
|
self.compose_path_node_recursive(path_node, &mut result, 0);
|
||||||
|
@ -45,7 +47,7 @@ impl Composer {
|
||||||
fn compose_path_node_recursive(
|
fn compose_path_node_recursive(
|
||||||
&self,
|
&self,
|
||||||
path_node: &PathNode,
|
path_node: &PathNode,
|
||||||
texts: &mut Vec<String>,
|
entries: &mut Vec<Entrie>,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
) {
|
) {
|
||||||
for child in &path_node.children {
|
for child in &path_node.children {
|
||||||
|
@ -53,15 +55,12 @@ impl Composer {
|
||||||
let dir_suffix = self.get_dir_suffix(child);
|
let dir_suffix = self.get_dir_suffix(child);
|
||||||
let indent = self.get_indent(depth);
|
let indent = self.get_indent(depth);
|
||||||
|
|
||||||
let text = format!(
|
let display_text = format!(
|
||||||
"{}{}{}{}",
|
"{indent} {dir_prefix} {name}{dir_suffix}",
|
||||||
indent,
|
name = child.display_text
|
||||||
dir_prefix,
|
|
||||||
child.display_text.clone(),
|
|
||||||
dir_suffix,
|
|
||||||
);
|
);
|
||||||
texts.push(text);
|
entries.push(Entrie::new(&child.path, display_text));
|
||||||
self.compose_path_node_recursive(child, texts, depth + 1);
|
self.compose_path_node_recursive(child, entries, depth + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,16 +95,16 @@ impl Composer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_indent(&self, depth: usize) -> String {
|
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 if self.config.composition.use_utf8 {
|
||||||
'·'
|
'·'
|
||||||
} else {
|
} else {
|
||||||
'-'
|
'-'
|
||||||
};
|
};
|
||||||
let indent = " ".repeat(self.config.composition.indent as usize - 1);
|
|
||||||
|
|
||||||
format!("{}{}", indent_char, indent).repeat(depth)
|
format!("{char_indent}{indent}").repeat(depth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright (c) 2019-2022 golmman
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
||||||
|
|
||||||
use crate::model::config::Config;
|
use crate::model::config::Config;
|
||||||
use crate::view::composer::Composer;
|
use crate::view::composer::Composer;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
pub use mode::Mode;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub mod composer;
|
pub mod composer;
|
||||||
|
mod mode;
|
||||||
mod print;
|
mod print;
|
||||||
mod scroll;
|
mod scroll;
|
||||||
mod update;
|
mod update;
|
||||||
|
|
||||||
pub struct Pager<W: Write> {
|
pub struct Pager<W: Write> {
|
||||||
config: Config,
|
config: Config,
|
||||||
|
pub mode: Mode,
|
||||||
pub cursor_row: i32,
|
pub cursor_row: i32,
|
||||||
|
pub current_entry: PathBuf,
|
||||||
out: W,
|
out: W,
|
||||||
terminal_cols: i32,
|
terminal_cols: i32,
|
||||||
terminal_rows: i32,
|
terminal_rows: i32,
|
||||||
|
@ -34,12 +40,14 @@ impl<W: Write> Pager<W> {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
current_entry: Path::new(&config.setup.working_dir).to_path_buf(),
|
||||||
config,
|
config,
|
||||||
cursor_row: 0,
|
cursor_row: 0,
|
||||||
out,
|
out,
|
||||||
terminal_cols: 0,
|
terminal_cols: 0,
|
||||||
terminal_rows: 0,
|
terminal_rows: 0,
|
||||||
text_row: 0,
|
text_row: 0,
|
||||||
|
mode: Mode::Normal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
21
src/view/mode.rs
Normal file
21
src/view/mode.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,8 @@
|
||||||
// Copyright (c) 2019-2022 golmman
|
// Copyright (c) 2019-2022 golmman
|
||||||
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
||||||
|
|
||||||
use crate::view::Pager;
|
use crate::{controller::Entrie, view::Pager};
|
||||||
use std::io::Write;
|
use std::{io::Write, path};
|
||||||
use termion::terminal_size;
|
use termion::terminal_size;
|
||||||
|
|
||||||
impl<W: Write> Pager<W> {
|
impl<W: Write> Pager<W> {
|
||||||
|
@ -26,7 +26,7 @@ impl<W: Write> Pager<W> {
|
||||||
pub fn update(
|
pub fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
cursor_row_delta: i32,
|
cursor_row_delta: i32,
|
||||||
text_entries: &[String],
|
entries: &[Entrie],
|
||||||
header_text: Option<String>,
|
header_text: Option<String>,
|
||||||
) {
|
) {
|
||||||
self.update_terminal_size();
|
self.update_terminal_size();
|
||||||
|
@ -34,12 +34,12 @@ impl<W: Write> Pager<W> {
|
||||||
let spacing_bot = self.config.debug.spacing_bot;
|
let spacing_bot = self.config.debug.spacing_bot;
|
||||||
let spacing_top = self.config.debug.spacing_top;
|
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() {
|
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(),
|
"editor" => self.scroll_like_editor(),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
|
@ -55,18 +55,32 @@ impl<W: Write> Pager<W> {
|
||||||
for i in 0..displayable_rows {
|
for i in 0..displayable_rows {
|
||||||
let index = first_index + i;
|
let index = first_index + i;
|
||||||
|
|
||||||
if index >= 0 && index < text_entries.len() as i32 {
|
if index >= 0 && index < entries.len() as i32 {
|
||||||
let text_entry = &text_entries[index as usize];
|
let entry = &entries[index as usize];
|
||||||
|
|
||||||
if index == self.cursor_row {
|
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::<String>())
|
||||||
|
});
|
||||||
|
self.print_text_entry_emphasized(
|
||||||
|
rename_name.as_ref().unwrap_or(&entry.display_text),
|
||||||
|
1 + spacing_top + i,
|
||||||
|
)
|
||||||
} else {
|
} 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() {
|
if let Some(header_text) = header_text.as_ref() {
|
||||||
self.print_header(header_text);
|
self.print_header(header_text);
|
||||||
|
|
Loading…
Reference in a new issue