use super::vim_input::{Outcome, VimInput};
use crate::action::Action;
use crate::commands;
use crate::theme::Theme;
use ratatui::Frame;
use ratatui::crossterm::cursor::SetCursorStyle;
use ratatui::crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
pub struct CommandLine {
pub input: VimInput,
selected: usize,
}
impl CommandLine {
pub(crate) fn diagnostics(&self, full: bool) -> serde_json::Value {
serde_json::json!({"selected":self.selected, "input":self.input.diagnostics(full)})
}
pub fn new() -> Self {
CommandLine {
input: VimInput::transient(),
selected: 0,
}
}
fn options(&self) -> Vec<&'static commands::Command> {
commands::filter(&self.input.value())
}
pub fn handle_key(&mut self, key: KeyEvent) -> Action {
match key.code {
KeyCode::Down => {
let len = self.options().len();
if len > 0 {
self.selected = (self.selected + 1).min(len - 1);
}
return Action::Noop;
}
KeyCode::Up => {
self.selected = self.selected.saturating_sub(1);
return Action::Noop;
}
_ => {}
}
match self.input.handle_key(key) {
Outcome::Submitted => {
let options = self.options();
match options.get(self.selected) {
Some(cmd) => Action::RunCommand(cmd.name.to_string()),
None => {
let text = self.input.value();
if !text.is_empty() && text.bytes().all(|b| b.is_ascii_digit()) {
Action::RunCommand(text)
} else {
Action::ClosePopup }
}
}
}
Outcome::Cancelled => Action::ClosePopup,
Outcome::Changed => {
self.selected = 0;
Action::Noop
}
Outcome::Noop => Action::Noop,
}
}
pub fn cursor_style(&self) -> Option<SetCursorStyle> {
Some(SetCursorStyle::SteadyBar)
}
pub fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
let sem = &theme.semantic;
let options = self.options();
let shown = options.len().min(6) as u16;
if shown > 0 {
let list_area = Rect {
x: area.x,
y: area.y + area.height - 1 - shown - 2,
width: area.width.min(56),
height: shown + 2,
};
frame.render_widget(Clear, list_area);
let block = Block::default()
.borders(Borders::ALL)
.border_type(theme.border_type())
.border_style(Style::default().fg(sem.border_focused))
.style(Style::default().bg(sem.mantle));
let inner = block.inner(list_area);
frame.render_widget(block, list_area);
let rows: Vec<Line> = options
.iter()
.take(6)
.enumerate()
.map(|(i, cmd)| {
let picked = i == self.selected;
let style = if picked {
Style::default().fg(sem.selection_fg).bg(sem.selection_bg)
} else {
Style::default().fg(sem.text)
};
Line::from(vec![
Span::styled(
if picked { "▌" } else { " " },
Style::default().fg(sem.border_focused),
),
Span::styled(
format!("{:10}", cmd.name),
style.add_modifier(Modifier::BOLD),
),
Span::styled(cmd.summary, Style::default().fg(sem.subtext0)),
])
})
.collect();
frame.render_widget(Paragraph::new(rows), inner);
}
let strip = Rect {
x: area.x,
y: area.y + area.height - 1,
width: area.width,
height: 1,
};
frame.render_widget(Clear, strip);
let value = self.input.value();
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::styled(
":",
Style::default()
.fg(sem.mode_leader)
.add_modifier(Modifier::BOLD),
),
Span::styled(value.clone(), Style::default().fg(sem.text)),
]))
.style(Style::default().bg(sem.mantle)),
strip,
);
let x = strip.x + 1 + self.input.cursor() as u16;
if x < strip.x + strip.width {
crate::diagnostics::place_cursor(frame, (x, strip.y));
}
}
}
impl Default for CommandLine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::crossterm::event::{KeyEventKind, KeyEventState, KeyModifiers};
fn key(code: KeyCode) -> KeyEvent {
KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}
}
#[test]
fn typing_filters_and_enter_runs_selected() {
let mut cl = CommandLine::new();
for c in "set".chars() {
assert_eq!(cl.handle_key(key(KeyCode::Char(c))), Action::Noop);
}
assert_eq!(
cl.handle_key(key(KeyCode::Enter)),
Action::RunCommand("settings".into())
);
}
#[test]
fn arrows_move_the_pick_and_esc_cancels() {
let mut cl = CommandLine::new();
assert_eq!(cl.selected, 0);
cl.handle_key(key(KeyCode::Down));
assert_eq!(cl.selected, 1);
cl.handle_key(key(KeyCode::Up));
assert_eq!(cl.selected, 0);
assert_eq!(cl.handle_key(key(KeyCode::Esc)), Action::ClosePopup);
}
#[test]
fn clone_command_runs() {
let mut cl = CommandLine::new();
for c in "clone".chars() {
cl.handle_key(key(KeyCode::Char(c)));
}
assert_eq!(
cl.handle_key(key(KeyCode::Enter)),
Action::RunCommand("clone".into())
);
}
}