raftcli 1.15.5

Command line interface for raft framework and serial monitoring
use crate::line_editor::{LineEditAction, LineEditor};
use crate::native_terminal::{self, KeyEvent, NativeTerminal};


pub struct TerminalIO {
    cursor_col: u16,
    cursor_row: u16,
    cols: u16,
    rows: u16,
    is_error: bool,
    editor: LineEditor,
    terminal: NativeTerminal,
}

impl TerminalIO {
    pub fn new(history_file_path: &str) -> TerminalIO {
        let terminal = NativeTerminal::new().expect("Failed to initialize terminal");
        TerminalIO {
            cursor_col: 0,
            cursor_row: 0,
            cols: 0,
            rows: 0,
            is_error: false,
            editor: LineEditor::new(history_file_path),
            terminal,
        }
    }

    pub fn init(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        let (cols, rows) = self.terminal.size();
        self.cols = cols;
        self.rows = rows;

        self.terminal.clear_screen();

        // Set scroll region to all rows except the last (prompt row)
        if self.rows > 1 {
            self.terminal.set_scroll_region(0, self.rows - 2);
        }
        self.terminal.move_to(0, 0);
        Ok(())
    }

    pub fn cleanup(&mut self) {
        self.terminal.cleanup();
    }

    pub fn handle_key_event(
        &mut self,
        key_event: KeyEvent,
        send_command: impl FnOnce(String),
    ) -> bool {
        match self.editor.handle_key(&key_event) {
            LineEditAction::Exit => return false,
            LineEditAction::Submit(command) => {
                send_command(command);
                self.print("", false);
            }
            LineEditAction::Updated => {
                self.print("", false);
            }
            LineEditAction::None => {}
        }
        true
    }

    /// Poll for a terminal event with the given timeout.
    pub fn poll_event(&mut self, timeout: std::time::Duration) -> bool {
        self.terminal.poll_event(timeout)
    }

    /// Read the next terminal event.
    pub fn read_event(&mut self) -> Option<native_terminal::TermEvent> {
        self.terminal.read_event()
    }

    pub fn handle_resize(&mut self, cols: u16, rows: u16) {
        self.cols = cols;
        self.rows = rows;
        // Clamp cursor to new bounds
        if self.cursor_row >= rows.saturating_sub(1) {
            self.cursor_row = rows.saturating_sub(2);
        }
        // Update scroll region
        if self.rows > 1 {
            self.terminal.set_scroll_region(0, self.rows - 2);
        }
        self.draw_prompt();
    }
    
    pub fn print(&mut self, data: &str, force_show: bool) {
        if !force_show && self.is_error {
            return;
        }

        // Clear error flag
        self.is_error = false;

        // Hide cursor while updating the scroll region
        self.terminal.hide_cursor();

        // Clear the prompt line
        self.terminal.move_to(0, self.rows - 1);
        self.terminal.clear_line();

        // Move into the scroll region to the saved output position
        self.terminal.move_to(self.cursor_col, self.cursor_row);

        // Set scroll region so the terminal handles scrolling within the output area
        if self.rows > 1 {
            self.terminal.set_scroll_region(0, self.rows - 2);
        }
        self.terminal.move_to(self.cursor_col, self.cursor_row);

        // Display the received data — let the terminal scroll naturally
        if !data.is_empty() {
            self.display_received_data(data);
        }

        // After printing, we need to know where the cursor ended up.
        // Since the terminal handles scrolling natively, we track position
        // by counting what we wrote.
        self.update_cursor_after_print(data);

        // Draw the prompt on the fixed bottom row
        self.draw_prompt();
    }

    fn update_cursor_after_print(&mut self, data: &str) {
        let max_row = self.rows.saturating_sub(2);
        for ch in data.chars() {
            match ch {
                '\n' => {
                    self.cursor_col = 0;
                    if self.cursor_row < max_row {
                        self.cursor_row += 1;
                    }
                    // If at max_row, the terminal scrolled — row stays
                }
                '\r' => {
                    self.cursor_col = 0;
                }
                c if !c.is_control() => {
                    self.cursor_col += 1;
                    if self.cursor_col >= self.cols {
                        self.cursor_col = 0;
                        if self.cursor_row < max_row {
                            self.cursor_row += 1;
                        }
                    }
                }
                _ => {}
            }
        }
    }

    fn draw_prompt(&mut self) {
        // Temporarily reset scroll region so we can write on the last row
        self.terminal.reset_scroll_region();
        self.terminal.move_to(0, self.rows - 1);
        self.terminal.clear_line();
        self.terminal.set_color_yellow();
        let buf = self.editor.buffer_str();
        self.terminal.write_str(&format!("> {}", buf));
        self.terminal.reset_color();
        // Restore scroll region first (DECSTBM can reset cursor position)
        if self.rows > 1 {
            self.terminal.set_scroll_region(0, self.rows - 2);
        }
        // Position cursor on the prompt line *after* restoring scroll region
        let cursor_col = 2 + self.editor.cursor_pos() as u16;
        self.terminal.move_to(cursor_col, self.rows - 1);
        self.terminal.show_cursor();
        self.terminal.flush();
    }

    pub fn show_error(&mut self, error_msg: &str) {
        self.terminal.reset_scroll_region();
        self.terminal.move_to(0, self.rows - 1);
        self.terminal.clear_line();
        self.terminal.set_color_red();
        self.terminal.write_str(&format!("! {}", error_msg));
        self.terminal.reset_color();
        self.terminal.flush();
        if self.rows > 1 {
            self.terminal.set_scroll_region(0, self.rows - 2);
        }
        self.is_error = true;
    }

    pub fn show_info(&mut self, info_msg: &str) {
        self.terminal.reset_scroll_region();
        self.terminal.move_to(0, self.rows - 1);
        self.terminal.clear_line();
        self.terminal.set_color_green();
        self.terminal.write_str(&format!("> {}", info_msg));
        self.terminal.reset_color();
        self.terminal.flush();
        if self.rows > 1 {
            self.terminal.set_scroll_region(0, self.rows - 2);
        }
    }

    pub fn clear_info(&mut self) {
        self.print("", true);
    }

    pub fn display_received_data(&mut self, data: &str) {
        self.terminal.write_str(data);
    }
}