use ratatui::prelude::*;
use ratatui::widgets::*;
#[derive(Debug, Clone)]
pub enum ConsoleLine {
Stdout(String),
Stderr(String),
}
pub struct LuaConsole {
pub output_history: Vec<ConsoleLine>,
pub scroll_offset: usize,
pub input: String,
pub multiline_buffer: Vec<String>,
pub is_multiline: bool,
pub command_history: Vec<String>,
pub history_index: Option<usize>,
pub temp_input: String,
pub text_edit_position: usize,
}
impl LuaConsole {
pub fn new() -> Self {
Self {
output_history: Vec::new(),
scroll_offset: 0,
input: String::new(),
multiline_buffer: Vec::new(),
is_multiline: false,
command_history: Vec::new(),
history_index: None,
temp_input: String::new(),
text_edit_position: 0,
}
}
pub fn wrap_text_to_width(&self, text: &str, max_width: usize) -> Vec<String> {
if text.is_empty() {
return vec![String::new()];
}
if text.len() <= max_width {
return vec![text.to_string()];
}
let mut lines = Vec::new();
let mut current_line = String::new();
let mut current_width = 0;
for word in text.split_whitespace() {
let word_width = word.len();
if current_width + word_width + 1 > max_width && !current_line.is_empty() {
lines.push(current_line.trim().to_string());
current_line.clear();
current_width = 0;
}
if !current_line.is_empty() {
current_line.push(' ');
current_width += 1;
}
current_line.push_str(word);
current_width += word_width;
}
if !current_line.is_empty() {
lines.push(current_line.trim().to_string());
}
if lines.is_empty() {
lines.push(text.to_string());
}
lines
}
pub fn add_output(&mut self, message: String, visible_width: usize) {
self.add_console_line(ConsoleLine::Stdout(message), visible_width);
}
pub fn add_error(&mut self, message: String, visible_width: usize) {
self.add_console_line(ConsoleLine::Stderr(message), visible_width);
}
fn add_console_line(&mut self, line: ConsoleLine, visible_width: usize) {
let message = match &line {
ConsoleLine::Stdout(msg) => msg.clone(),
ConsoleLine::Stderr(msg) => msg.clone(),
};
let wrapped_lines = self.wrap_text_to_width(&message, visible_width.saturating_sub(4));
for wrapped_line in wrapped_lines {
let console_line = match &line {
ConsoleLine::Stdout(_) => ConsoleLine::Stdout(wrapped_line),
ConsoleLine::Stderr(_) => ConsoleLine::Stderr(wrapped_line),
};
self.output_history.push(console_line);
}
if self.output_history.len() > 1000 {
self.output_history.drain(0..500);
}
}
#[allow(dead_code)]
pub fn add_error_lines(&mut self, lines: Vec<String>, visible_width: usize) {
for line in lines {
self.add_error(line, visible_width);
}
}
pub fn ensure_initialized(&mut self, visible_width: usize) {
if self.output_history.is_empty() {
self.add_output(
"Welcome to Lua REPL! Type Lua code and press Enter to execute.".to_string(),
visible_width,
);
self.add_output(
"Supports multiline input: functions, if/do blocks, etc.".to_string(),
visible_width,
);
self.add_output(
"Use print() to output text, dir() to explore, help() for assistance.".to_string(),
visible_width,
);
self.add_output(
"Press Esc to exit, Ctrl+C to cancel multiline input.".to_string(),
visible_width,
);
self.add_output(
"Use ↑/↓ arrows to navigate command history, Ctrl+L to clear screen.".to_string(),
visible_width,
);
self.add_output(
"Press Tab for function/variable completion, Esc to exit.".to_string(),
visible_width,
);
self.add_output("".to_string(), visible_width);
}
}
#[allow(dead_code)]
pub fn clear(&mut self) {
self.scroll_offset = 0;
self.output_history.clear();
}
pub fn history_up(&mut self) -> bool {
match self.history_index {
None => {
if !self.command_history.is_empty() {
self.temp_input = self.input.clone();
self.history_index = Some(self.command_history.len() - 1);
self.input = self.command_history[self.command_history.len() - 1].clone();
self.text_edit_position = self.input.len();
true
} else {
false
}
}
Some(index) => {
if index > 0 {
self.history_index = Some(index - 1);
self.input = self.command_history[index - 1].clone();
self.text_edit_position = self.input.len();
true
} else {
false
}
}
}
}
pub fn history_down(&mut self) -> bool {
match self.history_index {
None => false,
Some(index) => {
if index < self.command_history.len() - 1 {
self.history_index = Some(index + 1);
self.input = self.command_history[index + 1].clone();
self.text_edit_position = self.input.len();
true
} else {
self.history_index = None;
self.input = self.temp_input.clone();
self.text_edit_position = self.input.len();
true
}
}
}
}
pub fn reset_history_navigation(&mut self) {
self.history_index = None;
self.temp_input.clear();
}
pub fn add_to_history(&mut self, command: String) {
if command.trim().is_empty() {
return;
}
if self.command_history.last() != Some(&command) {
self.command_history.push(command);
}
if self.command_history.len() > 100 {
self.command_history.drain(0..50);
}
}
pub fn is_input_complete(&self) -> bool {
let input = self.input.trim();
if input.is_empty() {
return false;
}
let input_lower = input.to_lowercase();
if input_lower.contains("if ") && !input_lower.contains(" then") {
return false;
}
if input_lower.contains("if ")
&& input_lower.contains(" then")
&& !input_lower.contains(" end")
{
return false;
}
if input_lower.contains("for ") && !input_lower.contains(" do") {
return false;
}
if input_lower.contains("for ")
&& input_lower.contains(" do")
&& !input_lower.contains(" end")
{
return false;
}
if input_lower.contains("while ") && !input_lower.contains(" do") {
return false;
}
if input_lower.contains("while ")
&& input_lower.contains(" do")
&& !input_lower.contains(" end")
{
return false;
}
if input_lower.contains("function ") && !input_lower.contains(" end") {
return false;
}
let mut paren_count = 0;
let mut bracket_count = 0;
let mut brace_count = 0;
let mut in_string = false;
let mut escape_next = false;
for ch in input.chars() {
if escape_next {
escape_next = false;
continue;
}
if ch == '\\' && in_string {
escape_next = true;
continue;
}
if ch == '"' || ch == '\'' {
in_string = !in_string;
continue;
}
if !in_string {
match ch {
'(' => paren_count += 1,
')' => paren_count -= 1,
'[' => bracket_count += 1,
']' => bracket_count -= 1,
'{' => brace_count += 1,
'}' => brace_count -= 1,
_ => {}
}
}
}
paren_count == 0 && bracket_count == 0 && brace_count == 0
}
#[allow(dead_code)]
pub fn scroll_up(&mut self, visible_height: usize) {
let visible_lines = visible_height.saturating_sub(2);
let total_lines = self.output_history.len() + 1; if total_lines > visible_lines
&& self.scroll_offset < total_lines.saturating_sub(visible_lines)
{
self.scroll_offset += 1;
}
}
#[allow(dead_code)]
pub fn scroll_down(&mut self) {
if self.scroll_offset > 0 {
self.scroll_offset -= 1;
}
}
#[allow(dead_code)]
pub fn scroll_to_top(&mut self) {
self.scroll_offset = 0;
}
#[allow(dead_code)]
pub fn scroll_to_bottom(&mut self, visible_height: usize) {
let visible_lines = visible_height.saturating_sub(2);
let total_lines = self.output_history.len() + 1; if total_lines > visible_lines {
self.scroll_offset = total_lines.saturating_sub(visible_lines);
}
}
#[allow(dead_code)]
pub fn page_up(&mut self, visible_height: usize) {
let visible_lines = visible_height.saturating_sub(2);
for _ in 0..visible_lines {
self.scroll_up(visible_height);
}
}
#[allow(dead_code)]
pub fn page_down(&mut self, visible_height: usize) {
let visible_lines = visible_height.saturating_sub(2);
for _ in 0..visible_lines {
self.scroll_down();
}
}
}
pub fn render_console_output<'a>(console: &'a LuaConsole, visible_height: usize) -> Paragraph<'a> {
let visible_lines = visible_height.saturating_sub(2); let start_line = console.scroll_offset;
let mut lines: Vec<Line> = console
.output_history
.iter()
.skip(start_line)
.take(visible_lines)
.map(|console_line| {
match console_line {
ConsoleLine::Stdout(msg) => {
if msg.starts_with("> ") {
Line::from(Span::styled(
msg.clone(),
Style::default().fg(Color::Green).bold(),
))
} else {
Line::from(Span::styled(msg.clone(), Style::default().fg(Color::White)))
}
}
ConsoleLine::Stderr(msg) => {
Line::from(Span::styled(msg.clone(), Style::default().fg(Color::Red)))
}
}
})
.collect();
if lines.len() < visible_lines {
let input_line = render_console_input_line(console);
lines.push(input_line);
}
let paragraph = Paragraph::new(lines)
.block(
Block::default()
.title("Lua REPL Output")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Blue)),
)
.wrap(Wrap { trim: false });
paragraph
}
fn render_console_input_line(console: &LuaConsole) -> Line {
let input = &console.input;
let cursor_pos = console.text_edit_position;
let prompt = if console.is_multiline {
">> " } else {
"> " };
let mut spans = vec![Span::styled(
prompt.to_string(),
Style::default().fg(Color::Green).bold(),
)];
if input.is_empty() {
spans.push(Span::styled(
" ".to_string(),
Style::default().fg(Color::Black).bg(Color::Green),
));
} else {
let chars: Vec<char> = input.chars().collect();
for (i, &ch) in chars.iter().enumerate() {
if i == cursor_pos {
spans.push(Span::styled(
ch.to_string(),
Style::default().fg(Color::Black).bg(Color::Green),
));
} else {
spans.push(Span::styled(
ch.to_string(),
Style::default().fg(Color::Green).bold(),
));
}
}
if cursor_pos >= chars.len() {
spans.push(Span::styled(
" ".to_string(),
Style::default().fg(Color::Black).bg(Color::Green),
));
}
}
Line::from(spans)
}
pub fn render_console_footer<'a>(
console: &LuaConsole,
settings: &crate::settings::Settings,
) -> Block<'a> {
let mut spans = vec![];
if console.is_multiline {
render_tag(
&mut spans,
"Multiline Mode",
"Ctrl+C to cancel",
settings.colors.footer.command,
&settings.global.symbols,
);
}
render_tag(
&mut spans,
"Lua REPL",
"ESC to exit",
settings.colors.footer.other,
&settings.global.symbols,
);
render_tag(
&mut spans,
"History",
"↑↓ arrows",
settings.colors.footer.other,
&settings.global.symbols,
);
render_tag(
&mut spans,
"Scroll",
"Ctrl+↑↓ PgUp PgDn",
settings.colors.footer.other,
&settings.global.symbols,
);
if let Some(index) = console.history_index {
if !console.command_history.is_empty() {
render_tag(
&mut spans,
"Pos",
&format!("{}/{}", index + 1, console.command_history.len()),
settings.colors.footer.command,
&settings.global.symbols,
);
}
} else if !console.command_history.is_empty() {
render_tag(
&mut spans,
"History",
&format!("{}", console.command_history.len()),
settings.colors.footer.other,
&settings.global.symbols,
);
}
let line = Line::from(spans);
Block::default()
.title_style(Style::default().fg(Color::Black).bg(Color::LightGreen))
.title(line)
}
fn render_tag(
spans: &mut Vec<Span>,
label: &str,
value: &str,
style: Style,
symbols: &crate::settings::SymbolSettings,
) {
use crate::utils::reverse_style;
let rstyle = reverse_style(style);
spans.push(Span::styled(
format!("{}{}{}", symbols.tag_initial, label, symbols.tag_mid_left),
rstyle,
));
spans.push(Span::styled(
format!("{}{}{}", symbols.tag_mid_right, value, symbols.tag_end),
style,
));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wrap_text_to_width() {
let console = LuaConsole::new();
let short_text = "Hello world";
let wrapped = console.wrap_text_to_width(short_text, 76); assert_eq!(wrapped.len(), 1);
assert_eq!(wrapped[0], "Hello world");
let long_text = "This is a very long line of text that should definitely wrap when the width is limited to a reasonable size for console output";
let wrapped = console.wrap_text_to_width(long_text, 40);
assert!(wrapped.len() > 1);
for line in &wrapped {
assert!(
line.len() <= 40,
"Line '{}' is too long: {} chars",
line,
line.len()
);
}
let empty_wrapped = console.wrap_text_to_width("", 40);
assert_eq!(empty_wrapped.len(), 1);
assert_eq!(empty_wrapped[0], "");
let long_word = "supercalifragilisticexpialidocious";
let wrapped_word = console.wrap_text_to_width(long_word, 20);
assert_eq!(wrapped_word.len(), 1);
assert_eq!(wrapped_word[0], long_word);
}
#[test]
fn test_add_output_with_wrapping() {
let mut console = LuaConsole::new();
let visible_width = 50;
let long_message = "This is a very long error message that should be wrapped across multiple lines when added to the Lua console output history";
console.add_output(long_message.to_string(), visible_width);
assert!(console.output_history.len() > 1);
for console_line in &console.output_history {
let line_len = match console_line {
ConsoleLine::Stdout(msg) => msg.len(),
ConsoleLine::Stderr(msg) => msg.len(),
};
assert!(
line_len <= 46,
"Line '{:?}' is too long: {} chars",
console_line,
line_len
); }
}
#[test]
fn test_input_completion() {
let mut console = LuaConsole::new();
console.input = "if true then".to_string();
assert!(!console.is_input_complete());
console.input = "if true then end".to_string();
assert!(console.is_input_complete());
console.input = "local t = { a = 1, b = { c = 2 } }".to_string();
assert!(console.is_input_complete());
console.input = r#"print("hello world")"#.to_string();
assert!(console.is_input_complete());
}
}