UI tweaks on skills popup. (#8250)

Only display the skill name (not the folder), and truncate the skill
description to a maximum of two lines.
This commit is contained in:
xl-openai
2025-12-18 17:16:51 -08:00
committed by GitHub
parent 6c76d17713
commit dcc01198e2
5 changed files with 298 additions and 26 deletions

View File

@@ -9,6 +9,7 @@ use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Widget;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
use crate::key_hint::KeyBinding;
@@ -25,6 +26,77 @@ pub(crate) struct GenericDisplayRow {
pub wrap_indent: Option<usize>, // optional indent for wrapped lines
}
fn line_width(line: &Line<'_>) -> usize {
line.iter()
.map(|span| UnicodeWidthStr::width(span.content.as_ref()))
.sum()
}
fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static> {
if max_width == 0 {
return Line::from(Vec::<Span<'static>>::new());
}
let mut used = 0usize;
let mut spans_out: Vec<Span<'static>> = Vec::new();
for span in line.spans {
let text = span.content.into_owned();
let style = span.style;
let span_width = UnicodeWidthStr::width(text.as_str());
if span_width == 0 {
spans_out.push(Span::styled(text, style));
continue;
}
if used >= max_width {
break;
}
if used + span_width <= max_width {
used += span_width;
spans_out.push(Span::styled(text, style));
continue;
}
let mut truncated = String::new();
for ch in text.chars() {
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
if used + ch_width > max_width {
break;
}
truncated.push(ch);
used += ch_width;
}
if !truncated.is_empty() {
spans_out.push(Span::styled(truncated, style));
}
break;
}
Line::from(spans_out)
}
fn truncate_line_with_ellipsis_if_overflow(line: Line<'static>, max_width: usize) -> Line<'static> {
if max_width == 0 {
return Line::from(Vec::<Span<'static>>::new());
}
let width = line_width(&line);
if width <= max_width {
return line;
}
let truncated = truncate_line_to_width(line, max_width.saturating_sub(1));
let mut spans = truncated.spans;
let ellipsis_style = spans.last().map(|span| span.style).unwrap_or_default();
spans.push(Span::styled("", ellipsis_style));
Line::from(spans)
}
/// Compute a shared description-column start based on the widest visible name
/// plus two spaces of padding. Ensures at least one column is left for the
/// description.
@@ -235,6 +307,72 @@ pub(crate) fn render_rows(
}
}
/// Render rows as a single line each (no wrapping), truncating overflow with an ellipsis.
pub(crate) fn render_rows_single_line(
area: Rect,
buf: &mut Buffer,
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_results: usize,
empty_message: &str,
) {
if rows_all.is_empty() {
if area.height > 0 {
Line::from(empty_message.dim().italic()).render(area, buf);
}
return;
}
let visible_items = max_results
.min(rows_all.len())
.min(area.height.max(1) as usize);
let mut start_idx = state.scroll_top.min(rows_all.len().saturating_sub(1));
if let Some(sel) = state.selected_idx {
if sel < start_idx {
start_idx = sel;
} else if visible_items > 0 {
let bottom = start_idx + visible_items - 1;
if sel > bottom {
start_idx = sel + 1 - visible_items;
}
}
}
let desc_col = compute_desc_col(rows_all, start_idx, visible_items, area.width);
let mut cur_y = area.y;
for (i, row) in rows_all
.iter()
.enumerate()
.skip(start_idx)
.take(visible_items)
{
if cur_y >= area.y + area.height {
break;
}
let mut full_line = build_full_line(row, desc_col);
if Some(i) == state.selected_idx {
full_line.spans.iter_mut().for_each(|span| {
span.style = Style::default().fg(Color::Cyan).bold();
});
}
let full_line = truncate_line_with_ellipsis_if_overflow(full_line, area.width as usize);
full_line.render(
Rect {
x: area.x,
y: cur_y,
width: area.width,
height: 1,
},
buf,
);
cur_y = cur_y.saturating_add(1);
}
}
/// Compute the number of terminal rows required to render up to `max_results`
/// items from `rows_all` given the current scroll/selection state and the
/// available `width`. Accounts for description wrapping and alignment so the
@@ -281,7 +419,8 @@ pub(crate) fn measure_rows_height(
let opts = RtOptions::new(content_width as usize)
.initial_indent(Line::from(""))
.subsequent_indent(Line::from(" ".repeat(continuation_indent)));
total = total.saturating_add(word_wrap_line(&full_line, opts).len() as u16);
let wrapped_lines = word_wrap_line(&full_line, opts).len();
total = total.saturating_add(wrapped_lines as u16);
}
total.max(1)
}