diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..bd30149 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,254 @@ +use crossterm::{ + cursor, + event::{KeyCode, KeyModifiers}, + style, terminal, ExecutableCommand, QueueableCommand, +}; +use std::io::{Result, Write}; +use unicode_width::UnicodeWidthChar; + +// TODO: add punctuation. +const SEPARATORS: &[char] = [' ', '\t', '\n']; + +#[derive(Debug)] +pub enum InputEvent { + None, + Exit, + Text(String), +} + +pub struct Input { + stdout: W, + string: Vec, + clipboard: Vec, + cursor: usize, + view: usize, +} + +impl Input { + pub fn new(stdout: W) -> Input { + Input { + stdout, + string: Vec::new(), + clipboard: Vec::new(), + cursor: 0, + view: 0, + } + } + + fn cur_width(&self) -> u16 { + self.string[self.cursor].width().unwrap() as u16 + } + + pub fn handle_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> Result { + match (code, modifiers) { + (KeyCode::Esc, _) => return Ok(InputEvent::Exit), + (KeyCode::Char('l'), KeyModifiers::CONTROL) => { + self.stdout + .queue(terminal::Clear(terminal::ClearType::CurrentLine))? + .queue(cursor::MoveToColumn(0))? + .queue(style::Print(self.string.iter().collect::()))? + .flush()?; + } + (KeyCode::Left, KeyModifiers::NONE) => { + if self.cursor != 0 { + self.cursor -= 1; + let len = self.cur_width(); + 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))?; + } + } + (KeyCode::Home, KeyModifiers::NONE) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => { + self.cursor = 0; + self.view = 0; + self.stdout.execute(cursor::MoveToColumn(0))?; + } + (KeyCode::End, KeyModifiers::NONE) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => { + self.cursor = self.string.len(); + self.stdout + .execute(cursor::MoveToColumn(self.cursor as u16))?; + } + (KeyCode::Delete, KeyModifiers::NONE) => { + if self.cursor < self.string.len() { + self.string.remove(self.cursor); + self.stdout + .queue(terminal::Clear(terminal::ClearType::UntilNewLine))? + .queue(style::Print( + self.string[self.cursor..].iter().collect::(), + ))? + .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(); + self.cursor = 0; + self.view = 0; + self.stdout + .queue(cursor::MoveToColumn(0))? + .queue(terminal::Clear(terminal::ClearType::UntilNewLine))? + .queue(style::Print(self.string.iter().collect::()))? + .flush()?; + } + } + (KeyCode::Char('k'), KeyModifiers::CONTROL) => { + self.clipboard = self.string.drain(self.cursor..).collect(); + self.stdout + .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()?; + } + } + (KeyCode::Left, KeyModifiers::CONTROL) | (KeyCode::Char('b'), KeyModifiers::ALT) => { + 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))?; + } + 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))?; + } + self.stdout.flush()?; + } + (KeyCode::Char('w'), KeyModifiers::CONTROL) => { + let end = self.cursor; + 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))?; + } + 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))?; + } + self.string.drain(self.cursor..end); + self.stdout.flush()?; + } + (KeyCode::Right, KeyModifiers::CONTROL) | (KeyCode::Char('f'), KeyModifiers::ALT) => { + while self.cursor < self.string.len() + && SEPARATORS.contains(&self.string[self.cursor]) + { + let 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(); + self.cursor += 1; + self.stdout.queue(cursor::MoveRight(len))?; + } + self.stdout.flush()?; + } + (KeyCode::Backspace, _) | (KeyCode::Char('h'), KeyModifiers::CONTROL) => { + if let Some(c) = self.string.pop() { + let len = c.width().unwrap() as u16; + self.cursor -= 1; + self.stdout + .queue(cursor::MoveLeft(len))? + .queue(style::Print(' '))? + .queue(cursor::MoveLeft(1))? + .flush()?; + } + } + (KeyCode::Char(c), KeyModifiers::NONE) | (KeyCode::Char(c), KeyModifiers::SHIFT) => { + self.string.insert(self.cursor, c); + self.stdout + .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()?; + self.cursor += 1; + } + (KeyCode::Enter, _) => { + let text = self.string.drain(..).collect(); + self.cursor = 0; + self.view = 0; + return Ok(InputEvent::Text(text)); + } + key => todo!("{key:?}"), + } + Ok(InputEvent::None) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn insert() -> 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::Left, KeyModifiers::NONE)?; + input.handle_key(KeyCode::Char('b'), KeyModifiers::NONE)?; + let event = input.handle_key(KeyCode::Enter, KeyModifiers::NONE)?; + match event { + InputEvent::Text(text) => assert_eq!(text, "ba"), + evt => panic!("Wrong event: {evt:?}"), + } + Ok(()) + } + + #[test] + fn erase_left() -> 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)?; + let event = input.handle_key(KeyCode::Enter, KeyModifiers::NONE)?; + match event { + InputEvent::Text(text) => assert_eq!(text, "b"), + evt => panic!("Wrong event: {evt:?}"), + } + Ok(()) + } + + #[test] + fn erase_right() -> 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('k'), KeyModifiers::CONTROL)?; + let event = input.handle_key(KeyCode::Enter, KeyModifiers::NONE)?; + match event { + InputEvent::Text(text) => assert_eq!(text, "a"), + evt => panic!("Wrong event: {evt:?}"), + } + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index f815212..576ef80 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,19 @@ use crossterm::{ cursor, - event::{poll, read, Event, KeyCode, KeyEvent, KeyModifiers}, + event::{poll, read, Event}, style::{self, Color, Stylize}, terminal, ExecutableCommand, QueueableCommand, }; use image::imageops::FilterType; use sha1::{Digest, Sha1}; use std::io::{self, stdout, Read, Write}; -use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; +use unicode_width::UnicodeWidthStr; +mod input; mod logger; +use input::{Input, InputEvent}; + fn render_image(image: image::DynamicImage, width: u32, height: u32) -> io::Result<()> { let image = image .resize(width, height, FilterType::Triangle) @@ -62,7 +65,7 @@ impl Terminal { struct ChatTab<'a> { jid: String, logs: Vec>, - input: Input, + input: Input, } impl<'a> ChatTab<'a> { @@ -145,9 +148,22 @@ impl<'a> ChatTab<'a> { Event::FocusGained => print!("FocusGained"), Event::FocusLost => print!("FocusLost"), Event::Key(event) => { - match self.input.handle_events(event) { - Ok(MyEvent::Exit) => break, - foo => foo?, + match self.input.handle_key(event.code, event.modifiers) { + Ok(InputEvent::Exit) => break, + Ok(InputEvent::Text(message)) => { + /* + let item = logger::Item::Message(logger::LogMessage { + nick: "Link Mauve", + time: chrono::offset::Utc::now(), + message, + }); + self.logs.push(item); + */ + println!("{message:?}"); + } + foo => { + foo?; + } }; } Event::Mouse(event) => print!("{:?}", event), @@ -165,112 +181,10 @@ impl<'a> ChatTab<'a> { } } -enum MyEvent { - None, - Exit, -} - -struct Input { - string: Vec, - cursor: usize, - view: usize, -} - -impl Input { - fn new() -> Input { - Input { - string: Vec::new(), - cursor: 0, - view: 0, - } - } - - fn char_len(&self) -> u16 { - self.string[self.cursor].width().unwrap() as u16 - } - - fn handle_events(&mut self, event: KeyEvent) -> io::Result { - let mut stdout = stdout(); - let KeyEvent { - code, - modifiers, - .. - } = event; - match (code, modifiers) { - (KeyCode::Esc, _) => return Ok(MyEvent::Exit), - (KeyCode::Char('d'), KeyModifiers::CONTROL) => return Ok(MyEvent::Exit), - (KeyCode::Left, KeyModifiers::NONE) => { - if self.cursor != 0 { - self.cursor -= 1; - let len = self.char_len(); - stdout.execute(cursor::MoveLeft(len))?; - } - } - (KeyCode::Left, KeyModifiers::CONTROL) => { - let separators = [' ', '\n']; // TODO: add punctuation. - while self.cursor != 0 && separators.contains(&self.string[self.cursor - 1]) { - self.cursor -= 1; - let len = self.char_len(); - stdout.queue(cursor::MoveLeft(len))?; - } - while self.cursor != 0 && !separators.contains(&self.string[self.cursor - 1]) { - self.cursor -= 1; - let len = self.char_len(); - stdout.queue(cursor::MoveLeft(len))?; - } - stdout.flush()?; - } - (KeyCode::Right, KeyModifiers::NONE) => { - if self.cursor < self.string.len() { - let len = self.char_len(); - self.cursor += 1; - stdout.execute(cursor::MoveRight(len))?; - } - } - (KeyCode::Right, KeyModifiers::CONTROL) => { - todo!(); - } - (KeyCode::Home, KeyModifiers::NONE) => { - self.cursor = 0; - stdout.execute(cursor::MoveToColumn(0))?; - } - (KeyCode::End, KeyModifiers::NONE) => { - self.cursor = self.string.len(); - stdout.execute(cursor::MoveToColumn(self.cursor as u16))?; - } - (KeyCode::Backspace, _) => { - if let Some(c) = self.string.pop() { - let len = c.width().unwrap() as u16; - self.cursor -= 1; - stdout - .queue(cursor::MoveLeft(len))? - .queue(style::Print(' '))? - .queue(cursor::MoveLeft(1))? - .flush()?; - } - } - (KeyCode::Char(c), KeyModifiers::NONE) | (KeyCode::Char(c), KeyModifiers::SHIFT) => { - self.string.insert(self.cursor, c); - stdout - .queue(terminal::Clear(terminal::ClearType::UntilNewLine))? - .queue(style::Print( - &self.string[self.cursor..].iter().collect::(), - ))? - .flush()?; - self.cursor += 1; - } - (KeyCode::Enter, _) => println!("{}", self.string.iter().collect::()), - _ => todo!(), - } - Ok(MyEvent::None) - } -} - async fn do_main() -> io::Result<()> { terminal::enable_raw_mode()?; let mut term = Terminal::new(); - let mut stdout = stdout(); - stdout + stdout() .queue(terminal::SetTitle("poezio"))? .queue(terminal::DisableLineWrap)? .queue(terminal::Clear(terminal::ClearType::All))?; @@ -281,9 +195,10 @@ async fn do_main() -> io::Result<()> { file.read_to_string(&mut data)?; let logs = logger::parse_logs(&data).unwrap().1; let jid = String::from("linkmauve@jabberfr.org"); - let input = Input::new(); + let input = Input::new(stdout()); let mut tab = ChatTab { jid, logs, input }; tab.do_redraw(&term)?; + let mut stdout = stdout(); stdout.queue(cursor::MoveTo(0, 52))?; stdout.flush()?;