diff --git a/CHANGELOG.md b/CHANGELOG.md index a6f8c9b7..acbc20c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ **Binary features:** - Issue preview widgets in `issue select` +- Basic theming support with light and dark theme bundles +- Support for application settings **Library features:** diff --git a/Cargo.lock b/Cargo.lock index f1f9d454..6fff6e45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,6 +364,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chacha20" version = "0.9.1" @@ -435,6 +441,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "coolor" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e93977247fb916abeee1ff8c6594c9b421fd9c26c9b720a3944acb2a7de27b" + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -478,6 +490,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.1", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1040,9 +1077,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ "spin", ] @@ -1168,6 +1205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -1192,6 +1230,18 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1819,6 +1869,7 @@ dependencies = [ "anyhow", "fuzzy-matcher", "inquire", + "lazy_static", "lexopt", "libc", "log", @@ -1832,6 +1883,7 @@ dependencies = [ "serde_json", "signal-hook", "simple-logging", + "terminal-light", "termion 3.0.0", "textwrap", "thiserror", @@ -2111,6 +2163,17 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2221,9 +2284,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.5.2" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "spki" @@ -2404,6 +2467,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "terminal-light" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ecb7b6ab8a3eeff2b61770d313d1e971f184e29321785c62ef523b132437b7" +dependencies = [ + "coolor", + "crossterm", + "thiserror", + "xterm-query", +] + [[package]] name = "termion" version = "2.0.3" @@ -3071,6 +3146,16 @@ dependencies = [ "libc", ] +[[package]] +name = "xterm-query" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f29504d0a2ca8c1714781c1395a8a660d2557b2cf9c9669433153fc903e9bfc" +dependencies = [ + "nix", + "thiserror", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 1375b8b7..60eed7da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ anyhow = { version = "1" } inquire = { version = "0.7.4", default-features = false, features = ["termion", "editor"] } lexopt = { version = "0.3.0" } fuzzy-matcher = "0.3.7" +lazy_static = { version = "1.5.0" } libc = { version = "^0.2" } log = { version = "0.4.19" } nom = { version = "^7.1.0" } @@ -40,6 +41,7 @@ serde_json = { version = "1.0" } signal-hook = { version = "0.3.17" } timeago = { version = "0.4.1" } termion = { version = "3" } +terminal-light = { version = "1.4.0" } textwrap = { version = "0.16.0" } thiserror = { version = "1" } tokio = { version = "1.32.0", features = ["full"] } diff --git a/bin/commands/issue.rs b/bin/commands/issue.rs index 438e439e..7e8f7c91 100644 --- a/bin/commands/issue.rs +++ b/bin/commands/issue.rs @@ -7,6 +7,8 @@ use std::ffi::OsString; use anyhow::anyhow; +use lazy_static::lazy_static; + use radicle::identity::RepoId; use radicle::issue; @@ -18,6 +20,13 @@ use radicle_tui as tui; use tui::log; use crate::cob; +use crate::ui::TerminalInfo; + +lazy_static! { + static ref TERMINAL_INFO: TerminalInfo = TerminalInfo { + luma: Some(terminal_light::luma().unwrap_or_default()) + }; +} pub const HELP: Help = Help { name: "issue", @@ -143,6 +152,8 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul let (_, rid) = radicle::rad::cwd() .map_err(|_| anyhow!("this command must be run in the context of a project"))?; + let terminal_info = TERMINAL_INFO.clone(); + match options.op { Operation::Select { opts } => { let profile = ctx.profile()?; @@ -157,7 +168,8 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul mode: opts.mode, filter: opts.filter.clone(), }; - let output = select::App::new(context).run().await?; + + let output = select::App::new(context, terminal_info).run().await?; let output = output .map(|o| serde_json::to_string(&o).unwrap_or_default()) diff --git a/bin/commands/issue/select.rs b/bin/commands/issue/select.rs index d322e9ba..50b20bed 100644 --- a/bin/commands/issue/select.rs +++ b/bin/commands/issue/select.rs @@ -23,9 +23,10 @@ use radicle_tui as tui; use tui::store; use tui::store::StateValue; use tui::ui::span; +use tui::ui::theme::Theme; use tui::ui::widget::container::{ - Column, Container, Footer, FooterProps, Header, HeaderProps, SectionGroup, SectionGroupProps, - SplitContainer, SplitContainerFocus, SplitContainerProps, + Column, Container, ContainerProps, Footer, FooterProps, Header, HeaderProps, SectionGroup, + SectionGroupProps, SplitContainer, SplitContainerFocus, SplitContainerProps, }; use tui::ui::widget::input::{TextView, TextViewProps}; use tui::ui::widget::list::{Tree, TreeProps}; @@ -34,8 +35,10 @@ use tui::ui::widget::{PredefinedLayout, ToWidget, Widget}; use tui::{BoxedAny, Channel, Exit, PageStack}; use crate::cob::issue; +use crate::settings::{self, ThemeBundle, ThemeMode}; use crate::ui::items::{CommentItem, Filter, IssueItem, IssueItemFilter}; use crate::ui::widget::{IssueDetails, IssueDetailsProps}; +use crate::ui::TerminalInfo; use self::ui::{Browser, BrowserProps}; @@ -52,6 +55,7 @@ pub struct Context { pub struct App { context: Context, + terminal_info: TerminalInfo, } #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -230,16 +234,34 @@ pub struct State { preview: PreviewState, section: Option
, help: HelpState, + theme: Theme, } -impl TryFrom<&Context> for State { +impl TryFrom<(&Context, &TerminalInfo)> for State { type Error = anyhow::Error; - fn try_from(context: &Context) -> Result { + fn try_from(value: (&Context, &TerminalInfo)) -> Result { + let (context, terminal_info) = value; + let settings = settings::Settings::default(); + let issues = issue::all(&context.profile, &context.repository)?; let search = StateValue::new(context.filter.to_string()); let filter = IssueItemFilter::from_str(&search.read()).unwrap_or_default(); + let default_bundle = ThemeBundle::default(); + let theme_bundle = settings.theme.active_bundle().unwrap_or(&default_bundle); + let theme = match settings.theme.mode() { + ThemeMode::Auto => { + if terminal_info.is_dark() { + theme_bundle.dark.clone() + } else { + theme_bundle.light.clone() + } + } + ThemeMode::Light => theme_bundle.light.clone(), + ThemeMode::Dark => theme_bundle.dark.clone(), + }; + // Convert into UI items let mut items = vec![]; for issue in issues { @@ -282,6 +304,7 @@ impl TryFrom<&Context> for State { scroll: 0, cursor: (0, 0), }, + theme, }) } } @@ -426,13 +449,16 @@ impl store::State for State { } impl App { - pub fn new(context: Context) -> Self { - Self { context } + pub fn new(context: Context, terminal_info: TerminalInfo) -> Self { + Self { + context, + terminal_info, + } } pub async fn run(&self) -> Result> { let channel = Channel::default(); - let state = State::try_from(&self.context)?; + let state = State::try_from((&self.context, &self.terminal_info))?; let tx = channel.tx.clone(); let window = Window::default() @@ -471,6 +497,8 @@ fn browser_page(channel: &Channel) -> Widget { ShortcutsProps::default() .shortcuts(&shortcuts) + .shortcuts_keys_style(state.theme.shortcuts_keys_style) + .shortcuts_action_style(state.theme.shortcuts_action_style) .to_boxed_any() .into() }); @@ -548,9 +576,11 @@ fn issue(channel: &Channel) -> Widget { .top(issue_details(channel)) .bottom(comment_tree(channel)) .to_widget(tx.clone()) - .on_update(|_| { + .on_update(|state| { SplitContainerProps::default() .heights([Constraint::Length(5), Constraint::Min(1)]) + .border_color(state.theme.border_color) + .focus_border_color(state.theme.focus_border_color) .split_focus(SplitContainerFocus::Bottom) .to_boxed_any() .into() @@ -641,6 +671,13 @@ fn comment(channel: &Channel) -> Widget { }), ) .to_widget(tx.clone()) + .on_update(|state| { + ContainerProps::default() + .border_color(state.theme.border_color) + .focus_border_color(state.theme.focus_border_color) + .to_boxed_any() + .into() + }) } fn help_page(channel: &Channel) -> Widget { @@ -691,7 +728,14 @@ fn help_page(channel: &Channel) -> Widget { .into() }), ) - .to_widget(tx.clone()); + .to_widget(tx.clone()) + .on_update(|state| { + ContainerProps::default() + .border_color(state.theme.border_color) + .focus_border_color(state.theme.focus_border_color) + .to_boxed_any() + .into() + }); let shortcuts = Shortcuts::default().to_widget(tx.clone()).on_update(|_| { ShortcutsProps::default() diff --git a/bin/commands/issue/select/ui.rs b/bin/commands/issue/select/ui.rs index c9b5390f..2b36631c 100644 --- a/bin/commands/issue/select/ui.rs +++ b/bin/commands/issue/select/ui.rs @@ -124,8 +124,11 @@ impl Browser { .header(Header::default().to_widget(tx.clone()).on_update(|state| { // TODO: remove and use state directly let props = BrowserProps::from(state); + HeaderProps::default() .columns(props.header.clone()) + .border_color(state.theme.border_color) + .focus_border_color(state.theme.focus_border_color) .to_boxed_any() .into() })) @@ -155,12 +158,16 @@ impl Browser { FooterProps::default() .columns(browse_footer(&props)) + .border_color(state.theme.border_color) + .focus_border_color(state.theme.focus_border_color) .to_boxed_any() .into() })) .to_widget(tx.clone()) .on_update(|state| { ContainerProps::default() + .border_color(state.theme.border_color) + .focus_border_color(state.theme.focus_border_color) .hide_footer(BrowserProps::from(state).show_search) .to_boxed_any() .into() diff --git a/bin/main.rs b/bin/main.rs index c729e7ae..38aa9842 100644 --- a/bin/main.rs +++ b/bin/main.rs @@ -2,6 +2,7 @@ mod cob; mod commands; mod git; mod ui; +mod settings; use std::ffi::OsString; use std::io; diff --git a/bin/ui.rs b/bin/ui.rs index f445c86a..91bff47b 100644 --- a/bin/ui.rs +++ b/bin/ui.rs @@ -1,3 +1,14 @@ pub mod format; pub mod items; pub mod widget; + +#[derive(Clone, Debug)] +pub struct TerminalInfo { + pub luma: Option, +} + +impl TerminalInfo { + pub fn is_dark(&self) -> bool { + self.luma.unwrap_or_default() <= 0.6 + } +}