Messages show up

This commit is contained in:
Xithrius 2023-02-10 21:37:54 -08:00
parent 648dc42e5d
commit 064ae13474
No known key found for this signature in database
GPG Key ID: A867F27CC80B28C1
15 changed files with 288 additions and 366 deletions

98
Cargo.lock generated
View File

@ -120,9 +120,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.1.1"
version = "4.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec7a4128863c188deefe750ac1d1dfe66c236909f845af04beed823638dc1b2"
checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76"
dependencies = [
"bitflags",
"clap_derive",
@ -248,6 +248,22 @@ dependencies = [
"winapi",
]
[[package]]
name = "crossterm"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77f67c7faacd4db07a939f55d66a983a5355358a1f17d32cc9a8d01d1266b9ce"
dependencies = [
"bitflags",
"crossterm_winapi",
"libc",
"mio",
"parking_lot 0.12.1",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.0"
@ -495,9 +511,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "futures"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
dependencies = [
"futures-channel",
"futures-core",
@ -510,9 +526,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
dependencies = [
"futures-core",
"futures-sink",
@ -520,15 +536,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
[[package]]
name = "futures-executor"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
dependencies = [
"futures-core",
"futures-task",
@ -537,15 +553,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
[[package]]
name = "futures-macro"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
dependencies = [
"proc-macro2",
"quote",
@ -554,21 +570,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364"
[[package]]
name = "futures-task"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
[[package]]
name = "futures-util"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
dependencies = [
"futures-channel",
"futures-core",
@ -857,14 +873,14 @@ dependencies = [
[[package]]
name = "nix"
version = "0.26.1"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694"
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
dependencies = [
"autocfg",
"bitflags",
"cfg-if",
"libc",
"static_assertions",
]
[[package]]
@ -1183,9 +1199,9 @@ dependencies = [
[[package]]
name = "rustyline"
version = "10.1.0"
version = "10.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc2d30f0bb29c4308f902d4a147b2d6cb022b59771463d9785f37e21f544df7"
checksum = "c1e83c32c3f3c33b08496e0d1df9ea8c64d39adb8eb36a1ebb1440c690697aef"
dependencies = [
"bitflags",
"cfg-if",
@ -1276,9 +1292,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.91"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
dependencies = [
"itoa",
"ryu",
@ -1370,12 +1386,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "str-buf"
version = "1.0.6"
@ -1475,9 +1485,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.24.1"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
dependencies = [
"autocfg",
"libc",
@ -1546,9 +1556,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.7.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "772c1426ab886e7362aedf4abc9c0d1348a979517efedfc25862944d10137af0"
checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6"
dependencies = [
"serde",
"serde_spanned",
@ -1567,9 +1577,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.19.1"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90a238ee2e6ede22fb95350acc78e21dc40da00bb66c0334bde83de4ed89424e"
checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5"
dependencies = [
"indexmap",
"nom8",
@ -1628,7 +1638,7 @@ checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
dependencies = [
"bitflags",
"cassowary",
"crossterm",
"crossterm 0.25.0",
"unicode-segmentation",
"unicode-width",
]
@ -1640,7 +1650,7 @@ dependencies = [
"chrono",
"clap",
"color-eyre",
"crossterm",
"crossterm 0.26.0",
"dialoguer",
"fern",
"futures",
@ -1654,7 +1664,7 @@ dependencies = [
"serde_json",
"textwrap",
"tokio",
"toml 0.7.1",
"toml 0.7.2",
"tui",
"unicode-segmentation",
"unicode-width",
@ -1678,9 +1688,9 @@ dependencies = [
[[package]]
name = "unicode-segmentation"
version = "1.10.0"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"

View File

@ -13,20 +13,20 @@ keywords = ["tui", "twitch"]
categories = ["command-line-utilities"]
[dependencies]
crossterm = "0.25.0"
crossterm = "0.26.0"
tui = { version = "0.19.0", default-features = false, features = [ "crossterm" ] }
tokio = { version = "1.24.1", features = [ "rt", "macros", "rt-multi-thread" ] }
clap = { version = "4.0.32", features = [ "derive", "cargo" ] }
tokio = { version = "1.25.0", features = [ "rt", "macros", "rt-multi-thread" ] }
clap = { version = "4.1.4", features = [ "derive", "cargo" ] }
serde = { version = "1.0.152", features = [ "derive" ] }
serde_json = "1.0.91"
serde_json = "1.0.93"
unicode-width = "0.1.10"
unicode-segmentation = "1.10.0"
unicode-segmentation = "1.10.1"
chrono = "0.4.23"
irc = "0.15.0"
futures = "0.3.25"
toml = "0.7.1"
futures = "0.3.26"
toml = "0.7.2"
textwrap = "0.16.0"
rustyline = "10.1.0"
rustyline = "10.1.1"
lazy_static = "1.4.0"
fuzzy-matcher = "0.3.7"
regex = "1.7.0"

View File

@ -4,7 +4,7 @@ use std::{
};
use crossterm::{
cursor::{CursorShape, DisableBlinking, EnableBlinking, SetCursorShape},
cursor::{DisableBlinking, EnableBlinking, SetCursorStyle},
event::{DisableMouseCapture, EnableMouseCapture},
execute, queue,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
@ -39,18 +39,31 @@ pub fn reset_terminal() {
pub fn init_terminal(frontend_config: &FrontendConfig) -> Terminal<CrosstermBackend<Stdout>> {
enable_raw_mode().unwrap();
let blink = |a: SetCursorStyle, b: SetCursorStyle| -> SetCursorStyle {
if frontend_config.blinking_cursor {
a
} else {
b
}
};
let cursor_type = match frontend_config.cursor_shape {
CursorType::Line => CursorShape::Line,
CursorType::UnderScore => CursorShape::UnderScore,
CursorType::Block => CursorShape::Block,
CursorType::User => SetCursorStyle::DefaultUserShape,
CursorType::Line => blink(SetCursorStyle::BlinkingBar, SetCursorStyle::SteadyBar),
CursorType::Block => blink(SetCursorStyle::BlinkingBlock, SetCursorStyle::SteadyBlock),
CursorType::UnderScore => blink(
SetCursorStyle::BlinkingUnderScore,
SetCursorStyle::SteadyUnderScore,
),
};
let mut stdout = stdout();
queue!(
stdout,
EnterAlternateScreen,
EnableMouseCapture,
SetCursorShape(cursor_type),
cursor_type,
)
.unwrap();

View File

@ -12,7 +12,7 @@ use tui::style::Style;
use crate::{
handlers::{
config::{CompleteConfig, Theme},
data::Data,
data::MessageData,
filters::Filters,
storage::Storage,
},
@ -101,7 +101,7 @@ impl Scrolling {
pub struct App {
/// History of recorded messages (time, username, message, etc.)
pub messages: VecDeque<Data>,
pub messages: VecDeque<MessageData>,
/// Data loaded in from a JSON file.
pub storage: Storage,
/// Messages to be filtered out

View File

@ -2,23 +2,9 @@ use clap::{builder::PossibleValue, Parser, ValueEnum};
use crate::handlers::{
app::State,
config::{Alignment, CompleteConfig, Palette, Theme},
config::{CompleteConfig, Palette, Theme},
};
impl ValueEnum for Alignment {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Left, Self::Center, Self::Right]
}
fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
Some(PossibleValue::new(match self {
Self::Left => "left",
Self::Center => "center",
Self::Right => "right",
}))
}
}
impl ValueEnum for Palette {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Pastel, Self::Vibrant, Self::Warm, Self::Cool]
@ -95,9 +81,6 @@ pub struct Cli {
/// Maximum length for Twitch usernames
#[arg(short = 'u', long)]
pub max_username_length: Option<u16>,
/// Username column alignment
#[arg(short = 'a', long)]
pub username_alignment: Option<Alignment>,
/// Username color palette
#[arg(short, long)]
pub palette: Option<Palette>,
@ -141,9 +124,6 @@ pub fn merge_args_into_config(config: &mut CompleteConfig, args: Cli) {
if let Some(maximum_username_length) = args.max_username_length {
config.frontend.maximum_username_length = maximum_username_length;
}
if let Some(username_alignment) = args.username_alignment {
config.frontend.username_alignment = username_alignment;
}
if let Some(palette) = args.palette {
config.frontend.palette = palette;
}

View File

@ -91,8 +91,6 @@ pub struct FrontendConfig {
pub date_format: String,
/// The maximum length of a Twitch username.
pub maximum_username_length: u16,
/// Which side the username should be aligned to.
pub username_alignment: Alignment,
/// The color palette.
pub palette: Palette,
/// Show Title with time and channel.
@ -144,7 +142,6 @@ impl Default for FrontendConfig {
date_shown: true,
date_format: "%a %b %e %T %Y".to_string(),
maximum_username_length: 26,
username_alignment: Alignment::default(),
palette: Palette::default(),
title_shown: true,
margin: 0,
@ -159,35 +156,10 @@ impl Default for FrontendConfig {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Alignment {
Left,
Center,
Right,
}
impl Default for Alignment {
fn default() -> Self {
Self::Right
}
}
impl FromStr for Alignment {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"left" => Ok(Self::Left),
"center" => Ok(Self::Center),
_ => Ok(Self::Right),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum CursorType {
User,
Line,
Block,
UnderScore,
@ -200,14 +172,15 @@ impl FromStr for CursorType {
match s {
"line" => Ok(CursorType::Line),
"underscore" => Ok(CursorType::UnderScore),
_ => Ok(CursorType::Block),
"block" => Ok(CursorType::Block),
_ => Ok(CursorType::User),
}
}
}
impl Default for CursorType {
fn default() -> Self {
Self::Block
Self::User
}
}

View File

@ -13,7 +13,6 @@ use crate::{
utils::{
colors::hsl_to_rgb,
styles::{HIGHLIGHT_NAME_DARK, HIGHLIGHT_NAME_LIGHT, SYSTEM_CHAT},
text::align_text,
},
};
@ -22,14 +21,14 @@ lazy_static! {
}
#[derive(Debug, Clone)]
pub struct Data {
pub struct MessageData {
pub time_sent: DateTime<Local>,
pub author: String,
pub system: bool,
pub payload: String,
}
impl Data {
impl MessageData {
pub fn new(author: String, system: bool, payload: String) -> Self {
Self {
time_sent: Local::now(),
@ -63,119 +62,135 @@ impl Data {
pub fn to_row_and_num_search_results(
&self,
frontend_config: &FrontendConfig,
limit: usize,
width: usize,
search_highlight: Option<String>,
username_highlight: Option<String>,
theme_style: Style,
) -> (Vec<Row>, u32) {
let message = textwrap::fill(self.payload.as_str(), limit);
let username_highlight_style = username_highlight.map_or_else(Style::default, |username| {
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()
}
});
let mut num_search_matches = 0;
let msg_cells = search_highlight.map_or_else(
|| {
// If the user's name appears in a row, highlight it.
message
.split('\n')
.map(|s| {
Cell::from(Spans::from(vec![Span::styled(
s.to_owned(),
username_highlight_style,
)]))
})
.collect::<Vec<Cell>>()
},
|search| {
// Going through all the rows with a search to see if there's a fuzzy match.
// If there is, highlight said match in red.
message
.split('\n')
.map(|s| {
let chars = s.chars();
if let Some((_, indices)) = FUZZY_FINDER.fuzzy_indices(s, search.as_str()) {
num_search_matches += 1;
Cell::from(vec![Spans::from(
chars
.enumerate()
.map(|(i, s)| {
if indices.contains(&i) {
Span::styled(
s.to_string(),
Style::default()
.fg(Color::Red)
.add_modifier(Modifier::BOLD),
)
} else {
Span::raw(s.to_string())
}
})
.collect::<Vec<Span>>(),
)])
} else {
Cell::from(Spans::from(vec![Span::styled(
s.to_owned(),
username_highlight_style,
)]))
}
})
.collect::<Vec<Cell>>()
},
) -> (Vec<Spans>, u32) {
let message = textwrap::fill(
self.payload.as_str(),
width - self.author.len() - self.time_sent.to_string().len(),
);
let mut cell_vector = vec![
Cell::from(align_text(
&self.author,
frontend_config.username_alignment,
frontend_config.maximum_username_length,
))
.style(if self.system {
SYSTEM_CHAT
} else {
Style::default().fg(self.hash_username(&frontend_config.palette))
}),
msg_cells[0].clone(),
let message_spans = message
.split('\n')
.map(|s| Span::raw(s.to_owned()))
.collect::<Vec<Span>>();
let info = vec![
Span::from(
self.time_sent
.format(&frontend_config.date_format)
.to_string(),
),
Span::from(self.author.clone()),
message_spans[0].clone(),
];
if frontend_config.date_shown {
cell_vector.insert(
0,
Cell::from(
self.time_sent
.format(&frontend_config.date_format)
.to_string(),
),
);
};
let extra_message_spans = Spans::from(message_spans[0..].to_vec());
let mut row_vector = vec![Row::new(cell_vector).style(theme_style)];
(vec![Spans::from(info), extra_message_spans], 0)
if msg_cells.len() > 1 {
for cell in msg_cells.iter().skip(1) {
let mut wrapped_msg = vec![Cell::from(""), cell.clone()];
// let username_highlight_style = username_highlight.map_or_else(Style::default, |username| {
// 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()
// }
// });
if frontend_config.date_shown {
wrapped_msg.insert(0, Cell::from(""));
}
// let mut num_search_matches = 0;
row_vector.push(Row::new(wrapped_msg));
}
}
// let msg_cells = search_highlight.map_or_else(
// || {
// // If the user's name appears in a row, highlight it.
// message
// .split('\n')
// .map(|s| {
// Cell::from(Spans::from(vec![Span::styled(
// s.to_owned(),
// username_highlight_style,
// )]))
// })
// .collect::<Vec<Cell>>()
// },
// |search| {
// // Going through all the rows with a search to see if there's a fuzzy match.
// // If there is, highlight said match in red.
// message
// .split('\n')
// .map(|s| {
// let chars = s.chars();
(row_vector, num_search_matches)
// if let Some((_, indices)) = FUZZY_FINDER.fuzzy_indices(s, search.as_str()) {
// num_search_matches += 1;
// Cell::from(vec![Spans::from(
// chars
// .enumerate()
// .map(|(i, s)| {
// if indices.contains(&i) {
// Span::styled(
// s.to_string(),
// Style::default()
// .fg(Color::Red)
// .add_modifier(Modifier::BOLD),
// )
// } else {
// Span::raw(s.to_string())
// }
// })
// .collect::<Vec<Span>>(),
// )])
// } else {
// Cell::from(Spans::from(vec![Span::styled(
// s.to_owned(),
// username_highlight_style,
// )]))
// }
// })
// .collect::<Vec<Cell>>()
// },
// );
// let mut cell_vector = vec![
// Cell::from(self.author).style(if self.system {
// SYSTEM_CHAT
// } else {
// Style::default().fg(self.hash_username(&frontend_config.palette))
// }),
// msg_cells[0].clone(),
// ];
// if frontend_config.date_shown {
// cell_vector.insert(
// 0,
// Cell::from(
// self.time_sent
// .format(&frontend_config.date_format)
// .to_string(),
// ),
// );
// };
// let mut row_vector = vec![Row::new(cell_vector).style(theme_style)];
// if msg_cells.len() > 1 {
// for cell in msg_cells.iter().skip(1) {
// let mut wrapped_msg = vec![Cell::from(""), cell.clone()];
// if frontend_config.date_shown {
// wrapped_msg.insert(0, Cell::from(""));
// }
// row_vector.push(Row::new(wrapped_msg));
// }
// }
}
}
@ -189,16 +204,16 @@ impl<'conf> DataBuilder<'conf> {
DataBuilder { date_format }
}
pub fn user(user: String, payload: String) -> Data {
Data::new(user, false, payload)
pub fn user(user: String, payload: String) -> MessageData {
MessageData::new(user, false, payload)
}
pub fn system(self, payload: String) -> Data {
Data::new("System".to_string(), true, payload)
pub fn system(self, payload: String) -> MessageData {
MessageData::new("System".to_string(), true, payload)
}
pub fn twitch(self, payload: String) -> Data {
Data::new("Twitch".to_string(), true, payload)
pub fn twitch(self, payload: String) -> MessageData {
MessageData::new("Twitch".to_string(), true, payload)
}
}
@ -209,7 +224,7 @@ mod tests {
#[test]
fn test_username_hash() {
assert_eq!(
Data::new("human".to_string(), false, "beep boop".to_string())
MessageData::new("human".to_string(), false, "beep boop".to_string())
.hash_username(&Palette::Pastel),
Rgb(159, 223, 221)
);

View File

@ -10,7 +10,7 @@ use crate::{
user_input::events::{Event, Events, Key},
},
twitch::TwitchAction,
ui::statics::{CHANNEL_NAME_REGEX, TWITCH_MESSAGE_LIMIT},
ui::statics::{NAME_RESTRICTION_REGEX, TWITCH_MESSAGE_LIMIT},
};
pub enum TerminalAction {
@ -86,7 +86,7 @@ async fn handle_insert_enter_key(action: &mut UserActionAttributes<'_, '_>) {
let input_message = &mut app.input_buffer;
if input_message.is_empty()
|| !Regex::new(&CHANNEL_NAME_REGEX)
|| !Regex::new(&NAME_RESTRICTION_REGEX)
.unwrap()
.is_match(input_message)
{

View File

@ -8,7 +8,7 @@ use crate::{
handlers::{
app::App,
config::CompleteConfig,
data::Data,
data::MessageData,
user_input::{
events::{Config, Events, Key},
input::{handle_stateful_user_input, TerminalAction},
@ -22,7 +22,7 @@ pub async fn ui_driver(
mut config: CompleteConfig,
mut app: App,
tx: Sender<TwitchAction>,
mut rx: Receiver<Data>,
mut rx: Receiver<MessageData>,
) {
info!("Started UI driver.");

View File

@ -8,7 +8,7 @@ use tokio::{sync::mpsc::Sender, time::sleep};
use crate::handlers::{
config::CompleteConfig,
data::{Data, DataBuilder},
data::{DataBuilder, MessageData},
};
/// Initialize the config and send it to the client to connect to an IRC channel.
@ -37,7 +37,7 @@ pub async fn create_client_stream(config: CompleteConfig) -> (Client, ClientStre
/// If an error of any kind occurs, attempt to reconnect to the IRC channel.
pub async fn client_stream_reconnect(
err: Error,
tx: Sender<Data>,
tx: Sender<MessageData>,
data_builder: DataBuilder<'_>,
client: &mut Client,
stream: &mut ClientStream,

View File

@ -14,7 +14,7 @@ use tokio::sync::mpsc::{Receiver, Sender};
use crate::{
handlers::{
config::CompleteConfig,
data::{Data, DataBuilder},
data::{DataBuilder, MessageData},
},
twitch::{
badges::retrieve_user_badges,
@ -30,7 +30,7 @@ pub enum TwitchAction {
pub async fn twitch_irc(
mut config: CompleteConfig,
tx: Sender<Data>,
tx: Sender<MessageData>,
mut rx: Receiver<TwitchAction>,
) {
info!("Spawned Twitch IRC thread.");
@ -118,7 +118,7 @@ pub async fn twitch_irc(
async fn handle_message_command(
message: Message,
tx: Sender<Data>,
tx: Sender<MessageData>,
data_builder: DataBuilder<'_>,
badges: bool,
room_state_startup: bool,
@ -178,7 +178,7 @@ async fn handle_message_command(
None
}
pub async fn handle_roomstate(tx: &Sender<Data>, tags: &HashMap<&str, &str>) {
pub async fn handle_roomstate(tx: &Sender<MessageData>, tags: &HashMap<&str, &str>) {
let mut room_state = String::new();
for (name, value) in tags.iter() {

View File

@ -6,7 +6,7 @@ use tui::backend::Backend;
use crate::{
ui::{
components::{popups::centered_popup, render_insert_box},
statics::CHANNEL_NAME_REGEX,
statics::NAME_RESTRICTION_REGEX,
WindowAttributes,
},
utils::text::first_similarity,
@ -43,7 +43,7 @@ pub fn render_channel_switcher<T: Backend>(window: WindowAttributes<T>, channel_
Some(input_rect),
suggestion,
Some(Box::new(|s: String| -> bool {
Regex::new(&CHANNEL_NAME_REGEX)
Regex::new(&NAME_RESTRICTION_REGEX)
.unwrap()
.is_match(s.as_str())
})),

View File

@ -8,8 +8,8 @@ use tui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
terminal::Frame,
text::{Span, Spans},
widgets::{Block, Borders, Cell, Row, Table},
text::{Span, Spans, Text},
widgets::{Block, Borders, Cell, List, ListItem},
};
use crate::{
@ -19,7 +19,7 @@ use crate::{
},
utils::{
styles,
text::{align_text, title_spans, TitleStyle},
text::{title_spans, TitleStyle},
},
};
@ -73,33 +73,6 @@ impl<'a, 'b, 'c, T: Backend> WindowAttributes<'a, 'b, 'c, T> {
}
pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &CompleteConfig) {
let username_column_title = align_text(
"Username",
config.frontend.username_alignment,
config.frontend.maximum_username_length,
);
let mut column_titles = vec![username_column_title, "Message content".to_string()];
let mut table_constraints = vec![
Constraint::Length(config.frontend.maximum_username_length),
Constraint::Percentage(100),
];
if config.frontend.date_shown {
column_titles.insert(0, "Time".to_string());
table_constraints.insert(
0,
Constraint::Length(
Local::now()
.format(config.frontend.date_format.as_str())
.to_string()
.len() as u16,
),
);
}
// Constraints for different states of the application.
// Modify this in order to create new layouts.
let mut v_constraints = match app.get_state() {
@ -119,29 +92,22 @@ pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &Complet
let layout = LayoutAttributes::new(v_constraints, v_chunks);
// Horizontal chunks represents the table within the main chat window.
let h_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(table_constraints.clone())
.split(frame.size());
// 0'th index because no matter what index is obtained, they're the same height.
let general_chunk_height = layout.first_chunk().height as usize - 3;
// The chunk furthest to the right is the messages, that's the one we want.
let message_chunk_width = h_chunks[table_constraints.len() - 1].width as usize - 4;
// Making sure that messages do have a limit and don't eat up all the RAM.
app.messages.truncate(config.terminal.maximum_messages);
// Accounting for not all heights of rows to be the same due to text wrapping,
// so extra space needs to be used in order to scroll correctly.
let mut total_row_height: usize = 0;
let mut display_rows = VecDeque::new();
let mut messages: VecDeque<Spans> = VecDeque::new();
let mut scroll_offset = app.scrolling.get_offset();
let mut total_num_search_results = 0;
'outer: for data in &app.messages {
if app.filters.contaminated(data.payload.clone().as_str()) {
continue;
@ -160,31 +126,54 @@ pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &Complet
None
};
let (rows, num_results) = if app.input_buffer.is_empty() {
data.to_row_and_num_search_results(
&config.frontend,
message_chunk_width,
None,
username_highlight,
app.theme_style,
)
} else {
data.to_row_and_num_search_results(
&config.frontend,
message_chunk_width,
let (spans, num_results) = data.to_row_and_num_search_results(
&config.frontend,
frame.size().width as usize,
if app.input_buffer.is_empty() {
None
} else {
match &app.get_state() {
State::MessageSearch => Some(app.input_buffer.to_string()),
_ => None,
},
username_highlight,
app.theme_style,
)
};
total_num_search_results += num_results;
}
},
username_highlight,
app.theme_style,
);
for row in rows.iter().rev() {
// let message_wrapped = textwrap::fill(
// data.payload.as_str(),
// frame.size().width as usize - data.author.len() - data.time_sent.to_string().len()
// // (frame.size().width as u16) - data.author.len() as u16 - data.time_sent.to_string().len() as u16,
// );
// let message_split = message_wrapped.split('\n');
// let message = message_split
// .map(|s| Span::raw(s))
// .collect::<Vec<Span>>();
// let info = vec![
// Span::from(
// data.time_sent
// .format(&config.frontend.date_format)
// .to_string(),
// ),
// Span::from(data.author.clone()),
// message[0].clone(),
// ];
// let extra_message_spans = Spans::from(message[0..].to_vec());
// // (vec![Spans::from(info), extra_message_spans], 0)
// let spans = vec![Spans::from(info), extra_message_spans];
// total_num_search_results += num_results;
for span in spans.iter().rev() {
if total_row_height < general_chunk_height {
display_rows.push_front(row.clone());
messages.push_front(span.to_owned());
total_row_height += 1;
} else {
@ -196,7 +185,7 @@ pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &Complet
// Padding with empty rows so chat can go from bottom to top.
if general_chunk_height > total_row_height {
for _ in 0..(general_chunk_height - total_row_height) {
display_rows.push_front(Row::new(vec![Cell::from("")]));
messages.push_front(Spans::from(vec![Span::raw("")]));
}
}
@ -232,18 +221,24 @@ pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &Complet
Spans::default()
};
let table = Table::new(display_rows)
.header(Row::new(column_titles.clone()).style(styles::COLUMN_TITLE))
let mut final_messages = vec![];
for item in messages {
final_messages.push(ListItem::new(Text::from(item)));
}
let list = List::new(final_messages)
.block(
Block::default()
.borders(Borders::ALL)
.title(chat_title)
.style(app.theme_style),
)
.widths(&table_constraints)
.column_spacing(1);
.style(Style::default().fg(Color::White));
// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
// .highlight_symbol(">>");
frame.render_widget(table, layout.first_chunk());
frame.render_widget(list, layout.first_chunk());
if config.frontend.state_tabs {
components::render_state_tabs(frame, &layout, &app.get_state());
@ -271,6 +266,6 @@ pub fn draw_ui<T: Backend>(frame: &mut Frame<T>, app: &mut App, config: &Complet
State::ChannelSwitch => {
components::render_channel_switcher(window, config.storage.channels);
}
State::Normal => {}
_ => {}
}
}

View File

@ -78,5 +78,5 @@ lazy_static! {
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}$";
pub static ref NAME_RESTRICTION_REGEX: &'static str = "^[a-zA-Z0-9_]{4,25}$";
}

View File

@ -1,38 +1,8 @@
use rustyline::line_buffer::LineBuffer;
use textwrap::core::display_width;
use tui::{style::Style, text::Span};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::handlers::config::Alignment;
pub fn align_text(text: &str, alignment: Alignment, maximum_length: u16) -> String {
assert!(
maximum_length >= 1,
"Parameter of 'maximum_length' cannot be below 1."
);
// Compute the display width of `text` with support of emojis and CJK characters
let mut dw = display_width(text);
if dw > maximum_length as usize {
dw = maximum_length as usize;
}
match alignment {
Alignment::Right => {
let spacing = " ".repeat(maximum_length as usize - dw);
format!("{spacing}{text}")
}
Alignment::Center => {
let side_spaces =
" ".repeat(((maximum_length / 2) - (((dw / 2) as f32).floor() as u16)) as usize);
format!("{side_spaces}{text}{side_spaces}")
}
Alignment::Left => text.to_string(),
}
}
/// Acquiring the horizontal position of the cursor so it can be rendered visually.
pub fn get_cursor_position(line_buffer: &LineBuffer) -> usize {
line_buffer
@ -98,40 +68,6 @@ mod tests {
use super::*;
#[test]
#[should_panic(expected = "Parameter of 'maximum_length' cannot be below 1.")]
fn test_text_align_maximum_length() {
align_text("", Alignment::Left, 0);
}
#[test]
fn test_text_align_left() {
assert_eq!(align_text("a", Alignment::Left, 10), "a".to_string());
assert_eq!(align_text("a", Alignment::Left, 1), "a".to_string());
}
#[test]
fn test_text_align_right() {
assert_eq!(
align_text("a", Alignment::Right, 10),
format!("{}{}", " ".repeat(9), "a")
);
assert_eq!(align_text("a", Alignment::Right, 1), "a".to_string());
assert_eq!(align_text("你好", Alignment::Right, 5), " 你好");
assert_eq!(align_text("👑123", Alignment::Right, 6), " 👑123");
}
#[test]
fn test_text_align_center() {
assert_eq!(
align_text("a", Alignment::Center, 11),
format!("{}{}{}", " ".repeat(5), "a", " ".repeat(5))
);
assert_eq!(align_text("a", Alignment::Center, 1), "a".to_string());
assert_eq!(align_text("你好", Alignment::Center, 6), " 你好 ");
assert_eq!(align_text("👑123", Alignment::Center, 7), " 👑123 ");
}
#[test]
fn test_get_cursor_position_with_single_byte_graphemes() {
let text = "never gonna give you up";