themes that dont suck

This commit is contained in:
Tyler Longwell
2025-11-26 12:47:24 -05:00
parent 94648f2ff5
commit fbbf23ae62
8 changed files with 334 additions and 46 deletions
+20 -1
View File
@@ -6,6 +6,7 @@ use crate::components::popups::config::ConfigPopup;
use crate::components::popups::help::HelpPopup;
use crate::components::popups::message::MessagePopup;
use crate::components::popups::session::SessionPopup;
use crate::components::popups::theme::ThemePopup;
use crate::components::popups::todo::TodoPopup;
use crate::components::status::StatusComponent;
use crate::components::Component;
@@ -29,6 +30,7 @@ pub struct App<'a> {
builder_popup: BuilderPopup<'a>,
message_popup: MessagePopup,
config_popup: ConfigPopup,
theme_popup: ThemePopup,
pub last_popup_close_time: Option<Instant>,
}
@@ -52,6 +54,7 @@ impl<'a> App<'a> {
builder_popup: BuilderPopup::new(),
message_popup: MessagePopup::new(),
config_popup: ConfigPopup::new(),
theme_popup: ThemePopup::new(),
last_popup_close_time: None,
}
@@ -120,6 +123,15 @@ impl<'a> App<'a> {
}
return Ok(None);
}
if state.showing_theme_picker {
if let Some(action) = self.theme_popup.handle_event(event, state)? {
if matches!(action, Action::ClosePopup | Action::ChangeTheme(_)) {
self.last_popup_close_time = Some(std::time::Instant::now());
}
return Ok(Some(action));
}
return Ok(None);
}
Ok(None)
}
@@ -191,7 +203,8 @@ impl<'a> Component for App<'a> {
|| state.showing_session_picker
|| state.showing_command_builder
|| state.showing_message_info.is_some()
|| state.showing_config;
|| state.showing_config
|| state.showing_theme_picker;
if popup_active {
if let Some(action) = self.handle_popups(event, state)? {
@@ -238,6 +251,11 @@ impl<'a> Component for App<'a> {
}
fn render(&mut self, f: &mut Frame, area: Rect, state: &AppState) {
let theme = &state.config.theme;
let bg_block = ratatui::widgets::Block::default()
.style(ratatui::style::Style::default().bg(theme.base.background));
f.render_widget(bg_block, f.area());
let max_input_height = (f.area().height / 2).max(3);
let input_height = self.input.height(max_input_height);
@@ -264,5 +282,6 @@ impl<'a> Component for App<'a> {
self.message_popup.render(f, f.area(), state);
}
self.config_popup.render(f, f.area(), state);
self.theme_popup.render(f, f.area(), state);
}
}
+7 -3
View File
@@ -79,9 +79,13 @@ impl<'a> InputComponent<'a> {
.with_text(goose::agents::MANUAL_COMPACT_TRIGGER);
Some(Action::SendMessage(message))
}
"/theme" => parts
.get(1)
.map(|theme_name| Action::ChangeTheme(theme_name.to_string())),
"/theme" => {
if let Some(theme_name) = parts.get(1) {
Some(Action::ChangeTheme(theme_name.to_string()))
} else {
Some(Action::OpenThemePicker)
}
}
// Custom Commands
_ => {
let cmd_name = cmd.strip_prefix('/').unwrap_or(cmd);
@@ -3,4 +3,5 @@ pub mod config;
pub mod help;
pub mod message;
pub mod session;
pub mod theme;
pub mod todo;
@@ -0,0 +1,123 @@
use crate::components::Component;
use crate::services::events::Event;
use crate::state::action::Action;
use crate::state::AppState;
use crate::utils::layout::centered_rect;
use crate::utils::styles::Theme;
use anyhow::Result;
use crossterm::event::{KeyCode, MouseEventKind};
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState};
use ratatui::Frame;
pub struct ThemePopup {
list_state: ListState,
}
impl Default for ThemePopup {
fn default() -> Self {
Self::new()
}
}
impl ThemePopup {
pub fn new() -> Self {
let mut list_state = ListState::default();
list_state.select(Some(0));
Self { list_state }
}
}
impl Component for ThemePopup {
fn handle_event(&mut self, event: &Event, _state: &AppState) -> Result<Option<Action>> {
let names = Theme::all_names();
let len = names.len();
match event {
Event::Input(key) => match key.code {
KeyCode::Esc | KeyCode::Char('q') => return Ok(Some(Action::ClosePopup)),
KeyCode::Char('j') | KeyCode::Down => {
let cur = self.list_state.selected().unwrap_or(0);
self.list_state.select(Some((cur + 1) % len));
}
KeyCode::Char('k') | KeyCode::Up => {
let cur = self.list_state.selected().unwrap_or(0);
self.list_state
.select(Some(cur.checked_sub(1).unwrap_or(len - 1)));
}
KeyCode::Enter => {
if let Some(idx) = self.list_state.selected() {
if let Some(&name) = names.get(idx) {
return Ok(Some(Action::ChangeTheme(name.to_string())));
}
}
}
_ => {}
},
Event::Mouse(m) => match m.kind {
MouseEventKind::ScrollDown => {
let cur = self.list_state.selected().unwrap_or(0);
self.list_state.select(Some((cur + 1) % len));
}
MouseEventKind::ScrollUp => {
let cur = self.list_state.selected().unwrap_or(0);
self.list_state
.select(Some(cur.checked_sub(1).unwrap_or(len - 1)));
}
_ => {}
},
_ => {}
}
Ok(None)
}
fn render(&mut self, f: &mut Frame, area: Rect, state: &AppState) {
if !state.showing_theme_picker {
return;
}
let theme = &state.config.theme;
let names = Theme::all_names();
let area = centered_rect(30, 50, area);
f.render_widget(Clear, area);
let items: Vec<ListItem> = names
.iter()
.map(|&name| {
let is_current = name.eq_ignore_ascii_case(&theme.name);
let style = if is_current {
Style::default()
.fg(theme.status.success)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.base.foreground)
};
let suffix = if is_current { " ✓" } else { "" };
ListItem::new(Line::from(Span::styled(
format!("{name}{suffix}"),
style,
)))
})
.collect();
let block = Block::default()
.title("Select Theme (Enter to apply)")
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(Style::default().bg(theme.base.background));
let list = List::new(items)
.block(block)
.highlight_style(
Style::default()
.bg(theme.base.selection)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("▶ ");
f.render_stateful_widget(list, area, &mut self.list_state);
}
}
+1
View File
@@ -36,6 +36,7 @@ pub enum Action {
ResumeSession(String),
CreateNewSession,
ChangeTheme(String),
OpenThemePicker,
ClearChat,
ClosePopup,
OpenMessageInfo(usize),
+2
View File
@@ -39,6 +39,7 @@ pub struct AppState {
pub showing_command_builder: bool,
pub showing_message_info: Option<usize>,
pub showing_config: bool,
pub showing_theme_picker: bool,
pub has_worked: bool,
pub input_text_is_empty: bool,
pub model_context_limit: usize,
@@ -74,6 +75,7 @@ impl AppState {
showing_command_builder: false,
showing_message_info: None,
showing_config: false,
showing_theme_picker: false,
has_worked: false,
input_text_is_empty: true,
model_context_limit: 128_000,
+7
View File
@@ -149,6 +149,11 @@ fn handle_ui(state: &mut AppState, action: &Action) -> bool {
state.showing_command_builder = false;
state.showing_message_info = None;
state.showing_config = false;
state.showing_theme_picker = false;
true
}
Action::OpenThemePicker => {
state.showing_theme_picker = true;
true
}
Action::OpenMessageInfo(idx) => {
@@ -183,6 +188,8 @@ fn handle_misc(state: &mut AppState, action: &Action) -> bool {
}
Action::ChangeTheme(name) => {
state.config.theme = crate::utils::styles::Theme::from_name(name);
state.showing_theme_picker = false;
state.needs_refresh = true;
true
}
Action::SetInputEmpty(is_empty) => {
+173 -42
View File
@@ -37,13 +37,34 @@ impl Default for Theme {
}
impl Theme {
pub fn all_names() -> Vec<&'static str> {
vec![
"gemini",
"goose",
"light",
"dark",
"midnight",
"nord",
"dracula",
"matrix",
"tokyonight",
"solarized",
"retrowave",
]
}
pub fn from_name(name: &str) -> Self {
match name.to_lowercase().as_str() {
"goose" => Theme::goose(),
"light" => Theme::light(),
"dark" => Theme::dark(),
"midnight" => Theme::midnight(),
"nord" => Theme::nord(),
"dracula" => Theme::dracula(),
"matrix" => Theme::matrix(),
"tokyonight" | "tokyo" => Theme::tokyonight(),
"solarized" | "solar" => Theme::solarized(),
"retrowave" | "retro" | "synthwave" => Theme::retrowave(),
_ => Theme::gemini(),
}
}
@@ -54,7 +75,7 @@ impl Theme {
base: BaseColors {
background: Color::Reset,
foreground: Color::Rgb(205, 214, 244),
cursor: Color::Rgb(137, 220, 235),
cursor: Color::Rgb(166, 173, 200),
selection: Color::Rgb(49, 50, 68),
border: Color::Rgb(108, 112, 134),
border_active: Color::Rgb(137, 180, 250),
@@ -83,11 +104,11 @@ impl Theme {
user_message_foreground: Color::Rgb(180, 175, 170),
},
status: StatusColors {
info: Color::Blue,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
thinking: Color::Magenta,
info: Color::Rgb(138, 173, 189),
success: Color::Rgb(169, 182, 101),
warning: Color::Rgb(234, 178, 102),
error: Color::Rgb(204, 102, 102),
thinking: Color::Rgb(212, 163, 115),
},
}
}
@@ -96,20 +117,20 @@ impl Theme {
Self {
name: "Light".to_string(),
base: BaseColors {
background: Color::White,
foreground: Color::Rgb(31, 41, 55),
cursor: Color::Black,
selection: Color::Rgb(219, 234, 254),
border: Color::DarkGray,
border_active: Color::Blue,
user_message_foreground: Color::Rgb(15, 20, 27),
background: Color::Rgb(255, 255, 255),
foreground: Color::Rgb(36, 41, 46),
cursor: Color::Rgb(3, 102, 214),
selection: Color::Rgb(225, 228, 232),
border: Color::Rgb(225, 228, 232),
border_active: Color::Rgb(3, 102, 214),
user_message_foreground: Color::Rgb(88, 96, 105),
},
status: StatusColors {
info: Color::Blue,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
thinking: Color::Magenta,
info: Color::Rgb(3, 102, 214),
success: Color::Rgb(34, 134, 58),
warning: Color::Rgb(227, 98, 9),
error: Color::Rgb(215, 58, 73),
thinking: Color::Rgb(111, 66, 193),
},
}
}
@@ -118,20 +139,20 @@ impl Theme {
Self {
name: "Dark".to_string(),
base: BaseColors {
background: Color::Rgb(30, 30, 30),
foreground: Color::Rgb(212, 212, 212),
cursor: Color::White,
selection: Color::Rgb(38, 79, 120),
border: Color::Gray,
border_active: Color::Blue,
user_message_foreground: Color::Rgb(170, 170, 170),
background: Color::Rgb(40, 44, 52),
foreground: Color::Rgb(171, 178, 191),
cursor: Color::Rgb(97, 175, 239),
selection: Color::Rgb(62, 68, 81),
border: Color::Rgb(62, 68, 81),
border_active: Color::Rgb(97, 175, 239),
user_message_foreground: Color::Rgb(130, 137, 151),
},
status: StatusColors {
info: Color::Blue,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
thinking: Color::Magenta,
info: Color::Rgb(97, 175, 239),
success: Color::Rgb(152, 195, 121),
warning: Color::Rgb(229, 192, 123),
error: Color::Rgb(224, 108, 117),
thinking: Color::Rgb(198, 120, 221),
},
}
}
@@ -158,24 +179,134 @@ impl Theme {
}
}
pub fn nord() -> Self {
Self {
name: "Nord".to_string(),
base: BaseColors {
background: Color::Rgb(46, 52, 64),
foreground: Color::Rgb(216, 222, 233),
cursor: Color::Rgb(136, 192, 208),
selection: Color::Rgb(67, 76, 94),
border: Color::Rgb(67, 76, 94),
border_active: Color::Rgb(136, 192, 208),
user_message_foreground: Color::Rgb(176, 182, 196),
},
status: StatusColors {
info: Color::Rgb(129, 161, 193),
success: Color::Rgb(163, 190, 140),
warning: Color::Rgb(235, 203, 139),
error: Color::Rgb(191, 97, 106),
thinking: Color::Rgb(180, 142, 173),
},
}
}
pub fn dracula() -> Self {
Self {
name: "Dracula".to_string(),
base: BaseColors {
background: Color::Rgb(40, 42, 54),
foreground: Color::Rgb(248, 248, 242),
cursor: Color::Rgb(255, 121, 198),
selection: Color::Rgb(68, 71, 90),
border: Color::Rgb(68, 71, 90),
border_active: Color::Rgb(189, 147, 249),
user_message_foreground: Color::Rgb(189, 189, 189),
},
status: StatusColors {
info: Color::Rgb(139, 233, 253),
success: Color::Rgb(80, 250, 123),
warning: Color::Rgb(255, 184, 108),
error: Color::Rgb(255, 85, 85),
thinking: Color::Rgb(189, 147, 249),
},
}
}
pub fn matrix() -> Self {
Self {
name: "Matrix".to_string(),
base: BaseColors {
background: Color::Black,
foreground: Color::Green,
cursor: Color::Green,
selection: Color::DarkGray,
border: Color::DarkGray,
border_active: Color::Green,
user_message_foreground: Color::Rgb(0, 100, 0),
background: Color::Rgb(0, 10, 0),
foreground: Color::Rgb(0, 255, 65),
cursor: Color::Rgb(0, 255, 65),
selection: Color::Rgb(0, 60, 0),
border: Color::Rgb(0, 80, 0),
border_active: Color::Rgb(0, 255, 65),
user_message_foreground: Color::Rgb(0, 180, 45),
},
status: StatusColors {
info: Color::Green,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
thinking: Color::Green,
info: Color::Rgb(0, 200, 80),
success: Color::Rgb(0, 255, 65),
warning: Color::Rgb(180, 255, 0),
error: Color::Rgb(255, 50, 50),
thinking: Color::Rgb(0, 255, 130),
},
}
}
pub fn tokyonight() -> Self {
Self {
name: "TokyoNight".to_string(),
base: BaseColors {
background: Color::Rgb(26, 27, 38),
foreground: Color::Rgb(192, 202, 245),
cursor: Color::Rgb(125, 207, 255),
selection: Color::Rgb(43, 48, 74),
border: Color::Rgb(59, 66, 97),
border_active: Color::Rgb(125, 207, 255),
user_message_foreground: Color::Rgb(148, 156, 187),
},
status: StatusColors {
info: Color::Rgb(125, 207, 255),
success: Color::Rgb(158, 206, 106),
warning: Color::Rgb(224, 175, 104),
error: Color::Rgb(247, 118, 142),
thinking: Color::Rgb(187, 154, 247),
},
}
}
pub fn solarized() -> Self {
Self {
name: "Solarized".to_string(),
base: BaseColors {
background: Color::Rgb(0, 43, 54),
foreground: Color::Rgb(131, 148, 150),
cursor: Color::Rgb(38, 139, 210),
selection: Color::Rgb(7, 54, 66),
border: Color::Rgb(88, 110, 117),
border_active: Color::Rgb(38, 139, 210),
user_message_foreground: Color::Rgb(101, 123, 131),
},
status: StatusColors {
info: Color::Rgb(38, 139, 210),
success: Color::Rgb(133, 153, 0),
warning: Color::Rgb(181, 137, 0),
error: Color::Rgb(220, 50, 47),
thinking: Color::Rgb(108, 113, 196),
},
}
}
pub fn retrowave() -> Self {
Self {
name: "Retrowave".to_string(),
base: BaseColors {
background: Color::Rgb(22, 22, 30),
foreground: Color::Rgb(224, 222, 244),
cursor: Color::Rgb(255, 121, 198),
selection: Color::Rgb(52, 42, 72),
border: Color::Rgb(65, 55, 85),
border_active: Color::Rgb(255, 121, 198),
user_message_foreground: Color::Rgb(180, 175, 200),
},
status: StatusColors {
info: Color::Rgb(125, 207, 255),
success: Color::Rgb(115, 218, 202),
warning: Color::Rgb(255, 158, 100),
error: Color::Rgb(247, 118, 142),
thinking: Color::Rgb(187, 154, 247),
},
}
}