split tui/start fixing some functional issues
This commit is contained in:
parent
5b6eaed460
commit
68823e4816
8 changed files with 568 additions and 347 deletions
|
|
@ -205,15 +205,14 @@ Values are displayed with visual grouping separators to aid readability, but cli
|
||||||
|
|
||||||
| Base | Display (value view) | Display (memory view) | Clipboard |
|
| 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` |
|
| Bin | `1111 1111 1111 1111 1111 1111 1111 1111` | same | `0b11111111111111111111111111111111` |
|
||||||
| Oct | `0o37_777_777_777` | - | `0o37777777777` |
|
| Oct | `0o37_777_777_777` | - | `0o37777777777` |
|
||||||
| Dec | `4,294,967,295` | - | `4294967295` |
|
| Dec | `4,294,967,295` | - | `4294967295` |
|
||||||
|
|
||||||
Rationale for hex grouping:
|
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.
|
- **Display uses space per byte** (`FF FF FF FF`): matches hex editors, debuggers, `xxd` output. Consistent with binary's nibble-space grouping. Universally readable.
|
||||||
- **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). For this calculator, both use the same space-separated format.
|
||||||
- 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).
|
|
||||||
|
|
||||||
Binary always groups by nibble (4 bits) with spaces - universally expected by programmers.
|
Binary always groups by nibble (4 bits) with spaces - universally expected by programmers.
|
||||||
|
|
||||||
|
|
@ -595,9 +594,39 @@ $ tally "5 kg + 3 lb"
|
||||||
│ > 0xFF & 0x0F_ │
|
│ > 0xFF & 0x0F_ │
|
||||||
│ = 15 │
|
│ = 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)
|
### 8.3 Struct Visualizer (sub-view of Programmer Mode)
|
||||||
```
|
```
|
||||||
┌─ Tally ────────────────── [Programmer > Struct Layout] ─────────────┐
|
┌─ Tally ────────────────── [Programmer > Struct Layout] ─────────────┐
|
||||||
|
|
@ -670,7 +699,7 @@ Classic calculator layout. Expression builds at top, result previews live below
|
||||||
│ ├─────┼─────┼─────┼─────┼─────┤ │ │
|
│ ├─────┼─────┼─────┼─────┼─────┤ │ │
|
||||||
│ │ 4 │ 5 │ 6 │ × │ % │ │ │
|
│ │ 4 │ 5 │ 6 │ × │ % │ │ │
|
||||||
│ ├─────┼─────┼─────┼─────┼─────┤ │ │
|
│ ├─────┼─────┼─────┼─────┼─────┤ │ │
|
||||||
│ │ 1 │ 2 │ 3 │ − │ √ │ │ │
|
│ │ 1 │ 2 │ 3 │ - │ sqrt│ │ │
|
||||||
│ ├─────┼─────┼─────┼─────┼─────┤ │ │
|
│ ├─────┼─────┼─────┼─────┼─────┤ │ │
|
||||||
│ │ 0 │ . │ Ans │ + │ = │ │ │
|
│ │ 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
|
│ │ DEC 4,294,967,295 │ │ ← tappable to switch primary
|
||||||
│ │ HEX 0xFFFF_FFFF │ │
|
│ │ HEX 0xFFFF_FFFF │ │
|
||||||
│ │ OCT 0o37_777_777_777 │ │
|
│ │ OCT 0o37_777_777_777 │ │
|
||||||
│ │ BIN 1111 1111 1111 1111 111… │ │
|
│ │ BIN 1111 1111 1111 1111 11.. │ │
|
||||||
│ │ SIGN -1 (signed) │ │
|
│ │ SIGN -1 (signed) │ │
|
||||||
│ └──────────────────────────────────┘ │
|
│ └──────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
|
|
@ -814,8 +843,8 @@ Card-based interface with function selector at top.
|
||||||
│ │ CAGR = 20.11% │ │
|
│ │ CAGR = 20.11% │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ Formula: │ │
|
│ │ Formula: │ │
|
||||||
│ │ (25000/10000)^(1/5) − 1 │ │
|
│ │ (25000/10000)^(1/5) - 1 │ │
|
||||||
│ │ = 2.5^0.2 − 1 │ │
|
│ │ = 2.5^0.2 - 1 │ │
|
||||||
│ │ = 0.2011... │ │
|
│ │ = 0.2011... │ │
|
||||||
│ └─────────────────────────────────┘ │
|
│ └─────────────────────────────────┘ │
|
||||||
└─────────────────────────────────────────┘
|
└─────────────────────────────────────────┘
|
||||||
|
|
|
||||||
|
|
@ -231,16 +231,30 @@ Subcommands deferred until Phase 2 engine work is done.
|
||||||
- Variables persist across expressions (assignment + Ans)
|
- Variables persist across expressions (assignment + Ans)
|
||||||
- Verify: can type expressions, see results, browse history
|
- Verify: can type expressions, see results, browse history
|
||||||
|
|
||||||
### Task 5.3: Implement programmer mode TUI view - expression & base display
|
### Task 5.3: Implement programmer mode TUI view - expression & base display [PARTIAL]
|
||||||
- Create `tui/src/views/programmer.zig`
|
- Programmer mode accessible via Tab key
|
||||||
- Expression input (same as standard but in programmer mode)
|
|
||||||
- Multi-base result display (dec signed, unsigned, hex, oct, bin)
|
- Multi-base result display (dec signed, unsigned, hex, oct, bin)
|
||||||
- Quick-toggle keys: `d` highlights dec, `h` highlights hex, `o` oct, `b` bin
|
- Bit grid with navigation (arrows) and toggling (Space)
|
||||||
- Status bar showing current bit width, signedness, endianness
|
- Width toggle (Ctrl-W cycles 8->16->32->64)
|
||||||
- Width toggle (`w` key cycles 8->16->32->64), sign toggle (`s`), endian toggle (`e`)
|
- Ctrl-B toggles focus between input and bit grid
|
||||||
- Verify: expressions evaluate in programmer mode, base display updates, toggles work
|
- 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
|
- 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)
|
- Arrow key navigation (left/right moves cursor across bits, up/down between rows for 32/64-bit)
|
||||||
- Space/Enter toggles the bit under cursor
|
- Space/Enter toggles the bit under cursor
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ pub fn formatFloat(buf: []u8, value: f64) FormattedValue {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format an integer for programmer mode hex display.
|
/// Format an integer for programmer mode hex display.
|
||||||
/// Display: "0xFFFF_FFFF" (underscore per 16-bit word)
|
/// Display: "FF FF FF FF" (space per byte)
|
||||||
/// Raw: "0xFFFFFFFF" (no separators)
|
/// Raw: "0xFFFFFFFF" (no separators, with prefix)
|
||||||
pub fn formatHex(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue {
|
pub fn formatHex(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue {
|
||||||
const width = bit_width.bits();
|
const width = bit_width.bits();
|
||||||
const hex_digits: usize = @as(usize, width) / 4;
|
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];
|
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;
|
const display_start = pos;
|
||||||
buf[pos] = '0';
|
|
||||||
buf[pos + 1] = 'x';
|
|
||||||
pos += 2;
|
|
||||||
i = 0;
|
i = 0;
|
||||||
while (i < hex_digits) : (i += 1) {
|
while (i < hex_digits) : (i += 1) {
|
||||||
if (i > 0 and i % 4 == 0) {
|
if (i > 0 and i % 2 == 0) {
|
||||||
buf[pos] = '_';
|
buf[pos] = ' ';
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
const shift_amt: u6 = @intCast((hex_digits - 1 - i) * 4);
|
const shift_amt: u6 = @intCast((hex_digits - 1 - i) * 4);
|
||||||
|
|
@ -362,35 +359,35 @@ test "formatFloat: zero" {
|
||||||
test "formatHex: 8-bit" {
|
test "formatHex: 8-bit" {
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
const result = formatHex(&buf, 0xFF, .bits8);
|
const result = formatHex(&buf, 0xFF, .bits8);
|
||||||
try testing.expectEqualStrings("0xFF", result.display);
|
try testing.expectEqualStrings("FF", result.display);
|
||||||
try testing.expectEqualStrings("0xFF", result.raw);
|
try testing.expectEqualStrings("0xFF", result.raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "formatHex: 16-bit" {
|
test "formatHex: 16-bit" {
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
const result = formatHex(&buf, 0xABCD, .bits16);
|
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);
|
try testing.expectEqualStrings("0xABCD", result.raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "formatHex: 32-bit with grouping" {
|
test "formatHex: 32-bit with grouping" {
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
const result = formatHex(&buf, 0xDEADBEEF, .bits32);
|
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);
|
try testing.expectEqualStrings("0xDEADBEEF", result.raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "formatHex: 64-bit with grouping" {
|
test "formatHex: 64-bit with grouping" {
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
const result = formatHex(&buf, 0xDEAD_BEEF_CAFE_BABE, .bits64);
|
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);
|
try testing.expectEqualStrings("0xDEADBEEFCAFEBABE", result.raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "formatHex: zero 32-bit" {
|
test "formatHex: zero 32-bit" {
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
const result = formatHex(&buf, 0, .bits32);
|
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);
|
try testing.expectEqualStrings("0x00000000", result.raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,25 +79,26 @@ fn formatProgrammerResult(buf: []u8, result: engine.types.Integer, config: engin
|
||||||
const value = result.unsignedValue();
|
const value = result.unsignedValue();
|
||||||
const signed = result.signedValue();
|
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 hex_buf: [256]u8 = undefined;
|
||||||
var dec_buf: [256]u8 = undefined;
|
var dec_buf: [256]u8 = undefined;
|
||||||
var sdec_buf: [256]u8 = undefined;
|
var sdec_buf: [256]u8 = undefined;
|
||||||
|
var oct_buf: [256]u8 = undefined;
|
||||||
var bin_buf: [512]u8 = undefined;
|
var bin_buf: [512]u8 = undefined;
|
||||||
|
|
||||||
const hex = engine.formatter.formatHex(&hex_buf, value, config.bit_width);
|
const hex = engine.formatter.formatHex(&hex_buf, value, config.bit_width);
|
||||||
const dec = engine.formatter.formatDecimalUnsigned(&dec_buf, value);
|
const dec = engine.formatter.formatDecimalUnsigned(&dec_buf, value);
|
||||||
const sdec = engine.formatter.formatDecimalSigned(&sdec_buf, signed);
|
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 bin = engine.formatter.formatBinary(&bin_buf, value, config.bit_width);
|
||||||
|
|
||||||
const output = std.fmt.bufPrint(buf,
|
const output = std.fmt.bufPrint(buf,
|
||||||
\\ dec(signed): {s}
|
\\ dec(signed): {s}
|
||||||
\\ dec(unsigned): {s}
|
\\ dec(unsigned): {s}
|
||||||
\\ hex: {s}
|
\\ hex: {s}
|
||||||
|
\\ oct: {s}
|
||||||
\\ bin: {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 };
|
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);
|
const result = evaluate(arena.allocator(), "0xFF & 0x0F", .programmer, &buf);
|
||||||
try testing.expect(!result.is_error);
|
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, "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" {
|
test "evaluate: division by zero error" {
|
||||||
|
|
|
||||||
557
src/tui.zig
557
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 std = @import("std");
|
||||||
const vaxis = @import("vaxis");
|
const vaxis = @import("vaxis");
|
||||||
const vxfw = vaxis.vxfw;
|
const vxfw = vaxis.vxfw;
|
||||||
const engine = @import("engine");
|
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;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
/// TUI application state.
|
const Mode = enum { standard, programmer };
|
||||||
|
|
||||||
pub const App = struct {
|
pub const App = struct {
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
io: std.Io,
|
io: std.Io,
|
||||||
|
|
@ -13,14 +25,16 @@ pub const App = struct {
|
||||||
input: vxfw.TextField,
|
input: vxfw.TextField,
|
||||||
history: std.ArrayList(HistoryEntry),
|
history: std.ArrayList(HistoryEntry),
|
||||||
show_help: bool,
|
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,
|
history_browse_idx: ?usize,
|
||||||
/// Saved current input when user starts browsing history
|
|
||||||
saved_input: ?[]const u8,
|
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,
|
expr: []const u8,
|
||||||
result: []const u8,
|
result: []const u8,
|
||||||
is_error: bool,
|
is_error: bool,
|
||||||
|
|
@ -28,7 +42,7 @@ pub const App = struct {
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, io: std.Io) App {
|
pub fn init(allocator: Allocator, io: std.Io) App {
|
||||||
var text_field = vxfw.TextField.init(allocator);
|
var text_field = vxfw.TextField.init(allocator);
|
||||||
text_field.style = .{ .fg = .{ .index = 15 } };
|
text_field.style = .{ .fg = C.fg };
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.io = io,
|
.io = io,
|
||||||
|
|
@ -38,6 +52,11 @@ pub const App = struct {
|
||||||
.show_help = false,
|
.show_help = false,
|
||||||
.history_browse_idx = null,
|
.history_browse_idx = null,
|
||||||
.saved_input = 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 {
|
fn handleKey(self: *App, ctx: *vxfw.EventContext, key: vaxis.Key) !void {
|
||||||
if (key.matches('c', .{ .ctrl = true })) {
|
if (key.matches('c', .{ .ctrl = true }) or key.matches('d', .{ .ctrl = true })) {
|
||||||
ctx.quit = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key.matches('d', .{ .ctrl = true })) {
|
|
||||||
ctx.quit = true;
|
ctx.quit = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help toggle
|
|
||||||
if (self.show_help) {
|
if (self.show_help) {
|
||||||
// Any key dismisses help
|
|
||||||
self.show_help = false;
|
self.show_help = false;
|
||||||
ctx.redraw = true;
|
ctx.redraw = true;
|
||||||
return;
|
return;
|
||||||
|
|
@ -93,13 +106,57 @@ pub const App = struct {
|
||||||
return;
|
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, .{})) {
|
if (key.matches(vaxis.Key.enter, .{})) {
|
||||||
try self.submitExpression();
|
try self.submitExpression();
|
||||||
ctx.redraw = true;
|
ctx.redraw = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl-L: clear history
|
|
||||||
if (key.matches('l', .{ .ctrl = true })) {
|
if (key.matches('l', .{ .ctrl = true })) {
|
||||||
for (self.history.items) |entry| {
|
for (self.history.items) |entry| {
|
||||||
self.allocator.free(entry.expr);
|
self.allocator.free(entry.expr);
|
||||||
|
|
@ -115,52 +172,77 @@ pub const App = struct {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Up arrow: browse history (older)
|
if (!self.bit_grid_active) {
|
||||||
if (key.matches(vaxis.Key.up, .{})) {
|
if (key.matches(vaxis.Key.up, .{})) {
|
||||||
if (self.history.items.len == 0) return;
|
if (self.history.items.len == 0) return;
|
||||||
if (self.history_browse_idx) |idx| {
|
if (self.history_browse_idx) |idx| {
|
||||||
// Already browsing, go further back
|
if (idx + 1 < self.history.items.len) {
|
||||||
if (idx + 1 < self.history.items.len) {
|
self.history_browse_idx = idx + 1;
|
||||||
self.history_browse_idx = idx + 1;
|
self.setInputFromHistory(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();
|
|
||||||
} else {
|
} else {
|
||||||
self.history_browse_idx = idx - 1;
|
try self.saveCurrentInput();
|
||||||
self.setInputFromHistory(idx - 1);
|
self.history_browse_idx = 0;
|
||||||
|
self.setInputFromHistory(0);
|
||||||
}
|
}
|
||||||
ctx.redraw = true;
|
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 });
|
try self.input.handleEvent(ctx, .{ .key_press = key });
|
||||||
// Any edit resets history browsing
|
|
||||||
self.history_browse_idx = null;
|
self.history_browse_idx = null;
|
||||||
ctx.redraw = true;
|
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 {
|
fn submitExpression(self: *App) !void {
|
||||||
const first = self.input.buf.firstHalf();
|
const first = self.input.buf.firstHalf();
|
||||||
const second = self.input.buf.secondHalf();
|
const second = self.input.buf.secondHalf();
|
||||||
|
|
||||||
const expr_text = try self.allocator.alloc(u8, first.len + second.len);
|
const expr_text = try self.allocator.alloc(u8, first.len + second.len);
|
||||||
@memcpy(expr_text[0..first.len], first);
|
@memcpy(expr_text[0..first.len], first);
|
||||||
@memcpy(expr_text[first.len..], second);
|
@memcpy(expr_text[first.len..], second);
|
||||||
|
|
@ -170,36 +252,11 @@ pub const App = struct {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate directly through our persistent environment
|
if (self.mode == .programmer) {
|
||||||
const is_error, const display_text = if (engine.evalString(&self.env, self.allocator, expr_text)) |value| blk: {
|
try self.submitProgrammer(expr_text);
|
||||||
var fmt_buf: [4096]u8 = undefined;
|
} else {
|
||||||
const formatted = engine.formatter.formatFloat(&fmt_buf, value);
|
try self.submitStandard(expr_text);
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
self.input.clearRetainingCapacity();
|
self.input.clearRetainingCapacity();
|
||||||
self.history_browse_idx = null;
|
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 {
|
fn saveCurrentInput(self: *App) !void {
|
||||||
if (self.saved_input) |s| self.allocator.free(s);
|
if (self.saved_input) |s| self.allocator.free(s);
|
||||||
const first = self.input.buf.firstHalf();
|
const first = self.input.buf.firstHalf();
|
||||||
|
|
@ -220,7 +304,6 @@ pub const App = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setInputFromHistory(self: *App, browse_idx: usize) void {
|
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 actual_idx = self.history.items.len - 1 - browse_idx;
|
||||||
const entry = self.history.items[actual_idx];
|
const entry = self.history.items[actual_idx];
|
||||||
self.input.clearRetainingCapacity();
|
self.input.clearRetainingCapacity();
|
||||||
|
|
@ -236,269 +319,117 @@ pub const App = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Drawing --
|
||||||
|
|
||||||
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
|
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
|
||||||
const self: *App = @ptrCast(@alignCast(ptr));
|
const self: *App = @ptrCast(@alignCast(ptr));
|
||||||
const width = ctx.max.width orelse 80;
|
const width = ctx.max.width orelse 80;
|
||||||
const height = ctx.max.height orelse 24;
|
const height = ctx.max.height orelse 24;
|
||||||
|
|
||||||
var surface = try vxfw.Surface.init(ctx.arena, self.widget(), .{
|
var surface = try vxfw.Surface.init(ctx.arena, self.widget(), .{ .width = width, .height = height });
|
||||||
.width = width,
|
|
||||||
.height = height,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (self.show_help) {
|
if (self.show_help) {
|
||||||
self.drawHelp(&surface, width, height);
|
help_view.drawHelp(&surface, width, height);
|
||||||
return surface;
|
return surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Title bar (row 0) -- Molokai cyan on dark
|
// Title bar
|
||||||
const title_style: vaxis.Style = .{
|
draw.fillRow(&surface, 0, ' ', .{ .fg = C.cyan, .bg = C.bg, .bold = true });
|
||||||
.fg = .{ .rgb = .{ 0x66, 0xD9, 0xEF } }, // molokai cyan
|
draw.writeStr(&surface, 0, 1, "Tally", .{ .fg = C.cyan, .bg = C.bg, .bold = true });
|
||||||
.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);
|
|
||||||
|
|
||||||
// -- History (rows 2 to height-4) --
|
// Mode tabs
|
||||||
const hist_start: u16 = 2;
|
const std_style: vaxis.Style = if (self.mode == .standard)
|
||||||
const hist_end: u16 = height -| 4;
|
.{ .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) {
|
if (self.mode == .programmer) {
|
||||||
const avail_rows = hist_end -| hist_start;
|
programmer_view.drawProgrammerMode(self, &surface, width, height);
|
||||||
const max_entries = avail_rows / 2;
|
} else {
|
||||||
const start_idx = if (self.history.items.len > max_entries)
|
self.drawStandardMode(&surface, width, height);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- 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;
|
return surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drawHelp(self: *App, surface: *vxfw.Surface, width: u16, height: u16) void {
|
fn drawStandardMode(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);
|
|
||||||
|
|
||||||
_ = width;
|
_ = 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
|
const first = self.input.buf.firstHalf();
|
||||||
/// to a static [1]u8 array stored in .rodata.
|
const second = self.input.buf.secondHalf();
|
||||||
const ascii_graphemes: [128]*const [1]u8 = blk: {
|
var col: u16 = 3;
|
||||||
@setEvalBranchQuota(200);
|
for (first) |byte| {
|
||||||
var table: [128]*const [1]u8 = undefined;
|
draw.writeChar(surface, row, col, byte, .{ .fg = C.fg });
|
||||||
for (0..128) |i| {
|
col += 1;
|
||||||
table[i] = &[1]u8{@as(u8, @intCast(i))};
|
|
||||||
}
|
}
|
||||||
break :blk table;
|
for (second) |byte| {
|
||||||
};
|
draw.writeChar(surface, row, col, byte, .{ .fg = C.fg });
|
||||||
|
col += 1;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn writeStr(self: *App, surface: *vxfw.Surface, row: u16, col: u16, text: []const u8, style: vaxis.Style) void {
|
if (!self.bit_grid_active) {
|
||||||
_ = self;
|
const cursor_col: u16 = 3 + @as(u16, @intCast(self.input.buf.cursor));
|
||||||
const width = surface.size.width;
|
surface.cursor = .{ .col = cursor_col, .row = row, .shape = .block };
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
pub fn run(allocator: Allocator, io: std.Io, environ_map: *std.process.Environ.Map) !void {
|
||||||
var app = App.init(allocator, io);
|
var app = App.init(allocator, io);
|
||||||
defer app.deinit();
|
defer app.deinit();
|
||||||
|
|
|
||||||
65
src/tui/draw.zig
Normal file
65
src/tui/draw.zig
Normal file
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/tui/help.zig
Normal file
73
src/tui/help.zig
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
111
src/tui/programmer.zig
Normal file
111
src/tui/programmer.zig
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue