use crate::dsl::DSLWorkflow;
use crate::tui::help::{HelpContext, HelpViewState};
use crate::tui::views::editor::EditorMode;
use crate::tui::views::generator::GeneratorState;
use crate::tui::views::state_browser::StateBrowserState;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct AppState {
pub view_mode: ViewMode,
pub modal: Option<Modal>,
pub workflows: Vec<WorkflowEntry>,
pub selected_workflow: usize,
pub current_workflow: Option<DSLWorkflow>,
pub current_workflow_path: Option<PathBuf>,
pub execution_state: Option<ExecutionState>,
pub search_query: String,
pub viewer_state: ViewerState,
pub editor_state: EditorState,
pub help_state: HelpViewState,
pub state_browser: StateBrowserState,
pub generator_state: GeneratorState,
pub running: bool,
pub input_buffer: String,
}
impl AppState {
pub fn new() -> Self {
Self {
view_mode: ViewMode::WorkflowList,
modal: None,
workflows: Vec::new(),
selected_workflow: 0,
current_workflow: None,
current_workflow_path: None,
execution_state: None,
search_query: String::new(),
viewer_state: ViewerState::new(),
editor_state: EditorState::new(),
help_state: HelpViewState::new(HelpContext::General),
state_browser: StateBrowserState::default(),
generator_state: GeneratorState::new_create(),
running: true,
input_buffer: String::new(),
}
}
pub fn reset(&mut self) {
*self = Self::new();
}
pub fn has_modal(&self) -> bool {
self.modal.is_some()
}
pub fn close_modal(&mut self) {
self.modal = None;
}
pub fn filtered_workflows(&self) -> Vec<&WorkflowEntry> {
if self.search_query.is_empty() {
self.workflows.iter().collect()
} else {
let query = self.search_query.to_lowercase();
self.workflows
.iter()
.filter(|w| {
w.name.to_lowercase().contains(&query)
|| w.description
.as_ref()
.map(|d| d.to_lowercase().contains(&query))
.unwrap_or(false)
})
.collect()
}
}
}
impl Default for AppState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViewMode {
WorkflowList,
Viewer,
Editor,
Generator,
ExecutionMonitor,
StateBrowser,
Help,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Modal {
Confirm {
title: String,
message: String,
action: ConfirmAction,
},
Input {
title: String,
prompt: String,
default: String,
action: InputAction,
},
Error { title: String, message: String },
Success { title: String, message: String },
Info { title: String, message: String },
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConfirmAction {
DeleteWorkflow(PathBuf),
ExecuteWorkflow(PathBuf),
DiscardChanges,
Exit,
StopExecution,
}
#[derive(Debug, Clone, PartialEq)]
pub enum InputAction {
CreateWorkflow,
RenameWorkflow(PathBuf),
GenerateWorkflow,
SetWorkflowDescription,
SaveWorkflowAs,
}
#[derive(Debug, Clone, PartialEq)]
pub struct WorkflowEntry {
pub name: String,
pub path: PathBuf,
pub description: Option<String>,
pub version: Option<String>,
pub valid: bool,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExecutionState {
pub workflow_path: PathBuf,
pub status: ExecutionStatus,
pub current_agent: Option<String>,
pub current_task: Option<String>,
pub progress: f64,
pub log: Vec<String>,
pub completed_tasks: Vec<String>,
pub failed_tasks: Vec<String>,
pub started_at: std::time::Instant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionStatus {
Preparing,
Running,
Paused,
Completed,
Failed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkflowViewMode {
Condensed,
Full,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ViewerState {
pub scroll: u16,
pub section: ViewerSection,
pub expanded: Vec<String>,
pub view_mode: WorkflowViewMode,
}
impl ViewerState {
pub fn new() -> Self {
Self {
scroll: 0,
section: ViewerSection::Overview,
expanded: Vec::new(),
view_mode: WorkflowViewMode::Condensed,
}
}
pub fn reset(&mut self) {
*self = Self::new();
}
pub fn toggle_view_mode(&mut self) {
self.view_mode = match self.view_mode {
WorkflowViewMode::Condensed => WorkflowViewMode::Full,
WorkflowViewMode::Full => WorkflowViewMode::Condensed,
};
}
pub fn scroll_up(&mut self) {
self.scroll = self.scroll.saturating_sub(1);
}
pub fn scroll_down(&mut self, _max_lines: usize) {
self.scroll = self.scroll.saturating_add(1);
}
pub fn page_up(&mut self) {
self.scroll = self.scroll.saturating_sub(10);
}
pub fn page_down(&mut self, _max_lines: usize) {
self.scroll = self.scroll.saturating_add(10);
}
pub fn scroll_to_top(&mut self) {
self.scroll = 0;
}
pub fn scroll_to_bottom(&mut self, max_lines: usize) {
self.scroll = max_lines as u16;
}
}
impl Default for ViewerState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViewerSection {
Overview,
Agents,
Tasks,
Variables,
}
#[derive(Debug, Clone, PartialEq)]
pub struct EditorState {
pub mode: EditorMode,
pub cursor: (usize, usize),
pub scroll: (u16, u16),
pub modified: bool,
pub errors: Vec<EditorError>,
pub suggestions: Vec<String>,
pub content: String,
pub file_path: Option<PathBuf>,
pub validation_expanded: bool,
}
impl EditorState {
pub fn new() -> Self {
Self {
mode: EditorMode::Text,
cursor: (0, 0),
scroll: (0, 0),
modified: false,
errors: Vec::new(),
suggestions: Vec::new(),
content: String::new(),
file_path: None,
validation_expanded: false,
}
}
pub fn reset(&mut self) {
*self = Self::new();
}
pub fn has_file(&self) -> bool {
self.file_path.is_some()
}
pub fn is_empty(&self) -> bool {
self.content.is_empty()
}
pub fn cursor_line(&self) -> usize {
self.cursor.0
}
pub fn scroll_offset(&self) -> usize {
self.scroll.0 as usize
}
pub fn validate_cursor(&mut self) {
let lines: Vec<&str> = self.content.lines().collect();
if lines.is_empty() {
self.cursor = (0, 0);
return;
}
if self.cursor.0 >= lines.len() {
self.cursor.0 = lines.len().saturating_sub(1);
}
if self.cursor.0 < lines.len() {
let line_len = lines[self.cursor.0].len();
if self.cursor.1 > line_len {
self.cursor.1 = line_len;
}
}
}
}
impl Default for EditorState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EditorError {
pub line: usize,
pub column: Option<usize>,
pub message: String,
pub severity: ErrorSeverity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorSeverity {
Error,
Warning,
Info,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_state_new() {
let state = AppState::new();
assert_eq!(state.view_mode, ViewMode::WorkflowList);
assert!(state.modal.is_none());
assert!(state.workflows.is_empty());
assert_eq!(state.selected_workflow, 0);
assert!(state.current_workflow.is_none());
assert!(state.execution_state.is_none());
assert!(state.search_query.is_empty());
assert!(state.running);
}
#[test]
fn test_app_state_reset() {
let mut state = AppState::new();
state.view_mode = ViewMode::Editor;
state.search_query = "test".to_string();
state.running = false;
state.reset();
assert_eq!(state.view_mode, ViewMode::WorkflowList);
assert_eq!(state.search_query, "");
assert!(state.running);
}
#[test]
fn test_modal_management() {
let mut state = AppState::new();
assert!(!state.has_modal());
state.modal = Some(Modal::Success {
title: "Test".to_string(),
message: "Success".to_string(),
});
assert!(state.has_modal());
state.close_modal();
assert!(!state.has_modal());
}
#[test]
fn test_workflow_filtering_empty_query() {
let mut state = AppState::new();
state.workflows = vec![
WorkflowEntry {
name: "test1.yaml".to_string(),
path: PathBuf::from("test1.yaml"),
description: Some("First workflow".to_string()),
version: None,
valid: true,
errors: vec![],
},
WorkflowEntry {
name: "test2.yaml".to_string(),
path: PathBuf::from("test2.yaml"),
description: Some("Second workflow".to_string()),
version: None,
valid: true,
errors: vec![],
},
];
let filtered = state.filtered_workflows();
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_workflow_filtering_by_name() {
let mut state = AppState::new();
state.workflows = vec![
WorkflowEntry {
name: "test1.yaml".to_string(),
path: PathBuf::from("test1.yaml"),
description: Some("First workflow".to_string()),
version: None,
valid: true,
errors: vec![],
},
WorkflowEntry {
name: "demo.yaml".to_string(),
path: PathBuf::from("demo.yaml"),
description: Some("Demo workflow".to_string()),
version: None,
valid: true,
errors: vec![],
},
];
state.search_query = "test".to_string();
let filtered = state.filtered_workflows();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "test1.yaml");
}
#[test]
fn test_workflow_filtering_by_description() {
let mut state = AppState::new();
state.workflows = vec![
WorkflowEntry {
name: "a.yaml".to_string(),
path: PathBuf::from("a.yaml"),
description: Some("Database migration".to_string()),
version: None,
valid: true,
errors: vec![],
},
WorkflowEntry {
name: "b.yaml".to_string(),
path: PathBuf::from("b.yaml"),
description: Some("API testing".to_string()),
version: None,
valid: true,
errors: vec![],
},
];
state.search_query = "database".to_string();
let filtered = state.filtered_workflows();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "a.yaml");
}
#[test]
fn test_workflow_filtering_case_insensitive() {
let mut state = AppState::new();
state.workflows = vec![WorkflowEntry {
name: "TEST.yaml".to_string(),
path: PathBuf::from("TEST.yaml"),
description: Some("UPPER CASE".to_string()),
version: None,
valid: true,
errors: vec![],
}];
state.search_query = "test".to_string();
let filtered = state.filtered_workflows();
assert_eq!(filtered.len(), 1);
state.search_query = "upper".to_string();
let filtered = state.filtered_workflows();
assert_eq!(filtered.len(), 1);
}
#[test]
fn test_viewer_state_new() {
let state = ViewerState::new();
assert_eq!(state.scroll, 0);
assert_eq!(state.section, ViewerSection::Overview);
assert!(state.expanded.is_empty());
}
#[test]
fn test_editor_state_new() {
let state = EditorState::new();
assert_eq!(state.mode, EditorMode::Text);
assert_eq!(state.cursor, (0, 0));
assert_eq!(state.scroll, (0, 0));
assert!(!state.modified);
assert!(state.errors.is_empty());
assert!(state.suggestions.is_empty());
}
#[test]
fn test_view_mode_equality() {
assert_eq!(ViewMode::WorkflowList, ViewMode::WorkflowList);
assert_ne!(ViewMode::WorkflowList, ViewMode::Editor);
}
#[test]
fn test_execution_status_equality() {
assert_eq!(ExecutionStatus::Running, ExecutionStatus::Running);
assert_ne!(ExecutionStatus::Running, ExecutionStatus::Completed);
}
#[test]
fn test_modal_equality() {
let modal1 = Modal::Success {
title: "Test".to_string(),
message: "Success".to_string(),
};
let modal2 = Modal::Success {
title: "Test".to_string(),
message: "Success".to_string(),
};
let modal3 = Modal::Error {
title: "Test".to_string(),
message: "Error".to_string(),
};
assert_eq!(modal1, modal2);
assert_ne!(modal1, modal3);
}
#[test]
fn test_confirm_action_equality() {
let action1 = ConfirmAction::Exit;
let action2 = ConfirmAction::Exit;
let action3 = ConfirmAction::DiscardChanges;
assert_eq!(action1, action2);
assert_ne!(action1, action3);
}
#[test]
fn test_error_severity() {
assert_eq!(ErrorSeverity::Error, ErrorSeverity::Error);
assert_ne!(ErrorSeverity::Error, ErrorSeverity::Warning);
}
}