From e6838228c5287ffeed56804859a5a95e56ebcfa7 Mon Sep 17 00:00:00 2001 From: Loongphy Date: Mon, 18 May 2026 17:18:54 +0800 Subject: [PATCH] fix: enable list colors on windows terminals --- build.zig | 1 - src/cli/help.zig | 4 +- src/cli/live_remove.zig | 5 +- src/cli/live_switch.zig | 5 +- src/cli/live_view.zig | 9 +-- src/cli/login.zig | 10 +-- src/cli/output.zig | 121 ++++++++++++++++------------------ src/cli/picker_remove.zig | 7 +- src/cli/picker_switch.zig | 8 +-- src/cli/render.zig | 80 +++++++++++----------- src/cli/style.zig | 38 +++++++++-- src/cli/table_layout.zig | 38 +++++------ src/core/io_util.zig | 22 ++++++- src/terminal/color.zig | 24 +------ src/tui/table.zig | 7 +- tests/cli_picker_test.zig | 113 +++++++++++++++++++++++++++++-- tests/table_layout_test.zig | 16 ++++- tests/terminal_color_test.zig | 11 ---- 18 files changed, 313 insertions(+), 206 deletions(-) delete mode 100644 tests/terminal_color_test.zig diff --git a/build.zig b/build.zig index 5447ea21..236cf50f 100644 --- a/build.zig +++ b/build.zig @@ -54,7 +54,6 @@ pub fn build(b: *std.Build) void { "tests/registry_import_test.zig", "tests/registry_test.zig", "tests/session_test.zig", - "tests/terminal_color_test.zig", "tests/time_relative_test.zig", "tests/table_layout_test.zig", "tests/tui_display_test.zig", diff --git a/src/cli/help.zig b/src/cli/help.zig index c7cb9702..c9164ced 100644 --- a/src/cli/help.zig +++ b/src/cli/help.zig @@ -10,7 +10,7 @@ pub fn printHelp() !void { var stdout: io_util.Stdout = undefined; stdout.init(); const out = stdout.out(); - const use_color = style.stdoutColorEnabled(); + const use_color = stdout.color_enabled; try writeHelp(out, use_color); try out.flush(); } @@ -89,7 +89,7 @@ pub fn printCommandHelp(topic: HelpTopic) !void { var stdout: io_util.Stdout = undefined; stdout.init(); const out = stdout.out(); - try writeCommandHelp(out, style.stdoutColorEnabled(), topic); + try writeCommandHelp(out, stdout.color_enabled, topic); try out.flush(); } diff --git a/src/cli/live_remove.zig b/src/cli/live_remove.zig index ecae4f32..4361b1f5 100644 --- a/src/cli/live_remove.zig +++ b/src/cli/live_remove.zig @@ -3,6 +3,7 @@ const terminal_color = @import("../terminal/color.zig"); const selection = @import("selection.zig"); const row_data = @import("rows.zig"); const render = @import("render.zig"); +const style = @import("style.zig"); const picker = @import("picker.zig"); const tui_mod = @import("tui.zig"); const live_tui = @import("live_tui.zig"); @@ -106,15 +107,15 @@ pub fn runRemoveLiveActions( bounded_viewport.max_cols = tui.terminalCols(); frame.clearRetainingCapacity(); + var styled_frame = style.StyledWriter.init(&frame.writer, use_color); renderRemoveScreenViewport( - &frame.writer, + &styled_frame, borrowed.reg, rows.items, @max(@as(usize, 2), indexWidth(rows.selectable_row_indices.len)), rows.widths, cursor_idx, checked_flags, - use_color, status_line, action_message orelse "", number_buf[0..number_len], diff --git a/src/cli/live_switch.zig b/src/cli/live_switch.zig index be66c187..4b8d1cf5 100644 --- a/src/cli/live_switch.zig +++ b/src/cli/live_switch.zig @@ -3,6 +3,7 @@ const terminal_color = @import("../terminal/color.zig"); const selection = @import("selection.zig"); const row_data = @import("rows.zig"); const render = @import("render.zig"); +const style = @import("style.zig"); const picker = @import("picker.zig"); const tui_mod = @import("tui.zig"); const live_tui = @import("live_tui.zig"); @@ -140,14 +141,14 @@ pub fn runSwitchLiveActions( bounded_viewport.max_cols = tui.terminalCols(); frame.clearRetainingCapacity(); + var styled_frame = style.StyledWriter.init(&frame.writer, use_color); renderSwitchScreenViewport( - &frame.writer, + &styled_frame, borrowed.reg, rows.items, @max(@as(usize, 2), indexWidth(total_accounts)), rows.widths, selected_display_idx, - use_color, status_line, action_message orelse "", number_buf[0..number_len], diff --git a/src/cli/live_view.zig b/src/cli/live_view.zig index 6377d59b..fd3310c1 100644 --- a/src/cli/live_view.zig +++ b/src/cli/live_view.zig @@ -5,6 +5,7 @@ const terminal_color = @import("../terminal/color.zig"); const selection = @import("selection.zig"); const row_data = @import("rows.zig"); const render = @import("render.zig"); +const style = @import("style.zig"); const picker = @import("picker.zig"); const tui_mod = @import("tui.zig"); const live_tui = @import("live_tui.zig"); @@ -107,14 +108,14 @@ pub fn selectAccountWithLiveUpdates( bounded_viewport.max_cols = tui.terminalCols(); frame.clearRetainingCapacity(); + var styled_frame = style.StyledWriter.init(&frame.writer, use_color); renderSwitchScreenViewport( - &frame.writer, + &styled_frame, borrowed.reg, rows.items, @max(@as(usize, 2), indexWidth(total_accounts)), rows.widths, selected_display_idx, - use_color, status_line, "", number_buf[0..number_len], @@ -286,13 +287,13 @@ pub fn viewAccountsWithLiveUpdates( bounded_viewport.max_cols = tui.terminalCols(); frame.clearRetainingCapacity(); + var styled_frame = style.StyledWriter.init(&frame.writer, use_color); renderListScreenViewport( - &frame.writer, + &styled_frame, ¤t_display.reg, rows.items, @max(@as(usize, 2), indexWidth(rows.selectable_row_indices.len)), rows.widths, - use_color, status_line, bounded_viewport, ) catch |err| return mapTuiOutputError(err); diff --git a/src/cli/login.zig b/src/cli/login.zig index 68e8484b..b202c28e 100644 --- a/src/cli/login.zig +++ b/src/cli/login.zig @@ -1,8 +1,8 @@ const std = @import("std"); const app_runtime = @import("../core/runtime.zig"); +const io_util = @import("../core/io_util.zig"); const types = @import("types.zig"); const output = @import("output.zig"); -const style = @import("style.zig"); pub fn codexLoginArgs(opts: types.LoginOptions) []const []const u8 { return if (opts.device_auth) @@ -22,10 +22,10 @@ fn ensureCodexLoginSucceeded(term: std.process.Child.Term) !void { } fn writeCodexLoginLaunchFailureHint(err_name: []const u8) !void { - var buffer: [512]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - try output.writeCodexLoginLaunchFailureHintTo(out, err_name, style.stderrColorEnabled()); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + try output.writeCodexLoginLaunchFailureHintTo(out, err_name, stderr.color_enabled); try out.flush(); } diff --git a/src/cli/output.zig b/src/cli/output.zig index 0d70a797..7f24b73f 100644 --- a/src/cli/output.zig +++ b/src/cli/output.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const app_runtime = @import("../core/runtime.zig"); const builtin = @import("builtin"); const display_rows = @import("../tui/display.zig"); const registry = @import("../registry/root.zig"); @@ -21,10 +20,10 @@ pub fn importReportMarker(outcome: registry.ImportOutcome, is_windows: bool) []c } pub fn printUsageError(usage_err: *const UsageError) !void { - var buffer: [2048]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.print(" {s}\n\n", .{usage_err.message}); try help.writeUsageSection(out, usage_err.topic); @@ -45,9 +44,9 @@ pub fn printVersion() !void { pub fn printImportReport(report: *const registry.ImportReport) !void { var stdout: io_util.Stdout = undefined; stdout.init(); - var stderr_buffer: [4096]u8 = undefined; - var stderr_writer = std.Io.File.stderr().writer(app_runtime.io(), &stderr_buffer); - try writeImportReport(stdout.out(), &stderr_writer.interface, report); + var stderr: io_util.Stderr = undefined; + stderr.init(); + try writeImportReport(stdout.out(), stderr.out(), report); } pub fn writeImportReport( @@ -115,10 +114,10 @@ pub fn writeHintPrefixTo(out: *std.Io.Writer, use_color: bool) !void { } pub fn printAccountNotFoundError(query: []const u8) !void { - var buffer: [512]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.print(" no account matches '{s}'.\n", .{query}); try writeHintPrefixTo(out, use_color); @@ -127,10 +126,10 @@ pub fn printAccountNotFoundError(query: []const u8) !void { } pub fn printSwitchAccountNotFoundError(query: []const u8) !void { - var buffer: [768]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.print(" no switch target matches '{s}'.\n", .{query}); try writeHintPrefixTo(out, use_color); @@ -139,10 +138,10 @@ pub fn printSwitchAccountNotFoundError(query: []const u8) !void { } pub fn printAliasAccountNotFoundError(query: []const u8) !void { - var buffer: [768]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.print(" no alias target matches '{s}'.\n", .{query}); try writeHintPrefixTo(out, use_color); @@ -156,10 +155,10 @@ pub fn printAccountNotFoundErrors(queries: []const []const u8) !void { return printAccountNotFoundError(queries[0]); } - var buffer: [1024]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.writeAll(" no account matches: "); for (queries, 0..) |query, idx| { @@ -173,10 +172,10 @@ pub fn printAccountNotFoundErrors(queries: []const []const u8) !void { } pub fn printSwitchRequiresTtyError() !void { - var buffer: [512]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.writeAll(" interactive switch requires a TTY.\n"); try writeHintPrefixTo(out, use_color); @@ -185,10 +184,10 @@ pub fn printSwitchRequiresTtyError() !void { } pub fn printListRequiresTtyError() !void { - var buffer: [512]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.writeAll(" live list requires a TTY.\n"); try writeHintPrefixTo(out, use_color); @@ -197,10 +196,10 @@ pub fn printListRequiresTtyError() !void { } pub fn printRemoveRequiresTtyError() !void { - var buffer: [512]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.writeAll(" interactive remove requires a TTY.\n"); try writeHintPrefixTo(out, use_color); @@ -209,10 +208,10 @@ pub fn printRemoveRequiresTtyError() !void { } pub fn printAliasRequiresTtyError() !void { - var buffer: [512]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.writeAll(" multiple alias targets require a TTY.\n"); try writeHintPrefixTo(out, use_color); @@ -221,20 +220,20 @@ pub fn printAliasRequiresTtyError() !void { } pub fn printInvalidAliasError(reason: []const u8) !void { - var buffer: [768]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.print(" invalid alias: {s}\n", .{reason}); try out.flush(); } pub fn printDuplicateAliasError(alias_value: []const u8, email: []const u8) !void { - var buffer: [768]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.print(" alias '{s}' is already used by {s}.\n", .{ alias_value, email }); try out.flush(); @@ -267,10 +266,10 @@ pub fn printAliasCleared(rec: *const registry.AccountRecord, old_alias: []const } pub fn printInvalidRemoveSelectionError() !void { - var buffer: [512]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeErrorPrefixTo(out, use_color); try out.writeAll(" invalid remove selection input.\n"); try writeHintPrefixTo(out, use_color); @@ -321,10 +320,10 @@ pub fn writeRemoveConfirmationTo(out: *std.Io.Writer, labels: []const []const u8 } pub fn printRemoveConfirmationUnavailableError(labels: []const []const u8) !void { - var buffer: [1024]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - const use_color = style.stderrColorEnabled(); + var stderr: io_util.Stderr = undefined; + stderr.init(); + const out = stderr.out(); + const use_color = stderr.color_enabled; try writeMatchedAccountsListTo(out, labels); try writeErrorPrefixTo(out, use_color); try out.writeAll(" multiple accounts match the query in non-interactive mode.\n"); @@ -377,21 +376,13 @@ pub fn printSwitchedAccount( var stdout: io_util.Stdout = undefined; stdout.init(); const out = stdout.out(); - const use_color = style.stdoutColorEnabled(); + const use_color = stdout.color_enabled; if (use_color) try out.writeAll(style.ansi.green); try out.print("Switched to {s}\n", .{label}); if (use_color) try out.writeAll(style.ansi.reset); try out.flush(); } -fn writeCodexLoginLaunchFailureHint(err_name: []const u8, use_color: bool) !void { - var buffer: [512]u8 = undefined; - var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer); - const out = &writer.interface; - try writeCodexLoginLaunchFailureHintTo(out, err_name, use_color); - try out.flush(); -} - pub fn writeCodexLoginLaunchFailureHintTo(out: *std.Io.Writer, err_name: []const u8, use_color: bool) !void { try writeErrorPrefixTo(out, use_color); if (std.mem.eql(u8, err_name, "FileNotFound")) { diff --git a/src/cli/picker_remove.zig b/src/cli/picker_remove.zig index ff71ee00..79e32404 100644 --- a/src/cli/picker_remove.zig +++ b/src/cli/picker_remove.zig @@ -63,7 +63,7 @@ fn selectRemoveWithNumbers( if (reg.accounts.items.len == 0) return null; var rows = try buildSwitchRowsWithUsageOverrides(allocator, reg, usage_overrides); defer rows.deinit(allocator); - const use_color = style.stdoutColorEnabled(); + var styled_out = style.StyledWriter.init(out, stdout.color_enabled); const idx_width = @max(@as(usize, 2), indexWidth(rows.selectable_row_indices.len)); const widths = rows.widths; @@ -72,7 +72,7 @@ fn selectRemoveWithNumbers( @memset(checked, false); try out.writeAll("Select accounts to delete:\n\n"); - try renderRemoveList(out, reg, rows.items, idx_width, widths, null, checked, use_color); + try renderRemoveList(&styled_out, reg, rows.items, idx_width, widths, null, checked); try out.writeAll("Enter account numbers (comma/space separated, empty to cancel): "); try out.flush(); @@ -153,7 +153,8 @@ fn selectRemoveInteractive( try tui.resetFrame(); writeTuiPromptLine(out, "Select accounts to delete:", number_buf[0..number_len]) catch |err| return mapTuiOutputError(err); out.writeAll("\n") catch |err| return mapTuiOutputError(err); - renderRemoveList(out, reg, rows.items, idx_width, widths, idx, checked, use_color) catch |err| return mapTuiOutputError(err); + var styled_out = style.StyledWriter.init(out, use_color); + renderRemoveList(&styled_out, reg, rows.items, idx_width, widths, idx, checked) catch |err| return mapTuiOutputError(err); out.writeAll("\n") catch |err| return mapTuiOutputError(err); writeRemoveTuiFooter(out, use_color) catch |err| return mapTuiOutputError(err); try tui.flushOutput(); diff --git a/src/cli/picker_switch.zig b/src/cli/picker_switch.zig index 24bd321b..ad13b5aa 100644 --- a/src/cli/picker_switch.zig +++ b/src/cli/picker_switch.zig @@ -100,14 +100,14 @@ pub fn selectWithNumbers( try filterErroredRowsFromSelectableIndices(allocator, &rows); const total_accounts = accountRowCount(rows.items); if (total_accounts == 0) return null; - const use_color = style.stdoutColorEnabled(); + var styled_out = style.StyledWriter.init(out, stdout.color_enabled); const active_idx = activeSelectableIndex(&rows); const idx_width = @max(@as(usize, 2), indexWidth(total_accounts)); const widths = rows.widths; const active_display_idx = if (active_idx) |idx| displayedIndexForSelectable(&rows, idx) else null; try out.writeAll("Select account to activate:\n\n"); - try renderSwitchList(out, reg, rows.items, idx_width, widths, active_display_idx, use_color); + try renderSwitchList(&styled_out, reg, rows.items, idx_width, widths, active_display_idx); try out.writeAll("Select account number (or q to quit): "); try out.flush(); @@ -139,14 +139,14 @@ pub fn selectWithNumbersFromIndices( try filterErroredRowsFromSelectableIndices(allocator, &rows); const total_accounts = accountRowCount(rows.items); if (total_accounts == 0) return null; - const use_color = style.stdoutColorEnabled(); + var styled_out = style.StyledWriter.init(out, stdout.color_enabled); const active_idx = activeSelectableIndex(&rows); const idx_width = @max(@as(usize, 2), indexWidth(total_accounts)); const widths = rows.widths; const active_display_idx = if (active_idx) |idx| displayedIndexForSelectable(&rows, idx) else null; try out.writeAll("Select account to activate:\n\n"); - try renderSwitchList(out, reg, rows.items, idx_width, widths, active_display_idx, use_color); + try renderSwitchList(&styled_out, reg, rows.items, idx_width, widths, active_display_idx); try out.writeAll("Select account number (or q to quit): "); try out.flush(); diff --git a/src/cli/render.zig b/src/cli/render.zig index 6c722df4..a455ab36 100644 --- a/src/cli/render.zig +++ b/src/cli/render.zig @@ -8,6 +8,7 @@ const tui_mod = @import("tui.zig"); pub const SwitchWidths = row_data.SwitchWidths; pub const indexWidth = row_data.indexWidth; pub const LiveListViewport = table_layout.LiveListViewport; +pub const StyledWriter = style.StyledWriter; const SwitchRow = row_data.SwitchRow; const writeTuiPromptLine = tui_mod.writeTuiPromptLine; const writeSwitchTuiFooterBounded = tui_mod.writeSwitchTuiFooterBounded; @@ -32,14 +33,14 @@ pub fn renderSwitchScreen( action_line: []const u8, number_input: []const u8, ) !void { + var writer = style.StyledWriter.init(out, use_color); try renderSwitchScreenViewport( - out, + &writer, reg, rows, idx_width, widths, selected, - use_color, status_line, action_line, number_input, @@ -48,26 +49,25 @@ pub fn renderSwitchScreen( } pub fn renderSwitchScreenViewport( - out: *std.Io.Writer, + writer: *style.StyledWriter, reg: *registry.Registry, rows: []const SwitchRow, idx_width: usize, widths: SwitchWidths, selected: ?usize, - use_color: bool, status_line: []const u8, action_line: []const u8, number_input: []const u8, viewport: LiveListViewport, ) !void { - try writeTuiPromptLine(out, "Select account to activate:", number_input); - try renderSwitchListViewport(out, reg, rows, idx_width, widths, selected, use_color, viewport); + try writeTuiPromptLine(writer.out, "Select account to activate:", number_input); + try renderSwitchListViewport(writer, reg, rows, idx_width, widths, selected, viewport); if (status_line.len != 0) { - try writeLiveStatusLine(out, status_line, use_color, viewport.max_cols); + try writeLiveStatusLine(writer.out, status_line, writer.color_enabled, viewport.max_cols); } - try writeSwitchTuiFooterBounded(out, use_color, viewport.max_cols); + try writeSwitchTuiFooterBounded(writer.out, writer.color_enabled, viewport.max_cols); if (action_line.len != 0) { - try writeStyledTuiLineBounded(out, if (use_color) actionLineStyle(action_line) else "", action_line, viewport.max_cols); + try writeStyledTuiLineBounded(writer.out, if (writer.color_enabled) actionLineStyle(action_line) else "", action_line, viewport.max_cols); } } @@ -80,33 +80,32 @@ pub fn renderListScreen( use_color: bool, status_line: []const u8, ) !void { + var writer = style.StyledWriter.init(out, use_color); try renderListScreenViewport( - out, + &writer, reg, rows, idx_width, widths, - use_color, status_line, .{}, ); } pub fn renderListScreenViewport( - out: *std.Io.Writer, + writer: *style.StyledWriter, reg: *registry.Registry, rows: []const SwitchRow, idx_width: usize, widths: SwitchWidths, - use_color: bool, status_line: []const u8, viewport: LiveListViewport, ) !void { - try renderSwitchListViewport(out, reg, rows, idx_width, widths, null, use_color, viewport); + try renderSwitchListViewport(writer, reg, rows, idx_width, widths, null, viewport); if (status_line.len != 0) { - try writeLiveStatusLine(out, status_line, use_color, viewport.max_cols); + try writeLiveStatusLine(writer.out, status_line, writer.color_enabled, viewport.max_cols); } - try writeListTuiFooterBounded(out, use_color, viewport.max_cols); + try writeListTuiFooterBounded(writer.out, writer.color_enabled, viewport.max_cols); } pub fn renderRemoveScreen( @@ -122,15 +121,15 @@ pub fn renderRemoveScreen( action_line: []const u8, number_input: []const u8, ) !void { + var writer = style.StyledWriter.init(out, use_color); try renderRemoveScreenViewport( - out, + &writer, reg, rows, idx_width, widths, cursor, checked, - use_color, status_line, action_line, number_input, @@ -139,62 +138,59 @@ pub fn renderRemoveScreen( } pub fn renderRemoveScreenViewport( - out: *std.Io.Writer, + writer: *style.StyledWriter, reg: *registry.Registry, rows: []const SwitchRow, idx_width: usize, widths: SwitchWidths, cursor: ?usize, checked: []const bool, - use_color: bool, status_line: []const u8, action_line: []const u8, number_input: []const u8, viewport: LiveListViewport, ) !void { - try writeTuiPromptLine(out, "Select accounts to delete:", number_input); - try renderRemoveListViewport(out, reg, rows, idx_width, widths, cursor, checked, use_color, viewport); + try writeTuiPromptLine(writer.out, "Select accounts to delete:", number_input); + try renderRemoveListViewport(writer, reg, rows, idx_width, widths, cursor, checked, viewport); if (status_line.len != 0) { - try writeLiveStatusLine(out, status_line, use_color, viewport.max_cols); + try writeLiveStatusLine(writer.out, status_line, writer.color_enabled, viewport.max_cols); } - try writeRemoveTuiFooterBounded(out, use_color, viewport.max_cols); + try writeRemoveTuiFooterBounded(writer.out, writer.color_enabled, viewport.max_cols); if (action_line.len != 0) { - try writeStyledTuiLineBounded(out, if (use_color) actionLineStyle(action_line) else "", action_line, viewport.max_cols); + try writeStyledTuiLineBounded(writer.out, if (writer.color_enabled) actionLineStyle(action_line) else "", action_line, viewport.max_cols); } } pub fn renderSwitchList( - out: *std.Io.Writer, + writer: *style.StyledWriter, reg: *registry.Registry, rows: []const SwitchRow, idx_width: usize, widths: SwitchWidths, cursor: ?usize, - use_color: bool, ) !void { - try renderSwitchListViewport(out, reg, rows, idx_width, widths, cursor, use_color, .{}); + try renderSwitchListViewport(writer, reg, rows, idx_width, widths, cursor, .{}); } pub fn renderSwitchListViewport( - out: *std.Io.Writer, + writer: *style.StyledWriter, reg: *registry.Registry, rows: []const SwitchRow, idx_width: usize, widths: SwitchWidths, cursor: ?usize, - use_color: bool, viewport: LiveListViewport, ) !void { _ = reg; const prefix_width = 2 + idx_width + 1; const table = table_layout.accountTable(table_layout.boundWidths(widths, prefix_width, viewport.max_cols), prefix_width); - try table.writeHeader(out, use_color); + try table.writeHeader(writer); const visible = visibleRowRange(rows.len, viewport); var displayed_counter = dataRowCount(rows[0..visible.start]); for (rows[visible.start..visible.end]) |row| { if (row.is_header) { - try table.writeGroupRow(out, row.account, use_color); + try table.writeGroupRow(writer, row.account); continue; } @@ -208,50 +204,48 @@ pub fn renderSwitchListViewport( idx_width, ); try table.writeDataRow( - out, + writer, prefix, liveAccountCells(row), - if (use_color) switchRowStyle(row, is_cursor, is_active) else "", + switchRowStyle(row, is_cursor, is_active), ); displayed_counter += 1; } } pub fn renderRemoveList( - out: *std.Io.Writer, + writer: *style.StyledWriter, reg: *registry.Registry, rows: []const SwitchRow, idx_width: usize, widths: SwitchWidths, cursor: ?usize, checked: []const bool, - use_color: bool, ) !void { - try renderRemoveListViewport(out, reg, rows, idx_width, widths, cursor, checked, use_color, .{}); + try renderRemoveListViewport(writer, reg, rows, idx_width, widths, cursor, checked, .{}); } pub fn renderRemoveListViewport( - out: *std.Io.Writer, + writer: *style.StyledWriter, reg: *registry.Registry, rows: []const SwitchRow, idx_width: usize, widths: SwitchWidths, cursor: ?usize, checked: []const bool, - use_color: bool, viewport: LiveListViewport, ) !void { _ = reg; const checkbox_width: usize = 3; const prefix_width = 2 + checkbox_width + 1 + idx_width + 1; const table = table_layout.accountTable(table_layout.boundWidths(widths, prefix_width, viewport.max_cols), prefix_width); - try table.writeHeader(out, use_color); + try table.writeHeader(writer); const visible = visibleRowRange(rows.len, viewport); var selectable_counter = dataRowCount(rows[0..visible.start]); for (rows[visible.start..visible.end]) |row| { if (row.is_header) { - try table.writeGroupRow(out, row.account, use_color); + try table.writeGroupRow(writer, row.account); continue; } @@ -267,10 +261,10 @@ pub fn renderRemoveListViewport( idx_width, ); try table.writeDataRow( - out, + writer, prefix, liveAccountCells(row), - if (use_color) removeRowStyle(row, is_cursor, is_checked, is_active) else "", + removeRowStyle(row, is_cursor, is_checked, is_active), ); selectable_counter += 1; } diff --git a/src/cli/style.zig b/src/cli/style.zig index da946c63..cac3b5f1 100644 --- a/src/cli/style.zig +++ b/src/cli/style.zig @@ -1,4 +1,4 @@ -const terminal_color = @import("../terminal/color.zig"); +const std = @import("std"); pub const ansi = struct { pub const reset = "\x1b[0m"; @@ -8,10 +8,34 @@ pub const ansi = struct { pub const cyan = "\x1b[36m"; }; -pub fn stdoutColorEnabled() bool { - return terminal_color.stdoutColorEnabled(); -} +pub const StyledWriter = struct { + out: *std.Io.Writer, + color_enabled: bool, -pub fn stderrColorEnabled() bool { - return terminal_color.stderrColorEnabled(); -} + pub fn init(out: *std.Io.Writer, color_enabled: bool) StyledWriter { + return .{ + .out = out, + .color_enabled = color_enabled, + }; + } + + pub fn writeAll(self: *StyledWriter, bytes: []const u8) !void { + try self.out.writeAll(bytes); + } + + pub fn print(self: *StyledWriter, comptime fmt: []const u8, args: anytype) !void { + try self.out.print(fmt, args); + } + + pub fn writeStyle(self: *StyledWriter, ansi_style: []const u8) !void { + if (self.color_enabled and ansi_style.len != 0) try self.out.writeAll(ansi_style); + } + + pub fn reset(self: *StyledWriter) !void { + if (self.color_enabled) try self.out.writeAll(ansi.reset); + } + + pub fn flush(self: *StyledWriter) !void { + try self.out.flush(); + } +}; diff --git a/src/cli/table_layout.zig b/src/cli/table_layout.zig index 69c37b6a..37702a45 100644 --- a/src/cli/table_layout.zig +++ b/src/cli/table_layout.zig @@ -30,43 +30,43 @@ pub const LiveTable = struct { columns: [column_count]LiveTableColumn, prefix_width: usize, - pub fn writeHeader(self: *const LiveTable, out: *std.Io.Writer, use_color: bool) !void { - if (use_color) try out.writeAll(style.ansi.cyan); - try writeRepeat(out, ' ', self.prefix_width); - try self.writeCells(out, &.{ + pub fn writeHeader(self: *const LiveTable, writer: *style.StyledWriter) !void { + try writer.writeStyle(style.ansi.cyan); + try writeRepeat(writer.out, ' ', self.prefix_width); + try self.writeCells(writer.out, &.{ .{ .text = self.columns[0].header }, .{ .text = self.columns[1].header }, .{ .text = self.columns[2].header }, .{ .text = self.columns[3].header }, .{ .text = self.columns[4].header }, }); - if (use_color) try out.writeAll(style.ansi.reset); - try out.writeAll("\n"); + try writer.reset(); + try writer.writeAll("\n"); } - pub fn writeGroupRow(self: *const LiveTable, out: *std.Io.Writer, account: []const u8, use_color: bool) !void { - if (use_color) try out.writeAll(style.ansi.dim); - try writeRepeat(out, ' ', self.prefix_width); - try writeAccountTruncatedPadded(out, account, self.columns[0].width); - if (use_color) try out.writeAll(style.ansi.reset); - try out.writeAll("\n"); + pub fn writeGroupRow(self: *const LiveTable, writer: *style.StyledWriter, account: []const u8) !void { + try writer.writeStyle(style.ansi.dim); + try writeRepeat(writer.out, ' ', self.prefix_width); + try writeAccountTruncatedPadded(writer.out, account, self.columns[0].width); + try writer.reset(); + try writer.writeAll("\n"); } pub fn writeDataRow( self: *const LiveTable, - out: *std.Io.Writer, + writer: *style.StyledWriter, prefix: []const u8, cells: [column_count]Cell, ansi_style: []const u8, ) !void { - if (ansi_style.len != 0) try out.writeAll(ansi_style); - try out.writeAll(prefix); + try writer.writeStyle(ansi_style); + try writer.writeAll(prefix); if (prefix.len < self.prefix_width) { - try writeRepeat(out, ' ', self.prefix_width - prefix.len); + try writeRepeat(writer.out, ' ', self.prefix_width - prefix.len); } - try self.writeCells(out, &cells); - if (ansi_style.len != 0) try out.writeAll(style.ansi.reset); - try out.writeAll("\n"); + try self.writeCells(writer.out, &cells); + if (ansi_style.len != 0) try writer.reset(); + try writer.writeAll("\n"); } fn writeCells( diff --git a/src/core/io_util.zig b/src/core/io_util.zig index 34431fa7..7e68c147 100644 --- a/src/core/io_util.zig +++ b/src/core/io_util.zig @@ -1,15 +1,35 @@ const std = @import("std"); const app_runtime = @import("runtime.zig"); +const terminal_color = @import("../terminal/color.zig"); pub const Stdout = struct { buffer: [4096]u8 = undefined, writer: std.Io.File.Writer, + color_enabled: bool = false, pub fn init(self: *Stdout) void { - self.writer = std.Io.File.stdout().writer(app_runtime.io(), &self.buffer); + const file = std.Io.File.stdout(); + self.writer = file.writer(app_runtime.io(), &self.buffer); + self.color_enabled = terminal_color.fileColorEnabled(file); } pub fn out(self: *Stdout) *std.Io.Writer { return &self.writer.interface; } }; + +pub const Stderr = struct { + buffer: [4096]u8 = undefined, + writer: std.Io.File.Writer, + color_enabled: bool = false, + + pub fn init(self: *Stderr) void { + const file = std.Io.File.stderr(); + self.writer = file.writer(app_runtime.io(), &self.buffer); + self.color_enabled = terminal_color.fileColorEnabled(file); + } + + pub fn out(self: *Stderr) *std.Io.Writer { + return &self.writer.interface; + } +}; diff --git a/src/terminal/color.zig b/src/terminal/color.zig index eb484164..a300f115 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -1,28 +1,6 @@ const std = @import("std"); const app_runtime = @import("../core/runtime.zig"); -const builtin = @import("builtin"); - -pub fn shouldEnableColor(is_windows: bool, is_tty: bool) bool { - return is_tty and !is_windows; -} - -pub fn stdoutColorEnabled() bool { - return shouldEnableColor( - builtin.os.tag == .windows, - std.Io.File.stdout().isTty(app_runtime.io()) catch false, - ); -} - -pub fn stderrColorEnabled() bool { - return shouldEnableColor( - builtin.os.tag == .windows, - std.Io.File.stderr().isTty(app_runtime.io()) catch false, - ); -} pub fn fileColorEnabled(file: std.Io.File) bool { - return shouldEnableColor( - builtin.os.tag == .windows, - file.isTty(app_runtime.io()) catch false, - ); + return file.isTty(app_runtime.io()) catch false; } diff --git a/src/tui/table.zig b/src/tui/table.zig index 0dd65161..37896bc6 100644 --- a/src/tui/table.zig +++ b/src/tui/table.zig @@ -5,7 +5,6 @@ const display_rows = @import("display.zig"); const registry = @import("../registry/root.zig"); const io_util = @import("../core/io_util.zig"); const rate_limit = @import("rate_limit.zig"); -const terminal_color = @import("../terminal/color.zig"); const timefmt = @import("../time/relative.zig"); const resolveRateWindow = rate_limit.resolveRateWindow; @@ -20,10 +19,6 @@ const ansi = struct { const cyan = "\x1b[36m"; }; -fn colorEnabled() bool { - return terminal_color.stdoutColorEnabled(); -} - fn planDisplay(rec: *const registry.AccountRecord, missing: []const u8) []const u8 { if (rec.auth_mode != null and rec.auth_mode.? == .apikey) return "API_KEY"; if (registry.resolveDisplayPlan(rec)) |p| return registry.planLabel(p); @@ -45,7 +40,7 @@ fn printAccountsTable(reg: *registry.Registry, usage_overrides: ?[]const ?[]cons var stdout: io_util.Stdout = undefined; stdout.init(); const out = stdout.out(); - try writeAccountsTableWithUsageOverrides(out, reg, colorEnabled(), usage_overrides); + try writeAccountsTableWithUsageOverrides(out, reg, stdout.color_enabled, usage_overrides); try out.flush(); } diff --git a/tests/cli_picker_test.zig b/tests/cli_picker_test.zig index f6745051..081693fa 100644 --- a/tests/cli_picker_test.zig +++ b/tests/cli_picker_test.zig @@ -16,15 +16,9 @@ const accountIdForSelectable = cli.picker.accountIdForSelectable; const filterErroredRowsFromSelectableIndices = cli.rows.filterErroredRowsFromSelectableIndices; const renderSwitchScreen = cli.render.renderSwitchScreen; const renderRemoveScreen = cli.render.renderRemoveScreen; -const renderListScreenViewport = cli.render.renderListScreenViewport; -const renderSwitchScreenViewport = cli.render.renderSwitchScreenViewport; -const renderSwitchList = cli.render.renderSwitchList; -const renderRemoveList = cli.render.renderRemoveList; -const renderSwitchListViewport = cli.render.renderSwitchListViewport; -const renderRemoveListViewport = cli.render.renderRemoveListViewport; -const renderRemoveScreenViewport = cli.render.renderRemoveScreenViewport; const liveViewportStartForDisplayIndex = cli.render.liveViewportStartForDisplayIndex; const SwitchWidths = cli.render.SwitchWidths; +const StyledWriter = cli.render.StyledWriter; const buildSwitchRows = cli.rows.buildSwitchRows; const buildSwitchRowsWithUsageOverrides = cli.rows.buildSwitchRowsWithUsageOverrides; const SwitchRow = cli.rows.SwitchRow; @@ -44,6 +38,111 @@ const ansi = struct { const cyan = "\x1b[36m"; }; +fn renderListScreenViewport( + out: *std.Io.Writer, + reg: *registry.Registry, + rows: []const SwitchRow, + idx_width: usize, + widths: SwitchWidths, + use_color: bool, + status_line: []const u8, + viewport: LiveListViewport, +) !void { + var styled = StyledWriter.init(out, use_color); + try cli.render.renderListScreenViewport(&styled, reg, rows, idx_width, widths, status_line, viewport); +} + +fn renderSwitchScreenViewport( + out: *std.Io.Writer, + reg: *registry.Registry, + rows: []const SwitchRow, + idx_width: usize, + widths: SwitchWidths, + selected: ?usize, + use_color: bool, + status_line: []const u8, + action_line: []const u8, + number_input: []const u8, + viewport: LiveListViewport, +) !void { + var styled = StyledWriter.init(out, use_color); + try cli.render.renderSwitchScreenViewport(&styled, reg, rows, idx_width, widths, selected, status_line, action_line, number_input, viewport); +} + +fn renderRemoveScreenViewport( + out: *std.Io.Writer, + reg: *registry.Registry, + rows: []const SwitchRow, + idx_width: usize, + widths: SwitchWidths, + cursor: ?usize, + checked: []const bool, + use_color: bool, + status_line: []const u8, + action_line: []const u8, + number_input: []const u8, + viewport: LiveListViewport, +) !void { + var styled = StyledWriter.init(out, use_color); + try cli.render.renderRemoveScreenViewport(&styled, reg, rows, idx_width, widths, cursor, checked, status_line, action_line, number_input, viewport); +} + +fn renderSwitchList( + out: *std.Io.Writer, + reg: *registry.Registry, + rows: []const SwitchRow, + idx_width: usize, + widths: SwitchWidths, + cursor: ?usize, + use_color: bool, +) !void { + var styled = StyledWriter.init(out, use_color); + try cli.render.renderSwitchList(&styled, reg, rows, idx_width, widths, cursor); +} + +fn renderSwitchListViewport( + out: *std.Io.Writer, + reg: *registry.Registry, + rows: []const SwitchRow, + idx_width: usize, + widths: SwitchWidths, + cursor: ?usize, + use_color: bool, + viewport: LiveListViewport, +) !void { + var styled = StyledWriter.init(out, use_color); + try cli.render.renderSwitchListViewport(&styled, reg, rows, idx_width, widths, cursor, viewport); +} + +fn renderRemoveList( + out: *std.Io.Writer, + reg: *registry.Registry, + rows: []const SwitchRow, + idx_width: usize, + widths: SwitchWidths, + cursor: ?usize, + checked: []const bool, + use_color: bool, +) !void { + var styled = StyledWriter.init(out, use_color); + try cli.render.renderRemoveList(&styled, reg, rows, idx_width, widths, cursor, checked); +} + +fn renderRemoveListViewport( + out: *std.Io.Writer, + reg: *registry.Registry, + rows: []const SwitchRow, + idx_width: usize, + widths: SwitchWidths, + cursor: ?usize, + checked: []const bool, + use_color: bool, + viewport: LiveListViewport, +) !void { + var styled = StyledWriter.init(out, use_color); + try cli.render.renderRemoveListViewport(&styled, reg, rows, idx_width, widths, cursor, checked, viewport); +} + test "Scenario: Given q quit input when checking switch picker helpers then both line and key shortcuts cancel selection" { try std.testing.expect(isQuitInput("q")); try std.testing.expect(isQuitInput("Q")); diff --git a/tests/table_layout_test.zig b/tests/table_layout_test.zig index cf2985a6..feec7c16 100644 --- a/tests/table_layout_test.zig +++ b/tests/table_layout_test.zig @@ -4,8 +4,22 @@ const codex_auth = @import("codex_auth"); const cli = codex_auth.cli; const registry = codex_auth.registry; -const renderListScreenViewport = cli.render.renderListScreenViewport; const SwitchRow = cli.rows.SwitchRow; +const StyledWriter = cli.render.StyledWriter; + +fn renderListScreenViewport( + out: *std.Io.Writer, + reg: *registry.Registry, + rows: []const SwitchRow, + idx_width: usize, + widths: cli.render.SwitchWidths, + use_color: bool, + status_line: []const u8, + viewport: cli.render.LiveListViewport, +) !void { + var styled = StyledWriter.init(out, use_color); + try cli.render.renderListScreenViewport(&styled, reg, rows, idx_width, widths, status_line, viewport); +} fn makeTestRegistry() registry.Registry { return .{ diff --git a/tests/terminal_color_test.zig b/tests/terminal_color_test.zig deleted file mode 100644 index 4288076a..00000000 --- a/tests/terminal_color_test.zig +++ /dev/null @@ -1,11 +0,0 @@ -const std = @import("std"); -const terminal_color = @import("codex_auth").terminal.color; - -const shouldEnableColor = terminal_color.shouldEnableColor; - -test "Scenario: Given color support inputs when deciding ANSI output then Windows stays disabled" { - try std.testing.expect(!shouldEnableColor(true, true)); - try std.testing.expect(!shouldEnableColor(true, false)); - try std.testing.expect(shouldEnableColor(false, true)); - try std.testing.expect(!shouldEnableColor(false, false)); -}