macro_rules! out {
($($arg:tt)*) => {
$crate::emit(format_args!($($arg)*))
};
}
macro_rules! outln {
() => {
$crate::emit(format_args!("\n"))
};
($($arg:tt)*) => {
$crate::emit(format_args!("{}\n", format_args!($($arg)*)))
};
}
mod app;
mod commands;
mod git;
mod history;
mod tui;
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
use std::process::Command;
const AFTER: &str = concat!(
"\
Ways to run it (not subcommands):
slu <git command> real git, passed straight through (`slu commit -m \"fix\"`, `slu push`)
A passthrough is git itself, so the output and the exit code are git's own.
sluuz's own verbs print a table for people rather than data, except
`completions`, whose stdout is the script.
Run `slu <command> --help` for a command's details.",
"\n\n",
env!("CARGO_PKG_REPOSITORY"),
"\ncontributors: ",
env!("CARGO_PKG_AUTHORS"),
);
const LONG_VERSION: &str = concat!(
env!("CARGO_PKG_VERSION"),
"\n",
env!("CARGO_PKG_LICENSE"),
" ",
env!("CARGO_PKG_REPOSITORY"),
"\ncontributors: ",
env!("CARGO_PKG_AUTHORS"),
);
#[derive(Parser)]
#[command(
name = "sluuz",
bin_name = "slu",
version,
long_version = LONG_VERSION,
about,
after_help = AFTER,
arg_required_else_help = true
)]
struct Cli {
#[command(subcommand)]
command: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
#[command(verbatim_doc_comment)]
Search(commands::search::Args),
#[command(verbatim_doc_comment)]
Scan(commands::scan::Args),
#[command(verbatim_doc_comment)]
Repos(commands::repos::Args),
#[command(verbatim_doc_comment)]
Sync(commands::sync::Args),
#[command(verbatim_doc_comment)]
Tidy(commands::tidy::Args),
#[command(verbatim_doc_comment)]
Each(commands::each::Args),
#[command(verbatim_doc_comment)]
Trace(commands::trace::Args),
#[command(verbatim_doc_comment)]
Completions(commands::completions::Args),
#[command(name = "self", subcommand)]
Selfie(commands::selfcmd::Cmd),
#[command(verbatim_doc_comment)]
Iscan(commands::iscan::Args),
#[command(verbatim_doc_comment)]
Irepos(commands::irepos::Args),
#[command(verbatim_doc_comment)]
Ibranch(commands::ibranch::Args),
#[command(verbatim_doc_comment)]
Itag(commands::itag::Args),
#[command(verbatim_doc_comment)]
Iremote(commands::iremote::Args),
#[command(verbatim_doc_comment)]
Ilog(commands::ilog::Args),
#[command(verbatim_doc_comment)]
Istatus(commands::istatus::Args),
#[command(verbatim_doc_comment)]
Istash(commands::istash::Args),
#[command(external_subcommand)]
Git(Vec<String>),
}
fn main() {
let mut built = Cli::command();
built.build();
let template = help_template(&built);
let matches = Cli::command().help_template(template).get_matches();
let cli = match Cli::from_arg_matches(&matches) {
Ok(cli) => cli,
Err(e) => e.exit(),
};
match cli.command {
Cmd::Search(args) => commands::search::run(args),
Cmd::Scan(args) => commands::scan::run(args),
Cmd::Repos(args) => commands::repos::run(args),
Cmd::Sync(args) => commands::sync::run(args),
Cmd::Tidy(args) => commands::tidy::run(args),
Cmd::Each(args) => commands::each::run(args),
Cmd::Trace(args) => commands::trace::run(args),
Cmd::Ilog(args) => commands::ilog::run(args),
Cmd::Irepos(args) => commands::irepos::run(args),
Cmd::Ibranch(args) => commands::ibranch::run(args),
Cmd::Itag(args) => commands::itag::run(args),
Cmd::Iremote(args) => commands::iremote::run(args),
Cmd::Iscan(args) => commands::iscan::run(args),
Cmd::Istatus(args) => commands::istatus::run(args),
Cmd::Istash(args) => commands::istash::run(args),
Cmd::Selfie(cmd) => commands::selfcmd::run(cmd),
Cmd::Completions(args) => commands::completions::run(args),
Cmd::Git(args) => passthrough(&args),
}
}
fn help_template(cmd: &clap::Command) -> String {
let subs: Vec<&clap::Command> = cmd.get_subcommands().filter(|c| !c.is_hide_set()).collect();
let width = subs
.iter()
.map(|c| c.get_name().chars().count())
.max()
.unwrap_or(0);
let (tui, plain): (Vec<&&clap::Command>, Vec<&&clap::Command>) =
subs.iter().partition(|c| is_interactive(c));
let styles = cmd.get_styles();
let header = styles.get_header();
let literal = styles.get_literal();
let mut list = String::new();
for (heading, group) in [("Commands:", plain), ("Interactive (TUI):", tui)] {
list.push_str(&format!("{header}{heading}{header:#}\n"));
for c in group {
list.push_str(&entry(c, width, literal));
}
list.push('\n');
}
format!(
"{{about-with-newline}}\n{{usage-heading}} {{usage}}\n\n{list}{header}Options:{header:#}\n{{options}}{{after-help}}\n"
)
}
fn entry(cmd: &clap::Command, width: usize, literal: &clap::builder::styling::Style) -> String {
let about = cmd.get_about().map(ToString::to_string).unwrap_or_default();
let mut lines = about.lines();
let name = cmd.get_name();
let pad = " ".repeat(width - name.chars().count());
let mut row = format!(
" {literal}{name}{literal:#}{pad} {}\n",
lines.next().unwrap_or("")
);
for line in lines {
row.push_str(&format!(" {:<width$} {line}\n", ""));
}
row
}
fn is_interactive(cmd: &clap::Command) -> bool {
cmd.get_name().starts_with('i')
}
fn passthrough(args: &[String]) -> ! {
match Command::new("git").args(args).status() {
Ok(status) => std::process::exit(status.code().unwrap_or(1)),
Err(e) => {
eprintln!("slu: could not run git: {e}");
std::process::exit(127);
}
}
}
fn emit(args: std::fmt::Arguments) {
use std::io::Write;
match std::io::stdout().lock().write_fmt(args) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => std::process::exit(0),
Err(_) => {
eprintln!("error: could not write the output; is the disk it goes to full?");
std::process::exit(1);
}
}
}
#[cfg(test)]
mod tests {
use super::{Cli, help_template};
use clap::CommandFactory;
#[test]
fn every_interactive_command_is_listed_under_its_own_heading() {
let mut built = Cli::command();
built.build();
let rendered = help_template(&built);
let (plain, interactive) = rendered
.split_once("Interactive (TUI):")
.expect("the list is split into two groups");
let literal = built.get_styles().get_literal();
for sub in built.get_subcommands() {
let name = sub.get_name();
let row = format!("\n {literal}{name}{literal:#}");
let (half, group) = if name.starts_with('i') {
(interactive, "interactive")
} else {
(plain, "plain")
};
assert!(
half.contains(&row),
"`{name}` is missing from the {group} group"
);
}
}
}