various: fix TUI navigation performance; unicode rendering

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I027f00979bd5f354e3ea0257e4b8d8bf6a6a6964
This commit is contained in:
raf 2026-05-20 21:15:33 +03:00
commit 5d6abab1de
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
2 changed files with 38 additions and 35 deletions

View file

@ -168,7 +168,6 @@ impl TuiState {
} else { } else {
self.cursor + 1 self.cursor + 1
}; };
self.dirty = true;
} }
/// Move the cursor up by one, wrapping to `total - 1` at the top. /// Move the cursor up by one, wrapping to `total - 1` at the top.
@ -181,7 +180,6 @@ impl TuiState {
} else { } else {
self.cursor - 1 self.cursor - 1
}; };
self.dirty = true;
} }
/// Resize the window (e.g. terminal resized). Marks dirty so the /// Resize the window (e.g. terminal resized). Marks dirty so the
@ -515,30 +513,43 @@ impl SqliteClipboardDb {
.enumerate() .enumerate()
.map(|(i, entry)| { .map(|(i, entry)| {
let mut preview = String::new(); let mut preview = String::new();
let mut width = 0; let mut pwidth = 0usize;
for g in entry.1.graphemes(true) { for g in entry.1.graphemes(true) {
let g_width = UnicodeWidthStr::width(g); let gw = UnicodeWidthStr::width(g);
if width + g_width > preview_col { if pwidth + gw > preview_col {
preview.push('…'); preview.push('…');
pwidth += 1;
break; break;
} }
preview.push_str(g); preview.push_str(g);
width += g_width; pwidth += gw;
} }
let mut mime = String::new(); let preview_pad = preview_col.saturating_sub(pwidth);
let mut mwidth = 0; for _ in 0..preview_pad {
for g in entry.2.graphemes(true) { preview.push(' ');
let g_width = UnicodeWidthStr::width(g);
if mwidth + g_width > mime_col {
mime.push('…');
break;
}
mime.push_str(g);
mwidth += g_width;
} }
let mut mime_trunc = String::new();
let mut mwidth = 0usize;
for g in entry.2.graphemes(true) {
let gw = UnicodeWidthStr::width(g);
if mwidth + gw > mime_col {
mime_trunc.push('…');
mwidth += 1;
break;
}
mime_trunc.push_str(g);
mwidth += gw;
}
let mime_pad = mime_col.saturating_sub(mwidth);
let mime_padded = if mime_pad > 0 {
format!("{}{mime_trunc}", " ".repeat(mime_pad))
} else {
mime_trunc
};
let id = entry.0;
let mut spans = Vec::new(); let mut spans = Vec::new();
let (id, preview, mime) = entry;
if Some(i) == selected { if Some(i) == selected {
spans.push(Span::styled( spans.push(Span::styled(
highlight_symbol, highlight_symbol,
@ -554,23 +565,23 @@ impl SqliteClipboardDb {
)); ));
spans.push(Span::raw(" ")); spans.push(Span::raw(" "));
spans.push(Span::styled( spans.push(Span::styled(
format!("{preview:<preview_col$}"), preview,
Style::default() Style::default()
.fg(Color::Yellow) .fg(Color::Yellow)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
)); ));
spans.push(Span::raw(" ")); spans.push(Span::raw(" "));
spans.push(Span::styled( spans.push(Span::styled(
format!("{mime:>mime_col$}"), mime_padded,
Style::default().fg(Color::Green), Style::default().fg(Color::Green),
)); ));
} else { } else {
spans.push(Span::raw(" ")); spans.push(Span::raw(" "));
spans.push(Span::raw(format!("{id:>id_col$}"))); spans.push(Span::raw(format!("{id:>id_col$}")));
spans.push(Span::raw(" ")); spans.push(Span::raw(" "));
spans.push(Span::raw(format!("{preview:<preview_col$}"))); spans.push(Span::raw(preview));
spans.push(Span::raw(" ")); spans.push(Span::raw(" "));
spans.push(Span::raw(format!("{mime:>mime_col$}"))); spans.push(Span::raw(mime_padded));
} }
ListItem::new(Line::from(spans)) ListItem::new(Line::from(spans))
}) })
@ -635,15 +646,9 @@ impl SqliteClipboardDb {
if actions.search_backspace { if actions.search_backspace {
let new_query = tui let new_query = tui
.search_query .search_query
.chars() .char_indices()
.next_back() .next_back()
.map(|_| { .map(|(i, _)| tui.search_query[..i].to_string())
tui
.search_query
.chars()
.take(tui.search_query.len() - 1)
.collect::<String>()
})
.unwrap_or_default(); .unwrap_or_default();
if tui.set_search(new_query) { if tui.set_search(new_query) {
// Search changed, refresh count and reset // Search changed, refresh count and reset

View file

@ -77,6 +77,7 @@ use regex::Regex;
use rusqlite::{Connection, OptionalExtension, params}; use rusqlite::{Connection, OptionalExtension, params};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use unicode_width::UnicodeWidthChar;
pub const DEFAULT_MAX_ENTRY_SIZE: usize = 5_000_000; pub const DEFAULT_MAX_ENTRY_SIZE: usize = 5_000_000;
@ -1076,19 +1077,16 @@ pub fn preview_entry(data: &[u8], mime: Option<&str>, width: u32) -> String {
return trimmed.to_string(); return trimmed.to_string();
} }
// Only allocate new string if we need to replace whitespace
let mut result = String::with_capacity(width as usize + 1); let mut result = String::with_capacity(width as usize + 1);
for (char_count, c) in trimmed.chars().enumerate() { let mut disp = 0usize;
if char_count >= width as usize { for c in trimmed.chars() {
let cw = UnicodeWidthChar::width(c).unwrap_or(1);
if disp + cw > width as usize {
result.push('…'); result.push('…');
break; break;
} }
result.push(if c.is_whitespace() { ' ' } else { c });
if c.is_whitespace() { disp += cw;
result.push(' ');
} else {
result.push(c);
}
} }
return result; return result;
} }