sabiql 1.10.0

A fast, driver-less TUI for browsing and editing PostgreSQL databases
Documentation
use ratatui::style::Style;
use ratatui::text::{Line, Span};

use crate::app::model::browse::json_tree::{LineType, TreeLine, TreeValue};
use crate::ui::theme::ThemePalette;

const INDENT: &str = "  ";
const FOLD_EXPANDED: &str = "\u{25bc} "; //const FOLD_COLLAPSED: &str = "\u{25b6} "; //
fn json_escaped(s: &str) -> String {
    serde_json::to_string(s).unwrap_or_else(|_| format!("\"{s}\""))
}

pub fn json_tree_line_spans(
    line: &TreeLine,
    is_selected: bool,
    theme: &ThemePalette,
) -> Line<'static> {
    let mut spans: Vec<Span<'static>> = Vec::new();

    let indent = INDENT.repeat(line.depth);
    if !indent.is_empty() {
        spans.push(Span::styled(indent, Style::default().fg(theme.text_muted)));
    }

    match &line.line_type {
        LineType::ObjectOpen => {
            render_container_open(&mut spans, line, "{", "{...}", "keys", theme);
        }
        LineType::ObjectClose => {
            spans.push(Span::styled("}", Style::default().fg(theme.json_bracket)));
        }
        LineType::ArrayOpen => {
            render_container_open(&mut spans, line, "[", "[...]", "items", theme);
        }
        LineType::ArrayClose => {
            spans.push(Span::styled("]", Style::default().fg(theme.json_bracket)));
        }
        LineType::KeyValue => {
            push_key_spans(&mut spans, line.key.as_ref(), theme);
            push_value_span(&mut spans, &line.value, theme);
        }
        LineType::ArrayItem => {
            push_value_span(&mut spans, &line.value, theme);
        }
    }

    let line_style = if is_selected {
        Style::default().bg(theme.result_row_active_bg)
    } else {
        Style::default()
    };

    Line::from(spans).style(line_style)
}

fn render_container_open(
    spans: &mut Vec<Span<'static>>,
    line: &TreeLine,
    open_bracket: &'static str,
    collapsed_bracket: &'static str,
    count_label: &'static str,
    theme: &ThemePalette,
) {
    let fold = if line.collapsed {
        FOLD_COLLAPSED
    } else {
        FOLD_EXPANDED
    };
    spans.push(Span::styled(
        fold,
        Style::default().fg(theme.text_secondary),
    ));

    push_key_spans(spans, line.key.as_ref(), theme);

    let child_count = match &line.value {
        TreeValue::ObjectOpen { child_count } | TreeValue::ArrayOpen { child_count } => {
            *child_count
        }
        _ => 0,
    };

    if line.collapsed {
        spans.push(Span::styled(
            collapsed_bracket,
            Style::default().fg(theme.json_bracket),
        ));
    } else {
        spans.push(Span::styled(
            open_bracket,
            Style::default().fg(theme.json_bracket),
        ));
    }
    spans.push(Span::styled(
        format!(" [{child_count} {count_label}]"),
        Style::default().fg(theme.text_dim),
    ));
}

fn push_key_spans(spans: &mut Vec<Span<'static>>, key: Option<&String>, theme: &ThemePalette) {
    if let Some(key) = key {
        spans.push(Span::styled(
            json_escaped(key),
            Style::default().fg(theme.json_key),
        ));
        spans.push(Span::styled(
            ": ",
            Style::default().fg(theme.text_secondary),
        ));
    }
}

fn push_value_span(spans: &mut Vec<Span<'static>>, value: &TreeValue, theme: &ThemePalette) {
    match value {
        TreeValue::Null => {
            spans.push(Span::styled("null", Style::default().fg(theme.json_null)));
        }
        TreeValue::Bool(b) => {
            let s = if *b { "true" } else { "false" };
            spans.push(Span::styled(s, Style::default().fg(theme.json_bool)));
        }
        TreeValue::Number(n) => {
            spans.push(Span::styled(
                n.clone(),
                Style::default().fg(theme.json_number),
            ));
        }
        TreeValue::String(s) => {
            spans.push(Span::styled(
                json_escaped(s),
                Style::default().fg(theme.json_string),
            ));
        }
        TreeValue::ObjectOpen { .. } | TreeValue::ArrayOpen { .. } | TreeValue::Closing => {}
    }
}