add tui (unreviewed ai gen)
This commit is contained in:
parent
f73834b333
commit
18b112747d
5 changed files with 534 additions and 17 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -15,3 +15,4 @@ zig-out/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
coverage/
|
coverage/
|
||||||
|
zig-pkg/
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,12 @@ pub fn build(b: *std.Build) void {
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -- Vaxis dependency (TUI library) --
|
||||||
|
const vaxis_dep = b.dependency("vaxis", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
// -- Engine static library (for linking into the main binary) --
|
// -- Engine static library (for linking into the main binary) --
|
||||||
const engine_lib = b.addLibrary(.{
|
const engine_lib = b.addLibrary(.{
|
||||||
.linkage = .static,
|
.linkage = .static,
|
||||||
|
|
@ -44,6 +50,7 @@ pub fn build(b: *std.Build) void {
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "engine", .module = engine_mod },
|
.{ .name = "engine", .module = engine_mod },
|
||||||
|
.{ .name = "vaxis", .module = vaxis_dep.module("vaxis") },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -65,6 +72,7 @@ pub fn build(b: *std.Build) void {
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "engine", .module = engine_mod },
|
.{ .name = "engine", .module = engine_mod },
|
||||||
|
.{ .name = "vaxis", .module = vaxis_dep.module("vaxis") },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,10 @@
|
||||||
"engine",
|
"engine",
|
||||||
"src",
|
"src",
|
||||||
},
|
},
|
||||||
.dependencies = .{},
|
.dependencies = .{
|
||||||
|
.vaxis = .{
|
||||||
|
.url = "git+https://github.com/rockorager/libvaxis.git?ref=main#1dbbe575dff4586fe51e3217aa5c3fecdcbb6089",
|
||||||
|
.hash = "vaxis-0.6.0-BWNV_CrbCQCscGpzsAlR402rYQ_tV3aAl081c2iRRkka",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/main.zig
24
src/main.zig
|
|
@ -1,5 +1,6 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const engine = @import("engine");
|
const engine = @import("engine");
|
||||||
|
const tui = @import("tui.zig");
|
||||||
|
|
||||||
/// Result of CLI evaluation - pure data, no I/O.
|
/// Result of CLI evaluation - pure data, no I/O.
|
||||||
pub const CliResult = struct {
|
pub const CliResult = struct {
|
||||||
|
|
@ -131,21 +132,6 @@ const help_text =
|
||||||
\\
|
\\
|
||||||
;
|
;
|
||||||
|
|
||||||
const usage_text =
|
|
||||||
\\tally - a cross-platform calculator
|
|
||||||
\\
|
|
||||||
\\Usage:
|
|
||||||
\\ tally "<expression>" Evaluate expression
|
|
||||||
\\ tally -p "<expression>" Programmer mode
|
|
||||||
\\ tally --help Show help
|
|
||||||
\\
|
|
||||||
\\Examples:
|
|
||||||
\\ tally "2^32 - 1"
|
|
||||||
\\ tally -p "0xFF & 0x0F"
|
|
||||||
\\ tally "sin(pi/2) + 1"
|
|
||||||
\\
|
|
||||||
;
|
|
||||||
|
|
||||||
// -- Entry point: thin I/O shell over pure logic --
|
// -- Entry point: thin I/O shell over pure logic --
|
||||||
|
|
||||||
pub fn main(init: std.process.Init) u8 {
|
pub fn main(init: std.process.Init) u8 {
|
||||||
|
|
@ -158,7 +144,13 @@ pub fn main(init: std.process.Init) u8 {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (args.len <= 1) {
|
if (args.len <= 1) {
|
||||||
write(io, std.Io.File.stdout(), usage_text);
|
tui.run(allocator, io, init.environ_map) catch |err| {
|
||||||
|
write(io, std.Io.File.stderr(), "error: failed to start TUI: ");
|
||||||
|
const err_name = @errorName(err);
|
||||||
|
write(io, std.Io.File.stderr(), err_name);
|
||||||
|
write(io, std.Io.File.stderr(), "\n");
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
511
src/tui.zig
Normal file
511
src/tui.zig
Normal file
|
|
@ -0,0 +1,511 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const vaxis = @import("vaxis");
|
||||||
|
const vxfw = vaxis.vxfw;
|
||||||
|
const engine = @import("engine");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
/// TUI application state.
|
||||||
|
pub const App = struct {
|
||||||
|
allocator: Allocator,
|
||||||
|
io: std.Io,
|
||||||
|
env: engine.evaluator.Environment,
|
||||||
|
input: vxfw.TextField,
|
||||||
|
history: std.ArrayList(HistoryEntry),
|
||||||
|
show_help: bool,
|
||||||
|
/// Index into history for up/down navigation.
|
||||||
|
/// null means we're on a fresh input (not browsing history).
|
||||||
|
/// 0 = most recent entry, 1 = second most recent, etc.
|
||||||
|
history_browse_idx: ?usize,
|
||||||
|
/// Saved current input when user starts browsing history
|
||||||
|
saved_input: ?[]const u8,
|
||||||
|
|
||||||
|
const HistoryEntry = struct {
|
||||||
|
expr: []const u8,
|
||||||
|
result: []const u8,
|
||||||
|
is_error: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, io: std.Io) App {
|
||||||
|
var text_field = vxfw.TextField.init(allocator);
|
||||||
|
text_field.style = .{ .fg = .{ .index = 15 } };
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.io = io,
|
||||||
|
.env = engine.evaluator.Environment.init(allocator, .standard),
|
||||||
|
.input = text_field,
|
||||||
|
.history = .empty,
|
||||||
|
.show_help = false,
|
||||||
|
.history_browse_idx = null,
|
||||||
|
.saved_input = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *App) void {
|
||||||
|
self.input.deinit();
|
||||||
|
self.env.deinit();
|
||||||
|
for (self.history.items) |entry| {
|
||||||
|
self.allocator.free(entry.expr);
|
||||||
|
self.allocator.free(entry.result);
|
||||||
|
}
|
||||||
|
self.history.deinit(self.allocator);
|
||||||
|
if (self.saved_input) |s| self.allocator.free(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn widget(self: *App) vxfw.Widget {
|
||||||
|
return .{
|
||||||
|
.userdata = self,
|
||||||
|
.eventHandler = typeErasedEventHandler,
|
||||||
|
.drawFn = typeErasedDrawFn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
|
||||||
|
const self: *App = @ptrCast(@alignCast(ptr));
|
||||||
|
switch (event) {
|
||||||
|
.key_press => |key| try self.handleKey(ctx, key),
|
||||||
|
.init => ctx.redraw = true,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleKey(self: *App, ctx: *vxfw.EventContext, key: vaxis.Key) !void {
|
||||||
|
if (key.matches('c', .{ .ctrl = true })) {
|
||||||
|
ctx.quit = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.matches('d', .{ .ctrl = true })) {
|
||||||
|
ctx.quit = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help toggle
|
||||||
|
if (self.show_help) {
|
||||||
|
// Any key dismisses help
|
||||||
|
self.show_help = false;
|
||||||
|
ctx.redraw = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.matches('?', .{})) {
|
||||||
|
self.show_help = true;
|
||||||
|
ctx.redraw = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.matches(vaxis.Key.enter, .{})) {
|
||||||
|
try self.submitExpression();
|
||||||
|
ctx.redraw = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl-L: clear history
|
||||||
|
if (key.matches('l', .{ .ctrl = true })) {
|
||||||
|
for (self.history.items) |entry| {
|
||||||
|
self.allocator.free(entry.expr);
|
||||||
|
self.allocator.free(entry.result);
|
||||||
|
}
|
||||||
|
self.history.clearRetainingCapacity();
|
||||||
|
self.history_browse_idx = null;
|
||||||
|
if (self.saved_input) |s| {
|
||||||
|
self.allocator.free(s);
|
||||||
|
self.saved_input = null;
|
||||||
|
}
|
||||||
|
ctx.redraw = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up arrow: browse history (older)
|
||||||
|
if (key.matches(vaxis.Key.up, .{})) {
|
||||||
|
if (self.history.items.len == 0) return;
|
||||||
|
if (self.history_browse_idx) |idx| {
|
||||||
|
// Already browsing, go further back
|
||||||
|
if (idx + 1 < self.history.items.len) {
|
||||||
|
self.history_browse_idx = idx + 1;
|
||||||
|
self.setInputFromHistory(idx + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Start browsing: save current input, show most recent
|
||||||
|
try self.saveCurrentInput();
|
||||||
|
self.history_browse_idx = 0;
|
||||||
|
self.setInputFromHistory(0);
|
||||||
|
}
|
||||||
|
ctx.redraw = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Down arrow: browse history (newer)
|
||||||
|
if (key.matches(vaxis.Key.down, .{})) {
|
||||||
|
if (self.history_browse_idx) |idx| {
|
||||||
|
if (idx == 0) {
|
||||||
|
// Back to the saved input
|
||||||
|
self.history_browse_idx = null;
|
||||||
|
self.restoreSavedInput();
|
||||||
|
} else {
|
||||||
|
self.history_browse_idx = idx - 1;
|
||||||
|
self.setInputFromHistory(idx - 1);
|
||||||
|
}
|
||||||
|
ctx.redraw = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass all other keys to the text field for editing
|
||||||
|
try self.input.handleEvent(ctx, .{ .key_press = key });
|
||||||
|
// Any edit resets history browsing
|
||||||
|
self.history_browse_idx = null;
|
||||||
|
ctx.redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submitExpression(self: *App) !void {
|
||||||
|
const first = self.input.buf.firstHalf();
|
||||||
|
const second = self.input.buf.secondHalf();
|
||||||
|
|
||||||
|
const expr_text = try self.allocator.alloc(u8, first.len + second.len);
|
||||||
|
@memcpy(expr_text[0..first.len], first);
|
||||||
|
@memcpy(expr_text[first.len..], second);
|
||||||
|
|
||||||
|
if (expr_text.len == 0) {
|
||||||
|
self.allocator.free(expr_text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate directly through our persistent environment
|
||||||
|
const is_error, const display_text = if (engine.evalString(&self.env, self.allocator, expr_text)) |value| blk: {
|
||||||
|
var fmt_buf: [4096]u8 = undefined;
|
||||||
|
const formatted = engine.formatter.formatFloat(&fmt_buf, value);
|
||||||
|
break :blk .{ false, try self.allocator.dupe(u8, formatted.display) };
|
||||||
|
} else |err| blk: {
|
||||||
|
break :blk .{ true, @as([]const u8, switch (err) {
|
||||||
|
engine.CalcError.DivisionByZero => "error: division by zero",
|
||||||
|
engine.CalcError.UnknownFunction => "error: unknown function",
|
||||||
|
engine.CalcError.UnknownVariable => "error: unknown variable",
|
||||||
|
engine.CalcError.UnmatchedParen => "error: unmatched parenthesis",
|
||||||
|
engine.CalcError.UnexpectedToken => "error: unexpected token",
|
||||||
|
engine.CalcError.UnexpectedEnd => "error: unexpected end of expression",
|
||||||
|
engine.CalcError.InvalidNumber => "error: invalid number",
|
||||||
|
engine.CalcError.DomainError => "error: domain error",
|
||||||
|
engine.CalcError.Overflow => "error: overflow",
|
||||||
|
else => "error: evaluation error",
|
||||||
|
}) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const result_copy = if (is_error)
|
||||||
|
try self.allocator.dupe(u8, display_text)
|
||||||
|
else
|
||||||
|
display_text;
|
||||||
|
|
||||||
|
try self.history.append(self.allocator, .{
|
||||||
|
.expr = expr_text,
|
||||||
|
.result = result_copy,
|
||||||
|
.is_error = is_error,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.input.clearRetainingCapacity();
|
||||||
|
self.history_browse_idx = null;
|
||||||
|
if (self.saved_input) |s| {
|
||||||
|
self.allocator.free(s);
|
||||||
|
self.saved_input = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn saveCurrentInput(self: *App) !void {
|
||||||
|
if (self.saved_input) |s| self.allocator.free(s);
|
||||||
|
const first = self.input.buf.firstHalf();
|
||||||
|
const second = self.input.buf.secondHalf();
|
||||||
|
const text = try self.allocator.alloc(u8, first.len + second.len);
|
||||||
|
@memcpy(text[0..first.len], first);
|
||||||
|
@memcpy(text[first.len..], second);
|
||||||
|
self.saved_input = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setInputFromHistory(self: *App, browse_idx: usize) void {
|
||||||
|
// browse_idx 0 = most recent (last item), 1 = second most recent, etc.
|
||||||
|
const actual_idx = self.history.items.len - 1 - browse_idx;
|
||||||
|
const entry = self.history.items[actual_idx];
|
||||||
|
self.input.clearRetainingCapacity();
|
||||||
|
self.input.insertSliceAtCursor(entry.expr) catch return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restoreSavedInput(self: *App) void {
|
||||||
|
self.input.clearRetainingCapacity();
|
||||||
|
if (self.saved_input) |s| {
|
||||||
|
self.input.insertSliceAtCursor(s) catch return;
|
||||||
|
self.allocator.free(s);
|
||||||
|
self.saved_input = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
|
||||||
|
const self: *App = @ptrCast(@alignCast(ptr));
|
||||||
|
const width = ctx.max.width orelse 80;
|
||||||
|
const height = ctx.max.height orelse 24;
|
||||||
|
|
||||||
|
var surface = try vxfw.Surface.init(ctx.arena, self.widget(), .{
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (self.show_help) {
|
||||||
|
self.drawHelp(&surface, width, height);
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Title bar (row 0) -- Molokai cyan on dark
|
||||||
|
const title_style: vaxis.Style = .{
|
||||||
|
.fg = .{ .rgb = .{ 0x66, 0xD9, 0xEF } }, // molokai cyan
|
||||||
|
.bg = .{ .rgb = .{ 0x1B, 0x1D, 0x1E } }, // molokai bg
|
||||||
|
.bold = true,
|
||||||
|
};
|
||||||
|
const title_mode_style: vaxis.Style = .{
|
||||||
|
.fg = .{ .rgb = .{ 0xA6, 0xE2, 0x2E } }, // molokai green
|
||||||
|
.bg = .{ .rgb = .{ 0x1B, 0x1D, 0x1E } },
|
||||||
|
};
|
||||||
|
self.fillRow(&surface, 0, ' ', title_style);
|
||||||
|
self.writeStr(&surface, 0, 1, "Tally", title_style);
|
||||||
|
const mode_str = "[Standard]";
|
||||||
|
const mode_col = width -| @as(u16, @intCast(mode_str.len + 1));
|
||||||
|
self.writeStr(&surface, 0, mode_col, mode_str, title_mode_style);
|
||||||
|
|
||||||
|
// -- History (rows 2 to height-4) --
|
||||||
|
const hist_start: u16 = 2;
|
||||||
|
const hist_end: u16 = height -| 4;
|
||||||
|
|
||||||
|
if (self.history.items.len > 0) {
|
||||||
|
const avail_rows = hist_end -| hist_start;
|
||||||
|
const max_entries = avail_rows / 2;
|
||||||
|
const start_idx = if (self.history.items.len > max_entries)
|
||||||
|
self.history.items.len - max_entries
|
||||||
|
else
|
||||||
|
0;
|
||||||
|
|
||||||
|
var row: u16 = hist_start;
|
||||||
|
for (self.history.items[start_idx..]) |entry| {
|
||||||
|
if (row + 1 >= hist_end) break;
|
||||||
|
// Expression in muted text
|
||||||
|
self.writeStr(&surface, row, 2, entry.expr, .{
|
||||||
|
.fg = .{ .rgb = .{ 0x75, 0x71, 0x5E } }, // molokai muted
|
||||||
|
});
|
||||||
|
row += 1;
|
||||||
|
// Result: green for success, pink for error
|
||||||
|
const res_style: vaxis.Style = if (entry.is_error)
|
||||||
|
.{ .fg = .{ .rgb = .{ 0xF9, 0x26, 0x72 } } } // molokai pink
|
||||||
|
else
|
||||||
|
.{ .fg = .{ .rgb = .{ 0xA6, 0xE2, 0x2E } }, .bold = true }; // molokai green
|
||||||
|
self.writeStr(&surface, row, 4, "= ", res_style);
|
||||||
|
self.writeStr(&surface, row, 6, entry.result, res_style);
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Separator (row height-3) --
|
||||||
|
const sep_row = height -| 3;
|
||||||
|
self.fillRow(&surface, sep_row, '-', .{ .fg = .{ .rgb = .{ 0x46, 0x48, 0x3E } } }); // molokai comment-ish
|
||||||
|
|
||||||
|
// -- Input line (row height-2) --
|
||||||
|
const input_row = height -| 2;
|
||||||
|
self.writeStr(&surface, input_row, 1, "> ", .{
|
||||||
|
.fg = .{ .rgb = .{ 0xF9, 0x26, 0x72 } }, // molokai pink prompt
|
||||||
|
.bold = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = self.input.buf.firstHalf();
|
||||||
|
const second = self.input.buf.secondHalf();
|
||||||
|
const input_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0xF8, 0xF8, 0xF2 } } }; // molokai fg
|
||||||
|
var col: u16 = 3;
|
||||||
|
for (first) |byte| {
|
||||||
|
self.writeChar(&surface, input_row, col, byte, input_style);
|
||||||
|
col += 1;
|
||||||
|
}
|
||||||
|
for (second) |byte| {
|
||||||
|
self.writeChar(&surface, input_row, col, byte, input_style);
|
||||||
|
col += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor
|
||||||
|
const cursor_col: u16 = 3 + @as(u16, @intCast(self.input.buf.cursor));
|
||||||
|
surface.cursor = .{ .col = cursor_col, .row = input_row, .shape = .block };
|
||||||
|
|
||||||
|
// -- Status bar (row height-1) --
|
||||||
|
const status_row = height -| 1;
|
||||||
|
const status_style: vaxis.Style = .{
|
||||||
|
.fg = .{ .rgb = .{ 0x75, 0x71, 0x5E } }, // molokai muted
|
||||||
|
.bg = .{ .rgb = .{ 0x1B, 0x1D, 0x1E } }, // molokai bg
|
||||||
|
};
|
||||||
|
self.fillRow(&surface, status_row, ' ', status_style);
|
||||||
|
self.writeStr(&surface, status_row, 1, "?:help | Enter:eval | Ctrl-L:clear | Ctrl-C:quit", status_style);
|
||||||
|
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drawHelp(self: *App, surface: *vxfw.Surface, width: u16, height: u16) void {
|
||||||
|
const header_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0x66, 0xD9, 0xEF } }, .bold = true }; // cyan
|
||||||
|
const key_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0xE6, 0xDB, 0x74 } } }; // yellow
|
||||||
|
const text_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0xF8, 0xF8, 0xF2 } } }; // fg
|
||||||
|
const section_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0xAE, 0x81, 0xFF } }, .bold = true }; // purple
|
||||||
|
|
||||||
|
// Background
|
||||||
|
for (0..height) |r| {
|
||||||
|
self.fillRow(surface, @intCast(r), ' ', .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
var row: u16 = 1;
|
||||||
|
self.writeStr(surface, row, 2, "Tally - Help", header_style);
|
||||||
|
row += 2;
|
||||||
|
|
||||||
|
// Keybindings
|
||||||
|
self.writeStr(surface, row, 2, "Keybindings", section_style);
|
||||||
|
row += 1;
|
||||||
|
self.writeStr(surface, row, 4, "Enter", key_style);
|
||||||
|
self.writeStr(surface, row, 14, "Evaluate expression", text_style);
|
||||||
|
row += 1;
|
||||||
|
self.writeStr(surface, row, 4, "Ctrl-C", key_style);
|
||||||
|
self.writeStr(surface, row, 14, "Quit", text_style);
|
||||||
|
row += 1;
|
||||||
|
self.writeStr(surface, row, 4, "Ctrl-D", key_style);
|
||||||
|
self.writeStr(surface, row, 14, "Quit", text_style);
|
||||||
|
row += 1;
|
||||||
|
self.writeStr(surface, row, 4, "Ctrl-L", key_style);
|
||||||
|
self.writeStr(surface, row, 14, "Clear history", text_style);
|
||||||
|
row += 1;
|
||||||
|
self.writeStr(surface, row, 4, "?", key_style);
|
||||||
|
self.writeStr(surface, row, 14, "Toggle this help", text_style);
|
||||||
|
row += 2;
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
self.writeStr(surface, row, 2, "Variables", section_style);
|
||||||
|
row += 1;
|
||||||
|
self.writeStr(surface, row, 4, "Ans", key_style);
|
||||||
|
self.writeStr(surface, row, 14, "Last result", text_style);
|
||||||
|
row += 1;
|
||||||
|
self.writeStr(surface, row, 4, "X = expr", key_style);
|
||||||
|
self.writeStr(surface, row, 14, "Assign to variable (A-Z, any name)", text_style);
|
||||||
|
row += 2;
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
self.writeStr(surface, row, 2, "Constants", section_style);
|
||||||
|
row += 1;
|
||||||
|
self.writeStr(surface, row, 4, "pi", key_style);
|
||||||
|
self.writeStr(surface, row, 14, "3.14159...", text_style);
|
||||||
|
self.writeStr(surface, row, 30, "e", key_style);
|
||||||
|
self.writeStr(surface, row, 36, "2.71828...", text_style);
|
||||||
|
self.writeStr(surface, row, 52, "tau", key_style);
|
||||||
|
self.writeStr(surface, row, 60, "6.28318...", text_style);
|
||||||
|
row += 2;
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
self.writeStr(surface, row, 2, "Functions", section_style);
|
||||||
|
row += 1;
|
||||||
|
const func_lines = [_][]const u8{
|
||||||
|
"sin cos tan asin acos atan atan2",
|
||||||
|
"log (base-10) ln (natural) log2 exp",
|
||||||
|
"sqrt cbrt abs ceil floor round",
|
||||||
|
"factorial max(a,b) min(a,b)",
|
||||||
|
};
|
||||||
|
for (func_lines) |line| {
|
||||||
|
if (row >= height -| 2) break;
|
||||||
|
self.writeStr(surface, row, 4, line, text_style);
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
row += 1;
|
||||||
|
|
||||||
|
// Operators
|
||||||
|
if (row < height -| 4) {
|
||||||
|
self.writeStr(surface, row, 2, "Operators", section_style);
|
||||||
|
row += 1;
|
||||||
|
self.writeStr(surface, row, 4, "+ - * / % ^ (power)", text_style);
|
||||||
|
row += 1;
|
||||||
|
self.writeStr(surface, row, 4, "Implicit mul: 2pi 3(4+5) (2)(3)", text_style);
|
||||||
|
row += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Syntax
|
||||||
|
if (row < height -| 3) {
|
||||||
|
self.writeStr(surface, row, 2, "Number formats", section_style);
|
||||||
|
row += 1;
|
||||||
|
self.writeStr(surface, row, 4, "Decimal: 1_000_000 Hex: 0xFF Bin: 0b1010 Oct: 0o77", text_style);
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
const footer_row = height -| 1;
|
||||||
|
const footer_style: vaxis.Style = .{ .fg = .{ .rgb = .{ 0x75, 0x71, 0x5E } } }; // molokai muted
|
||||||
|
self.fillRow(surface, footer_row, ' ', footer_style);
|
||||||
|
self.writeStr(surface, footer_row, 1, "Press any key to return", footer_style);
|
||||||
|
|
||||||
|
_ = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Drawing helpers --
|
||||||
|
|
||||||
|
/// Static single-byte grapheme strings. Each entry is a pointer
|
||||||
|
/// to a static [1]u8 array stored in .rodata.
|
||||||
|
const ascii_graphemes: [128]*const [1]u8 = blk: {
|
||||||
|
@setEvalBranchQuota(200);
|
||||||
|
var table: [128]*const [1]u8 = undefined;
|
||||||
|
for (0..128) |i| {
|
||||||
|
table[i] = &[1]u8{@as(u8, @intCast(i))};
|
||||||
|
}
|
||||||
|
break :blk table;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn charGrapheme(byte: u8) []const u8 {
|
||||||
|
if (byte < 128) return ascii_graphemes[byte];
|
||||||
|
return " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fillRow(self: *App, surface: *vxfw.Surface, row: u16, char: u8, style: vaxis.Style) void {
|
||||||
|
_ = self;
|
||||||
|
const width = surface.size.width;
|
||||||
|
const grapheme: []const u8 = charGrapheme(char);
|
||||||
|
for (0..width) |col| {
|
||||||
|
const idx = @as(usize, row) * @as(usize, width) + col;
|
||||||
|
if (idx < surface.buffer.len) {
|
||||||
|
surface.buffer[idx] = .{
|
||||||
|
.char = .{ .grapheme = grapheme, .width = 1 },
|
||||||
|
.style = style,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeStr(self: *App, surface: *vxfw.Surface, row: u16, col: u16, text: []const u8, style: vaxis.Style) void {
|
||||||
|
_ = self;
|
||||||
|
const width = surface.size.width;
|
||||||
|
for (0..text.len) |i| {
|
||||||
|
const c = col + @as(u16, @intCast(i));
|
||||||
|
if (c >= width) break;
|
||||||
|
const idx = @as(usize, row) * @as(usize, width) + @as(usize, c);
|
||||||
|
if (idx < surface.buffer.len) {
|
||||||
|
surface.buffer[idx] = .{
|
||||||
|
.char = .{ .grapheme = charGrapheme(text[i]), .width = 1 },
|
||||||
|
.style = style,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeChar(self: *App, surface: *vxfw.Surface, row: u16, col: u16, byte: u8, style: vaxis.Style) void {
|
||||||
|
_ = self;
|
||||||
|
const width = surface.size.width;
|
||||||
|
if (col >= width) return;
|
||||||
|
const idx = @as(usize, row) * @as(usize, width) + @as(usize, col);
|
||||||
|
if (idx < surface.buffer.len) {
|
||||||
|
surface.buffer[idx] = .{
|
||||||
|
.char = .{ .grapheme = charGrapheme(byte), .width = 1 },
|
||||||
|
.style = style,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn run(allocator: Allocator, io: std.Io, environ_map: *std.process.Environ.Map) !void {
|
||||||
|
var app = App.init(allocator, io);
|
||||||
|
defer app.deinit();
|
||||||
|
|
||||||
|
var vx_buf: [4096]u8 = undefined;
|
||||||
|
var vx_app = try vxfw.App.init(io, allocator, environ_map, &vx_buf);
|
||||||
|
defer vx_app.deinit();
|
||||||
|
|
||||||
|
try vx_app.run(app.widget(), .{});
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue