kudune 0.2.0

Utility to setup Antelope nodes in Docker
Documentation
// SPDX-FileCopyrightText: 2026 DigiGaia SCCL
// SPDX-License-Identifier: AGPL-3.0-or-later

// NOTE: some of this file has been generated by Google Gemini

use std::fmt::Write;

use ratatui::{
    prelude::{Line, Span},
    backend::TestBackend,
    buffer::{Buffer, Cell},
    layout::{Alignment, Constraint},
    style::{Color, Modifier, Style},
    widgets::{Block, Borders, BorderType, Padding, Row, Table},
    Frame, Terminal,
};
use serde_json::Value;


/// Converts a Ratatui Buffer into a String containing ANSI escape sequences.
pub fn buffer_to_ansi(buffer: &Buffer) -> String {
    let mut out = String::new();
    let mut last_fg = Color::Reset;
    let mut last_bg = Color::Reset;
    let mut last_modifier = Modifier::empty();

    for y in buffer.area.top()..buffer.area.bottom() {
        for x in buffer.area.left()..buffer.area.right() {
            let cell = &buffer[(x, y)];

            // Check if style has changed to minimize escape sequence noise
            if cell.fg != last_fg || cell.bg != last_bg || cell.modifier != last_modifier {
                write!(out, "\x1b[0m").unwrap(); // Reset first
                apply_style(&mut out, cell);
                last_fg = cell.fg;
                last_bg = cell.bg;
                last_modifier = cell.modifier;
            }
            out.push_str(cell.symbol());
        }
        // Reset styles at end of line and add newline
        writeln!(out, "\x1b[0m").unwrap();
        last_fg = Color::Reset;
        last_bg = Color::Reset;
        last_modifier = Modifier::empty();
    }
    out
}

fn apply_style(out: &mut String, cell: &Cell) {
    // Handle Modifiers (Bold, Italic, etc.)
    if cell.modifier.contains(Modifier::BOLD) { write!(out, "\x1b[1m").unwrap(); }
    if cell.modifier.contains(Modifier::DIM) { write!(out, "\x1b[2m").unwrap(); }
    if cell.modifier.contains(Modifier::ITALIC) { write!(out, "\x1b[3m").unwrap(); }
    if cell.modifier.contains(Modifier::UNDERLINED) { write!(out, "\x1b[4m").unwrap(); }
    if cell.modifier.contains(Modifier::REVERSED) { write!(out, "\x1b[7m").unwrap(); }

    // Handle Foreground
    write_color(out, cell.fg, false);
    // Handle Background
    write_color(out, cell.bg, true);
}

fn write_color(out: &mut String, color: Color, is_bg: std::primitive::bool) {
    let prefix = if is_bg { "48" } else { "38" };
    match color {
        Color::Reset => {}
        Color::Black => write!(out, "\x1b[{}m", if is_bg { 40 } else { 30 }).unwrap(),
        Color::Red => write!(out, "\x1b[{}m", if is_bg { 41 } else { 31 }).unwrap(),
        Color::Green => write!(out, "\x1b[{}m", if is_bg { 42 } else { 32 }).unwrap(),
        Color::Yellow => write!(out, "\x1b[{}m", if is_bg { 43 } else { 33 }).unwrap(),
        Color::Blue => write!(out, "\x1b[{}m", if is_bg { 44 } else { 34 }).unwrap(),
        Color::Magenta => write!(out, "\x1b[{}m", if is_bg { 45 } else { 35 }).unwrap(),
        Color::Cyan => write!(out, "\x1b[{}m", if is_bg { 46 } else { 36 }).unwrap(),
        Color::Gray => write!(out, "\x1b[{}m", if is_bg { 100 } else { 90 }).unwrap(),
        Color::DarkGray => write!(out, "\x1b[{}m", if is_bg { 100 } else { 90 }).unwrap(),
        Color::LightRed => write!(out, "\x1b[{}m", if is_bg { 101 } else { 91 }).unwrap(),
        Color::LightGreen => write!(out, "\x1b[{}m", if is_bg { 102 } else { 92 }).unwrap(),
        Color::LightYellow => write!(out, "\x1b[{}m", if is_bg { 103 } else { 93 }).unwrap(),
        Color::LightBlue => write!(out, "\x1b[{}m", if is_bg { 104 } else { 94 }).unwrap(),
        Color::LightMagenta => write!(out, "\x1b[{}m", if is_bg { 105 } else { 95 }).unwrap(),
        Color::LightCyan => write!(out, "\x1b[{}m", if is_bg { 106 } else { 96 }).unwrap(),
        Color::White => write!(out, "\x1b[{}m", if is_bg { 107 } else { 97 }).unwrap(),
        Color::Indexed(i) => write!(out, "\x1b[{};5;{}m", prefix, i).unwrap(),
        Color::Rgb(r, g, b) => write!(out, "\x1b[{};2;{};{};{}m", prefix, r, g, b).unwrap(),
    }
}


pub fn render<F>(width: u16, height: u16, render_callback: F) -> String
where
    F: FnOnce(&mut Frame<'_>),
{
    let backend = TestBackend::new(width, height);
    let mut terminal = Terminal::new(backend).unwrap();

    terminal.draw(render_callback).unwrap();

    let buffer = terminal.backend().buffer();
    buffer_to_ansi(buffer)
}

pub fn terminal_size() -> (u16, u16) {
    crossterm::terminal::size().unwrap()
}

fn make_row<'a>(data: &Value, fields: &[&str]) -> Row<'a> {
    let mut col = vec![];
    for field in fields {
        col.push(data[field].as_str().unwrap().to_owned());
    }
    Row::new(col)
}

pub fn make_table<'a>(data: &[Value], fields: &[&'a str]) -> Table<'a> {
    let rows: Vec<_> = data.iter()
        .map(|row| make_row(row, fields))
        .collect();
    let widths = Constraint::from_fills(vec![1; fields.len()]);
    let header = Row::new(fields.to_owned())
        .style(Style::new().bold())
        .bottom_margin(1); // add space between the header and the rest of the rows

    Table::<'a>::new(rows, widths)
        .column_spacing(1)
    // .style(Style::new().blue())
        .header(header)
}

pub fn make_block<'a, T: Into<Line<'a>>>(title: T) -> Block<'a> {
    let mut title: Line<'a> = title.into();
    let space = Span::raw(" ");
    title.spans.insert(0, space.clone());
    title.spans.push(space);
    Block::default()
        .title_alignment(Alignment::Center)
        .title(title)
        .borders(Borders::ALL)
        .border_type(BorderType::Double)
        .padding(Padding::symmetric(2, 1))
}