begin help cleanup
This commit is contained in:
parent
15a3304e19
commit
db70b1f924
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 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
|
||||
/// input, date input, etc.). Returned by `handleInputBuffer` so
|
||||
/// the per-mode caller only needs to wire up the `committed`
|
||||
|
|
@ -1527,9 +1607,66 @@ pub const App = struct {
|
|||
self.status_len = len;
|
||||
}
|
||||
|
||||
fn getStatus(self: *App) []const u8 {
|
||||
if (self.status_len == 0) return "h/l tabs | j/k select | Enter expand | s select | / symbol | ? help";
|
||||
return self.status_msg[0..self.status_len];
|
||||
/// Returns the current status message. When no message has been
|
||||
/// set, builds a dynamic default hint composed from a small set
|
||||
/// 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 {
|
||||
|
|
@ -1775,13 +1912,13 @@ pub const App = struct {
|
|||
// Show account filter indicator when active, appended to status message
|
||||
if (self.states.portfolio.account_filter != null and self.active_tab == .portfolio) {
|
||||
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;
|
||||
for (0..@min(filter_text.len, width)) |i| {
|
||||
buf[i] = .{ .char = .{ .grapheme = glyph(filter_text[i]) }, .style = status_style };
|
||||
}
|
||||
} else {
|
||||
const msg = self.getStatus();
|
||||
const msg = self.getStatus(ctx.arena);
|
||||
for (0..@min(msg.len, width)) |i| {
|
||||
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 = "", .style = th.contentStyle() });
|
||||
|
||||
const actions = comptime std.enums.values(keybinds.Action);
|
||||
const action_labels = [_][]const u8{
|
||||
"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)",
|
||||
};
|
||||
// ── GLOBAL section ─────────────────────────────────────
|
||||
try lines.append(arena, .{ .text = " GLOBAL", .style = th.headerStyle() });
|
||||
|
||||
for (actions, 0..) |action, ai| {
|
||||
var key_strs: [8][]const u8 = undefined;
|
||||
var key_count: usize = 0;
|
||||
for (self.keymap.bindings) |b| {
|
||||
if (b.action == action and key_count < key_strs.len) {
|
||||
var key_buf: [32]u8 = undefined;
|
||||
if (keybinds.formatKeyCombo(b.key, &key_buf)) |s| {
|
||||
key_strs[key_count] = try arena.dupe(u8, s);
|
||||
key_count += 1;
|
||||
const global_actions = comptime std.enums.values(keybinds.Action);
|
||||
for (global_actions) |action| {
|
||||
const keys = try self.keysForGlobal(arena, action);
|
||||
if (keys.len == 0) continue;
|
||||
const keys_str = try std.mem.join(arena, ", ", keys);
|
||||
const label = keybinds.action_labels.get(action);
|
||||
try lines.append(arena, .{
|
||||
.text = try std.fmt.allocPrint(arena, " {s:<20} {s}", .{ keys_str, label }),
|
||||
.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() });
|
||||
|
|
|
|||
|
|
@ -489,12 +489,22 @@ fn clearSelections(state: *State) void {
|
|||
|
||||
fn setSelectionStatus(state: *const State, app: *App) void {
|
||||
const n = selectionCount(state);
|
||||
switch (n) {
|
||||
0 => app.setStatus(""),
|
||||
1 => app.setStatus("Selected 1 row — select one more + press 'c' to compare"),
|
||||
2 => app.setStatus("Selected 2 rows — press 'c' to compare"),
|
||||
else => unreachable,
|
||||
if (n == 0) {
|
||||
app.setStatus("");
|
||||
return;
|
||||
}
|
||||
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 ───────────────────────────────────────────
|
||||
|
|
@ -504,11 +514,19 @@ fn setSelectionStatus(state: *const State, app: *App) void {
|
|||
fn commitCompare(state: *State, app: *App) void {
|
||||
const sel_count = selectionCount(state);
|
||||
if (sel_count < 2) {
|
||||
if (sel_count == 0) {
|
||||
app.setStatus("Select two rows with 's' (or space), then press 'c' to compare");
|
||||
} else {
|
||||
app.setStatus("Select one more row with 's' (or space), then press 'c' to compare");
|
||||
}
|
||||
var arena_state = std.heap.ArenaAllocator.init(app.allocator);
|
||||
defer arena_state.deinit();
|
||||
const arena = arena_state.allocator();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -663,7 +681,11 @@ fn buildCompareFromSelections(state: *State, app: *App, sel_a: usize, sel_b: usi
|
|||
clearCompareView(state, app);
|
||||
state.compare_view = cv_with_labels;
|
||||
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 {
|
||||
|
|
@ -1035,10 +1057,25 @@ pub fn buildStyledLines(state: *State, app: *App, arena: std.mem.Allocator) ![]c
|
|||
if (state.compare_view) |cv| {
|
||||
// Compare view doesn't populate table metadata; reset so the
|
||||
// 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);
|
||||
// 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(
|
||||
arena,
|
||||
app.theme,
|
||||
|
|
@ -1048,6 +1085,7 @@ pub fn buildStyledLines(state: *State, app: *App, arena: std.mem.Allocator) ![]c
|
|||
rows,
|
||||
state.cursor,
|
||||
state.selections,
|
||||
table_hint,
|
||||
);
|
||||
|
||||
// Stash table metadata on State for the event handler's cursor-
|
||||
|
|
@ -1086,6 +1124,7 @@ pub fn renderHistoryLines(
|
|||
rows,
|
||||
0,
|
||||
.{ null, null },
|
||||
null,
|
||||
);
|
||||
return result.lines;
|
||||
}
|
||||
|
|
@ -1144,6 +1183,11 @@ pub fn renderHistoryLinesFull(
|
|||
rows: []const TableRow,
|
||||
cursor: 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 {
|
||||
var lines: std.ArrayList(StyledLine) = .empty;
|
||||
|
||||
|
|
@ -1211,10 +1255,11 @@ pub fn renderHistoryLinesFull(
|
|||
|
||||
var rlabel_buf: [32]u8 = undefined;
|
||||
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(
|
||||
arena,
|
||||
" Recent snapshots {s} (j/k: move, enter: expand/collapse, s/space: select, c: compare)",
|
||||
.{rlabel},
|
||||
" Recent snapshots {s} {s}",
|
||||
.{ rlabel, hint_text },
|
||||
);
|
||||
try lines.append(arena, .{ .text = table_header, .style = th.headerStyle() });
|
||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||
|
|
@ -1444,6 +1489,7 @@ pub fn renderCompareLines(
|
|||
arena: std.mem.Allocator,
|
||||
th: theme.Theme,
|
||||
cv: compare_view.CompareView,
|
||||
footer_hint: []const u8,
|
||||
) ![]const StyledLine {
|
||||
var lines: std.ArrayList(StyledLine) = .empty;
|
||||
|
||||
|
|
@ -1532,7 +1578,7 @@ pub fn renderCompareLines(
|
|||
// ── Footer hint ──
|
||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||
try lines.append(arena, .{
|
||||
.text = " Esc or 'c' to return to timeline",
|
||||
.text = footer_hint,
|
||||
.style = th.mutedStyle(),
|
||||
});
|
||||
|
||||
|
|
@ -1782,6 +1828,7 @@ test "renderHistoryLinesFull: cursor highlights selected row" {
|
|||
rows,
|
||||
1,
|
||||
.{ 0, 2 },
|
||||
null,
|
||||
);
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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_totals = false;
|
||||
|
|
@ -1870,7 +1917,7 @@ test "renderCompareLines: live-now shows 'today' not a date" {
|
|||
.added_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_no_symbols = false;
|
||||
|
|
@ -1899,7 +1946,7 @@ test "renderCompareLines: no hidden line when no add/remove" {
|
|||
.added_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| {
|
||||
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)",
|
||||
.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;
|
||||
for (lines) |l| {
|
||||
|
|
|
|||
|
|
@ -148,6 +148,36 @@ pub fn defaults() KeyMap {
|
|||
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 ────────────────────────────────────────
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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| {
|
||||
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() });
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
app.setStatus(info_msg);
|
||||
} 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) {
|
||||
.overlay_actuals => {
|
||||
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;
|
||||
}
|
||||
state.overlay_actuals = !state.overlay_actuals;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue