Skip to content

Commit

Permalink
feat: add help dialog theme configuration support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sarub0b0 committed Oct 9, 2024
1 parent f086756 commit dd552f5
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 60 deletions.
18 changes: 13 additions & 5 deletions example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

5 changes: 5 additions & 0 deletions src/config/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod border;
mod dialog;
mod filter;
mod header;
mod help;
mod search;
mod selection;
mod style;
Expand All @@ -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;

Expand All @@ -35,6 +37,9 @@ pub struct ThemeConfig {

#[serde(default)]
pub component: WidgetThemeConfig,

#[serde(default)]
pub help: HelpThemeConfig,
}

impl From<ThemeConfig> for TabTheme {
Expand Down
2 changes: 1 addition & 1 deletion src/config/theme/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ThemeStyleConfig>,

#[serde(default)]
Expand Down
158 changes: 158 additions & 0 deletions src/config/theme/help.rs
Original file line number Diff line number Diff line change
@@ -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<HelpThemeConfig> 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,
}
}
134 changes: 82 additions & 52 deletions src/features/help/dialog.rs
Original file line number Diff line number Diff line change
@@ -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] = &[
Expand Down Expand Up @@ -229,64 +234,57 @@ struct HelpBlock {
bindings: &'static [KeyBindings],
}

impl HelpBlock {
fn print(&self) -> Vec<String> {
let mut block = Vec::new();

block.push(format!("\x1b[1m[ {} ]\x1b[0m", self.title));
fn print_help_block(block: &HelpBlock, theme: &HelpItemTheme) -> Vec<String> {
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<String> = 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<String> = 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<HelpBlock>,
line
}

impl HelpText {
fn new(blocks: Vec<HelpBlock>) -> Self {
Self { blocks }
}

fn print(&self) -> Vec<String> {
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<String> {
blocks
.iter()
.flat_map(|block| {
let mut lines = print_help_block(block, theme);
lines.push("".to_string());
lines
})
.collect()
}

fn generate() -> Vec<String> {
let mut left = HelpText::new(LEFT_HELP_TEXT.to_vec()).print();
fn generate(theme: HelpItemTheme) -> Vec<String> {
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());

Expand Down Expand Up @@ -321,18 +319,50 @@ fn generate() -> Vec<String> {
.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(),
}
Expand Down
Loading

0 comments on commit dd552f5

Please sign in to comment.