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).
171 lines
6.1 KiB
Rust
171 lines
6.1 KiB
Rust
//! 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 0–8 (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
|
||
}
|
||
}
|