zinc-wallet-cli 0.4.0

Agent-first Bitcoin + Ordinals CLI wallet with account-based taproot ordinals + native segwit payment addresses (optional human mode)
use crate::ui::ZincTheme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::*;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
use zinc_core::Network;

pub struct BrandedHeader<'a> {
    pub title: &'a str,
    pub profile_name: &'a str,
    pub theme: &'a ZincTheme,
    pub network: Network,
    pub account_index: u32,
    pub is_loading: bool,
    pub tick: u64,
    pub _ascii_mode: bool,
}

impl Widget for BrandedHeader<'_> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let block = Block::default()
            .borders(Borders::BOTTOM)
            .border_type(BorderType::Plain)
            .style(Style::default().bg(self.theme.surface_glass))
            .border_style(
                Style::default()
                    .fg(self.theme.border)
                    .add_modifier(Modifier::DIM),
            );

        block.render(area, buf);

        let content_area = Rect {
            x: area.x + 2,
            y: area.y + 1,
            width: area.width.saturating_sub(4),
            height: 1,
        };

        if content_area.width == 0 {
            return;
        }

        let logo = Span::styled(
            " ZINC ",
            Style::default()
                .fg(self.theme.charcoal)
                .bg(self.theme.accent)
                .add_modifier(Modifier::BOLD),
        );
        let title = Span::styled(
            self.title.to_uppercase(),
            Style::default()
                .fg(self.theme.text_primary)
                .add_modifier(Modifier::BOLD),
        );

        let network_str = match self.network {
            Network::Bitcoin => "MAINNET",
            Network::Testnet => "TESTNET",
            Network::Regtest => "REGTEST",
            Network::Signet => "SIGNET",
            _ => "UNKNOWN",
        };

        let profile_label = if self.profile_name == "default" {
            String::new()
        } else {
            format!("{}  ·  ", self.profile_name.to_uppercase())
        };

        let dot_color = if self.tick.is_multiple_of(2) {
            self.theme.selection
        } else {
            self.theme.accent
        };

        let status_spans: Vec<Span<'static>> = if self.is_loading {
            let track_width: usize = 10;
            let head_pos = (self.tick % 40) as usize;

            let head = if head_pos < track_width {
                head_pos
            } else if head_pos < track_width + 4 {
                track_width - 1
            } else {
                39 - head_pos
            };

            let trail_len: usize = 4;

            let mut spans = vec![Span::raw(" ")];

            for i in 0..track_width {
                let dist = head.saturating_sub(i);

                let (ch, color) = if dist == 0 {
                    ("", self.theme.accent)
                } else if dist <= trail_len {
                    ("", Color::Rgb(245, 158 - (dist * 30) as u8, 11))
                } else {
                    ("", self.theme.border)
                };

                spans.push(Span::styled(ch, Style::default().fg(color)));
            }

            spans.push(Span::raw("  "));
            spans.push(Span::styled(
                "syncing",
                Style::default()
                    .fg(self.theme.text_muted)
                    .add_modifier(Modifier::ITALIC),
            ));

            spans
        } else {
            vec![
                Span::styled("", Style::default().fg(dot_color)),
                Span::styled(
                    "  LIVE",
                    Style::default()
                        .fg(self.theme.text_muted)
                        .add_modifier(Modifier::BOLD),
                ),
            ]
        };

        let context_info = vec![
            Span::styled(
                &profile_label[..],
                Style::default().fg(self.theme.text_muted),
            ),
            Span::raw("    "),
            Span::styled(
                "ACCOUNT",
                Style::default()
                    .fg(self.theme.text_muted)
                    .add_modifier(Modifier::DIM),
            ),
            Span::raw(" "),
            Span::styled(
                format!("#{}", self.account_index + 1),
                Style::default()
                    .fg(self.theme.cream)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("      "),
            Span::styled(
                network_str,
                Style::default()
                    .fg(self.theme.accent)
                    .add_modifier(Modifier::BOLD),
            ),
        ];

        let left_content = Line::from(vec![logo, Span::raw("  "), title]);
        let right_content = Line::from([status_spans, context_info].concat());

        Paragraph::new(left_content).render(content_area, buf);

        let right_width = right_content.width() as u16;
        if content_area.width > right_width {
            let right_area = Rect {
                x: content_area.right().saturating_sub(right_width),
                y: content_area.y,
                width: right_width,
                height: 1,
            };
            Paragraph::new(right_content).render(right_area, buf);
        }
    }
}