-
Notifications
You must be signed in to change notification settings - Fork 814
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add function to temporarily suspend application mode #1541
Conversation
import: | ||
- https://docs.python.org/3/objects.inv |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows cross-linking to the standard library docs using the same syntax as internal links; used here https://github.com/Textualize/textual/pull/1541/files#diff-681b255c8a47359bc04668c27ab9770e5326a66af7931e4e267e8e205cb43aeeR148
def action_open_repl(self): | ||
with self.suspend(): | ||
repl = InteractiveConsole() | ||
repl.interact() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In #1150 I used
def action_suspend(self) -> None:
with self.suspend():
print("Hi!")
print("Resuming soon...")
time.sleep(2)
as a reproduction case, which would simplify this example if desired, but I figured doing something interactive would be more interesting and inspiring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice.
Have you checked this against Windows? I never intended start and stop application mode to be repeatable, so I'm not certain it can start after being stopped.
Good call; it does not seem to work the same way on Windows, at least with the REPL example. Investigating... |
The main problem I was hitting was that, after getting out of the terminal (interesting issue with that; I'll get back to it in a moment), I get:
Which is happening because we're still inside this block when I believe this can be resolved by using terminal_in = sys.__stdin__
terminal_out = sys.__stdout__ in Second issue: no Third issue: running |
# Conflicts: # CHANGELOG.md
Alright, take two, now with better cross-platform support! I realized I could do |
docs/guide/actions.md
Outdated
@@ -132,3 +132,23 @@ Textual supports the following builtin actions which are defined on the app. | |||
- [action_switch_screen][textual.app.App.action_switch_screen] | |||
- [action_screenshot][textual.app.App.action_screenshot] | |||
- [action_toggle_dark][textual.app.App.action_toggle_dark] | |||
|
|||
## Temporarily suspending the application |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "App Basics" might be a better place for this. It's only tangentially related to actions.
src/textual/app.py
Outdated
# print("hi") | ||
# | ||
# The print will sometimes not go to sys.__stdout__ as expected. | ||
time.sleep(0.025) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be masking a race condition. I wonder if we need to do a flush after stopping application mode so no escape sequences arrive after the yield.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦🏻♂️ of course! I will experiment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initial results: self.console.file.flush()
before yield
does not fix the problem. Given that it's random whether the prints appear or not, the sleep is definitely masking a race condition, but I'm not sure where it is.
But here's where it gets extra weird: I tried adding some debug code to _NullFile.write
to see if the writes were still going there, but it seems like they aren't, which makes it seem like they're just completely vanishing, which doesn't make much sense. The investigation continues...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious! Do you think "hi!"
is not being written at all?
We have a thread that writes to stdout, so that we can prepare the next frame while the previous frame is being printed. I wonder if that thread is still writing in the background for a fraction of a second.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha! That's promising - I didn't realize that self.console.file
was that thread (should have print
ed it!)
Lines 284 to 299 in 0be1d96
if sys.__stdout__ is None: | |
file = _NullFile() | |
else: | |
self._writer_thread = _WriterThread() | |
self._writer_thread.start() | |
file = self._writer_thread | |
self.console = Console( | |
file=file, | |
markup=False, | |
highlight=False, | |
emoji=False, | |
legacy_windows=False, | |
_environ=environ, | |
) | |
self.error_console = Console(markup=False, stderr=True) |
And _WriterThread.flush
is a no-op!
Lines 190 to 192 in 0be1d96
def flush(self) -> None: | |
"""Flush the file (a no-op, because flush is done in the thread).""" | |
return |
I bet the problem lies in there - I'll try to find some time to investigate today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it must have been that! (Maybe printing before the escape code to stop the alternate screen was printed?)
Here's my test case:
from textual.app import App, ComposeResult
from textual.widgets import Footer, Static
class SuspendApp(App):
BINDINGS = [
("r", "open_repl", "Open REPL"),
]
def compose(self) -> ComposeResult:
yield Static(
"Press r to open the Python built-in REPL. Run quit() to return to the app."
)
yield Footer()
def action_open_repl(self) -> None:
import time
for _ in range(10):
with self.suspend():
print(f'hi {_}!')
time.sleep(1)
print(f'bye {_}!\n')
if __name__ == "__main__":
app = SuspendApp()
app.run()
I've got two potential fixes tested: I can either give _WriterThread
the ability to temporarily pause by adding some additional threading goop to it (I've pushed this version up to the branch) (inspired by https://stackoverflow.com/questions/33640283/thread-that-i-can-pause-and-resume), or I can join
the _WriterThread
before yield
ing and then start a new one afterwards. Something like:
@contextmanager
def suspend(self) -> Iterator[None]:
driver = self._driver
if driver is not None:
driver.stop_application_mode()
if self._writer_thread is not None:
self._writer_thread.stop()
with redirect_stdout(sys.__stdout__), redirect_stderr(sys.__stderr__):
yield
if self._writer_thread is not None:
self._writer_thread = _WriterThread()
self._writer_thread.start()
self.console.file = self._writer_thread
driver.start_application_mode()
I am not sure which would be better. The threading goop seems more maintainable long-term since it's self-contained, but could cause performance problems in that tight loop. Thoughts?
# Conflicts: # CHANGELOG.md # src/textual/app.py
Currently looking to see if this will play well with #1582 -- right now playing in https://github.com/davep/textual/tree/sleep-resume; nothing concrete yet, more mulling over how it might work. |
Having stolen Textualize#1541 from https://github.com/JoshKarpel this builds on that good work to get killing the process with SIGTSTP to work. Initial testing is looking good. See Textualize#1582.
Will do, thanks. I'll keep this one open for the moment, just until I'm happy that the other is heading in the right direction. I would have added to this one but I utterly failed to work out which magic incantation would let me grab a PR from upstream into the origin of my fork and let me work on that. Got to love
Pfft; we're all in the same timezone and that still happens. ¯\(ツ)/¯ |
Pulling out the very core of Textualize#1541 to start to build it up again and experiment and test (getting into the forge so I can then pull it down onto Windows and test there).
Adding Josh Karpel as a co-author here; not because of the docstring, but the core idea started with Textualize#1541 and this is a reimplementation of that code in the current version of Textual. Co-authored-by: Josh Karpel <[email protected]>
Wee bit later on but... making it over the line as part of #4064. |
This PR adds the
App.suspend()
method that I've been using in my Textual projects. This originally came up in #1150 (and related issues), and a few people have since asked how to do this kind of thing on Discord, so it seems that enough people want this behavior that supporting it in core Textual might make sense.I added a section for it in the docs, but I wasn't really sure where to do it, so I just put it under the Actions documentation, assuming that most people would be interested in this in the context of something like "open a text editor when the user presses a key". One thing that was a little awkward is that "application mode" isn't really a term used in the docs, so I had to kind of dance around the language.