use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
prelude::Stylize,
style::{Color, Style},
text::Line,
widgets::{Block, Borders, Paragraph},
};
use super::Component;
use crate::tui::actions::Action;
#[derive(Default)]
pub struct LinePrompt {
digits: String,
}
impl LinePrompt {
pub fn new() -> Self {
Self::default()
}
pub fn popup_area(&self, area: Rect) -> Rect {
let width = 40.min(area.width);
let height = 4.min(area.height);
let x = area.x + (area.width.saturating_sub(width)) / 2;
let y = area.y + (area.height.saturating_sub(height)) / 2;
Rect::new(x, y, width, height)
}
pub fn cursor_screen_position(&self, area: Rect) -> (u16, u16) {
let col = area.x + 2 + self.digits.chars().count() as u16;
(col, area.y + 1)
}
}
impl Component for LinePrompt {
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
match key.code {
KeyCode::Char(c) if c.is_ascii_digit() => {
self.digits.push(c);
Ok(Some(Action::Render))
}
KeyCode::Backspace => {
self.digits.pop();
Ok(Some(Action::Render))
}
KeyCode::Enter => match self.digits.parse::<usize>() {
Ok(line) if line > 0 => Ok(Some(Action::JumpToLineSubmitted(line))),
_ => Ok(Some(Action::DialogCancelled)),
},
KeyCode::Esc => Ok(Some(Action::DialogCancelled)),
_ => Ok(None),
}
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(1)])
.split(area);
let block = Block::default()
.title(" Go to line ".bold().fg(Color::Cyan))
.borders(Borders::ALL)
.border_style(Style::new().fg(Color::Cyan));
frame.render_widget(
Paragraph::new(Line::from(format!(":{}", self.digits))).block(block),
layout[0],
);
frame.render_widget(
Paragraph::new(" Enter: jump | Esc: cancel ").style(Style::new().fg(Color::DarkGray)),
layout[1],
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyModifiers;
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn digits_accumulate_and_submit_as_a_line_number() {
let mut prompt = LinePrompt::new();
prompt.handle_key_event(key(KeyCode::Char('4'))).unwrap();
prompt.handle_key_event(key(KeyCode::Char('2'))).unwrap();
assert_eq!(
prompt.handle_key_event(key(KeyCode::Enter)).unwrap(),
Some(Action::JumpToLineSubmitted(42))
);
}
#[test]
fn non_digit_characters_are_ignored_entirely() {
let mut prompt = LinePrompt::new();
assert_eq!(
prompt.handle_key_event(key(KeyCode::Char('x'))).unwrap(),
None
);
prompt.handle_key_event(key(KeyCode::Char('7'))).unwrap();
assert_eq!(
prompt.handle_key_event(key(KeyCode::Enter)).unwrap(),
Some(Action::JumpToLineSubmitted(7))
);
}
#[test]
fn backspace_edits_and_empty_enter_cancels() {
let mut prompt = LinePrompt::new();
prompt.handle_key_event(key(KeyCode::Char('9'))).unwrap();
prompt.handle_key_event(key(KeyCode::Backspace)).unwrap();
assert_eq!(
prompt.handle_key_event(key(KeyCode::Enter)).unwrap(),
Some(Action::DialogCancelled)
);
}
#[test]
fn esc_cancels() {
let mut prompt = LinePrompt::new();
assert_eq!(
prompt.handle_key_event(key(KeyCode::Esc)).unwrap(),
Some(Action::DialogCancelled)
);
}
#[test]
fn zero_is_rejected_as_a_cancel_not_a_jump() {
let mut prompt = LinePrompt::new();
prompt.handle_key_event(key(KeyCode::Char('0'))).unwrap();
assert_eq!(
prompt.handle_key_event(key(KeyCode::Enter)).unwrap(),
Some(Action::DialogCancelled)
);
}
}