use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::Widget;
use crate::app::prompt::PromptBuffer;
use crate::theme::Theme;
pub const PROMPT_CUE: &str = "> ";
pub const PROMPT_CONTINUATION: &str = ". ";
const CUE_WIDTH: u16 = 2;
#[derive(Debug)]
pub struct PromptWidget<'a> {
pub prompt: &'a PromptBuffer,
pub theme: Theme,
}
impl PromptWidget<'_> {
#[must_use]
pub fn cursor_position(&self) -> (u16, u16) {
let col = CUE_WIDTH.saturating_add(self.prompt.cursor_column());
let row = u16::try_from(self.prompt.cursor_row()).unwrap_or(u16::MAX);
(col, row)
}
#[must_use]
pub fn cursor_column(&self) -> u16 {
self.cursor_position().0
}
}
impl Widget for PromptWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.height == 0 || area.width == 0 {
return;
}
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
buf[(x, y)].set_char(' ');
}
}
let cue_style = Style::default().fg(self.theme.primary);
let body_style = Style::default().fg(self.theme.primary);
let cont_style = Style::default().fg(self.theme.metadata);
let visible_rows = usize::from(area.height);
for visible_row in 0..visible_rows {
let buf_row = visible_row;
let line_chars = self.prompt.line(buf_row);
let Some(chars) = line_chars else {
break;
};
let body: String = chars.iter().collect();
let cue = if buf_row == 0 {
PROMPT_CUE
} else {
PROMPT_CONTINUATION
};
let cue_span = Span::styled(cue, if buf_row == 0 { cue_style } else { cont_style });
let body_span = Span::styled(body, body_style);
let row_area = Rect {
x: area.x,
y: area.y + u16::try_from(visible_row).unwrap_or(u16::MAX),
width: area.width,
height: 1,
};
Line::from(vec![cue_span, body_span]).render(row_area, buf);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
fn render(prompt: &PromptBuffer, width: u16, height: u16) -> Vec<String> {
let backend = TestBackend::new(width, height);
let mut term = Terminal::new(backend).expect("terminal");
term.draw(|f| {
let w = PromptWidget {
prompt,
theme: Theme::default(),
};
f.render_widget(w, f.area());
})
.expect("draw");
let buf = term.backend().buffer().clone();
(0..buf.area.height)
.map(|y| {
(0..buf.area.width)
.map(|x| buf[(x, y)].symbol().to_string())
.collect::<String>()
})
.collect()
}
#[test]
fn single_row_prompt_uses_primary_cue() {
let mut p = PromptBuffer::new();
for c in "/help".chars() {
p.insert(c);
}
let lines = render(&p, 20, 1);
assert_eq!(lines[0].trim_end(), "> /help");
}
#[test]
fn second_row_uses_continuation_cue() {
let mut p = PromptBuffer::new();
for c in "abc".chars() {
p.insert(c);
}
p.insert_newline();
for c in "def".chars() {
p.insert(c);
}
let lines = render(&p, 20, 2);
assert_eq!(lines[0].trim_end(), "> abc");
assert_eq!(lines[1].trim_end(), ". def");
}
#[test]
fn cursor_position_accounts_for_cue_and_row() {
let mut p = PromptBuffer::new();
for c in "ab".chars() {
p.insert(c);
}
p.insert_newline();
for c in "cdef".chars() {
p.insert(c);
}
let w = PromptWidget {
prompt: &p,
theme: Theme::default(),
};
assert_eq!(w.cursor_position(), (6, 1));
}
}