better account filter for interactive mode
This commit is contained in:
parent
c3c9c21556
commit
ddd47dad66
3 changed files with 298 additions and 38 deletions
169
src/tui.zig
169
src/tui.zig
|
|
@ -95,6 +95,7 @@ pub const InputMode = enum {
|
||||||
symbol_input,
|
symbol_input,
|
||||||
help,
|
help,
|
||||||
account_picker,
|
account_picker,
|
||||||
|
account_search,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const StyledLine = struct {
|
pub const StyledLine = struct {
|
||||||
|
|
@ -310,8 +311,14 @@ pub const App = struct {
|
||||||
// Account filter state
|
// Account filter state
|
||||||
account_filter: ?[]const u8 = null, // active account filter (owned copy; null = all accounts)
|
account_filter: ?[]const u8 = null, // active account filter (owned copy; null = all accounts)
|
||||||
filtered_positions: ?[]zfin.Position = null, // positions for filtered account (from positionsForAccount)
|
filtered_positions: ?[]zfin.Position = null, // positions for filtered account (from positionsForAccount)
|
||||||
account_list: std.ArrayList([]const u8) = .empty, // distinct accounts from portfolio lots (borrowed from portfolio)
|
account_list: std.ArrayList([]const u8) = .empty, // distinct accounts from portfolio lots (ordered by accounts.srf)
|
||||||
|
account_numbers: std.ArrayList(?[]const u8) = .empty, // account_number from accounts.srf (parallel to account_list)
|
||||||
|
account_shortcut_keys: std.ArrayList(u8) = .empty, // auto-assigned shortcut key per account (parallel to account_list)
|
||||||
account_picker_cursor: usize = 0, // cursor position in picker (0 = "All accounts")
|
account_picker_cursor: usize = 0, // cursor position in picker (0 = "All accounts")
|
||||||
|
account_search_buf: [64]u8 = undefined,
|
||||||
|
account_search_len: usize = 0,
|
||||||
|
account_search_matches: std.ArrayList(usize) = .empty, // indices into account_list matching search
|
||||||
|
account_search_cursor: usize = 0, // cursor within search_matches
|
||||||
|
|
||||||
// Options navigation (inline expand/collapse like portfolio)
|
// Options navigation (inline expand/collapse like portfolio)
|
||||||
options_cursor: usize = 0, // selected row in flattened options view
|
options_cursor: usize = 0, // selected row in flattened options view
|
||||||
|
|
@ -388,6 +395,9 @@ pub const App = struct {
|
||||||
if (self.mode == .account_picker) {
|
if (self.mode == .account_picker) {
|
||||||
return self.handleAccountPickerKey(ctx, key);
|
return self.handleAccountPickerKey(ctx, key);
|
||||||
}
|
}
|
||||||
|
if (self.mode == .account_search) {
|
||||||
|
return self.handleAccountSearchKey(ctx, key);
|
||||||
|
}
|
||||||
if (self.mode == .help) {
|
if (self.mode == .help) {
|
||||||
self.mode = .normal;
|
self.mode = .normal;
|
||||||
return ctx.consumeAndRedraw();
|
return ctx.consumeAndRedraw();
|
||||||
|
|
@ -620,19 +630,44 @@ pub const App = struct {
|
||||||
fn handleAccountPickerKey(self: *App, ctx: *vaxis.vxfw.EventContext, key: vaxis.Key) void {
|
fn handleAccountPickerKey(self: *App, ctx: *vaxis.vxfw.EventContext, key: vaxis.Key) void {
|
||||||
const total_items = self.account_list.items.len + 1; // +1 for "All accounts"
|
const total_items = self.account_list.items.len + 1; // +1 for "All accounts"
|
||||||
|
|
||||||
// Cancel: return to normal mode without changing the filter
|
if (key.codepoint == vaxis.Key.escape or key.codepoint == 'q') {
|
||||||
if (key.codepoint == vaxis.Key.escape) {
|
|
||||||
self.mode = .normal;
|
self.mode = .normal;
|
||||||
return ctx.consumeAndRedraw();
|
return ctx.consumeAndRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm: apply the selected account filter
|
|
||||||
if (key.codepoint == vaxis.Key.enter) {
|
if (key.codepoint == vaxis.Key.enter) {
|
||||||
self.applyAccountPickerSelection();
|
self.applyAccountPickerSelection();
|
||||||
return ctx.consumeAndRedraw();
|
return ctx.consumeAndRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the keymap for navigation actions
|
// '/' enters search mode
|
||||||
|
if (key.matches('/', .{})) {
|
||||||
|
self.mode = .account_search;
|
||||||
|
self.account_search_len = 0;
|
||||||
|
self.updateAccountSearchMatches();
|
||||||
|
return ctx.consumeAndRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'A' selects "All accounts" instantly
|
||||||
|
if (key.matches('A', .{})) {
|
||||||
|
self.account_picker_cursor = 0;
|
||||||
|
self.applyAccountPickerSelection();
|
||||||
|
return ctx.consumeAndRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check shortcut keys for instant selection
|
||||||
|
if (key.codepoint < std.math.maxInt(u7) and key.matches(key.codepoint, .{})) {
|
||||||
|
const ch: u8 = @intCast(key.codepoint);
|
||||||
|
for (self.account_shortcut_keys.items, 0..) |shortcut, i| {
|
||||||
|
if (shortcut == ch) {
|
||||||
|
self.account_picker_cursor = i + 1; // +1 for "All accounts" at 0
|
||||||
|
self.applyAccountPickerSelection();
|
||||||
|
return ctx.consumeAndRedraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation via keymap
|
||||||
const action = self.keymap.matchAction(key) orelse return;
|
const action = self.keymap.matchAction(key) orelse return;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.select_next => {
|
.select_next => {
|
||||||
|
|
@ -658,6 +693,111 @@ pub const App = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles keypresses in account_search mode (/ search within picker).
|
||||||
|
fn handleAccountSearchKey(self: *App, ctx: *vaxis.vxfw.EventContext, key: vaxis.Key) void {
|
||||||
|
// Escape: cancel search, return to picker
|
||||||
|
if (key.codepoint == vaxis.Key.escape) {
|
||||||
|
self.mode = .account_picker;
|
||||||
|
self.account_search_len = 0;
|
||||||
|
return ctx.consumeAndRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter: select the first match (or current search cursor)
|
||||||
|
if (key.codepoint == vaxis.Key.enter) {
|
||||||
|
if (self.account_search_matches.items.len > 0) {
|
||||||
|
const match_idx = self.account_search_matches.items[self.account_search_cursor];
|
||||||
|
self.account_picker_cursor = match_idx + 1; // +1 for "All accounts"
|
||||||
|
}
|
||||||
|
self.account_search_len = 0;
|
||||||
|
self.applyAccountPickerSelection();
|
||||||
|
return ctx.consumeAndRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+N / Ctrl+P or arrow keys to cycle through matches
|
||||||
|
if (key.matches('n', .{ .ctrl = true }) or key.codepoint == vaxis.Key.down) {
|
||||||
|
if (self.account_search_matches.items.len > 0 and
|
||||||
|
self.account_search_cursor < self.account_search_matches.items.len - 1)
|
||||||
|
self.account_search_cursor += 1;
|
||||||
|
return ctx.consumeAndRedraw();
|
||||||
|
}
|
||||||
|
if (key.matches('p', .{ .ctrl = true }) or key.codepoint == vaxis.Key.up) {
|
||||||
|
if (self.account_search_cursor > 0)
|
||||||
|
self.account_search_cursor -= 1;
|
||||||
|
return ctx.consumeAndRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backspace
|
||||||
|
if (key.codepoint == vaxis.Key.backspace) {
|
||||||
|
if (self.account_search_len > 0) {
|
||||||
|
self.account_search_len -= 1;
|
||||||
|
self.updateAccountSearchMatches();
|
||||||
|
}
|
||||||
|
return ctx.consumeAndRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+U: clear search
|
||||||
|
if (key.matches('u', .{ .ctrl = true })) {
|
||||||
|
self.account_search_len = 0;
|
||||||
|
self.updateAccountSearchMatches();
|
||||||
|
return ctx.consumeAndRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printable ASCII
|
||||||
|
if (key.codepoint < std.math.maxInt(u7) and std.ascii.isPrint(@intCast(key.codepoint)) and self.account_search_len < self.account_search_buf.len) {
|
||||||
|
self.account_search_buf[self.account_search_len] = @intCast(key.codepoint);
|
||||||
|
self.account_search_len += 1;
|
||||||
|
self.updateAccountSearchMatches();
|
||||||
|
return ctx.consumeAndRedraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update search match indices based on current search string.
|
||||||
|
fn updateAccountSearchMatches(self: *App) void {
|
||||||
|
self.account_search_matches.clearRetainingCapacity();
|
||||||
|
const query = self.account_search_buf[0..self.account_search_len];
|
||||||
|
if (query.len == 0) return;
|
||||||
|
|
||||||
|
var lower_query: [64]u8 = undefined;
|
||||||
|
for (query, 0..) |c, i| lower_query[i] = std.ascii.toLower(c);
|
||||||
|
const lq = lower_query[0..query.len];
|
||||||
|
|
||||||
|
for (self.account_list.items, 0..) |acct, i| {
|
||||||
|
if (containsLower(acct, lq)) {
|
||||||
|
self.account_search_matches.append(self.allocator, i) catch continue;
|
||||||
|
} else if (i < self.account_numbers.items.len) {
|
||||||
|
if (self.account_numbers.items[i]) |num| {
|
||||||
|
if (containsLower(num, lq)) {
|
||||||
|
self.account_search_matches.append(self.allocator, i) catch continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.account_search_cursor >= self.account_search_matches.items.len) {
|
||||||
|
self.account_search_cursor = if (self.account_search_matches.items.len > 0)
|
||||||
|
self.account_search_matches.items.len - 1
|
||||||
|
else
|
||||||
|
0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn containsLower(haystack: []const u8, needle_lower: []const u8) bool {
|
||||||
|
if (needle_lower.len == 0) return true;
|
||||||
|
if (haystack.len < needle_lower.len) return false;
|
||||||
|
const end = haystack.len - needle_lower.len + 1;
|
||||||
|
for (0..end) |start| {
|
||||||
|
var matched = true;
|
||||||
|
for (0..needle_lower.len) |j| {
|
||||||
|
if (std.ascii.toLower(haystack[start + j]) != needle_lower[j]) {
|
||||||
|
matched = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matched) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply the current account picker selection and return to normal mode.
|
/// Apply the current account picker selection and return to normal mode.
|
||||||
fn applyAccountPickerSelection(self: *App) void {
|
fn applyAccountPickerSelection(self: *App) void {
|
||||||
if (self.account_picker_cursor == 0) {
|
if (self.account_picker_cursor == 0) {
|
||||||
|
|
@ -683,6 +823,20 @@ pub const App = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load accounts.srf if not already loaded. Derives path from portfolio_path.
|
||||||
|
pub fn ensureAccountMap(self: *App) void {
|
||||||
|
if (self.account_map != null) return;
|
||||||
|
const ppath = self.portfolio_path orelse return;
|
||||||
|
const dir_end = if (std.mem.lastIndexOfScalar(u8, ppath, std.fs.path.sep)) |idx| idx + 1 else 0;
|
||||||
|
const acct_path = std.fmt.allocPrint(self.allocator, "{s}accounts.srf", .{ppath[0..dir_end]}) catch return;
|
||||||
|
defer self.allocator.free(acct_path);
|
||||||
|
|
||||||
|
if (std.fs.cwd().readFileAlloc(self.allocator, acct_path, 1024 * 1024)) |acct_data| {
|
||||||
|
defer self.allocator.free(acct_data);
|
||||||
|
self.account_map = zfin.analysis.parseAccountsFile(self.allocator, acct_data) catch null;
|
||||||
|
} else |_| {}
|
||||||
|
}
|
||||||
|
|
||||||
/// Set or clear the account filter. Owns the string via allocator.
|
/// Set or clear the account filter. Owns the string via allocator.
|
||||||
pub fn setAccountFilter(self: *App, name: ?[]const u8) void {
|
pub fn setAccountFilter(self: *App, name: ?[]const u8) void {
|
||||||
if (self.account_filter) |old| self.allocator.free(old);
|
if (self.account_filter) |old| self.allocator.free(old);
|
||||||
|
|
@ -1352,6 +1506,9 @@ pub const App = struct {
|
||||||
self.portfolio_rows.deinit(self.allocator);
|
self.portfolio_rows.deinit(self.allocator);
|
||||||
self.options_rows.deinit(self.allocator);
|
self.options_rows.deinit(self.allocator);
|
||||||
self.account_list.deinit(self.allocator);
|
self.account_list.deinit(self.allocator);
|
||||||
|
self.account_numbers.deinit(self.allocator);
|
||||||
|
self.account_shortcut_keys.deinit(self.allocator);
|
||||||
|
self.account_search_matches.deinit(self.allocator);
|
||||||
if (self.account_filter) |af| self.allocator.free(af);
|
if (self.account_filter) |af| self.allocator.free(af);
|
||||||
if (self.filtered_positions) |fp| self.allocator.free(fp);
|
if (self.filtered_positions) |fp| self.allocator.free(fp);
|
||||||
if (self.watchlist_prices) |*wp| wp.deinit();
|
if (self.watchlist_prices) |*wp| wp.deinit();
|
||||||
|
|
@ -1455,7 +1612,7 @@ pub const App = struct {
|
||||||
|
|
||||||
if (self.mode == .help) {
|
if (self.mode == .help) {
|
||||||
try self.drawStyledContent(ctx.arena, buf, width, height, try self.buildHelpStyledLines(ctx.arena));
|
try self.drawStyledContent(ctx.arena, buf, width, height, try self.buildHelpStyledLines(ctx.arena));
|
||||||
} else if (self.mode == .account_picker) {
|
} else if (self.mode == .account_picker or self.mode == .account_search) {
|
||||||
try portfolio_tab.drawAccountPicker(self, ctx.arena, buf, width, height);
|
try portfolio_tab.drawAccountPicker(self, ctx.arena, buf, width, height);
|
||||||
} else {
|
} else {
|
||||||
switch (self.active_tab) {
|
switch (self.active_tab) {
|
||||||
|
|
|
||||||
|
|
@ -40,23 +40,7 @@ pub fn loadData(app: *App) void {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load account tax type metadata file (optional)
|
// Load account tax type metadata file (optional)
|
||||||
if (app.account_map == null) {
|
app.ensureAccountMap();
|
||||||
if (app.portfolio_path) |ppath| {
|
|
||||||
const dir_end = if (std.mem.lastIndexOfScalar(u8, ppath, std.fs.path.sep)) |idx| idx + 1 else 0;
|
|
||||||
const acct_path = std.fmt.allocPrint(app.allocator, "{s}accounts.srf", .{ppath[0..dir_end]}) catch {
|
|
||||||
loadDataFinish(app, pf, summary);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
defer app.allocator.free(acct_path);
|
|
||||||
|
|
||||||
if (std.fs.cwd().readFileAlloc(app.allocator, acct_path, 1024 * 1024)) |acct_data| {
|
|
||||||
defer app.allocator.free(acct_data);
|
|
||||||
app.account_map = zfin.analysis.parseAccountsFile(app.allocator, acct_data) catch null;
|
|
||||||
} else |_| {
|
|
||||||
// accounts.srf is optional -- analysis works without it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadDataFinish(app, pf, summary);
|
loadDataFinish(app, pf, summary);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -548,33 +548,73 @@ pub fn rebuildPortfolioRows(app: *App) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the sorted list of distinct account names from portfolio lots.
|
/// Build the ordered list of distinct account names from portfolio lots.
|
||||||
/// Called after portfolio data is loaded or reloaded.
|
/// Order: accounts.srf file order first, then any remaining accounts alphabetically.
|
||||||
|
/// Also assigns shortcut keys and loads account numbers from accounts.srf.
|
||||||
pub fn buildAccountList(app: *App) void {
|
pub fn buildAccountList(app: *App) void {
|
||||||
app.account_list.clearRetainingCapacity();
|
app.account_list.clearRetainingCapacity();
|
||||||
|
app.account_numbers.clearRetainingCapacity();
|
||||||
|
app.account_shortcut_keys.clearRetainingCapacity();
|
||||||
|
|
||||||
const pf = app.portfolio orelse return;
|
const pf = app.portfolio orelse return;
|
||||||
|
|
||||||
// Use a set to deduplicate
|
// Collect distinct account names from portfolio lots
|
||||||
var seen = std.StringHashMap(void).init(app.allocator);
|
var seen = std.StringHashMap(void).init(app.allocator);
|
||||||
defer seen.deinit();
|
defer seen.deinit();
|
||||||
|
|
||||||
|
var lot_accounts = std.ArrayList([]const u8).empty;
|
||||||
|
defer lot_accounts.deinit(app.allocator);
|
||||||
|
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.account) |acct| {
|
if (lot.account) |acct| {
|
||||||
if (acct.len > 0 and !seen.contains(acct)) {
|
if (acct.len > 0 and !seen.contains(acct)) {
|
||||||
seen.put(acct, {}) catch continue;
|
seen.put(acct, {}) catch continue;
|
||||||
app.account_list.append(app.allocator, acct) catch continue;
|
lot_accounts.append(app.allocator, acct) catch continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort alphabetically
|
app.ensureAccountMap();
|
||||||
std.mem.sort([]const u8, app.account_list.items, {}, struct {
|
|
||||||
|
// Phase 1: add accounts in accounts.srf order (if available)
|
||||||
|
if (app.account_map) |am| {
|
||||||
|
for (am.entries) |entry| {
|
||||||
|
if (seen.contains(entry.account)) {
|
||||||
|
app.account_list.append(app.allocator, entry.account) catch continue;
|
||||||
|
app.account_numbers.append(app.allocator, entry.account_number) catch continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: add accounts not in accounts.srf, sorted alphabetically
|
||||||
|
var extras = std.ArrayList([]const u8).empty;
|
||||||
|
defer extras.deinit(app.allocator);
|
||||||
|
|
||||||
|
for (lot_accounts.items) |acct| {
|
||||||
|
var found = false;
|
||||||
|
for (app.account_list.items) |existing| {
|
||||||
|
if (std.mem.eql(u8, acct, existing)) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) extras.append(app.allocator, acct) catch continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std.mem.sort([]const u8, extras.items, {}, struct {
|
||||||
fn lessThan(_: void, a: []const u8, b: []const u8) bool {
|
fn lessThan(_: void, a: []const u8, b: []const u8) bool {
|
||||||
return std.mem.lessThan(u8, a, b);
|
return std.mem.lessThan(u8, a, b);
|
||||||
}
|
}
|
||||||
}.lessThan);
|
}.lessThan);
|
||||||
|
|
||||||
|
for (extras.items) |acct| {
|
||||||
|
app.account_list.append(app.allocator, acct) catch continue;
|
||||||
|
app.account_numbers.append(app.allocator, null) catch continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign shortcut keys: 1-9, 0, then b-z (skipping conflict keys)
|
||||||
|
assignShortcutKeys(app);
|
||||||
|
|
||||||
// If the current filter no longer exists in the new list, clear it
|
// If the current filter no longer exists in the new list, clear it
|
||||||
if (app.account_filter) |af| {
|
if (app.account_filter) |af| {
|
||||||
var found = false;
|
var found = false;
|
||||||
|
|
@ -588,6 +628,21 @@ pub fn buildAccountList(app: *App) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shortcut_key_order = "1234567890bcdefhimnoptuvwxyz";
|
||||||
|
|
||||||
|
fn assignShortcutKeys(app: *App) void {
|
||||||
|
app.account_shortcut_keys.clearRetainingCapacity();
|
||||||
|
var key_idx: usize = 0;
|
||||||
|
for (0..app.account_list.items.len) |_| {
|
||||||
|
if (key_idx < shortcut_key_order.len) {
|
||||||
|
app.account_shortcut_keys.append(app.allocator, shortcut_key_order[key_idx]) catch continue;
|
||||||
|
key_idx += 1;
|
||||||
|
} else {
|
||||||
|
app.account_shortcut_keys.append(app.allocator, 0) catch continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Recompute filtered_positions when portfolio or account filter changes.
|
/// Recompute filtered_positions when portfolio or account filter changes.
|
||||||
fn recomputeFilteredPositions(app: *App) void {
|
fn recomputeFilteredPositions(app: *App) void {
|
||||||
if (app.filtered_positions) |fp| app.allocator.free(fp);
|
if (app.filtered_positions) |fp| app.allocator.free(fp);
|
||||||
|
|
@ -1265,28 +1320,92 @@ pub fn drawAccountPicker(app: *App, arena: std.mem.Allocator, buf: []vaxis.Cell,
|
||||||
const th = app.theme;
|
const th = app.theme;
|
||||||
var lines: std.ArrayList(tui.StyledLine) = .empty;
|
var lines: std.ArrayList(tui.StyledLine) = .empty;
|
||||||
|
|
||||||
|
const is_searching = app.mode == .account_search;
|
||||||
|
|
||||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
try lines.append(arena, .{ .text = " Filter by Account", .style = th.headerStyle() });
|
try lines.append(arena, .{ .text = " Filter by Account", .style = th.headerStyle() });
|
||||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
|
|
||||||
|
// Build a set of search-highlighted indices for fast lookup
|
||||||
|
var search_highlight = std.AutoHashMap(usize, void).init(arena);
|
||||||
|
var search_cursor_idx: ?usize = null;
|
||||||
|
if (is_searching and app.account_search_matches.items.len > 0) {
|
||||||
|
for (app.account_search_matches.items, 0..) |match_idx, si| {
|
||||||
|
search_highlight.put(match_idx, {}) catch {};
|
||||||
|
if (si == app.account_search_cursor) search_cursor_idx = match_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Item 0 = "All accounts" (clears filter)
|
// Item 0 = "All accounts" (clears filter)
|
||||||
const total_items = app.account_list.items.len + 1;
|
const total_items = app.account_list.items.len + 1;
|
||||||
for (0..total_items) |i| {
|
for (0..total_items) |i| {
|
||||||
const is_selected = i == app.account_picker_cursor;
|
const is_selected = if (is_searching)
|
||||||
const marker: []const u8 = if (is_selected) " > " else " ";
|
(if (search_cursor_idx) |sci| i == sci + 1 else false)
|
||||||
const label: []const u8 = if (i == 0) "All accounts" else app.account_list.items[i - 1];
|
else
|
||||||
const text = try std.fmt.allocPrint(arena, "{s}{s}", .{ marker, label });
|
i == app.account_picker_cursor;
|
||||||
const style = if (is_selected) th.selectStyle() else th.contentStyle();
|
const marker: []const u8 = if (is_selected) " > " else " ";
|
||||||
try lines.append(arena, .{ .text = text, .style = style });
|
|
||||||
|
if (i == 0) {
|
||||||
|
const text = try std.fmt.allocPrint(arena, "{s}A: All accounts", .{marker});
|
||||||
|
const style = if (is_selected) th.selectStyle() else th.contentStyle();
|
||||||
|
const dimmed = is_searching and app.account_search_len > 0;
|
||||||
|
try lines.append(arena, .{ .text = text, .style = if (dimmed) th.mutedStyle() else style });
|
||||||
|
} else {
|
||||||
|
const acct_idx = i - 1;
|
||||||
|
const label = app.account_list.items[acct_idx];
|
||||||
|
const shortcut: u8 = if (acct_idx < app.account_shortcut_keys.items.len) app.account_shortcut_keys.items[acct_idx] else 0;
|
||||||
|
const acct_num: ?[]const u8 = if (acct_idx < app.account_numbers.items.len) app.account_numbers.items[acct_idx] else null;
|
||||||
|
|
||||||
|
const text = if (acct_num) |num|
|
||||||
|
(if (shortcut != 0)
|
||||||
|
try std.fmt.allocPrint(arena, "{s}{c}: {s} ({s})", .{ marker, shortcut, label, num })
|
||||||
|
else
|
||||||
|
try std.fmt.allocPrint(arena, "{s} {s} ({s})", .{ marker, label, num }))
|
||||||
|
else if (shortcut != 0)
|
||||||
|
try std.fmt.allocPrint(arena, "{s}{c}: {s}", .{ marker, shortcut, label })
|
||||||
|
else
|
||||||
|
try std.fmt.allocPrint(arena, "{s} {s}", .{ marker, label });
|
||||||
|
|
||||||
|
var style = if (is_selected) th.selectStyle() else th.contentStyle();
|
||||||
|
if (is_searching and app.account_search_len > 0) {
|
||||||
|
if (search_highlight.contains(acct_idx)) {
|
||||||
|
if (!is_selected) style = th.headerStyle();
|
||||||
|
} else {
|
||||||
|
style = th.mutedStyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try lines.append(arena, .{ .text = text, .style = style });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
// Search prompt at the bottom
|
||||||
|
if (is_searching) {
|
||||||
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
|
const query = app.account_search_buf[0..app.account_search_len];
|
||||||
|
const match_count = app.account_search_matches.items.len;
|
||||||
|
const prompt = if (query.len > 0)
|
||||||
|
try std.fmt.allocPrint(arena, " /{s} ({d} match{s})", .{
|
||||||
|
query,
|
||||||
|
match_count,
|
||||||
|
if (match_count != 1) @as([]const u8, "es") else "",
|
||||||
|
})
|
||||||
|
else
|
||||||
|
try std.fmt.allocPrint(arena, " /", .{});
|
||||||
|
try lines.append(arena, .{ .text = prompt, .style = th.headerStyle() });
|
||||||
|
} else {
|
||||||
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
|
try lines.append(arena, .{ .text = " /: search j/k: navigate Enter: select Esc: cancel", .style = th.mutedStyle() });
|
||||||
|
}
|
||||||
|
|
||||||
// Scroll so cursor is visible
|
// Scroll so cursor is visible
|
||||||
const cursor_line = app.account_picker_cursor + account_picker_header_lines;
|
const effective_cursor = if (is_searching)
|
||||||
|
(if (search_cursor_idx) |sci| sci + 1 else 0)
|
||||||
|
else
|
||||||
|
app.account_picker_cursor;
|
||||||
|
const cursor_line = effective_cursor + account_picker_header_lines;
|
||||||
var start: usize = 0;
|
var start: usize = 0;
|
||||||
if (cursor_line >= height) {
|
if (cursor_line >= height) {
|
||||||
start = cursor_line - height + 2; // keep one line of padding below
|
start = cursor_line - height + 2;
|
||||||
}
|
}
|
||||||
start = @min(start, if (lines.items.len > 0) lines.items.len - 1 else 0);
|
start = @min(start, if (lines.items.len > 0) lines.items.len - 1 else 0);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue