use std::{
iter::FromIterator,
mem::replace,
time::Instant,
};
use crossterm::event::{
KeyCode,
KeyEvent,
KeyModifiers,
};
use futures::{
channel::mpsc,
prelude::*,
};
use ringbuffer::{
AllocRingBuffer,
RingBuffer,
RingBufferExt,
RingBufferWrite,
};
use textwrap::core::{
Fragment,
Word,
};
use tracing::debug;
use tui::{
buffer::Buffer,
layout::Rect,
widgets::Widget,
};
use unicode_segmentation::UnicodeSegmentation;
use textwrap::word_separators::WordSeparator;
use crate::{
script::Flags,
};
use crossterm::event::Event;
fn find_words<'a>(line: &'a str) -> Box<dyn Iterator<Item = Word<'a>> + 'a> {
textwrap::word_separators::AsciiSpace::default().find_words(line)
}
pub struct InputBuffer {
buffer: String,
history: AllocRingBuffer<String>,
history_selection: Option<isize>,
searching: bool,
prompt: String,
cursor: usize,
flags: Flags,
output_tx: mpsc::Sender<String>,
render_tx: mpsc::Sender<Instant>,
}
impl InputBuffer {
pub fn new(
flags: Flags,
output_tx: mpsc::Sender<String>,
render_tx: mpsc::Sender<Instant>,
) -> Self {
InputBuffer {
buffer: String::new(),
cursor: 0,
history: AllocRingBuffer::new(),
history_selection: None,
searching: false,
prompt: "> ".into(),
flags,
output_tx,
render_tx,
}
}
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
pub fn insert(&mut self, c: char) {
if self.cursor == self.buffer.len() {
self.buffer.push(c)
} else {
self.buffer.insert(self.cursor, c)
}
self.cursor += 1;
if self.searching {
self.search_next();
} else {
self.history_selection = None;
}
}
pub fn previous(&mut self) {
self.searching = false;
let i = self.history_selection.unwrap_or(0) - 1;
if let Some(cmd) = self.history.get(i) {
self.buffer = cmd.clone();
self.history_selection = Some(i);
self.cursor = self.buffer.len();
}
}
pub fn next(&mut self) {
self.searching = false;
let i = self.history_selection.take().unwrap_or(0) + 1;
if i >= 0 {
return;
}
if let Some(cmd) = self.history.get(i) {
self.buffer = cmd.clone();
self.history_selection = Some(i);
self.cursor = self.buffer.len();
}
}
pub fn search_next(&mut self) {
for i in self.history_selection.unwrap_or(0)..(-1 * (self.history.len() as isize)) {
if i == 0 {
continue;
}
let entry = match self.history.get(i) {
Some(entry) => entry,
None => {
self.history_selection = None;
return;
}
};
if entry.starts_with(&self.buffer) {
self.history_selection = Some(i);
return;
}
}
}
pub fn start_search(&mut self) {
self.searching = true;
self.history_selection = Some(0);
self.buffer.clear();
self.cursor = 0;
}
pub fn wrapped(&self, len: usize) -> InputBufferWrapped {
let mut full_input = self.prompt.clone();
if self.searching {
full_input.push_str("(search) [");
full_input.push_str(&self.buffer);
full_input.push_str(" ] ");
} else if let Some(i) = self.history_selection {
if let Some(entry) = self.history.get(i) {
full_input.push_str(entry);
}
} else {
full_input.push_str(&self.buffer);
}
let line_count = full_input.lines().count();
let mut lines = Vec::with_capacity(line_count);
for line in full_input
.lines()
.map(find_words)
.map(Vec::from_iter)
{
let wrapped = textwrap::wrap_algorithms::wrap_first_fit(&line, &[len; 512]);
wrapped
.into_iter()
.map(|l| {
l.iter()
.fold(String::with_capacity(l.len() * 5), |mut acc, w| {
acc.push_str(&w);
for _ in 0..w.whitespace_width() {
acc.push(' ');
}
acc
})
})
.for_each(|l| lines.push(l));
}
let mut prompt_len = self
.prompt
.graphemes(true)
.filter(|g| !g.contains('\n') && !g.contains('\r'))
.count();
if self.searching {
prompt_len += "(search) [".len();
}
debug!(?lines, "input lines");
InputBufferWrapped {
lines,
prompt_len,
flags: &self.flags,
cursor: self.cursor,
}
}
pub fn clear(&mut self) {
self.searching = false;
self.history_selection = None;
self.buffer.clear();
self.cursor = 0;
}
pub fn accept(&mut self) -> String {
let maybe_cmd = if self.searching {
self.searching = false;
let selection = self.history_selection.take();
selection.and_then(|i| self.history.get(i))
} else {
None
};
let cmd = match maybe_cmd {
Some(cmd) => cmd.clone(),
None => {
let len = self.buffer.len();
self.cursor = 0;
replace(&mut self.buffer, String::with_capacity(len))
}
};
tracing::debug!(?cmd, "Accepted cmd");
self.clear();
if !self.flags.noecho() {
self.history.push(cmd.clone());
}
cmd
}
pub async fn handle_user_event(&mut self, event: &Event) -> bool {
match event {
Event::Key(KeyEvent {
code: key,
modifiers: KeyModifiers::NONE,
}) => match key {
KeyCode::Enter => {
let cmd = self.accept();
if let Err(err) = self.output_tx.send(cmd).await {
tracing::error!(?err, "error sending input line");
};
debug!("sent command");
}
KeyCode::Backspace => {
self.backspace();
}
KeyCode::Up => {
self.previous();
}
KeyCode::Down => {
self.next();
}
KeyCode::Char(c) => {
self.insert(*c);
}
_ => return false,
},
Event::Key(KeyEvent {
code: key,
modifiers: KeyModifiers::CONTROL,
}) => match key {
KeyCode::Char('d') => {
if self.is_empty() {
self.flags.exit();
} else {
self.delete();
}
}
KeyCode::Char('a') => {
self.cursor_home();
}
KeyCode::Char('e') => {
self.cursor_end();
}
KeyCode::Char('b') => {
self.cursor_back();
}
KeyCode::Char('f') => {
self.cursor_forward();
}
KeyCode::Char('c') => {
self.clear();
}
KeyCode::Char('k') => {
self.delete_forward();
}
KeyCode::Char('r') => {
if self.searching {
self.search_next();
} else {
self.start_search();
}
}
_ => return false,
},
Event::Key(KeyEvent {
code: key,
modifiers: KeyModifiers::ALT,
}) => match key {
KeyCode::Char('f') => {
self.cursor_fword();
}
KeyCode::Char('b') => {
self.cursor_bword();
}
KeyCode::Char('\u{7f}') => {
self.backspace_word();
}
_ => return false,
},
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers,
}) => {
let mut c = *c;
if *modifiers == KeyModifiers::SHIFT {
c = c.to_ascii_uppercase();
}
self.insert(c);
}
_ => return false,
}
let _ = self.render_tx.send(Instant::now()).await;
true
}
pub fn backspace(&mut self) {
if self.cursor > 0 {
self.buffer.remove(self.cursor - 1);
}
self.cursor_back();
}
pub fn backspace_word(&mut self) {
let start = self.find_word_bwd();
self.buffer.drain(start..self.cursor);
self.cursor = start;
}
pub fn delete(&mut self) {
if self.buffer.len() > 0 && self.cursor < self.buffer.len() {
self.buffer.remove(self.cursor);
}
}
pub fn cursor_end(&mut self) {
self.cursor = self.buffer.len();
}
pub fn cursor_home(&mut self) {
self.cursor = 0;
}
pub fn cursor_back(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
pub fn cursor_forward(&mut self) {
self.cursor += 1;
if self.cursor > self.buffer.len() {
self.cursor = self.buffer.len();
}
}
pub fn cursor_bword(&mut self) {
self.cursor = self.find_word_bwd();
}
pub fn cursor_fword(&mut self) {
self.cursor = self.find_word_fwd();
}
pub fn delete_forward(&mut self) {
if self.cursor < self.buffer.len() {
self.buffer.drain(self.cursor..);
}
}
fn find_word_bwd(&self) -> usize {
let mut cursor = self.cursor;
let mut chars = self
.buffer
.chars()
.rev()
.skip(self.buffer.len() - self.cursor);
for char in &mut chars {
cursor -= 1;
if char.is_alphanumeric() {
break;
}
}
for char in &mut chars {
cursor -= 1;
if !char.is_alphanumeric() {
return cursor + 1;
}
}
return 0;
}
fn find_word_fwd(&self) -> usize {
let mut chars = self.buffer.chars().enumerate().skip(self.cursor);
for (_, char) in &mut chars {
if char.is_alphanumeric() {
break;
}
}
for (cursor, char) in &mut chars {
if !char.is_alphanumeric() {
return cursor;
}
}
return self.buffer.len();
}
}
pub struct InputBufferWrapped<'a> {
lines: Vec<String>,
prompt_len: usize,
cursor: usize,
flags: &'a Flags,
}
impl<'a> InputBufferWrapped<'a> {
pub fn len(&self) -> usize {
self.lines.len()
}
pub fn cursor_pos(&self) -> (usize, usize) {
let mut pos: usize = 0;
let mut target = self.prompt_len;
if !self.flags.noecho() {
target += self.cursor;
}
let mut x = 0;
let mut y = 0;
'outer: for line in &self.lines {
if pos >= target {
break 'outer;
}
for _ in line.graphemes(true) {
if pos >= target {
break 'outer;
}
pos += 1;
x += 1;
}
if pos >= target {
break;
}
y += 1;
x = 0;
}
(x, y)
}
}
impl<'a> Widget for InputBufferWrapped<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut count: usize = 0;
for (y, line) in self.lines.into_iter().enumerate() {
let mut x = 0;
for grapheme in line.graphemes(true) {
if grapheme.contains('\r') || grapheme.contains('\n') {
continue;
}
if self.flags.noecho() && count >= self.prompt_len {
return;
}
buf.get_mut(area.x + x as u16, area.y + y as u16)
.set_symbol(grapheme);
x += 1;
count += 1;
}
}
}
}