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 L2 - (none) task lifecycle is reported at L3
23 L3 - task lifecycle events
24 L4 - tick and render calls
28- ProgressTracker outputs to the provided stream (default: stderr).
29- Dashboard outputs to the provided stream (default: stdout).
30- quiet=True suppresses all output (for --json modes).
31- Dashboard.finish() prints final line state on non-TTY streams.
35- oct.core.diagnostics (_dbg structured logger)
38from __future__
import annotations
43from collections.abc
import Iterator
44from contextlib
import contextmanager
45from typing
import TextIO
46from oct.core.diagnostics
import _dbg
53 """Task checklist with optional per-task progress bar."""
60 stream: TextIO |
None =
None,
74 self.
_done: dict[str, tuple[str, str]] = {}
81 _dbg(
"OCT-CORE", func, f
"task start: {name} (total={total})", 3)
94 def tick(self, n: int = 1) ->
None:
108 line = f
" {marker} {name}"
110 line += f
" {summary}"
111 self.
_done[name] = (
"done", summary)
116 def fail_task(self, name: str, *, summary: str =
"") ->
None:
122 line = f
" {marker} {name}"
124 line += f
" {summary}"
125 self.
_done[name] = (
"fail", summary)
130 def skip_task(self, name: str, *, summary: str =
"") ->
None:
136 line = f
" {marker} {name}"
138 line += f
" {summary}"
139 self.
_done[name] = (
"skip", summary)
145 def task(self, name: str, total: int = 0) -> Iterator[ProgressTracker]:
156 _dbg(
"OCT-CORE", func, f
"tracker finished; {len(self._done)} tasks done", 3)
162 if t
not in self.
_done:
169 self.
_stream.write(text +
"\n")
171 except (OSError, ValueError):
183 pct = min(progress / total, 1.0)
184 filled = int(pct * _BAR_WIDTH)
185 bar =
"=" * filled +
">" * (1
if filled < _BAR_WIDTH
else 0)
186 bar = bar.ljust(_BAR_WIDTH)
187 text = f
" Running: {name} [{bar}] {int(pct * 100)}% {progress}/{total}"
189 elapsed = time.monotonic() - start
190 text = f
" Running: {name} ... elapsed {int(elapsed)}s"
192 self.
_stream.write(f
"\r{text}\033[K")
194 except (OSError, ValueError):
201 except (OSError, ValueError):
206 """Fixed-size terminal display with in-place line updates and live timer.
208 Prints a full template on render(), then overwrites individual lines via
209 ANSI cursor movement as data becomes available. A background daemon thread
210 refreshes the footer line every second so elapsed-time counters stay live.
212 On non-TTY streams the template is NOT printed during render(); instead,
213 update_line() silently accumulates changes and finish() prints the final
214 state in one shot - clean output for CI and pipes.
220 stream: TextIO |
None =
None,
238 def render(self, lines: list[str], *, footer_line: int = -1,
239 tasks_total: int = 0) ->
None:
240 """Print all lines. *footer_line* is the index auto-updated by the
241 background timer (default: third-from-last)."""
251 self.
_stream.write(line +
"\n")
260 """Replace the line at *index* with *text*."""
268 f
"\033[{up}A\r{text}\033[K\033[{up}B\r",
273 """Set current task name and reset the per-task elapsed timer."""
280 """Increment done counter and clear the current task name."""
286 """Stop the timer thread and render final state."""
288 _dbg(
"OCT-CORE", func, f
"dashboard finished; {len(self._lines)} lines", 3)
296 final = f
" All {self._tasks_total} checks completed in {elapsed:.1f}s"
300 self.
_stream.write(line +
"\n")
319 elapsed = time.monotonic() - start
320 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)
Iterator[ProgressTracker] task(self, str name, int total=0)