| author | |
| committer | |
| log | a109b1aeeb0f22f9c20f5c5a3312ca0d810e0d86 |
| tree | 41636b28fb41f7996b2857ad592021bfcab7218d |
| parent | 3054df53b55f49b1421c222591bbd4d7f7e935c0 |
| signature | Commit is signed but in an unrecognized format. |
19 files changed, 513 insertions(+), 506 deletions(-)
framework/bundle.ts+1-1| ... | @@ -401,5 +401,5 @@ import * as sg from "#sitegen"; | ... | @@ -401,5 +401,5 @@ import * as sg from "#sitegen"; |
| 401 | import type { PageOrView } from "./generate.ts"; | 401 | import type { PageOrView } from "./generate.ts"; |
| 402 | import type * as view from "#sitegen/view"; | 402 | import type * as view from "#sitegen/view"; |
| 403 | import * as meta from "lib/render/meta.ts"; | 403 | import * as meta from "lib/render/meta.ts"; |
| 404 | import * as progress from "lib/log/progress.ts"; | 404 | import * as progress from "lib/progress.ts"; |
| 405 | import { ASSERT, UNWRAP } from "lib/assert.ts"; | 405 | import { ASSERT, UNWRAP } from "lib/assert.ts"; |
framework/font.ts+1-1| ... | @@ -216,5 +216,5 @@ import * as incr from "./incremental.ts"; | ... | @@ -216,5 +216,5 @@ import * as incr from "./incremental.ts"; |
| 216 | import * as crypto from "node:crypto"; | 216 | import * as crypto from "node:crypto"; |
| 217 | import * as async from "lib/async.ts"; | 217 | import * as async from "lib/async.ts"; |
| 218 | import * as error from "lib/error.ts"; | 218 | import * as error from "lib/error.ts"; |
| 219 | import * as progress from "../lib/log/progress.ts"; | 219 | import * as progress from "../lib/progress.ts"; |
| 220 | import { ASSERT } from "lib/assert.ts"; | 220 | import { ASSERT } from "lib/assert.ts"; |
framework/generate.ts+1-1| ... | @@ -401,7 +401,7 @@ import * as meta from "lib/render/meta.ts"; | ... | @@ -401,7 +401,7 @@ import * as meta from "lib/render/meta.ts"; |
| 401 | import * as render from "lib/render.ts"; | 401 | import * as render from "lib/render.ts"; |
| 402 | import * as sg from "#sitegen"; | 402 | import * as sg from "#sitegen"; |
| 403 | import type { FileItem } from "#sitegen"; | 403 | import type { FileItem } from "#sitegen"; |
| 404 | import * as progress from "../lib/log/progress.ts"; | 404 | import * as progress from "../lib/progress.ts"; |
| 405 | 405 | ||
| 406 | import * as bundle from "./bundle.ts"; | 406 | import * as bundle from "./bundle.ts"; |
| 407 | import * as css from "./css.ts"; | 407 | import * as css from "./css.ts"; |
framework/incremental.ts+1-1| ... | @@ -736,7 +736,7 @@ import * as util from "node:util"; | ... | @@ -736,7 +736,7 @@ import * as util from "node:util"; |
| 736 | import * as crypto from "node:crypto"; | 736 | import * as crypto from "node:crypto"; |
| 737 | import * as zlib from "node:zlib"; | 737 | import * as zlib from "node:zlib"; |
| 738 | 738 | ||
| 739 | import * as progress from "lib/log/progress.ts"; | 739 | import * as progress from "lib/progress.ts"; |
| 740 | import * as stack from "lib/log/stack.ts"; | 740 | import * as stack from "lib/log/stack.ts"; |
| 741 | import { formatSize } from "@/file-viewer/format.ts"; | 741 | import { formatSize } from "@/file-viewer/format.ts"; |
| 742 | 742 |
lib/deno.json+4-3| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | { | 1 | { |
| 2 | "name": "@clo/lib", | 2 | "name": "@clo/lib", |
| 3 | "description": "high quality typescript libraries", | 3 | "description": "high quality typescript libraries", |
| 4 | "version": "2.0.0", | 4 | "version": "3.0.0", |
| 5 | "license": "MIT", | 5 | "license": "MIT", |
| 6 | "exports": { | 6 | "exports": { |
| 7 | "./assert.ts": "./assert.ts", | 7 | "./assert.ts": "./assert.ts", |
| ... | @@ -9,11 +9,11 @@ | ... | @@ -9,11 +9,11 @@ |
| 9 | "./error.ts": "./error.ts", | 9 | "./error.ts": "./error.ts", |
| 10 | "./log.ts": "./log.ts", | 10 | "./log.ts": "./log.ts", |
| 11 | "./log/headless.ts": "./log/headless.ts", | 11 | "./log/headless.ts": "./log/headless.ts", |
| 12 | "./log/progress.ts": "./log/progress.ts", | ||
| 13 | "./log/stack.ts": "./log/stack.ts", | 12 | "./log/stack.ts": "./log/stack.ts", |
| 14 | "./Lru.ts": "./Lru.ts", | 13 | "./Lru.ts": "./Lru.ts", |
| 15 | "./mime.ts": "./mime.ts", | 14 | "./mime.ts": "./mime.ts", |
| 16 | "./node.ts": "./node.ts", | 15 | "./node.ts": "./node.ts", |
| 16 | "./progress.ts": "./progress.ts", | ||
| 17 | "./queue.ts": "./queue.ts", | 17 | "./queue.ts": "./queue.ts", |
| 18 | "./render.ts": "./render.ts", | 18 | "./render.ts": "./render.ts", |
| 19 | "./render/jsx-runtime.ts": "./render/jsx-runtime.ts", | 19 | "./render/jsx-runtime.ts": "./render/jsx-runtime.ts", |
| ... | @@ -37,4 +37,5 @@ | ... | @@ -37,4 +37,5 @@ |
| 37 | "readme.npm.md", | 37 | "readme.npm.md", |
| 38 | "deno.lock" | 38 | "deno.lock" |
| 39 | ] | 39 | ] |
| 40 | } | ||
| \ No newline at end of file | |||
| 40 | } | ||
| 41 | |||
lib/log.ts+1-1| ... | @@ -1,6 +1,6 @@ | ... | @@ -1,6 +1,6 @@ |
| 1 | /** | 1 | /** |
| 2 | * By using `lib/log.ts`, your application gets easy scoped logging as well as | 2 | * By using `lib/log.ts`, your application gets easy scoped logging as well as |
| 3 | * integration with terminal widgets such as `lib/log/progress.ts`. | 3 | * integration with terminal widgets such as `lib/progress.ts`. |
| 4 | * | 4 | * |
| 5 | * The pattern for using this module is to shadow the global `console` with a | 5 | * The pattern for using this module is to shadow the global `console` with a |
| 6 | * local scope: | 6 | * local scope: |
lib/log/progress.sample.ts deleted-73| ... | @@ -1,73 +0,0 @@ | ||
| 1 | const root = progress.start("progress node", { estimate: 30 }); | ||
| 2 | for (let i = 0; i < 10; i += 1) { | ||
| 3 | { | ||
| 4 | const subNodes = Array.from( | ||
| 5 | { length: 4 }, | ||
| 6 | (_) => root.start("subtask " + random()), | ||
| 7 | ); | ||
| 8 | await delay(); | ||
| 9 | subNodes[0]!.end(); | ||
| 10 | const n = subNodes[1]!.start("meowing", { estimate: 7 }); | ||
| 11 | const m = subNodes[1]!.start("purring", { estimate: 7 }); | ||
| 12 | let w: progress.Node | null = null; | ||
| 13 | let x: progress.Node | null = null; | ||
| 14 | let y: progress.Node | null = null; | ||
| 15 | let z: progress.Node | null = null; | ||
| 16 | for (let i = 0; i < 14; i += 1) { | ||
| 17 | if (Math.random() > 0.5 && n.value < n.estimate) { | ||
| 18 | n.inc(); | ||
| 19 | } else { | ||
| 20 | m.inc(); | ||
| 21 | } | ||
| 22 | await short(); | ||
| 23 | if (i === 5) subNodes[3]!.end(); | ||
| 24 | if (i === 3) { | ||
| 25 | x = subNodes[2]!.start("other task"); | ||
| 26 | y = x.start("deeply nested"); | ||
| 27 | z = y.start("job"); | ||
| 28 | z.log.info("with inline logs"); | ||
| 29 | z.log.warn("with inline logs"); | ||
| 30 | } | ||
| 31 | if (i == 8) z?.end(); | ||
| 32 | if (i === 6) w = y!.start("magic"); | ||
| 33 | if (i == 12) y!.end(); | ||
| 34 | if (i === 10) subNodes[3]!.end(); | ||
| 35 | } | ||
| 36 | await delay(); | ||
| 37 | n.inc(); | ||
| 38 | await delay(); | ||
| 39 | n.end(); | ||
| 40 | root.inc(); | ||
| 41 | subNodes.forEach((x) => x.end()); | ||
| 42 | } | ||
| 43 | { | ||
| 44 | const subNodes = Array.from( | ||
| 45 | { length: 8 }, | ||
| 46 | (_) => root.start("subtask with count " + random()), | ||
| 47 | ); | ||
| 48 | subNodes.forEach((x) => x.value = 1); | ||
| 49 | await delay(); | ||
| 50 | subNodes[0]!.end(); | ||
| 51 | for (let i = 0; i < 80; i += 1) { | ||
| 52 | const node = subNodes[Math.floor(Math.random() * 8)]!; | ||
| 53 | node.inc(); | ||
| 54 | if (node.value > 10) node.end(); | ||
| 55 | await short(); | ||
| 56 | } | ||
| 57 | await delay(); | ||
| 58 | subNodes.forEach((x) => x.end()); | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | async function delay() { | ||
| 63 | await timers.setTimeout(Math.random() * 100 + 200); | ||
| 64 | } | ||
| 65 | async function short() { | ||
| 66 | await timers.setTimeout(Math.random() * 50 + 50); | ||
| 67 | } | ||
| 68 | function random() { | ||
| 69 | return Math.floor(Math.random() * 9999999).toString(32); | ||
| 70 | } | ||
| 71 | |||
| 72 | import * as progress from "lib/log/progress.ts"; | ||
| 73 | import * as timers from "node:timers/promises"; | ||
lib/log/progress.ts deleted-416| ... | @@ -1,416 +0,0 @@ | ||
| 1 | // This file is under heavy construction. | ||
| 2 | // - I am happy with the overall design | ||
| 3 | // - Will rename things | ||
| 4 | // - Will be re-working the entire module to have a headless | ||
| 5 | // core, but still leaving the top level `start` function. | ||
| 6 | // - There are an overwhelming amount of bugs | ||
| 7 | // | ||
| 8 | // a progress tree based on my past uses with clover console v3 and the Zig | ||
| 9 | // `Progress` API. Pass `Ref` to functions which should track progress, calling | ||
| 10 | // `.start()` to create child items. Or, multiple top-level progress bars can | ||
| 11 | // be created with the singleton's `start()` function. | ||
| 12 | |||
| 13 | /** | ||
| 14 | * creates a new trackable unit of work as a child of this one. | ||
| 15 | * when given an estimate, a progress bar is rendered. | ||
| 16 | */ | ||
| 17 | export function start(text: string, opts?: StartOptions): Node { | ||
| 18 | return global.start(text, opts); | ||
| 19 | } | ||
| 20 | |||
| 21 | export interface Ref { | ||
| 22 | /** | ||
| 23 | * creates a new trackable unit of work as a child of this one. | ||
| 24 | * when given an estimate, a progress bar is rendered. | ||
| 25 | */ | ||
| 26 | start(text: string, opts?: StartOptions): Node; | ||
| 27 | } | ||
| 28 | |||
| 29 | export interface Node extends Ref, Disposable { | ||
| 30 | /** reactive */ | ||
| 31 | text: string; | ||
| 32 | /** reactive */ | ||
| 33 | value: number; | ||
| 34 | /** reactive */ | ||
| 35 | estimate: number; | ||
| 36 | /** | ||
| 37 | * reactive, default `true`. `false` will hides a bar + printing estimate, | ||
| 38 | * but preseving auto-end behavior of `inc` | ||
| 39 | */ | ||
| 40 | showEstimate: boolean; | ||
| 41 | /** | ||
| 42 | * reactive, default `false`. when true, this progress item is hidden if | ||
| 43 | * there are no children. | ||
| 44 | */ | ||
| 45 | passive: boolean; | ||
| 46 | /** | ||
| 47 | * reactive, default `false`. when true, this progress item is hidden | ||
| 48 | */ | ||
| 49 | hidden: boolean; | ||
| 50 | /** reactive. null means decide based on nested depth + terminal height */ | ||
| 51 | maxHeight: null | number; | ||
| 52 | /** reactive. specify sorting for children */ | ||
| 53 | sortChildren: ((a: SortNode, b: SortNode) => number) | null; | ||
| 54 | |||
| 55 | /** defaults to increasing by 1. hitting estimate calls end */ | ||
| 56 | inc(value?: number): void; | ||
| 57 | |||
| 58 | /** A scoped logger set to output inline to this item. */ | ||
| 59 | log: log.Scope; | ||
| 60 | |||
| 61 | /** end this Node or RootNode. ending collapses children */ | ||
| 62 | end(): void; | ||
| 63 | } | ||
| 64 | |||
| 65 | export interface StartOptions { | ||
| 66 | value?: number | undefined | null; | ||
| 67 | estimate?: number | undefined | null; | ||
| 68 | passive?: boolean | undefined | null; | ||
| 69 | hidden?: boolean | undefined | null; | ||
| 70 | showEstimate?: boolean | undefined | null; | ||
| 71 | maxHeight?: number | undefined | null; | ||
| 72 | sortChildren?: Node["sortChildren"] | undefined | null; | ||
| 73 | } | ||
| 74 | |||
| 75 | /** a valid `Node` without any output */ | ||
| 76 | const nullNode: Node = /* @__PURE__ */ | ||
| 77 | (function start(text: string, opts?: StartOptions): Node { | ||
| 78 | const node = newNode(text, opts)[1]; | ||
| 79 | node.start = start; | ||
| 80 | node.log = log.scoped(""); | ||
| 81 | return node; | ||
| 82 | })("root"); | ||
| 83 | |||
| 84 | export { nullNode as null }; | ||
| 85 | |||
| 86 | export type SortNode = | ||
| 87 | & Pick< | ||
| 88 | Node, | ||
| 89 | | "text" | ||
| 90 | | "value" | ||
| 91 | | "estimate" | ||
| 92 | | "passive" | ||
| 93 | | "hidden" | ||
| 94 | | "showEstimate" | ||
| 95 | | "sortChildren" | ||
| 96 | > | ||
| 97 | & { children: SortNode[] }; | ||
| 98 | |||
| 99 | export interface Internal extends SortNode { | ||
| 100 | logs: string[]; | ||
| 101 | children: Internal[]; | ||
| 102 | parent: Internal | null; | ||
| 103 | signal(msg: Signal): void; | ||
| 104 | } | ||
| 105 | |||
| 106 | type Signal = | ||
| 107 | | "draw" | ||
| 108 | | { type: "end"; title: string; logs: string[]; state: Internal } | ||
| 109 | | { type: "line"; line: string }; | ||
| 110 | |||
| 111 | function newNode( | ||
| 112 | text: string, | ||
| 113 | opts: StartOptions = {}, | ||
| 114 | ): [Internal, Node] { | ||
| 115 | const state: Internal = { | ||
| 116 | text, | ||
| 117 | value: opts.value ?? 0, | ||
| 118 | estimate: opts.estimate ?? 0, | ||
| 119 | showEstimate: opts.showEstimate ?? true, | ||
| 120 | passive: opts.passive ?? false, | ||
| 121 | hidden: opts.hidden ?? false, | ||
| 122 | logs: [], | ||
| 123 | children: [], | ||
| 124 | parent: null, | ||
| 125 | sortChildren: opts.sortChildren ?? null, | ||
| 126 | signal() { | ||
| 127 | throw new Error("no signaler"); | ||
| 128 | }, | ||
| 129 | }; | ||
| 130 | let detached = false; | ||
| 131 | function end() { | ||
| 132 | if (detached) return; | ||
| 133 | let title = state.text; | ||
| 134 | if (state.parent) { | ||
| 135 | const i = state.parent.children.indexOf(state); | ||
| 136 | ASSERT(i !== -1); | ||
| 137 | let p: Internal | null = state; | ||
| 138 | while (p = p.parent) title = p.text + " / " + title; | ||
| 139 | state.parent.children.splice(i, 1); | ||
| 140 | state.parent = null; | ||
| 141 | detached = true; | ||
| 142 | } | ||
| 143 | state.signal({ type: "end", title, logs: state.logs, state }); | ||
| 144 | state.logs = []; | ||
| 145 | } | ||
| 146 | function mutate() { | ||
| 147 | const { parent } = state; | ||
| 148 | parent?.sortChildren && parent.children.sort(parent.sortChildren); | ||
| 149 | state.signal("draw"); | ||
| 150 | } | ||
| 151 | const scope = headless.logger({ | ||
| 152 | log: { | ||
| 153 | writeLine(line) { | ||
| 154 | if (detached) return state.signal({ type: "line", line }); | ||
| 155 | state.logs.push(...line.split("\n")); | ||
| 156 | state.signal("draw"); | ||
| 157 | }, | ||
| 158 | }, | ||
| 159 | colors: process.stderr.isTTY, | ||
| 160 | }); | ||
| 161 | const binding: Node = { | ||
| 162 | start(text, opts) { | ||
| 163 | const [child, node] = newNode(text, opts); | ||
| 164 | state.children.push(child); | ||
| 165 | state.sortChildren && state.children.sort(state.sortChildren); | ||
| 166 | child.parent = state; | ||
| 167 | child.signal = state.signal; | ||
| 168 | state.signal("draw"); | ||
| 169 | return node; | ||
| 170 | }, | ||
| 171 | get text() { | ||
| 172 | return state.text; | ||
| 173 | }, | ||
| 174 | set text(value) { | ||
| 175 | state.text = value; | ||
| 176 | mutate(); | ||
| 177 | }, | ||
| 178 | get value() { | ||
| 179 | return state.value; | ||
| 180 | }, | ||
| 181 | set value(value) { | ||
| 182 | state.value = value; | ||
| 183 | mutate(); | ||
| 184 | }, | ||
| 185 | get estimate() { | ||
| 186 | return state.estimate; | ||
| 187 | }, | ||
| 188 | set estimate(value) { | ||
| 189 | state.estimate = value; | ||
| 190 | mutate(); | ||
| 191 | }, | ||
| 192 | get showEstimate() { | ||
| 193 | return state.showEstimate; | ||
| 194 | }, | ||
| 195 | set showEstimate(value) { | ||
| 196 | state.showEstimate = value; | ||
| 197 | mutate(); | ||
| 198 | }, | ||
| 199 | get passive() { | ||
| 200 | return state.passive; | ||
| 201 | }, | ||
| 202 | set passive(value) { | ||
| 203 | state.passive = value; | ||
| 204 | mutate(); | ||
| 205 | }, | ||
| 206 | get hidden() { | ||
| 207 | return state.hidden; | ||
| 208 | }, | ||
| 209 | set hidden(value) { | ||
| 210 | state.hidden = value; | ||
| 211 | mutate(); | ||
| 212 | }, | ||
| 213 | maxHeight: null, | ||
| 214 | // get maxHeight() { | ||
| 215 | // return state.maxHeight; | ||
| 216 | // }, | ||
| 217 | // set maxHeight(value) { | ||
| 218 | // state.maxHeight = value; | ||
| 219 | // mutate(); | ||
| 220 | // }, | ||
| 221 | get sortChildren() { | ||
| 222 | return state.sortChildren; | ||
| 223 | }, | ||
| 224 | set sortChildren(value) { | ||
| 225 | state.sortChildren = value; | ||
| 226 | value && state.children.sort(value); | ||
| 227 | state.signal("draw"); | ||
| 228 | }, | ||
| 229 | inc(delta = 1) { | ||
| 230 | state.value += delta; | ||
| 231 | if (state.estimate > 0 && state.value >= state.estimate) { | ||
| 232 | end(); | ||
| 233 | } else { | ||
| 234 | mutate(); | ||
| 235 | } | ||
| 236 | }, | ||
| 237 | log: scope, | ||
| 238 | end: () => void end(), | ||
| 239 | [Symbol.dispose]: () => void end(), | ||
| 240 | }; | ||
| 241 | return [state, binding]; | ||
| 242 | } | ||
| 243 | |||
| 244 | function formatProgress(now: number, list: Internal[]): string { | ||
| 245 | let out = ""; | ||
| 246 | for (const top of list) { | ||
| 247 | if (top.passive && !hasChildren(top)) continue; | ||
| 248 | out += renderMainLine(top, now, 0) + "\n"; | ||
| 249 | out += renderChildren(top, now, []); | ||
| 250 | } | ||
| 251 | return out.trimEnd(); | ||
| 252 | } | ||
| 253 | |||
| 254 | /** These functions are used to test `lib/log/progress` */ | ||
| 255 | export namespace internals { | ||
| 256 | export function node( | ||
| 257 | text: string, | ||
| 258 | props: Partial<Internal>, | ||
| 259 | children: Internal[], | ||
| 260 | ): Internal { | ||
| 261 | const [node] = newNode(text); | ||
| 262 | Object.assign(node, props); | ||
| 263 | node.children.push(...children); | ||
| 264 | return node; | ||
| 265 | } | ||
| 266 | export const format = formatProgress; | ||
| 267 | } | ||
| 268 | |||
| 269 | const spinnerFps = 12.5; | ||
| 270 | |||
| 271 | const box = { | ||
| 272 | tee: "├─ ", | ||
| 273 | line: "│ ", | ||
| 274 | langle: "└─ ", | ||
| 275 | }; | ||
| 276 | const barChars = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"]; | ||
| 277 | const fullBar = "█"; | ||
| 278 | const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] | ||
| 279 | .map((frame) => ansi.style(ansi.fgBlue, frame)); | ||
| 280 | |||
| 281 | function renderMainLine(state: Internal, now: number, depth: number) { | ||
| 282 | if (state.estimate && state.showEstimate) { | ||
| 283 | return ansi.style( | ||
| 284 | ansi.bgBrightBlack + ansi.fgBlue, | ||
| 285 | getUnicodeBar( | ||
| 286 | state.value / state.estimate, | ||
| 287 | Math.max(4, depth > 0 ? 12 : 25), | ||
| 288 | ), | ||
| 289 | ) + | ||
| 290 | ` [${state.value}/${state.estimate}] ` + state.text; | ||
| 291 | } | ||
| 292 | const frame = Math.floor(now / (1000 / spinnerFps)) % spinner.length; | ||
| 293 | return (depth === 0 ? spinner[frame] + " " : "") + | ||
| 294 | (state.value > 0 ? `[${state.value}] ` : "") + state.text; | ||
| 295 | } | ||
| 296 | |||
| 297 | function hasChildren(states: Internal): boolean { | ||
| 298 | return states.children.some((x) => | ||
| 299 | !x.hidden && (!x.passive || hasChildren(x)) | ||
| 300 | ); | ||
| 301 | } | ||
| 302 | |||
| 303 | function renderChildren(state: Internal, now: number, depth: boolean[]) { | ||
| 304 | let maxHeight = 50; // TODO: | ||
| 305 | let truncated = 0; | ||
| 306 | let out = ""; | ||
| 307 | let { children } = state; | ||
| 308 | children = children.filter((x) => | ||
| 309 | !x.hidden && (!x.passive || hasChildren(x)) | ||
| 310 | ); | ||
| 311 | if (children.length === 0) return ""; | ||
| 312 | for (let i = 0, { length } = children; i < length; i += 1) { | ||
| 313 | const child = UNWRAP(children[i]); | ||
| 314 | let item = ""; | ||
| 315 | const left = depth.map((x) => x ? box.line : " ").join(""); | ||
| 316 | item += left + (i === length - 1 && !truncated ? box.langle : box.tee); | ||
| 317 | item += renderMainLine(child, now, 1 + depth.length) + "\n"; | ||
| 318 | item += renderChildren(child, now, depth.concat(i < length - 1)); | ||
| 319 | const h = string.countNewlines(item); | ||
| 320 | if (h > maxHeight) { | ||
| 321 | truncated += 1; | ||
| 322 | continue; | ||
| 323 | } | ||
| 324 | const logLines = child?.logs.slice(-Math.min(3, maxHeight)) ?? []; | ||
| 325 | for (const line of logLines) { | ||
| 326 | item += left + box.line + " " + ansi.style(ansi.fgBrightBlack, ">") + | ||
| 327 | " " + line + "\n"; | ||
| 328 | } | ||
| 329 | maxHeight -= h + logLines.length; | ||
| 330 | out += item; | ||
| 331 | } | ||
| 332 | if (truncated) { | ||
| 333 | out += depth.map((x) => x ? box.line : " ").join("") + box.langle; | ||
| 334 | out += ansi.style(ansi.fgBrightBlack, `[${truncated} more]`); | ||
| 335 | } | ||
| 336 | return out; | ||
| 337 | } | ||
| 338 | |||
| 339 | /** | ||
| 340 | * This function is derived from an old program I wrote back in 2020 called `f` | ||
| 341 | * which did ffmpeg handling. It is probably one of the coolest progress bars | ||
| 342 | * ever imagined. | ||
| 343 | */ | ||
| 344 | export function getUnicodeBar(progress: number, width: number): string { | ||
| 345 | if (progress >= 1) return fullBar.repeat(width); | ||
| 346 | if (progress <= 0 || Number.isNaN(progress)) return " ".repeat(width); | ||
| 347 | |||
| 348 | const wholeWidth = Math.floor(progress * width); | ||
| 349 | const remainderWidth = (progress * width) % 1; | ||
| 350 | const partWidth = Math.floor(remainderWidth * 8); | ||
| 351 | let partChar = barChars[partWidth]; | ||
| 352 | if (width - wholeWidth - 1 < 0) partChar = ""; | ||
| 353 | |||
| 354 | const fill = fullBar.repeat(wholeWidth); | ||
| 355 | const empty = " ".repeat(width - wholeWidth - 1); | ||
| 356 | |||
| 357 | return fill + partChar + empty; | ||
| 358 | } | ||
| 359 | |||
| 360 | /** Run a progress tree without a dependency on a TTY */ | ||
| 361 | export class Headless { | ||
| 362 | constructor( | ||
| 363 | /** | ||
| 364 | * `startWidget` is only ever called from `start` | ||
| 365 | * `writeLine` is used for logs from completed nodes | ||
| 366 | */ | ||
| 367 | public env: Pick<headless.WidgetHost, "writeLine" | "startWidget">, | ||
| 368 | ) {} | ||
| 369 | |||
| 370 | active: Internal[] = []; | ||
| 371 | |||
| 372 | start(text: string, opts?: StartOptions): Node { | ||
| 373 | const [state, node] = newNode(text, opts); | ||
| 374 | this.active.push(state); | ||
| 375 | state.signal = this.#signal; | ||
| 376 | const rerender = this.#rerender; | ||
| 377 | if (rerender) rerender(); | ||
| 378 | else log.startWidget(this.widget); | ||
| 379 | return node; | ||
| 380 | } | ||
| 381 | |||
| 382 | widget: log.Widget = { | ||
| 383 | format: (now) => formatProgress(now, this.active), | ||
| 384 | onChange: ( | ||
| 385 | rerender, | ||
| 386 | ) => (this.#rerender = rerender, () => this.#rerender = null), | ||
| 387 | }; | ||
| 388 | |||
| 389 | #rerender?: (() => void) | null; | ||
| 390 | #signal = (msg: Signal) => { | ||
| 391 | if (msg === "draw") this.#rerender?.(); | ||
| 392 | else if (msg.type === "line") this.env.writeLine(msg.line); | ||
| 393 | else if (msg.type === "end") { | ||
| 394 | const indexOfActive = this.active.indexOf(msg.state); | ||
| 395 | if (indexOfActive !== -1) this.active.splice(indexOfActive, 1); | ||
| 396 | if (msg.logs.length === 0) { | ||
| 397 | this.#rerender?.(); | ||
| 398 | return; | ||
| 399 | } | ||
| 400 | const { logs } = msg; | ||
| 401 | const count = `${logs.length} line${logs.length === 1 ? "" : "s"}`; | ||
| 402 | const header = `[${count} from ${msg.title}]`; | ||
| 403 | this.env.writeLine(ansi.style(ansi.fgBrightBlack, header)); | ||
| 404 | this.env.writeLine(msg.logs.join("\n")); | ||
| 405 | } | ||
| 406 | }; | ||
| 407 | } | ||
| 408 | |||
| 409 | const global = /** @__PURE__ */ new Headless(log); | ||
| 410 | |||
| 411 | import * as process from "node:process"; | ||
| 412 | import * as log from "lib/log.ts"; | ||
| 413 | import * as headless from "lib/log/headless.ts"; | ||
| 414 | import * as ansi from "lib/string/ansi.ts"; | ||
| 415 | import * as string from "lib/string.ts"; | ||
| 416 | import { ASSERT, UNWRAP } from "lib/assert.ts"; | ||
lib/progress.sample.ts created+73| ... | @@ -0,0 +1,73 @@ | ||
| 1 | const root = progress.start("progress node", { estimate: 30 }); | ||
| 2 | for (let i = 0; i < 10; i += 1) { | ||
| 3 | { | ||
| 4 | const subNodes = Array.from( | ||
| 5 | { length: 4 }, | ||
| 6 | (_) => root.start("subtask " + random()), | ||
| 7 | ); | ||
| 8 | await delay(); | ||
| 9 | subNodes[0]!.end(); | ||
| 10 | const n = subNodes[1]!.start("meowing", { estimate: 7 }); | ||
| 11 | const m = subNodes[1]!.start("purring", { estimate: 7 }); | ||
| 12 | let w: progress.Node | null = null; | ||
| 13 | let x: progress.Node | null = null; | ||
| 14 | let y: progress.Node | null = null; | ||
| 15 | let z: progress.Node | null = null; | ||
| 16 | for (let i = 0; i < 14; i += 1) { | ||
| 17 | if (Math.random() > 0.5 && n.value < n.estimate) { | ||
| 18 | n.inc(); | ||
| 19 | } else { | ||
| 20 | m.inc(); | ||
| 21 | } | ||
| 22 | await short(); | ||
| 23 | if (i === 5) subNodes[3]!.end(); | ||
| 24 | if (i === 3) { | ||
| 25 | x = subNodes[2]!.start("other task"); | ||
| 26 | y = x.start("deeply nested"); | ||
| 27 | z = y.start("job"); | ||
| 28 | z.log.info("with inline logs"); | ||
| 29 | z.log.warn("with inline logs"); | ||
| 30 | } | ||
| 31 | if (i == 8) z?.end(); | ||
| 32 | if (i === 6) w = y!.start("magic"); | ||
| 33 | if (i == 12) y!.end(); | ||
| 34 | if (i === 10) subNodes[3]!.end(); | ||
| 35 | } | ||
| 36 | await delay(); | ||
| 37 | n.inc(); | ||
| 38 | await delay(); | ||
| 39 | n.end(); | ||
| 40 | root.inc(); | ||
| 41 | subNodes.forEach((x) => x.end()); | ||
| 42 | } | ||
| 43 | { | ||
| 44 | const subNodes = Array.from( | ||
| 45 | { length: 8 }, | ||
| 46 | (_) => root.start("subtask with count " + random()), | ||
| 47 | ); | ||
| 48 | subNodes.forEach((x) => x.value = 1); | ||
| 49 | await delay(); | ||
| 50 | subNodes[0]!.end(); | ||
| 51 | for (let i = 0; i < 80; i += 1) { | ||
| 52 | const node = subNodes[Math.floor(Math.random() * 8)]!; | ||
| 53 | node.inc(); | ||
| 54 | if (node.value > 10) node.end(); | ||
| 55 | await short(); | ||
| 56 | } | ||
| 57 | await delay(); | ||
| 58 | subNodes.forEach((x) => x.end()); | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | async function delay() { | ||
| 63 | await timers.setTimeout(Math.random() * 100 + 200); | ||
| 64 | } | ||
| 65 | async function short() { | ||
| 66 | await timers.setTimeout(Math.random() * 50 + 50); | ||
| 67 | } | ||
| 68 | function random() { | ||
| 69 | return Math.floor(Math.random() * 9999999).toString(32); | ||
| 70 | } | ||
| 71 | |||
| 72 | import * as progress from "lib/progress.ts"; | ||
| 73 | import * as timers from "node:timers/promises"; | ||
lib/progress.ts created+416| ... | @@ -0,0 +1,416 @@ | ||
| 1 | // This file is under heavy construction. | ||
| 2 | // - I am happy with the overall design | ||
| 3 | // - Will rename things | ||
| 4 | // - Will be re-working the entire module to have a headless | ||
| 5 | // core, but still leaving the top level `start` function. | ||
| 6 | // - There are an overwhelming amount of bugs | ||
| 7 | // | ||
| 8 | // a progress tree based on my past uses with clover console v3 and the Zig | ||
| 9 | // `Progress` API. Pass `Ref` to functions which should track progress, calling | ||
| 10 | // `.start()` to create child items. Or, multiple top-level progress bars can | ||
| 11 | // be created with the singleton's `start()` function. | ||
| 12 | |||
| 13 | /** | ||
| 14 | * creates a new trackable unit of work as a child of this one. | ||
| 15 | * when given an estimate, a progress bar is rendered. | ||
| 16 | */ | ||
| 17 | export function start(text: string, opts?: StartOptions): Node { | ||
| 18 | return global.start(text, opts); | ||
| 19 | } | ||
| 20 | |||
| 21 | export interface Ref { | ||
| 22 | /** | ||
| 23 | * creates a new trackable unit of work as a child of this one. | ||
| 24 | * when given an estimate, a progress bar is rendered. | ||
| 25 | */ | ||
| 26 | start(text: string, opts?: StartOptions): Node; | ||
| 27 | } | ||
| 28 | |||
| 29 | export interface Node extends Ref, Disposable { | ||
| 30 | /** reactive */ | ||
| 31 | text: string; | ||
| 32 | /** reactive */ | ||
| 33 | value: number; | ||
| 34 | /** reactive */ | ||
| 35 | estimate: number; | ||
| 36 | /** | ||
| 37 | * reactive, default `true`. `false` will hides a bar + printing estimate, | ||
| 38 | * but preseving auto-end behavior of `inc` | ||
| 39 | */ | ||
| 40 | showEstimate: boolean; | ||
| 41 | /** | ||
| 42 | * reactive, default `false`. when true, this progress item is hidden if | ||
| 43 | * there are no children. | ||
| 44 | */ | ||
| 45 | passive: boolean; | ||
| 46 | /** | ||
| 47 | * reactive, default `false`. when true, this progress item is hidden | ||
| 48 | */ | ||
| 49 | hidden: boolean; | ||
| 50 | /** reactive. null means decide based on nested depth + terminal height */ | ||
| 51 | maxHeight: null | number; | ||
| 52 | /** reactive. specify sorting for children */ | ||
| 53 | sortChildren: ((a: SortNode, b: SortNode) => number) | null; | ||
| 54 | |||
| 55 | /** defaults to increasing by 1. hitting estimate calls end */ | ||
| 56 | inc(value?: number): void; | ||
| 57 | |||
| 58 | /** A scoped logger set to output inline to this item. */ | ||
| 59 | log: log.Scope; | ||
| 60 | |||
| 61 | /** end this Node or RootNode. ending collapses children */ | ||
| 62 | end(): void; | ||
| 63 | } | ||
| 64 | |||
| 65 | export interface StartOptions { | ||
| 66 | value?: number | undefined | null; | ||
| 67 | estimate?: number | undefined | null; | ||
| 68 | passive?: boolean | undefined | null; | ||
| 69 | hidden?: boolean | undefined | null; | ||
| 70 | showEstimate?: boolean | undefined | null; | ||
| 71 | maxHeight?: number | undefined | null; | ||
| 72 | sortChildren?: Node["sortChildren"] | undefined | null; | ||
| 73 | } | ||
| 74 | |||
| 75 | /** a valid `Node` without any output */ | ||
| 76 | const nullNode: Node = /* @__PURE__ */ | ||
| 77 | (function start(text: string, opts?: StartOptions): Node { | ||
| 78 | const node = newNode(text, opts)[1]; | ||
| 79 | node.start = start; | ||
| 80 | node.log = log.scoped(""); | ||
| 81 | return node; | ||
| 82 | })("root"); | ||
| 83 | |||
| 84 | export { nullNode as null }; | ||
| 85 | |||
| 86 | export type SortNode = | ||
| 87 | & Pick< | ||
| 88 | Node, | ||
| 89 | | "text" | ||
| 90 | | "value" | ||
| 91 | | "estimate" | ||
| 92 | | "passive" | ||
| 93 | | "hidden" | ||
| 94 | | "showEstimate" | ||
| 95 | | "sortChildren" | ||
| 96 | > | ||
| 97 | & { children: SortNode[] }; | ||
| 98 | |||
| 99 | export interface Internal extends SortNode { | ||
| 100 | logs: string[]; | ||
| 101 | children: Internal[]; | ||
| 102 | parent: Internal | null; | ||
| 103 | signal(msg: Signal): void; | ||
| 104 | } | ||
| 105 | |||
| 106 | type Signal = | ||
| 107 | | "draw" | ||
| 108 | | { type: "end"; title: string; logs: string[]; state: Internal } | ||
| 109 | | { type: "line"; line: string }; | ||
| 110 | |||
| 111 | function newNode( | ||
| 112 | text: string, | ||
| 113 | opts: StartOptions = {}, | ||
| 114 | ): [Internal, Node] { | ||
| 115 | const state: Internal = { | ||
| 116 | text, | ||
| 117 | value: opts.value ?? 0, | ||
| 118 | estimate: opts.estimate ?? 0, | ||
| 119 | showEstimate: opts.showEstimate ?? true, | ||
| 120 | passive: opts.passive ?? false, | ||
| 121 | hidden: opts.hidden ?? false, | ||
| 122 | logs: [], | ||
| 123 | children: [], | ||
| 124 | parent: null, | ||
| 125 | sortChildren: opts.sortChildren ?? null, | ||
| 126 | signal() { | ||
| 127 | throw new Error("no signaler"); | ||
| 128 | }, | ||
| 129 | }; | ||
| 130 | let detached = false; | ||
| 131 | function end() { | ||
| 132 | if (detached) return; | ||
| 133 | let title = state.text; | ||
| 134 | if (state.parent) { | ||
| 135 | const i = state.parent.children.indexOf(state); | ||
| 136 | ASSERT(i !== -1); | ||
| 137 | let p: Internal | null = state; | ||
| 138 | while (p = p.parent) title = p.text + " / " + title; | ||
| 139 | state.parent.children.splice(i, 1); | ||
| 140 | state.parent = null; | ||
| 141 | detached = true; | ||
| 142 | } | ||
| 143 | state.signal({ type: "end", title, logs: state.logs, state }); | ||
| 144 | state.logs = []; | ||
| 145 | } | ||
| 146 | function mutate() { | ||
| 147 | const { parent } = state; | ||
| 148 | parent?.sortChildren && parent.children.sort(parent.sortChildren); | ||
| 149 | state.signal("draw"); | ||
| 150 | } | ||
| 151 | const scope = headless.logger({ | ||
| 152 | log: { | ||
| 153 | writeLine(line) { | ||
| 154 | if (detached) return state.signal({ type: "line", line }); | ||
| 155 | state.logs.push(...line.split("\n")); | ||
| 156 | state.signal("draw"); | ||
| 157 | }, | ||
| 158 | }, | ||
| 159 | colors: process.stderr.isTTY, | ||
| 160 | }); | ||
| 161 | const binding: Node = { | ||
| 162 | start(text, opts) { | ||
| 163 | const [child, node] = newNode(text, opts); | ||
| 164 | state.children.push(child); | ||
| 165 | state.sortChildren && state.children.sort(state.sortChildren); | ||
| 166 | child.parent = state; | ||
| 167 | child.signal = state.signal; | ||
| 168 | state.signal("draw"); | ||
| 169 | return node; | ||
| 170 | }, | ||
| 171 | get text() { | ||
| 172 | return state.text; | ||
| 173 | }, | ||
| 174 | set text(value) { | ||
| 175 | state.text = value; | ||
| 176 | mutate(); | ||
| 177 | }, | ||
| 178 | get value() { | ||
| 179 | return state.value; | ||
| 180 | }, | ||
| 181 | set value(value) { | ||
| 182 | state.value = value; | ||
| 183 | mutate(); | ||
| 184 | }, | ||
| 185 | get estimate() { | ||
| 186 | return state.estimate; | ||
| 187 | }, | ||
| 188 | set estimate(value) { | ||
| 189 | state.estimate = value; | ||
| 190 | mutate(); | ||
| 191 | }, | ||
| 192 | get showEstimate() { | ||
| 193 | return state.showEstimate; | ||
| 194 | }, | ||
| 195 | set showEstimate(value) { | ||
| 196 | state.showEstimate = value; | ||
| 197 | mutate(); | ||
| 198 | }, | ||
| 199 | get passive() { | ||
| 200 | return state.passive; | ||
| 201 | }, | ||
| 202 | set passive(value) { | ||
| 203 | state.passive = value; | ||
| 204 | mutate(); | ||
| 205 | }, | ||
| 206 | get hidden() { | ||
| 207 | return state.hidden; | ||
| 208 | }, | ||
| 209 | set hidden(value) { | ||
| 210 | state.hidden = value; | ||
| 211 | mutate(); | ||
| 212 | }, | ||
| 213 | maxHeight: null, | ||
| 214 | // get maxHeight() { | ||
| 215 | // return state.maxHeight; | ||
| 216 | // }, | ||
| 217 | // set maxHeight(value) { | ||
| 218 | // state.maxHeight = value; | ||
| 219 | // mutate(); | ||
| 220 | // }, | ||
| 221 | get sortChildren() { | ||
| 222 | return state.sortChildren; | ||
| 223 | }, | ||
| 224 | set sortChildren(value) { | ||
| 225 | state.sortChildren = value; | ||
| 226 | value && state.children.sort(value); | ||
| 227 | state.signal("draw"); | ||
| 228 | }, | ||
| 229 | inc(delta = 1) { | ||
| 230 | state.value += delta; | ||
| 231 | if (state.estimate > 0 && state.value >= state.estimate) { | ||
| 232 | end(); | ||
| 233 | } else { | ||
| 234 | mutate(); | ||
| 235 | } | ||
| 236 | }, | ||
| 237 | log: scope, | ||
| 238 | end: () => void end(), | ||
| 239 | [Symbol.dispose]: () => void end(), | ||
| 240 | }; | ||
| 241 | return [state, binding]; | ||
| 242 | } | ||
| 243 | |||
| 244 | function formatProgress(now: number, list: Internal[]): string { | ||
| 245 | let out = ""; | ||
| 246 | for (const top of list) { | ||
| 247 | if (top.passive && !hasChildren(top)) continue; | ||
| 248 | out += renderMainLine(top, now, 0) + "\n"; | ||
| 249 | out += renderChildren(top, now, []); | ||
| 250 | } | ||
| 251 | return out.trimEnd(); | ||
| 252 | } | ||
| 253 | |||
| 254 | /** These functions are used to test `lib/progress` */ | ||
| 255 | export namespace internals { | ||
| 256 | export function node( | ||
| 257 | text: string, | ||
| 258 | props: Partial<Internal>, | ||
| 259 | children: Internal[], | ||
| 260 | ): Internal { | ||
| 261 | const [node] = newNode(text); | ||
| 262 | Object.assign(node, props); | ||
| 263 | node.children.push(...children); | ||
| 264 | return node; | ||
| 265 | } | ||
| 266 | export const format = formatProgress; | ||
| 267 | } | ||
| 268 | |||
| 269 | const spinnerFps = 12.5; | ||
| 270 | |||
| 271 | const box = { | ||
| 272 | tee: "├─ ", | ||
| 273 | line: "│ ", | ||
| 274 | langle: "└─ ", | ||
| 275 | }; | ||
| 276 | const barChars = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"]; | ||
| 277 | const fullBar = "█"; | ||
| 278 | const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] | ||
| 279 | .map((frame) => ansi.style(ansi.fgBlue, frame)); | ||
| 280 | |||
| 281 | function renderMainLine(state: Internal, now: number, depth: number) { | ||
| 282 | if (state.estimate && state.showEstimate) { | ||
| 283 | return ansi.style( | ||
| 284 | ansi.bgBrightBlack + ansi.fgBlue, | ||
| 285 | getUnicodeBar( | ||
| 286 | state.value / state.estimate, | ||
| 287 | Math.max(4, depth > 0 ? 12 : 25), | ||
| 288 | ), | ||
| 289 | ) + | ||
| 290 | ` [${state.value}/${state.estimate}] ` + state.text; | ||
| 291 | } | ||
| 292 | const frame = Math.floor(now / (1000 / spinnerFps)) % spinner.length; | ||
| 293 | return (depth === 0 ? spinner[frame] + " " : "") + | ||
| 294 | (state.value > 0 ? `[${state.value}] ` : "") + state.text; | ||
| 295 | } | ||
| 296 | |||
| 297 | function hasChildren(states: Internal): boolean { | ||
| 298 | return states.children.some((x) => | ||
| 299 | !x.hidden && (!x.passive || hasChildren(x)) | ||
| 300 | ); | ||
| 301 | } | ||
| 302 | |||
| 303 | function renderChildren(state: Internal, now: number, depth: boolean[]) { | ||
| 304 | let maxHeight = 50; // TODO: | ||
| 305 | let truncated = 0; | ||
| 306 | let out = ""; | ||
| 307 | let { children } = state; | ||
| 308 | children = children.filter((x) => | ||
| 309 | !x.hidden && (!x.passive || hasChildren(x)) | ||
| 310 | ); | ||
| 311 | if (children.length === 0) return ""; | ||
| 312 | for (let i = 0, { length } = children; i < length; i += 1) { | ||
| 313 | const child = UNWRAP(children[i]); | ||
| 314 | let item = ""; | ||
| 315 | const left = depth.map((x) => x ? box.line : " ").join(""); | ||
| 316 | item += left + (i === length - 1 && !truncated ? box.langle : box.tee); | ||
| 317 | item += renderMainLine(child, now, 1 + depth.length) + "\n"; | ||
| 318 | item += renderChildren(child, now, depth.concat(i < length - 1)); | ||
| 319 | const h = string.countNewlines(item); | ||
| 320 | if (h > maxHeight) { | ||
| 321 | truncated += 1; | ||
| 322 | continue; | ||
| 323 | } | ||
| 324 | const logLines = child?.logs.slice(-Math.min(3, maxHeight)) ?? []; | ||
| 325 | for (const line of logLines) { | ||
| 326 | item += left + box.line + " " + ansi.style(ansi.fgBrightBlack, ">") + | ||
| 327 | " " + line + "\n"; | ||
| 328 | } | ||
| 329 | maxHeight -= h + logLines.length; | ||
| 330 | out += item; | ||
| 331 | } | ||
| 332 | if (truncated) { | ||
| 333 | out += depth.map((x) => x ? box.line : " ").join("") + box.langle; | ||
| 334 | out += ansi.style(ansi.fgBrightBlack, `[${truncated} more]`); | ||
| 335 | } | ||
| 336 | return out; | ||
| 337 | } | ||
| 338 | |||
| 339 | /** | ||
| 340 | * This function is derived from an old program I wrote back in 2020 called `f` | ||
| 341 | * which did ffmpeg handling. It is probably one of the coolest progress bars | ||
| 342 | * ever imagined. | ||
| 343 | */ | ||
| 344 | export function getUnicodeBar(progress: number, width: number): string { | ||
| 345 | if (progress >= 1) return fullBar.repeat(width); | ||
| 346 | if (progress <= 0 || Number.isNaN(progress)) return " ".repeat(width); | ||
| 347 | |||
| 348 | const wholeWidth = Math.floor(progress * width); | ||
| 349 | const remainderWidth = (progress * width) % 1; | ||
| 350 | const partWidth = Math.floor(remainderWidth * 8); | ||
| 351 | let partChar = barChars[partWidth]; | ||
| 352 | if (width - wholeWidth - 1 < 0) partChar = ""; | ||
| 353 | |||
| 354 | const fill = fullBar.repeat(wholeWidth); | ||
| 355 | const empty = " ".repeat(width - wholeWidth - 1); | ||
| 356 | |||
| 357 | return fill + partChar + empty; | ||
| 358 | } | ||
| 359 | |||
| 360 | /** Run a progress tree without a dependency on a TTY */ | ||
| 361 | export class Headless { | ||
| 362 | constructor( | ||
| 363 | /** | ||
| 364 | * `startWidget` is only ever called from `start` | ||
| 365 | * `writeLine` is used for logs from completed nodes | ||
| 366 | */ | ||
| 367 | public env: Pick<headless.WidgetHost, "writeLine" | "startWidget">, | ||
| 368 | ) {} | ||
| 369 | |||
| 370 | active: Internal[] = []; | ||
| 371 | |||
| 372 | start(text: string, opts?: StartOptions): Node { | ||
| 373 | const [state, node] = newNode(text, opts); | ||
| 374 | this.active.push(state); | ||
| 375 | state.signal = this.#signal; | ||
| 376 | const rerender = this.#rerender; | ||
| 377 | if (rerender) rerender(); | ||
| 378 | else log.startWidget(this.widget); | ||
| 379 | return node; | ||
| 380 | } | ||
| 381 | |||
| 382 | widget: log.Widget = { | ||
| 383 | format: (now) => formatProgress(now, this.active), | ||
| 384 | onChange: ( | ||
| 385 | rerender, | ||
| 386 | ) => (this.#rerender = rerender, () => this.#rerender = null), | ||
| 387 | }; | ||
| 388 | |||
| 389 | #rerender?: (() => void) | null; | ||
| 390 | #signal = (msg: Signal) => { | ||
| 391 | if (msg === "draw") this.#rerender?.(); | ||
| 392 | else if (msg.type === "line") this.env.writeLine(msg.line); | ||
| 393 | else if (msg.type === "end") { | ||
| 394 | const indexOfActive = this.active.indexOf(msg.state); | ||
| 395 | if (indexOfActive !== -1) this.active.splice(indexOfActive, 1); | ||
| 396 | if (msg.logs.length === 0) { | ||
| 397 | this.#rerender?.(); | ||
| 398 | return; | ||
| 399 | } | ||
| 400 | const { logs } = msg; | ||
| 401 | const count = `${logs.length} line${logs.length === 1 ? "" : "s"}`; | ||
| 402 | const header = `[${count} from ${msg.title}]`; | ||
| 403 | this.env.writeLine(ansi.style(ansi.fgBrightBlack, header)); | ||
| 404 | this.env.writeLine(msg.logs.join("\n")); | ||
| 405 | } | ||
| 406 | }; | ||
| 407 | } | ||
| 408 | |||
| 409 | const global = /** @__PURE__ */ new Headless(log); | ||
| 410 | |||
| 411 | import * as process from "node:process"; | ||
| 412 | import * as log from "lib/log.ts"; | ||
| 413 | import * as headless from "lib/log/headless.ts"; | ||
| 414 | import * as ansi from "lib/string/ansi.ts"; | ||
| 415 | import * as string from "lib/string.ts"; | ||
| 416 | import { ASSERT, UNWRAP } from "lib/assert.ts"; | ||
lib/readme.changes.md+6| ... | @@ -1,5 +1,11 @@ | ... | @@ -1,5 +1,11 @@ |
| 1 | # notable changes in clover's typescript library | 1 | # notable changes in clover's typescript library |
| 2 | 2 | ||
| 3 | ## v3 | ||
| 4 | |||
| 5 | ### breaking | ||
| 6 | |||
| 7 | - promote `log/progress.ts` to top level `progress.ts` | ||
| 8 | |||
| 3 | ## v2 | 9 | ## v2 |
| 4 | 10 | ||
| 5 | ### breaking | 11 | ### breaking |
lib/readme.md+1-1| ... | @@ -19,11 +19,11 @@ depend on each other. there are no external dependencies; code is easy to audit. | ... | @@ -19,11 +19,11 @@ depend on each other. there are no external dependencies; code is easy to audit. |
| 19 | - `error.ts` - interop with the `unknown` type in error handlers | 19 | - `error.ts` - interop with the `unknown` type in error handlers |
| 20 | - `log.ts` - logging, terminal i/o, interactive widgets | 20 | - `log.ts` - logging, terminal i/o, interactive widgets |
| 21 | - `log/headless.ts` - use `log` and `log/progress` without a tty | 21 | - `log/headless.ts` - use `log` and `log/progress` without a tty |
| 22 | - `log/progress.ts` - progress bar, spinner, and tree | ||
| 23 | - `log/stack.ts` - stack trace parse and formatter | 22 | - `log/stack.ts` - stack trace parse and formatter |
| 24 | - `Lru.ts` - least recently used cache | 23 | - `Lru.ts` - least recently used cache |
| 25 | - `mime.ts` - trimmed database of ext -> mime types | 24 | - `mime.ts` - trimmed database of ext -> mime types |
| 26 | - `node.ts` - the Node.js javascript runtime | 25 | - `node.ts` - the Node.js javascript runtime |
| 26 | - `progress.ts` - cli progress bars, nesting status indication | ||
| 27 | - `queue.ts` - priority queue with rescheduling | 27 | - `queue.ts` - priority queue with rescheduling |
| 28 | - `render.ts` - html generation with extensions. | 28 | - `render.ts` - html generation with extensions. |
| 29 | - `render/jsx-runtime.ts` - integration with JSX syntax | 29 | - `render/jsx-runtime.ts` - integration with JSX syntax |
lib/subprocess/ffmpeg.ts+2-2| ... | @@ -8,7 +8,7 @@ export interface SpawnOptions { | ... | @@ -8,7 +8,7 @@ export interface SpawnOptions { |
| 8 | /** | 8 | /** |
| 9 | * Spawn an instance of `ffmpeg` and bind it's live status to the given | 9 | * Spawn an instance of `ffmpeg` and bind it's live status to the given |
| 10 | * `progress.Node`. Logs are routed through the progress node, and dismissed on | 10 | * `progress.Node`. Logs are routed through the progress node, and dismissed on |
| 11 | * exit. `lib/log/progress.ts` will buffer and colocate the logs when running | 11 | * exit. `lib/progress.ts` will buffer and colocate the logs when running |
| 12 | * parallel jobs. | 12 | * parallel jobs. |
| 13 | * | 13 | * |
| 14 | * await ffmpeg.spawn({ | 14 | * await ffmpeg.spawn({ |
| ... | @@ -194,5 +194,5 @@ import * as readline from "node:readline"; | ... | @@ -194,5 +194,5 @@ import * as readline from "node:readline"; |
| 194 | import * as process from "node:process"; | 194 | import * as process from "node:process"; |
| 195 | import events from "node:events"; | 195 | import events from "node:events"; |
| 196 | import * as path from "node:path"; | 196 | import * as path from "node:path"; |
| 197 | import type * as progress from "lib/log/progress.ts"; | 197 | import type * as progress from "lib/progress.ts"; |
| 198 | import * as string from "lib/string.ts"; | 198 | import * as string from "lib/string.ts"; |
readme.md+1-1| ... | @@ -24,7 +24,7 @@ tools power <https://paperclover.net>. | ... | @@ -24,7 +24,7 @@ tools power <https://paperclover.net>. |
| 24 | - Dynamic pages. Static pages can be regenerated (and cached) at runtime. | 24 | - Dynamic pages. Static pages can be regenerated (and cached) at runtime. |
| 25 | - Font subsetting to reduce page bandwidth. | 25 | - Font subsetting to reduce page bandwidth. |
| 26 | - **General purpose typescript libraries.** (See [`./lib`](./lib/readme.md)) | 26 | - **General purpose typescript libraries.** (See [`./lib`](./lib/readme.md)) |
| 27 | - [Logging](./lib/log.ts) and [progress](./lib/log/progress.ts) indicators. | 27 | - [Logging](./lib/log.ts) and [progress](./lib/progress.ts) indicators. |
| 28 | - [Priority Queue](./lib/queue.ts) with automatic reschedulability. | 28 | - [Priority Queue](./lib/queue.ts) with automatic reschedulability. |
| 29 | - [Meta Tags](./framework/lib/meta.ts) and Open Graph generation. | 29 | - [Meta Tags](./framework/lib/meta.ts) and Open Graph generation. |
| 30 | - Databases with a minimal SQLite wrapper. | 30 | - Databases with a minimal SQLite wrapper. |
src/deploy.ts+1-1| ... | @@ -55,7 +55,7 @@ export async function main() { | ... | @@ -55,7 +55,7 @@ export async function main() { |
| 55 | 55 | ||
| 56 | import * as incr from "../framework/incremental.ts"; | 56 | import * as incr from "../framework/incremental.ts"; |
| 57 | import { generate } from "../framework/generate.ts"; | 57 | import { generate } from "../framework/generate.ts"; |
| 58 | import * as progress from "lib/log/progress.ts"; | 58 | import * as progress from "lib/progress.ts"; |
| 59 | import * as subprocess from "lib/subprocess.ts"; | 59 | import * as subprocess from "lib/subprocess.ts"; |
| 60 | import * as rsync from "./file-viewer/rsync.ts"; | 60 | import * as rsync from "./file-viewer/rsync.ts"; |
| 61 | import { Path } from "#sitegen/path"; | 61 | import { Path } from "#sitegen/path"; |
src/file-viewer/bin/scan3.ts+1-1| ... | @@ -1015,7 +1015,7 @@ import * as subprocess from "lib/subprocess.ts"; | ... | @@ -1015,7 +1015,7 @@ import * as subprocess from "lib/subprocess.ts"; |
| 1015 | import { Path } from "#sitegen/path"; | 1015 | import { Path } from "#sitegen/path"; |
| 1016 | import * as queue from "../../../lib/queue.ts"; | 1016 | import * as queue from "../../../lib/queue.ts"; |
| 1017 | import * as log from "../../../lib/log.ts"; | 1017 | import * as log from "../../../lib/log.ts"; |
| 1018 | import * as progress from "../../../lib/log/progress.ts"; | 1018 | import * as progress from "lib/progress.ts"; |
| 1019 | import * as ts from "../../../lib/ts.ts"; | 1019 | import * as ts from "../../../lib/ts.ts"; |
| 1020 | 1020 | ||
| 1021 | import * as child_process from "node:child_process"; | 1021 | import * as child_process from "node:child_process"; |
src/file-viewer/models/derived.ts+1-1| ... | @@ -200,5 +200,5 @@ import { Path } from "#sitegen/path"; | ... | @@ -200,5 +200,5 @@ import { Path } from "#sitegen/path"; |
| 200 | import * as fs from "node:fs"; | 200 | import * as fs from "node:fs"; |
| 201 | import * as crypto from "node:crypto"; | 201 | import * as crypto from "node:crypto"; |
| 202 | import * as queue from "lib/queue.ts"; | 202 | import * as queue from "lib/queue.ts"; |
| 203 | import * as progress from "lib/log/progress.ts"; | 203 | import * as progress from "lib/progress.ts"; |
| 204 | import { ASSERT } from "lib/assert.ts"; | 204 | import { ASSERT } from "lib/assert.ts"; |
src/file-viewer/rsync.ts+1-1| ... | @@ -183,5 +183,5 @@ export class Parse { | ... | @@ -183,5 +183,5 @@ export class Parse { |
| 183 | import * as child_process from "node:child_process"; | 183 | import * as child_process from "node:child_process"; |
| 184 | import * as readline from "node:readline"; | 184 | import * as readline from "node:readline"; |
| 185 | import events from "node:events"; | 185 | import events from "node:events"; |
| 186 | import * as progress from "lib/log/progress.ts"; | 186 | import * as progress from "lib/progress.ts"; |
| 187 | import { formatSize } from "@/file-viewer/format.ts"; | 187 | import { formatSize } from "@/file-viewer/format.ts"; |
src/package-lib.ts+1-1| ... | @@ -219,7 +219,7 @@ export function main() { | ... | @@ -219,7 +219,7 @@ export function main() { |
| 219 | ); | 219 | ); |
| 220 | } | 220 | } |
| 221 | 221 | ||
| 222 | import * as progress from "lib/log/progress.ts"; | 222 | import * as progress from "lib/progress.ts"; |
| 223 | import * as path from "node:path"; | 223 | import * as path from "node:path"; |
| 224 | import * as fs from "node:fs/promises"; | 224 | import * as fs from "node:fs/promises"; |
| 225 | import * as subprocess from "lib/subprocess.ts"; | 225 | import * as subprocess from "lib/subprocess.ts"; |