restructure for single test binary/simplify Coverage module (drop to 30% thresh temp)
This commit is contained in:
parent
24924460d2
commit
313ef83065
22 changed files with 348 additions and 410 deletions
|
|
@ -29,7 +29,7 @@ repos:
|
|||
- id: test
|
||||
name: Run zig build test
|
||||
entry: zig
|
||||
args: ["build", "coverage", "-Dcoverage-threshold=80"]
|
||||
args: ["build", "coverage", "-Dcoverage-threshold=30"]
|
||||
language: system
|
||||
types: [file]
|
||||
pass_filenames: false
|
||||
|
|
|
|||
69
build.zig
69
build.zig
|
|
@ -23,8 +23,9 @@ pub fn build(b: *std.Build) void {
|
|||
|
||||
const srf_mod = srf_dep.module("srf");
|
||||
|
||||
// Library module -- the public API for consumers of zfin
|
||||
const mod = b.addModule("zfin", .{
|
||||
// Library module -- the public API for downstream consumers of zfin.
|
||||
// Internal code (CLI, TUI) uses file-path imports instead.
|
||||
_ = b.addModule("zfin", .{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.imports = &.{
|
||||
|
|
@ -32,34 +33,23 @@ pub fn build(b: *std.Build) void {
|
|||
},
|
||||
});
|
||||
|
||||
// Shared imports for TUI and CLI modules
|
||||
const tui_imports: []const std.Build.Module.Import = &.{
|
||||
.{ .name = "zfin", .module = mod },
|
||||
// Shared imports for the unified module (CLI + TUI + lib in one module).
|
||||
// Only external deps -- internal imports use file paths so that Zig's
|
||||
// test runner can discover tests across the entire source tree.
|
||||
const imports: []const std.Build.Module.Import = &.{
|
||||
.{ .name = "srf", .module = srf_mod },
|
||||
.{ .name = "vaxis", .module = vaxis_dep.module("vaxis") },
|
||||
.{ .name = "z2d", .module = z2d_dep.module("z2d") },
|
||||
};
|
||||
|
||||
const tui_mod = b.addModule("tui", .{
|
||||
.root_source_file = b.path("src/tui/main.zig"),
|
||||
.target = target,
|
||||
.imports = tui_imports,
|
||||
});
|
||||
|
||||
const cli_imports: []const std.Build.Module.Import = &.{
|
||||
.{ .name = "zfin", .module = mod },
|
||||
.{ .name = "srf", .module = srf_mod },
|
||||
.{ .name = "tui", .module = tui_mod },
|
||||
};
|
||||
|
||||
// Unified executable (CLI + TUI in one binary)
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "zfin",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/cli/main.zig"),
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = cli_imports,
|
||||
.imports = imports,
|
||||
}),
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
|
@ -73,30 +63,18 @@ pub fn build(b: *std.Build) void {
|
|||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// Tests: Zig test discovery doesn't cross module boundaries, so each
|
||||
// module (lib, TUI, CLI) needs its own test target.
|
||||
// Tests: single binary, single module. refAllDeclsRecursive in
|
||||
// main.zig discovers all tests via file imports.
|
||||
const test_step = b.step("test", "Run all tests");
|
||||
|
||||
const mod_tests = b.addTest(.{ .root_module = mod });
|
||||
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
|
||||
|
||||
const tui_tests = b.addTest(.{ .root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/tui/main.zig"),
|
||||
const tests = b.addTest(.{ .root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = tui_imports,
|
||||
.imports = imports,
|
||||
}) });
|
||||
test_step.dependOn(&b.addRunArtifact(tui_tests).step);
|
||||
test_step.dependOn(&b.addRunArtifact(tests).step);
|
||||
|
||||
const cli_tests = b.addTest(.{ .root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/cli/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = cli_imports,
|
||||
}) });
|
||||
test_step.dependOn(&b.addRunArtifact(cli_tests).step);
|
||||
|
||||
// Docs
|
||||
// Docs (still uses the library module for clean public API docs)
|
||||
const lib = b.addLibrary(.{
|
||||
.name = "zfin",
|
||||
.root_module = b.createModule(.{
|
||||
|
|
@ -117,19 +95,12 @@ pub fn build(b: *std.Build) void {
|
|||
|
||||
// Coverage: `zig build coverage` (uses kcov, Linux x86_64/aarch64 only)
|
||||
{
|
||||
const cov = Coverage.init(b);
|
||||
_ = cov.addModule(mod, "zfin-lib");
|
||||
var cov = Coverage.init(b);
|
||||
_ = cov.addModule(b.createModule(.{
|
||||
.root_source_file = b.path("src/tui/main.zig"),
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = tui_imports,
|
||||
}), "zfin-tui");
|
||||
_ = cov.addModule(b.createModule(.{
|
||||
.root_source_file = b.path("src/cli/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = cli_imports,
|
||||
}), "zfin-cli");
|
||||
.imports = imports,
|
||||
}), "zfin");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ const Coverage = @This();
|
|||
///
|
||||
/// Use `zig build coverage --verbose` to see per-file coverage breakdown.
|
||||
///
|
||||
/// Call `addModule()` on the returned value to add test modules to the coverage run.
|
||||
/// Each module gets its own kcov invocation, threshold check, and output subdirectory.
|
||||
/// Call `addModule()` on the returned value to add the test module to the
|
||||
/// coverage run.
|
||||
///
|
||||
/// Because addModule creates a new test executable from the root module provided,
|
||||
/// if there are any linking steps being done to your test executable, those
|
||||
|
|
@ -79,10 +79,12 @@ pub fn init(b: *Build) Coverage {
|
|||
};
|
||||
}
|
||||
|
||||
/// Add a test module to the coverage run. Each module gets its own kcov
|
||||
/// invocation and threshold check, all wired into the shared "coverage" step.
|
||||
/// Add a test module to the coverage run. Runs kcov on the test binary,
|
||||
/// then reads the coverage JSON and prints a summary (with per-file
|
||||
/// breakdown if --verbose). Fails if below -Dcoverage-threshold.
|
||||
///
|
||||
/// Returns the test executable so the caller can add any extra linking steps.
|
||||
pub fn addModule(self: Coverage, root_module: *Build.Module, name: []const u8) *Build.Step.Compile {
|
||||
pub fn addModule(self: *Coverage, root_module: *Build.Module, name: []const u8) *Build.Step.Compile {
|
||||
const b = self.b;
|
||||
|
||||
// Set up kcov run: filter to src/ only, use custom CSS for HTML report
|
||||
|
|
@ -105,11 +107,26 @@ pub fn addModule(self: Coverage, root_module: *Build.Module, name: []const u8) *
|
|||
run_coverage.step.dependOn(&test_exe.step);
|
||||
run_coverage.step.dependOn(&self.run_download.step);
|
||||
|
||||
// Wire up the threshold check step (reads coverage.json after kcov finishes)
|
||||
const json_path = b.fmt("{s}/{s}/coverage.json", .{ self.coverage_dir, name });
|
||||
const check_step = create(b, test_exe, json_path, self.coverage_threshold);
|
||||
check_step.step.dependOn(&run_coverage.step);
|
||||
self.coverage_step.dependOn(&check_step.step);
|
||||
// Wire up the threshold check step after kcov completes
|
||||
const check = b.allocator.create(Coverage) catch @panic("OOM");
|
||||
check.* = .{
|
||||
.b = b,
|
||||
.coverage_step = undefined,
|
||||
.coverage_dir = undefined,
|
||||
.coverage_threshold = undefined,
|
||||
.kcov_path = undefined,
|
||||
.run_download = undefined,
|
||||
.step = Build.Step.init(.{
|
||||
.id = .custom,
|
||||
.name = "check coverage",
|
||||
.owner = b,
|
||||
.makeFn = make,
|
||||
}),
|
||||
.json_path = b.fmt("{s}/{s}/coverage.json", .{ self.coverage_dir, name }),
|
||||
.threshold = self.coverage_threshold,
|
||||
};
|
||||
check.step.dependOn(&run_coverage.step);
|
||||
self.coverage_step.dependOn(&check.step);
|
||||
|
||||
return test_exe;
|
||||
}
|
||||
|
|
@ -124,47 +141,20 @@ coverage_threshold: u7,
|
|||
kcov_path: []const u8,
|
||||
run_download: *Build.Step.Run,
|
||||
|
||||
// Fields used by make() for per-module threshold checking.
|
||||
// These are only meaningful on instances created by create(), not by init().
|
||||
// Fields used by make() for the threshold check (set by addModule)
|
||||
step: Build.Step = undefined,
|
||||
json_path: []const u8 = "",
|
||||
threshold: u7 = 0,
|
||||
test_exe: *Build.Step.Compile = undefined,
|
||||
|
||||
/// Create a coverage check step that reads the kcov JSON output after
|
||||
/// the coverage run completes and verifies the threshold is met.
|
||||
fn create(owner: *Build, test_exe: *Build.Step.Compile, json_path: []const u8, threshold: u7) *Coverage {
|
||||
const check = owner.allocator.create(Coverage) catch @panic("OOM");
|
||||
check.* = .{
|
||||
.b = owner,
|
||||
.coverage_step = undefined,
|
||||
.coverage_dir = "",
|
||||
.coverage_threshold = 0,
|
||||
.kcov_path = "",
|
||||
.run_download = undefined,
|
||||
.step = Build.Step.init(.{
|
||||
.id = .custom,
|
||||
.name = "check coverage",
|
||||
.owner = owner,
|
||||
.makeFn = make,
|
||||
}),
|
||||
.json_path = json_path,
|
||||
.threshold = threshold,
|
||||
.test_exe = test_exe,
|
||||
};
|
||||
return check;
|
||||
}
|
||||
|
||||
// This must be kept in step with kcov coverage.json format
|
||||
// This must be kept in step with kcov per-binary coverage.json format
|
||||
const CoverageReport = struct {
|
||||
percent_covered: f64,
|
||||
files: []const CoverageFile,
|
||||
};
|
||||
|
||||
const CoverageFile = struct {
|
||||
file: []const u8,
|
||||
covered_lines: usize,
|
||||
total_lines: usize,
|
||||
percent_low: u7,
|
||||
percent_high: u7,
|
||||
command: []const u8,
|
||||
date: []const u8,
|
||||
files: []File,
|
||||
};
|
||||
|
||||
const File = struct {
|
||||
|
|
@ -173,48 +163,78 @@ const File = struct {
|
|||
covered_lines: usize,
|
||||
total_lines: usize,
|
||||
|
||||
pub fn coverageLessThanDesc(context: []File, lhs: File, rhs: File) bool {
|
||||
_ = context;
|
||||
pub fn coverageLessThanDesc(_: void, lhs: File, rhs: File) bool {
|
||||
return lhs.percent_covered > rhs.percent_covered;
|
||||
}
|
||||
};
|
||||
|
||||
/// Build step make function: reads the kcov coverage.json output,
|
||||
/// prints summary (and per-file breakdown if verbose), and fails
|
||||
/// the build if coverage is below the configured threshold.
|
||||
/// Build step make function: reads kcov JSON output, prints a summary
|
||||
/// (with per-file breakdown if verbose), and fails if below threshold.
|
||||
fn make(step: *Build.Step, options: Build.Step.MakeOptions) !void {
|
||||
_ = options;
|
||||
const check: *Coverage = @fieldParentPtr("step", step);
|
||||
const allocator = step.owner.allocator;
|
||||
|
||||
const file = try std.fs.cwd().openFile(check.json_path, .{});
|
||||
const file = std.fs.cwd().openFile(check.json_path, .{}) catch |err| {
|
||||
return step.fail("Failed to open coverage report {s}: {}", .{ check.json_path, err });
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
const content = try file.readToEndAlloc(allocator, 10 * 1024 * 1024);
|
||||
defer allocator.free(content);
|
||||
|
||||
const json = try std.json.parseFromSlice(CoverageReport, allocator, content, .{});
|
||||
const json = std.json.parseFromSlice(CoverageReport, allocator, content, .{
|
||||
.ignore_unknown_fields = true,
|
||||
}) catch |err| {
|
||||
return step.fail("Failed to parse coverage JSON: {}", .{err});
|
||||
};
|
||||
defer json.deinit();
|
||||
const coverage = json.value;
|
||||
|
||||
var total_covered: usize = 0;
|
||||
var total_lines: usize = 0;
|
||||
|
||||
var file_list = std.ArrayList(File).empty;
|
||||
defer file_list.deinit(allocator);
|
||||
|
||||
for (json.value.files) |f| {
|
||||
const pct: f64 = if (f.total_lines > 0)
|
||||
@as(f64, @floatFromInt(f.covered_lines)) / @as(f64, @floatFromInt(f.total_lines)) * 100.0
|
||||
else
|
||||
0;
|
||||
try file_list.append(allocator, .{
|
||||
.file = f.file,
|
||||
.covered_lines = f.covered_lines,
|
||||
.total_lines = f.total_lines,
|
||||
.percent_covered = pct,
|
||||
});
|
||||
total_covered += f.covered_lines;
|
||||
total_lines += f.total_lines;
|
||||
}
|
||||
|
||||
std.mem.sort(File, file_list.items, {}, File.coverageLessThanDesc);
|
||||
|
||||
var stdout_buffer: [1024]u8 = undefined;
|
||||
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
|
||||
const stdout = &stdout_writer.interface;
|
||||
if (step.owner.verbose) {
|
||||
const files = coverage.files;
|
||||
std.mem.sort(File, files, files, File.coverageLessThanDesc);
|
||||
for (files) |f|
|
||||
for (file_list.items) |f| {
|
||||
try stdout.print(
|
||||
"{d: >5.1}% {d: >5}/{d: <5}:{s}\n",
|
||||
.{ f.percent_covered, f.covered_lines, f.total_lines, f.file },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const total_pct: f64 = if (total_lines > 0)
|
||||
@as(f64, @floatFromInt(total_covered)) / @as(f64, @floatFromInt(total_lines)) * 100.0
|
||||
else
|
||||
0;
|
||||
try stdout.print(
|
||||
"Total test coverage: {d}% ({d}/{d})\n",
|
||||
.{ coverage.percent_covered, coverage.covered_lines, coverage.total_lines },
|
||||
"Total test coverage: {d:.2}% ({d}/{d})\n",
|
||||
.{ total_pct, total_covered, total_lines },
|
||||
);
|
||||
try stdout.flush();
|
||||
|
||||
if (@as(u7, @intFromFloat(@floor(coverage.percent_covered))) < check.threshold)
|
||||
return step.fail("Coverage {d}% is below threshold {d}%", .{ coverage.percent_covered, check.threshold });
|
||||
if (@as(u7, @intFromFloat(@floor(total_pct))) < check.threshold)
|
||||
return step.fail("Coverage {d:.2}% is below threshold {d}%", .{ total_pct, check.threshold });
|
||||
}
|
||||
|
|
|
|||
220
src/cli/main.zig
220
src/cli/main.zig
|
|
@ -1,220 +0,0 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const fmt = zfin.format;
|
||||
const tui = @import("tui");
|
||||
const cli = @import("common.zig");
|
||||
|
||||
const usage =
|
||||
\\Usage: zfin <command> [options]
|
||||
\\
|
||||
\\Commands:
|
||||
\\ interactive [opts] Launch interactive TUI
|
||||
\\ perf <SYMBOL> Show 1yr/3yr/5yr/10yr trailing returns (Morningstar-style)
|
||||
\\ quote <SYMBOL> Show latest quote with chart and history
|
||||
\\ history <SYMBOL> Show recent price history
|
||||
\\ divs <SYMBOL> Show dividend history
|
||||
\\ splits <SYMBOL> Show split history
|
||||
\\ options <SYMBOL> Show options chain (all expirations)
|
||||
\\ earnings <SYMBOL> Show earnings history and upcoming
|
||||
\\ etf <SYMBOL> Show ETF profile (holdings, sectors, expense ratio)
|
||||
\\ portfolio [FILE] Load and analyze a portfolio (default: portfolio.srf)
|
||||
\\ analysis [FILE] Show portfolio analysis (default: portfolio.srf)
|
||||
\\ enrich <FILE|SYMBOL> Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)
|
||||
\\ lookup <CUSIP> Look up CUSIP to ticker via OpenFIGI
|
||||
\\ cache stats Show cache statistics
|
||||
\\ cache clear Clear all cached data
|
||||
\\
|
||||
\\Global options:
|
||||
\\ --no-color Disable colored output
|
||||
\\
|
||||
\\Interactive mode options:
|
||||
\\ -p, --portfolio <FILE> Portfolio file (.srf)
|
||||
\\ -w, --watchlist <FILE> Watchlist file (default: watchlist.srf)
|
||||
\\ -s, --symbol <SYMBOL> Initial symbol (default: VTI)
|
||||
\\ --chart <MODE> Chart graphics: auto, braille, or WxH (e.g. 1920x1080)
|
||||
\\ --default-keys Print default keybindings
|
||||
\\ --default-theme Print default theme
|
||||
\\
|
||||
\\Options command options:
|
||||
\\ --ntm <N> Show +/- N strikes near the money (default: 8)
|
||||
\\
|
||||
\\Portfolio command options:
|
||||
\\ If no file is given, defaults to portfolio.srf in the current directory.
|
||||
\\ -w, --watchlist <FILE> Watchlist file
|
||||
\\ --refresh Force refresh (ignore cache, re-fetch all prices)
|
||||
\\
|
||||
\\Analysis command:
|
||||
\\ Reads metadata.srf (classification) and accounts.srf (tax types)
|
||||
\\ from the same directory as the portfolio file.
|
||||
\\ If no file is given, defaults to portfolio.srf in the current directory.
|
||||
\\
|
||||
\\Environment Variables:
|
||||
\\ TWELVEDATA_API_KEY Twelve Data API key (primary: prices)
|
||||
\\ POLYGON_API_KEY Polygon.io API key (dividends, splits)
|
||||
\\ FINNHUB_API_KEY Finnhub API key (earnings)
|
||||
\\ ALPHAVANTAGE_API_KEY Alpha Vantage API key (ETF profiles)
|
||||
\\ OPENFIGI_API_KEY OpenFIGI API key (CUSIP lookup, optional)
|
||||
\\ ZFIN_CACHE_DIR Cache directory (default: ~/.cache/zfin)
|
||||
\\ NO_COLOR Disable colored output (https://no-color.org)
|
||||
\\
|
||||
;
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const args = try std.process.argsAlloc(allocator);
|
||||
defer std.process.argsFree(allocator, args);
|
||||
|
||||
// Single buffered writer for all stdout output
|
||||
var stdout_buf: [4096]u8 = undefined;
|
||||
var stdout_writer = std.fs.File.stdout().writer(&stdout_buf);
|
||||
const out: *std.Io.Writer = &stdout_writer.interface;
|
||||
|
||||
if (args.len < 2) {
|
||||
try out.writeAll(usage);
|
||||
try out.flush();
|
||||
return;
|
||||
}
|
||||
|
||||
// Scan for global --no-color flag
|
||||
var no_color_flag = false;
|
||||
for (args[1..]) |arg| {
|
||||
if (std.mem.eql(u8, arg, "--no-color")) no_color_flag = true;
|
||||
}
|
||||
const color = fmt.shouldUseColor(no_color_flag);
|
||||
|
||||
var config = zfin.Config.fromEnv(allocator);
|
||||
defer config.deinit();
|
||||
const command = args[1];
|
||||
|
||||
if (std.mem.eql(u8, command, "help") or std.mem.eql(u8, command, "--help") or std.mem.eql(u8, command, "-h")) {
|
||||
try out.writeAll(usage);
|
||||
try out.flush();
|
||||
return;
|
||||
}
|
||||
|
||||
// Interactive TUI -- delegates to the TUI module (owns its own DataService)
|
||||
if (std.mem.eql(u8, command, "interactive") or std.mem.eql(u8, command, "i")) {
|
||||
try out.flush();
|
||||
try tui.run(allocator, config, args);
|
||||
return;
|
||||
}
|
||||
|
||||
var svc = zfin.DataService.init(allocator, config);
|
||||
defer svc.deinit();
|
||||
|
||||
if (std.mem.eql(u8, command, "perf")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'perf' requires a symbol argument\n");
|
||||
try commands.perf.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "quote")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'quote' requires a symbol argument\n");
|
||||
try commands.quote.run(allocator, config, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "history")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'history' requires a symbol argument\n");
|
||||
try commands.history.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "divs")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'divs' requires a symbol argument\n");
|
||||
try commands.divs.run(allocator, &svc, config, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "splits")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'splits' requires a symbol argument\n");
|
||||
try commands.splits.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "options")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'options' requires a symbol argument\n");
|
||||
// Parse --ntm flag
|
||||
var ntm: usize = 8;
|
||||
var ai: usize = 3;
|
||||
while (ai < args.len) : (ai += 1) {
|
||||
if (std.mem.eql(u8, args[ai], "--ntm") and ai + 1 < args.len) {
|
||||
ai += 1;
|
||||
ntm = std.fmt.parseInt(usize, args[ai], 10) catch 8;
|
||||
}
|
||||
}
|
||||
try commands.options.run(allocator, &svc, args[2], ntm, color, out);
|
||||
} else if (std.mem.eql(u8, command, "earnings")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'earnings' requires a symbol argument\n");
|
||||
try commands.earnings.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "etf")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'etf' requires a symbol argument\n");
|
||||
try commands.etf.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "portfolio")) {
|
||||
// Parse -w/--watchlist and --refresh flags; file path is first non-flag arg (default: portfolio.srf)
|
||||
var watchlist_path: ?[]const u8 = null;
|
||||
var force_refresh = false;
|
||||
var file_path: []const u8 = "portfolio.srf";
|
||||
var pi: usize = 2;
|
||||
while (pi < args.len) : (pi += 1) {
|
||||
if ((std.mem.eql(u8, args[pi], "--watchlist") or std.mem.eql(u8, args[pi], "-w")) and pi + 1 < args.len) {
|
||||
pi += 1;
|
||||
watchlist_path = args[pi];
|
||||
} else if (std.mem.eql(u8, args[pi], "--refresh")) {
|
||||
force_refresh = true;
|
||||
} else if (std.mem.eql(u8, args[pi], "--no-color")) {
|
||||
// already handled globally
|
||||
} else {
|
||||
file_path = args[pi];
|
||||
}
|
||||
}
|
||||
try commands.portfolio.run(allocator, config, &svc, file_path, watchlist_path, force_refresh, color, out);
|
||||
} else if (std.mem.eql(u8, command, "lookup")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'lookup' requires a CUSIP argument\n");
|
||||
try commands.lookup.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "cache")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'cache' requires a subcommand (stats, clear)\n");
|
||||
try commands.cache.run(allocator, config, args[2], out);
|
||||
} else if (std.mem.eql(u8, command, "enrich")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'enrich' requires a portfolio file path or symbol\n");
|
||||
try commands.enrich.run(allocator, config, args[2], out);
|
||||
} else if (std.mem.eql(u8, command, "analysis")) {
|
||||
// File path is first non-flag arg (default: portfolio.srf)
|
||||
var analysis_file: []const u8 = "portfolio.srf";
|
||||
for (args[2..]) |arg| {
|
||||
if (!std.mem.startsWith(u8, arg, "--")) {
|
||||
analysis_file = arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
try commands.analysis.run(allocator, config, &svc, analysis_file, color, out);
|
||||
} else {
|
||||
try cli.stderrPrint("Unknown command. Run 'zfin help' for usage.\n");
|
||||
}
|
||||
|
||||
// Single flush for all stdout output
|
||||
try out.flush();
|
||||
}
|
||||
|
||||
// ── Command modules ──────────────────────────────────────────
|
||||
const commands = struct {
|
||||
const perf = @import("commands/perf.zig");
|
||||
const quote = @import("commands/quote.zig");
|
||||
const history = @import("commands/history.zig");
|
||||
const divs = @import("commands/divs.zig");
|
||||
const splits = @import("commands/splits.zig");
|
||||
const options = @import("commands/options.zig");
|
||||
const earnings = @import("commands/earnings.zig");
|
||||
const etf = @import("commands/etf.zig");
|
||||
const portfolio = @import("commands/portfolio.zig");
|
||||
const lookup = @import("commands/lookup.zig");
|
||||
const cache = @import("commands/cache.zig");
|
||||
const analysis = @import("commands/analysis.zig");
|
||||
const enrich = @import("commands/enrich.zig");
|
||||
};
|
||||
|
||||
// Ensure test runner discovers tests in all imported modules
|
||||
comptime {
|
||||
_ = cli;
|
||||
_ = commands.perf;
|
||||
_ = commands.quote;
|
||||
_ = commands.history;
|
||||
_ = commands.divs;
|
||||
_ = commands.splits;
|
||||
_ = commands.options;
|
||||
_ = commands.earnings;
|
||||
_ = commands.etf;
|
||||
_ = commands.portfolio;
|
||||
_ = commands.lookup;
|
||||
_ = commands.cache;
|
||||
_ = commands.analysis;
|
||||
_ = commands.enrich;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
/// CLI `analysis` command: show portfolio analysis breakdowns.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, config: zfin.Config, subcommand: []const u8, out: *std.Io.Writer) !void {
|
||||
if (std.mem.eql(u8, subcommand, "stats")) {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const zfin = @import("../root.zig");
|
||||
pub const fmt = zfin.format;
|
||||
|
||||
// ── Default CLI colors (match TUI default Monokai theme) ─────
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, config: zfin.Config, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
|
||||
/// CLI `enrich` command: bootstrap a metadata.srf file from Alpha Vantage OVERVIEW data.
|
||||
/// Reads the portfolio, extracts stock symbols, fetches sector/industry/country for each,
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
if (!zfin.OpenFigi.isCusipLike(cusip)) {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, ntm: usize, color: bool, out: *std.Io.Writer) !void {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, file_path: []const u8, watchlist_path: ?[]const u8, force_refresh: bool, color: bool, out: *std.Io.Writer) !void {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
/// Quote data extracted from the real-time API (or synthesized from candles).
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const cli = @import("../common.zig");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
221
src/main.zig
221
src/main.zig
|
|
@ -1,27 +1,208 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("zfin");
|
||||
const zfin = @import("root.zig");
|
||||
const fmt = zfin.format;
|
||||
const tui = @import("tui.zig");
|
||||
const cli = @import("commands/common.zig");
|
||||
|
||||
const usage =
|
||||
\\Usage: zfin <command> [options]
|
||||
\\
|
||||
\\Commands:
|
||||
\\ interactive [opts] Launch interactive TUI
|
||||
\\ perf <SYMBOL> Show 1yr/3yr/5yr/10yr trailing returns (Morningstar-style)
|
||||
\\ quote <SYMBOL> Show latest quote with chart and history
|
||||
\\ history <SYMBOL> Show recent price history
|
||||
\\ divs <SYMBOL> Show dividend history
|
||||
\\ splits <SYMBOL> Show split history
|
||||
\\ options <SYMBOL> Show options chain (all expirations)
|
||||
\\ earnings <SYMBOL> Show earnings history and upcoming
|
||||
\\ etf <SYMBOL> Show ETF profile (holdings, sectors, expense ratio)
|
||||
\\ portfolio [FILE] Load and analyze a portfolio (default: portfolio.srf)
|
||||
\\ analysis [FILE] Show portfolio analysis (default: portfolio.srf)
|
||||
\\ enrich <FILE|SYMBOL> Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)
|
||||
\\ lookup <CUSIP> Look up CUSIP to ticker via OpenFIGI
|
||||
\\ cache stats Show cache statistics
|
||||
\\ cache clear Clear all cached data
|
||||
\\
|
||||
\\Global options:
|
||||
\\ --no-color Disable colored output
|
||||
\\
|
||||
\\Interactive mode options:
|
||||
\\ -p, --portfolio <FILE> Portfolio file (.srf)
|
||||
\\ -w, --watchlist <FILE> Watchlist file (default: watchlist.srf)
|
||||
\\ -s, --symbol <SYMBOL> Initial symbol (default: VTI)
|
||||
\\ --chart <MODE> Chart graphics: auto, braille, or WxH (e.g. 1920x1080)
|
||||
\\ --default-keys Print default keybindings
|
||||
\\ --default-theme Print default theme
|
||||
\\
|
||||
\\Options command options:
|
||||
\\ --ntm <N> Show +/- N strikes near the money (default: 8)
|
||||
\\
|
||||
\\Portfolio command options:
|
||||
\\ If no file is given, defaults to portfolio.srf in the current directory.
|
||||
\\ -w, --watchlist <FILE> Watchlist file
|
||||
\\ --refresh Force refresh (ignore cache, re-fetch all prices)
|
||||
\\
|
||||
\\Analysis command:
|
||||
\\ Reads metadata.srf (classification) and accounts.srf (tax types)
|
||||
\\ from the same directory as the portfolio file.
|
||||
\\ If no file is given, defaults to portfolio.srf in the current directory.
|
||||
\\
|
||||
\\Environment Variables:
|
||||
\\ TWELVEDATA_API_KEY Twelve Data API key (primary: prices)
|
||||
\\ POLYGON_API_KEY Polygon.io API key (dividends, splits)
|
||||
\\ FINNHUB_API_KEY Finnhub API key (earnings)
|
||||
\\ ALPHAVANTAGE_API_KEY Alpha Vantage API key (ETF profiles)
|
||||
\\ OPENFIGI_API_KEY OpenFIGI API key (CUSIP lookup, optional)
|
||||
\\ ZFIN_CACHE_DIR Cache directory (default: ~/.cache/zfin)
|
||||
\\ NO_COLOR Disable colored output (https://no-color.org)
|
||||
\\
|
||||
;
|
||||
|
||||
pub fn main() !void {
|
||||
// Prints to stderr, ignoring potential errors.
|
||||
std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
|
||||
try zfin.bufferedPrint();
|
||||
}
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
test "simple test" {
|
||||
const gpa = std.testing.allocator;
|
||||
var list: std.ArrayList(i32) = .empty;
|
||||
defer list.deinit(gpa); // Try commenting this out and see if zig detects the memory leak!
|
||||
try list.append(gpa, 42);
|
||||
try std.testing.expectEqual(@as(i32, 42), list.pop());
|
||||
}
|
||||
const args = try std.process.argsAlloc(allocator);
|
||||
defer std.process.argsFree(allocator, args);
|
||||
|
||||
test "fuzz example" {
|
||||
const Context = struct {
|
||||
fn testOne(context: @This(), input: []const u8) anyerror!void {
|
||||
_ = context;
|
||||
// Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
|
||||
try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input));
|
||||
// Single buffered writer for all stdout output
|
||||
var stdout_buf: [4096]u8 = undefined;
|
||||
var stdout_writer = std.fs.File.stdout().writer(&stdout_buf);
|
||||
const out: *std.Io.Writer = &stdout_writer.interface;
|
||||
|
||||
if (args.len < 2) {
|
||||
try out.writeAll(usage);
|
||||
try out.flush();
|
||||
return;
|
||||
}
|
||||
};
|
||||
try std.testing.fuzz(Context{}, Context.testOne, .{});
|
||||
|
||||
// Scan for global --no-color flag
|
||||
var no_color_flag = false;
|
||||
for (args[1..]) |arg| {
|
||||
if (std.mem.eql(u8, arg, "--no-color")) no_color_flag = true;
|
||||
}
|
||||
const color = fmt.shouldUseColor(no_color_flag);
|
||||
|
||||
var config = zfin.Config.fromEnv(allocator);
|
||||
defer config.deinit();
|
||||
const command = args[1];
|
||||
|
||||
if (std.mem.eql(u8, command, "help") or std.mem.eql(u8, command, "--help") or std.mem.eql(u8, command, "-h")) {
|
||||
try out.writeAll(usage);
|
||||
try out.flush();
|
||||
return;
|
||||
}
|
||||
|
||||
// Interactive TUI -- delegates to the TUI module (owns its own DataService)
|
||||
if (std.mem.eql(u8, command, "interactive") or std.mem.eql(u8, command, "i")) {
|
||||
try out.flush();
|
||||
try tui.run(allocator, config, args);
|
||||
return;
|
||||
}
|
||||
|
||||
var svc = zfin.DataService.init(allocator, config);
|
||||
defer svc.deinit();
|
||||
|
||||
if (std.mem.eql(u8, command, "perf")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'perf' requires a symbol argument\n");
|
||||
try commands.perf.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "quote")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'quote' requires a symbol argument\n");
|
||||
try commands.quote.run(allocator, config, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "history")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'history' requires a symbol argument\n");
|
||||
try commands.history.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "divs")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'divs' requires a symbol argument\n");
|
||||
try commands.divs.run(allocator, &svc, config, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "splits")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'splits' requires a symbol argument\n");
|
||||
try commands.splits.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "options")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'options' requires a symbol argument\n");
|
||||
// Parse --ntm flag
|
||||
var ntm: usize = 8;
|
||||
var ai: usize = 3;
|
||||
while (ai < args.len) : (ai += 1) {
|
||||
if (std.mem.eql(u8, args[ai], "--ntm") and ai + 1 < args.len) {
|
||||
ai += 1;
|
||||
ntm = std.fmt.parseInt(usize, args[ai], 10) catch 8;
|
||||
}
|
||||
}
|
||||
try commands.options.run(allocator, &svc, args[2], ntm, color, out);
|
||||
} else if (std.mem.eql(u8, command, "earnings")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'earnings' requires a symbol argument\n");
|
||||
try commands.earnings.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "etf")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'etf' requires a symbol argument\n");
|
||||
try commands.etf.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "portfolio")) {
|
||||
// Parse -w/--watchlist and --refresh flags; file path is first non-flag arg (default: portfolio.srf)
|
||||
var watchlist_path: ?[]const u8 = null;
|
||||
var force_refresh = false;
|
||||
var file_path: []const u8 = "portfolio.srf";
|
||||
var pi: usize = 2;
|
||||
while (pi < args.len) : (pi += 1) {
|
||||
if ((std.mem.eql(u8, args[pi], "--watchlist") or std.mem.eql(u8, args[pi], "-w")) and pi + 1 < args.len) {
|
||||
pi += 1;
|
||||
watchlist_path = args[pi];
|
||||
} else if (std.mem.eql(u8, args[pi], "--refresh")) {
|
||||
force_refresh = true;
|
||||
} else if (std.mem.eql(u8, args[pi], "--no-color")) {
|
||||
// already handled globally
|
||||
} else {
|
||||
file_path = args[pi];
|
||||
}
|
||||
}
|
||||
try commands.portfolio.run(allocator, config, &svc, file_path, watchlist_path, force_refresh, color, out);
|
||||
} else if (std.mem.eql(u8, command, "lookup")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'lookup' requires a CUSIP argument\n");
|
||||
try commands.lookup.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "cache")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'cache' requires a subcommand (stats, clear)\n");
|
||||
try commands.cache.run(allocator, config, args[2], out);
|
||||
} else if (std.mem.eql(u8, command, "enrich")) {
|
||||
if (args.len < 3) return try cli.stderrPrint("Error: 'enrich' requires a portfolio file path or symbol\n");
|
||||
try commands.enrich.run(allocator, config, args[2], out);
|
||||
} else if (std.mem.eql(u8, command, "analysis")) {
|
||||
// File path is first non-flag arg (default: portfolio.srf)
|
||||
var analysis_file: []const u8 = "portfolio.srf";
|
||||
for (args[2..]) |arg| {
|
||||
if (!std.mem.startsWith(u8, arg, "--")) {
|
||||
analysis_file = arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
try commands.analysis.run(allocator, config, &svc, analysis_file, color, out);
|
||||
} else {
|
||||
try cli.stderrPrint("Unknown command. Run 'zfin help' for usage.\n");
|
||||
}
|
||||
|
||||
// Single flush for all stdout output
|
||||
try out.flush();
|
||||
}
|
||||
|
||||
// ── Command modules ──────────────────────────────────────────
|
||||
const commands = struct {
|
||||
const perf = @import("commands/perf.zig");
|
||||
const quote = @import("commands/quote.zig");
|
||||
const history = @import("commands/history.zig");
|
||||
const divs = @import("commands/divs.zig");
|
||||
const splits = @import("commands/splits.zig");
|
||||
const options = @import("commands/options.zig");
|
||||
const earnings = @import("commands/earnings.zig");
|
||||
const etf = @import("commands/etf.zig");
|
||||
const portfolio = @import("commands/portfolio.zig");
|
||||
const lookup = @import("commands/lookup.zig");
|
||||
const cache = @import("commands/cache.zig");
|
||||
const analysis = @import("commands/analysis.zig");
|
||||
const enrich = @import("commands/enrich.zig");
|
||||
};
|
||||
|
||||
// Single test binary: all source is in one module (file imports, no module
|
||||
// boundaries), so refAllDeclsRecursive discovers every test in the tree.
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,9 +62,3 @@ pub const OpenFigi = @import("providers/openfigi.zig");
|
|||
|
||||
// -- Re-export SRF for portfolio file loading --
|
||||
pub const srf = @import("srf");
|
||||
|
||||
// -- Tests --
|
||||
test {
|
||||
const std = @import("std");
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
const std = @import("std");
|
||||
const vaxis = @import("vaxis");
|
||||
const zfin = @import("zfin");
|
||||
const zfin = @import("root.zig");
|
||||
const fmt = zfin.format;
|
||||
const keybinds = @import("keybinds.zig");
|
||||
const theme_mod = @import("theme.zig");
|
||||
const chart_mod = @import("chart.zig");
|
||||
const keybinds = @import("tui/keybinds.zig");
|
||||
const theme_mod = @import("tui/theme.zig");
|
||||
const chart_mod = @import("tui/chart.zig");
|
||||
|
||||
/// Comptime-generated table of single-character grapheme slices with static lifetime.
|
||||
/// This avoids dangling pointers from stack-allocated temporaries in draw functions.
|
||||
|
|
@ -384,14 +384,7 @@ const App = struct {
|
|||
const sw = fmt.sym_col_width;
|
||||
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)
|
||||
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) {
|
||||
|
|
@ -1277,7 +1270,10 @@ const App = struct {
|
|||
// Check if any lots are DRIP
|
||||
var has_drip = false;
|
||||
for (matching.items) |lot| {
|
||||
if (lot.drip) { has_drip = true; break; }
|
||||
if (lot.drip) {
|
||||
has_drip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_drip) {
|
||||
|
|
@ -3252,7 +3248,6 @@ const App = struct {
|
|||
// Track header line count for mouse click mapping (after all non-data lines)
|
||||
self.options_header_lines = lines.items.len;
|
||||
|
||||
|
||||
// Flat list of options rows with inline expand/collapse
|
||||
for (self.options_rows.items, 0..) |row, ri| {
|
||||
const is_cursor = ri == self.options_cursor;
|
||||
|
|
@ -3630,17 +3625,14 @@ const App = struct {
|
|||
const action_labels = [_][]const u8{
|
||||
"Quit", "Refresh", "Previous tab", "Next tab",
|
||||
"Tab 1", "Tab 2", "Tab 3", "Tab 4",
|
||||
"Tab 5", "Tab 6", "Scroll down", "Scroll up", "Scroll to top",
|
||||
"Scroll to bottom", "Page down", "Page up", "Select next",
|
||||
"Select prev", "Expand/collapse", "Select symbol", "Change symbol (search)",
|
||||
"This help", "Edit portfolio/watchlist",
|
||||
"Reload portfolio from disk",
|
||||
"Toggle all calls (options)", "Toggle all puts (options)",
|
||||
"Filter +/- 1 NTM", "Filter +/- 2 NTM", "Filter +/- 3 NTM",
|
||||
"Filter +/- 4 NTM", "Filter +/- 5 NTM", "Filter +/- 6 NTM",
|
||||
"Filter +/- 7 NTM", "Filter +/- 8 NTM", "Filter +/- 9 NTM",
|
||||
"Chart: next timeframe", "Chart: prev timeframe",
|
||||
"Sort: next column", "Sort: prev column", "Sort: reverse order",
|
||||
"Tab 5", "Tab 6", "Scroll down", "Scroll up",
|
||||
"Scroll to top", "Scroll to bottom", "Page down", "Page up",
|
||||
"Select next", "Select prev", "Expand/collapse", "Select symbol",
|
||||
"Change symbol (search)", "This help", "Edit portfolio/watchlist", "Reload portfolio from disk",
|
||||
"Toggle all calls (options)", "Toggle all puts (options)", "Filter +/- 1 NTM", "Filter +/- 2 NTM",
|
||||
"Filter +/- 3 NTM", "Filter +/- 4 NTM", "Filter +/- 5 NTM", "Filter +/- 6 NTM",
|
||||
"Filter +/- 7 NTM", "Filter +/- 8 NTM", "Filter +/- 9 NTM", "Chart: next timeframe",
|
||||
"Chart: prev timeframe", "Sort: next column", "Sort: prev column", "Sort: reverse order",
|
||||
};
|
||||
|
||||
for (actions, 0..) |action, ai| {
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
const std = @import("std");
|
||||
const z2d = @import("z2d");
|
||||
const zfin = @import("zfin");
|
||||
const zfin = @import("../root.zig");
|
||||
const theme_mod = @import("theme.zig");
|
||||
|
||||
const Surface = z2d.Surface;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue