docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
This commit is contained in:
1 parent
ff0fb56c67
commit
a02b555e66
1458 files changed
+10729
-21750
No files matched your search
@@ -59,7 +59,7 @@ pub fn indexed_to_rgb(index: u8) -> (u8, u8, u8) {
|
||||
/// ramp (232–255), returning whichever has the smallest squared Euclidean
|
||||
/// distance.
|
||||
pub fn nearest_indexed(r: u8, g: u8, b: u8) -> u8 {
|
||||
// --- nearest in the 6×6×6 color cube (16–231) ---
|
||||
// nearest in the 6×6×6 color cube (16–231)
|
||||
let ri = nearest_cube_channel(r);
|
||||
let gi = nearest_cube_channel(g);
|
||||
let bi = nearest_cube_channel(b);
|
||||
@@ -73,7 +73,7 @@ pub fn nearest_indexed(r: u8, g: u8, b: u8) -> u8 {
|
||||
CUBE_VALUES[bi as usize],
|
||||
);
|
||||
|
||||
// --- nearest in the grayscale ramp (232–255) ---
|
||||
// nearest in the grayscale ramp (232–255)
|
||||
// Ramp values: 8, 18, 28, …, 238 (24 entries)
|
||||
let lum = (r as u16 + g as u16 + b as u16) / 3;
|
||||
let gray_step = if lum <= 3 {
|
||||
@@ -131,7 +131,7 @@ fn color_to_rgb(color: Color) -> Option<(u8, u8, u8)> {
|
||||
///
|
||||
/// Useful when downstream code must produce RGB for *every* color value
|
||||
/// — e.g. progress-bar gradients that lerp across named breakpoints, or
|
||||
/// OSC 12 cursor-color updates that must emit an RGB triple regardless
|
||||
/// OSC 12 cursor-color writes that must emit an RGB triple regardless
|
||||
/// of terminal color depth.
|
||||
///
|
||||
/// Named-color RGB matches the xterm 16-color palette used by
|
||||
@@ -166,11 +166,10 @@ pub fn resolve_to_rgb(color: Color) -> Option<(u8, u8, u8)> {
|
||||
/// Blend a single color channel: lerp from base toward original based on opacity.
|
||||
///
|
||||
/// - `opacity = 0.0`: returns `base` (fully faded)
|
||||
/// - `opacity = 1.0`: returns `original` (no change)
|
||||
/// - `opacity = 1.0`: returns `original` (Unchanged)
|
||||
#[inline]
|
||||
pub fn blend_channel(base: u8, original: u8, opacity: f32) -> u8 {
|
||||
// result = base + (original - base) * opacity
|
||||
// = base * (1 - opacity) + original * opacity
|
||||
let result = base as f32 * (1.0 - opacity) + original as f32 * opacity;
|
||||
result.round() as u8
|
||||
}
|
||||
@@ -178,7 +177,7 @@ pub fn blend_channel(base: u8, original: u8, opacity: f32) -> u8 {
|
||||
/// Blend a color toward a base color based on opacity.
|
||||
///
|
||||
/// - `opacity = 0.0`: returns `base` (fully faded)
|
||||
/// - `opacity = 1.0`: returns `original` (no change)
|
||||
/// - `opacity = 1.0`: returns `original` (Unchanged)
|
||||
///
|
||||
/// Supports both `Color::Rgb` and `Color::Indexed` colors (indexed colors are
|
||||
/// converted to their RGB equivalents for blending). When either input is
|
||||
@@ -212,9 +211,9 @@ pub fn blend_color(base: Color, original: Color, opacity: f32) -> Option<Color>
|
||||
/// its colors toward the background.
|
||||
///
|
||||
/// - `opacity = 0.0`: fully faded to base color
|
||||
/// - `opacity = 1.0`: no change (original colors)
|
||||
/// - `opacity = 1.0`: Unchanged (original colors)
|
||||
///
|
||||
/// Named ANSI colors are left unchanged.
|
||||
/// Named ANSI colors are left `unchanged`.
|
||||
pub fn blend_line(line: Line<'static>, base: Color, opacity: f32) -> Line<'static> {
|
||||
let blended_spans: Vec<Span<'static>> = line
|
||||
.spans
|
||||
@@ -239,9 +238,9 @@ pub fn blend_line(line: Line<'static>, base: Color, opacity: f32) -> Line<'stati
|
||||
/// explicitly colored text.
|
||||
///
|
||||
/// - `opacity = 0.0`: fully faded to base color
|
||||
/// - `opacity = 1.0`: no change (original colors)
|
||||
/// - `opacity = 1.0`: Unchanged (original colors)
|
||||
///
|
||||
/// Named ANSI colors are left unchanged.
|
||||
/// Named ANSI colors are left `unchanged`.
|
||||
pub fn blend_line_with_default(
|
||||
line: Line<'static>,
|
||||
base: Color,
|
||||
@@ -269,10 +268,10 @@ pub fn blend_line_with_default(
|
||||
/// This blends both foreground and background colors of each cell toward
|
||||
/// `base_color` based on `opacity`:
|
||||
/// - `opacity = 0.0`: fully faded (cells become base_color)
|
||||
/// - `opacity = 1.0`: no change
|
||||
/// - `opacity = 1.0`: Unchanged
|
||||
///
|
||||
/// Both RGB and Indexed colors are blended; named ANSI colors (Color::Red, etc.)
|
||||
/// are left unchanged since their RGB values are terminal-dependent.
|
||||
/// are left `unchanged` since their RGB values are terminal-dependent.
|
||||
pub fn fade_region(buf: &mut Buffer, area: Rect, base_color: Color, opacity: f32) {
|
||||
blend_area(
|
||||
buf,
|
||||
@@ -285,10 +284,10 @@ pub fn fade_region(buf: &mut Buffer, area: Rect, base_color: Color, opacity: f32
|
||||
/// Blend fg and/or bg of every cell in an area toward target colors.
|
||||
///
|
||||
/// Each parameter is `Option<(target, opacity)>`:
|
||||
/// - `None`: leave that channel unchanged
|
||||
/// - `None`: leave that channel `unchanged`
|
||||
/// - `Some((target, opacity))`: blend toward `target` at `opacity`
|
||||
/// - `opacity = 0.0`: fully target (original gone)
|
||||
/// - `opacity = 1.0`: no change (original kept)
|
||||
/// - `opacity = 1.0`: Unchanged (original kept)
|
||||
///
|
||||
/// Both RGB and Indexed colors are blended; named ANSI color cells are skipped.
|
||||
pub fn blend_area(
|
||||
@@ -383,7 +382,8 @@ mod tests {
|
||||
// opacity = 0.5: halfway between
|
||||
assert_eq!(blend_channel(0, 100, 0.5), 50);
|
||||
assert_eq!(blend_channel(100, 200, 0.5), 150);
|
||||
assert_eq!(blend_channel(0, 255, 0.5), 128); // 127.5 rounds to 128
|
||||
// 127.5 rounds to 128
|
||||
assert_eq!(blend_channel(0, 255, 0.5), 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -403,7 +403,7 @@ mod tests {
|
||||
let faded = blend_color(base, original, 0.0);
|
||||
assert_eq!(faded, Some(Color::Rgb(0, 0, 0)));
|
||||
|
||||
// No change
|
||||
// Unchanged
|
||||
let unchanged = blend_color(base, original, 1.0);
|
||||
assert_eq!(unchanged, Some(Color::Rgb(100, 150, 200)));
|
||||
|
||||
@@ -415,8 +415,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_blend_color_indexed_returns_indexed() {
|
||||
// Both indexed → result is indexed (quantized back to 256-color palette)
|
||||
let base = Color::Indexed(232); // near-black (8, 8, 8)
|
||||
let original = Color::Indexed(255); // near-white (238, 238, 238)
|
||||
// near-black (8, 8, 8)
|
||||
let base = Color::Indexed(232);
|
||||
// near-white (238, 238, 238)
|
||||
let original = Color::Indexed(255);
|
||||
|
||||
let half = blend_color(base, original, 0.5).unwrap();
|
||||
assert!(matches!(half, Color::Indexed(_)));
|
||||
@@ -433,7 +435,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_blend_color_mixed_returns_indexed() {
|
||||
let rgb = Color::Rgb(100, 100, 100);
|
||||
let indexed = Color::Indexed(5); // magenta (128, 0, 128)
|
||||
// magenta (128, 0, 128)
|
||||
let indexed = Color::Indexed(5);
|
||||
|
||||
// Mixed: indexed base + rgb original → Indexed result (quantized)
|
||||
let result = blend_color(indexed, rgb, 0.5);
|
||||
@@ -483,8 +486,8 @@ mod tests {
|
||||
|
||||
// Check cells are faded
|
||||
if let Some(cell) = buf.cell((0, 0)) {
|
||||
assert_eq!(cell.fg, Color::Rgb(100, 100, 100)); // 200 * 0.5
|
||||
assert_eq!(cell.bg, Color::Rgb(25, 25, 25)); // 50 * 0.5
|
||||
assert_eq!(cell.fg, Color::Rgb(100, 100, 100));
|
||||
assert_eq!(cell.bg, Color::Rgb(25, 25, 25));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,14 +510,14 @@ mod tests {
|
||||
// Only fade a 2x2 region in the middle
|
||||
fade_region(&mut buf, Rect::new(1, 1, 2, 2), base, 0.0);
|
||||
|
||||
// Corner should be unchanged
|
||||
// Corner should be `unchanged`
|
||||
assert_eq!(buf.cell((0, 0)).unwrap().fg, Color::Rgb(100, 100, 100));
|
||||
|
||||
// Middle should be fully faded
|
||||
assert_eq!(buf.cell((1, 1)).unwrap().fg, Color::Rgb(0, 0, 0));
|
||||
assert_eq!(buf.cell((2, 2)).unwrap().fg, Color::Rgb(0, 0, 0));
|
||||
|
||||
// Other corner unchanged
|
||||
// Other corner `unchanged`
|
||||
assert_eq!(buf.cell((3, 3)).unwrap().fg, Color::Rgb(100, 100, 100));
|
||||
}
|
||||
|
||||
@@ -535,7 +538,7 @@ mod tests {
|
||||
|
||||
// fg blended to 50%
|
||||
assert_eq!(buf.cell((0, 0)).unwrap().fg, Color::Rgb(100, 50, 0));
|
||||
// bg unchanged
|
||||
// bg `unchanged`
|
||||
assert_eq!(buf.cell((0, 0)).unwrap().bg, Color::Rgb(10, 10, 10));
|
||||
}
|
||||
|
||||
@@ -554,7 +557,7 @@ mod tests {
|
||||
let target = Color::Rgb(0, 0, 0);
|
||||
blend_area(&mut buf, Rect::new(0, 0, 2, 1), None, Some((target, 0.5)));
|
||||
|
||||
// fg unchanged
|
||||
// fg `unchanged`
|
||||
assert_eq!(buf.cell((0, 0)).unwrap().fg, Color::Rgb(200, 200, 200));
|
||||
// bg blended to 50%
|
||||
assert_eq!(buf.cell((0, 0)).unwrap().bg, Color::Rgb(50, 50, 50));
|
||||
@@ -611,7 +614,8 @@ mod tests {
|
||||
fn test_blend_area_named_color_skipped() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
if let Some(cell) = buf.cell_mut((0, 0)) {
|
||||
cell.set_fg(Color::Red); // named color — blend_color returns None
|
||||
// named color — blend_color returns None
|
||||
cell.set_fg(Color::Red);
|
||||
cell.set_bg(Color::Red);
|
||||
}
|
||||
|
||||
@@ -622,7 +626,7 @@ mod tests {
|
||||
Some((Color::Rgb(0, 0, 0), 0.5)),
|
||||
);
|
||||
|
||||
// Named colors should be unchanged (blend_color returns None for them)
|
||||
// Named colors should be `unchanged` (blend_color returns None for them)
|
||||
assert_eq!(buf.cell((0, 0)).unwrap().fg, Color::Red);
|
||||
assert_eq!(buf.cell((0, 0)).unwrap().bg, Color::Red);
|
||||
}
|
||||
@@ -631,12 +635,14 @@ mod tests {
|
||||
fn test_blend_area_indexed_colors_blended() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
if let Some(cell) = buf.cell_mut((0, 0)) {
|
||||
cell.set_fg(Color::Indexed(255)); // near-white grayscale
|
||||
// near-white grayscale
|
||||
cell.set_fg(Color::Indexed(255));
|
||||
cell.set_bg(Color::Indexed(255));
|
||||
}
|
||||
|
||||
// Blend toward black (indexed 232 = #080808, but we use indexed 16 = #000000)
|
||||
let target = Color::Indexed(16); // black in the color cube
|
||||
// black in the color cube
|
||||
let target = Color::Indexed(16);
|
||||
blend_area(
|
||||
&mut buf,
|
||||
Rect::new(0, 0, 1, 1),
|
||||
|
||||
@@ -368,7 +368,7 @@ pub fn draw_frame(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// An unchanged frame must emit zero bytes to the PTY.
|
||||
/// An `unchanged` frame must emit zero bytes to the PTY.
|
||||
#[test]
|
||||
fn idle_frame_emits_zero_bytes() {
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
|
||||
@@ -12,8 +12,7 @@ use ratatui::widgets::{Block, BorderType, Borders, Widget};
|
||||
use crate::gboom::GboomHud;
|
||||
use crate::render::safe_buf::SafeBuf;
|
||||
|
||||
/// Render the GBOOM popup chrome. Returns the popup `Rect`,
|
||||
/// or `None` if the area is too small to play in.
|
||||
/// Returns the popup `Rect`, or `None` when the area is too small to play in.
|
||||
pub fn render_gboom_overlay(
|
||||
buf: &mut Buffer,
|
||||
area: Rect,
|
||||
@@ -28,7 +27,6 @@ pub fn render_gboom_overlay(
|
||||
|
||||
crate::render::color::dim_area(buf, area, bg, 0.5);
|
||||
|
||||
// 90% centered popup, like the video viewer.
|
||||
let popup_width = ((area.width as u32 * 90) / 100)
|
||||
.max(30)
|
||||
.min(area.width as u32) as u16;
|
||||
@@ -49,7 +47,6 @@ pub fn render_gboom_overlay(
|
||||
.style(Style::default().bg(bg))
|
||||
.render(popup_rect, buf);
|
||||
|
||||
// Title centered in the top border, in the iconic logo red.
|
||||
let title = " GBOOM ";
|
||||
let [r, g, b] = crate::gboom::GBOOM_RED;
|
||||
let title_style = Style::default()
|
||||
@@ -60,14 +57,13 @@ pub fn render_gboom_overlay(
|
||||
let tx = popup_rect.x + (popup_rect.width.saturating_sub(tw)) / 2;
|
||||
buf.set_span_safe(tx, popup_rect.y, &Span::styled(title, title_style), tw);
|
||||
|
||||
// HUD on the bottom border row.
|
||||
render_hud_bar(buf, popup_rect, hud, border_fg, bg);
|
||||
|
||||
Some(popup_rect)
|
||||
}
|
||||
|
||||
/// Render the HUD on the popup's bottom border row:
|
||||
/// `HP 100 · KILLS 0/8` left, controls hint right.
|
||||
/// Overwrites the popup's bottom border row: `HP 100 · KILLS 0/8` on the left,
|
||||
/// controls hint on the right.
|
||||
fn render_hud_bar(buf: &mut Buffer, popup_rect: Rect, hud: &GboomHud, dim_fg: Color, bg: Color) {
|
||||
let bar_y = popup_rect.y + popup_rect.height.saturating_sub(1);
|
||||
let inner_width = popup_rect.width.saturating_sub(2) as usize;
|
||||
@@ -75,8 +71,7 @@ fn render_hud_bar(buf: &mut Buffer, popup_rect: Rect, hud: &GboomHud, dim_fg: Co
|
||||
return;
|
||||
}
|
||||
|
||||
// Health-bar semantics: green when comfortable, amber when hurting,
|
||||
// GBOOM red when critical.
|
||||
// Green when comfortable, amber when hurting, GBOOM red when critical.
|
||||
let hp_color = if hud.hp > 60 {
|
||||
Color::Rgb(126, 200, 96)
|
||||
} else if hud.hp > 30 {
|
||||
|
||||
@@ -70,8 +70,8 @@ pub fn paint_match_highlights(
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the terminal's REVERSED attribute so the fg/bg swap is native and
|
||||
/// respects the user's theme.
|
||||
/// REVERSED leaves the fg/bg swap to the terminal, so highlights follow the
|
||||
/// user's theme instead of hardcoded colors.
|
||||
fn invert_cell(cell: &mut ratatui::buffer::Cell) {
|
||||
cell.modifier.insert(ratatui::style::Modifier::REVERSED);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ pub(super) fn paint_path_line(
|
||||
bg: Color,
|
||||
) {
|
||||
let raw = path.display().to_string();
|
||||
// The 6 reserved columns are the "Path: " prefix.
|
||||
let label = format!(
|
||||
"Path: {}",
|
||||
truncate_path_for_overlay(&raw, width.saturating_sub(6) as usize)
|
||||
|
||||
@@ -109,7 +109,7 @@ pub fn truncate_str(s: &str, max_width: usize) -> String {
|
||||
/// exhausted mid-span, that span is truncated and `…` is appended. Spans
|
||||
/// beyond the budget are dropped. All styles are preserved.
|
||||
///
|
||||
/// Returns the line unchanged if it already fits.
|
||||
/// Returns the line `unchanged` if it already fits.
|
||||
pub fn truncate_line(line: Line<'static>, max_width: usize) -> Line<'static> {
|
||||
if max_width == 0 {
|
||||
return Line::from(vec![]);
|
||||
@@ -152,7 +152,7 @@ pub fn truncate_line(line: Line<'static>, max_width: usize) -> Line<'static> {
|
||||
/// Clip or pad a styled `Line` to exactly `width` display columns.
|
||||
///
|
||||
/// Wider lines are clipped on grapheme boundaries (a multi-`char` grapheme like
|
||||
/// `⚠\u{FE0F}` is never split) with no ellipsis; narrower lines are padded with
|
||||
/// `⚠\u{FE0F}` is never split) with no ellipsis; narrower lines are `padded` with
|
||||
/// trailing spaces. This keeps a rendered row "self-owning" — the app writes a
|
||||
/// real cell in every column, so a terminal drawing a glyph wider than the app
|
||||
/// measured cannot strand a stale cell past the row (the markdown-table ghost
|
||||
@@ -346,10 +346,11 @@ mod tests {
|
||||
let s = "hello — world";
|
||||
let result = truncate_str(s, 8);
|
||||
assert!(result.ends_with('…'));
|
||||
assert!(result.len() <= 12); // safe byte length
|
||||
// safe byte length
|
||||
assert!(result.len() <= 12);
|
||||
}
|
||||
|
||||
// ── truncate_line tests ─────────────────────────────────────────
|
||||
// truncate_line tests
|
||||
|
||||
#[test]
|
||||
fn truncate_line_fits() {
|
||||
@@ -395,7 +396,7 @@ mod tests {
|
||||
assert!(result.spans.is_empty());
|
||||
}
|
||||
|
||||
// ── fit_line_to_width tests ─────────────────────────────────────
|
||||
// fit_line_to_width tests
|
||||
|
||||
fn line_text(line: &Line<'static>) -> String {
|
||||
line.spans.iter().map(|s| s.content.as_ref()).collect()
|
||||
@@ -438,7 +439,7 @@ mod tests {
|
||||
#[test]
|
||||
fn fit_line_clips_grapheme_straddle_in_later_span() {
|
||||
// The straddle happens in a later span: keep "ab", then 1 col left →
|
||||
// ⚠️ (width 2) won't fit → dropped whole and padded.
|
||||
// ⚠️ (width 2) won't fit → dropped whole and `padded`.
|
||||
let line = Line::from(vec![Span::raw("ab"), Span::raw("\u{26A0}\u{FE0F}cd")]);
|
||||
let out = fit_line_to_width(line, 3);
|
||||
assert_eq!(line_text(&out).width(), 3);
|
||||
@@ -502,7 +503,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── cascade_truncate tests ────────────────────────────────────
|
||||
// cascade_truncate tests
|
||||
|
||||
#[test]
|
||||
fn cascade_truncate_all_fit() {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
//! Low-level rendering utilities.
|
||||
//!
|
||||
//! Generic rendering primitives used by the scrollback and viewport.
|
||||
//! Low-level rendering primitives shared by the scrollback and viewport.
|
||||
pub mod color;
|
||||
pub mod draw;
|
||||
pub mod gboom_overlay;
|
||||
|
||||
@@ -48,7 +48,8 @@ impl LinkOverlay {
|
||||
link.col_end
|
||||
);
|
||||
if link.col_start > link.col_end {
|
||||
return; // Silently skip inverted ranges in release mode.
|
||||
// Silently skip inverted ranges in release mode.
|
||||
return;
|
||||
}
|
||||
self.links.push(link);
|
||||
}
|
||||
@@ -541,7 +542,7 @@ mod tests {
|
||||
scan_lines_for_url_overlays(rows.into_iter(), content_x, media_paths, overlay);
|
||||
}
|
||||
|
||||
// ── local_link_to_file_url ──
|
||||
// local_link_to_file_url
|
||||
|
||||
#[test]
|
||||
fn local_link_relative_resolves_to_generated_media() {
|
||||
@@ -592,7 +593,7 @@ mod tests {
|
||||
assert!(local_link_to_file_url("../images/1.jpg", &media).is_none());
|
||||
}
|
||||
|
||||
// ── tool_path_file_url ──
|
||||
// tool_path_file_url
|
||||
|
||||
#[test]
|
||||
fn tool_path_file_url_resolves_relative_against_cwd() {
|
||||
@@ -637,7 +638,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── LinkOverlay ──
|
||||
// LinkOverlay
|
||||
|
||||
#[test]
|
||||
fn overlay_empty_by_default() {
|
||||
@@ -661,7 +662,7 @@ mod tests {
|
||||
assert_eq!(overlay.links()[0].screen_row, 5);
|
||||
}
|
||||
|
||||
// ── scan_lines_for_url_overlays ──
|
||||
// scan_lines_for_url_overlays
|
||||
|
||||
use ratatui::text::{Line as RLine, Span as RSpan};
|
||||
|
||||
@@ -690,7 +691,8 @@ mod tests {
|
||||
assert_eq!(link.screen_row, 5);
|
||||
// "See " = 4 display cols, content_x = 2
|
||||
assert_eq!(link.col_start, 6);
|
||||
assert_eq!(link.col_end, 6 + 19); // "https://example.com" = 19 chars
|
||||
// "https://example.com" = 19 chars
|
||||
assert_eq!(link.col_end, 6 + 19);
|
||||
assert_eq!(link.id, None);
|
||||
}
|
||||
|
||||
@@ -796,7 +798,7 @@ mod tests {
|
||||
assert_eq!(overlay.links()[0].col_end, 10 + 16);
|
||||
}
|
||||
|
||||
// ── File path detection ──
|
||||
// File path detection
|
||||
|
||||
#[test]
|
||||
fn scan_detects_absolute_file_path() {
|
||||
@@ -875,7 +877,7 @@ mod tests {
|
||||
#[test]
|
||||
fn scan_detects_media_path_soft_wrapped_across_rows() {
|
||||
// Regression: media-tool output prose wraps the long session path
|
||||
// across visual rows (`joiner: Some("")` mid-word break). Previously
|
||||
// across visual rows (`joiner: Some("")` mid-word break). earlier
|
||||
// each row was scanned in isolation, so only the `/Users/alice`
|
||||
// fragment on the first row matched and became clickable.
|
||||
let row0 =
|
||||
@@ -1162,7 +1164,7 @@ mod tests {
|
||||
&*overlay.links()[0].url,
|
||||
"file:///tmp/release/Demo%20App.app"
|
||||
);
|
||||
assert_eq!(overlay.links()[0].col_start, 5); // "open "
|
||||
assert_eq!(overlay.links()[0].col_start, 5);
|
||||
assert_eq!(
|
||||
overlay.links()[0].col_end,
|
||||
5 + UnicodeWidthStr::width(path) as u16
|
||||
@@ -1184,7 +1186,7 @@ mod tests {
|
||||
assert_eq!(overlay.links()[0].col_end, 4 + 12);
|
||||
}
|
||||
|
||||
// ── Home-relative (`~/`) path detection ──
|
||||
// Home-relative (`~/`) path detection
|
||||
|
||||
#[test]
|
||||
fn scan_detects_tilde_file_path() {
|
||||
@@ -1277,7 +1279,7 @@ mod tests {
|
||||
id: None,
|
||||
});
|
||||
assert!(overlay.overlaps(5, 10, 20));
|
||||
assert!(!overlay.overlaps(6, 10, 20)); // different row
|
||||
assert!(!overlay.overlaps(6, 10, 20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1290,10 +1292,11 @@ mod tests {
|
||||
url: Arc::from("https://a.example"),
|
||||
id: None,
|
||||
});
|
||||
assert!(overlay.overlaps(0, 15, 25)); // right overlap
|
||||
assert!(overlay.overlaps(0, 5, 15)); // left overlap
|
||||
assert!(!overlay.overlaps(0, 20, 30)); // adjacent, no overlap
|
||||
assert!(!overlay.overlaps(0, 0, 10)); // adjacent left
|
||||
assert!(overlay.overlaps(0, 15, 25));
|
||||
assert!(overlay.overlaps(0, 5, 15));
|
||||
// adjacent, no overlap
|
||||
assert!(!overlay.overlaps(0, 20, 30));
|
||||
assert!(!overlay.overlaps(0, 0, 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -17,9 +17,7 @@ use ratatui::widgets::{Block, BorderType, Borders, Clear, Widget};
|
||||
use super::line_utils::{truncate_line, truncate_str};
|
||||
use super::safe_buf::SafeBuf;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PreviewStyle — configurable colors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Visual styling for the preview overlay.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -43,9 +41,7 @@ impl PreviewStyle {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PreviewConfig — layout configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Layout configuration for the preview overlay.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -89,9 +85,7 @@ impl Default for PreviewConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// render_preview_overlay — main rendering function
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Render a multiline preview overlay.
|
||||
///
|
||||
@@ -129,7 +123,8 @@ pub fn render_preview_overlay(
|
||||
// Calculate content layout
|
||||
let needs_dots = total > config.preview_lines * 2;
|
||||
let content_lines: usize = if needs_dots {
|
||||
config.preview_lines * 2 + 1 // top + dots + bottom
|
||||
// top + dots + bottom
|
||||
config.preview_lines * 2 + 1
|
||||
} else {
|
||||
total
|
||||
};
|
||||
@@ -255,7 +250,7 @@ fn render_line(buf: &mut Buffer, x: u16, y: u16, width: u16, line: &str, style:
|
||||
}
|
||||
|
||||
/// Paint the hint into the bottom border row, left-aligned after the
|
||||
/// corner and one dash, padded with a space on each side so the text
|
||||
/// corner and one dash, `padded` with a space on each side so the text
|
||||
/// stands off the dashes: `╰─ enter to expand ────╯`. The corners and
|
||||
/// one dash per side are never overwritten. Skipped entirely when the
|
||||
/// box is too narrow for readable text.
|
||||
@@ -283,9 +278,7 @@ fn render_border_hint(buf: &mut Buffer, box_area: Rect, hint: &Line<'static>, bg
|
||||
buf.set_line_safe(box_area.x + 2, y, &Line::from(spans), box_area.width - 4);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -293,9 +286,12 @@ mod tests {
|
||||
|
||||
fn test_style() -> PreviewStyle {
|
||||
PreviewStyle::new(
|
||||
Color::Indexed(234), // grayscale 28 — dark bg
|
||||
Color::Indexed(189), // (215,215,255) — light text
|
||||
Color::Indexed(60), // (95,95,135) — dim border
|
||||
// grayscale 28 — dark bg
|
||||
Color::Indexed(234),
|
||||
// (215,215,255) — light text
|
||||
Color::Indexed(189),
|
||||
// (95,95,135) — dim border
|
||||
Color::Indexed(60),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -317,7 +313,8 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let result = render_preview_overlay(
|
||||
&mut buf,
|
||||
Rect::new(0, 0, 10, 3), // below min_height=5
|
||||
// below min_height=5
|
||||
Rect::new(0, 0, 10, 3),
|
||||
"hello\nworld",
|
||||
test_style(),
|
||||
PreviewConfig::default(),
|
||||
@@ -435,7 +432,7 @@ mod tests {
|
||||
);
|
||||
assert!(result.is_some());
|
||||
let rect = result.unwrap();
|
||||
assert_eq!(rect.width, 50); // 100 * 0.5 = 50
|
||||
assert_eq!(rect.width, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -57,9 +57,7 @@ impl<'a> From<Box<dyn Renderable + 'a>> for RenderableItem<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Standard Implementations
|
||||
// ============================================================================
|
||||
|
||||
/// Unit type renders as nothing (0 height).
|
||||
impl Renderable for () {
|
||||
|
||||
@@ -6,25 +6,19 @@
|
||||
//! a crash.
|
||||
//!
|
||||
//! This extension trait provides `set_line_safe` / `set_span_safe` /
|
||||
//! `set_string_safe` that silently skip the write when `y` is outside the
|
||||
//! buffer — trading a single missed frame for a panic-free resize.
|
||||
//! `set_string_safe`, which skip the write when the target row lies outside
|
||||
//! the buffer or `x` is past its right edge — trading a single missed frame
|
||||
//! for a panic-free resize.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::{Line, Span};
|
||||
|
||||
/// Extension trait for bounds-checked buffer writes.
|
||||
pub trait SafeBuf {
|
||||
/// Like `Buffer::set_line` but returns immediately when `y` is outside
|
||||
/// the buffer area.
|
||||
fn set_line_safe(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16);
|
||||
|
||||
/// Like `Buffer::set_span` but returns immediately when `y` is outside
|
||||
/// the buffer area.
|
||||
fn set_span_safe(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16);
|
||||
|
||||
/// Like `Buffer::set_string` but returns immediately when `y` is outside
|
||||
/// the buffer area.
|
||||
fn set_string_safe<S: AsRef<str>>(&mut self, x: u16, y: u16, string: S, style: Style);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ pub const SCROLLBAR_TOTAL_COLS: u16 = SCROLLBAR_GAP_COLS + SCROLLBAR_TRACK_COLS;
|
||||
/// Layout:
|
||||
/// - `content_area`: original area minus [`SCROLLBAR_TOTAL_COLS`] on the right
|
||||
/// - `scrollbar_area`: the last column of the original area (1 cell wide)
|
||||
/// - The column between them is the "gap" (left intentionally blank)
|
||||
/// - The column between them is the "gap" (left deliberately blank)
|
||||
///
|
||||
/// Returns `(content_area, None)` when the terminal is too narrow.
|
||||
///
|
||||
@@ -126,7 +126,8 @@ pub fn needs_scrollbar(total_lines: u16, viewport_lines: u16) -> bool {
|
||||
}
|
||||
|
||||
/// Whether the view is at the bottom (following mode position).
|
||||
#[allow(dead_code)] // Useful helper, kept for future use
|
||||
// Useful helper, kept for future use
|
||||
#[allow(dead_code)]
|
||||
pub fn is_at_bottom(total_lines: u16, viewport_lines: u16, offset: u16) -> bool {
|
||||
let max_offset = total_lines.saturating_sub(viewport_lines);
|
||||
offset >= max_offset
|
||||
@@ -384,7 +385,8 @@ mod tests {
|
||||
|
||||
// Content overflows (20 > 10) - should reserve scrollbar space
|
||||
let (content, scrollbar) = maybe_split_for_scrollbar(area, 20);
|
||||
assert_eq!(content.width, 38); // Reduced by 2 for gap + scrollbar track
|
||||
// Reduced by 2 for gap + scrollbar track
|
||||
assert_eq!(content.width, 38);
|
||||
assert!(scrollbar.is_some());
|
||||
}
|
||||
|
||||
@@ -394,24 +396,29 @@ mod tests {
|
||||
|
||||
// Content fits (5 <= 10) - should give full width to content
|
||||
let (content, scrollbar) = maybe_split_for_scrollbar(area, 5);
|
||||
assert_eq!(content.width, 40); // Full width
|
||||
assert_eq!(content.width, 40);
|
||||
assert!(scrollbar.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_needs_scrollbar() {
|
||||
assert!(needs_scrollbar(100, 10)); // Content > viewport
|
||||
assert!(!needs_scrollbar(10, 10)); // Content == viewport
|
||||
assert!(!needs_scrollbar(5, 10)); // Content < viewport
|
||||
// Content > viewport
|
||||
assert!(needs_scrollbar(100, 10));
|
||||
// Content == viewport
|
||||
assert!(!needs_scrollbar(10, 10));
|
||||
// Content < viewport
|
||||
assert!(!needs_scrollbar(5, 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_at_bottom() {
|
||||
// total=100, viewport=10 -> max_offset=90
|
||||
assert!(is_at_bottom(100, 10, 90)); // At bottom
|
||||
assert!(is_at_bottom(100, 10, 95)); // Past bottom (clamped)
|
||||
assert!(!is_at_bottom(100, 10, 89)); // One line above bottom
|
||||
assert!(!is_at_bottom(100, 10, 0)); // At top
|
||||
assert!(is_at_bottom(100, 10, 90));
|
||||
// Past bottom (clamped)
|
||||
assert!(is_at_bottom(100, 10, 95));
|
||||
// One line above bottom
|
||||
assert!(!is_at_bottom(100, 10, 89));
|
||||
assert!(!is_at_bottom(100, 10, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -258,7 +258,6 @@ fn first_param(params: &Params, default: u16) -> u16 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a 0-7 ANSI color index to a named ratatui color.
|
||||
fn ansi16(n: u16) -> Color {
|
||||
match n {
|
||||
0 => Color::Black,
|
||||
@@ -272,7 +271,6 @@ fn ansi16(n: u16) -> Color {
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a 0-7 bright ANSI color index to a named ratatui color.
|
||||
fn ansi16_bright(n: u16) -> Color {
|
||||
match n {
|
||||
0 => Color::DarkGray,
|
||||
|
||||
@@ -7,7 +7,6 @@ use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::line_utils::truncate_str;
|
||||
|
||||
/// Read/Edit tool-header path paint surface.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ToolPathSurface {
|
||||
/// Basename only.
|
||||
@@ -183,7 +182,6 @@ pub fn path_basename(path: &str, budget: usize) -> String {
|
||||
truncate_str(name, budget)
|
||||
}
|
||||
|
||||
/// Compatibility formatter: compact basename with `Some(width)`, else stored path.
|
||||
pub fn path_for_tool_header(path: &str, width: Option<usize>, reserved: usize) -> String {
|
||||
match width {
|
||||
Some(width) => path_basename(path, width.saturating_sub(reserved)),
|
||||
@@ -191,7 +189,6 @@ pub fn path_for_tool_header(path: &str, width: Option<usize>, reserved: usize) -
|
||||
}
|
||||
}
|
||||
|
||||
/// Path text for a Read/Edit tool-header surface.
|
||||
pub fn path_for_tool_surface(
|
||||
path: &str,
|
||||
surface: ToolPathSurface,
|
||||
|
||||
@@ -12,8 +12,6 @@ use ratatui::widgets::{Block, BorderType, Borders, Widget};
|
||||
use crate::prompt_images::VideoViewerState;
|
||||
use crate::render::safe_buf::SafeBuf;
|
||||
|
||||
/// Render the video viewer popup chrome. Returns the popup `Rect`,
|
||||
/// or `None` if the area is too small.
|
||||
pub fn render_video_overlay(
|
||||
buf: &mut Buffer,
|
||||
area: Rect,
|
||||
@@ -28,7 +26,6 @@ pub fn render_video_overlay(
|
||||
|
||||
crate::render::color::dim_area(buf, area, bg, 0.5);
|
||||
|
||||
// 90% centered popup.
|
||||
let popup_width = ((area.width as u32 * 90) / 100)
|
||||
.max(28)
|
||||
.min(area.width as u32) as u16;
|
||||
@@ -49,7 +46,6 @@ pub fn render_video_overlay(
|
||||
.style(Style::default().bg(bg))
|
||||
.render(popup_rect, buf);
|
||||
|
||||
// Title centered in top border.
|
||||
let title = match viewer.title {
|
||||
Some(ref name) => format!(
|
||||
" {} ({}\u{00d7}{}) ",
|
||||
@@ -68,13 +64,11 @@ pub fn render_video_overlay(
|
||||
let tx = popup_rect.x + (popup_rect.width.saturating_sub(tw)) / 2;
|
||||
buf.set_span_safe(tx, popup_rect.y, &Span::styled(&title, title_style), tw);
|
||||
|
||||
// Progress bar on the bottom border row.
|
||||
render_progress_bar(buf, popup_rect, viewer, text_fg, border_fg, bg);
|
||||
|
||||
Some(popup_rect)
|
||||
}
|
||||
|
||||
/// Render the progress bar on the popup's bottom border row.
|
||||
fn render_progress_bar(
|
||||
buf: &mut Buffer,
|
||||
popup_rect: Rect,
|
||||
|
||||
@@ -114,7 +114,8 @@ pub(crate) fn byte_offset_to_display_col(text: &str, byte_offset: usize) -> usiz
|
||||
/// forward. If a future change to the wrapping pipeline breaks this
|
||||
/// invariant, consider switching to a two-stage approach that mirrors
|
||||
/// `word_wrap_line_with_joiners` exactly.
|
||||
#[allow(clippy::single_range_in_vec_init)] // intentional: single range = full text, no wrapping
|
||||
// intentional: single range = full text, no wrapping
|
||||
#[allow(clippy::single_range_in_vec_init)]
|
||||
pub fn wrap_byte_ranges_matching(text: &str, width: usize) -> Vec<Range<usize>> {
|
||||
if width == 0 || text.is_empty() {
|
||||
return vec![0..text.len()];
|
||||
@@ -362,7 +363,7 @@ fn is_table_line(line: &Line<'_>) -> bool {
|
||||
/// this wrap layer re-injecting the prefix spans (with their styles) on
|
||||
/// continuation rows, so keep the two shapes in agreement.
|
||||
fn blockquote_prefix_len(flat: &str) -> usize {
|
||||
const BAR_BYTES: usize = '\u{2502}'.len_utf8(); // 3
|
||||
const BAR_BYTES: usize = '\u{2502}'.len_utf8();
|
||||
let mut len = 0;
|
||||
let mut chars = flat.chars();
|
||||
while let Some('\u{2502}') = chars.next() {
|
||||
@@ -1187,14 +1188,15 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- byte_range_to_row_cols tests -----------------------------------------
|
||||
// byte_range_to_row_cols tests
|
||||
|
||||
#[test]
|
||||
fn highlight_single_row_match() {
|
||||
// "hello world" on one row (no wrapping).
|
||||
let text = "hello world";
|
||||
let ranges = vec![0..11]; // one row, full text
|
||||
let segments = byte_range_to_row_cols(text, &ranges, 6..11); // "world"
|
||||
// one row, full text
|
||||
let ranges = vec![0..11];
|
||||
let segments = byte_range_to_row_cols(text, &ranges, 6..11);
|
||||
assert_eq!(
|
||||
segments,
|
||||
vec![HighlightSegment {
|
||||
@@ -1306,8 +1308,9 @@ mod tests {
|
||||
// Byte layout: a(1) b(1) —(3) c(1) d(1) = 7 bytes total.
|
||||
// Display: a(0) b(1) —(2) c(3) d(4) = 5 display columns.
|
||||
let text = "ab\u{2014}cd";
|
||||
assert_eq!(text.len(), 7); // 2 + 3 + 2 bytes
|
||||
let ranges = vec![0..7]; // one row
|
||||
// 2 + 3 + 2 bytes
|
||||
assert_eq!(text.len(), 7);
|
||||
let ranges = vec![0..7];
|
||||
// Match "cd" = bytes 5..7, display cols 3..5.
|
||||
let segments = byte_range_to_row_cols(text, &ranges, 5..7);
|
||||
assert_eq!(
|
||||
@@ -1340,7 +1343,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// -- Table line detection / no-wrap tests ----------------------------------
|
||||
// Table line detection / no-wrap tests
|
||||
|
||||
#[test]
|
||||
fn table_line_box_drawing_not_wrapped() {
|
||||
@@ -1362,14 +1365,15 @@ mod tests {
|
||||
assert_eq!(joiners, vec![None]);
|
||||
}
|
||||
|
||||
/// Regression: a table row narrower than the content width must be padded
|
||||
/// Regression: a table row narrower than the content width must be `padded`
|
||||
/// so the app owns every column (otherwise a wide-glyph width disagreement
|
||||
/// strands a ghost cell).
|
||||
#[test]
|
||||
fn table_row_padded_to_content_width() {
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
let line = Line::from("│ Status │ Note │"); // 23 display columns
|
||||
// 23 display columns
|
||||
let line = Line::from("│ Status │ Note │");
|
||||
assert_eq!(concat_line(&line).width(), 23);
|
||||
|
||||
let content_width = 40;
|
||||
@@ -1390,7 +1394,7 @@ mod tests {
|
||||
|
||||
/// Faithful repro: a body row with an emoji-presentation sequence
|
||||
/// (`⚠\u{FE0F}`) and an em-dash — the glyphs that desynced the cursor — must
|
||||
/// be padded to exactly the content width.
|
||||
/// be `padded` to exactly the content width.
|
||||
#[test]
|
||||
fn table_row_with_emoji_and_em_dash_fills_content_width() {
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
Reference in new issue
Block a user