Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor emote parsing, fix some emote display issues #529

Merged
merged 5 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Remove unneeded DIACRITICS_ZERO and Spans with ZERO_WIDTH_SPACE
I found out kitty does not need to have the DIACRITICS_ZERO specified,
and we can avoid it entirely.
This also removes the Spans(ZERO_WIDTH_SPACE) between emotes/text, as
they are not needed, and were causing rendering issues with ratatui.
  • Loading branch information
Nogesma committed Feb 5, 2024
commit 7e08ecf462c27e6e0389244c281de18e59980d3d
23 changes: 5 additions & 18 deletions src/handlers/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
utils::{
colors::{hsl_to_rgb, u32_to_color},
emotes::{
get_emote_offset, UnicodePlaceholder, DIACRITICS_ZERO, PRIVATE_USE_UNICODE,
ZERO_WIDTH_SPACE, ZERO_WIDTH_SPACE_STR,
get_emote_offset, UnicodePlaceholder, PRIVATE_USE_UNICODE, ZERO_WIDTH_SPACE,
ZERO_WIDTH_SPACE_STR,
},
styles::{
DATETIME_DARK, DATETIME_LIGHT, HIGHLIGHT_NAME_DARK, HIGHLIGHT_NAME_LIGHT, SYSTEM_CHAT,
Expand Down Expand Up @@ -104,7 +104,7 @@
return Some(s.len() - 1 - chars.as_str().len());
}
}
None

Check warning on line 107 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L107

Added line #L107 was not covered by tests
})
.collect()
}
Expand All @@ -117,8 +117,8 @@
*emotes = &emotes[1..];
Span::styled(content, Style::default().fg(id).underline_color(pid))
} else {
error!("Emote index >= emotes.len()");
Span::raw(content)

Check warning on line 121 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L120-L121

Added lines #L120 - L121 were not covered by tests
}
}

Expand Down Expand Up @@ -197,22 +197,20 @@
));
}
*start_index += ZERO_WIDTH_SPACE_STR.len();
spans.push(Span::raw(ZERO_WIDTH_SPACE_STR));
}

*start_index = start_index.saturating_sub(ZERO_WIDTH_SPACE_STR.len());
spans.pop();
spans
}
}

