Suggestions for commands and mentions

Also moved `TerminalAction` to the `src/terminal.rs` file
This commit is contained in:
Xithrius 2023-04-27 14:36:31 -07:00
parent 81dfd5019f
commit 2617c9ce3f
No known key found for this signature in database
GPG Key ID: A867F27CC80B28C1
11 changed files with 101 additions and 344 deletions

View File

@ -16,8 +16,9 @@ use crate::{
filters::Filters,
state::State,
storage::Storage,
user_input::{events::Event, input::TerminalAction},
user_input::events::Event,
},
terminal::TerminalAction,
twitch::TwitchAction,
ui::{
components::{Component, Components},

View File

@ -1,292 +0,0 @@
// use regex::Regex;
// use rustyline::{At, Word};
// use tokio::sync::broadcast::Sender;
// use crate::{
// emotes::{unload_all_emotes, Emotes},
// handlers::{
// app::App,
// config::CompleteConfig,
// data::DataBuilder,
// state::State,
// user_input::events::{Event, Events, Key},
// },
// twitch::TwitchAction,
// ui::statics::{NAME_RESTRICTION_REGEX, TWITCH_MESSAGE_LIMIT},
// };
use crate::handlers::state::State;
pub enum TerminalAction {
Quit,
BackOneLayer,
SwitchState(State),
}
// struct UserActionAttributes<'a, 'b> {
// app: &'a mut App,
// config: &'b mut CompleteConfig,
// tx: Sender<TwitchAction>,
// key: Key,
// }
// impl<'a, 'b> UserActionAttributes<'a, 'b> {
// fn new(
// app: &'a mut App,
// config: &'b mut CompleteConfig,
// tx: Sender<TwitchAction>,
// key: Key,
// ) -> Self {
// Self {
// app,
// config,
// tx,
// key,
// }
// }
// }
// fn handle_insert_enter_key(action: &mut UserActionAttributes<'_, '_>, emotes: &mut Emotes) {
// let UserActionAttributes {
// app,
// config,
// key: _,
// tx,
// } = action;
// match app.get_state() {
// State::Insert => {
// let input_message = &mut app.input_buffer;
// if input_message.is_empty()
// || app.filters.contaminated(input_message.as_str())
// || input_message.len() > *TWITCH_MESSAGE_LIMIT
// {
// return;
// }
// let mut message = DataBuilder::user(
// config.twitch.username.to_string(),
// input_message.to_string(),
// );
// message.parse_emotes(emotes);
// app.messages.push_front(message);
// tx.send(TwitchAction::Privmsg(input_message.to_string()))
// .unwrap();
// if let Some(msg) = input_message.strip_prefix('@') {
// app.storage.add("mentions", msg.to_string());
// }
// let mut possible_command = String::new();
// input_message.clone_into(&mut possible_command);
// input_message.update("", 0);
// if possible_command.as_str() == "/clear" {
// app.clear_messages();
// }
// }
// State::ChannelSwitch => {
// let input_message = &mut app.input_buffer;
// if input_message.is_empty()
// || !Regex::new(&NAME_RESTRICTION_REGEX)
// .unwrap()
// .is_match(input_message)
// {
// return;
// }
// // TODO: if input message is the same as the current config, return to normal state.
// app.messages.clear();
// unload_all_emotes(emotes);
// tx.send(TwitchAction::Join(input_message.to_string()))
// .unwrap();
// config.twitch.channel = input_message.to_string();
// app.storage.add("channels", input_message.to_string());
// input_message.update("", 0);
// app.set_state(State::Normal);
// }
// _ => {}
// }
// }
// fn handle_insert_type_movements(action: &mut UserActionAttributes<'_, '_>, emotes: &mut Emotes) {
// let UserActionAttributes {
// app,
// config: _,
// key,
// tx: _,
// } = action;
// let input_buffer = &mut app.input_buffer;
// match key {
// Key::Up => {
// if app.get_state() == State::Insert {
// app.set_state(State::Normal);
// }
// }
// Key::Ctrl('f') | Key::Right => {
// input_buffer.move_forward(1);
// }
// Key::Ctrl('b') | Key::Left => {
// input_buffer.move_backward(1);
// }
// Key::Ctrl('a') | Key::Home => {
// input_buffer.move_home();
// }
// Key::Ctrl('e') | Key::End => {
// input_buffer.move_end();
// }
// Key::Alt('f') => {
// input_buffer.move_to_next_word(At::AfterEnd, Word::Emacs, 1);
// }
// Key::Alt('b') => {
// input_buffer.move_to_prev_word(Word::Emacs, 1);
// }
// Key::Ctrl('t') => {
// input_buffer.transpose_chars();
// }
// Key::Alt('t') => {
// input_buffer.transpose_words(1);
// }
// Key::Ctrl('u') => {
// input_buffer.discard_line();
// }
// Key::Ctrl('k') => {
// input_buffer.kill_line();
// }
// Key::Ctrl('w') => {
// input_buffer.delete_prev_word(Word::Emacs, 1);
// }
// Key::Ctrl('d') => {
// input_buffer.delete(1);
// }
// Key::Backspace | Key::Delete => {
// input_buffer.backspace(1);
// }
// Key::Tab => {
// let suggestion = app.buffer_suggestion.clone();
// if let Some(suggestion_buffer) = suggestion {
// app.input_buffer
// .update(suggestion_buffer.as_str(), suggestion_buffer.len());
// }
// }
// Key::Enter => handle_insert_enter_key(action, emotes),
// Key::Char(c) => {
// input_buffer.insert(*c, 1);
// }
// Key::Esc => {
// input_buffer.update("", 0);
// app.set_state(State::Normal);
// }
// _ => {}
// }
// }
// fn handle_user_scroll(app: &mut App, key: Key) {
// match app.get_state() {
// State::Insert | State::MessageSearch | State::Normal => {
// let limit = app.scrolling.get_offset() < app.messages.len();
// match key {
// Key::ScrollUp => {
// if limit {
// app.scrolling.up();
// } else if app.scrolling.inverted() {
// app.scrolling.down();
// }
// }
// Key::ScrollDown => {
// if app.scrolling.inverted() {
// if limit {
// app.scrolling.up();
// }
// } else {
// app.scrolling.down();
// }
// }
// _ => {}
// }
// }
// _ => {}
// }
// }
// pub async fn handle_stateful_user_input(
// events: &mut Events,
// app: &mut App,
// config: &mut CompleteConfig,
// tx: Sender<TwitchAction>,
// emotes: &mut Emotes,
// ) -> Option<TerminalAction> {
// if let Some(Event::Input(key)) = events.next().await {
// handle_user_scroll(app, key);
// match app.get_state() {
// State::Help => {
// if matches!(key, Key::Esc) {
// if let Some(previous_state) = app.get_previous_state() {
// app.set_state(previous_state);
// }
// }
// }
// State::ChannelSwitch => {
// if matches!(key, Key::Esc) {
// if let Some(previous_state) = app.get_previous_state() {
// app.set_state(previous_state);
// } else {
// app.set_state(config.terminal.start_state.clone());
// }
// } else {
// let mut action = UserActionAttributes::new(app, config, tx, key);
// handle_insert_type_movements(&mut action, emotes);
// }
// }
// State::Insert | State::MessageSearch => {
// let mut action = UserActionAttributes::new(app, config, tx, key);
// handle_insert_type_movements(&mut action, emotes);
// }
// State::Normal => match key {
// Key::Char('c') => app.set_state(State::Normal),
// Key::Char('s') => app.set_state(State::ChannelSwitch),
// Key::Ctrl('f') => app.set_state(State::MessageSearch),
// // Key::Ctrl('d') => app.debug.toggle(),
// Key::Ctrl('t') => app.filters.toggle(),
// Key::Ctrl('r') => app.filters.reverse(),
// Key::Char('i') | Key::Insert => app.set_state(State::Insert),
// Key::Char('@' | '/') => {
// app.set_state(State::Insert);
// app.input_buffer.update(&key.to_string(), 1);
// }
// Key::Ctrl('p') => panic!("Manual panic triggered by user."),
// Key::Char('S') => app.set_state(State::Dashboard),
// Key::Char('?') => app.set_state(State::Help),
// Key::Char('q') => return Some(TerminalAction::Quitting),
// Key::Esc => {
// app.scrolling.jump_to(0);
// app.set_state(State::Normal);
// }
// _ => {}
// },
// _ => {}
// }
// }
// None
// }

View File

@ -1,3 +1,2 @@
pub mod events;
pub mod input;
pub mod scrolling;

View File

@ -11,13 +11,17 @@ use crate::{
config::CompleteConfig,
data::MessageData,
state::State,
user_input::{
events::{Config, Events, Key},
input::TerminalAction,
},
user_input::events::{Config, Events, Key},
},
};
pub enum TerminalAction {
Quit,
BackOneLayer,
SwitchState(State),
ClearMessages,
}
pub async fn ui_driver(
config: CompleteConfig,
mut app: App,
@ -100,6 +104,9 @@ pub async fn ui_driver(
app.set_state(state);
}
TerminalAction::ClearMessages => {
app.clear_messages();
}
}
}
}

