Something went wrong. Try again.
atproto git client
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251//! The grouped command list `atgc --help` prints.//!//! clap renders top-level subcommands as one flat `Commands:` column and has//! no way to split it: `help_heading` is an argument setting, and//! `subcommand_help_heading` renames the single section rather than adding a//! second one. A dozen verbs in one alphabetical run read as a dozen//! unrelated things, when a reader arrives with a task and wants the two or//! three lines it could be in.//!//! So the section is rendered here instead: the same two-space, one-column//! shape clap would have printed, split under headings, handed to the root//! command as its `before_help`, and put back where `Commands:` was by a help//! template that drops clap's own list. Only the grouping is written down —//! every name, its column and its line of help still come out of the command//! tree, so a command whose `about` changes says the new thing here without//! anyone touching this file, and the one thing that can go stale (which//! group a command is in) is what this module's tests hold.
use clap::builder::{StyledStr, Styles};use clap::{Command, CommandFactory, FromArgMatches};use std::fmt::Write as _;
/// The top-level commands, in the groups and the order `--help` shows them.////// Every visible subcommand belongs to exactly one group, `help` included —/// it is clap's own and is listed rather than left to trail the list/// unexplained. The order inside a group is not alphabetical: it is the order/// somebody meets the commands in, so `repo` comes before the pull requests/// that are opened against it.////// The cut is by subject, because a reader arrives with a task rather than/// with a taxonomy: what atgc is, who you are, what you are doing to a repo,/// and what to do when it does not work. That is also why `report` is in the/// troubleshooting group despite being the only command there that writes/// anything — it is where a `doctor` row and a `logs` line end up, and/// somebody looking for it is already in that group.////// `Other` holds the two commands no subject fits, and they resist for/// opposite reasons. `completion` is run once, at install, and never again;/// `api` is authenticated access to the whole lexicon and is the most/// powerful thing here. What they share is only that no heading above is/// true of them, which is what the word means. Forcing either into a group/// that nearly fits would cost a reader more than the honest label does.const GROUPS: &[(&str, &[&str])] = &[ ("Meta", &["about", "agent", "help"]), ("Account management", &["auth", "key"]), ( "Repository operations", &["repo", "browse", "search", "issue", "pr", "stack"], ), ( "Checking and troubleshooting", &["doctor", "logs", "report"], ), ("Other", &["api", "completion"]),];
/// Parse argv, with the grouped list standing in for clap's `Commands:`.////// This is `Cli::parse()` with two settings applied to the command first,/// and it has to be spelled out rather than derived because both of them/// are computed from the very command they are set on.pub fn parse() -> crate::Cli { let mut cmd = crate::Cli::command(); // `help` is added while building, so build before reading the tree: a // list assembled from an unbuilt command is one command short, and the // missing one is the command a lost user reaches for. Parsing builds // again, which clap makes a no-op. cmd.build(); let list = command_list(&cmd); let template = template(cmd.get_styles()); let matches = cmd.before_help(list).help_template(template).get_matches(); crate::Cli::from_arg_matches(&matches).unwrap_or_else(|err| err.exit())}
/// clap's default help, with `{before-help}` moved to where the command/// list belongs and the list itself left out.////// `{options}` writes the options without their heading — the default/// template gets that heading from `{all-args}`, which would also write the/// flat `Commands:` section this exists to replace. So the heading is/// written here, in the command's own header style, to keep it looking like/// every other heading clap prints.fn template(styles: &Styles) -> String { let header = styles.get_header(); format!( "{{about-with-newline}}\n\ {{usage-heading}} {{usage}}\n\ \n\ {{before-help}}{header}Options:{header:#}\n\ {{options}}{{after-help}}" )}
/// Render the visible subcommands as headed groups.////// The filter is mechanical, not a policy: nothing in this tree is hidden./// It used to be — a top-level `login` alias was kept out of the listing so/// that shell history would not break — and that alias is gone, along with/// the `auth log` one that `main`'s tests pin as a parse failure. atgc does/// not keep hidden aliases; a spelling that changes changes, and the error/// says where the command went.fn command_list(cmd: &Command) -> StyledStr { let styles = cmd.get_styles(); let header = styles.get_header(); let literal = styles.get_literal();
let listed: Vec<&Command> = cmd.get_subcommands().filter(|s| !s.is_hide_set()).collect(); // One column across the whole list rather than one per group: the // groups are read as a single list that happens to have headings in it, // and three different left margins would say otherwise. let width = listed .iter() .map(|s| s.get_name().len()) .max() .unwrap_or_default();
let mut out = String::new(); for (heading, names) in GROUPS { if !out.is_empty() { out.push('\n'); } let _ = writeln!(out, "{header}{heading}:{header:#}"); for sub in names .iter() .filter_map(|name| listed.iter().find(|s| s.get_name() == *name)) { let name = sub.get_name(); let pad = " ".repeat(width - name.len()); let about = sub.get_about().map(|a| a.ansi().to_string()); let _ = writeln!( out, " {literal}{name}{literal:#}{pad} {}", about.unwrap_or_default() ); } } // clap follows `{before-help}` with a blank line of its own, so the // block ends on its last command rather than on a newline. out.pop(); out.into()}
#[cfg(test)]mod tests { use super::*;
/// The grouping covers the command tree exactly, in both directions. /// /// This is the whole cost of writing the groups down by hand: a command /// added to `Command` and not to a group would simply stop being listed, /// and nothing else in the build would notice — `--help` would print a /// list that quietly omits a verb, which is worse than the flat list /// this replaced. The other direction catches the rename that leaves a /// group naming a command that no longer exists. #[test] fn every_visible_command_is_in_exactly_one_group() { let mut cmd = crate::Cli::command(); cmd.build(); let visible: Vec<&str> = cmd .get_subcommands() .filter(|s| !s.is_hide_set()) .map(|s| s.get_name()) .collect(); let grouped: Vec<&str> = GROUPS .iter() .flat_map(|(_, names)| *names) .copied() .collect();
for name in &visible { assert_eq!( grouped.iter().filter(|g| *g == name).count(), 1, "`{name}` is in {} groups, not 1", grouped.iter().filter(|g| *g == name).count() ); } for name in &grouped { assert!( visible.contains(name), "the groups name `{name}`, which is not a visible command" ); } }
/// The rendered block is the list, headed and in group order. #[test] fn the_list_is_grouped_and_complete() { let mut cmd = crate::Cli::command(); cmd.build(); let rendered = command_list(&cmd).ansi().to_string(); // Styling is on in this rendering; the assertions read the text // around the escapes rather than trying to reproduce them. // // Every heading, in the order the const declares them: a group that // renders in the wrong place reads as a different taxonomy, and the // headings are the whole of what this module adds. let mut at = 0; for (heading, _) in GROUPS { let found = rendered .find(&format!("{heading}:")) .unwrap_or_else(|| panic!("no `{heading}:` heading in:\n{rendered}")); assert!(found >= at, "`{heading}` is out of order:\n{rendered}"); at = found; } for name in GROUPS.iter().flat_map(|(_, names)| *names) { assert!(rendered.contains(name), "`{name}` is not in the list"); } // Logging in is `atgc auth login`, and there is no top-level alias // for it. Subcommands are not lifted to the top level here: the // grouping above is what makes a long list readable, and a command // that appears in two places undoes that for the one reader who // needs it most. assert!( !rendered.contains("alias for `auth login`"), "a top-level `login` is back in the list:\n{rendered}" ); assert!(rendered.contains("Work with pull requests")); }
/// The help renders with the list in it and clap's flat section gone. #[test] fn help_shows_the_groups_instead_of_a_flat_list() { let mut cmd = crate::Cli::command(); cmd.build(); let list = command_list(&cmd); let template = template(cmd.get_styles()); let help = cmd .before_help(list) .help_template(template) .render_help() .ansi() .to_string(); assert!(!help.contains("Commands:"), "the flat list is still there"); assert!(help.contains("Repository operations:")); // The parts of the default help the template has to carry itself. assert!(help.contains("Usage:")); assert!(help.contains("Options:")); assert!(help.contains("--account")); // `{after-help}` is in the template, and the root command puts the // project's page there. A template that dropped it would take the // one address in `--help` with it, silently. assert!( help.trim_end().ends_with(env!("CARGO_PKG_HOMEPAGE")), "the help does not end on the site:\n{help}" ); assert!(help.contains("https://atgc.codes"), "no site in:\n{help}"); }}