Skip to content
Closed
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
3 changes: 3 additions & 0 deletions docs/commands/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ codex-auth list --skip-api
- `--api` is accepted as an explicit equivalent to default mode.
- `--skip-api` forbids remote API calls for this command.
- `--live` keeps refreshing the terminal view and requires a TTY.
- In a TTY, plain `list` opens a sortable table without scheduled live refresh. Without a TTY, it prints a static table.

When local-only refresh is active, only the active account can be updated from local rollout files. Non-active rows use the stored registry snapshot.

Expand All @@ -34,4 +35,6 @@ When local-only refresh is active, only the active account can be updated from l
- Usage cells show remaining percent and reset time when that data is known.
- Remote refresh failures can render row overlays such as `401`, `403`, `TimedOut`, or `MissingAuth`.
- `LAST ACTIVITY` is based on the last stored usage update time.
- In the interactive table, click a table header to sort by that column; click the same header again to reverse the direction.
- When the interactive table exits, it prints the final visible table, preserving the current sort order.
- Shared table layout policy is documented in [docs/table-layout.md](../table-layout.md).
2 changes: 2 additions & 0 deletions docs/commands/remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ codex-auth remove --all
- The default picker stays local-only so deletion is not blocked by refresh work.
- `--api` attempts a best-effort foreground refresh for picker display.
- `--skip-api` explicitly forbids remote refresh.
- Click a table header to sort by that column; click the same header again to reverse the direction.
- `q` quits without deleting accounts.

## Live Remove
Expand All @@ -25,6 +26,7 @@ codex-auth remove --all
- Removed rows disappear from the current display immediately.
- Existing row overlays stay in place until the next scheduled refresh.
- The active account shown after deletion comes from the persisted registry state.
- Click a table header to sort by that column; click the same header again to reverse the direction.

## Query Remove

Expand Down
2 changes: 2 additions & 0 deletions docs/commands/switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ codex-auth switch <query>

- The picker uses the same account ordering as `list`.
- `q` quits without switching.
- Click a table header to sort by that column; click the same header again to reverse the direction.
- `--api` forces foreground remote refresh before rendering.
- `--skip-api` renders from stored data and local-only active-account refresh where available.

Expand All @@ -25,6 +26,7 @@ codex-auth switch <query>
- A successful switch patches the current display immediately.
- In-flight refresh results are discarded after a manual switch.
- Existing usage overlays stay visible until the next scheduled refresh.
- Click a table header to sort by that column; click the same header again to reverse the direction.

## Query Switch

Expand Down
5 changes: 2 additions & 3 deletions docs/table-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ before expanding long labels.

This applies to `switch`, `remove`, `list`, and `list --live` because they all
render through the shared table code in `src/cli/table_layout.zig` and are
called by the renderers in `src/cli/render.zig`. The
width-priority rules only matter when a viewport width is known, which is
typically the live case.
called by the renderers in `src/cli/render.zig`. The width-priority rules only
matter when a viewport width is known, which is typically the live case.

## Column Width Priority

Expand Down
1 change: 1 addition & 0 deletions src/cli/live.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub const SwitchLiveActionController = selection.SwitchLiveActionController;
pub const RemoveLiveActionController = selection.RemoveLiveActionController;

