add ascii to programmer mode

This commit is contained in:
Emil Lerch 2026-07-24 12:09:29 -07:00
parent 51b63b4fb8
commit 6a5bdf1e5e
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 113 additions and 11 deletions

View file

@ -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:

View file

@ -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);
}

View file

@ -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);