color schemes
This commit is contained in:
@@ -12,7 +12,7 @@ tokio = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
serde = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
rustyline = "17.0.1"
|
||||
dirs = "5.0"
|
||||
|
||||
@@ -11,9 +11,11 @@ use tracing::{error, info};
|
||||
mod retro_tui;
|
||||
mod tui;
|
||||
mod ui_writer_impl;
|
||||
mod theme;
|
||||
use retro_tui::RetroTui;
|
||||
use tui::SimpleOutput;
|
||||
use ui_writer_impl::{ConsoleUiWriter, RetroTuiWriter};
|
||||
use theme::ColorTheme;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "g3")]
|
||||
@@ -54,6 +56,10 @@ pub struct Cli {
|
||||
/// Use retro terminal UI (inspired by 80s sci-fi)
|
||||
#[arg(long)]
|
||||
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<()> {
|
||||
@@ -171,7 +177,7 @@ pub async fn run() -> Result<()> {
|
||||
|
||||
if cli.retro {
|
||||
// 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 {
|
||||
// Use standard terminal UI
|
||||
let output = SimpleOutput::new();
|
||||
@@ -183,15 +189,24 @@ pub async fn run() -> Result<()> {
|
||||
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 std::time::Duration;
|
||||
|
||||
// Set environment variable to suppress println in other crates
|
||||
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
|
||||
let tui = RetroTui::start().await?;
|
||||
let tui = RetroTui::start(theme).await?;
|
||||
|
||||
// Create agent with RetroTuiWriter
|
||||
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 => {
|
||||
tui.cursor_right();
|
||||
}
|
||||
KeyCode::Home => {
|
||||
KeyCode::Home if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.cursor_home();
|
||||
}
|
||||
KeyCode::End => {
|
||||
KeyCode::End if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.cursor_end();
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
|
||||
@@ -18,16 +18,9 @@ use std::time::{Duration, Instant};
|
||||
use tokio::sync::mpsc;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
// Retro sci-fi color scheme inspired by Alien terminals
|
||||
const TERMINAL_GREEN: Color = Color::Rgb(136, 244, 152); // Mid green
|
||||
const TERMINAL_AMBER: Color = Color::Rgb(242, 204, 148); // Softer amber for warnings
|
||||
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
|
||||
use crate::theme::ColorTheme;
|
||||
|
||||
// Color theme will be loaded dynamically
|
||||
|
||||
// Scrolling configuration
|
||||
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
|
||||
struct TerminalState {
|
||||
/// Color theme
|
||||
theme: ColorTheme,
|
||||
/// Current input buffer
|
||||
input_buffer: String,
|
||||
/// Cursor position in input buffer (for editing)
|
||||
@@ -119,8 +114,9 @@ struct TerminalState {
|
||||
}
|
||||
|
||||
impl TerminalState {
|
||||
fn new() -> Self {
|
||||
fn new(theme: ColorTheme) -> Self {
|
||||
Self {
|
||||
theme,
|
||||
input_buffer: String::new(),
|
||||
cursor_position: 0,
|
||||
output_history: vec![
|
||||
@@ -332,7 +328,7 @@ pub struct RetroTui {
|
||||
|
||||
impl RetroTui {
|
||||
/// Create and start the retro terminal UI
|
||||
pub async fn start() -> Result<Self> {
|
||||
pub async fn start(theme: ColorTheme) -> Result<Self> {
|
||||
// Setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
@@ -343,7 +339,7 @@ impl RetroTui {
|
||||
// Create message channel
|
||||
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));
|
||||
|
||||
// Clone for the background task
|
||||
@@ -575,16 +571,16 @@ impl RetroTui {
|
||||
}
|
||||
|
||||
// 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
|
||||
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)
|
||||
if activity_height > 0 {
|
||||
// Apply fade effect by adjusting opacity through color intensity
|
||||
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
|
||||
@@ -600,6 +596,7 @@ impl RetroTui {
|
||||
state.context_info,
|
||||
&state.provider_info,
|
||||
state.status_blink,
|
||||
&state.theme,
|
||||
);
|
||||
})?;
|
||||
|
||||
@@ -607,7 +604,7 @@ impl RetroTui {
|
||||
}
|
||||
|
||||
/// 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_len = prompt.len();
|
||||
|
||||
@@ -664,14 +661,14 @@ impl RetroTui {
|
||||
}
|
||||
|
||||
let input = Paragraph::new(display_text)
|
||||
.style(Style::default().fg(TERMINAL_GREEN))
|
||||
.style(Style::default().fg(theme.terminal_green.to_color()))
|
||||
.block(
|
||||
Block::default()
|
||||
.title(" COMMAND INPUT ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(TERMINAL_DIM_GREEN))
|
||||
.style(Style::default().bg(TERMINAL_BG)),
|
||||
.border_style(Style::default().fg(theme.terminal_dim_green.to_color()))
|
||||
.style(Style::default().bg(theme.terminal_bg.to_color())),
|
||||
);
|
||||
|
||||
f.render_widget(input, area);
|
||||
@@ -683,6 +680,7 @@ impl RetroTui {
|
||||
area: Rect,
|
||||
output_history: &[String],
|
||||
scroll_offset: usize,
|
||||
theme: &ColorTheme,
|
||||
) {
|
||||
// Calculate visible lines (no borders now, but padding takes 2 lines)
|
||||
let visible_height = area.height.saturating_sub(2) as usize; // Account for padding
|
||||
@@ -719,7 +717,7 @@ impl RetroTui {
|
||||
return Line::from(Span::styled(
|
||||
format!(" {}", cleaned),
|
||||
Style::default()
|
||||
.bg(TERMINAL_AMBER)
|
||||
.bg(theme.terminal_amber.to_color())
|
||||
.fg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
@@ -730,7 +728,7 @@ impl RetroTui {
|
||||
return Line::from(Span::styled(
|
||||
format!(" {}", cleaned),
|
||||
Style::default()
|
||||
.bg(TERMINAL_GREEN)
|
||||
.bg(theme.terminal_green.to_color())
|
||||
.fg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
@@ -741,7 +739,7 @@ impl RetroTui {
|
||||
return Line::from(Span::styled(
|
||||
format!(" {}", cleaned),
|
||||
Style::default()
|
||||
.bg(TERMINAL_RED)
|
||||
.bg(theme.terminal_red.to_color())
|
||||
.fg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
@@ -755,31 +753,31 @@ impl RetroTui {
|
||||
{
|
||||
return Line::from(Span::styled(
|
||||
format!(" {}", line),
|
||||
Style::default().fg(TERMINAL_DIM_GREEN),
|
||||
Style::default().fg(theme.terminal_dim_green.to_color()),
|
||||
));
|
||||
}
|
||||
// Apply different colors based on content
|
||||
let style = if line.starts_with("ERROR:") {
|
||||
Style::default()
|
||||
.fg(TERMINAL_RED)
|
||||
.fg(theme.terminal_red.to_color())
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if line.starts_with('>') {
|
||||
Style::default().fg(TERMINAL_CYAN)
|
||||
Style::default().fg(theme.terminal_cyan.to_color())
|
||||
} else if line.starts_with("SYSTEM:")
|
||||
|| line.starts_with("WEYLAND")
|
||||
|| line.starts_with("MU/TH/UR")
|
||||
{
|
||||
Style::default()
|
||||
.fg(TERMINAL_AMBER)
|
||||
.fg(theme.terminal_amber.to_color())
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if line.starts_with("SYSTEM INITIALIZED")
|
||||
|| line.starts_with("AWAITING COMMAND")
|
||||
{
|
||||
Style::default()
|
||||
.fg(TERMINAL_DIM_GREEN)
|
||||
.fg(theme.terminal_dim_green.to_color())
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(TERMINAL_GREEN)
|
||||
Style::default().fg(theme.terminal_green.to_color())
|
||||
};
|
||||
|
||||
Line::from(Span::styled(format!(" {}", line), style))
|
||||
@@ -793,7 +791,7 @@ impl RetroTui {
|
||||
.borders(Borders::NONE)
|
||||
// Add padding to maintain the same spacing as borders would provide
|
||||
.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 });
|
||||
|
||||
@@ -806,7 +804,7 @@ impl RetroTui {
|
||||
.end_symbol(Some("▼"))
|
||||
.track_symbol(Some("│"))
|
||||
.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)
|
||||
.position(scroll)
|
||||
@@ -829,6 +827,7 @@ impl RetroTui {
|
||||
area: Rect,
|
||||
state: &TerminalState,
|
||||
opacity: f32,
|
||||
theme: &ColorTheme,
|
||||
) {
|
||||
// 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() {
|
||||
vec![Line::from(Span::styled(
|
||||
" 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 {
|
||||
state.tool_activity
|
||||
@@ -880,11 +879,11 @@ impl RetroTui {
|
||||
.map(|line| {
|
||||
// Style the header lines differently
|
||||
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() {
|
||||
Style::default()
|
||||
} 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))
|
||||
})
|
||||
@@ -897,15 +896,15 @@ impl RetroTui {
|
||||
.title(" TOOL DETAIL ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(fade_color(TERMINAL_DIM_GREEN)))
|
||||
.style(Style::default().bg(TERMINAL_BG)),
|
||||
.border_style(Style::default().fg(fade_color(theme.terminal_dim_green.to_color())))
|
||||
.style(Style::default().bg(theme.terminal_bg.to_color())),
|
||||
)
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
f.render_widget(tool_output, chunks[0]);
|
||||
|
||||
// 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
|
||||
@@ -915,6 +914,7 @@ impl RetroTui {
|
||||
token_wave: &VecDeque<f64>,
|
||||
sse_wave: &VecDeque<f64>,
|
||||
opacity: f32,
|
||||
theme: &ColorTheme,
|
||||
) {
|
||||
// Apply fade effect by adjusting colors based on opacity
|
||||
let fade_color = |color: Color| -> Color {
|
||||
@@ -934,8 +934,8 @@ impl RetroTui {
|
||||
.title(" ACTIVITY ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(fade_color(TERMINAL_DIM_GREEN)))
|
||||
.style(Style::default().bg(TERMINAL_BG));
|
||||
.border_style(Style::default().fg(fade_color(theme.terminal_dim_green.to_color())))
|
||||
.style(Style::default().bg(theme.terminal_bg.to_color()));
|
||||
|
||||
// Calculate inner area for chart
|
||||
let inner = block.inner(area);
|
||||
@@ -963,8 +963,8 @@ impl RetroTui {
|
||||
graph_chunks[0],
|
||||
token_wave,
|
||||
"TOKENS",
|
||||
fade_color(TERMINAL_CYAN),
|
||||
fade_color(TERMINAL_DIM_GREEN),
|
||||
fade_color(theme.terminal_cyan.to_color()),
|
||||
fade_color(theme.terminal_dim_green.to_color()),
|
||||
opacity,
|
||||
);
|
||||
|
||||
@@ -974,8 +974,8 @@ impl RetroTui {
|
||||
graph_chunks[1],
|
||||
sse_wave,
|
||||
"SSE",
|
||||
fade_color(TERMINAL_GREEN),
|
||||
fade_color(TERMINAL_DIM_GREEN),
|
||||
fade_color(theme.terminal_green.to_color()),
|
||||
fade_color(theme.terminal_dim_green.to_color()),
|
||||
opacity,
|
||||
);
|
||||
}
|
||||
@@ -987,7 +987,7 @@ impl RetroTui {
|
||||
wave_data: &VecDeque<f64>,
|
||||
label: &str,
|
||||
wave_color: Color,
|
||||
axis_color: Color,
|
||||
_axis_color: Color,
|
||||
_opacity: f32,
|
||||
) {
|
||||
let width = area.width as usize;
|
||||
@@ -1038,6 +1038,7 @@ impl RetroTui {
|
||||
context_info: (u32, u32, f32),
|
||||
provider_info: &(String, String),
|
||||
status_blink: bool,
|
||||
theme: &ColorTheme,
|
||||
) {
|
||||
let (used, total, percentage) = context_info;
|
||||
|
||||
@@ -1052,15 +1053,15 @@ impl RetroTui {
|
||||
let (status_color, status_text) = if status_line == "PROCESSING" {
|
||||
// Blink the PROCESSING status
|
||||
if status_blink {
|
||||
(TERMINAL_DARK_AMBER, status_line)
|
||||
(theme.terminal_dark_amber.to_color(), status_line)
|
||||
} else {
|
||||
(TERMINAL_BG, " ") // Hide text by matching background
|
||||
(theme.terminal_bg.to_color(), " ") // Hide text by matching background
|
||||
}
|
||||
} else if status_line == "READY" {
|
||||
(TERMINAL_PALE_BLUE, status_line)
|
||||
(theme.terminal_pale_blue.to_color(), status_line)
|
||||
} else {
|
||||
// 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
|
||||
@@ -1068,7 +1069,7 @@ impl RetroTui {
|
||||
Span::styled(
|
||||
" STATUS: ",
|
||||
Style::default()
|
||||
.fg(TERMINAL_AMBER)
|
||||
.fg(theme.terminal_amber.to_color())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
@@ -1080,25 +1081,25 @@ impl RetroTui {
|
||||
Span::styled(
|
||||
" | CONTEXT: ",
|
||||
Style::default()
|
||||
.fg(TERMINAL_AMBER)
|
||||
.fg(theme.terminal_amber.to_color())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
format!("{} {:.1}% ({}/{})", meter, percentage, used, total),
|
||||
Style::default()
|
||||
.fg(TERMINAL_AMBER)
|
||||
.fg(theme.terminal_amber.to_color())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
" | ",
|
||||
Style::default()
|
||||
.fg(TERMINAL_AMBER)
|
||||
.fg(theme.terminal_amber.to_color())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
format!("{} ", model),
|
||||
Style::default()
|
||||
.fg(TERMINAL_AMBER)
|
||||
.fg(theme.terminal_amber.to_color())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
];
|
||||
@@ -1106,7 +1107,7 @@ impl RetroTui {
|
||||
let status_line = Line::from(status_spans);
|
||||
|
||||
let status = Paragraph::new(status_line)
|
||||
.style(Style::default().bg(TERMINAL_BG))
|
||||
.style(Style::default().bg(theme.terminal_bg.to_color()))
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
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