mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
themes that dont suck
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ pub enum Action {
|
||||
ResumeSession(String),
|
||||
CreateNewSession,
|
||||
ChangeTheme(String),
|
||||
OpenThemePicker,
|
||||
ClearChat,
|
||||
ClosePopup,
|
||||
OpenMessageInfo(usize),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user