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 index: Option<usize>,
21 #[command(subcommand)]
22 cmd: Option<MarksSubCommand>,
23}
24
25#[derive(Debug, Subcommand)]
26pub enum MarksSubCommand {
27 List,
29 Set(MarksSetCommand),
31 Open(MarksOpenCommand),
33 Delete(MarksDeleteCommand),
35}
36
37#[derive(Debug, Args)]
38pub struct MarksSetCommand {
39 index: Option<usize>,
41 #[arg(long, short)]
42 path: Option<String>,
44}
45
46#[derive(Debug, Args)]
47pub struct MarksOpenCommand {
48 #[arg(add = ArgValueCandidates::new(get_completion_candidates))]
49 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: Option<usize>,
59 #[arg(long, short)]
60 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}