toon_format/tui/state/
repl_state.rs

1//! REPL state - separate from command mode
2
3use std::collections::HashMap;
4
5/// REPL session state
6#[derive(Debug, Clone)]
7pub struct ReplState {
8    /// Whether REPL is active
9    pub active: bool,
10    /// Current input line
11    pub input: String,
12    /// Session history (output lines)
13    pub output: Vec<ReplLine>,
14    /// Variables stored in session
15    pub variables: HashMap<String, String>,
16    /// Command history
17    pub history: Vec<String>,
18    /// History index for navigation
19    pub history_index: Option<usize>,
20    /// Last result (for _ variable)
21    pub last_result: Option<String>,
22    /// Scroll offset for output
23    pub scroll_offset: usize,
24}
25
26/// A line in the REPL output
27#[derive(Debug, Clone)]
28pub struct ReplLine {
29    pub kind: ReplLineKind,
30    pub content: String,
31}
32
33#[derive(Debug, Clone, PartialEq)]
34pub enum ReplLineKind {
35    Prompt,
36    Success,
37    Error,
38    Info,
39}
40
41impl ReplState {
42    pub fn new() -> Self {
43        Self {
44            active: false,
45            input: String::new(),
46            output: vec![ReplLine {
47                kind: ReplLineKind::Info,
48                content: "TOON REPL - Type 'help' for commands, 'exit' to close".to_string(),
49            }],
50            variables: HashMap::new(),
51            history: Vec::new(),
52            history_index: None,
53            last_result: None,
54            scroll_offset: 0,
55        }
56    }
57
58    pub fn activate(&mut self) {
59        self.active = true;
60        self.input.clear();
61        self.history_index = None;
62    }
63
64    pub fn deactivate(&mut self) {
65        self.active = false;
66        self.input.clear();
67        self.history_index = None;
68    }
69
70    pub fn add_prompt(&mut self, cmd: &str) {
71        self.output.push(ReplLine {
72            kind: ReplLineKind::Prompt,
73            content: format!("> {cmd}"),
74        });
75    }
76
77    pub fn add_success(&mut self, msg: String) {
78        for line in msg.lines() {
79            self.output.push(ReplLine {
80                kind: ReplLineKind::Success,
81                content: line.to_string(),
82            });
83        }
84    }
85
86    pub fn add_error(&mut self, msg: String) {
87        self.output.push(ReplLine {
88            kind: ReplLineKind::Error,
89            content: format!("✗ {msg}"),
90        });
91    }
92
93    pub fn add_info(&mut self, msg: String) {
94        let content = if msg.is_empty() || msg.starts_with("  ") || msg.starts_with("📖") {
95            msg
96        } else {
97            format!("✓ {msg}")
98        };
99
100        self.output.push(ReplLine {
101            kind: ReplLineKind::Info,
102            content,
103        });
104    }
105
106    pub fn add_to_history(&mut self, cmd: String) {
107        if cmd.trim().is_empty() {
108            return;
109        }
110        if self.history.last() == Some(&cmd) {
111            return;
112        }
113        self.history.push(cmd);
114        if self.history.len() > 100 {
115            self.history.remove(0);
116        }
117    }
118
119    pub fn history_up(&mut self) {
120        if self.history.is_empty() {
121            return;
122        }
123        let new_index = match self.history_index {
124            None => Some(self.history.len() - 1),
125            Some(0) => Some(0),
126            Some(i) => Some(i - 1),
127        };
128        if let Some(idx) = new_index {
129            self.input = self.history[idx].clone();
130            self.history_index = new_index;
131        }
132    }
133
134    pub fn history_down(&mut self) {
135        match self.history_index {
136            None => (),
137            Some(i) if i >= self.history.len() - 1 => {
138                self.input.clear();
139                self.history_index = None;
140            }
141            Some(i) => {
142                let new_idx = i + 1;
143                self.input = self.history[new_idx].clone();
144                self.history_index = Some(new_idx);
145            }
146        }
147    }
148
149    pub fn scroll_up(&mut self) {
150        if self.scroll_offset > 0 {
151            self.scroll_offset -= 1;
152        }
153    }
154
155    pub fn scroll_down(&mut self, visible_lines: usize) {
156        let max_scroll = self.output.len().saturating_sub(visible_lines);
157        if self.scroll_offset < max_scroll {
158            self.scroll_offset += 1;
159        }
160    }
161
162    pub fn scroll_to_bottom(&mut self) {
163        if self.output.len() <= 30 {
164            self.scroll_offset = 0;
165        } else {
166            self.scroll_offset = self.output.len().saturating_sub(30);
167        }
168    }
169}
170
171impl Default for ReplState {
172    fn default() -> Self {
173        Self::new()
174    }
175}