Files
claw-code/rust/crates/rusty-claude-cli/src/input.rs
Yeachan-Heo 44e4758078 feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification

All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00

249 lines
6.3 KiB
Rust

use std::io::{self, Write};
use crossterm::cursor::MoveToColumn;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
use crossterm::queue;
use crossterm::style::Print;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputBuffer {
buffer: String,
cursor: usize,
}
impl InputBuffer {
#[must_use]
pub fn new() -> Self {
Self {
buffer: String::new(),
cursor: 0,
}
}
pub fn insert(&mut self, ch: char) {
self.buffer.insert(self.cursor, ch);
self.cursor += ch.len_utf8();
}
pub fn insert_newline(&mut self) {
self.insert('\n');
}
pub fn backspace(&mut self) {
if self.cursor == 0 {
return;
}
let previous = self.buffer[..self.cursor]
.char_indices()
.last()
.map_or(0, |(idx, _)| idx);
self.buffer.drain(previous..self.cursor);
self.cursor = previous;
}
pub fn move_left(&mut self) {
if self.cursor == 0 {
return;
}
self.cursor = self.buffer[..self.cursor]
.char_indices()
.last()
.map_or(0, |(idx, _)| idx);
}
pub fn move_right(&mut self) {
if self.cursor >= self.buffer.len() {
return;
}
if let Some(next) = self.buffer[self.cursor..].chars().next() {
self.cursor += next.len_utf8();
}
}
pub fn move_home(&mut self) {
self.cursor = 0;
}
pub fn move_end(&mut self) {
self.cursor = self.buffer.len();
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.buffer
}
#[cfg(test)]
#[must_use]
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn clear(&mut self) {
self.buffer.clear();
self.cursor = 0;
}
}
pub struct LineEditor {
prompt: String,
}
impl LineEditor {
#[must_use]
pub fn new(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
}
}
pub fn read_line(&self) -> io::Result<Option<String>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
let mut input = InputBuffer::new();
self.redraw(&mut stdout, &input)?;
loop {
let event = event::read()?;
if let Event::Key(key) = event {
match Self::handle_key(key, &mut input) {
EditorAction::Continue => self.redraw(&mut stdout, &input)?,
EditorAction::Submit => {
disable_raw_mode()?;
writeln!(stdout)?;
return Ok(Some(input.as_str().to_owned()));
}
EditorAction::Cancel => {
disable_raw_mode()?;
writeln!(stdout)?;
return Ok(None);
}
}
}
}
}
fn handle_key(key: KeyEvent, input: &mut InputBuffer) -> EditorAction {
match key {
KeyEvent {
code: KeyCode::Char('c'),
modifiers,
..
} if modifiers.contains(KeyModifiers::CONTROL) => EditorAction::Cancel,
KeyEvent {
code: KeyCode::Char('j'),
modifiers,
..
} if modifiers.contains(KeyModifiers::CONTROL) => {
input.insert_newline();
EditorAction::Continue
}
KeyEvent {
code: KeyCode::Enter,
modifiers,
..
} if modifiers.contains(KeyModifiers::SHIFT) => {
input.insert_newline();
EditorAction::Continue
}
KeyEvent {
code: KeyCode::Enter,
..
} => EditorAction::Submit,
KeyEvent {
code: KeyCode::Backspace,
..
} => {
input.backspace();
EditorAction::Continue
}
KeyEvent {
code: KeyCode::Left,
..
} => {
input.move_left();
EditorAction::Continue
}
KeyEvent {
code: KeyCode::Right,
..
} => {
input.move_right();
EditorAction::Continue
}
KeyEvent {
code: KeyCode::Home,
..
} => {
input.move_home();
EditorAction::Continue
}
KeyEvent {
code: KeyCode::End, ..
} => {
input.move_end();
EditorAction::Continue
}
KeyEvent {
code: KeyCode::Esc, ..
} => {
input.clear();
EditorAction::Cancel
}
KeyEvent {
code: KeyCode::Char(ch),
modifiers,
..
} if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT => {
input.insert(ch);
EditorAction::Continue
}
_ => EditorAction::Continue,
}
}
fn redraw(&self, out: &mut impl Write, input: &InputBuffer) -> io::Result<()> {
let display = input.as_str().replace('\n', "\\n\n> ");
queue!(
out,
MoveToColumn(0),
Clear(ClearType::CurrentLine),
Print(&self.prompt),
Print(display),
)?;
out.flush()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EditorAction {
Continue,
Submit,
Cancel,
}
#[cfg(test)]
mod tests {
use super::InputBuffer;
#[test]
fn supports_basic_line_editing() {
let mut input = InputBuffer::new();
input.insert('h');
input.insert('i');
input.move_end();
input.insert_newline();
input.insert('x');
assert_eq!(input.as_str(), "hi\nx");
assert_eq!(input.cursor(), 4);
input.move_left();
input.backspace();
assert_eq!(input.as_str(), "hix");
assert_eq!(input.cursor(), 2);
}
}