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,142 @@
|
||||
//! Accented wrapper - adds an accent line on the left.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::Style;
|
||||
|
||||
use crate::render::Renderable;
|
||||
|
||||
/// Wraps content with an accent line on the left.
|
||||
///
|
||||
/// The accent line takes 1 column. Content renders in the remaining space.
|
||||
///
|
||||
/// ```text
|
||||
/// │A│ Content here... │
|
||||
/// │A│ More content... │
|
||||
/// ↑
|
||||
/// Accent column (1 char)
|
||||
/// ```
|
||||
pub struct Accented<'a, T> {
|
||||
inner: &'a T,
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a, T> Accented<'a, T> {
|
||||
/// Create a new accented wrapper.
|
||||
pub fn new(inner: &'a T, style: Style) -> Self {
|
||||
Self { inner, style }
|
||||
}
|
||||
|
||||
/// Create with just a foreground color.
|
||||
pub fn with_fg(inner: &'a T, color: ratatui::style::Color) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
style: Style::default().fg(color),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Renderable> Renderable for Accented<'_, T> {
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
// Accent takes 1 column, so content gets width - 1
|
||||
let content_width = width.saturating_sub(1);
|
||||
self.inner.desired_height(content_width)
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Split horizontally: [accent (1)] [content (rest)]
|
||||
let [accent_area, content_area] =
|
||||
Layout::horizontal([Constraint::Length(1), Constraint::Min(0)]).areas(area);
|
||||
|
||||
// Draw accent line for all rows
|
||||
for y in accent_area.y..accent_area.y + accent_area.height {
|
||||
buf.set_string(accent_area.x, y, crate::glyphs::accent_bar(), self.style);
|
||||
}
|
||||
|
||||
// Render inner content
|
||||
if content_area.width > 0 {
|
||||
self.inner.render(content_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::style::Color;
|
||||
|
||||
/// Simple test content that renders as fixed height.
|
||||
struct TestContent {
|
||||
height: u16,
|
||||
text: &'static str,
|
||||
}
|
||||
|
||||
impl Renderable for TestContent {
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
for y in area.y..area.y + area.height.min(self.height) {
|
||||
buf.set_string(area.x, y, self.text, Style::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_desired_height_accounts_for_accent() {
|
||||
let content = TestContent {
|
||||
height: 3,
|
||||
text: "test",
|
||||
};
|
||||
let accented = Accented::with_fg(&content, Color::Blue);
|
||||
|
||||
// Width 80 -> content gets 79
|
||||
assert_eq!(accented.desired_height(80), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_places_accent() {
|
||||
let content = TestContent {
|
||||
height: 2,
|
||||
text: "Hi",
|
||||
};
|
||||
let accented = Accented::with_fg(&content, Color::Blue);
|
||||
|
||||
let area = Rect::new(0, 0, 10, 2);
|
||||
let mut buf = Buffer::empty(area);
|
||||
accented.render(area, &mut buf);
|
||||
|
||||
// Check accent line is in column 0
|
||||
assert_eq!(
|
||||
buf.cell((0, 0)).unwrap().symbol(),
|
||||
crate::glyphs::accent_bar()
|
||||
);
|
||||
assert_eq!(
|
||||
buf.cell((0, 1)).unwrap().symbol(),
|
||||
crate::glyphs::accent_bar()
|
||||
);
|
||||
|
||||
// Check content starts at column 1
|
||||
assert_eq!(buf.cell((1, 0)).unwrap().symbol(), "H");
|
||||
assert_eq!(buf.cell((2, 0)).unwrap().symbol(), "i");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_empty_area() {
|
||||
let content = TestContent {
|
||||
height: 1,
|
||||
text: "x",
|
||||
};
|
||||
let accented = Accented::with_fg(&content, Color::Blue);
|
||||
|
||||
let area = Rect::new(0, 0, 0, 0);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
accented.render(area, &mut buf); // Should not panic
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
//! BlockRenderer - bridges BlockContent to Renderable.
|
||||
//!
|
||||
//! This wrapper takes a block that implements `BlockContent` and renders
|
||||
//! its output into a given area, implementing the `Renderable` trait.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
|
||||
use crate::appearance::AppearanceConfig;
|
||||
use crate::render::{Renderable, SafeBuf};
|
||||
use crate::scrollback::block::BlockContent;
|
||||
use crate::scrollback::types::{BlockBackground, BlockContext, DisplayMode};
|
||||
use crate::theme::Theme;
|
||||
|
||||
/// Renders a `BlockContent` implementation as a `Renderable`.
|
||||
pub struct BlockRenderer<'a, B> {
|
||||
block: &'a B,
|
||||
mode: DisplayMode,
|
||||
is_running: bool,
|
||||
raw: bool,
|
||||
background: Option<ratatui::style::Color>,
|
||||
max_lines: Option<u16>,
|
||||
appearance: AppearanceConfig,
|
||||
}
|
||||
|
||||
impl<'a, B> BlockRenderer<'a, B> {
|
||||
pub fn new(block: &'a B) -> Self {
|
||||
Self {
|
||||
block,
|
||||
mode: DisplayMode::Expanded,
|
||||
is_running: false,
|
||||
raw: false,
|
||||
background: None,
|
||||
max_lines: None,
|
||||
appearance: AppearanceConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mode(mut self, mode: DisplayMode) -> Self {
|
||||
self.mode = mode;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn running(mut self, is_running: bool) -> Self {
|
||||
self.is_running = is_running;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn raw(mut self, raw: bool) -> Self {
|
||||
self.raw = raw;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn background(mut self, bg: ratatui::style::Color) -> Self {
|
||||
self.background = Some(bg);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_lines(mut self, max: u16) -> Self {
|
||||
self.max_lines = Some(max);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn appearance(mut self, appearance: AppearanceConfig) -> Self {
|
||||
self.appearance = appearance;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockContent> BlockRenderer<'_, B> {
|
||||
fn make_context(&self, width: u16) -> BlockContext {
|
||||
BlockContext {
|
||||
mode: self.mode,
|
||||
is_running: self.is_running,
|
||||
width,
|
||||
raw: self.raw,
|
||||
max_lines: self.max_lines,
|
||||
appearance: self.appearance.clone(),
|
||||
is_selected: false,
|
||||
cwd: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_background(&self, block_bg: BlockBackground) -> Option<ratatui::style::Color> {
|
||||
// Explicit override takes precedence
|
||||
if let Some(bg) = self.background {
|
||||
return Some(bg);
|
||||
}
|
||||
|
||||
// Otherwise use block's declared background
|
||||
let theme = Theme::current();
|
||||
match block_bg {
|
||||
BlockBackground::None => None,
|
||||
BlockBackground::Light => Some(theme.bg_light),
|
||||
BlockBackground::Dark => Some(theme.bg_dark),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockContent> Renderable for BlockRenderer<'_, B> {
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
let ctx = self.make_context(width);
|
||||
let output = self.block.output(&ctx);
|
||||
let has_vpad = self.block.has_vpad(&ctx);
|
||||
|
||||
let content_height = output.len() as u16;
|
||||
let vpad = if has_vpad { 2 } else { 0 }; // top + bottom
|
||||
|
||||
content_height + vpad
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ctx = self.make_context(area.width);
|
||||
let output = self.block.output(&ctx);
|
||||
let has_vpad = self.block.has_vpad(&ctx);
|
||||
let block_bg = self.block.background(&ctx);
|
||||
|
||||
// Resolve background color
|
||||
let bg_color = self.resolve_background(block_bg);
|
||||
|
||||
// Fill background if specified
|
||||
if let Some(bg) = bg_color {
|
||||
let bg_style = Style::default().bg(bg);
|
||||
for y in area.y..area.y + area.height {
|
||||
for x in area.x..area.x + area.width {
|
||||
if let Some(cell) = buf.cell_mut((x, y)) {
|
||||
cell.set_style(bg_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut row = area.y;
|
||||
let max_row = area.y + area.height;
|
||||
|
||||
// Top vpad (empty row)
|
||||
if has_vpad && row < max_row {
|
||||
row += 1;
|
||||
}
|
||||
|
||||
// Content lines
|
||||
for line in &output.lines {
|
||||
if row >= max_row {
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply line-specific background if set
|
||||
// Respects bg_start_col for partial background
|
||||
if let Some(line_bg) = line.background {
|
||||
let bg_x = area.x + line.bg_start_col;
|
||||
let bg_width = area.width.saturating_sub(line.bg_start_col);
|
||||
if bg_width > 0 {
|
||||
let line_rect = Rect::new(bg_x, row, bg_width, 1);
|
||||
buf.set_style(line_rect, Style::default().bg(line_bg));
|
||||
}
|
||||
}
|
||||
|
||||
// Render the line content
|
||||
buf.set_line_safe(area.x, row, &line.content, area.width);
|
||||
row += 1;
|
||||
}
|
||||
|
||||
// Bottom vpad (empty row) - just skip, background already applied
|
||||
// (no explicit rendering needed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scrollback::block::StubBlock;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::style::Color;
|
||||
|
||||
#[test]
|
||||
fn test_desired_height_with_vpad() {
|
||||
let block = StubBlock::new("Hello", Color::Blue);
|
||||
let renderer = BlockRenderer::new(&block);
|
||||
|
||||
// StubBlock has 1 line + vpad (top + bottom) = 3
|
||||
assert_eq!(renderer.desired_height(80), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_fills_area() {
|
||||
let block = StubBlock::new("Test content", Color::Blue);
|
||||
let renderer = BlockRenderer::new(&block);
|
||||
|
||||
let area = Rect::new(0, 0, 20, 5);
|
||||
let mut buf = Buffer::empty(area);
|
||||
renderer.render(area, &mut buf);
|
||||
|
||||
// Row 0 = top vpad (empty)
|
||||
// Row 1 = content "Test content"
|
||||
// Row 2 = bottom vpad (empty)
|
||||
// Content should be at row 1
|
||||
let content_cell = buf.cell((0, 1)).unwrap();
|
||||
assert_eq!(content_cell.symbol(), "T");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_with_explicit_background() {
|
||||
let block = StubBlock::new("BG test", Color::Blue);
|
||||
let renderer = BlockRenderer::new(&block).background(Color::Red);
|
||||
|
||||
let area = Rect::new(0, 0, 10, 3);
|
||||
let mut buf = Buffer::empty(area);
|
||||
renderer.render(area, &mut buf);
|
||||
|
||||
// All cells should have red background
|
||||
for y in 0..3 {
|
||||
for x in 0..10 {
|
||||
assert_eq!(buf.cell((x, y)).unwrap().bg, Color::Red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_display_modes() {
|
||||
let block = StubBlock::new("Mode test", Color::Blue);
|
||||
|
||||
let expanded = BlockRenderer::new(&block).mode(DisplayMode::Expanded);
|
||||
let collapsed = BlockRenderer::new(&block).mode(DisplayMode::Collapsed);
|
||||
|
||||
// Both should have same height for StubBlock (it doesn't vary by mode)
|
||||
assert_eq!(expanded.desired_height(80), collapsed.desired_height(80));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,80 @@
|
||||
//! Wrapper types for composable rendering.
|
||||
//!
|
||||
//! These wrappers implement the `Renderable` trait and add decorations
|
||||
//! (accent lines, padding, etc.) around inner content.
|
||||
//!
|
||||
//! ## Composition Example
|
||||
//!
|
||||
//! ```text
|
||||
//! Padded::standard( // Adds left=2, right=1 padding
|
||||
//! &Accented::with_fg( // Adds accent line on left
|
||||
//! &BlockRenderer::new(&block), // Renders block content
|
||||
//! Color::Blue,
|
||||
//! )
|
||||
//! )
|
||||
//! ```
|
||||
//!
|
||||
//! This produces:
|
||||
//! ```text
|
||||
//! │PP│A│ Content here... │P│
|
||||
//! ↑↑ ↑ ↑
|
||||
//! │ └─ Accent └─ Right padding
|
||||
//! └───── Left padding
|
||||
//! ```
|
||||
|
||||
mod accented;
|
||||
mod block_renderer;
|
||||
mod entry_renderer;
|
||||
mod padded;
|
||||
|
||||
pub use accented::Accented;
|
||||
pub use block_renderer::BlockRenderer;
|
||||
pub use entry_renderer::EntryRenderer;
|
||||
pub(crate) use entry_renderer::group_header_chrome_prefix_width;
|
||||
pub use padded::Padded;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::render::Renderable;
|
||||
use crate::scrollback::block::StubBlock;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
|
||||
/// Test that wrappers can be composed together.
|
||||
#[test]
|
||||
fn test_wrapper_composition() {
|
||||
let block = StubBlock::new("Hello", Color::Blue);
|
||||
let renderer = BlockRenderer::new(&block);
|
||||
let accented = Accented::with_fg(&renderer, Color::Blue);
|
||||
let padded = Padded::standard(&accented);
|
||||
|
||||
// Check height calculation chains correctly
|
||||
// BlockRenderer: 3 (1 content + 2 vpad)
|
||||
// Accented: takes 1 column, height unchanged
|
||||
// Padded: takes 3 columns (2+1), height unchanged
|
||||
assert_eq!(padded.desired_height(80), 3);
|
||||
|
||||
// Render and verify structure
|
||||
let area = Rect::new(0, 0, 20, 3);
|
||||
let mut buf = Buffer::empty(area);
|
||||
padded.render(area, &mut buf);
|
||||
|
||||
// Layout should be:
|
||||
// Cols 0-1: padding (empty)
|
||||
// Col 2: accent line
|
||||
// Cols 3+: content
|
||||
// Last col: right padding
|
||||
|
||||
// Check accent is at column 2
|
||||
assert_eq!(buf.cell((2, 0)).unwrap().symbol(), "┃");
|
||||
assert_eq!(buf.cell((2, 1)).unwrap().symbol(), "┃");
|
||||
assert_eq!(buf.cell((2, 2)).unwrap().symbol(), "┃");
|
||||
|
||||
// Check content starts at column 3 (after accent)
|
||||
// Row 0 is vpad (empty), row 1 is content
|
||||
assert_eq!(buf.cell((3, 1)).unwrap().symbol(), "H");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
//! Padded wrapper - adds horizontal padding around content.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::Style;
|
||||
|
||||
use crate::render::Renderable;
|
||||
|
||||
/// Wraps content with horizontal padding.
|
||||
///
|
||||
/// Adds left and right padding columns. Optionally fills padding with a background.
|
||||
///
|
||||
/// ```text
|
||||
/// │PP│ Content here... │P│
|
||||
/// │PP│ More content... │P│
|
||||
/// ↑↑ ↑
|
||||
/// Left padding (2) Right padding (1)
|
||||
/// ```
|
||||
pub struct Padded<'a, T> {
|
||||
inner: &'a T,
|
||||
left: u16,
|
||||
right: u16,
|
||||
bg: Option<ratatui::style::Color>,
|
||||
}
|
||||
|
||||
impl<'a, T> Padded<'a, T> {
|
||||
/// Create a new padded wrapper.
|
||||
pub fn new(inner: &'a T, left: u16, right: u16) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
left,
|
||||
right,
|
||||
bg: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with standard pager padding (2 left, 1 right).
|
||||
pub fn standard(inner: &'a T) -> Self {
|
||||
Self::new(inner, 2, 1)
|
||||
}
|
||||
|
||||
/// Set background color for padding and content area.
|
||||
pub fn with_bg(mut self, color: ratatui::style::Color) -> Self {
|
||||
self.bg = Some(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Renderable> Renderable for Padded<'_, T> {
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
// Padding takes left + right columns
|
||||
let content_width = width.saturating_sub(self.left + self.right);
|
||||
self.inner.desired_height(content_width)
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill background if specified
|
||||
if let Some(bg) = self.bg {
|
||||
let bg_style = Style::default().bg(bg);
|
||||
for y in area.y..area.y + area.height {
|
||||
for x in area.x..area.x + area.width {
|
||||
if let Some(cell) = buf.cell_mut((x, y)) {
|
||||
cell.set_style(bg_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Split horizontally: [left pad] [content] [right pad]
|
||||
let [_left_area, content_area, _right_area] = Layout::horizontal([
|
||||
Constraint::Length(self.left),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(self.right),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
// Render inner content
|
||||
if content_area.width > 0 {
|
||||
self.inner.render(content_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::style::Color;
|
||||
|
||||
/// Simple test content that renders as fixed height.
|
||||
struct TestContent {
|
||||
height: u16,
|
||||
text: &'static str,
|
||||
}
|
||||
|
||||
impl Renderable for TestContent {
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
for y in area.y..area.y + area.height.min(self.height) {
|
||||
buf.set_string(area.x, y, self.text, Style::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_desired_height_accounts_for_padding() {
|
||||
let content = TestContent {
|
||||
height: 3,
|
||||
text: "test",
|
||||
};
|
||||
let padded = Padded::new(&content, 2, 1);
|
||||
|
||||
// Width 80 -> content gets 77 (80 - 2 - 1)
|
||||
assert_eq!(padded.desired_height(80), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_standard_padding() {
|
||||
let content = TestContent {
|
||||
height: 1,
|
||||
text: "x",
|
||||
};
|
||||
let padded = Padded::standard(&content);
|
||||
|
||||
// Standard is 2 left, 1 right
|
||||
// Width 10 -> content gets 7
|
||||
assert_eq!(padded.desired_height(10), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_places_content_with_offset() {
|
||||
let content = TestContent {
|
||||
height: 1,
|
||||
text: "Hi",
|
||||
};
|
||||
let padded = Padded::new(&content, 2, 1);
|
||||
|
||||
let area = Rect::new(0, 0, 10, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
padded.render(area, &mut buf);
|
||||
|
||||
// Content starts at column 2 (after 2-char left padding)
|
||||
assert_eq!(buf.cell((2, 0)).unwrap().symbol(), "H");
|
||||
assert_eq!(buf.cell((3, 0)).unwrap().symbol(), "i");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_with_background() {
|
||||
let content = TestContent {
|
||||
height: 1,
|
||||
text: "X",
|
||||
};
|
||||
let padded = Padded::new(&content, 1, 1).with_bg(Color::Blue);
|
||||
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
padded.render(area, &mut buf);
|
||||
|
||||
// All cells should have blue background
|
||||
for x in 0..5 {
|
||||
let cell = buf.cell((x, 0)).unwrap();
|
||||
assert_eq!(cell.bg, Color::Blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user