Skip to main content

codeberg_cli/render/ui/
mod.rs

1use forgejo_api::structs::StateType;
2use miette::{Context, IntoDiagnostic};
3
4/// Given a list of `items`, this function allows the user to interactively select a subset of the
5/// given list.
6///
7/// - To guide the user, you have to provide a `prompt`
8/// - The `is_selected` closure can help to preselect a certain set of items that pass the
9///   predicate
10/// - Some types are not natively displayable. In these cases you can decide what to show to the
11///   user for each item with `f_display`
12pub fn multi_fuzzy_select_with_key<T>(
13    items: &[T],
14    prompt: impl AsRef<str>,
15    is_selected: impl Fn(&T) -> bool,
16    f_display: impl Fn(&T) -> String,
17) -> miette::Result<Vec<&T>> {
18    // collect pre-selected items
19    let already_selected = items
20        .iter()
21        .enumerate()
22        .filter(|(_, elem)| is_selected(elem))
23        .map(|(idx, _)| idx)
24        .collect::<Vec<_>>();
25
26    // collect what's shown to the user
27    let displayed_items = items.iter().map(f_display).collect::<Vec<_>>();
28
29    // do the interactive selection
30    let selected_indices = inquire::MultiSelect::new(prompt.as_ref(), displayed_items)
31        .with_default(&already_selected)
32        .raw_prompt()
33        .into_diagnostic()
34        .context("There's nothing to select from")?;
35
36    // get the items for the selected indices
37    let selected_items = selected_indices
38        .into_iter()
39        .map(|raw| raw.index)
40        .filter_map(|idx| items.get(idx))
41        .collect::<Vec<_>>();
42
43    Ok(selected_items)
44}
45
46/// Basically the same as [`fuzzy_select_with_key_with_default`] without a default value
47pub fn fuzzy_select_with_key<T>(
48    items: &[T],
49    prompt: impl AsRef<str>,
50    f_display: impl Fn(&T) -> String,
51) -> miette::Result<&T> {
52    fuzzy_select_with_key_with_default(items, prompt, f_display, None)
53}
54
55/// Given a list of `items`, this function allows the user to interactively select a *exactly one*
56/// item of the given list.
57///
58/// - To guide the user, you have to provide a `prompt`
59/// - Some types are not natively displayable. In these cases you can decide what to show to the
60///   user for each item with `f_display`
61/// - The `default_index` optional index value can pre-select one item
62pub fn fuzzy_select_with_key_with_default<T>(
63    items: &[T],
64    prompt: impl AsRef<str>,
65    f_display: impl Fn(&T) -> String,
66    default_index: Option<usize>,
67) -> miette::Result<&T> {
68    // return `None` if we have nothing to select from
69    if items.is_empty() {
70        miette::bail!("Nothing to select from. Aborting.")
71    }
72
73    let displayed_items = items.iter().map(f_display).collect::<Vec<_>>();
74
75    // build standard dialogue
76    let mut dialogue = inquire::Select::new(prompt.as_ref(), displayed_items);
77
78    // optionally add default selection
79    if let Some(index) = default_index {
80        dialogue = dialogue.with_starting_cursor(index);
81    }
82
83    // select an item by key
84    let selected_index = dialogue.raw_prompt().into_diagnostic()?.index;
85
86    Ok(&items[selected_index])
87}
88
89/// Common confimation prompt (y/n) which maps
90///
91/// - `y` -> `true`
92/// - `n` -> `false`
93pub fn confirm_with_prompt(prompt: impl AsRef<str>) -> miette::Result<bool> {
94    inquire::Confirm::new(prompt.as_ref())
95        .with_help_message("(y/n)?")
96        .prompt()
97        .into_diagnostic()
98}
99
100/// Select a [`forgejo_api::structs::StateType`] and serialize it the right way into a [`String`]
101/// ... It has to be lowercase, otherwise the API will not detect it
102pub fn select_state(current_state: Option<StateType>) -> miette::Result<String> {
103    fuzzy_select_with_key(
104        &[StateType::Open, StateType::Closed],
105        "Select the desired state",
106        |f| format!("{f:?}"),
107    )
108    .or(current_state
109        .as_ref()
110        .context("Not even current issue state available, aborting!"))
111    .and_then(|state| {
112        serde_json::to_string(&state)
113            .into_diagnostic()
114            .context("Couldn't convert given `state` properly for the API call!")
115    })
116    .map(|state| {
117        state
118            .trim_start_matches('"')
119            .trim_end_matches('"')
120            .to_string()
121    })
122}