use crate::config::presets::*;
use crate::ui::widgets::{DialogPosition, DialogSize};
use crate::utils::parse_color;
use ratatui::style::{Color, Style};
use serde::Deserialize;
use std::sync::LazyLock;
#[derive(Deserialize, Debug)]
#[serde(default)]
pub(crate) struct Theme {
name: Option<String>,
selection: ColorPair,
underline: ColorPair,
accent: ColorPair,
entry: ColorPair,
directory: ColorPair,
separator: ColorPair,
selection_icon: String,
parent: PaneTheme,
preview: PaneTheme,
path: ColorPair,
status_line: ColorPair,
#[serde(deserialize_with = "deserialize_color_field")]
exe_color: Color,
symlink: SymlinkTheme,
marker: MarkerTheme,
widget: WidgetTheme,
info: WidgetTheme,
}
impl Default for Theme {
fn default() -> Self {
Theme {
name: None,
accent: ColorPair {
fg: Color::Indexed(238),
..ColorPair::default()
},
selection: ColorPair {
bg: Color::Indexed(236),
..ColorPair::default()
},
underline: ColorPair::default(),
entry: ColorPair::default(),
directory: ColorPair {
fg: Color::Blue,
..ColorPair::default()
},
separator: ColorPair {
fg: Color::Indexed(238),
..ColorPair::default()
},
selection_icon: "".into(),
parent: PaneTheme::default(),
preview: PaneTheme::default(),
path: ColorPair {
fg: Color::Magenta,
..ColorPair::default()
},
status_line: ColorPair::default(),
exe_color: Color::LightGreen,
symlink: SymlinkTheme::default(),
marker: MarkerTheme::default(),
widget: WidgetTheme::default(),
info: WidgetTheme {
title: ColorPair {
fg: Color::Magenta,
..ColorPair::default()
},
..WidgetTheme::default()
},
}
}
}
macro_rules! override_if_changed {
($target:ident, $user:ident, $default:ident, $field:ident) => {
if $user.$field != $default.$field {
$target.$field = $user.$field.clone();
}
};
}
impl Theme {
pub(crate) fn internal_defaults() -> &'static Self {
static DEFAULT: LazyLock<Theme> = LazyLock::new(Theme::default);
&DEFAULT
}
pub(crate) fn accent_style(&self) -> Style {
self.accent.style_or(&Theme::internal_defaults().accent)
}
pub(crate) fn selection_style(&self) -> Style {
self.selection
.style_or(&Theme::internal_defaults().selection)
}
pub(crate) fn underline_style(&self) -> Style {
self.underline
.style_or(&Theme::internal_defaults().underline)
}
pub(crate) fn entry_style(&self) -> Style {
self.entry.style_or(&Theme::internal_defaults().entry)
}
pub(crate) fn directory_style(&self) -> Style {
self.directory
.style_or(&Theme::internal_defaults().directory)
}
pub(crate) fn separator_style(&self) -> Style {
self.separator
.style_or(&Theme::internal_defaults().separator)
}
pub(crate) fn path_style(&self) -> Style {
self.path.style_or(&Theme::internal_defaults().path)
}
pub(crate) fn status_line_style(&self) -> Style {
self.status_line
.style_or(&Theme::internal_defaults().status_line)
}
pub(crate) fn symlink_theme(&self) -> SymlinkTheme {
let defaults = Theme::internal_defaults().symlink;
SymlinkTheme {
directory: self.symlink.directory.or(defaults.directory),
file: self.symlink.file.or(defaults.file),
target: self.symlink.target.or(defaults.target),
}
}
pub(crate) fn parent_selection_style(&self) -> Style {
if self.parent.selection_mode == SelectionMode::Off {
return Style::default();
}
self.parent.selection_style(&self.selection)
}
pub(crate) fn preview_selection_style(&self) -> Style {
if self.preview.selection_mode == SelectionMode::Off {
return Style::default();
}
self.preview.selection_style(&self.selection)
}
pub(crate) fn preview_item_style(&self) -> Style {
self.preview.entry_style(&self.entry)
}
pub(crate) fn parent_item_style(&self) -> Style {
self.parent.entry_style(&self.entry)
}
pub(crate) fn exe_color(&self) -> Color {
self.exe_color
}
#[inline]
pub(crate) fn selection_icon(&self) -> &str {
&self.selection_icon
}
#[inline]
pub(crate) fn preview(&self) -> &PaneTheme {
&self.preview
}
#[inline]
pub(crate) fn marker(&self) -> &MarkerTheme {
&self.marker
}
#[inline]
pub(crate) fn widget(&self) -> &WidgetTheme {
&self.widget
}
#[inline]
pub(crate) fn info(&self) -> &WidgetTheme {
&self.info
}
pub(crate) fn with_overrides(self) -> Self {
let preset = match self.name.as_deref() {
Some("gruvbox-dark-hard") => Some(gruvbox_dark_hard()),
Some("gruvbox-dark") => Some(gruvbox_dark()),
Some("gruvbox-light") => Some(gruvbox_light()),
Some("catppuccin-mocha") => Some(catppuccin_mocha()),
Some("catppuccin-frappe") => Some(catppuccin_frappe()),
Some("catppuccin-macchiato") => Some(catppuccin_mocha()),
Some("catppuccin-latte") => Some(catppuccin_latte()),
Some("nord") => Some(nord()),
Some("two-dark") => Some(two_dark()),
Some("one-dark") => Some(one_dark()),
Some("solarized-dark") => Some(solarized_dark()),
Some("solarized-light") => Some(solarized_light()),
Some("dracula") => Some(dracula()),
Some("monokai") => Some(monokai()),
Some("nightfox") => Some(nightfox()),
Some("carbonfox") => Some(carbonfox()),
Some("tokyonight") => Some(tokyonight_night()),
Some("tokyonight-storm") => Some(tokyonight_storm()),
Some("tokyonight-day") => Some(tokyonight_day()),
Some("everforest") => Some(everforest()),
Some("rose-pine") | Some("rose_pine") => Some(rose_pine()),
_ => None,
};
if let Some(mut base) = preset {
base.apply_user_overrides(self);
base
} else {
self
}
}
pub(crate) fn bat_theme_name(&self) -> &'static str {
self.name
.as_deref()
.map(Theme::map_to_bat_theme)
.unwrap_or("TwoDark")
}
fn map_to_bat_theme(internal_theme: &str) -> &'static str {
match internal_theme {
"default" => "TwoDark",
"two-dark" => "TwoDark",
"one-dark" => "OneHalfDark",
"gruvbox-dark" | "gruvbox-dark-hard" | "gruvbox" => "gruvbox-dark",
"gruvbox-light" => "gruvbox-light",
"tokyonight-night" | "tokyonight" | "tokyonight-storm" => "TwoDark",
"catppuccin-latte" => "Catppuccin Latte",
"catppuccin-frappe" => "Catppuccin Frappe",
"catppuccin-macchiato" => "Catppuccin Macchiato",
"catppuccin-mocha" | "catppuccin" => "Catppuccin Mocha",
"nightfox" | "carbonfox" | "rose-pine" | "everforest" => "TwoDark",
"monokai" => "Monokai Extended (default)",
"nord" => "Nord",
"solarized-dark" => "Solarized (dark)",
"solarized-light" => "Solarized (light)",
"dracula" => "Dracula",
_ => "TwoDark",
}
}
fn apply_user_overrides(&mut self, user: Theme) {
let defaults = Theme::default();
override_if_changed!(self, user, defaults, accent);
override_if_changed!(self, user, defaults, selection);
override_if_changed!(self, user, defaults, underline);
override_if_changed!(self, user, defaults, entry);
override_if_changed!(self, user, defaults, directory);
override_if_changed!(self, user, defaults, separator);
override_if_changed!(self, user, defaults, parent);
override_if_changed!(self, user, defaults, preview);
override_if_changed!(self, user, defaults, path);
override_if_changed!(self, user, defaults, status_line);
override_if_changed!(self, user, defaults, symlink);
override_if_changed!(self, user, defaults, selection_icon);
override_if_changed!(self, user, defaults, marker);
override_if_changed!(self, user, defaults, widget);
override_if_changed!(self, user, defaults, info);
if user.name.is_some() {
self.name = user.name.clone();
}
}
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub(crate) struct ColorPair {
#[serde(default, deserialize_with = "deserialize_color_field")]
fg: Color,
#[serde(default, deserialize_with = "deserialize_color_field")]
bg: Color,
}
impl Default for ColorPair {
fn default() -> Self {
Self {
fg: Color::Reset,
bg: Color::Reset,
}
}
}
impl ColorPair {
pub(crate) fn resolve(&self, other: &ColorPair) -> Self {
Self {
fg: if self.fg == Color::Reset {
other.fg
} else {
self.fg
},
bg: if self.bg == Color::Reset {
other.bg
} else {
self.bg
},
}
}
pub(crate) fn style_or(&self, fallback: &ColorPair) -> Style {
let resovled = self.resolve(fallback);
Style::default().fg(resovled.fg).bg(resovled.bg)
}
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
enum SelectionMode {
#[default]
On,
Off,
}
#[derive(Deserialize, Debug, PartialEq, Clone, Copy, Default)]
#[serde(default)]
pub(crate) struct PaneTheme {
#[serde(flatten)]
color: ColorPair,
selection: Option<ColorPair>,
selection_mode: SelectionMode,
}
impl PaneTheme {
pub(crate) fn selection_style(&self, fallback: &ColorPair) -> Style {
let default = &Theme::internal_defaults().selection;
match self.selection {
Some(pane_sel) => pane_sel.style_or(&fallback.resolve(default)),
None => fallback.style_or(default),
}
}
pub(crate) fn entry_style(&self, fallback: &ColorPair) -> Style {
self.color.style_or(fallback)
}
pub(crate) fn style_or(&self, fallback: &ColorPair) -> Style {
self.color.style_or(fallback)
}
pub(crate) fn effective_style_or_theme(&self) -> Style {
self.style_or(&Theme::internal_defaults().entry)
}
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
#[serde(default)]
pub(crate) struct MarkerTheme {
icon: String,
#[serde(flatten)]
color: ColorPair,
clipboard: Option<ColorPair>,
}
impl MarkerTheme {
#[inline]
pub(crate) fn icon(&self) -> &str {
&self.icon
}
pub(crate) fn style_or_theme(&self) -> Style {
self.color.style_or(&MarkerTheme::default().color)
}
pub(crate) fn clipboard_style_or_theme(&self) -> Style {
match &self.clipboard {
Some(c) => c.style_or(&MarkerTheme::default().clipboard.unwrap()),
None => self.style_or_theme(),
}
}
}
impl Default for MarkerTheme {
fn default() -> Self {
MarkerTheme {
icon: "*".to_string(),
color: ColorPair {
fg: Color::Yellow,
bg: Color::Reset,
},
clipboard: Some(ColorPair {
fg: Color::Green,
bg: Color::Reset,
}),
}
}
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
#[serde(default)]
pub(crate) struct WidgetTheme {
color: ColorPair,
border: ColorPair,
title: ColorPair,
position: Option<DialogPosition>,
size: Option<DialogSize>,
confirm_size: Option<DialogSize>,
move_size: Option<DialogSize>,
find_visible_results: Option<usize>,
find_width: Option<u16>,
go_to_help: GoToHelpTheme,
}
impl WidgetTheme {
#[inline]
pub(crate) fn position(&self) -> &Option<DialogPosition> {
&self.position
}
#[inline]
pub(crate) fn size(&self) -> &Option<DialogSize> {
&self.size
}
#[inline]
pub(crate) fn confirm_size(&self) -> &Option<DialogSize> {
&self.confirm_size
}
pub(crate) fn confirm_size_or(&self, fallback: DialogSize) -> DialogSize {
self.confirm_size()
.as_ref()
.or_else(|| self.size().as_ref())
.copied()
.unwrap_or(fallback)
}
pub(crate) fn move_size(&self) -> &Option<DialogSize> {
&self.move_size
}
pub(crate) fn move_size_or(&self, fallback: DialogSize) -> DialogSize {
self.move_size()
.as_ref()
.or_else(|| self.size().as_ref())
.copied()
.unwrap_or(fallback)
}
pub(crate) fn border_style_or(&self, fallback: Style) -> Style {
self.border.style_or(&ColorPair {
fg: fallback.fg.unwrap_or(Color::Reset),
bg: fallback.bg.unwrap_or(Color::Reset),
})
}
pub(crate) fn fg_or(&self, fallback: Style) -> Style {
self.color.style_or(&ColorPair {
fg: fallback.fg.unwrap_or(Color::Reset),
bg: fallback.bg.unwrap_or(Color::Reset),
})
}
pub(crate) fn bg_or(&self, fallback: Style) -> Style {
self.color.style_or(&ColorPair {
fg: fallback.fg.unwrap_or(Color::Reset),
bg: fallback.bg.unwrap_or(Color::Reset),
})
}
pub(crate) fn fg_or_theme(&self) -> Style {
self.fg_or(Style::default().fg(Theme::internal_defaults().info.color.fg))
}
pub(crate) fn bg_or_theme(&self) -> Style {
self.bg_or(Style::default().bg(Theme::internal_defaults().info.color.bg))
}
pub(crate) fn title_style_or_theme(&self) -> Style {
self.title.style_or(&Theme::internal_defaults().info.title)
}
pub(crate) fn find_visible_or(&self, fallback: usize) -> usize {
self.find_visible_results.unwrap_or(fallback)
}
pub(crate) fn find_width_or(&self, fallback: u16) -> u16 {
self.find_width.unwrap_or(fallback)
}
pub(crate) fn go_to_help_size(&self) -> DialogSize {
self.go_to_help
.size
.or(Theme::internal_defaults().widget.go_to_help.size)
.unwrap_or(DialogSize::Custom(38, 3))
}
pub(crate) fn go_to_help_position(&self) -> DialogPosition {
self.go_to_help
.position
.or(Theme::internal_defaults().widget.go_to_help.position)
.unwrap_or(DialogPosition::Bottom)
}
}
impl Default for WidgetTheme {
fn default() -> Self {
WidgetTheme {
color: ColorPair::default(),
border: ColorPair::default(),
title: ColorPair::default(),
position: Some(DialogPosition::Center),
size: Some(DialogSize::Small),
confirm_size: Some(DialogSize::Large),
move_size: Some(DialogSize::Custom(70, 14)),
find_visible_results: Some(5),
find_width: Some(40),
go_to_help: GoToHelpTheme::default(),
}
}
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
#[serde(default)]
pub(crate) struct SymlinkTheme {
#[serde(deserialize_with = "deserialize_color_field")]
directory: Color,
#[serde(deserialize_with = "deserialize_color_field")]
file: Color,
#[serde(deserialize_with = "deserialize_color_field")]
target: Color,
}
impl Default for SymlinkTheme {
fn default() -> Self {
Self {
directory: Color::Cyan,
file: Color::Indexed(150),
target: Color::Magenta,
}
}
}
impl SymlinkTheme {
pub(crate) fn directory(&self) -> Color {
self.directory
}
pub(crate) fn file(&self) -> Color {
self.file
}
pub(crate) fn target(&self) -> Color {
self.target
}
}
trait ColorFallback {
fn or(self, fallback: Color) -> Color;
}
impl ColorFallback for Color {
fn or(self, fallback: Color) -> Color {
if let Color::Reset = self {
fallback
} else {
self
}
}
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
#[serde(default)]
pub(super) struct GoToHelpTheme {
size: Option<DialogSize>,
position: Option<DialogPosition>,
}
impl Default for GoToHelpTheme {
fn default() -> Self {
GoToHelpTheme {
size: Some(DialogSize::Custom(58, 3)),
position: Some(DialogPosition::Bottom),
}
}
}
fn deserialize_color_field<'de, D>(deserializer: D) -> Result<Color, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(parse_color(&s))
}
fn rgb(c: (u8, u8, u8)) -> Color {
Color::Rgb(c.0, c.1, c.2)
}
pub(crate) struct Palette {
pub(crate) base: (u8, u8, u8),
pub(crate) surface: (u8, u8, u8),
pub(crate) overlay: (u8, u8, u8),
pub(crate) primary: (u8, u8, u8),
pub(crate) secondary: (u8, u8, u8),
pub(crate) directory: (u8, u8, u8),
}
pub(crate) fn make_theme(name: &str, palette: Palette, icon: &str) -> Theme {
let primary = rgb(palette.primary);
let secondary = rgb(palette.secondary);
let muted = rgb(palette.overlay);
let struct_color = rgb(palette.surface);
let base_bg = rgb(palette.base);
let dir_color = rgb(palette.directory);
Theme {
name: Some(name.to_string()),
accent: ColorPair {
fg: struct_color,
..ColorPair::default()
},
selection: ColorPair {
bg: struct_color,
..ColorPair::default()
},
directory: ColorPair {
fg: dir_color,
..ColorPair::default()
},
separator: ColorPair {
fg: struct_color,
..ColorPair::default()
},
path: ColorPair {
fg: muted,
..ColorPair::default()
},
status_line: ColorPair {
fg: Color::Reset,
bg: base_bg,
},
symlink: SymlinkTheme {
directory: secondary,
file: secondary,
target: Color::Magenta,
},
marker: MarkerTheme {
icon: icon.to_string(),
color: ColorPair {
fg: primary,
..ColorPair::default()
},
clipboard: Some(ColorPair {
fg: secondary,
..ColorPair::default()
}),
},
widget: WidgetTheme {
title: ColorPair {
fg: muted,
..ColorPair::default()
},
border: ColorPair {
fg: struct_color,
..ColorPair::default()
},
..WidgetTheme::default()
},
info: WidgetTheme {
title: ColorPair {
fg: secondary,
..ColorPair::default()
},
border: ColorPair {
fg: struct_color,
..ColorPair::default()
},
..WidgetTheme::default()
},
..Theme::default()
}
}