portfolio tab help cleanup
This commit is contained in:
parent
db70b1f924
commit
ca6683feef
2 changed files with 200 additions and 29 deletions
|
|
@ -1981,7 +1981,7 @@ pub const App = struct {
|
|||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||
|
||||
// ── GLOBAL section ─────────────────────────────────────
|
||||
try lines.append(arena, .{ .text = " GLOBAL", .style = th.headerStyle() });
|
||||
try lines.append(arena, .{ .text = " Global", .style = th.headerStyle() });
|
||||
|
||||
const global_actions = comptime std.enums.values(keybinds.Action);
|
||||
for (global_actions) |action| {
|
||||
|
|
@ -2001,8 +2001,9 @@ pub const App = struct {
|
|||
// tabLabel returns " {N}:{Name} " with surrounding spaces;
|
||||
// trim for the header.
|
||||
const trimmed_label = std.mem.trim(u8, active_label, " ");
|
||||
const without_number = trimmed_label[std.mem.findScalar(u8, trimmed_label, ':').? + 1 ..];
|
||||
try lines.append(arena, .{
|
||||
.text = try std.fmt.allocPrint(arena, " ACTIVE TAB: {s}", .{trimmed_label}),
|
||||
.text = try std.fmt.allocPrint(arena, " Active Tab: {s}", .{without_number}),
|
||||
.style = th.headerStyle(),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ pub const tab = struct {
|
|||
});
|
||||
|
||||
pub const status_hints: []const Action = &.{
|
||||
.sort_col_prev,
|
||||
.sort_col_next,
|
||||
.sort_reverse,
|
||||
.open_account_picker,
|
||||
|
|
@ -1507,34 +1508,103 @@ pub fn drawContent(state: *State, app: *App, arena: std.mem.Allocator, buf: []va
|
|||
}
|
||||
|
||||
fn drawWelcomeScreen(app: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, width: u16, height: u16) !void {
|
||||
const th = app.theme;
|
||||
const welcome_lines = [_]StyledLine{
|
||||
.{ .text = "", .style = th.contentStyle() },
|
||||
.{ .text = " zfin", .style = th.headerStyle() },
|
||||
.{ .text = "", .style = th.contentStyle() },
|
||||
.{ .text = " No portfolio loaded.", .style = th.mutedStyle() },
|
||||
.{ .text = "", .style = th.contentStyle() },
|
||||
.{ .text = " Getting started:", .style = th.contentStyle() },
|
||||
.{ .text = " / Enter a stock symbol (e.g. AAPL, VTI)", .style = th.contentStyle() },
|
||||
.{ .text = "", .style = th.contentStyle() },
|
||||
.{ .text = " Portfolio mode:", .style = th.contentStyle() },
|
||||
.{ .text = " zfin -p portfolio.srf Load a portfolio file", .style = th.mutedStyle() },
|
||||
.{ .text = " portfolio.srf Auto-loaded from cwd if present", .style = th.mutedStyle() },
|
||||
.{ .text = "", .style = th.contentStyle() },
|
||||
.{ .text = " Navigation:", .style = th.contentStyle() },
|
||||
.{ .text = " h / l Previous / next tab", .style = th.mutedStyle() },
|
||||
.{ .text = " j / k Select next / prev item", .style = th.mutedStyle() },
|
||||
.{ .text = " Enter Expand position lots", .style = th.mutedStyle() },
|
||||
.{ .text = " s Select symbol for other tabs", .style = th.mutedStyle() },
|
||||
.{ .text = " 1-5 Jump to tab", .style = th.mutedStyle() },
|
||||
.{ .text = " ? Full help", .style = th.mutedStyle() },
|
||||
.{ .text = " q Quit", .style = th.mutedStyle() },
|
||||
.{ .text = "", .style = th.contentStyle() },
|
||||
.{ .text = " Sample portfolio.srf:", .style = th.contentStyle() },
|
||||
.{ .text = " symbol::VTI,shares::100,open_date::2024-01-15,open_price::220.50", .style = th.mutedStyle() },
|
||||
.{ .text = " symbol::AAPL,shares::50,open_date::2024-03-01,open_price::170.00", .style = th.mutedStyle() },
|
||||
// Resolve key bindings dynamically so the welcome screen reflects
|
||||
// the user's actual keymap (defaults or overridden via keys.srf).
|
||||
// Each `keysForGlobal` returns at least one key — global default
|
||||
// bindings always exist (verified by the comptime conflict
|
||||
// validator + tests).
|
||||
const keys: WelcomeKeys = .{
|
||||
.symbol_input = (try app.keysForGlobal(arena, .symbol_input))[0],
|
||||
.select_next = (try app.keysForGlobal(arena, .select_next))[0],
|
||||
.select_prev = (try app.keysForGlobal(arena, .select_prev))[0],
|
||||
.prev_tab = (try app.keysForGlobal(arena, .prev_tab))[0],
|
||||
.next_tab = (try app.keysForGlobal(arena, .next_tab))[0],
|
||||
.help = (try app.keysForGlobal(arena, .help))[0],
|
||||
.quit = (try app.keysForGlobal(arena, .quit))[0],
|
||||
.tab_1 = (try app.keysForGlobal(arena, .tab_1))[0],
|
||||
.tab_5 = (try app.keysForGlobal(arena, .tab_5))[0],
|
||||
.expand_collapse = (try app.keysForTabAction(arena, "portfolio", "expand_collapse"))[0],
|
||||
.select_symbol = (try app.keysForTabAction(arena, "portfolio", "select_symbol"))[0],
|
||||
};
|
||||
try app.drawStyledContent(arena, buf, width, height, &welcome_lines);
|
||||
const lines = try buildWelcomeScreenLines(arena, app.theme, keys);
|
||||
try app.drawStyledContent(arena, buf, width, height, lines);
|
||||
}
|
||||
|
||||
/// Pre-resolved key bindings used by `buildWelcomeScreenLines`. All
|
||||
/// fields are formatted key strings (e.g. `"j"`, `"ctrl+f"`) sourced
|
||||
/// from the live keymap; the renderer doesn't know about the keymap.
|
||||
pub const WelcomeKeys = struct {
|
||||
symbol_input: []const u8,
|
||||
select_next: []const u8,
|
||||
select_prev: []const u8,
|
||||
prev_tab: []const u8,
|
||||
next_tab: []const u8,
|
||||
help: []const u8,
|
||||
quit: []const u8,
|
||||
tab_1: []const u8,
|
||||
tab_5: []const u8,
|
||||
expand_collapse: []const u8,
|
||||
select_symbol: []const u8,
|
||||
};
|
||||
|
||||
/// Build the styled lines for the welcome screen shown when no
|
||||
/// portfolio is loaded. Pure function over (arena, theme, keys);
|
||||
/// no App access. Easy to unit-test by passing fixture keys.
|
||||
pub fn buildWelcomeScreenLines(
|
||||
arena: std.mem.Allocator,
|
||||
th: theme.Theme,
|
||||
keys: WelcomeKeys,
|
||||
) ![]const StyledLine {
|
||||
var lines: std.ArrayListUnmanaged(StyledLine) = .empty;
|
||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||
try lines.append(arena, .{ .text = " zfin", .style = th.headerStyle() });
|
||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||
try lines.append(arena, .{ .text = " No portfolio loaded.", .style = th.mutedStyle() });
|
||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||
try lines.append(arena, .{ .text = " Getting started:", .style = th.contentStyle() });
|
||||
try lines.append(arena, .{
|
||||
.text = try std.fmt.allocPrint(arena, " {s:<10} Enter a stock symbol (e.g. AAPL, VTI)", .{keys.symbol_input}),
|
||||
.style = th.contentStyle(),
|
||||
});
|
||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||
try lines.append(arena, .{ .text = " Portfolio mode:", .style = th.contentStyle() });
|
||||
try lines.append(arena, .{ .text = " zfin -p portfolio.srf Load a portfolio file", .style = th.mutedStyle() });
|
||||
try lines.append(arena, .{ .text = " portfolio.srf Auto-loaded from cwd if present", .style = th.mutedStyle() });
|
||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||
try lines.append(arena, .{ .text = " Navigation:", .style = th.contentStyle() });
|
||||
try lines.append(arena, .{
|
||||
.text = try std.fmt.allocPrint(arena, " {s} / {s} Previous / next tab", .{ keys.prev_tab, keys.next_tab }),
|
||||
.style = th.mutedStyle(),
|
||||
});
|
||||
try lines.append(arena, .{
|
||||
.text = try std.fmt.allocPrint(arena, " {s} / {s} Select next / prev item", .{ keys.select_next, keys.select_prev }),
|
||||
.style = th.mutedStyle(),
|
||||
});
|
||||
try lines.append(arena, .{
|
||||
.text = try std.fmt.allocPrint(arena, " {s:<10} Expand position lots", .{keys.expand_collapse}),
|
||||
.style = th.mutedStyle(),
|
||||
});
|
||||
try lines.append(arena, .{
|
||||
.text = try std.fmt.allocPrint(arena, " {s:<10} Select symbol for other tabs", .{keys.select_symbol}),
|
||||
.style = th.mutedStyle(),
|
||||
});
|
||||
try lines.append(arena, .{
|
||||
.text = try std.fmt.allocPrint(arena, " {s}-{s} Jump to tab", .{ keys.tab_1, keys.tab_5 }),
|
||||
.style = th.mutedStyle(),
|
||||
});
|
||||
try lines.append(arena, .{
|
||||
.text = try std.fmt.allocPrint(arena, " {s:<10} Full help", .{keys.help}),
|
||||
.style = th.mutedStyle(),
|
||||
});
|
||||
try lines.append(arena, .{
|
||||
.text = try std.fmt.allocPrint(arena, " {s:<10} Quit", .{keys.quit}),
|
||||
.style = th.mutedStyle(),
|
||||
});
|
||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||
try lines.append(arena, .{ .text = " Sample portfolio.srf:", .style = th.contentStyle() });
|
||||
try lines.append(arena, .{ .text = " symbol::VTI,shares::100,open_date::2024-01-15,open_price::220.50", .style = th.mutedStyle() });
|
||||
try lines.append(arena, .{ .text = " symbol::AAPL,shares::50,open_date::2024-03-01,open_price::170.00", .style = th.mutedStyle() });
|
||||
return lines.toOwnedSlice(arena);
|
||||
}
|
||||
|
||||
/// Reload portfolio file from disk without re-fetching prices.
|
||||
|
|
@ -1779,3 +1849,103 @@ pub fn drawAccountPicker(state: *State, app: *App, arena: std.mem.Allocator, buf
|
|||
|
||||
try app.drawStyledContent(arena, buf, width, height, lines.items[start..]);
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test "buildWelcomeScreenLines: includes resolved keys in expected slots" {
|
||||
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena_state.deinit();
|
||||
const arena = arena_state.allocator();
|
||||
|
||||
const keys: WelcomeKeys = .{
|
||||
.symbol_input = "/",
|
||||
.select_next = "j",
|
||||
.select_prev = "k",
|
||||
.prev_tab = "h",
|
||||
.next_tab = "l",
|
||||
.help = "?",
|
||||
.quit = "q",
|
||||
.tab_1 = "1",
|
||||
.tab_5 = "5",
|
||||
.expand_collapse = "enter",
|
||||
.select_symbol = "s",
|
||||
};
|
||||
|
||||
const lines = try buildWelcomeScreenLines(arena, theme.default_theme, keys);
|
||||
|
||||
// Concatenate all line text for substring assertions.
|
||||
var all: std.ArrayListUnmanaged(u8) = .empty;
|
||||
for (lines) |l| {
|
||||
try all.appendSlice(arena, l.text);
|
||||
try all.append(arena, '\n');
|
||||
}
|
||||
const text = all.items;
|
||||
|
||||
// Header + body sections present.
|
||||
try testing.expect(std.mem.indexOf(u8, text, "zfin") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "No portfolio loaded.") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "Getting started:") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "Portfolio mode:") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "Navigation:") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "Sample portfolio.srf:") != null);
|
||||
|
||||
// All resolved keys appear in their respective rows. Format
|
||||
// strings use `{s:<10}` (ten-char field) and ` ` (two-space)
|
||||
// gap before the description, so the gap between key and
|
||||
// description = (10 - keylen) padding + 2 separator.
|
||||
try testing.expect(std.mem.indexOf(u8, text, "/ Enter a stock symbol") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "h / l Previous / next tab") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "j / k Select next / prev item") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "enter Expand position lots") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "s Select symbol for other tabs") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "1-5 Jump to tab") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "? Full help") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "q Quit") != null);
|
||||
}
|
||||
|
||||
test "buildWelcomeScreenLines: respects rebound keys" {
|
||||
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena_state.deinit();
|
||||
const arena = arena_state.allocator();
|
||||
|
||||
// User-rebound keys: arbitrary substitutions.
|
||||
const keys: WelcomeKeys = .{
|
||||
.symbol_input = "ctrl+s",
|
||||
.select_next = "down",
|
||||
.select_prev = "up",
|
||||
.prev_tab = "shift+tab",
|
||||
.next_tab = "tab",
|
||||
.help = "F1",
|
||||
.quit = "ctrl+q",
|
||||
.tab_1 = "f1",
|
||||
.tab_5 = "f5",
|
||||
.expand_collapse = "space",
|
||||
.select_symbol = "x",
|
||||
};
|
||||
|
||||
const lines = try buildWelcomeScreenLines(arena, theme.default_theme, keys);
|
||||
|
||||
var all: std.ArrayListUnmanaged(u8) = .empty;
|
||||
for (lines) |l| {
|
||||
try all.appendSlice(arena, l.text);
|
||||
try all.append(arena, '\n');
|
||||
}
|
||||
const text = all.items;
|
||||
|
||||
// Verify every rebound key is rendered.
|
||||
try testing.expect(std.mem.indexOf(u8, text, "ctrl+s") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "shift+tab / tab") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "down / up") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "space") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "x Select symbol") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "f1-f5") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "F1 Full help") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "ctrl+q Quit") != null);
|
||||
|
||||
// No default keys leaked through (sanity).
|
||||
try testing.expect(std.mem.indexOf(u8, text, " / Enter a stock symbol") == null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "h / l") == null);
|
||||
try testing.expect(std.mem.indexOf(u8, text, "j / k") == null);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue