| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- use std::io::{self, IsTerminal, 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>> {
- if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
- return self.read_line_fallback();
- }
- 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 read_line_fallback(&self) -> io::Result<Option<String>> {
- let mut stdout = io::stdout();
- write!(stdout, "{}", self.prompt)?;
- stdout.flush()?;
- let mut buffer = String::new();
- let bytes_read = io::stdin().read_line(&mut buffer)?;
- if bytes_read == 0 {
- return Ok(None);
- }
- while matches!(buffer.chars().last(), Some('\n' | '\r')) {
- buffer.pop();
- }
- Ok(Some(buffer))
- }
- 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);
- }
- }
|