libs/progress-display.lua
1-- {{{ progress-display.lua
2-- Pure-Lua mirror of the C progress renderer (libs/vulkan-compute/src/vk_compute.c
3-- vkc_progress_*). Lua stages that have no reason to load the Vulkan shared
4-- library -- HTML generation, word pages -- use this so their progress bar
5-- looks identical to the GPU stages and obeys the same rules:
6--
7-- * VKC_DEBUG set (run.sh --debug) -> verbose: one plain, newline-terminated
8-- line per update, so a redirected log keeps the full history of a run.
9-- * else stdout is a TTY -> animated: updates overwrite one line with a "\r"
10-- Unicode bar (█ done, ░ pending).
11-- * else (piped to a file / cron, no debug) -> quiet: nothing is drawn.
12--
13-- The C version is the source of truth for the look; this is kept byte-for-byte
14-- compatible (same bar width, same glyphs, same "label [bar] cur/total (pct%)
15-- suffix" layout) so a reader cannot tell which stage drew a given bar.
16local ffi = require("ffi")
17-- isatty lives in libc; pcall guards the (impossible-in-practice) case where
18-- the symbol is unavailable, so a missing isatty degrades to "not a TTY"
19-- (quiet) rather than erroring a whole HTML run over a progress bar.
20pcall(ffi.cdef, "int isatty(int fd);")
21
22local M = {}
23
24local BAR_WIDTH = 40
25local MODE_QUIET, MODE_BAR, MODE_VERBOSE = 0, 1, 2
26
27-- {{{ local function resolve_mode()
28-- Resolved once and memoised: neither stdout's TTY-ness nor VKC_DEBUG changes
29-- during a run. Mirrors vkc_progress_mode()'s ordering -- debug is checked
30-- BEFORE isatty, so --debug through a pipe still yields verbose lines (the
31-- whole point of --debug) instead of falling through to quiet.
32local cached_mode = nil
33local function resolve_mode()
34 if cached_mode ~= nil then return cached_mode end
35 local debug_flag = os.getenv("VKC_DEBUG")
36 if debug_flag and debug_flag ~= "" then
37 cached_mode = MODE_VERBOSE
38 else
39 local is_tty = false
40 local ok, result = pcall(function() return ffi.C.isatty(1) end)
41 if ok and result ~= 0 then is_tty = true end
42 cached_mode = is_tty and MODE_BAR or MODE_QUIET
43 end
44 return cached_mode
45end
46-- }}}
47
48-- {{{ function M.mode()
49-- Exposes the resolved mode (0 quiet / 1 bar / 2 verbose) so callers can
50-- throttle: animate every step in bar mode, but emit sparse lines when verbose.
51function M.mode()
52 return resolve_mode()
53end
54-- }}}
55
56-- {{{ function M.update(label, current, total, suffix)
57-- Draw one progress frame. suffix is optional extra text (e.g. rate / ETA)
58-- appended after the percentage. Cheap to call; in bar mode call as often as
59-- you like, in verbose mode throttle to keep the log readable.
60function M.update(label, current, total, suffix)
61 local mode = resolve_mode()
62 if mode == MODE_QUIET then return end
63
64 local frac = (total > 0) and (current / total) or 1.0
65 if frac > 1.0 then frac = 1.0 end -- callers may overshoot
66 local pct = frac * 100
67 local tail = suffix and (" " .. suffix) or ""
68
69 if mode == MODE_VERBOSE then
70 io.write(string.format("%s %d/%d (%.0f%%)%s\n", label, current, total, pct, tail))
71 io.flush()
72 return
73 end
74
75 -- Animated bar. Trailing spaces clear any leftover tail from a previously
76 -- longer suffix (ETA strings shrink as a run finishes).
77 local filled = math.floor(frac * BAR_WIDTH)
78 local bar = string.rep("█", filled) .. string.rep("░", BAR_WIDTH - filled)
79 io.write(string.format("\r%s [%s] %d/%d (%3.0f%%)%s ",
80 label, bar, current, total, pct, tail))
81 io.flush()
82end
83-- }}}
84
85-- {{{ function M.finish()
86-- Close an animated line with a newline. No-op in verbose/quiet modes (they
87-- never left the cursor mid-line).
88function M.finish()
89 if resolve_mode() == MODE_BAR then
90 io.write("\n")
91 io.flush()
92 end
93end
94-- }}}
95
96return M
97-- }}}
98