From b66b9391a55e77d8d7771ae4a134711330a09434 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 19 Mar 2026 11:30:12 -0700 Subject: [PATCH] remove magic numbers --- src/tui.zig | 192 ++++++++++++++++++++------------------ src/tui/portfolio_tab.zig | 21 ++++- 2 files changed, 121 insertions(+), 92 deletions(-) diff --git a/src/tui.zig b/src/tui.zig index b1175b0..5b2c93f 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -358,103 +358,117 @@ pub const App = struct { return ctx.consumeAndRedraw(); }, .left => { - if (mouse.type == .press) { - if (mouse.row == 0) { - var col: i16 = 0; - for (tabs) |t| { - const lbl_len: i16 = @intCast(t.label().len); - if (mouse.col >= col and mouse.col < col + lbl_len) { - if (t == .earnings and self.earnings_disabled) return; - self.active_tab = t; - self.scroll_offset = 0; - self.loadTabData(); - return ctx.consumeAndRedraw(); + if (mouse.type != .press) return; + // Tab bar: click to switch tabs + if (mouse.row == 0) { + var col: i16 = 0; + for (tabs) |t| { + const lbl_len: i16 = @intCast(t.label().len); + if (mouse.col >= col and mouse.col < col + lbl_len) { + if (t == .earnings and self.earnings_disabled) return; + self.active_tab = t; + self.scroll_offset = 0; + self.loadTabData(); + return ctx.consumeAndRedraw(); + } + col += lbl_len; + } + } + // Portfolio tab: click header to sort, click row to expand/collapse + if (self.active_tab == .portfolio and mouse.row > 0) { + const content_row = @as(usize, @intCast(mouse.row)) + self.scroll_offset; + // Click on column header row -> sort by that column + if (self.portfolio_header_lines > 0 and content_row == self.portfolio_header_lines - 1) { + const col = @as(usize, @intCast(mouse.col)); + const new_field: ?PortfolioSortField = + if (col < portfolio_tab.col_end_symbol) + .symbol + else if (col < portfolio_tab.col_end_shares) + .shares + else if (col < portfolio_tab.col_end_avg_cost) + .avg_cost + else if (col < portfolio_tab.col_end_price) + .price + else if (col < portfolio_tab.col_end_market_value) + .market_value + else if (col < portfolio_tab.col_end_gain_loss) + .gain_loss + else if (col < portfolio_tab.col_end_weight) + .weight + else if (col < portfolio_tab.col_end_date) + null // Date (not sortable) + else + .account; + if (new_field) |nf| { + if (nf == self.portfolio_sort_field) { + self.portfolio_sort_dir = self.portfolio_sort_dir.flip(); + } else { + self.portfolio_sort_field = nf; + self.portfolio_sort_dir = if (nf == .symbol or nf == .account) .asc else .desc; } - col += lbl_len; + self.sortPortfolioAllocations(); + self.rebuildPortfolioRows(); + return ctx.consumeAndRedraw(); } } - if (self.active_tab == .portfolio and mouse.row > 0) { + if (content_row >= self.portfolio_header_lines and self.portfolio_rows.items.len > 0) { + const line_idx = content_row - self.portfolio_header_lines; + if (line_idx < self.portfolio_line_count and line_idx < self.portfolio_line_to_row.len) { + const row_idx = self.portfolio_line_to_row[line_idx]; + if (row_idx < self.portfolio_rows.items.len) { + self.cursor = row_idx; + self.toggleExpand(); + return ctx.consumeAndRedraw(); + } + } + } + } + // Quote tab: click on timeframe selector to switch timeframes + if (self.active_tab == .quote and mouse.row > 0) { + if (self.chart.timeframe_row) |tf_row| { const content_row = @as(usize, @intCast(mouse.row)) + self.scroll_offset; - // Click on column header row -> sort by that column - if (self.portfolio_header_lines > 0 and content_row == self.portfolio_header_lines - 1) { - // Column boundaries derived from sym_col_width (sw). - // prefix(4) + Symbol(sw+1) + Shares(8+1) + AvgCost(10+1) + Price(10+1) + MV(16+1) + G/L(14+1) + Weight(8) - const sw = fmt.sym_col_width; + if (content_row == tf_row) { + // " Chart: [6M] YTD 1Y 3Y 5Y ([ ] to change)" + // Prefix " Chart: " = 9 chars, then each TF takes label_len+2 (brackets/spaces) + 1 gap const col = @as(usize, @intCast(mouse.col)); - const new_field: ?PortfolioSortField = - if (col < 4 + sw + 1) .symbol else if (col < 4 + sw + 10) .shares else if (col < 4 + sw + 21) .avg_cost else if (col < 4 + sw + 32) .price else if (col < 4 + sw + 49) .market_value else if (col < 4 + sw + 64) .gain_loss else if (col < 4 + sw + 73) .weight else if (col < 4 + sw + 87) null // Date (not sortable) - else .account; - if (new_field) |nf| { - if (nf == self.portfolio_sort_field) { - self.portfolio_sort_dir = self.portfolio_sort_dir.flip(); - } else { - self.portfolio_sort_field = nf; - self.portfolio_sort_dir = if (nf == .symbol or nf == .account) .asc else .desc; + const prefix_len: usize = 9; // " Chart: " + if (col >= prefix_len) { + const timeframes = [_]chart_mod.Timeframe{ .@"6M", .ytd, .@"1Y", .@"3Y", .@"5Y" }; + var x: usize = prefix_len; + for (timeframes) |tf| { + const lbl_len = tf.label().len; + const slot_width = lbl_len + 2 + 1; // [XX] + space or XX + space + if (col >= x and col < x + slot_width) { + if (tf != self.chart.timeframe) { + self.chart.timeframe = tf; + self.setStatus(tf.label()); + return ctx.consumeAndRedraw(); + } + break; + } + x += slot_width; } - self.sortPortfolioAllocations(); - self.rebuildPortfolioRows(); + } + } + } + } + // Options tab: single-click to select and expand/collapse + if (self.active_tab == .options and mouse.row > 0) { + const content_row = @as(usize, @intCast(mouse.row)) + self.scroll_offset; + if (content_row >= self.options_header_lines and self.options_rows.items.len > 0) { + // Walk options_rows tracking styled line position to find which + // row was clicked. Each row = 1 styled line, except puts_header + // which emits an extra blank line before it. + const target_line = content_row - self.options_header_lines; + var current_line: usize = 0; + for (self.options_rows.items, 0..) |orow, oi| { + if (orow.kind == .puts_header) current_line += 1; // extra blank + if (current_line == target_line) { + self.options_cursor = oi; + self.toggleOptionsExpand(); return ctx.consumeAndRedraw(); } - } - if (content_row >= self.portfolio_header_lines and self.portfolio_rows.items.len > 0) { - const line_idx = content_row - self.portfolio_header_lines; - if (line_idx < self.portfolio_line_count and line_idx < self.portfolio_line_to_row.len) { - const row_idx = self.portfolio_line_to_row[line_idx]; - if (row_idx < self.portfolio_rows.items.len) { - self.cursor = row_idx; - self.toggleExpand(); - return ctx.consumeAndRedraw(); - } - } - } - } - // Quote tab: click on timeframe selector to switch timeframes - if (self.active_tab == .quote and mouse.row > 0) { - if (self.chart.timeframe_row) |tf_row| { - const content_row = @as(usize, @intCast(mouse.row)) + self.scroll_offset; - if (content_row == tf_row) { - // " Chart: [6M] YTD 1Y 3Y 5Y ([ ] to change)" - // Prefix " Chart: " = 9 chars, then each TF takes label_len+2 (brackets/spaces) + 1 gap - const col = @as(usize, @intCast(mouse.col)); - const prefix_len: usize = 9; // " Chart: " - if (col >= prefix_len) { - const timeframes = [_]chart_mod.Timeframe{ .@"6M", .ytd, .@"1Y", .@"3Y", .@"5Y" }; - var x: usize = prefix_len; - for (timeframes) |tf| { - const lbl_len = tf.label().len; - const slot_width = lbl_len + 2 + 1; // [XX] + space or XX + space - if (col >= x and col < x + slot_width) { - if (tf != self.chart.timeframe) { - self.chart.timeframe = tf; - self.setStatus(tf.label()); - return ctx.consumeAndRedraw(); - } - break; - } - x += slot_width; - } - } - } - } - } - // Options tab: single-click to select and expand/collapse - if (self.active_tab == .options and mouse.row > 0) { - const content_row = @as(usize, @intCast(mouse.row)) + self.scroll_offset; - if (content_row >= self.options_header_lines and self.options_rows.items.len > 0) { - // Walk options_rows tracking styled line position to find which - // row was clicked. Each row = 1 styled line, except puts_header - // which emits an extra blank line before it. - const target_line = content_row - self.options_header_lines; - var current_line: usize = 0; - for (self.options_rows.items, 0..) |orow, oi| { - if (orow.kind == .puts_header) current_line += 1; // extra blank - if (current_line == target_line) { - self.options_cursor = oi; - self.toggleOptionsExpand(); - return ctx.consumeAndRedraw(); - } - current_line += 1; - } + current_line += 1; } } } diff --git a/src/tui/portfolio_tab.zig b/src/tui/portfolio_tab.zig index 5755353..ff48620 100644 --- a/src/tui/portfolio_tab.zig +++ b/src/tui/portfolio_tab.zig @@ -13,9 +13,24 @@ const PortfolioSortField = tui.PortfolioSortField; const colLabel = tui.colLabel; const glyph = tui.glyph; -// Portfolio column layout: gain/loss column start position (display columns). -// prefix(4) + sym(sym_col_width+1) + shares(9) + avgcost(11) + price(11) + mv(17) = 4 + sym_col_width + 49 -const gl_col_start: usize = 4 + fmt.sym_col_width + 49; +// Portfolio column layout (display columns). +// Each column width includes its trailing separator space. +// prefix(4) + sym(sw+1) + shares(8+1) + avgcost(10+1) + price(10+1) + mv(16+1) + gl(14+1) + weight(8) + date(13+1) + account +const prefix_cols: usize = 4; +const sw: usize = fmt.sym_col_width; + +/// Cumulative column end positions for click-to-sort hit testing. +pub const col_end_symbol: usize = prefix_cols + sw + 1; +pub const col_end_shares: usize = col_end_symbol + 9; +pub const col_end_avg_cost: usize = col_end_shares + 11; +pub const col_end_price: usize = col_end_avg_cost + 11; +pub const col_end_market_value: usize = col_end_price + 17; +pub const col_end_gain_loss: usize = col_end_market_value + 15; +pub const col_end_weight: usize = col_end_gain_loss + 9; +pub const col_end_date: usize = col_end_weight + 14; + +// Gain/loss column start position (used for alt-style coloring) +const gl_col_start: usize = col_end_market_value; // ── Data loading ──────────────────────────────────────────────