Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ nucleo-matcher = "0.3.1"
tokio = {version="1.49.0", features=["full"]}
tokio-stream = {version="0.1.18", features = ["sync"]}
log4rs_test_utils = "0.2.3"
yamlpatch = "0.11.0"
yamlpath = "0.33.0"

# The profile that 'dist' will build with
[profile.dist]
Expand Down
4 changes: 3 additions & 1 deletion docs/gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ There are three main views:

1. *Help view*: Shows available commands.

Use <kbd>Tab</kbd> to switch between the first two views, and <kbd>Ctrl+H</kbd> for help.
Use <kbd>Tab</kbd> to switch between the first two views, and <kbd>Ctrl+h</kbd> for help.

* <kbd>Enter</kbd>: Go to selected directory

Expand All @@ -59,6 +59,8 @@ Use <kbd>Tab</kbd> to switch between the first two views, and <kbd>Ctrl+H</kbd>

* <kbd>Ctrl+f</kbd> Switch between exact and fuzzy search

* <kbd>F12</kbd>: Open the configuration view

Also, you can simply type a string to filter directories history or shortcuts.

## Search
Expand Down
118 changes: 106 additions & 12 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
use std::{env, fs, io::Write, path::PathBuf};
use std::{
env, fs,
io::Write,
path::PathBuf,
sync::{Arc, OnceLock},
};

use chrono::{DateTime, Local};
use log::{debug, error, info, trace};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use yamlpatch::{Op, Patch, apply_yaml_patches};
use yamlpath::route;

use crate::theme::{Theme, ThemeStyles};

pub(crate) const CDIR_CONFIG_VAR: &str = "CDIR_CONFIG";

static CONFIG_FILE_PATH: OnceLock<PathBuf> = OnceLock::new();