pub fn to_vec(
&self,
frontend_config: &FrontendConfig,
width: usize,
search_highlight: Option<&str>,
username_highlight: Option<&str>,
) -> Vec<Line> {

Check warning on line 213 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L207-L213

Added lines #L207 - L213 were not covered by tests
// Theme styles
let username_theme = match frontend_config.theme {
Theme::Dark => HIGHLIGHT_NAME_DARK,
Expand All @@ -234,7 +232,7 @@
.map(|name| {
self.payload
.match_indices(name)
.flat_map(|(index, _)| index..(index + name.len()))

Check warning on line 235 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L235

Added line #L235 was not covered by tests
.collect::<Vec<usize>>()
})
.unwrap_or_default();
Expand All @@ -244,11 +242,11 @@
.and_then(|query| {
FUZZY_FINDER
.fuzzy_indices(&self.payload, query)
.map(|(_, indices)| {
// `username_highlight` indices are byte indices, whereas `fuzzy_indices` returns char indices.
// Convert those char indices to byte indices, which are easier to work with.
Self::char_to_byte_indices(&self.payload, indices.into_iter())
})

Check warning on line 249 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L245-L249

Added lines #L245 - L249 were not covered by tests
})
.unwrap_or_default();

Expand All @@ -256,22 +254,22 @@
let username = (&username_highlight as &[usize], username_theme);

// Message prefix
let time_sent = if frontend_config.show_datetimes {
Some(
self.time_sent
.format(&frontend_config.datetime_format)
.to_string(),
)

Check warning on line 262 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L257-L262

Added lines #L257 - L262 were not covered by tests
} else {
None

Check warning on line 264 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L264

Added line #L264 was not covered by tests
};

// Add 1 for the space after the timestamp
let time_sent_len = time_sent.as_ref().map_or(0, |t| t.len() + 1);

Check warning on line 268 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L268

Added line #L268 was not covered by tests

let prefix_len = if frontend_config.username_shown {
// Add 2 for the ": "
time_sent_len + self.author.len() + 2

Check warning on line 272 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L272

Added line #L272 was not covered by tests
} else {
time_sent_len
};
Expand All @@ -293,19 +291,19 @@

let username_alignment = if frontend_config.username_shown {
if frontend_config.right_align_usernames {
NAME_MAX_CHARACTERS.saturating_sub(self.author.width()) + 1

Check warning on line 294 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L294

Added line #L294 was not covered by tests
} else {
1
}
} else {
1

Check warning on line 299 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L299

Added line #L299 was not covered by tests
};

let mut first_row: Vec<Span<'_>> = vec![];

if let Some(t) = time_sent {

Check warning on line 304 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L304

Added line #L304 was not covered by tests
first_row.extend(vec![
Span::styled(t, datetime_theme),

Check warning on line 306 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L306

Added line #L306 was not covered by tests
Span::raw(" ".repeat(username_alignment)),
]);
}
Expand All @@ -313,7 +311,7 @@
if frontend_config.username_shown {
first_row.extend(vec![
Span::styled(&self.author, author_theme),
Span::raw(": "),

Check warning on line 314 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L314

Added line #L314 was not covered by tests
]);
}

Expand All @@ -321,27 +319,27 @@

// Unwrapping is safe because of the empty check above
let mut first_line = lines.next().unwrap();
let first_line_msg = split_cow_in_place(&mut first_line, prefix_len);

let mut emotes = &self.emotes[..];

Check warning on line 324 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L322-L324

Added lines #L322 - L324 were not covered by tests

first_row.extend(Self::build_line(

Check warning on line 326 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L326

Added line #L326 was not covered by tests
first_line_msg,
&mut next_index,
search,
username,
&mut emotes,

Check warning on line 331 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L331

Added line #L331 was not covered by tests
));

let mut rows = vec![Line::from(first_row)];

rows.extend(lines.map(|line| {
Line::from(Self::build_line(

Check warning on line 337 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L337

Added line #L337 was not covered by tests
line,
&mut next_index,
search,
username,
&mut emotes,

Check warning on line 342 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L342

Added line #L342 was not covered by tests
))
}));

Expand All @@ -353,103 +351,100 @@
/// The emote will then be displayed by the terminal by encoding its id in its foreground color, and its pid in its underline color.
/// Ratatui removes all ansi escape sequences, so the id/pid of the emote is stored and encoded in [`MessageData::to_vec`].
pub fn parse_emotes(&mut self, emotes: &mut Emotes) {
if emotes.emotes.is_empty() {
return;
}

Check warning on line 357 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L354-L357

Added lines #L354 - L357 were not covered by tests
let mut words = Vec::new();

self.payload.split(' ').for_each(|word| {
let Some((filename, zero_width)) = emotes.emotes.get(word) else {
words.push(Word::Text(word.to_string()));
return;

Check warning on line 363 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L361-L363

Added lines #L361 - L363 were not covered by tests
};

let Ok(loaded_emote) = load_emote(
word,
filename,
*zero_width,
&mut emotes.info,
emotes.cell_size,
)
.map_err(|e| warn!("Unable to load emote {word} ({filename}): {e}")) else {
emotes.emotes.remove(word);
words.push(Word::Text(word.to_string()));
return;

Check warning on line 376 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L366-L376

Added lines #L366 - L376 were not covered by tests
};

if loaded_emote.overlay {

Check warning on line 379 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L379

Added line #L379 was not covered by tests
// Check if last word is emote.
if let Some(Word::Emote(v)) = words.last_mut() {
v.push(loaded_emote.into());
return;

Check warning on line 383 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L381-L383

Added lines #L381 - L383 were not covered by tests
}
}

words.push(Word::Emote(vec![loaded_emote.into()]));

Check warning on line 387 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L387

Added line #L387 was not covered by tests
});

self.payload.clear();

Check warning on line 390 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L390

Added line #L390 was not covered by tests

// Join words by space, or by zero-width spaces if one of them is an emote.
for w in words {
match w {
Word::Text(s) => {
if !self.payload.is_empty() {
self.payload.push(
if self.payload.ends_with(PRIVATE_USE_UNICODE)
|| self.payload.ends_with(DIACRITICS_ZERO)
{
self.payload
.push(if self.payload.ends_with(PRIVATE_USE_UNICODE) {
ZERO_WIDTH_SPACE

Check warning on line 399 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L393-L399

Added lines #L393 - L399 were not covered by tests
} else {
' '

Check warning on line 401 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L401

Added line #L401 was not covered by tests
},
);
});
}
self.payload.push_str(&s);

Check warning on line 404 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L403-L404

Added lines #L403 - L404 were not covered by tests
}
Word::Emote(v) => {
// Unwrapping here is fine as v is never empty.
let max_width = v
.iter()
.max_by_key(|e| e.width)
.expect("Emotes should never be empty")
.width as f32;

Check warning on line 412 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L406-L412

Added lines #L406 - L412 were not covered by tests
let cols = (max_width / emotes.cell_size.0).ceil() as u16;

let mut iter = v.into_iter();

let EmoteData { id, pid, width } = iter.next().unwrap();

let (_, col_offset) =
get_emote_offset(width as u16, emotes.cell_size.0 as u16, cols);

Check warning on line 420 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L415-L420

Added lines #L415 - L420 were not covered by tests

if let Err(e) = display_emote(id, pid, cols) {
warn!("Unable to display emote: {e}");
continue;

Check warning on line 424 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L424

Added line #L424 was not covered by tests
}

iter.enumerate().for_each(|(layer, emote)| {
if let Err(e) = overlay_emote(
(id, pid),
emote,
layer as u32,
cols,
col_offset,
emotes.cell_size.0 as u16,
) {

Check warning on line 435 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L427-L435

Added lines #L427 - L435 were not covered by tests
warn!("Unable to display overlay: {e}");
}
});

self.emotes.push((u32_to_color(id), u32_to_color(pid)));

Check warning on line 440 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L440

Added line #L440 was not covered by tests

if !self.payload.is_empty() {
self.payload.push(ZERO_WIDTH_SPACE);
}

Check warning on line 444 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L442-L444

Added lines #L442 - L444 were not covered by tests

self.payload
.extend(UnicodePlaceholder::new(cols as usize).iter());

Check warning on line 447 in src/handlers/data.rs

View check run for this annotation

Codecov / codecov/patch

src/handlers/data.rs#L446-L447

Added lines #L446 - L447 were not covered by tests
}
}
}
Expand Down Expand Up @@ -674,13 +669,9 @@
spans,
vec![
Span::raw("foo"),
Span::raw(ZERO_WIDTH_SPACE_STR),
Span::styled(emote_w_3, STYLES[0]),
Span::raw(ZERO_WIDTH_SPACE_STR),
Span::styled(emote_w_1, STYLES[1]),
Span::raw(ZERO_WIDTH_SPACE_STR),
Span::raw("bar baz"),
Span::raw(ZERO_WIDTH_SPACE_STR),
Span::styled(emote_w_2, STYLES[2]),
]
);
Expand Down Expand Up @@ -762,11 +753,8 @@
spans,
vec![
Span::raw("foo"),
Span::raw(ZERO_WIDTH_SPACE_STR),
Span::styled(emote_w_3, STYLES[0]),
Span::raw(ZERO_WIDTH_SPACE_STR),
Span::styled(emote_w_1, STYLES[1]),
Span::raw(ZERO_WIDTH_SPACE_STR),
Span::styled("b", STYLES[1]),
Span::styled("a", STYLES[1]),
Span::raw("r"),
Expand All @@ -776,7 +764,6 @@
Span::styled("b", STYLES[0]),
Span::styled("a", STYLES[1]),
Span::raw("z"),
Span::raw(ZERO_WIDTH_SPACE_STR),
Span::styled(emote_w_2, STYLES[2]),
]
);
Expand Down
31 changes: 7 additions & 24 deletions src/utils/emotes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@
use std::iter;

pub const PRIVATE_USE_UNICODE: char = '\u{10EEEE}';
pub const DIACRITICS_ZERO: char = '\u{305}';

pub const ZERO_WIDTH_SPACE: char = '\u{200B}';
pub const ZERO_WIDTH_SPACE_STR: &str = "\u{200B}";

pub const fn emotes_enabled(frontend: &FrontendConfig) -> bool {
frontend.twitch_emotes
|| frontend.betterttv_emotes
|| frontend.seventv_emotes
|| frontend.frankerfacez_emotes
}

Check warning on line 13 in src/utils/emotes.rs

View check run for this annotation

Codecov / codecov/patch

src/utils/emotes.rs#L8-L13

Added lines #L8 - L13 were not covered by tests

pub const fn get_emote_offset(width: u16, cell_width: u16, cols: u16) -> (u16, u16) {
let w = (width + if cols % 2 == 0 { 0 } else { cell_width } + 1) / 2;
Expand All @@ -32,27 +30,20 @@
///
/// A unicode placeholder consists of multiple [`PRIVATE_USE_UNICODE`] so that it takes the same amount of space on screen as the image.
///
/// [`PRIVATE_USE_UNICODE`] characters need to be followed by a diacritic indicating their position in the image.
/// For [`PRIVATE_USE_UNICODE`] adjacent to each other, only the first one needs to indicate its position.
/// The position for the other ones will be deduced automatically.
///
/// As all twitch emotes have a height of 1 row, we only need the [`DIACRITICS_ZERO`], which indicates a position of `(col, row) = (0, 0)`.
///
/// The format for a Unicode placeholder is `{PRIVATE_USE_UNICODE} + {DIACRITICS_ZERO} + {PRIVATE_USE_UNICODE} * (width - 1)`
/// The format for a Unicode placeholder is `{PRIVATE_USE_UNICODE} * width`
///
/// [Reference](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders)
pub struct UnicodePlaceholder(usize);

impl UnicodePlaceholder {
pub const fn new(width: usize) -> Self {
assert!(width > 0);
// Add 1 for the diacritic
Self(width + 1)
Self(width)
}

#[allow(unused)]
pub const fn len(&self) -> usize {
DIACRITICS_ZERO.len_utf8() + PRIVATE_USE_UNICODE.len_utf8() * (self.0 - 1)
PRIVATE_USE_UNICODE.len_utf8() * self.0
}

pub fn iter(&'_ self) -> impl Iterator<Item = char> + '_ {
Expand All @@ -62,8 +53,6 @@

if count > self.0 {
None
} else if count == 2 {
Some(DIACRITICS_ZERO)
} else {
Some(PRIVATE_USE_UNICODE)
}
Expand Down Expand Up @@ -172,30 +161,24 @@
fn unicode_placeholders() {
assert_eq!(
UnicodePlaceholder::new(1).string(),
format!("{PRIVATE_USE_UNICODE}{DIACRITICS_ZERO}")
format!("{PRIVATE_USE_UNICODE}")
);
assert_eq!(
UnicodePlaceholder::new(2).string(),
format!("{PRIVATE_USE_UNICODE}{DIACRITICS_ZERO}{PRIVATE_USE_UNICODE}")
format!("{PRIVATE_USE_UNICODE}{PRIVATE_USE_UNICODE}")
);
assert_eq!(
UnicodePlaceholder::new(3).string(),
format!(
"{PRIVATE_USE_UNICODE}{DIACRITICS_ZERO}{PRIVATE_USE_UNICODE}{PRIVATE_USE_UNICODE}"
)
format!("{PRIVATE_USE_UNICODE}{PRIVATE_USE_UNICODE}{PRIVATE_USE_UNICODE}")
);

let up = UnicodePlaceholder::new(3);

assert_eq!(
up.len(),
PRIVATE_USE_UNICODE.len_utf8() * 3 + DIACRITICS_ZERO.len_utf8()
);
assert_eq!(up.len(), PRIVATE_USE_UNICODE.len_utf8() * 3);

let mut iter = up.iter();

assert_eq!(iter.next(), Some(PRIVATE_USE_UNICODE));
assert_eq!(iter.next(), Some(DIACRITICS_ZERO));
assert_eq!(iter.next(), Some(PRIVATE_USE_UNICODE));
assert_eq!(iter.next(), Some(PRIVATE_USE_UNICODE));
assert_eq!(iter.next(), None);
Expand Down
Loading