Skip to content
Open
Show file tree
Hide file tree
Changes from all 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