Highlight user's name in chat. (#206)

This commit is contained in:
Xithrius 2022-08-28 22:58:41 -07:00 committed by GitHub
parent e75b001452
commit f07ffd3900
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 135 additions and 97 deletions

7
Cargo.lock generated
View File

@ -662,12 +662,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "memchr"
version = "2.5.0"
@ -1450,7 +1444,6 @@ dependencies = [
"irc",
"lazy_static",
"log",
"maplit",
"regex",
"rustyline",
"serde",

View File

@ -34,7 +34,6 @@ regex = "1.6.0"
color-eyre = "0.6.2"
log = "0.4.17"
fern = "0.6.1"
maplit = "1.0.2"
[[bin]]
bench = false

View File

@ -47,3 +47,5 @@ margin = 0
badges = true
# The theme of the frontend, dark (default), or light.
theme = "dark"
# If the user's name appears in chat, highlight it.
username_highlight = true

View File

@ -93,6 +93,8 @@ pub struct FrontendConfig {
pub badges: bool,
/// Theme, being either light or dark.
pub theme: Theme,
/// If the username should be highlighted when it appears in chat.
pub username_highlight: bool,
}
impl Default for TwitchConfig {
@ -129,6 +131,7 @@ impl Default for FrontendConfig {
margin: 0,
badges: false,
theme: Theme::Dark,
username_highlight: true,
}
}
}

View File

