codeberg-cli 0.5.5

CLI Tool for codeberg similar to gh and glab
Documentation
use forgejo_api::structs::StateType;
use miette::{Context, IntoDiagnostic};

/// Given a list of `items`, this function allows the user to interactively select a subset of the
/// given list.
///
/// - To guide the user, you have to provide a `prompt`
/// - The `is_selected` closure can help to preselect a certain set of items that pass the
///   predicate
/// - Some types are not natively displayable. In these cases you can decide what to show to the
///   user for each item with `f_display`
pub fn multi_fuzzy_select_with_key<T>(
    items: &[T],
    prompt: impl AsRef<str>,
    is_selected: impl Fn(&T) -> bool,
    f_display: impl Fn(&T) -> String,
) -> miette::Result<Vec<&T>> {
    // collect pre-selected items
    let already_selected = items
        .iter()
        .enumerate()
        .filter(|(_, elem)| is_selected(elem))
        .map(|(idx, _)| idx)
        .collect::<Vec<_>>();

    // collect what's shown to the user
    let displayed_items = items.iter().map(f_display).collect::<Vec<_>>();

    // do the interactive selection
    let selected_indices = inquire::MultiSelect::new(prompt.as_ref(), displayed_items)
        .with_default(&already_selected)
        .raw_prompt()
        .into_diagnostic()
        .context("There's nothing to select from")?;

    // get the items for the selected indices
    let selected_items = selected_indices
        .into_iter()
        .map(|raw| raw.index)
        .filter_map(|idx| items.get(idx))
        .collect::<Vec<_>>();

    Ok(selected_items)
}

/// Basically the same as [`fuzzy_select_with_key_with_default`] without a default value
pub fn fuzzy_select_with_key<T>(
    items: &[T],
    prompt: impl AsRef<str>,
    f_display: impl Fn(&T) -> String,
) -> miette::Result<&T> {
    fuzzy_select_with_key_with_default(items, prompt, f_display, None)
}

/// Given a list of `items`, this function allows the user to interactively select a *exactly one*
/// item of the given list.
///
/// - To guide the user, you have to provide a `prompt`
/// - Some types are not natively displayable. In these cases you can decide what to show to the
///   user for each item with `f_display`
/// - The `default_index` optional index value can pre-select one item
pub fn fuzzy_select_with_key_with_default<T>(
    items: &[T],
    prompt: impl AsRef<str>,
    f_display: impl Fn(&T) -> String,
    default_index: Option<usize>,
) -> miette::Result<&T> {
    // return `None` if we have nothing to select from
    if items.is_empty() {
        miette::bail!("Nothing to select from. Aborting.")
    }

    let displayed_items = items.iter().map(f_display).collect::<Vec<_>>();

    // build standard dialogue
    let mut dialogue = inquire::Select::new(prompt.as_ref(), displayed_items);

    // optionally add default selection
    if let Some(index) = default_index {
        dialogue = dialogue.with_starting_cursor(index);
    }

    // select an item by key
    let selected_index = dialogue.raw_prompt().into_diagnostic()?.index;

    Ok(&items[selected_index])
}

/// Common confimation prompt (y/n) which maps
///
/// - `y` -> `true`
/// - `n` -> `false`
pub fn confirm_with_prompt(prompt: impl AsRef<str>) -> miette::Result<bool> {
    inquire::Confirm::new(prompt.as_ref())
        .with_help_message("(y/n)?")
        .prompt()
        .into_diagnostic()
}

/// Select a [`forgejo_api::structs::StateType`] and serialize it the right way into a [`String`]
/// ... It has to be lowercase, otherwise the API will not detect it
pub fn select_state(current_state: Option<StateType>) -> miette::Result<String> {
    fuzzy_select_with_key(
        &[StateType::Open, StateType::Closed],
        "Select the desired state",
        |f| format!("{f:?}"),
    )
    .or(current_state
        .as_ref()
        .context("Not even current issue state available, aborting!"))
    .and_then(|state| {
        serde_json::to_string(&state)
            .into_diagnostic()
            .context("Couldn't convert given `state` properly for the API call!")
    })
    .map(|state| {
        state
            .trim_start_matches('"')
            .trim_end_matches('"')
            .to_string()
    })
}