use crate::{color::SystemColor, component::Elements, frame::Color, markup::Border};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum JamoWidth {
#[default]
Platform,
Unicode,
Narrow,
Wide,
}
impl JamoWidth {
const fn from_caps(value: u8) -> Self {
match value {
1 => Self::Narrow,
2 => Self::Wide,
_ => Self::Platform,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Charset {
#[default]
Unicode,
NerdFont,
Ascii,
}
#[derive(Clone, Copy)]
pub struct Grid {
pub fill: char,
pub lead: &'static str,
pub mid: &'static str,
pub tail: &'static str,
pub top: (char, char, char),
pub middle: (char, char, char),
pub bottom: (char, char, char),
}
impl Charset {
pub const fn icon(self, icon: crate::Icon) -> &'static str {
icon.glyph(self)
}
pub fn icon_named(self, name: &str) -> Option<&'static str> {
crate::Icon::from_name(name).map(|icon| self.icon(icon))
}
pub const fn border(self, border: Border) -> (char, char, char, char, char, char) {
match self {
Self::Ascii => ('+', '+', '+', '+', '-', '|'),
_ => match border {
Border::Square => ('┌', '┐', '└', '┘', '─', '│'),
Border::Dash => ('┌', '┐', '└', '┘', '╌', '┆'),
Border::Round => ('╭', '╮', '╰', '╯', '─', '│'),
Border::Heavy => ('┏', '┓', '┗', '┛', '━', '┃'),
Border::Double => ('╔', '╗', '╚', '╝', '═', '║'),
},
}
}
pub const fn cursor(self) -> &'static str {
match self {
Self::Unicode => "❯ ",
Self::NerdFont => "\u{f054} ",
Self::Ascii => "> ",
}
}
pub const fn radio(self, selected: bool) -> &'static str {
match (self, selected) {
(Self::Ascii, true) => "(o)",
(Self::Ascii, false) => "( )",
(Self::NerdFont, true) => "\u{f192}",
(Self::NerdFont, false) => "\u{f10c}",
(_, true) => "◉",
(_, false) => "○",
}
}
pub(crate) const fn checkbox(self, checked: bool) -> &'static str {
match (self, checked) {
(Self::Ascii, true) => "[x]",
(Self::Ascii, false) => "[ ]",
(Self::NerdFont, true) => "\u{f14a}",
(Self::NerdFont, false) => "\u{f096}",
(_, true) => "☑",
(_, false) => "☐",
}
}
pub(crate) const fn expander(self, open: bool) -> &'static str {
match (self, open) {
(Self::Ascii, true) => "v ",
(Self::Ascii, false) => "> ",
(_, true) => "▾ ",
(_, false) => "▸ ",
}
}
pub(crate) const fn guides(self, family: Border) -> (&'static str, &'static str, &'static str) {
match self {
Self::Ascii => ("|-", "`-", "| "),
_ => match family {
Border::Square => ("├─", "└─", "│ "),
Border::Dash => ("├╌", "└╌", "┆ "),
Border::Round => ("├─", "╰─", "│ "),
Border::Heavy => ("┣━", "┗━", "┃ "),
Border::Double => ("╠═", "╚═", "║ "),
},
}
}
pub(crate) const fn rule(self) -> char {
match self {
Self::Ascii => '-',
_ => '─',
}
}
pub(crate) const fn rule_fill(self, requested: char) -> char {
if matches!(self, Self::Ascii) && !requested.is_ascii() {
self.rule()
} else {
requested
}
}
pub(crate) const fn quote_rail(self) -> &'static str {
match self {
Self::Ascii => "| ",
_ => "│ ",
}
}
pub const fn grid(self) -> Grid {
match self {
Self::Ascii => Grid {
fill: '-',
lead: "| ",
mid: " | ",
tail: " |",
top: ('+', '+', '+'),
middle: ('+', '+', '+'),
bottom: ('+', '+', '+'),
},
_ => Grid {
fill: '─',
lead: "│ ",
mid: " │ ",
tail: " │",
top: ('┌', '┬', '┐'),
middle: ('├', '┼', '┤'),
bottom: ('└', '┴', '┘'),
},
}
}
pub const fn scrollbar(self) -> (&'static str, &'static str) {
match self {
Self::Ascii => ("|", "#"),
_ => ("│", "█"),
}
}
pub(crate) const fn progress(self) -> (&'static str, &'static str) {
match self {
Self::Ascii => ("#", "."),
_ => ("█", "░"),
}
}
pub(crate) const fn pill_caps(self) -> (&'static str, &'static str) {
match self {
Self::Ascii => ("", ""),
_ => ("▐", "▌"),
}
}
pub const fn rail(self) -> &'static str {
match self {
Self::Ascii => "| ",
_ => "▎ ",
}
}
pub(crate) const fn status_band(self) -> (&'static str, &'static str, &'static str) {
match self {
Self::Ascii => ("", ">", ">"),
Self::Unicode => ("", "›", "›"),
Self::NerdFont => ("\u{e0b6}", "\u{e0b1}", "\u{e0b0}"),
}
}
pub(crate) const fn status_band_end(self) -> (&'static str, &'static str, &'static str) {
match self {
Self::Ascii => ("<", ">", ""),
Self::Unicode => ("‹", "›", ""),
Self::NerdFont => ("\u{e0b2}", "\u{e0b1}", ""),
}
}
pub(crate) const fn shadow(self) -> Option<&'static str> {
match self {
Self::Ascii => None,
_ => Some("▀"),
}
}
pub const fn spinner(self) -> crate::anim::Frames {
match self {
Self::Ascii => crate::anim::Frames::SPINNER_ASCII,
_ => crate::anim::Frames::SPINNER,
}
}
pub(crate) const fn beam(self) -> &'static str {
match self {
Self::Ascii => "_",
_ => "▏",
}
}
pub const fn check(self) -> &'static str {
match self {
Self::Ascii => "*",
Self::NerdFont => "\u{f00c}",
Self::Unicode => "✓",
}
}
pub(crate) const fn note_icon(self) -> &'static str {
match self {
Self::Ascii => "[i]",
Self::NerdFont => "\u{f05a}",
Self::Unicode => "ℹ",
}
}
pub(crate) const fn arrows(self) -> (&'static str, &'static str) {
match self {
Self::Ascii => ("<", ">"),
_ => ("◂", "▸"),
}
}
pub(crate) const fn dropdown(self) -> &'static str {
match self {
Self::Ascii => " v",
_ => " ▾",
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Appearance {
#[default]
Dark,
Light,
}
impl Appearance {
pub const fn from_rgb16(red: u16, green: u16, blue: u16) -> Self {
let weighted = 299 * red as u64 + 587 * green as u64 + 114 * blue as u64;
if weighted < 500 * u16::MAX as u64 {
Self::Dark
} else {
Self::Light
}
}
pub const fn from_rgb8(red: u8, green: u8, blue: u8) -> Self {
Self::from_rgb16((red as u16) * 0x101, (green as u16) * 0x101, (blue as u16) * 0x101)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Theme {
pub fg: Color,
pub accent: Color,
pub info: Color,
pub ok: Color,
pub warn: Color,
pub err: Color,
pub muted: Color,
pub border: Color,
pub surface: Color,
pub hover: Color,
pub shadow: Color,
pub contrast: Color,
}
impl Default for Theme {
fn default() -> Self {
Self {
fg: Color::Rgb(0xc8, 0xcc, 0xd4),
accent: Color::Rgb(0x61, 0xaf, 0xef),
info: Color::Rgb(0x56, 0xb6, 0xc2),
ok: Color::Rgb(0x98, 0xc3, 0x79),
warn: Color::Rgb(0xe5, 0xc0, 0x7b),
err: Color::Rgb(0xe0, 0x6c, 0x75),
muted: Color::Rgb(0x5c, 0x63, 0x70),
border: Color::Rgb(0x45, 0x4b, 0x58),
surface: Color::Rgb(0x3a, 0x3f, 0x4b),
hover: Color::Rgb(0x2c, 0x31, 0x3a),
shadow: Color::Rgb(0x05, 0x07, 0x0c),
contrast: Color::Rgb(0x10, 0x12, 0x16),
}
}
}
impl Theme {
pub const fn for_appearance(appearance: Appearance) -> Self {
match appearance {
Appearance::Dark => Self {
fg: Color::Rgb(0xc8, 0xcc, 0xd4),
accent: Color::Rgb(0x61, 0xaf, 0xef),
info: Color::Rgb(0x56, 0xb6, 0xc2),
ok: Color::Rgb(0x98, 0xc3, 0x79),
warn: Color::Rgb(0xe5, 0xc0, 0x7b),
err: Color::Rgb(0xe0, 0x6c, 0x75),
muted: Color::Rgb(0x5c, 0x63, 0x70),
border: Color::Rgb(0x45, 0x4b, 0x58),
surface: Color::Rgb(0x3a, 0x3f, 0x4b),
hover: Color::Rgb(0x2c, 0x31, 0x3a),
shadow: Color::Rgb(0x05, 0x07, 0x0c),
contrast: Color::Rgb(0x10, 0x12, 0x16),
},
Appearance::Light => Self {
fg: Color::Rgb(0x24, 0x28, 0x30),
accent: Color::Rgb(0x00, 0x5f, 0xaf),
info: Color::Rgb(0x00, 0x72, 0x7d),
ok: Color::Rgb(0x3f, 0x70, 0x19),
warn: Color::Rgb(0x8a, 0x5a, 0x00),
err: Color::Rgb(0xb0, 0x24, 0x32),
muted: Color::Rgb(0x6b, 0x70, 0x78),
border: Color::Rgb(0xd0, 0xd7, 0xde),
surface: Color::Rgb(0xe2, 0xe5, 0xea),
hover: Color::Rgb(0xed, 0xef, 0xf2),
shadow: Color::Rgb(0xb8, 0xbd, 0xc7),
contrast: Color::Rgb(0xff, 0xff, 0xff),
},
}
}
pub(crate) fn token(&self, name: &str) -> Option<Color> {
Some(match name {
"fg" => self.fg,
"accent" => self.accent,
"info" => self.info,
"ok" => self.ok,
"warn" => self.warn,
"err" => self.err,
"muted" => self.muted,
"border" => self.border,
"surface" => self.surface,
"hover" => self.hover,
"shadow" => self.shadow,
"contrast" => self.contrast,
_ => return SystemColor::parse(name).map(|system| system.resolve(self)),
})
}
pub(crate) fn is_token(name: &str) -> bool {
matches!(
name,
"fg"
| "accent"
| "info" | "ok"
| "warn" | "err"
| "muted"
| "border"
| "surface"
| "hover"
| "shadow"
| "contrast"
) || SystemColor::parse(name).is_some()
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Graphics {
#[default]
Cells,
Sixel,
KittyDirect,
KittyPlaceholders,
Iterm2,
}
#[derive(Clone, Debug)]
pub struct UiContext {
pub appearance: Appearance,
pub charset: Charset,
pub graphics: Graphics,
pub jamo_width: JamoWidth,
pub theme: Theme,
pub elements: Elements,
pub now: std::time::Duration,
pub revision: u64,
pub loader: Option<crate::ImageLoader>,
}
impl Default for UiContext {
fn default() -> Self {
Self {
appearance: Appearance::default(),
charset: Charset::default(),
graphics: Graphics::default(),
jamo_width: crate::rich::jamo_width(),
theme: Theme::default(),
elements: Elements::default(),
now: std::time::Duration::default(),
revision: 0,
loader: None,
}
}
}
impl UiContext {
pub fn set_jamo_width(&mut self, width: JamoWidth) -> bool {
self.jamo_width = width;
crate::rich::set_jamo_width(width)
}
pub fn apply_terminal_caps(&mut self, caps: &crate::TerminalCaps) -> bool {
self.graphics = caps.graphics;
let mut changed = self.charset != caps.charset;
self.charset = caps.charset;
changed |= self.set_jamo_width(JamoWidth::from_caps(caps.jamo_width));
if let Some((red, green, blue)) = caps.background {
let appearance = Appearance::from_rgb16(red, green, blue);
if appearance != self.appearance {
self.appearance = appearance;
self.theme = Theme::for_appearance(appearance);
changed = true;
}
}
changed
}
pub fn with_terminal_caps(mut self, caps: &crate::TerminalCaps) -> Self {
self.apply_terminal_caps(caps);
self
}
}
impl PartialEq for UiContext {
fn eq(&self, other: &Self) -> bool {
self.charset == other.charset
&& self.appearance == other.appearance
&& self.graphics == other.graphics
&& self.jamo_width == other.jamo_width
&& self.theme == other.theme
&& self.elements.ptr_eq(&other.elements)
}
}
impl Eq for UiContext {}
#[cfg(test)]
mod tests {
use super::{Appearance, Theme};
#[test]
fn bt601_classifies_boundary_colors_at_both_component_depths() {
assert_eq!(Appearance::from_rgb8(0, 0, 0), Appearance::Dark);
assert_eq!(Appearance::from_rgb8(255, 255, 255), Appearance::Light);
assert_eq!(Appearance::from_rgb8(127, 127, 127), Appearance::Dark);
assert_eq!(Appearance::from_rgb8(128, 128, 128), Appearance::Light);
assert_eq!(Appearance::from_rgb16(0, 0, 0), Appearance::Dark);
assert_eq!(Appearance::from_rgb16(u16::MAX, u16::MAX, u16::MAX), Appearance::Light);
assert_eq!(Appearance::from_rgb16(0x7fff, 0x7fff, 0x7fff), Appearance::Dark);
assert_eq!(Appearance::from_rgb16(0x8000, 0x8000, 0x8000), Appearance::Light);
}
#[test]
fn appearance_palettes_are_distinct_and_cover_every_token() {
let dark = Theme::for_appearance(Appearance::Dark);
let light = Theme::for_appearance(Appearance::Light);
assert_ne!(dark, light);
for token in [
"fg", "accent", "info", "ok", "warn", "err", "muted", "surface", "hover", "shadow",
"contrast",
] {
assert!(dark.token(token).is_some(), "dark palette misses {token}");
assert!(light.token(token).is_some(), "light palette misses {token}");
}
}
}