Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add custom theming #69

Merged
merged 5 commits into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.foreground_inactive
}),
);
let down_arrow = Span::styled(
"ᐯ",
Style::default().fg(if more_down {
Color::Reset
THEME.text_normal
} else {
Color::DarkGray
THEME.foreground_inactive
}),
);

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 @@ -39,6 +40,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
7 changes: 7 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
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)]
pub background: Option<Color>,
#[serde(deserialize_with = "deserialize_color_hex_string")]
pub foreground_inactive: 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,
foreground_inactive: 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)
}
}
13 changes: 7 additions & 6 deletions src/widget/add_stock.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 })
Expand Down
10 changes: 6 additions & 4 deletions src/widget/block.rs
Original file line number Diff line number Diff line change
@@ -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)))
}
Loading