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,
|
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`
|
/// Root widget for the interactive TUI. Implements the vaxis `vxfw.Widget`
|
||||||
/// interface via `widget()`, which wires `typeErasedEventHandler` and
|
/// interface via `widget()`, which wires `typeErasedEventHandler` and
|
||||||
/// `typeErasedDrawFn` as callbacks. Passed to `vxfw.App.run()` as the
|
/// `typeErasedDrawFn` as callbacks. Passed to `vxfw.App.run()` as the
|
||||||
|
|
@ -293,20 +309,8 @@ pub const App = struct {
|
||||||
account_map: ?zfin.analysis.AccountMap = null,
|
account_map: ?zfin.analysis.AccountMap = null,
|
||||||
|
|
||||||
// Chart state (Kitty graphics)
|
// Chart state (Kitty graphics)
|
||||||
chart_config: chart_mod.ChartConfig = .{},
|
chart: ChartState = .{},
|
||||||
vx_app: ?*vaxis.vxfw.App = null, // set during run(), for Kitty graphics access
|
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 {
|
pub fn widget(self: *App) vaxis.vxfw.Widget {
|
||||||
return .{
|
return .{
|
||||||
|
|
@ -420,7 +424,7 @@ pub const App = struct {
|
||||||
}
|
}
|
||||||
// Quote tab: click on timeframe selector to switch timeframes
|
// Quote tab: click on timeframe selector to switch timeframes
|
||||||
if (self.active_tab == .quote and mouse.row > 0) {
|
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;
|
const content_row = @as(usize, @intCast(mouse.row)) + self.scroll_offset;
|
||||||
if (content_row == tf_row) {
|
if (content_row == tf_row) {
|
||||||
// " Chart: [6M] YTD 1Y 3Y 5Y ([ ] to change)"
|
// " Chart: [6M] YTD 1Y 3Y 5Y ([ ] to change)"
|
||||||
|
|
@ -434,8 +438,8 @@ pub const App = struct {
|
||||||
const lbl_len = tf.label().len;
|
const lbl_len = tf.label().len;
|
||||||
const slot_width = lbl_len + 2 + 1; // [XX] + space or XX + space
|
const slot_width = lbl_len + 2 + 1; // [XX] + space or XX + space
|
||||||
if (col >= x and col < x + slot_width) {
|
if (col >= x and col < x + slot_width) {
|
||||||
if (tf != self.chart_timeframe) {
|
if (tf != self.chart.timeframe) {
|
||||||
self.chart_timeframe = tf;
|
self.chart.timeframe = tf;
|
||||||
self.setStatus(tf.label());
|
self.setStatus(tf.label());
|
||||||
return ctx.consumeAndRedraw();
|
return ctx.consumeAndRedraw();
|
||||||
}
|
}
|
||||||
|
|
@ -686,17 +690,17 @@ pub const App = struct {
|
||||||
},
|
},
|
||||||
.chart_timeframe_next => {
|
.chart_timeframe_next => {
|
||||||
if (self.active_tab == .quote) {
|
if (self.active_tab == .quote) {
|
||||||
self.chart_timeframe = self.chart_timeframe.next();
|
self.chart.timeframe = self.chart.timeframe.next();
|
||||||
self.chart_dirty = true;
|
self.chart.dirty = true;
|
||||||
self.setStatus(self.chart_timeframe.label());
|
self.setStatus(self.chart.timeframe.label());
|
||||||
return ctx.consumeAndRedraw();
|
return ctx.consumeAndRedraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.chart_timeframe_prev => {
|
.chart_timeframe_prev => {
|
||||||
if (self.active_tab == .quote) {
|
if (self.active_tab == .quote) {
|
||||||
self.chart_timeframe = self.chart_timeframe.prev();
|
self.chart.timeframe = self.chart.timeframe.prev();
|
||||||
self.chart_dirty = true;
|
self.chart.dirty = true;
|
||||||
self.setStatus(self.chart_timeframe.label());
|
self.setStatus(self.chart.timeframe.label());
|
||||||
return ctx.consumeAndRedraw();
|
return ctx.consumeAndRedraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -939,7 +943,7 @@ pub const App = struct {
|
||||||
self.trailing_me_total = null;
|
self.trailing_me_total = null;
|
||||||
self.risk_metrics = null;
|
self.risk_metrics = null;
|
||||||
self.scroll_offset = 0;
|
self.scroll_offset = 0;
|
||||||
self.chart_dirty = true;
|
self.chart.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refreshCurrentTab(self: *App) void {
|
fn refreshCurrentTab(self: *App) void {
|
||||||
|
|
@ -968,7 +972,7 @@ pub const App = struct {
|
||||||
self.perf_loaded = false;
|
self.perf_loaded = false;
|
||||||
self.freeCandles();
|
self.freeCandles();
|
||||||
self.freeDividends();
|
self.freeDividends();
|
||||||
self.chart_dirty = true;
|
self.chart.dirty = true;
|
||||||
},
|
},
|
||||||
.earnings => {
|
.earnings => {
|
||||||
self.earnings_loaded = false;
|
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,
|
.portfolio_path = portfolio_path,
|
||||||
.symbol = symbol,
|
.symbol = symbol,
|
||||||
.has_explicit_symbol = has_explicit_symbol,
|
.has_explicit_symbol = has_explicit_symbol,
|
||||||
.chart_config = chart_config,
|
.chart = .{ .config = chart_config },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (portfolio_path) |path| {
|
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 app_inst.vx_app = null;
|
||||||
defer {
|
defer {
|
||||||
// Free any chart image before vaxis is torn down
|
// 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);
|
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(), .{});
|
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;
|
const arena = ctx.arena;
|
||||||
|
|
||||||
// Determine whether to use Kitty graphics
|
// Determine whether to use Kitty graphics
|
||||||
const use_kitty = switch (self.chart_config.mode) {
|
const use_kitty = switch (self.chart.config.mode) {
|
||||||
.braille => false,
|
.braille => false,
|
||||||
.kitty => true,
|
.kitty => true,
|
||||||
.auto => if (self.vx_app) |va| va.vx.caps.kitty_graphics else false,
|
.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" };
|
const timeframes = [_]chart_mod.Timeframe{ .@"6M", .ytd, .@"1Y", .@"3Y", .@"5Y" };
|
||||||
for (timeframes) |tf| {
|
for (timeframes) |tf| {
|
||||||
const lbl = tf.label();
|
const lbl = tf.label();
|
||||||
if (tf == self.chart_timeframe) {
|
if (tf == self.chart.timeframe) {
|
||||||
tf_buf[tf_pos] = '[';
|
tf_buf[tf_pos] = '[';
|
||||||
tf_pos += 1;
|
tf_pos += 1;
|
||||||
@memcpy(tf_buf[tf_pos..][0..lbl.len], lbl);
|
@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)";
|
const hint = " ([ ] to change)";
|
||||||
@memcpy(tf_buf[tf_pos..][0..hint.len], hint);
|
@memcpy(tf_buf[tf_pos..][0..hint.len], hint);
|
||||||
tf_pos += hint.len;
|
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() });
|
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;
|
if (px_w < 100 or px_h < 100) return;
|
||||||
// Apply resolution cap from chart config
|
// Apply resolution cap from chart config
|
||||||
const capped_w = @min(px_w, self.chart_config.max_width);
|
const capped_w = @min(px_w, self.chart.config.max_width);
|
||||||
const capped_h = @min(px_h, self.chart_config.max_height);
|
const capped_h = @min(px_h, self.chart.config.max_height);
|
||||||
|
|
||||||
// Check if we need to re-render the chart image
|
// Check if we need to re-render the chart image
|
||||||
const symbol_changed = self.chart_symbol_len != self.symbol.len or
|
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);
|
!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 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
|
// Free old image
|
||||||
if (self.chart_image_id) |old_id| {
|
if (self.chart.image_id) |old_id| {
|
||||||
if (self.vx_app) |va| {
|
if (self.vx_app) |va| {
|
||||||
va.vx.freeImage(va.tty.writer(), old_id);
|
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,
|
// 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(
|
const chart_result = chart_mod.renderChart(
|
||||||
self.allocator,
|
self.allocator,
|
||||||
c,
|
c,
|
||||||
self.chart_timeframe,
|
self.chart.timeframe,
|
||||||
capped_w,
|
capped_w,
|
||||||
capped_h,
|
capped_h,
|
||||||
th,
|
th,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
self.chart_dirty = false;
|
self.chart.dirty = false;
|
||||||
var err_buf: [128]u8 = undefined;
|
var err_buf: [128]u8 = undefined;
|
||||||
const msg = std.fmt.bufPrint(&err_buf, "Chart render failed: {s}", .{@errorName(err)}) catch "Chart render failed";
|
const msg = std.fmt.bufPrint(&err_buf, "Chart render failed: {s}", .{@errorName(err)}) catch "Chart render failed";
|
||||||
self.setStatus(msg);
|
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.
|
// This avoids the PNG encode → file write → file read → PNG decode roundtrip.
|
||||||
const base64_enc = std.base64.standard.Encoder;
|
const base64_enc = std.base64.standard.Encoder;
|
||||||
const b64_buf = self.allocator.alloc(u8, base64_enc.calcSize(chart_result.rgb_data.len)) catch {
|
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");
|
self.setStatus("Chart: base64 alloc failed");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -183,31 +183,31 @@ fn drawWithKittyChart(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell
|
||||||
chart_result.height,
|
chart_result.height,
|
||||||
.rgb,
|
.rgb,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
self.chart_dirty = false;
|
self.chart.dirty = false;
|
||||||
var err_buf: [128]u8 = undefined;
|
var err_buf: [128]u8 = undefined;
|
||||||
const msg = std.fmt.bufPrint(&err_buf, "Image transmit failed: {s}", .{@errorName(err)}) catch "Image transmit failed";
|
const msg = std.fmt.bufPrint(&err_buf, "Image transmit failed: {s}", .{@errorName(err)}) catch "Image transmit failed";
|
||||||
self.setStatus(msg);
|
self.setStatus(msg);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.chart_image_id = img.id;
|
self.chart.image_id = img.id;
|
||||||
self.chart_image_width = @intCast(chart_cols);
|
self.chart.image_width = @intCast(chart_cols);
|
||||||
self.chart_image_height = chart_rows;
|
self.chart.image_height = chart_rows;
|
||||||
|
|
||||||
// Track what we rendered
|
// Track what we rendered
|
||||||
const sym_len = @min(self.symbol.len, 16);
|
const sym_len = @min(self.symbol.len, 16);
|
||||||
@memcpy(self.chart_symbol[0..sym_len], self.symbol[0..sym_len]);
|
@memcpy(self.chart.symbol[0..sym_len], self.symbol[0..sym_len]);
|
||||||
self.chart_symbol_len = sym_len;
|
self.chart.symbol_len = sym_len;
|
||||||
self.chart_timeframe_rendered = self.chart_timeframe;
|
self.chart.timeframe_rendered = self.chart.timeframe;
|
||||||
self.chart_price_min = chart_result.price_min;
|
self.chart.price_min = chart_result.price_min;
|
||||||
self.chart_price_max = chart_result.price_max;
|
self.chart.price_max = chart_result.price_max;
|
||||||
self.chart_rsi_latest = chart_result.rsi_latest;
|
self.chart.rsi_latest = chart_result.rsi_latest;
|
||||||
self.chart_dirty = false;
|
self.chart.dirty = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place the image in the cell buffer
|
// 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
|
// Place image at the first cell of the chart area
|
||||||
const chart_row_start: usize = header_rows;
|
const chart_row_start: usize = header_rows;
|
||||||
const chart_col_start: usize = 1; // 1 col left margin
|
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,
|
.img_id = img_id,
|
||||||
.options = .{
|
.options = .{
|
||||||
.size = .{
|
.size = .{
|
||||||
.rows = self.chart_image_height,
|
.rows = self.chart.image_height,
|
||||||
.cols = self.chart_image_width,
|
.cols = self.chart.image_width,
|
||||||
},
|
},
|
||||||
.scale = .contain,
|
.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) ───────────
|
// ── Axis labels (terminal text in the right margin) ───────────
|
||||||
// The chart image uses layout fractions: price=72%, gap=8%, RSI=20%
|
// The chart image uses layout fractions: price=72%, gap=8%, RSI=20%
|
||||||
// Map these to terminal rows to position labels.
|
// Map these to terminal rows to position labels.
|
||||||
const img_rows = self.chart_image_height;
|
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_col: usize = @as(usize, chart_col_start) + @as(usize, self.chart.image_width) + 1;
|
||||||
const label_style = th.mutedStyle();
|
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%)
|
// Price axis labels — evenly spaced across the price panel (top 72%)
|
||||||
const price_panel_rows = @as(f64, @floatFromInt(img_rows)) * 0.72;
|
const price_panel_rows = @as(f64, @floatFromInt(img_rows)) * 0.72;
|
||||||
const n_price_labels: usize = 5;
|
const n_price_labels: usize = 5;
|
||||||
for (0..n_price_labels) |i| {
|
for (0..n_price_labels) |i| {
|
||||||
const frac = @as(f64, @floatFromInt(i)) / @as(f64, @floatFromInt(n_price_labels - 1));
|
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_f = @as(f64, @floatFromInt(chart_row_start)) + frac * price_panel_rows;
|
||||||
const row: usize = @intFromFloat(@round(row_f));
|
const row: usize = @intFromFloat(@round(row_f));
|
||||||
if (row >= height) continue;
|
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
|
// 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) {
|
if (detail_start_row + 8 < height) {
|
||||||
var detail_lines: std.ArrayList(StyledLine) = .empty;
|
var detail_lines: std.ArrayList(StyledLine) = .empty;
|
||||||
try detail_lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
try detail_lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue