begin help cleanup
This commit is contained in:
parent
b31db345ad
commit
4d538cc24a
6 changed files with 313 additions and 71 deletions
242
src/tui.zig
242
src/tui.zig
|
|
@ -889,6 +889,86 @@ pub const App = struct {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return all formatted key strings bound to the given global
|
||||||
|
/// `action`. Allocated in `arena`. Empty slice if no binding
|
||||||
|
/// exists. Order matches the keymap's binding order.
|
||||||
|
///
|
||||||
|
/// Used by the help overlay and dynamic status hints to render
|
||||||
|
/// "actual current key" rather than hardcoded literals — so that
|
||||||
|
/// rebinding a key in `keys.srf` updates the displayed name.
|
||||||
|
pub fn keysForGlobal(self: *const App, arena: std.mem.Allocator, action: keybinds.Action) ![][]const u8 {
|
||||||
|
var out: std.ArrayList([]const u8) = .empty;
|
||||||
|
for (self.keymap.bindings) |b| {
|
||||||
|
if (b.action != action) continue;
|
||||||
|
var key_buf: [32]u8 = undefined;
|
||||||
|
const s = keybinds.formatKeyCombo(b.key, &key_buf) orelse continue;
|
||||||
|
try out.append(arena, try arena.dupe(u8, s));
|
||||||
|
}
|
||||||
|
return out.toOwnedSlice(arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return all formatted key strings bound to the named tab-local
|
||||||
|
/// action in the given tab's keymap. Looks up user overrides
|
||||||
|
/// first (`tabOverridesFor(scope)`), falling back to the tab
|
||||||
|
/// module's `default_bindings`. The action name is matched against
|
||||||
|
/// variant names of the tab's `Action` enum.
|
||||||
|
///
|
||||||
|
/// `scope` is the tab tag name (e.g. `"options"`, `"history"`)
|
||||||
|
/// matching the `tab_modules` registry. Comptime so we can resolve
|
||||||
|
/// the tab's Action enum type.
|
||||||
|
pub fn keysForTabAction(
|
||||||
|
self: *const App,
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
comptime scope: []const u8,
|
||||||
|
action_tag_name: []const u8,
|
||||||
|
) ![][]const u8 {
|
||||||
|
const Module = @field(tab_modules, scope);
|
||||||
|
var out: std.ArrayList([]const u8) = .empty;
|
||||||
|
|
||||||
|
// Prefer user overrides for this scope when present.
|
||||||
|
if (self.keymap.tabOverridesFor(scope)) |overrides| {
|
||||||
|
for (overrides) |ovr| {
|
||||||
|
if (!std.mem.eql(u8, ovr.action_name, action_tag_name)) continue;
|
||||||
|
var key_buf: [32]u8 = undefined;
|
||||||
|
const s = keybinds.formatKeyCombo(ovr.key, &key_buf) orelse continue;
|
||||||
|
try out.append(arena, try arena.dupe(u8, s));
|
||||||
|
}
|
||||||
|
return out.toOwnedSlice(arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No overrides — read from the tab's default_bindings.
|
||||||
|
for (Module.tab.default_bindings) |binding| {
|
||||||
|
if (!std.mem.eql(u8, @tagName(binding.action), action_tag_name)) continue;
|
||||||
|
// Tab modules use `tab_framework.KeyCombo`, which is
|
||||||
|
// structurally identical to but type-distinct from
|
||||||
|
// `keybinds.KeyCombo`. Repack for `formatKeyCombo`.
|
||||||
|
const combo: keybinds.KeyCombo = .{
|
||||||
|
.codepoint = binding.key.codepoint,
|
||||||
|
.mods = binding.key.mods,
|
||||||
|
};
|
||||||
|
var key_buf: [32]u8 = undefined;
|
||||||
|
const s = keybinds.formatKeyCombo(combo, &key_buf) orelse continue;
|
||||||
|
try out.append(arena, try arena.dupe(u8, s));
|
||||||
|
}
|
||||||
|
return out.toOwnedSlice(arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: like `keysForTabAction` but resolves to whichever
|
||||||
|
/// tab is currently active. Comptime-walks `tab_modules` to find
|
||||||
|
/// the matching scope.
|
||||||
|
fn keysForActiveTabAction(
|
||||||
|
self: *const App,
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
action_tag_name: []const u8,
|
||||||
|
) ![][]const u8 {
|
||||||
|
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
|
||||||
|
if (std.mem.eql(u8, field.name, @tagName(self.active_tab))) {
|
||||||
|
return self.keysForTabAction(arena, field.name, action_tag_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &.{};
|
||||||
|
}
|
||||||
|
|
||||||
/// Outcome of a single keypress in an input-mode buffer (symbol
|
/// Outcome of a single keypress in an input-mode buffer (symbol
|
||||||
/// input, date input, etc.). Returned by `handleInputBuffer` so
|
/// input, date input, etc.). Returned by `handleInputBuffer` so
|
||||||
/// the per-mode caller only needs to wire up the `committed`
|
/// the per-mode caller only needs to wire up the `committed`
|
||||||
|
|
@ -1527,9 +1607,66 @@ pub const App = struct {
|
||||||
self.status_len = len;
|
self.status_len = len;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getStatus(self: *App) []const u8 {
|
/// Returns the current status message. When no message has been
|
||||||
if (self.status_len == 0) return "h/l tabs | j/k select | Enter expand | s select | / symbol | ? help";
|
/// set, builds a dynamic default hint composed from a small set
|
||||||
return self.status_msg[0..self.status_len];
|
/// of always-shown global keys plus the active tab's
|
||||||
|
/// `status_hints`. Allocated in `arena` for the dynamic default;
|
||||||
|
/// the user-set buffer is returned by reference.
|
||||||
|
fn getStatus(self: *App, arena: std.mem.Allocator) []const u8 {
|
||||||
|
if (self.status_len > 0) return self.status_msg[0..self.status_len];
|
||||||
|
return self.buildDefaultStatusHint(arena) catch
|
||||||
|
"h/l tabs | j/k select | / symbol | ? help";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the dynamic default status hint: a small set of always-
|
||||||
|
/// shown global keys (tab nav, cursor, symbol input, help) plus
|
||||||
|
/// the active tab's `status_hints` actions resolved against its
|
||||||
|
/// current bindings. Each fragment is `key label`.
|
||||||
|
fn buildDefaultStatusHint(self: *App, arena: std.mem.Allocator) ![]const u8 {
|
||||||
|
var fragments: std.ArrayList([]const u8) = .empty;
|
||||||
|
|
||||||
|
// Always-shown globals. Each fragment uses the FIRST bound
|
||||||
|
// key for the action; full lists go to the help overlay.
|
||||||
|
const globals = [_]struct {
|
||||||
|
action: keybinds.Action,
|
||||||
|
label: []const u8,
|
||||||
|
}{
|
||||||
|
.{ .action = .prev_tab, .label = "tabs" },
|
||||||
|
.{ .action = .select_next, .label = "select" },
|
||||||
|
.{ .action = .symbol_input, .label = "symbol" },
|
||||||
|
.{ .action = .help, .label = "help" },
|
||||||
|
};
|
||||||
|
for (globals) |g| {
|
||||||
|
const keys = try self.keysForGlobal(arena, g.action);
|
||||||
|
if (keys.len == 0) continue;
|
||||||
|
try fragments.append(arena, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"{s} {s}",
|
||||||
|
.{ keys[0], g.label },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active tab's status_hints — comptime walk to get the right
|
||||||
|
// Action enum + label table.
|
||||||
|
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
|
||||||
|
if (std.mem.eql(u8, field.name, @tagName(self.active_tab))) {
|
||||||
|
const Module = @field(tab_modules, field.name);
|
||||||
|
for (Module.tab.status_hints) |hint_action| {
|
||||||
|
const action_name = @tagName(hint_action);
|
||||||
|
const keys = try self.keysForTabAction(arena, field.name, action_name);
|
||||||
|
if (keys.len == 0) continue;
|
||||||
|
const label = Module.tab.action_labels.get(hint_action);
|
||||||
|
if (label.len == 0) continue;
|
||||||
|
try fragments.append(arena, try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"{s} {s}",
|
||||||
|
.{ keys[0], label },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std.mem.join(arena, " | ", fragments.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn freePortfolioSummary(self: *App) void {
|
pub fn freePortfolioSummary(self: *App) void {
|
||||||
|
|
@ -1775,13 +1912,13 @@ pub const App = struct {
|
||||||
// Show account filter indicator when active, appended to status message
|
// Show account filter indicator when active, appended to status message
|
||||||
if (self.states.portfolio.account_filter != null and self.active_tab == .portfolio) {
|
if (self.states.portfolio.account_filter != null and self.active_tab == .portfolio) {
|
||||||
const af = self.states.portfolio.account_filter.?;
|
const af = self.states.portfolio.account_filter.?;
|
||||||
const msg = self.getStatus();
|
const msg = self.getStatus(ctx.arena);
|
||||||
const filter_text = std.fmt.allocPrint(ctx.arena, "{s} [Account: {s}]", .{ msg, af }) catch msg;
|
const filter_text = std.fmt.allocPrint(ctx.arena, "{s} [Account: {s}]", .{ msg, af }) catch msg;
|
||||||
for (0..@min(filter_text.len, width)) |i| {
|
for (0..@min(filter_text.len, width)) |i| {
|
||||||
buf[i] = .{ .char = .{ .grapheme = glyph(filter_text[i]) }, .style = status_style };
|
buf[i] = .{ .char = .{ .grapheme = glyph(filter_text[i]) }, .style = status_style };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const msg = self.getStatus();
|
const msg = self.getStatus(ctx.arena);
|
||||||
for (0..@min(msg.len, width)) |i| {
|
for (0..@min(msg.len, width)) |i| {
|
||||||
buf[i] = .{ .char = .{ .grapheme = glyph(msg[i]) }, .style = status_style };
|
buf[i] = .{ .char = .{ .grapheme = glyph(msg[i]) }, .style = status_style };
|
||||||
}
|
}
|
||||||
|
|
@ -1843,54 +1980,61 @@ pub const App = struct {
|
||||||
try lines.append(arena, .{ .text = " zfin TUI -- Keybindings", .style = th.headerStyle() });
|
try lines.append(arena, .{ .text = " zfin TUI -- Keybindings", .style = th.headerStyle() });
|
||||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
|
|
||||||
const actions = comptime std.enums.values(keybinds.Action);
|
// ── GLOBAL section ─────────────────────────────────────
|
||||||
const action_labels = [_][]const u8{
|
try lines.append(arena, .{ .text = " GLOBAL", .style = th.headerStyle() });
|
||||||
"Quit", "Refresh", "Previous tab", "Next tab",
|
|
||||||
"Tab 1", "Tab 2", "Tab 3", "Tab 4",
|
|
||||||
"Tab 5", "Tab 6", "Tab 7", "Scroll down",
|
|
||||||
"Scroll up", "Scroll to top", "Scroll to bottom", "Page down",
|
|
||||||
"Page up", "Select next", "Select prev", "Expand/collapse",
|
|
||||||
"Select symbol", "Change symbol (search)", "This help", "Reload portfolio from disk",
|
|
||||||
"Toggle all calls (options)", "Toggle all puts (options)", "Filter +/- 1 NTM", "Filter +/- 2 NTM",
|
|
||||||
"Filter +/- 3 NTM", "Filter +/- 4 NTM", "Filter +/- 5 NTM", "Filter +/- 6 NTM",
|
|
||||||
"Filter +/- 7 NTM", "Filter +/- 8 NTM", "Filter +/- 9 NTM", "Chart: next timeframe",
|
|
||||||
"Chart: prev timeframe", "History: cycle metric", "Sort: next column", "Sort: prev column",
|
|
||||||
"Sort: reverse order", "Account filter (portfolio)",
|
|
||||||
};
|
|
||||||
|
|
||||||
for (actions, 0..) |action, ai| {
|
const global_actions = comptime std.enums.values(keybinds.Action);
|
||||||
var key_strs: [8][]const u8 = undefined;
|
for (global_actions) |action| {
|
||||||
var key_count: usize = 0;
|
const keys = try self.keysForGlobal(arena, action);
|
||||||
for (self.keymap.bindings) |b| {
|
if (keys.len == 0) continue;
|
||||||
if (b.action == action and key_count < key_strs.len) {
|
const keys_str = try std.mem.join(arena, ", ", keys);
|
||||||
var key_buf: [32]u8 = undefined;
|
const label = keybinds.action_labels.get(action);
|
||||||
if (keybinds.formatKeyCombo(b.key, &key_buf)) |s| {
|
try lines.append(arena, .{
|
||||||
key_strs[key_count] = try arena.dupe(u8, s);
|
.text = try std.fmt.allocPrint(arena, " {s:<20} {s}", .{ keys_str, label }),
|
||||||
key_count += 1;
|
.style = th.contentStyle(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ACTIVE TAB section ─────────────────────────────────
|
||||||
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
|
const active_label = tabLabel(self.active_tab);
|
||||||
|
// tabLabel returns " {N}:{Name} " with surrounding spaces;
|
||||||
|
// trim for the header.
|
||||||
|
const trimmed_label = std.mem.trim(u8, active_label, " ");
|
||||||
|
try lines.append(arena, .{
|
||||||
|
.text = try std.fmt.allocPrint(arena, " ACTIVE TAB: {s}", .{trimmed_label}),
|
||||||
|
.style = th.headerStyle(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Comptime walk to dispatch to the active tab's Action enum.
|
||||||
|
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
|
||||||
|
if (std.mem.eql(u8, field.name, @tagName(self.active_tab))) {
|
||||||
|
const Module = @field(tab_modules, field.name);
|
||||||
|
const tab_actions = comptime std.enums.values(Module.Action);
|
||||||
|
if (tab_actions.len == 0) {
|
||||||
|
try lines.append(arena, .{
|
||||||
|
.text = " (no tab-local actions)",
|
||||||
|
.style = th.mutedStyle(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
inline for (tab_actions) |action| {
|
||||||
|
const action_name = @tagName(action);
|
||||||
|
const label = Module.tab.action_labels.get(action);
|
||||||
|
// Skip empty labels (placeholder tabs).
|
||||||
|
if (label.len > 0) {
|
||||||
|
const keys = try self.keysForTabAction(arena, field.name, action_name);
|
||||||
|
const keys_str = if (keys.len == 0)
|
||||||
|
try arena.dupe(u8, "(unbound)")
|
||||||
|
else
|
||||||
|
try std.mem.join(arena, ", ", keys);
|
||||||
|
try lines.append(arena, .{
|
||||||
|
.text = try std.fmt.allocPrint(arena, " {s:<20} {s}", .{ keys_str, label }),
|
||||||
|
.style = th.contentStyle(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (key_count == 0) continue;
|
|
||||||
|
|
||||||
var combined_buf: [128]u8 = undefined;
|
|
||||||
var pos: usize = 0;
|
|
||||||
for (0..key_count) |ki| {
|
|
||||||
if (ki > 0) {
|
|
||||||
if (pos + 2 <= combined_buf.len) {
|
|
||||||
combined_buf[pos] = ',';
|
|
||||||
combined_buf[pos + 1] = ' ';
|
|
||||||
pos += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const ks = key_strs[ki];
|
|
||||||
if (pos + ks.len <= combined_buf.len) {
|
|
||||||
@memcpy(combined_buf[pos..][0..ks.len], ks);
|
|
||||||
pos += ks.len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const label_text = if (ai < action_labels.len) action_labels[ai] else @tagName(action);
|
|
||||||
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " {s:<20} {s}", .{ combined_buf[0..pos], label_text }), .style = th.contentStyle() });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
|
|
|
||||||
|
|
@ -489,12 +489,22 @@ fn clearSelections(state: *State) void {
|
||||||
|
|
||||||
fn setSelectionStatus(state: *const State, app: *App) void {
|
fn setSelectionStatus(state: *const State, app: *App) void {
|
||||||
const n = selectionCount(state);
|
const n = selectionCount(state);
|
||||||
switch (n) {
|
if (n == 0) {
|
||||||
0 => app.setStatus(""),
|
app.setStatus("");
|
||||||
1 => app.setStatus("Selected 1 row — select one more + press 'c' to compare"),
|
return;
|
||||||
2 => app.setStatus("Selected 2 rows — press 'c' to compare"),
|
|
||||||
else => unreachable,
|
|
||||||
}
|
}
|
||||||
|
var arena_state = std.heap.ArenaAllocator.init(app.allocator);
|
||||||
|
defer arena_state.deinit();
|
||||||
|
const arena = arena_state.allocator();
|
||||||
|
const c_keys = app.keysForTabAction(arena, "history", "compare_commit") catch return;
|
||||||
|
const c_key = c_keys[0];
|
||||||
|
var buf: [128]u8 = undefined;
|
||||||
|
const msg = switch (n) {
|
||||||
|
1 => std.fmt.bufPrint(&buf, "Selected 1 row — select one more + press '{s}' to compare", .{c_key}) catch return,
|
||||||
|
2 => std.fmt.bufPrint(&buf, "Selected 2 rows — press '{s}' to compare", .{c_key}) catch return,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
app.setStatus(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Compare commit ───────────────────────────────────────────
|
// ── Compare commit ───────────────────────────────────────────
|
||||||
|
|
@ -504,11 +514,19 @@ fn setSelectionStatus(state: *const State, app: *App) void {
|
||||||
fn commitCompare(state: *State, app: *App) void {
|
fn commitCompare(state: *State, app: *App) void {
|
||||||
const sel_count = selectionCount(state);
|
const sel_count = selectionCount(state);
|
||||||
if (sel_count < 2) {
|
if (sel_count < 2) {
|
||||||
if (sel_count == 0) {
|
var arena_state = std.heap.ArenaAllocator.init(app.allocator);
|
||||||
app.setStatus("Select two rows with 's' (or space), then press 'c' to compare");
|
defer arena_state.deinit();
|
||||||
} else {
|
const arena = arena_state.allocator();
|
||||||
app.setStatus("Select one more row with 's' (or space), then press 'c' to compare");
|
const s_keys = app.keysForTabAction(arena, "history", "compare_select") catch return;
|
||||||
}
|
const c_keys = app.keysForTabAction(arena, "history", "compare_commit") catch return;
|
||||||
|
const s_key = s_keys[0];
|
||||||
|
const c_key = c_keys[0];
|
||||||
|
var buf: [192]u8 = undefined;
|
||||||
|
const msg = if (sel_count == 0)
|
||||||
|
std.fmt.bufPrint(&buf, "Select two rows with '{s}' (or space), then press '{s}' to compare", .{ s_key, c_key }) catch return
|
||||||
|
else
|
||||||
|
std.fmt.bufPrint(&buf, "Select one more row with '{s}' (or space), then press '{s}' to compare", .{ s_key, c_key }) catch return;
|
||||||
|
app.setStatus(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -663,7 +681,11 @@ fn buildCompareFromSelections(state: *State, app: *App, sel_a: usize, sel_b: usi
|
||||||
clearCompareView(state, app);
|
clearCompareView(state, app);
|
||||||
state.compare_view = cv_with_labels;
|
state.compare_view = cv_with_labels;
|
||||||
state.compare_resources = resources;
|
state.compare_resources = resources;
|
||||||
app.setStatus("Comparing — Esc or 'c' to return to timeline");
|
const cancel_keys = try app.keysForTabAction(arena, "history", "compare_cancel");
|
||||||
|
const commit_keys = try app.keysForTabAction(arena, "history", "compare_commit");
|
||||||
|
var status_buf: [128]u8 = undefined;
|
||||||
|
const msg = std.fmt.bufPrint(&status_buf, "Comparing — {s} or '{s}' to return to timeline", .{ cancel_keys[0], commit_keys[0] }) catch "Comparing — return to timeline";
|
||||||
|
app.setStatus(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn liveLiquid(app: *const App) f64 {
|
fn liveLiquid(app: *const App) f64 {
|
||||||
|
|
@ -1035,10 +1057,25 @@ pub fn buildStyledLines(state: *State, app: *App, arena: std.mem.Allocator) ![]c
|
||||||
if (state.compare_view) |cv| {
|
if (state.compare_view) |cv| {
|
||||||
// Compare view doesn't populate table metadata; reset so the
|
// Compare view doesn't populate table metadata; reset so the
|
||||||
// cursor state remains sensible if the user exits back.
|
// cursor state remains sensible if the user exits back.
|
||||||
return renderCompareLines(arena, app.theme, cv);
|
const cancel_keys = try app.keysForTabAction(arena, "history", "compare_cancel");
|
||||||
|
const commit_keys = try app.keysForTabAction(arena, "history", "compare_commit");
|
||||||
|
const footer = try std.fmt.allocPrint(arena, " {s} or '{s}' to return to timeline", .{ cancel_keys[0], commit_keys[0] });
|
||||||
|
return renderCompareLines(arena, app.theme, cv, footer);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = try collectTableRows(arena, state, app);
|
const rows = try collectTableRows(arena, state, app);
|
||||||
|
// Resolve key bindings for the table-header hint so the displayed
|
||||||
|
// keys reflect the user's actual keymap (defaults or overridden).
|
||||||
|
const enter_keys = try app.keysForTabAction(arena, "history", "expand_collapse");
|
||||||
|
const select_keys = try app.keysForTabAction(arena, "history", "compare_select");
|
||||||
|
const commit_keys = try app.keysForTabAction(arena, "history", "compare_commit");
|
||||||
|
const j_keys = try app.keysForGlobal(arena, .select_next);
|
||||||
|
const k_keys = try app.keysForGlobal(arena, .select_prev);
|
||||||
|
const table_hint = try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"({s}/{s}: move, {s}: expand/collapse, {s}/space: select, {s}: compare)",
|
||||||
|
.{ j_keys[0], k_keys[0], enter_keys[0], select_keys[0], commit_keys[0] },
|
||||||
|
);
|
||||||
const result = try renderHistoryLinesFull(
|
const result = try renderHistoryLinesFull(
|
||||||
arena,
|
arena,
|
||||||
app.theme,
|
app.theme,
|
||||||
|
|
@ -1048,6 +1085,7 @@ pub fn buildStyledLines(state: *State, app: *App, arena: std.mem.Allocator) ![]c
|
||||||
rows,
|
rows,
|
||||||
state.cursor,
|
state.cursor,
|
||||||
state.selections,
|
state.selections,
|
||||||
|
table_hint,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Stash table metadata on State for the event handler's cursor-
|
// Stash table metadata on State for the event handler's cursor-
|
||||||
|
|
@ -1086,6 +1124,7 @@ pub fn renderHistoryLines(
|
||||||
rows,
|
rows,
|
||||||
0,
|
0,
|
||||||
.{ null, null },
|
.{ null, null },
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
return result.lines;
|
return result.lines;
|
||||||
}
|
}
|
||||||
|
|
@ -1144,6 +1183,11 @@ pub fn renderHistoryLinesFull(
|
||||||
rows: []const TableRow,
|
rows: []const TableRow,
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
selections: [2]?usize,
|
selections: [2]?usize,
|
||||||
|
/// Optional override for the parenthetical key hints in the
|
||||||
|
/// "Recent snapshots" header. When null, falls back to the
|
||||||
|
/// default-keymap literal. Pass an arena-allocated string from
|
||||||
|
/// `buildStyledLines` for live-resolved keys.
|
||||||
|
table_header_hint: ?[]const u8,
|
||||||
) !HistoryRender {
|
) !HistoryRender {
|
||||||
var lines: std.ArrayList(StyledLine) = .empty;
|
var lines: std.ArrayList(StyledLine) = .empty;
|
||||||
|
|
||||||
|
|
@ -1211,10 +1255,11 @@ pub fn renderHistoryLinesFull(
|
||||||
|
|
||||||
var rlabel_buf: [32]u8 = undefined;
|
var rlabel_buf: [32]u8 = undefined;
|
||||||
const rlabel = view.fmtResolutionLabel(&rlabel_buf, resolution_override, resolution);
|
const rlabel = view.fmtResolutionLabel(&rlabel_buf, resolution_override, resolution);
|
||||||
|
const hint_text = table_header_hint orelse "(j/k: move, enter: expand/collapse, s/space: select, c: compare)";
|
||||||
const table_header = try std.fmt.allocPrint(
|
const table_header = try std.fmt.allocPrint(
|
||||||
arena,
|
arena,
|
||||||
" Recent snapshots {s} (j/k: move, enter: expand/collapse, s/space: select, c: compare)",
|
" Recent snapshots {s} {s}",
|
||||||
.{rlabel},
|
.{ rlabel, hint_text },
|
||||||
);
|
);
|
||||||
try lines.append(arena, .{ .text = table_header, .style = th.headerStyle() });
|
try lines.append(arena, .{ .text = table_header, .style = th.headerStyle() });
|
||||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
|
|
@ -1444,6 +1489,7 @@ pub fn renderCompareLines(
|
||||||
arena: std.mem.Allocator,
|
arena: std.mem.Allocator,
|
||||||
th: theme.Theme,
|
th: theme.Theme,
|
||||||
cv: compare_view.CompareView,
|
cv: compare_view.CompareView,
|
||||||
|
footer_hint: []const u8,
|
||||||
) ![]const StyledLine {
|
) ![]const StyledLine {
|
||||||
var lines: std.ArrayList(StyledLine) = .empty;
|
var lines: std.ArrayList(StyledLine) = .empty;
|
||||||
|
|
||||||
|
|
@ -1532,7 +1578,7 @@ pub fn renderCompareLines(
|
||||||
// ── Footer hint ──
|
// ── Footer hint ──
|
||||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
try lines.append(arena, .{
|
try lines.append(arena, .{
|
||||||
.text = " Esc or 'c' to return to timeline",
|
.text = footer_hint,
|
||||||
.style = th.mutedStyle(),
|
.style = th.mutedStyle(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1782,6 +1828,7 @@ test "renderHistoryLinesFull: cursor highlights selected row" {
|
||||||
rows,
|
rows,
|
||||||
1,
|
1,
|
||||||
.{ 0, 2 },
|
.{ 0, 2 },
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
try testing.expectEqual(@as(usize, 3), result.table_row_count);
|
try testing.expectEqual(@as(usize, 3), result.table_row_count);
|
||||||
|
|
@ -1832,7 +1879,7 @@ test "renderCompareLines: emits header, totals, symbols, hidden-count, footer" {
|
||||||
.removed_count = 0,
|
.removed_count = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const lines = try renderCompareLines(a, th, cv);
|
const lines = try renderCompareLines(a, th, cv, " Esc or 'c' to return to timeline");
|
||||||
|
|
||||||
var saw_header = false;
|
var saw_header = false;
|
||||||
var saw_totals = false;
|
var saw_totals = false;
|
||||||
|
|
@ -1870,7 +1917,7 @@ test "renderCompareLines: live-now shows 'today' not a date" {
|
||||||
.added_count = 0,
|
.added_count = 0,
|
||||||
.removed_count = 0,
|
.removed_count = 0,
|
||||||
};
|
};
|
||||||
const lines = try renderCompareLines(a, th, cv);
|
const lines = try renderCompareLines(a, th, cv, " Esc or 'c' to return to timeline");
|
||||||
|
|
||||||
var saw_today = false;
|
var saw_today = false;
|
||||||
var saw_no_symbols = false;
|
var saw_no_symbols = false;
|
||||||
|
|
@ -1899,7 +1946,7 @@ test "renderCompareLines: no hidden line when no add/remove" {
|
||||||
.added_count = 0,
|
.added_count = 0,
|
||||||
.removed_count = 0,
|
.removed_count = 0,
|
||||||
};
|
};
|
||||||
const lines = try renderCompareLines(a, th, cv);
|
const lines = try renderCompareLines(a, th, cv, " Esc or 'c' to return to timeline");
|
||||||
|
|
||||||
for (lines) |l| {
|
for (lines) |l| {
|
||||||
try testing.expect(std.mem.indexOf(u8, l.text, "hidden") == null);
|
try testing.expect(std.mem.indexOf(u8, l.text, "hidden") == null);
|
||||||
|
|
@ -1930,7 +1977,7 @@ test "renderCompareLines: bucket labels override ISO dates in header" {
|
||||||
.then_label = "Q1 2025 (ended 2025-03-28)",
|
.then_label = "Q1 2025 (ended 2025-03-28)",
|
||||||
.now_label = null,
|
.now_label = null,
|
||||||
};
|
};
|
||||||
const lines = try renderCompareLines(a, th, cv);
|
const lines = try renderCompareLines(a, th, cv, " Esc or 'c' to return to timeline");
|
||||||
|
|
||||||
var found_header = false;
|
var found_header = false;
|
||||||
for (lines) |l| {
|
for (lines) |l| {
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,36 @@ pub fn defaults() KeyMap {
|
||||||
return .{ .bindings = &global_default_bindings };
|
return .{ .bindings = &global_default_bindings };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display labels for each global Action variant. Used by the help
|
||||||
|
/// overlay and status-line hint to render human-readable names
|
||||||
|
/// alongside the resolved key bindings. Parallel in shape to each
|
||||||
|
/// tab module's own `tab.action_labels`.
|
||||||
|
pub const action_labels = std.enums.EnumArray(Action, []const u8).init(.{
|
||||||
|
.quit = "Quit",
|
||||||
|
.refresh = "Refresh",
|
||||||
|
.prev_tab = "Previous tab",
|
||||||
|
.next_tab = "Next tab",
|
||||||
|
.tab_1 = "Tab 1",
|
||||||
|
.tab_2 = "Tab 2",
|
||||||
|
.tab_3 = "Tab 3",
|
||||||
|
.tab_4 = "Tab 4",
|
||||||
|
.tab_5 = "Tab 5",
|
||||||
|
.tab_6 = "Tab 6",
|
||||||
|
.tab_7 = "Tab 7",
|
||||||
|
.tab_8 = "Tab 8",
|
||||||
|
.scroll_down = "Half page down",
|
||||||
|
.scroll_up = "Half page up",
|
||||||
|
.scroll_top = "Scroll to top",
|
||||||
|
.scroll_bottom = "Scroll to bottom",
|
||||||
|
.page_down = "Page down",
|
||||||
|
.page_up = "Page up",
|
||||||
|
.select_next = "Select next",
|
||||||
|
.select_prev = "Select previous",
|
||||||
|
.symbol_input = "Symbol input",
|
||||||
|
.help = "Help",
|
||||||
|
.reload_portfolio = "Reload portfolio",
|
||||||
|
});
|
||||||
|
|
||||||
// ── SRF serialization ────────────────────────────────────────
|
// ── SRF serialization ────────────────────────────────────────
|
||||||
|
|
||||||
const special_key_names = [_]struct { name: []const u8, cp: u21 }{
|
const special_key_names = [_]struct { name: []const u8, cp: u21 }{
|
||||||
|
|
@ -653,3 +683,11 @@ test "defaults returns valid keymap" {
|
||||||
}
|
}
|
||||||
try std.testing.expect(found_quit);
|
try std.testing.expect(found_quit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "action_labels: every Action variant has a non-empty label" {
|
||||||
|
inline for (std.meta.fields(Action)) |f| {
|
||||||
|
const action: Action = @enumFromInt(f.value);
|
||||||
|
const label = action_labels.get(action);
|
||||||
|
try std.testing.expect(label.len > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -465,7 +465,11 @@ pub fn buildStyledLines(app: *App, arena: std.mem.Allocator) ![]const StyledLine
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chains[0].underlying_price) |price| {
|
if (chains[0].underlying_price) |price| {
|
||||||
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Underlying: {f} {d} expiration(s) +/- {d} strikes NTM (Ctrl+1-9 to change)", .{ Money.from(price), chains.len, state.near_the_money }), .style = th.contentStyle() });
|
// Resolve filter_1 and filter_9 keys for the hint, so a
|
||||||
|
// user-rebound keymap shows the actual range.
|
||||||
|
const f1_keys = try app.keysForTabAction(arena, "options", "filter_1");
|
||||||
|
const f9_keys = try app.keysForTabAction(arena, "options", "filter_9");
|
||||||
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Underlying: {f} {d} expiration(s) +/- {d} strikes NTM ({s}..{s} to change)", .{ Money.from(price), chains.len, state.near_the_money, f1_keys[0], f9_keys[0] }), .style = th.contentStyle() });
|
||||||
}
|
}
|
||||||
|
|
||||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
|
|
|
||||||
|
|
@ -601,7 +601,10 @@ pub fn loadPortfolioData(state: *State, app: *App) void {
|
||||||
const info_msg = std.fmt.bufPrint(&info_buf, "Loaded {d} symbols ({d} fetched) | r/F5 to refresh", .{ syms.len, fetch_count }) catch "Loaded | r/F5 to refresh";
|
const info_msg = std.fmt.bufPrint(&info_buf, "Loaded {d} symbols ({d} fetched) | r/F5 to refresh", .{ syms.len, fetch_count }) catch "Loaded | r/F5 to refresh";
|
||||||
app.setStatus(info_msg);
|
app.setStatus(info_msg);
|
||||||
} else {
|
} else {
|
||||||
app.setStatus("j/k navigate | Enter expand | s select symbol | / search | ? help");
|
// Empty status — App's getStatus() will fall back to the
|
||||||
|
// dynamic default hint composed from the active tab's
|
||||||
|
// status_hints + global keys.
|
||||||
|
app.setStatus("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,13 @@ pub const tab = struct {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.overlay_actuals => {
|
.overlay_actuals => {
|
||||||
if (state.as_of == null) {
|
if (state.as_of == null) {
|
||||||
app.setStatus("Overlay only available with --as-of (press d to set)");
|
var arena_state = std.heap.ArenaAllocator.init(app.allocator);
|
||||||
|
defer arena_state.deinit();
|
||||||
|
const arena = arena_state.allocator();
|
||||||
|
const d_keys = app.keysForTabAction(arena, "projections", "as_of_input") catch return;
|
||||||
|
var buf: [128]u8 = undefined;
|
||||||
|
const msg = std.fmt.bufPrint(&buf, "Overlay only available with --as-of (press {s} to set)", .{d_keys[0]}) catch return;
|
||||||
|
app.setStatus(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.overlay_actuals = !state.overlay_actuals;
|
state.overlay_actuals = !state.overlay_actuals;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue