From 9738f4fb04d9993e4c3b0763cd950eee4da3fdbd Mon Sep 17 00:00:00 2001 From: Lucas Pickering Date: Fri, 3 May 2024 20:14:42 -0400 Subject: [PATCH] Make colors customizable Primary, secondary, and error colors can be customized. More things to be added in the future (e.g. pane sizing). Also, change default color schema to blue+yellow. Closes #193 --- CHANGELOG.md | 2 + Cargo.lock | 5 + Cargo.toml | 2 +- docs/src/SUMMARY.md | 1 + docs/src/api/configuration/index.md | 1 + docs/src/api/configuration/theme.md | 22 +++ src/config.rs | 9 +- src/tui.rs | 2 +- src/tui/context.rs | 9 +- src/tui/view.rs | 2 +- src/tui/view/common.rs | 2 +- src/tui/view/common/button.rs | 4 +- src/tui/view/common/list.rs | 2 +- src/tui/view/common/modal.rs | 6 +- src/tui/view/common/table.rs | 20 ++- src/tui/view/common/tabs.rs | 2 +- src/tui/view/common/template_preview.rs | 12 +- src/tui/view/common/text_box.rs | 10 +- src/tui/view/common/text_window.rs | 4 +- src/tui/view/component/help.rs | 2 +- src/tui/view/component/recipe_list.rs | 2 +- src/tui/view/theme.rs | 222 +++++++++++++----------- 22 files changed, 202 insertions(+), 141 deletions(-) create mode 100644 docs/src/api/configuration/theme.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 24336df1..3b990d79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Add option to chain values from response header rather than body ([#184](https://github.com/LucasPickering/slumber/issues/184)) - Add action to save response body to file ([#183](https://github.com/LucasPickering/slumber/issues/183)) +- Add `theme` field to the config, to configure colors ([#193](https://github.com/LucasPickering/slumber/issues/193)) + - [See docs](https://slumber.lucaspickering.me/book/api/configuration/theme.html) for more info ### Changed diff --git a/Cargo.lock b/Cargo.lock index 557205ed..48b2293b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,9 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +dependencies = [ + "serde", +] [[package]] name = "block" @@ -330,6 +333,7 @@ dependencies = [ "cfg-if", "itoa", "ryu", + "serde", "static_assertions", ] @@ -1438,6 +1442,7 @@ dependencies = [ "itertools", "lru", "paste", + "serde", "stability", "strum", "unicode-segmentation", diff --git a/Cargo.toml b/Cargo.toml index 98229d51..ac050b5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ nom = "7.1.3" notify = {version = "^6.1.1", default-features = false, features = ["macos_fsevent"]} open = "5.1.1" pretty_assertions = "1.4.0" -ratatui = {version = "^0.26.0", features = ["unstable-rendered-line-info"]} +ratatui = {version = "^0.26.0", features = ["serde", "unstable-rendered-line-info"]} reqwest = {version = "^0.11.20", default-features = false, features = ["rustls-tls"]} rmp-serde = "^1.1.2" rusqlite = {version = "^0.30.0", default-features = false, features = ["bundled", "chrono", "uuid"]} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 9e5ebc97..42560f99 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -35,6 +35,7 @@ - [Content Type](./api/request_collection/content_type.md) - [Configuration](./api/configuration/index.md) - [Input Bindings](./api/configuration/input_bindings.md) + - [Theme](./api/configuration/theme.md) # Troubleshooting diff --git a/docs/src/api/configuration/index.md b/docs/src/api/configuration/index.md index 52ec0f32..ed2cecbf 100644 --- a/docs/src/api/configuration/index.md +++ b/docs/src/api/configuration/index.md @@ -26,3 +26,4 @@ If the root directory doesn't exist yet, you can create it yourself or have Slum | `preview_templates` | `boolean` | Render template values in the TUI? If false, the raw template will be shown. | `true` | | `ignore_certificate_hosts` | `string[]` | Hostnames whose TLS certificate errors will be ignored. [More info](../../troubleshooting/tls.md) | `[]` | | `input_bindings` | `mapping[Action, KeyCombination[]]` | Override default input bindings. [More info](./input_bindings.md) | `{}` | +| `theme` | [`Theme`](./theme.md) | Visual customizations | `{}` | diff --git a/docs/src/api/configuration/theme.md b/docs/src/api/configuration/theme.md new file mode 100644 index 00000000..084c4c5d --- /dev/null +++ b/docs/src/api/configuration/theme.md @@ -0,0 +1,22 @@ +# Theme + +Theming allows you to customize the appearance of the Slumber TUI. To start, [open up your configuration file](../configuration/index.md#location--creation) and add some theme settings: + +```yaml +theme: + primary_color: green + secondary_color: blue +``` + +## Fields + +| Field | Type | Description | +| -------------------- | ------- | -------------------------------------------------------------------- | +| `primary_color` | `Color` | Color of most emphasized content | +| `primary_text_color` | `Color` | Color of text on top of the primary color (generally white or black) | +| `secondary_color` | `Color` | Color of secondary notable content | +| `error_color` | `Color` | Color representing error messages | + +## Color Format + +Colors can be specified as names (e.g. "yellow"), RGB codes (e.g. `#ffff00`) or ANSI color indexes. See the [Ratatui docs](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html#impl-FromStr-for-Color) for more details on color deserialization. diff --git a/src/config.rs b/src/config.rs index 37d7d795..60f087eb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,8 @@ use crate::{ - tui::input::{Action, InputBinding}, + tui::{ + input::{Action, InputBinding}, + view::Theme, + }, util::{ parse_yaml, paths::{DataDirectory, FileGuard}, @@ -25,9 +28,10 @@ pub struct Config { /// Should templates be rendered inline in the UI, or should we show the /// raw text? pub preview_templates: bool, - /// Overrides for default key bindings pub input_bindings: IndexMap, + /// Visual configuration for the TUI (e.g. colors) + pub theme: Theme, } impl Config { @@ -70,6 +74,7 @@ impl Default for Config { ignore_certificate_hosts: Vec::new(), preview_templates: true, input_bindings: IndexMap::default(), + theme: Theme::default(), } } } diff --git a/src/tui.rs b/src/tui.rs index 0d12fc27..1447b5cc 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -2,7 +2,7 @@ pub mod context; pub mod input; pub mod message; mod util; -mod view; +pub mod view; use crate::{ collection::{Collection, CollectionFile, ProfileId, RecipeId}, diff --git a/src/tui/context.rs b/src/tui/context.rs index 5aaf2fdf..e6d52f78 100644 --- a/src/tui/context.rs +++ b/src/tui/context.rs @@ -2,7 +2,7 @@ use crate::{ config::Config, db::CollectionDatabase, http::HttpEngine, - tui::{input::InputEngine, view::Theme}, + tui::{input::InputEngine, view::Styles}, }; use std::sync::OnceLock; @@ -22,8 +22,8 @@ static CONTEXT: OnceLock = OnceLock::new(); pub struct TuiContext { /// App-level configuration pub config: Config, - /// Visual theme. Colors! - pub theme: Theme, + /// Visual styles, derived from the theme + pub styles: Styles, /// Input:action bindings pub input_engine: InputEngine, /// For sending HTTP requests @@ -36,12 +36,13 @@ pub struct TuiContext { impl TuiContext { /// Initialize global context. Should be called only once, during startup. pub fn init(config: Config, database: CollectionDatabase) { + let styles = Styles::new(&config.theme); let input_engine = InputEngine::new(config.input_bindings.clone()); let http_engine = HttpEngine::new(&config, database.clone()); CONTEXT .set(Self { config, - theme: Theme::default(), + styles, input_engine, http_engine, database, diff --git a/src/tui/view.rs b/src/tui/view.rs index e83f6a42..60438eb5 100644 --- a/src/tui/view.rs +++ b/src/tui/view.rs @@ -8,7 +8,7 @@ mod util; pub use common::modal::{IntoModal, ModalPriority}; pub use state::RequestState; -pub use theme::Theme; +pub use theme::{Styles, Theme}; pub use util::{Confirm, PreviewPrompter}; use crate::{ diff --git a/src/tui/view/common.rs b/src/tui/view/common.rs index a2a81ee6..bb4d453a 100644 --- a/src/tui/view/common.rs +++ b/src/tui/view/common.rs @@ -43,7 +43,7 @@ impl<'a> Generate for Pane<'a> { Self: 'this, { let (border_type, border_style) = - TuiContext::get().theme.pane.border(self.is_focused); + TuiContext::get().styles.pane.border(self.is_focused); Block::default() .borders(Borders::ALL) .border_type(border_type) diff --git a/src/tui/view/common/button.rs b/src/tui/view/common/button.rs index 6caecb58..2851727e 100644 --- a/src/tui/view/common/button.rs +++ b/src/tui/view/common/button.rs @@ -33,11 +33,11 @@ impl<'a> Generate for Button<'a> { where Self: 'this, { - let theme = &TuiContext::get().theme; + let styles = &TuiContext::get().styles; Span { content: self.text.into(), style: if self.is_focused { - theme.text.highlight + styles.text.highlight } else { Default::default() }, diff --git a/src/tui/view/common/list.rs b/src/tui/view/common/list.rs index c994b1ba..20757767 100644 --- a/src/tui/view/common/list.rs +++ b/src/tui/view/common/list.rs @@ -33,6 +33,6 @@ where ratatui::widgets::List::new(items) .block(block) - .highlight_style(TuiContext::get().theme.list.highlight) + .highlight_style(TuiContext::get().styles.list.highlight) } } diff --git a/src/tui/view/common/modal.rs b/src/tui/view/common/modal.rs index 44480456..7ff43d5f 100644 --- a/src/tui/view/common/modal.rs +++ b/src/tui/view/common/modal.rs @@ -136,7 +136,7 @@ impl EventHandler for ModalQueue { impl Draw for ModalQueue { fn draw(&self, frame: &mut Frame, _: (), area: Rect) { if let Some(modal) = self.queue.front() { - let theme = &TuiContext::get().theme; + let styles = &TuiContext::get().styles; let (width, height) = modal.dimensions(); // The child gave us the content dimensions, we need to add one cell @@ -150,8 +150,8 @@ impl Draw for ModalQueue { let block = Block::default() .title(modal.title()) .borders(Borders::ALL) - .border_style(theme.modal.border) - .border_type(theme.modal.border_type); + .border_style(styles.modal.border) + .border_type(styles.modal.border_type); let inner_area = block.inner(area); // Draw the outline of the modal diff --git a/src/tui/view/common/table.rs b/src/tui/view/common/table.rs index fa0c73b1..af9dd71f 100644 --- a/src/tui/view/common/table.rs +++ b/src/tui/view/common/table.rs @@ -75,30 +75,32 @@ impl<'a, const COLS: usize> Generate for Table<'a, COLS, Row<'a>> { where Self: 'this, { - let theme = &TuiContext::get().theme; + let styles = &TuiContext::get().styles; let rows = self.rows.into_iter().enumerate().map(|(i, row)| { // Apply theme styles, but let the row's individual styles override let base_style = if self.alternate_row_style && i % 2 == 1 { - theme.table.alt + styles.table.alt } else { - theme.table.text + styles.table.text }; let row_style = Styled::style(&row); row.set_style(base_style.patch(row_style)) }); let mut table = ratatui::widgets::Table::new(rows, self.column_widths) - .highlight_style(theme.table.highlight); + .highlight_style(styles.table.highlight); // Add title if let Some(title) = self.title { table = table.block( - Block::default().title(title).title_style(theme.table.title), + Block::default() + .title(title) + .title_style(styles.table.title), ); } // Add optional header if given if let Some(header) = self.header { - table = table.header(Row::new(header).style(theme.table.header)); + table = table.header(Row::new(header).style(styles.table.header)); } table @@ -139,7 +141,7 @@ where where Self: 'this, { - let theme = &TuiContext::get().theme; + let styles = &TuiContext::get().styles; // Include the given cells, then tack on the checkbox for enabled state Row::new( iter::once( @@ -152,9 +154,9 @@ where .chain(self.cells.into_iter().map(Cell::from)), ) .style(if self.enabled { - theme.table.text + styles.table.text } else { - theme.table.disabled + styles.table.disabled }) } } diff --git a/src/tui/view/common/tabs.rs b/src/tui/view/common/tabs.rs index 135644b0..bb81efeb 100644 --- a/src/tui/view/common/tabs.rs +++ b/src/tui/view/common/tabs.rs @@ -63,7 +63,7 @@ where frame.render_widget( ratatui::widgets::Tabs::new(T::iter().map(|e| e.to_string())) .select(self.tabs.selected_index()) - .highlight_style(TuiContext::get().theme.tab.highlight), + .highlight_style(TuiContext::get().styles.tab.highlight), area, ) } diff --git a/src/tui/view/common/template_preview.rs b/src/tui/view/common/template_preview.rs index e60e756c..bccf0e3c 100644 --- a/src/tui/view/common/template_preview.rs +++ b/src/tui/view/common/template_preview.rs @@ -110,7 +110,7 @@ impl<'a> TextStitcher<'a> { template: &'a Template, chunks: &'a [TemplateChunk], ) -> Text<'a> { - let theme = &TuiContext::get().theme; + let styles = &TuiContext::get().styles; // Each chunk will get its own styling, but we can't just make each // chunk a Span, because one chunk might have multiple lines. And we @@ -122,8 +122,8 @@ impl<'a> TextStitcher<'a> { let chunk_text = Self::get_chunk_text(template, chunk); let style = match &chunk { TemplateChunk::Raw(_) => Style::default(), - TemplateChunk::Rendered { .. } => theme.template_preview.text, - TemplateChunk::Error(_) => theme.template_preview.error, + TemplateChunk::Rendered { .. } => styles.template_preview.text, + TemplateChunk::Error(_) => styles.template_preview.error, }; stitcher.add_chunk(chunk_text, style); @@ -230,11 +230,11 @@ mod tests { selected_profile: Some(profile_id), ); let chunks = template.render_chunks(&context).await; - let theme = &TuiContext::get().theme; + let styles = &TuiContext::get().styles; let text = TextStitcher::stitch_chunks(&template, &chunks); - let rendered_style = theme.template_preview.text; - let error_style = theme.template_preview.error; + let rendered_style = styles.template_preview.text; + let error_style = styles.template_preview.error; let expected = Text::from(vec![ Line::from("intro"), Line::from(Span::styled("🧡", rendered_style)), diff --git a/src/tui/view/common/text_box.rs b/src/tui/view/common/text_box.rs index 28b142a6..ac337274 100644 --- a/src/tui/view/common/text_box.rs +++ b/src/tui/view/common/text_box.rs @@ -215,12 +215,12 @@ impl EventHandler for TextBox { impl Draw for TextBox { fn draw(&self, frame: &mut Frame, _: (), area: Rect) { - let theme = &TuiContext::get().theme; + let styles = &TuiContext::get().styles; // Hide top secret data let text: Text = if self.state.text.is_empty() { Line::from(self.placeholder_text.as_str()) - .style(theme.text_box.placeholder) + .style(styles.text_box.placeholder) .into() } else if self.sensitive { Masked::new(&self.state.text, '•').into() @@ -230,9 +230,9 @@ impl Draw for TextBox { // Draw the text let style = if self.is_valid() { - theme.text_box.text + styles.text_box.text } else { - theme.text_box.invalid + styles.text_box.invalid }; frame.render_widget(Paragraph::new(text).style(style), area); @@ -246,7 +246,7 @@ impl Draw for TextBox { }; frame .buffer_mut() - .set_style(cursor_area, theme.text_box.cursor); + .set_style(cursor_area, styles.text_box.cursor); } } } diff --git a/src/tui/view/common/text_window.rs b/src/tui/view/common/text_window.rs index c07aea09..9c48019a 100644 --- a/src/tui/view/common/text_window.rs +++ b/src/tui/view/common/text_window.rs @@ -117,7 +117,7 @@ where for<'a> &'a T: Generate = Text<'a>>, { fn draw(&self, frame: &mut Frame, _: (), area: Rect) { - let theme = &TuiContext::get().theme; + let styles = &TuiContext::get().styles; let text = Paragraph::new(self.text.generate()); // Assume no line wrapping when calculating line count let text_height = text.line_count(u16::MAX) as u16; @@ -146,7 +146,7 @@ where .collect::>(), ) .alignment(Alignment::Right) - .style(theme.text_window.line_number), + .style(styles.text_window.line_number), gutter_area, ); diff --git a/src/tui/view/component/help.rs b/src/tui/view/component/help.rs index 1f0dfb1c..f2bf3a3d 100644 --- a/src/tui/view/component/help.rs +++ b/src/tui/view/component/help.rs @@ -41,7 +41,7 @@ impl Draw for HelpFooter { frame.render_widget( Paragraph::new(text) .alignment(Alignment::Right) - .style(tui_context.theme.text.highlight), + .style(tui_context.styles.text.highlight), area, ); } diff --git a/src/tui/view/component/recipe_list.rs b/src/tui/view/component/recipe_list.rs index 820762d9..3016da13 100644 --- a/src/tui/view/component/recipe_list.rs +++ b/src/tui/view/component/recipe_list.rs @@ -203,7 +203,7 @@ impl Draw for RecipeListPane { .collect_vec(); let list = ratatui::widgets::List::new(items) .block(pane.generate()) - .highlight_style(context.theme.list.highlight); + .highlight_style(context.styles.list.highlight); frame.render_stateful_widget( list, diff --git a/src/tui/view/theme.rs b/src/tui/view/theme.rs index a62da2b4..323df6de 100644 --- a/src/tui/view/theme.rs +++ b/src/tui/view/theme.rs @@ -2,46 +2,131 @@ use ratatui::{ style::{Color, Modifier, Style}, widgets::BorderType, }; +use serde::{Deserialize, Serialize}; -/// Configurable visual settings for the UI. Styles are grouped into sub-structs -/// generally by component. -#[derive(Debug)] +/// User-configurable visual settings. These are used to generate the full style +/// set. +#[derive(Debug, Serialize, Deserialize)] +#[serde(default)] pub struct Theme { - pub list: ThemeList, - pub modal: ThemeModal, - pub pane: ThemePane, - pub tab: ThemeTab, - pub table: ThemeTable, - pub template_preview: ThemeTemplatePreview, - pub text: ThemeText, - pub text_box: ThemeTextBox, - pub text_window: ThemeTextWindow, + pub primary_color: Color, + /// Theoretically we could calculate this bsed on primary color, but for + /// named or indexed colors, we don't know the exact RGB code since it + /// depends on the user's terminal theme. It's much easier and less + /// fallible to just have the user specify it. + pub primary_text_color: Color, + pub secondary_color: Color, + pub error_color: Color, } -impl Theme { - // Ideally these should be part of the theme, but that requires some sort of - // two-stage themeing - pub const PRIMARY_COLOR: Color = Color::LightGreen; - pub const ERROR_COLOR: Color = Color::Red; +impl Default for Theme { + fn default() -> Self { + Self { + primary_color: Color::Blue, + primary_text_color: Color::White, + secondary_color: Color::Yellow, + error_color: Color::Red, + } + } +} + +/// Concrete styles for the TUI, generated from the theme. We *could* make this +/// entire thing user-configurable, but that would be way too complex. The theme +/// provides users some basic settings, then we figure out the minutae from +/// there. Styles are grouped into sub-structs generally by component. +#[derive(Debug)] +pub struct Styles { + pub list: ListStyles, + pub modal: ModalStyles, + pub pane: PaneStyles, + pub tab: TabStyles, + pub table: TableStyles, + pub template_preview: TemplatePreviewStyles, + pub text: TextStyle, + pub text_box: TextBoxStyle, + pub text_window: TextWindowStyle, +} + +impl Styles { + pub fn new(theme: &Theme) -> Self { + Self { + list: ListStyles { + highlight: Style::default() + .bg(theme.primary_color) + .fg(theme.primary_text_color) + .add_modifier(Modifier::BOLD), + }, + modal: ModalStyles { + border: Style::default().fg(theme.primary_color), + border_type: BorderType::Double, + }, + pane: PaneStyles { + border: Style::default(), + border_selected: Style::default() + .fg(theme.primary_color) + .add_modifier(Modifier::BOLD), + border_type: BorderType::Plain, + border_type_selected: BorderType::Double, + }, + tab: TabStyles { + highlight: Style::default() + .fg(theme.primary_color) + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::UNDERLINED), + }, + table: TableStyles { + header: Style::default() + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::UNDERLINED), + text: Style::default(), + alt: Style::default().bg(Color::DarkGray), + disabled: Style::default().add_modifier(Modifier::DIM), + highlight: Style::default() + .bg(theme.primary_color) + .fg(theme.primary_text_color) + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::UNDERLINED), + title: Style::default().add_modifier(Modifier::BOLD), + }, + template_preview: TemplatePreviewStyles { + text: Style::default().fg(theme.secondary_color), + error: Style::default().bg(theme.error_color), + }, + text: TextStyle { + highlight: Style::default() + .fg(theme.primary_text_color) + .bg(theme.primary_color), + }, + text_box: TextBoxStyle { + text: Style::default().bg(Color::DarkGray), + cursor: Style::default().bg(Color::White).fg(Color::Black), + placeholder: Style::default().fg(Color::Black), + invalid: Style::default().bg(Color::LightRed), + }, + text_window: TextWindowStyle { + line_number: Style::default().fg(Color::DarkGray), + }, + } + } } /// Styles for List component #[derive(Debug)] -pub struct ThemeList { +pub struct ListStyles { /// Highlighted item in a list pub highlight: Style, } /// Styles for the Modal component #[derive(Debug)] -pub struct ThemeModal { +pub struct ModalStyles { pub border: Style, pub border_type: BorderType, } /// Styles for Pane component #[derive(Debug)] -pub struct ThemePane { +pub struct PaneStyles { /// Pane border when not selected/focused pub border: Style, /// Pane border when selected/focused @@ -52,16 +137,27 @@ pub struct ThemePane { pub border_type_selected: BorderType, } +impl PaneStyles { + /// Get the type and style of the border for a pane + pub fn border(&self, is_focused: bool) -> (BorderType, Style) { + if is_focused { + (self.border_type_selected, self.border_selected) + } else { + (self.border_type, self.border) + } + } +} + /// Styles for Tab component #[derive(Debug)] -pub struct ThemeTab { +pub struct TabStyles { /// Highlighted tab in a tab group pub highlight: Style, } /// Styles for Table component #[derive(Debug)] -pub struct ThemeTable { +pub struct TableStyles { /// Table column header text pub header: Style, pub text: Style, @@ -73,21 +169,21 @@ pub struct ThemeTable { /// Styles for TemplatePreview component #[derive(Debug)] -pub struct ThemeTemplatePreview { +pub struct TemplatePreviewStyles { pub text: Style, pub error: Style, } /// General text styles #[derive(Debug)] -pub struct ThemeText { +pub struct TextStyle { /// Text that needs some visual emphasis/separation pub highlight: Style, } /// Styles for TextBox component #[derive(Debug)] -pub struct ThemeTextBox { +pub struct TextBoxStyle { pub text: Style, pub cursor: Style, pub placeholder: Style, @@ -96,81 +192,7 @@ pub struct ThemeTextBox { /// Styles for TextWindow component #[derive(Debug)] -pub struct ThemeTextWindow { +pub struct TextWindowStyle { /// Line numbers on large text areas pub line_number: Style, } - -impl Default for Theme { - fn default() -> Self { - Self { - list: ThemeList { - highlight: Style::default() - .bg(Self::PRIMARY_COLOR) - .fg(Color::Black) - .add_modifier(Modifier::BOLD), - }, - modal: ThemeModal { - border: Style::default().fg(Self::PRIMARY_COLOR), - border_type: BorderType::Double, - }, - pane: ThemePane { - border: Style::default(), - border_selected: Style::default() - .fg(Self::PRIMARY_COLOR) - .add_modifier(Modifier::BOLD), - border_type: BorderType::Plain, - border_type_selected: BorderType::Double, - }, - tab: ThemeTab { - highlight: Style::default() - .fg(Self::PRIMARY_COLOR) - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::UNDERLINED), - }, - table: ThemeTable { - header: Style::default() - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::UNDERLINED), - text: Style::default(), - alt: Style::default().bg(Color::DarkGray), - disabled: Style::default().add_modifier(Modifier::DIM), - highlight: Style::default() - .bg(Self::PRIMARY_COLOR) - .fg(Color::Black) - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::UNDERLINED), - title: Style::default().add_modifier(Modifier::BOLD), - }, - template_preview: ThemeTemplatePreview { - text: Style::default().fg(Color::Blue), - error: Style::default().bg(Self::ERROR_COLOR), - }, - text: ThemeText { - highlight: Style::default() - .fg(Color::Black) - .bg(Self::PRIMARY_COLOR), - }, - text_box: ThemeTextBox { - text: Style::default().bg(Color::DarkGray), - cursor: Style::default().bg(Color::White).fg(Color::Black), - placeholder: Style::default().fg(Color::Black), - invalid: Style::default().bg(Color::LightRed), - }, - text_window: ThemeTextWindow { - line_number: Style::default().fg(Color::DarkGray), - }, - } - } -} - -impl ThemePane { - /// Get the type and style of the border for a pane - pub fn border(&self, is_focused: bool) -> (BorderType, Style) { - if is_focused { - (self.border_type_selected, self.border_selected) - } else { - (self.border_type, self.border) - } - } -}