scripts/neocities-push-progress.lua

109 lines

1#!/usr/bin/env luajit
2-- {{{ neocities-push-progress.lua
3-- Live per-directory progress for `neocities push`.
4--
5-- For the general reader: the Neocities upload client prints one line per file as
6-- it goes -- e.g. "Uploading similar-different/different/4902-01.html ... SUCCESS".
7-- During a deploy that is thousands of lines scrolling by, impossible to read. This
8-- filter consumes that stream and instead draws ONE progress bar per top-level
9-- directory under similar-different/ (chronological, similar, different, wordcloud,
10-- media, source, gallery, and "other" for loose root files), each filling as its
11-- section uploads. It is a VIEWER only -- it changes nothing, just reformats the
12-- push output. deploy-to-neocities pipes push through it when writing to a terminal.
13--
14-- Usage (driven by deploy-to-neocities):
15-- neocities push ... 2>&1 | luajit neocities-push-progress.lua <bucket:total> ...
16-- Each argument is a directory name and how many files it holds, e.g. different:7904
17-- -- the totals are the bar denominators, computed once from the staged copy.
18-- }}}
19
20-- {{{ parse the bucket:total arguments (their order is the display order)
21local order, total, done = {}, {}, {}
22for _, a in ipairs(arg) do
23 local name, n = a:match("^(.-):(%d+)$")
24 if name and name ~= "" then
25 order[#order + 1] = name
26 total[name] = tonumber(n)
27 done[name] = 0
28 end
29end
30local known = {}
31for _, n in ipairs(order) do known[n] = true end
32if #order == 0 then
33 -- Nothing to chart (no buckets passed): act as a pass-through so output is not lost.
34 for line in io.lines() do io.write(line, "\n") end
35 return
36end
37-- }}}
38
39local BAR_W = 28
40local failures = {}
41local first = true
42
43-- {{{ local function totals()
44-- Sum uploaded / total across all buckets, for the header line.
45local function totals()
46 local td, tt = 0, 0
47 for _, n in ipairs(order) do td = td + done[n]; tt = tt + total[n] end
48 return td, tt
49end
50-- }}}
51
52-- {{{ local function format_bar(name)
53local function format_bar(name)
54 local t, d = total[name] or 0, done[name] or 0
55 local frac = t > 0 and d / t or 0
56 if frac > 1 then frac = 1 end -- never overflow if remote held extras
57 local filled = math.floor(frac * BAR_W + 0.5)
58 local bar = string.rep("█", filled) .. string.rep("░", BAR_W - filled)
59 return string.format(" %-13s %s %6d/%-6d %3d%%",
60 name, bar, d, t, math.floor(frac * 100 + 0.5))
61end
62-- }}}
63
64-- {{{ local function render()
65-- Redraw the header + every bar IN PLACE: move the cursor up over the block drawn
66-- last frame, then rewrite each line (clearing it first). Assumes a terminal; the
67-- caller only pipes us here when stdout is a TTY.
68local function render()
69 local nlines = #order + 1
70 if not first then io.write(string.format("\27[%dA", nlines)) end
71 first = false
72 local td, tt = totals()
73 io.write("\27[2K", string.format("Deploying to similar-different/ %d/%d files\n", td, tt))
74 for _, n in ipairs(order) do
75 io.write("\27[2K", format_bar(n), "\n")
76 end
77 io.flush()
78end
79-- }}}
80
81render() -- initial frame, so later cursor-up has a block to return to
82
83-- {{{ consume the push stream, one line per uploaded file
84for line in io.lines() do
85 -- "Uploading <path> ... <STATUS>" -- pull out the path, bucket by its first dir.
86 local path = line:match("^Uploading%s+(.-)%s+%.%.%.")
87 if path then
88 local rel = path:gsub("^similar%-different/", "")
89 local bucket = rel:match("^([^/]+)/") or "other"
90 if not known[bucket] then bucket = "other" end -- fold surprises into other
91 if done[bucket] ~= nil then done[bucket] = done[bucket] + 1 end
92 if line:find("FAIL") or line:find("ERROR") then failures[#failures + 1] = line end
93 render()
94 end
95 -- Non-"Uploading" lines (banners, blank lines) are not counted; genuine errors
96 -- are captured above and the push exit code still propagates through the pipe.
97end
98-- }}}
99
100-- {{{ final summary (below the finished bars)
101io.write("\n")
102local td, tt = totals()
103io.write(string.format("Done: uploaded %d of %d staged files to similar-different/.\n", td, tt))
104if #failures > 0 then
105 io.write(string.format("%d failure(s):\n", #failures))
106 for _, f in ipairs(failures) do io.write(" ", f, "\n") end
107end
108-- }}}
109