mirror of
https://github.com/Xithrius/twitch-tui.git
synced 2024-09-11 13:45:30 +03:00
Highlight user's name in chat. (#206)
This commit is contained in:
parent
e75b001452
commit
f07ffd3900
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>>()
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
@ -1,2 +1 @@
|
||||
pub mod chatting;
|
||||
pub mod message_search;
|
||||
|
@ -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 {
|
||||
|
@ -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())
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
@ -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}$";
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user