progress.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import logging
  2. from textual.app import App
  3. from textual.containers import Horizontal
  4. from textual.widgets import Label, ProgressBar
  5. logger = logging.getLogger(__name__)
  6. # Number of stacked progress-bar rows (levels 0 .. MAX_LEVELS-1). Deep enough for
  7. # the k-fold nesting: root(0) -> run_kfold(1) -> fold task(2) -> train/eval(3) ->
  8. # epochs/passes(4) -> batches(5). main.py builds exactly this many rows.
  9. MAX_LEVELS = 6
  10. class ProgressTracker:
  11. """Manages hierarchical progress bars for the UI thread."""
  12. def __init__(self, app: App, level: int = 0):
  13. self.app = app
  14. self.level = level
  15. def get_sub_tracker(self, title: str = "") -> "ProgressTracker":
  16. """Returns a sub-tracker for child processes."""
  17. if self.level >= MAX_LEVELS - 1:
  18. return self # Max depth reached
  19. sub = ProgressTracker(self.app, self.level + 1)
  20. if title:
  21. sub.set_title(title)
  22. return sub
  23. def set_title(self, title: str) -> None:
  24. def _update_title():
  25. try:
  26. self.app.query_one(f"#progress_title_{self.level}", Label).update(title)
  27. except Exception:
  28. pass
  29. self.app.call_from_thread(_update_title)
  30. def update(self, total: int | None = None, advance: int = 0) -> None:
  31. def _do_update():
  32. try:
  33. bar = self.app.query_one(f"#progress_bar_{self.level}", ProgressBar)
  34. row = self.app.query_one(f"#progress_row_{self.level}", Horizontal)
  35. stats = self.app.query_one(f"#progress_stats_{self.level}", Label)
  36. if total is not None:
  37. bar.update(total=total)
  38. if advance > 0:
  39. bar.advance(advance)
  40. # Format Completed / Total label
  41. if bar.total is not None and bar.total > 0:
  42. stats.update(f"{int(bar.progress)} / {int(bar.total)}")
  43. else:
  44. stats.update("")
  45. # Visibility logic: Visible only when value/total is non-zero
  46. is_active = (bar.progress > 0) or (
  47. bar.total is not None and bar.total > 0
  48. )
  49. row.display = is_active
  50. # Cascading clear logic: Clear sub-bars if we advance or reset
  51. if advance > 0 or total == 0:
  52. self._clear_subs()
  53. except Exception as e:
  54. logger.error(f"Failed to update progress: {e}")
  55. self.app.call_from_thread(_do_update)
  56. def reset(self) -> None:
  57. """Clears and hides this progress bar and cascades to all children."""
  58. def _do_reset():
  59. try:
  60. bar = self.app.query_one(f"#progress_bar_{self.level}", ProgressBar)
  61. row = self.app.query_one(f"#progress_row_{self.level}", Horizontal)
  62. title = self.app.query_one(f"#progress_title_{self.level}", Label)
  63. stats = self.app.query_one(f"#progress_stats_{self.level}", Label)
  64. bar.progress = 0
  65. bar.total = None
  66. title.update("")
  67. stats.update("")
  68. row.display = False
  69. self._clear_subs()
  70. except Exception:
  71. pass
  72. pass
  73. self.app.call_from_thread(_do_reset)
  74. def _clear_subs(self) -> None:
  75. """Clears and hides all child progress bars (must run in UI thread)."""
  76. for l in range(self.level + 1, MAX_LEVELS):
  77. try:
  78. bar = self.app.query_one(f"#progress_bar_{l}", ProgressBar)
  79. row = self.app.query_one(f"#progress_row_{l}", Horizontal)
  80. title = self.app.query_one(f"#progress_title_{l}", Label)
  81. stats = self.app.query_one(f"#progress_stats_{l}", Label)
  82. bar.progress = 0
  83. bar.total = None
  84. title.update("")
  85. stats.update("")
  86. row.display = False
  87. except Exception:
  88. pass