Improve input somewhat
This commit is contained in:
parent
928e9aeb7c
commit
7e0df50788
2 changed files with 279 additions and 110 deletions
254
src/input.rs
Normal file
254
src/input.rs
Normal file
|
@ -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<W: Write> {
|
||||||
|
stdout: W,
|
||||||
|
string: Vec<char>,
|
||||||
|
clipboard: Vec<char>,
|
||||||
|
cursor: usize,
|
||||||
|
view: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> Input<W> {
|
||||||
|
pub fn new(stdout: W) -> Input<W> {
|
||||||
|
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<InputEvent> {
|
||||||
|
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::<String>()))?
|
||||||
|
.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::<String>(),
|
||||||
|
))?
|
||||||
|
.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::<String>()))?
|
||||||
|
.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::<String>()))?
|
||||||
|
.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::<String>(),
|
||||||
|
))?
|
||||||
|
.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::<String>(),
|
||||||
|
))?
|
||||||
|
.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(())
|
||||||
|
}
|
||||||
|
}
|
135
src/main.rs
135
src/main.rs
|
@ -1,16 +1,19 @@
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor,
|
cursor,
|
||||||
event::{poll, read, Event, KeyCode, KeyEvent, KeyModifiers},
|
event::{poll, read, Event},
|
||||||
style::{self, Color, Stylize},
|
style::{self, Color, Stylize},
|
||||||
terminal, ExecutableCommand, QueueableCommand,
|
terminal, ExecutableCommand, QueueableCommand,
|
||||||
};
|
};
|
||||||
use image::imageops::FilterType;
|
use image::imageops::FilterType;
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
use std::io::{self, stdout, Read, Write};
|
use std::io::{self, stdout, Read, Write};
|
||||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
mod input;
|
||||||
mod logger;
|
mod logger;
|
||||||
|
|
||||||
|
use input::{Input, InputEvent};
|
||||||
|
|
||||||
fn render_image(image: image::DynamicImage, width: u32, height: u32) -> io::Result<()> {
|
fn render_image(image: image::DynamicImage, width: u32, height: u32) -> io::Result<()> {
|
||||||
let image = image
|
let image = image
|
||||||
.resize(width, height, FilterType::Triangle)
|
.resize(width, height, FilterType::Triangle)
|
||||||
|
@ -62,7 +65,7 @@ impl Terminal {
|
||||||
struct ChatTab<'a> {
|
struct ChatTab<'a> {
|
||||||
jid: String,
|
jid: String,
|
||||||
logs: Vec<logger::Item<'a>>,
|
logs: Vec<logger::Item<'a>>,
|
||||||
input: Input,
|
input: Input<io::Stdout>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ChatTab<'a> {
|
impl<'a> ChatTab<'a> {
|
||||||
|
@ -145,9 +148,22 @@ impl<'a> ChatTab<'a> {
|
||||||
Event::FocusGained => print!("FocusGained"),
|
Event::FocusGained => print!("FocusGained"),
|
||||||
Event::FocusLost => print!("FocusLost"),
|
Event::FocusLost => print!("FocusLost"),
|
||||||
Event::Key(event) => {
|
Event::Key(event) => {
|
||||||
match self.input.handle_events(event) {
|
match self.input.handle_key(event.code, event.modifiers) {
|
||||||
Ok(MyEvent::Exit) => break,
|
Ok(InputEvent::Exit) => break,
|
||||||
foo => foo?,
|
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),
|
Event::Mouse(event) => print!("{:?}", event),
|
||||||
|
@ -165,112 +181,10 @@ impl<'a> ChatTab<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MyEvent {
|
|
||||||
None,
|
|
||||||
Exit,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Input {
|
|
||||||
string: Vec<char>,
|
|
||||||
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<MyEvent> {
|
|
||||||
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::<String>(),
|
|
||||||
))?
|
|
||||||
.flush()?;
|
|
||||||
self.cursor += 1;
|
|
||||||
}
|
|
||||||
(KeyCode::Enter, _) => println!("{}", self.string.iter().collect::<String>()),
|
|
||||||
_ => todo!(),
|
|
||||||
}
|
|
||||||
Ok(MyEvent::None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn do_main() -> io::Result<()> {
|
async fn do_main() -> io::Result<()> {
|
||||||
terminal::enable_raw_mode()?;
|
terminal::enable_raw_mode()?;
|
||||||
let mut term = Terminal::new();
|
let mut term = Terminal::new();
|
||||||
let mut stdout = stdout();
|
stdout()
|
||||||
stdout
|
|
||||||
.queue(terminal::SetTitle("poezio"))?
|
.queue(terminal::SetTitle("poezio"))?
|
||||||
.queue(terminal::DisableLineWrap)?
|
.queue(terminal::DisableLineWrap)?
|
||||||
.queue(terminal::Clear(terminal::ClearType::All))?;
|
.queue(terminal::Clear(terminal::ClearType::All))?;
|
||||||
|
@ -281,9 +195,10 @@ async fn do_main() -> io::Result<()> {
|
||||||
file.read_to_string(&mut data)?;
|
file.read_to_string(&mut data)?;
|
||||||
let logs = logger::parse_logs(&data).unwrap().1;
|
let logs = logger::parse_logs(&data).unwrap().1;
|
||||||
let jid = String::from("linkmauve@jabberfr.org");
|
let jid = String::from("linkmauve@jabberfr.org");
|
||||||
let input = Input::new();
|
let input = Input::new(stdout());
|
||||||
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();
|
||||||
stdout.queue(cursor::MoveTo(0, 52))?;
|
stdout.queue(cursor::MoveTo(0, 52))?;
|
||||||
stdout.flush()?;
|
stdout.flush()?;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue