use std::collections::HashMap;
use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::sanitize;
use crate::widgets::block::{Block, BorderStyle};
use crate::widgets::Widget;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FlowStepType {
Single,
Choice,
Confirm,
Input,
WaitForKey,
}
#[derive(Debug, Clone)]
pub struct FlowStep {
pub id: String,
pub title: String,
pub content: String,
pub step_type: FlowStepType,
pub choices: Vec<FlowChoice>,
pub required_panes: Vec<String>,
pub auto_open_panes: Vec<String>,
pub on_complete: Option<String>,
pub on_skip: Option<String>,
pub highlight_color: Color,
pub visible: bool,
}
impl FlowStep {
pub fn new(id: &str, title: &str, content: &str) -> Self {
Self {
id: id.to_string(),
title: title.to_string(),
content: content.to_string(),
step_type: FlowStepType::Single,
choices: Vec::new(),
required_panes: Vec::new(),
auto_open_panes: Vec::new(),
on_complete: None,
on_skip: None,
highlight_color: Color::rgb(137, 180, 250),
visible: true,
}
}
pub fn with_type(mut self, st: FlowStepType) -> Self {
self.step_type = st;
self
}
pub fn with_choice(mut self, choice: FlowChoice) -> Self {
self.choices.push(choice);
self
}
pub fn with_required_pane(mut self, pane_name: &str) -> Self {
self.required_panes.push(pane_name.to_string());
self
}
pub fn with_auto_open_pane(mut self, pane_name: &str) -> Self {
self.auto_open_panes.push(pane_name.to_string());
self
}
pub fn with_highlight_color(mut self, c: Color) -> Self {
self.highlight_color = c;
self
}
pub fn with_on_complete(mut self, step_id: &str) -> Self {
self.on_complete = Some(step_id.to_string());
self
}
pub fn with_on_skip(mut self, step_id: &str) -> Self {
self.on_skip = Some(step_id.to_string());
self
}
}
#[derive(Debug, Clone)]
pub struct FlowChoice {
pub label: String,
pub value: String,
pub next_step: Option<String>,
pub action: FlowAction,
}
impl FlowChoice {
pub fn new(label: &str, value: &str) -> Self {
Self {
label: label.to_string(),
value: value.to_string(),
next_step: None,
action: FlowAction::None,
}
}
pub fn with_next_step(mut self, step_id: &str) -> Self {
self.next_step = Some(step_id.to_string());
self
}
pub fn with_action(mut self, action: FlowAction) -> Self {
self.action = action;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FlowAction {
None,
TogglePane(String),
OpenPane(String),
ClosePane(String),
RunCommand(String),
ExpandSnippet(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FlowState {
Inactive,
Running,
Paused,
Completed,
Cancelled,
}
#[derive(Debug, Clone)]
pub struct FlowManager {
pub name: String,
pub steps: Vec<FlowStep>,
pub current_step: Option<String>,
pub state: FlowState,
pub step_index: HashMap<String, usize>,
pub selected_choice: usize,
pub results: HashMap<String, String>,
pub highlight_color: Color,
pub border_color: Color,
pub progress_visible: bool,
}
impl FlowManager {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
steps: Vec::new(),
current_step: None,
state: FlowState::Inactive,
step_index: HashMap::new(),
selected_choice: 0,
results: HashMap::new(),
highlight_color: Color::rgb(137, 180, 250),
border_color: Color::rgb(88, 166, 255),
progress_visible: true,
}
}
pub fn with_highlight_color(mut self, c: Color) -> Self {
self.highlight_color = c;
self
}
pub fn with_border_color(mut self, c: Color) -> Self {
self.border_color = c;
self
}
pub fn with_progress_visible(mut self, v: bool) -> Self {
self.progress_visible = v;
self
}
pub fn add_step(&mut self, step: FlowStep) {
let idx = self.steps.len();
self.step_index.insert(step.id.clone(), idx);
self.steps.push(step);
}
pub fn start(&mut self) -> Option<String> {
if self.steps.is_empty() {
self.state = FlowState::Completed;
return None;
}
self.state = FlowState::Running;
let first_id = self.steps[0].id.clone();
self.current_step = Some(first_id.clone());
self.selected_choice = 0;
self.results.clear();
Some(first_id)
}
pub fn cancel(&mut self) {
self.state = FlowState::Cancelled;
self.current_step = None;
}
pub fn reset(&mut self) {
self.state = FlowState::Inactive;
self.current_step = None;
self.selected_choice = 0;
self.results.clear();
}
pub fn current_step_index(&self) -> Option<usize> {
self.current_step
.as_ref()
.and_then(|id| self.step_index.get(id))
.copied()
}
pub fn total_steps(&self) -> usize {
self.steps.len()
}
pub fn progress(&self) -> f32 {
if self.steps.is_empty() {
return 0.0;
}
self.current_step_index()
.map(|i| (i as f32) / (self.steps.len() as f32))
.unwrap_or(1.0)
}
pub fn advance(&mut self) -> Option<String> {
let idx = self.current_step_index()?;
let current = &self.steps[idx];
if let Some(on_complete) = ¤t.on_complete {
self.results.insert(current.id.clone(), on_complete.clone());
}
if let Some(choice) = current.choices.get(self.selected_choice) {
self.results
.insert(current.id.clone(), choice.value.clone());
if let Some(next) = &choice.next_step {
self.current_step = Some(next.clone());
self.selected_choice = 0;
return Some(next.clone());
}
}
let next_idx = idx + 1;
if next_idx < self.steps.len() {
let next_id = self.steps[next_idx].id.clone();
self.current_step = Some(next_id.clone());
self.selected_choice = 0;
Some(next_id)
} else {
self.state = FlowState::Completed;
self.current_step = None;
None
}
}
pub fn go_back(&mut self) -> Option<String> {
let idx = self.current_step_index()?;
if idx == 0 {
return None;
}
let prev_id = self.steps[idx - 1].id.clone();
self.current_step = Some(prev_id.clone());
self.selected_choice = 0;
Some(prev_id)
}
pub fn skip(&mut self) -> Option<String> {
let idx = self.current_step_index()?;
let current = &self.steps[idx];
if let Some(on_skip) = ¤t.on_skip {
self.results.insert(current.id.clone(), on_skip.clone());
}
self.advance()
}
pub fn select_choice(&mut self, idx: usize) -> bool {
let step_idx = match self.current_step_index() {
Some(i) => i,
None => return false,
};
if idx < self.steps[step_idx].choices.len() {
self.selected_choice = idx;
true
} else {
false
}
}
pub fn select_choice_up(&mut self) {
if self.selected_choice > 0 {
self.selected_choice -= 1;
}
}
pub fn select_choice_down(&mut self) {
let max = self
.current_step_ref()
.map(|s| s.choices.len())
.unwrap_or(0);
if max > 0 && self.selected_choice + 1 < max {
self.selected_choice += 1;
}
}
pub fn current_step_ref(&self) -> Option<&FlowStep> {
self.current_step
.as_ref()
.and_then(|id| self.step_index.get(id))
.and_then(|&i| self.steps.get(i))
}
pub fn current_step_mut(&mut self) -> Option<&mut FlowStep> {
let id = self.current_step.clone()?;
let idx = self.step_index.get(&id).copied()?;
self.steps.get_mut(idx)
}
pub fn get_required_actions(&self) -> Vec<FlowAction> {
let mut actions = Vec::new();
if let Some(step) = self.current_step_ref() {
for pane_name in &step.auto_open_panes {
actions.push(FlowAction::OpenPane(pane_name.clone()));
}
}
actions
}
pub fn get_toggle_actions(&self) -> Vec<FlowAction> {
if let Some(step) = self.current_step_ref() {
if let Some(choice) = step.choices.get(self.selected_choice) {
if choice.action != FlowAction::None {
return vec![choice.action.clone()];
}
}
}
Vec::new()
}
pub fn handle_key(&mut self, key: char) -> FlowKeyResult {
match self.state {
FlowState::Running => match key {
'j' | '2' => {
self.select_choice_down();
FlowKeyResult::None
}
'k' | '8' => {
self.select_choice_up();
FlowKeyResult::None
}
'\n' | '\r' => {
if let Some(next) = self.advance() {
FlowKeyResult::StepChanged(next)
} else {
FlowKeyResult::Completed
}
}
' ' => {
if let Some(step) = self.current_step_ref() {
if step.step_type == FlowStepType::Choice && !step.choices.is_empty() {
if let Some(next) = self.advance() {
return FlowKeyResult::StepChanged(next);
} else {
return FlowKeyResult::Completed;
}
}
}
FlowKeyResult::None
}
'n' => {
if let Some(next) = self.advance() {
FlowKeyResult::StepChanged(next)
} else {
FlowKeyResult::Completed
}
}
'p' => {
if let Some(prev) = self.go_back() {
FlowKeyResult::StepChanged(prev)
} else {
FlowKeyResult::None
}
}
's' => {
if let Some(next) = self.skip() {
FlowKeyResult::StepChanged(next)
} else {
FlowKeyResult::Completed
}
}
'q' | '\x1b' => {
self.cancel();
FlowKeyResult::Cancelled
}
_ => FlowKeyResult::None,
},
FlowState::Completed | FlowState::Cancelled => FlowKeyResult::None,
FlowState::Inactive | FlowState::Paused => FlowKeyResult::None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FlowKeyResult {
None,
StepChanged(String),
Completed,
Cancelled,
}
impl Default for FlowManager {
fn default() -> Self {
Self::new("Flow")
}
}
#[derive(Debug, Clone)]
pub struct FlowRenderer {
pub show_progress: bool,
pub show_choices: bool,
pub highlight_color: Color,
pub dim_color: Color,
pub border_color: Color,
pub title_color: Color,
}
impl FlowRenderer {
pub fn new() -> Self {
Self {
show_progress: true,
show_choices: true,
highlight_color: Color::rgb(137, 180, 250),
dim_color: Color::rgb(108, 112, 134),
border_color: Color::rgb(88, 166, 255),
title_color: Color::rgb(205, 214, 244),
}
}
pub fn with_highlight_color(mut self, c: Color) -> Self {
self.highlight_color = c;
self
}
pub fn with_border_color(mut self, c: Color) -> Self {
self.border_color = c;
self
}
pub fn render_flow(&self, flow: &FlowManager, buffer: &mut Buffer, area: Rect) {
if flow.state == FlowState::Inactive || flow.state == FlowState::Cancelled {
return;
}
let step_num = flow.current_step_index().map(|i| i + 1).unwrap_or(0);
let title = format!(
"{} — Step {} of {}",
flow.name,
step_num,
flow.total_steps()
);
let block = Block::new(&title)
.with_borders(BorderStyle::Rounded)
.with_border_color(self.border_color);
block.render(buffer, area);
let inner = block.inner(area);
if inner.width == 0 || inner.height == 0 {
return;
}
let mut y = inner.y as usize;
let mut remaining_height = inner.height as usize;
if self.show_progress && remaining_height > 0 {
let progress_bar_width = (inner.width as usize).min(40);
let filled = (flow.progress() * progress_bar_width as f32) as usize;
let mut bar = String::from("[");
for i in 0..progress_bar_width {
if i < filled {
bar.push('â–ˆ');
} else {
bar.push('â–‘');
}
}
bar.push(']');
let bar_x = inner.x as usize
+ (inner.width as usize).saturating_sub(progress_bar_width + 2) / 2;
buffer.set_str(bar_x, y, &bar, self.highlight_color, None);
y += 1;
remaining_height = remaining_height.saturating_sub(1);
}
if remaining_height > 0 {
y += 1;
remaining_height = remaining_height.saturating_sub(1);
}
if let Some(step) = flow.current_step_ref() {
if remaining_height > 0 {
let title = sanitize::truncate_str(&step.title, inner.width as usize);
buffer.set_str(inner.x as usize, y, &title, self.title_color, None);
y += 1;
remaining_height = remaining_height.saturating_sub(1);
}
if remaining_height > 0 {
y += 1;
remaining_height = remaining_height.saturating_sub(1);
}
if remaining_height > 0 {
let lines: Vec<&str> = step.content.lines().collect();
for line in lines {
if remaining_height == 0 {
break;
}
let sanitized = sanitize::truncate_str(line, inner.width as usize);
buffer.set_str(inner.x as usize, y, &sanitized, self.dim_color, None);
y += 1;
remaining_height = remaining_height.saturating_sub(1);
}
}
if self.show_choices && !step.choices.is_empty() && remaining_height > 0 {
y += 1;
remaining_height = remaining_height.saturating_sub(1);
for (i, choice) in step.choices.iter().enumerate() {
if remaining_height == 0 {
break;
}
let is_selected = i == flow.selected_choice;
let prefix = if is_selected { " > " } else { " " };
let color = if is_selected {
self.highlight_color
} else {
self.dim_color
};
let label = sanitize::truncate_str(
&choice.label,
(inner.width as usize).saturating_sub(4),
);
let line = format!("{}{}", prefix, label);
buffer.set_str(inner.x as usize, y, &line, color, None);
y += 1;
remaining_height = remaining_height.saturating_sub(1);
}
}
if remaining_height > 0 {
y += 1;
remaining_height = remaining_height.saturating_sub(1);
}
if remaining_height > 0 {
let help = "j/k: navigate Enter: confirm n: next p: back s: skip q: cancel";
let help = sanitize::truncate_str(help, inner.width as usize);
buffer.set_str(inner.x as usize, y, &help, self.dim_color, None);
}
}
if flow.state == FlowState::Completed && remaining_height > 0 {
y += 1;
let done = "Flow completed! Press any key to close.";
let done = sanitize::truncate_str(done, inner.width as usize);
buffer.set_str(inner.x as usize, y, &done, self.highlight_color, None);
}
}
}
impl Default for FlowRenderer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_flow() -> FlowManager {
let mut flow = FlowManager::new("Setup");
flow.add_step(
FlowStep::new("welcome", "Welcome", "Press Enter to start.")
.with_type(FlowStepType::Single)
.with_on_complete("step2"),
);
flow.add_step(
FlowStep::new("step2", "Choose Theme", "Select a theme:")
.with_type(FlowStepType::Choice)
.with_choice(FlowChoice::new("Dark", "dark").with_next_step("step3"))
.with_choice(FlowChoice::new("Light", "light").with_next_step("step3")),
);
flow.add_step(
FlowStep::new("step3", "Done", "Setup complete!").with_type(FlowStepType::Single),
);
flow
}
#[test]
fn test_flow_start() {
let mut flow = test_flow();
let next = flow.start();
assert_eq!(next, Some("welcome".into()));
assert_eq!(flow.state, FlowState::Running);
}
#[test]
fn test_flow_advance() {
let mut flow = test_flow();
flow.start();
let next = flow.advance();
assert_eq!(next, Some("step2".into()));
}
#[test]
fn test_flow_advance_with_choice() {
let mut flow = test_flow();
flow.start();
flow.advance();
flow.select_choice(0);
let next = flow.advance();
assert_eq!(next, Some("step3".into()));
assert_eq!(flow.results.get("step2"), Some(&"dark".into()));
}
#[test]
fn test_flow_advance_to_end() {
let mut flow = test_flow();
flow.start();
flow.advance();
flow.select_choice(1);
flow.advance();
let next = flow.advance();
assert_eq!(next, None);
assert_eq!(flow.state, FlowState::Completed);
}
#[test]
fn test_flow_go_back() {
let mut flow = test_flow();
flow.start();
flow.advance();
let prev = flow.go_back();
assert_eq!(prev, Some("welcome".into()));
}
#[test]
fn test_flow_go_back_at_start() {
let mut flow = test_flow();
flow.start();
let prev = flow.go_back();
assert_eq!(prev, None);
}
#[test]
fn test_flow_skip() {
let mut flow = test_flow();
flow.start();
let next = flow.skip();
assert_eq!(next, Some("step2".into()));
}
#[test]
fn test_flow_cancel() {
let mut flow = test_flow();
flow.start();
flow.cancel();
assert_eq!(flow.state, FlowState::Cancelled);
assert!(flow.current_step.is_none());
}
#[test]
fn test_flow_reset() {
let mut flow = test_flow();
flow.start();
flow.advance();
flow.reset();
assert_eq!(flow.state, FlowState::Inactive);
assert!(flow.results.is_empty());
}
#[test]
fn test_flow_progress() {
let mut flow = test_flow();
flow.start();
assert!((flow.progress() - 0.0).abs() < f32::EPSILON);
flow.advance();
assert!((flow.progress() - 1.0 / 3.0).abs() < 0.01);
}
#[test]
fn test_flow_select_choice() {
let mut flow = test_flow();
flow.start();
flow.advance();
assert!(flow.select_choice(0));
assert!(!flow.select_choice(5));
}
#[test]
fn test_flow_handle_key() {
let mut flow = test_flow();
flow.start();
assert_eq!(flow.handle_key('j'), FlowKeyResult::None);
assert_eq!(flow.handle_key('k'), FlowKeyResult::None);
assert_eq!(
flow.handle_key('n'),
FlowKeyResult::StepChanged("step2".into())
);
assert_eq!(flow.handle_key('q'), FlowKeyResult::Cancelled);
}
#[test]
fn test_flow_required_actions() {
let mut flow = FlowManager::new("Test");
flow.add_step(
FlowStep::new("s1", "Step", "Do something")
.with_auto_open_pane("terminal")
.with_auto_open_pane("files"),
);
flow.start();
let actions = flow.get_required_actions();
assert_eq!(actions.len(), 2);
}
#[test]
fn test_flow_renderer_render() {
let mut flow = test_flow();
flow.start();
let renderer = FlowRenderer::new();
let mut buf = Buffer::new(60, 20);
renderer.render_flow(&flow, &mut buf, Rect::new(0, 0, 60, 20));
}
#[test]
fn test_flow_renderer_tiny_area_no_panic() {
let mut flow = test_flow();
flow.start();
flow.advance();
let renderer = FlowRenderer::new();
let mut buf = Buffer::new(7, 8);
renderer.render_flow(&flow, &mut buf, Rect::new(0, 0, 7, 8));
}
#[test]
fn test_flow_renderer_inactive() {
let flow = FlowManager::new("Test");
let renderer = FlowRenderer::new();
let mut buf = Buffer::new(60, 20);
renderer.render_flow(&flow, &mut buf, Rect::new(0, 0, 60, 20));
}
}