mirror of
https://github.com/Xithrius/twitch-tui.git
synced 2024-10-04 09:07:33 +03:00
Suggestions for commands and mentions
Also moved `TerminalAction` to the `src/terminal.rs` file
This commit is contained in:
parent
81dfd5019f
commit
2617c9ce3f
@ -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},
|
||||
|
@ -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
|
||||
// }
|
@ -1,3 +1,2 @@
|
||||
pub mod events;
|
||||
pub mod input;
|
||||
pub mod scrolling;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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},
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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},
|
||||
};
|
||||
|
@ -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()];
|
||||
|
Loading…
Reference in New Issue
Block a user