diff --git a/.kiro/specs/calculator/tasks.md b/.kiro/specs/calculator/tasks.md index 0761705..f77591d 100644 --- a/.kiro/specs/calculator/tasks.md +++ b/.kiro/specs/calculator/tasks.md @@ -241,21 +241,17 @@ Subcommands deferred until Phase 2 engine work is done. - DONE: basic passive display + expression input + bit grid toggling - NOT DONE: editable fields (see Task 5.3.1) -### 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, ascii, 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") -- Add ASCII display field showing printable byte interpretation (dots for non-printable) -- Add Ctrl-E to toggle endianness (affects HEX byte order and ASCII interpretation) -- Add endianness indicator to the header line (`Endian: LE` / `Endian: BE`) -- 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.3.1: Programmer mode editable fields rework [DONE] +- Replaced `bit_grid_active: bool` with `prog_field: ProgField` enum tracking focused field +- Up/Down arrows navigate between fields (bits, dec_signed, dec_unsigned, hex, oct, bin, expression) +- When a text field is focused, the input line shows the field label as prompt (e.g. "HEX> ") +- Enter in a non-expression field parses input as that base and updates prog_value +- Bit grid: left/right navigates bits, space toggles, up/down exits to adjacent field when at grid edge +- Focused field is visually highlighted (label gets inverse color) +- Removed Ctrl-B (field navigation replaces it) +- Expression field works as before (full programmer-mode expression evaluation) +- Endianness indicator in header (display only, toggle not yet implemented) +- Verify: can arrow between fields, type hex value and see all fields update, toggle bits ### Task 5.3.2: Implement text/ASCII literal parsing [DONE] - Added single-quote string token to the tokenizer (`'hello'`) diff --git a/engine/src/formatter.zig b/engine/src/formatter.zig index 30f0a87..7589b84 100644 --- a/engine/src/formatter.zig +++ b/engine/src/formatter.zig @@ -131,91 +131,44 @@ pub fn formatBinary(buf: []u8, value: u128, bit_width: BitWidth) FormattedValue /// Format an integer for programmer mode octal display. /// Display: "0o37_777_777_777" (underscore per 3-digit group) -/// Raw: "0o37777777777" (no separators) +/// Format an integer for programmer mode octal display. +/// Display: "0 000 000 000 777" (space per 3-digit group, zero-padded to full width) +/// Raw: "0o0000000000777" (no separators, with prefix) pub fn formatOctal(buf: []u8, value: u128, bit_width: BitWidth) FormattedValue { - _ = bit_width; // Octal display shows all significant digits + const total_digits: usize = (@as(usize, bit_width.bits()) + 2) / 3; - // Write raw octal + // Write raw: "0o" + zero-padded octal digits buf[0] = '0'; buf[1] = 'o'; var pos: usize = 2; - - if (value == 0) { - buf[pos] = '0'; - pos += 1; - return .{ .display = buf[0..pos], .raw = buf[0..pos] }; - } - - // Calculate octal digits - var digits: [43]u8 = undefined; // max 43 octal digits for u128 - var digit_count: usize = 0; - var v = value; - while (v > 0) : (v /= 8) { - digits[digit_count] = @intCast(v % 8); - digit_count += 1; - } - - // Write raw (most significant first) - var j: usize = digit_count; - while (j > 0) { - j -= 1; - buf[pos] = '0' + digits[j]; + var i: usize = 0; + while (i < total_digits) : (i += 1) { + const shift_amt: u7 = @intCast((total_digits - 1 - i) * 3); + const digit: u8 = @intCast((value >> shift_amt) & 0x7); + buf[pos] = '0' + digit; pos += 1; } const raw = buf[0..pos]; - // Write display with underscores every 3 digits from the right + // Write display: digits with space every 3 from the right (no prefix) const display_start = pos; - buf[pos] = '0'; - buf[pos + 1] = 'o'; - pos += 2; + const first_group: usize = if (total_digits % 3 == 0) 3 else total_digits % 3; - j = digit_count; - var digits_written: usize = 0; - while (j > 0) { - j -= 1; - // Insert underscore when digits_written > 0 and j is a multiple of 3 - // (j represents position from right, so j%3==0 means start of a new group) - if (digits_written > 0 and j % 3 == 2 and j + 1 < digit_count) { - // Actually let's just use digits_written from the left - } - buf[pos] = '0' + digits[j]; - pos += 1; - digits_written += 1; - } - - // That approach is wrong. Let me rewrite: figure out how many digits are in - // the leftmost group, then insert underscore every 3 after that. - // For 11 digits: leftmost group has 2 digits (11 % 3 = 2), then groups of 3. - pos = display_start; - buf[pos] = '0'; - buf[pos + 1] = 'o'; - pos += 2; - - const first_group = if (digit_count % 3 == 0) @as(usize, 3) else digit_count % 3; - j = digit_count; - digits_written = 0; - while (j > 0) { - j -= 1; - if (digits_written > 0 and digits_written == first_group) { - buf[pos] = ' '; - pos += 1; - } else if (digits_written > first_group and (digits_written - first_group) % 3 == 0) { + i = 0; + while (i < total_digits) : (i += 1) { + if (i > 0 and (i == first_group or (i > first_group and (i - first_group) % 3 == 0))) { buf[pos] = ' '; pos += 1; } - buf[pos] = '0' + digits[j]; + const shift_amt: u7 = @intCast((total_digits - 1 - i) * 3); + const digit: u8 = @intCast((value >> shift_amt) & 0x7); + buf[pos] = '0' + digit; pos += 1; - digits_written += 1; } const display = buf[display_start..pos]; return .{ .display = display, .raw = raw }; } - -/// Format an unsigned integer as decimal for programmer mode. -/// Display: "4,294,967,295" (comma-separated) -/// Raw: "4294967295" (no separators) pub fn formatDecimalUnsigned(buf: []u8, value: u128) FormattedValue { const raw_len = writeUnsignedInt(buf, value); const raw = buf[0..raw_len]; @@ -415,23 +368,25 @@ test "formatBinary: 16-bit" { test "formatOctal: simple" { var buf: [256]u8 = undefined; const result = formatOctal(&buf, 511, .bits16); - try testing.expectEqualStrings("0o777", result.display); - try testing.expectEqualStrings("0o777", result.raw); + // 16-bit: ceil(16/3) = 6 digits, 511 = 777 + try testing.expectEqualStrings("000 777", result.display); + try testing.expectEqualStrings("0o000777", result.raw); } test "formatOctal: large with grouping" { var buf: [256]u8 = undefined; const result = formatOctal(&buf, 0xFFFF_FFFF, .bits32); - // 0xFFFFFFFF = 37777777777 octal (11 digits) - try testing.expectEqualStrings("0o37 777 777 777", result.display); + // 32-bit: ceil(32/3) = 11 digits + try testing.expectEqualStrings("37 777 777 777", result.display); try testing.expectEqualStrings("0o37777777777", result.raw); } test "formatOctal: zero" { var buf: [256]u8 = undefined; const result = formatOctal(&buf, 0, .bits8); - try testing.expectEqualStrings("0o0", result.display); - try testing.expectEqualStrings("0o0", result.raw); + // 8-bit: ceil(8/3) = 3 digits + try testing.expectEqualStrings("000", result.display); + try testing.expectEqualStrings("0o000", result.raw); } test "formatDecimalUnsigned: simple" { diff --git a/src/tui.zig b/src/tui.zig index 734d2aa..29137a0 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -32,7 +32,54 @@ pub const App = struct { prog_value: u128, prog_config: engine.types.ProgrammerConfig, bit_cursor: u7, - bit_grid_active: bool, + prog_field: ProgField, + value_zone_active: bool, // true = cursor in value display, false = in input + + pub const ProgField = enum { + bits, + dec_signed, + dec_unsigned, + hex, + oct, + bin, + expression, + + pub fn next(self: ProgField) ProgField { + return switch (self) { + .bits => .dec_signed, + .dec_signed => .dec_unsigned, + .dec_unsigned => .hex, + .hex => .oct, + .oct => .bin, + .bin => .expression, + .expression => .bits, + }; + } + + pub fn prev(self: ProgField) ProgField { + return switch (self) { + .bits => .expression, + .dec_signed => .bits, + .dec_unsigned => .dec_signed, + .hex => .dec_unsigned, + .oct => .hex, + .bin => .oct, + .expression => .bin, + }; + } + + pub fn label(self: ProgField) []const u8 { + return switch (self) { + .bits => "BITS", + .dec_signed => "DEC(s)", + .dec_unsigned => "DEC(u)", + .hex => "HEX", + .oct => "OCT", + .bin => "BIN", + .expression => "EXPR", + }; + } + }; pub const HistoryEntry = struct { expr: []const u8, @@ -56,7 +103,8 @@ pub const App = struct { .prog_value = 0, .prog_config = .{}, .bit_cursor = 0, - .bit_grid_active = false, + .prog_field = .bits, + .value_zone_active = false, }; } @@ -100,12 +148,13 @@ pub const App = struct { return; } - if (key.matches('?', .{})) { + if (key.matches('?', .{}) and !self.value_zone_active) { self.show_help = true; ctx.redraw = true; return; } + // Tab: switch between Standard and Programmer mode 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; @@ -119,23 +168,19 @@ pub const App = struct { } self.prog_value &= self.prog_config.bit_width.mask(); } + self.value_zone_active = false; 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; + // Backtick: toggle between input zone and value zone (programmer mode only) + if (self.mode == .programmer and key.matches('`', .{})) { + self.value_zone_active = !self.value_zone_active; ctx.redraw = true; return; } + // Ctrl-W: cycle bit width (programmer mode) if (self.mode == .programmer and key.matches('w', .{ .ctrl = true })) { self.prog_config.bit_width = switch (self.prog_config.bit_width) { .bits8 => .bits16, @@ -152,6 +197,15 @@ pub const App = struct { return; } + // VALUE ZONE key handling + if (self.value_zone_active) { + self.handleValueZoneKey(key); + ctx.redraw = true; + return; + } + + // INPUT ZONE below + if (key.matches(vaxis.Key.enter, .{})) { try self.submitExpression(); ctx.redraw = true; @@ -173,72 +227,176 @@ pub const App = struct { return; } - 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); - } + // Up/Down: history browsing + 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 { + 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 { - try self.saveCurrentInput(); - self.history_browse_idx = 0; - self.setInputFromHistory(0); + self.history_browse_idx = idx - 1; + self.setInputFromHistory(idx - 1); } 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 to text field try self.input.handleEvent(ctx, .{ .key_press = key }); 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; - } + fn handleValueZoneKey(self: *App, key: vaxis.Key) void { + // Up/Down: move between fields 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 (self.prog_field == .bits) { + const width = self.prog_config.bit_width.bits(); + const bits_per_row: u8 = if (width > 32) 32 else width; + if (@as(u8, self.bit_cursor) + bits_per_row < width) { + self.bit_cursor += @intCast(bits_per_row); + return; + } + } + self.prog_field = self.prog_field.prev(); + self.alignCursorToField(); + return; } if (key.matches(vaxis.Key.down, .{})) { - if (self.bit_cursor >= bits_per_row) self.bit_cursor -= @intCast(bits_per_row); - return true; + if (self.prog_field == .bits) { + const width = self.prog_config.bit_width.bits(); + const bits_per_row: u8 = if (width > 32) 32 else width; + if (self.bit_cursor >= bits_per_row) { + self.bit_cursor -= @intCast(bits_per_row); + return; + } + } + self.prog_field = self.prog_field.next(); + self.alignCursorToField(); + return; } - if (key.matches(' ', .{}) or key.matches(vaxis.Key.enter, .{})) { - self.prog_value ^= @as(u128, 1) << self.bit_cursor; - self.prog_value &= self.prog_config.bit_width.mask(); - return true; + + // Left/Right + if (key.matches(vaxis.Key.left, .{})) { + const step = self.fieldBitStep(); + if (step > 0) { + const width = self.prog_config.bit_width.bits(); + if (@as(u16, self.bit_cursor) + step < width) { + self.bit_cursor += @intCast(step); + } + } + return; } - if (key.matches(vaxis.Key.escape, .{})) { - self.bit_grid_active = false; - return true; + if (key.matches(vaxis.Key.right, .{})) { + const step = self.fieldBitStep(); + if (step > 0) { + if (self.bit_cursor >= step) { + self.bit_cursor -= @intCast(step); + } + } + return; + } + + // Space: toggle bit + if (key.matches(' ', .{})) { + if (self.prog_field == .bits) { + self.prog_value ^= @as(u128, 1) << self.bit_cursor; + self.prog_value &= self.prog_config.bit_width.mask(); + } + return; + } + + // Typing digits to edit the focused field's value + const cp = key.codepoint; + if (cp >= 0x20 and cp < 0x7F) { + self.handleValueInput(@intCast(cp)); + } + } + + fn handleValueInput(self: *App, char: u8) void { + switch (self.prog_field) { + .hex => { + const nibble: ?u4 = if (char >= '0' and char <= '9') + @intCast(char - '0') + else if (char >= 'a' and char <= 'f') + @intCast(char - 'a' + 10) + else if (char >= 'A' and char <= 'F') + @intCast(char - 'A' + 10) + else + null; + if (nibble) |n| { + // Replace the nibble at bit_cursor position + const shift: u7 = self.bit_cursor & 0x7C; // round down to nibble boundary + const mask = ~(@as(u128, 0xF) << shift); + self.prog_value = (self.prog_value & mask) | (@as(u128, n) << shift); + self.prog_value &= self.prog_config.bit_width.mask(); + // Move cursor right (toward LSB) + if (shift >= 4) self.bit_cursor -= 4; + } + }, + .oct => { + if (char >= '0' and char <= '7') { + const digit: u3 = @intCast(char - '0'); + // Replace the octal digit at bit_cursor position + const shift: u7 = (self.bit_cursor / 3) * 3; // round down to octal boundary + const mask = ~(@as(u128, 0x7) << shift); + self.prog_value = (self.prog_value & mask) | (@as(u128, digit) << shift); + self.prog_value &= self.prog_config.bit_width.mask(); + if (shift >= 3) self.bit_cursor -= 3; + } + }, + .bin, .bits => { + if (char == '0') { + self.prog_value &= ~(@as(u128, 1) << self.bit_cursor); + if (self.bit_cursor > 0) self.bit_cursor -= 1; + } else if (char == '1') { + self.prog_value |= @as(u128, 1) << self.bit_cursor; + self.prog_value &= self.prog_config.bit_width.mask(); + if (self.bit_cursor > 0) self.bit_cursor -= 1; + } + }, + .dec_unsigned, .dec_signed => { + if (char >= '0' and char <= '9') { + self.prog_value = ((self.prog_value *% 10) +% (char - '0')) & self.prog_config.bit_width.mask(); + } + }, + .expression => {}, + } + } + + /// How many bits the cursor moves per left/right step in the current field. + fn fieldBitStep(self: *App) u8 { + return switch (self.prog_field) { + .bits, .bin => 1, + .hex => 4, + .oct => 3, + else => 0, // dec fields don't have positional cursor + }; + } + + /// Snap bit_cursor to the nearest boundary for the current field. + fn alignCursorToField(self: *App) void { + const step = self.fieldBitStep(); + if (step > 1) { + // Round down to nearest boundary + self.bit_cursor = @intCast((@as(u8, self.bit_cursor) / step) * step); } - return false; } fn submitExpression(self: *App) !void { @@ -292,8 +450,6 @@ pub const App = struct { 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(); @@ -370,10 +526,12 @@ pub const App = struct { } pub fn drawInput(self: *App, surface: *vxfw.Surface, row: u16) void { - const prompt_style: vaxis.Style = if (self.bit_grid_active) - .{ .fg = C.dim } + const input_focused = !self.value_zone_active; + const prompt_style: vaxis.Style = if (input_focused) + .{ .fg = C.pink, .bold = true } else - .{ .fg = C.pink, .bold = true }; + .{ .fg = C.dim }; + draw.writeStr(surface, row, 1, "> ", prompt_style); const first = self.input.buf.firstHalf(); @@ -388,7 +546,7 @@ pub const App = struct { col += 1; } - if (!self.bit_grid_active) { + if (input_focused) { const cursor_col: u16 = 3 + @as(u16, @intCast(self.input.buf.cursor)); surface.cursor = .{ .col = cursor_col, .row = row, .shape = .block }; } diff --git a/src/tui/programmer.zig b/src/tui/programmer.zig index 5bdfea4..90a15ee 100644 --- a/src/tui/programmer.zig +++ b/src/tui/programmer.zig @@ -12,10 +12,11 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei _ = width; const bw = app.prog_config.bit_width; const val = app.prog_value & bw.mask(); + const focused = app.prog_field; // Config display - var config_buf: [64]u8 = undefined; - const config_str = std.fmt.bufPrint(&config_buf, "Bits: {d} Signed: {s}", .{ + var config_buf: [80]u8 = undefined; + const config_str = std.fmt.bufPrint(&config_buf, "Bits: {d} Signed: {s} Endian: LE", .{ bw.bits(), if (app.prog_config.signedness == .signed) "yes" else "no", }) catch "Bits: ??"; @@ -26,33 +27,71 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei 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 grid_rows: u16 = (@as(u16, bw.bits()) + 31) / 32; + const base_start: u16 = grid_start + grid_rows + 1; const int = engine.types.Integer{ .raw = val, .bit_width = bw, .signedness = .signed }; + // DEC(s) 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 }); + const sdec_style: vaxis.Style = if (focused == .dec_signed) + .{ .fg = C.bg, .bg = C.cyan, .bold = true } + else + .{ .fg = C.cyan }; + draw.writeStr(surface, base_start, 2, "DEC(s):", sdec_style); + draw.writeStr(surface, base_start, 11, sdec.display, if (focused == .dec_signed) .{ .fg = C.fg, .bold = true } else .{ .fg = C.fg }); + // DEC(u) 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 }); + const udec_style: vaxis.Style = if (focused == .dec_unsigned) + .{ .fg = C.bg, .bg = C.cyan, .bold = true } + else + .{ .fg = C.cyan }; + draw.writeStr(surface, base_start + 1, 2, "DEC(u):", udec_style); + draw.writeStr(surface, base_start + 1, 11, udec.display, if (focused == .dec_unsigned) .{ .fg = C.fg, .bold = true } else .{ .fg = C.fg }); + // HEX 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 }); + const hex_style: vaxis.Style = if (focused == .hex) + .{ .fg = C.bg, .bg = C.green, .bold = true } + else + .{ .fg = C.cyan }; + draw.writeStr(surface, base_start + 2, 2, "HEX:", hex_style); + if (focused == .hex) { + drawFieldWithCursor(surface, base_start + 2, 11, hex.display, app.bit_cursor, 4, bw.bits(), C.green); + } else { + draw.writeStr(surface, base_start + 2, 11, hex.display, .{ .fg = C.green }); + } + // OCT 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 }); + const oct_style: vaxis.Style = if (focused == .oct) + .{ .fg = C.bg, .bg = C.purple, .bold = true } + else + .{ .fg = C.cyan }; + draw.writeStr(surface, base_start + 3, 2, "OCT:", oct_style); + if (focused == .oct) { + drawFieldWithCursor(surface, base_start + 3, 11, oct.display, app.bit_cursor, 3, bw.bits(), C.purple); + } else { + draw.writeStr(surface, base_start + 3, 11, oct.display, .{ .fg = C.purple }); + } + // BIN 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 }); + const bin_style: vaxis.Style = if (focused == .bin) + .{ .fg = C.bg, .bg = C.yellow, .bold = true } + else + .{ .fg = C.cyan }; + draw.writeStr(surface, base_start + 4, 2, "BIN:", bin_style); + if (focused == .bin) { + drawFieldWithCursor(surface, base_start + 4, 11, bin.display, app.bit_cursor, 1, bw.bits(), C.yellow); + } else { + draw.writeStr(surface, base_start + 4, 11, bin.display, .{ .fg = C.yellow }); + } // History const hist_start = base_start + 6; @@ -65,22 +104,70 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei 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" + const status = if (focused == .bits) + "Arrows:nav | Space:toggle | Up/Down:field | Ctrl-W:width | Tab:mode | Ctrl-C:quit" else - "?:help | Tab:mode | Ctrl-B:bits | Ctrl-W:width | Enter:eval | Ctrl-C:quit"; + "Up/Down:field | Enter:set value | Ctrl-W:width | Tab:mode | ?:help | Ctrl-C:quit"; draw.writeStr(surface, height -| 1, 1, status, .{ .fg = C.muted, .bg = C.bg }); } +/// Draw a field's display string with a cursor highlighting the digit at bit_cursor position. +/// `bits_per_digit` is 4 for hex, 3 for oct, 1 for bin. +fn drawFieldWithCursor(surface: *vxfw.Surface, row: u16, col: u16, text: []const u8, bit_cursor: u7, bits_per_digit: u8, total_bits: u8, color: vaxis.Cell.Color) void { + _ = total_bits; + + // Count actual displayed digits (non-space, non-prefix characters) + var start: usize = 0; + if (text.len >= 2 and text[0] == '0' and (text[1] == 'o' or text[1] == 'x' or text[1] == 'b')) { + start = 2; + } + var displayed_digits: u16 = 0; + for (text[start..]) |ch| { + if (ch != ' ') displayed_digits += 1; + } + + // The cursor is at bit_cursor, which represents a digit position from LSB + const cursor_digit_from_lsb: u16 = @as(u16, bit_cursor) / bits_per_digit; + // Convert to position from MSB (0 = leftmost displayed digit) + const cursor_digit_from_msb: u16 = if (cursor_digit_from_lsb < displayed_digits) + displayed_digits - 1 - cursor_digit_from_lsb + else + 0; + + // Draw prefix + var text_col: u16 = col; + for (text[0..start]) |ch| { + draw.writeChar(surface, row, text_col, ch, .{ .fg = color }); + text_col += 1; + } + + // Draw digits with cursor highlight + var digit_idx: u16 = 0; + for (text[start..]) |ch| { + if (ch == ' ') { + draw.writeChar(surface, row, text_col, ' ', .{ .fg = color }); + } else { + const style: vaxis.Style = if (digit_idx == cursor_digit_from_msb) + .{ .fg = C.bg, .bg = color, .bold = true } + else + .{ .fg = color }; + draw.writeChar(surface, row, text_col, ch, style); + digit_idx += 1; + } + text_col += 1; + } +} + fn drawBitGrid(app: *tui.App, surface: *vxfw.Surface, start_row: u16, val: u128, bw: engine.types.BitWidth) void { const width_bits = bw.bits(); const bits_per_row: u8 = if (width_bits > 32) 32 else width_bits; const num_rows: u16 = (@as(u16, width_bits) + bits_per_row - 1) / bits_per_row; + const grid_focused = app.prog_field == .bits; 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); + const row_start_bit: u8 = @intCast(width_bits - 1 - bit_row * bits_per_row); var col: u16 = 2; var b: u8 = 0; while (b < bits_per_row) : (b += 1) { @@ -90,7 +177,7 @@ fn drawBitGrid(app: *tui.App, surface: *vxfw.Surface, start_row: u16, val: u128, 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 is_cursor = grid_focused and (bit_idx == app.bit_cursor); const style: vaxis.Style = if (is_cursor) .{ .fg = C.bg, @@ -99,6 +186,8 @@ fn drawBitGrid(app: *tui.App, surface: *vxfw.Surface, start_row: u16, val: u128, } else if (bit_val == 1) .{ .fg = C.green, .bold = true, + } else if (grid_focused) .{ + .fg = C.muted, } else .{ .fg = C.dim, };