diff --git a/Cargo.toml b/Cargo.toml index f3b2725..5c2cb75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] 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"] } nom = { version = "7.1.3", default-features = false, features = ["alloc"] } hsluv = { version = "0.3.1", default-features = false } diff --git a/src/input.rs b/src/input.rs index bd30149..b8cd089 100644 --- a/src/input.rs +++ b/src/input.rs @@ -4,10 +4,10 @@ use crossterm::{ style, terminal, ExecutableCommand, QueueableCommand, }; use std::io::{Result, Write}; -use unicode_width::UnicodeWidthChar; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; // TODO: add punctuation. -const SEPARATORS: &[char] = [' ', '\t', '\n']; +const SEPARATORS: [char; 3] = [' ', '\t', '\n']; #[derive(Debug)] pub enum InputEvent { @@ -35,13 +35,47 @@ impl Input { } } + #[cfg(test)] + fn from(stdout: W, input: &str) -> Input { + let string: Vec = input.chars().collect(); + Input { + stdout, + cursor: string.len(), + string, + clipboard: Vec::new(), + view: 0, + } + } + 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::()))?; + if !end.is_empty() { + self.stdout + .queue(style::Print(end.iter().collect::()))? + .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 { match (code, modifiers) { (KeyCode::Esc, _) => return Ok(InputEvent::Exit), + (KeyCode::Char('d'), KeyModifiers::CONTROL) => return Ok(InputEvent::Exit), (KeyCode::Char('l'), KeyModifiers::CONTROL) => { self.stdout .queue(terminal::Clear(terminal::ClearType::CurrentLine))? @@ -53,14 +87,18 @@ impl Input { if self.cursor != 0 { self.cursor -= 1; let len = self.cur_width(); - self.stdout.execute(cursor::MoveLeft(len))?; + if len > 0 { + self.stdout.execute(cursor::MoveLeft(len))?; + } } } (KeyCode::Right, KeyModifiers::NONE) => { if self.cursor < self.string.len() { let len = self.cur_width(); 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) => { @@ -70,21 +108,22 @@ impl Input { } (KeyCode::End, KeyModifiers::NONE) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => { self.cursor = self.string.len(); + let len = self.string.iter().map(|c| c.width().unwrap_or(0) as u16).sum(); self.stdout - .execute(cursor::MoveToColumn(self.cursor as u16))?; + .execute(cursor::MoveToColumn(len))?; } (KeyCode::Delete, KeyModifiers::NONE) => { if self.cursor < self.string.len() { self.string.remove(self.cursor); + let string = self.string[self.cursor..].iter().collect::(); + let len = string.width() as u16; self.stdout .queue(terminal::Clear(terminal::ClearType::UntilNewLine))? - .queue(style::Print( - self.string[self.cursor..].iter().collect::(), - ))? + .queue(style::Print(string))? + .queue(cursor::MoveLeft(len))? .flush()?; } } - (KeyCode::Char('d'), KeyModifiers::CONTROL) => return Ok(InputEvent::Exit), (KeyCode::Char('u'), KeyModifiers::CONTROL) => { if self.cursor > 0 { self.clipboard = self.string.drain(..self.cursor).collect(); @@ -94,6 +133,7 @@ impl Input { .queue(cursor::MoveToColumn(0))? .queue(terminal::Clear(terminal::ClearType::UntilNewLine))? .queue(style::Print(self.string.iter().collect::()))? + .queue(cursor::MoveToColumn(0))? .flush()?; } } @@ -103,64 +143,72 @@ impl Input { .execute(terminal::Clear(terminal::ClearType::UntilNewLine))?; } (KeyCode::Char('y'), KeyModifiers::CONTROL) => { - if !self.clipboard.is_empty() { - let end = self.string.split_off(self.cursor); - self.string.extend(&self.clipboard); - self.string.extend(end); - self.stdout - .queue(terminal::Clear(terminal::ClearType::UntilNewLine))? - .queue(style::Print(self.string.iter().collect::()))? - .flush()?; - } + // TODO: find a better solution than a clone here. + let clipboard = self.clipboard.clone(); + self.paste(&clipboard)?; } (KeyCode::Left, KeyModifiers::CONTROL) | (KeyCode::Char('b'), KeyModifiers::ALT) => { + let mut len = 0; while self.cursor != 0 && SEPARATORS.contains(&self.string[self.cursor - 1]) { self.cursor -= 1; - let len = self.cur_width(); - self.stdout.queue(cursor::MoveLeft(len))?; + len += self.cur_width(); } while self.cursor != 0 && !SEPARATORS.contains(&self.string[self.cursor - 1]) { self.cursor -= 1; - let len = self.cur_width(); - self.stdout.queue(cursor::MoveLeft(len))?; + len += self.cur_width(); + } + if len > 0 { + self.stdout.execute(cursor::MoveLeft(len))?; } - self.stdout.flush()?; } (KeyCode::Char('w'), KeyModifiers::CONTROL) => { let end = self.cursor; + let mut len = 0; while self.cursor != 0 && SEPARATORS.contains(&self.string[self.cursor - 1]) { self.cursor -= 1; - let len = self.cur_width(); - self.stdout.queue(cursor::MoveLeft(len))?; + len += self.cur_width(); } while self.cursor != 0 && !SEPARATORS.contains(&self.string[self.cursor - 1]) { self.cursor -= 1; - let len = self.cur_width(); - self.stdout.queue(cursor::MoveLeft(len))?; + len += self.cur_width(); + } + if self.cursor < end { + self.string.drain(self.cursor..end); + let string = self.string[self.cursor..].iter().collect::(); + 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) => { + let mut len = 0; while self.cursor < self.string.len() && SEPARATORS.contains(&self.string[self.cursor]) { - let len = self.cur_width(); + len += self.cur_width(); self.cursor += 1; - self.stdout.queue(cursor::MoveRight(len))?; } while self.cursor < self.string.len() && !SEPARATORS.contains(&self.string[self.cursor]) { - let len = self.cur_width(); + len += self.cur_width(); 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) => { + let end = self.string.split_off(self.cursor); + if !end.is_empty() { + } 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.stdout .queue(cursor::MoveLeft(len))? @@ -175,19 +223,13 @@ impl Input { .queue(terminal::Clear(terminal::ClearType::UntilNewLine))? .queue(style::Print( &self.string[self.cursor..].iter().collect::(), - ))? - .flush()?; - 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::(), - ))? - .flush()?; + ))?; + let len: u16 = self.string[self.cursor..].iter().map(|c| c.width().unwrap_or(0) as u16).sum(); self.cursor += 1; + if self.cursor < self.string.len() { + self.stdout.queue(cursor::MoveLeft(len))?; + } + self.stdout.flush()?; } (KeyCode::Enter, _) => { let text = self.string.drain(..).collect(); @@ -251,4 +293,36 @@ mod test { } 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(()) + } } diff --git a/src/main.rs b/src/main.rs index 576ef80..1ef1df8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -118,7 +118,7 @@ impl<'a> ChatTab<'a> { background: Some(Color::DarkGrey), }; stdout - .queue(cursor::MoveTo(0, 51))? + .queue(cursor::MoveTo(0, term.height as u16 - 2))? .queue(style::SetColors(background))? .queue(style::Print("["))? .queue(style::PrintStyledContent( @@ -134,7 +134,6 @@ impl<'a> ChatTab<'a> { print!(""); stdout .queue(style::ResetColor)? - .queue(cursor::MoveTo(0, 52))? .flush()?; Ok(()) } @@ -167,7 +166,10 @@ impl<'a> ChatTab<'a> { }; } Event::Mouse(event) => print!("{:?}", event), - //Event::Paste(data) => print!("{:?}", data), + Event::Paste(string) => { + let text: Vec = string.chars().collect(); + self.input.paste(&text)?; + } Event::Resize(width, height) => { term.width = width as usize; term.height = height as usize; @@ -187,6 +189,7 @@ async fn do_main() -> io::Result<()> { stdout() .queue(terminal::SetTitle("poezio"))? .queue(terminal::DisableLineWrap)? + .queue(crossterm::event::EnableBracketedPaste)? .queue(terminal::Clear(terminal::ClearType::All))?; let image = image::open("/home/linkmauve/avatar.png").unwrap(); render_image(image, 16, 16)?; @@ -199,7 +202,7 @@ async fn do_main() -> io::Result<()> { let mut tab = ChatTab { jid, logs, input }; tab.do_redraw(&term)?; let mut stdout = stdout(); - stdout.queue(cursor::MoveTo(0, 52))?; + stdout.queue(cursor::MoveTo(0, term.height as u16 - 1))?; stdout.flush()?; tab.handle_events(&mut term)?;