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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Added
- support for color themes and light mode([#28](https://github.com/extrawurst/gitui/issues/28))
## [0.2.6] - 2020-05-18 ## [0.2.6] - 2020-05-18
### Fixed ### Fixed

50
Cargo.lock generated
View file

@ -59,6 +59,15 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base64"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.11.0" version = "0.11.0"
@ -82,6 +91,12 @@ dependencies = [
"constant_time_eq", "constant_time_eq",
] ]
[[package]]
name = "byteorder"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]] [[package]]
name = "cassowary" name = "cassowary"
version = "0.3.0" version = "0.3.0"
@ -299,8 +314,10 @@ dependencies = [
"itertools", "itertools",
"log", "log",
"rayon-core", "rayon-core",
"ron",
"scopeguard", "scopeguard",
"scopetime", "scopetime",
"serde",
"simplelog", "simplelog",
"tui", "tui",
] ]
@ -654,13 +671,24 @@ dependencies = [
"winapi 0.3.8", "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]] [[package]]
name = "rust-argon2" name = "rust-argon2"
version = "0.7.0" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017"
dependencies = [ dependencies = [
"base64", "base64 0.11.0",
"blake2b_simd", "blake2b_simd",
"constant_time_eq", "constant_time_eq",
"crossbeam-utils", "crossbeam-utils",
@ -685,6 +713,26 @@ dependencies = [
"log", "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]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.1.15" version = "0.1.15"

View file

@ -31,8 +31,10 @@ scopeguard = "1.1"
bitflags = "1.2" bitflags = "1.2"
chrono = "0.4" chrono = "0.4"
backtrace = { version = "0.3" } backtrace = { version = "0.3" }
ron = "0.5.1"
scopetime = { path = "./scopetime", version = "0.1" } scopetime = { path = "./scopetime", version = "0.1" }
asyncgit = { path = "./asyncgit", version = "0.2" } asyncgit = { path = "./asyncgit", version = "0.2" }
serde = "1.0.110"
[features] [features]
default=[] default=[]
@ -45,6 +47,6 @@ members=[
] ]
[profile.release] [profile.release]
lto = true lto = true
opt-level = 'z' # Optimize for size. opt-level = 'z' # Optimize for size.
codegen-units = 1 codegen-units = 1

View file

@ -80,6 +80,13 @@ this will log to:
* `$XDG_CACHE_HOME/gitui/gitui.log` (linux using `XDG`) * `$XDG_CACHE_HOME/gitui/gitui.log` (linux using `XDG`)
* `$HOME/.cache/gitui/gitui.log` (linux) * `$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 # inspiration
* https://github.com/jesseduffield/lazygit * https://github.com/jesseduffield/lazygit

View file

@ -1,3 +1,4 @@
use crate::ui::style::Theme;
use crate::{ use crate::{
accessors, accessors,
components::{ components::{
@ -17,10 +18,11 @@ use itertools::Itertools;
use log::trace; use log::trace;
use std::borrow::Cow; use std::borrow::Cow;
use strings::commands; use strings::commands;
use tui::style::Style;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::Modifier,
widgets::{Block, Borders, Paragraph, Tabs, Text}, widgets::{Block, Borders, Paragraph, Tabs, Text},
Frame, Frame,
}; };
@ -37,6 +39,7 @@ pub struct App {
revlog: Revlog, revlog: Revlog,
status_tab: Status, status_tab: Status,
queue: Queue, queue: Queue,
theme: Theme,
} }
// public interface // public interface
@ -44,17 +47,21 @@ impl App {
/// ///
pub fn new(sender: &Sender<AsyncNotification>) -> Self { pub fn new(sender: &Sender<AsyncNotification>) -> Self {
let queue = Queue::default(); let queue = Queue::default();
let theme = Theme::init();
Self { Self {
reset: ResetComponent::new(queue.clone()), reset: ResetComponent::new(queue.clone(), theme),
commit: CommitComponent::new(queue.clone()), commit: CommitComponent::new(queue.clone(), theme),
do_quit: false, do_quit: false,
current_commands: Vec::new(), current_commands: Vec::new(),
help: HelpComponent::default(), help: HelpComponent::new(theme),
msg: MsgComponent::default(), msg: MsgComponent::default(),
tab: 0, tab: 0,
revlog: Revlog::new(&sender), revlog: Revlog::new(&sender, theme),
status_tab: Status::new(&sender, &queue), status_tab: Status::new(&sender, &queue, theme),
queue, queue,
theme,
} }
} }
@ -84,6 +91,7 @@ impl App {
f, f,
chunks_main[2], chunks_main[2],
self.current_commands.as_slice(), self.current_commands.as_slice(),
self.theme,
); );
self.draw_popups(f); self.draw_popups(f);
@ -320,10 +328,9 @@ impl App {
Tabs::default() Tabs::default()
.block(Block::default().borders(Borders::BOTTOM)) .block(Block::default().borders(Borders::BOTTOM))
.titles(&[strings::TAB_STATUS, strings::TAB_LOG]) .titles(&[strings::TAB_STATUS, strings::TAB_LOG])
.style(Style::default().fg(Color::White))
.highlight_style( .highlight_style(
Style::default() self.theme
.fg(Color::Yellow) .tab(true)
.modifier(Modifier::UNDERLINED), .modifier(Modifier::UNDERLINED),
) )
.divider(strings::TAB_DIVIDER) .divider(strings::TAB_DIVIDER)
@ -336,28 +343,20 @@ impl App {
f: &mut Frame<B>, f: &mut Frame<B>,
r: Rect, r: Rect,
cmds: &[CommandInfo], cmds: &[CommandInfo],
theme: Theme,
) { ) {
let splitter = Text::Styled( let splitter = Text::Styled(
Cow::from(strings::CMD_SPLITTER), Cow::from(strings::CMD_SPLITTER),
Style::default(), 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 let texts = cmds
.iter() .iter()
.filter_map(|c| { .filter_map(|c| {
if c.show_in_quickbar() { if c.show_in_quickbar() {
Some(Text::Styled( Some(Text::Styled(
Cow::from(c.text.name), Cow::from(c.text.name),
if c.enabled { theme.toolbar(c.enabled),
style_enabled
} else {
style_disabled
},
)) ))
} else { } else {
None None

View file

@ -3,6 +3,7 @@ use super::{
statustree::{MoveSelection, StatusTree}, statustree::{MoveSelection, StatusTree},
CommandBlocking, DrawableComponent, CommandBlocking, DrawableComponent,
}; };
use crate::ui::style::Theme;
use crate::{ use crate::{
components::{CommandInfo, Component}, components::{CommandInfo, Component},
keys, keys,
@ -13,13 +14,7 @@ use asyncgit::{hash, sync, StatusItem, StatusItemType, CWD};
use crossterm::event::Event; use crossterm::event::Event;
use std::{borrow::Cow, convert::From, path::Path}; use std::{borrow::Cow, convert::From, path::Path};
use strings::commands; use strings::commands;
use tui::{ use tui::{backend::Backend, layout::Rect, widgets::Text, Frame};
backend::Backend,
layout::Rect,
style::{Color, Style},
widgets::Text,
Frame,
};
/// ///
pub struct ChangesComponent { pub struct ChangesComponent {
@ -30,6 +25,7 @@ pub struct ChangesComponent {
show_selection: bool, show_selection: bool,
is_working_dir: bool, is_working_dir: bool,
queue: Queue, queue: Queue,
theme: Theme,
} }
impl ChangesComponent { impl ChangesComponent {
@ -39,6 +35,7 @@ impl ChangesComponent {
focus: bool, focus: bool,
is_working_dir: bool, is_working_dir: bool,
queue: Queue, queue: Queue,
theme: Theme,
) -> Self { ) -> Self {
Self { Self {
title: title.to_string(), title: title.to_string(),
@ -48,6 +45,7 @@ impl ChangesComponent {
show_selection: focus, show_selection: focus,
is_working_dir, is_working_dir,
queue, queue,
theme,
} }
} }
@ -154,9 +152,8 @@ impl ChangesComponent {
item: &FileTreeItem, item: &FileTreeItem,
width: u16, width: u16,
selected: bool, selected: bool,
theme: Theme,
) -> Option<Text> { ) -> Option<Text> {
let select_color = Color::Rgb(0, 0, 100);
let indent_str = if item.info.indent == 0 { let indent_str = if item.info.indent == 0 {
String::from("") String::from("")
} else { } else {
@ -189,18 +186,14 @@ impl ChangesComponent {
format!("{} {}{}", status_char, indent_str, file) format!("{} {}{}", status_char, indent_str, file)
}; };
let mut style = let status = status_item
Style::default().fg(Self::item_color( .status
status_item .unwrap_or(StatusItemType::Modified);
.status
.unwrap_or(StatusItemType::Modified),
));
if selected { Some(Text::Styled(
style = style.bg(select_color); Cow::from(txt),
} theme.item(status, selected),
))
Some(Text::Styled(Cow::from(txt), style))
} }
FileTreeItemKind::Path(path_collapsed) => { FileTreeItemKind::Path(path_collapsed) => {
@ -222,27 +215,14 @@ impl ChangesComponent {
) )
}; };
let mut style = Style::default(); Some(Text::Styled(
Cow::from(txt),
if selected { theme.text(true, selected),
style = style.bg(select_color); ))
}
Some(Text::Styled(Cow::from(txt), style))
} }
} }
} }
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 { fn item_status_char(item_type: Option<StatusItemType>) -> char {
if let Some(item_type) = item_type { if let Some(item_type) = item_type {
match item_type { match item_type {
@ -287,6 +267,7 @@ impl DrawableComponent for ChangesComponent {
.tree .tree
.selection .selection
.map_or(false, |e| e == idx), .map_or(false, |e| e == idx),
self.theme,
) )
}, },
); );
@ -298,6 +279,7 @@ impl DrawableComponent for ChangesComponent {
items, items,
self.tree.selection.map(|idx| idx - selection_offset), self.tree.selection.map(|idx| idx - selection_offset),
self.focused, self.focused,
self.theme,
); );
} }
} }

View file

@ -2,6 +2,8 @@ use super::{
visibility_blocking, CommandBlocking, CommandInfo, Component, visibility_blocking, CommandBlocking, CommandInfo, Component,
DrawableComponent, DrawableComponent,
}; };
use crate::components::dialog_paragraph;
use crate::ui::style::Theme;
use crate::{ use crate::{
queue::{InternalEvent, NeedsUpdate, Queue}, queue::{InternalEvent, NeedsUpdate, Queue},
strings, ui, strings, ui,
@ -12,11 +14,11 @@ use log::error;
use std::borrow::Cow; use std::borrow::Cow;
use strings::commands; use strings::commands;
use sync::HookResult; use sync::HookResult;
use tui::style::Style;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Alignment, Rect}, layout::Rect,
style::{Color, Style}, widgets::{Clear, Text},
widgets::{Block, Borders, Clear, Paragraph, Text},
Frame, Frame,
}; };
@ -24,6 +26,7 @@ pub struct CommitComponent {
msg: String, msg: String,
visible: bool, visible: bool,
queue: Queue, queue: Queue,
theme: Theme,
} }
impl DrawableComponent for CommitComponent { impl DrawableComponent for CommitComponent {
@ -32,22 +35,19 @@ impl DrawableComponent for CommitComponent {
let txt = if self.msg.is_empty() { let txt = if self.msg.is_empty() {
[Text::Styled( [Text::Styled(
Cow::from(strings::COMMIT_MSG), Cow::from(strings::COMMIT_MSG),
Style::default().fg(Color::DarkGray), self.theme.text(false, false),
)] )]
} else { } 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()); let area = ui::centered_rect(60, 20, f.size());
f.render_widget(Clear, area); f.render_widget(Clear, area);
f.render_widget( f.render_widget(
Paragraph::new(txt.iter()) dialog_paragraph(strings::COMMIT_TITLE, txt.iter()),
.block(
Block::default()
.title(strings::COMMIT_TITLE)
.borders(Borders::ALL),
)
.alignment(Alignment::Left),
area, area,
); );
} }
@ -112,11 +112,12 @@ impl Component for CommitComponent {
impl CommitComponent { impl CommitComponent {
/// ///
pub fn new(queue: Queue) -> Self { pub fn new(queue: Queue, theme: Theme) -> Self {
Self { Self {
queue, queue,
msg: String::default(), msg: String::default(),
visible: false, visible: false,
theme,
} }
} }

View file

@ -1,4 +1,5 @@
use super::{CommandBlocking, DrawableComponent, ScrollType}; use super::{CommandBlocking, DrawableComponent, ScrollType};
use crate::ui::style::Theme;
use crate::{ use crate::{
components::{CommandInfo, Component}, components::{CommandInfo, Component},
keys, keys,
@ -9,10 +10,11 @@ use asyncgit::{hash, DiffLine, DiffLineType, FileDiff};
use crossterm::event::Event; use crossterm::event::Event;
use std::{borrow::Cow, cmp, convert::TryFrom}; use std::{borrow::Cow, cmp, convert::TryFrom};
use strings::commands; use strings::commands;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Alignment, Rect}, layout::{Alignment, Rect},
style::{Color, Modifier, Style}, style::Modifier,
symbols, symbols,
widgets::{Block, Borders, Paragraph, Text}, widgets::{Block, Borders, Paragraph, Text},
Frame, Frame,
@ -34,11 +36,12 @@ pub struct DiffComponent {
current: Current, current: Current,
selected_hunk: Option<u16>, selected_hunk: Option<u16>,
queue: Queue, queue: Queue,
theme: Theme,
} }
impl DiffComponent { impl DiffComponent {
/// ///
pub fn new(queue: Queue) -> Self { pub fn new(queue: Queue, theme: Theme) -> Self {
Self { Self {
focused: false, focused: false,
queue, queue,
@ -47,6 +50,7 @@ impl DiffComponent {
diff: FileDiff::default(), diff: FileDiff::default(),
scroll: 0, scroll: 0,
current_height: 0, current_height: 0,
theme,
} }
} }
/// ///
@ -171,6 +175,7 @@ impl DiffComponent {
selection == line_cursor, selection == line_cursor,
hunk_selected, hunk_selected,
i == hunk_len as usize - 1, i == hunk_len as usize - 1,
self.theme,
); );
lines_added += 1; lines_added += 1;
} }
@ -191,22 +196,10 @@ impl DiffComponent {
selected: bool, selected: bool,
selected_hunk: bool, selected_hunk: bool,
end_of_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() let style = theme.text(false, selected || selected_hunk);
.bg(if selected || selected_hunk {
select_color
} else {
Color::Reset
})
.fg(Color::DarkGray);
if end_of_hunk { if end_of_hunk {
text.push(Text::Styled( 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 = let trimmed =
line.content.trim_matches(|c| c == '\n' || c == '\r'); line.content.trim_matches(|c| c == '\n' || c == '\r');
@ -251,16 +233,10 @@ impl DiffComponent {
//TODO: allow customize tabsize //TODO: allow customize tabsize
let content = Cow::from(filled.replace("\t", " ")); let content = Cow::from(filled.replace("\t", " "));
text.push(match line.line_type { text.push(Text::Styled(
DiffLineType::Delete => { content,
Text::Styled(content, style_delete) theme.diff_line(line.line_type, selected),
} ));
DiffLineType::Add => Text::Styled(content, style_add),
DiffLineType::Header => {
Text::Styled(content, style_header)
}
_ => Text::Styled(content, style_default),
});
} }
fn hunk_visible( fn hunk_visible(
@ -299,13 +275,6 @@ impl DiffComponent {
impl DrawableComponent for DiffComponent { impl DrawableComponent for DiffComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, r: Rect) { fn draw<B: Backend>(&mut self, f: &mut Frame<B>, r: Rect) {
self.current_height = r.height.saturating_sub(2); 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 = let title =
format!("{}{}", strings::TITLE_DIFF, self.current.path); format!("{}{}", strings::TITLE_DIFF, self.current.path);
f.render_widget( f.render_widget(
@ -314,8 +283,12 @@ impl DrawableComponent for DiffComponent {
Block::default() Block::default()
.title(title.as_str()) .title(title.as_str())
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(style_border) .border_style(self.theme.block(self.focused))
.title_style(style_title), .title_style(
self.theme
.text(self.focused, false)
.modifier(Modifier::BOLD),
),
) )
.alignment(Alignment::Left), .alignment(Alignment::Left),
r, r,
@ -414,7 +387,6 @@ mod tests {
#[test] #[test]
fn test_lineendings() { fn test_lineendings() {
let mut text = Vec::new(); let mut text = Vec::new();
DiffComponent::add_line( DiffComponent::add_line(
&mut text, &mut text,
10, 10,
@ -425,6 +397,7 @@ mod tests {
false, false,
false, false,
false, false,
crate::ui::style::DARK_THEME,
); );
assert_eq!(text.len(), 2); assert_eq!(text.len(), 2);

View file

@ -2,26 +2,27 @@ use super::{
visibility_blocking, CommandBlocking, CommandInfo, Component, visibility_blocking, CommandBlocking, CommandInfo, Component,
DrawableComponent, DrawableComponent,
}; };
use crate::ui::style::Theme;
use crate::{keys, strings, ui, version::Version}; use crate::{keys, strings, ui, version::Version};
use asyncgit::hash; use asyncgit::hash;
use crossterm::event::Event; use crossterm::event::Event;
use itertools::Itertools; use itertools::Itertools;
use std::{borrow::Cow, cmp, convert::TryFrom}; use std::{borrow::Cow, cmp, convert::TryFrom};
use strings::commands; use strings::commands;
use tui::style::{Modifier, Style};
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style},
widgets::{Block, Borders, Clear, Paragraph, Text}, widgets::{Block, Borders, Clear, Paragraph, Text},
Frame, Frame,
}; };
/// ///
#[derive(Default)]
pub struct HelpComponent { pub struct HelpComponent {
cmds: Vec<CommandInfo>, cmds: Vec<CommandInfo>,
visible: bool, visible: bool,
selection: u16, selection: u16,
theme: Theme,
} }
impl DrawableComponent for HelpComponent { impl DrawableComponent for HelpComponent {
@ -68,10 +69,13 @@ impl DrawableComponent for HelpComponent {
f.render_widget( f.render_widget(
Paragraph::new( Paragraph::new(
vec![Text::Raw(Cow::from(format!( vec![Text::Styled(
"gitui {}", Cow::from(format!(
Version::new(), "gitui {}",
)))] Version::new(),
)),
Style::default(),
)]
.iter(), .iter(),
) )
.alignment(Alignment::Right), .alignment(Alignment::Right),
@ -150,6 +154,14 @@ impl Component for HelpComponent {
} }
impl 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>) { pub fn set_cmds(&mut self, cmds: Vec<CommandInfo>) {
self.cmds = cmds self.cmds = cmds
@ -187,7 +199,7 @@ impl HelpComponent {
{ {
txt.push(Text::Styled( txt.push(Text::Styled(
Cow::from(format!(" {}\n", key)), Cow::from(format!(" {}\n", key)),
Style::default().fg(Color::Black).bg(Color::Gray), Style::default().modifier(Modifier::REVERSED),
)); ));
txt.extend( txt.extend(
@ -216,13 +228,10 @@ impl HelpComponent {
); );
} }
let style = if is_selected { Text::Styled(
Style::default().fg(Color::Yellow) Cow::from(out),
} else { self.theme.text(true, is_selected),
Style::default() )
};
Text::Styled(Cow::from(out), style)
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );

View file

@ -18,6 +18,8 @@ pub use filetree::FileTreeItemKind;
pub use help::HelpComponent; pub use help::HelpComponent;
pub use msg::MsgComponent; pub use msg::MsgComponent;
pub use reset::ResetComponent; pub use reset::ResetComponent;
use tui::layout::Alignment;
use tui::widgets::{Block, Borders, Paragraph, Text};
/// creates accessors for a list of components /// creates accessors for a list of components
/// ///
@ -114,3 +116,15 @@ pub trait Component {
/// ///
fn show(&mut self) {} 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, visibility_blocking, CommandBlocking, CommandInfo, Component,
DrawableComponent, DrawableComponent,
}; };
use crate::components::dialog_paragraph;
use crate::{keys, strings, ui}; use crate::{keys, strings, ui};
use crossterm::event::Event; use crossterm::event::Event;
use std::borrow::Cow; use std::borrow::Cow;
use strings::commands; use strings::commands;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Alignment, Rect}, layout::Rect,
widgets::{Block, Borders, Clear, Paragraph, Text}, widgets::{Clear, Text},
Frame, Frame,
}; };
@ -27,14 +28,8 @@ impl DrawableComponent for MsgComponent {
let area = ui::centered_rect_absolute(65, 25, f.size()); let area = ui::centered_rect_absolute(65, 25, f.size());
f.render_widget(Clear, area); f.render_widget(Clear, area);
f.render_widget( f.render_widget(
Paragraph::new(txt.iter()) dialog_paragraph(strings::MSG_TITLE, txt.iter())
.block( .wrap(true),
Block::default()
.title(strings::MSG_TITLE)
.borders(Borders::ALL),
)
.wrap(true)
.alignment(Alignment::Left),
area, area,
); );
} }

View file

@ -7,14 +7,15 @@ use crate::{
strings, ui, strings, ui,
}; };
use crate::components::dialog_paragraph;
use crate::ui::style::Theme;
use crossterm::event::{Event, KeyCode}; use crossterm::event::{Event, KeyCode};
use std::borrow::Cow; use std::borrow::Cow;
use strings::commands; use strings::commands;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Alignment, Rect}, layout::Rect,
style::{Color, Style}, widgets::{Clear, Text},
widgets::{Block, Borders, Clear, Paragraph, Text},
Frame, Frame,
}; };
@ -23,6 +24,7 @@ pub struct ResetComponent {
target: Option<ResetItem>, target: Option<ResetItem>,
visible: bool, visible: bool,
queue: Queue, queue: Queue,
theme: Theme,
} }
impl DrawableComponent for ResetComponent { impl DrawableComponent for ResetComponent {
@ -31,19 +33,13 @@ impl DrawableComponent for ResetComponent {
let mut txt = Vec::new(); let mut txt = Vec::new();
txt.push(Text::Styled( txt.push(Text::Styled(
Cow::from(strings::RESET_MSG), Cow::from(strings::RESET_MSG),
Style::default().fg(Color::Red), self.theme.text_danger(),
)); ));
let area = ui::centered_rect(30, 20, f.size()); let area = ui::centered_rect(30, 20, f.size());
f.render_widget(Clear, area); f.render_widget(Clear, area);
f.render_widget( f.render_widget(
Paragraph::new(txt.iter()) dialog_paragraph(strings::RESET_TITLE, txt.iter()),
.block(
Block::default()
.title(strings::RESET_TITLE)
.borders(Borders::ALL),
)
.alignment(Alignment::Left),
area, area,
); );
} }
@ -106,11 +102,12 @@ impl Component for ResetComponent {
impl ResetComponent { impl ResetComponent {
/// ///
pub fn new(queue: Queue) -> Self { pub fn new(queue: Queue, theme: Theme) -> Self {
Self { Self {
target: None, target: None,
visible: false, visible: false,
queue, queue,
theme,
} }
} }
/// ///

View file

@ -31,6 +31,7 @@ use scopeguard::defer;
use scopetime::scope_time; use scopetime::scope_time;
use simplelog::{Config, LevelFilter, WriteLogger}; use simplelog::{Config, LevelFilter, WriteLogger};
use spinner::Spinner; use spinner::Spinner;
use std::path::PathBuf;
use std::{ use std::{
env, fs, env, fs,
fs::File, fs::File,
@ -175,12 +176,18 @@ fn start_terminal<W: Write>(
Ok(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() { fn setup_logging() {
if env::var("GITUI_LOGGING").is_ok() { if env::var("GITUI_LOGGING").is_ok() {
let mut path = dirs::cache_dir().unwrap(); let mut path = get_app_config_path();
path.push("gitui");
path.push("gitui.log"); path.push("gitui.log");
fs::create_dir_all(path.parent().unwrap()).unwrap();
let _ = WriteLogger::init( let _ = WriteLogger::init(
LevelFilter::Trace, LevelFilter::Trace,

View file

@ -1,5 +1,6 @@
mod utils; mod utils;
use crate::ui::style::Theme;
use crate::{ use crate::{
components::{ components::{
CommandBlocking, CommandInfo, Component, DrawableComponent, CommandBlocking, CommandInfo, Component, DrawableComponent,
@ -17,34 +18,13 @@ use sync::Tags;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Alignment, Rect}, layout::{Alignment, Rect},
style::{Color, Style},
widgets::{Block, Borders, Paragraph, Text}, widgets::{Block, Borders, Paragraph, Text},
Frame, Frame,
}; };
use utils::{ItemBatch, LogEntry}; 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 ELEMENTS_PER_LINE: usize = 10;
static SLICE_SIZE: usize = 1200; static SLICE_SIZE: usize = 1200;
/// ///
pub struct Revlog { pub struct Revlog {
selection: usize, selection: usize,
@ -57,11 +37,15 @@ pub struct Revlog {
tags: Tags, tags: Tags,
current_size: (u16, u16), current_size: (u16, u16),
scroll_top: usize, scroll_top: usize,
theme: Theme,
} }
impl Revlog { impl Revlog {
/// ///
pub fn new(sender: &Sender<AsyncNotification>) -> Self { pub fn new(
sender: &Sender<AsyncNotification>,
theme: Theme,
) -> Self {
Self { Self {
items: ItemBatch::default(), items: ItemBatch::default(),
git_log: AsyncLog::new(sender.clone()), git_log: AsyncLog::new(sender.clone()),
@ -73,6 +57,7 @@ impl Revlog {
tags: Tags::new(), tags: Tags::new(),
current_size: (0, 0), current_size: (0, 0),
scroll_top: 0, scroll_top: 0,
theme,
} }
} }
@ -171,44 +156,27 @@ impl Revlog {
selected: bool, selected: bool,
txt: &mut Vec<Text<'a>>, txt: &mut Vec<Text<'a>>,
tags: Option<String>, tags: Option<String>,
theme: Theme,
) { ) {
let count_before = txt.len(); let count_before = txt.len();
let splitter_txt = Cow::from(" "); let splitter_txt = Cow::from(" ");
let splitter = if selected { let splitter =
Text::Styled( Text::Styled(splitter_txt, theme.text(true, selected));
splitter_txt,
Style::new().bg(COLOR_SELECTION_BG),
)
} else {
Text::Raw(splitter_txt)
};
txt.push(Text::Styled( txt.push(Text::Styled(
Cow::from(&e.hash[0..7]), Cow::from(&e.hash[0..7]),
if selected { theme.table(0, selected),
STYLE_HASH_SELECTED
} else {
STYLE_HASH
},
)); ));
txt.push(splitter.clone()); txt.push(splitter.clone());
txt.push(Text::Styled( txt.push(Text::Styled(
Cow::from(e.time.as_str()), Cow::from(e.time.as_str()),
if selected { theme.table(1, selected),
STYLE_TIME_SELECTED
} else {
STYLE_TIME
},
)); ));
txt.push(splitter.clone()); txt.push(splitter.clone());
txt.push(Text::Styled( txt.push(Text::Styled(
Cow::from(e.author.as_str()), Cow::from(e.author.as_str()),
if selected { theme.table(2, selected),
STYLE_AUTHOR_SELECTED
} else {
STYLE_AUTHOR
},
)); ));
txt.push(splitter.clone()); txt.push(splitter.clone());
txt.push(Text::Styled( txt.push(Text::Styled(
@ -217,20 +185,12 @@ impl Revlog {
} else { } else {
String::from("") String::from("")
}), }),
if selected { theme.tab(true).bg(theme.text(true, selected).bg),
STYLE_TAG_SELECTED
} else {
STYLE_TAG
},
)); ));
txt.push(splitter); txt.push(splitter);
txt.push(Text::Styled( txt.push(Text::Styled(
Cow::from(e.msg.as_str()), Cow::from(e.msg.as_str()),
if selected { theme.text(true, selected),
STYLE_MSG_SELECTED
} else {
STYLE_MSG
},
)); ));
txt.push(Text::Raw(Cow::from("\n"))); txt.push(Text::Raw(Cow::from("\n")));
@ -248,7 +208,13 @@ impl Revlog {
} else { } else {
None None
}; };
Self::add_entry(e, idx == selection, &mut txt, tag); Self::add_entry(
e,
idx == selection,
&mut txt,
tag,
self.theme,
);
} }
txt txt

View file

@ -1,3 +1,4 @@
use crate::ui::style::Theme;
use crate::{ use crate::{
accessors, accessors,
components::{ components::{
@ -99,6 +100,7 @@ impl Status {
pub fn new( pub fn new(
sender: &Sender<AsyncNotification>, sender: &Sender<AsyncNotification>,
queue: &Queue, queue: &Queue,
theme: Theme,
) -> Self { ) -> Self {
Self { Self {
visible: true, visible: true,
@ -109,14 +111,16 @@ impl Status {
true, true,
true, true,
queue.clone(), queue.clone(),
theme,
), ),
index: ChangesComponent::new( index: ChangesComponent::new(
strings::TITLE_INDEX, strings::TITLE_INDEX,
false, false,
false, false,
queue.clone(), queue.clone(),
theme,
), ),
diff: DiffComponent::new(queue.clone()), diff: DiffComponent::new(queue.clone(), theme),
git_diff: AsyncDiff::new(sender.clone()), git_diff: AsyncDiff::new(sender.clone()),
git_status: AsyncStatus::new(sender.clone()), git_status: AsyncStatus::new(sender.clone()),
} }

View file

@ -1,10 +1,11 @@
mod scrolllist; mod scrolllist;
pub(crate) mod style;
use crate::ui::style::Theme;
use scrolllist::ScrollableList; use scrolllist::ScrollableList;
use tui::style::Modifier;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Text}, widgets::{Block, Borders, Text},
Frame, Frame,
}; };
@ -76,24 +77,24 @@ pub fn draw_list<'b, B: Backend, L>(
items: L, items: L,
select: Option<usize>, select: Option<usize>,
selected: bool, selected: bool,
theme: Theme,
) where ) where
L: Iterator<Item = Text<'b>>, L: Iterator<Item = Text<'b>>,
{ {
let mut style_border = Style::default().fg(Color::DarkGray); let style = if selected {
let mut style_title = Style::default(); theme.block(selected).modifier(Modifier::BOLD)
if selected { } else {
style_border = style_border.fg(Color::Gray); theme.block(selected)
style_title = style_title.modifier(Modifier::BOLD); };
}
let list = ScrollableList::new(items) let list = ScrollableList::new(items)
.block( .block(
Block::default() Block::default()
.title(title) .title(title)
.borders(Borders::ALL) .borders(Borders::ALL)
.title_style(style_title) .title_style(style)
.border_style(style_border), .border_style(theme.block(selected)),
) )
.scroll(select.unwrap_or_default()) .scroll(select.unwrap_or_default());
.style(Style::default().fg(Color::White));
f.render_widget(list, r) f.render_widget(list, r)
} }

View file

@ -38,11 +38,6 @@ where
self self
} }
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn scroll(mut self, index: usize) -> Self { pub fn scroll(mut self, index: usize) -> Self {
self.scroll = index; self.scroll = index;
self 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),
}
}
}