Skip to content

Commit 84a6c5c

Browse files
author
Factory Bot
committed
feat(tui-components): add ColorScheme and migrate Toast/SelectionList with dynamic theming
- Add color_scheme.rs with customizable ColorScheme struct (dark/light themes) - Migrate Toast component with ColorScheme support and with_colors() builder - Add SelectionList component with navigation, search, shortcuts, and ColorScheme - Centralize ROUNDED_BORDER from 5 files to single import - Fix compilation errors (unstable feature, platform deps, unsafe extern) - Fix brain animation FPS (4 -> 120 FPS) on welcome screen - Update spinner styles (Breathing, HalfCircle) - Update checkbox symbol [x] -> [✓] - Update key_hints separator and styling
1 parent a33e6ed commit 84a6c5c

File tree

86 files changed

+5384
-236
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+5384
-236
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cortex-core/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ tracing = { workspace = true }
2424
serde = { workspace = true }
2525
serde_json = { workspace = true }
2626

27-
[target.'cfg(unix)'.dependencies]
28-
libc = "0.2"
29-
3027
# Markdown rendering dependencies
3128
opentui-syntax = { workspace = true }
3229
opentui-text = { workspace = true }
@@ -37,6 +34,9 @@ ahash = { workspace = true }
3734
tree-sitter = { workspace = true }
3835
tree-sitter-bash = { workspace = true }
3936

37+
[target.'cfg(unix)'.dependencies]
38+
libc = "0.2"
39+
4040
[dev-dependencies]
4141
tokio-test = { workspace = true }
4242

cortex-tui-capture/src/screenshot_generator.rs

Lines changed: 54 additions & 42 deletions
Large diffs are not rendered by default.

