introduce chart state to hold the 13 chart state fields
This commit is contained in:
parent
21a45d5309
commit
863111d801
2 changed files with 64 additions and 60 deletions
58
src/tui.zig
58
src/tui.zig
|
|
@ -192,6 +192,22 @@ pub const OptionsRow = struct {
|
|||
contract: ?zfin.OptionContract = null,
|
||||
};
|
||||
|
||||
pub const ChartState = struct {
|
||||
config: chart_mod.ChartConfig = .{},
|
||||
timeframe: chart_mod.Timeframe = .@"1Y",
|
||||
image_id: ?u32 = null, // currently transmitted Kitty image ID
|
||||
image_width: u16 = 0, // image width in cells
|
||||
image_height: u16 = 0, // image height in cells
|
||||
symbol: [16]u8 = undefined, // symbol the chart was rendered for
|
||||
symbol_len: usize = 0,
|
||||
timeframe_rendered: ?chart_mod.Timeframe = null, // timeframe the chart was rendered for
|
||||
timeframe_row: ?usize = null, // screen row of the timeframe selector (for mouse clicks)
|
||||
dirty: bool = true, // needs re-render
|
||||
price_min: f64 = 0,
|
||||
price_max: f64 = 0,
|
||||
rsi_latest: ?f64 = null,
|
||||
};
|
||||
|
||||
/// Root widget for the interactive TUI. Implements the vaxis `vxfw.Widget`
|
||||
/// interface via `widget()`, which wires `typeErasedEventHandler` and
|
||||
/// `typeErasedDrawFn` as callbacks. Passed to `vxfw.App.run()` as the
|
||||
|
|
@ -293,20 +309,8 @@ pub const App = struct {
|
|||
account_map: ?zfin.analysis.AccountMap = null,
|
||||
|
||||
// Chart state (Kitty graphics)
|
||||
chart_config: chart_mod.ChartConfig = .{},
|
||||
chart: ChartState = .{},
|
||||
vx_app: ?*vaxis.vxfw.App = null, // set during run(), for Kitty graphics access
|
||||
chart_timeframe: chart_mod.Timeframe = .@"1Y",
|
||||
chart_image_id: ?u32 = null, // currently transmitted Kitty image ID
|
||||
chart_image_width: u16 = 0, // image width in cells
|
||||
chart_image_height: u16 = 0, // image height in cells
|
||||
chart_symbol: [16]u8 = undefined, // symbol the chart was rendered for
|
||||
chart_symbol_len: usize = 0,
|
||||
chart_timeframe_rendered: ?chart_mod.Timeframe = null, // timeframe the chart was rendered for
|
||||
chart_timeframe_row: ?usize = null, // screen row of the timeframe selector (for mouse clicks)
|
||||
chart_dirty: bool = true, // needs re-render
|
||||
chart_price_min: f64 = 0,
|
||||
chart_price_max: f64 = 0,
|
||||
chart_rsi_latest: ?f64 = null,
|
||||
|
||||
pub fn widget(self: *App) vaxis.vxfw.Widget {
|
||||
return .{
|
||||
|
|
@ -420,7 +424,7 @@ pub const App = struct {
|
|||
}
|
||||
// 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| {
|
||||
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)"
|
||||
|
|
@ -434,8 +438,8 @@ pub const App = struct {
|
|||
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;
|
||||
if (tf != self.chart.timeframe) {
|
||||
self.chart.timeframe = tf;
|
||||
self.setStatus(tf.label());
|
||||
return ctx.consumeAndRedraw();
|
||||
}
|
||||
|
|
@ -686,17 +690,17 @@ pub const App = struct {
|
|||
},
|
||||
.chart_timeframe_next => {
|
||||
if (self.active_tab == .quote) {
|
||||
self.chart_timeframe = self.chart_timeframe.next();
|
||||
self.chart_dirty = true;
|
||||
self.setStatus(self.chart_timeframe.label());
|
||||
self.chart.timeframe = self.chart.timeframe.next();
|
||||
self.chart.dirty = true;
|
||||
self.setStatus(self.chart.timeframe.label());
|
||||
return ctx.consumeAndRedraw();
|
||||
}
|
||||
},
|
||||
.chart_timeframe_prev => {
|
||||
if (self.active_tab == .quote) {
|
||||
self.chart_timeframe = self.chart_timeframe.prev();
|
||||
self.chart_dirty = true;
|
||||
self.setStatus(self.chart_timeframe.label());
|
||||
self.chart.timeframe = self.chart.timeframe.prev();
|
||||
self.chart.dirty = true;
|
||||
self.setStatus(self.chart.timeframe.label());
|
||||
return ctx.consumeAndRedraw();
|
||||
}
|
||||
},
|
||||
|
|
@ -939,7 +943,7 @@ pub const App = struct {
|
|||
self.trailing_me_total = null;
|
||||
self.risk_metrics = null;
|
||||
self.scroll_offset = 0;
|
||||
self.chart_dirty = true;
|
||||
self.chart.dirty = true;
|
||||
}
|
||||
|
||||
fn refreshCurrentTab(self: *App) void {
|
||||
|
|
@ -968,7 +972,7 @@ pub const App = struct {
|
|||
self.perf_loaded = false;
|
||||
self.freeCandles();
|
||||
self.freeDividends();
|
||||
self.chart_dirty = true;
|
||||
self.chart.dirty = true;
|
||||
},
|
||||
.earnings => {
|
||||
self.earnings_loaded = false;
|
||||
|
|
@ -1827,7 +1831,7 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, args: []const []co
|
|||
.portfolio_path = portfolio_path,
|
||||
.symbol = symbol,
|
||||
.has_explicit_symbol = has_explicit_symbol,
|
||||
.chart_config = chart_config,
|
||||
.chart = .{ .config = chart_config },
|
||||
};
|
||||
|
||||
if (portfolio_path) |path| {
|
||||
|
|
@ -1929,9 +1933,9 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, args: []const []co
|
|||
defer app_inst.vx_app = null;
|
||||
defer {
|
||||
// Free any chart image before vaxis is torn down
|
||||
if (app_inst.chart_image_id) |id| {
|
||||
if (app_inst.chart.image_id) |id| {
|
||||
vx_app.vx.freeImage(vx_app.tty.writer(), id);
|
||||
app_inst.chart_image_id = null;
|
||||
app_inst.chart.image_id = null;
|
||||
}
|
||||
}
|
||||
try vx_app.run(app_inst.widget(), .{});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ pub fn drawContent(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell, w
|
|||
const arena = ctx.arena;
|
||||
|
||||
// Determine whether to use Kitty graphics
|
||||
const use_kitty = switch (self.chart_config.mode) {
|
||||
const use_kitty = switch (self.chart.config.mode) {
|
||||
.braille => false,
|
||||
.kitty => true,
|
||||
.auto => if (self.vx_app) |va| va.vx.caps.kitty_graphics else false,
|
||||
|
|
@ -80,7 +80,7 @@ fn drawWithKittyChart(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell
|
|||
const timeframes = [_]chart_mod.Timeframe{ .@"6M", .ytd, .@"1Y", .@"3Y", .@"5Y" };
|
||||
for (timeframes) |tf| {
|
||||
const lbl = tf.label();
|
||||
if (tf == self.chart_timeframe) {
|
||||
if (tf == self.chart.timeframe) {
|
||||
tf_buf[tf_pos] = '[';
|
||||
tf_pos += 1;
|
||||
@memcpy(tf_buf[tf_pos..][0..lbl.len], lbl);
|
||||
|
|
@ -101,7 +101,7 @@ fn drawWithKittyChart(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell
|
|||
const hint = " ([ ] to change)";
|
||||
@memcpy(tf_buf[tf_pos..][0..hint.len], hint);
|
||||
tf_pos += hint.len;
|
||||
self.chart_timeframe_row = lines.items.len; // track which row the timeframe line is on
|
||||
self.chart.timeframe_row = lines.items.len; // track which row the timeframe line is on
|
||||
try lines.append(arena, .{ .text = try arena.dupe(u8, tf_buf[0..tf_pos]), .style = th.mutedStyle() });
|
||||
}
|
||||
|
||||
|
|
@ -129,21 +129,21 @@ fn drawWithKittyChart(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell
|
|||
|
||||
if (px_w < 100 or px_h < 100) return;
|
||||
// Apply resolution cap from chart config
|
||||
const capped_w = @min(px_w, self.chart_config.max_width);
|
||||
const capped_h = @min(px_h, self.chart_config.max_height);
|
||||
const capped_w = @min(px_w, self.chart.config.max_width);
|
||||
const capped_h = @min(px_h, self.chart.config.max_height);
|
||||
|
||||
// Check if we need to re-render the chart image
|
||||
const symbol_changed = self.chart_symbol_len != self.symbol.len or
|
||||
!std.mem.eql(u8, self.chart_symbol[0..self.chart_symbol_len], self.symbol);
|
||||
const tf_changed = self.chart_timeframe_rendered == null or self.chart_timeframe_rendered.? != self.chart_timeframe;
|
||||
const symbol_changed = self.chart.symbol_len != self.symbol.len or
|
||||
!std.mem.eql(u8, self.chart.symbol[0..self.chart.symbol_len], self.symbol);
|
||||
const tf_changed = self.chart.timeframe_rendered == null or self.chart.timeframe_rendered.? != self.chart.timeframe;
|
||||
|
||||
if (self.chart_dirty or symbol_changed or tf_changed) {
|
||||
if (self.chart.dirty or symbol_changed or tf_changed) {
|
||||
// Free old image
|
||||
if (self.chart_image_id) |old_id| {
|
||||
if (self.chart.image_id) |old_id| {
|
||||
if (self.vx_app) |va| {
|
||||
va.vx.freeImage(va.tty.writer(), old_id);
|
||||
}
|
||||
self.chart_image_id = null;
|
||||
self.chart.image_id = null;
|
||||
}
|
||||
|
||||
// Render and transmit — use the app's main allocator, NOT the arena,
|
||||
|
|
@ -152,12 +152,12 @@ fn drawWithKittyChart(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell
|
|||
const chart_result = chart_mod.renderChart(
|
||||
self.allocator,
|
||||
c,
|
||||
self.chart_timeframe,
|
||||
self.chart.timeframe,
|
||||
capped_w,
|
||||
capped_h,
|
||||
th,
|
||||
) catch |err| {
|
||||
self.chart_dirty = false;
|
||||
self.chart.dirty = false;
|
||||
var err_buf: [128]u8 = undefined;
|
||||
const msg = std.fmt.bufPrint(&err_buf, "Chart render failed: {s}", .{@errorName(err)}) catch "Chart render failed";
|
||||
self.setStatus(msg);
|
||||
|
|
@ -169,7 +169,7 @@ fn drawWithKittyChart(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell
|
|||
// This avoids the PNG encode → file write → file read → PNG decode roundtrip.
|
||||
const base64_enc = std.base64.standard.Encoder;
|
||||
const b64_buf = self.allocator.alloc(u8, base64_enc.calcSize(chart_result.rgb_data.len)) catch {
|
||||
self.chart_dirty = false;
|
||||
self.chart.dirty = false;
|
||||
self.setStatus("Chart: base64 alloc failed");
|
||||
return;
|
||||
};
|
||||
|
|
@ -183,31 +183,31 @@ fn drawWithKittyChart(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell
|
|||
chart_result.height,
|
||||
.rgb,
|
||||
) catch |err| {
|
||||
self.chart_dirty = false;
|
||||
self.chart.dirty = false;
|
||||
var err_buf: [128]u8 = undefined;
|
||||
const msg = std.fmt.bufPrint(&err_buf, "Image transmit failed: {s}", .{@errorName(err)}) catch "Image transmit failed";
|
||||
self.setStatus(msg);
|
||||
return;
|
||||
};
|
||||
|
||||
self.chart_image_id = img.id;
|
||||
self.chart_image_width = @intCast(chart_cols);
|
||||
self.chart_image_height = chart_rows;
|
||||
self.chart.image_id = img.id;
|
||||
self.chart.image_width = @intCast(chart_cols);
|
||||
self.chart.image_height = chart_rows;
|
||||
|
||||
// Track what we rendered
|
||||
const sym_len = @min(self.symbol.len, 16);
|
||||
@memcpy(self.chart_symbol[0..sym_len], self.symbol[0..sym_len]);
|
||||
self.chart_symbol_len = sym_len;
|
||||
self.chart_timeframe_rendered = self.chart_timeframe;
|
||||
self.chart_price_min = chart_result.price_min;
|
||||
self.chart_price_max = chart_result.price_max;
|
||||
self.chart_rsi_latest = chart_result.rsi_latest;
|
||||
self.chart_dirty = false;
|
||||
@memcpy(self.chart.symbol[0..sym_len], self.symbol[0..sym_len]);
|
||||
self.chart.symbol_len = sym_len;
|
||||
self.chart.timeframe_rendered = self.chart.timeframe;
|
||||
self.chart.price_min = chart_result.price_min;
|
||||
self.chart.price_max = chart_result.price_max;
|
||||
self.chart.rsi_latest = chart_result.rsi_latest;
|
||||
self.chart.dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Place the image in the cell buffer
|
||||
if (self.chart_image_id) |img_id| {
|
||||
if (self.chart.image_id) |img_id| {
|
||||
// Place image at the first cell of the chart area
|
||||
const chart_row_start: usize = header_rows;
|
||||
const chart_col_start: usize = 1; // 1 col left margin
|
||||
|
|
@ -220,8 +220,8 @@ fn drawWithKittyChart(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell
|
|||
.img_id = img_id,
|
||||
.options = .{
|
||||
.size = .{
|
||||
.rows = self.chart_image_height,
|
||||
.cols = self.chart_image_width,
|
||||
.rows = self.chart.image_height,
|
||||
.cols = self.chart.image_width,
|
||||
},
|
||||
.scale = .contain,
|
||||
},
|
||||
|
|
@ -232,17 +232,17 @@ fn drawWithKittyChart(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell
|
|||
// ── Axis labels (terminal text in the right margin) ───────────
|
||||
// The chart image uses layout fractions: price=72%, gap=8%, RSI=20%
|
||||
// Map these to terminal rows to position labels.
|
||||
const img_rows = self.chart_image_height;
|
||||
const label_col: usize = @as(usize, chart_col_start) + @as(usize, self.chart_image_width) + 1;
|
||||
const img_rows = self.chart.image_height;
|
||||
const label_col: usize = @as(usize, chart_col_start) + @as(usize, self.chart.image_width) + 1;
|
||||
const label_style = th.mutedStyle();
|
||||
|
||||
if (label_col + 8 <= width and img_rows >= 4 and self.chart_price_max > self.chart_price_min) {
|
||||
if (label_col + 8 <= width and img_rows >= 4 and self.chart.price_max > self.chart.price_min) {
|
||||
// Price axis labels — evenly spaced across the price panel (top 72%)
|
||||
const price_panel_rows = @as(f64, @floatFromInt(img_rows)) * 0.72;
|
||||
const n_price_labels: usize = 5;
|
||||
for (0..n_price_labels) |i| {
|
||||
const frac = @as(f64, @floatFromInt(i)) / @as(f64, @floatFromInt(n_price_labels - 1));
|
||||
const price_val = self.chart_price_max - frac * (self.chart_price_max - self.chart_price_min);
|
||||
const price_val = self.chart.price_max - frac * (self.chart.price_max - self.chart.price_min);
|
||||
const row_f = @as(f64, @floatFromInt(chart_row_start)) + frac * price_panel_rows;
|
||||
const row: usize = @intFromFloat(@round(row_f));
|
||||
if (row >= height) continue;
|
||||
|
|
@ -290,7 +290,7 @@ fn drawWithKittyChart(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell
|
|||
}
|
||||
|
||||
// Render quote details below the chart image as styled text
|
||||
const detail_start_row = header_rows + self.chart_image_height;
|
||||
const detail_start_row = header_rows + self.chart.image_height;
|
||||
if (detail_start_row + 8 < height) {
|
||||
var detail_lines: std.ArrayList(StyledLine) = .empty;
|
||||
try detail_lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue