M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.
Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
ptyctl, ptyctl-cli, third_party/ unchanged; proto package
xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
(templates re-encrypted)
Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
module & dc_log, heap-profile uploader, auth-diagnostics uploader,
session-analytics halves of feedback; local zero-egress observability
preserved in new kigi-log crate (unified log, --debug firehose,
subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
shell util
Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted
Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean
Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
(new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
fast-worktree); RSS measurement tests serialized via serial_test
Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
notices sustained; kigi-tools ported-code notices extended; README,
CONTRIBUTING, SECURITY, AGENTS.md rewritten
Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
This commit is contained in:
commit
d6c20fc13f
2612 files changed
+1353757
No files matched your search
@@ -0,0 +1,605 @@
|
||||
//! Line and string utility functions for ratatui text manipulation.
|
||||
|
||||
use ratatui::text::{Line, Span};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
pub use super::tool_paths::{path_basename, path_for_tool_header, shorten_path};
|
||||
|
||||
/// Clone a borrowed ratatui `Line` into an owned `'static` line.
|
||||
pub fn line_to_static(line: &Line<'_>) -> Line<'static> {
|
||||
Line {
|
||||
style: line.style,
|
||||
alignment: line.alignment,
|
||||
spans: line
|
||||
.spans
|
||||
.iter()
|
||||
.map(|s| Span {
|
||||
style: s.style,
|
||||
content: std::borrow::Cow::Owned(s.content.to_string()),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Append owned copies of borrowed lines to `out`.
|
||||
pub fn push_owned_lines(src: &[Line<'_>], out: &mut Vec<Line<'static>>) {
|
||||
for l in src {
|
||||
out.push(line_to_static(l));
|
||||
}
|
||||
}
|
||||
|
||||
/// True for a character unsafe to render from untrusted/server text:
|
||||
/// C0/C1 controls (the terminal-escape-injection vector) plus the Unicode
|
||||
/// bidi-control and zero-width/format set (Trojan-Source spoofing) — U+061C,
|
||||
/// U+200B–200F, U+202A–202E, U+2060–206F, U+FEFF.
|
||||
///
|
||||
/// Shared by every untrusted-text strip/scrub site (chip labels, toast error
|
||||
/// scrub, settings editor input) so the set never drifts between them.
|
||||
pub fn is_unsafe_display_char(c: char) -> bool {
|
||||
c.is_control()
|
||||
|| matches!(
|
||||
c,
|
||||
'\u{061C}'
|
||||
| '\u{200B}'..='\u{200F}'
|
||||
| '\u{202A}'..='\u{202E}'
|
||||
| '\u{2060}'..='\u{206F}'
|
||||
| '\u{FEFF}'
|
||||
)
|
||||
}
|
||||
|
||||
/// Polyfill for nightly-only [`str::floor_char_boundary`].
|
||||
/// Snaps a byte index down to the nearest char boundary.
|
||||
pub fn floor_char_boundary(s: &str, index: usize) -> usize {
|
||||
let index = index.min(s.len());
|
||||
let mut i = index;
|
||||
while i > 0 && !s.is_char_boundary(i) {
|
||||
i -= 1;
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
/// Byte offset at which cumulative display width exceeds `max_width`.
|
||||
/// Returns `s.len()` when the entire string fits.
|
||||
pub fn byte_offset_at_width(s: &str, max_width: usize) -> usize {
|
||||
let mut width = 0;
|
||||
for (i, ch) in s.char_indices() {
|
||||
let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
|
||||
if width + cw > max_width {
|
||||
return i;
|
||||
}
|
||||
width += cw;
|
||||
}
|
||||
s.len()
|
||||
}
|
||||
|
||||
/// Truncate a string to fit within `max_width` display columns.
|
||||
///
|
||||
/// Uses Unicode-aware width measurement (handles CJK wide chars,
|
||||
/// multi-byte UTF-8 like em-dash, etc.). If truncated, the last character
|
||||
/// is replaced with `…` so the result fits within `max_width`.
|
||||
///
|
||||
/// Returns the original string (owned) if it already fits.
|
||||
pub fn truncate_str(s: &str, max_width: usize) -> String {
|
||||
if max_width == 0 {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let end = byte_offset_at_width(s, max_width);
|
||||
let needs_ellipsis = end < s.len();
|
||||
|
||||
if needs_ellipsis && max_width > 1 {
|
||||
// Back up one char to make room for '…' (1 display column).
|
||||
let truncated_end = s[..end]
|
||||
.char_indices()
|
||||
.next_back()
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0);
|
||||
format!("{}…", &s[..truncated_end])
|
||||
} else if needs_ellipsis {
|
||||
"…".to_string()
|
||||
} else {
|
||||
s[..end].to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Truncate a styled `Line` (multiple spans) to fit within `max_width` display columns.
|
||||
///
|
||||
/// Walks spans left-to-right, consuming width budget. When the budget is
|
||||
/// 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.
|
||||
pub fn truncate_line(line: Line<'static>, max_width: usize) -> Line<'static> {
|
||||
if max_width == 0 {
|
||||
return Line::from(vec![]);
|
||||
}
|
||||
|
||||
let total: usize = line.spans.iter().map(|s| s.content.width()).sum();
|
||||
if total <= max_width {
|
||||
return line;
|
||||
}
|
||||
|
||||
// Need room for the ellipsis (1 column).
|
||||
let budget = max_width.saturating_sub(1);
|
||||
let mut used = 0usize;
|
||||
let mut out: Vec<Span<'static>> = Vec::new();
|
||||
|
||||
for span in line.spans {
|
||||
let sw = span.content.width();
|
||||
if used + sw <= budget {
|
||||
// Entire span fits.
|
||||
used += sw;
|
||||
out.push(span);
|
||||
} else {
|
||||
// Partial fit — truncate this span.
|
||||
let remaining = budget - used;
|
||||
if remaining > 0 {
|
||||
let truncated = take_width(&span.content, remaining);
|
||||
out.push(Span::styled(truncated, span.style));
|
||||
}
|
||||
// Append ellipsis with the same style as the last span.
|
||||
let ellipsis_style = out.last().map(|s| s.style).unwrap_or_default();
|
||||
out.push(Span::styled("\u{2026}", ellipsis_style));
|
||||
return Line::from(out);
|
||||
}
|
||||
}
|
||||
|
||||
// Shouldn't reach here (total > max_width checked above), but be safe.
|
||||
Line::from(out)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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
|
||||
/// glyph bug). Width uses [`UnicodeWidthStr`], matching the table layout.
|
||||
///
|
||||
/// `width` must be a bounded display width: the pad branch allocates
|
||||
/// `width - total` spaces.
|
||||
pub fn fit_line_to_width<'a>(line: Line<'a>, width: usize) -> Line<'a> {
|
||||
let total: usize = line.spans.iter().map(|s| s.content.width()).sum();
|
||||
if total == width {
|
||||
return line;
|
||||
}
|
||||
|
||||
let Line {
|
||||
style,
|
||||
alignment,
|
||||
mut spans,
|
||||
} = line;
|
||||
|
||||
if total < width {
|
||||
spans.push(Span::raw(" ".repeat(width - total)));
|
||||
return Line {
|
||||
style,
|
||||
alignment,
|
||||
spans,
|
||||
};
|
||||
}
|
||||
|
||||
// Wider than width: clip on grapheme boundaries, no ellipsis.
|
||||
let mut out: Vec<Span<'a>> = Vec::new();
|
||||
let mut used = 0usize;
|
||||
for span in spans {
|
||||
let sw = span.content.width();
|
||||
if used + sw <= width {
|
||||
used += sw;
|
||||
out.push(span);
|
||||
if used == width {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// This span straddles the boundary — take whole graphemes that fit.
|
||||
let remaining = width - used;
|
||||
let mut taken = String::new();
|
||||
let mut taken_width = 0usize;
|
||||
for g in span.content.graphemes(true) {
|
||||
let gw = g.width();
|
||||
if taken_width + gw > remaining {
|
||||
break;
|
||||
}
|
||||
taken_width += gw;
|
||||
taken.push_str(g);
|
||||
}
|
||||
if !taken.is_empty() {
|
||||
out.push(Span::styled(taken, span.style));
|
||||
used += taken_width;
|
||||
}
|
||||
// A straddling wide grapheme leaves a 1-column gap; pad it.
|
||||
if used < width {
|
||||
out.push(Span::raw(" ".repeat(width - used)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Line {
|
||||
style,
|
||||
alignment,
|
||||
spans: out,
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the first `n` display columns from a string.
|
||||
fn take_width(s: &str, n: usize) -> String {
|
||||
let mut width = 0;
|
||||
let mut end = s.len();
|
||||
for (i, ch) in s.char_indices() {
|
||||
let cw = ch.width().unwrap_or(0);
|
||||
if width + cw > n {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
width += cw;
|
||||
}
|
||||
s[..end].to_string()
|
||||
}
|
||||
|
||||
/// Cascade-truncate multiple text elements to fit within `avail` display columns.
|
||||
///
|
||||
/// Returns `(type, description, activity, meta)` truncated to fit.
|
||||
/// Priority (highest first): type, activity, meta. Description is truncated
|
||||
/// first. If overhead (type + activity + meta) >= avail, description is dropped
|
||||
/// and the remaining elements are cascaded: meta is dropped first, then
|
||||
/// activity is truncated, then type.
|
||||
pub fn cascade_truncate(
|
||||
avail: usize,
|
||||
type_text: &str,
|
||||
description: &str,
|
||||
activity_text: &str,
|
||||
meta_text: &str,
|
||||
) -> (String, String, String, String) {
|
||||
let overhead = type_text.width() + activity_text.width() + meta_text.width();
|
||||
if overhead <= avail {
|
||||
let desc_max = avail - overhead;
|
||||
(
|
||||
type_text.to_string(),
|
||||
truncate_str(description, desc_max),
|
||||
activity_text.to_string(),
|
||||
meta_text.to_string(),
|
||||
)
|
||||
} else {
|
||||
let mut budget = avail;
|
||||
let td = if type_text.width() <= budget {
|
||||
budget -= type_text.width();
|
||||
type_text.to_string()
|
||||
} else {
|
||||
let s = truncate_str(type_text, budget);
|
||||
budget = 0;
|
||||
s
|
||||
};
|
||||
let ad = if budget == 0 {
|
||||
String::new()
|
||||
} else if activity_text.width() <= budget {
|
||||
budget -= activity_text.width();
|
||||
activity_text.to_string()
|
||||
} else {
|
||||
let s = truncate_str(activity_text, budget);
|
||||
budget = 0;
|
||||
s
|
||||
};
|
||||
let md = if budget == 0 {
|
||||
String::new()
|
||||
} else if meta_text.width() <= budget {
|
||||
meta_text.to_string()
|
||||
} else {
|
||||
truncate_str(meta_text, budget)
|
||||
};
|
||||
(td, String::new(), ad, md)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_unsafe_display_char_covers_controls_and_bidi_format() {
|
||||
// Safe: ordinary printable text (incl. legitimate RTL letters).
|
||||
for c in ['a', ' ', '/', '\u{00e9}', '\u{05d0}'] {
|
||||
assert!(!is_unsafe_display_char(c), "{c:?} must be safe");
|
||||
}
|
||||
// Unsafe: C0/C1 controls + the full bidi-control / zero-width set.
|
||||
for c in [
|
||||
'\u{1b}', '\n', '\t', '\u{061C}', '\u{200B}', '\u{200F}', '\u{202E}', '\u{2066}',
|
||||
'\u{2069}', '\u{206F}', '\u{FEFF}',
|
||||
] {
|
||||
assert!(
|
||||
is_unsafe_display_char(c),
|
||||
"{:#06x} must be unsafe",
|
||||
c as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_str_fits() {
|
||||
assert_eq!(truncate_str("hello", 10), "hello");
|
||||
assert_eq!(truncate_str("hello", 5), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_str_truncates() {
|
||||
assert_eq!(truncate_str("hello world!", 5), "hell…");
|
||||
assert_eq!(truncate_str("abcdef", 4), "abc…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_str_empty_and_zero() {
|
||||
assert_eq!(truncate_str("hello", 0), "");
|
||||
assert_eq!(truncate_str("", 5), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_str_width_1() {
|
||||
assert_eq!(truncate_str("hello", 1), "…");
|
||||
assert_eq!(truncate_str("x", 1), "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_str_multibyte() {
|
||||
// em-dash is 1 display column but 3 bytes
|
||||
let s = "hello — world";
|
||||
let result = truncate_str(s, 8);
|
||||
assert!(result.ends_with('…'));
|
||||
assert!(result.len() <= 12); // safe byte length
|
||||
}
|
||||
|
||||
// ── truncate_line tests ─────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn truncate_line_fits() {
|
||||
let line = Line::from(vec![Span::raw("Hello "), Span::raw("world")]);
|
||||
let result = truncate_line(line, 20);
|
||||
assert_eq!(result.spans.len(), 2);
|
||||
assert_eq!(result.spans[0].content.as_ref(), "Hello ");
|
||||
assert_eq!(result.spans[1].content.as_ref(), "world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_line_cuts_mid_span() {
|
||||
let line = Line::from(vec![
|
||||
Span::raw("Edit "),
|
||||
Span::raw("very/long/path/to/file.rs"),
|
||||
]);
|
||||
// Total = 29, budget = 15 → "Edit very/long…"
|
||||
let result = truncate_line(line, 15);
|
||||
let text: String = result.spans.iter().map(|s| s.content.as_ref()).collect();
|
||||
assert!(text.ends_with('\u{2026}'));
|
||||
assert!(text.width() <= 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_line_drops_later_spans() {
|
||||
let line = Line::from(vec![
|
||||
Span::raw("Search "),
|
||||
Span::raw("pattern"),
|
||||
Span::raw(" in "),
|
||||
Span::raw("path"),
|
||||
Span::raw(" (5 matches)"),
|
||||
]);
|
||||
let result = truncate_line(line, 18);
|
||||
let text: String = result.spans.iter().map(|s| s.content.as_ref()).collect();
|
||||
assert!(text.ends_with('\u{2026}'));
|
||||
assert!(text.width() <= 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_line_zero_width() {
|
||||
let line = Line::from(vec![Span::raw("hello")]);
|
||||
let result = truncate_line(line, 0);
|
||||
assert!(result.spans.is_empty());
|
||||
}
|
||||
|
||||
// ── fit_line_to_width tests ─────────────────────────────────────
|
||||
|
||||
fn line_text(line: &Line<'static>) -> String {
|
||||
line.spans.iter().map(|s| s.content.as_ref()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_line_pads_short_line() {
|
||||
let line = Line::from(vec![Span::raw("│ a │")]);
|
||||
let out = fit_line_to_width(line, 10);
|
||||
assert_eq!(line_text(&out).width(), 10);
|
||||
assert_eq!(line_text(&out), "│ a │ ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_line_exact_width_unchanged() {
|
||||
let line = Line::from(vec![Span::raw("hello")]);
|
||||
let out = fit_line_to_width(line, 5);
|
||||
assert_eq!(out.spans.len(), 1);
|
||||
assert_eq!(line_text(&out), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_line_clips_long_line_no_ellipsis() {
|
||||
let line = Line::from(vec![Span::raw("│ Column A │ Column B │")]);
|
||||
let out = fit_line_to_width(line, 8);
|
||||
assert_eq!(line_text(&out).width(), 8);
|
||||
assert_eq!(line_text(&out), "│ Column");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_line_does_not_split_emoji_grapheme() {
|
||||
// a(1)+b(1)+⚠️(2) = 4. Clipping to 3 must drop the width-2 grapheme
|
||||
// whole (never split it) and pad → "ab" + 1 space.
|
||||
let line = Line::from(vec![Span::raw("ab\u{26A0}\u{FE0F}")]);
|
||||
let out = fit_line_to_width(line, 3);
|
||||
assert_eq!(line_text(&out).width(), 3);
|
||||
assert_eq!(line_text(&out), "ab ");
|
||||
}
|
||||
|
||||
#[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.
|
||||
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);
|
||||
assert_eq!(line_text(&out), "ab ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_line_drops_subsequent_spans_after_clip() {
|
||||
let line = Line::from(vec![
|
||||
Span::raw("hello"),
|
||||
Span::raw(" world"),
|
||||
Span::raw("!!!"),
|
||||
]);
|
||||
let out = fit_line_to_width(line, 5);
|
||||
assert_eq!(line_text(&out), "hello");
|
||||
// The straddling/later spans must be dropped entirely.
|
||||
assert_eq!(out.spans.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_line_takes_partial_of_later_span() {
|
||||
let line = Line::from(vec![Span::raw("ab"), Span::raw("cdef")]);
|
||||
let out = fit_line_to_width(line, 4);
|
||||
assert_eq!(line_text(&out), "abcd");
|
||||
assert_eq!(line_text(&out).width(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_line_zero_width_returns_empty() {
|
||||
let line = Line::from(vec![Span::raw("│ a │")]);
|
||||
let out = fit_line_to_width(line, 0);
|
||||
assert_eq!(line_text(&out), "");
|
||||
assert_eq!(line_text(&out).width(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_line_preserves_span_styles_when_padding() {
|
||||
let bold = ratatui::style::Style::new().add_modifier(ratatui::style::Modifier::BOLD);
|
||||
let line = Line::from(vec![Span::styled("hi", bold)]);
|
||||
let out = fit_line_to_width(line, 5);
|
||||
assert_eq!(line_text(&out).width(), 5);
|
||||
assert!(
|
||||
out.spans[0]
|
||||
.style
|
||||
.add_modifier
|
||||
.contains(ratatui::style::Modifier::BOLD)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_tool_path_api_remains_available_at_line_utils_path() {
|
||||
assert_eq!(shorten_path("verylongfilename.rs", 10), "verylongf…");
|
||||
assert_eq!(path_basename("/repo/src/main.rs", 80), "main.rs");
|
||||
assert_eq!(
|
||||
path_for_tool_header("/repo/src/main.rs", Some(80), "Read ".len()),
|
||||
"main.rs"
|
||||
);
|
||||
assert_eq!(
|
||||
path_for_tool_header("/repo/src/main.rs", None, "Read ".len()),
|
||||
"/repo/src/main.rs"
|
||||
);
|
||||
}
|
||||
|
||||
// ── cascade_truncate tests ────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cascade_truncate_all_fit() {
|
||||
let (t, d, a, m) =
|
||||
cascade_truncate(50, "type ", "description", " \u{2014} running", " meta");
|
||||
assert_eq!(t, "type ");
|
||||
assert_eq!(d, "description");
|
||||
assert_eq!(a, " \u{2014} running");
|
||||
assert_eq!(m, " meta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_truncate_desc_truncated() {
|
||||
let (t, d, a, m) = cascade_truncate(
|
||||
25,
|
||||
"type ",
|
||||
"long description here",
|
||||
" \u{2014} running",
|
||||
" meta",
|
||||
);
|
||||
assert_eq!(t, "type ");
|
||||
assert_eq!(d, "lo\u{2026}");
|
||||
assert_eq!(a, " \u{2014} running");
|
||||
assert_eq!(m, " meta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_truncate_desc_gone_meta_truncated() {
|
||||
// overhead = 6+10+6 = 22 > avail 20 → desc gone, type 6 + activity 10 + meta truncated to 4
|
||||
let (t, d, a, m) = cascade_truncate(20, "type ", "desc", " \u{2014} running", " meta");
|
||||
assert_eq!(t, "type ");
|
||||
assert_eq!(d, "");
|
||||
assert_eq!(a, " \u{2014} running");
|
||||
assert_eq!(m, " m\u{2026}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_truncate_meta_and_activity_gone() {
|
||||
// avail=8, type=6 fits (budget=2), activity truncated to 2, meta gone
|
||||
let (t, d, a, m) = cascade_truncate(8, "type ", "desc", " \u{2014} running", " meta");
|
||||
assert_eq!(t, "type ");
|
||||
assert_eq!(d, "");
|
||||
assert_eq!(a, " \u{2026}");
|
||||
assert_eq!(m, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_truncate_type_truncated() {
|
||||
let (t, d, a, m) = cascade_truncate(3, "type ", "desc", " \u{2014} running", " meta");
|
||||
assert_eq!(t, "ty\u{2026}");
|
||||
assert_eq!(d, "");
|
||||
assert_eq!(a, "");
|
||||
assert_eq!(m, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_truncate_zero_avail() {
|
||||
let (t, d, a, m) = cascade_truncate(0, "type ", "desc", " \u{2014} running", " meta");
|
||||
assert_eq!(t, "");
|
||||
assert_eq!(d, "");
|
||||
assert_eq!(a, "");
|
||||
assert_eq!(m, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_truncate_unicode() {
|
||||
// ✗ = 1 display column; — = 1 display column
|
||||
let (t, d, a, m) = cascade_truncate(10, "\u{2717} ", "description", " \u{2014} run", "");
|
||||
assert_eq!(t, "\u{2717} ");
|
||||
assert_eq!(d, "\u{2026}");
|
||||
assert_eq!(a, " \u{2014} run");
|
||||
assert_eq!(m, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_truncate_overhead_equals_avail() {
|
||||
// overhead exactly equals avail → desc empty, everything else fits
|
||||
let (t, d, a, m) = cascade_truncate(22, "type ", "desc", " \u{2014} running", " meta");
|
||||
assert_eq!(t, "type ");
|
||||
assert_eq!(d, "");
|
||||
assert_eq!(a, " \u{2014} running");
|
||||
assert_eq!(m, " meta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_truncate_avail_one() {
|
||||
let (t, d, a, m) = cascade_truncate(1, "type", "desc", "act", "meta");
|
||||
assert_eq!(t, "\u{2026}");
|
||||
assert_eq!((d.as_str(), a.as_str(), m.as_str()), ("", "", ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_truncate_all_empty() {
|
||||
let (t, d, a, m) = cascade_truncate(10, "", "", "", "");
|
||||
assert_eq!(
|
||||
(t.as_str(), d.as_str(), a.as_str(), m.as_str()),
|
||||
("", "", "", "")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user