Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ In the age of AI-assisted development, **tokens are the new energy**. They power
- **Interactive TUI Mode** - Beautiful terminal UI powered by Ratatui (default mode)
- 4 interactive views: Overview, Models, Daily, Stats
- Keyboard & mouse navigation
- GitHub-style contribution graph with 9 color themes
- GitHub-style contribution graph with 15 color themes
- Real-time filtering and sorting
- Zero flicker rendering
- **Multi-platform support** - Track usage across OpenCode, Claude Code, Codex CLI, Cursor IDE, Gemini CLI, Amp, Droid, OpenClaw, Pi, Kimi CLI, Qwen CLI, Roo Code, Kilo, Mux, and Synthetic
Expand Down Expand Up @@ -238,12 +238,12 @@ The interactive TUI mode provides:
- `c/d/t`: Sort by cost/date/tokens
- `s`: Open source picker dialog
- `g`: Open group-by picker dialog (model, client+model, client+provider+model)
- `p`: Cycle through 9 color themes
- `p`: Cycle through 15 color themes
- `r`: Refresh data
- `e`: Export to JSON
- `q`: Quit
- **Mouse Support**: Click tabs, buttons, and filters
- **Themes**: Green, Halloween, Teal, Blue, Pink, Purple, Orange, Monochrome, YlGnBu
- **Themes**: Green, Halloween, Teal, Blue, Pink, Purple, Orange, Monochrome, YlGnBu, Gtuvbox, Tokyo Night, Catppuccin, Solarized, Gruvbox, One Dark
- **Settings Persistence**: Preferences saved to `~/.config/tokscale/settings.json` (see [Configuration](#configuration))

### Group-By Strategies
Expand Down Expand Up @@ -480,7 +480,7 @@ Tokscale stores settings in `~/.config/tokscale/settings.json`:

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `colorPalette` | string | `"blue"` | TUI color theme (green, halloween, teal, blue, pink, purple, orange, monochrome, ylgnbu) |
| `colorPalette` | string | `"blue"` | TUI color theme (`green`, `halloween`, `teal`, `blue`, `pink`, `purple`, `orange`, `monochrome`, `ylgnbu`, `gtuvbox`, `tokyo-night`, `catppuccin`, `solarized`, `gruvbox`, `one-dark`) |
| `includeUnusedModels` | boolean | `false` | Show models with zero tokens in reports |
| `autoRefreshEnabled` | boolean | `false` | Enable auto-refresh in TUI |
| `autoRefreshMs` | number | `60000` | Auto-refresh interval (30000-3600000ms) |
Expand Down
24 changes: 18 additions & 6 deletions crates/tokscale-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ struct Cli {
#[command(subcommand)]
command: Option<Commands>,

#[arg(short, long, default_value = "blue")]
theme: String,
#[arg(short, long)]
theme: Option<String>,

#[arg(short, long, default_value = "0")]
refresh: u64,
Expand Down Expand Up @@ -582,7 +582,7 @@ fn main() -> Result<()> {
)
} else {
tui::run(
&cli.theme,
cli.theme.as_deref(),
cli.refresh,
cli.debug,
clients,
Expand Down Expand Up @@ -654,7 +654,7 @@ fn main() -> Result<()> {
)
} else {
tui::run(
&cli.theme,
cli.theme.as_deref(),
cli.refresh,
cli.debug,
clients,
Expand Down Expand Up @@ -765,7 +765,7 @@ fn main() -> Result<()> {
let (since, until) = build_date_filter(today, week, month, since, until);
let year = normalize_year_filter(today, week, month, year);
tui::run(
&cli.theme,
cli.theme.as_deref(),
cli.refresh,
cli.debug,
clients,
Expand Down Expand Up @@ -935,7 +935,7 @@ fn main() -> Result<()> {
)
} else {
tui::run(
&cli.theme,
cli.theme.as_deref(),
cli.refresh,
cli.debug,
clients,
Expand Down Expand Up @@ -3369,6 +3369,18 @@ fn run_headless_command(
mod tests {
use super::*;

#[test]
fn test_cli_theme_is_none_when_not_provided() {
let cli = Cli::parse_from(["tokscale"]);
assert_eq!(cli.theme, None);
}

#[test]
fn test_cli_theme_parses_explicit_value() {
let cli = Cli::parse_from(["tokscale", "--theme", "gtuvbox"]);
assert_eq!(cli.theme, Some("gtuvbox".to_string()));
}

#[test]
fn test_build_client_filter_all_false() {
let flags = ClientFlags {
Expand Down
54 changes: 41 additions & 13 deletions crates/tokscale-cli/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use super::ui::dialog::{ClientPickerDialog, DialogStack};

/// Configuration for TUI initialization
pub struct TuiConfig {
pub theme: String,
pub theme: Option<String>,
pub refresh: u64,
pub sessions_path: Option<String>,
pub clients: Option<Vec<String>>,
Expand Down Expand Up @@ -158,10 +158,7 @@ pub struct App {
impl App {
pub fn new_with_cached_data(config: TuiConfig, cached_data: Option<UsageData>) -> Result<Self> {
let settings = Settings::load();
let theme_name: ThemeName = config
.theme
.parse()
.unwrap_or_else(|_| settings.theme_name());
let theme_name = resolve_theme_name(config.theme.as_deref(), &settings);
let theme = Theme::from_name(theme_name);

let mut enabled_clients = HashSet::new();
Expand Down Expand Up @@ -947,10 +944,18 @@ impl App {
}
}

fn resolve_theme_name(theme_arg: Option<&str>, settings: &Settings) -> ThemeName {
theme_arg
.and_then(|theme| theme.parse().ok())
.unwrap_or_else(|| settings.theme_name())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::tui::data::{ModelUsage, TokenBreakdown};
use crate::tui::settings::Settings;
use crate::tui::themes::ThemeName;

#[test]
fn test_tab_all() {
Expand Down Expand Up @@ -999,10 +1004,33 @@ mod tests {
assert_eq!(Tab::Stats.short_name(), "Sta");
}

#[test]
fn test_resolve_theme_name_uses_settings_when_cli_theme_missing() {
let settings = Settings {
color_palette: "gtuvbox".to_string(),
..Settings::default()
};

assert_eq!(resolve_theme_name(None, &settings), ThemeName::Gtuvbox);
}

#[test]
fn test_resolve_theme_name_prefers_cli_theme_over_settings() {
let settings = Settings {
color_palette: "gtuvbox".to_string(),
..Settings::default()
};

assert_eq!(
resolve_theme_name(Some("orange"), &settings),
ThemeName::Orange
);
}

#[test]
fn test_reset_selection() {
let config = TuiConfig {
theme: "blue".to_string(),
theme: Some("blue".to_string()),
refresh: 0,
sessions_path: None,
clients: None,
Expand All @@ -1027,7 +1055,7 @@ mod tests {
#[test]
fn test_move_selection_up() {
let config = TuiConfig {
theme: "blue".to_string(),
theme: Some("blue".to_string()),
refresh: 0,
sessions_path: None,
clients: None,
Expand Down Expand Up @@ -1070,7 +1098,7 @@ mod tests {
#[test]
fn test_move_selection_down() {
let config = TuiConfig {
theme: "blue".to_string(),
theme: Some("blue".to_string()),
refresh: 0,
sessions_path: None,
clients: None,
Expand Down Expand Up @@ -1113,7 +1141,7 @@ mod tests {
#[test]
fn test_clamp_selection() {
let config = TuiConfig {
theme: "blue".to_string(),
theme: Some("blue".to_string()),
refresh: 0,
sessions_path: None,
clients: None,
Expand Down Expand Up @@ -1150,7 +1178,7 @@ mod tests {
#[test]
fn test_set_sort() {
let config = TuiConfig {
theme: "blue".to_string(),
theme: Some("blue".to_string()),
refresh: 0,
sessions_path: None,
clients: None,
Expand Down Expand Up @@ -1184,7 +1212,7 @@ mod tests {
#[test]
fn test_should_quit() {
let config = TuiConfig {
theme: "blue".to_string(),
theme: Some("blue".to_string()),
refresh: 0,
sessions_path: None,
clients: None,
Expand All @@ -1202,7 +1230,7 @@ mod tests {

fn make_app() -> App {
let config = TuiConfig {
theme: "blue".to_string(),
theme: Some("blue".to_string()),
refresh: 0,
sessions_path: None,
clients: None,
Expand Down Expand Up @@ -1541,7 +1569,7 @@ mod tests {
app.handle_key_event(key(KeyCode::Char('p')));
assert_ne!(app.theme.name, initial_theme);

for _ in 0..8 {
for _ in 0..(ThemeName::all().len() - 1) {
app.handle_key_event(key(KeyCode::Char('p')));
}
assert_eq!(app.theme.name, initial_theme);
Expand Down
4 changes: 2 additions & 2 deletions crates/tokscale-cli/src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use tokscale_core::ClientId;

#[allow(clippy::too_many_arguments)]
pub fn run(
theme: &str,
theme: Option<&str>,
refresh: u64,
debug: bool,
clients: Option<Vec<String>>,
Expand All @@ -54,7 +54,7 @@ pub fn run(
}

let config = TuiConfig {
theme: theme.to_string(),
theme: theme.map(str::to_string),
refresh,
sessions_path: None,
clients: clients.clone(),
Expand Down
Loading