From 68823e481614c722490ba332a010144f6ed69bc7 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 18 Jul 2026 12:35:17 -0700 Subject: [PATCH] split tui/start fixing some functional issues --- .kiro/specs/calculator/design.md | 47 ++- .kiro/specs/calculator/tasks.md | 30 +- engine/src/formatter.zig | 23 +- src/main.zig | 9 +- src/tui.zig | 557 ++++++++++++++----------------- src/tui/draw.zig | 65 ++++ src/tui/help.zig | 73 ++++ src/tui/programmer.zig | 111 ++++++ 8 files changed, 568 insertions(+), 347 deletions(-) create mode 100644 src/tui/draw.zig create mode 100644 src/tui/help.zig create mode 100644 src/tui/programmer.zig diff --git a/.kiro/specs/calculator/design.md b/.kiro/specs/calculator/design.md index 1e5ce64..fdf75ea 100644 --- a/.kiro/specs/calculator/design.md +++ b/.kiro/specs/calculator/design.md @@ -205,15 +205,14 @@ Values are displayed with visual grouping separators to aid readability, but cli | Base | Display (value view) | Display (memory view) | Clipboard | |------|---------------------|----------------------|-----------| -| Hex | `0xFFFF_FFFF` | `FF FF FF FF` | `0xFFFFFFFF` | +| Hex | `FF FF FF FF` | `FF FF FF FF` | `0xFFFFFFFF` | | Bin | `1111 1111 1111 1111 1111 1111 1111 1111` | same | `0b11111111111111111111111111111111` | | Oct | `0o37_777_777_777` | - | `0o37777777777` | | Dec | `4,294,967,295` | - | `4294967295` | Rationale for hex grouping: -- **Value view uses underscore per 16-bit word** (`0xFFFF_FFFF`): matches Zig/Rust literal syntax - this is a programmer tool and values should look like code you'd actually type. Compact, familiar. -- **Memory/byte view uses spaces per byte** (`FF FF FF FF`): matches hex editors, debuggers, `xxd` output. Used in struct visualizer and bit grid context. -- These are two different views of the same data: the *value* (what you'd write in source code) vs. the *memory* (how it sits in RAM). +- **Display uses space per byte** (`FF FF FF FF`): matches hex editors, debuggers, `xxd` output. Consistent with binary's nibble-space grouping. Universally readable. +- These are two different views of the same data: the *value* (what you'd write in source code) vs. the *memory* (how it sits in RAM). For this calculator, both use the same space-separated format. Binary always groups by nibble (4 bits) with spaces - universally expected by programmers. @@ -595,9 +594,39 @@ $ tally "5 kg + 3 lb" │ > 0xFF & 0x0F_ │ │ = 15 │ │ │ -└──── ←→:nav Space:toggle w:width s:sign e:endian y:yank ── Tab:mode ────────┘ +└──── arrows:nav Space:toggle w:width s:sign e:endian y:yank -- Tab:mode ----┘ ``` +**NOTE: The wireframe above (8.2) is superseded by the editable-fields +design below. Keeping the old wireframe for reference during transition.** + +#### 8.2.1 Programmer Mode - Editable Fields (REVISED DESIGN) + +The programmer mode is an **editable multi-field form** where every +representation of the value is live-editable. A change in any field +instantly updates all others. Closer to integer.exposed than a +traditional calculator with a prompt. + +Fields (navigable with Up/Down): +1. Bit grid - arrow keys move, Space toggles individual bits +2. DEC(s) - editable signed decimal +3. DEC(u) - editable unsigned decimal +4. HEX - editable hex (space-separated bytes: FF 00 AB CD) +5. OCT - editable octal +6. BIN - editable binary (nibble-separated: 1111 0000) +7. Expression - full expression evaluator (& | ^ ~ << >> etc.) + +Navigation: +- Up/Down moves focus between fields +- When a text field is focused, typing replaces its content; + value updates on Enter (or live as you type - TBD) +- When bit grid is focused: left/right/up/down navigate, Space toggles +- Tab cycles between Standard/Programmer mode +- Ctrl-W cycles bit width (8/16/32/64) + +This replaces the current model where there's a passive display + +separate input prompt at the bottom. + ### 8.3 Struct Visualizer (sub-view of Programmer Mode) ``` ┌─ Tally ────────────────── [Programmer > Struct Layout] ─────────────┐ @@ -670,7 +699,7 @@ Classic calculator layout. Expression builds at top, result previews live below │ ├─────┼─────┼─────┼─────┼─────┤ │ │ │ │ 4 │ 5 │ 6 │ × │ % │ │ │ │ ├─────┼─────┼─────┼─────┼─────┤ │ │ -│ │ 1 │ 2 │ 3 │ − │ √ │ │ │ +│ │ 1 │ 2 │ 3 │ - │ sqrt│ │ │ │ ├─────┼─────┼─────┼─────┼─────┤ │ │ │ │ 0 │ . │ Ans │ + │ = │ │ │ │ └─────┴─────┴─────┴─────┴─────┘ │ │ @@ -698,7 +727,7 @@ Split into three vertical zones: **value display**, **bit grid**, and **input bu │ │ DEC 4,294,967,295 │ │ ← tappable to switch primary │ │ HEX 0xFFFF_FFFF │ │ │ │ OCT 0o37_777_777_777 │ │ -│ │ BIN 1111 1111 1111 1111 111… │ │ +│ │ BIN 1111 1111 1111 1111 11.. │ │ │ │ SIGN -1 (signed) │ │ │ └──────────────────────────────────┘ │ │ │ @@ -814,8 +843,8 @@ Card-based interface with function selector at top. │ │ CAGR = 20.11% │ │ │ │ │ │ │ │ Formula: │ │ -│ │ (25000/10000)^(1/5) − 1 │ │ -│ │ = 2.5^0.2 − 1 │ │ +│ │ (25000/10000)^(1/5) - 1 │ │ +│ │ = 2.5^0.2 - 1 │ │ │ │ = 0.2011... │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────────┘ diff --git a/.kiro/specs/calculator/tasks.md b/.kiro/specs/calculator/tasks.md index bc64e14..f894d76 100644 --- a/.kiro/specs/calculator/tasks.md +++ b/.kiro/specs/calculator/tasks.md @@ -231,16 +231,30 @@ Subcommands deferred until Phase 2 engine work is done. - Variables persist across expressions (assignment + Ans) - Verify: can type expressions, see results, browse history -### Task 5.3: Implement programmer mode TUI view - expression & base display -- Create `tui/src/views/programmer.zig` -- Expression input (same as standard but in programmer mode) +### Task 5.3: Implement programmer mode TUI view - expression & base display [PARTIAL] +- Programmer mode accessible via Tab key - Multi-base result display (dec signed, unsigned, hex, oct, bin) -- Quick-toggle keys: `d` highlights dec, `h` highlights hex, `o` oct, `b` bin -- Status bar showing current bit width, signedness, endianness -- Width toggle (`w` key cycles 8->16->32->64), sign toggle (`s`), endian toggle (`e`) -- Verify: expressions evaluate in programmer mode, base display updates, toggles work +- Bit grid with navigation (arrows) and toggling (Space) +- Width toggle (Ctrl-W cycles 8->16->32->64) +- Ctrl-B toggles focus between input and bit grid +- Expression evaluation through programmer engine +- DONE: basic passive display + expression input + bit grid toggling +- NOT DONE: editable fields (see Task 5.3.1) -### Task 5.4: Implement programmer mode TUI view - bit grid +### Task 5.3.1: Programmer mode editable fields rework [NOT STARTED] +- Replace passive display + input prompt with navigable editable fields +- Each base representation (DEC signed, DEC unsigned, HEX, OCT, BIN) is an + editable text field +- Up/Down arrows move focus between fields (bit grid, dec, hex, oct, bin, expression) +- Typing in any field parses the value in that base and updates all other fields +- Bit grid is one of the navigable fields (not a separate mode toggled with Ctrl-B) +- Expression field at the bottom for full programmer-mode expressions +- Remove Ctrl-B keybinding (field navigation replaces it) +- HEX field accepts/displays space-separated bytes (e.g. "FF 00 AB CD") +- Verify: can arrow between fields, edit hex and see decimal update, toggle bits + and see all fields update, evaluate expression and see result in all fields + +### Task 5.4: Implement programmer mode TUI view - bit grid [DONE - merged into 5.3] - Bit grid widget: 8 bits per group, groups separated by space - Arrow key navigation (left/right moves cursor across bits, up/down between rows for 32/64-bit) - Space/Enter toggles the bit under cursor diff --git a/engine/src/formatter.zig b/engine/src/formatter.zig index 29fba34..394d3b5 100644 --- a/engine/src/formatter.zig +++ b/engine/src/formatter.zig @@ -55,8 +55,8 @@ pub fn formatFloat(buf: []u8, value: f64) FormattedValue { } /// Format an integer for programmer mode hex display. -/// Display: "0xFFFF_FFFF" (underscore per 16-bit word) -/// Raw: "0xFFFFFFFF" (no separators) +/// Display: "FF FF FF FF" (space per byte) +/// Raw: "0xFFFFFFFF" (no separators, with prefix) pub fn formatHex(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { const width = bit_width.bits(); const hex_digits: usize = @as(usize, width) / 4; @@ -74,15 +74,12 @@ pub fn formatHex(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { } const raw = buf[0..pos]; - // Write display after raw: "0x" + hex digits with _ every 4 digits (16-bit word boundary) + // Write display after raw: hex digits with space per byte (every 2 digits) const display_start = pos; - buf[pos] = '0'; - buf[pos + 1] = 'x'; - pos += 2; i = 0; while (i < hex_digits) : (i += 1) { - if (i > 0 and i % 4 == 0) { - buf[pos] = '_'; + if (i > 0 and i % 2 == 0) { + buf[pos] = ' '; pos += 1; } const shift_amt: u6 = @intCast((hex_digits - 1 - i) * 4); @@ -362,35 +359,35 @@ test "formatFloat: zero" { test "formatHex: 8-bit" { var buf: [256]u8 = undefined; const result = formatHex(&buf, 0xFF, .bits8); - try testing.expectEqualStrings("0xFF", result.display); + try testing.expectEqualStrings("FF", result.display); try testing.expectEqualStrings("0xFF", result.raw); } test "formatHex: 16-bit" { var buf: [256]u8 = undefined; const result = formatHex(&buf, 0xABCD, .bits16); - try testing.expectEqualStrings("0xABCD", result.display); + try testing.expectEqualStrings("AB CD", result.display); try testing.expectEqualStrings("0xABCD", result.raw); } test "formatHex: 32-bit with grouping" { var buf: [256]u8 = undefined; const result = formatHex(&buf, 0xDEADBEEF, .bits32); - try testing.expectEqualStrings("0xDEAD_BEEF", result.display); + try testing.expectEqualStrings("DE AD BE EF", result.display); try testing.expectEqualStrings("0xDEADBEEF", result.raw); } test "formatHex: 64-bit with grouping" { var buf: [256]u8 = undefined; const result = formatHex(&buf, 0xDEAD_BEEF_CAFE_BABE, .bits64); - try testing.expectEqualStrings("0xDEAD_BEEF_CAFE_BABE", result.display); + try testing.expectEqualStrings("DE AD BE EF CA FE BA BE", result.display); try testing.expectEqualStrings("0xDEADBEEFCAFEBABE", result.raw); } test "formatHex: zero 32-bit" { var buf: [256]u8 = undefined; const result = formatHex(&buf, 0, .bits32); - try testing.expectEqualStrings("0x0000_0000", result.display); + try testing.expectEqualStrings("00 00 00 00", result.display); try testing.expectEqualStrings("0x00000000", result.raw); } diff --git a/src/main.zig b/src/main.zig index 8d99ca1..1172f75 100644 --- a/src/main.zig +++ b/src/main.zig @@ -79,25 +79,26 @@ fn formatProgrammerResult(buf: []u8, result: engine.types.Integer, config: engin const value = result.unsignedValue(); const signed = result.signedValue(); - // Format into sections of buf - // We'll build the output string piece by piece var hex_buf: [256]u8 = undefined; var dec_buf: [256]u8 = undefined; var sdec_buf: [256]u8 = undefined; + var oct_buf: [256]u8 = undefined; var bin_buf: [512]u8 = undefined; const hex = engine.formatter.formatHex(&hex_buf, value, config.bit_width); const dec = engine.formatter.formatDecimalUnsigned(&dec_buf, value); const sdec = engine.formatter.formatDecimalSigned(&sdec_buf, signed); + const oct = engine.formatter.formatOctal(&oct_buf, value, config.bit_width); const bin = engine.formatter.formatBinary(&bin_buf, value, config.bit_width); const output = std.fmt.bufPrint(buf, \\ dec(signed): {s} \\ dec(unsigned): {s} \\ hex: {s} + \\ oct: {s} \\ bin: {s} \\ - , .{ sdec.display, dec.display, hex.display, bin.display }) catch { + , .{ sdec.display, dec.display, hex.display, oct.display, bin.display }) catch { return .{ .output = "error: buffer overflow\n", .is_error = true }; }; @@ -306,7 +307,7 @@ test "evaluate: programmer mode" { const result = evaluate(arena.allocator(), "0xFF & 0x0F", .programmer, &buf); try testing.expect(!result.is_error); try testing.expect(std.mem.indexOf(u8, result.output, "15") != null); - try testing.expect(std.mem.indexOf(u8, result.output, "0x0000_0000_0000_000F") != null); + try testing.expect(std.mem.indexOf(u8, result.output, "00 00 00 00 00 00 00 0F") != null); } test "evaluate: division by zero error" { diff --git a/src/tui.zig b/src/tui.zig index 047403b..d8c5982 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -1,11 +1,23 @@ +//! Tally TUI - interactive calculator interface. +//! +//! Split into sub-modules: +//! - draw.zig: shared drawing primitives and color palette +//! - tui/programmer.zig: programmer mode layout +//! - tui/help.zig: help overlay + const std = @import("std"); const vaxis = @import("vaxis"); const vxfw = vaxis.vxfw; const engine = @import("engine"); +const draw = @import("tui/draw.zig"); +const programmer_view = @import("tui/programmer.zig"); +const help_view = @import("tui/help.zig"); +const C = draw.C; const Allocator = std.mem.Allocator; -/// TUI application state. +const Mode = enum { standard, programmer }; + pub const App = struct { allocator: Allocator, io: std.Io, @@ -13,14 +25,16 @@ pub const App = struct { input: vxfw.TextField, history: std.ArrayList(HistoryEntry), show_help: bool, - /// Index into history for up/down navigation. - /// null means we're on a fresh input (not browsing history). - /// 0 = most recent entry, 1 = second most recent, etc. history_browse_idx: ?usize, - /// Saved current input when user starts browsing history saved_input: ?[]const u8, + mode: Mode, + // Programmer mode state + prog_value: u64, + prog_config: engine.types.ProgrammerConfig, + bit_cursor: u6, + bit_grid_active: bool, - const HistoryEntry = struct { + pub const HistoryEntry = struct { expr: []const u8, result: []const u8, is_error: bool, @@ -28,7 +42,7 @@ pub const App = struct { pub fn init(allocator: Allocator, io: std.Io) App { var text_field = vxfw.TextField.init(allocator); - text_field.style = .{ .fg = .{ .index = 15 } }; + text_field.style = .{ .fg = C.fg }; return .{ .allocator = allocator, .io = io, @@ -38,6 +52,11 @@ pub const App = struct { .show_help = false, .history_browse_idx = null, .saved_input = null, + .mode = .standard, + .prog_value = 0, + .prog_config = .{}, + .bit_cursor = 0, + .bit_grid_active = false, }; } @@ -70,18 +89,12 @@ pub const App = struct { } fn handleKey(self: *App, ctx: *vxfw.EventContext, key: vaxis.Key) !void { - if (key.matches('c', .{ .ctrl = true })) { - ctx.quit = true; - return; - } - if (key.matches('d', .{ .ctrl = true })) { + if (key.matches('c', .{ .ctrl = true }) or key.matches('d', .{ .ctrl = true })) { ctx.quit = true; return; } - // Help toggle if (self.show_help) { - // Any key dismisses help self.show_help = false; ctx.redraw = true; return; @@ -93,13 +106,57 @@ pub const App = struct { return; } + if (key.matches(vaxis.Key.tab, .{})) { + self.mode = if (self.mode == .standard) .programmer else .standard; + self.env.mode = if (self.mode == .standard) .standard else .programmer; + if (self.mode == .programmer) { + const ans = self.env.ans; + if (ans >= 0 and ans == @trunc(ans) and ans < 18446744073709551616.0) { + self.prog_value = @intFromFloat(ans); + } else if (ans < 0 and ans == @trunc(ans) and ans >= -9223372036854775808.0) { + const signed: i64 = @intFromFloat(ans); + self.prog_value = @bitCast(signed); + } + self.prog_value &= self.prog_config.bit_width.mask(); + } + ctx.redraw = true; + return; + } + + if (self.mode == .programmer and self.bit_grid_active) { + if (self.handleBitGridKey(key)) { + ctx.redraw = true; + return; + } + } + + if (self.mode == .programmer and key.matches('b', .{ .ctrl = true })) { + self.bit_grid_active = !self.bit_grid_active; + ctx.redraw = true; + return; + } + + if (self.mode == .programmer and key.matches('w', .{ .ctrl = true })) { + self.prog_config.bit_width = switch (self.prog_config.bit_width) { + .bits8 => .bits16, + .bits16 => .bits32, + .bits32 => .bits64, + .bits64 => .bits8, + }; + self.prog_value &= self.prog_config.bit_width.mask(); + if (self.bit_cursor >= self.prog_config.bit_width.bits()) { + self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1); + } + ctx.redraw = true; + return; + } + if (key.matches(vaxis.Key.enter, .{})) { try self.submitExpression(); ctx.redraw = true; return; } - // Ctrl-L: clear history if (key.matches('l', .{ .ctrl = true })) { for (self.history.items) |entry| { self.allocator.free(entry.expr); @@ -115,52 +172,77 @@ pub const App = struct { return; } - // Up arrow: browse history (older) - if (key.matches(vaxis.Key.up, .{})) { - if (self.history.items.len == 0) return; - if (self.history_browse_idx) |idx| { - // Already browsing, go further back - if (idx + 1 < self.history.items.len) { - self.history_browse_idx = idx + 1; - self.setInputFromHistory(idx + 1); - } - } else { - // Start browsing: save current input, show most recent - try self.saveCurrentInput(); - self.history_browse_idx = 0; - self.setInputFromHistory(0); - } - ctx.redraw = true; - return; - } - - // Down arrow: browse history (newer) - if (key.matches(vaxis.Key.down, .{})) { - if (self.history_browse_idx) |idx| { - if (idx == 0) { - // Back to the saved input - self.history_browse_idx = null; - self.restoreSavedInput(); + if (!self.bit_grid_active) { + if (key.matches(vaxis.Key.up, .{})) { + if (self.history.items.len == 0) return; + if (self.history_browse_idx) |idx| { + if (idx + 1 < self.history.items.len) { + self.history_browse_idx = idx + 1; + self.setInputFromHistory(idx + 1); + } } else { - self.history_browse_idx = idx - 1; - self.setInputFromHistory(idx - 1); + try self.saveCurrentInput(); + self.history_browse_idx = 0; + self.setInputFromHistory(0); } ctx.redraw = true; + return; + } + if (key.matches(vaxis.Key.down, .{})) { + if (self.history_browse_idx) |idx| { + if (idx == 0) { + self.history_browse_idx = null; + self.restoreSavedInput(); + } else { + self.history_browse_idx = idx - 1; + self.setInputFromHistory(idx - 1); + } + ctx.redraw = true; + } + return; } - return; } - // Pass all other keys to the text field for editing try self.input.handleEvent(ctx, .{ .key_press = key }); - // Any edit resets history browsing self.history_browse_idx = null; ctx.redraw = true; } + fn handleBitGridKey(self: *App, key: vaxis.Key) bool { + const width = self.prog_config.bit_width.bits(); + // bits_per_row matches the drawing layout + const bits_per_row: u8 = if (width > 32) 32 else width; + if (key.matches(vaxis.Key.left, .{})) { + if (self.bit_cursor < width - 1) self.bit_cursor += 1; + return true; + } + if (key.matches(vaxis.Key.right, .{})) { + if (self.bit_cursor > 0) self.bit_cursor -= 1; + return true; + } + if (key.matches(vaxis.Key.up, .{})) { + if (@as(u8, self.bit_cursor) + bits_per_row < width) self.bit_cursor += @intCast(bits_per_row); + return true; + } + if (key.matches(vaxis.Key.down, .{})) { + if (self.bit_cursor >= bits_per_row) self.bit_cursor -= @intCast(bits_per_row); + return true; + } + if (key.matches(' ', .{}) or key.matches(vaxis.Key.enter, .{})) { + self.prog_value ^= @as(u64, 1) << self.bit_cursor; + self.prog_value &= self.prog_config.bit_width.mask(); + return true; + } + if (key.matches(vaxis.Key.escape, .{})) { + self.bit_grid_active = false; + return true; + } + return false; + } + fn submitExpression(self: *App) !void { const first = self.input.buf.firstHalf(); const second = self.input.buf.secondHalf(); - const expr_text = try self.allocator.alloc(u8, first.len + second.len); @memcpy(expr_text[0..first.len], first); @memcpy(expr_text[first.len..], second); @@ -170,36 +252,11 @@ pub const App = struct { return; } - // Evaluate directly through our persistent environment - const is_error, const display_text = if (engine.evalString(&self.env, self.allocator, expr_text)) |value| blk: { - var fmt_buf: [4096]u8 = undefined; - const formatted = engine.formatter.formatFloat(&fmt_buf, value); - break :blk .{ false, try self.allocator.dupe(u8, formatted.display) }; - } else |err| blk: { - break :blk .{ true, @as([]const u8, switch (err) { - engine.CalcError.DivisionByZero => "error: division by zero", - engine.CalcError.UnknownFunction => "error: unknown function", - engine.CalcError.UnknownVariable => "error: unknown variable", - engine.CalcError.UnmatchedParen => "error: unmatched parenthesis", - engine.CalcError.UnexpectedToken => "error: unexpected token", - engine.CalcError.UnexpectedEnd => "error: unexpected end of expression", - engine.CalcError.InvalidNumber => "error: invalid number", - engine.CalcError.DomainError => "error: domain error", - engine.CalcError.Overflow => "error: overflow", - else => "error: evaluation error", - }) }; - }; - - const result_copy = if (is_error) - try self.allocator.dupe(u8, display_text) - else - display_text; - - try self.history.append(self.allocator, .{ - .expr = expr_text, - .result = result_copy, - .is_error = is_error, - }); + if (self.mode == .programmer) { + try self.submitProgrammer(expr_text); + } else { + try self.submitStandard(expr_text); + } self.input.clearRetainingCapacity(); self.history_browse_idx = null; @@ -209,6 +266,33 @@ pub const App = struct { } } + fn submitStandard(self: *App, expr_text: []const u8) !void { + const is_error, const display_text = if (engine.evalString(&self.env, self.allocator, expr_text)) |value| blk: { + var fmt_buf: [4096]u8 = undefined; + const formatted = engine.formatter.formatFloat(&fmt_buf, value); + break :blk .{ false, try self.allocator.dupe(u8, formatted.display) }; + } else |err| blk: { + break :blk .{ true, try self.allocator.dupe(u8, errorStr(err)) }; + }; + + try self.history.append(self.allocator, .{ .expr = expr_text, .result = display_text, .is_error = is_error }); + } + + fn submitProgrammer(self: *App, expr_text: []const u8) !void { + const is_error, const display_text = if (engine.evalProgrammerString(self.allocator, expr_text, self.prog_config)) |int| blk: { + self.prog_value = int.unsignedValue(); + var buf: [256]u8 = undefined; + const dec = engine.formatter.formatDecimalUnsigned(&buf, int.unsignedValue()); + break :blk .{ false, try self.allocator.dupe(u8, dec.display) }; + } else |err| blk: { + break :blk .{ true, try self.allocator.dupe(u8, errorStr(err)) }; + }; + + try self.history.append(self.allocator, .{ .expr = expr_text, .result = display_text, .is_error = is_error }); + } + + // -- History helpers -- + fn saveCurrentInput(self: *App) !void { if (self.saved_input) |s| self.allocator.free(s); const first = self.input.buf.firstHalf(); @@ -220,7 +304,6 @@ pub const App = struct { } fn setInputFromHistory(self: *App, browse_idx: usize) void { - // browse_idx 0 = most recent (last item), 1 = second most recent, etc. const actual_idx = self.history.items.len - 1 - browse_idx; const entry = self.history.items[actual_idx]; self.input.clearRetainingCapacity(); @@ -236,269 +319,117 @@ pub const App = struct { } } + // -- Drawing -- + fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { const self: *App = @ptrCast(@alignCast(ptr)); const width = ctx.max.width orelse 80; const height = ctx.max.height orelse 24; - var surface = try vxfw.Surface.init(ctx.arena, self.widget(), .{ - .width = width, - .height = height, - }); + var surface = try vxfw.Surface.init(ctx.arena, self.widget(), .{ .width = width, .height = height }); if (self.show_help) { - self.drawHelp(&surface, width, height); + help_view.drawHelp(&surface, width, height); return surface; } - // -- Title bar (row 0) -- Molokai cyan on dark - const title_style: vaxis.Style = .{ - .fg = .{ .rgb = .{ 0x66, 0xD9, 0xEF } }, // molokai cyan - .bg = .{ .rgb = .{ 0x1B, 0x1D, 0x1E } }, // molokai bg - .bold = true, - }; - const title_mode_style: vaxis.Style = .{ - .fg = .{ .rgb = .{ 0xA6, 0xE2, 0x2E } }, // molokai green - .bg = .{ .rgb = .{ 0x1B, 0x1D, 0x1E } }, - }; - self.fillRow(&surface, 0, ' ', title_style); - self.writeStr(&surface, 0, 1, "Tally", title_style); - const mode_str = "[Standard]"; - const mode_col = width -| @as(u16, @intCast(mode_str.len + 1)); - self.writeStr(&surface, 0, mode_col, mode_str, title_mode_style); + // Title bar + draw.fillRow(&surface, 0, ' ', .{ .fg = C.cyan, .bg = C.bg, .bold = true }); + draw.writeStr(&surface, 0, 1, "Tally", .{ .fg = C.cyan, .bg = C.bg, .bold = true }); - // -- History (rows 2 to height-4) -- - const hist_start: u16 = 2; - const hist_end: u16 = height -| 4; + // Mode tabs + const std_style: vaxis.Style = if (self.mode == .standard) + .{ .fg = C.bg, .bg = C.green, .bold = true } + else + .{ .fg = C.muted, .bg = C.bg }; + const prog_style: vaxis.Style = if (self.mode == .programmer) + .{ .fg = C.bg, .bg = C.orange, .bold = true } + else + .{ .fg = C.muted, .bg = C.bg }; + const tab_col = width -| 26; + draw.writeStr(&surface, 0, tab_col, " Standard ", std_style); + draw.writeStr(&surface, 0, tab_col + 10, " Programmer ", prog_style); - if (self.history.items.len > 0) { - const avail_rows = hist_end -| hist_start; - const max_entries = avail_rows / 2; - const start_idx = if (self.history.items.len > max_entries) - self.history.items.len - max_entries - else - 0; - - var row: u16 = hist_start; - for (self.history.items[start_idx..]) |entry| { - if (row + 1 >= hist_end) break; - // Expression in muted text - self.writeStr(&surface, row, 2, entry.expr, .{ - .fg = .{ .rgb = .{ 0x75, 0x71, 0x5E } }, // molokai muted - }); - row += 1; - // Result: green for success, pink for error - const res_style: vaxis.Style = if (entry.is_error) - .{ .fg = .{ .rgb = .{ 0xF9, 0x26, 0x72 } } } // molokai pink - else - .{ .fg = .{ .rgb = .{ 0xA6, 0xE2, 0x2E } }, .bold = true }; // molokai green - self.writeStr(&surface, row, 4, "= ", res_style); - self.writeStr(&surface, row, 6, entry.result, res_style); - row += 1; - } + if (self.mode == .programmer) { + programmer_view.drawProgrammerMode(self, &surface, width, height); + } else { + self.drawStandardMode(&surface, width, height); } - // -- Separator (row height-3) -- - const sep_row = height -| 3; - self.fillRow(&surface, sep_row, '-', .{ .fg = .{ .rgb = .{ 0x46, 0x48, 0x3E } } }); // molokai comment-ish - - // -- Input line (row height-2) -- - const input_row = height -| 2; - self.writeStr(&surface, input_row, 1, "> ", .{ - .fg = .{ .rgb = .{ 0xF9, 0x26, 0x72 } }, // molokai pink prompt - .bold = true, - }); - - const first = self.input.buf.firstHalf(); - const second = self.input.buf.secondHalf(); - const input_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0xF8, 0xF8, 0xF2 } } }; // molokai fg - var col: u16 = 3; - for (first) |byte| { - self.writeChar(&surface, input_row, col, byte, input_style); - col += 1; - } - for (second) |byte| { - self.writeChar(&surface, input_row, col, byte, input_style); - col += 1; - } - - // Cursor - const cursor_col: u16 = 3 + @as(u16, @intCast(self.input.buf.cursor)); - surface.cursor = .{ .col = cursor_col, .row = input_row, .shape = .block }; - - // -- Status bar (row height-1) -- - const status_row = height -| 1; - const status_style: vaxis.Style = .{ - .fg = .{ .rgb = .{ 0x75, 0x71, 0x5E } }, // molokai muted - .bg = .{ .rgb = .{ 0x1B, 0x1D, 0x1E } }, // molokai bg - }; - self.fillRow(&surface, status_row, ' ', status_style); - self.writeStr(&surface, status_row, 1, "?:help | Enter:eval | Ctrl-L:clear | Ctrl-C:quit", status_style); - return surface; } - fn drawHelp(self: *App, surface: *vxfw.Surface, width: u16, height: u16) void { - const header_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0x66, 0xD9, 0xEF } }, .bold = true }; // cyan - const key_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0xE6, 0xDB, 0x74 } } }; // yellow - const text_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0xF8, 0xF8, 0xF2 } } }; // fg - const section_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0xAE, 0x81, 0xFF } }, .bold = true }; // purple - - // Background - for (0..height) |r| { - self.fillRow(surface, @intCast(r), ' ', .{}); - } - - var row: u16 = 1; - self.writeStr(surface, row, 2, "Tally - Help", header_style); - row += 2; - - // Keybindings - self.writeStr(surface, row, 2, "Keybindings", section_style); - row += 1; - self.writeStr(surface, row, 4, "Enter", key_style); - self.writeStr(surface, row, 14, "Evaluate expression", text_style); - row += 1; - self.writeStr(surface, row, 4, "Ctrl-C", key_style); - self.writeStr(surface, row, 14, "Quit", text_style); - row += 1; - self.writeStr(surface, row, 4, "Ctrl-D", key_style); - self.writeStr(surface, row, 14, "Quit", text_style); - row += 1; - self.writeStr(surface, row, 4, "Ctrl-L", key_style); - self.writeStr(surface, row, 14, "Clear history", text_style); - row += 1; - self.writeStr(surface, row, 4, "?", key_style); - self.writeStr(surface, row, 14, "Toggle this help", text_style); - row += 2; - - // Variables - self.writeStr(surface, row, 2, "Variables", section_style); - row += 1; - self.writeStr(surface, row, 4, "Ans", key_style); - self.writeStr(surface, row, 14, "Last result", text_style); - row += 1; - self.writeStr(surface, row, 4, "X = expr", key_style); - self.writeStr(surface, row, 14, "Assign to variable (A-Z, any name)", text_style); - row += 2; - - // Constants - self.writeStr(surface, row, 2, "Constants", section_style); - row += 1; - self.writeStr(surface, row, 4, "pi", key_style); - self.writeStr(surface, row, 14, "3.14159...", text_style); - self.writeStr(surface, row, 30, "e", key_style); - self.writeStr(surface, row, 36, "2.71828...", text_style); - self.writeStr(surface, row, 52, "tau", key_style); - self.writeStr(surface, row, 60, "6.28318...", text_style); - row += 2; - - // Functions - self.writeStr(surface, row, 2, "Functions", section_style); - row += 1; - const func_lines = [_][]const u8{ - "sin cos tan asin acos atan atan2", - "log (base-10) ln (natural) log2 exp", - "sqrt cbrt abs ceil floor round", - "factorial max(a,b) min(a,b)", - }; - for (func_lines) |line| { - if (row >= height -| 2) break; - self.writeStr(surface, row, 4, line, text_style); - row += 1; - } - row += 1; - - // Operators - if (row < height -| 4) { - self.writeStr(surface, row, 2, "Operators", section_style); - row += 1; - self.writeStr(surface, row, 4, "+ - * / % ^ (power)", text_style); - row += 1; - self.writeStr(surface, row, 4, "Implicit mul: 2pi 3(4+5) (2)(3)", text_style); - row += 2; - } - - // Syntax - if (row < height -| 3) { - self.writeStr(surface, row, 2, "Number formats", section_style); - row += 1; - self.writeStr(surface, row, 4, "Decimal: 1_000_000 Hex: 0xFF Bin: 0b1010 Oct: 0o77", text_style); - row += 1; - } - - // Footer - const footer_row = height -| 1; - const footer_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0x75, 0x71, 0x5E } } }; // molokai muted - self.fillRow(surface, footer_row, ' ', footer_style); - self.writeStr(surface, footer_row, 1, "Press any key to return", footer_style); - + fn drawStandardMode(self: *App, surface: *vxfw.Surface, width: u16, height: u16) void { _ = width; + drawHistory(self.history.items, surface, 2, height -| 4); + draw.fillRow(surface, height -| 3, '-', .{ .fg = C.dim }); + self.drawInput(surface, height -| 2); + draw.fillRow(surface, height -| 1, ' ', .{ .fg = C.muted, .bg = C.bg }); + draw.writeStr(surface, height -| 1, 1, "?:help | Tab:mode | Enter:eval | Ctrl-L:clear | Ctrl-C:quit", .{ .fg = C.muted, .bg = C.bg }); } - // -- Drawing helpers -- + pub fn drawInput(self: *App, surface: *vxfw.Surface, row: u16) void { + const prompt_style: vaxis.Style = if (self.bit_grid_active) + .{ .fg = C.dim } + else + .{ .fg = C.pink, .bold = true }; + draw.writeStr(surface, row, 1, "> ", prompt_style); - /// Static single-byte grapheme strings. Each entry is a pointer - /// to a static [1]u8 array stored in .rodata. - const ascii_graphemes: [128]*const [1]u8 = blk: { - @setEvalBranchQuota(200); - var table: [128]*const [1]u8 = undefined; - for (0..128) |i| { - table[i] = &[1]u8{@as(u8, @intCast(i))}; + const first = self.input.buf.firstHalf(); + const second = self.input.buf.secondHalf(); + var col: u16 = 3; + for (first) |byte| { + draw.writeChar(surface, row, col, byte, .{ .fg = C.fg }); + col += 1; } - break :blk table; - }; - - fn charGrapheme(byte: u8) []const u8 { - if (byte < 128) return ascii_graphemes[byte]; - return " "; - } - - fn fillRow(self: *App, surface: *vxfw.Surface, row: u16, char: u8, style: vaxis.Style) void { - _ = self; - const width = surface.size.width; - const grapheme: []const u8 = charGrapheme(char); - for (0..width) |col| { - const idx = @as(usize, row) * @as(usize, width) + col; - if (idx < surface.buffer.len) { - surface.buffer[idx] = .{ - .char = .{ .grapheme = grapheme, .width = 1 }, - .style = style, - }; - } + for (second) |byte| { + draw.writeChar(surface, row, col, byte, .{ .fg = C.fg }); + col += 1; } - } - fn writeStr(self: *App, surface: *vxfw.Surface, row: u16, col: u16, text: []const u8, style: vaxis.Style) void { - _ = self; - const width = surface.size.width; - for (0..text.len) |i| { - const c = col + @as(u16, @intCast(i)); - if (c >= width) break; - const idx = @as(usize, row) * @as(usize, width) + @as(usize, c); - if (idx < surface.buffer.len) { - surface.buffer[idx] = .{ - .char = .{ .grapheme = charGrapheme(text[i]), .width = 1 }, - .style = style, - }; - } - } - } - - fn writeChar(self: *App, surface: *vxfw.Surface, row: u16, col: u16, byte: u8, style: vaxis.Style) void { - _ = self; - const width = surface.size.width; - if (col >= width) return; - const idx = @as(usize, row) * @as(usize, width) + @as(usize, col); - if (idx < surface.buffer.len) { - surface.buffer[idx] = .{ - .char = .{ .grapheme = charGrapheme(byte), .width = 1 }, - .style = style, - }; + if (!self.bit_grid_active) { + const cursor_col: u16 = 3 + @as(u16, @intCast(self.input.buf.cursor)); + surface.cursor = .{ .col = cursor_col, .row = row, .shape = .block }; } } }; +pub fn drawHistory(items: []const App.HistoryEntry, surface: *vxfw.Surface, start: u16, end: u16) void { + if (items.len == 0) return; + const avail_rows = end -| start; + const max_entries = avail_rows / 2; + const start_idx = if (items.len > max_entries) items.len - max_entries else 0; + + var row: u16 = start; + for (items[start_idx..]) |entry| { + if (row + 1 >= end) break; + draw.writeStr(surface, row, 2, entry.expr, .{ .fg = C.muted }); + row += 1; + const res_style: vaxis.Style = if (entry.is_error) + .{ .fg = C.pink } + else + .{ .fg = C.green, .bold = true }; + draw.writeStr(surface, row, 4, "= ", res_style); + draw.writeStr(surface, row, 6, entry.result, res_style); + row += 1; + } +} + +fn errorStr(err: engine.CalcError) []const u8 { + return switch (err) { + engine.CalcError.DivisionByZero => "error: division by zero", + engine.CalcError.UnknownFunction => "error: unknown function", + engine.CalcError.UnknownVariable => "error: unknown variable", + engine.CalcError.UnmatchedParen => "error: unmatched parenthesis", + engine.CalcError.UnexpectedToken => "error: unexpected token", + engine.CalcError.UnexpectedEnd => "error: unexpected end of expression", + engine.CalcError.InvalidNumber => "error: invalid number", + engine.CalcError.DomainError => "error: domain error", + engine.CalcError.Overflow => "error: overflow", + else => "error: evaluation error", + }; +} + pub fn run(allocator: Allocator, io: std.Io, environ_map: *std.process.Environ.Map) !void { var app = App.init(allocator, io); defer app.deinit(); diff --git a/src/tui/draw.zig b/src/tui/draw.zig new file mode 100644 index 0000000..bf4fd27 --- /dev/null +++ b/src/tui/draw.zig @@ -0,0 +1,65 @@ +//! Shared drawing helpers for the TUI. + +const vaxis = @import("vaxis"); +const vxfw = vaxis.vxfw; + +/// Molokai color palette. +pub const C = struct { + pub const bg: vaxis.Cell.Color = .{ .rgb = .{ 0x1B, 0x1D, 0x1E } }; + pub const fg: vaxis.Cell.Color = .{ .rgb = .{ 0xF8, 0xF8, 0xF2 } }; + pub const muted: vaxis.Cell.Color = .{ .rgb = .{ 0x75, 0x71, 0x5E } }; + pub const dim: vaxis.Cell.Color = .{ .rgb = .{ 0x46, 0x48, 0x3E } }; + pub const cyan: vaxis.Cell.Color = .{ .rgb = .{ 0x66, 0xD9, 0xEF } }; + pub const green: vaxis.Cell.Color = .{ .rgb = .{ 0xA6, 0xE2, 0x2E } }; + pub const pink: vaxis.Cell.Color = .{ .rgb = .{ 0xF9, 0x26, 0x72 } }; + pub const yellow: vaxis.Cell.Color = .{ .rgb = .{ 0xE6, 0xDB, 0x74 } }; + pub const purple: vaxis.Cell.Color = .{ .rgb = .{ 0xAE, 0x81, 0xFF } }; + pub const orange: vaxis.Cell.Color = .{ .rgb = .{ 0xFD, 0x97, 0x1F } }; +}; + +/// Static single-byte grapheme strings with static lifetime. +const ascii_graphemes: [128]*const [1]u8 = blk: { + @setEvalBranchQuota(200); + var table: [128]*const [1]u8 = undefined; + for (0..128) |i| { + table[i] = &[1]u8{@as(u8, @intCast(i))}; + } + break :blk table; +}; + +pub fn charGrapheme(byte: u8) []const u8 { + if (byte < 128) return ascii_graphemes[byte]; + return " "; +} + +pub fn fillRow(surface: *vxfw.Surface, row: u16, char: u8, style: vaxis.Style) void { + const w = surface.size.width; + const grapheme: []const u8 = charGrapheme(char); + for (0..w) |col| { + const idx = @as(usize, row) * @as(usize, w) + col; + if (idx < surface.buffer.len) { + surface.buffer[idx] = .{ .char = .{ .grapheme = grapheme, .width = 1 }, .style = style }; + } + } +} + +pub fn writeStr(surface: *vxfw.Surface, row: u16, col: u16, text: []const u8, style: vaxis.Style) void { + const w = surface.size.width; + for (0..text.len) |i| { + const c = col + @as(u16, @intCast(i)); + if (c >= w) break; + const idx = @as(usize, row) * @as(usize, w) + @as(usize, c); + if (idx < surface.buffer.len) { + surface.buffer[idx] = .{ .char = .{ .grapheme = charGrapheme(text[i]), .width = 1 }, .style = style }; + } + } +} + +pub fn writeChar(surface: *vxfw.Surface, row: u16, col: u16, byte: u8, style: vaxis.Style) void { + const w = surface.size.width; + if (col >= w) return; + const idx = @as(usize, row) * @as(usize, w) + @as(usize, col); + if (idx < surface.buffer.len) { + surface.buffer[idx] = .{ .char = .{ .grapheme = charGrapheme(byte), .width = 1 }, .style = style }; + } +} diff --git a/src/tui/help.zig b/src/tui/help.zig new file mode 100644 index 0000000..1af1c4c --- /dev/null +++ b/src/tui/help.zig @@ -0,0 +1,73 @@ +//! Help overlay for the TUI. + +const vaxis = @import("vaxis"); +const vxfw = vaxis.vxfw; +const draw = @import("draw.zig"); +const C = draw.C; + +pub fn drawHelp(surface: *vxfw.Surface, width: u16, height: u16) void { + for (0..height) |r| { + draw.fillRow(surface, @intCast(r), ' ', .{}); + } + + var row: u16 = 1; + draw.writeStr(surface, row, 2, "Tally - Help", .{ .fg = C.cyan, .bold = true }); + row += 2; + + draw.writeStr(surface, row, 2, "Keybindings", .{ .fg = C.purple, .bold = true }); + row += 1; + const keys = [_][2][]const u8{ + .{ "Enter", "Evaluate expression" }, + .{ "Tab", "Switch mode (Standard/Programmer)" }, + .{ "Ctrl-C/D", "Quit" }, + .{ "Ctrl-L", "Clear history" }, + .{ "Up/Down", "Browse history" }, + .{ "?", "Toggle this help" }, + }; + for (keys) |kv| { + draw.writeStr(surface, row, 4, kv[0], .{ .fg = C.yellow }); + draw.writeStr(surface, row, 18, kv[1], .{ .fg = C.fg }); + row += 1; + } + row += 1; + + draw.writeStr(surface, row, 2, "Programmer Mode", .{ .fg = C.purple, .bold = true }); + row += 1; + const prog_keys = [_][2][]const u8{ + .{ "Ctrl-B", "Toggle bit grid focus" }, + .{ "Ctrl-W", "Cycle bit width (8/16/32/64)" }, + .{ "Space", "Toggle bit (in grid)" }, + .{ "Arrows", "Navigate bits (in grid)" }, + .{ "Esc", "Return to input (from grid)" }, + }; + for (prog_keys) |kv| { + if (row >= height -| 4) break; + draw.writeStr(surface, row, 4, kv[0], .{ .fg = C.yellow }); + draw.writeStr(surface, row, 18, kv[1], .{ .fg = C.fg }); + row += 1; + } + row += 1; + + if (row < height -| 6) { + draw.writeStr(surface, row, 2, "Functions", .{ .fg = C.purple, .bold = true }); + row += 1; + draw.writeStr(surface, row, 4, "sin cos tan asin acos atan log ln sqrt abs", .{ .fg = C.fg }); + row += 1; + draw.writeStr(surface, row, 4, "ceil floor round factorial max min exp", .{ .fg = C.fg }); + row += 1; + } + + if (row < height -| 4) { + row += 1; + draw.writeStr(surface, row, 2, "Operators", .{ .fg = C.purple, .bold = true }); + row += 1; + draw.writeStr(surface, row, 4, "Standard: + - * / % ^ (power)", .{ .fg = C.fg }); + row += 1; + draw.writeStr(surface, row, 4, "Programmer: & | ^ (xor) ~ << >> >>> ** rol ror", .{ .fg = C.fg }); + row += 1; + } + + draw.fillRow(surface, height -| 1, ' ', .{ .fg = C.muted }); + draw.writeStr(surface, height -| 1, 1, "Press any key to return", .{ .fg = C.muted }); + _ = width; +} diff --git a/src/tui/programmer.zig b/src/tui/programmer.zig new file mode 100644 index 0000000..92f56be --- /dev/null +++ b/src/tui/programmer.zig @@ -0,0 +1,111 @@ +//! Programmer mode drawing for the TUI. + +const std = @import("std"); +const vaxis = @import("vaxis"); +const vxfw = vaxis.vxfw; +const engine = @import("engine"); +const draw = @import("draw.zig"); +const tui = @import("../tui.zig"); +const C = draw.C; + +pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, height: u16) void { + _ = width; + const bw = app.prog_config.bit_width; + const val = app.prog_value & bw.mask(); + + // Config display + var config_buf: [64]u8 = undefined; + const config_str = std.fmt.bufPrint(&config_buf, "Bits: {d} Signed: {s}", .{ + bw.bits(), + if (app.prog_config.signedness == .signed) "yes" else "no", + }) catch "Bits: ??"; + draw.writeStr(surface, 2, 2, config_str, .{ .fg = C.muted }); + + // Bit grid + const grid_start: u16 = 4; + drawBitGrid(app, surface, grid_start, val, bw); + + // Multi-base display below bit grid + const base_start: u16 = grid_start + 3 + @as(u16, if (bw.bits() > 32) 1 else 0); + const int = engine.types.Integer{ .raw = val, .bit_width = bw, .signedness = .signed }; + + var sdec_buf: [256]u8 = undefined; + const sdec = engine.formatter.formatDecimalSigned(&sdec_buf, int.signedValue()); + draw.writeStr(surface, base_start, 2, "DEC(s):", .{ .fg = C.cyan }); + draw.writeStr(surface, base_start, 11, sdec.display, .{ .fg = C.fg }); + + var udec_buf: [256]u8 = undefined; + const udec = engine.formatter.formatDecimalUnsigned(&udec_buf, val); + draw.writeStr(surface, base_start + 1, 2, "DEC(u):", .{ .fg = C.cyan }); + draw.writeStr(surface, base_start + 1, 11, udec.display, .{ .fg = C.fg }); + + var hex_buf: [256]u8 = undefined; + const hex = engine.formatter.formatHex(&hex_buf, val, bw); + draw.writeStr(surface, base_start + 2, 2, "HEX:", .{ .fg = C.cyan }); + draw.writeStr(surface, base_start + 2, 11, hex.display, .{ .fg = C.green }); + + var oct_buf: [256]u8 = undefined; + const oct = engine.formatter.formatOctal(&oct_buf, val, bw); + draw.writeStr(surface, base_start + 3, 2, "OCT:", .{ .fg = C.cyan }); + draw.writeStr(surface, base_start + 3, 11, oct.display, .{ .fg = C.purple }); + + var bin_buf: [512]u8 = undefined; + const bin = engine.formatter.formatBinary(&bin_buf, val, bw); + draw.writeStr(surface, base_start + 4, 2, "BIN:", .{ .fg = C.cyan }); + draw.writeStr(surface, base_start + 4, 11, bin.display, .{ .fg = C.yellow }); + + // History + const hist_start = base_start + 6; + const hist_end = height -| 4; + if (hist_start < hist_end) { + tui.drawHistory(app.history.items, surface, hist_start, hist_end); + } + + // Separator + input + status + draw.fillRow(surface, height -| 3, '-', .{ .fg = C.dim }); + app.drawInput(surface, height -| 2); + draw.fillRow(surface, height -| 1, ' ', .{ .fg = C.muted, .bg = C.bg }); + const status = if (app.bit_grid_active) + "Arrows:nav | Space:toggle | Esc:input | Ctrl-W:width | Tab:mode | Ctrl-C:quit" + else + "?:help | Tab:mode | Ctrl-B:bits | Ctrl-W:width | Enter:eval | Ctrl-C:quit"; + draw.writeStr(surface, height -| 1, 1, status, .{ .fg = C.muted, .bg = C.bg }); +} + +fn drawBitGrid(app: *tui.App, surface: *vxfw.Surface, start_row: u16, val: u64, bw: engine.types.BitWidth) void { + const width_bits = bw.bits(); + const bits_per_row: u7 = if (width_bits > 32) 32 else width_bits; + const num_rows: u16 = (@as(u16, width_bits) + bits_per_row - 1) / bits_per_row; + + var row = start_row; + var bit_row: u16 = 0; + while (bit_row < num_rows) : (bit_row += 1) { + const row_start_bit: u7 = @intCast(width_bits - 1 - bit_row * bits_per_row); + var col: u16 = 2; + var b: u8 = 0; + while (b < bits_per_row) : (b += 1) { + const bit_idx = row_start_bit -| b; + if (bit_idx >= width_bits) break; + + if (b > 0 and b % 4 == 0) col += 1; + + const bit_val: u8 = @intCast((val >> @intCast(bit_idx)) & 1); + const is_cursor = app.bit_grid_active and (bit_idx == app.bit_cursor); + + const style: vaxis.Style = if (is_cursor) .{ + .fg = C.bg, + .bg = C.cyan, + .bold = true, + } else if (bit_val == 1) .{ + .fg = C.green, + .bold = true, + } else .{ + .fg = C.dim, + }; + + draw.writeChar(surface, row, col, '0' + bit_val, style); + col += 1; + } + row += 1; + } +}