Repository: tarkah/tickrs (language: rust)

Issue:
Custom theming and keybinds
Add config file with same settings as command line. Would also be nice to add support for custom theme colors and key bindings

As suggested by @rafi here: https://github.com/tarkah/tickrs/issues/62#issuecomment-778013840


Discussion / hints:
Config file support has been added. Will look into theming and keybinds next.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e79ddd..037620b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,11 @@ and `Removed`.
 
 ## [Unreleased]
 
+### Added
+
+- Custom themes can now be applied. See the [themes wiki] entry for more
+  information ([#69])
+
 ## [0.11.0] - 2021-02-12
 
 ### Added
@@ -87,4 +92,6 @@ and `Removed`.
 [#59]: https://github.com/tarkah/tickrs/pull/59
 [#63]: https://github.com/tarkah/tickrs/pull/63
 [#66]: https://github.com/tarkah/tickrs/pull/66
-[#67]: https://github.com/tarkah/tickrs/pull/67
\ No newline at end of file
+[#67]: https://github.com/tarkah/tickrs/pull/67
+[#69]: https://github.com/tarkah/tickrs/pull/69
+[themes wiki]: https://github.com/tarkah/tickrs/wiki/Themes
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 1b7b499..fb11e1c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1425,6 +1425,7 @@ dependencies = [
  "bitflags",
  "cassowary",
  "crossterm 0.18.2",
+ "serde",
  "unicode-segmentation",
  "unicode-width",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 3769db6..5c03eab 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -35,4 +35,4 @@ futures = "0.3"
 
 better-panic = "0.2"
 crossterm = "0.19"
-tui = { version = "0.14", default-features = false, features = ["crossterm"] }
+tui = { version = "0.14", default-features = false, features = ["crossterm","serde"] }
diff --git a/src/draw.rs b/src/draw.rs
index 7ef6a78..7701282 100644
--- a/src/draw.rs
+++ b/src/draw.rs
@@ -1,6 +1,6 @@
 use tui::backend::Backend;
 use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
-use tui::style::{Color, Style};
+use tui::style::Style;
 use tui::text::{Span, Spans, Text};
 use tui::widgets::{Block, Borders, Paragraph, Tabs, Wrap};
 use tui::{Frame, Terminal};
@@ -10,7 +10,7 @@ use crate::common::TimeFrame;
 use crate::widget::{
     block, AddStockWidget, OptionsWidget, StockSummaryWidget, StockWidget, HELP_HEIGHT, HELP_WIDTH,
 };
-use crate::SHOW_VOLUMES;
+use crate::{SHOW_VOLUMES, THEME};
 
 pub fn draw<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) {
     let current_size = terminal.size().unwrap_or_default();
@@ -25,6 +25,12 @@ pub fn draw<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) {
 
     terminal
         .draw(|mut frame| {
+            // Set background color
+            frame.render_widget(
+                Block::default().style(Style::default().bg(THEME.background())),
+                frame.size(),
+            );
+
             if app.debug.enabled && app.mode == Mode::AddStock {
                 // layout[0] - Main window
                 // layout[1] - Add Stock window
@@ -100,7 +106,7 @@ fn draw_main<B: Backend>(frame: &mut Frame<B>, app: &mut App, area: Rect) {
         .split(area);
 
     if !app.stocks.is_empty() {
-        frame.render_widget(crate::widget::block::new(" Tabs ", None), layout[0]);
+        frame.render_widget(crate::widget::block::new(" Tabs "), layout[0]);
         layout[0] = add_padding(layout[0], 1, PaddingDirection::All);
 
         // header[0] - Stock symbol tabs
@@ -121,8 +127,8 @@ fn draw_main<B: Backend>(frame: &mut Frame<B>, app: &mut App, area: Rect) {
             frame.render_widget(
                 Tabs::new(tabs)
                     .select(app.current_tab)
-                    .style(Style::default().fg(Color::Cyan))
-                    .highlight_style(Style::default().fg(Color::Yellow)),
+                    .style(Style::default().fg(THEME.text_secondary))
+                    .highlight_style(Style::default().fg(THEME.text_primary)),
                 header[0],
             );
         }
@@ -131,7 +137,11 @@ fn draw_main<B: Backend>(frame: &mut Frame<B>, app: &mut App, area: Rect) {
         if !app.hide_help {
             frame.render_widget(
                 Paragraph::new(Text::styled("Help '?'", Style::default()))
-                    .style(Style::reset())
+                    .style(
+                        Style::default()
+                            .fg(THEME.text_normal)
+                            .bg(THEME.background()),
+                    )
                     .alignment(Alignment::Center),
                 header[1],
             );
@@ -183,7 +193,7 @@ fn draw_add_stock<B: Backend>(frame: &mut Frame<B>, app: &mut App, area: Rect) {
 }
 
 fn draw_summary<B: Backend>(frame: &mut Frame<B>, app: &mut App, mut area: Rect) {
-    let border = block::new(" Summary ", None);
+    let border = block::new(" Summary ");
     frame.render_widget(border, area);
     area = add_padding(area, 1, PaddingDirection::All);
     area = add_padding(area, 1, PaddingDirection::Right);
@@ -255,7 +265,11 @@ fn draw_summary<B: Backend>(frame: &mut Frame<B>, app: &mut App, mut area: Rect)
     if !app.hide_help {
         frame.render_widget(
             Paragraph::new(Text::styled("Help '?'", Style::default()))
-                .style(Style::reset())
+                .style(
+                    Style::default()
+                        .fg(THEME.text_normal)
+                        .bg(THEME.background()),
+                )
                 .alignment(Alignment::Center),
             header[1],
         );
@@ -283,9 +297,11 @@ fn draw_summary<B: Backend>(frame: &mut Frame<B>, app: &mut App, mut area: Rect)
         layout[2] = add_padding(layout[2], offset, PaddingDirection::Top);
 
         frame.render_widget(
-            Block::default()
-                .borders(Borders::TOP)
-                .border_style(Style::reset()),
+            Block::default().borders(Borders::TOP).border_style(
+                Style::default()
+                    .fg(THEME.border_secondary)
+                    .bg(THEME.background()),
+            ),
             layout[2],
         );
 
@@ -305,8 +321,8 @@ fn draw_summary<B: Backend>(frame: &mut Frame<B>, app: &mut App, mut area: Rect)
 
         let tabs = Tabs::new(time_frames)
             .select(app.summary_time_frame.idx())
-            .style(Style::default().fg(Color::Cyan))
-            .highlight_style(Style::default().fg(Color::Yellow));
+            .style(Style::default().fg(THEME.text_secondary))
+            .highlight_style(Style::default().fg(THEME.text_primary));
 
         frame.render_widget(tabs, bottom_layout[0]);
 
@@ -316,17 +332,17 @@ fn draw_summary<B: Backend>(frame: &mut Frame<B>, app: &mut App, mut area: Rect)
         let up_arrow = Span::styled(
             "ᐱ",
             Style::default().fg(if more_up {
-                Color::Reset
+                THEME.text_normal
             } else {
-                Color::DarkGray
+                THEME.gray
             }),
         );
         let down_arrow = Span::styled(
             "ᐯ",
             Style::default().fg(if more_down {
-                Color::Reset
+                THEME.text_normal
             } else {
-                Color::DarkGray
+                THEME.gray
             }),
         );
 
diff --git a/src/main.rs b/src/main.rs
index b8db723..6d46630 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -23,6 +23,7 @@ mod event;
 mod opts;
 mod service;
 mod task;
+mod theme;
 mod widget;
 
 lazy_static! {
@@ -40,6 +41,7 @@ lazy_static! {
     pub static ref TRUNC_PRE: bool = OPTS.trunc_pre;
     pub static ref SHOW_VOLUMES: RwLock<bool> = RwLock::new(OPTS.show_volumes);
     pub static ref DEFAULT_TIMESTAMPS: RwLock<HashMap<TimeFrame, Vec<i64>>> = Default::default();
+    pub static ref THEME: theme::Theme = OPTS.theme.unwrap_or_default();
 }
 
 fn main() {
diff --git a/src/opts.rs b/src/opts.rs
index d480fa0..f0e0c9d 100644
--- a/src/opts.rs
+++ b/src/opts.rs
@@ -5,6 +5,7 @@ use serde::Deserialize;
 use structopt::StructOpt;
 
 use crate::common::TimeFrame;
+use crate::theme::Theme;
 
 pub fn resolve_opts() -> Opts {
     let mut opts = get_cli_opts();
@@ -24,6 +25,9 @@ pub fn resolve_opts() -> Opts {
         opts.show_x_labels = opts.show_x_labels || config_opts.show_x_labels;
         opts.summary = opts.summary || config_opts.summary;
         opts.trunc_pre = opts.trunc_pre || config_opts.trunc_pre;
+
+        // Theme
+        opts.theme = config_opts.theme;
     }
 
     opts
@@ -111,6 +115,9 @@ pub struct Opts {
     #[structopt(long)]
     /// Truncate pre market graphing to only 30 minutes prior to markets opening
     pub trunc_pre: bool,
+
+    #[structopt(skip)]
+    pub theme: Option<Theme>,
 }
 
 const DEFAULT_CONFIG: &str = "---
@@ -151,4 +158,20 @@ const DEFAULT_CONFIG: &str = "---
 
 # Truncate pre market graphing to only 30 minutes prior to markets opening
 #trunc_pre: true
+
+# Apply a custom theme
+#theme:
+#  # Background is optional, otherwise it'll use your terminals background color
+#  #background: '#403E41'
+#  gray: '#727072'
+#  profit: '#ADD977'
+#  loss: '#FA648A'
+#  text_normal: '#FCFCFA'
+#  text_primary: '#FFDA65'
+#  text_secondary: '#79DBEA'
+#  border_primary: '#FC9766'
+#  border_secondary: '#FCFCFA'
+#  border_axis: '#FC9766'
+#  highlight_focused: '#FC9766'
+#  highlight_unfocused: '#727072'
 ";
diff --git a/src/theme.rs b/src/theme.rs
new file mode 100644
index 0000000..fba7355
--- /dev/null
+++ b/src/theme.rs
@@ -0,0 +1,142 @@
+use serde::Deserialize;
+use tui::style::Color;
+
+use self::de::{deserialize_color_hex_string, deserialize_option_color_hex_string};
+
+#[derive(Debug, Clone, Copy, Deserialize)]
+pub struct Theme {
+    #[serde(deserialize_with = "deserialize_option_color_hex_string")]
+    #[serde(default)]
+    background: Option<Color>,
+    #[serde(deserialize_with = "deserialize_color_hex_string")]
+    pub gray: Color,
+    #[serde(deserialize_with = "deserialize_color_hex_string")]
+    pub profit: Color,
+    #[serde(deserialize_with = "deserialize_color_hex_string")]
+    pub loss: Color,
+    #[serde(deserialize_with = "deserialize_color_hex_string")]
+    pub text_normal: Color,
+    #[serde(deserialize_with = "deserialize_color_hex_string")]
+    pub text_primary: Color,
+    #[serde(deserialize_with = "deserialize_color_hex_string")]
+    pub text_secondary: Color,
+    #[serde(deserialize_with = "deserialize_color_hex_string")]
+    pub border_primary: Color,
+    #[serde(deserialize_with = "deserialize_color_hex_string")]
+    pub border_secondary: Color,
+    #[serde(deserialize_with = "deserialize_color_hex_string")]
+    pub border_axis: Color,
+    #[serde(deserialize_with = "deserialize_color_hex_string")]
+    pub highlight_focused: Color,
+    #[serde(deserialize_with = "deserialize_color_hex_string")]
+    pub highlight_unfocused: Color,
+}
+
+impl Theme {
+    pub fn background(self) -> Color {
+        self.background.unwrap_or(Color::Reset)
+    }
+}
+
+impl Default for Theme {
+    fn default() -> Self {
+        Theme {
+            background: None,
+            gray: Color::DarkGray,
+            profit: Color::Green,
+            loss: Color::Red,
+            text_normal: Color::Reset,
+            text_primary: Color::Yellow,
+            text_secondary: Color::Cyan,
+            border_primary: Color::Blue,
+            border_secondary: Color::Reset,
+            border_axis: Color::Blue,
+            highlight_focused: Color::Yellow,
+            highlight_unfocused: Color::DarkGray,
+        }
+    }
+}
+
+fn hex_to_color(hex: &str) -> Option<Color> {
+    if hex.len() == 7 {
+        let hash = &hex[0..1];
+        let r = u8::from_str_radix(&hex[1..3], 16);
+        let g = u8::from_str_radix(&hex[3..5], 16);
+        let b = u8::from_str_radix(&hex[5..7], 16);
+
+        return match (hash, r, g, b) {
+            ("#", Ok(r), Ok(g), Ok(b)) => Some(Color::Rgb(r, g, b)),
+            _ => None,
+        };
+    }
+
+    None
+}
+
+mod de {
+    use std::fmt;
+
+    use serde::de::{self, Error, Unexpected, Visitor};
+
+    use super::{hex_to_color, Color};
+
+    pub(crate) fn deserialize_color_hex_string<'de, D>(deserializer: D) -> Result<Color, D::Error>
+    where
+        D: de::Deserializer<'de>,
+    {
+        struct ColorVisitor;
+
+        impl<'de> Visitor<'de> for ColorVisitor {
+            type Value = Color;
+
+            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                formatter.write_str("a hex string in the format of '#09ACDF'")
+            }
+
+            #[allow(clippy::unnecessary_unwrap)]
+            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
+            where
+                E: Error,
+            {
+                if let Some(color) = hex_to_color(s) {
+                    return Ok(color);
+                }
+
+                Err(de::Error::invalid_value(Unexpected::Str(s), &self))
+            }
+        }
+
+        deserializer.deserialize_any(ColorVisitor)
+    }
+
+    pub(crate) fn deserialize_option_color_hex_string<'de, D>(
+        deserializer: D,
+    ) -> Result<Option<Color>, D::Error>
+    where
+        D: de::Deserializer<'de>,
+    {
+        struct ColorVisitor;
+
+        impl<'de> Visitor<'de> for ColorVisitor {
+            type Value = Option<Color>;
+
+            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                formatter.write_str("a hex string in the format of '#09ACDF'")
+            }
+
+            #[allow(clippy::unnecessary_unwrap)]
+            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
+            where
+                E: Error,
+            {
+                if let Some(color) = hex_to_color(s) {
+                    return Ok(Some(color));
+                }
+
+                Err(de::Error::invalid_value(Unexpected::Str(s), &self))
+            }
+        }
+
+        deserializer.deserialize_any(ColorVisitor)
+    }
+}
diff --git a/src/widget/add_stock.rs b/src/widget/add_stock.rs
index 7fcd352..21a6cf3 100644
--- a/src/widget/add_stock.rs
+++ b/src/widget/add_stock.rs
@@ -1,10 +1,11 @@
 use tui::buffer::Buffer;
 use tui::layout::{Alignment, Rect};
-use tui::style::{Color, Modifier, Style};
+use tui::style::{Modifier, Style};
 use tui::text::{Span, Spans};
 use tui::widgets::{Paragraph, StatefulWidget, Widget, Wrap};
 
 use super::block;
+use crate::THEME;
 
 pub struct AddStockState {
     search_string: String,
@@ -49,26 +50,26 @@ impl StatefulWidget for AddStockWidget {
     fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
         let spans = if !state.has_user_input && state.error_msg.is_some() {
             Spans::from(vec![
-                Span::styled("> ", Style::default()),
+                Span::styled("> ", Style::default().fg(THEME.text_normal)),
                 Span::styled(
                     state.error_msg.as_ref().unwrap(),
-                    Style::default().add_modifier(Modifier::BOLD).fg(Color::Red),
+                    Style::default().add_modifier(Modifier::BOLD).fg(THEME.loss),
                 ),
             ])
         } else {
             Spans::from(vec![
-                Span::styled("> ", Style::default()),
+                Span::styled("> ", Style::default().fg(THEME.text_normal)),
                 Span::styled(
                     &state.search_string,
                     Style::default()
                         .add_modifier(Modifier::BOLD)
-                        .fg(Color::Cyan),
+                        .fg(THEME.text_secondary),
                 ),
             ])
         };
 
         Paragraph::new(spans)
-            .block(block::new(" Add Ticker ", None))
+            .block(block::new(" Add Ticker "))
             .style(Style::default())
             .alignment(Alignment::Left)
             .wrap(Wrap { trim: true })
diff --git a/src/widget/block.rs b/src/widget/block.rs
index 50a3e87..afeb575 100644
--- a/src/widget/block.rs
+++ b/src/widget/block.rs
@@ -1,10 +1,12 @@
-use tui::style::{Color, Style};
+use tui::style::Style;
 use tui::text::Span;
 use tui::widgets::{Block, Borders};
 
-pub fn new(title: &str, border_color: Option<Color>) -> Block {
+use crate::THEME;
+
+pub fn new(title: &str) -> Block {
     Block::default()
         .borders(Borders::ALL)
-        .border_style(Style::default().fg(border_color.unwrap_or(Color::Blue)))
-        .title(Span::styled(title, Style::reset()))
+        .border_style(Style::default().fg(THEME.border_primary))
+        .title(Span::styled(title, Style::default().fg(THEME.text_normal)))
 }
diff --git a/src/widget/help.rs b/src/widget/help.rs
index 1ee5ce9..a191bcf 100644
--- a/src/widget/help.rs
+++ b/src/widget/help.rs
@@ -6,6 +6,7 @@ use tui::widgets::{Paragraph, Widget};
 
 use super::block;
 use crate::draw::{add_padding, PaddingDirection};
+use crate::THEME;
 
 const TEXT: &str = r#"
 Quit: q or <Ctrl+c>
@@ -59,13 +60,18 @@ impl HelpWidget {
 
 impl Widget for HelpWidget {
     fn render(self, mut area: Rect, buf: &mut Buffer) {
-        block::new(" Help - <ESC> to go back ", None).render(area, buf);
+        block::new(" Help - <ESC> to go back ").render(area, buf);
         area = add_padding(area, 1, PaddingDirection::All);
         area = add_padding(area, 1, PaddingDirection::Left);
 
         let text: Vec<_> = TEXT
             .lines()
-            .map(|line| Spans::from(Span::styled(format!("{}\n", line), Style::default())))
+            .map(|line| {
+                Spans::from(Span::styled(
+                    format!("{}\n", line),
+                    Style::default().fg(THEME.text_normal),
+                ))
+            })
             .collect();
 
         Paragraph::new(text).render(area, buf);
diff --git a/src/widget/options.rs b/src/widget/options.rs
index 8cd3ab9..a2043cb 100644
--- a/src/widget/options.rs
+++ b/src/widget/options.rs
@@ -4,7 +4,7 @@ use std::hash::{Hash, Hasher};
 use chrono::NaiveDateTime;
 use tui::buffer::Buffer;
 use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
-use tui::style::{Color, Modifier, Style};
+use tui::style::{Modifier, Style};
 use tui::text::{Span, Spans};
 use tui::widgets::{
     Block, Borders, Cell, List, ListItem, ListState, Paragraph, Row, StatefulWidget, Table,
@@ -15,6 +15,7 @@ use super::{block, CachableWidget, CacheState};
 use crate::api::model::{OptionsData, OptionsQuote};
 use crate::draw::{add_padding, PaddingDirection};
 use crate::service::{self, Service};
+use crate::THEME;
 
 #[derive(Clone, Copy, PartialEq, Hash)]
 enum OptionType {
@@ -246,7 +247,7 @@ impl CachableWidget<OptionsState> for OptionsWidget {
     }
 
     fn render(self, mut area: Rect, buf: &mut Buffer, state: &mut OptionsState) {
-        block::new(" Options ", None).render(area, buf);
+        block::new(" Options ").render(area, buf);
         area = add_padding(area, 1, PaddingDirection::All);
 
         // chunks[0] - call / put selector
@@ -268,7 +269,7 @@ impl CachableWidget<OptionsState> for OptionsWidget {
             let call_put_selector = vec![
                 Span::styled(
                     "Call",
-                    Style::default().fg(Color::Green).add_modifier(
+                    Style::default().fg(THEME.profit).add_modifier(
                         if state.selected_type == OptionType::Call {
                             Modifier::BOLD | Modifier::UNDERLINED
                         } else {
@@ -279,7 +280,7 @@ impl CachableWidget<OptionsState> for OptionsWidget {
                 Span::styled(" | ", Style::default()),
                 Span::styled(
                     "Put",
-                    Style::default().fg(Color::Red).add_modifier(
+                    Style::default().fg(THEME.loss).add_modifier(
                         if state.selected_type == OptionType::Put {
                             Modifier::BOLD | Modifier::UNDERLINED
                         } else {
@@ -292,14 +293,21 @@ impl CachableWidget<OptionsState> for OptionsWidget {
             chunks[0] = add_padding(chunks[0], 1, PaddingDirection::Left);
             chunks[0] = add_padding(chunks[0], 1, PaddingDirection::Right);
 
-            Paragraph::new(Spans::from(call_put_selector))
-                .style(Style::reset())
-                .alignment(Alignment::Center)
-                .render(chunks[0], buf);
-
             Block::default()
+                .style(Style::default().fg(THEME.border_secondary))
                 .borders(Borders::BOTTOM)
                 .render(chunks[0], buf);
+
+            chunks[0] = add_padding(chunks[0], 1, PaddingDirection::Bottom);
+
+            Paragraph::new(Spans::from(call_put_selector))
+                .style(
+                    Style::default()
+                        .fg(THEME.text_normal)
+                        .bg(THEME.background()),
+                )
+                .alignment(Alignment::Center)
+                .render(chunks[0], buf);
         }
 
         // selector_chunks[0] - date selector
@@ -314,6 +322,7 @@ impl CachableWidget<OptionsState> for OptionsWidget {
             selector_chunks[0] = add_padding(selector_chunks[0], 1, PaddingDirection::Left);
 
             Block::default()
+                .style(Style::default().fg(THEME.border_secondary))
                 .borders(Borders::RIGHT)
                 .render(selector_chunks[0], buf);
             selector_chunks[0] = add_padding(selector_chunks[0], 2, PaddingDirection::Right);
@@ -331,12 +340,16 @@ impl CachableWidget<OptionsState> for OptionsWidget {
                 .collect::<Vec<_>>();
 
             let list = List::new(dates)
-                .style(Style::reset())
+                .style(
+                    Style::default()
+                        .fg(THEME.text_normal)
+                        .bg(THEME.background()),
+                )
                 .highlight_style(Style::default().bg(
                     if state.selection_mode == SelectionMode::Dates {
-                        Color::LightBlue
+                        THEME.highlight_focused
                     } else {
-                        Color::DarkGray
+                        THEME.highlight_unfocused
                     },
                 ));
 
@@ -349,8 +362,11 @@ impl CachableWidget<OptionsState> for OptionsWidget {
                 list_state.select(Some(idx));
             }
 
-            Paragraph::new(Span::styled("Date", Style::default().fg(Color::Cyan)))
-                .render(selector_chunks[0], buf);
+            Paragraph::new(Span::styled(
+                "Date",
+                Style::default().fg(THEME.text_secondary),
+            ))
+            .render(selector_chunks[0], buf);
 
             selector_chunks[0] = add_padding(selector_chunks[0], 2, PaddingDirection::Top);
 
@@ -375,27 +391,31 @@ impl CachableWidget<OptionsState> for OptionsWidget {
                         Cell::from(format!("{: >7.2}%", d.percent_change)),
                     ])
                     .style(Style::default().fg(if d.percent_change >= 0.0 {
-                        Color::Green
+                        THEME.profit
                     } else {
-                        Color::Red
+                        THEME.loss
                     }))
                 });
 
                 let table = Table::new(rows)
                     .header(
                         Row::new(vec!["Strike", "Price", "% Change"])
-                            .style(Style::default().fg(Color::Cyan))
+                            .style(Style::default().fg(THEME.text_secondary))
                             .bottom_margin(1),
                     )
-                    .style(Style::reset())
+                    .style(
+                        Style::default()
+                            .fg(THEME.text_normal)
+                            .bg(THEME.background()),
+                    )
                     .highlight_style(
                         Style::default()
                             .bg(if state.selection_mode == SelectionMode::Options {
-                                Color::LightBlue
+                                THEME.highlight_focused
                             } else {
-                                Color::DarkGray
+                                THEME.highlight_unfocused
                             })
-                            .fg(Color::Reset),
+                            .fg(THEME.text_normal),
                     )
                     .widths(&[
                         Constraint::Length(8),
@@ -421,9 +441,12 @@ impl CachableWidget<OptionsState> for OptionsWidget {
             chunks[1] = add_padding(chunks[1], 1, PaddingDirection::Right);
 
             Block::default()
+                .style(Style::default().fg(THEME.border_secondary))
                 .borders(Borders::BOTTOM)
                 .render(chunks[1], buf);
 
+            chunks[1] = add_padding(chunks[1], 1, PaddingDirection::Bottom);
+
             if let Some(idx) = state.selected_option {
                 let option_range = if state.selected_type == OptionType::Call {
                     &state.data().as_ref().unwrap().calls[..]
@@ -521,8 +544,20 @@ impl CachableWidget<OptionsState> for OptionsWidget {
                         )),
                     ];
 
-                    Paragraph::new(column_0).render(columns[0], buf);
-                    Paragraph::new(column_1).render(columns[1], buf);
+                    Paragraph::new(column_0)
+                        .style(
+                            Style::default()
+                                .fg(THEME.text_normal)
+                                .bg(THEME.background()),
+                        )
+                        .render(columns[0], buf);
+                    Paragraph::new(column_1)
+                        .style(
+                            Style::default()
+                                .fg(THEME.text_normal)
+                                .bg(THEME.background()),
+                        )
+                        .render(columns[1], buf);
                 }
             }
         }
diff --git a/src/widget/stock.rs b/src/widget/stock.rs
index 2a08eaf..699caa8 100644
--- a/src/widget/stock.rs
+++ b/src/widget/stock.rs
@@ -3,7 +3,7 @@ use std::hash::{Hash, Hasher};
 use itertools::Itertools;
 use tui::buffer::Buffer;
 use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
-use tui::style::{Color, Modifier, Style};
+use tui::style::{Modifier, Style};
 use tui::symbols::{bar, Marker};
 use tui::text::{Span, Spans};
 use tui::widgets::{
@@ -18,7 +18,7 @@ use crate::draw::{add_padding, PaddingDirection};
 use crate::service::{self, Service};
 use crate::{
     DEFAULT_TIMESTAMPS, ENABLE_PRE_POST, HIDE_PREV_CLOSE, HIDE_TOGGLE, SHOW_VOLUMES, SHOW_X_LABELS,
-    TIME_FRAME, TRUNC_PRE,
+    THEME, TIME_FRAME, TRUNC_PRE,
 };
 
 const NUM_LOADING_TICKS: usize = 4;
@@ -437,7 +437,10 @@ impl StockState {
         for (idx, chunk) in dates.chunks(chunk_size).enumerate() {
             if idx == 0 {
                 labels.push(chunk.get(0).map_or(Span::raw("".to_string()), |d| {
-                    Span::raw(self.time_frame.format_time(*d))
+                    Span::styled(
+                        self.time_frame.format_time(*d),
+                        Style::default().fg(THEME.text_normal),
+                    )
                 }));
             }
 
@@ -445,7 +448,10 @@ impl StockState {
                 chunk
                     .get(chunk.len() - 1)
                     .map_or(Span::raw("".to_string()), |d| {
-                        Span::raw(self.time_frame.format_time(*d))
+                        Span::styled(
+                            self.time_frame.format_time(*d),
+                            Style::default().fg(THEME.text_normal),
+                        )
                     }),
             );
         }
@@ -460,9 +466,18 @@ impl StockState {
     pub fn y_labels(&self, min: f64, max: f64) -> Vec<Span> {
         if self.loaded() {
             vec![
-                Span::raw(format!("{:>8.2}", (min - 0.05))),
-                Span::raw(format!("{:>8.2}", ((min - 0.05) + (max + 0.05)) / 2.0)),
-                Span::raw(format!("{:>8.2}", max + 0.05)),
+                Span::styled(
+                    format!("{:>8.2}", (min - 0.05)),
+                    Style::default().fg(THEME.text_normal),
+                ),
+                Span::styled(
+                    format!("{:>8.2}", ((min - 0.05) + (max + 0.05)) / 2.0),
+                    Style::default().fg(THEME.text_normal),
+                ),
+                Span::styled(
+                    format!("{:>8.2}", max + 0.05),
+                    Style::default().fg(THEME.text_normal),
+                ),
             ]
         } else {
             vec![
@@ -559,20 +574,17 @@ impl CachableWidget<StockState> for StockWidget {
 
         // Draw widget block
         {
-            block::new(
-                &format!(
-                    " {}{:<4} ",
-                    state.symbol,
-                    if loaded {
-                        format!(" - {}", company_name)
-                    } else if state.profile.is_some() {
-                        format!(" - {}{:<4}", company_name, loading_indicator)
-                    } else {
-                        loading_indicator
-                    }
-                ),
-                None,
-            )
+            block::new(&format!(
+                " {}{:<4} ",
+                state.symbol,
+                if loaded {
+                    format!(" - {}", company_name)
+                } else if state.profile.is_some() {
+                    format!(" - {}{:<4}", company_name, loading_indicator)
+                } else {
+                    loading_indicator
+                }
+            ))
             .render(area, buf);
             area = add_padding(area, 1, PaddingDirection::All);
             area = add_padding(area, 1, PaddingDirection::Left);
@@ -617,7 +629,7 @@ impl CachableWidget<StockState> for StockWidget {
                         },
                         Style::default()
                             .add_modifier(Modifier::BOLD)
-                            .fg(Color::Yellow),
+                            .fg(THEME.text_primary),
                     ),
                     Span::styled(
                         if loaded {
@@ -628,9 +640,9 @@ impl CachableWidget<StockState> for StockWidget {
                         Style::default()
                             .add_modifier(Modifier::BOLD)
                             .fg(if pct_change >= 0.0 {
-                                Color::Green
+                                THEME.profit
                             } else {
-                                Color::Red
+                                THEME.loss
                             }),
                     ),
                 ]),
@@ -642,7 +654,7 @@ impl CachableWidget<StockState> for StockWidget {
                         } else {
                             "".to_string()
                         },
-                        Style::default().fg(Color::LightCyan),
+                        Style::default().fg(THEME.text_secondary),
                     ),
                 ]),
                 Spans::from(vec![
@@ -653,7 +665,7 @@ impl CachableWidget<StockState> for StockWidget {
                         } else {
                             "".to_string()
                         },
-                        Style::default().fg(Color::LightCyan),
+                        Style::default().fg(THEME.text_secondary),
                     ),
                 ]),
                 Spans::default(),
@@ -661,19 +673,23 @@ impl CachableWidget<StockState> for StockWidget {
                     Span::styled("v: ", Style::default()),
                     Span::styled(
                         if loaded { vol } else { "".to_string() },
-                        Style::default().fg(Color::LightCyan),
+                        Style::default().fg(THEME.text_secondary),
                     ),
                 ]),
             ];
 
             Paragraph::new(company_info)
-                .style(Style::reset())
+                .style(
+                    Style::default()
+                        .fg(THEME.text_normal)
+                        .bg(THEME.background()),
+                )
                 .alignment(Alignment::Left)
                 .wrap(Wrap { trim: true })
                 .render(info_chunks[0], buf);
 
             if !*HIDE_TOGGLE {
-                let toggle_block = block::new(" Toggle ", None);
+                let toggle_block = block::new(" Toggle ");
                 toggle_block.render(info_chunks[1], buf);
                 info_chunks[1] = add_padding(info_chunks[1], 2, PaddingDirection::Left);
                 info_chunks[1] = add_padding(info_chunks[1], 1, PaddingDirection::Top);
@@ -687,27 +703,27 @@ impl CachableWidget<StockState> for StockWidget {
                     toggle_info.push(Spans::from(Span::styled(
                         "Volumes  'v'",
                         Style::default().bg(if show_volumes {
-                            Color::DarkGray
+                            THEME.highlight_unfocused
                         } else {
-                            Color::Reset
+                            THEME.background()
                         }),
                     )));
 
                     toggle_info.push(Spans::from(Span::styled(
                         "X Labels 'x'",
                         Style::default().bg(if show_x_labels {
-                            Color::DarkGray
+                            THEME.highlight_unfocused
                         } else {
-                            Color::Reset
+                            THEME.background()
                         }),
                     )));
 
                     toggle_info.push(Spans::from(Span::styled(
                         "Pre Post 'p'",
                         Style::default().bg(if enable_pre_post {
-                            Color::DarkGray
+                            THEME.highlight_unfocused
                         } else {
-                            Color::Reset
+                            THEME.background()
                         }),
                     )));
                 }
@@ -716,15 +732,19 @@ impl CachableWidget<StockState> for StockWidget {
                     toggle_info.push(Spans::from(Span::styled(
                         "Options  'o'",
                         Style::default().bg(if state.show_options {
-                            Color::DarkGray
+                            THEME.highlight_unfocused
                         } else {
-                            Color::Reset
+                            THEME.background()
                         }),
                     )));
                 }
 
                 Paragraph::new(toggle_info)
-                    .style(Style::reset())
+                    .style(
+                        Style::default()
+                            .fg(THEME.text_normal)
+                            .bg(THEME.background()),
+                    )
                     .alignment(Alignment::Left)
                     .render(info_chunks[1], buf);
             }
@@ -848,15 +868,19 @@ impl CachableWidget<StockState> for StockWidget {
 
             let mut datasets = vec![Dataset::default()
                 .marker(Marker::Braille)
-                .style(Style::default().fg(
-                    if trading_period != TradingPeriod::Regular && enable_pre_post {
-                        Color::DarkGray
-                    } else if pct_change >= 0.0 {
-                        Color::Green
-                    } else {
-                        Color::Red
-                    },
-                ))
+                .style(
+                    Style::default()
+                        .fg(
+                            if trading_period != TradingPeriod::Regular && enable_pre_post {
+                                THEME.gray
+                            } else if pct_change >= 0.0 {
+                                THEME.profit
+                            } else {
+                                THEME.loss
+                            },
+                        )
+                        .bg(THEME.background()),
+                )
                 .graph_type(graph_type)
                 .data(&reg_prices)];
 
@@ -865,13 +889,15 @@ impl CachableWidget<StockState> for StockWidget {
                     Dataset::default()
                         .marker(Marker::Braille)
                         .style(
-                            Style::default().fg(if trading_period != TradingPeriod::Post {
-                                Color::DarkGray
-                            } else if pct_change >= 0.0 {
-                                Color::Green
-                            } else {
-                                Color::Red
-                            }),
+                            Style::default()
+                                .fg(if trading_period != TradingPeriod::Post {
+                                    THEME.gray
+                                } else if pct_change >= 0.0 {
+                                    THEME.profit
+                                } else {
+                                    THEME.loss
+                                })
+                                .bg(THEME.background()),
                         )
                         .graph_type(GraphType::Line)
                         .data(&data),
@@ -884,13 +910,15 @@ impl CachableWidget<StockState> for StockWidget {
                     Dataset::default()
                         .marker(Marker::Braille)
                         .style(
-                            Style::default().fg(if trading_period != TradingPeriod::Pre {
-                                Color::DarkGray
-                            } else if pct_change >= 0.0 {
-                                Color::Green
-                            } else {
-                                Color::Red
-                            }),
+                            Style::default()
+                                .fg(if trading_period != TradingPeriod::Pre {
+                                    THEME.gray
+                                } else if pct_change >= 0.0 {
+                                    THEME.profit
+                                } else {
+                                    THEME.loss
+                                })
+                                .bg(THEME.background()),
                         )
                         .graph_type(GraphType::Line)
                         .data(&data),
@@ -902,7 +930,7 @@ impl CachableWidget<StockState> for StockWidget {
                     0,
                     Dataset::default()
                         .marker(Marker::Braille)
-                        .style(Style::default().fg(Color::DarkGray))
+                        .style(Style::default().fg(THEME.gray).bg(THEME.background()))
                         .graph_type(GraphType::Line)
                         .data(&data),
                 );
@@ -958,7 +986,7 @@ impl CachableWidget<StockState> for StockWidget {
 
                     Block::default()
                         .borders(Borders::LEFT)
-                        .border_style(Style::default().fg(Color::Blue))
+                        .border_style(Style::default().fg(THEME.border_axis))
                         .render(volume_chunks, buf);
 
                     volume_chunks.x += 1;
@@ -966,15 +994,17 @@ impl CachableWidget<StockState> for StockWidget {
                     BarChart::default()
                         .bar_gap(0)
                         .bar_set(bar::NINE_LEVELS)
-                        .style(Style::default().fg(Color::DarkGray))
+                        .style(Style::default().fg(THEME.gray).bg(THEME.background()))
                         .data(&volumes)
                         .render(volume_chunks, buf);
                 }
             }
 
             Chart::new(datasets)
+                .style(Style::default().bg(THEME.background()))
                 .block(
                     Block::default()
+                        .style(Style::default().fg(THEME.border_secondary))
                         .borders(Borders::TOP)
                         .border_style(Style::default()),
                 )
@@ -983,7 +1013,7 @@ impl CachableWidget<StockState> for StockWidget {
 
                     if show_x_labels && loaded {
                         axis.labels(x_labels)
-                            .style(Style::default().fg(Color::LightBlue))
+                            .style(Style::default().fg(THEME.border_axis))
                     } else {
                         axis
                     }
@@ -992,7 +1022,7 @@ impl CachableWidget<StockState> for StockWidget {
                     Axis::default()
                         .bounds(state.y_bounds(min, max))
                         .labels(state.y_labels(min, max))
-                        .style(Style::default().fg(Color::LightBlue)),
+                        .style(Style::default().fg(THEME.border_axis)),
                 )
                 .render(graph_chunks[0], buf);
         }
@@ -1006,13 +1036,15 @@ impl CachableWidget<StockState> for StockWidget {
 
             Tabs::new(tab_names)
                 .block(
-                    Block::default()
-                        .borders(Borders::TOP)
-                        .border_style(Style::reset()),
+                    Block::default().borders(Borders::TOP).border_style(
+                        Style::default()
+                            .fg(THEME.border_secondary)
+                            .bg(THEME.background()),
+                    ),
                 )
                 .select(state.time_frame.idx())
-                .style(Style::default().fg(Color::Cyan))
-                .highlight_style(Style::default().fg(Color::Yellow))
+                .style(Style::default().fg(THEME.text_secondary))
+                .highlight_style(Style::default().fg(THEME.text_primary))
                 .render(chunks[2], buf);
         }
     }
diff --git a/src/widget/stock_summary.rs b/src/widget/stock_summary.rs
index 2f41104..5098b50 100644
--- a/src/widget/stock_summary.rs
+++ b/src/widget/stock_summary.rs
@@ -1,7 +1,7 @@
 use itertools::Itertools;
 use tui::buffer::Buffer;
 use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
-use tui::style::{Color, Modifier, Style};
+use tui::style::{Modifier, Style};
 use tui::symbols::{bar, Marker};
 use tui::text::{Span, Spans};
 use tui::widgets::{
@@ -12,7 +12,7 @@ use super::stock::StockState;
 use super::{CachableWidget, CacheState};
 use crate::common::*;
 use crate::draw::{add_padding, PaddingDirection};
-use crate::{ENABLE_PRE_POST, HIDE_PREV_CLOSE, SHOW_VOLUMES};
+use crate::{ENABLE_PRE_POST, HIDE_PREV_CLOSE, SHOW_VOLUMES, THEME};
 
 pub struct StockSummaryWidget {}
 
@@ -58,17 +58,24 @@ impl CachableWidget<StockState> for StockSummaryWidget {
             }
         );
         Block::default()
-            .title(format!(
-                " {}{} ",
-                &title[..24.min(title.len())],
-                if loaded {
-                    "".to_string()
-                } else {
-                    format!("{:<4}", loading_indicator)
-                }
+            .title(Span::styled(
+                format!(
+                    " {}{} ",
+                    &title[..24.min(title.len())],
+                    if loaded {
+                        "".to_string()
+                    } else {
+                        format!("{:<4}", loading_indicator)
+                    }
+                ),
+                Style::default().fg(THEME.text_normal),
             ))
             .borders(Borders::TOP)
-            .border_style(Style::reset())
+            .border_style(
+                Style::default()
+                    .fg(THEME.border_secondary)
+                    .bg(THEME.background()),
+            )
             .render(area, buf);
         area = add_padding(area, 1, PaddingDirection::Top);
 
@@ -86,7 +93,7 @@ impl CachableWidget<StockState> for StockSummaryWidget {
 
             let prices = vec![
                 Spans::from(vec![
-                    Span::styled("c: ", Style::default()),
+                    Span::styled("c: ", Style::default().fg(THEME.text_normal)),
                     Span::styled(
                         if loaded {
                             format!("{:.2} {}", state.current_price(), currency)
@@ -95,37 +102,37 @@ impl CachableWidget<StockState> for StockSummaryWidget {
                         },
                         Style::default()
                             .add_modifier(Modifier::BOLD)
-                            .fg(Color::Yellow),
+                            .fg(THEME.text_primary),
                     ),
                 ]),
                 Spans::from(vec![
-                    Span::styled("h: ", Style::default()),
+                    Span::styled("h: ", Style::default().fg(THEME.text_normal)),
                     Span::styled(
                         if loaded {
                             format!("{:.2}", high)
                         } else {
                             "".to_string()
                         },
-                        Style::default().fg(Color::LightCyan),
+                        Style::default().fg(THEME.text_secondary),
                     ),
                 ]),
                 Spans::from(vec![
-                    Span::styled("l: ", Style::default()),
+                    Span::styled("l: ", Style::default().fg(THEME.text_normal)),
                     Span::styled(
                         if loaded {
                             format!("{:.2}", low)
                         } else {
                             "".to_string()
                         },
-                        Style::default().fg(Color::LightCyan),
+                        Style::default().fg(THEME.text_secondary),
                     ),
                 ]),
                 Spans::default(),
                 Spans::from(vec![
-                    Span::styled("v: ", Style::default()),
+                    Span::styled("v: ", Style::default().fg(THEME.text_normal)),
                     Span::styled(
                         if loaded { vol } else { "".to_string() },
-                        Style::default().fg(Color::LightCyan),
+                        Style::default().fg(THEME.text_secondary),
                     ),
                 ]),
             ];
@@ -139,17 +146,19 @@ impl CachableWidget<StockState> for StockSummaryWidget {
                 Style::default()
                     .add_modifier(Modifier::BOLD)
                     .fg(if pct_change >= 0.0 {
-                        Color::Green
+                        THEME.profit
                     } else {
-                        Color::Red
+                        THEME.loss
                     }),
             )];
 
             Paragraph::new(prices)
+                .style(Style::default().bg(THEME.background()))
                 .alignment(Alignment::Left)
                 .render(layout[0], buf);
 
             Paragraph::new(Spans::from(pct))
+                .style(Style::default().bg(THEME.background()))
                 .alignment(Alignment::Right)
                 .render(layout[0], buf);
         }
@@ -270,11 +279,11 @@ impl CachableWidget<StockState> for StockSummaryWidget {
                 .marker(Marker::Braille)
                 .style(Style::default().fg(
                     if trading_period != TradingPeriod::Regular && enable_pre_post {
-                        Color::DarkGray
+                        THEME.gray
                     } else if pct_change >= 0.0 {
-                        Color::Green
+                        THEME.profit
                     } else {
-                        Color::Red
+                        THEME.loss
                     },
                 ))
                 .graph_type(graph_type)
@@ -286,11 +295,11 @@ impl CachableWidget<StockState> for StockSummaryWidget {
                         .marker(Marker::Braille)
                         .style(
                             Style::default().fg(if trading_period != TradingPeriod::Post {
-                                Color::DarkGray
+                                THEME.gray
                             } else if pct_change >= 0.0 {
-                                Color::Green
+                                THEME.profit
                             } else {
-                                Color::Red
+                                THEME.loss
                             }),
                         )
                         .graph_type(GraphType::Line)
@@ -305,11 +314,11 @@ impl CachableWidget<StockState> for StockSummaryWidget {
                         .marker(Marker::Braille)
                         .style(
                             Style::default().fg(if trading_period != TradingPeriod::Pre {
-                                Color::DarkGray
+                                THEME.gray
                             } else if pct_change >= 0.0 {
-                                Color::Green
+                                THEME.profit
                             } else {
-                                Color::Red
+                                THEME.loss
                             }),
                         )
                         .graph_type(GraphType::Line)
@@ -322,7 +331,7 @@ impl CachableWidget<StockState> for StockSummaryWidget {
                     0,
                     Dataset::default()
                         .marker(Marker::Braille)
-                        .style(Style::default().fg(Color::DarkGray))
+                        .style(Style::default().fg(THEME.gray))
                         .graph_type(GraphType::Line)
                         .data(&data),
                 );
@@ -368,7 +377,7 @@ impl CachableWidget<StockState> for StockSummaryWidget {
 
                         Block::default()
                             .borders(Borders::LEFT)
-                            .border_style(Style::default().fg(Color::Blue))
+                            .border_style(Style::default().fg(THEME.border_axis))
                             .render(volume_chunks, buf);
 
                         volume_chunks.x += 1;
@@ -376,7 +385,7 @@ impl CachableWidget<StockState> for StockSummaryWidget {
                         BarChart::default()
                             .bar_gap(0)
                             .bar_set(bar::NINE_LEVELS)
-                            .style(Style::default().fg(Color::DarkGray))
+                            .style(Style::default().fg(THEME.gray))
                             .data(&volumes)
                             .render(volume_chunks, buf);
                     }
@@ -384,13 +393,14 @@ impl CachableWidget<StockState> for StockSummaryWidget {
             }
 
             Chart::new(datasets)
+                .style(Style::default().bg(THEME.background()))
                 .block(Block::default().border_style(Style::default()))
                 .x_axis(Axis::default().bounds(state.x_bounds(start, end, &data)))
                 .y_axis(
                     Axis::default()
                         .bounds(state.y_bounds(min, max))
                         .labels(state.y_labels(min, max))
-                        .style(Style::default().fg(Color::LightBlue)),
+                        .style(Style::default().fg(THEME.border_axis)),
                 )
                 .render(graph_chunks[0], buf);
         }


Test patch: