use ratatui::style::{Modifier, Style};
#[derive(Debug, Clone, Copy)]
pub struct TableStyles {
pub header: Style,
pub row: Style,
pub highlight: Style,
pub stripe: Style,
}
#[derive(Debug, Clone, Copy)]
pub struct ListStyles {
pub base: Style,
pub highlight: Style,
pub symbol: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct TabStyles {
pub active: Style,
pub inactive: Style,
}
#[derive(Debug, Clone, Copy)]
pub struct GaugeStyles {
pub filled: Style,
pub base: Style,
}
#[derive(Debug, Clone, Copy)]
pub struct StateStyles {
pub normal: Style,
pub focused: Style,
pub selected: Style,
pub disabled: Style,
}
#[derive(Debug, Clone, Copy)]
pub struct InputStyles {
pub text: Style,
pub placeholder: Style,
pub cursor: Style,
pub prompt: Style,
pub border: Style,
pub border_focused: Style,
}
#[derive(Debug, Clone, Copy)]
pub struct ScrollbarStyles {
pub track: Style,
pub thumb: Style,
}
#[derive(Debug, Clone, Copy)]
pub struct NotificationStyles {
pub info: Style,
pub success: Style,
pub warning: Style,
pub error: Style,
pub body: Style,
pub background: Style,
}
impl TableStyles {
pub(crate) fn from_theme(theme: &(impl crate::Theme + ?Sized)) -> Self {
Self {
header: Style::default()
.fg(theme.accent())
.add_modifier(Modifier::BOLD),
row: Style::default().fg(theme.text()),
highlight: Style::default()
.fg(theme.text_bright())
.bg(theme.surface())
.add_modifier(Modifier::BOLD),
stripe: Style::default().bg(theme.stripe()),
}
}
}
impl ListStyles {
pub(crate) fn from_theme(theme: &(impl crate::Theme + ?Sized)) -> Self {
Self {
base: Style::default().fg(theme.text()),
highlight: Style::default()
.fg(theme.accent())
.bg(theme.surface())
.add_modifier(Modifier::BOLD),
symbol: "\u{25b6} ",
}
}
}
impl TabStyles {
pub(crate) fn from_theme(theme: &(impl crate::Theme + ?Sized)) -> Self {
Self {
active: Style::default()
.fg(theme.accent())
.add_modifier(Modifier::BOLD),
inactive: Style::default().fg(theme.text_dim()),
}
}
}
impl GaugeStyles {
pub(crate) fn from_theme(theme: &(impl crate::Theme + ?Sized)) -> Self {
Self {
filled: Style::default()
.fg(theme.accent())
.add_modifier(Modifier::BOLD),
base: Style::default().fg(theme.border()),
}
}
}
impl ScrollbarStyles {
pub(crate) fn from_theme(theme: &(impl crate::Theme + ?Sized)) -> Self {
Self {
track: Style::default().fg(theme.border()),
thumb: Style::default().fg(theme.text_dim()),
}
}
}
impl NotificationStyles {
pub(crate) fn from_theme(theme: &(impl crate::Theme + ?Sized)) -> Self {
Self {
info: Style::default().fg(theme.info()),
success: Style::default().fg(theme.success()),
warning: Style::default().fg(theme.warning()),
error: Style::default().fg(theme.error()),
body: Style::default().fg(theme.text_bright()),
background: Style::default().bg(theme.surface()),
}
}
}
impl InputStyles {
pub(crate) fn from_theme(theme: &(impl crate::Theme + ?Sized)) -> Self {
Self {
text: Style::default().fg(theme.text()),
placeholder: Style::default().fg(theme.text_dim()),
cursor: Style::default()
.fg(theme.accent())
.add_modifier(Modifier::REVERSED),
prompt: Style::default()
.fg(theme.accent())
.add_modifier(Modifier::BOLD),
border: Style::default().fg(theme.border()),
border_focused: Style::default().fg(theme.accent()),
}
}
}
impl StateStyles {
pub(crate) fn from_theme(theme: &(impl crate::Theme + ?Sized)) -> Self {
Self {
normal: Style::default().fg(theme.text()),
focused: Style::default()
.fg(theme.accent())
.add_modifier(Modifier::BOLD),
selected: Style::default().fg(theme.text_bright()).bg(theme.surface()),
disabled: Style::default().fg(theme.text_dim()),
}
}
#[must_use]
pub fn resolve(&self, focused: bool, selected: bool, disabled: bool) -> Style {
if disabled {
self.disabled
} else if selected {
self.selected
} else if focused {
self.focused
} else {
self.normal
}
}
}
#[must_use]
pub fn zebra_rows(
rows: Vec<ratatui::widgets::Row<'_>>,
stripe_style: Style,
) -> Vec<ratatui::widgets::Row<'_>> {
rows.into_iter()
.enumerate()
.map(|(i, row)| {
if i % 2 == 0 {
row.style(stripe_style)
} else {
row
}
})
.collect()
}
#[cfg(test)]
mod tests {
use ratatui::style::Color;
use ratatui::widgets::Row;
use super::*;
use crate::{CatppuccinMocha, Dracula, NoColor, Theme};
#[test]
fn table_header_is_bold_accent() {
let ts = TableStyles::from_theme(&CatppuccinMocha);
assert_eq!(ts.header.fg, Some(CatppuccinMocha.accent));
assert!(ts.header.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn table_highlight_has_surface_bg() {
let ts = TableStyles::from_theme(&CatppuccinMocha);
assert_eq!(ts.highlight.bg, Some(CatppuccinMocha.surface));
}
#[test]
fn table_stripe_uses_derived_stripe_color() {
let ts = TableStyles::from_theme(&Dracula);
assert_eq!(ts.stripe.bg, Some(Dracula.stripe()));
assert_ne!(ts.stripe.bg, Some(Dracula.surface));
}
#[test]
fn list_highlight_uses_accent() {
let ls = ListStyles::from_theme(&CatppuccinMocha);
assert_eq!(ls.highlight.fg, Some(CatppuccinMocha.accent));
}
#[test]
fn list_symbol_is_arrow() {
let ls = ListStyles::from_theme(&CatppuccinMocha);
assert!(ls.symbol.contains('\u{25b6}'));
}
#[test]
fn tab_active_is_accent_bold() {
let ts = TabStyles::from_theme(&CatppuccinMocha);
assert_eq!(ts.active.fg, Some(CatppuccinMocha.accent));
assert!(ts.active.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn tab_inactive_is_dim() {
let ts = TabStyles::from_theme(&CatppuccinMocha);
assert_eq!(ts.inactive.fg, Some(CatppuccinMocha.text_dim));
}
#[test]
fn gauge_filled_is_accent() {
let gs = GaugeStyles::from_theme(&CatppuccinMocha);
assert_eq!(gs.filled.fg, Some(CatppuccinMocha.accent));
}
#[test]
fn state_resolve_disabled_wins() {
let ss = StateStyles::from_theme(&CatppuccinMocha);
let style = ss.resolve(true, true, true);
assert_eq!(style, ss.disabled);
}
#[test]
fn state_resolve_selected_over_focused() {
let ss = StateStyles::from_theme(&CatppuccinMocha);
let style = ss.resolve(true, true, false);
assert_eq!(style, ss.selected);
}
#[test]
fn state_resolve_focused() {
let ss = StateStyles::from_theme(&CatppuccinMocha);
let style = ss.resolve(true, false, false);
assert_eq!(style, ss.focused);
}
#[test]
fn state_resolve_normal() {
let ss = StateStyles::from_theme(&CatppuccinMocha);
let style = ss.resolve(false, false, false);
assert_eq!(style, ss.normal);
}
#[test]
fn input_placeholder_is_dim() {
let is = InputStyles::from_theme(&CatppuccinMocha);
assert_eq!(is.placeholder.fg, Some(CatppuccinMocha.text_dim));
}
#[test]
fn input_cursor_is_accent_reversed() {
let is = InputStyles::from_theme(&CatppuccinMocha);
assert_eq!(is.cursor.fg, Some(CatppuccinMocha.accent));
assert!(is.cursor.add_modifier.contains(Modifier::REVERSED));
}
#[test]
fn input_border_focused_is_accent() {
let is = InputStyles::from_theme(&CatppuccinMocha);
assert_eq!(is.border_focused.fg, Some(CatppuccinMocha.accent));
}
#[test]
fn scrollbar_thumb_is_dim() {
let ss = ScrollbarStyles::from_theme(&CatppuccinMocha);
assert_eq!(ss.thumb.fg, Some(CatppuccinMocha.text_dim));
}
#[test]
fn scrollbar_track_is_border() {
let ss = ScrollbarStyles::from_theme(&CatppuccinMocha);
assert_eq!(ss.track.fg, Some(CatppuccinMocha.border));
}
#[test]
fn notification_error_uses_error_color() {
let ns = NotificationStyles::from_theme(&CatppuccinMocha);
assert_eq!(ns.error.fg, Some(CatppuccinMocha.error));
}
#[test]
fn notification_info_uses_info_color() {
let ns = NotificationStyles::from_theme(&CatppuccinMocha);
assert_eq!(ns.info.fg, Some(CatppuccinMocha.info));
}
#[test]
fn notification_success_uses_success_color() {
let ns = NotificationStyles::from_theme(&CatppuccinMocha);
assert_eq!(ns.success.fg, Some(CatppuccinMocha.success));
}
#[test]
fn notification_background_uses_surface() {
let ns = NotificationStyles::from_theme(&CatppuccinMocha);
assert_eq!(ns.background.bg, Some(CatppuccinMocha.surface));
}
#[test]
fn no_color_table_uses_reset() {
let ts = TableStyles::from_theme(&NoColor);
assert_eq!(ts.header.fg, Some(Color::Reset));
}
#[test]
fn no_color_state_uses_reset() {
let ss = StateStyles::from_theme(&NoColor);
assert_eq!(ss.normal.fg, Some(Color::Reset));
}
#[test]
fn no_color_input_uses_reset() {
let is = InputStyles::from_theme(&NoColor);
assert_eq!(is.text.fg, Some(Color::Reset));
assert_eq!(is.placeholder.fg, Some(Color::Reset));
}
#[test]
fn no_color_scrollbar_uses_reset() {
let ss = ScrollbarStyles::from_theme(&NoColor);
assert_eq!(ss.track.fg, Some(Color::Reset));
}
#[test]
fn no_color_notification_uses_reset() {
let ns = NotificationStyles::from_theme(&NoColor);
assert_eq!(ns.info.fg, Some(Color::Reset));
assert_eq!(ns.error.fg, Some(Color::Reset));
}
#[test]
fn zebra_applies_to_even_rows() {
let stripe = Style::default().bg(Color::DarkGray);
let rows = vec![Row::new(["a"]), Row::new(["b"]), Row::new(["c"])];
let striped = zebra_rows(rows, stripe);
assert_eq!(striped.len(), 3);
}
#[test]
fn zebra_empty_rows() {
let striped = zebra_rows(Vec::new(), Style::default());
assert!(striped.is_empty());
}
}