use dialoguer::{MultiSelect, Select, theme::ColorfulTheme};
use eyre::Result;
use skim::prelude::*;
use std::io::{Cursor, IsTerminal, Write};
pub fn has_skim_support() -> bool {
std::io::stdin().is_terminal() && std::io::stderr().is_terminal()
}
fn select_with_skim(items: &[String], prompt: &str) -> Option<String> {
if items.is_empty() {
return None;
}
let prompt_str = format!("{}> ", prompt);
let options = SkimOptionsBuilder::default()
.prompt(Some(&prompt_str))
.height(Some("40%"))
.multi(false)
.reverse(true)
.build()
.ok()?;
let input = items.join("\n");
let item_reader = SkimItemReader::default();
let items = item_reader.of_bufread(Cursor::new(input));
let output = Skim::run_with(&options, Some(items))?;
let mut stderr = std::io::stderr().lock();
let _ = stderr.write_all(b"\x1b[0m\x1b[J");
let _ = stderr.flush();
if output.is_abort {
return None;
}
output
.selected_items
.first()
.map(|item| item.output().to_string())
}
fn select_with_dialoguer(items: &[String], prompt: &str) -> Option<String> {
if items.is_empty() {
return None;
}
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt(prompt)
.items(items)
.default(0)
.interact_opt()
.ok()
.flatten()?;
Some(items[selection].clone())
}
pub fn select(items: &[String], prompt: &str) -> Option<String> {
if items.is_empty() {
return None;
}
if !has_skim_support() {
if items.len() == 1 {
return Some(items[0].clone());
}
return None;
}
select_with_skim(items, prompt).or_else(|| select_with_dialoguer(items, prompt))
}
pub fn select_item<T, F>(items: &[T], display_fn: F, prompt: &str) -> Result<Option<T>>
where
T: Clone,
F: Fn(&T) -> String,
{
if items.is_empty() {
return Ok(None);
}
let display_items: Vec<String> = items.iter().map(&display_fn).collect();
let selected_display = select(&display_items, prompt);
match selected_display {
Some(display) => {
let idx = display_items.iter().position(|d| d == &display);
Ok(idx.map(|i| items[i].clone()))
}
None => Ok(None),
}
}
fn select_multi_with_skim(items: &[String], prompt: &str) -> Option<Vec<String>> {
if items.is_empty() {
return None;
}
let prompt_str = format!("{}> ", prompt);
let options = SkimOptionsBuilder::default()
.prompt(Some(&prompt_str))
.height(Some("40%"))
.multi(true)
.reverse(true)
.build()
.ok()?;
let input = items.join("\n");
let item_reader = SkimItemReader::default();
let items = item_reader.of_bufread(Cursor::new(input));
let output = Skim::run_with(&options, Some(items))?;
let mut stderr = std::io::stderr().lock();
let _ = stderr.write_all(b"\x1b[0m\x1b[J");
let _ = stderr.flush();
if output.is_abort {
return None;
}
let selected: Vec<String> = output
.selected_items
.iter()
.map(|item| item.output().to_string())
.collect();
if selected.is_empty() {
None
} else {
Some(selected)
}
}
fn select_multi_with_dialoguer(items: &[String], prompt: &str) -> Option<Vec<String>> {
if items.is_empty() {
return None;
}
let selections = MultiSelect::with_theme(&ColorfulTheme::default())
.with_prompt(prompt)
.items(items)
.interact_opt()
.ok()
.flatten()?;
if selections.is_empty() {
return None;
}
Some(selections.iter().map(|&i| items[i].clone()).collect())
}
pub fn select_multi(items: &[String], prompt: &str) -> Option<Vec<String>> {
if items.is_empty() {
return None;
}
if !has_skim_support() {
if items.len() == 1 {
return Some(vec![items[0].clone()]);
}
return None;
}
if let Some(result) = select_multi_with_skim(items, prompt) {
return Some(result);
}
select_multi_with_dialoguer(items, prompt)
}