1#!/usr/bin/env python
2
3"""Example: run a slow computation on a worker thread and show its progress and result.
4
5Press :kbd:`Enter` on the button to start the computation and :kbd:`q` to exit.
6The interface keeps answering keys while the worker runs.
7"""
8
9from __future__ import annotations
10
11import contextlib
12import threading
13import time
14
15import urwid
16
17STEPS = 20
18
19
20def compute(loop: urwid.MainLoop, progress: urwid.ProgressBar, status: urwid.Text) -> None:
21 """Sum the squares of 1 to :data:`STEPS` slowly, reporting each step to the loop.
22
23 Runs on the worker thread: widgets are changed only through :meth:`urwid.MainLoop.call_from_thread`.
24
25 :param loop: main loop that shows the progress
26 :param progress: bar showing the steps done
27 :param status: text showing the result
28 """
29 total = 0
30 # RuntimeError: the loop stopped, so nothing shows the result any more
31 with contextlib.suppress(RuntimeError):
32 for step in range(1, STEPS + 1):
33 time.sleep(0.2)
34 total += step * step
35 loop.call_from_thread(progress.set_completion, step)
36 loop.call_from_thread(status.set_text, f"Done: the sum of squares is {total}.")
37
38
39def main() -> None:
40 """Run the example."""
41 status = urwid.Text("Not started.")
42 progress = urwid.ProgressBar("pg normal", "pg complete", done=STEPS)
43 worker: threading.Thread | None = None
44
45 def start(_button: urwid.Button) -> None:
46 """Start the worker, unless it is still running."""
47 nonlocal worker
48 if worker is not None and worker.is_alive():
49 return
50 status.set_text("Working...")
51 progress.set_completion(0)
52 # A daemon thread, so that q does not wait for the computation to finish.
53 worker = threading.Thread(target=compute, args=(loop, progress, status), daemon=True)
54 worker.start()
55
56 def handle_key(key: str | tuple[str, int, int, int]) -> None:
57 """Exit on :kbd:`q`.
58
59 :param key: unhandled key or mouse event
60 :raises urwid.ExitMainLoop: when the user presses :kbd:`q`
61 """
62 if key in {"q", "Q"}:
63 raise urwid.ExitMainLoop
64
65 body = urwid.Pile([urwid.Button("Compute", on_press=start), progress, status])
66 loop = urwid.MainLoop(
67 urwid.Filler(body, valign=urwid.TOP),
68 palette=[("pg normal", "white", "dark gray"), ("pg complete", "white", "dark blue")],
69 unhandled_input=handle_key,
70 )
71 loop.run()
72
73
74if __name__ == "__main__":
75 main()