1use std::{
2 collections::HashMap,
3 env::current_dir,
4 fs::canonicalize,
5 path::{Path, PathBuf},
6 process::{Command, Stdio},
7};
8
9use crate::{
10 configs::{
11 CloneRepoSwitchConfig, Config, ConfigExport, SearchDirectory, SessionSortOrderConfig,
12 },
13 dirty_paths::DirtyUtf8Path,
14 execute_command, get_single_selection,
15 marks::{marks_command, MarksCommand},
16 picker::Preview,
17 repos::RepoProvider,
18 session::{create_sessions, SessionContainer},
19 tmux::Tmux,
20 Result, TmsError,
21};
22use clap::{Args, Parser, Subcommand};
23use clap_complete::{ArgValueCandidates, CompletionCandidate};
24use error_stack::ResultExt;
25use ratatui::style::Color;
26
27#[derive(Debug, Parser)]
28#[command(author, version)]
29pub struct Cli {
31 #[command(subcommand)]
32 command: Option<CliCommand>,
33}
34
35#[derive(Debug, Subcommand)]
36pub enum CliCommand {
37 #[command(arg_required_else_help = true)]
38 Config(Box<ConfigCommand>),
40 Start,
42 Switch,
44 Windows,
46 Kill,
48 Sessions,
50 #[command(arg_required_else_help = true)]
51 Rename(RenameCommand),
53 Refresh(RefreshCommand),
55 CloneRepo(CloneRepoCommand),
57 InitRepo(InitRepoCommand),
59 Bookmark(BookmarkCommand),
61 OpenSession(OpenSessionCommand),
63 Marks(MarksCommand),
65}
66
67#[derive(Debug, Args)]
68#[clap(args_conflicts_with_subcommands = true)]
69pub struct ConfigCommand {
70 #[command(flatten)]
71 args: ConfigArgs,
72 #[command(subcommand)]
73 subcommand: Option<ConfigSubCommand>,
74}
75
76#[derive(Debug, Subcommand)]
77pub enum ConfigSubCommand {
78 List(ConfigSubCommandArgs),
80}
81
82#[derive(Debug, Args)]
83pub struct ConfigSubCommandArgs {
84 #[arg(short, long)]
85 defaults: bool,
87}
88
89#[derive(Debug, Args)]
90pub struct ConfigArgs {
91 #[arg(short = 'p', long = "paths", value_name = "search paths", num_args = 1..)]
92 search_paths: Option<Vec<String>>,
94 #[arg(short = 's', long = "session", value_name = "default session")]
95 default_session: Option<String>,
97 #[arg(long = "excluded", value_name = "excluded dirs", num_args = 1..)]
98 excluded_dirs: Option<Vec<String>>,
100 #[arg(long = "remove", value_name = "remove dir", num_args = 1..)]
101 remove_dir: Option<Vec<String>>,
103 #[arg(long = "full-path", value_name = "true | false")]
104 display_full_path: Option<bool>,
106 #[arg(long, value_name = "true | false")]
107 search_submodules: Option<bool>,
109 #[arg(long, value_name = "true | false")]
110 recursive_submodules: Option<bool>,
112 #[arg(long, value_name = "true | false")]
113 switch_filter_unknown: Option<bool>,
115 #[arg(long, short = 'd', value_name = "max depth", num_args = 1..)]
116 max_depths: Option<Vec<usize>>,
119 #[arg(long, value_name = "#rrggbb")]
120 picker_highlight_color: Option<Color>,
122 #[arg(long, value_name = "#rrggbb")]
123 picker_highlight_text_color: Option<Color>,
125 #[arg(long, value_name = "#rrggbb")]
126 picker_border_color: Option<Color>,
128 #[arg(long, value_name = "#rrggbb")]
129 picker_info_color: Option<Color>,
131 #[arg(long, value_name = "#rrggbb")]
132 picker_prompt_color: Option<Color>,
134 #[arg(long, value_name = "Alphabetical | LastAttached")]
135 session_sort_order: Option<SessionSortOrderConfig>,
137 #[arg(long, value_name = "Always | Never | Foreground", verbatim_doc_comment)]
138 clone_repo_switch: Option<CloneRepoSwitchConfig>,
144}
145
146#[derive(Debug, Args)]
147pub struct RenameCommand {
148 name: String,
150}
151
152#[derive(Debug, Args)]
153pub struct RefreshCommand {
154 name: Option<String>,
156}
157
158#[derive(Debug, Args)]
159pub struct CloneRepoCommand {
160 repository: String,
162 #[arg(long)]
163 path: Option<String>,
166 #[arg(long)]
167 name: Option<String>,
170}
171
172#[derive(Debug, Args)]
173pub struct InitRepoCommand {
174 repository: String,
176}
177
178#[derive(Debug, Args)]
179pub struct BookmarkCommand {
180 #[arg(long, short)]
181 delete: bool,
183 path: Option<String>,
185}
186
187#[derive(Debug, Args)]
188pub struct OpenSessionCommand {
189 #[arg(add = ArgValueCandidates::new(open_session_completion_candidates))]
190 session: Box<str>,
192}
193
194impl Cli {
195 pub fn handle_sub_commands(&self, tmux: &Tmux) -> Result<SubCommandGiven> {
196 let config = Config::new().change_context(TmsError::ConfigError)?;
198
199 match &self.command {
200 Some(CliCommand::Start) => {
201 start_command(config, tmux)?;
202 Ok(SubCommandGiven::Yes)
203 }
204
205 Some(CliCommand::Switch) => {
206 switch_command(config, tmux)?;
207 Ok(SubCommandGiven::Yes)
208 }
209
210 Some(CliCommand::Windows) => {
211 windows_command(&config, tmux)?;
212 Ok(SubCommandGiven::Yes)
213 }
214 Some(CliCommand::Config(args)) => {
216 config_command(args, config)?;
217 Ok(SubCommandGiven::Yes)
218 }
219
220 Some(CliCommand::Kill) => {
222 kill_subcommand(config, tmux)?;
223 Ok(SubCommandGiven::Yes)
224 }
225
226 Some(CliCommand::Sessions) => {
229 sessions_subcommand(tmux)?;
230 Ok(SubCommandGiven::Yes)
231 }
232
233 Some(CliCommand::Rename(args)) => {
236 rename_subcommand(args, tmux)?;
237 Ok(SubCommandGiven::Yes)
238 }
239 Some(CliCommand::Refresh(args)) => {
240 refresh_command(args, &config, tmux)?;
241 Ok(SubCommandGiven::Yes)
242 }
243
244 Some(CliCommand::CloneRepo(args)) => {
245 clone_repo_command(args, config, tmux)?;
246 Ok(SubCommandGiven::Yes)
247 }
248
249 Some(CliCommand::InitRepo(args)) => {
250 init_repo_command(args, config, tmux)?;
251 Ok(SubCommandGiven::Yes)
252 }
253
254 Some(CliCommand::Bookmark(args)) => {
255 bookmark_command(args, config)?;
256 Ok(SubCommandGiven::Yes)
257 }
258
259 Some(CliCommand::OpenSession(args)) => {
260 open_session_command(args, config, tmux)?;
261 Ok(SubCommandGiven::Yes)
262 }
263
264 Some(CliCommand::Marks(args)) => {
265 marks_command(args, config, tmux)?;
266 Ok(SubCommandGiven::Yes)
267 }
268
269 None => Ok(SubCommandGiven::No(config.into())),
270 }
271 }
272}
273
274fn start_command(config: Config, tmux: &Tmux) -> Result<()> {
275 if let Some(sessions) = &config.sessions {
276 for session in sessions {
277 let session_path = session
278 .path
279 .as_ref()
280 .map(shellexpand::full)
281 .transpose()
282 .change_context(TmsError::IoError)?;
283
284 tmux.new_session(session.name.as_deref(), session_path.as_deref());
285
286 if let Some(windows) = &session.windows {
287 for window in windows {
288 let window_path = window
289 .path
290 .as_ref()
291 .map(shellexpand::full)
292 .transpose()
293 .change_context(TmsError::IoError)?;
294
295 tmux.new_window(window.name.as_deref(), window_path.as_deref(), None);
296
297 if let Some(window_command) = &window.command {
298 tmux.send_keys(window_command, None);
299 }
300 }
301 tmux.kill_window(":1");
302 }
303 }
304 tmux.attach_session(None, None);
305 } else {
306 tmux.tmux();
307 }
308
309 Ok(())
310}
311
312fn switch_command(config: Config, tmux: &Tmux) -> Result<()> {
313 let sessions = tmux
314 .list_sessions("'#{?session_attached,,#{session_name}#,#{session_last_attached}}'")
315 .replace('\'', "")
316 .replace("\n\n", "\n");
317
318 let mut sessions: Vec<(&str, &str)> = sessions
319 .trim()
320 .split('\n')
321 .filter_map(|s| s.split_once(','))
322 .collect();
323
324 if let Some(SessionSortOrderConfig::LastAttached) = config.session_sort_order {
325 sessions.sort_by(|a, b| b.1.cmp(a.1));
326 }
327
328 let mut sessions: Vec<String> = sessions.into_iter().map(|s| s.0.to_string()).collect();
329 if let Some(true) = config.switch_filter_unknown {
330 let configured = create_sessions(&config)?;
331
332 sessions = sessions
333 .into_iter()
334 .filter(|session| configured.find_session(session).is_some())
335 .collect::<Vec<String>>();
336 }
337
338 if let Some(target_session) =
339 get_single_selection(&sessions, Some(Preview::SessionPane), &config, tmux)?
340 {
341 tmux.switch_client(&target_session.replace('.', "_"));
342 }
343
344 Ok(())
345}
346
347fn windows_command(config: &Config, tmux: &Tmux) -> Result<()> {
348 let windows = tmux.list_windows("'#{?window_attached,,#{window_id} #{window_name}}'", None);
349
350 let windows: Vec<String> = windows
351 .replace('\'', "")
352 .replace("\n\n", "\n")
353 .trim()
354 .split('\n')
355 .map(|s| s.to_string())
356 .collect();
357
358 if let Some(target_window) =
359 get_single_selection(&windows, Some(Preview::WindowPane), config, tmux)?
360 {
361 if let Some((windex, _)) = target_window.split_once(' ') {
362 tmux.select_window(windex);
363 }
364 }
365 Ok(())
366}
367
368fn config_command(cmd: &ConfigCommand, mut config: Config) -> Result<()> {
369 match &cmd.subcommand {
370 None => {}
371 Some(ConfigSubCommand::List(args)) => {
372 let config = if args.defaults {
373 Config::default()
374 } else {
375 config
376 };
377 let config = ConfigExport::from(config);
378 let toml_pretty =
379 toml::to_string_pretty(&config).change_context(TmsError::ConfigError)?;
380 println!("{}", toml_pretty);
381 return Ok(());
382 }
383 };
384 let args = &cmd.args;
385 let max_depths = args.max_depths.clone().unwrap_or_default();
386 config.search_dirs = match &args.search_paths {
387 Some(paths) => Some(
388 paths
389 .iter()
390 .zip(max_depths.into_iter().chain(std::iter::repeat(10)))
391 .map(|(path, depth)| {
392 let path = if path.ends_with('/') {
393 let mut modified_path = path.clone();
394 modified_path.pop();
395 modified_path
396 } else {
397 path.clone()
398 };
399 shellexpand::full(&path)
400 .map(|val| (val.to_string(), depth))
401 .change_context(TmsError::IoError)
402 })
403 .collect::<Result<Vec<(String, usize)>>>()?
404 .iter()
405 .map(|(path, depth)| {
406 canonicalize(path)
407 .map(|val| SearchDirectory::new(val, *depth))
408 .change_context(TmsError::IoError)
409 })
410 .collect::<Result<Vec<SearchDirectory>>>()?,
411 ),
412 None => config.search_dirs,
413 };
414
415 if let Some(default_session) = args
416 .default_session
417 .clone()
418 .map(|val| val.replace('.', "_"))
419 {
420 config.default_session = Some(default_session);
421 }
422
423 if let Some(display) = args.display_full_path {
424 config.display_full_path = Some(display.to_owned());
425 }
426
427 if let Some(submodules) = args.search_submodules {
428 config.search_submodules = Some(submodules.to_owned());
429 }
430
431 if let Some(submodules) = args.recursive_submodules {
432 config.recursive_submodules = Some(submodules.to_owned());
433 }
434
435 if let Some(switch_filter_unknown) = args.switch_filter_unknown {
436 config.switch_filter_unknown = Some(switch_filter_unknown.to_owned());
437 }
438
439 if let Some(dirs) = &args.excluded_dirs {
440 let current_excluded = config.excluded_dirs;
441 match current_excluded {
442 Some(mut excl_dirs) => {
443 excl_dirs.extend(dirs.iter().map(|str| str.to_string()));
444 config.excluded_dirs = Some(excl_dirs)
445 }
446 None => {
447 config.excluded_dirs = Some(dirs.iter().map(|str| str.to_string()).collect());
448 }
449 }
450 }
451 if let Some(dirs) = &args.remove_dir {
452 let current_excluded = config.excluded_dirs;
453 match current_excluded {
454 Some(mut excl_dirs) => {
455 dirs.iter().for_each(|dir| excl_dirs.retain(|x| x != dir));
456 config.excluded_dirs = Some(excl_dirs);
457 }
458 None => todo!(),
459 }
460 }
461
462 if let Some(color) = &args.picker_highlight_color {
463 let mut picker_colors = config.picker_colors.unwrap_or_default();
464 picker_colors.highlight_color = Some(*color);
465 config.picker_colors = Some(picker_colors);
466 }
467 if let Some(color) = &args.picker_highlight_text_color {
468 let mut picker_colors = config.picker_colors.unwrap_or_default();
469 picker_colors.highlight_text_color = Some(*color);
470 config.picker_colors = Some(picker_colors);
471 }
472 if let Some(color) = &args.picker_border_color {
473 let mut picker_colors = config.picker_colors.unwrap_or_default();
474 picker_colors.border_color = Some(*color);
475 config.picker_colors = Some(picker_colors);
476 }
477 if let Some(color) = &args.picker_info_color {
478 let mut picker_colors = config.picker_colors.unwrap_or_default();
479 picker_colors.info_color = Some(*color);
480 config.picker_colors = Some(picker_colors);
481 }
482 if let Some(color) = &args.picker_prompt_color {
483 let mut picker_colors = config.picker_colors.unwrap_or_default();
484 picker_colors.prompt_color = Some(*color);
485 config.picker_colors = Some(picker_colors);
486 }
487
488 if let Some(order) = &args.session_sort_order {
489 config.session_sort_order = Some(order.to_owned());
490 }
491
492 if let Some(switch) = &args.clone_repo_switch {
493 config.clone_repo_switch = Some(switch.to_owned());
494 }
495
496 config.save().change_context(TmsError::ConfigError)?;
497 println!("Configuration has been stored");
498 Ok(())
499}
500
501fn kill_subcommand(config: Config, tmux: &Tmux) -> Result<()> {
502 let mut current_session = tmux.display_message("'#S'");
503 current_session.retain(|x| x != '\'' && x != '\n');
504
505 let sessions = tmux
506 .list_sessions("'#{?session_attached,,#{session_name}#,#{session_last_attached}}'")
507 .replace('\'', "")
508 .replace("\n\n", "\n");
509
510 let mut sessions: Vec<(&str, &str)> = sessions
511 .trim()
512 .split('\n')
513 .filter_map(|s| s.split_once(','))
514 .collect();
515
516 if let Some(SessionSortOrderConfig::LastAttached) = config.session_sort_order {
517 sessions.sort_by(|a, b| b.1.cmp(a.1));
518 }
519
520 let to_session = if config.default_session.is_some()
521 && sessions
522 .iter()
523 .any(|session| session.0 == config.default_session.as_deref().unwrap())
524 && current_session != config.default_session.as_deref().unwrap()
525 {
526 config.default_session.as_deref()
527 } else {
528 sessions.first().map(|s| s.0)
529 };
530 if let Some(to_session) = to_session {
531 tmux.switch_client(to_session);
532 }
533 tmux.kill_session(¤t_session);
534
535 Ok(())
536}
537
538fn sessions_subcommand(tmux: &Tmux) -> Result<()> {
539 let mut current_session = tmux.display_message("'#S'");
540 current_session.retain(|x| x != '\'' && x != '\n');
541 let current_session_star = format!("{current_session}*");
542
543 let sessions = tmux
544 .list_sessions("#S")
545 .split('\n')
546 .map(String::from)
547 .collect::<Vec<String>>();
548
549 let mut new_string = String::new();
550
551 for session in &sessions {
552 if session == ¤t_session {
553 new_string.push_str(¤t_session_star);
554 } else {
555 new_string.push_str(session);
556 }
557 new_string.push(' ')
558 }
559 println!("{new_string}");
560 std::thread::sleep(std::time::Duration::from_millis(100));
561 tmux.refresh_client();
562
563 Ok(())
564}
565
566fn rename_subcommand(args: &RenameCommand, tmux: &Tmux) -> Result<()> {
567 let new_session_name = &args.name;
568
569 let current_session = tmux
570 .display_message("'#S'")
571 .trim()
572 .replace('\'', "")
573 .to_string();
574
575 let panes = tmux.list_windows(
576 "'#{window_index}.#{pane_index},#{pane_current_command},#{pane_current_path}'",
577 None,
578 );
579
580 let mut paneid_to_pane_deatils: HashMap<String, HashMap<String, String>> = HashMap::new();
581 let all_panes: Vec<String> = panes
582 .trim()
583 .split('\n')
584 .map(|window| {
585 let mut _window: Vec<&str> = window.split(',').collect();
586
587 let pane_index = _window[0].replace('\'', "");
588 let pane_details: HashMap<String, String> = HashMap::from([
589 (String::from("command"), _window[1].to_string()),
590 (
591 String::from("cwd"),
592 _window[2].to_string().replace('\'', ""),
593 ),
594 ]);
595
596 paneid_to_pane_deatils.insert(pane_index.to_string(), pane_details);
597
598 pane_index.to_string()
599 })
600 .collect();
601
602 let first_pane_details = &paneid_to_pane_deatils[all_panes.first().unwrap()];
603
604 let new_session_path: String =
605 String::from(&first_pane_details["cwd"]).replace(¤t_session, new_session_name);
606
607 let move_command_args: Vec<String> =
608 [first_pane_details["cwd"].clone(), new_session_path.clone()].to_vec();
609 execute_command("mv", move_command_args);
610
611 for pane_index in all_panes.iter() {
612 let pane_details = &paneid_to_pane_deatils[pane_index];
613
614 let old_path = &pane_details["cwd"];
615 let new_path = old_path.replace(¤t_session, new_session_name);
616
617 let change_dir_cmd = format!("cd {new_path}");
618 tmux.send_keys(&change_dir_cmd, Some(pane_index));
619 }
620
621 tmux.rename_session(new_session_name);
622 tmux.attach_session(None, Some(&new_session_path));
623
624 Ok(())
625}
626
627fn refresh_command(args: &RefreshCommand, config: &Config, tmux: &Tmux) -> Result<()> {
628 let session_name = args
629 .name
630 .clone()
631 .unwrap_or(tmux.display_message("'#S'"))
632 .trim()
633 .replace('\'', "");
634 let session_path = tmux
636 .display_message("'#{session_path}'")
637 .trim()
638 .replace('\'', "");
639
640 let existing_window_names: Vec<_> = tmux
641 .list_windows("'#{window_name}'", Some(&session_name))
642 .lines()
643 .map(|line| line.replace('\'', ""))
644 .collect();
645
646 if let Ok(repository) = RepoProvider::open(Path::new(&session_path), config) {
647 let mut num_worktree_windows = 0;
648 if let Ok(worktrees) = repository.worktrees() {
649 for worktree in worktrees.iter() {
650 let worktree_name = worktree.name();
651 if existing_window_names.contains(&worktree_name) {
652 num_worktree_windows += 1;
653 continue;
654 }
655 if worktree.is_prunable() {
656 continue;
658 }
659 num_worktree_windows += 1;
660 tmux.new_window(
661 Some(&worktree_name),
662 Some(&worktree.path()?.to_string()?),
663 Some(&session_name),
664 );
665 }
666 }
667 if !repository.is_bare() {
669 let count_current_windows = tmux
670 .list_windows("'#{window_name}'", Some(&session_name))
671 .lines()
672 .count();
673 if count_current_windows <= num_worktree_windows {
674 tmux.new_window(None, Some(&session_path), Some(&session_name));
675 }
676 }
677 }
678
679 Ok(())
680}
681
682fn pick_search_path(config: &Config, tmux: &Tmux) -> Result<Option<PathBuf>> {
683 let search_dirs = config
684 .search_dirs
685 .as_ref()
686 .ok_or(TmsError::ConfigError)
687 .attach_printable("No search path configured")?
688 .iter()
689 .filter(|dir| dir.depth > 0)
690 .map(|dir| dir.path.to_string())
691 .filter_map(|path| path.ok())
692 .collect::<Vec<String>>();
693
694 let path = if search_dirs.len() > 1 {
695 get_single_selection(&search_dirs, Some(Preview::Directory), config, tmux)?
696 } else {
697 let first = search_dirs
698 .first()
699 .ok_or(TmsError::ConfigError)
700 .attach_printable("No search path configured")?;
701 Some(first.clone())
702 };
703
704 let expanded = path
705 .as_ref()
706 .map(|path| shellexpand::full(path).change_context(TmsError::IoError))
707 .transpose()?
708 .map(|path| PathBuf::from(path.as_ref()));
709 Ok(expanded)
710}
711
712fn clone_repo_command(args: &CloneRepoCommand, config: Config, tmux: &Tmux) -> Result<()> {
713 let Some(mut path) = (if let Some(p) = &args.path {
714 Some(
715 PathBuf::from(p)
716 .canonicalize()
717 .change_context(TmsError::IoError)?,
718 )
719 } else {
720 pick_search_path(&config, tmux)?
721 }) else {
722 return Ok(());
723 };
724
725 let repo_name = args.name.as_deref().unwrap_or_else(|| {
726 let (_, name) = args
727 .repository
728 .trim_end_matches('/')
729 .rsplit_once('/')
730 .expect("Repository path contains '/'");
731 name.trim_end_matches(".git")
732 });
733 path.push(repo_name);
734
735 let previous_session = tmux.current_session("#{session_name}");
736
737 let repo = RepoProvider::open(git_clone(&args.repository, &path)?, &config)?;
738
739 let mut session_name = repo_name.to_string();
740
741 let switch = match config.clone_repo_switch.unwrap_or_default() {
742 CloneRepoSwitchConfig::Always => true,
743 CloneRepoSwitchConfig::Never => false,
744 CloneRepoSwitchConfig::Foreground => {
745 let active_session = tmux.current_session("#{session_name}");
746 previous_session == active_session
747 }
748 };
749
750 if tmux.session_exists(&session_name) {
751 session_name = format!(
752 "{}/{}",
753 path.parent()
754 .unwrap()
755 .file_name()
756 .expect("The file name doesn't end in `..`")
757 .to_string()?,
758 session_name
759 );
760 }
761
762 tmux.new_session(Some(&session_name), Some(&path.display().to_string()));
763 tmux.set_up_tmux_env(&repo, &session_name)?;
764 if switch {
765 tmux.switch_to_session(&session_name);
766 }
767
768 Ok(())
769}
770
771fn git_clone<'a>(repo: &str, target: &'a Path) -> Result<&'a Path> {
772 std::fs::create_dir_all(target).change_context(TmsError::IoError)?;
773 let mut cmd = Command::new("git")
774 .current_dir(target.parent().ok_or(TmsError::IoError)?)
775 .args(["clone", repo, target.to_str().ok_or(TmsError::NonUtf8Path)?])
776 .stdout(Stdio::inherit())
777 .stderr(Stdio::inherit())
778 .spawn()
779 .change_context(TmsError::GitError)?;
780
781 cmd.wait().change_context(TmsError::GitError)?;
782 Ok(target)
783}
784
785fn init_repo_command(args: &InitRepoCommand, config: Config, tmux: &Tmux) -> Result<()> {
786 let Some(mut path) = pick_search_path(&config, tmux)? else {
787 return Ok(());
788 };
789 path.push(&args.repository);
790
791 let repo = gix::init(&path).change_context(TmsError::GitError)?;
792 let repo = RepoProvider::Git(Box::new(repo));
793
794 let mut session_name = args.repository.to_string();
795
796 if tmux.session_exists(&session_name) {
797 session_name = format!(
798 "{}/{}",
799 path.parent()
800 .unwrap()
801 .file_name()
802 .expect("The file name doesn't end in `..`")
803 .to_string()?,
804 session_name
805 );
806 }
807
808 tmux.new_session(Some(&session_name), Some(&path.display().to_string()));
809 tmux.set_up_tmux_env(&repo, &session_name)?;
810 tmux.switch_to_session(&session_name);
811
812 Ok(())
813}
814
815fn bookmark_command(args: &BookmarkCommand, mut config: Config) -> Result<()> {
816 let path = if let Some(path) = &args.path {
817 path.to_owned()
818 } else {
819 current_dir()
820 .change_context(TmsError::IoError)?
821 .to_string()
822 .change_context(TmsError::IoError)?
823 };
824
825 if !args.delete {
826 config.add_bookmark(path);
827 } else {
828 config.delete_bookmark(path);
829 }
830
831 config.save().change_context(TmsError::ConfigError)?;
832
833 Ok(())
834}
835
836fn open_session_command(args: &OpenSessionCommand, config: Config, tmux: &Tmux) -> Result<()> {
837 let sessions = create_sessions(&config)?;
838
839 if let Some(session) = sessions.find_session(&args.session) {
840 session.switch_to(tmux, &config)?;
841 Ok(())
842 } else {
843 Err(TmsError::SessionNotFound(args.session.to_string()).into())
844 }
845}
846
847fn open_session_completion_candidates() -> Vec<CompletionCandidate> {
848 Config::new()
849 .change_context(TmsError::ConfigError)
850 .and_then(|config| create_sessions(&config))
851 .map(|sessions| {
852 sessions
853 .list()
854 .iter()
855 .map(CompletionCandidate::new)
856 .collect::<Vec<_>>()
857 })
858 .unwrap_or_default()
859}
860
861pub enum SubCommandGiven {
862 Yes,
863 No(Box<Config>),
864}