use crate::output::should_use_colors;
use dialoguer::{Confirm, Input, MultiSelect, Select, theme::ColorfulTheme, theme::SimpleTheme};
use eyre::Result;
use skim::prelude::*;
use std::io::{Cursor, IsTerminal, Write};
#[derive(Debug, PartialEq, Eq)]
enum ThemeKind {
Colorful,
Simple,
}
fn theme_kind() -> ThemeKind {
if should_use_colors() {
ThemeKind::Colorful
} else {
ThemeKind::Simple
}
}
fn dialoguer_theme() -> Box<dyn dialoguer::theme::Theme> {
match theme_kind() {
ThemeKind::Colorful => Box::new(ColorfulTheme::default()),
ThemeKind::Simple => Box::new(SimpleTheme),
}
}
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))?;
if should_use_colors() {
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 theme = dialoguer_theme();
let selection = Select::with_theme(theme.as_ref())
.with_prompt(prompt)
.items(items)
.default(0)
.interact_opt()
.ok()
.flatten()?;
Some(items[selection].clone())
}
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))?;
if should_use_colors() {
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 theme = dialoguer_theme();
let selections = MultiSelect::with_theme(theme.as_ref())
.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)
}
pub fn confirm(msg: &str, yes_flag: bool) -> bool {
if yes_flag {
return true;
}
if !std::io::stdin().is_terminal() {
return false;
}
let theme = dialoguer_theme();
Confirm::with_theme(theme.as_ref())
.with_prompt(msg)
.default(false)
.interact()
.unwrap_or(false)
}
pub fn confirm_typed(prompt_msg: &str, expected: &str, yes_flag: bool) -> Result<bool> {
if yes_flag {
return Ok(true);
}
if !std::io::stdin().is_terminal() {
return Ok(false);
}
let theme = dialoguer_theme();
let typed: String = Input::with_theme(theme.as_ref())
.with_prompt(prompt_msg)
.allow_empty(true)
.interact_text()?;
Ok(typed.trim() == expected)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::{TEST_LOCK, set_no_color};
#[test]
fn confirm_short_circuits_to_true_when_yes_flag_set() {
assert!(confirm("anything", true));
}
#[test]
fn confirm_returns_false_in_non_tty_without_yes_flag() {
assert!(!confirm("anything", false));
}
#[test]
fn confirm_typed_short_circuits_to_true_when_yes_flag_set() {
assert!(confirm_typed("type the name", "freshrss", true).unwrap());
}
#[test]
fn confirm_typed_returns_false_in_non_tty_without_yes_flag() {
assert!(!confirm_typed("type the name", "freshrss", false).unwrap());
}
#[test]
fn theme_kind_is_simple_when_no_color_flag_set() {
let _guard = TEST_LOCK.lock().unwrap();
set_no_color(true);
assert_eq!(theme_kind(), ThemeKind::Simple);
set_no_color(false);
}
#[test]
fn dialoguer_theme_does_not_panic_in_either_branch() {
let _guard = TEST_LOCK.lock().unwrap();
set_no_color(true);
let _simple = dialoguer_theme();
set_no_color(false);
let _maybe_colorful = dialoguer_theme();
}
}