diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b75ba..6f5eed7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ ## Unreleased +### Added + +- `:config` to point to the config file if exists. +- felix listens to the change of the config file, and re-read the config automatically. +- Refactor around `State::new()`. + - Add `config_path` field to `State`. + ## v2.11.1 (2023-12-10) ### Fixed @@ -34,7 +41,7 @@ - Add `FxError::InvalidPath` to handle invalid unicode in file path. ## v2.9.0 (2023-10-22) - + ### Added - Change color of untracked/changed files or directories containing such files. Default color is Red(1). You can change it in the config file. - Add `git2`. diff --git a/src/config.rs b/src/config.rs index de8e6d7..ca20ffd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,51 +9,11 @@ pub const FELIX: &str = "felix"; const CONFIG_FILE: &str = "config.yaml"; const CONFIG_FILE_ANOTHER_EXT: &str = "config.yml"; -#[allow(dead_code)] -const CONFIG_EXAMPLE: &str = r###" -# Default exec command when open files. -# If not set, will default to $EDITOR. -# default: nvim - -# Whether to match the behavior of vim exit keybindings -# i.e. `ZQ` exits without cd to LWD (Last Working Directory) while `ZZ` cd to LWD -# match_vim_exit_behavior: false - -# key (the command you want to use when opening files): [values] (extensions) -# In the key, You can use arguments. -# exec: -# zathura: -# [pdf] -# 'feh -.': -# [jpg, jpeg, png, gif, svg, hdr] - -# The foreground color of directory, file and symlink. -# Pick one of the following: -# Black // 0 -# Red // 1 -# Green // 2 -# Yellow // 3 -# Blue // 4 -# Magenta // 5 -# Cyan // 6 -# White // 7 -# LightBlack // 8 -# LightRed // 9 -# LightGreen // 10 -# LightYellow // 11 -# LightBlue // 12 -# LightMagenta // 13 -# LightCyan // 14 -# LightWhite // 15 -# Rgb(u8, u8, u8) -# AnsiValue(u8) -# Default to LightCyan(dir), LightWhite(file), LightYellow(symlink) and Red(changed/untracked files in git repositories). -# color: -# dir_fg: LightCyan -# file_fg: LightWhite -# symlink_fg: LightYellow -# dirty_fg: Red -"###; +#[derive(Debug, Clone)] +pub struct ConfigWithPath { + pub config_path: Option, + pub config: Config, +} #[derive(Deserialize, Debug, Clone)] pub struct Config { @@ -115,13 +75,16 @@ impl Default for Config { } } -fn read_config(p: &Path) -> Result { +pub fn read_config(p: &Path) -> Result { let s = read_to_string(p)?; let deserialized: Config = serde_yaml::from_str(&s)?; - Ok(deserialized) + Ok(ConfigWithPath { + config_path: Some(p.to_path_buf()), + config: deserialized, + }) } -pub fn read_config_or_default() -> Result { +pub fn read_config_or_default() -> Result { //First, declare default config file path. let (config_file_path1, config_file_path2) = { let mut config_path = { @@ -172,6 +135,9 @@ pub fn read_config_or_default() -> Result { if let Some(config_file) = config_file { read_config(&config_file) } else { - Ok(Config::default()) + Ok(ConfigWithPath { + config_path: None, + config: Config::default(), + }) } } diff --git a/src/layout.rs b/src/layout.rs index 40502e7..0cad56c 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -2,10 +2,11 @@ use super::config::*; use super::errors::FxError; use super::functions::*; use super::nums::*; -use super::session::SortKey; +use super::session::{read_session, SortKey}; use super::state::{ItemInfo, BEGINNING_ROW}; use super::term::*; +use log::error; use serde::{Deserialize, Serialize}; pub const MAX_SIZE_TO_PREVIEW: u64 = 1_000_000_000; @@ -16,7 +17,7 @@ pub const PROPER_WIDTH: u16 = 28; pub const TIME_WIDTH: u16 = 16; const EXTRA_SPACES: u16 = 3; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Layout { pub nums: Num, pub y: u16, @@ -46,20 +47,67 @@ pub enum PreviewType { Binary, } -#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone)] +#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Default)] pub enum Side { + #[default] Preview, Reg, None, } -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone, Copy, Default)] pub enum Split { + #[default] Vertical, Horizontal, } impl Layout { + pub fn new(session_path: &std::path::Path, config: Config) -> Result { + let (original_column, original_row) = terminal_size()?; + // Return error if terminal size may cause panic + if original_column < 4 { + error!("Too small terminal size (less than 4 columns)."); + return Err(FxError::TooSmallWindowSize); + }; + if original_row < 4 { + error!("Too small terminal size. (less than 4 rows)"); + return Err(FxError::TooSmallWindowSize); + }; + + // Prepare state fields. + let (time_start, name_max) = make_layout(original_column); + let session = read_session(session_path); + let split = session.split.unwrap_or_default(); + let has_bat = check_bat(); + let has_chafa = check_chafa(); + let is_kitty = check_kitty_support(); + + let colors = config.color.unwrap_or_default(); + + Ok(Layout { + nums: Num::new(), + y: BEGINNING_ROW, + terminal_row: original_row, + terminal_column: original_column, + name_max_len: name_max, + time_start_pos: time_start, + sort_by: session.sort_by, + show_hidden: session.show_hidden, + side: match session.preview.unwrap_or(false) { + true => Side::Preview, + false => Side::None, + }, + split, + preview_start: (0, 0), + preview_space: (0, 0), + has_bat, + has_chafa, + is_kitty, + colors, + }) + } + pub fn is_preview(&self) -> bool { self.side == Side::Preview } @@ -364,3 +412,28 @@ pub fn make_layout(column: u16) -> (u16, usize) { (time_start, name_max) } } + +/// Check if bat is installed. +fn check_bat() -> bool { + std::process::Command::new("bat") + .arg("--help") + .output() + .is_ok() +} + +/// Check if chafa is installed. +fn check_chafa() -> bool { + std::process::Command::new("chafa") + .arg("--help") + .output() + .is_ok() +} + +/// Check if the terminal is Kitty or not +fn check_kitty_support() -> bool { + if let Ok(term) = std::env::var("TERM") { + term.contains("kitty") + } else { + false + } +} diff --git a/src/nums.rs b/src/nums.rs index 4b7720f..3aa05af 100644 --- a/src/nums.rs +++ b/src/nums.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Default)] pub struct Num { pub index: usize, pub skip: u16, diff --git a/src/op.rs b/src/op.rs index d2a54ed..0590a28 100644 --- a/src/op.rs +++ b/src/op.rs @@ -3,7 +3,7 @@ use super::state::ItemBuffer; use log::info; use std::path::PathBuf; -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct Operation { pub pos: usize, pub op_list: Vec, diff --git a/src/run.rs b/src/run.rs index f551bb2..4e49053 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,5 +1,4 @@ -#![allow(unreachable_code)] -use super::config::FELIX; +use super::config::{read_config, FELIX}; use super::errors::FxError; use super::functions::*; use super::layout::{PreviewType, Split}; @@ -18,6 +17,8 @@ use std::env; use std::io::{stdout, Write}; use std::panic; use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::thread; use std::time::Instant; const TRASH: &str = "Trash"; @@ -108,7 +109,7 @@ pub fn run(arg: PathBuf, log: bool) -> Result<(), FxError> { path }; - //Initialize app state. Inside State::new(), config file is read or created. + //Initialize app state. Inside `State::new()`, config file is read. let mut state = State::new(&session_path)?; state.trash_dir = trash_dir_path; state.lwd_file = lwd_file_path; @@ -169,7 +170,55 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> { } screen.flush()?; + // Spawn another thread to watch the config file. + let mut modified_time = match &state.config_path { + Some(config_path) => config_path.metadata().unwrap().modified().ok(), + None => None, + }; + let wait_update = Arc::new(Mutex::new(false)); + let wait_update_clone = wait_update.clone(); + let config_path_clone = state.config_path.clone(); + // if config file does not exist, no watching. + if modified_time.is_some() { + // Every 2 secondes, check if the config file is updated. + thread::spawn(move || loop { + thread::sleep(std::time::Duration::from_secs(2)); + if *wait_update_clone.lock().unwrap() { + continue; + } + let metadata = config_path_clone.as_ref().unwrap().metadata(); + if let Ok(metadata) = metadata { + let new_modified = metadata.modified().ok(); + if modified_time != new_modified { + if let Ok(mut updated) = wait_update_clone.lock() { + *updated = true; + modified_time = new_modified; + } else { + break; + } + } + } + }); + } + 'main: loop { + // Check if config file is updated + if state.config_path.is_some() { + if let Ok(mut wait_update) = wait_update.lock() { + if *wait_update { + if let Ok(c) = read_config(state.config_path.as_ref().unwrap()) { + state.set_config(c.config); + state.redraw(state.layout.y); + print_info("New config set.", state.layout.y); + } else { + // If reading the config file fails, leave the config as is. + print_warning("Something wrong with the config file.", state.layout.y); + } + *wait_update = false; + } + } + } + if state.is_out_of_bounds() { state.layout.nums.reset(); state.redraw(BEGINNING_ROW); @@ -2062,73 +2111,109 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> { let command = commands[0]; if commands.len() == 1 { - if command == "q" { - //quit - break 'main; - } else if command == "cd" || command == "z" { - //go to the home directory - let home_dir = dirs::home_dir() - .ok_or_else(|| { - FxError::Dirs( - "Cannot read home dir." - .to_string(), - ) - })?; - if let Err(e) = - state.chdir(&home_dir, Move::Jump) - { - print_warning(e, state.layout.y); + match command { + "q" => { + //quit + break 'main; } - break 'command; - } else if command == "e" { - //reload current dir - state.keyword = None; - state.layout.nums.reset(); - state.reload(BEGINNING_ROW)?; - break 'command; - } else if command == "h" { - //show help - state.show_help(&screen)?; - state.redraw(state.layout.y); - break 'command; - } else if command == "reg" { - //:reg - Show registers - if state.layout.is_preview() { - state.layout.show_reg(); + "cd" | "z" => { + //go to the home directory + let home_dir = dirs::home_dir() + .ok_or_else(|| { + FxError::Dirs( + "Cannot read home dir." + .to_string(), + ) + })?; + if let Err(e) = + state.chdir(&home_dir, Move::Jump) + { + print_warning(e, state.layout.y); + } + break 'command; + } + "e" => { + //reload current dir + state.keyword = None; + state.layout.nums.reset(); + state.reload(BEGINNING_ROW)?; + break 'command; + } + "h" => { + //show help + state.show_help(&screen)?; state.redraw(state.layout.y); - } else if state.layout.is_reg() { - go_to_info_line_and_reset(); - hide_cursor(); - state.move_cursor(state.layout.y); - } else { - state.layout.show_reg(); - let (new_column, new_row) = state - .layout - .update_column_and_row()?; - state.refresh( - new_column, - new_row, - state.layout.y, - )?; - go_to_info_line_and_reset(); - hide_cursor(); - state.move_cursor(state.layout.y); + break 'command; } - break 'command; - } else if command == "trash" { - //move to trash dir - state.layout.nums.reset(); - if let Err(e) = state.chdir( - &(state.trash_dir.clone()), - Move::Jump, - ) { - print_warning(e, state.layout.y); + "reg" => { + //:reg - Show registers + if state.layout.is_preview() { + state.layout.show_reg(); + state.redraw(state.layout.y); + } else if state.layout.is_reg() { + go_to_info_line_and_reset(); + hide_cursor(); + state.move_cursor(state.layout.y); + } else { + state.layout.show_reg(); + let (new_column, new_row) = state + .layout + .update_column_and_row()?; + state.refresh( + new_column, + new_row, + state.layout.y, + )?; + go_to_info_line_and_reset(); + hide_cursor(); + state.move_cursor(state.layout.y); + } + break 'command; } - break 'command; - } else if command == "empty" { - //empty the trash dir - state.empty_trash(&screen)?; - break 'command; + "trash" => { + //move to trash dir + state.layout.nums.reset(); + if let Err(e) = state.chdir( + &(state.trash_dir.clone()), + Move::Jump, + ) { + print_warning(e, state.layout.y); + } + break 'command; + } + "empty" => { + //empty the trash dir + state.empty_trash(&screen)?; + break 'command; + } + "config" => { + //move to the directory that contains + //config path + state.layout.nums.reset(); + if let Some(ref config_path) = + state.config_path + { + if let Err(e) = state.chdir( + config_path + .clone() + .parent() + .unwrap(), + Move::Jump, + ) { + print_warning( + e, + state.layout.y, + ); + } + } else { + print_warning( + "Cannot find the config path.", + state.layout.y, + ) + } + break 'command; + } + _ => {} } } else if commands.len() == 2 && command == "cd" { if let Ok(target) = diff --git a/src/session.rs b/src/session.rs index 2b3ad01..f3ac3a6 100644 --- a/src/session.rs +++ b/src/session.rs @@ -18,8 +18,9 @@ pub struct Session { pub split: Option, } -#[derive(Deserialize, Serialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone, Default)] pub enum SortKey { + #[default] Name, Time, } diff --git a/src/state.rs b/src/state.rs index 7cfecf6..fa40bed 100644 --- a/src/state.rs +++ b/src/state.rs @@ -15,7 +15,7 @@ use chrono::prelude::*; use crossterm::event::KeyEventKind; use crossterm::event::{Event, KeyCode, KeyEvent}; use crossterm::style::Stylize; -use log::{error, info}; +use log::info; use std::collections::VecDeque; use std::collections::{BTreeMap, BTreeSet}; use std::env; @@ -40,13 +40,13 @@ use std::os::unix::fs::PermissionsExt; pub const BEGINNING_ROW: u16 = 3; pub const EMPTY_WARNING: &str = "Are you sure to empty the trash directory? (if yes: y)"; -const BASE32: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct State { pub list: Vec, pub current_dir: PathBuf, pub trash_dir: PathBuf, + pub config_path: Option, pub lwd_file: Option, pub match_vim_exit_behavior: bool, pub has_zoxide: bool, @@ -63,7 +63,7 @@ pub struct State { pub is_ro: bool, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Registers { pub unnamed: Vec, pub zero: Vec, @@ -235,102 +235,38 @@ impl State { pub fn new(session_path: &std::path::Path) -> Result { //Read config file. //Use default configuration if the file does not exist or cannot be read. - let config = read_config_or_default(); - let config = match config { - Ok(c) => c, + let config_with_path = read_config_or_default(); + let (config_path, config) = match config_with_path { + Ok(c) => (c.config_path, c.config), Err(e) => { eprintln!("Cannot read the config file properly.\nError: {}\nfelix launches with default configuration.", e); - Config::default() + (None, Config::default()) } }; + let mut state = State::default(); + state.set_config(config.clone()); - let session = read_session(session_path); - let (original_column, original_row) = terminal_size()?; - - // Return error if terminal size may cause panic - if original_column < 4 { - error!("Too small terminal size (less than 4 columns)."); - return Err(FxError::TooSmallWindowSize); - }; - if original_row < 4 { - error!("Too small terminal size. (less than 4 rows)"); - return Err(FxError::TooSmallWindowSize); - }; - - let (time_start, name_max) = make_layout(original_column); - - let color = config.color.unwrap_or_default(); - - let split = session.split.unwrap_or(Split::Vertical); - - let has_bat = check_bat(); - let has_chafa = check_chafa(); let has_zoxide = check_zoxide(); - let is_kitty = check_kitty_support(); Ok(State { - list: Vec::new(), - registers: Registers { - unnamed: vec![], - zero: vec![], - numbered: VecDeque::new(), - named: BTreeMap::new(), - }, - operations: Operation { - pos: 0, - op_list: Vec::new(), - }, - current_dir: PathBuf::new(), - trash_dir: PathBuf::new(), - lwd_file: None, - match_vim_exit_behavior: config.match_vim_exit_behavior.unwrap_or_default(), + config_path, has_zoxide, - default: config - .default - .unwrap_or_else(|| env::var("EDITOR").unwrap_or_default()), - commands: to_extension_map(&config.exec), - layout: Layout { - nums: Num::new(), - y: BEGINNING_ROW, - terminal_row: original_row, - terminal_column: original_column, - name_max_len: name_max, - time_start_pos: time_start, - colors: ConfigColor { - dir_fg: color.dir_fg, - file_fg: color.file_fg, - symlink_fg: color.symlink_fg, - dirty_fg: color.dirty_fg, - }, - sort_by: session.sort_by, - show_hidden: session.show_hidden, - side: if session.preview.unwrap_or(false) { - Side::Preview - } else { - Side::None - }, - split, - preview_start: match split { - Split::Vertical => (0, 0), - Split::Horizontal => (0, 0), - }, - preview_space: match split { - Split::Vertical => (0, 0), - Split::Horizontal => (0, 0), - }, - has_bat, - has_chafa, - is_kitty, - }, - jumplist: JumpList::default(), - c_memo: Vec::new(), - p_memo: Vec::new(), - keyword: None, - v_start: None, - is_ro: false, + layout: Layout::new(session_path, config)?, + ..state }) } + /// Set configuration from config file. + pub fn set_config(&mut self, config: Config) { + self.default = config + .default + .unwrap_or_else(|| env::var("EDITOR").unwrap_or_default()); + self.match_vim_exit_behavior = config.match_vim_exit_behavior.unwrap_or_default(); + self.commands = to_extension_map(&config.exec); + let colors = config.color.unwrap_or_default(); + self.layout.colors = colors; + } + /// Select item that the cursor points to. pub fn get_item(&self) -> Result<&ItemInfo, FxError> { self.list @@ -1360,33 +1296,6 @@ impl State { } } - /// Creates temp file for directory. Works like touch, but with randomized suffix - #[allow(dead_code)] - pub fn create_temp(&mut self, is_dir: bool) -> Result { - let mut new_name = self.current_dir.join(".tmp"); - if new_name.exists() { - let mut nanos = std::time::SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .subsec_nanos(); - let encoded: &mut [u8] = &mut [0, 0, 0, 0, 0]; - for i in 0..5 { - let v = (nanos & 0x1f) as usize; - encoded[4 - i] = BASE32[v]; - nanos >>= 5; - } - new_name = self - .current_dir - .join(format!(".tmp_{}", String::from_utf8(encoded.to_vec())?)) - } - if is_dir { - std::fs::create_dir(new_name.clone())?; - } else { - std::fs::File::create(new_name.clone())?; - } - Ok(new_name) - } - /// Show help pub fn show_help(&self, mut screen: &Stdout) -> Result<(), FxError> { clear_all(); @@ -1954,22 +1863,6 @@ fn read_item(entry: fs::DirEntry) -> ItemInfo { // Ok(result) // } -/// Check if bat is installed. -fn check_bat() -> bool { - std::process::Command::new("bat") - .arg("--help") - .output() - .is_ok() -} - -/// Check if chafa is installed. -fn check_chafa() -> bool { - std::process::Command::new("chafa") - .arg("--help") - .output() - .is_ok() -} - /// Check if zoxide is installed. fn check_zoxide() -> bool { std::process::Command::new("zoxide") @@ -1978,15 +1871,6 @@ fn check_zoxide() -> bool { .is_ok() } -/// Check if the terminal is Kitty or not -fn check_kitty_support() -> bool { - if let Ok(term) = std::env::var("TERM") { - term.contains("kitty") - } else { - false - } -} - /// Set content type from ItemInfo. fn set_preview_content_type(item: &mut ItemInfo) { if item.file_size > MAX_SIZE_TO_PREVIEW {