1use core::fmt::Debug;
7use core::fmt::Display;
8use std::io::Cursor;
9
10use rootcause::report;
11use skim::Skim;
12use skim::SkimItem;
13use skim::SkimOutput;
14use skim::options::SkimOptions;
15use skim::prelude::SkimItemReader;
16use skim::prelude::SkimItemReaderOption;
17
18pub mod git_branch;
19
20pub fn minimal_multi_select<T: Display>(opts: Vec<T>) -> rootcause::Result<Option<Vec<T>>> {
26 if opts.is_empty() {
27 return Ok(None);
28 }
29
30 let (output, display_texts) = run_skim_prompt(&opts, select_options(true))?;
31 if output.is_abort || output.selected_items.is_empty() {
32 return Ok(None);
33 }
34
35 let mut selected_indices: Vec<usize> = output
36 .selected_items
37 .iter()
38 .filter_map(|mi| find_selected_index(&display_texts, &mi.item))
39 .collect();
40 selected_indices.sort_unstable();
41
42 let mut indexed_opts: Vec<Option<T>> = opts.into_iter().map(Some).collect();
43 let selected: Vec<T> = selected_indices
44 .into_iter()
45 .filter_map(|i| indexed_opts.get_mut(i).and_then(Option::take))
46 .collect();
47
48 if selected.is_empty() {
49 Ok(None)
50 } else {
51 Ok(Some(selected))
52 }
53}
54
55pub fn minimal_select<T: Display>(opts: Vec<T>) -> rootcause::Result<Option<T>> {
60 if opts.is_empty() {
61 return Ok(None);
62 }
63
64 let (output, display_texts) = run_skim_prompt(&opts, select_options(false))?;
65 if output.is_abort || output.selected_items.is_empty() {
66 return Ok(None);
67 }
68
69 let index = output
70 .selected_items
71 .first()
72 .and_then(|mi| find_selected_index(&display_texts, &mi.item))
73 .ok_or_else(|| report!("failed to recover selected item index"))?;
74
75 opts.into_iter()
76 .nth(index)
77 .map(Some)
78 .ok_or_else(|| report!("selected index out of bounds").attach(format!("index={index}")))
79}
80
81pub fn text_prompt(message: &str) -> rootcause::Result<Option<String>> {
86 let Some(output) = run_simple_prompt(simple_prompt_options(message, "3").build(), "")? else {
87 return Ok(None);
88 };
89 let query = output.query.trim().to_owned();
90 if query.is_empty() { Ok(None) } else { Ok(Some(query)) }
91}
92
93pub fn yes_no_select(title: &str) -> rootcause::Result<Option<bool>> {
98 let mut options = simple_prompt_options(title, "10%");
99 options.no_sort = true;
100
101 let Some(output) = run_simple_prompt(options.build(), "Yes\nNo")? else {
102 return Ok(None);
103 };
104 if output.selected_items.is_empty() {
105 return Ok(None);
106 }
107
108 let selected_text = output.selected_items.first().map(|mi| mi.item.output().into_owned());
109
110 Ok(Some(selected_text.as_deref() == Some("Yes")))
111}
112
113pub fn get_item_from_cli_args_or_select<'a, CAS, O, OBA, OF>(
123 cli_args: &'a [String],
124 mut cli_arg_selector: CAS,
125 items: Vec<O>,
126 item_find_by_arg: OBA,
127) -> rootcause::Result<Option<O>>
128where
129 O: Clone + Debug + Display,
130 CAS: FnMut(&(usize, &String)) -> bool,
131 OBA: Fn(&'a str) -> OF,
132 OF: FnMut(&O) -> bool + 'a,
133{
134 if let Some((_, cli_arg)) = cli_args.iter().enumerate().find(|x| cli_arg_selector(x)) {
135 let mut item_find = item_find_by_arg(cli_arg);
136 return Ok(Some(items.iter().find(|x| item_find(*x)).cloned().ok_or_else(
137 || report!("missing item matching CLI arg").attach(format!("cli_arg={cli_arg} items={items:#?}")),
138 )?));
139 }
140 minimal_select(items)
141}
142
143fn run_simple_prompt(options: SkimOptions, input: &str) -> rootcause::Result<Option<SkimOutput>> {
145 let items = SkimItemReader::default().of_bufread(Cursor::new(input.to_owned()));
146 let output =
147 Skim::run_with(options, Some(items)).map_err(|e| report!("skim failed to run").attach(e.to_string()))?;
148 if output.is_abort {
149 return Ok(None);
150 }
151 Ok(Some(output))
152}
153
154fn run_skim_prompt<T: Display>(opts: &[T], options: SkimOptions) -> rootcause::Result<(SkimOutput, Vec<String>)> {
157 let display_texts: Vec<String> = opts.iter().map(ToString::to_string).collect();
158 let input = display_texts.join("\n");
159 let reader_opts = SkimItemReaderOption::from_options(&options);
160 let items = SkimItemReader::new(reader_opts).of_bufread(Cursor::new(input));
161 let output =
162 Skim::run_with(options, Some(items)).map_err(|e| report!("skim failed to run").attach(e.to_string()))?;
163 Ok((output, display_texts))
164}
165
166fn find_selected_index(display_texts: &[String], item: &std::sync::Arc<dyn SkimItem>) -> Option<usize> {
169 let output_text = item.output();
170 display_texts.iter().position(|t| *t == *output_text)
171}
172
173fn base_skim_options() -> SkimOptions {
175 let mut opts = SkimOptions::default();
176 opts.reverse = true;
177 opts.no_info = true;
178 opts.exact = true;
179 opts.bind = vec!["enter:accept".into(), "esc:abort".into(), "ctrl-c:abort".into()];
180 opts
181}
182
183fn simple_prompt_options(prompt: &str, height: &str) -> SkimOptions {
185 let mut opts = base_skim_options();
186 opts.height = height.into();
187 opts.prompt = format!("{prompt} ");
188 opts
189}
190
191fn select_options(multi: bool) -> SkimOptions {
193 let mut opts = base_skim_options();
194 opts.multi = multi;
195 opts.ansi = true;
196 opts.height = "40%".into();
197 if multi {
198 opts.bind.extend(["ctrl-e:toggle".into(), "ctrl-a:toggle-all".into()]);
199 }
200 opts.build()
201}