Implement color themes #65 (closes #28)

This commit is contained in:
Mehran Kordi 2020-05-19 20:19:30 +02:00 committed by GitHub
parent e73cdb67bc
commit 4ec1a4e94b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 470 additions and 231 deletions

View file

@ -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

50
Cargo.lock generated
View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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<AsyncNotification>) -> 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<B>,
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

View file

@ -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<Text> {
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<StatusItemType>) -> 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,
);
}
}

View file

@ -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,
}
}

View file

@ -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<u16>,
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<B: Backend>(&mut self, f: &mut Frame<B>, 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);

View file

@ -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<CommandInfo>,
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<CommandInfo>) {
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::<Vec<_>>(),
);

View file

@ -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<Item = &'t Text<'t>>,
{
Paragraph::new(content)
.block(Block::default().title(title).borders(Borders::ALL))
.alignment(Alignment::Left)
}

View file

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

View file

@ -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<ResetItem>,
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,
}
}
///

View file

@ -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<W: Write>(
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,

View file

@ -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<AsyncNotification>) -> Self {
pub fn new(
sender: &Sender<AsyncNotification>,
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<Text<'a>>,
tags: Option<String>,
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

View file

@ -1,3 +1,4 @@
use crate::ui::style::Theme;
use crate::{
accessors,
components::{
@ -99,6 +100,7 @@ impl Status {
pub fn new(
sender: &Sender<AsyncNotification>,
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()),
}

View file

@ -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<usize>,
selected: bool,
theme: Theme,
) where
L: Iterator<Item = Text<'b>>,
{
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)
}

View file

@ -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

237
src/ui/style.rs Normal file
View file

@ -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<Theme, std::io::Error> {
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<ColorDef> 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),
}
}
}