quinjet 0.0.46

A fast, live, keyboard-first Git source-control interface for the terminal
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use super::*;

pub(crate) fn dispatch() -> Result<Launch> {
    completion::auto_install();
    let cli = Cli::parse();
    if let Some(target) = cli.remote.as_deref() {
        let (terminal, implicit_terminal, folder) = match cli.command.as_ref() {
            None => (true, true, cli.repository.as_path()),
            Some(Verb::Tui(args)) => (true, false, terminal_path(&args.path, &cli.repository)),
            Some(_) => (false, false, cli.repository.as_path()),
        };
        return remote::run(target, terminal, implicit_terminal, folder).map(Launch::Finished);
    }
    let mut out = Emitter::new(cli.json);
    let verb = match cli.command {
        None => {
            return Ok(Launch::Terminal(Box::new(TerminalOptions {
                path: cli.repository,
                no_mouse: false,
                webhook_listen: None,
                theme: ThemeName::default(),
                appearance: AppearanceChoice::default(),
                pull_request: cli.pull_request,
            })));
        }
        Some(Verb::Tui(args)) => {
            return Ok(Launch::Terminal(Box::new(TerminalOptions {
                path: terminal_path(&args.path, &cli.repository).to_path_buf(),
                no_mouse: args.no_mouse,
                webhook_listen: args.webhook_listen,
                theme: args.theme,
                appearance: args.appearance,
                pull_request: args.pull_request.or(cli.pull_request),
            })));
        }
        Some(Verb::Completions(args)) => {
            return completions(&out, &args).map(Launch::Finished);
        }
        Some(Verb::Man(args)) => return manual(&out, &args).map(Launch::Finished),
        Some(Verb::Capabilities) => return capabilities(&out).map(Launch::Finished),
        Some(Verb::Remote { command }) => {
            return remote::manage(&out, command).map(Launch::Finished);
        }
        Some(Verb::Update(args)) => {
            out.start_progress("Checking for updates")?;
            let result = update::run(&out, args.check);
            out.finish_progress();
            return result.map(Launch::Finished);
        }
        Some(other) => other,
    };
    if cli.pull_request.is_some() {
        return Err(Failure::new(
            EXIT_FAILURE,
            "--pr only applies to the terminal interface; use `quinjet pr view <number>` instead",
        )
        .into());
    }
    if let Some(label) = verb.progress_label() {
        out.start_progress(label)?;
    }
    let result = (|| {
        let repository = Repository::discover(&cli.repository)?;
        let mut session = Session::new(repository);
        run(&mut session, &out, verb)
    })();
    out.finish_progress();
    result.map(Launch::Finished)
}

fn terminal_path<'a>(positional: &'a Path, repository: &'a Path) -> &'a Path {
    if positional == Path::new(".") {
        repository
    } else {
        positional
    }
}

pub(super) fn completions(out: &Emitter, args: &CompletionsArgs) -> Result<u8> {
    let shell = args
        .shell
        .or_else(completion::detected_shell)
        .context("could not detect a supported shell; name one explicitly")?;
    if args.install {
        let paths = if args.automatic {
            completion::maintain(shell)?
        } else {
            completion::install(shell)?
        };
        let paths: Vec<String> = paths
            .iter()
            .map(|path| path.display().to_string())
            .collect();
        return out
            .emit(
                &CompletionInstallation {
                    shell: shell.to_string(),
                    shortcut: "q",
                    paths: &paths,
                },
                || {
                    let mut text = format!("Installed {shell} shell integration\n");
                    for path in &paths {
                        text.push_str("  ");
                        text.push_str(path);
                        text.push('\n');
                    }
                    text
                },
            )
            .map(|()| 0);
    }
    let script = completion::script(shell)?;
    out.emit(
        &CompletionScript {
            shell: shell.to_string(),
            script: &script,
        },
        || script.clone(),
    )?;
    Ok(0)
}

pub(super) fn manual(out: &Emitter, args: &ManArgs) -> Result<u8> {
    let mut command = Cli::command();
    command.build();
    let Some(directory) = args.dir.as_deref() else {
        let page = render_page(&command, PROGRAM)?;
        let text = String::from_utf8(page).context("the manual page was not valid UTF-8")?;
        return out
            .emit(&ManualPage { page: &text }, || text.clone())
            .map(|()| 0);
    };
    fs::create_dir_all(directory)
        .with_context(|| format!("failed to create {}", directory.display()))?;
    let mut written = Vec::new();
    write_pages(&command, PROGRAM, directory, &mut written)?;
    out.emit(&ManualPages { pages: &written }, || {
        let mut text = format!("Wrote {} pages to {}\n", written.len(), directory.display());
        for page in &written {
            text.push_str("  ");
            text.push_str(page);
            text.push('\n');
        }
        text
    })?;
    Ok(0)
}

pub(super) fn capabilities(out: &Emitter) -> Result<u8> {
    let mut command = Cli::command();
    command.build();
    let mut commands = Vec::new();
    collect_capabilities(&command, &[], &mut commands);
    let document = CapabilityDocument {
        schema_version: 1,
        version: env!("CARGO_PKG_VERSION"),
        output_modes: ["text", "json"],
        commands,
    };
    out.emit(&document, || render_capabilities(&document))?;
    Ok(0)
}

