From dd552f590d17b6a4058200cbb2ca203c7b373f70 Mon Sep 17 00:00:00 2001 From: kosay Date: Thu, 10 Oct 2024 01:39:00 +0900 Subject: [PATCH] feat: add help dialog theme configuration support This commit introduces support for configuring the help dialog theme. It includes the addition of a new `HelpThemeConfig` struct, which allows customization of title, key, and description styles. The changes also modify the `HelpDialog` to accept a theme configuration and apply the specified styles to the help dialog elements. This enhancement improves the flexibility and customization of the help dialog appearance. --- example/config.yaml | 18 ++-- src/config/theme.rs | 5 ++ src/config/theme/dialog.rs | 2 +- src/config/theme/help.rs | 158 +++++++++++++++++++++++++++++++++++ src/features/help/dialog.rs | 134 +++++++++++++++++------------ src/ui/widget.rs | 2 +- src/workers/render/window.rs | 2 +- 7 files changed, 261 insertions(+), 60 deletions(-) create mode 100644 src/config/theme/help.rs diff --git a/example/config.yaml b/example/config.yaml index 106cf8029..87fa0aee2 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -99,11 +99,19 @@ theme: modifier: reversed dialog: ## 未設定の場合、theme.base で設定した背景色を使う - base: - fg_color: "#000000" - bg_color: "#ffffff" + # base: + # bg_color: "#000000" size: ## 0.0 ~ 100.0 - width: 50 + width: 85 ## 0.0 ~ 100.0 - height: 50 + height: 85 + ## Help dialog + help: + title: + fg_color: yellow + key: + fg_color: lightcyan + desc: + fg_color: gray + diff --git a/src/config/theme.rs b/src/config/theme.rs index eb4e0b9bc..7cc07e7b0 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -3,6 +3,7 @@ mod border; mod dialog; mod filter; mod header; +mod help; mod search; mod selection; mod style; @@ -19,6 +20,7 @@ pub use self::tab::TabThemeConfig; pub use base::BaseThemeConfig; pub use border::BorderThemeConfig; pub use dialog::DialogThemeConfig; +pub use help::HelpThemeConfig; pub use style::ThemeStyleConfig; pub use widget::WidgetThemeConfig; @@ -35,6 +37,9 @@ pub struct ThemeConfig { #[serde(default)] pub component: WidgetThemeConfig, + + #[serde(default)] + pub help: HelpThemeConfig, } impl From for TabTheme { diff --git a/src/config/theme/dialog.rs b/src/config/theme/dialog.rs index 000b325b6..501935427 100644 --- a/src/config/theme/dialog.rs +++ b/src/config/theme/dialog.rs @@ -6,7 +6,7 @@ use super::ThemeStyleConfig; #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] pub struct DialogThemeConfig { - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub base: Option, #[serde(default)] diff --git a/src/config/theme/help.rs b/src/config/theme/help.rs new file mode 100644 index 000000000..5cd9ef8bc --- /dev/null +++ b/src/config/theme/help.rs @@ -0,0 +1,158 @@ +use ratatui::style::{Color, Modifier}; +use serde::{Deserialize, Serialize}; + +use crate::features::help::HelpItemTheme; + +use super::ThemeStyleConfig; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct HelpThemeConfig { + #[serde(default = "default_title_style")] + pub title: ThemeStyleConfig, + + #[serde(default = "default_key_style")] + pub key: ThemeStyleConfig, + + #[serde(default, alias = "description", alias = "desc")] + pub desc: ThemeStyleConfig, +} + +fn default_title_style() -> ThemeStyleConfig { + ThemeStyleConfig { + modifier: Modifier::BOLD, + ..Default::default() + } +} + +fn default_key_style() -> ThemeStyleConfig { + ThemeStyleConfig { + fg_color: Some(Color::LightCyan), + ..Default::default() + } +} + +impl Default for HelpThemeConfig { + fn default() -> Self { + Self { + title: default_title_style(), + key: default_key_style(), + desc: Default::default(), + } + } +} + +impl From for HelpItemTheme { + fn from(config: HelpThemeConfig) -> Self { + HelpItemTheme { + title_style: config.title.into(), + key_style: config.key.into(), + desc_style: config.desc.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use indoc::indoc; + use pretty_assertions::assert_eq; + use ratatui::style::{Color, Modifier}; + use serde_yaml; + + #[test] + fn test_help_theme_config_defaults() { + let config = HelpThemeConfig::default(); + + // Check default title style + assert_eq!(config.title.modifier, Modifier::BOLD); + assert_eq!(config.title.fg_color, None); + assert_eq!(config.title.bg_color, None); + + // Check default key style + assert_eq!(config.key.fg_color, Some(Color::LightCyan)); + assert_eq!(config.key.modifier, Modifier::empty()); + assert_eq!(config.key.bg_color, None); + + // Check default desc style + assert_eq!(config.desc.fg_color, None); + assert_eq!(config.desc.modifier, Modifier::empty()); + assert_eq!(config.desc.bg_color, None); + } + + #[test] + fn test_help_theme_config_yaml_serialization() { + let config = HelpThemeConfig { + title: ThemeStyleConfig { + fg_color: Some(Color::Yellow), + ..Default::default() + }, + key: ThemeStyleConfig { + fg_color: Some(Color::Cyan), + ..Default::default() + }, + desc: ThemeStyleConfig { + fg_color: Some(Color::Gray), + ..Default::default() + }, + }; + let serialized = serde_yaml::to_string(&config).unwrap(); + + // Expected YAML string + let expected_yaml = indoc! { r#" + title: + fg_color: yellow + key: + fg_color: cyan + desc: + fg_color: gray + "#}; + + assert_eq!(serialized, expected_yaml); + } + + #[test] + fn test_help_theme_config_yaml_deserialization() { + let yaml_str = indoc! { r#" + title: + fg_color: yellow + key: + fg_color: cyan + desc: + fg_color: gray + "# }; + + let deserialized: HelpThemeConfig = serde_yaml::from_str(yaml_str).unwrap(); + let expected = HelpThemeConfig { + title: ThemeStyleConfig { + fg_color: Some(Color::Yellow), + ..Default::default() + }, + key: ThemeStyleConfig { + fg_color: Some(Color::Cyan), + ..Default::default() + }, + desc: ThemeStyleConfig { + fg_color: Some(Color::Gray), + ..Default::default() + }, + }; + + assert_eq!(deserialized, expected); + } + + #[test] + fn test_help_theme_config_from() { + let config = HelpThemeConfig::default(); + let help_item_theme: HelpItemTheme = config.clone().into(); + + assert_eq!(help_item_theme.title_style, config.title.into()); + assert_eq!(help_item_theme.key_style, config.key.into()); + assert_eq!(help_item_theme.desc_style, config.desc.into()); + } + + #[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)] + struct Nested { + #[serde(default)] + help: HelpThemeConfig, + } +} diff --git a/src/features/help/dialog.rs b/src/features/help/dialog.rs index 99880b6a8..a1a6e5719 100644 --- a/src/features/help/dialog.rs +++ b/src/features/help/dialog.rs @@ -1,9 +1,14 @@ +use ratatui::style::{Color, Modifier, Style}; use unicode_width::UnicodeWidthStr; use crate::{ ansi::{AnsiEscapeSequence, TextParser}, + config::theme::ThemeConfig, features::component_id::HELP_DIALOG_ID, - ui::widget::{Text, Widget, WidgetBase}, + ui::widget::{ + ansi_color::style_to_ansi, SearchForm, SearchFormTheme, Text, TextTheme, Widget, + WidgetBase, WidgetTheme, + }, }; const LEFT_HELP_TEXT: &[HelpBlock] = &[ @@ -229,64 +234,57 @@ struct HelpBlock { bindings: &'static [KeyBindings], } -impl HelpBlock { - fn print(&self) -> Vec { - let mut block = Vec::new(); - - block.push(format!("\x1b[1m[ {} ]\x1b[0m", self.title)); +fn print_help_block(block: &HelpBlock, theme: &HelpItemTheme) -> Vec { + let mut line = Vec::new(); - let max_key_len = self - .bindings - .iter() - .map(|b| b.keys().width()) - .max() - .expect("no bindings"); + line.push(format!( + "{}[ {} ]\x1b[39m", + style_to_ansi(theme.title_style), + block.title + )); - let lines: Vec = self - .bindings - .iter() - .map(|b| { - format!( - "\x1b[96m{:>pad$}:\x1b[0m {}", - b.keys(), - b.desc(), - pad = max_key_len - ) - }) - .collect(); + let max_key_len = block + .bindings + .iter() + .map(|b| b.keys().width()) + .max() + .expect("no bindings"); - block.extend(lines); + let lines: Vec = block + .bindings + .iter() + .map(|b| { + format!( + "{}{:>pad$}:\x1b[39m {}{}", + style_to_ansi(theme.key_style), + b.keys(), + style_to_ansi(theme.desc_style), + b.desc(), + pad = max_key_len + ) + }) + .collect(); - block - } -} + line.extend(lines); -#[derive(Clone)] -struct HelpText { - blocks: Vec, + line } -impl HelpText { - fn new(blocks: Vec) -> Self { - Self { blocks } - } - - fn print(&self) -> Vec { - self.blocks - .iter() - .flat_map(|b| { - let mut b = b.print(); - b.push("".to_string()); - b - }) - .collect() - } +fn print_help_blocks(blocks: &[HelpBlock], theme: &HelpItemTheme) -> Vec { + blocks + .iter() + .flat_map(|block| { + let mut lines = print_help_block(block, theme); + lines.push("".to_string()); + lines + }) + .collect() } -fn generate() -> Vec { - let mut left = HelpText::new(LEFT_HELP_TEXT.to_vec()).print(); +fn generate(theme: HelpItemTheme) -> Vec { + let mut left = print_help_blocks(LEFT_HELP_TEXT, &theme); - let mut right = HelpText::new(RIGHT_HELP_TEXT.to_vec()).print(); + let mut right = print_help_blocks(RIGHT_HELP_TEXT, &theme); let len = left.len().max(right.len()); @@ -321,18 +319,50 @@ fn generate() -> Vec { .collect() } +#[derive(Clone)] +pub struct HelpItemTheme { + pub title_style: Style, + pub key_style: Style, + pub desc_style: Style, +} + +impl Default for HelpItemTheme { + fn default() -> Self { + Self { + title_style: Style::default().add_modifier(Modifier::BOLD), + key_style: Style::default().fg(Color::LightCyan), + desc_style: Style::default(), + } + } +} + #[derive(Debug)] pub struct HelpDialog { pub widget: Widget<'static>, } impl HelpDialog { - pub fn new() -> Self { + pub fn new(theme: ThemeConfig) -> Self { + let widget_theme = WidgetTheme::from(theme.component.clone()); + let text_theme = TextTheme::from(theme.component.clone()); + let search_theme = SearchFormTheme::from(theme.component.clone()); + + let widget_base = WidgetBase::builder() + .title("Help") + .theme(widget_theme) + .build(); + + let search_form = SearchForm::builder().theme(search_theme).build(); + + let item_theme = HelpItemTheme::from(theme.help.clone()); + Self { widget: Text::builder() .id(HELP_DIALOG_ID) - .widget_base(WidgetBase::builder().title("Help").build()) - .items(generate()) + .widget_base(widget_base) + .search_form(search_form) + .theme(text_theme) + .items(generate(item_theme)) .build() .into(), } diff --git a/src/ui/widget.rs b/src/ui/widget.rs index f5a29a94f..189fb417d 100644 --- a/src/ui/widget.rs +++ b/src/ui/widget.rs @@ -1,4 +1,4 @@ -mod ansi_color; +pub mod ansi_color; mod clear; mod line; mod styled_graphemes; diff --git a/src/workers/render/window.rs b/src/workers/render/window.rs index 88a0e2e70..b67ab325a 100644 --- a/src/workers/render/window.rs +++ b/src/workers/render/window.rs @@ -239,7 +239,7 @@ impl WindowInit { let HelpDialog { widget: help_dialog, - } = HelpDialog::new(); + } = HelpDialog::new(self.theme.clone()); let YamlDialog { widget: yaml_dialog,