use std::io;
use std::io::Write;
use crate::theme::Theme;
use crate::{ctrlc, theme};
use console::{Key, Term};
use termcolor::{Buffer, WriteColor};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Navigation {
Next,
Back,
Jump(usize),
Hub,
Stay,
Done,
}
pub type SectionFn<'a, S> = Box<dyn Fn(&mut S, &Theme) -> io::Result<Navigation> + 'a>;
pub struct Section<'a, S> {
pub label: String,
run: SectionFn<'a, S>,
}
impl<'a, S> Section<'a, S> {
pub fn new<L, F>(label: L, run: F) -> Self
where
L: Into<String>,
F: Fn(&mut S, &Theme) -> io::Result<Navigation> + 'a,
{
Self {
label: label.into(),
run: Box::new(run),
}
}
}
pub struct Wizard<'a, S> {
title: String,
sections: Vec<Section<'a, S>>,
current: usize,
theme: &'a Theme,
visited: Vec<bool>,
term: Term,
}
impl<'a, S> Wizard<'a, S> {
pub fn new<T: Into<String>>(title: T) -> Self {
Self {
title: title.into(),
sections: Vec::new(),
current: 0,
theme: &theme::DEFAULT,
visited: Vec::new(),
term: Term::stderr(),
}
}
pub fn section<L, F>(mut self, label: L, run: F) -> Self
where
L: Into<String>,
F: Fn(&mut S, &Theme) -> io::Result<Navigation> + 'a,
{
self.sections.push(Section::new(label, run));
self.visited.push(false);
self
}
pub fn theme(mut self, theme: &'a Theme) -> Self {
self.theme = theme;
self
}
pub fn run(mut self, state: &mut S) -> io::Result<()> {
if self.sections.is_empty() {
return Ok(());
}
let ctrlc_handle = ctrlc::show_cursor_after_ctrlc(&self.term)?;
self.visited[0] = true;
loop {
let section = &self.sections[self.current];
let result = (section.run)(state, self.theme);
match result {
Ok(Navigation::Next) => {
if self.current + 1 < self.sections.len() {
self.current += 1;
self.visited[self.current] = true;
}
}
Ok(Navigation::Back) => {
if self.current > 0 {
self.current -= 1;
self.visited[self.current] = true;
}
}
Ok(Navigation::Jump(idx)) => {
if idx < self.sections.len() {
self.current = idx;
self.visited[self.current] = true;
}
}
Ok(Navigation::Hub) => {
self.current = 0;
}
Ok(Navigation::Stay) => {
}
Ok(Navigation::Done) => {
self.term.show_cursor()?;
ctrlc_handle.close();
return Ok(());
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => {
if self.current > 0 {
self.current -= 1;
self.visited[self.current] = true;
} else {
self.term.show_cursor()?;
ctrlc_handle.close();
return Err(e);
}
}
Err(e) => {
self.term.show_cursor()?;
ctrlc_handle.close();
return Err(e);
}
}
}
}
pub fn render_breadcrumb(&self) -> io::Result<String> {
let mut out = Buffer::ansi();
out.set_color(&self.theme.title)?;
writeln!(out, "{}", self.title)?;
for (i, section) in self.sections.iter().enumerate() {
if i > 0 {
out.set_color(&self.theme.description)?;
write!(out, "{}", self.theme.breadcrumb_separator)?;
}
let is_current = i == self.current;
let is_visited = self.visited[i];
if is_current {
out.set_color(&self.theme.breadcrumb_active)?;
write!(out, "[{}:{}]", i + 1, section.label)?;
} else if is_visited {
out.set_color(&self.theme.breadcrumb_clickable)?;
write!(out, "{}:{}", i + 1, section.label)?;
} else {
out.set_color(&self.theme.breadcrumb_future)?;
write!(out, "{}:{}", i + 1, section.label)?;
}
}
writeln!(out)?;
out.set_color(&self.theme.description)?;
let width = self.term.size().1 as usize;
let line_width = width.min(50);
writeln!(out, "{}", "─".repeat(line_width))?;
writeln!(out)?;
out.reset()?;
Ok(std::str::from_utf8(out.as_slice()).unwrap().to_string())
}
}
pub fn handle_navigation_key(key: Key, current: usize, section_count: usize) -> Option<Navigation> {
match key {
Key::Escape => {
if current > 0 {
Some(Navigation::Back)
} else {
None }
}
Key::Char(c) if c.is_ascii_digit() && c != '0' => {
let idx = (c as usize) - ('1' as usize);
if idx < section_count {
Some(Navigation::Jump(idx))
} else {
None
}
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_navigation_enum() {
assert_eq!(Navigation::Next, Navigation::Next);
assert_eq!(Navigation::Back, Navigation::Back);
assert_eq!(Navigation::Jump(0), Navigation::Jump(0));
assert_ne!(Navigation::Jump(0), Navigation::Jump(1));
assert_eq!(Navigation::Hub, Navigation::Hub);
assert_eq!(Navigation::Stay, Navigation::Stay);
assert_eq!(Navigation::Done, Navigation::Done);
}
#[test]
fn test_handle_navigation_key() {
assert_eq!(
handle_navigation_key(Key::Char('1'), 2, 4),
Some(Navigation::Jump(0))
);
assert_eq!(
handle_navigation_key(Key::Char('3'), 0, 4),
Some(Navigation::Jump(2))
);
assert_eq!(
handle_navigation_key(Key::Char('5'), 0, 4),
None );
assert_eq!(
handle_navigation_key(Key::Char('0'), 0, 4),
None );
assert_eq!(
handle_navigation_key(Key::Escape, 2, 4),
Some(Navigation::Back)
);
assert_eq!(
handle_navigation_key(Key::Escape, 0, 4),
None );
assert_eq!(handle_navigation_key(Key::Enter, 0, 4), None);
assert_eq!(handle_navigation_key(Key::ArrowDown, 0, 4), None);
}
#[test]
fn test_section_creation() {
let section: Section<'_, i32> = Section::new("Test", |_state, _theme| Ok(Navigation::Next));
assert_eq!(section.label, "Test");
}
#[test]
fn test_wizard_builder() {
let wizard: Wizard<'_, i32> = Wizard::new("Test Wizard")
.section("Step 1", |_state, _theme| Ok(Navigation::Next))
.section("Step 2", |_state, _theme| Ok(Navigation::Done));
assert_eq!(wizard.sections.len(), 2);
assert_eq!(wizard.sections[0].label, "Step 1");
assert_eq!(wizard.sections[1].label, "Step 2");
}
}