repl_lib/
lib.rs

1// Copyright (c) 2025 Sebastian Ibanez
2// Author: Sebastian Ibanez
3// Created: 2025-09-17
4
5use std::fmt::Display;
6
7use term_manager::TermManager;
8
9/// Result type alias for repl_lib operations.
10pub type Result<T> = std::result::Result<T, Error>;
11
12/// Function type for processing input lines.
13pub type ProcessLineFunc = Box<dyn FnMut(String) -> Result<String>>;
14
15/// Function type for determining if a line is complete.
16pub type LineCompletionFunc = Box<dyn FnMut(String) -> bool>;
17
18/// Error type for REPL operations.
19#[derive(Debug)]
20pub enum Error {
21    InitFail(String),
22    IoFlush(String),
23    IoRead(String),
24    IoWrite(String),
25    ProcessLine(String),
26}
27
28impl Display for Error {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            Error::InitFail(s) => write!(f, "initialization failed: {}", s),
32            Error::IoFlush(s) => write!(f, "IO flush error: {}", s),
33            Error::IoRead(s) => write!(f, "IO read error: {}", s),
34            Error::IoWrite(s) => write!(f, "IO write error: {}", s),
35            Error::ProcessLine(s) => write!(f, "Process Line error: {}", s),
36        }
37    }
38}
39
40/// Represents a single line of input with cursor position.
41#[derive(Clone, Debug)]
42pub struct Line {
43    text: String,
44    cursor_pos: usize,
45}
46
47impl Line {
48    /// Creates a new empty line.
49    pub fn new() -> Self {
50        Self {
51            text: String::new(),
52            cursor_pos: 0,
53        }
54    }
55
56    /// Inserts a character at the current cursor position.
57    pub fn insert_char(&mut self, c: char) {
58        self.text.insert(self.cursor_pos, c);
59        self.cursor_pos += 1;
60    }
61
62    /// Removes the character before the cursor.
63    pub fn backspace(&mut self) {
64        if self.cursor_pos > 0 {
65            self.cursor_pos -= 1;
66            self.text.remove(self.cursor_pos);
67        }
68    }
69
70    /// Moves cursor one position to the left.
71    pub fn move_left(&mut self) {
72        if self.cursor_pos > 0 {
73            self.cursor_pos -= 1;
74        }
75    }
76
77    /// Moves cursor one position to the right.
78    pub fn move_right(&mut self) {
79        if self.cursor_pos < self.text.len() {
80            self.cursor_pos += 1;
81        }
82    }
83
84    /// Returns the text content of the line.
85    pub fn text(&self) -> &str {
86        &self.text
87    }
88}
89
90impl Display for Line {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        write!(f, "{}", self.text)
93    }
94}
95
96/// Type of input being processed by the REPL.
97#[derive(Copy, Clone, Debug)]
98enum InputType {
99    Normal,
100    Escape,
101    EscapeSequence,
102}
103
104/// Internal state for REPL operation flow.
105#[derive(Copy, Clone, Debug)]
106enum ReplState {
107    Continue,
108    Break,
109}
110
111/// Interactive Read-Eval-Print Loop implementation.
112pub struct Repl {
113    tmanager: TermManager,
114    lines: Vec<Line>,
115    current_line: usize,
116    escape_buffer: Vec<u8>,
117    input_state: InputType,
118    process_line: ProcessLineFunc,
119    is_line_complete: LineCompletionFunc,
120    prompt: String,
121    banner: String,
122    welcome_msg: String,
123}
124
125impl Repl {
126    /// Create a new REPL instance.
127    ///
128    /// ### Arguments
129    ///
130    /// * `prompt` - The prompt string to display
131    /// * `banner` - Startup banner to display.
132    /// * `welcome_msg` - Welcome message to display.
133    /// * `process_line` - Function to process completed lines
134    /// * `line_is_finished` - Function to determine if a line is terminated
135    pub fn new(
136        prompt: String,
137        banner: String,
138        welcome_msg: String,
139        process_line: ProcessLineFunc,
140        line_is_terminated: LineCompletionFunc,
141    ) -> Result<Self> {
142        let tmanager = TermManager::new().or_else(|e| {
143            let msg = format!("failed to initialized Repl: {}", e);
144            Err(Error::InitFail(msg))
145        })?;
146        let mut lines: Vec<Line> = Vec::new();
147        lines.push(Line::new());
148        let current_line = 0;
149        let escape_buffer = Vec::new();
150        let input_state = InputType::Normal;
151
152        Ok(Repl {
153            tmanager,
154            lines,
155            current_line,
156            escape_buffer,
157            input_state,
158            process_line,
159            is_line_complete: line_is_terminated,
160            prompt,
161            banner,
162            welcome_msg,
163        })
164    }
165
166    /// Prints the welcome banner and message.
167    pub fn print_welcome(&mut self) {
168        println!("{}\n{}", self.banner, self.welcome_msg);
169    }
170
171    /// Prints the REPL prompt.
172    pub fn print_prompt(&mut self) {
173        print!("{}", self.prompt);
174    }
175
176    /// Gets a line by index from the history.
177    pub fn get_line(&self, index: usize) -> Option<&Line> {
178        self.lines.get(index)
179    }
180
181    /// Read and process input until a complete line is entered.
182    pub fn process_input(&mut self) -> Result<String> {
183        self.tmanager
184            .flush()
185            .map_err(|_| Error::IoFlush("unable to flush stdout".into()))?;
186
187        let mut output: Option<String> = None;
188
189        loop {
190            let mut buf = [0u8; 1];
191            self.tmanager
192                .read(&mut buf)
193                .map_err(|e| Error::IoRead(format!("error reading from stdin: {}", e)))?;
194            let c = buf[0];
195
196            self.input_state = match self.input_state {
197                InputType::Escape => {
198                    self.escape_buffer.push(c);
199                    if c == b'[' {
200                        InputType::EscapeSequence
201                    } else {
202                        self.escape_buffer.clear();
203                        InputType::Normal
204                    }
205                }
206                InputType::EscapeSequence => {
207                    self.escape_buffer.push(c);
208                    if self.escape_buffer.len() == 2 && self.escape_buffer[0] == b'[' {
209                        let final_byte = c;
210                        self.handle_escape_sequence(final_byte)?;
211                        self.escape_buffer.clear();
212                        InputType::Normal
213                    } else {
214                        InputType::EscapeSequence
215                    }
216                }
217                InputType::Normal => match self.handle_normal_input(c)? {
218                    ReplState::Break => {
219                        let finished_line = self
220                            .get_line(self.current_line.saturating_sub(1))
221                            .map(|l| l.text.clone())
222                            .unwrap_or_default();
223                        output = Some((self.process_line)(finished_line)?);
224
225                        self.lines.push(Line::new());
226                        self.current_line = self.lines.len() - 1;
227
228                        break;
229                    }
230                    ReplState::Continue => self.input_state,
231                },
232            };
233        }
234
235        Ok(output.unwrap_or_default())
236    }
237
238    /// Handles ANSI escape sequences (arrow keys).
239    fn handle_escape_sequence(&mut self, c: u8) -> Result<()> {
240        match c {
241            b'A' => {
242                // Up arrow: recall previous line in history
243                if self.current_line > 0 {
244                    self.current_line -= 1;
245                    self.redraw_current_line()?;
246                }
247            }
248            b'B' => {
249                // Down arrow: recall next line in history
250                if self.current_line + 1 < self.lines.len() {
251                    self.current_line += 1;
252                    self.redraw_current_line()?;
253                } else {
254                    self.lines.push(Line::new());
255                    self.current_line = self.lines.len() - 1;
256                    self.redraw_current_line()?;
257                }
258            }
259            b'C' => {
260                // Right arrow
261                if let Some(line) = self.lines.get_mut(self.current_line) {
262                    line.move_right();
263                    self.redraw_current_line()?;
264                }
265            }
266            b'D' => {
267                // Left arrow
268                if let Some(line) = self.lines.get_mut(self.current_line) {
269                    line.move_left();
270                    self.redraw_current_line()?;
271                }
272            }
273            _ => {}
274        }
275
276        self.escape_buffer.clear();
277        self.input_state = InputType::Normal;
278
279        Ok(())
280    }
281
282    /// Handles normal character input and control characters.
283    fn handle_normal_input(&mut self, c: u8) -> Result<ReplState> {
284        let current_line = self
285            .lines
286            .get_mut(self.current_line)
287            .ok_or_else(|| Error::ProcessLine("no active line".into()))?;
288
289        match c {
290            b'\n' | b'\r' => {
291                // Newline/enter line
292                if (self.is_line_complete)(current_line.text.clone()) {
293                    println!();
294                    Ok(ReplState::Break)
295                } else {
296                    current_line.insert_char('\n');
297                    Ok(ReplState::Continue)
298                }
299            }
300            0x7F => {
301                // Backspace
302                current_line.backspace();
303                self.redraw_current_line()?;
304                Ok(ReplState::Continue)
305            }
306            0x01 => {
307                // Ctrl-A = move to line start
308                current_line.cursor_pos = 0;
309                self.redraw_current_line()?;
310                Ok(ReplState::Continue)
311            }
312            0x05 => {
313                // Ctrl-E = move to line end
314                current_line.cursor_pos = current_line.text.len();
315                self.redraw_current_line()?;
316                Ok(ReplState::Continue)
317            }
318            0x1B => {
319                // Escape
320                self.input_state = InputType::Escape;
321                Ok(ReplState::Continue)
322            }
323            c if c.is_ascii_control() => Ok(ReplState::Continue),
324            c => {
325                current_line.insert_char(c as char);
326                self.redraw_current_line()?;
327                Ok(ReplState::Continue)
328            }
329        }
330    }
331
332    /// Redraws the current line with proper cursor positioning.
333    fn redraw_current_line(&mut self) -> Result<()> {
334        let line = self
335            .lines
336            .get(self.current_line)
337            .ok_or_else(|| Error::ProcessLine("no active line for redraw".into()))?;
338
339        print!("\r{}{}\x1b[K", self.prompt, line.text);
340        let right_after_prompt = self.prompt.len() + line.cursor_pos;
341        let total_len = self.prompt.len() + line.text.len();
342        if total_len > right_after_prompt {
343            print!("\x1b[{}D", total_len - right_after_prompt);
344        }
345
346        self.tmanager
347            .flush()
348            .map_err(|_| Error::IoFlush("unable to flush stdout".into()))?;
349        Ok(())
350    }
351}