cortex-tui-components/examples/dump_previews.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ fn preview_spinner() -> String {
306306
ascii.push_str("Loading indicators with various styles.\n\n");
307307

308308
let styles = [
309+
(SpinnerStyle::Breathing, "Breathing (default)"),
310+
(SpinnerStyle::HalfCircle, "HalfCircle (tools)"),
309311
(SpinnerStyle::Dots, "Dots"),
310312
(SpinnerStyle::Line, "Line"),
311313
(SpinnerStyle::Braille, "Braille"),
@@ -319,7 +321,7 @@ fn preview_spinner() -> String {
319321

320322
ascii.push_str(&format!("**{}:**\n", name));
321323
ascii.push_str("```\n");
322-
ascii.push_str(&render_to_ascii(&spinner, 25, 1));
324+
ascii.push_str(&render_to_ascii(&spinner, 35, 1));
323325
ascii.push_str("```\n\n");
324326
}
325327

cortex-tui-components/src/checkbox.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ impl Widget for &CheckboxGroup {
9999
}
100100

101101
let is_focused = i == self.focused;
102-
let checkbox = if item.checked { "[x]" } else { "[ ]" };
102+
let checkbox = if item.checked { "[]" } else { "[ ]" };
103103

104104
let style = if item.disabled {
105105
Style::default().fg(TEXT_DIM)
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//! Color Scheme for customizable TUI components.
2+
//!
3+
//! Provides a flexible color configuration that can be customized
4+
//! or use sensible defaults from cortex-core.
5+
6+
use cortex_core::style::{
7+
CYAN_PRIMARY, ERROR, INFO, SUCCESS, SURFACE_0, SURFACE_1, TEXT, TEXT_DIM, TEXT_MUTED, VOID,
8+
WARNING,
9+
};
10+
use ratatui::style::Color;
11+
12+
/// A flexible color scheme for TUI components.
13+
///
14+
/// All components in cortex-tui-components can be customized with this scheme.
15+
/// Use `Default::default()` for the standard Cortex theme.
16+
#[derive(Debug, Clone, Copy)]
17+
pub struct ColorScheme {
18+
/// Primary accent color (selection, focus, highlights)
19+
pub accent: Color,
20+
/// Normal text color
21+
pub text: Color,
22+
/// Secondary/dimmed text color
23+
pub text_dim: Color,
24+
/// Muted/disabled text color
25+
pub text_muted: Color,
26+
/// Primary background/surface color
27+
pub surface: Color,
28+
/// Alternative background color
29+
pub surface_alt: Color,
30+
/// Inverted/void color (for text on accent backgrounds)
31+
pub void: Color,
32+
/// Success indicator color
33+
pub success: Color,
34+
/// Warning indicator color
35+
pub warning: Color,
36+
/// Error indicator color
37+
pub error: Color,
38+
/// Info indicator color
39+
pub info: Color,
40+
}
41+
42+
impl Default for ColorScheme {
43+
fn default() -> Self {
44+
Self {
45+
accent: CYAN_PRIMARY,
46+
text: TEXT,
47+
text_dim: TEXT_DIM,
48+
text_muted: TEXT_MUTED,
49+
surface: SURFACE_0,
50+
surface_alt: SURFACE_1,
51+
void: VOID,
52+
success: SUCCESS,
53+
warning: WARNING,
54+
error: ERROR,
55+
info: INFO,
56+
}
57+
}
58+
}
59+
60+
impl ColorScheme {
61+
/// Creates a new color scheme with all colors specified.
62+
pub fn new(
63+
accent: Color,
64+
text: Color,
65+
text_dim: Color,
66+
text_muted: Color,
67+
surface: Color,
68+
surface_alt: Color,
69+
void: Color,
70+
success: Color,
71+
warning: Color,
72+
error: Color,
73+
info: Color,
74+
) -> Self {
75+
Self {
76+
accent,
77+
text,
78+
text_dim,
79+
text_muted,
80+
surface,
81+
surface_alt,
82+
void,
83+
success,
84+
warning,
85+
error,
86+
info,
87+
}
88+
}
89+
90+
/// Creates a color scheme with a custom accent color.
91+
pub fn with_accent(mut self, accent: Color) -> Self {
92+
self.accent = accent;
93+
self
94+
}
95+
96+
/// Creates a color scheme with custom text colors.
97+
pub fn with_text(mut self, text: Color, dim: Color, muted: Color) -> Self {
98+
self.text = text;
99+
self.text_dim = dim;
100+
self.text_muted = muted;
101+
self
102+
}
103+
104+
/// Creates a color scheme with custom surface colors.
105+
pub fn with_surface(mut self, surface: Color, alt: Color) -> Self {
106+
self.surface = surface;
107+
self.surface_alt = alt;
108+
self
109+
}
110+
111+
/// Creates a color scheme with custom status colors.
112+
pub fn with_status(mut self, success: Color, warning: Color, error: Color, info: Color) -> Self {
113+
self.success = success;
114+
self.warning = warning;
115+
self.error = error;
116+
self.info = info;
117+
self
118+
}
119+
120+
/// Creates a light theme color scheme.
121+
pub fn light() -> Self {
122+
Self {
123+
accent: Color::Rgb(0, 150, 100),
124+
text: Color::Rgb(30, 30, 30),
125+
text_dim: Color::Rgb(100, 100, 100),
126+
text_muted: Color::Rgb(150, 150, 150),
127+
surface: Color::Rgb(255, 255, 255),
128+
surface_alt: Color::Rgb(240, 240, 240),
129+
void: Color::Rgb(255, 255, 255),
130+
success: Color::Rgb(0, 150, 0),
131+
warning: Color::Rgb(200, 150, 0),
132+
error: Color::Rgb(200, 50, 50),
133+
info: Color::Rgb(50, 100, 200),
134+
}
135+
}
136+
137+
/// Creates a dark theme color scheme (default Cortex theme).
138+
pub fn dark() -> Self {
139+
Self::default()
140+
}
141+
}
142+
143+
#[cfg(test)]
144+
mod tests {
145+
use super::*;
146+
147+
#[test]
148+
fn test_default_scheme() {
149+
let scheme = ColorScheme::default();
150+
assert_eq!(scheme.accent, CYAN_PRIMARY);
151+
assert_eq!(scheme.text, TEXT);
152+
}
153+
154+
#[test]
155+
fn test_with_accent() {
156+
let scheme = ColorScheme::default().with_accent(Color::Red);
157+
assert_eq!(scheme.accent, Color::Red);
158+
}
159+
160+
#[test]
161+
fn test_light_theme() {
162+
let scheme = ColorScheme::light();
163+
// Light theme should have light surface
164+
if let Color::Rgb(r, _, _) = scheme.surface {
165+
assert!(r > 200);
166+
}
167+
}
168+
}

cortex-tui-components/src/key_hints.rs

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ impl KeyHintsBar {
4747
pub fn new() -> Self {
4848
Self {
4949
hints: Vec::new(),
50-
separator: " ".to_string(),
50+
separator: " · ".to_string(), // Middle dot separator like main TUI
5151
}
5252
}
5353

@@ -88,8 +88,8 @@ impl KeyHintsBar {
8888
if i > 0 {
8989
width += self.separator.len();
9090
}
91-
// Format: [key] description
92-
width += 1 + hint.key.len() + 1 + 1 + hint.description.len();
91+
// Format: key description (no brackets)
92+
width += hint.key.len() + 1 + hint.description.len();
9393
}
9494
width
9595
}
@@ -102,7 +102,8 @@ impl KeyHintsBar {
102102
let mut current_width = 0;
103103

104104
for hint in &self.hints {
105-
let hint_width = 1 + hint.key.len() + 1 + 1 + hint.description.len();
105+
// No brackets: key + space + description
106+
let hint_width = hint.key.len() + 1 + hint.description.len();
106107
let needed = if result.is_empty() {
107108
hint_width
108109
} else {
@@ -145,15 +146,14 @@ impl Widget for KeyHintsBar {
145146
}
146147

147148
let key_style = Style::default().fg(CYAN_PRIMARY).bg(SURFACE_1);
148-
let bracket_style = Style::default().fg(TEXT_DIM).bg(SURFACE_1);
149-
let desc_style = Style::default().fg(TEXT).bg(SURFACE_1);
149+
let desc_style = Style::default().fg(TEXT_DIM).bg(SURFACE_1);
150150
let sep_style = Style::default().fg(TEXT_DIM).bg(SURFACE_1);
151151

152152
let mut x = area.x + 1;
153153

154154
for (i, hint) in hints.iter().enumerate() {
155155
if i > 0 {
156-
// Separator
156+
// Separator (middle dot style like main TUI)
157157
for ch in self.separator.chars() {
158158
if x < area.right() {
159159
if let Some(cell) = buf.cell_mut((x, area.y)) {
@@ -164,15 +164,7 @@ impl Widget for KeyHintsBar {
164164
}
165165
}
166166

167-
// Opening bracket
168-
if x < area.right() {
169-
if let Some(cell) = buf.cell_mut((x, area.y)) {
170-
cell.set_char('[').set_style(bracket_style);
171-
}
172-
x += 1;
173-
}
174-
175-
// Key
167+
// Key (no brackets, like main TUI)
176168
for ch in hint.key.chars() {
177169
if x < area.right() {
178170
if let Some(cell) = buf.cell_mut((x, area.y)) {
@@ -182,14 +174,6 @@ impl Widget for KeyHintsBar {
182174
}
183175
}
184176

185-
// Closing bracket
186-
if x < area.right() {
187-
if let Some(cell) = buf.cell_mut((x, area.y)) {
188-
cell.set_char(']').set_style(bracket_style);
189-
}
190-
x += 1;
191-
}
192-
193177
// Space
194178
if x < area.right() {
195179
if let Some(cell) = buf.cell_mut((x, area.y)) {

cortex-tui-components/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
pub mod borders;
114114
pub mod card;
115115
pub mod checkbox;
116+
pub mod color_scheme;
116117
pub mod component;
117118
pub mod dropdown;
118119
pub mod focus;
@@ -125,6 +126,7 @@ pub mod panel;
125126
pub mod popup;
126127
pub mod radio;
127128
pub mod scroll;
129+
pub mod selection_list;
128130
pub mod selector;
129131
pub mod spinner;
130132
pub mod text;
@@ -139,6 +141,7 @@ pub mod prelude {
139141
pub use crate::borders::{BorderStyle, RoundedBorder};
140142
pub use crate::card::{Card, CardBuilder};
141143
pub use crate::checkbox::{CheckboxGroup, CheckboxItem};
144+
pub use crate::color_scheme::ColorScheme;
142145
pub use crate::component::{Component, ComponentResult, FocusState};
143146
pub use crate::dropdown::{Dropdown, DropdownItem, DropdownPosition, DropdownState};
144147
pub use crate::focus::{FocusDirection, FocusManager};
@@ -151,10 +154,11 @@ pub mod prelude {
151154
pub use crate::popup::{Popup, PopupPosition};
152155
pub use crate::radio::{RadioGroup, RadioItem};
153156
pub use crate::scroll::{ScrollState, Scrollable};
157+
pub use crate::selection_list::{SelectionItem, SelectionList, SelectionResult};
154158
pub use crate::selector::{SelectItem, SelectResult, Selector, SelectorState};
155159
pub use crate::spinner::{LoadingSpinner, SpinnerStyle};
156160
pub use crate::text::{StyledText, TextStyle};
157-
pub use crate::toast::{Toast, ToastLevel, ToastManager};
161+
pub use crate::toast::{Toast, ToastLevel, ToastManager, ToastPosition, ToastWidget};
158162
}
159163

160164
// Re-export cortex-core style for convenience

0 commit comments

Comments
 (0)