Skip to content

Commit 10ddda4

Browse files
authored
Merge pull request #91 from Ayan-sh03/dev/Ayan-sh03/light-theme-support
feat(tui): add light theme support with auto-detection
2 parents 7135b12 + 8392355 commit 10ddda4

File tree

6 files changed

+167
-41
lines changed

6 files changed

+167
-41
lines changed

shai-cli/src/tui/app.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ use super::input::UserAction;
4040
use crate::tui::perm::PermissionWidget;
4141
use crate::tui::perm_alt_screen::AlternateScreenPermissionModal;
4242
use super::perm::PermissionModalAction;
43+
use super::theme::Theme;
4344

4445

4546
pub enum AppModalState<'a> {
@@ -72,6 +73,8 @@ pub struct App<'a> {
7273

7374
pub(crate) total_input_tokens: u32,
7475
pub(crate) total_output_tokens: u32,
76+
77+
pub(crate) theme: Theme, // UI theme (dark/light)
7578
}
7679

7780

@@ -168,20 +171,24 @@ impl App<'_> {
168171
// UI-related Internals
169172
impl App<'_> {
170173
pub fn new() -> Self {
174+
let theme = Theme::from_env(); // Read from SHAI_TUI_THEME env var
175+
let palette = theme.palette();
176+
171177
Self {
172178
terminal: None,
173179
terminal_height: 5,
174180
agent: None,
175181
custom_agent: None,
176182
formatter: PrettyFormatter::new(),
177183
state: AppModalState::InputShown,
178-
input: InputArea::new(),
184+
input: InputArea::new(palette),
179185
commands: Self::list_command(),
180186
exit: false,
181187
running_tools: HashMap::new(),
182188
permission_queue: VecDeque::new(),
183189
total_input_tokens: 0,
184190
total_output_tokens: 0,
191+
theme,
185192
}
186193
}
187194

@@ -277,6 +284,14 @@ impl App<'_> {
277284
return Ok(());
278285
}
279286

