Files
Kigi-CLI/crates/codegen/kigi-tui/src/views/progress_bar.rs
T
ZacharyZhang-NY d6c20fc13f 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).
2026-07-17 05:31:01 -04:00

171 lines
6.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Unicode block progress bar at 1/8th-cell resolution via the LEFT
//! fractional blocks `▏▎▍▌▋▊▉█`.
//!
//! Consolas (the default ConHost font) is missing the narrow ones
//! (U+258F..=U+2589) — see microsoft/terminal#387 — so on legacy
//! ConHost we substitute the shade glyphs `░▒▓` from CP437 instead.
//! Same eighth-resolution input; the cell just reads as a density
//! pattern rather than a true left-justified bar.
//!
//! ```ignore
//! render_progress_bar(buf, x, y, 5, 0.42, fg_color, bg_color);
//! ```
use ratatui::buffer::Buffer;
use ratatui::style::{Color, Style};
use ratatui::text::Span;
/// LEFT-fractional block glyphs, indexed 08 (0 = empty, 8 = full).
const BLOCKS: [&str; 9] = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
/// Shade substitutes used on hosts that can't render the LEFT-fractional
/// blocks. Same index domain as [`BLOCKS`] so call sites stay uniform.
const SHADES: [&str; 9] = ["", "░", "░", "░", "▒", "▒", "▓", "▓", "█"];
/// Per-cell partial-fill glyph table — `BLOCKS` everywhere except legacy
/// ConHost, where we substitute `SHADES`.
fn partial_blocks() -> &'static [&'static str; 9] {
if crate::glyphs::is_legacy_windows_console() {
&SHADES
} else {
&BLOCKS
}
}
/// Split a fill fraction into (whole cells, remainder eighths).
fn cell_breakdown(width: u16, value: f32) -> (u16, usize) {
let value = value.clamp(0.0, 1.0);
let total_eighths = (value * width as f32 * 8.0).round() as u16;
let full = (total_eighths / 8).min(width);
let remainder = (total_eighths % 8) as usize;
(full, remainder)
}
/// Per-cell `(symbol, is_filled)` for a `width`-cell bar at `value`
/// fill. Single source of truth for both renderers below.
fn bar_cells(width: u16, value: f32) -> impl Iterator<Item = (&'static str, /* filled */ bool)> {
let (full, remainder) = cell_breakdown(width, value);
let glyphs = partial_blocks();
(0..width).map(move |i| {
if i < full {
(glyphs[8], true)
} else if i == full && remainder > 0 {
(glyphs[remainder], true)
} else {
(" ", false)
}
})
}
/// Build a progress bar as styled spans (one per cell).
///
/// Each span has `fg` on `bg`, suitable for composing into a `Line`.
pub fn progress_bar_spans(width: u16, value: f32, fg: Color, bg: Color) -> Vec<Span<'static>> {
let fg_style = Style::default().fg(fg).bg(bg);
let bg_style = Style::default().bg(bg);
bar_cells(width, value)
.map(|(symbol, filled)| {
let style = if filled { fg_style } else { bg_style };
Span::styled(symbol.to_string(), style)
})
.collect()
}
/// Render a progress bar into the buffer at the given position.
///
/// - `width`: number of character cells for the bar
/// - `value`: fill fraction in `0.0..=1.0` (clamped)
/// - `fg`: color for the filled portion
/// - `bg`: background color for the track (filled + empty cells)
pub fn render_progress_bar(
buf: &mut Buffer,
x: u16,
y: u16,
width: u16,
value: f32,
fg: Color,
bg: Color,
) {
let fg_style = Style::default().fg(fg).bg(bg);
let bg_style = Style::default().bg(bg);
for (i, (symbol, filled)) in bar_cells(width, value).enumerate() {
let Some(cell) = buf.cell_mut((x + i as u16, y)) else {
continue;
};
cell.set_symbol(symbol);
cell.set_style(if filled { fg_style } else { bg_style });
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::layout::Rect;
#[test]
fn test_empty_bar() {
let area = Rect::new(0, 0, 10, 1);
let mut buf = Buffer::empty(area);
render_progress_bar(&mut buf, 0, 0, 5, 0.0, Color::White, Color::Black);
for i in 0..5u16 {
assert_eq!(buf[(i, 0)].symbol(), " ");
}
}
#[test]
fn test_full_bar() {
let area = Rect::new(0, 0, 10, 1);
let mut buf = Buffer::empty(area);
render_progress_bar(&mut buf, 0, 0, 5, 1.0, Color::White, Color::Black);
for i in 0..5u16 {
assert_eq!(buf[(i, 0)].symbol(), "█");
}
}
#[test]
fn test_half_bar() {
let area = Rect::new(0, 0, 10, 1);
let mut buf = Buffer::empty(area);
render_progress_bar(&mut buf, 0, 0, 4, 0.5, Color::White, Color::Black);
// 50% of 4 cells = 2 full blocks
assert_eq!(buf[(0, 0)].symbol(), "█");
assert_eq!(buf[(1, 0)].symbol(), "█");
assert_eq!(buf[(2, 0)].symbol(), " ");
assert_eq!(buf[(3, 0)].symbol(), " ");
}
#[test]
fn test_partial_block() {
let area = Rect::new(0, 0, 10, 1);
let mut buf = Buffer::empty(area);
// 25% of 4 cells = 1 full block (8 eighths). Actually 0.25*4*8 = 8 = 1 full.
// Let's use 12.5% of 4 cells = 0.125*4*8 = 4 eighths = half block on cell 0
render_progress_bar(&mut buf, 0, 0, 4, 0.125, Color::White, Color::Black);
assert_eq!(buf[(0, 0)].symbol(), "▌"); // 4/8 = half
assert_eq!(buf[(1, 0)].symbol(), " ");
}
#[test]
fn cell_breakdown_keeps_eighths_resolution() {
// 0.5 * 4 * 8 = 16 eighths → 2 full + 0 remainder.
assert_eq!(cell_breakdown(4, 0.5), (2, 0));
// 0.125 * 4 * 8 = 4 eighths → 0 full + 4 remainder.
assert_eq!(cell_breakdown(4, 0.125), (0, 4));
// 0.03 * 5 * 8 = 1.2 → rounds to 1 eighth → 0 full + 1 remainder.
// On legacy that picks SHADES[1] = "░"; on truecolor it picks
// BLOCKS[1] = "▏". Either way ~3% does NOT light a full cell.
assert_eq!(cell_breakdown(5, 0.03), (0, 1));
// Out-of-range clamped.
assert_eq!(cell_breakdown(4, 2.0), (4, 0));
}
#[test]
fn shades_and_blocks_tables_match_in_length() {
// The two glyph tables must share the same index domain so call
// sites can swap them without branching on the host.
assert_eq!(BLOCKS.len(), SHADES.len());
assert_eq!(BLOCKS[0], SHADES[0]); // both empty
assert_eq!(BLOCKS[8], SHADES[8]); // both full block
}
}