diff --git a/.kiro/specs/calculator/design.md b/.kiro/specs/calculator/design.md index e9b9acc..fb9b458 100644 --- a/.kiro/specs/calculator/design.md +++ b/.kiro/specs/calculator/design.md @@ -619,10 +619,14 @@ Fields (navigable with Up/Down): 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. ASCII - shows printable ASCII interpretation of the bytes - (e.g., `.ELF` or `hell` - dots for non-printable) +5. ASCII - read-only (derived) view of the bytes, one glyph per byte, + MSB first, aligned column-for-column under the HEX row directly above + it. Printable bytes (0x20-0x7E) show as themselves; every other byte + shows as `.`, matching the `xxd` hex-dump convention (e.g. `.ELF`, + `...ascii`). Not part of the Up/Down focus cycle since it has nothing + to edit. +6. OCT - editable octal +7. BIN - editable binary (nibble-separated: 1111 0000) 8. Expression - full expression evaluator (& | ^ ~ << >> etc.) Navigation: diff --git a/engine/src/formatter.zig b/engine/src/formatter.zig index 7589b84..6938dd4 100644 --- a/engine/src/formatter.zig +++ b/engine/src/formatter.zig @@ -169,6 +169,49 @@ pub fn formatOctal(buf: []u8, value: u128, bit_width: BitWidth) FormattedValue { return .{ .display = display, .raw = raw }; } +/// Format an integer's bytes as an ASCII representation for programmer mode. +/// One glyph per byte, most-significant byte first (matching the hex byte view). +/// Printable bytes (0x20-0x7E) render as themselves; every other byte renders +/// as '.', matching the convention used by `xxd` and similar hex dumps. +/// +/// Display: " . . . a s c i i" - each byte is a leading space + glyph, +/// space-separated, so the row lines up column-for-column under formatHex. +/// Raw: "...ascii" - contiguous glyphs, clipboard-friendly. +pub fn formatAscii(buf: []u8, value: u128, bit_width: BitWidth) FormattedValue { + const num_bytes: usize = @as(usize, bit_width.bits()) / 8; + + // Raw: contiguous glyphs, MSB first. + var pos: usize = 0; + var k: usize = 0; + while (k < num_bytes) : (k += 1) { + const shift_amt: u7 = @intCast((num_bytes - 1 - k) * 8); + const byte: u8 = @intCast((value >> shift_amt) & 0xFF); + buf[pos] = asciiGlyph(byte); + pos += 1; + } + const raw = buf[0..pos]; + + // Display: " c" per byte, space-separated, so glyph k sits under the + // right hex digit of byte k in the hex row. + const display_start = pos; + k = 0; + while (k < num_bytes) : (k += 1) { + if (k > 0) { + buf[pos] = ' '; + pos += 1; + } + buf[pos] = ' '; + pos += 1; + const shift_amt: u7 = @intCast((num_bytes - 1 - k) * 8); + const byte: u8 = @intCast((value >> shift_amt) & 0xFF); + buf[pos] = asciiGlyph(byte); + pos += 1; + } + const display = buf[display_start..pos]; + + return .{ .display = display, .raw = raw }; +} + pub fn formatDecimalUnsigned(buf: []u8, value: u128) FormattedValue { const raw_len = writeUnsignedInt(buf, value); const raw = buf[0..raw_len]; @@ -201,6 +244,13 @@ fn hexDigit(nibble: u4) u8 { return 'A' + @as(u8, nibble) - 10; } +/// Map a byte to its printable glyph, or '.' if outside the printable ASCII +/// range (0x20-0x7E), matching the `xxd` hex-dump convention. +fn asciiGlyph(byte: u8) u8 { + if (byte >= 0x20 and byte <= 0x7E) return byte; + return '.'; +} + fn writeUnsignedInt(buf: []u8, value: u128) usize { if (value == 0) { buf[0] = '0'; @@ -437,3 +487,45 @@ test "formatFloat: regular float" { const result = formatFloat(&buf, 3.14159); try testing.expect(std.mem.indexOf(u8, result.display, "3.14") != null); } + +test "formatAscii: printable word 64-bit" { + var buf: [128]u8 = undefined; + // 0x0000006173636969 -> bytes 00 00 00 61 73 63 69 69 -> "...ascii" + const result = formatAscii(&buf, 0x0000006173636969, .bits64); + try testing.expectEqualStrings("...ascii", result.raw); + // Display aligns one glyph per byte, space-separated + try testing.expectEqualStrings(" . . . a s c i i", result.display); +} + +test "formatAscii: 8-bit printable" { + var buf: [128]u8 = undefined; + const result = formatAscii(&buf, 0x41, .bits8); + try testing.expectEqualStrings("A", result.raw); + try testing.expectEqualStrings(" A", result.display); +} + +test "formatAscii: 8-bit non-printable becomes dot" { + var buf: [128]u8 = undefined; + const result = formatAscii(&buf, 0x00, .bits8); + try testing.expectEqualStrings(".", result.raw); +} + +test "formatAscii: control and high bytes become dots" { + var buf: [128]u8 = undefined; + // 0x1B (ESC) and 0xFF are both non-printable + const result = formatAscii(&buf, 0x1BFF, .bits16); + try testing.expectEqualStrings("..", result.raw); +} + +test "formatAscii: boundary bytes 0x20 and 0x7E are printable" { + var buf: [128]u8 = undefined; + // 0x20 = space, 0x7E = '~' + const result = formatAscii(&buf, 0x207E, .bits16); + try testing.expectEqualStrings(" ~", result.raw); +} + +test "formatAscii: 0x7F is non-printable" { + var buf: [128]u8 = undefined; + const result = formatAscii(&buf, 0x7F, .bits8); + try testing.expectEqualStrings(".", result.raw); +} diff --git a/src/tui/programmer.zig b/src/tui/programmer.zig index 90a15ee..776a71c 100644 --- a/src/tui/programmer.zig +++ b/src/tui/programmer.zig @@ -65,6 +65,12 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei draw.writeStr(surface, base_start + 2, 11, hex.display, .{ .fg = C.green }); } + // ASCII (derived, read-only): one glyph per byte, aligned under HEX. + var ascii_buf: [128]u8 = undefined; + const ascii = engine.formatter.formatAscii(&ascii_buf, val, bw); + draw.writeStr(surface, base_start + 3, 2, "ASCII:", .{ .fg = C.cyan }); + draw.writeStr(surface, base_start + 3, 11, ascii.display, .{ .fg = C.orange }); + // OCT var oct_buf: [256]u8 = undefined; const oct = engine.formatter.formatOctal(&oct_buf, val, bw); @@ -72,11 +78,11 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei .{ .fg = C.bg, .bg = C.purple, .bold = true } else .{ .fg = C.cyan }; - draw.writeStr(surface, base_start + 3, 2, "OCT:", oct_style); + draw.writeStr(surface, base_start + 4, 2, "OCT:", oct_style); if (focused == .oct) { - drawFieldWithCursor(surface, base_start + 3, 11, oct.display, app.bit_cursor, 3, bw.bits(), C.purple); + drawFieldWithCursor(surface, base_start + 4, 11, oct.display, app.bit_cursor, 3, bw.bits(), C.purple); } else { - draw.writeStr(surface, base_start + 3, 11, oct.display, .{ .fg = C.purple }); + draw.writeStr(surface, base_start + 4, 11, oct.display, .{ .fg = C.purple }); } // BIN @@ -86,15 +92,15 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei .{ .fg = C.bg, .bg = C.yellow, .bold = true } else .{ .fg = C.cyan }; - draw.writeStr(surface, base_start + 4, 2, "BIN:", bin_style); + draw.writeStr(surface, base_start + 5, 2, "BIN:", bin_style); if (focused == .bin) { - drawFieldWithCursor(surface, base_start + 4, 11, bin.display, app.bit_cursor, 1, bw.bits(), C.yellow); + drawFieldWithCursor(surface, base_start + 5, 11, bin.display, app.bit_cursor, 1, bw.bits(), C.yellow); } else { - draw.writeStr(surface, base_start + 4, 11, bin.display, .{ .fg = C.yellow }); + draw.writeStr(surface, base_start + 5, 11, bin.display, .{ .fg = C.yellow }); } // History - const hist_start = base_start + 6; + const hist_start = base_start + 7; const hist_end = height -| 4; if (hist_start < hist_end) { tui.drawHistory(app.history.items, surface, hist_start, hist_end);