tms/
cli.rs

1use std::{
2    collections::HashMap,
3    env::current_dir,
4    fs::canonicalize,
5    path::{Path, PathBuf},
6};
7
8use crate::{
9    configs::{Config, SearchDirectory, SessionSortOrderConfig},
10    dirty_paths::DirtyUtf8Path,
11    execute_command, get_single_selection,
12    picker::Preview,
13    session::{create_sessions, SessionContainer},
14    tmux::Tmux,
15    Result, TmsError,
16};
17use clap::{Args, Command, CommandFactory, Parser, Subcommand};
18use clap_complete::{generate, Generator, Shell};
19use error_stack::ResultExt;
20use git2::{build::RepoBuilder, FetchOptions, RemoteCallbacks, Repository};
21use ratatui::style::Color;
22
23#[derive(Debug, Parser)]
24#[command(author, version)]
25///Scan for all git folders in specified directorires, select one and open it as a new tmux session
26pub struct Cli {
27    #[arg(long = "generate", value_enum)]
28    generator: Option<Shell>,
29    #[command(subcommand)]
30    command: Option<CliCommand>,
31}
32
33#[derive(Debug, Subcommand)]
34pub enum CliCommand {
35    #[command(arg_required_else_help = true)]
36    /// Configure the defaults for search paths and excluded directories
37    Config(Box<ConfigCommand>),
38    /// Initialize tmux with the default sessions
39    Start,
40    /// Display other sessions with a fuzzy finder and a preview window
41    Switch,
42    /// Display the current session's windows with a fuzzy finder and a preview window
43    Windows,
44    /// Kill the current tmux session and jump to another
45    Kill,
46    /// Show running tmux sessions with asterisk on the current session
47    Sessions,
48    #[command(arg_required_else_help = true)]
49    /// Rename the active session and the working directory
50    Rename(RenameCommand),
51    /// Creates new worktree windows for the selected session
52    Refresh(RefreshCommand),
53    /// Clone repository and create a new session for it
54    CloneRepo(CloneRepoCommand),
55    /// Initialize empty repository
56    InitRepo(InitRepoCommand),
57    /// Bookmark a directory so it is available to select along with the Git repositories
58    Bookmark(BookmarkCommand),
59}
60
61#[derive(Debug, Args)]
62pub struct ConfigCommand {
63    #[arg(short = 'p', long = "paths", value_name = "search paths", num_args = 1..)]
64    /// The paths to search through. Shell like expansions such as '~' are supported
65    search_paths: Option<Vec<String>>,
66    #[arg(short = 's', long = "session", value_name = "default session")]
67    /// The default session to switch to (if available) when killing another session
68    default_session: Option<String>,
69    #[arg(long = "excluded", value_name = "excluded dirs", num_args = 1..)]
70    /// As many directory names as desired to not be searched over
71    excluded_dirs: Option<Vec<String>>,
72    #[arg(long = "remove", value_name = "remove dir", num_args = 1..)]
73    /// As many directory names to be removed from exclusion list
74    remove_dir: Option<Vec<String>>,
75    #[arg(long = "full-path", value_name = "true | false")]
76    /// Use the full path when displaying directories
77    display_full_path: Option<bool>,
78    #[arg(long, value_name = "true | false")]
79    /// Also show initialized submodules
80    search_submodules: Option<bool>,
81    #[arg(long, value_name = "true | false")]
82    /// Search submodules for submodules
83    recursive_submodules: Option<bool>,
84    #[arg(long, value_name = "true | false")]
85    ///Only include sessions from search paths in the switcher
86    switch_filter_unknown: Option<bool>,
87    #[arg(long, short = 'd', value_name = "max depth", num_args = 1..)]
88    /// The maximum depth to traverse when searching for repositories in search paths, length
89    /// should match the number of search paths if specified (defaults to 10)
90    max_depths: Option<Vec<usize>>,
91    #[arg(long, value_name = "#rrggbb")]
92    /// Background color of the highlighted item in the picker
93    picker_highlight_color: Option<Color>,
94    #[arg(long, value_name = "#rrggbb")]
95    /// Text color of the hightlighted item in the picker
96    picker_highlight_text_color: Option<Color>,
97    #[arg(long, value_name = "#rrggbb")]
98    /// Color of the borders between widgets in the picker
99    picker_border_color: Option<Color>,
100    #[arg(long, value_name = "#rrggbb")]
101    /// Color of the item count in the picker
102    picker_info_color: Option<Color>,
103    #[arg(long, value_name = "#rrggbb")]
104    /// Color of the prompt in the picker
105    picker_prompt_color: Option<Color>,
106    #[arg(long, value_name = "Alphabetical | LastAttached")]
107    /// Set the sort order of the sessions in the switch command
108    session_sort_order: Option<SessionSortOrderConfig>,
109}
110
111#[derive(Debug, Args)]
112pub struct RenameCommand {
113    /// The new session's name
114    name: String,
115}
116
117#[derive(Debug, Args)]
118pub struct RefreshCommand {
119    /// The session's name. If not provided gets current session
120    name: Option<String>,
121}
122
123#[derive(Debug, Args)]
124pub struct CloneRepoCommand {
125    /// Git repository to clone
126    repository: String,
127}
128
129#[derive(Debug, Args)]
130pub struct InitRepoCommand {
131    /// Name of the repository to initialize
132    repository: String,
133}
134
135#[derive(Debug, Args)]
136pub struct BookmarkCommand {
137    #[arg(long, short)]
138    /// Delete instead of add a bookmark
139    delete: bool,
140    /// Path to bookmark, if left empty bookmark the current directory.
141    path: Option<String>,
142}
143
144impl Cli {
145    pub fn handle_sub_commands(&self, tmux: &Tmux) -> Result<SubCommandGiven> {
146        if let Some(generator) = self.generator {
147            let mut cmd = Cli::command();
148            print_completions(generator, &mut cmd);
149            return Ok(SubCommandGiven::Yes);
150        }
151
152        // Get the configuration from the config file
153        let config = Config::new().change_context(TmsError::ConfigError)?;
154
155        match &self.command {
156            Some(CliCommand::Start) => {
157                start_command(config, tmux)?;
158                Ok(SubCommandGiven::Yes)
159            }
160
161            Some(CliCommand::Switch) => {
162                switch_command(config, tmux)?;
163                Ok(SubCommandGiven::Yes)
164            }
165
166            Some(CliCommand::Windows) => {
167                windows_command(&config, tmux)?;
168                Ok(SubCommandGiven::Yes)
169            }
170            // Handle the config subcommand
171            Some(CliCommand::Config(args)) => {
172                config_command(args, config)?;
173                Ok(SubCommandGiven::Yes)
174            }
175
176            // The kill subcommand will kill the current session and switch to another one
177            Some(CliCommand::Kill) => {
178                kill_subcommand(config, tmux)?;
179                Ok(SubCommandGiven::Yes)
180            }
181
182            // The sessions subcommand will print the sessions with an asterisk over the current
183            // session
184            Some(CliCommand::Sessions) => {
185                sessions_subcommand(tmux)?;
186                Ok(SubCommandGiven::Yes)
187            }
188
189            // Rename the active session and the working directory
190            // rename
191            Some(CliCommand::Rename(args)) => {
192                rename_subcommand(args, tmux)?;
193                Ok(SubCommandGiven::Yes)
194            }
195            Some(CliCommand::Refresh(args)) => {
196                refresh_command(args, tmux)?;
197                Ok(SubCommandGiven::Yes)
198            }
199
200            Some(CliCommand::CloneRepo(args)) => {
201                clone_repo_command(args, config, tmux)?;
202                Ok(SubCommandGiven::Yes)
203            }
204
205            Some(CliCommand::InitRepo(args)) => {
206                init_repo_command(args, config, tmux)?;
207                Ok(SubCommandGiven::Yes)
208            }
209
210            Some(CliCommand::Bookmark(args)) => {
211                bookmark_command(args, config)?;
212                Ok(SubCommandGiven::Yes)
213            }
214
215            None => Ok(SubCommandGiven::No(config.into())),
216        }
217    }
218}
219
220fn start_command(config: Config, tmux: &Tmux) -> Result<()> {
221    if let Some(sessions) = &config.sessions {
222        for session in sessions {
223            let session_path = session
224                .path
225                .as_ref()
226                .map(shellexpand::full)
227                .transpose()
228                .change_context(TmsError::IoError)?;
229
230            tmux.new_session(session.name.as_deref(), session_path.as_deref());
231
232            if let Some(windows) = &session.windows {
233                for window in windows {
234                    let window_path = window
235                        .path
236                        .as_ref()
237                        .map(shellexpand::full)
238                        .transpose()
239                        .change_context(TmsError::IoError)?;
240
241                    tmux.new_window(window.name.as_deref(), window_path.as_deref(), None);
242
243                    if let Some(window_command) = &window.command {
244                        tmux.send_keys(window_command, None);
245                    }
246                }
247                tmux.kill_window(":1");
248            }
249        }
250        tmux.attach_session(None, None);
251    } else {
252        tmux.tmux();
253    }
254
255    Ok(())
256}
257
258fn switch_command(config: Config, tmux: &Tmux) -> Result<()> {
259    let sessions = tmux
260        .list_sessions("'#{?session_attached,,#{session_name}#,#{session_last_attached}}'")
261        .replace('\'', "")
262        .replace("\n\n", "\n");
263
264    let mut sessions: Vec<(&str, &str)> = sessions
265        .trim()
266        .split('\n')
267        .filter_map(|s| s.split_once(','))
268        .collect();
269
270    if let Some(SessionSortOrderConfig::LastAttached) = config.session_sort_order {
271        sessions.sort_by(|a, b| b.1.cmp(a.1));
272    }
273
274    let mut sessions: Vec<String> = sessions.into_iter().map(|s| s.0.to_string()).collect();
275    if let Some(true) = config.switch_filter_unknown {
276        let configured = create_sessions(&config)?;
277
278        sessions = sessions
279            .into_iter()
280            .filter(|session| configured.find_session(session).is_some())
281            .collect::<Vec<String>>();
282    }
283
284    if let Some(target_session) =
285        get_single_selection(&sessions, Preview::SessionPane, &config, tmux)?
286    {
287        tmux.switch_client(&target_session.replace('.', "_"));
288    }
289
290    Ok(())
291}
292
293fn windows_command(config: &Config, tmux: &Tmux) -> Result<()> {
294    let windows = tmux.list_windows("'#{?window_attached,,#{window_id} #{window_name}}'", None);
295
296    let windows: Vec<String> = windows
297        .replace('\'', "")
298        .replace("\n\n", "\n")
299        .trim()
300        .split('\n')
301        .map(|s| s.to_string())
302        .collect();
303
304    if let Some(target_window) = get_single_selection(&windows, Preview::WindowPane, config, tmux)?
305    {
306        if let Some((windex, _)) = target_window.split_once(' ') {
307            tmux.select_window(windex);
308        }
309    }
310    Ok(())
311}
312
313fn config_command(args: &ConfigCommand, mut config: Config) -> Result<()> {
314    let max_depths = args.max_depths.clone().unwrap_or_default();
315    config.search_dirs = match &args.search_paths {
316        Some(paths) => Some(
317            paths
318                .iter()
319                .zip(max_depths.into_iter().chain(std::iter::repeat(10)))
320                .map(|(path, depth)| {
321                    let path = if path.ends_with('/') {
322                        let mut modified_path = path.clone();
323                        modified_path.pop();
324                        modified_path
325                    } else {
326                        path.clone()
327                    };
328                    shellexpand::full(&path)
329                        .map(|val| (val.to_string(), depth))
330                        .change_context(TmsError::IoError)
331                })
332                .collect::<Result<Vec<(String, usize)>>>()?
333                .iter()
334                .map(|(path, depth)| {
335                    canonicalize(path)
336                        .map(|val| SearchDirectory::new(val, *depth))
337                        .change_context(TmsError::IoError)
338                })
339                .collect::<Result<Vec<SearchDirectory>>>()?,
340        ),
341        None => config.search_dirs,
342    };
343
344    if let Some(default_session) = args
345        .default_session
346        .clone()
347        .map(|val| val.replace('.', "_"))
348    {
349        config.default_session = Some(default_session);
350    }
351
352    if let Some(display) = args.display_full_path {
353        config.display_full_path = Some(display.to_owned());
354    }
355
356    if let Some(submodules) = args.search_submodules {
357        config.search_submodules = Some(submodules.to_owned());
358    }
359
360    if let Some(submodules) = args.recursive_submodules {
361        config.recursive_submodules = Some(submodules.to_owned());
362    }
363
364    if let Some(switch_filter_unknown) = args.switch_filter_unknown {
365        config.switch_filter_unknown = Some(switch_filter_unknown.to_owned());
366    }
367
368    if let Some(dirs) = &args.excluded_dirs {
369        let current_excluded = config.excluded_dirs;
370        match current_excluded {
371            Some(mut excl_dirs) => {
372                excl_dirs.extend(dirs.iter().map(|str| str.to_string()));
373                config.excluded_dirs = Some(excl_dirs)
374            }
375            None => {
376                config.excluded_dirs = Some(dirs.iter().map(|str| str.to_string()).collect());
377            }
378        }
379    }
380    if let Some(dirs) = &args.remove_dir {
381        let current_excluded = config.excluded_dirs;
382        match current_excluded {
383            Some(mut excl_dirs) => {
384                dirs.iter().for_each(|dir| excl_dirs.retain(|x| x != dir));
385                config.excluded_dirs = Some(excl_dirs);
386            }
387            None => todo!(),
388        }
389    }
390
391    if let Some(color) = &args.picker_highlight_color {
392        let mut picker_colors = config.picker_colors.unwrap_or_default();
393        picker_colors.highlight_color = Some(*color);
394        config.picker_colors = Some(picker_colors);
395    }
396    if let Some(color) = &args.picker_highlight_text_color {
397        let mut picker_colors = config.picker_colors.unwrap_or_default();
398        picker_colors.highlight_text_color = Some(*color);
399        config.picker_colors = Some(picker_colors);
400    }
401    if let Some(color) = &args.picker_border_color {
402        let mut picker_colors = config.picker_colors.unwrap_or_default();
403        picker_colors.border_color = Some(*color);
404        config.picker_colors = Some(picker_colors);
405    }
406    if let Some(color) = &args.picker_info_color {
407        let mut picker_colors = config.picker_colors.unwrap_or_default();
408        picker_colors.info_color = Some(*color);
409        config.picker_colors = Some(picker_colors);
410    }
411    if let Some(color) = &args.picker_prompt_color {
412        let mut picker_colors = config.picker_colors.unwrap_or_default();
413        picker_colors.prompt_color = Some(*color);
414        config.picker_colors = Some(picker_colors);
415    }
416
417    if let Some(order) = &args.session_sort_order {
418        config.session_sort_order = Some(order.to_owned());
419    }
420
421    config.save().change_context(TmsError::ConfigError)?;
422    println!("Configuration has been stored");
423    Ok(())
424}
425
426fn kill_subcommand(config: Config, tmux: &Tmux) -> Result<()> {
427    let mut current_session = tmux.display_message("'#S'");
428    current_session.retain(|x| x != '\'' && x != '\n');
429
430    let sessions = tmux
431        .list_sessions("'#{?session_attached,,#{session_name}#,#{session_last_attached}}'")
432        .replace('\'', "")
433        .replace("\n\n", "\n");
434
435    let mut sessions: Vec<(&str, &str)> = sessions
436        .trim()
437        .split('\n')
438        .filter_map(|s| s.split_once(','))
439        .collect();
440
441    if let Some(SessionSortOrderConfig::LastAttached) = config.session_sort_order {
442        sessions.sort_by(|a, b| b.1.cmp(a.1));
443    }
444
445    let to_session = if config.default_session.is_some()
446        && sessions
447            .iter()
448            .any(|session| session.0 == config.default_session.as_deref().unwrap())
449        && current_session != config.default_session.as_deref().unwrap()
450    {
451        config.default_session.as_deref()
452    } else {
453        sessions.first().map(|s| s.0)
454    };
455    if let Some(to_session) = to_session {
456        tmux.switch_client(to_session);
457    }
458    tmux.kill_session(&current_session);
459
460    Ok(())
461}
462
463fn sessions_subcommand(tmux: &Tmux) -> Result<()> {
464    let mut current_session = tmux.display_message("'#S'");
465    current_session.retain(|x| x != '\'' && x != '\n');
466    let current_session_star = format!("{current_session}*");
467
468    let sessions = tmux
469        .list_sessions("#S")
470        .split('\n')
471        .map(String::from)
472        .collect::<Vec<String>>();
473
474    let mut new_string = String::new();
475
476    for session in &sessions {
477        if session == &current_session {
478            new_string.push_str(&current_session_star);
479        } else {
480            new_string.push_str(session);
481        }
482        new_string.push(' ')
483    }
484    println!("{new_string}");
485    std::thread::sleep(std::time::Duration::from_millis(100));
486    tmux.refresh_client();
487
488    Ok(())
489}
490
491fn rename_subcommand(args: &RenameCommand, tmux: &Tmux) -> Result<()> {
492    let new_session_name = &args.name;
493
494    let current_session = tmux.display_message("'#S'");
495    let current_session = current_session.trim();
496
497    let panes = tmux.list_windows(
498        "'#{window_index}.#{pane_index},#{pane_current_command},#{pane_current_path}'",
499        None,
500    );
501
502    let mut paneid_to_pane_deatils: HashMap<String, HashMap<String, String>> = HashMap::new();
503    let all_panes: Vec<String> = panes
504        .trim()
505        .split('\n')
506        .map(|window| {
507            let mut _window: Vec<&str> = window.split(',').collect();
508
509            let pane_index = _window[0];
510            let pane_details: HashMap<String, String> = HashMap::from([
511                (String::from("command"), _window[1].to_string()),
512                (String::from("cwd"), _window[2].to_string()),
513            ]);
514
515            paneid_to_pane_deatils.insert(pane_index.to_string(), pane_details);
516
517            _window[0].to_string()
518        })
519        .collect();
520
521    let first_pane_details = &paneid_to_pane_deatils[all_panes.first().unwrap()];
522
523    let new_session_path: String =
524        String::from(&first_pane_details["cwd"]).replace(current_session, new_session_name);
525
526    let move_command_args: Vec<String> =
527        [first_pane_details["cwd"].clone(), new_session_path.clone()].to_vec();
528    execute_command("mv", move_command_args);
529
530    for pane_index in all_panes.iter() {
531        let pane_details = &paneid_to_pane_deatils[pane_index];
532
533        let old_path = &pane_details["cwd"];
534        let new_path = old_path.replace(current_session, new_session_name);
535
536        let change_dir_cmd = format!("\"cd {new_path}\"");
537        tmux.send_keys(&change_dir_cmd, Some(pane_index));
538    }
539
540    tmux.rename_session(new_session_name);
541    tmux.attach_session(None, Some(&new_session_path));
542
543    Ok(())
544}
545
546fn refresh_command(args: &RefreshCommand, tmux: &Tmux) -> Result<()> {
547    let session_name = args
548        .name
549        .clone()
550        .unwrap_or(tmux.display_message("'#S'"))
551        .trim()
552        .replace('\'', "");
553    // For each window there should be the branch names
554    let session_path = tmux
555        .display_message("'#{session_path}'")
556        .trim()
557        .replace('\'', "");
558
559    let existing_window_names: Vec<_> = tmux
560        .list_windows("'#{window_name}'", Some(&session_name))
561        .lines()
562        .map(|line| line.replace('\'', ""))
563        .collect();
564
565    if let Ok(repository) = Repository::open(&session_path) {
566        let mut num_worktree_windows = 0;
567        if let Ok(worktrees) = repository.worktrees() {
568            for worktree_name in worktrees.iter().flatten() {
569                let worktree = repository
570                    .find_worktree(worktree_name)
571                    .change_context(TmsError::GitError)?;
572                if existing_window_names.contains(&String::from(worktree_name)) {
573                    num_worktree_windows += 1;
574                    continue;
575                }
576                if !worktree.is_prunable(None).unwrap_or_default() {
577                    num_worktree_windows += 1;
578                    // prunable worktrees can have an invalid path so skip that
579                    tmux.new_window(
580                        Some(worktree_name),
581                        Some(&worktree.path().to_string()?),
582                        Some(&session_name),
583                    );
584                }
585            }
586        }
587        //check if a window is needed for non worktree
588        if !repository.is_bare() {
589            let count_current_windows = tmux
590                .list_windows("'#{window_name}'", Some(&session_name))
591                .lines()
592                .count();
593            if count_current_windows <= num_worktree_windows {
594                tmux.new_window(None, Some(&session_path), Some(&session_name));
595            }
596        }
597    }
598
599    Ok(())
600}
601
602fn pick_search_path(config: &Config, tmux: &Tmux) -> Result<Option<PathBuf>> {
603    let search_dirs = config
604        .search_dirs
605        .as_ref()
606        .ok_or(TmsError::ConfigError)
607        .attach_printable("No search path configured")?
608        .iter()
609        .map(|dir| dir.path.to_string())
610        .filter_map(|path| path.ok())
611        .collect::<Vec<String>>();
612
613    let path = if search_dirs.len() > 1 {
614        get_single_selection(&search_dirs, Preview::Directory, config, tmux)?
615    } else {
616        let first = search_dirs
617            .first()
618            .ok_or(TmsError::ConfigError)
619            .attach_printable("No search path configured")?;
620        Some(first.clone())
621    };
622
623    let expanded = path
624        .as_ref()
625        .map(|path| shellexpand::full(path).change_context(TmsError::IoError))
626        .transpose()?
627        .map(|path| PathBuf::from(path.as_ref()));
628    Ok(expanded)
629}
630
631fn clone_repo_command(args: &CloneRepoCommand, config: Config, tmux: &Tmux) -> Result<()> {
632    let Some(mut path) = pick_search_path(&config, tmux)? else {
633        return Ok(());
634    };
635
636    let (_, repo_name) = args
637        .repository
638        .rsplit_once('/')
639        .expect("Repository path contains '/'");
640    let repo_name = repo_name.trim_end_matches(".git");
641    path.push(repo_name);
642
643    let repo = git_clone(&args.repository, &path)?;
644
645    let mut session_name = repo_name.to_string();
646
647    if tmux.session_exists(&session_name) {
648        session_name = format!(
649            "{}/{}",
650            path.parent()
651                .unwrap()
652                .file_name()
653                .expect("The file name doesn't end in `..`")
654                .to_string()?,
655            session_name
656        );
657    }
658
659    tmux.new_session(Some(&session_name), Some(&path.display().to_string()));
660    tmux.set_up_tmux_env(&repo, &session_name)?;
661    tmux.switch_to_session(&session_name);
662
663    Ok(())
664}
665
666fn git_clone(repo: &str, target: &Path) -> Result<Repository> {
667    let mut callbacks = RemoteCallbacks::new();
668    callbacks.credentials(git_credentials_callback);
669    let mut fo = FetchOptions::new();
670    fo.remote_callbacks(callbacks);
671    let mut builder = RepoBuilder::new();
672    builder.fetch_options(fo);
673
674    builder
675        .clone(repo, target)
676        .change_context(TmsError::GitError)
677}
678
679fn git_credentials_callback(
680    user: &str,
681    user_from_url: Option<&str>,
682    _cred: git2::CredentialType,
683) -> std::result::Result<git2::Cred, git2::Error> {
684    let user = match user_from_url {
685        Some(user) => user,
686        None => user,
687    };
688
689    git2::Cred::ssh_key_from_agent(user)
690}
691
692fn init_repo_command(args: &InitRepoCommand, config: Config, tmux: &Tmux) -> Result<()> {
693    let Some(mut path) = pick_search_path(&config, tmux)? else {
694        return Ok(());
695    };
696    path.push(&args.repository);
697
698    let repo = Repository::init(&path).change_context(TmsError::GitError)?;
699
700    let mut session_name = args.repository.to_string();
701
702    if tmux.session_exists(&session_name) {
703        session_name = format!(
704            "{}/{}",
705            path.parent()
706                .unwrap()
707                .file_name()
708                .expect("The file name doesn't end in `..`")
709                .to_string()?,
710            session_name
711        );
712    }
713
714    tmux.new_session(Some(&session_name), Some(&path.display().to_string()));
715    tmux.set_up_tmux_env(&repo, &session_name)?;
716    tmux.switch_to_session(&session_name);
717
718    Ok(())
719}
720
721fn bookmark_command(args: &BookmarkCommand, mut config: Config) -> Result<()> {
722    let path = if let Some(path) = &args.path {
723        path.to_owned()
724    } else {
725        current_dir()
726            .change_context(TmsError::IoError)?
727            .to_string()
728            .change_context(TmsError::IoError)?
729    };
730
731    if !args.delete {
732        config.add_bookmark(path);
733    } else {
734        config.delete_bookmark(path);
735    }
736
737    config.save().change_context(TmsError::ConfigError)?;
738
739    Ok(())
740}
741
742fn print_completions<G: Generator>(gen: G, cmd: &mut Command) {
743    let name = if let Ok(exe) = std::env::current_exe() {
744        if let Some(exe) = exe.file_name() {
745            exe.to_string_lossy().to_string()
746        } else {
747            cmd.get_name().to_string()
748        }
749    } else {
750        cmd.get_name().to_string()
751    };
752    generate(gen, cmd, name, &mut std::io::stdout());
753}
754
755pub enum SubCommandGiven {
756    Yes,
757    No(Box<Config>),
758}