From 9f44042470bb57c1cc3065617eb1eeb0e469113e Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 30 Dec 2025 19:37:58 +0000 Subject: [PATCH 01/10] Refactor UI styles to use theme struct for better organization. --- src/ui/mod.rs | 6 ++++-- src/ui/styles.rs | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0451c31..9baf5d3 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -8,9 +8,11 @@ use ratatui::text::Line; use ratatui::widgets::{Block, Widget}; use crate::app::{App, FilterField, Focus}; +use crate::ui::styles::Theme; impl Widget for &App { fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { + let theme = Theme::default_dark(); let chunks = Layout::vertical([ Constraint::Length(1), Constraint::Length(6), @@ -19,8 +21,8 @@ impl Widget for &App { ]) .split(area); - let header_style = styles::header(); - let footer_style = styles::footer(); + let header_style = theme.header; + let footer_style = theme.footer; let groups_block_style = styles::groups_block(self.focus == Focus::Groups); let filter_block_style = styles::filter_block(self.focus == Focus::Filter); diff --git a/src/ui/styles.rs b/src/ui/styles.rs index 06c852b..9c73afd 100644 --- a/src/ui/styles.rs +++ b/src/ui/styles.rs @@ -1,4 +1,20 @@ -use ratatui::style::{Color, Style}; +use crate::app::Focus; +use ratatui::style::{Color, Modifier, Style}; + +#[derive(Clone, Debug)] +pub struct Theme { + pub header: Style, + pub footer: Style, +} + +impl Theme { + pub fn default_dark() -> Self { + Theme { + header: Style::default().bg(Color::Rgb(10, 10, 10)).fg(Color::White), + footer: Style::default().bg(Color::Rgb(10, 10, 10)).fg(Color::Gray), + } + } +} pub fn header() -> Style { Style::default().bg(Color::Rgb(10, 10, 10)).fg(Color::White) From 66a77fdbcc1e557da424bc95f9224918648ae3e8 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 30 Dec 2025 20:06:54 +0000 Subject: [PATCH 02/10] Add theme switching functionality with dark and light themes. --- src/app/clipboard.rs | 3 +++ src/app/filters.rs | 2 ++ src/app/keymap.rs | 13 +++++++++++++ src/app/mod.rs | 5 +++++ src/main.rs | 3 +++ src/ui/mod.rs | 6 ++++-- src/ui/results.rs | 3 +++ src/ui/styles.rs | 17 ++++++++++------- 8 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs index 8c8abaf..4fe6ac9 100644 --- a/src/app/clipboard.rs +++ b/src/app/clipboard.rs @@ -3,6 +3,7 @@ use std::time::Instant; use arboard::Clipboard; use crate::app::App; +use crate::ui::styles::Theme; impl App { pub fn results_text(&self) -> String { @@ -36,6 +37,8 @@ mod tests { App { app_title: "Test".to_string(), + theme: Theme::default_dark(), + theme_name: "dark".to_string(), exit: false, lines: lines.into_iter().map(|s| s.to_string()).collect(), filter_cursor_pos: 0, diff --git a/src/app/filters.rs b/src/app/filters.rs index 7971302..eb8c5f2 100644 --- a/src/app/filters.rs +++ b/src/app/filters.rs @@ -195,6 +195,8 @@ mod tests { App { app_title: "Test".to_string(), + theme: Theme::default_dark(), + theme_name: "dark".to_string(), exit: false, lines: Vec::new(), filter_cursor_pos: 0, diff --git a/src/app/keymap.rs b/src/app/keymap.rs index ce5492b..fa722cd 100644 --- a/src/app/keymap.rs +++ b/src/app/keymap.rs @@ -1,4 +1,5 @@ use super::{App, FilterField, Focus}; +use crate::ui::styles::Theme; use ratatui::crossterm::event::{KeyCode, KeyEventKind}; use std::io; @@ -192,6 +193,16 @@ impl App { self.apply_time_preset("-24m"); } + KeyCode::Char('T') if !self.editing => { + if self.theme_name == "dark" { + self.theme = Theme::light(); + self.theme_name = "light".to_string(); + } else { + self.theme = Theme::default_dark(); + self.theme_name = "dark".to_string(); + } + } + _ => {} } @@ -215,6 +226,8 @@ mod tests { App { app_title: "Test".to_string(), + theme: Theme::default_dark(), + theme_name: "dark".to_string(), exit: false, lines: Vec::new(), filter_cursor_pos: 0, diff --git a/src/app/mod.rs b/src/app/mod.rs index 3ccd552..166afec 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -7,6 +7,7 @@ use std::sync::atomic::Ordering; use std::sync::mpsc::{Receiver, Sender}; use std::time::{Duration, Instant}; +use crate::ui::styles::Theme; use chrono::Utc; use ratatui::crossterm::event; use ratatui::prelude::Rect; @@ -43,6 +44,8 @@ pub struct SavedFilter { pub struct App { pub app_title: String, + pub theme: Theme, + pub theme_name: String, pub exit: bool, pub lines: Vec, pub filter_cursor_pos: usize, @@ -493,6 +496,8 @@ mod tests { App { app_title: "Test".to_string(), + theme: Theme::default_dark(), + theme_name: "dark".to_string(), exit: false, lines: Vec::new(), filter_cursor_pos: 0, diff --git a/src/main.rs b/src/main.rs index e17258c..92e858c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod app; mod aws; mod ui; +use crate::ui::styles::Theme; use app::{App, FilterField, Focus}; use aws::fetch_log_groups; @@ -35,6 +36,8 @@ fn main() -> io::Result<()> { let mut app = App { app_title: APP_TITLE.to_string(), + theme: Theme::default_dark(), + theme_name: "dark".to_string(), exit: false, lines: Vec::new(), filter_cursor_pos: 0, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9baf5d3..9cceffa 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,5 +1,5 @@ mod results; -mod styles; +pub mod styles; use ratatui::layout::{Constraint, Layout}; use ratatui::prelude::Rect; @@ -12,7 +12,7 @@ use crate::ui::styles::Theme; impl Widget for &App { fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let theme = Theme::default_dark(); + let theme = self.theme.clone(); let chunks = Layout::vertical([ Constraint::Length(1), Constraint::Length(6), @@ -430,6 +430,8 @@ mod ui_tests { App { app_title: "lumberjack".to_string(), + theme: Theme::default_dark(), + theme_name: "dark".to_string(), exit: false, lines: vec![], filter_cursor_pos: 0, diff --git a/src/ui/results.rs b/src/ui/results.rs index 765d5b0..7a6032e 100644 --- a/src/ui/results.rs +++ b/src/ui/results.rs @@ -134,11 +134,14 @@ mod tests { use std::time::Instant; use crate::app::{App, FilterField, Focus}; + use crate::ui::styles::Theme; fn make_results_app(lines: Vec<&str>) -> App { let (tx, rx) = mpsc::channel(); App { app_title: "Test".to_string(), + theme: Theme::default_dark(), + theme_name: "dark".to_string(), exit: false, lines: lines.into_iter().map(|s| s.to_string()).collect(), filter_cursor_pos: 0, diff --git a/src/ui/styles.rs b/src/ui/styles.rs index 9c73afd..d9051b9 100644 --- a/src/ui/styles.rs +++ b/src/ui/styles.rs @@ -14,14 +14,17 @@ impl Theme { footer: Style::default().bg(Color::Rgb(10, 10, 10)).fg(Color::Gray), } } -} - -pub fn header() -> Style { - Style::default().bg(Color::Rgb(10, 10, 10)).fg(Color::White) -} -pub fn footer() -> Style { - Style::default().bg(Color::Rgb(10, 10, 10)).fg(Color::Gray) + pub fn light() -> Self { + Theme { + header: Style::default() + .bg(Color::Rgb(240, 240, 240)) + .fg(Color::Black), + footer: Style::default() + .bg(Color::Rgb(240, 240, 240)) + .fg(Color::Black), + } + } } pub fn groups_block(focus: bool) -> Style { From 5519c46d4dd60bb10cbc639e1882cae72ea2a257 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 30 Dec 2025 20:13:39 +0000 Subject: [PATCH 03/10] Refactor imports and fix test dependencies in app and ui modules. --- src/app/clipboard.rs | 8 +++----- src/app/filters.rs | 1 + src/app/keymap.rs | 1 + src/ui/mod.rs | 2 +- src/ui/styles.rs | 3 +-- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs index 4fe6ac9..6080eae 100644 --- a/src/app/clipboard.rs +++ b/src/app/clipboard.rs @@ -1,9 +1,6 @@ -use std::time::Instant; - -use arboard::Clipboard; - use crate::app::App; -use crate::ui::styles::Theme; +use arboard::Clipboard; +use std::time::Instant; impl App { pub fn results_text(&self) -> String { @@ -29,6 +26,7 @@ impl App { #[cfg(test)] mod tests { use crate::app::{App, FilterField, Focus}; + use crate::ui::styles::Theme; use std::sync::mpsc; use std::time::Instant as StdInstant; diff --git a/src/app/filters.rs b/src/app/filters.rs index eb8c5f2..072c8a1 100644 --- a/src/app/filters.rs +++ b/src/app/filters.rs @@ -187,6 +187,7 @@ impl App { mod tests { use super::*; use crate::app::{App, Focus}; + use crate::ui::styles::Theme; use std::sync::mpsc; use std::time::Instant as StdInstant; diff --git a/src/app/keymap.rs b/src/app/keymap.rs index fa722cd..159b09d 100644 --- a/src/app/keymap.rs +++ b/src/app/keymap.rs @@ -213,6 +213,7 @@ impl App { #[cfg(test)] mod tests { use crate::app::{App, FilterField, Focus}; + use crate::ui::styles::Theme; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::sync::mpsc; use std::time::Instant as StdInstant; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9cceffa..6fb0068 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -8,7 +8,6 @@ use ratatui::text::Line; use ratatui::widgets::{Block, Widget}; use crate::app::{App, FilterField, Focus}; -use crate::ui::styles::Theme; impl Widget for &App { fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { @@ -419,6 +418,7 @@ impl Widget for &App { #[cfg(test)] mod ui_tests { use super::*; + use crate::ui::styles::Theme; use ratatui::{buffer::Buffer, layout::Rect}; use std::sync::atomic::AtomicBool; use std::sync::{Arc, mpsc}; diff --git a/src/ui/styles.rs b/src/ui/styles.rs index d9051b9..d3ddfe7 100644 --- a/src/ui/styles.rs +++ b/src/ui/styles.rs @@ -1,5 +1,4 @@ -use crate::app::Focus; -use ratatui::style::{Color, Modifier, Style}; +use ratatui::style::{Color, Style}; #[derive(Clone, Debug)] pub struct Theme { From bb17fb5e7515fa6b0adda90b096c0d22d10547df Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 30 Dec 2025 20:28:20 +0000 Subject: [PATCH 04/10] Add green theme option for light theme in keymap.rs. --- src/app/keymap.rs | 3 +++ src/ui/styles.rs | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/app/keymap.rs b/src/app/keymap.rs index 159b09d..d5c11de 100644 --- a/src/app/keymap.rs +++ b/src/app/keymap.rs @@ -197,6 +197,9 @@ impl App { if self.theme_name == "dark" { self.theme = Theme::light(); self.theme_name = "light".to_string(); + } else if self.theme_name == "light" { + self.theme = Theme::green(); + self.theme_name = "green".to_string(); } else { self.theme = Theme::default_dark(); self.theme_name = "dark".to_string(); diff --git a/src/ui/styles.rs b/src/ui/styles.rs index d3ddfe7..e120efd 100644 --- a/src/ui/styles.rs +++ b/src/ui/styles.rs @@ -24,6 +24,13 @@ impl Theme { .fg(Color::Black), } } + + pub fn green() -> Self { + Theme { + header: Style::default().bg(Color::Black).fg(Color::Rgb(0, 255, 0)), + footer: Style::default().bg(Color::Black).fg(Color::Rgb(0, 255, 0)), + } + } } pub fn groups_block(focus: bool) -> Style { From b6bb2f79316f4be3d882cdd56f1bbff54983fe46 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 30 Dec 2025 20:56:32 +0000 Subject: [PATCH 05/10] Refactor UI styles to use theme struct for customization. --- src/ui/mod.rs | 56 +++++----- src/ui/styles.rs | 264 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 235 insertions(+), 85 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 6fb0068..c35f115 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -23,16 +23,16 @@ impl Widget for &App { let header_style = theme.header; let footer_style = theme.footer; - let groups_block_style = styles::groups_block(self.focus == Focus::Groups); - let filter_block_style = styles::filter_block(self.focus == Focus::Filter); - let results_block_style = styles::results_block(self.focus == Focus::Results); + let groups_block_style = styles::groups_block(&theme, self.focus == Focus::Groups); + let filter_block_style = styles::filter_block(&theme, self.focus == Focus::Filter); + let results_block_style = styles::results_block(&theme, self.focus == Focus::Results); - let groups_item_style = styles::group_item(self.focus == Focus::Groups); - let groups_selected_style = styles::groups_selected(self.focus == Focus::Groups); + let groups_item_style = styles::group_item(&theme, self.focus == Focus::Groups); + let groups_selected_style = styles::groups_selected(&theme, self.focus == Focus::Groups); - let groups_border = styles::pane_border(self.focus == Focus::Groups); - let filter_border = styles::pane_border(self.focus == Focus::Filter); - let results_border = styles::pane_border(self.focus == Focus::Results); + let groups_border = styles::pane_border(&theme, self.focus == Focus::Groups); + let filter_border = styles::pane_border(&theme, self.focus == Focus::Filter); + let results_border = styles::pane_border(&theme, self.focus == Focus::Results); buf.set_style(chunks[0], header_style); buf.set_style(chunks[3], footer_style); @@ -139,7 +139,7 @@ impl Widget for &App { let dots = ".".repeat(self.dots); let msg = format!("Searching{dots}"); - Line::from(msg).style(styles::default_gray()).render( + Line::from(msg).style(styles::default_gray(&theme)).render( Rect { x: results_inner.x, y: results_inner.y, @@ -160,7 +160,7 @@ impl Widget for &App { let field_style = |field: FilterField| { let active = self.focus == Focus::Filter && field == self.filter_field; - styles::filter_field(active, active && self.editing) + styles::filter_field(&theme, active, active && self.editing) }; let line = |label: &str, value: &str| format!("{label}: {value}"); @@ -243,7 +243,7 @@ impl Widget for &App { // draw a vertical bar cursor if let Some(cell) = buf.cell_mut((x, y)) { - cell.set_char('▏').set_style(styles::cursor()); + cell.set_char('▏').set_style(styles::cursor(&theme)); } } } @@ -272,7 +272,7 @@ impl Widget for &App { let presets_x = filter_inner.x + pane_width.saturating_sub(text_width); Line::from(presets_text) - .style(styles::presets_hint()) + .style(styles::presets_hint(&theme)) .render( Rect { x: presets_x, @@ -300,8 +300,8 @@ impl Widget for &App { let block = Block::bordered() .title("Save filter") - .style(styles::popup_block()) - .border_style(styles::popup_border()); + .style(styles::popup_block(&theme)) + .border_style(styles::popup_border(&theme)); let inner = block.inner(popup_area); block.render(popup_area, buf); @@ -320,15 +320,17 @@ impl Widget for &App { ); let name_line = format!("{}", self.save_filter_name); - Line::from(name_line).style(styles::popup_border()).render( - Rect { - x: inner.x, - y: inner.y + 1, - width: inner.width, - height: 1, - }, - buf, - ); + Line::from(name_line) + .style(styles::popup_border(&theme)) + .render( + Rect { + x: inner.x, + y: inner.y + 1, + width: inner.width, + height: 1, + }, + buf, + ); // Hint line Line::from("Enter Save Esc Cancel") @@ -362,8 +364,8 @@ impl Widget for &App { let block = Block::bordered() .title("Load filter") - .style(styles::popup_block()) - .border_style(styles::popup_border()); + .style(styles::popup_block(&theme)) + .border_style(styles::popup_border(&theme)); let inner = block.inner(popup_area); block.render(popup_area, buf); @@ -381,7 +383,7 @@ impl Widget for &App { }; let line = format!("{marker} {}", f.name); let style = if idx == self.load_filter_selected { - styles::popup_border() + styles::popup_border(&theme) } else { Style::default().fg(Color::White) }; @@ -401,7 +403,7 @@ impl Widget for &App { // Hint line at the bottom of the popup Line::from("Enter Load Esc Cancel") - .style(styles::default_gray()) + .style(styles::default_gray(&theme)) .render( Rect { x: inner.x, diff --git a/src/ui/styles.rs b/src/ui/styles.rs index e120efd..57f157e 100644 --- a/src/ui/styles.rs +++ b/src/ui/styles.rs @@ -1,9 +1,35 @@ -use ratatui::style::{Color, Style}; +use ratatui::style::{Color, Modifier, Style}; #[derive(Clone, Debug)] pub struct Theme { pub header: Style, pub footer: Style, + + pub groups_block_focused: Style, + pub groups_block_unfocused: Style, + pub groups_item_focused: Style, + pub groups_item_unfocused: Style, + pub groups_selected_focused: Style, + pub groups_selected_unfocused: Style, + + pub filter_block_focused: Style, + pub filter_block_unfocused: Style, + + pub results_block_focused: Style, + pub results_block_unfocused: Style, + + pub pane_border_focused: Style, + pub pane_border_unfocused: Style, + + pub default_gray: Style, + pub filter_field_active_editing: Style, + pub filter_field_active_idle: Style, + pub filter_field_inactive: Style, + + pub popup_block: Style, + pub popup_border: Style, + pub presets_hint: Style, + pub cursor: Style, } impl Theme { @@ -11,117 +37,239 @@ impl Theme { Theme { header: Style::default().bg(Color::Rgb(10, 10, 10)).fg(Color::White), footer: Style::default().bg(Color::Rgb(10, 10, 10)).fg(Color::Gray), + groups_block_focused: Style::default().bg(Color::Black).fg(Color::White), + groups_block_unfocused: Style::default() + .bg(Color::Rgb(14, 14, 14)) + .fg(Color::Rgb(140, 140, 140)), + + groups_item_focused: Style::default().bg(Color::Black).fg(Color::White), + groups_item_unfocused: Style::default() + .bg(Color::Rgb(14, 14, 14)) + .fg(Color::Rgb(140, 140, 140)), + + groups_selected_focused: Style::default() + .bg(Color::Rgb(40, 40, 40)) + .fg(Color::White) + .add_modifier(Modifier::BOLD), + groups_selected_unfocused: Style::default().bg(Color::Rgb(18, 18, 18)).fg(Color::White), + + filter_block_focused: Style::default().bg(Color::Rgb(20, 20, 20)).fg(Color::White), + filter_block_unfocused: Style::default() + .bg(Color::Rgb(20, 20, 20)) + .fg(Color::Rgb(140, 140, 140)), + + results_block_focused: Style::default().bg(Color::Rgb(5, 5, 5)).fg(Color::White), + results_block_unfocused: Style::default() + .bg(Color::Rgb(14, 14, 14)) + .fg(Color::Rgb(140, 140, 140)), + + pane_border_focused: Style::default().fg(Color::Yellow), + pane_border_unfocused: Style::default(), + + default_gray: Style::default().fg(Color::Gray), + + filter_field_active_editing: Style::default().bg(Color::Gray).fg(Color::Black), + filter_field_active_idle: Style::default().fg(Color::White).bg(Color::Rgb(20, 20, 20)), + filter_field_inactive: Style::default() + .fg(Color::Rgb(100, 100, 100)) + .bg(Color::Rgb(20, 20, 20)), + + popup_block: Style::default().bg(Color::Rgb(30, 30, 30)).fg(Color::White), + popup_border: Style::default().fg(Color::Yellow), + + presets_hint: Style::default().fg(Color::Rgb(50, 50, 50)), + cursor: Style::default().fg(Color::White).bg(Color::Rgb(20, 20, 20)), } } pub fn light() -> Self { - Theme { - header: Style::default() - .bg(Color::Rgb(240, 240, 240)) - .fg(Color::Black), - footer: Style::default() - .bg(Color::Rgb(240, 240, 240)) - .fg(Color::Black), - } + // Start from dark to fill all fields, then override what we care about. + let mut t = Theme::default_dark(); + + let bg = Color::Rgb(240, 240, 240); + let bg_alt = Color::Rgb(230, 230, 230); + let text = Color::Rgb(30, 30, 30); + + // Header / footer + t.header = Style::default().bg(bg).fg(text); + t.footer = Style::default().bg(bg).fg(text); + + // Groups block background + t.groups_block_focused = Style::default().bg(bg_alt).fg(text); + t.groups_block_unfocused = Style::default().bg(bg_alt).fg(text); + + // Group items + t.groups_item_unfocused = Style::default().bg(bg_alt).fg(text); + t.groups_item_focused = t.groups_item_unfocused; + + t.groups_selected_focused = Style::default() + .bg(Color::Rgb(210, 210, 210)) + .fg(text) + .add_modifier(Modifier::BOLD); + t.groups_selected_unfocused = Style::default().bg(Color::Rgb(220, 220, 220)).fg(text); + + // Filter block + t.filter_block_focused = Style::default().bg(bg).fg(text); + t.filter_block_unfocused = Style::default().bg(bg).fg(text); + + // Results block + t.results_block_focused = Style::default().bg(bg).fg(text); + t.results_block_unfocused = Style::default().bg(bg).fg(text); + + // Borders + t.pane_border_focused = Style::default().fg(Color::Rgb(80, 80, 80)); + t.pane_border_unfocused = Style::default().fg(Color::Rgb(180, 180, 180)); + + // Default gray text (used for "Searching..." etc.) + t.default_gray = Style::default().fg(Color::Rgb(120, 120, 120)); + + // Filter fields + t.filter_field_inactive = Style::default().bg(bg).fg(Color::Rgb(120, 120, 120)); + t.filter_field_active_idle = Style::default().bg(Color::Rgb(220, 220, 220)).fg(text); + t.filter_field_active_editing = Style::default().bg(Color::Rgb(200, 200, 200)).fg(text); + + // Popup + t.popup_block = Style::default().bg(Color::Rgb(245, 245, 245)).fg(text); + t.popup_border = Style::default().fg(Color::Rgb(100, 100, 100)); + + // Presets hint, cursor + t.presets_hint = Style::default().fg(Color::Rgb(100, 100, 100)); + t.cursor = Style::default().fg(text).bg(Color::Rgb(220, 220, 220)); + + t } pub fn green() -> Self { - Theme { - header: Style::default().bg(Color::Black).fg(Color::Rgb(0, 255, 0)), - footer: Style::default().bg(Color::Black).fg(Color::Rgb(0, 255, 0)), - } + let mut t = Theme::default_dark(); + + let green = Color::Rgb(0, 255, 0); + let dark_bg = Color::Black; + let band_bg = Color::Rgb(0, 80, 0); + let bright_bg = Color::Rgb(0, 120, 0); + + t.header = Style::default().bg(dark_bg).fg(green); + t.footer = Style::default().bg(dark_bg).fg(green); + + t.groups_block_focused = Style::default().bg(dark_bg).fg(green); + // Unselected items: plain phosphor style + t.groups_item_unfocused = Style::default().bg(dark_bg).fg(green); + // Selected item (when Groups pane is focused): brighter band with bold + t.groups_selected_focused = Style::default() + .bg(bright_bg) + .fg(green) + .add_modifier(Modifier::BOLD); + + t.groups_item_focused = Style::default().bg(dark_bg).fg(green); + t.groups_item_unfocused = Style::default().bg(dark_bg).fg(green); + + t.groups_selected_unfocused = Style::default().bg(dark_bg).fg(green); + + t.filter_block_focused = Style::default().bg(dark_bg).fg(green); + t.filter_block_unfocused = Style::default().bg(dark_bg).fg(green); + + t.results_block_focused = Style::default().bg(dark_bg).fg(green); + t.results_block_unfocused = Style::default().bg(dark_bg).fg(green); + + t.pane_border_focused = Style::default().fg(green); + t.pane_border_unfocused = Style::default().fg(green); + + t.default_gray = Style::default().fg(green); + + // Inactive filter fields: black bg, green text + t.filter_field_inactive = Style::default().bg(dark_bg).fg(green); + + // Active filter field (not editing): dark green band with bright text + t.filter_field_active_idle = Style::default().bg(band_bg).fg(Color::Rgb(0, 255, 0)); + + // Active filter field while editing: bright green band with black text + t.filter_field_active_editing = Style::default().bg(green).fg(Color::Black); + + t.popup_block = Style::default().bg(dark_bg).fg(green); + t.popup_border = Style::default().fg(green); + + t.presets_hint = Style::default().fg(green); + t.cursor = Style::default().fg(green).bg(dark_bg); + + t } } -pub fn groups_block(focus: bool) -> Style { +pub fn groups_block(theme: &Theme, focus: bool) -> Style { if focus { - Style::default().bg(Color::Black).fg(Color::White) + theme.groups_block_focused } else { - Style::default() - .bg(Color::Rgb(14, 14, 14)) - .fg(Color::Rgb(140, 140, 140)) + theme.groups_block_unfocused } } -pub fn group_item(focused: bool) -> Style { +pub fn group_item(theme: &Theme, focused: bool) -> Style { if focused { - Style::default().bg(Color::Black).fg(Color::White) + theme.groups_item_focused } else { - Style::default() - .bg(Color::Rgb(14, 14, 14)) - .fg(Color::Rgb(140, 140, 140)) + theme.groups_item_unfocused } } -pub fn groups_selected(focus: bool) -> Style { +pub fn groups_selected(theme: &Theme, focus: bool) -> Style { if focus { - Style::default() - .bg(Color::Rgb(40, 40, 40)) - .fg(Color::White) - .add_modifier(ratatui::style::Modifier::BOLD) + theme.groups_selected_focused } else { - Style::default().bg(Color::Rgb(18, 18, 18)).fg(Color::White) + theme.groups_selected_unfocused } } -pub fn filter_block(focus: bool) -> Style { +pub fn filter_block(theme: &Theme, focus: bool) -> Style { if focus { - Style::default().bg(Color::Rgb(20, 20, 20)).fg(Color::White) + theme.filter_block_focused } else { - Style::default() - .bg(Color::Rgb(20, 20, 20)) - .fg(Color::Rgb(140, 140, 140)) + theme.filter_block_unfocused } } -pub fn results_block(focus: bool) -> Style { +pub fn results_block(theme: &Theme, focus: bool) -> Style { if focus { - Style::default().bg(Color::Rgb(5, 5, 5)).fg(Color::White) + theme.results_block_focused } else { - Style::default() - .bg(Color::Rgb(14, 14, 14)) - .fg(Color::Rgb(140, 140, 140)) + theme.results_block_unfocused } } -pub fn pane_border(focus: bool) -> Style { +pub fn pane_border(theme: &Theme, focus: bool) -> Style { if focus { - Style::default().fg(Color::Yellow) + theme.pane_border_focused } else { - Style::default() + theme.pane_border_unfocused } } -pub fn default_gray() -> Style { - Style::default().fg(Color::Gray) +pub fn default_gray(theme: &Theme) -> Style { + theme.default_gray } -pub fn filter_field(field_is_active: bool, editing: bool) -> Style { +pub fn filter_field(theme: &Theme, field_is_active: bool, editing: bool) -> Style { if field_is_active { if editing { - Style::default().bg(Color::Gray).fg(Color::Black) + theme.filter_field_active_editing } else { - Style::default().fg(Color::White).bg(Color::Rgb(20, 20, 20)) + theme.filter_field_active_idle } } else { - Style::default() - .fg(Color::Rgb(100, 100, 100)) - .bg(Color::Rgb(20, 20, 20)) + theme.filter_field_inactive } } -pub fn popup_block() -> Style { - Style::default().bg(Color::Rgb(30, 30, 30)).fg(Color::White) +pub fn popup_block(theme: &Theme) -> Style { + theme.popup_block } -pub fn popup_border() -> Style { - Style::default().fg(Color::Yellow) +pub fn popup_border(theme: &Theme) -> Style { + theme.popup_border } -pub fn presets_hint() -> Style { - Style::default().fg(Color::Rgb(50, 50, 50)) +pub fn presets_hint(theme: &Theme) -> Style { + theme.presets_hint } -pub fn cursor() -> Style { - Style::default().fg(Color::White).bg(Color::Rgb(20, 20, 20)) +pub fn cursor(theme: &Theme) -> Style { + theme.cursor } From 5ae3e49b4a95c9b67756b879b55694248365f6f2 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 30 Dec 2025 21:11:39 +0000 Subject: [PATCH 06/10] Update UI header style and green theme colors for better readability. --- src/ui/mod.rs | 2 ++ src/ui/styles.rs | 26 +++++++++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c35f115..b07661d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -52,7 +52,9 @@ impl Widget for &App { ); Line::from(self.app_title.as_str()) .bold() + .style(theme.header) .render(header[0], buf); + Line::from(header_right_text) .right_aligned() .style(header_style) diff --git a/src/ui/styles.rs b/src/ui/styles.rs index 57f157e..7b834b0 100644 --- a/src/ui/styles.rs +++ b/src/ui/styles.rs @@ -142,12 +142,15 @@ impl Theme { pub fn green() -> Self { let mut t = Theme::default_dark(); - let green = Color::Rgb(0, 255, 0); + let green = Color::Rgb(160, 255, 0); let dark_bg = Color::Black; - let band_bg = Color::Rgb(0, 80, 0); - let bright_bg = Color::Rgb(0, 120, 0); + let band_bg = Color::Rgb(0, 40, 0); + let bright_bg = Color::Rgb(0, 90, 0); - t.header = Style::default().bg(dark_bg).fg(green); + t.header = Style::default() + .bg(dark_bg) + .fg(green) + .add_modifier(Modifier::BOLD); t.footer = Style::default().bg(dark_bg).fg(green); t.groups_block_focused = Style::default().bg(dark_bg).fg(green); @@ -159,8 +162,10 @@ impl Theme { .fg(green) .add_modifier(Modifier::BOLD); - t.groups_item_focused = Style::default().bg(dark_bg).fg(green); + // t.groups_item_focused = Style::default().bg(dark_bg).fg(green); + // t.groups_item_unfocused = Style::default().bg(dark_bg).fg(green); t.groups_item_unfocused = Style::default().bg(dark_bg).fg(green); + t.groups_item_focused = t.groups_item_unfocused; t.groups_selected_unfocused = Style::default().bg(dark_bg).fg(green); @@ -178,11 +183,14 @@ impl Theme { // Inactive filter fields: black bg, green text t.filter_field_inactive = Style::default().bg(dark_bg).fg(green); - // Active filter field (not editing): dark green band with bright text - t.filter_field_active_idle = Style::default().bg(band_bg).fg(Color::Rgb(0, 255, 0)); + // Active idle filter field: dark green band + t.filter_field_active_idle = Style::default().bg(band_bg).fg(green); - // Active filter field while editing: bright green band with black text - t.filter_field_active_editing = Style::default().bg(green).fg(Color::Black); + // Active editing filter field: brighter band, maybe bold + t.filter_field_active_editing = Style::default() + .bg(bright_bg) + .fg(green) + .add_modifier(Modifier::BOLD); t.popup_block = Style::default().bg(dark_bg).fg(green); t.popup_border = Style::default().fg(green); From f817b219f945dc645640a0f332c3c3df75419803 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 30 Dec 2025 21:13:01 +0000 Subject: [PATCH 07/10] Update lumberjack version to 0.3.0 in Cargo.toml and Cargo.lock --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5ced3d..18a77ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1250,7 +1250,7 @@ dependencies = [ [[package]] name = "lumberjack" -version = "0.2.4" +version = "0.3.0" dependencies = [ "arboard", "aws-config", diff --git a/Cargo.toml b/Cargo.toml index a61de78..ed85546 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lumberjack" -version = "0.2.4" +version = "0.3.0" edition = "2024" [dependencies] From 5cfea126b43b96caba8a76c3e7fc1b9adb4aae54 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 30 Dec 2025 21:17:23 +0000 Subject: [PATCH 08/10] Add support for Retro Green CRT color theme. --- README.md | 6 ++++++ src/ui/styles.rs | 56 +++++++++++++++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 65df89a..ec4c0ce 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,11 @@ Built in **Rust**, powered by **ratatui**, **crossterm**, and the **AWS SDK for - `1/2/3/4` for time presets - `t` to tail - `y` to copy all results + - `T` to cycle color themes (Dark → Light → Green CRT) +- 🎨 Theme support + - Dark (default) + - Light + - Retro Green CRT (phosphor-style, neon green on black) - 🌑 Focus-aware panes (Groups / Filter / Results) with clear borders and styles --- @@ -79,6 +84,7 @@ cargo run -- --profile= --region= - `s` – Save current filter (opens name popup; persists to `~/.config/lumberjack/filters.json`) - `F` – Load saved filter (opens popup with saved filter names) - `t` – Toggle tail/stream mode for results +- `T` – Cycle color themes (Dark → Light → Green CRT) - `Esc` – Cancel editing, group search, or close popups - `y` – Copy all Results to clipboard (when Results pane is focused) - `q` – Quit (except while editing or in group search) diff --git a/src/ui/styles.rs b/src/ui/styles.rs index 7b834b0..9829373 100644 --- a/src/ui/styles.rs +++ b/src/ui/styles.rs @@ -140,62 +140,78 @@ impl Theme { } pub fn green() -> Self { + // Start from dark to fill all fields, then override. let mut t = Theme::default_dark(); + // Neon-ish phosphor green: slightly yellowish, bright let green = Color::Rgb(160, 255, 0); let dark_bg = Color::Black; - let band_bg = Color::Rgb(0, 40, 0); - let bright_bg = Color::Rgb(0, 90, 0); + let band_bg = Color::Rgb(0, 40, 0); // very dark green band + let bright_bg = Color::Rgb(0, 90, 0); // brighter band for strong highlight + // Header / footer: pure phosphor look t.header = Style::default() .bg(dark_bg) .fg(green) .add_modifier(Modifier::BOLD); t.footer = Style::default().bg(dark_bg).fg(green); + // --- Groups pane --- + + // Block background t.groups_block_focused = Style::default().bg(dark_bg).fg(green); - // Unselected items: plain phosphor style + t.groups_block_unfocused = Style::default().bg(dark_bg).fg(green); + + // Unselected items t.groups_item_unfocused = Style::default().bg(dark_bg).fg(green); - // Selected item (when Groups pane is focused): brighter band with bold + t.groups_item_focused = t.groups_item_unfocused; + + // Selected item (pane focused): bright band, bold t.groups_selected_focused = Style::default() .bg(bright_bg) .fg(green) .add_modifier(Modifier::BOLD); - // t.groups_item_focused = Style::default().bg(dark_bg).fg(green); - // t.groups_item_unfocused = Style::default().bg(dark_bg).fg(green); - t.groups_item_unfocused = Style::default().bg(dark_bg).fg(green); - t.groups_item_focused = t.groups_item_unfocused; + // Selected item (pane unfocused): darker band, still visible + t.groups_selected_unfocused = Style::default().bg(band_bg).fg(green); - t.groups_selected_unfocused = Style::default().bg(dark_bg).fg(green); + // --- Filter pane --- t.filter_block_focused = Style::default().bg(dark_bg).fg(green); t.filter_block_unfocused = Style::default().bg(dark_bg).fg(green); - t.results_block_focused = Style::default().bg(dark_bg).fg(green); - t.results_block_unfocused = Style::default().bg(dark_bg).fg(green); - - t.pane_border_focused = Style::default().fg(green); - t.pane_border_unfocused = Style::default().fg(green); - - t.default_gray = Style::default().fg(green); - - // Inactive filter fields: black bg, green text + // Inactive fields: black bg, green text t.filter_field_inactive = Style::default().bg(dark_bg).fg(green); - // Active idle filter field: dark green band + // Active (not editing): dark green band t.filter_field_active_idle = Style::default().bg(band_bg).fg(green); - // Active editing filter field: brighter band, maybe bold + // Active (editing): brighter band, bold t.filter_field_active_editing = Style::default() .bg(bright_bg) .fg(green) .add_modifier(Modifier::BOLD); + // --- Results pane --- + + t.results_block_focused = Style::default().bg(dark_bg).fg(green); + t.results_block_unfocused = Style::default().bg(dark_bg).fg(green); + + // --- Borders, text, cursor, popups --- + + t.pane_border_focused = Style::default().fg(green); + t.pane_border_unfocused = Style::default().fg(green); + + // "Gray" in green mode is just green + t.default_gray = Style::default().fg(green); + + // Popups: same phosphor look t.popup_block = Style::default().bg(dark_bg).fg(green); t.popup_border = Style::default().fg(green); t.presets_hint = Style::default().fg(green); + + // Cursor: thin green bar on black t.cursor = Style::default().fg(green).bg(dark_bg); t From 55163739d99bb5e2dd4639d5833850ad3b28d5c5 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 30 Dec 2025 21:20:38 +0000 Subject: [PATCH 09/10] Cycle through dark, light, green themes with 'T' key press tests. --- src/app/keymap.rs | 19 +++++++++++++++++++ src/ui/mod.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/app/keymap.rs b/src/app/keymap.rs index d5c11de..cc69a78 100644 --- a/src/app/keymap.rs +++ b/src/app/keymap.rs @@ -301,4 +301,23 @@ mod tests { assert_eq!(app.filter_query, "abc"); assert_eq!(app.filter_cursor_pos, 2); // back between 'b' and 'c' } + + #[test] + fn theme_cycles_dark_light_green_on_t() { + let mut app = app_with_filter_query(""); + // Ensure starting theme is dark + assert_eq!(app.theme_name, "dark"); + + // First T: dark -> light + app.handle_key_event(key(KeyCode::Char('T'))).unwrap(); + assert_eq!(app.theme_name, "light"); + + // Second T: light -> green + app.handle_key_event(key(KeyCode::Char('T'))).unwrap(); + assert_eq!(app.theme_name, "green"); + + // Third T: green -> dark + app.handle_key_event(key(KeyCode::Char('T'))).unwrap(); + assert_eq!(app.theme_name, "dark"); + } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b07661d..0f528f1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -728,4 +728,30 @@ mod ui_tests { "should not render 'Iare' artifact" ); } + + #[test] + fn header_and_footer_use_current_theme() { + let mut app = make_app(); // starts with Theme::default_dark() + let area = Rect::new(0, 0, 80, 10); + + // Render with dark theme + let mut buf_dark = Buffer::empty(area); + (&app).render(area, &mut buf_dark); + + // Switch to light theme + app.theme = crate::ui::styles::Theme::light(); + + let mut buf_light = Buffer::empty(area); + (&app).render(area, &mut buf_light); + + // Compare a cell in the header (e.g., first column of first row) + let dark_cell = buf_dark.cell((area.x, area.y)).unwrap(); + let light_cell = buf_light.cell((area.x, area.y)).unwrap(); + + assert_ne!( + dark_cell.style().bg, + light_cell.style().bg, + "expected header background to change when theme changes" + ); + } } From df341b184443c2485e1a7c9125fb7c5c6eda6c44 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 30 Dec 2025 21:26:55 +0000 Subject: [PATCH 10/10] Add Themes option to status bar for easy access. --- src/ui/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0f528f1..b770c1e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -65,7 +65,7 @@ impl Widget for &App { } else if self.group_search_active { format!("Search groups: {}", self.group_search_input) } else { - "Tab Switch pane ↑↓ Move Enter Edit/Run t Tail y Copy Esc Cancel q Quit" + "Tab Switch pane ↑↓ Move Enter Edit/Run t Tail y Copy Esc Cancel T Themes q Quit" .to_string() };