use crate::line_buffer::LineBuffer;
use crate::Prompt;
use crate::{
history_search::{BasicSearch, BasicSearchCommand},
line_buffer::InsertionPoint,
};
use crossterm::{
cursor::{position, MoveTo, MoveToColumn, RestorePosition, SavePosition},
event::{poll, read, Event, KeyCode, KeyEvent, KeyModifiers},
style::{Color, Print, ResetColor, SetForegroundColor},
terminal::{self, Clear, ClearType},
QueueableCommand, Result,
};
use std::collections::VecDeque;
use std::{
io::{stdout, Stdout, Write},
time::Duration,
};
const HISTORY_SIZE: usize = 100;
static PROMPT_INDICATOR: &str = "〉";
const PROMPT_COLOR: Color = Color::Blue;
pub enum EditCommand {
MoveToStart,
MoveToEnd,
MoveLeft,
MoveRight,
MoveWordLeft,
MoveWordRight,
InsertChar(char),
Backspace,
Delete,
AppendToHistory,
PreviousHistory,
NextHistory,
SearchHistory,
Clear,
CutFromStart,
CutToEnd,
CutWordLeft,
CutWordRight,
InsertCutBuffer,
UppercaseWord,
LowercaseWord,
CapitalizeChar,
SwapWords,
SwapGraphemes,
}
pub struct Reedline {
line_buffer: LineBuffer,
cut_buffer: String,
history: VecDeque<String>,
history_cursor: i64,
has_history: bool,
history_search: Option<BasicSearch>,
stdout: Stdout,
}
pub enum Signal {
Success(String),
CtrlC,
CtrlD,
CtrlL,
}
impl Default for Reedline {
fn default() -> Self {
Self::new()
}
}
impl Reedline {
pub fn new() -> Reedline {
let history = VecDeque::with_capacity(HISTORY_SIZE);
let history_cursor = -1i64;
let has_history = false;
let cut_buffer = String::new();
let stdout = stdout();
Reedline {
line_buffer: LineBuffer::new(),
cut_buffer,
history,
history_cursor,
has_history,
history_search: None,
stdout,
}
}
pub fn print_history(&mut self) -> Result<()> {
self.print_crlf()?;
let history: Vec<_> = self.history.iter().rev().cloned().enumerate().collect();
for (i, entry) in history {
self.print_line(&format!("{}\t{}", i + 1, entry))?;
}
Ok(())
}
pub fn read_line(&mut self) -> Result<Signal> {
terminal::enable_raw_mode()?;
let result = self.read_line_helper();
terminal::disable_raw_mode()?;
result
}
pub fn print_message(&mut self, msg: &str) -> Result<()> {
self.stdout
.queue(Print("\n"))?
.queue(MoveToColumn(1))?
.queue(Print(msg))?
.queue(Print("\n"))?
.queue(MoveToColumn(1))?;
self.stdout.flush()?;
Ok(())
}
pub fn print_line(&mut self, msg: &str) -> Result<()> {
self.stdout
.queue(Print(msg))?
.queue(Print("\n"))?
.queue(MoveToColumn(1))?;
self.stdout.flush()?;
Ok(())
}
pub fn print_crlf(&mut self) -> Result<()> {
self.stdout.queue(Print("\n"))?.queue(MoveToColumn(1))?;
self.stdout.flush()?;
Ok(())
}
pub fn print_events(&mut self) -> Result<()> {
terminal::enable_raw_mode()?;
let result = self.print_events_helper();
terminal::disable_raw_mode()?;
result
}
fn run_history_commands(&mut self, commands: &[EditCommand]) {
for command in commands {
match command {
EditCommand::InsertChar(c) => {
let search = self
.history_search
.as_mut()
.expect("couldn't get history_search as mutable");
search.step(BasicSearchCommand::InsertChar(*c), &self.history);
}
EditCommand::Backspace => {
let search = self
.history_search
.as_mut()
.expect("couldn't get history_search as mutable");
search.step(BasicSearchCommand::Backspace, &self.history);
}
EditCommand::SearchHistory => {
let search = self
.history_search
.as_mut()
.expect("couldn't get history_search as mutable");
search.step(BasicSearchCommand::Next, &self.history);
}
EditCommand::MoveRight => {
}
_ => self.history_search = None,
}
}
}
fn run_edit_commands(&mut self, commands: &[EditCommand]) {
if self.history_search.is_some() {
self.run_history_commands(commands);
return;
}
for command in commands {
match command {
EditCommand::MoveToStart => self.line_buffer.move_to_start(),
EditCommand::MoveToEnd => {
self.line_buffer.move_to_end();
}
EditCommand::MoveLeft => self.line_buffer.move_left(),
EditCommand::MoveRight => self.line_buffer.move_right(),
EditCommand::MoveWordLeft => {
self.line_buffer.move_word_left();
}
EditCommand::MoveWordRight => {
self.line_buffer.move_word_right();
}
EditCommand::InsertChar(c) => {
let insertion_point = self.line_buffer.insertion_point();
self.line_buffer.insert_char(insertion_point, *c);
}
EditCommand::Backspace => {
let left_index = self.line_buffer.grapheme_left_index();
let insertion_offset = self.insertion_point().offset;
if left_index < insertion_offset {
self.clear_range(left_index..insertion_offset);
self.set_insertion_point(left_index);
}
}
EditCommand::Delete => {
let right_index = self.line_buffer.grapheme_right_index();
let insertion_offset = self.insertion_point().offset;
if right_index > insertion_offset {
self.clear_range(insertion_offset..right_index);
}
}
EditCommand::Clear => {
self.line_buffer.clear();
self.set_insertion_point(0);
}
EditCommand::AppendToHistory => {
if self.history.len() + 1 == HISTORY_SIZE {
self.history.pop_back();
}
if self
.history
.front()
.map_or(true, |entry| entry.as_str() != self.insertion_line())
{
self.history.push_front(self.insertion_line().to_string());
}
self.has_history = true;
self.history_cursor = -1;
}
EditCommand::PreviousHistory => {
if self.has_history && self.history_cursor < (self.history.len() as i64 - 1) {
self.history_cursor += 1;
let history_entry = self
.history
.get(self.history_cursor as usize)
.unwrap()
.clone();
self.set_buffer(history_entry.clone());
self.move_to_end();
}
}
EditCommand::NextHistory => {
if self.history_cursor >= 0 {
self.history_cursor -= 1;
}
let new_buffer = if self.history_cursor < 0 {
String::new()
} else {
self.history
.get(self.history_cursor as usize)
.unwrap()
.clone()
};
self.set_buffer(new_buffer.clone());
self.move_to_end();
}
EditCommand::SearchHistory => {
self.history_search = Some(BasicSearch::new(self.insertion_line().to_string()));
}
EditCommand::CutFromStart => {
let insertion_offset = self.insertion_point().offset;
if insertion_offset > 0 {
self.cut_buffer.replace_range(
..,
&self.line_buffer.insertion_line()[..insertion_offset],
);
self.clear_to_insertion_point();
}
}
EditCommand::CutToEnd => {
let cut_slice =
&self.line_buffer.insertion_line()[self.insertion_point().offset..];
if !cut_slice.is_empty() {
self.cut_buffer.replace_range(.., cut_slice);
self.clear_to_end();
}
}
EditCommand::CutWordLeft => {
let insertion_offset = self.insertion_point().offset;
let left_index = self.line_buffer.word_left_index();
if left_index < insertion_offset {
let cut_range = left_index..insertion_offset;
self.cut_buffer.replace_range(
..,
&self.line_buffer.insertion_line()[cut_range.clone()],
);
self.clear_range(cut_range);
self.set_insertion_point(left_index);
}
}
EditCommand::CutWordRight => {
let insertion_offset = self.insertion_point().offset;
let right_index = self.line_buffer.word_right_index();
if right_index > insertion_offset {
let cut_range = insertion_offset..right_index;
self.cut_buffer.replace_range(
..,
&self.line_buffer.insertion_line()[cut_range.clone()],
);
self.clear_range(cut_range);
}
}
EditCommand::InsertCutBuffer => {
let insertion_offset = self.insertion_point().offset;
self.line_buffer
.insert_str(insertion_offset, &self.cut_buffer);
self.set_insertion_point(insertion_offset + self.cut_buffer.len());
}
EditCommand::UppercaseWord => {
let insertion_offset = self.insertion_point().offset;
let right_index = self.line_buffer.word_right_index();
if right_index > insertion_offset {
let change_range = insertion_offset..right_index;
let uppercased = self.insertion_line()[change_range.clone()].to_uppercase();
self.line_buffer.replace_range(change_range, &uppercased);
self.line_buffer.move_word_right();
}
}
EditCommand::LowercaseWord => {
let insertion_offset = self.insertion_point().offset;
let right_index = self.line_buffer.word_right_index();
if right_index > insertion_offset {
let change_range = insertion_offset..right_index;
let lowercased = self.insertion_line()[change_range.clone()].to_lowercase();
self.line_buffer.replace_range(change_range, &lowercased);
self.line_buffer.move_word_right();
}
}
EditCommand::CapitalizeChar => {
if self.line_buffer.on_whitespace() {
self.line_buffer.move_word_right();
self.line_buffer.move_word_left();
}
let insertion_offset = self.insertion_point().offset;
let right_index = self.line_buffer.grapheme_right_index();
if right_index > insertion_offset {
let change_range = insertion_offset..right_index;
let uppercased = self.insertion_line()[change_range.clone()].to_uppercase();
self.line_buffer.replace_range(change_range, &uppercased);
self.line_buffer.move_word_right();
}
}
EditCommand::SwapWords => {
let old_insertion_point = self.insertion_point().offset;
self.line_buffer.move_word_right();
let word_2_end = self.insertion_point().offset;
self.line_buffer.move_word_left();
let word_2_start = self.insertion_point().offset;
self.line_buffer.move_word_left();
let word_1_start = self.insertion_point().offset;
let word_1_end = self.line_buffer.word_right_index();
if word_1_start < word_1_end
&& word_1_end < word_2_start
&& word_2_start < word_2_end
{
let insertion_line = self.insertion_line();
let word_1 = insertion_line[word_1_start..word_1_end].to_string();
let word_2 = insertion_line[word_2_start..word_2_end].to_string();
self.line_buffer
.replace_range(word_2_start..word_2_end, &word_1);
self.line_buffer
.replace_range(word_1_start..word_1_end, &word_2);
self.set_insertion_point(word_2_end);
} else {
self.set_insertion_point(old_insertion_point);
}
}
EditCommand::SwapGraphemes => {
let insertion_offset = self.insertion_point().offset;
if insertion_offset == 0 {
self.line_buffer.move_right()
} else if insertion_offset == self.line_buffer.insertion_line().len() {
self.line_buffer.move_left()
}
let grapheme_1_start = self.line_buffer.grapheme_left_index();
let grapheme_2_end = self.line_buffer.grapheme_right_index();
if grapheme_1_start < insertion_offset && grapheme_2_end > insertion_offset {
let grapheme_1 =
self.insertion_line()[grapheme_1_start..insertion_offset].to_string();
let grapheme_2 =
self.insertion_line()[insertion_offset..grapheme_2_end].to_string();
self.line_buffer
.replace_range(insertion_offset..grapheme_2_end, &grapheme_1);
self.line_buffer
.replace_range(grapheme_1_start..insertion_offset, &grapheme_2);
self.set_insertion_point(grapheme_2_end);
} else {
self.set_insertion_point(insertion_offset);
}
}
}
}
}
fn insertion_point(&self) -> InsertionPoint {
self.line_buffer.insertion_point()
}
fn set_insertion_point(&mut self, pos: usize) {
let mut insertion_point = self.line_buffer.insertion_point();
insertion_point.offset = pos;
self.line_buffer.set_insertion_point(insertion_point)
}
fn insertion_line(&self) -> &str {
self.line_buffer.insertion_line()
}
fn set_buffer(&mut self, buffer: String) {
self.line_buffer.set_buffer(buffer)
}
fn move_to_end(&mut self) {
self.line_buffer.move_to_end()
}
fn clear_to_end(&mut self) {
self.line_buffer.clear_to_end()
}
fn clear_to_insertion_point(&mut self) {
self.line_buffer.clear_to_insertion_point()
}
fn clear_range<R>(&mut self, range: R)
where
R: std::ops::RangeBounds<usize>,
{
self.line_buffer.clear_range(range)
}
fn maybe_wrap(&self, terminal_width: u16, start_offset: u16, c: char) -> bool {
use unicode_width::UnicodeWidthStr;
let mut test_buffer = self.insertion_line().to_string();
test_buffer.push(c);
let display_width = UnicodeWidthStr::width(test_buffer.as_str()) + start_offset as usize;
display_width >= terminal_width as usize
}
fn print_events_helper(&mut self) -> Result<()> {
loop {
if poll(Duration::from_millis(5_000))? {
let event = read()?;
self.print_message(&format!("Event::{:?}", event))?;
if event == Event::Key(KeyCode::Esc.into()) {
break;
}
} else {
self.print_message("Waiting for you to type...")?;
}
}
Ok(())
}
pub fn clear_screen(&mut self) -> Result<()> {
let (_, num_lines) = terminal::size()?;
for _ in 0..2 * num_lines {
self.stdout.queue(Print("\n"))?;
}
self.stdout.queue(MoveTo(0, 0))?;
self.stdout.flush()?;
Ok(())
}
fn queue_prompt(&mut self) -> Result<()> {
let mut prompt = Prompt::new(PROMPT_INDICATOR, 1);
self.stdout
.queue(MoveToColumn(0))?
.queue(SetForegroundColor(PROMPT_COLOR))?
.queue(Print(prompt.print_prompt()))?
.queue(ResetColor)?;
Ok(())
}
fn queue_prompt_indicator(&mut self) -> Result<()> {
self.stdout
.queue(MoveToColumn(0))?
.queue(SetForegroundColor(PROMPT_COLOR))?
.queue(Print(PROMPT_INDICATOR))?
.queue(ResetColor)?;
Ok(())
}
fn buffer_paint(&mut self, prompt_offset: (u16, u16)) -> Result<()> {
let new_index = self.insertion_point().offset;
let insertion_line = self.insertion_line().to_string();
self.stdout
.queue(MoveTo(prompt_offset.0, prompt_offset.1))?;
self.stdout.queue(Print(&insertion_line[0..new_index]))?;
self.stdout.queue(SavePosition)?;
self.stdout.queue(Print(&insertion_line[new_index..]))?;
self.stdout.queue(Clear(ClearType::FromCursorDown))?;
self.stdout.queue(RestorePosition)?;
self.stdout.flush()?;
Ok(())
}
fn history_search_paint(&mut self) -> Result<()> {
let search = self
.history_search
.as_ref()
.expect("couldn't get history_search reference");
let status = if search.result.is_none() && !search.search_string.is_empty() {
"failed "
} else {
""
};
self.stdout
.queue(MoveToColumn(0))?
.queue(SetForegroundColor(Color::Blue))?
.queue(Print(format!(
"({}reverse-search)`{}':",
status, search.search_string
)))?
.queue(ResetColor)?;
match search.result {
Some((history_index, offset)) => {
let history_result = &self.history[history_index];
self.stdout.queue(Print(&history_result[..offset]))?;
self.stdout.queue(SavePosition)?;
self.stdout.queue(Print(&history_result[offset..]))?;
self.stdout.queue(Clear(ClearType::UntilNewLine))?;
self.stdout.queue(RestorePosition)?;
}
None => {
self.stdout.queue(Clear(ClearType::UntilNewLine))?;
}
}
self.stdout.flush()?;
Ok(())
}
fn read_line_helper(&mut self) -> Result<Signal> {
terminal::enable_raw_mode()?;
self.queue_prompt()?;
let mut terminal_size = terminal::size()?;
let mut prompt_offset = position()?;
let mut line_count = 1;
if self.history_search.is_some() {
self.history_search_paint()?;
} else {
self.buffer_paint(prompt_offset)?;
}
self.stdout.flush()?;
loop {
match read()? {
Event::Key(KeyEvent {
code,
modifiers: KeyModifiers::CONTROL,
}) => match code {
KeyCode::Char('d') => {
if self.line_buffer.is_empty() {
return Ok(Signal::CtrlD);
} else {
self.run_edit_commands(&[EditCommand::Delete]);
}
}
KeyCode::Char('a') => {
self.run_edit_commands(&[EditCommand::MoveToStart]);
}
KeyCode::Char('e') => {
self.run_edit_commands(&[EditCommand::MoveToEnd]);
}
KeyCode::Char('k') => {
self.run_edit_commands(&[EditCommand::CutToEnd]);
}
KeyCode::Char('u') => {
self.run_edit_commands(&[EditCommand::CutFromStart]);
}
KeyCode::Char('y') => {
self.run_edit_commands(&[EditCommand::InsertCutBuffer]);
}
KeyCode::Char('b') => {
self.run_edit_commands(&[EditCommand::MoveLeft]);
}
KeyCode::Char('f') => {
self.run_edit_commands(&[EditCommand::MoveRight]);
}
KeyCode::Char('c') => {
self.run_edit_commands(&[EditCommand::Clear]);
return Ok(Signal::CtrlC);
}
KeyCode::Char('l') => {
return Ok(Signal::CtrlL);
}
KeyCode::Char('h') => {
self.run_edit_commands(&[EditCommand::Backspace]);
}
KeyCode::Char('w') => {
self.run_edit_commands(&[EditCommand::CutWordLeft]);
}
KeyCode::Left => {
self.run_edit_commands(&[EditCommand::MoveWordLeft]);
}
KeyCode::Right => {
self.run_edit_commands(&[EditCommand::MoveWordRight]);
}
KeyCode::Char('p') => {
self.run_edit_commands(&[EditCommand::PreviousHistory]);
}
KeyCode::Char('n') => {
self.run_edit_commands(&[EditCommand::NextHistory]);
}
KeyCode::Char('r') => {
self.run_edit_commands(&[EditCommand::SearchHistory]);
}
KeyCode::Char('t') => {
self.run_edit_commands(&[EditCommand::SwapGraphemes]);
}
_ => {}
},
Event::Key(KeyEvent {
code,
modifiers: KeyModifiers::ALT,
}) => match code {
KeyCode::Char('b') => {
self.run_edit_commands(&[EditCommand::MoveWordLeft]);
}
KeyCode::Char('f') => {
self.run_edit_commands(&[EditCommand::MoveWordRight]);
}
KeyCode::Char('d') => {
self.run_edit_commands(&[EditCommand::CutWordRight]);
}
KeyCode::Left => {
self.run_edit_commands(&[EditCommand::MoveWordLeft]);
}
KeyCode::Right => {
self.run_edit_commands(&[EditCommand::MoveWordRight]);
}
KeyCode::Char('u') => {
self.run_edit_commands(&[EditCommand::UppercaseWord]);
}
KeyCode::Char('l') => {
self.run_edit_commands(&[EditCommand::LowercaseWord]);
}
KeyCode::Char('c') => {
self.run_edit_commands(&[EditCommand::CapitalizeChar]);
}
KeyCode::Char('t') => {
self.run_edit_commands(&[EditCommand::SwapWords]);
}
_ => {}
},
Event::Key(KeyEvent { code, modifiers: _ }) => {
match code {
KeyCode::Char(c) => {
let line_start = if self.insertion_point().line == 0 {
prompt_offset.0
} else {
0
};
if self.maybe_wrap(terminal_size.0, line_start, c) {
let (original_column, original_row) = position()?;
self.run_edit_commands(&[
EditCommand::InsertChar(c),
EditCommand::MoveRight,
]);
self.buffer_paint(prompt_offset)?;
let (new_column, _) = position()?;
if new_column < original_column
&& original_row == (terminal_size.1 - 1)
&& line_count == 1
{
prompt_offset.1 -= 1;
line_count += 1;
}
} else {
self.run_edit_commands(&[
EditCommand::InsertChar(c),
EditCommand::MoveRight,
]);
}
}
KeyCode::Backspace => {
self.run_edit_commands(&[EditCommand::Backspace]);
}
KeyCode::Delete => {
self.run_edit_commands(&[EditCommand::Delete]);
}
KeyCode::Home => {
self.run_edit_commands(&[EditCommand::MoveToStart]);
}
KeyCode::End => {
self.run_edit_commands(&[EditCommand::MoveToEnd]);
}
KeyCode::Enter => match self.history_search.clone() {
Some(search) => {
self.queue_prompt_indicator()?;
if let Some((history_index, _)) = search.result {
self.line_buffer
.set_buffer(self.history[history_index].clone());
}
self.history_search = None;
}
None => {
let buffer = self.insertion_line().to_string();
self.run_edit_commands(&[
EditCommand::AppendToHistory,
EditCommand::Clear,
]);
return Ok(Signal::Success(buffer));
}
},
KeyCode::Up => {
self.run_edit_commands(&[EditCommand::PreviousHistory]);
}
KeyCode::Down => {
self.run_edit_commands(&[EditCommand::NextHistory]);
}
KeyCode::Left => {
self.run_edit_commands(&[EditCommand::MoveLeft]);
}
KeyCode::Right => {
self.run_edit_commands(&[EditCommand::MoveRight]);
}
_ => {}
};
}
Event::Mouse(event) => {
self.print_message(&format!("{:?}", event))?;
}
Event::Resize(width, height) => {
terminal_size = (width, height);
}
}
if self.history_search.is_some() {
self.history_search_paint()?;
} else {
self.buffer_paint(prompt_offset)?;
}
}
}
}