287+
// Handle theme toggle with Ctrl+T
288+
if matches!(key_event.code, KeyCode::Char('t')) && key_event.modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
289+
self.theme.toggle();
290+
let new_palette = self.theme.palette();
291+
self.input.set_palette(new_palette);
292+
return Ok(());
293+
}
294+
280295
match &mut self.state {
281296
AppModalState::InputShown => {
282297
let action = self.input.handle_event(key_event).await;
@@ -322,10 +337,12 @@ impl App<'_> {
322337
match &self.state {
323338
AppModalState::InputShown if !self.permission_queue.is_empty() => {
324339
let (request_id, request) = self.permission_queue.front().unwrap();
340+
let palette = self.theme.palette();
325341
let widget = PermissionWidget::new(
326342
request_id.clone(),
327343
request.clone(),
328-
self.permission_queue.len()
344+
self.permission_queue.len(),
345+
palette
329346
);
330347

331348
let terminal_height = self.terminal.as_ref()
@@ -335,7 +352,7 @@ impl App<'_> {
335352

336353
if widget.height() > terminal_height.saturating_sub(5) {
337354
// Use alternate screen for large modals
338-
if let Ok(mut modal) = AlternateScreenPermissionModal::new(&widget) {
355+
if let Ok(mut modal) = AlternateScreenPermissionModal::new(&widget, palette) {
339356
let action = modal.run().await.unwrap_or(PermissionModalAction::Nope);
340357
self.handle_permission_action(action).await?;
341358
}

shai-cli/src/tui/command.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::{collections::HashMap, io, time::Duration};
22
use shai_llm::ToolCallMethod;
33

44
use crate::tui::App;
5+
use super::theme::Theme;
56

67
impl App<'_> {
78
pub(crate) fn list_command() -> HashMap<(String, String),Vec<String>> {
@@ -10,6 +11,7 @@ impl App<'_> {
1011
(("/auth","select a provider"), vec![]),
1112
(("/tc","set the tool call method: [fc | fc2 | so]"), vec!["method"]),
1213
(("/tokens","display token usage (input/output)"), vec![]),
14+
(("/theme","set theme: [dark | light | toggle]"), vec!["mode"]),
1315
])
1416
.into_iter()
1517
.map(|((cmd,desc),args)|((cmd.to_string(),desc.to_string()),args.into_iter().map(|s|s.to_string()).collect()))
@@ -65,6 +67,35 @@ impl App<'_> {
6567
);
6668
self.input.alert_msg(&msg, Duration::from_secs(5));
6769
}
70+
"/theme" => {
71+
match args.into_iter().next() {
72+
Some("dark") => {
73+
self.theme = Theme::Dark;
74+
let new_palette = self.theme.palette();
75+
self.input.set_palette(new_palette);
76+
self.input.alert_msg("Theme set to dark", Duration::from_secs(2));
77+
}
78+
Some("light") => {
79+
self.theme = Theme::Light;
80+
let new_palette = self.theme.palette();
81+
self.input.set_palette(new_palette);
82+
self.input.alert_msg("Theme set to light", Duration::from_secs(2));
83+
}
84+
Some("toggle") => {
85+
self.theme.toggle();
86+
let new_palette = self.theme.palette();
87+
self.input.set_palette(new_palette);
88+
let theme_name = match self.theme {
89+
Theme::Dark => "dark",
90+
Theme::Light => "light",
91+
};
92+
self.input.alert_msg(&format!("Theme toggled to {}", theme_name), Duration::from_secs(2));
93+
}
94+
_ => {
95+
self.input.alert_msg("Usage: /theme [dark|light|toggle]", Duration::from_secs(3));
96+
}
97+
}
98+
}
6899
_ => {
69100
self.input.alert_msg("command unknown", Duration::from_secs(1));
70101
}

shai-cli/src/tui/input.rs

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use tui_textarea::{Input, TextArea};
2020

2121
use crate::{tui::{cmdnav::CommandNav, helper::HelpArea}};
2222

23-
use super::theme::SHAI_YELLOW;
23+
use super::theme::{SHAI_YELLOW, ThemePalette};
2424

2525
pub enum UserAction {
2626
Nope,
@@ -72,10 +72,13 @@ pub struct InputArea<'a> {
7272

7373
// gitignore patterns (loaded once)
7474
gitignore_patterns: Vec<String>,
75+
76+
// theme colors
77+
palette: ThemePalette,
7578
}
7679

77-
impl Default for InputArea<'_> {
78-
fn default() -> Self {
80+
impl InputArea<'_> {
81+
pub fn new(palette: ThemePalette) -> Self {
7982
Self {
8083
agent_running: false,
8184
input: TextArea::default(),
@@ -98,20 +101,19 @@ impl Default for InputArea<'_> {
98101
suggestion_index: None,
99102
suggestion_search: None,
100103
gitignore_patterns: Self::load_gitignore_patterns(),
104+
palette,
101105
}
102106
}
103-
}
104-
105-
impl InputArea<'_> {
106-
pub fn new() -> Self {
107-
Self::default()
108-
}
109107

110108
pub fn set_history(&mut self, history: Vec<String>) {
111109
self.history = history;
112110
self.history_index = self.history.len();
113111
}
114112

113+
pub fn set_palette(&mut self, palette: ThemePalette) {
114+
self.palette = palette;
115+
}
116+
115117
// Parse .gitignore and return list of patterns to ignore
116118
fn load_gitignore_patterns() -> Vec<String> {
117119
if let Ok(content) = fs::read_to_string(".gitignore") {
@@ -609,15 +611,14 @@ impl InputArea<'_> {
609611
]).areas(area);
610612

611613
// status
612-
f.render_widget(Span::styled(self.get_status_text(), Style::default().fg(Color::Yellow)), status);
614+
f.render_widget(Span::styled(self.get_status_text(), Style::default().fg(self.palette.status)), status);
613615

614616
// Input - clone and apply block styling
615617
let block = Block::default()
616618
.borders(Borders::ALL)
617619
.border_set(border::ROUNDED)
618620
.padding(Padding { left: 1, right: 1, top: 0, bottom: 0 })
619-
.border_style(Style::default().fg(Color::DarkGray));
620-
//.border_style(Style::default().bold().fg(Color::Rgb(SHAI_YELLOW.0, SHAI_YELLOW.1, SHAI_YELLOW.2)));
621+
.border_style(Style::default().fg(self.palette.border));
621622
let inner = block.inner(input_area);
622623
f.render_widget(block, input_area);
623624

@@ -626,11 +627,11 @@ impl InputArea<'_> {
626627

627628
// Set placeholder and block
628629
self.input.set_placeholder_text("? for help");
629-
self.input.set_placeholder_style(Style::default().fg(Color::DarkGray));
630-
self.input.set_style(Style::default().fg(Color::White));
630+
self.input.set_placeholder_style(Style::default().fg(self.palette.placeholder));
631+
self.input.set_style(Style::default().fg(self.palette.input_text));
631632
self.input.set_cursor_style(Style::default()
632-
.fg(Color::White)
633-
.bg(if !self.input.lines()[0].is_empty() { Color::White } else { Color::Reset }));
633+
.fg(self.palette.cursor_fg)
634+
.bg(if !self.input.lines()[0].is_empty() { self.palette.cursor_bg } else { Color::Reset }));
634635
self.input.set_cursor_line_style(Style::default());
635636
f.render_widget(&self.input, prompt);
636637

@@ -643,13 +644,13 @@ impl InputArea<'_> {
643644

644645
let helper_text = self.check_helper_msg();
645646
f.render_widget(
646-
Span::styled(helper_text, Style::default().fg(Color::DarkGray).dim()),
647+
Span::styled(helper_text, Style::default().fg(self.palette.method_label).dim()),
647648
helper_left
648649
);
649650

650651
// Status
651652
f.render_widget(
652-
Span::styled(self.method_str(), Style::default().fg(Color::DarkGray)),
653+
Span::styled(self.method_str(), Style::default().fg(self.palette.method_label)),
653654
helper_right
654655
);
655656

@@ -676,9 +677,9 @@ impl InputArea<'_> {
676677
.map(|(window_idx, path)| {
677678
let actual_idx = start + window_idx;
678679
let style = if Some(actual_idx) == self.suggestion_index {
679-
Style::default().fg(Color::Yellow).bg(Color::DarkGray)
680+
Style::default().fg(self.palette.suggestion_selected_fg).bg(self.palette.suggestion_selected_bg)
680681
} else {
681-
Style::default().fg(Color::White)
682+
Style::default().fg(self.palette.suggestion_normal)
682683
};
683684
ListItem::new(path.as_str()).style(style)
684685
})
@@ -694,7 +695,7 @@ impl InputArea<'_> {
694695
.block(Block::default()
695696
.borders(Borders::ALL)
696697
.border_set(border::ROUNDED)
697-
.border_style(Style::default().fg(Color::DarkGray))
698+
.border_style(Style::default().fg(self.palette.border))
698699
.title(title));
699700

700701
f.render_widget(suggestions_list, suggestions_area);

shai-cli/src/tui/perm.rs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use ratatui::{
1313
use shai_core::{agent::{events::PermissionRequest, output::PrettyFormatter, PermissionResponse}, tools::{ToolCall, ToolResult}};
1414
// Removed tui_textarea dependency for colored preview
1515

16-
use super::theme::SHAI_YELLOW;
16+
use super::theme::{SHAI_YELLOW, ThemePalette};
1717

1818
pub enum PermissionModalAction {
1919
Nope,
@@ -33,11 +33,12 @@ pub struct PermissionWidget<'a> {
3333
formatted_request: String,
3434
preview_text: Text<'a>,
3535
scroll_offset: usize,
36-
scroll_state: ScrollbarState
36+
scroll_state: ScrollbarState,
37+
palette: ThemePalette,
3738
}
3839

3940
impl PermissionWidget<'_> {
40-
pub fn new(request_id: String, request: PermissionRequest, total: usize) -> Self {
41+
pub fn new(request_id: String, request: PermissionRequest, total: usize, palette: ThemePalette) -> Self {
4142
let formatter = PrettyFormatter::new();
4243
let formatted_request = formatter.format_toolcall(&request.call, request.preview.as_ref());
4344
let preview_text = formatted_request.into_text().unwrap();
@@ -51,7 +52,8 @@ impl PermissionWidget<'_> {
5152
formatted_request,
5253
preview_text,
5354
scroll_offset: 0,
54-
scroll_state: ScrollbarState::new(content_length)
55+
scroll_state: ScrollbarState::new(content_length),
56+
palette,
5557
}
5658
}
5759

@@ -150,7 +152,7 @@ impl PermissionWidget<'_> {
150152
.borders(Borders::ALL)
151153
.border_set(border::ROUNDED)
152154
.padding(Padding{left: 1, right: 1, top: 1, bottom: 1})
153-
.border_style(Style::default().fg(Color::Cyan))
155+
.border_style(Style::default().fg(self.palette.status))
154156
.title(if self.remaining_perms > 1 {
155157
format!(" 🔐 Permission Required ({}/{}) ", 1, self.remaining_perms)
156158
} else {
@@ -166,20 +168,20 @@ impl PermissionWidget<'_> {
166168
let tool_name = PrettyFormatter::capitalize_first(&call.tool_name);
167169
let context = PrettyFormatter::extract_primary_param(&call.parameters, &call.tool_name);
168170
let mut title = Line::from(vec![
169-
Span::styled("🔧 ", Color::White),
170-
Span::styled(tool_name, Style::new().white().bold())
171+
Span::styled("🔧 ", self.palette.input_text),
172+
Span::styled(tool_name, Style::default().fg(self.palette.input_text).bold())
171173
]);
172174
if let Some((_,ctx)) = context {
173-
title.push_span(Span::styled(format!("({})", ctx), Style::new().white()));
175+
title.push_span(Span::styled(format!("({})", ctx), Style::default().fg(self.palette.input_text)));
174176
};
175177

176178
let block = Block::default()
177179
.borders(Borders::ALL)
178180
.border_set(border::ROUNDED)
179181
.padding(Padding{left: 1, right: 1, top: 0, bottom: 0})
180182
.title(title)
181-
.title_style(Style::default().fg(Color::White))
182-
.border_style(Style::default().fg(Color::DarkGray));
183+
.title_style(Style::default().fg(self.palette.input_text))
184+
.border_style(Style::default().fg(self.palette.border));
183185

184186
let inner = block.inner(tool);
185187
f.render_widget(block, tool);
@@ -192,7 +194,7 @@ impl PermissionWidget<'_> {
192194
// Render scrollbar if content is longer than area
193195
if self.preview_text.lines.len() > inner.height as usize {
194196
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
195-
.style(Style::default().fg(Color::DarkGray));
197+
.style(Style::default().fg(self.palette.border));
196198
f.render_stateful_widget(scrollbar, inner, &mut self.scroll_state.clone());
197199
}
198200

@@ -201,13 +203,13 @@ impl PermissionWidget<'_> {
201203
for (i,s) in items.into_iter().enumerate() {
202204
if i == self.selected_index {
203205
lines.push(Line::from(vec![
204-
Span::styled("❯ ", Color::White),
205-
Span::styled(s, Color::White)
206+
Span::styled("❯ ", self.palette.suggestion_selected_fg),
207+
Span::styled(s, self.palette.suggestion_selected_fg)
206208
]));
207209
} else {
208210
lines.push(Line::from(vec![
209-
Span::styled(" ", Color::DarkGray),
210-
Span::styled(s, Color::DarkGray)
211+
Span::styled(" ", self.palette.placeholder),
212+
Span::styled(s, self.palette.placeholder)
211213
]));
212214
};
213215
}

shai-cli/src/tui/perm_alt_screen.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,21 @@ use ratatui::{
1717
use shai_core::agent::events::PermissionRequest;
1818

1919
use super::perm::{PermissionWidget, PermissionModalAction};
20+
use super::theme::ThemePalette;
2021

2122
pub struct AlternateScreenPermissionModal<'a> {
2223
widget: PermissionWidget<'a>,
2324
}
2425

2526
impl AlternateScreenPermissionModal<'_> {
26-
pub fn new(widget: &PermissionWidget) -> io::Result<Self> {
27+
pub fn new(widget: &PermissionWidget, palette: ThemePalette) -> io::Result<Self> {
2728
Ok(Self {
2829
widget: PermissionWidget::new(
2930
widget.request_id.clone(),
3031
widget.request.clone(),
31-
widget.remaining_perms)
32+
widget.remaining_perms,
33+
palette
34+
)
3235
})
3336
}
3437

0 commit comments

Comments
 (0)