use std::io;
use std::ops::ControlFlow;
use std::time::Duration;
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use tuika::components::{Flex, Rule, Text};
use tuika::term::capabilities::Capabilities;
use tuika::{Event, KeyCode, Padding, Runner, RunnerConfig, Theme, themes, view};
const PROBE_TIMEOUT: Duration = Duration::from_millis(100);
#[derive(Clone, Copy, PartialEq, Eq)]
enum Source {
Probed,
AnsiSlots,
}
impl Source {
fn label(self) -> &'static str {
match self {
Source::Probed => "probed — derived from the colors this terminal reported",
Source::AnsiSlots => "themes::TERMINAL — the terminal answered nothing, ANSI slots",
}
}
}
fn probe() -> io::Result<(Theme, Source)> {
crossterm::terminal::enable_raw_mode()?;
let (_caps, palette) = Capabilities::query_with_palette(PROBE_TIMEOUT);
crossterm::terminal::disable_raw_mode()?;
Ok(match Theme::from_terminal(&palette) {
Some(theme) => (theme, Source::Probed),
None => (themes::TERMINAL, Source::AnsiSlots),
})
}
fn describe(color: Color) -> String {
match color {
Color::Rgb(r, g, b) => format!("#{r:02x}{g:02x}{b:02x}"),
Color::Reset => "terminal default".to_string(),
Color::Indexed(i) => format!("ansi {i}"),
other => format!("{other:?}"),
}
}
fn slots(theme: &Theme) -> Vec<(&'static str, Color)> {
vec![
("background", theme.background),
("surface", theme.surface),
("text", theme.text),
("muted", theme.muted),
("dim", theme.dim),
("accent", theme.accent),
("accent_alt", theme.accent_alt),
("border", theme.border),
("selection_bg", theme.selection_bg),
("code.keyword", theme.code.keyword),
("code.string", theme.code.string),
("code.comment", theme.code.comment),
]
}
fn build(theme: &Theme, source: Source) -> tuika::Element {
let rows: Vec<tuika::Element> = slots(theme)
.into_iter()
.map(|(name, color)| {
let line = Line::from(vec![
Span::styled("████", Style::default().fg(color)),
Span::styled(
format!(" {name:<13} {}", describe(color)),
Style::default().fg(theme.text),
),
]);
tuika::element(Text::new(vec![line]))
})
.collect();
let mut palette = Flex::column();
for row in rows {
palette = palette.fixed(1, row);
}
view! {
col(padding = Padding::all(1), gap = 1,
background = Style::default().bg(theme.background)) {
fixed(3) {
boxed(title = Line::from(Span::styled(" inherited theme ", theme.accent_style()))) {
text(source.label().to_string())
}
}
fixed(14) {
boxed(title = Line::from(Span::styled(" slots ", theme.accent_style()))) {
node(palette)
}
}
fixed(1) { node(Rule::new()) }
fixed(1) {
text("q / esc to quit".to_string())
}
}
}
}
fn main() -> io::Result<()> {
let probe_only = std::env::args().any(|a| a == "--probe");
let (theme, source) = probe()?;
if probe_only {
println!("source: {}", source.label());
for (name, color) in slots(&theme) {
println!("{name}: {}", describe(color));
}
return Ok(());
}
let runner = Runner::new(RunnerConfig::default());
runner.run(
&theme,
|_frame| build(&theme, source),
|event| match event {
Event::Key(key) if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) => {
ControlFlow::Break(())
}
_ => ControlFlow::Continue(()),
},
)
}