use std::char;
use std::fmt::Write;
use termwiz::cell::{AttributeChange, CellAttributes};
use termwiz::color::{AnsiColor, ColorAttribute};
use termwiz::input::KeyEvent;
use termwiz::surface::change::Change;
use termwiz::surface::Position;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::display::DisplayAction;
use crate::error::Error;
use crate::prompt_history::PromptHistory;
use crate::screen::Screen;
use crate::util;
type PromptRunFn = dyn FnMut(&mut Screen, &str) -> Result<DisplayAction, Error>;
pub(crate) struct Prompt {
prompt: String,
history: PromptHistory,
run: Option<Box<PromptRunFn>>,
}
pub(crate) struct PromptState {
value: Vec<char>,
offset: usize,
position: usize,
}
impl PromptState {
pub(crate) fn new() -> PromptState {
PromptState {
value: Vec::new(),
offset: 0,
position: 0,
}
}
pub(crate) fn load(data: &str) -> PromptState {
let mut value = Vec::new();
let mut iter = data.chars();
while let Some(c) = iter.next() {
if c == '\\' {
if let Some(c) = iter.next() {
if c == 'x' {
if let (Some(c1), Some(c2)) = (iter.next(), iter.next()) {
let hex: String = [c1, c2].iter().collect();
if let Some(c) =
u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32)
{
value.push(c);
}
}
} else {
value.push(c);
}
}
} else {
value.push(c);
}
}
let position = value.len();
PromptState {
value,
offset: 0,
position,
}
}
pub(crate) fn save(&self) -> String {
let mut data = String::new();
for &c in self.value.iter() {
if c == '\\' {
data.push_str("\\\\");
} else if c < ' ' || c == '\x7f' {
write!(data, "\\x{:02X}", c as u8).expect("writes to strings can't fail")
} else {
data.push(c);
}
}
data
}
pub(crate) fn cursor_position(&self) -> usize {
let mut position = 0;
for c in self.value[self.offset..self.position].iter() {
position += render_width(*c);
}
position
}
fn clamp_offset(&mut self, width: usize) {
if self.offset > self.position {
self.offset = self.position;
}
while self.cursor_position() < 5 && self.offset > 0 {
self.offset -= 1;
}
while self.cursor_position() > width - 5 && self.offset < self.position {
self.offset += 1;
}
}
fn render(&mut self, changes: &mut Vec<Change>, mut position: usize, width: usize) {
let mut start = self.offset;
let mut end = self.offset;
while end < self.value.len() {
let c = self.value[end];
if let Some(render) = special_render(self.value[end]) {
if end > start {
let value: String = self.value[start..end].iter().collect();
changes.push(Change::Text(value));
}
let render = util::truncate_string(render, 0, width - position);
position += render.width();
changes.push(Change::Attribute(AttributeChange::Reverse(true)));
changes.push(Change::Text(render));
changes.push(Change::Attribute(AttributeChange::Reverse(false)));
start = end + 1;
if position >= width {
break;
}
} else {
let w = c.width().unwrap_or(0);
if position + w > width {
break;
}
position += w;
}
end += 1;
}
if end > start {
let value: String = self.value[start..end].iter().collect();
changes.push(Change::Text(value));
}
if position < width {
changes.push(Change::ClearToEndOfLine(ColorAttribute::default()));
}
}
fn insert_char(&mut self, c: char, width: usize) -> DisplayAction {
self.value.insert(self.position, c);
self.position += 1;
if self.position == self.value.len() && self.cursor_position() < width - 5 {
DisplayAction::Change(Change::Text(c.to_string()))
} else {
DisplayAction::RefreshPrompt
}
}
fn insert_str(&mut self, s: &str) -> DisplayAction {
let old_len = self.value.len();
self.value.splice(self.position..self.position, s.chars());
self.position += self.value.len() - old_len;
DisplayAction::RefreshPrompt
}
fn delete_prev_char(&mut self) -> DisplayAction {
if self.position > 0 {
self.value.remove(self.position - 1);
self.position -= 1;
DisplayAction::RefreshPrompt
} else {
DisplayAction::None
}
}
fn delete_next_char(&mut self) -> DisplayAction {
if self.position < self.value.len() {
self.value.remove(self.position);
DisplayAction::RefreshPrompt
} else {
DisplayAction::None
}
}
fn delete_prev_word(&mut self) -> DisplayAction {
let dest = move_word_backwards(self.value.as_slice(), self.position);
if dest != self.position {
self.value.splice(dest..self.position, None);
self.position = dest;
DisplayAction::RefreshPrompt
} else {
DisplayAction::None
}
}
fn delete_next_word(&mut self) -> DisplayAction {
let dest = move_word_forwards(self.value.as_slice(), self.position);
if dest != self.position {
self.value.splice(self.position..dest, None);
DisplayAction::RefreshPrompt
} else {
DisplayAction::None
}
}
fn move_next_char(&mut self) -> DisplayAction {
if self.position < self.value.len() {
self.position += 1;
while self.position < self.value.len() {
let w = render_width(self.value[self.position]);
if w != 0 {
break;
}
self.position += 1;
}
DisplayAction::RefreshPrompt
} else {
DisplayAction::None
}
}
fn move_prev_char(&mut self) -> DisplayAction {
if self.position > 0 {
while self.position > 0 {
self.position -= 1;
let w = render_width(self.value[self.position]);
if w != 0 {
break;
}
}
DisplayAction::RefreshPrompt
} else {
DisplayAction::None
}
}
fn move_next_word(&mut self) -> DisplayAction {
let dest = move_word_forwards(self.value.as_slice(), self.position);
if dest != self.position {
self.position = dest;
DisplayAction::RefreshPrompt
} else {
DisplayAction::None
}
}
fn move_prev_word(&mut self) -> DisplayAction {
let dest = move_word_backwards(self.value.as_slice(), self.position);
if dest != self.position {
self.position = dest;
DisplayAction::RefreshPrompt
} else {
DisplayAction::None
}
}
fn delete_to_end(&mut self) -> DisplayAction {
if self.position < self.value.len() {
self.value.splice(self.position.., None);
DisplayAction::RefreshPrompt
} else {
DisplayAction::None
}
}
fn delete_to_start(&mut self) -> DisplayAction {
if self.position > 0 {
self.value.splice(..self.position, None);
self.position = 0;
DisplayAction::RefreshPrompt
} else {
DisplayAction::None
}
}
fn move_to_end(&mut self) -> DisplayAction {
self.position = self.value.len();
DisplayAction::RefreshPrompt
}
fn move_to_start(&mut self) -> DisplayAction {
self.position = 0;
DisplayAction::RefreshPrompt
}
fn transpose_chars(&mut self) -> DisplayAction {
if self.position > 0 && self.value.len() > 1 {
if self.position < self.value.len() {
self.position += 1;
}
self.value.swap(self.position - 2, self.position - 1);
DisplayAction::RefreshPrompt
} else {
DisplayAction::None
}
}
}
impl Prompt {
pub(crate) fn new(ident: impl Into<String>, prompt: &str, run: Box<PromptRunFn>) -> Prompt {
Prompt {
prompt: prompt.to_string(),
history: PromptHistory::open(ident),
run: Some(run),
}
}
fn state(&self) -> &PromptState {
self.history.state()
}
fn state_mut(&mut self) -> &mut PromptState {
self.history.state_mut()
}
pub(crate) fn cursor_position(&self) -> usize {
self.prompt.width() + 4 + self.state().cursor_position()
}
pub(crate) fn render(&mut self, changes: &mut Vec<Change>, row: usize, width: usize) {
changes.push(Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Absolute(row),
});
changes.push(Change::AllAttributes(
CellAttributes::default()
.set_foreground(AnsiColor::Black)
.set_background(AnsiColor::Silver)
.clone(),
));
changes.push(Change::Text(format!(" {} ", self.prompt)));
changes.push(Change::AllAttributes(CellAttributes::default()));
changes.push(Change::Text(" ".into()));
let offset = self.prompt.width() + 4;
self.state_mut().render(changes, offset, width);
}
pub(crate) fn dispatch_key(&mut self, key: KeyEvent, width: usize) -> DisplayAction {
use termwiz::input::{KeyCode::*, Modifiers};
const CTRL: Modifiers = Modifiers::CTRL;
const NONE: Modifiers = Modifiers::NONE;
const ALT: Modifiers = Modifiers::ALT;
let value_width = width - self.prompt.width() - 4;
let action = match (key.modifiers, key.key) {
(NONE, Enter) | (CTRL, Char('J')) | (CTRL, Char('M')) => {
let _ = self.history.save();
let mut run = self.run.take();
let value: String = self.state().value[..].iter().collect();
return DisplayAction::Run(Box::new(move |screen: &mut Screen| {
screen.clear_prompt();
if let Some(ref mut run) = run {
run(screen, &value)
} else {
Ok(DisplayAction::Render)
}
}));
}
(NONE, Escape) | (CTRL, Char('C')) => {
return DisplayAction::Run(Box::new(|screen: &mut Screen| {
screen.clear_prompt();
Ok(DisplayAction::Render)
}));
}
(NONE, Char(c)) => self.state_mut().insert_char(c, value_width),
(NONE, Backspace) | (CTRL, Char('H')) => self.state_mut().delete_prev_char(),
(NONE, Delete) | (CTRL, Char('D')) => self.state_mut().delete_next_char(),
(CTRL, Char('W')) | (ALT, Backspace) => self.state_mut().delete_prev_word(),
(ALT, Char('d')) => self.state_mut().delete_next_word(),
(NONE, RightArrow) | (CTRL, Char('F')) => self.state_mut().move_next_char(),
(NONE, LeftArrow) | (CTRL, Char('B')) => self.state_mut().move_prev_char(),
(CTRL, RightArrow) | (ALT, Char('f')) => self.state_mut().move_next_word(),
(CTRL, LeftArrow) | (ALT, Char('b')) => self.state_mut().move_prev_word(),
(CTRL, Char('K')) => self.state_mut().delete_to_end(),
(CTRL, Char('U')) => self.state_mut().delete_to_start(),
(NONE, End) | (CTRL, Char('E')) => self.state_mut().move_to_end(),
(NONE, Home) | (CTRL, Char('A')) => self.state_mut().move_to_start(),
(CTRL, Char('T')) => self.state_mut().transpose_chars(),
(NONE, UpArrow) => self.history.previous(),
(NONE, DownArrow) => self.history.next(),
_ => return DisplayAction::None,
};
self.state_mut().clamp_offset(value_width);
action
}
pub(crate) fn paste(&mut self, text: &str, width: usize) -> DisplayAction {
let value_width = width - self.prompt.width() - 4;
let action = self.state_mut().insert_str(text);
self.state_mut().clamp_offset(value_width);
action
}
}
fn move_word_forwards(value: &[char], mut position: usize) -> usize {
let len = value.len();
while position < len && value[position].is_whitespace() {
position += 1;
}
while position < len && !value[position].is_whitespace() {
position += 1;
}
position
}
fn move_word_backwards(value: &[char], mut position: usize) -> usize {
while position > 0 {
position -= 1;
if !value[position].is_whitespace() {
break;
}
}
while position > 0 {
if value[position].is_whitespace() {
position += 1;
break;
}
position -= 1;
}
position
}
fn render_width(c: char) -> usize {
if c < ' ' || c == '\x7F' {
4
} else if let Some(w) = c.width() {
w
} else {
8
}
}
fn special_render(c: char) -> Option<String> {
if c < ' ' || c == '\x7F' {
Some(format!("<{:02X}>", c as u8))
} else if c.width().is_none() {
Some(format!("<U+{:04X}>", c as u32))
} else {
None
}
}