use std::io::{self, Write};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crossterm::{cursor, queue, terminal};
use crate::event::InputMode;
pub enum InputAction {
None,
Activated(char),
Submit(String, InputMode),
ViewMessage(String),
Cancel,
Interrupt,
EndSession,
}
#[derive(Default)]
pub struct InputHandler {
buffer: String,
active: bool,
}
impl InputHandler {
pub fn new() -> Self {
Self::default()
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn activate(&mut self) {
self.buffer.clear();
self.active = true;
}
pub fn deactivate(&mut self) {
self.active = false;
self.buffer.clear();
}
pub fn handle_key(&mut self, event: &KeyEvent) -> InputAction {
if !self.active {
match event.code {
KeyCode::Char('c') if event.modifiers.contains(KeyModifiers::CONTROL) => {
return InputAction::Interrupt;
}
KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => {
return InputAction::EndSession;
}
KeyCode::Char(c) => {
self.activate();
self.buffer.push(c);
return InputAction::Activated(c);
}
_ => return InputAction::None,
}
}
match event.code {
KeyCode::Char('c') if event.modifiers.contains(KeyModifiers::CONTROL) => {
InputAction::Interrupt
}
KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => {
InputAction::EndSession
}
KeyCode::Char(c) => {
self.buffer.push(c);
let mut out = io::stdout();
queue!(out, crossterm::style::Print(c)).ok();
out.flush().ok();
InputAction::None
}
KeyCode::Backspace => {
if !self.buffer.is_empty() {
self.buffer.pop();
let mut out = io::stdout();
queue!(
out,
cursor::MoveLeft(1),
terminal::Clear(terminal::ClearType::UntilNewLine),
)
.ok();
out.flush().ok();
}
InputAction::None
}
KeyCode::Enter => {
let text = self.buffer.clone();
self.deactivate();
let mut out = io::stdout();
queue!(
out,
crossterm::style::Print("\r"),
terminal::Clear(terminal::ClearType::CurrentLine),
)
.ok();
out.flush().ok();
if text.is_empty() {
return InputAction::None;
}
if let Some(query) = parse_view_command(&text) {
return InputAction::ViewMessage(query);
}
let mode = if event.modifiers.contains(KeyModifiers::ALT) {
InputMode::FollowUp
} else {
InputMode::Steering
};
InputAction::Submit(text, mode)
}
KeyCode::Esc => {
self.deactivate();
let mut out = io::stdout();
queue!(
out,
crossterm::style::Print("\r"),
terminal::Clear(terminal::ClearType::CurrentLine),
)
.ok();
out.flush().ok();
InputAction::Cancel
}
_ => InputAction::None,
}
}
}
fn parse_view_command(text: &str) -> Option<String> {
let rest = text.trim().strip_prefix(':')?;
if rest.is_empty() {
return None;
}
if let Some((left, right)) = rest.split_once('/')
&& let (Ok(p), Ok(c)) = (left.parse::<usize>(), right.parse::<usize>())
{
if p > 0 && c > 0 {
return Some(format!("{p}/{c}"));
}
return None;
}
if let Ok(n) = rest.parse::<usize>() {
return if n > 0 { Some(n.to_string()) } else { None };
}
let (name, tail) = if let Some(bracket) = rest.find('[') {
let after = rest[bracket + 1..].strip_suffix(']')?;
let _: isize = after.parse().ok()?;
(&rest[..bracket], &rest[bracket..])
} else {
(rest, "")
};
if name.is_empty()
|| !name
.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '-' | ':' | '_'))
{
return None;
}
Some(format!("{name}{tail}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_view_command_numeric() {
assert_eq!(parse_view_command(":1"), Some("1".to_string()));
assert_eq!(parse_view_command(":42"), Some("42".to_string()));
assert_eq!(parse_view_command(":0"), None);
assert_eq!(parse_view_command(": 3"), None); }
#[test]
fn parse_view_command_slash_notation() {
assert_eq!(parse_view_command(":2/1"), Some("2/1".to_string()));
assert_eq!(parse_view_command(":10/3"), Some("10/3".to_string()));
assert_eq!(parse_view_command(":0/1"), None);
assert_eq!(parse_view_command(":1/0"), None);
assert_eq!(parse_view_command(":a/1"), None);
}
#[test]
fn parse_view_command_label() {
assert_eq!(parse_view_command(":Bash"), Some("Bash".to_string()));
assert_eq!(parse_view_command(":Bash[0]"), Some("Bash[0]".to_string()));
assert_eq!(
parse_view_command(":Edit[-1]"),
Some("Edit[-1]".to_string())
);
assert_eq!(
parse_view_command(":Thinking[2]"),
Some("Thinking[2]".to_string())
);
assert_eq!(
parse_view_command(":llms-fetch:fetch"),
Some("llms-fetch:fetch".to_string())
);
}
#[test]
fn parse_view_command_invalid() {
assert_eq!(parse_view_command("hello"), None);
assert_eq!(parse_view_command(""), None);
assert_eq!(parse_view_command(":"), None);
assert_eq!(parse_view_command(":[0]"), None); assert_eq!(parse_view_command(":Bash[abc]"), None); }
}