aether-wisp 0.6.3

A terminal UI for AI coding agents via the Agent Client Protocol (ACP)
Documentation
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Clear, Padding, Widget};

use crate::theme::Theme;

pub(crate) const MODAL_HORIZONTAL_PADDING: u16 = 2;
pub(crate) const MODAL_VERTICAL_PADDING: u16 = 1;

const HEADER_FOOTER_VERTICAL_PADDING: u16 = MODAL_VERTICAL_PADDING;

pub(crate) const MODAL_VERTICAL_CHROME: u16 = 2 + 2 * HEADER_FOOTER_VERTICAL_PADDING + 2 * MODAL_VERTICAL_PADDING;

pub(crate) struct ModalFrame<'a> {
    title: &'a str,
    title_right: Option<&'a str>,
    footer: Option<Line<'a>>,
    width: Constraint,
    height: Constraint,
    theme: &'a Theme,
}

impl<'a> ModalFrame<'a> {
    pub(crate) fn new(
        title: &'a str,
        footer: Option<Line<'a>>,
        width: Constraint,
        height: Constraint,
        theme: &'a Theme,
    ) -> Self {
        Self { title, title_right: None, footer, width, height, theme }
    }

    /// A small right-aligned chip naming who is asking, so the request reads
    /// `Request ─ server` instead of protocol vocabulary.
    pub(crate) fn title_right(mut self, name: &'a str) -> Self {
        self.title_right = Some(name);
        self
    }

    pub(crate) fn area(&self, outer: Rect) -> Rect {
        outer.centered(self.width, self.height)
    }

    pub(crate) fn inner(&self, outer: Rect) -> Rect {
        self.block().inner(chrome_area(self.area(outer)))
    }

    fn block(&self) -> Block<'_> {
        let horizontal_padding = " ".repeat(usize::from(MODAL_HORIZONTAL_PADDING));
        let mut block = Block::default()
            .title_top(Span::styled(
                format!("{horizontal_padding}{}{horizontal_padding}", self.title),
                Style::new().fg(self.theme.accent).add_modifier(Modifier::BOLD),
            ))
            .padding(Padding::symmetric(MODAL_HORIZONTAL_PADDING, MODAL_VERTICAL_PADDING));

        if let Some(name) = self.title_right {
            block = block.title_top(
                Line::from(Span::styled(
                    format!("{horizontal_padding}{name}{horizontal_padding}"),
                    Style::new().fg(self.theme.muted),
                ))
                .right_aligned(),
            );
        }

        if let Some(mut footer) = self.footer.clone() {
            footer.spans.insert(0, Span::raw(horizontal_padding.clone()));
            footer.spans.push(Span::raw(horizontal_padding));
            block = block.title_bottom(footer);
        }
        block
    }
}

impl Widget for &ModalFrame<'_> {
    fn render(self, outer: Rect, buf: &mut Buffer) {
        let area = self.area(outer);
        Clear.render(area, buf);
        buf.set_style(area, Style::new().bg(self.theme.background));
        self.block().render(chrome_area(area), buf);
    }
}

fn chrome_area(area: Rect) -> Rect {
    if area.height <= MODAL_VERTICAL_CHROME {
        return area;
    }
    Rect {
        y: area.y.saturating_add(HEADER_FOOTER_VERTICAL_PADDING),
        height: area.height - 2 * HEADER_FOOTER_VERTICAL_PADDING,
        ..area
    }
}