From 70759ebca4cfede0a51e05d7eb365a06c84369bc Mon Sep 17 00:00:00 2001 From: Peter Simonsson Date: Sun, 21 Apr 2024 18:12:31 +0200 Subject: [PATCH] Add support for bookmarking a directory Any directory can be bookmarked, doesn't need to contain a git repo. Fixes #93 --- src/cli.rs | 39 ++++++++++++++++++++++++++++++- src/configs.rs | 52 ++++++++++++++++++++++++++++++++++++++++-- src/main.rs | 62 ++++++++++++++++++++++++++++++++++++++++++-------- tests/cli.rs | 1 + 4 files changed, 141 insertions(+), 13 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 63101ea..6f1bfa9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fs::canonicalize, path::Path}; +use std::{collections::HashMap, env::current_dir, fs::canonicalize, path::Path}; use crate::{ configs::{Config, SearchDirectory, SessionSortOrderConfig}, @@ -44,6 +44,8 @@ pub enum CliCommand { Refresh(RefreshCommand), /// Clone repository into the first search path and create a new session for it CloneRepo(CloneRepoCommand), + /// Bookmark a directory so it is available to select along with the Git repositories + Bookmark(BookmarkCommand), } #[derive(Debug, Args)] @@ -114,6 +116,15 @@ pub struct CloneRepoCommand { repository: String, } +#[derive(Debug, Args)] +pub struct BookmarkCommand { + #[arg(long, short)] + /// Delete instead of add a bookmark + delete: bool, + /// Path to bookmark, if left empty bookmark the current directory. + path: Option, +} + impl Cli { pub fn handle_sub_commands(&self, tmux: &Tmux) -> Result { // Get the configuration from the config file @@ -169,6 +180,11 @@ impl Cli { Ok(SubCommandGiven::Yes) } + Some(CliCommand::Bookmark(args)) => { + bookmark_command(args, config)?; + Ok(SubCommandGiven::Yes) + } + None => Ok(SubCommandGiven::No(config.into())), } } @@ -642,6 +658,27 @@ fn git_credentials_callback( git2::Cred::ssh_key_from_agent(user) } +fn bookmark_command(args: &BookmarkCommand, mut config: Config) -> Result<()> { + let path = if let Some(path) = &args.path { + path.to_owned() + } else { + current_dir() + .change_context(TmsError::IoError)? + .to_string() + .change_context(TmsError::IoError)? + }; + + if !args.delete { + config.add_bookmark(path); + } else { + config.delete_bookmark(path); + } + + config.save().change_context(TmsError::ConfigError)?; + + Ok(()) +} + pub enum SubCommandGiven { Yes, No(Box), diff --git a/src/configs.rs b/src/configs.rs index d9d0869..a9f9871 100644 --- a/src/configs.rs +++ b/src/configs.rs @@ -1,11 +1,11 @@ use clap::ValueEnum; use error_stack::ResultExt; use serde_derive::{Deserialize, Serialize}; -use std::{env, fmt::Display, fs::canonicalize, io::Write, path::PathBuf}; +use std::{collections::HashMap, env, fmt::Display, fs::canonicalize, io::Write, path::PathBuf}; use ratatui::style::{Color, Style}; -use crate::{keymap::Keymap, Suggestion}; +use crate::{dirty_paths::DirtyUtf8Path, keymap::Keymap, Suggestion}; type Result = error_stack::Result; @@ -46,6 +46,7 @@ pub struct Config { pub sessions: Option>, pub picker_colors: Option, pub shortcuts: Option, + pub bookmarks: Option>, } impl Config { @@ -180,6 +181,53 @@ impl Config { Ok(search_dirs) } + + pub fn add_bookmark(&mut self, path: String) { + let bookmarks = &mut self.bookmarks; + match bookmarks { + Some(ref mut bookmarks) => { + if !bookmarks.contains(&path) { + bookmarks.push(path); + } + } + None => { + self.bookmarks = Some(vec![path]); + } + } + } + + pub fn delete_bookmark(&mut self, path: String) { + if let Some(ref mut bookmarks) = self.bookmarks { + if let Some(idx) = bookmarks.iter().position(|bookmark| *bookmark == path) { + bookmarks.remove(idx); + } + } + } + + pub fn bookmark_paths(&self) -> HashMap { + let mut ret = HashMap::new(); + + if let Some(bookmarks) = &self.bookmarks { + for bookmark in bookmarks { + if let Ok(path) = PathBuf::from(bookmark).canonicalize() { + let name = if let Some(true) = self.display_full_path { + Some(path.display().to_string()) + } else { + path.file_name() + .expect("should end with a directory") + .to_string() + .ok() + }; + + if let Some(name) = name { + ret.insert(name, path); + } + } + } + } + + ret + } } #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/main.rs b/src/main.rs index 31789d5..632d5b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ +use std::{collections::HashMap, path::PathBuf}; + use clap::Parser; use error_stack::{Report, ResultExt}; +use git2::Repository; use tms::{ cli::{Cli, SubCommandGiven}, dirty_paths::DirtyUtf8Path, @@ -32,6 +35,8 @@ fn main() -> Result<()> { SubCommandGiven::No(config) => config, // continue }; + let bookmarks = config.bookmark_paths(); + // Find repositories and present them with the fuzzy finder let repos = find_repos( config.search_dirs().change_context(TmsError::ConfigError)?, @@ -41,8 +46,12 @@ fn main() -> Result<()> { config.recursive_submodules, )?; - let repo_name = if let Some(str) = get_single_selection( - &repos.list(), + let mut dirs = repos.list(); + + dirs.append(&mut bookmarks.keys().map(|b| b.to_string()).collect()); + + let selected_str = if let Some(str) = get_single_selection( + &dirs, Preview::None, config.picker_colors, config.shortcuts, @@ -53,9 +62,21 @@ fn main() -> Result<()> { return Ok(()); }; - let found_repo = repos - .find_repo(&repo_name) - .expect("The internal representation of the selected repository should be present"); + if let Some(found_repo) = repos.find_repo(&selected_str) { + switch_to_repo_session(selected_str, found_repo, &tmux, config.display_full_path)?; + } else { + switch_to_bookmark_session(selected_str, &tmux, bookmarks)?; + } + + Ok(()) +} + +fn switch_to_repo_session( + selected_str: String, + found_repo: &Repository, + tmux: &Tmux, + display_full_path: Option, +) -> Result<()> { let path = if found_repo.is_bare() { found_repo.path().to_string()? } else { @@ -66,22 +87,43 @@ fn main() -> Result<()> { .change_context(TmsError::IoError)? .to_string()? }; - let repo_short_name = (if config.display_full_path == Some(true) { - std::path::PathBuf::from(&repo_name) + let repo_short_name = (if display_full_path == Some(true) { + std::path::PathBuf::from(&selected_str) .file_name() .expect("None of the paths here should terminate in `..`") .to_string()? } else { - repo_name + selected_str }) .replace('.', "_"); - if !session_exists(&repo_short_name, &tmux) { + if !session_exists(&repo_short_name, tmux) { tmux.new_session(Some(&repo_short_name), Some(&path)); - set_up_tmux_env(found_repo, &repo_short_name, &tmux)?; + set_up_tmux_env(found_repo, &repo_short_name, tmux)?; } switch_to_session(&repo_short_name, &tmux); Ok(()) } + +fn switch_to_bookmark_session( + selected_str: String, + tmux: &Tmux, + bookmarks: HashMap, +) -> Result<()> { + let path = &bookmarks[&selected_str]; + let session_name = path + .file_name() + .expect("Bookmarks should not end in `..`") + .to_string()? + .replace('.', "_"); + + if !session_exists(&session_name, &tmux) { + tmux.new_session(Some(&session_name), path.to_str()); + } + + switch_to_session(&session_name, &tmux); + + Ok(()) +} diff --git a/tests/cli.rs b/tests/cli.rs index abdb2ba..fe606e7 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -60,6 +60,7 @@ fn tms_config() -> anyhow::Result<()> { prompt_color: Some(picker_prompt_color.clone()), }), shortcuts: None, + bookmarks: None, }; let mut tms = Command::cargo_bin("tms")?;