pub const selectAccountWithLiveUpdates = live_view.selectAccountWithLiveUpdates;
pub const viewAccountsWithSortableTable = live_view.viewAccountsWithSortableTable;
pub const viewAccountsWithLiveUpdates = live_view.viewAccountsWithLiveUpdates;
pub const runSwitchLiveActions = live_switch.runSwitchLiveActions;
pub const runRemoveLiveActions = live_remove.runRemoveLiveActions;
18 changes: 15 additions & 3 deletions src/cli/live_remove.zig
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub fn runRemoveLiveActions(

var number_buf: [8]u8 = undefined;
var number_len: usize = 0;
var sort_spec: ?row_data.SortSpec = null;
var viewport_start: usize = 0;
var follow_selection = true;
var needs_render = true;
Expand Down Expand Up @@ -82,7 +83,7 @@ pub fn runRemoveLiveActions(
}
if (needs_render or now_second != last_render_second) {
const borrowed = current_display.borrowed();
const rows = try rows_cache.ensure(allocator, borrowed);
const rows = try rows_cache.ensureSortable(allocator, borrowed, sort_spec);

const cursor_idx = try live_tui.resolveSelectedIndex(allocator, &cursor_account_key, rows, borrowed.reg);
try checked_flags_buf.resize(allocator, rows.selectable_row_indices.len);
Expand Down Expand Up @@ -135,14 +136,14 @@ pub fn runRemoveLiveActions(
.ready => |key_count| {
if (key_count != 0) {
const borrowed = current_display.borrowed();
const rows = try rows_cache.ensure(allocator, borrowed);
const rows = try rows_cache.ensureSortable(allocator, borrowed, sort_spec);
const page_rows = live_tui.maxTableRows(
tui.terminalRows(),
live_tui.switchFixedLines("status", action_message orelse ""),
);
const wheel_rows = live_tui.mouseWheelRows(page_rows);

for (key_buf[0..key_count]) |key| {
key_loop: for (key_buf[0..key_count]) |key| {
const cursor_idx = try live_tui.resolveSelectedIndex(allocator, &cursor_account_key, rows, borrowed.reg);
switch (key) {
.move_up => {
Expand Down Expand Up @@ -248,6 +249,17 @@ pub fn runRemoveLiveActions(
}
},
.redraw => needs_render = true,
.mouse_click => |click| {
const idx_width = @max(@as(usize, 2), indexWidth(rows.selectable_row_indices.len));
if (live_tui.removeHeaderSortFieldForClick(rows, idx_width, 2, tui.terminalCols(), click)) |field| {
sort_spec = live_tui.toggledSortSpec(sort_spec, field);
viewport_start = 0;
follow_selection = true;
rows_cache.invalidate(allocator);
needs_render = true;
break :key_loop;
}
},
.byte => |ch| {
if (isQuitKey(ch)) return;
if (ch == 'k') {
Expand Down
20 changes: 16 additions & 4 deletions src/cli/live_switch.zig
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub fn runSwitchLiveActions(
var number_buf: [8]u8 = undefined;
var number_len: usize = 0;
var auto_switch_state = live_tui.LiveAutoSwitchState.init(controller.auto_switch);
var sort_spec: ?row_data.SortSpec = null;
var viewport_start: usize = 0;
var follow_selection = true;
var needs_render = true;
Expand Down Expand Up @@ -93,7 +94,7 @@ pub fn runSwitchLiveActions(

if (auto_switch_state.takePending()) {
const borrowed = current_display.borrowed();
const rows = try rows_cache.ensureSelectable(allocator, borrowed);
const rows = try rows_cache.ensureSortable(allocator, borrowed, sort_spec);
if (try maybeAutoSwitchTargetKeyAlloc(allocator, borrowed, rows)) |target_key| {
defer allocator.free(target_key);
const outcome = controller.apply_selection(controller.refresh.context, allocator, borrowed, target_key) catch |err| {
Expand Down Expand Up @@ -122,7 +123,7 @@ pub fn runSwitchLiveActions(

if (needs_render or now_second != last_render_second) {
const borrowed = current_display.borrowed();
const rows = try rows_cache.ensureSelectable(allocator, borrowed);
const rows = try rows_cache.ensureSortable(allocator, borrowed, sort_spec);
const total_accounts = accountRowCount(rows.items);
const selected_idx = try live_tui.resolveSelectedIndex(allocator, &selected_account_key, rows, borrowed.reg);
const status_line = try controller.refresh.build_status_line(controller.refresh.context, allocator, borrowed);
Expand Down Expand Up @@ -168,15 +169,15 @@ pub fn runSwitchLiveActions(
.ready => |key_count| {
if (key_count != 0) {
const borrowed = current_display.borrowed();
const rows = try rows_cache.ensureSelectable(allocator, borrowed);
const rows = try rows_cache.ensureSortable(allocator, borrowed, sort_spec);
const total_accounts = accountRowCount(rows.items);
const page_rows = live_tui.maxTableRows(
tui.terminalRows(),
live_tui.switchFixedLines("status", action_message orelse ""),
);
const wheel_rows = live_tui.mouseWheelRows(page_rows);

for (key_buf[0..key_count]) |key| {
key_loop: for (key_buf[0..key_count]) |key| {
const selected_idx = try live_tui.resolveSelectedIndex(allocator, &selected_account_key, rows, borrowed.reg);
switch (key) {
.move_up => {
Expand Down Expand Up @@ -284,6 +285,17 @@ pub fn runSwitchLiveActions(
}
},
.redraw => needs_render = true,
.mouse_click => |click| {
const idx_width = @max(@as(usize, 2), indexWidth(total_accounts));
if (live_tui.switchHeaderSortFieldForClick(rows, idx_width, 2, tui.terminalCols(), click)) |field| {
sort_spec = live_tui.toggledSortSpec(sort_spec, field);
viewport_start = 0;
follow_selection = true;
rows_cache.invalidate(allocator);
needs_render = true;
break :key_loop;
}
},
.byte => |ch| {
if (isQuitKey(ch)) return;
if (ch == 'k') {
Expand Down
102 changes: 102 additions & 0 deletions src/cli/live_tui.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const picker = @import("picker.zig");
const render = @import("render.zig");
const row_data = @import("rows.zig");
const selection = @import("selection.zig");
const table_layout = @import("table_layout.zig");
const tui_mod = @import("tui.zig");

pub const tick_ms = tui_mod.live_ui_tick_ms;
Expand Down Expand Up @@ -235,6 +236,82 @@ pub fn applyListViewportKey(
}
}

pub fn toggledSortSpec(current: ?row_data.SortSpec, field: row_data.SortField) row_data.SortSpec {
if (current) |spec| {
if (spec.field == field) {
return .{
.field = field,
.direction = if (spec.direction == .asc) .desc else .asc,
};
}
}
return .{ .field = field, .direction = .asc };
}

pub fn listHeaderSortFieldForClick(
rows: *const row_data.SwitchRows,
idx_width: usize,
max_cols: ?usize,
click: tui_mod.TuiMouseClick,
) ?row_data.SortField {
return tableHeaderSortFieldForClick(rows, 2 + idx_width + 1, 1, max_cols, click);
}

pub fn switchHeaderSortFieldForClick(
rows: *const row_data.SwitchRows,
idx_width: usize,
header_row: usize,
max_cols: ?usize,
click: tui_mod.TuiMouseClick,
) ?row_data.SortField {
return tableHeaderSortFieldForClick(rows, 2 + idx_width + 1, header_row, max_cols, click);
}

pub fn removeHeaderSortFieldForClick(
rows: *const row_data.SwitchRows,
idx_width: usize,
header_row: usize,
max_cols: ?usize,
click: tui_mod.TuiMouseClick,
) ?row_data.SortField {
return tableHeaderSortFieldForClick(rows, 2 + 3 + 1 + idx_width + 1, header_row, max_cols, click);
}

pub fn tableHeaderSortFieldForClick(
rows: *const row_data.SwitchRows,
prefix_width: usize,
header_row: usize,
max_cols: ?usize,
click: tui_mod.TuiMouseClick,
) ?row_data.SortField {
if (click.row != header_row) return null;

const bounded = table_layout.boundWidths(rows.widths, prefix_width, max_cols);
const widths = [_]usize{
bounded.email,
bounded.plan,
bounded.rate_5h,
bounded.rate_week,
bounded.last,
};
const fields = [_]row_data.SortField{
.account,
.plan,
.five_hour,
.weekly,
.last_activity,
};

var start_col = prefix_width + 1;
for (widths, 0..) |width, idx| {
if (width != 0 and click.col >= start_col and click.col < start_col + width) {
return fields[idx];
}
start_col += width + 2;
}
return null;
}

pub fn buildSelectableRows(
allocator: std.mem.Allocator,
display: selection.SwitchSelectionDisplay,
Expand Down Expand Up @@ -269,6 +346,17 @@ pub const RowsCache = struct {
return &self.rows.?;
}

pub fn ensureList(
self: *RowsCache,
allocator: std.mem.Allocator,
display: selection.SwitchSelectionDisplay,
sort_spec: ?row_data.SortSpec,
) !*row_data.SwitchRows {
if (self.rows) |*rows| return rows;
self.rows = try row_data.buildListRowsWithUsageOverrides(allocator, display.reg, display.usage_overrides, sort_spec);
return &self.rows.?;
}

pub fn ensureSelectable(
self: *RowsCache,
allocator: std.mem.Allocator,
Expand All @@ -280,6 +368,20 @@ pub const RowsCache = struct {
self.rows = rows;
return &self.rows.?;
}

pub fn ensureSortable(
self: *RowsCache,
allocator: std.mem.Allocator,
display: selection.SwitchSelectionDisplay,
sort_spec: ?row_data.SortSpec,
) !*row_data.SwitchRows {
if (self.rows) |*rows| return rows;
var rows = try row_data.buildSortableRowsWithUsageOverrides(allocator, display.reg, null, display.usage_overrides, sort_spec);
errdefer rows.deinit(allocator);
try row_data.filterErroredRowsFromSelectableIndices(allocator, &rows);
self.rows = rows;
return &self.rows.?;
}
};

pub fn resolveSelectedIndex(
Expand Down
Loading
Loading