pub(super) fn collect_capabilities(
    command: &clap::Command,
    parent: &[String],
    commands: &mut Vec<CommandCapability>,
) {
    let mut path = parent.to_vec();
    path.push(command.get_name().to_owned());
    let arguments = command
        .get_arguments()
        .filter(|argument| !argument.is_hide_set())
        .filter(|argument| argument.get_id() != "help" && argument.get_id() != "version")
        .map(|argument| {
            let (min_values, max_values) = argument.get_num_args().map_or((0, Some(0)), |range| {
                let maximum = range.max_values();
                (
                    range.min_values(),
                    (maximum != usize::MAX).then_some(maximum),
                )
            });
            ArgumentCapability {
                id: argument.get_id().to_string(),
                help: argument.get_help().map(ToString::to_string),
                short: argument.get_short(),
                long: argument.get_long().map(str::to_owned),
                positional: argument.is_positional(),
                required: argument.is_required_set(),
                action: argument_action(argument.get_action()),
                min_values,
                max_values,
                value_names: argument
                    .get_value_names()
                    .map(|names| names.iter().map(ToString::to_string).collect())
                    .unwrap_or_default(),
                possible_values: argument
                    .get_possible_values()
                    .iter()
                    .map(|value| value.get_name().to_owned())
                    .collect(),
                default_values: argument
                    .get_default_values()
                    .iter()
                    .map(|value| value.to_string_lossy().into_owned())
                    .collect(),
            }
        })
        .collect();
    let usage = command.clone().render_usage().to_string();
    let groups = command
        .get_groups()
        .map(|group| {
            let mut configured = group.clone();
            ArgumentGroupCapability {
                id: configured.get_id().to_string(),
                arguments: configured.get_args().map(ToString::to_string).collect(),
                required: configured.is_required_set(),
                multiple: configured.is_multiple(),
            }
        })
        .collect();
    commands.push(CommandCapability {
        path: path.join(" "),
        about: command.get_about().map(ToString::to_string),
        usage,
        arguments,
        groups,
        subcommands: command
            .get_subcommands()
            .filter(|child| child.get_name() != "help")
            .map(|child| child.get_name().to_owned())
            .collect(),
    });
    for child in command
        .get_subcommands()
        .filter(|child| child.get_name() != "help")
    {
        collect_capabilities(child, &path, commands);
    }
}

pub(super) const fn argument_action(action: &clap::ArgAction) -> &'static str {
    match action {
        clap::ArgAction::Set => "set",
        clap::ArgAction::Append => "append",
        clap::ArgAction::SetTrue => "set_true",
        clap::ArgAction::SetFalse => "set_false",
        clap::ArgAction::Count => "count",
        clap::ArgAction::Help => "help",
        clap::ArgAction::HelpShort => "help_short",
        clap::ArgAction::HelpLong => "help_long",
        clap::ArgAction::Version => "version",
        _ => "other",
    }
}

pub(super) fn render_capabilities(document: &CapabilityDocument) -> String {
    let mut text = format!(
        "Quinjet {} command capabilities (schema {})\n\n",
        document.version, document.schema_version
    );
    for command in &document.commands {
        text.push_str(&command.path);
        if let Some(about) = &command.about {
            text.push_str("  ");
            text.push_str(about);
        }
        text.push('\n');
    }
    text.push_str("\nUse --json for arguments, values, and command relationships.\n");
    text
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct CapabilityDocument {
    schema_version: u8,
    version: &'static str,
    output_modes: [&'static str; 2],
    commands: Vec<CommandCapability>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct CommandCapability {
    path: String,
    about: Option<String>,
    usage: String,
    arguments: Vec<ArgumentCapability>,
    groups: Vec<ArgumentGroupCapability>,
    subcommands: Vec<String>,
}

#[derive(Serialize)]
pub(super) struct ArgumentGroupCapability {
    id: String,
    arguments: Vec<String>,
    required: bool,
    multiple: bool,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ArgumentCapability {
    id: String,
    help: Option<String>,
    short: Option<char>,
    long: Option<String>,
    positional: bool,
    required: bool,
    action: &'static str,
    min_values: usize,
    max_values: Option<usize>,
    value_names: Vec<String>,
    possible_values: Vec<String>,
    default_values: Vec<String>,
}

pub(super) fn render_page(command: &clap::Command, name: &str) -> Result<Vec<u8>> {
    let mut page = Vec::new();
    Man::new(command.clone().display_name(name.to_owned()))
        .title(name.to_uppercase())
        .render(&mut page)
        .with_context(|| format!("failed to render the manual page for {name}"))?;
    Ok(page)
}

pub(super) fn write_pages(
    command: &clap::Command,
    name: &str,
    directory: &Path,
    written: &mut Vec<String>,
) -> Result<()> {
    let file = directory.join(format!("{name}.1"));
    fs::write(&file, render_page(command, name)?)
        .with_context(|| format!("failed to write {}", file.display()))?;
    written.push(file.display().to_string());
    for child in command
        .get_subcommands()
        .filter(|child| child.get_name() != "help")
    {
        write_pages(
            child,
            &format!("{name}-{}", child.get_name()),
            directory,
            written,
        )?;
    }
    Ok(())
}

#[derive(Serialize)]
pub(super) struct CompletionScript<'a> {
    shell: String,
    script: &'a str,
}

#[derive(Serialize)]
pub(super) struct CompletionInstallation<'a> {
    shell: String,
    shortcut: &'static str,
    paths: &'a [String],
}

#[derive(Serialize)]
pub(super) struct ManualPage<'a> {
    page: &'a str,
}

#[derive(Serialize)]
pub(super) struct ManualPages<'a> {
    pages: &'a [String],
}