use unicode_width::UnicodeWidthChar;
use crate::event::*;
use crate::{Error, RenderSnapshot, Terminal, Theme};
#[derive(Debug, Clone)]
pub struct InputCursor {
value: String,
cursor: usize,
}
impl Default for InputCursor {
fn default() -> Self {
Self::new(String::new(), 0)
}
}
impl InputCursor {
pub fn new(value: String, cursor: usize) -> Self {
Self { value, cursor }
}
pub fn from(value: String) -> Self {
let cursor = value.char_indices().count();
Self { value, cursor }
}
fn chars(&self) -> std::str::CharIndices {
self.value.char_indices()
}
fn len(&self) -> usize {
self.chars().count()
}
fn char_at(&self, index: usize) -> Option<(usize, char)> {
self.chars().nth(index)
}
pub fn col(&self) -> u16 {
let col = self
.chars()
.take(self.cursor)
.map(|(_, c)| c.width().unwrap_or(0))
.sum::<usize>();
u16::try_from(col).unwrap_or(0)
}
pub fn value(&self) -> String {
self.value.clone()
}
pub fn set_value(&mut self, value: String) -> &mut Self {
self.value = value;
self
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn set_cursor(&mut self, cursor: usize) -> &mut Self {
self.cursor = cursor;
self
}
pub fn split(&self) -> (String, String, String) {
let (left, mut cursor, right) = self.chars().enumerate().fold(
(String::new(), String::new(), String::new()),
|(mut left, mut cursor, mut right), (i, (_, c))| {
match i.cmp(&self.cursor) {
std::cmp::Ordering::Less => {
left.push(c);
}
std::cmp::Ordering::Equal => {
cursor.push(c);
}
std::cmp::Ordering::Greater => {
right.push(c);
}
}
(left, cursor, right)
},
);
if cursor.is_empty() && self.cursor.saturating_add(1) > self.len() {
cursor.push(' ');
}
(left, cursor, right)
}
pub fn is_empty(&self) -> bool {
self.value.trim().is_empty()
}
pub fn move_left(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
pub fn move_right(&mut self) {
self.cursor = std::cmp::min(self.cursor.saturating_add(1), self.len());
}
pub fn move_home(&mut self) {
self.cursor = 0;
}
pub fn move_end(&mut self) {
self.cursor = self.len();
}
pub fn insert(&mut self, chr: char) {
match self.char_at(self.cursor) {
Some((i, _)) => {
self.value.insert(i, chr);
}
None => {
self.value.push(chr);
}
}
self.cursor = self.cursor.saturating_add(1);
}
pub fn delete_left_char(&mut self) {
if self.cursor == 0 {
return;
}
let cursor = self.cursor.saturating_sub(1);
let found = self.char_at(cursor).map(|(i, _)| i);
if let Some(i) = found {
self.value.remove(i);
self.cursor = cursor;
}
}
fn prev_word_index(&mut self) -> usize {
let mut found_word = false;
let chars = self
.chars()
.enumerate()
.collect::<Vec<_>>()
.into_iter()
.take(self.cursor)
.rev();
for (i, (_, c)) in chars {
if c.is_whitespace() {
if found_word {
return i.saturating_add(1);
}
} else {
found_word = true;
}
}
0
}
pub fn delete_left_word(&mut self) {
let start = self.prev_word_index();
let mut value = String::new();
for (i, (_, c)) in self.chars().enumerate() {
if i < start || self.cursor <= i {
value.push(c);
}
}
self.value = value;
self.cursor = start;
}
pub fn delete_right_char(&mut self) {
if self.cursor >= self.len() {
return;
}
let found = self.char_at(self.cursor).map(|(i, _)| i);
if let Some(i) = found {
self.value.remove(i);
}
}
pub fn delete_rest_line(&mut self) {
let found = self.char_at(self.cursor).map(|(i, _)| i);
if let Some(i) = found {
self.value = self.value[..i].to_string();
}
}
pub fn delete_line(&mut self) {
self.cursor = 0;
self.value = String::new();
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PromptState {
Active,
Submit,
Cancel,
Error(String),
Fatal(String),
}
impl std::fmt::Display for PromptState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PromptState::Active => write!(f, "Active"),
PromptState::Submit => write!(f, "Submit"),
PromptState::Cancel => write!(f, "Cancel"),
PromptState::Error(msg) => write!(f, "Error({})", msg),
PromptState::Fatal(msg) => write!(f, "Fatal({})", msg),
}
}
}
#[derive(Debug)]
pub enum PromptInput {
None,
Cursor(InputCursor),
Raw(String),
}
impl Default for PromptInput {
fn default() -> Self {
Self::None
}
}
#[derive(Debug)]
pub enum PromptBody {
None,
Raw(String),
}
impl Default for PromptBody {
fn default() -> Self {
Self::None
}
}
pub trait Validator<T> {
fn validate(&self, value: &T) -> Result<(), String>;
}
impl<T, F> Validator<T> for F
where
F: Fn(&T) -> Result<(), String>,
{
fn validate(&self, value: &T) -> Result<(), String> {
self(value)
}
}
#[derive(Debug, Default)]
pub struct RenderPayload {
pub input: PromptInput,
pub body: PromptBody,
pub message: String,
pub hint: Option<String>,
pub placeholder: Option<String>,
}
impl RenderPayload {
pub fn new(message: String, hint: Option<String>, placeholder: Option<String>) -> Self {
Self {
message,
hint,
placeholder,
..Default::default()
}
}
pub fn input(mut self, input: PromptInput) -> Self {
self.input = input;
self
}
pub fn body(mut self, body: PromptBody) -> Self {
self.body = body;
self
}
}
pub trait Prompt {
type Output;
fn setup(&mut self) -> Result<(), Error> {
Ok(())
}
fn validate(&self) -> Result<(), String> {
Ok(())
}
fn handle(&mut self, code: KeyCode, modifiers: KeyModifiers) -> PromptState;
fn submit(&mut self) -> Self::Output;
fn render(&mut self, state: &PromptState) -> Result<RenderPayload, String>;
}
pub struct Promptuity<'a, W: std::io::Write> {
term: &'a mut dyn Terminal<W>,
theme: &'a mut dyn Theme<W>,
state: PromptState,
intro: Option<String>,
outro: Option<String>,
finished: bool,
}
impl<'a, W: std::io::Write> Drop for Promptuity<'a, W> {
fn drop(&mut self) {
if !self.finished {
self.term.disable_raw().expect("Failed to disable raw mode");
}
}
}
impl<'a, W: std::io::Write> Promptuity<'a, W> {
pub fn new(term: &'a mut dyn Terminal<W>, theme: &'a mut dyn Theme<W>) -> Self {
Self {
term,
theme,
state: PromptState::Active,
intro: None,
outro: None,
finished: false,
}
}
pub fn term(&mut self) -> &mut dyn Terminal<W> {
self.term
}
pub fn with_intro(&mut self, intro: impl std::fmt::Display) -> &mut Self {
self.intro = Some(intro.to_string());
self
}
pub fn with_outro(&mut self, outro: impl std::fmt::Display) -> &mut Self {
self.outro = Some(outro.to_string());
self
}
pub fn begin(&mut self) -> Result<(), Error> {
self.term.enable_raw()?;
self.theme.begin(self.term, self.intro.clone())?;
Ok(())
}
pub fn finish(&mut self) -> Result<(), Error> {
self.theme
.finish(self.term, &self.state, self.outro.clone())?;
self.term.disable_raw()?;
self.finished = true;
Ok(())
}
pub fn step(&mut self, message: impl std::fmt::Display) -> Result<(), Error> {
self.theme.step(self.term, message.to_string())?;
Ok(())
}
pub fn log(&mut self, message: impl std::fmt::Display) -> Result<(), Error> {
self.theme.log(self.term, message.to_string())?;
Ok(())
}
pub fn info(&mut self, message: impl std::fmt::Display) -> Result<(), Error> {
self.theme.info(self.term, message.to_string())?;
Ok(())
}
pub fn warn(&mut self, message: impl std::fmt::Display) -> Result<(), Error> {
self.theme.warn(self.term, message.to_string())?;
Ok(())
}
pub fn error(&mut self, message: impl std::fmt::Display) -> Result<(), Error> {
self.theme.error(self.term, message.to_string())?;
Ok(())
}
pub fn success(&mut self, message: impl std::fmt::Display) -> Result<(), Error> {
self.theme.success(self.term, message.to_string())?;
Ok(())
}
pub fn prompt<O>(&mut self, prompt: &mut dyn Prompt<Output = O>) -> Result<O, Error> {
prompt.setup()?;
self.state = PromptState::Active;
self.render(prompt)?;
loop {
let (code, modifiers) = self.term.read_key()?;
let state = prompt.handle(code, modifiers);
self.state = match state {
PromptState::Submit => {
if let Err(msg) = prompt.validate() {
PromptState::Error(msg)
} else {
PromptState::Submit
}
}
state => state,
};
self.render(prompt)?;
match self.state.clone() {
PromptState::Cancel => {
self.finish()?;
return Err(Error::Cancel);
}
PromptState::Fatal(msg) => {
self.finish()?;
return Err(Error::Prompt(msg));
}
PromptState::Submit => {
return Ok(prompt.submit());
}
_ => {}
}
}
}
fn render<O>(&mut self, prompt: &mut dyn Prompt<Output = O>) -> Result<(), Error> {
let res = prompt.render(&self.state).map_err(Error::Prompt)?;
self.theme.render(
self.term,
RenderSnapshot {
state: &self.state,
input: res.input,
body: res.body,
message: res.message,
hint: res.hint,
placeholder: res.placeholder,
},
)?;
Ok(())
}
}