View File

@ -8,11 +8,9 @@ use crate::{
config::SharedCompleteConfig,
state::State,
storage::SharedStorage,
user_input::{
events::{Event, Key},
input::TerminalAction,
},
user_input::events::{Event, Key},
},
terminal::TerminalAction,
twitch::TwitchAction,
ui::{
components::{utils::InputWidget, Component},

View File

@ -24,10 +24,10 @@ use crate::{
storage::SharedStorage,
user_input::{
events::{Event, Key},
input::TerminalAction,
scrolling::Scrolling,
},
},
terminal::TerminalAction,
twitch::TwitchAction,
ui::components::{utils::centered_rect, ChannelSwitcherWidget, ChatInputWidget, Component},
utils::text::{title_spans, TitleStyle},
@ -51,7 +51,7 @@ impl ChatWidget {
storage: &SharedStorage,
filters: SharedFilters,
) -> Self {
let chat_input = ChatInputWidget::new(config.clone(), tx.clone());
let chat_input = ChatInputWidget::new(config.clone(), tx.clone(), storage.clone());
let channel_input = ChannelSwitcherWidget::new(config.clone(), tx.clone(), storage.clone());
let scroll_offset = Scrolling::new(config.borrow().frontend.inverted_scrolling);

View File

@ -5,34 +5,74 @@ use crate::{
emotes::Emotes,
handlers::{
config::SharedCompleteConfig,
user_input::{
events::{Event, Key},
input::TerminalAction,
},
storage::SharedStorage,
user_input::events::{Event, Key},
},
terminal::TerminalAction,
twitch::TwitchAction,
ui::{
components::{utils::InputWidget, Component},
statics::TWITCH_MESSAGE_LIMIT,
statics::{COMMANDS, TWITCH_MESSAGE_LIMIT},
},
utils::text::first_similarity,
};
pub struct ChatInputWidget {
_config: SharedCompleteConfig,
config: SharedCompleteConfig,
storage: SharedStorage,
input: InputWidget,
tx: Sender<TwitchAction>,
}
impl ChatInputWidget {
pub fn new(config: SharedCompleteConfig, tx: Sender<TwitchAction>) -> Self {
pub fn new(
config: SharedCompleteConfig,
tx: Sender<TwitchAction>,
storage: SharedStorage,
) -> Self {
let input_validator = Box::new(|s: String| -> bool { s.len() < *TWITCH_MESSAGE_LIMIT });
let input = InputWidget::new(config.clone(), "Chat", Some(input_validator), None);
let input_suggester = Box::new(|storage: SharedStorage, s: String| -> Option<String> {
s.chars()
.next()
.and_then(|start_character| match start_character {
'/' => {
let possible_suggestion = first_similarity(
&COMMANDS
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>(),
&s[1..],
);
let default_suggestion = possible_suggestion.clone();
possible_suggestion.map_or(default_suggestion, |s| Some(format!("/{s}")))
}
'@' => {
let possible_suggestion =
first_similarity(&storage.borrow().get("mentions"), &s[1..]);
let default_suggestion = possible_suggestion.clone();
possible_suggestion.map_or(default_suggestion, |s| Some(format!("@{s}")))
}
_ => None,
})
});
let input = InputWidget::new(
config.clone(),
"Chat",
Some(input_validator),
Some((storage.clone(), input_suggester)),
);
Self {
_config: config,
tx,
config,
storage,
input,
tx,
}
}
@ -61,11 +101,25 @@ impl Component for ChatInputWidget {
match key {
Key::Enter => {
if self.input.is_valid() {
let current_input = self.input.to_string();
self.tx
.send(TwitchAction::Privmsg(self.input.to_string()))
.send(TwitchAction::Privmsg(current_input.clone()))
.unwrap();
self.input.update("");
if let Some(message) = current_input.strip_prefix('@') {
if self.config.borrow().storage.mentions {
self.storage
.borrow_mut()
.add("mentions", message.to_string());
}
} else if let Some(message) = current_input.strip_prefix('/') {
if message == "clear" {
return Some(TerminalAction::ClearMessages);
}
}
}
}
Key::Esc => {
@ -77,21 +131,6 @@ impl Component for ChatInputWidget {
}
}
// if let Some(msg) = input_message.strip_prefix('@') {
// app.storage.add("mentions", msg.to_string());
// }
// let mut possible_command = String::new();
// input_message.clone_into(&mut possible_command);
// input_message.update("", 0);
// if possible_command.as_str() == "/clear" {
// app.clear_messages();
// }
// }
None
}
}

View File

@ -16,11 +16,9 @@ use crate::{
config::SharedCompleteConfig,
state::State,
storage::SharedStorage,
user_input::{
events::{Event, Key},
input::TerminalAction,
},
user_input::events::{Event, Key},
},
terminal::TerminalAction,
twitch::TwitchAction,
ui::components::{utils::centered_rect, ChannelSwitcherWidget, Component},
utils::styles::DASHBOARD_TITLE_COLOR,

View File

@ -28,11 +28,9 @@ use crate::{
config::SharedCompleteConfig,
filters::SharedFilters,
storage::SharedStorage,
user_input::{
events::{Event, Key},
input::TerminalAction,
},
user_input::events::{Event, Key},
},
terminal::TerminalAction,
twitch::TwitchAction,
};

View File

@ -13,11 +13,9 @@ use crate::{
handlers::{
config::SharedCompleteConfig,
storage::SharedStorage,
user_input::{
events::{Event, Key},
input::TerminalAction,
},
user_input::events::{Event, Key},
},
terminal::TerminalAction,
ui::{components::Component, statics::LINE_BUFFER_CAPACITY},
utils::text::{get_cursor_position, title_spans, TitleStyle},
};

View File

@ -61,9 +61,11 @@ pub fn title_spans<'a>(contents: &'a [TitleStyle<'a>], style: Style) -> Vec<Span
}
/// Within an array of strings, find the first partial or full match, if any.
#[allow(dead_code)]
// TODO: Implement suggestions for new widgets
pub fn first_similarity(possibilities: &[String], search: &str) -> Option<String> {
if search.is_empty() {
return None;
}
possibilities
.iter()
.filter(|s| s.starts_with(search))
@ -123,6 +125,15 @@ mod tests {
assert_eq!(s.width(), "[ Time: Some time ]".len());
}
#[test]
fn test_first_similarity_no_search_no_output() {
let v = vec!["asdf".to_string()];
let output = first_similarity(&v, "");
assert_eq!(output, None);
}
#[test]
fn test_first_similarity_some_output() {
let v = vec!["Nope".to_string()];