diff --git a/Cargo.lock b/Cargo.lock index 266e18d3..df42a189 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -991,6 +991,7 @@ dependencies = [ "dirs", "futures", "irc", + "lazy_static", "rand", "rustyline", "serde", diff --git a/Cargo.toml b/Cargo.toml index 42cdcf85..2b3dd345 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ serde = { version = "1.0.130", features = ["derive"] } textwrap = "0.14.2" dirs = "4.0.0" rustyline = "9.0.0" +lazy_static = "1.4.0" [[bin]] bench = false diff --git a/README.md b/README.md index 350323da..8f3e1b74 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ | Key | Description | |-------|------------------------------------------------------------------------------------------------------| - | `?` | Have the keybinds window appear. | - | `i` | Enter insert mode for sending messages. Exit this mode with `Esc`. | - | `Esc` | Exits out of layered windows, such as going from insert mode, to normal, to exiting the application. | | `c` | Go to the chat window chat. | + | `i` | Enter insert mode for sending messages. Exit this mode with `Esc`. | + | `?` | Have the keybinds window appear. | | `q` | Quit out of the entire application. | + | `Esc` | Exits out of layered windows, such as going from insert mode, to normal, to exiting the application. | @@ -26,6 +26,19 @@ |------------|-------------------------------------------------------------| | `Ctrl + w` | Cuts a single word (from the cursor to the next whitespace) | | `Ctrl + u` | Cuts the entire line | + | `Ctrl + f` | Move cursor to the right | + | `Ctrl + b` | Move cursor to the left | + | `Ctrl + a` | Move cursor to the start | + | `Ctrl + e` | Move cursor to the end | + | `Alt + f` | Move to the end of the next word | + | `Alt + b` | Move to the start of the previous word | + | `Ctrl + t` | Swap previous item with current item | + | `Alt + t` | Swap previous word with current word | + | `Ctrl + u` | Remove everything before the cursor | + | `Ctrl + k` | Remove everything after the cursor | + | `Ctrl + w` | Remove the previous word | + | `Ctrl + d` | Remove item to the right | + | `Esc` | Drop back to previous window layer | diff --git a/src/terminal.rs b/src/terminal.rs index 8f904bd5..72edad4d 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -151,13 +151,16 @@ pub async fn ui_driver( } Key::Enter => { let input_message = app.input_text.as_str(); - app.messages.push_front(data_builder.user( - config.twitch.username.to_string(), - input_message.to_string(), - )); - tx.send(input_message.to_string()).await.unwrap(); - app.input_text.update("", 0); + if !input_message.is_empty() { + app.messages.push_front(data_builder.user( + config.twitch.username.to_string(), + input_message.to_string(), + )); + + tx.send(input_message.to_string()).await.unwrap(); + app.input_text.update("", 0); + } } Key::Char(c) => { app.input_text.insert(c, 1); diff --git a/src/ui/help.rs b/src/ui/help.rs index 15939e3b..85589fe8 100644 --- a/src/ui/help.rs +++ b/src/ui/help.rs @@ -6,7 +6,10 @@ use tui::{ widgets::{Block, Borders, Row, Table}, }; -use crate::utils::{styles, text::vector2_col_max}; +use crate::{ + ui::keys::{COLUMN_TITLES, INSERT_MODE, NORMAL_MODE}, + utils::{styles, text::vector_column_max}, +}; pub fn draw_keybinds_ui(frame: &mut Frame) -> Result<()> where @@ -15,37 +18,46 @@ where let vertical_chunks = Layout::default() .direction(Direction::Vertical) .margin(5) - .constraints([Constraint::Percentage(100)].as_ref()) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(frame.size()); - let mut keybinds = vec![ - vec!["Description", "Keybind"], - vec!["Bring up the chat window", "c"], - vec!["Keybinds help (this window)", "?"], - vec![ - "Exit out layer window/entire app when in normal mode", - "Esc", - ], - vec!["Quit this application", "q"], - ]; - - let (maximum_description_width, maximum_keybind_width) = vector2_col_max(&keybinds); - - let column_names = keybinds.remove(0); - - let table_widths = vec![ - Constraint::Min(maximum_description_width), - Constraint::Min(maximum_keybind_width), - ]; - - let table = Table::new(keybinds.iter().map(|k| Row::new(k.iter().copied()))) - .header(Row::new(column_names.iter().copied()).style(styles::COLUMN_TITLE)) - .block(Block::default().borders(Borders::ALL).title("[ Keybinds ]")) - .widths(&table_widths) + // Normal mode keybinds + let normal_table_widths = vector_column_max(&INSERT_MODE, None) + .into_iter() + .map(Constraint::Min) + .collect::>(); + + let normal_mode_table = Table::new(NORMAL_MODE.iter().map(|k| Row::new(k.iter().copied()))) + .header(Row::new(COLUMN_TITLES.iter().copied()).style(styles::COLUMN_TITLE)) + .block( + Block::default() + .borders(Borders::ALL) + .title("[ Normal Mode Keybinds ]"), + ) + .widths(&normal_table_widths) + .column_spacing(2) + .style(styles::BORDER_NAME); + + frame.render_widget(normal_mode_table, vertical_chunks[0]); + + // Insert mode keybinds + let insert_table_widths = vector_column_max(&INSERT_MODE, None) + .into_iter() + .map(Constraint::Min) + .collect::>(); + + let insert_mode_table = Table::new(INSERT_MODE.iter().map(|k| Row::new(k.iter().copied()))) + .header(Row::new(COLUMN_TITLES.iter().copied()).style(styles::COLUMN_TITLE)) + .block( + Block::default() + .borders(Borders::ALL) + .title("[ Insert Mode Keybinds ]"), + ) + .widths(&insert_table_widths) .column_spacing(2) .style(styles::BORDER_NAME); - frame.render_widget(table, vertical_chunks[0]); + frame.render_widget(insert_mode_table, vertical_chunks[1]); Ok(()) } diff --git a/src/ui/keys.rs b/src/ui/keys.rs new file mode 100644 index 00000000..e73a1547 --- /dev/null +++ b/src/ui/keys.rs @@ -0,0 +1,27 @@ +use lazy_static::lazy_static; + +lazy_static! { + pub static ref COLUMN_TITLES: Vec<&'static str> = vec!["Keybind", "Description"]; + pub static ref NORMAL_MODE: Vec> = vec![ + vec!["c", "Chat window"], + vec!["i", "Insert mode"], + vec!["?", "Bring up this window"], + vec!["q", "Quit this application"], + vec!["Esc", "Drop back to previous window layer"], + ]; + pub static ref INSERT_MODE: Vec> = vec![ + vec!["Ctrl + f", "Move cursor to the right"], + vec!["Ctrl + b", "Move cursor to the left"], + vec!["Ctrl + a", "Move cursor to the start"], + vec!["Ctrl + e", "Move cursor to the end"], + vec!["Alt + f", "Move to the end of the next word"], + vec!["Alt + b", "Move to the start of the previous word"], + vec!["Ctrl + t", "Swap previous item with current item"], + vec!["Alt + t", "Swap previous word with current word"], + vec!["Ctrl + u", "Remove everything before the cursor"], + vec!["Ctrl + k", "Remove everything after the cursor"], + vec!["Ctrl + w", "Remove the previous word"], + vec!["Ctrl + d", "Remove item to the right"], + vec!["Esc", "Drop back to previous window layer"], + ]; +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8b92505a..e6023e45 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,2 +1,3 @@ pub mod chat; pub mod help; +pub mod keys; diff --git a/src/utils/text.rs b/src/utils/text.rs index 4b65121a..61252169 100644 --- a/src/utils/text.rs +++ b/src/utils/text.rs @@ -1,3 +1,5 @@ +use std::vec::IntoIter; + use rustyline::line_buffer::LineBuffer; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; @@ -25,14 +27,49 @@ pub fn align_text(text: &str, alignment: &str, maximum_length: u16) -> String { } } -pub fn vector2_col_max(vec2: &[Vec]) -> (u16, u16) +pub enum VectorColumnMax { + One(T), + All(Vec), +} + +impl IntoIterator for VectorColumnMax { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self { + VectorColumnMax::All(item) => item.into_iter(), + VectorColumnMax::One(item) => vec![item].into_iter(), + } + } +} + +pub fn vector_column_max(vec: &[Vec], indexer: Option) -> IntoIter where T: AsRef, { - let col0 = vec2.iter().map(|v| v[0].as_ref().len()).max().unwrap(); - let col1 = vec2.iter().map(|v| v[1].as_ref().len()).max().unwrap(); + if vec.is_empty() { + panic!("Vector length should be greater than or equal to 1.") + } + + let column_max = |vec: &[Vec], index: usize| -> u16 { + vec.iter().map(|v| v[index].as_ref().len()).max().unwrap() as u16 + }; + + match indexer { + Some(index) => VectorColumnMax::One(column_max(vec, index)).into_iter(), + None => { + let column_amount = vec[0].len(); + + let mut column_max_lengths: Vec = vec![]; - (col0 as u16, col1 as u16) + for i in 0..column_amount { + column_max_lengths.push(column_max(vec, i)); + } + + VectorColumnMax::All(column_max_lengths).into_iter() + } + } } pub fn get_cursor_position(line_buffer: &LineBuffer) -> usize { @@ -50,7 +87,7 @@ mod tests { #[test] #[should_panic(expected = "Parameter of 'maximum_length' cannot be below 1.")] - fn test_maximum_length() { + fn test_text_align_maximum_length() { align_text("", "left", 0); } @@ -79,26 +116,42 @@ mod tests { } #[test] - fn test_reference_string_vec2() { - let vec2 = vec![vec!["", "s"], vec!["longer string", "lll"]]; + #[should_panic(expected = "Vector length should be greater than or equal to 1.")] + fn test_vector_column_max_empty_vector() { + let vec: Vec> = vec![]; - let (col0, col1) = vector2_col_max(&vec2); + vector_column_max(&vec, None); + } + + #[test] + fn test_vector_column_max_reference_strings() { + let vec = vec![vec!["", "s"], vec!["longer string", "lll"]]; - assert_eq!(col0, 13); - assert_eq!(col1, 3); + let mut output_vec_all = vector_column_max(&vec, None); + + assert_eq!(output_vec_all.next(), Some(13)); + assert_eq!(output_vec_all.next(), Some(3)); + + let mut output_vec_one = vector_column_max(&vec, Some(0)); + + assert_eq!(output_vec_one.next(), Some(13)); } #[test] - fn test_string_vec2() { - let vec2 = vec![ + fn test_vector_column_max_strings() { + let vec = vec![ vec!["".to_string(), "another".to_string()], vec!["".to_string(), "the last string".to_string()], ]; - let (col0, col1) = vector2_col_max(&vec2); + let mut output_vec_all = vector_column_max(&vec, None); + + assert_eq!(output_vec_all.next(), Some(0)); + assert_eq!(output_vec_all.next(), Some(15)); + + let mut output_vec_one = vector_column_max(&vec, Some(0)); - assert_eq!(col0, 0); - assert_eq!(col1, 15); + assert_eq!(output_vec_one.next(), Some(0)); } #[test]