diff --git a/CHANGELOG.md b/CHANGELOG.md index 74b67c21..6ae49493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- support for color themes and light mode([#28](https://github.com/extrawurst/gitui/issues/28)) ## [0.2.6] - 2020-05-18 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 552fd550..e778f213 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,15 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + [[package]] name = "base64" version = "0.11.0" @@ -82,6 +91,12 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + [[package]] name = "cassowary" version = "0.3.0" @@ -299,8 +314,10 @@ dependencies = [ "itertools", "log", "rayon-core", + "ron", "scopeguard", "scopetime", + "serde", "simplelog", "tui", ] @@ -654,13 +671,24 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "ron" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ece421e0c4129b90e4a35b6f625e472e96c552136f5093a2f4fa2bbb75a62d5" +dependencies = [ + "base64 0.10.1", + "bitflags", + "serde", +] + [[package]] name = "rust-argon2" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" dependencies = [ - "base64", + "base64 0.11.0", "blake2b_simd", "constant_time_eq", "crossbeam-utils", @@ -685,6 +713,26 @@ dependencies = [ "log", ] +[[package]] +name = "serde" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "signal-hook" version = "0.1.15" diff --git a/Cargo.toml b/Cargo.toml index cf5ffe53..354cff97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,8 +31,10 @@ scopeguard = "1.1" bitflags = "1.2" chrono = "0.4" backtrace = { version = "0.3" } +ron = "0.5.1" scopetime = { path = "./scopetime", version = "0.1" } asyncgit = { path = "./asyncgit", version = "0.2" } +serde = "1.0.110" [features] default=[] @@ -45,6 +47,6 @@ members=[ ] [profile.release] -lto = true +lto = true opt-level = 'z' # Optimize for size. codegen-units = 1 \ No newline at end of file diff --git a/README.md b/README.md index 56f25eba..3abdb6b4 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,13 @@ this will log to: * `$XDG_CACHE_HOME/gitui/gitui.log` (linux using `XDG`) * `$HOME/.cache/gitui/gitui.log` (linux) +# color theme + +to change the colors of the program you have to modify `theme.ron` file +[Ron format](https://github.com/ron-rs/ron) located at config path (same as log paths). the list of valid +colors can be found in [ColorDef](./src/ui/style.rs#ColorDef) struct. note that rgb colors might not be available +on some platforms. + # inspiration * https://github.com/jesseduffield/lazygit diff --git a/src/app.rs b/src/app.rs index 28a30d5f..e1ce02c5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ +use crate::ui::style::Theme; use crate::{ accessors, components::{ @@ -17,10 +18,11 @@ use itertools::Itertools; use log::trace; use std::borrow::Cow; use strings::commands; +use tui::style::Style; use tui::{ backend::Backend, layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, + style::Modifier, widgets::{Block, Borders, Paragraph, Tabs, Text}, Frame, }; @@ -37,6 +39,7 @@ pub struct App { revlog: Revlog, status_tab: Status, queue: Queue, + theme: Theme, } // public interface @@ -44,17 +47,21 @@ impl App { /// pub fn new(sender: &Sender) -> Self { let queue = Queue::default(); + + let theme = Theme::init(); + Self { - reset: ResetComponent::new(queue.clone()), - commit: CommitComponent::new(queue.clone()), + reset: ResetComponent::new(queue.clone(), theme), + commit: CommitComponent::new(queue.clone(), theme), do_quit: false, current_commands: Vec::new(), - help: HelpComponent::default(), + help: HelpComponent::new(theme), msg: MsgComponent::default(), tab: 0, - revlog: Revlog::new(&sender), - status_tab: Status::new(&sender, &queue), + revlog: Revlog::new(&sender, theme), + status_tab: Status::new(&sender, &queue, theme), queue, + theme, } } @@ -84,6 +91,7 @@ impl App { f, chunks_main[2], self.current_commands.as_slice(), + self.theme, ); self.draw_popups(f); @@ -320,10 +328,9 @@ impl App { Tabs::default() .block(Block::default().borders(Borders::BOTTOM)) .titles(&[strings::TAB_STATUS, strings::TAB_LOG]) - .style(Style::default().fg(Color::White)) .highlight_style( - Style::default() - .fg(Color::Yellow) + self.theme + .tab(true) .modifier(Modifier::UNDERLINED), ) .divider(strings::TAB_DIVIDER) @@ -336,28 +343,20 @@ impl App { f: &mut Frame, r: Rect, cmds: &[CommandInfo], + theme: Theme, ) { let splitter = Text::Styled( Cow::from(strings::CMD_SPLITTER), Style::default(), ); - let style_enabled = - Style::default().fg(Color::White).bg(Color::Blue); - - let style_disabled = - Style::default().fg(Color::DarkGray).bg(Color::Blue); let texts = cmds .iter() .filter_map(|c| { if c.show_in_quickbar() { Some(Text::Styled( Cow::from(c.text.name), - if c.enabled { - style_enabled - } else { - style_disabled - }, + theme.toolbar(c.enabled), )) } else { None diff --git a/src/components/changes.rs b/src/components/changes.rs index 66418a03..6ea16519 100644 --- a/src/components/changes.rs +++ b/src/components/changes.rs @@ -3,6 +3,7 @@ use super::{ statustree::{MoveSelection, StatusTree}, CommandBlocking, DrawableComponent, }; +use crate::ui::style::Theme; use crate::{ components::{CommandInfo, Component}, keys, @@ -13,13 +14,7 @@ use asyncgit::{hash, sync, StatusItem, StatusItemType, CWD}; use crossterm::event::Event; use std::{borrow::Cow, convert::From, path::Path}; use strings::commands; -use tui::{ - backend::Backend, - layout::Rect, - style::{Color, Style}, - widgets::Text, - Frame, -}; +use tui::{backend::Backend, layout::Rect, widgets::Text, Frame}; /// pub struct ChangesComponent { @@ -30,6 +25,7 @@ pub struct ChangesComponent { show_selection: bool, is_working_dir: bool, queue: Queue, + theme: Theme, } impl ChangesComponent { @@ -39,6 +35,7 @@ impl ChangesComponent { focus: bool, is_working_dir: bool, queue: Queue, + theme: Theme, ) -> Self { Self { title: title.to_string(), @@ -48,6 +45,7 @@ impl ChangesComponent { show_selection: focus, is_working_dir, queue, + theme, } } @@ -154,9 +152,8 @@ impl ChangesComponent { item: &FileTreeItem, width: u16, selected: bool, + theme: Theme, ) -> Option { - let select_color = Color::Rgb(0, 0, 100); - let indent_str = if item.info.indent == 0 { String::from("") } else { @@ -189,18 +186,14 @@ impl ChangesComponent { format!("{} {}{}", status_char, indent_str, file) }; - let mut style = - Style::default().fg(Self::item_color( - status_item - .status - .unwrap_or(StatusItemType::Modified), - )); + let status = status_item + .status + .unwrap_or(StatusItemType::Modified); - if selected { - style = style.bg(select_color); - } - - Some(Text::Styled(Cow::from(txt), style)) + Some(Text::Styled( + Cow::from(txt), + theme.item(status, selected), + )) } FileTreeItemKind::Path(path_collapsed) => { @@ -222,27 +215,14 @@ impl ChangesComponent { ) }; - let mut style = Style::default(); - - if selected { - style = style.bg(select_color); - } - - Some(Text::Styled(Cow::from(txt), style)) + Some(Text::Styled( + Cow::from(txt), + theme.text(true, selected), + )) } } } - fn item_color(item_type: StatusItemType) -> Color { - match item_type { - StatusItemType::Modified => Color::LightYellow, - StatusItemType::New => Color::LightGreen, - StatusItemType::Deleted => Color::LightRed, - StatusItemType::Renamed => Color::LightMagenta, - _ => Color::White, - } - } - fn item_status_char(item_type: Option) -> char { if let Some(item_type) = item_type { match item_type { @@ -287,6 +267,7 @@ impl DrawableComponent for ChangesComponent { .tree .selection .map_or(false, |e| e == idx), + self.theme, ) }, ); @@ -298,6 +279,7 @@ impl DrawableComponent for ChangesComponent { items, self.tree.selection.map(|idx| idx - selection_offset), self.focused, + self.theme, ); } } diff --git a/src/components/commit.rs b/src/components/commit.rs index 637fa3df..2cdd042e 100644 --- a/src/components/commit.rs +++ b/src/components/commit.rs @@ -2,6 +2,8 @@ use super::{ visibility_blocking, CommandBlocking, CommandInfo, Component, DrawableComponent, }; +use crate::components::dialog_paragraph; +use crate::ui::style::Theme; use crate::{ queue::{InternalEvent, NeedsUpdate, Queue}, strings, ui, @@ -12,11 +14,11 @@ use log::error; use std::borrow::Cow; use strings::commands; use sync::HookResult; +use tui::style::Style; use tui::{ backend::Backend, - layout::{Alignment, Rect}, - style::{Color, Style}, - widgets::{Block, Borders, Clear, Paragraph, Text}, + layout::Rect, + widgets::{Clear, Text}, Frame, }; @@ -24,6 +26,7 @@ pub struct CommitComponent { msg: String, visible: bool, queue: Queue, + theme: Theme, } impl DrawableComponent for CommitComponent { @@ -32,22 +35,19 @@ impl DrawableComponent for CommitComponent { let txt = if self.msg.is_empty() { [Text::Styled( Cow::from(strings::COMMIT_MSG), - Style::default().fg(Color::DarkGray), + self.theme.text(false, false), )] } else { - [Text::Raw(Cow::from(self.msg.clone()))] + [Text::Styled( + Cow::from(self.msg.clone()), + Style::default(), + )] }; let area = ui::centered_rect(60, 20, f.size()); f.render_widget(Clear, area); f.render_widget( - Paragraph::new(txt.iter()) - .block( - Block::default() - .title(strings::COMMIT_TITLE) - .borders(Borders::ALL), - ) - .alignment(Alignment::Left), + dialog_paragraph(strings::COMMIT_TITLE, txt.iter()), area, ); } @@ -112,11 +112,12 @@ impl Component for CommitComponent { impl CommitComponent { /// - pub fn new(queue: Queue) -> Self { + pub fn new(queue: Queue, theme: Theme) -> Self { Self { queue, msg: String::default(), visible: false, + theme, } } diff --git a/src/components/diff.rs b/src/components/diff.rs index 4c62eae4..5aa99857 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -1,4 +1,5 @@ use super::{CommandBlocking, DrawableComponent, ScrollType}; +use crate::ui::style::Theme; use crate::{ components::{CommandInfo, Component}, keys, @@ -9,10 +10,11 @@ use asyncgit::{hash, DiffLine, DiffLineType, FileDiff}; use crossterm::event::Event; use std::{borrow::Cow, cmp, convert::TryFrom}; use strings::commands; + use tui::{ backend::Backend, layout::{Alignment, Rect}, - style::{Color, Modifier, Style}, + style::Modifier, symbols, widgets::{Block, Borders, Paragraph, Text}, Frame, @@ -34,11 +36,12 @@ pub struct DiffComponent { current: Current, selected_hunk: Option, queue: Queue, + theme: Theme, } impl DiffComponent { /// - pub fn new(queue: Queue) -> Self { + pub fn new(queue: Queue, theme: Theme) -> Self { Self { focused: false, queue, @@ -47,6 +50,7 @@ impl DiffComponent { diff: FileDiff::default(), scroll: 0, current_height: 0, + theme, } } /// @@ -171,6 +175,7 @@ impl DiffComponent { selection == line_cursor, hunk_selected, i == hunk_len as usize - 1, + self.theme, ); lines_added += 1; } @@ -191,22 +196,10 @@ impl DiffComponent { selected: bool, selected_hunk: bool, end_of_hunk: bool, + theme: Theme, ) { - let select_color = Color::Rgb(0, 0, 100); - let style_default = Style::default().bg(if selected { - select_color - } else { - Color::Reset - }); - { - let style = Style::default() - .bg(if selected || selected_hunk { - select_color - } else { - Color::Reset - }) - .fg(Color::DarkGray); + let style = theme.text(false, selected || selected_hunk); if end_of_hunk { text.push(Text::Styled( @@ -227,17 +220,6 @@ impl DiffComponent { } } - let style_delete = Style::default() - .fg(Color::Red) - .bg(if selected { select_color } else { Color::Reset }); - let style_add = Style::default() - .fg(Color::Green) - .bg(if selected { select_color } else { Color::Reset }); - let style_header = Style::default() - .fg(Color::White) - .bg(if selected { select_color } else { Color::Reset }) - .modifier(Modifier::BOLD); - let trimmed = line.content.trim_matches(|c| c == '\n' || c == '\r'); @@ -251,16 +233,10 @@ impl DiffComponent { //TODO: allow customize tabsize let content = Cow::from(filled.replace("\t", " ")); - text.push(match line.line_type { - DiffLineType::Delete => { - Text::Styled(content, style_delete) - } - DiffLineType::Add => Text::Styled(content, style_add), - DiffLineType::Header => { - Text::Styled(content, style_header) - } - _ => Text::Styled(content, style_default), - }); + text.push(Text::Styled( + content, + theme.diff_line(line.line_type, selected), + )); } fn hunk_visible( @@ -299,13 +275,6 @@ impl DiffComponent { impl DrawableComponent for DiffComponent { fn draw(&mut self, f: &mut Frame, r: Rect) { self.current_height = r.height.saturating_sub(2); - let mut style_border = Style::default().fg(Color::DarkGray); - let mut style_title = Style::default(); - if self.focused { - style_border = style_border.fg(Color::Gray); - style_title = style_title.modifier(Modifier::BOLD); - } - let title = format!("{}{}", strings::TITLE_DIFF, self.current.path); f.render_widget( @@ -314,8 +283,12 @@ impl DrawableComponent for DiffComponent { Block::default() .title(title.as_str()) .borders(Borders::ALL) - .border_style(style_border) - .title_style(style_title), + .border_style(self.theme.block(self.focused)) + .title_style( + self.theme + .text(self.focused, false) + .modifier(Modifier::BOLD), + ), ) .alignment(Alignment::Left), r, @@ -414,7 +387,6 @@ mod tests { #[test] fn test_lineendings() { let mut text = Vec::new(); - DiffComponent::add_line( &mut text, 10, @@ -425,6 +397,7 @@ mod tests { false, false, false, + crate::ui::style::DARK_THEME, ); assert_eq!(text.len(), 2); diff --git a/src/components/help.rs b/src/components/help.rs index 43e0305d..b9787338 100644 --- a/src/components/help.rs +++ b/src/components/help.rs @@ -2,26 +2,27 @@ use super::{ visibility_blocking, CommandBlocking, CommandInfo, Component, DrawableComponent, }; +use crate::ui::style::Theme; use crate::{keys, strings, ui, version::Version}; use asyncgit::hash; use crossterm::event::Event; use itertools::Itertools; use std::{borrow::Cow, cmp, convert::TryFrom}; use strings::commands; +use tui::style::{Modifier, Style}; use tui::{ backend::Backend, layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Style}, widgets::{Block, Borders, Clear, Paragraph, Text}, Frame, }; /// -#[derive(Default)] pub struct HelpComponent { cmds: Vec, visible: bool, selection: u16, + theme: Theme, } impl DrawableComponent for HelpComponent { @@ -68,10 +69,13 @@ impl DrawableComponent for HelpComponent { f.render_widget( Paragraph::new( - vec![Text::Raw(Cow::from(format!( - "gitui {}", - Version::new(), - )))] + vec![Text::Styled( + Cow::from(format!( + "gitui {}", + Version::new(), + )), + Style::default(), + )] .iter(), ) .alignment(Alignment::Right), @@ -150,6 +154,14 @@ impl Component for HelpComponent { } impl HelpComponent { + pub fn new(theme: Theme) -> Self { + Self { + cmds: vec![], + visible: false, + selection: 0, + theme, + } + } /// pub fn set_cmds(&mut self, cmds: Vec) { self.cmds = cmds @@ -187,7 +199,7 @@ impl HelpComponent { { txt.push(Text::Styled( Cow::from(format!(" {}\n", key)), - Style::default().fg(Color::Black).bg(Color::Gray), + Style::default().modifier(Modifier::REVERSED), )); txt.extend( @@ -216,13 +228,10 @@ impl HelpComponent { ); } - let style = if is_selected { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }; - - Text::Styled(Cow::from(out), style) + Text::Styled( + Cow::from(out), + self.theme.text(true, is_selected), + ) }) .collect::>(), ); diff --git a/src/components/mod.rs b/src/components/mod.rs index e0920d25..fd1f3235 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -18,6 +18,8 @@ pub use filetree::FileTreeItemKind; pub use help::HelpComponent; pub use msg::MsgComponent; pub use reset::ResetComponent; +use tui::layout::Alignment; +use tui::widgets::{Block, Borders, Paragraph, Text}; /// creates accessors for a list of components /// @@ -114,3 +116,15 @@ pub trait Component { /// fn show(&mut self) {} } + +fn dialog_paragraph<'a, 't, T>( + title: &'a str, + content: T, +) -> Paragraph<'a, 't, T> +where + T: Iterator>, +{ + Paragraph::new(content) + .block(Block::default().title(title).borders(Borders::ALL)) + .alignment(Alignment::Left) +} diff --git a/src/components/msg.rs b/src/components/msg.rs index 78847a89..bf8daacd 100644 --- a/src/components/msg.rs +++ b/src/components/msg.rs @@ -2,14 +2,15 @@ use super::{ visibility_blocking, CommandBlocking, CommandInfo, Component, DrawableComponent, }; +use crate::components::dialog_paragraph; use crate::{keys, strings, ui}; use crossterm::event::Event; use std::borrow::Cow; use strings::commands; use tui::{ backend::Backend, - layout::{Alignment, Rect}, - widgets::{Block, Borders, Clear, Paragraph, Text}, + layout::Rect, + widgets::{Clear, Text}, Frame, }; @@ -27,14 +28,8 @@ impl DrawableComponent for MsgComponent { let area = ui::centered_rect_absolute(65, 25, f.size()); f.render_widget(Clear, area); f.render_widget( - Paragraph::new(txt.iter()) - .block( - Block::default() - .title(strings::MSG_TITLE) - .borders(Borders::ALL), - ) - .wrap(true) - .alignment(Alignment::Left), + dialog_paragraph(strings::MSG_TITLE, txt.iter()) + .wrap(true), area, ); } diff --git a/src/components/reset.rs b/src/components/reset.rs index e5b481ad..e755e5bd 100644 --- a/src/components/reset.rs +++ b/src/components/reset.rs @@ -7,14 +7,15 @@ use crate::{ strings, ui, }; +use crate::components::dialog_paragraph; +use crate::ui::style::Theme; use crossterm::event::{Event, KeyCode}; use std::borrow::Cow; use strings::commands; use tui::{ backend::Backend, - layout::{Alignment, Rect}, - style::{Color, Style}, - widgets::{Block, Borders, Clear, Paragraph, Text}, + layout::Rect, + widgets::{Clear, Text}, Frame, }; @@ -23,6 +24,7 @@ pub struct ResetComponent { target: Option, visible: bool, queue: Queue, + theme: Theme, } impl DrawableComponent for ResetComponent { @@ -31,19 +33,13 @@ impl DrawableComponent for ResetComponent { let mut txt = Vec::new(); txt.push(Text::Styled( Cow::from(strings::RESET_MSG), - Style::default().fg(Color::Red), + self.theme.text_danger(), )); let area = ui::centered_rect(30, 20, f.size()); f.render_widget(Clear, area); f.render_widget( - Paragraph::new(txt.iter()) - .block( - Block::default() - .title(strings::RESET_TITLE) - .borders(Borders::ALL), - ) - .alignment(Alignment::Left), + dialog_paragraph(strings::RESET_TITLE, txt.iter()), area, ); } @@ -106,11 +102,12 @@ impl Component for ResetComponent { impl ResetComponent { /// - pub fn new(queue: Queue) -> Self { + pub fn new(queue: Queue, theme: Theme) -> Self { Self { target: None, visible: false, queue, + theme, } } /// diff --git a/src/main.rs b/src/main.rs index 356455e7..7115ace0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,7 @@ use scopeguard::defer; use scopetime::scope_time; use simplelog::{Config, LevelFilter, WriteLogger}; use spinner::Spinner; +use std::path::PathBuf; use std::{ env, fs, fs::File, @@ -175,12 +176,18 @@ fn start_terminal( Ok(terminal) } +#[must_use] +pub fn get_app_config_path() -> PathBuf { + let mut path = dirs::cache_dir().unwrap(); + path.push("gitui"); + fs::create_dir_all(&path).unwrap(); + path +} + fn setup_logging() { if env::var("GITUI_LOGGING").is_ok() { - let mut path = dirs::cache_dir().unwrap(); - path.push("gitui"); + let mut path = get_app_config_path(); path.push("gitui.log"); - fs::create_dir_all(path.parent().unwrap()).unwrap(); let _ = WriteLogger::init( LevelFilter::Trace, diff --git a/src/tabs/revlog/mod.rs b/src/tabs/revlog/mod.rs index e430c1e1..982f25f9 100644 --- a/src/tabs/revlog/mod.rs +++ b/src/tabs/revlog/mod.rs @@ -1,5 +1,6 @@ mod utils; +use crate::ui::style::Theme; use crate::{ components::{ CommandBlocking, CommandInfo, Component, DrawableComponent, @@ -17,34 +18,13 @@ use sync::Tags; use tui::{ backend::Backend, layout::{Alignment, Rect}, - style::{Color, Style}, widgets::{Block, Borders, Paragraph, Text}, Frame, }; use utils::{ItemBatch, LogEntry}; -const COLOR_SELECTION_BG: Color = Color::Blue; - -const STYLE_TAG: Style = Style::new().fg(Color::Yellow); -const STYLE_HASH: Style = Style::new().fg(Color::Magenta); -const STYLE_TIME: Style = Style::new().fg(Color::Blue); -const STYLE_AUTHOR: Style = Style::new().fg(Color::Green); -const STYLE_MSG: Style = Style::new().fg(Color::Reset); - -const STYLE_TAG_SELECTED: Style = - Style::new().fg(Color::Yellow).bg(COLOR_SELECTION_BG); -const STYLE_HASH_SELECTED: Style = - Style::new().fg(Color::Magenta).bg(COLOR_SELECTION_BG); -const STYLE_TIME_SELECTED: Style = - Style::new().fg(Color::White).bg(COLOR_SELECTION_BG); -const STYLE_AUTHOR_SELECTED: Style = - Style::new().fg(Color::Green).bg(COLOR_SELECTION_BG); -const STYLE_MSG_SELECTED: Style = - Style::new().fg(Color::Reset).bg(COLOR_SELECTION_BG); - static ELEMENTS_PER_LINE: usize = 10; static SLICE_SIZE: usize = 1200; - /// pub struct Revlog { selection: usize, @@ -57,11 +37,15 @@ pub struct Revlog { tags: Tags, current_size: (u16, u16), scroll_top: usize, + theme: Theme, } impl Revlog { /// - pub fn new(sender: &Sender) -> Self { + pub fn new( + sender: &Sender, + theme: Theme, + ) -> Self { Self { items: ItemBatch::default(), git_log: AsyncLog::new(sender.clone()), @@ -73,6 +57,7 @@ impl Revlog { tags: Tags::new(), current_size: (0, 0), scroll_top: 0, + theme, } } @@ -171,44 +156,27 @@ impl Revlog { selected: bool, txt: &mut Vec>, tags: Option, + theme: Theme, ) { let count_before = txt.len(); let splitter_txt = Cow::from(" "); - let splitter = if selected { - Text::Styled( - splitter_txt, - Style::new().bg(COLOR_SELECTION_BG), - ) - } else { - Text::Raw(splitter_txt) - }; + let splitter = + Text::Styled(splitter_txt, theme.text(true, selected)); txt.push(Text::Styled( Cow::from(&e.hash[0..7]), - if selected { - STYLE_HASH_SELECTED - } else { - STYLE_HASH - }, + theme.table(0, selected), )); txt.push(splitter.clone()); txt.push(Text::Styled( Cow::from(e.time.as_str()), - if selected { - STYLE_TIME_SELECTED - } else { - STYLE_TIME - }, + theme.table(1, selected), )); txt.push(splitter.clone()); txt.push(Text::Styled( Cow::from(e.author.as_str()), - if selected { - STYLE_AUTHOR_SELECTED - } else { - STYLE_AUTHOR - }, + theme.table(2, selected), )); txt.push(splitter.clone()); txt.push(Text::Styled( @@ -217,20 +185,12 @@ impl Revlog { } else { String::from("") }), - if selected { - STYLE_TAG_SELECTED - } else { - STYLE_TAG - }, + theme.tab(true).bg(theme.text(true, selected).bg), )); txt.push(splitter); txt.push(Text::Styled( Cow::from(e.msg.as_str()), - if selected { - STYLE_MSG_SELECTED - } else { - STYLE_MSG - }, + theme.text(true, selected), )); txt.push(Text::Raw(Cow::from("\n"))); @@ -248,7 +208,13 @@ impl Revlog { } else { None }; - Self::add_entry(e, idx == selection, &mut txt, tag); + Self::add_entry( + e, + idx == selection, + &mut txt, + tag, + self.theme, + ); } txt diff --git a/src/tabs/status.rs b/src/tabs/status.rs index f7e78d3c..c3384932 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -1,3 +1,4 @@ +use crate::ui::style::Theme; use crate::{ accessors, components::{ @@ -99,6 +100,7 @@ impl Status { pub fn new( sender: &Sender, queue: &Queue, + theme: Theme, ) -> Self { Self { visible: true, @@ -109,14 +111,16 @@ impl Status { true, true, queue.clone(), + theme, ), index: ChangesComponent::new( strings::TITLE_INDEX, false, false, queue.clone(), + theme, ), - diff: DiffComponent::new(queue.clone()), + diff: DiffComponent::new(queue.clone(), theme), git_diff: AsyncDiff::new(sender.clone()), git_status: AsyncStatus::new(sender.clone()), } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 33a614dc..f829b7a1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,10 +1,11 @@ mod scrolllist; - +pub(crate) mod style; +use crate::ui::style::Theme; use scrolllist::ScrollableList; +use tui::style::Modifier; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, widgets::{Block, Borders, Text}, Frame, }; @@ -76,24 +77,24 @@ pub fn draw_list<'b, B: Backend, L>( items: L, select: Option, selected: bool, + theme: Theme, ) where L: Iterator>, { - let mut style_border = Style::default().fg(Color::DarkGray); - let mut style_title = Style::default(); - if selected { - style_border = style_border.fg(Color::Gray); - style_title = style_title.modifier(Modifier::BOLD); - } + let style = if selected { + theme.block(selected).modifier(Modifier::BOLD) + } else { + theme.block(selected) + }; + let list = ScrollableList::new(items) .block( Block::default() .title(title) .borders(Borders::ALL) - .title_style(style_title) - .border_style(style_border), + .title_style(style) + .border_style(theme.block(selected)), ) - .scroll(select.unwrap_or_default()) - .style(Style::default().fg(Color::White)); + .scroll(select.unwrap_or_default()); f.render_widget(list, r) } diff --git a/src/ui/scrolllist.rs b/src/ui/scrolllist.rs index d301d76f..36027879 100644 --- a/src/ui/scrolllist.rs +++ b/src/ui/scrolllist.rs @@ -38,11 +38,6 @@ where self } - pub fn style(mut self, style: Style) -> Self { - self.style = style; - self - } - pub fn scroll(mut self, index: usize) -> Self { self.scroll = index; self diff --git a/src/ui/style.rs b/src/ui/style.rs new file mode 100644 index 00000000..aaee2ad1 --- /dev/null +++ b/src/ui/style.rs @@ -0,0 +1,237 @@ +use crate::get_app_config_path; +use asyncgit::{DiffLineType, StatusItemType}; +use ron::de::from_bytes; +use ron::ser::{to_string_pretty, PrettyConfig}; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::PathBuf; +use tui::style::{Color, Modifier, Style}; + +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy)] +pub struct Theme { + selected_tab: ColorDef, + command_foreground: ColorDef, + command_background: ColorDef, + command_disabled: ColorDef, + diff_line_add: ColorDef, + diff_line_delete: ColorDef, + diff_file_added: ColorDef, + diff_file_removed: ColorDef, + diff_file_moved: ColorDef, + diff_file_modified: ColorDef, + table_colors: [ColorDef; 3], +} + +pub const DARK_THEME: Theme = Theme { + selected_tab: ColorDef::Yellow, + command_foreground: ColorDef::White, + command_background: ColorDef::Rgb(0, 0, 100), + command_disabled: ColorDef::DarkGray, + diff_line_add: ColorDef::Green, + diff_line_delete: ColorDef::Red, + diff_file_added: ColorDef::LightGreen, + diff_file_removed: ColorDef::LightRed, + diff_file_moved: ColorDef::LightMagenta, + diff_file_modified: ColorDef::Yellow, + table_colors: [ + ColorDef::Magenta, + ColorDef::Blue, + ColorDef::Green, + ], +}; + +impl Theme { + pub fn block(&self, focus: bool) -> Style { + if focus { + Style::default() + } else { + Style::default().fg(self.command_disabled.into()) + } + } + + pub fn tab(&self, selected: bool) -> Style { + if selected { + Style::default().fg(self.selected_tab.into()) + } else { + Style::default() + } + } + + pub fn text(&self, enabled: bool, selected: bool) -> Style { + match (enabled, selected) { + (false, _) => { + Style::default().fg(self.command_disabled.into()) + } + (true, false) => Style::default(), + (true, true) => { + Style::default().bg(self.command_background.into()) + } + } + } + + pub fn item(&self, typ: StatusItemType, selected: bool) -> Style { + let style = match typ { + StatusItemType::New => { + Style::default().fg(self.diff_file_added.into()) + } + StatusItemType::Modified => { + Style::default().fg(self.diff_file_modified.into()) + } + StatusItemType::Deleted => { + Style::default().fg(self.diff_file_removed.into()) + } + StatusItemType::Renamed => { + Style::default().fg(self.diff_file_moved.into()) + } + _ => Style::default(), + }; + + self.apply_select(style, selected) + } + + fn apply_select(&self, style: Style, selected: bool) -> Style { + if selected { + style.bg(self.command_background.into()) + } else { + style + } + } + + pub fn diff_line( + &self, + typ: DiffLineType, + selected: bool, + ) -> Style { + let style = match typ { + DiffLineType::Add => { + Style::default().fg(self.diff_line_add.into()) + } + DiffLineType::Delete => { + Style::default().fg(self.diff_line_delete.into()) + } + DiffLineType::Header => { + Style::default().modifier(Modifier::BOLD) + } + _ => Style::default(), + }; + + self.apply_select(style, selected) + } + + pub fn text_danger(&self) -> Style { + Style::default().fg(self.diff_file_removed.into()) + } + + pub fn toolbar(&self, enabled: bool) -> Style { + if enabled { + Style::default().fg(self.command_foreground.into()) + } else { + Style::default().fg(self.command_disabled.into()) + } + .bg(self.command_background.into()) + } + + pub fn table(&self, column: usize, selected: bool) -> Style { + self.apply_select( + Style::default().fg(self.table_colors[column].into()), + selected, + ) + } + + fn save(&self) -> Result<(), std::io::Error> { + let theme_file = Self::get_theme_file(); + let mut file = File::create(theme_file)?; + let data = to_string_pretty(self, PrettyConfig::default()) + .map_err(|_| std::io::Error::from_raw_os_error(100))?; + file.write_all(data.as_bytes())?; + Ok(()) + } + + fn get_theme_file() -> PathBuf { + let app_home = get_app_config_path(); + app_home.join("theme.ron") + } + + fn read_file( + theme_file: PathBuf, + ) -> Result { + if theme_file.exists() { + let mut f = File::open(theme_file)?; + let mut buffer = Vec::new(); + f.read_to_end(&mut buffer)?; + + Ok(from_bytes(&buffer).map_err(|_| { + std::io::Error::from_raw_os_error(100) + })?) + } else { + Err(std::io::Error::from_raw_os_error(100)) + } + } + + pub fn init() -> Theme { + if let Ok(x) = Theme::read_file(Theme::get_theme_file()) { + x + } else { + DARK_THEME.save().unwrap_or_default(); + DARK_THEME + } + } +} + +/// we duplicate the Color definition from `tui` crate to implement Serde serialisation +/// this enum can be removed once [tui-#292](https://github.com/fdehau/tui-rs/issues/292) is resolved +#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +pub enum ColorDef { + Reset, + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + Gray, + DarkGray, + LightRed, + LightGreen, + LightYellow, + LightBlue, + LightMagenta, + LightCyan, + White, + Rgb(u8, u8, u8), + Indexed(u8), +} + +impl Default for ColorDef { + fn default() -> Self { + ColorDef::Reset + } +} + +impl From for Color { + fn from(def: ColorDef) -> Self { + match def { + ColorDef::Reset => Color::Reset, + ColorDef::Black => Color::Black, + ColorDef::Red => Color::Red, + ColorDef::Green => Color::Green, + ColorDef::Yellow => Color::Yellow, + ColorDef::Blue => Color::Blue, + ColorDef::Magenta => Color::Magenta, + ColorDef::Cyan => Color::Cyan, + ColorDef::Gray => Color::Gray, + ColorDef::DarkGray => Color::DarkGray, + ColorDef::LightRed => Color::LightRed, + ColorDef::LightGreen => Color::LightGreen, + ColorDef::LightYellow => Color::LightYellow, + ColorDef::LightBlue => Color::LightBlue, + ColorDef::LightMagenta => Color::LightMagenta, + ColorDef::LightCyan => Color::LightCyan, + ColorDef::White => Color::White, + ColorDef::Rgb(a, b, c) => Color::Rgb(a, b, c), + ColorDef::Indexed(x) => Color::Indexed(x), + } + } +}