diff --git a/Cargo.lock b/Cargo.lock index 1384b1c..212d9f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1178,6 +1178,7 @@ dependencies = [ "cosmic-files", "cosmic-syntax-theme", "cosmic-text", + "dirs", "env_logger 0.10.2", "fork", "grep", diff --git a/Cargo.toml b/Cargo.toml index ad7bcd0..d47e115 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ license = "GPL-3.0-only" rust-version = "1.71" [dependencies] +dirs = "5" env_logger = "0.10.0" grep = "0.3.1" ignore = "0.4.21" diff --git a/src/config.rs b/src/config.rs index fb389ae..6fd9b91 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ use cosmic::{ }; use cosmic_text::Metrics; use serde::{Deserialize, Serialize}; +use std::{collections::VecDeque, path::PathBuf}; pub const CONFIG_VERSION: u64 = 1; @@ -33,6 +34,9 @@ pub struct Config { pub font_name: String, pub font_size: u16, pub line_numbers: bool, + //TODO: move to state? + pub recent_files: VecDeque, + pub recent_projects: VecDeque, pub syntax_theme_dark: String, pub syntax_theme_light: String, pub tab_width: u16, @@ -48,6 +52,8 @@ impl Default for Config { font_name: "Fira Mono".to_string(), font_size: 14, line_numbers: true, + recent_files: VecDeque::new(), + recent_projects: VecDeque::new(), syntax_theme_dark: "COSMIC Dark".to_string(), syntax_theme_light: "COSMIC Light".to_string(), tab_width: 4, diff --git a/src/main.rs b/src/main.rs index e01be3c..28238e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -174,6 +174,8 @@ pub enum Action { NewWindow, OpenFileDialog, OpenProjectDialog, + OpenRecentFile(usize), + OpenRecentProject(usize), Paste, Quit, Redo, @@ -215,6 +217,8 @@ impl Action { Self::NewWindow => Message::NewWindow, Self::OpenFileDialog => Message::OpenFileDialog, Self::OpenProjectDialog => Message::OpenProjectDialog, + Self::OpenRecentFile(index) => Message::OpenRecentFile(*index), + Self::OpenRecentProject(index) => Message::OpenRecentProject(*index), Self::Paste => Message::Paste, Self::Quit => Message::Quit, Self::Redo => Message::Redo, @@ -301,6 +305,8 @@ pub enum Message { OpenGitDiff(PathBuf, GitDiff), OpenProjectDialog, OpenProjectResult(DialogResult), + OpenRecentFile(usize), + OpenRecentProject(usize), OpenSearchResult(usize, usize), Paste, PasteValue(String), @@ -468,6 +474,11 @@ impl App { // Save the absolute path self.projects.push((name.to_string(), path.to_path_buf())); + + // Add to recent projects, ensuring only one entry + self.config.recent_projects.retain(|x| x != path); + self.config.recent_projects.push_front(path.to_path_buf()); + self.save_config_no_update(); } _ => { log::error!("failed to open project {:?}: not a directory", path); @@ -521,6 +532,11 @@ impl App { return Some(entity); } + // Add to recent files, ensuring only one entry + self.config.recent_files.retain(|x| x != &canonical); + self.config.recent_files.push_front(canonical.to_path_buf()); + self.save_config_no_update(); + let mut tab = EditorTab::new(&self.config); tab.open(canonical); tab.watch(&mut self.watcher_opt); @@ -553,13 +569,16 @@ impl App { } fn save_config(&mut self) -> Command { + self.save_config_no_update(); + self.update_config() + } + + fn save_config_no_update(&mut self) { if let Some(ref config_handler) = self.config_handler { if let Err(err) = self.config.write_entry(config_handler) { log::error!("failed to save config: {}", err); } } - - self.update_config() } fn update_focus(&self) -> Command { @@ -1577,6 +1596,17 @@ impl Application for App { } } } + Message::OpenRecentFile(index) => { + if let Some(path) = self.config.recent_files.get(index).cloned() { + self.open_tab(Some(path)); + return self.update_tab(); + } + } + Message::OpenRecentProject(index) => { + if let Some(path) = self.config.recent_files.get(index).cloned() { + self.open_project(path); + } + } Message::OpenSearchResult(file_i, line_i) => { let path_cursor_opt = match &self.project_search_result { Some(project_search_result) => match project_search_result.files.get(file_i) { diff --git a/src/menu.rs b/src/menu.rs index e00e2c8..9cfb1e4 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -158,6 +158,26 @@ pub fn menu_bar<'a>( ) }; + let home_dir_opt = dirs::home_dir(); + let format_path = |path: &PathBuf| -> String { + if let Some(home_dir) = &home_dir_opt { + if let Ok(part) = path.strip_prefix(home_dir) { + return format!("~/{}", part.display()); + } + } + path.display().to_string() + }; + + let mut recent_files = Vec::with_capacity(config.recent_files.len()); + for (i, path) in config.recent_files.iter().enumerate() { + recent_files.push(menu_item(format_path(path), Action::OpenRecentFile(i))); + } + + let mut recent_projects = Vec::with_capacity(config.recent_projects.len()); + for (i, path) in config.recent_projects.iter().enumerate() { + recent_projects.push(menu_item(format_path(path), Action::OpenRecentProject(i))); + } + let mut close_projects = Vec::with_capacity(projects.len()); for (project_i, (name, _path)) in projects.iter().enumerate() { close_projects.push(menu_item(name.clone(), Action::CloseProject(project_i))); @@ -171,17 +191,11 @@ pub fn menu_bar<'a>( menu_item(fl!("new-window"), Action::NewWindow), MenuTree::new(horizontal_rule(1)), menu_item(fl!("open-file"), Action::OpenFileDialog), - MenuTree::with_children( - menu_folder(fl!("open-recent-file")), - vec![menu_item(fl!("todo"), Action::Todo)], - ), + MenuTree::with_children(menu_folder(fl!("open-recent-file")), recent_files), menu_item(fl!("close-file"), Action::CloseFile), MenuTree::new(horizontal_rule(1)), menu_item(fl!("menu-open-project"), Action::OpenProjectDialog), - MenuTree::with_children( - menu_folder(fl!("open-recent-project")), - vec![menu_item(fl!("todo"), Action::Todo)], - ), + MenuTree::with_children(menu_folder(fl!("open-recent-project")), recent_projects), MenuTree::with_children(menu_folder(fl!("close-project")), close_projects), MenuTree::new(horizontal_rule(1)), menu_item(fl!("save"), Action::Save), @@ -263,7 +277,7 @@ pub fn menu_bar<'a>( ), ]) .item_height(ItemHeight::Dynamic(40)) - .item_width(ItemWidth::Uniform(240)) + .item_width(ItemWidth::Uniform(320)) .spacing(4.0) .into() }