const DEFAULT_DB_PATH: fn() -> Option<PathBuf> = || {
let mut path = dirs::data_dir().unwrap();
path.push("cdir");
Expand Down Expand Up @@ -41,8 +51,8 @@ const DEFAULT_THEME: fn() -> Option<String> = || Some(String::from("default"));

const DEFAULT_COLORS: fn() -> Theme = || serde_yaml::from_str("").unwrap();

const DEFAULT_DATE_FORMATER: fn() -> Box<dyn Fn(i64) -> String> =
|| Box::from(|_| String::from(""));
const DEFAULT_DATE_FORMATER: fn() -> Arc<dyn Fn(i64) -> String + Send + Sync> =
|| Arc::from(|_| String::from(""));

const DEFAULT_NONE: fn() -> Option<String> = || None;

Expand Down Expand Up @@ -99,13 +109,9 @@ pub struct Config {
pub styles: ThemeStyles,

#[serde(skip, default = "DEFAULT_DATE_FORMATER")]
pub date_formater: Box<dyn Fn(i64) -> String>,
pub date_formater: Arc<dyn Fn(i64) -> String + Send + Sync>,
}

// Not really true, but good enough for our use case as it is the case after initialization (immutable config)
unsafe impl Send for Config {}
unsafe impl Sync for Config {}

impl Config {
fn build_config_file_path(config_file_path: Option<PathBuf>) -> PathBuf {
if let Some(path) = config_file_path {
Expand All @@ -125,14 +131,20 @@ impl Config {
path
}

pub fn load(config_file_path: Option<PathBuf>) -> Result<Config, String> {
pub fn initialize_and_load(config_file_path: Option<PathBuf>) -> Result<Config, String> {
let path = Self::build_config_file_path(config_file_path);

if !path.exists() {
Self::initialize(path.clone());
Self::install_themes(path.clone());
}

CONFIG_FILE_PATH.get_or_init(|| path.clone());

Self::load(path)
}

pub fn load(path: PathBuf) -> Result<Config, String> {
let file = std::fs::File::open(path.clone());

match serde_yaml::from_reader(file.unwrap()) {
Expand All @@ -148,7 +160,7 @@ impl Config {
self.styles = ThemeStyles::from(&actual_theme);

let date_format = self.date_format.clone();
self.date_formater = Box::from(move |s: i64| {
self.date_formater = Arc::from(move |s: i64| {
DateTime::from_timestamp(s, 0)
.unwrap()
.with_timezone(&Local::now().timezone())
Expand Down Expand Up @@ -394,6 +406,88 @@ impl Config {
std::fs::write(&theme_path, content)
.unwrap_or_else(|_| panic!("Failed create the theme file {:?}", theme_path));
}

// Save the configuration by applying a patch to the existing config file, to preserve comments and formatting as much as possible
pub(crate) fn save(&self) -> Result<(), String> {
info!("Saving configuration to {:?}", CONFIG_FILE_PATH.get());

let config_path = CONFIG_FILE_PATH
.get()
.ok_or_else(|| String::from("Config file path not initialized"))?
.clone();
let config_source = fs::read_to_string(&config_path)
.map_err(|e| format!("Failed to read config file {:?}: {}", config_path, e))?;
let document = yamlpath::Document::new(config_source.clone())
.map_err(|e| format!("Failed to parse config file {:?}: {}", config_path, e))?;

let config_from_file: Config = serde_yaml::from_str(&config_source)
.map_err(|e| format!("Failed to parse config file {:?}: {}", config_path, e))?;

let mut patches: Vec<Patch> = Vec::new();
let mut add_or_replace = |key: &str, value: Value| {
let key_owned = key.to_string();
let key_route = route!(key_owned.clone());
if document.query_exists(&key_route) {
patches.push(Patch {
route: key_route,
operation: Op::Replace(value),
});
} else {
patches.push(Patch {
route: route!(),
operation: Op::Add {
key: key_owned,
value,
},
});
}
};

if config_from_file.smart_suggestions_active != self.smart_suggestions_active {
add_or_replace(
"smart_suggestions_active",
Value::Bool(self.smart_suggestions_active),
);
}

if config_from_file.smart_suggestions_count != self.smart_suggestions_count {
add_or_replace(
"smart_suggestions_count",
Value::Number(serde_yaml::Number::from(
self.smart_suggestions_count as u64,
)),
);
}

if config_from_file.smart_suggestions_depth != self.smart_suggestions_depth {
add_or_replace(
"smart_suggestions_depth",
Value::Number(serde_yaml::Number::from(
self.smart_suggestions_depth as u64,
)),
);
}

if config_from_file.path_search_include_shortcuts != self.path_search_include_shortcuts {
add_or_replace(
"path_search_include_shortcuts",
Value::Bool(self.path_search_include_shortcuts),
);
}

if patches.is_empty() {
return Ok(());
}

let updated_document = apply_yaml_patches(&document, &patches)
.map_err(|e| format!("Failed to apply config patch {:?}: {}", config_path, e))?;
let updated_source = updated_document.source().to_string();

fs::write(&config_path, updated_source)
.map_err(|e| format!("Failed to write config file {:?}: {}", config_path, e))?;

Ok(())
}
}

impl Default for Config {
Expand All @@ -410,7 +504,7 @@ impl Default for Config {
smart_suggestions_depth: DEFAULT_SMART_SUGGESTIONS_DEPTH(),
smart_suggestions_count: DEFAULT_SMART_SUGGESTIONS_COUNT(),
themes_directory_path: Default::default(),
date_formater: Box::new(|date| date.to_string()),
date_formater: Arc::new(|date| date.to_string()),
db_path: Default::default(),
log_config_path: Default::default(),
path_search_include_shortcuts: true,
Expand Down Expand Up @@ -439,7 +533,7 @@ impl Clone for Config {
path_search_include_shortcuts: self.path_search_include_shortcuts,
date_format: self.date_format.clone(),
// Provide a new default closure for date_formater
date_formater: Box::new(|date| date.to_string()),
date_formater: Arc::new(|date| date.to_string()),
}
}
}
67 changes: 67 additions & 0 deletions src/config_button.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::{
rc::Rc,
sync::{Arc, Mutex},
};

use ratatui::{
layout::{Alignment, Position, Rect},
prelude::Style,
widgets::Paragraph,
};

use crate::{
config::Config,
config_view::ConfigView,
tui::{ManagerAction, View, ViewBuilder, ViewManager},
};

pub struct ConfigButton {
vm: Rc<ViewManager>,
config: Arc<Mutex<Config>>,
}

impl ConfigButton {
pub fn builder(vm: Rc<ViewManager>, config: Arc<Mutex<Config>>) -> ViewBuilder {
ViewBuilder::from(Box::new(ConfigButton { config, vm }))
}
}

impl View for ConfigButton {
fn draw(&mut self, frame: &mut ratatui::Frame, area: Rect, _active: bool) {
let config_lock = self.config.lock().unwrap();
// Fill the frame with the background color if defined
if let Some(bg_color) = &config_lock.styles.background_color {
// let area = frame.area();
let background = Paragraph::new("").style(Style::default().bg(*bg_color));
frame.render_widget(background, area);
}

let pa = Paragraph::new("F12: Config")
.style(
Style::default()
.bg(config_lock.styles.header_bg_color.unwrap())
.fg(config_lock.styles.header_fg_color.unwrap()),
)
.alignment(Alignment::Center);
frame.render_widget(pa, area);
}

fn handle_mouse_event(
&mut self,
area: Rect,
mouse_event: crossterm::event::MouseEvent,
) -> crate::tui::ManagerAction {
let mouse_position = Position::new(mouse_event.column, mouse_event.row);
if mouse_event.kind
== crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left)
&& area.contains(mouse_position)
{
self.vm.show_modal_generic(
ConfigView::builder(self.vm.clone(), self.config.clone()),
None,
);
return ManagerAction::new(true);
}
ManagerAction::new(false)
}
}
Loading