Skip to content

Commit

Permalink
add custom theming (#69)
Browse files Browse the repository at this point in the history
* add custom theming

* rename to gray

* add to default config

* update CHANGELOG
  • Loading branch information
tarkah authored Feb 17, 2021
1 parent f60accb commit 8a084df
Show file tree
Hide file tree
Showing 13 changed files with 436 additions and 159 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
[#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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
50 changes: 33 additions & 17 deletions src/draw.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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],
);
}
Expand All @@ -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],
);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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],
);
Expand Down Expand Up @@ -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],
);

Expand All @@ -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]);

Expand All @@ -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
}),
);

Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod event;
mod opts;
mod service;
mod task;
mod theme;
mod widget;

lazy_static! {
Expand All @@ -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() {
Expand Down
23 changes: 23 additions & 0 deletions src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -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 = "---
Expand Down Expand Up @@ -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'
";
142 changes: 142 additions & 0 deletions src/theme.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 8a084df

Please sign in to comment.