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(terminal: &mut Terminal, app: &mut App) { let current_size = terminal.size().unwrap_or_default(); @@ -25,6 +25,12 @@ pub fn draw(terminal: &mut Terminal, 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(frame: &mut Frame, 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(frame: &mut Frame, 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(frame: &mut Frame, 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(frame: &mut Frame, app: &mut App, area: Rect) { } fn draw_summary(frame: &mut Frame, 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(frame: &mut Frame, 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(frame: &mut Frame, 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(frame: &mut Frame, 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(frame: &mut Frame, 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 = RwLock::new(OPTS.show_volumes); pub static ref DEFAULT_TIMESTAMPS: RwLock>> = 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, } 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, + #[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 { + 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 + 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(self, s: &str) -> Result + 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, D::Error> + where + D: de::Deserializer<'de>, + { + struct ColorVisitor; + + impl<'de> Visitor<'de> for ColorVisitor { + type Value = Option; + + 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(self, s: &str) -> Result + 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) -> 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 @@ -59,13 +60,18 @@ impl HelpWidget { impl Widget for HelpWidget { fn render(self, mut area: Rect, buf: &mut Buffer) { - block::new(" Help - to go back ", None).render(area, buf); + block::new(" Help - 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 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 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 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 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 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 for OptionsWidget { .collect::>(); 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 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 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 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 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 { 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 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 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 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 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 for StockWidget { } else { "".to_string() }, - Style::default().fg(Color::LightCyan), + Style::default().fg(THEME.text_secondary), ), ]), Spans::default(), @@ -661,19 +673,23 @@ impl CachableWidget 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 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 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 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(®_prices)]; @@ -865,13 +889,15 @@ impl CachableWidget 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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); }