Skip to content

Instantly share code, notes, and snippets.

@kfsone
Last active September 7, 2023 14:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kfsone/15b37b28eb93a6aa74a750530a10313f to your computer and use it in GitHub Desktop.
Save kfsone/15b37b28eb93a6aa74a750530a10313f to your computer and use it in GitHub Desktop.
ChecklistTracker for rich.
from __future__ import annotations
from typing import List, Optional
from rich.console import Console, ConsoleOptions, RenderableType
from rich.live import Live
from rich.spinner import Spinner
from rich.table import Table
from rich.text import Text
class ChecklistTracker:
"""
Provides a running checklist based on a rich Table.
When a new row is added, it has no check state, and it shows a spinner to let the
user know it is working.
Once a row is finished, it is assigned a check state (ok/nok) and a color to match,
and the spinner is replaced with the result of the check.
"""
columns: List[str] # column identities
rows: List[List[str]] # rows of columns
checks: Optional[List[bool]] # ok/fail status of each row
ok_style: str # rich style for OK rows
nok_style: str # rich style for non-OK rows
table: Table
def __init__(self, fields: List[str], ok: str="green", nok: str="red") -> None:
"""
Construct a checklist tracker from a list of fields to show, and styling
for rows that check as 'ok' vs 'nok' (not ok).
"""
self.columns = ["Check"] + (fields if isinstance(fields, list) else list(fields)) + ["Result"]
self.rows = []
self.checks = []
self.styles = { None: "dim", True: ok, False: nok }
self.table = None
def build_table(self) -> None:
"""
Internal method: Constructs the table, so we don't do it every refresh.
"""
self.table = Table(*self.columns, show_header=False, show_lines=False, show_edge=False, box=None)
styles = self.styles
for (row, check) in zip(self.rows, self.checks):
self.table.add_row(*row, style=styles[check])
def start_list_item(self, *props: List[str], extra: str="") -> None:
"""
Begins a new row in the checklist.
"""
self.rows.append([Spinner("dots")] + list(Text(p) for p in props) + [Text(extra or "")])
self.checks.append(None)
self.build_table()
def stop_list_item(self, ok: bool, result: str) -> None:
"""
Updates the last entry in the list with an ok/nok state and replaces the spinner with result text.
"""
self.rows[-1][0] = "✓" if ok else "✗"
self.rows[-1][-1] = Text(result)
self.checks[-1] = ok
self.build_table()
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderableType:
if self.table:
yield self.table
if __name__ == "__main__":
import time
files = [
"fred.txt",
"barney.txt",
"over9000.lines",
]
items = {
"first": [ ["some", "command"], True, "over the rainbow" ],
"second": [ ["sudo", "fetch", "beer"], False, "none in fridge"]
}
styles = { True: "green", False: "red" }
tracker = ChecklistTracker(["Filename", "Step", "Result"])
with Live(tracker, auto_refresh=True, refresh_per_second=5) as live: # We'll refresh manually
for filepath in files:
for step, detail in items.items():
ident = f"{filepath}/{step}"
active = tracker.start_list_item(filepath, step, extra=f"cmd: {' '.join(detail[0])}")
time.sleep(1.4)
tracker.stop_list_item(detail[1], detail[2])
live.refresh()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment