color schemes
This commit is contained in:
@@ -12,7 +12,7 @@ tokio = { workspace = true }
|
|||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
rustyline = "17.0.1"
|
rustyline = "17.0.1"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ use tracing::{error, info};
|
|||||||
mod retro_tui;
|
mod retro_tui;
|
||||||
mod tui;
|
mod tui;
|
||||||
mod ui_writer_impl;
|
mod ui_writer_impl;
|
||||||
|
mod theme;
|
||||||
use retro_tui::RetroTui;
|
use retro_tui::RetroTui;
|
||||||
use tui::SimpleOutput;
|
use tui::SimpleOutput;
|
||||||
use ui_writer_impl::{ConsoleUiWriter, RetroTuiWriter};
|
use ui_writer_impl::{ConsoleUiWriter, RetroTuiWriter};
|
||||||
|
use theme::ColorTheme;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "g3")]
|
#[command(name = "g3")]
|
||||||
@@ -54,6 +56,10 @@ pub struct Cli {
|
|||||||
/// Use retro terminal UI (inspired by 80s sci-fi)
|
/// Use retro terminal UI (inspired by 80s sci-fi)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub retro: bool,
|
pub retro: bool,
|
||||||
|
|
||||||
|
/// Color theme for retro mode (default, dracula, or path to theme file)
|
||||||
|
#[arg(long, value_name = "THEME")]
|
||||||
|
pub theme: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run() -> Result<()> {
|
pub async fn run() -> Result<()> {
|
||||||
@@ -171,7 +177,7 @@ pub async fn run() -> Result<()> {
|
|||||||
|
|
||||||
if cli.retro {
|
if cli.retro {
|
||||||
// Use retro terminal UI
|
// Use retro terminal UI
|
||||||
run_interactive_retro(config, cli.show_prompt, cli.show_code).await?;
|
run_interactive_retro(config, cli.show_prompt, cli.show_code, cli.theme).await?;
|
||||||
} else {
|
} else {
|
||||||
// Use standard terminal UI
|
// Use standard terminal UI
|
||||||
let output = SimpleOutput::new();
|
let output = SimpleOutput::new();
|
||||||
@@ -183,15 +189,24 @@ pub async fn run() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: bool) -> Result<()> {
|
async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: bool, theme_name: Option<String>) -> Result<()> {
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
// Set environment variable to suppress println in other crates
|
// Set environment variable to suppress println in other crates
|
||||||
std::env::set_var("G3_RETRO_MODE", "1");
|
std::env::set_var("G3_RETRO_MODE", "1");
|
||||||
|
|
||||||
|
// Load the color theme
|
||||||
|
let theme = match ColorTheme::load(theme_name.as_deref()) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to load theme: {}. Using default.", e);
|
||||||
|
ColorTheme::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize the retro terminal UI
|
// Initialize the retro terminal UI
|
||||||
let tui = RetroTui::start().await?;
|
let tui = RetroTui::start(theme).await?;
|
||||||
|
|
||||||
// Create agent with RetroTuiWriter
|
// Create agent with RetroTuiWriter
|
||||||
let ui_writer = RetroTuiWriter::new(tui.clone());
|
let ui_writer = RetroTuiWriter::new(tui.clone());
|
||||||
@@ -266,10 +281,10 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
|
|||||||
KeyCode::Right => {
|
KeyCode::Right => {
|
||||||
tui.cursor_right();
|
tui.cursor_right();
|
||||||
}
|
}
|
||||||
KeyCode::Home => {
|
KeyCode::Home if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
tui.cursor_home();
|
tui.cursor_home();
|
||||||
}
|
}
|
||||||
KeyCode::End => {
|
KeyCode::End if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
tui.cursor_end();
|
tui.cursor_end();
|
||||||
}
|
}
|
||||||
KeyCode::Delete => {
|
KeyCode::Delete => {
|
||||||
|
|||||||
@@ -18,16 +18,9 @@ use std::time::{Duration, Instant};
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
// Retro sci-fi color scheme inspired by Alien terminals
|
use crate::theme::ColorTheme;
|
||||||
const TERMINAL_GREEN: Color = Color::Rgb(136, 244, 152); // Mid green
|
|
||||||
const TERMINAL_AMBER: Color = Color::Rgb(242, 204, 148); // Softer amber for warnings
|
// Color theme will be loaded dynamically
|
||||||
const TERMINAL_DIM_GREEN: Color = Color::Rgb(154, 174, 135); // softer vintage green for borders
|
|
||||||
const TERMINAL_BG: Color = Color::Rgb(0, 10, 0); // Very dark green background
|
|
||||||
const TERMINAL_CYAN: Color = Color::Rgb(0, 255, 255); // Cyan for highlights
|
|
||||||
const TERMINAL_RED: Color = Color::Rgb(239, 119, 109); // Red for errors or negative diffs
|
|
||||||
const TERMINAL_PALE_BLUE: Color = Color::Rgb(173, 234, 251); // Pale blue for READY status
|
|
||||||
const TERMINAL_DARK_AMBER: Color = Color::Rgb(204, 119, 34); // Dark amber for PROCESSING status
|
|
||||||
const TERMINAL_WHITE: Color = Color::Rgb(218, 218, 219); // Dimmer white for punchy text
|
|
||||||
|
|
||||||
// Scrolling configuration
|
// Scrolling configuration
|
||||||
const SCROLL_PAST_END_BUFFER: usize = 10; // Extra lines to allow scrolling past the end
|
const SCROLL_PAST_END_BUFFER: usize = 10; // Extra lines to allow scrolling past the end
|
||||||
@@ -64,6 +57,8 @@ pub enum TuiMessage {
|
|||||||
|
|
||||||
/// Shared state for the retro terminal
|
/// Shared state for the retro terminal
|
||||||
struct TerminalState {
|
struct TerminalState {
|
||||||
|
/// Color theme
|
||||||
|
theme: ColorTheme,
|
||||||
/// Current input buffer
|
/// Current input buffer
|
||||||
input_buffer: String,
|
input_buffer: String,
|
||||||
/// Cursor position in input buffer (for editing)
|
/// Cursor position in input buffer (for editing)
|
||||||
@@ -119,8 +114,9 @@ struct TerminalState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalState {
|
impl TerminalState {
|
||||||
fn new() -> Self {
|
fn new(theme: ColorTheme) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
theme,
|
||||||
input_buffer: String::new(),
|
input_buffer: String::new(),
|
||||||
cursor_position: 0,
|
cursor_position: 0,
|
||||||
output_history: vec![
|
output_history: vec![
|
||||||
@@ -332,7 +328,7 @@ pub struct RetroTui {
|
|||||||
|
|
||||||
impl RetroTui {
|
impl RetroTui {
|
||||||
/// Create and start the retro terminal UI
|
/// Create and start the retro terminal UI
|
||||||
pub async fn start() -> Result<Self> {
|
pub async fn start(theme: ColorTheme) -> Result<Self> {
|
||||||
// Setup terminal
|
// Setup terminal
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
@@ -343,7 +339,7 @@ impl RetroTui {
|
|||||||
// Create message channel
|
// Create message channel
|
||||||
let (tx, mut rx) = mpsc::unbounded_channel::<TuiMessage>();
|
let (tx, mut rx) = mpsc::unbounded_channel::<TuiMessage>();
|
||||||
|
|
||||||
let state = Arc::new(Mutex::new(TerminalState::new()));
|
let state = Arc::new(Mutex::new(TerminalState::new(theme)));
|
||||||
let terminal = Arc::new(Mutex::new(terminal));
|
let terminal = Arc::new(Mutex::new(terminal));
|
||||||
|
|
||||||
// Clone for the background task
|
// Clone for the background task
|
||||||
@@ -575,16 +571,16 @@ impl RetroTui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw header/input area
|
// Draw header/input area
|
||||||
Self::draw_input_area(f, chunks[0], &state.input_buffer, state.cursor_position, state.cursor_blink, state.is_processing);
|
Self::draw_input_area(f, chunks[0], &state.input_buffer, state.cursor_position, state.cursor_blink, state.is_processing, &state.theme);
|
||||||
|
|
||||||
// Draw main output area
|
// Draw main output area
|
||||||
Self::draw_output_area(f, chunks[1], &state.output_history, state.scroll_offset);
|
Self::draw_output_area(f, chunks[1], &state.output_history, state.scroll_offset, &state.theme);
|
||||||
|
|
||||||
// Draw activity area only if it's visible (during animation or when shown)
|
// Draw activity area only if it's visible (during animation or when shown)
|
||||||
if activity_height > 0 {
|
if activity_height > 0 {
|
||||||
// Apply fade effect by adjusting opacity through color intensity
|
// Apply fade effect by adjusting opacity through color intensity
|
||||||
let opacity = state.activity_animation;
|
let opacity = state.activity_animation;
|
||||||
Self::draw_activity_area(f, chunks[2], state, opacity);
|
Self::draw_activity_area(f, chunks[2], state, opacity, &state.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw status bar - use the last chunk which is either index 2 or 3
|
// Draw status bar - use the last chunk which is either index 2 or 3
|
||||||
@@ -600,6 +596,7 @@ impl RetroTui {
|
|||||||
state.context_info,
|
state.context_info,
|
||||||
&state.provider_info,
|
&state.provider_info,
|
||||||
state.status_blink,
|
state.status_blink,
|
||||||
|
&state.theme,
|
||||||
);
|
);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -607,7 +604,7 @@ impl RetroTui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the input area with prompt
|
/// Draw the input area with prompt
|
||||||
fn draw_input_area(f: &mut Frame, area: Rect, input_buffer: &str, cursor_position: usize, cursor_blink: bool, is_processing: bool) {
|
fn draw_input_area(f: &mut Frame, area: Rect, input_buffer: &str, cursor_position: usize, cursor_blink: bool, is_processing: bool, theme: &ColorTheme) {
|
||||||
let prompt = "g3> ";
|
let prompt = "g3> ";
|
||||||
let prompt_len = prompt.len();
|
let prompt_len = prompt.len();
|
||||||
|
|
||||||
@@ -664,14 +661,14 @@ impl RetroTui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let input = Paragraph::new(display_text)
|
let input = Paragraph::new(display_text)
|
||||||
.style(Style::default().fg(TERMINAL_GREEN))
|
.style(Style::default().fg(theme.terminal_green.to_color()))
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.title(" COMMAND INPUT ")
|
.title(" COMMAND INPUT ")
|
||||||
.title_alignment(Alignment::Center)
|
.title_alignment(Alignment::Center)
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(TERMINAL_DIM_GREEN))
|
.border_style(Style::default().fg(theme.terminal_dim_green.to_color()))
|
||||||
.style(Style::default().bg(TERMINAL_BG)),
|
.style(Style::default().bg(theme.terminal_bg.to_color())),
|
||||||
);
|
);
|
||||||
|
|
||||||
f.render_widget(input, area);
|
f.render_widget(input, area);
|
||||||
@@ -683,6 +680,7 @@ impl RetroTui {
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
output_history: &[String],
|
output_history: &[String],
|
||||||
scroll_offset: usize,
|
scroll_offset: usize,
|
||||||
|
theme: &ColorTheme,
|
||||||
) {
|
) {
|
||||||
// Calculate visible lines (no borders now, but padding takes 2 lines)
|
// Calculate visible lines (no borders now, but padding takes 2 lines)
|
||||||
let visible_height = area.height.saturating_sub(2) as usize; // Account for padding
|
let visible_height = area.height.saturating_sub(2) as usize; // Account for padding
|
||||||
@@ -719,7 +717,7 @@ impl RetroTui {
|
|||||||
return Line::from(Span::styled(
|
return Line::from(Span::styled(
|
||||||
format!(" {}", cleaned),
|
format!(" {}", cleaned),
|
||||||
Style::default()
|
Style::default()
|
||||||
.bg(TERMINAL_AMBER)
|
.bg(theme.terminal_amber.to_color())
|
||||||
.fg(Color::Black)
|
.fg(Color::Black)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
));
|
));
|
||||||
@@ -730,7 +728,7 @@ impl RetroTui {
|
|||||||
return Line::from(Span::styled(
|
return Line::from(Span::styled(
|
||||||
format!(" {}", cleaned),
|
format!(" {}", cleaned),
|
||||||
Style::default()
|
Style::default()
|
||||||
.bg(TERMINAL_GREEN)
|
.bg(theme.terminal_green.to_color())
|
||||||
.fg(Color::Black)
|
.fg(Color::Black)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
));
|
));
|
||||||
@@ -741,7 +739,7 @@ impl RetroTui {
|
|||||||
return Line::from(Span::styled(
|
return Line::from(Span::styled(
|
||||||
format!(" {}", cleaned),
|
format!(" {}", cleaned),
|
||||||
Style::default()
|
Style::default()
|
||||||
.bg(TERMINAL_RED)
|
.bg(theme.terminal_red.to_color())
|
||||||
.fg(Color::Black)
|
.fg(Color::Black)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
));
|
));
|
||||||
@@ -755,31 +753,31 @@ impl RetroTui {
|
|||||||
{
|
{
|
||||||
return Line::from(Span::styled(
|
return Line::from(Span::styled(
|
||||||
format!(" {}", line),
|
format!(" {}", line),
|
||||||
Style::default().fg(TERMINAL_DIM_GREEN),
|
Style::default().fg(theme.terminal_dim_green.to_color()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Apply different colors based on content
|
// Apply different colors based on content
|
||||||
let style = if line.starts_with("ERROR:") {
|
let style = if line.starts_with("ERROR:") {
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(TERMINAL_RED)
|
.fg(theme.terminal_red.to_color())
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
} else if line.starts_with('>') {
|
} else if line.starts_with('>') {
|
||||||
Style::default().fg(TERMINAL_CYAN)
|
Style::default().fg(theme.terminal_cyan.to_color())
|
||||||
} else if line.starts_with("SYSTEM:")
|
} else if line.starts_with("SYSTEM:")
|
||||||
|| line.starts_with("WEYLAND")
|
|| line.starts_with("WEYLAND")
|
||||||
|| line.starts_with("MU/TH/UR")
|
|| line.starts_with("MU/TH/UR")
|
||||||
{
|
{
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(TERMINAL_AMBER)
|
.fg(theme.terminal_amber.to_color())
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
} else if line.starts_with("SYSTEM INITIALIZED")
|
} else if line.starts_with("SYSTEM INITIALIZED")
|
||||||
|| line.starts_with("AWAITING COMMAND")
|
|| line.starts_with("AWAITING COMMAND")
|
||||||
{
|
{
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(TERMINAL_DIM_GREEN)
|
.fg(theme.terminal_dim_green.to_color())
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(TERMINAL_GREEN)
|
Style::default().fg(theme.terminal_green.to_color())
|
||||||
};
|
};
|
||||||
|
|
||||||
Line::from(Span::styled(format!(" {}", line), style))
|
Line::from(Span::styled(format!(" {}", line), style))
|
||||||
@@ -793,7 +791,7 @@ impl RetroTui {
|
|||||||
.borders(Borders::NONE)
|
.borders(Borders::NONE)
|
||||||
// Add padding to maintain the same spacing as borders would provide
|
// Add padding to maintain the same spacing as borders would provide
|
||||||
.padding(ratatui::widgets::Padding::new(1, 1, 1, 1))
|
.padding(ratatui::widgets::Padding::new(1, 1, 1, 1))
|
||||||
.style(Style::default().bg(TERMINAL_BG)),
|
.style(Style::default().bg(theme.terminal_bg.to_color())),
|
||||||
)
|
)
|
||||||
.wrap(Wrap { trim: false });
|
.wrap(Wrap { trim: false });
|
||||||
|
|
||||||
@@ -806,7 +804,7 @@ impl RetroTui {
|
|||||||
.end_symbol(Some("▼"))
|
.end_symbol(Some("▼"))
|
||||||
.track_symbol(Some("│"))
|
.track_symbol(Some("│"))
|
||||||
.thumb_symbol("█")
|
.thumb_symbol("█")
|
||||||
.style(Style::default().fg(TERMINAL_DIM_GREEN));
|
.style(Style::default().fg(theme.terminal_dim_green.to_color()));
|
||||||
|
|
||||||
let mut scrollbar_state = ScrollbarState::new(total_lines)
|
let mut scrollbar_state = ScrollbarState::new(total_lines)
|
||||||
.position(scroll)
|
.position(scroll)
|
||||||
@@ -829,6 +827,7 @@ impl RetroTui {
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
state: &TerminalState,
|
state: &TerminalState,
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
|
theme: &ColorTheme,
|
||||||
) {
|
) {
|
||||||
// Note: scroll_offset is managed by the state and auto-scrolls to show latest content when new data arrives
|
// Note: scroll_offset is managed by the state and auto-scrolls to show latest content when new data arrives
|
||||||
|
|
||||||
@@ -870,7 +869,7 @@ impl RetroTui {
|
|||||||
let visible_lines: Vec<Line> = if state.tool_activity.is_empty() {
|
let visible_lines: Vec<Line> = if state.tool_activity.is_empty() {
|
||||||
vec![Line::from(Span::styled(
|
vec![Line::from(Span::styled(
|
||||||
" No tool activity yet",
|
" No tool activity yet",
|
||||||
Style::default().fg(fade_color(TERMINAL_DIM_GREEN)).add_modifier(Modifier::ITALIC),
|
Style::default().fg(fade_color(theme.terminal_dim_green.to_color())).add_modifier(Modifier::ITALIC),
|
||||||
))]
|
))]
|
||||||
} else {
|
} else {
|
||||||
state.tool_activity
|
state.tool_activity
|
||||||
@@ -880,11 +879,11 @@ impl RetroTui {
|
|||||||
.map(|line| {
|
.map(|line| {
|
||||||
// Style the header lines differently
|
// Style the header lines differently
|
||||||
let style = if line.starts_with('[') && line.contains(']') {
|
let style = if line.starts_with('[') && line.contains(']') {
|
||||||
Style::default().fg(fade_color(TERMINAL_CYAN)).add_modifier(Modifier::BOLD)
|
Style::default().fg(fade_color(theme.terminal_cyan.to_color())).add_modifier(Modifier::BOLD)
|
||||||
} else if line.is_empty() {
|
} else if line.is_empty() {
|
||||||
Style::default()
|
Style::default()
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(fade_color(TERMINAL_GREEN))
|
Style::default().fg(fade_color(theme.terminal_green.to_color()))
|
||||||
};
|
};
|
||||||
Line::from(Span::styled(format!(" {}", line), style))
|
Line::from(Span::styled(format!(" {}", line), style))
|
||||||
})
|
})
|
||||||
@@ -897,15 +896,15 @@ impl RetroTui {
|
|||||||
.title(" TOOL DETAIL ")
|
.title(" TOOL DETAIL ")
|
||||||
.title_alignment(Alignment::Center)
|
.title_alignment(Alignment::Center)
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(fade_color(TERMINAL_DIM_GREEN)))
|
.border_style(Style::default().fg(fade_color(theme.terminal_dim_green.to_color())))
|
||||||
.style(Style::default().bg(TERMINAL_BG)),
|
.style(Style::default().bg(theme.terminal_bg.to_color())),
|
||||||
)
|
)
|
||||||
.wrap(Wrap { trim: false });
|
.wrap(Wrap { trim: false });
|
||||||
|
|
||||||
f.render_widget(tool_output, chunks[0]);
|
f.render_widget(tool_output, chunks[0]);
|
||||||
|
|
||||||
// Draw right half - Activity graphs with wave animations
|
// Draw right half - Activity graphs with wave animations
|
||||||
Self::draw_activity_graphs(f, chunks[1], &state.token_wave_history, &state.sse_wave_history, opacity);
|
Self::draw_activity_graphs(f, chunks[1], &state.token_wave_history, &state.sse_wave_history, opacity, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw activity graphs with wave animations for tokens and SSEs
|
/// Draw activity graphs with wave animations for tokens and SSEs
|
||||||
@@ -915,6 +914,7 @@ impl RetroTui {
|
|||||||
token_wave: &VecDeque<f64>,
|
token_wave: &VecDeque<f64>,
|
||||||
sse_wave: &VecDeque<f64>,
|
sse_wave: &VecDeque<f64>,
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
|
theme: &ColorTheme,
|
||||||
) {
|
) {
|
||||||
// Apply fade effect by adjusting colors based on opacity
|
// Apply fade effect by adjusting colors based on opacity
|
||||||
let fade_color = |color: Color| -> Color {
|
let fade_color = |color: Color| -> Color {
|
||||||
@@ -934,8 +934,8 @@ impl RetroTui {
|
|||||||
.title(" ACTIVITY ")
|
.title(" ACTIVITY ")
|
||||||
.title_alignment(Alignment::Center)
|
.title_alignment(Alignment::Center)
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(fade_color(TERMINAL_DIM_GREEN)))
|
.border_style(Style::default().fg(fade_color(theme.terminal_dim_green.to_color())))
|
||||||
.style(Style::default().bg(TERMINAL_BG));
|
.style(Style::default().bg(theme.terminal_bg.to_color()));
|
||||||
|
|
||||||
// Calculate inner area for chart
|
// Calculate inner area for chart
|
||||||
let inner = block.inner(area);
|
let inner = block.inner(area);
|
||||||
@@ -963,8 +963,8 @@ impl RetroTui {
|
|||||||
graph_chunks[0],
|
graph_chunks[0],
|
||||||
token_wave,
|
token_wave,
|
||||||
"TOKENS",
|
"TOKENS",
|
||||||
fade_color(TERMINAL_CYAN),
|
fade_color(theme.terminal_cyan.to_color()),
|
||||||
fade_color(TERMINAL_DIM_GREEN),
|
fade_color(theme.terminal_dim_green.to_color()),
|
||||||
opacity,
|
opacity,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -974,8 +974,8 @@ impl RetroTui {
|
|||||||
graph_chunks[1],
|
graph_chunks[1],
|
||||||
sse_wave,
|
sse_wave,
|
||||||
"SSE",
|
"SSE",
|
||||||
fade_color(TERMINAL_GREEN),
|
fade_color(theme.terminal_green.to_color()),
|
||||||
fade_color(TERMINAL_DIM_GREEN),
|
fade_color(theme.terminal_dim_green.to_color()),
|
||||||
opacity,
|
opacity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -987,7 +987,7 @@ impl RetroTui {
|
|||||||
wave_data: &VecDeque<f64>,
|
wave_data: &VecDeque<f64>,
|
||||||
label: &str,
|
label: &str,
|
||||||
wave_color: Color,
|
wave_color: Color,
|
||||||
axis_color: Color,
|
_axis_color: Color,
|
||||||
_opacity: f32,
|
_opacity: f32,
|
||||||
) {
|
) {
|
||||||
let width = area.width as usize;
|
let width = area.width as usize;
|
||||||
@@ -1038,6 +1038,7 @@ impl RetroTui {
|
|||||||
context_info: (u32, u32, f32),
|
context_info: (u32, u32, f32),
|
||||||
provider_info: &(String, String),
|
provider_info: &(String, String),
|
||||||
status_blink: bool,
|
status_blink: bool,
|
||||||
|
theme: &ColorTheme,
|
||||||
) {
|
) {
|
||||||
let (used, total, percentage) = context_info;
|
let (used, total, percentage) = context_info;
|
||||||
|
|
||||||
@@ -1052,15 +1053,15 @@ impl RetroTui {
|
|||||||
let (status_color, status_text) = if status_line == "PROCESSING" {
|
let (status_color, status_text) = if status_line == "PROCESSING" {
|
||||||
// Blink the PROCESSING status
|
// Blink the PROCESSING status
|
||||||
if status_blink {
|
if status_blink {
|
||||||
(TERMINAL_DARK_AMBER, status_line)
|
(theme.terminal_dark_amber.to_color(), status_line)
|
||||||
} else {
|
} else {
|
||||||
(TERMINAL_BG, " ") // Hide text by matching background
|
(theme.terminal_bg.to_color(), " ") // Hide text by matching background
|
||||||
}
|
}
|
||||||
} else if status_line == "READY" {
|
} else if status_line == "READY" {
|
||||||
(TERMINAL_PALE_BLUE, status_line)
|
(theme.terminal_pale_blue.to_color(), status_line)
|
||||||
} else {
|
} else {
|
||||||
// Default to amber for other statuses
|
// Default to amber for other statuses
|
||||||
(TERMINAL_AMBER, status_line)
|
(theme.terminal_amber.to_color(), status_line)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build the status line with different colored spans
|
// Build the status line with different colored spans
|
||||||
@@ -1068,7 +1069,7 @@ impl RetroTui {
|
|||||||
Span::styled(
|
Span::styled(
|
||||||
" STATUS: ",
|
" STATUS: ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(TERMINAL_AMBER)
|
.fg(theme.terminal_amber.to_color())
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
@@ -1080,25 +1081,25 @@ impl RetroTui {
|
|||||||
Span::styled(
|
Span::styled(
|
||||||
" | CONTEXT: ",
|
" | CONTEXT: ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(TERMINAL_AMBER)
|
.fg(theme.terminal_amber.to_color())
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{} {:.1}% ({}/{})", meter, percentage, used, total),
|
format!("{} {:.1}% ({}/{})", meter, percentage, used, total),
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(TERMINAL_AMBER)
|
.fg(theme.terminal_amber.to_color())
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
" | ",
|
" | ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(TERMINAL_AMBER)
|
.fg(theme.terminal_amber.to_color())
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{} ", model),
|
format!("{} ", model),
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(TERMINAL_AMBER)
|
.fg(theme.terminal_amber.to_color())
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -1106,7 +1107,7 @@ impl RetroTui {
|
|||||||
let status_line = Line::from(status_spans);
|
let status_line = Line::from(status_spans);
|
||||||
|
|
||||||
let status = Paragraph::new(status_line)
|
let status = Paragraph::new(status_line)
|
||||||
.style(Style::default().bg(TERMINAL_BG))
|
.style(Style::default().bg(theme.terminal_bg.to_color()))
|
||||||
.alignment(Alignment::Left);
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
f.render_widget(status, area);
|
f.render_widget(status, area);
|
||||||
|
|||||||
185
crates/g3-cli/src/theme.rs
Normal file
185
crates/g3-cli/src/theme.rs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
use ratatui::style::Color;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
/// Color theme configuration for the retro TUI
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ColorTheme {
|
||||||
|
/// Name of the theme
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// Main terminal text color (for general output)
|
||||||
|
pub terminal_green: ColorValue,
|
||||||
|
|
||||||
|
/// Warning/system messages color
|
||||||
|
pub terminal_amber: ColorValue,
|
||||||
|
|
||||||
|
/// Border and dim text color
|
||||||
|
pub terminal_dim_green: ColorValue,
|
||||||
|
|
||||||
|
/// Background color
|
||||||
|
pub terminal_bg: ColorValue,
|
||||||
|
|
||||||
|
/// Highlight/emphasis color
|
||||||
|
pub terminal_cyan: ColorValue,
|
||||||
|
|
||||||
|
/// Error/negative diff color
|
||||||
|
pub terminal_red: ColorValue,
|
||||||
|
|
||||||
|
/// READY status color
|
||||||
|
pub terminal_pale_blue: ColorValue,
|
||||||
|
|
||||||
|
/// PROCESSING status color
|
||||||
|
pub terminal_dark_amber: ColorValue,
|
||||||
|
|
||||||
|
/// Bright/punchy text color
|
||||||
|
pub terminal_white: ColorValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a color value that can be serialized/deserialized
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ColorValue {
|
||||||
|
/// RGB color with r, g, b components
|
||||||
|
Rgb { r: u8, g: u8, b: u8 },
|
||||||
|
/// Named color
|
||||||
|
Named(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorValue {
|
||||||
|
/// Convert to ratatui Color
|
||||||
|
pub fn to_color(&self) -> Color {
|
||||||
|
match self {
|
||||||
|
ColorValue::Rgb { r, g, b } => Color::Rgb(*r, *g, *b),
|
||||||
|
ColorValue::Named(name) => match name.to_lowercase().as_str() {
|
||||||
|
"black" => Color::Black,
|
||||||
|
"red" => Color::Red,
|
||||||
|
"green" => Color::Green,
|
||||||
|
"yellow" => Color::Yellow,
|
||||||
|
"blue" => Color::Blue,
|
||||||
|
"magenta" => Color::Magenta,
|
||||||
|
"cyan" => Color::Cyan,
|
||||||
|
"gray" | "grey" => Color::Gray,
|
||||||
|
"darkgray" | "darkgrey" => Color::DarkGray,
|
||||||
|
"lightred" => Color::LightRed,
|
||||||
|
"lightgreen" => Color::LightGreen,
|
||||||
|
"lightyellow" => Color::LightYellow,
|
||||||
|
"lightblue" => Color::LightBlue,
|
||||||
|
"lightmagenta" => Color::LightMagenta,
|
||||||
|
"lightcyan" => Color::LightCyan,
|
||||||
|
"white" => Color::White,
|
||||||
|
_ => Color::White, // Default fallback
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorTheme {
|
||||||
|
/// Load a theme from a JSON file
|
||||||
|
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
|
let content = fs::read_to_string(path)?;
|
||||||
|
let theme: ColorTheme = serde_json::from_str(&content)?;
|
||||||
|
Ok(theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save a theme to a JSON file
|
||||||
|
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
||||||
|
let content = serde_json::to_string_pretty(self)?;
|
||||||
|
fs::write(path, content)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default retro sci-fi theme (inspired by Alien terminals)
|
||||||
|
pub fn default() -> Self {
|
||||||
|
ColorTheme {
|
||||||
|
name: "Retro Sci-Fi".to_string(),
|
||||||
|
terminal_green: ColorValue::Rgb { r: 136, g: 244, b: 152 },
|
||||||
|
terminal_amber: ColorValue::Rgb { r: 242, g: 204, b: 148 },
|
||||||
|
terminal_dim_green: ColorValue::Rgb { r: 154, g: 174, b: 135 },
|
||||||
|
terminal_bg: ColorValue::Rgb { r: 0, g: 10, b: 0 },
|
||||||
|
terminal_cyan: ColorValue::Rgb { r: 0, g: 255, b: 255 },
|
||||||
|
terminal_red: ColorValue::Rgb { r: 239, g: 119, b: 109 },
|
||||||
|
terminal_pale_blue: ColorValue::Rgb { r: 173, g: 234, b: 251 },
|
||||||
|
terminal_dark_amber: ColorValue::Rgb { r: 204, g: 119, b: 34 },
|
||||||
|
terminal_white: ColorValue::Rgb { r: 218, g: 218, b: 219 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the Dracula theme
|
||||||
|
pub fn dracula() -> Self {
|
||||||
|
ColorTheme {
|
||||||
|
name: "Dracula".to_string(),
|
||||||
|
terminal_green: ColorValue::Rgb { r: 80, g: 250, b: 123 }, // Dracula green
|
||||||
|
terminal_amber: ColorValue::Rgb { r: 255, g: 184, b: 108 }, // Dracula orange
|
||||||
|
terminal_dim_green: ColorValue::Rgb { r: 98, g: 114, b: 164 }, // Dracula comment
|
||||||
|
terminal_bg: ColorValue::Rgb { r: 40, g: 42, b: 54 }, // Dracula background
|
||||||
|
terminal_cyan: ColorValue::Rgb { r: 139, g: 233, b: 253 }, // Dracula cyan
|
||||||
|
terminal_red: ColorValue::Rgb { r: 255, g: 85, b: 85 }, // Dracula red
|
||||||
|
terminal_pale_blue: ColorValue::Rgb { r: 189, g: 147, b: 249 }, // Dracula purple
|
||||||
|
terminal_dark_amber: ColorValue::Rgb { r: 255, g: 121, b: 198 }, // Dracula pink
|
||||||
|
terminal_white: ColorValue::Rgb { r: 248, g: 248, b: 242 }, // Dracula foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a theme by name or from file
|
||||||
|
pub fn load(theme_name: Option<&str>) -> Result<Self> {
|
||||||
|
match theme_name {
|
||||||
|
None => Ok(Self::default()),
|
||||||
|
Some("default") | Some("retro") => Ok(Self::default()),
|
||||||
|
Some("dracula") => Ok(Self::dracula()),
|
||||||
|
Some(path) => {
|
||||||
|
// Try to load from file
|
||||||
|
if Path::new(path).exists() {
|
||||||
|
Self::from_file(path)
|
||||||
|
} else {
|
||||||
|
// Try to find in standard locations
|
||||||
|
let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
|
||||||
|
let theme_file = home.join(".config").join("g3").join("themes").join(format!("{}.json", path));
|
||||||
|
if theme_file.exists() {
|
||||||
|
Self::from_file(theme_file)
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Theme '{}' not found", path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create example theme files in the user's config directory
|
||||||
|
pub fn create_example_themes() -> Result<()> {
|
||||||
|
let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
|
||||||
|
let themes_dir = home.join(".config").join("g3").join("themes");
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
fs::create_dir_all(&themes_dir)?;
|
||||||
|
|
||||||
|
// Save default theme
|
||||||
|
let default_theme = ColorTheme::default();
|
||||||
|
default_theme.to_file(themes_dir.join("retro.json"))?;
|
||||||
|
|
||||||
|
// Save Dracula theme
|
||||||
|
let dracula_theme = ColorTheme::dracula();
|
||||||
|
dracula_theme.to_file(themes_dir.join("dracula.json"))?;
|
||||||
|
|
||||||
|
// Create a custom example theme (Matrix-inspired)
|
||||||
|
let matrix_theme = ColorTheme {
|
||||||
|
name: "Matrix".to_string(),
|
||||||
|
terminal_green: ColorValue::Rgb { r: 0, g: 255, b: 0 },
|
||||||
|
terminal_amber: ColorValue::Rgb { r: 0, g: 200, b: 0 },
|
||||||
|
terminal_dim_green: ColorValue::Rgb { r: 0, g: 100, b: 0 },
|
||||||
|
terminal_bg: ColorValue::Rgb { r: 0, g: 0, b: 0 },
|
||||||
|
terminal_cyan: ColorValue::Rgb { r: 0, g: 255, b: 128 },
|
||||||
|
terminal_red: ColorValue::Rgb { r: 255, g: 0, b: 0 },
|
||||||
|
terminal_pale_blue: ColorValue::Rgb { r: 0, g: 255, b: 200 },
|
||||||
|
terminal_dark_amber: ColorValue::Rgb { r: 0, g: 150, b: 0 },
|
||||||
|
terminal_white: ColorValue::Rgb { r: 200, g: 255, b: 200 },
|
||||||
|
};
|
||||||
|
matrix_theme.to_file(themes_dir.join("matrix.json"))?;
|
||||||
|
|
||||||
|
println!("Example theme files created in: {}", themes_dir.display());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user