@ -1,6 +1,7 @@
use chrono::{offset::Local, DateTime};
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use lazy_static::lazy_static;
use regex::Regex;
use tui::{
style::{Color, Color::Rgb, Modifier, Style},
text::{Span, Spans},
@ -9,9 +10,15 @@ use tui::{
use crate::{
handlers::config::{FrontendConfig, Palette},
utils::{colors::hsl_to_rgb, styles, text::align_text},
utils::{
colors::hsl_to_rgb,
styles::{self, HIGHLIGHT_NAME_DARK, HIGHLIGHT_NAME_LIGHT},
text::align_text,
},
};
use super::config::Theme;
lazy_static! {
pub static ref FUZZY_FINDER: SkimMatcherV2 = SkimMatcherV2::default();
}
@ -94,7 +101,8 @@ impl Data {
&self,
frontend_config: &FrontendConfig,
limit: &usize,
highlight: Option<String>,
search_highlight: Option<String>,
username_highlight: Option<String>,
theme_style: Style,
) -> Vec<Row> {
let message = if let PayLoad::Message(m) = &self.payload {
@ -103,7 +111,23 @@ impl Data {
panic!("Data.to_row() can only take an enum of PayLoad::Message.");
};
let msg_cells: Vec<Cell> = if let Some(search) = highlight {
let username_highlight_style = if let Some(username) = username_highlight {
if Regex::new(format!("^.*{}.*$", username).as_str())
.unwrap()
.is_match(&message)
{
match frontend_config.theme {
Theme::Light => HIGHLIGHT_NAME_LIGHT,
_ => HIGHLIGHT_NAME_DARK,
}
} else {
Style::default()
}
} else {
Style::default()
};
let msg_cells = if let Some(search) = search_highlight {
message
.split('\n')
.map(|s| {
@ -128,14 +152,22 @@ impl Data {
.collect::<Vec<Span>>(),
)])
} else {
Cell::from(s.to_owned())
Cell::from(Spans::from(vec![Span::styled(
s.to_owned(),
username_highlight_style,
)]))
}
})
.collect::<Vec<Cell>>()
} else {
message
.split('\n')
.map(|c| Cell::from(c.to_string()))
.map(|s| {
Cell::from(Spans::from(vec![Span::styled(
s.to_owned(),
username_highlight_style,
)]))
})
.collect::<Vec<Cell>>()
};

View File

@ -10,6 +10,7 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use log::{debug, info};
use regex::Regex;
use rustyline::{At, Word};
use tokio::sync::mpsc::{Receiver, Sender};
use tui::{backend::CrosstermBackend, layout::Constraint, Terminal};
@ -22,7 +23,10 @@ use crate::{
event::{Config, Event, Events, Key},
},
twitch::Action,
ui::{draw_ui, statics::TWITCH_MESSAGE_LIMIT},
ui::{
draw_ui,
statics::{CHANNEL_NAME_REGEX, TWITCH_MESSAGE_LIMIT},
},
utils::text::align_text,
};
@ -250,19 +254,25 @@ pub async fn ui_driver(
let input_message =
app.input_buffers.get_mut(&app.selected_buffer).unwrap();
if !input_message.is_empty() {
app.messages.clear();
tx.send(Action::Join(input_message.to_string()))
.await
.unwrap();
config.twitch.channel = input_message.to_string();
app.storage
.add("channels".to_string(), input_message.to_string())
if input_message.is_empty()
|| !Regex::new(*CHANNEL_NAME_REGEX)
.unwrap()
.is_match(input_message)
{
continue;
}
app.messages.clear();
tx.send(Action::Join(input_message.to_string()))
.await
.unwrap();
config.twitch.channel = input_message.to_string();
app.storage
.add("channels".to_string(), input_message.to_string());
input_message.update("", 0);
app.selected_buffer = BufferName::Chat;

View File

@ -10,7 +10,11 @@ use crate::{
};
pub fn ui_insert_message<T: Backend>(window: WindowAttributes<T>, mention_suggestions: bool) {
let WindowAttributes { frame, app, layout } = window;
let WindowAttributes {
frame: _,
app,
layout: _,
} = &window;
let input_buffer = app.current_buffer();
@ -56,9 +60,13 @@ pub fn ui_insert_message<T: Backend>(window: WindowAttributes<T>, mention_sugges
};
insert_box_chunk(
frame,
app,
layout,
window,
format!(
"Message Input: {} / {}",
current_input.len(),
*TWITCH_MESSAGE_LIMIT
)
.as_str(),
None,
suggestion,
Some(Box::new(|s: String| -> bool {

View File

@ -1,36 +0,0 @@
use tui::{
backend::Backend,
style::{Color, Style},
widgets::{Block, Borders, Paragraph},
};
use crate::{ui::WindowAttributes, utils::text::get_cursor_position};
pub fn ui_search_messages<T: Backend>(window: WindowAttributes<T>) {
let WindowAttributes { frame, layout, app } = window;
let input_buffer = app.current_buffer();
let cursor_pos = get_cursor_position(input_buffer);
let input_rect = layout.chunks[layout.constraints.len() - 1];
frame.set_cursor(
(input_rect.x + cursor_pos as u16 + 1)
.min(input_rect.x + input_rect.width.saturating_sub(2)),
input_rect.y + 1,
);
let paragraph = Paragraph::new(input_buffer.as_str())
.block(
Block::default()
.borders(Borders::ALL)
.title("[ Message Search ]")
.border_style(Style::default().fg(Color::Yellow)),
)
.scroll((
0,
((cursor_pos + 3) as u16).saturating_sub(input_rect.width),
));
frame.render_widget(paragraph, input_rect);
}

View File

@ -1,2 +1 @@
pub mod chatting;
pub mod message_search;

View File

@ -1,12 +1,6 @@
use std::{
collections::{HashMap, VecDeque},
vec,
};
use std::{collections::VecDeque, vec};
use chrono::offset::Local;
use color_eyre::eyre::ContextCompat;
use lazy_static::lazy_static;
use maplit::hashmap;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
@ -23,13 +17,12 @@ use crate::{
data::PayLoad,
},
ui::{
chunks::{chatting::ui_insert_message, message_search::ui_search_messages},
chunks::chatting::ui_insert_message,
popups::{channels::ui_switch_channels, help::ui_show_keybinds},
},
utils::{
styles,
text::title_spans,
text::{get_cursor_position, TitleStyle},
text::{get_cursor_position, title_spans, TitleStyle},
},
};
@ -37,15 +30,6 @@ pub mod chunks;
pub mod popups;
pub mod statics;
lazy_static! {
pub static ref LAYOUTS: HashMap<State, Vec<Constraint>> = hashmap! {
State::Normal => vec![Constraint::Min(1)],
State::Insert => vec![Constraint::Min(1), Constraint::Length(3)],
State::Help => vec![Constraint::Min(1)],
State::ChannelSwitch => vec![Constraint::Min(1)]
};
}
#[derive(Debug, Clone)]
pub struct LayoutAttributes {
constraints: Vec<Constraint>,
@ -77,10 +61,10 @@ where
}
pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &CompleteConfig) {
let v_constraints = LAYOUTS
.get(&app.state)
.wrap_err(format!("Could not find layout {:?}.", &app.state))
.unwrap();
let v_constraints = match app.state {
State::Insert | State::MessageSearch => vec![Constraint::Min(1), Constraint::Length(3)],
_ => vec![Constraint::Min(1)],
};
let v_chunks = Layout::default()
.direction(Direction::Vertical)
@ -130,6 +114,12 @@ pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &Complet
let buffer = app.current_buffer();
let username_highlight = if config.frontend.username_highlight {
Some(config.twitch.username.clone())
} else {
None
};
let rows = if !buffer.is_empty() {
data.to_row(
&config.frontend,
@ -138,6 +128,7 @@ pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &Complet
BufferName::MessageHighlighter => Some(buffer.to_string()),
_ => None,
},
username_highlight,
app.theme_style,
)
} else {
@ -145,6 +136,7 @@ pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &Complet
&config.frontend,
&message_chunk_width,
None,
username_highlight,
app.theme_style,
)
};
@ -213,7 +205,7 @@ pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &Complet
match window.app.state {
// States of the application that require a chunk of the main window
State::Insert => ui_insert_message(window, config.storage.mentions),
State::MessageSearch => ui_search_messages(window),
State::MessageSearch => insert_box_chunk(window, "Message Search", None, None, None),
// States that require popups
State::Help => ui_show_keybinds(window),
@ -227,14 +219,14 @@ pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &Complet
/// input_validation checks if the user's input is valid, changes window
/// theme to red if invalid, default otherwise.
pub fn insert_box_chunk<T: Backend>(
frame: &mut Frame<T>,
app: &mut App,
layout: LayoutAttributes,
window: WindowAttributes<T>,
box_title: &str,
input_rectangle: Option<Rect>,
// buffer: &LineBuffer,
suggestion: Option<String>,
input_validation: Option<Box<dyn FnOnce(String) -> bool>>,
) {
let WindowAttributes { frame, layout, app } = window;
let buffer = app.current_buffer();
let cursor_pos = get_cursor_position(buffer);
@ -278,7 +270,7 @@ pub fn insert_box_chunk<T: Backend>(
Block::default()
.borders(Borders::ALL)
.title(title_spans(
vec![TitleStyle::Single("Message input")],
vec![TitleStyle::Single(box_title)],
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
))
.border_style(Style::default().fg(if valid_input {

View File

@ -1,16 +1,22 @@
use regex::Regex;
use tui::backend::Backend;
use crate::{
ui::{
insert_box_chunk,
popups::{centered_popup, WindowType},
statics::CHANNEL_NAME_REGEX,
WindowAttributes,
},
utils::text::suggestion_query,
};
pub fn ui_switch_channels<T: Backend>(window: WindowAttributes<T>, channel_suggestions: bool) {
let WindowAttributes { frame, app, layout } = window;
let WindowAttributes {
frame,
app,
layout: _,
} = &window;
let input_buffer = app.current_buffer();
@ -29,5 +35,15 @@ pub fn ui_switch_channels<T: Backend>(window: WindowAttributes<T>, channel_sugge
None
};
insert_box_chunk(frame, app, layout, Some(input_rect), suggestion, None);
insert_box_chunk(
window,
"Channel",
Some(input_rect),
suggestion,
Some(Box::new(|s: String| -> bool {
Regex::new(*CHANNEL_NAME_REGEX)
.unwrap()
.is_match(s.as_str())
})),
);
}

View File

@ -28,6 +28,8 @@ lazy_static! {
vec!["Tab", "Fill in suggestion, if available"],
vec!["Enter", "Confirm the input text to go through"],
];
// https://help.twitch.tv/s/article/chat-commands?language=en_US
pub static ref COMMANDS: Vec<&'static str> = vec![
"ban",
"unban",
@ -66,4 +68,7 @@ lazy_static! {
// https://discuss.dev.twitch.tv/t/irc-bot-and-message-lengths/23327/4
pub static ref TWITCH_MESSAGE_LIMIT: usize = 500;
// https://www.reddit.com/r/Twitch/comments/32w5b2/username_requirements/
pub static ref CHANNEL_NAME_REGEX: &'static str = "^[a-zA-Z0-9_]{4,25}$";
}

View File

@ -12,12 +12,27 @@ pub const BORDER_NAME_LIGHT: Style = Style {
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
};
pub const HIGHLIGHT_NAME_DARK: Style = Style {
fg: Some(Color::Black),
bg: Some(Color::White),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
};
pub const HIGHLIGHT_NAME_LIGHT: Style = Style {
fg: Some(Color::White),
bg: Some(Color::Black),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
};
pub const COLUMN_TITLE: Style = Style {
fg: Some(Color::LightCyan),
bg: None,
add_modifier: Modifier::BOLD,
sub_modifier: Modifier::empty(),
};
pub const SYSTEM_CHAT: Style = Style {
fg: Some(Color::Red),
bg: None,