8Provide reusable progress display for long-running OCT commands.
12- ProgressTracker: scrolling task checklist with per-task progress bar.
13- Dashboard: fixed-size terminal display with in-place line updates and a
14 background timer thread that refreshes the footer every second.
15- Auto-detect TTY vs pipe; Dashboard prints final state on non-TTY.
16- Thread-safe for concurrent callers (e.g. linter ThreadPoolExecutor).
22 L3 — task lifecycle events
23 L4 — tick and render calls
27- ProgressTracker outputs to the provided stream (default: stderr).
28- Dashboard outputs to the provided stream (default: stdout).
29- quiet=True suppresses all output (for --json modes).
30- Dashboard.finish() prints final line state on non-TTY streams.
33from __future__
import annotations
38from contextlib
import contextmanager
39from typing
import TextIO
46 """Task checklist with optional per-task progress bar."""
53 stream: TextIO |
None =
None,
67 self.
_done: dict[str, tuple[str, str]] = {}
85 def tick(self, n: int = 1) ->
None:
99 line = f
" {marker} {name}"
101 line += f
" {summary}"
102 self.
_done[name] = (
"done", summary)
107 def fail_task(self, name: str, *, summary: str =
"") ->
None:
113 line = f
" {marker} {name}"
115 line += f
" {summary}"
116 self.
_done[name] = (
"fail", summary)
121 def skip_task(self, name: str, *, summary: str =
"") ->
None:
127 line = f
" {marker} {name}"
129 line += f
" {summary}"
130 self.
_done[name] = (
"skip", summary)
136 def task(self, name: str, total: int = 0):
151 if t
not in self.
_done:
158 self.
_stream.write(text +
"\n")
160 except (OSError, ValueError):
172 pct = min(progress / total, 1.0)
173 filled = int(pct * _BAR_WIDTH)
174 bar =
"=" * filled +
">" * (1
if filled < _BAR_WIDTH
else 0)
175 bar = bar.ljust(_BAR_WIDTH)
176 text = f
" Running: {name} [{bar}] {int(pct * 100)}% {progress}/{total}"
178 elapsed = time.monotonic() - start
179 text = f
" Running: {name} ... elapsed {int(elapsed)}s"
181 self.
_stream.write(f
"\r{text}\033[K")
183 except (OSError, ValueError):
190 except (OSError, ValueError):
195 """Fixed-size terminal display with in-place line updates and live timer.
197 Prints a full template on render(), then overwrites individual lines via
198 ANSI cursor movement as data becomes available. A background daemon thread
199 refreshes the footer line every second so elapsed-time counters stay live.
201 On non-TTY streams the template is NOT printed during render(); instead,
202 update_line() silently accumulates changes and finish() prints the final
203 state in one shot — clean output for CI and pipes.
209 stream: TextIO |
None =
None,
227 def render(self, lines: list[str], *, footer_line: int = -1,
228 tasks_total: int = 0) ->
None:
229 """Print all lines. *footer_line* is the index auto-updated by the
230 background timer (default: third-from-last)."""
240 self.
_stream.write(line +
"\n")
249 """Replace the line at *index* with *text*."""
257 f
"\033[{up}A\r{text}\033[K\033[{up}B\r",
262 """Set current task name and reset the per-task elapsed timer."""
269 """Increment done counter and clear the current task name."""
275 """Stop the timer thread and render final state."""
283 final = f
" All {self._tasks_total} checks completed in {elapsed:.1f}s"
287 self.
_stream.write(line +
"\n")
306 elapsed = time.monotonic() - start
307 text = f
" [{done}/{total}] {name} ... elapsed {int(elapsed)}s"
threading.Thread|None _timer_thread
None set_task(self, str name)
None update_line(self, int index, str text)
None render(self, list[str] lines, *, int footer_line=-1, int tasks_total=0)
None __init__(self, *, TextIO|None stream=None, bool quiet=False)
None _update_footer(self)
None start_task(self, str name, int total=0)
None skip_task(self, str name, *, str summary="")
None _render_status(self)
None complete_task(self, str name, *, str summary="")
None _write_line(self, str text)
None fail_task(self, str name, *, str summary="")
None __init__(self, list[str] tasks, *, str title="", TextIO|None stream=None, bool quiet=False)
task(self, str name, int total=0)