progress.py 3.7 KB

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