Skip to content
Merged
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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ All notable changes to this project are documented in this file.

## Unreleased

## 0.4.3 - 2026-07-22

- Added a persisted `n` shortcut to cycle display formatting through Classic, System Compact, and System Full for numbers, dates, times, and calendar labels.
- Clarified token pair charts with the `TOKENS (TOTAL / NON-CACHED)` heading.
- Added compact `K/M/B/T` notation for large dashboard token values in System Compact mode, including values inside vertical chart bars. System Full keeps those values expanded, while Classic output remains unchanged.
- Added mouse controls for selecting the visible statistic, chart timeframe, and chart orientation; clicking the footer cycles the display style on every screen.
- Added mouse controls for selecting the visible statistic, chart timeframe, chart orientation, screen, and display style.
- Restored compact one-line control strips on the Usage and Activity screens and highlighted the padded group labels with the chart's darker cyan.
- Removed duplicate shortcut hints from screen headers, leaving the complete shortcut reference in the footer and help overlay.
- Added clickable screen tabs to the outer title and matching framed View/Projects controls to the Activity screen.
- System Full now groups expanded integers with regular spaces while retaining system-localized dates, times, and decimals.
- Added a compact `STYLE CLASS/SCOMP/SFULL` selector after the Usage bar controls and a bottom-border Quit action with a safe Yes/No confirmation dialog; the `q` shortcut opens the same dialog.
- Added the current locale-aware date and time to the `TODAY` card border.
- Vertical chart values now compact only when an individual bar is too narrow, with an exact locale-aware value tooltip on mouse hover.
- Exit confirmation can be disabled from the quit dialog, persists in `state.json`, and can be re-enabled through the confirmed checkbox beside `QUIT`.

## 0.3.6 - 2026-06-05

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "comon"
version = "0.4.2"
version = "0.4.3"
edition = "2021"
license = "Apache-2.0"

Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,12 @@ comon --scan-time-budget-ms 1500 --max-jsonl-line-kib 512
- `w` Toggle timeframe (Week/Month)
- `f` Toggle layout (Horz/Vert)
- `n` Cycle display formatting (Classic/System Compact/System Full)
- Mouse: click the top tabs to switch screens; click Tokens/Time/Runs, Week/Month, Horz/Vert, or Activity project-count controls; click the footer to cycle display formatting
- Mouse: click the top tabs, Usage/Activity controls (including the Usage style selector), or the bottom-right Quit action
- Mouse: hover a filled vertical chart bar to see its exact date and full locale-aware value
- `s` / `F2` Switch between Usage and Session history
- `r` / `F5` Refresh current screen
- `?` Help overlay
- `Esc` / `q` Quit
- `q` Quit (with confirmation)
- `Enter` / `y` Continue past "no sessions found" warning (when shown)
- Session history: `Up` / `Down` / mouse wheel navigate, `Enter` / `Right` open project sessions, `Backspace` / `Left` / `Esc` go back

