diff --git a/vterm/commands.py b/vterm/commands.py index 17eba9f..80bd56b 100644 --- a/vterm/commands.py +++ b/vterm/commands.py @@ -264,6 +264,7 @@ class VtermClearScreenCommand(sublime_plugin.TextCommand): return # the renderer thread owns the screen; it applies this on its next tick terminal.pending_clear_screen = True + terminal.wake() self.view.run_command("vterm_clear_undo_stack") def is_enabled(self): diff --git a/vterm/listeners.py b/vterm/listeners.py index 4287620..e6acffe 100644 --- a/vterm/listeners.py +++ b/vterm/listeners.py @@ -27,6 +27,7 @@ class VtermCoreEventListener(sublime_plugin.EventListener): # background — via the renderer tick: a render here would race the # renderer thread, which owns the emulator terminal.pending_render = True + terminal.wake() view.run_command("vterm_show_cursor", {"scroll": False}) elif view.settings().get("vterm_view") and not view.settings().get("vterm_view.finished"): # the plugin host restarted; this tab's terminal is unreachable diff --git a/vterm/terminal.py b/vterm/terminal.py index c80c9f5..76b6c4a 100644 --- a/vterm/terminal.py +++ b/vterm/terminal.py @@ -30,6 +30,9 @@ FEED_LIMIT = 256 * 1024 # SUPPRESS_HISTORY_AFTER_CLEAR in emulator.py must outlast this REPAINT_NUDGE_DELAY = 0.25 +# extra wait once a tick finds nothing pending, on top of RENDER_PERIOD. +IDLE_TICK = 0.2 + def view_size(view, default=None): """Terminal (rows, columns) that fit the view's viewport.""" @@ -116,6 +119,12 @@ class Terminal: self.pending_render = False # a UTF-8 sequence split across read chunks must not be dropped self._decoder = codecs.getincrementaldecoder("utf-8")("replace") + # set by the reader (and the UI thread) to end the renderer's idle park + self._wake = threading.Event() + + def wake(self): + """Cut short the renderer's idle wait, for a flag set from another thread""" + self._wake.set() @classmethod def from_id(cls, vid): @@ -203,6 +212,7 @@ class Terminal: """Terminate the child and drop this terminal from the registry.""" LOG.debug("[kill] terminating process") self.alive = False + self._wake.set() scheme_manager.remove_listener(self.apply_scheme) try: self.process.terminate() @@ -254,11 +264,13 @@ class Terminal: except EOFError: break chunks.append(self._decoder.decode(raw)) + self._wake.set() if done[0] or not self.is_hosted(): LOG.debug("[reader] stops") break self.alive = False done[0] = True + self._wake.set() def feed_data(): """Parse up to FEED_LIMIT of pending output into the screen.""" @@ -280,9 +292,10 @@ class Terminal: """Own the emulator: parse, then ask sublime to redraw, once per tick.""" while True: # a hidden tab is not drawn at all: the screen keeps the state - # and one render on unhide catches the view up. The tick stays at - # full rate either way, or the parser would fall behind the pty + # and one render on unhide catches the view up. visible = self.is_visible() + + self._wake.clear() with intermission(period=RENDER_PERIOD): feed_data() if self.screen.in_sync_frame(): @@ -300,6 +313,16 @@ class Terminal: LOG.debug("[renderer] stops") break + # nothing left to do: park until woken, instead of spinning at + # RENDER_PERIOD. A sync frame or a teardown skipped this by + # leaving the block above, and keeps its own cadence. + # dirty rows only count while visible: nothing clears them for a + # hidden tab, and on_activated wakes us with pending_render + if not ( + chunks or self.pending_render or self.pending_clear or self.pending_clear_screen or (visible and self.screen.dirty) + ): + self._wake.wait(IDLE_TICK) + while chunks: feed_data() done[0] = True