Skip to main content

tms/
marks.rs

1use std::{env::current_dir, path::PathBuf};
2
3use clap::{Args, Subcommand};
4use clap_complete::{ArgValueCandidates, CompletionCandidate};
5use error_stack::ResultExt;
6
7use crate::{
8    configs::Config,
9    dirty_paths::DirtyUtf8Path,
10    error::{Result, TmsError},
11    session::Session,
12    tmux::Tmux,
13};
14
15#[derive(Debug, Args)]
16#[clap(args_conflicts_with_subcommands = true)]
17pub struct MarksCommand {
18    #[arg(add  = ArgValueCandidates::new(get_completion_candidates))]
19    /// The index of the mark to open
20    index: Option<usize>,
21    #[command(subcommand)]
22    cmd: Option<MarksSubCommand>,
23}
24
25#[derive(Debug, Subcommand)]
26pub enum MarksSubCommand {
27    /// List all marks
28    List,
29    /// Add a session mark
30    Set(MarksSetCommand),
31    /// Open the session at index
32    Open(MarksOpenCommand),
33    /// Delete marks
34    Delete(MarksDeleteCommand),
35}
36
37#[derive(Debug, Args)]
38pub struct MarksSetCommand {
39    /// Index of mark to set, if empty will append after the last item
40    index: Option<usize>,
41    #[arg(long, short)]
42    /// Path to project directory, if empty will use the current directory
43    path: Option<String>,
44}
45
46#[derive(Debug, Args)]
47pub struct MarksOpenCommand {
48    #[arg(add  = ArgValueCandidates::new(get_completion_candidates))]
49    /// The index of the mark to open
50    index: usize,
51}
52
53#[derive(Debug, Args)]
54#[group(required = true, multiple = false)]
55pub struct MarksDeleteCommand {
56    #[arg(add  = ArgValueCandidates::new(get_completion_candidates))]
57    /// Index of mark to delete
58    index: Option<usize>,
59    #[arg(long, short)]
60    /// Delete all items
61    all: bool,
62}
63
64fn get_completion_candidates() -> Vec<CompletionCandidate> {
65    let config = Config::new().unwrap_or_default();
66    let marks = get_marks(&config).unwrap_or_default();
67    marks
68        .iter()
69        .map(|(index, session)| {
70            CompletionCandidate::new(index.to_string()).help(Some(session.name.clone().into()))
71        })
72        .collect::<Vec<_>>()
73}
74
75pub fn marks_command(args: &MarksCommand, config: Config, tmux: &Tmux) -> Result<()> {
76    match (&args.cmd, args.index) {
77        (None, None) => list(config),
78        (_, Some(index)) => open(index, &config, tmux),
79        (Some(MarksSubCommand::List), _) => list(config),
80        (Some(MarksSubCommand::Set(args)), _) => set(args, config),
81        (Some(MarksSubCommand::Open(args)), _) => open(args.index, &config, tmux),
82        (Some(MarksSubCommand::Delete(args)), _) => delete(args, config),
83    }
84}
85
86fn list(config: Config) -> Result<()> {
87    let items = get_marks(&config).unwrap_or_default();
88    items.iter().for_each(|(index, session)| {
89        println!("{index}: {} ({})", session.name, session.path().display());
90    });
91    Ok(())
92}
93
94fn set(args: &MarksSetCommand, mut config: Config) -> Result<()> {
95    let index = args.index.unwrap_or_else(|| {
96        let items = get_marks(&config).unwrap_or_default();
97        items
98            .iter()
99            .enumerate()
100            .take_while(|(i, (index, _))| i == index)
101            .count()
102    });
103
104    let path = if let Some(path) = &args.path {
105        path.to_owned()
106    } else {
107        current_dir()
108            .change_context(TmsError::IoError)?
109            .to_string()
110            .change_context(TmsError::IoError)?
111    };
112    config.add_mark(path, index);
113    config.save().change_context(TmsError::ConfigError)
114}
115
116fn get_marks(config: &Config) -> Option<Vec<(usize, Session)>> {
117    let items = config.marks.as_ref()?;
118    let mut items = items
119        .iter()
120        .filter_map(|(index, item)| {
121            let index = index.parse::<usize>().ok();
122            let session = path_to_session(item).ok();
123            index.zip(session)
124        })
125        .collect::<Vec<_>>();
126    items.sort_by(|(a, _), (b, _)| a.cmp(b));
127    Some(items)
128}
129
130fn open(index: usize, config: &Config, tmux: &Tmux) -> Result<()> {
131    let path = config
132        .marks
133        .as_ref()
134        .and_then(|items| items.get(&index.to_string()))
135        .ok_or(TmsError::ConfigError)
136        .attach_printable(format!("Session with index {} not found in marks", index))?;
137
138    let session = path_to_session(path)?;
139
140    session.switch_to(tmux, config)
141}
142
143fn path_to_session(path: &String) -> Result<Session> {
144    let path = shellexpand::full(path)
145        .change_context(TmsError::IoError)
146        .and_then(|p| {
147            PathBuf::from(p.to_string())
148                .canonicalize()
149                .change_context(TmsError::IoError)
150        })?;
151
152    let session_name = path
153        .file_name()
154        .expect("The file name doesn't end in `..`")
155        .to_string()?;
156    let session = Session::new(session_name, crate::session::SessionType::Bookmark(path));
157    Ok(session)
158}
159
160fn delete(args: &MarksDeleteCommand, mut config: Config) -> Result<()> {
161    if args.all {
162        config.clear_marks();
163    } else if let Some(index) = args.index {
164        config.delete_mark(index);
165    } else {
166        unreachable!("One of the args is required by clap");
167    }
168    config.save().change_context(TmsError::ConfigError)
169}