Expand Down Expand Up @@ -342,7 +343,9 @@ CI also runs this check on each push and pull request via `.github/workflows/asc
- Usage stats are derived from Codex session JSONL logs. If you have no session data yet, values will be empty.
- Limits/credits require Codex App Server to start successfully (auth, environment, and a usable working directory). CoMon auto-detects `codex` on `PATH` and common Windows Codex App bundle locations; use `--codex-bin` or `--app-server-bin` when needed.
- comon stores local app state in `~/.comon/state.json` by default (or `$COMON_HOME`, or `--comon-home`).
- Display formatting starts in Classic mode; press `n` to cycle through Classic, System Compact, and System Full. Both System modes use the operating system locale for numbers, dates, times, and calendar labels; Compact abbreviates dashboard token values, while Full keeps them expanded except for the pre-existing compact summary values. The choice is saved in `state.json` without changing stored data.
- Display formatting starts in Classic mode; press `n` or use `STYLE CLASS/SCOMP/SFULL` in the Usage controls to choose Classic, System Compact, or System Full. Both System modes use the operating system locale for dates, times, decimals, and calendar labels; Compact uses the detected thousands separator and abbreviates dashboard token values, while Full groups expanded integers with regular spaces. The choice is saved in `state.json` without changing stored data.
- Vertical chart labels preserve the selected style when they fit and compact only individual values that exceed their bar width. Hovering a filled bar shows the exact value.
- The quit dialog's `Don't show again` checkbox disables future `q`/`QUIT` confirmations after a confirmed exit. The checkbox beside `QUIT` shows that saved state; clicking it asks before enabling or disabling confirmation.
- comon stores scan cache in `~/.comon/comon.db` to avoid rereading unchanged session files.
- Large session logs are parsed incrementally with persisted parser offsets in `comon.db`; unchanged files are reused from cache.
- If historical days look incomplete after adding old session files, run once with `--full-scan --scan-time-budget-ms 0` to force a full reparse and refresh cached summaries.
Expand Down
Binary file added docs/screenshots/usage-system-full.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
184 changes: 180 additions & 4 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,19 @@ pub(crate) enum ActiveScreen {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UiClickAction {
SetScreen(ActiveScreen),
SetDisplayStyle(DisplayStyle),
SetMetric(UsageMetric),
SetRange(ChartRange),
SetOrientation(ChartOrientation),
DecreaseProjects,
IncreaseProjects,
CycleDisplayStyle,
PromptQuit,
CancelQuit,
ConfirmQuit,
ToggleQuitDontAskAgain,
PromptQuitConfirmationPreference,
ConfirmQuitConfirmationPreference,
CancelQuitConfirmationPreference,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -138,8 +145,13 @@ pub(crate) struct AppState {
pub(crate) workspace_path: Option<std::path::PathBuf>,
pub(crate) no_sessions_confirm_open: bool,
pub(crate) no_sessions_confirm_dismissed: bool,
pub(crate) quit_confirm_open: bool,
pub(crate) quit_dont_ask_again: bool,
pub(crate) skip_quit_confirmation: bool,
pub(crate) quit_preference_prompt: Option<bool>,
pub(crate) display_style: DisplayStyle,
pub(crate) system_locale: SystemLocale,
pub(crate) mouse_position: Option<(u16, u16)>,
pub(crate) ui_hit_targets: Vec<UiHitTarget>,

pub(crate) usage: Option<LocalUsageSnapshot>,
Expand Down Expand Up @@ -177,6 +189,7 @@ struct PersistedUiState {
workspace_path: Option<PathBuf>,
no_sessions_confirm_dismissed: bool,
display_style: DisplayStyle,
skip_quit_confirmation: bool,
}

impl PersistedUiState {
Expand All @@ -189,6 +202,7 @@ impl PersistedUiState {
workspace_path,
no_sessions_confirm_dismissed: false,
display_style: DisplayStyle::Classic,
skip_quit_confirmation: false,
}
}

Expand All @@ -201,6 +215,7 @@ impl PersistedUiState {
workspace_path: state.workspace_path.clone(),
no_sessions_confirm_dismissed: state.no_sessions_confirm_dismissed,
display_style: state.display_style,
skip_quit_confirmation: state.skip_quit_confirmation,
}
}
}
Expand Down Expand Up @@ -231,6 +246,8 @@ struct StoredGlobalState {
orientation: Option<String>,
activity_project_limit: Option<usize>,
display_style: Option<String>,
#[serde(default)]
skip_quit_confirmation: bool,
last_workspace_path: Option<String>,
updated_at: i64,
}
Expand Down Expand Up @@ -479,8 +496,13 @@ async fn run_inner(
workspace_path: restored_ui_state.workspace_path.clone(),
no_sessions_confirm_open: false,
no_sessions_confirm_dismissed: restored_ui_state.no_sessions_confirm_dismissed,
quit_confirm_open: false,
quit_dont_ask_again: false,
skip_quit_confirmation: restored_ui_state.skip_quit_confirmation,
quit_preference_prompt: None,
display_style: restored_ui_state.display_style,
system_locale: config.system_locale.clone(),
mouse_position: None,
ui_hit_targets: Vec::new(),
usage: None,
usage_updated_at: None,
Expand Down Expand Up @@ -603,17 +625,115 @@ fn handle_input_event(
usage_refresh_tx: &mpsc::Sender<()>,
limits_refresh_tx: &mpsc::Sender<()>,
) -> Result<InputOutcome> {
if let Some(desired_skip_confirmation) = state.quit_preference_prompt {
return match event {
Event::Mouse(mouse) if mouse.kind == MouseEventKind::Down(MouseButton::Left) => {
match ui_click_action_at(&state.ui_hit_targets, mouse.column, mouse.row) {
Some(UiClickAction::ConfirmQuitConfirmationPreference) => {
state.skip_quit_confirmation = desired_skip_confirmation;
state.quit_preference_prompt = None;
Ok(InputOutcome::Continue(true))
}
Some(UiClickAction::CancelQuitConfirmationPreference) => {
state.quit_preference_prompt = None;
Ok(InputOutcome::Continue(true))
}
_ => Ok(InputOutcome::Continue(false)),
}
}
Event::Key(key) if key.kind == KeyEventKind::Press => match (key.code, key.modifiers) {
(KeyCode::Char('y'), _) | (KeyCode::Char('Y'), _) => {
state.skip_quit_confirmation = desired_skip_confirmation;
state.quit_preference_prompt = None;
Ok(InputOutcome::Continue(true))
}
(KeyCode::Char('c'), KeyModifiers::CONTROL) => Ok(InputOutcome::Quit),
(KeyCode::Enter, _)
| (KeyCode::Esc, _)
| (KeyCode::Char('n'), _)
| (KeyCode::Char('N'), _)
| (KeyCode::Char('q'), _)
| (KeyCode::Char('Q'), _) => {
state.quit_preference_prompt = None;
Ok(InputOutcome::Continue(true))
}
_ => Ok(InputOutcome::Continue(false)),
},
Event::Resize(_, _) => Ok(InputOutcome::Continue(true)),
_ => Ok(InputOutcome::Continue(false)),
};
}

if state.quit_confirm_open {
return match event {
Event::Mouse(mouse) if mouse.kind == MouseEventKind::Down(MouseButton::Left) => {
match ui_click_action_at(&state.ui_hit_targets, mouse.column, mouse.row) {
Some(UiClickAction::ConfirmQuit) => {
state.skip_quit_confirmation = state.quit_dont_ask_again;
Ok(InputOutcome::Quit)
}
Some(UiClickAction::CancelQuit) => {
state.quit_confirm_open = false;
state.quit_dont_ask_again = false;
Ok(InputOutcome::Continue(true))
}
Some(UiClickAction::ToggleQuitDontAskAgain) => {
state.quit_dont_ask_again = !state.quit_dont_ask_again;
Ok(InputOutcome::Continue(true))
}
_ => Ok(InputOutcome::Continue(false)),
}
}
Event::Key(key) if key.kind == KeyEventKind::Press => match (key.code, key.modifiers) {
(KeyCode::Char('y'), _) | (KeyCode::Char('Y'), _) => {
state.skip_quit_confirmation = state.quit_dont_ask_again;
Ok(InputOutcome::Quit)
}
(KeyCode::Char('c'), KeyModifiers::CONTROL) => Ok(InputOutcome::Quit),
(KeyCode::Char(' '), _) => {
state.quit_dont_ask_again = !state.quit_dont_ask_again;
Ok(InputOutcome::Continue(true))
}
(KeyCode::Enter, _)
| (KeyCode::Esc, _)
| (KeyCode::Char('n'), _)
| (KeyCode::Char('N'), _)
| (KeyCode::Char('q'), _)
| (KeyCode::Char('Q'), _) => {
state.quit_confirm_open = false;
state.quit_dont_ask_again = false;
Ok(InputOutcome::Continue(true))
}
_ => Ok(InputOutcome::Continue(false)),
},
Event::Resize(_, _) => Ok(InputOutcome::Continue(true)),
_ => Ok(InputOutcome::Continue(false)),
};
}

if let Event::Mouse(mouse) = &event {
let mouse_position = Some((mouse.column, mouse.row));
let mouse_moved = state.mouse_position != mouse_position;
state.mouse_position = mouse_position;

if state.show_help || state.no_sessions_confirm_open {
return Ok(InputOutcome::Continue(false));
}
if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
if let Some(action) = ui_click_action_at(&state.ui_hit_targets, mouse.column, mouse.row)
{
if action == UiClickAction::ConfirmQuit
|| (action == UiClickAction::PromptQuit && state.skip_quit_confirmation)
{
return Ok(InputOutcome::Quit);
}
let changed = apply_ui_click_action(state, action);
return Ok(InputOutcome::Continue(changed));
}
}
if mouse.kind == MouseEventKind::Moved {
return Ok(InputOutcome::Continue(mouse_moved));
}
}

if let Event::Key(key) = &event {
Expand All @@ -622,7 +742,14 @@ fn handle_input_event(
}

match (key.code, key.modifiers) {
(KeyCode::Char('q'), _) => return Ok(InputOutcome::Quit),
(KeyCode::Char('q'), _) | (KeyCode::Char('Q'), _) => {
if state.skip_quit_confirmation {
return Ok(InputOutcome::Quit);
}
state.quit_confirm_open = true;
state.quit_dont_ask_again = false;
return Ok(InputOutcome::Continue(true));
}
(KeyCode::Char('c'), KeyModifiers::CONTROL) => return Ok(InputOutcome::Quit),
(KeyCode::Char('s'), _) | (KeyCode::Char('S'), _) | (KeyCode::F(2), _) => {
state.active_screen = next_active_screen(state.active_screen);
Expand All @@ -646,6 +773,7 @@ fn handle_input_event(
}

if matches!(event, Event::Resize(_, _)) {
state.mouse_position = None;
return Ok(InputOutcome::Continue(true));
}

Expand Down Expand Up @@ -693,6 +821,11 @@ fn apply_ui_click_action(state: &mut AppState, action: UiClickAction) -> bool {
state.active_screen = screen;
changed
}
UiClickAction::SetDisplayStyle(style) => {
let changed = state.display_style != style;
state.display_style = style;
changed
}
UiClickAction::SetMetric(metric) => {
let changed = state.metric != metric;
state.metric = metric;
Expand Down Expand Up @@ -726,8 +859,36 @@ fn apply_ui_click_action(state: &mut AppState, action: UiClickAction) -> bool {
state.activity_project_limit = next;
changed
}
UiClickAction::CycleDisplayStyle => {
state.display_style = state.display_style.toggled();
UiClickAction::PromptQuit => {
state.quit_confirm_open = true;
state.quit_dont_ask_again = false;
true
}
UiClickAction::CancelQuit => {
state.quit_confirm_open = false;
state.quit_dont_ask_again = false;
true
}
UiClickAction::ConfirmQuit => {
state.skip_quit_confirmation = state.quit_dont_ask_again;
false
}
UiClickAction::ToggleQuitDontAskAgain => {
state.quit_dont_ask_again = !state.quit_dont_ask_again;
true
}
UiClickAction::PromptQuitConfirmationPreference => {
state.quit_preference_prompt = Some(!state.skip_quit_confirmation);
true
}
UiClickAction::ConfirmQuitConfirmationPreference => {
if let Some(desired_skip_confirmation) = state.quit_preference_prompt.take() {
state.skip_quit_confirmation = desired_skip_confirmation;
}
true
}
UiClickAction::CancelQuitConfirmationPreference => {
state.quit_preference_prompt = None;
true
}
}
Expand Down Expand Up @@ -997,6 +1158,7 @@ fn load_persisted_ui_state(
state.display_style = style;
}
}
state.skip_quit_confirmation = store.global.skip_quit_confirmation;

if let Some(workspace_path) = state.workspace_path.as_ref() {
let workspace_key = workspace_path.to_string_lossy();
Expand Down Expand Up @@ -1025,6 +1187,7 @@ fn save_persisted_ui_state(comon_home: &Path, state: &PersistedUiState) -> Resul
.clamp(MIN_ACTIVITY_PROJECT_LIMIT, MAX_ACTIVITY_PROJECT_LIMIT),
);
store.global.display_style = Some(state.display_style.store_value().to_string());
store.global.skip_quit_confirmation = state.skip_quit_confirmation;
store.global.last_workspace_path = workspace_path_text.clone();
store.global.updated_at = now;

Expand Down Expand Up @@ -1259,6 +1422,19 @@ mod tests {
let _ = std::fs::remove_dir_all(comon_home);
}

#[test]
fn quit_confirmation_preference_round_trips_through_state_store() {
let comon_home = make_temp_dir("quit-confirmation-preference");
let mut state = PersistedUiState::default_for_workspace(None);
state.skip_quit_confirmation = true;

save_persisted_ui_state(&comon_home, &state).expect("save persisted ui state");
let loaded = load_persisted_ui_state(&comon_home, None).expect("load persisted ui state");
assert!(loaded.skip_quit_confirmation);

let _ = std::fs::remove_dir_all(comon_home);
}

#[test]
fn legacy_system_style_restores_as_system_compact() {
let comon_home = make_temp_dir("legacy-system-display-style");
Expand Down
Loading
Loading