Files
foxhunt/bin/fxt/src/tui/theme.rs
jgrusewski ab101f1110 feat: MCP server mode + TUI cockpit framework
MCP server (bin/fxt/src/mcp/):
- JSON-RPC 2.0 protocol over stdin/stdout
- 32 tool definitions across 11 domains
- McpServer with initialize/tools_list/tools_call handlers
- 21 unit tests

TUI cockpits (bin/fxt/src/tui/):
- Purple/cyan/dark navy theme from design spec
- 6 cockpit views: overview, training, trading, services, risk, data
- Crossterm event loop with key handling (1-6 switch, q quit, ? help)
- CockpitView trait for pluggable cockpit rendering

All 75 tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:48:59 +01:00

141 lines
4.2 KiB
Rust

//! Color theme and style helpers for the Foxhunt TUI.
//!
//! All colors are derived from the Foxhunt brand: purple + cyan on dark navy.
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Borders};
// ---------------------------------------------------------------------------
// Brand palette
// ---------------------------------------------------------------------------
/// Dark navy background (#0F0F23).
pub const BG: Color = Color::Rgb(15, 15, 35);
/// Slightly lighter surface (#1A1A2E).
pub const SURFACE: Color = Color::Rgb(26, 26, 46);
/// Primary text -- light gray (#E0E0E0).
pub const TEXT: Color = Color::Rgb(224, 224, 224);
/// Primary accent -- purple (#8B5CF6).
pub const PRIMARY: Color = Color::Rgb(139, 92, 246);
/// Secondary accent -- cyan (#06B6D4).
pub const SECONDARY: Color = Color::Rgb(6, 182, 212);
/// Success -- green (#10B981).
pub const SUCCESS: Color = Color::Rgb(16, 185, 129);
/// Warning -- amber (#F59E0B).
pub const WARNING: Color = Color::Rgb(245, 158, 11);
/// Error -- red (#EF4444).
pub const ERROR: Color = Color::Rgb(239, 68, 68);
/// Muted text -- gray (#6B7280).
pub const MUTED: Color = Color::Rgb(107, 114, 128);
/// Border -- dim purple (#4C1D95).
pub const BORDER: Color = Color::Rgb(76, 29, 149);
/// Highlight -- bright cyan (#22D3EE).
pub const HIGHLIGHT: Color = Color::Rgb(34, 211, 238);
// ---------------------------------------------------------------------------
// Style helpers
// ---------------------------------------------------------------------------
/// Bold primary-colored text for titles.
pub fn title_style() -> Style {
Style::default().fg(PRIMARY).add_modifier(Modifier::BOLD)
}
/// Dim purple border style.
pub fn border_style() -> Style {
Style::default().fg(BORDER)
}
/// Green for healthy, red for unhealthy.
pub fn status_style(healthy: bool) -> Style {
if healthy {
Style::default().fg(SUCCESS)
} else {
Style::default().fg(ERROR)
}
}
/// Header row style for tables -- secondary accent, bold.
pub fn header_style() -> Style {
Style::default()
.fg(SECONDARY)
.add_modifier(Modifier::BOLD)
}
/// Standard bordered block with a purple title.
pub fn block(title: &str) -> Block<'_> {
Block::default()
.borders(Borders::ALL)
.border_style(border_style())
.title(title.to_owned())
.title_style(title_style())
}
/// Muted text style.
pub fn muted_style() -> Style {
Style::default().fg(MUTED)
}
/// Accent style for highlighted values.
pub fn accent_style() -> Style {
Style::default().fg(HIGHLIGHT)
}
/// Warning style.
pub fn warning_style() -> Style {
Style::default().fg(WARNING)
}
// ---------------------------------------------------------------------------
// Formatting helpers
// ---------------------------------------------------------------------------
/// Format a float as a dollar string with thousand separators and 2 decimal places.
/// Example: `format_usd(1_250_000.0)` => `"$1,250,000.00"`.
pub fn format_usd(value: f64) -> String {
let abs = value.abs();
let integer_part = abs as u64;
let frac = abs - integer_part as f64;
let int_str = format_u64_with_commas(integer_part);
let sign = if value < 0.0 { "-" } else { "" };
format!("{sign}${int_str}.{:02}", (frac * 100.0).round() as u64)
}
/// Format a float as a signed dollar string with thousand separators.
/// Example: `format_usd_signed(-4375.0)` => `"-$4,375.00"`.
pub fn format_usd_signed(value: f64) -> String {
let abs = value.abs();
let integer_part = abs as u64;
let frac = abs - integer_part as f64;
let int_str = format_u64_with_commas(integer_part);
let sign = if value < 0.0 { "-" } else { "+" };
format!("{sign}${int_str}.{:02}", (frac * 100.0).round() as u64)
}
/// Format a u64 with thousand separators.
fn format_u64_with_commas(n: u64) -> String {
let s = n.to_string();
let bytes = s.as_bytes();
let len = bytes.len();
#[allow(clippy::integer_division)]
let cap = len + len / 3;
let mut result = String::with_capacity(cap);
for (i, &b) in bytes.iter().enumerate() {
if i > 0 && (len - i) % 3 == 0 {
result.push(',');
}
result.push(b as char);
}
result
}