Improve input some more
This commit is contained in:
parent
7e0df50788
commit
4eccb02c19
3 changed files with 130 additions and 53 deletions
|
@ -7,7 +7,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.23", default-features = false, features = ["clock"] }
|
chrono = { version = "0.4.23", default-features = false, features = ["clock"] }
|
||||||
crossterm = { version = "0.26", default-features = false }
|
crossterm = { version = "0.26", default-features = false, features = ["bracketed-paste"] }
|
||||||
image = { version = "0.24", default-features = false, features = ["png", "jpeg"] }
|
image = { version = "0.24", default-features = false, features = ["png", "jpeg"] }
|
||||||
nom = { version = "7.1.3", default-features = false, features = ["alloc"] }
|
nom = { version = "7.1.3", default-features = false, features = ["alloc"] }
|
||||||
hsluv = { version = "0.3.1", default-features = false }
|
hsluv = { version = "0.3.1", default-features = false }
|
||||||
|
|
170
src/input.rs
170
src/input.rs
|
@ -4,10 +4,10 @@ use crossterm::{
|
||||||
style, terminal, ExecutableCommand, QueueableCommand,
|
style, terminal, ExecutableCommand, QueueableCommand,
|
||||||
};
|
};
|
||||||
use std::io::{Result, Write};
|
use std::io::{Result, Write};
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
|
|
||||||
// TODO: add punctuation.
|
// TODO: add punctuation.
|
||||||
const SEPARATORS: &[char] = [' ', '\t', '\n'];
|
const SEPARATORS: [char; 3] = [' ', '\t', '\n'];
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum InputEvent {
|
pub enum InputEvent {
|
||||||
|
@ -35,13 +35,47 @@ impl<W: Write> Input<W> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn from(stdout: W, input: &str) -> Input<W> {
|
||||||
|
let string: Vec<char> = input.chars().collect();
|
||||||
|
Input {
|
||||||
|
stdout,
|
||||||
|
cursor: string.len(),
|
||||||
|
string,
|
||||||
|
clipboard: Vec::new(),
|
||||||
|
view: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn cur_width(&self) -> u16 {
|
fn cur_width(&self) -> u16 {
|
||||||
self.string[self.cursor].width().unwrap() as u16
|
self.string[self.cursor].width().unwrap_or(0) as u16
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste(&mut self, text: &[char]) -> Result<()> {
|
||||||
|
if !text.is_empty() {
|
||||||
|
let end = self.string.split_off(self.cursor);
|
||||||
|
let text_len = text.len();
|
||||||
|
let width: u16 = end.iter().map(|c| c.width().unwrap_or(0) as u16).sum();
|
||||||
|
self.stdout
|
||||||
|
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
||||||
|
.queue(style::Print(text.iter().collect::<String>()))?;
|
||||||
|
if !end.is_empty() {
|
||||||
|
self.stdout
|
||||||
|
.queue(style::Print(end.iter().collect::<String>()))?
|
||||||
|
.queue(cursor::MoveLeft(width))?;
|
||||||
|
}
|
||||||
|
self.stdout.flush()?;
|
||||||
|
self.string.extend(text);
|
||||||
|
self.string.extend(end);
|
||||||
|
self.cursor += text_len;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> Result<InputEvent> {
|
pub fn handle_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> Result<InputEvent> {
|
||||||
match (code, modifiers) {
|
match (code, modifiers) {
|
||||||
(KeyCode::Esc, _) => return Ok(InputEvent::Exit),
|
(KeyCode::Esc, _) => return Ok(InputEvent::Exit),
|
||||||
|
(KeyCode::Char('d'), KeyModifiers::CONTROL) => return Ok(InputEvent::Exit),
|
||||||
(KeyCode::Char('l'), KeyModifiers::CONTROL) => {
|
(KeyCode::Char('l'), KeyModifiers::CONTROL) => {
|
||||||
self.stdout
|
self.stdout
|
||||||
.queue(terminal::Clear(terminal::ClearType::CurrentLine))?
|
.queue(terminal::Clear(terminal::ClearType::CurrentLine))?
|
||||||
|
@ -53,14 +87,18 @@ impl<W: Write> Input<W> {
|
||||||
if self.cursor != 0 {
|
if self.cursor != 0 {
|
||||||
self.cursor -= 1;
|
self.cursor -= 1;
|
||||||
let len = self.cur_width();
|
let len = self.cur_width();
|
||||||
self.stdout.execute(cursor::MoveLeft(len))?;
|
if len > 0 {
|
||||||
|
self.stdout.execute(cursor::MoveLeft(len))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KeyCode::Right, KeyModifiers::NONE) => {
|
(KeyCode::Right, KeyModifiers::NONE) => {
|
||||||
if self.cursor < self.string.len() {
|
if self.cursor < self.string.len() {
|
||||||
let len = self.cur_width();
|
let len = self.cur_width();
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
self.stdout.execute(cursor::MoveRight(len))?;
|
if len > 0 {
|
||||||
|
self.stdout.execute(cursor::MoveRight(len))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KeyCode::Home, KeyModifiers::NONE) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => {
|
(KeyCode::Home, KeyModifiers::NONE) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => {
|
||||||
|
@ -70,21 +108,22 @@ impl<W: Write> Input<W> {
|
||||||
}
|
}
|
||||||
(KeyCode::End, KeyModifiers::NONE) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => {
|
(KeyCode::End, KeyModifiers::NONE) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => {
|
||||||
self.cursor = self.string.len();
|
self.cursor = self.string.len();
|
||||||
|
let len = self.string.iter().map(|c| c.width().unwrap_or(0) as u16).sum();
|
||||||
self.stdout
|
self.stdout
|
||||||
.execute(cursor::MoveToColumn(self.cursor as u16))?;
|
.execute(cursor::MoveToColumn(len))?;
|
||||||
}
|
}
|
||||||
(KeyCode::Delete, KeyModifiers::NONE) => {
|
(KeyCode::Delete, KeyModifiers::NONE) => {
|
||||||
if self.cursor < self.string.len() {
|
if self.cursor < self.string.len() {
|
||||||
self.string.remove(self.cursor);
|
self.string.remove(self.cursor);
|
||||||
|
let string = self.string[self.cursor..].iter().collect::<String>();
|
||||||
|
let len = string.width() as u16;
|
||||||
self.stdout
|
self.stdout
|
||||||
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
||||||
.queue(style::Print(
|
.queue(style::Print(string))?
|
||||||
self.string[self.cursor..].iter().collect::<String>(),
|
.queue(cursor::MoveLeft(len))?
|
||||||
))?
|
|
||||||
.flush()?;
|
.flush()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KeyCode::Char('d'), KeyModifiers::CONTROL) => return Ok(InputEvent::Exit),
|
|
||||||
(KeyCode::Char('u'), KeyModifiers::CONTROL) => {
|
(KeyCode::Char('u'), KeyModifiers::CONTROL) => {
|
||||||
if self.cursor > 0 {
|
if self.cursor > 0 {
|
||||||
self.clipboard = self.string.drain(..self.cursor).collect();
|
self.clipboard = self.string.drain(..self.cursor).collect();
|
||||||
|
@ -94,6 +133,7 @@ impl<W: Write> Input<W> {
|
||||||
.queue(cursor::MoveToColumn(0))?
|
.queue(cursor::MoveToColumn(0))?
|
||||||
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
||||||
.queue(style::Print(self.string.iter().collect::<String>()))?
|
.queue(style::Print(self.string.iter().collect::<String>()))?
|
||||||
|
.queue(cursor::MoveToColumn(0))?
|
||||||
.flush()?;
|
.flush()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,64 +143,72 @@ impl<W: Write> Input<W> {
|
||||||
.execute(terminal::Clear(terminal::ClearType::UntilNewLine))?;
|
.execute(terminal::Clear(terminal::ClearType::UntilNewLine))?;
|
||||||
}
|
}
|
||||||
(KeyCode::Char('y'), KeyModifiers::CONTROL) => {
|
(KeyCode::Char('y'), KeyModifiers::CONTROL) => {
|
||||||
if !self.clipboard.is_empty() {
|
// TODO: find a better solution than a clone here.
|
||||||
let end = self.string.split_off(self.cursor);
|
let clipboard = self.clipboard.clone();
|
||||||
self.string.extend(&self.clipboard);
|
self.paste(&clipboard)?;
|
||||||
self.string.extend(end);
|
|
||||||
self.stdout
|
|
||||||
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
|
||||||
.queue(style::Print(self.string.iter().collect::<String>()))?
|
|
||||||
.flush()?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(KeyCode::Left, KeyModifiers::CONTROL) | (KeyCode::Char('b'), KeyModifiers::ALT) => {
|
(KeyCode::Left, KeyModifiers::CONTROL) | (KeyCode::Char('b'), KeyModifiers::ALT) => {
|
||||||
|
let mut len = 0;
|
||||||
while self.cursor != 0 && SEPARATORS.contains(&self.string[self.cursor - 1]) {
|
while self.cursor != 0 && SEPARATORS.contains(&self.string[self.cursor - 1]) {
|
||||||
self.cursor -= 1;
|
self.cursor -= 1;
|
||||||
let len = self.cur_width();
|
len += self.cur_width();
|
||||||
self.stdout.queue(cursor::MoveLeft(len))?;
|
|
||||||
}
|
}
|
||||||
while self.cursor != 0 && !SEPARATORS.contains(&self.string[self.cursor - 1]) {
|
while self.cursor != 0 && !SEPARATORS.contains(&self.string[self.cursor - 1]) {
|
||||||
self.cursor -= 1;
|
self.cursor -= 1;
|
||||||
let len = self.cur_width();
|
len += self.cur_width();
|
||||||
self.stdout.queue(cursor::MoveLeft(len))?;
|
}
|
||||||
|
if len > 0 {
|
||||||
|
self.stdout.execute(cursor::MoveLeft(len))?;
|
||||||
}
|
}
|
||||||
self.stdout.flush()?;
|
|
||||||
}
|
}
|
||||||
(KeyCode::Char('w'), KeyModifiers::CONTROL) => {
|
(KeyCode::Char('w'), KeyModifiers::CONTROL) => {
|
||||||
let end = self.cursor;
|
let end = self.cursor;
|
||||||
|
let mut len = 0;
|
||||||
while self.cursor != 0 && SEPARATORS.contains(&self.string[self.cursor - 1]) {
|
while self.cursor != 0 && SEPARATORS.contains(&self.string[self.cursor - 1]) {
|
||||||
self.cursor -= 1;
|
self.cursor -= 1;
|
||||||
let len = self.cur_width();
|
len += self.cur_width();
|
||||||
self.stdout.queue(cursor::MoveLeft(len))?;
|
|
||||||
}
|
}
|
||||||
while self.cursor != 0 && !SEPARATORS.contains(&self.string[self.cursor - 1]) {
|
while self.cursor != 0 && !SEPARATORS.contains(&self.string[self.cursor - 1]) {
|
||||||
self.cursor -= 1;
|
self.cursor -= 1;
|
||||||
let len = self.cur_width();
|
len += self.cur_width();
|
||||||
self.stdout.queue(cursor::MoveLeft(len))?;
|
}
|
||||||
|
if self.cursor < end {
|
||||||
|
self.string.drain(self.cursor..end);
|
||||||
|
let string = self.string[self.cursor..].iter().collect::<String>();
|
||||||
|
let len2 = string.width() as u16;
|
||||||
|
self.stdout
|
||||||
|
.queue(cursor::MoveLeft(len))?
|
||||||
|
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
||||||
|
.queue(style::Print(string))?
|
||||||
|
.queue(cursor::MoveLeft(len2))?
|
||||||
|
.flush()?;
|
||||||
|
self.stdout.flush()?;
|
||||||
}
|
}
|
||||||
self.string.drain(self.cursor..end);
|
|
||||||
self.stdout.flush()?;
|
|
||||||
}
|
}
|
||||||
(KeyCode::Right, KeyModifiers::CONTROL) | (KeyCode::Char('f'), KeyModifiers::ALT) => {
|
(KeyCode::Right, KeyModifiers::CONTROL) | (KeyCode::Char('f'), KeyModifiers::ALT) => {
|
||||||
|
let mut len = 0;
|
||||||
while self.cursor < self.string.len()
|
while self.cursor < self.string.len()
|
||||||
&& SEPARATORS.contains(&self.string[self.cursor])
|
&& SEPARATORS.contains(&self.string[self.cursor])
|
||||||
{
|
{
|
||||||
let len = self.cur_width();
|
len += self.cur_width();
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
self.stdout.queue(cursor::MoveRight(len))?;
|
|
||||||
}
|
}
|
||||||
while self.cursor < self.string.len()
|
while self.cursor < self.string.len()
|
||||||
&& !SEPARATORS.contains(&self.string[self.cursor])
|
&& !SEPARATORS.contains(&self.string[self.cursor])
|
||||||
{
|
{
|
||||||
let len = self.cur_width();
|
len += self.cur_width();
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
self.stdout.queue(cursor::MoveRight(len))?;
|
|
||||||
}
|
}
|
||||||
self.stdout.flush()?;
|
if len > 0 {
|
||||||
|
self.stdout.execute(cursor::MoveRight(len))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(KeyCode::Backspace, _) | (KeyCode::Char('h'), KeyModifiers::CONTROL) => {
|
(KeyCode::Backspace, _) | (KeyCode::Char('h'), KeyModifiers::CONTROL) => {
|
||||||
|
let end = self.string.split_off(self.cursor);
|
||||||
|
if !end.is_empty() {
|
||||||
|
}
|
||||||
if let Some(c) = self.string.pop() {
|
if let Some(c) = self.string.pop() {
|
||||||
let len = c.width().unwrap() as u16;
|
let len = c.width().unwrap_or(0) as u16;
|
||||||
self.cursor -= 1;
|
self.cursor -= 1;
|
||||||
self.stdout
|
self.stdout
|
||||||
.queue(cursor::MoveLeft(len))?
|
.queue(cursor::MoveLeft(len))?
|
||||||
|
@ -175,19 +223,13 @@ impl<W: Write> Input<W> {
|
||||||
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
||||||
.queue(style::Print(
|
.queue(style::Print(
|
||||||
&self.string[self.cursor..].iter().collect::<String>(),
|
&self.string[self.cursor..].iter().collect::<String>(),
|
||||||
))?
|
))?;
|
||||||
.flush()?;
|
let len: u16 = self.string[self.cursor..].iter().map(|c| c.width().unwrap_or(0) as u16).sum();
|
||||||
self.cursor += 1;
|
|
||||||
}
|
|
||||||
(KeyCode::Char('j'), KeyModifiers::CONTROL) => {
|
|
||||||
self.string.insert(self.cursor, '\n');
|
|
||||||
self.stdout
|
|
||||||
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
|
||||||
.queue(style::Print(
|
|
||||||
&self.string[self.cursor..].iter().collect::<String>(),
|
|
||||||
))?
|
|
||||||
.flush()?;
|
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
|
if self.cursor < self.string.len() {
|
||||||
|
self.stdout.queue(cursor::MoveLeft(len))?;
|
||||||
|
}
|
||||||
|
self.stdout.flush()?;
|
||||||
}
|
}
|
||||||
(KeyCode::Enter, _) => {
|
(KeyCode::Enter, _) => {
|
||||||
let text = self.string.drain(..).collect();
|
let text = self.string.drain(..).collect();
|
||||||
|
@ -251,4 +293,36 @@ mod test {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paste() -> Result<()> {
|
||||||
|
let mut stdout = Vec::new();
|
||||||
|
let mut input = Input::new(&mut stdout);
|
||||||
|
input.handle_key(KeyCode::Char('a'), KeyModifiers::NONE)?;
|
||||||
|
input.handle_key(KeyCode::Char('b'), KeyModifiers::NONE)?;
|
||||||
|
input.handle_key(KeyCode::Left, KeyModifiers::NONE)?;
|
||||||
|
input.handle_key(KeyCode::Char('u'), KeyModifiers::CONTROL)?;
|
||||||
|
input.handle_key(KeyCode::Right, KeyModifiers::NONE)?;
|
||||||
|
input.handle_key(KeyCode::Char('y'), KeyModifiers::CONTROL)?;
|
||||||
|
input.handle_key(KeyCode::Char('y'), KeyModifiers::CONTROL)?;
|
||||||
|
let event = input.handle_key(KeyCode::Enter, KeyModifiers::NONE)?;
|
||||||
|
match event {
|
||||||
|
InputEvent::Text(text) => assert_eq!(text, "baa"),
|
||||||
|
evt => panic!("Wrong event: {evt:?}"),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn long_emoji() -> Result<()> {
|
||||||
|
let mut stdout = Vec::new();
|
||||||
|
let mut input = Input::from(&mut stdout, "👩👩👧👧");
|
||||||
|
input.handle_key(KeyCode::Left, KeyModifiers::NONE)?;
|
||||||
|
let event = input.handle_key(KeyCode::Enter, KeyModifiers::NONE)?;
|
||||||
|
match event {
|
||||||
|
InputEvent::Text(text) => assert_eq!(text, "👩\u{200d}👩\u{200d}👧\u{200d}👧"),
|
||||||
|
evt => panic!("Wrong event: {evt:?}"),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
11
src/main.rs
11
src/main.rs
|
@ -118,7 +118,7 @@ impl<'a> ChatTab<'a> {
|
||||||
background: Some(Color::DarkGrey),
|
background: Some(Color::DarkGrey),
|
||||||
};
|
};
|
||||||
stdout
|
stdout
|
||||||
.queue(cursor::MoveTo(0, 51))?
|
.queue(cursor::MoveTo(0, term.height as u16 - 2))?
|
||||||
.queue(style::SetColors(background))?
|
.queue(style::SetColors(background))?
|
||||||
.queue(style::Print("["))?
|
.queue(style::Print("["))?
|
||||||
.queue(style::PrintStyledContent(
|
.queue(style::PrintStyledContent(
|
||||||
|
@ -134,7 +134,6 @@ impl<'a> ChatTab<'a> {
|
||||||
print!("[K");
|
print!("[K");
|
||||||
stdout
|
stdout
|
||||||
.queue(style::ResetColor)?
|
.queue(style::ResetColor)?
|
||||||
.queue(cursor::MoveTo(0, 52))?
|
|
||||||
.flush()?;
|
.flush()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -167,7 +166,10 @@ impl<'a> ChatTab<'a> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Event::Mouse(event) => print!("{:?}", event),
|
Event::Mouse(event) => print!("{:?}", event),
|
||||||
//Event::Paste(data) => print!("{:?}", data),
|
Event::Paste(string) => {
|
||||||
|
let text: Vec<char> = string.chars().collect();
|
||||||
|
self.input.paste(&text)?;
|
||||||
|
}
|
||||||
Event::Resize(width, height) => {
|
Event::Resize(width, height) => {
|
||||||
term.width = width as usize;
|
term.width = width as usize;
|
||||||
term.height = height as usize;
|
term.height = height as usize;
|
||||||
|
@ -187,6 +189,7 @@ async fn do_main() -> io::Result<()> {
|
||||||
stdout()
|
stdout()
|
||||||
.queue(terminal::SetTitle("poezio"))?
|
.queue(terminal::SetTitle("poezio"))?
|
||||||
.queue(terminal::DisableLineWrap)?
|
.queue(terminal::DisableLineWrap)?
|
||||||
|
.queue(crossterm::event::EnableBracketedPaste)?
|
||||||
.queue(terminal::Clear(terminal::ClearType::All))?;
|
.queue(terminal::Clear(terminal::ClearType::All))?;
|
||||||
let image = image::open("/home/linkmauve/avatar.png").unwrap();
|
let image = image::open("/home/linkmauve/avatar.png").unwrap();
|
||||||
render_image(image, 16, 16)?;
|
render_image(image, 16, 16)?;
|
||||||
|
@ -199,7 +202,7 @@ async fn do_main() -> io::Result<()> {
|
||||||
let mut tab = ChatTab { jid, logs, input };
|
let mut tab = ChatTab { jid, logs, input };
|
||||||
tab.do_redraw(&term)?;
|
tab.do_redraw(&term)?;
|
||||||
let mut stdout = stdout();
|
let mut stdout = stdout();
|
||||||
stdout.queue(cursor::MoveTo(0, 52))?;
|
stdout.queue(cursor::MoveTo(0, term.height as u16 - 1))?;
|
||||||
stdout.flush()?;
|
stdout.flush()?;
|
||||||
|
|
||||||
tab.handle_events(&mut term)?;
|
tab.handle_events(&mut term)?;
|
||||||
|
|
Loading…
Reference in a new issue