TUI State Management in XHS-Downloader#
Overview#
XHS-Downloader's Textual TUI routes all application log output through a single shared Print wrapper object on the XHS application instance. This object holds a mutable func pointer that each TUI screen overwrites in its on_mount handler β with no corresponding teardown when the screen is popped. The result is that the pointer can be left pointing at a destroyed widget, causing silent state corruption when the user navigates between screens.
The Print Wrapper#
Print is a small callable wrapper defined in source/application/app.py:
- Constructed with
func = rich.print(the default terminal printer). XHS.__init__createsself.print = Print()and passes it down toManagerfor use throughout the application layer .- All application-level messages are emitted via
XHS.logging, which callslogging(self.print, text, style)insource/module/tools.py. That helper callsself.print()to get the current callable, then dispatches eitherfunc(string)(forrich.print) orfunc.write(string, scroll_end=True)(for a TextualRichLogwidget) .
Because XHS is a singleton , the Print object and its func pointer are global to the entire TUI session.
How Screens Claim the Output Target#
Both interactive TUI screens redirect output by assigning their own RichLog widget to xhs.print.func inside on_mount:
| Screen | File | Assignment |
|---|---|---|
Index (main download screen) | source/TUI/index.py:80 | self.xhs.print.func = self.tip |
Monitor (clipboard listener) | source/TUI/monitor.py:49 | self.xhs.print.func = self.query_one(RichLog) |
There is no on_unmount or equivalent teardown in either screen that resets func back to rich.print or to the other screen's widget.
Lifecycle and Corruption Risk#
The TUI app initializes and pushes the Index screen on mount . Navigation flow when entering and exiting clipboard monitor mode:
Index (on_mount β sets print.func = Index.RichLog)
βββ push_screen(Monitor(...))
βββ Monitor (on_mount β sets print.func = Monitor.RichLog)
βββ action_close β app.action_back() β Monitor is popped
# print.func now points to Monitor's destroyed RichLog
# Index.on_mount does NOT re-run; func is never reset
When Monitor.action_close is called , it stops the monitor loop and calls app.action_back(). Textual pops Monitor off the screen stack, but print.func is never restored. Any subsequent call to XHS.logging will try to call .write() on the now-unmounted RichLog from Monitor, instead of the visible Index log widget.
The same issue applies to the settings refresh flow: refresh_screen tears down and recreates all screens, meaning the Index screen gets a brand-new RichLog β but print.func still points to the old (destroyed) widget until the new Index mounts and reassigns it.
Key Entry Points for Debugging / Fixing#
| Concern | Location |
|---|---|
Print class definition | source/application/app.py:87-97 |
print.func assigned in Index | source/TUI/index.py:76-85 |
print.func assigned in Monitor | source/TUI/monitor.py:47-50 |
Monitor close / screen pop | source/TUI/monitor.py:52-54 |
logging dispatch logic | source/module/tools.py:42-51 |
| Screen install / teardown on settings save | source/TUI/app.py:73-105 |
Mitigation approaches to consider:
- Add an
on_unmounthandler toMonitorthat resetsself.xhs.print.func = print(or to a sentinel that drops writes silently). - Implement
on_resumeinIndex(Textual's callback for when a screen returns to focus) to re-assignprint.functo its ownRichLog, ensuring the pointer is always valid whenIndexis the active screen.