Skip to content

Flush window title updates without waiting for a prompt - #1790

Merged
tleonhardt merged 17 commits into
mainfrom
fix/window-title-flush-v2
Oct 30, 2026
Merged

tleonhardt merged 17 commits into
mainfrom
fix/window-title-flush-v2

Conversation

@tleonhardt

@tleonhardt tleonhardt commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

This continues #1783 by @KennyMcSimpson, whose commits are merged into this branch unchanged. The
investigation, the breaking change that makes set_window_title() an instance method, the docs and
example cleanup, and most of the regression tests are Kenny's work. Thank you, Kenny!

Closes #1782. Supersedes #1783.

What changed on top of #1783

#1783 serialized title writes with rendering by wrapping each Cmd's output driver in a locking proxy
and always creating a per-Cmd driver. Review found side effects in the rendering path:

  • Always passing output= to PromptSession disabled prompt-toolkit's TERM=dumb fallback.
  • Apps created inside create_app_session(output=...) (for example prompt-toolkit's SSH/telnet servers)
    no longer used that session's output when stdout is sys.stdout.
  • isinstance checks on renderer.output, such as the Windows mouse handler's, failed on the proxy.
  • The lock covered each output call, not a whole render pass, and every render call paid for the proxy.

This PR keeps the change inside set_window_title():

  • The title is written and flushed on main_session.output, the same output the main prompt, custom
    input prompts, and select() menus use.
  • When another thread calls it while one of those prompts is running, the write is scheduled on the
    prompt's event loop with call_soon_threadsafe(), so it never runs concurrently with the renderer.
    Calls on the loop thread, or with no prompt running, write directly.
  • If the loop stops before running the callback, the prompt's cleanup in _read_raw_input() and
    select() writes the pending title, so it isn't lost.
  • _LockedOutput, the main_session property setter, and the forced create_output() call are removed.

Tests

  • Kept Kenny's tests for commands, real prompts, custom prompts, select() menus, concurrent instances,
    interrupted setup, DummyOutput, Win32, and replacing main_session. Tests for worker threads now
    check that the title is written on the prompt's event loop rather than before set_window_title()
    returns.
  • Removed the tests that only applied to the proxy and lock.
  • Added tests for the ambient AppSession output, for a loop that stops before running the callback,
    for a closed loop, and for errors with and without a running prompt.
  • make check, make test (2816 passed), and make docs-test pass locally.

Follow-up review fixes

  • _get_prompt_loop() reads the select() AppSession and its app once, since the main thread clears
    them when select() finishes.
  • Output errors are ignored when a title is written on a prompt's event loop or during prompt and
    select() cleanup, so they can't trigger prompt-toolkit's "Press ENTER to continue..." handler or
    replace the prompt's result or exception. Errors still propagate when no prompt is running.
  • The title lock is reentrant, so a signal handler which sets the title mid-write doesn't deadlock.
    A nested call only records its title, and the outer call keeps writing until nothing is pending, so
    the latest title wins and writes never nest in the same stream.
  • Quiet writes ignore ValueError from a closed stream as well as OSError.
  • A title left pending by a prompt is written after patch_stdout() exits, so it can't race with its
    writer thread.
  • When stdin isn't a terminal but stdout is, titles are written to stdout instead of being dropped by
    the main session's DummyOutput. The fallback output is created the first time a title needs it,
    from the stdout captured when the app was created, so titles still reach the terminal while a
    command's output is redirected.
  • The docs no longer claim a title set from another thread is flushed immediately. It appears once
    the prompt's loop runs its next callback.

Third review round

  • A prompt counts as running while its application's event loop is set. That includes prompt-toolkit's
    teardown, which clears is_running while its loop can still write to the terminal.
  • While a prompt is running, titles are always scheduled on its loop, even when set_window_title() is
    called on that loop. This stops a signal handler from nesting a title write inside prompt-toolkit's
    own flush.
  • Whether the fallback stdout is a terminal is checked the first time a title needs it, so
    constructing a Cmd with a stdout that lacks isatty() or is closed works as before.
  • The set_window_title() docstring and the prompt docs describe the stdout fallback.
  • Test workers run through futures, so their exceptions reach the test without a blind except or
    # noqa: BLE001.
  • Covering prompt-toolkit apps other than the main prompt, custom prompts and select() is tracked
    in set_window_title(): find any running prompt-toolkit app on the terminal, not just two known ones #1792.

Fourth review round

  • While a title callback is queued on a prompt's loop, later titles only replace the pending title,
    so a burst of updates schedules one callback.
  • The stdout fallback keeps the original stdout in its own field and records whether it has been
    checked in a flag. (A cached_property doesn't work here, because inspect.getmembers() evaluates
    properties while a Cmd is constructed.)
  • The docstring and prompt docs list every case where prompts use a dummy output. They also note that
    a direct write from another thread isn't synchronized with other writers to the terminal.
  • The remaining direct-write races, with a prompt that's just starting and with patch_stdout()'s
    writer thread during teardown, are tracked in set_window_title(): find any running prompt-toolkit app on the terminal, not just two known ones #1792 alongside the app-discovery redesign.

KennyMcSimpson and others added 10 commits October 25, 2026 04:14
…king the output

Replace the locked output proxy from #1783 with a smaller change confined to set_window_title(). The title is
written and flushed on main_session.output. When another thread calls it while a prompt or select() menu is
running, the write is scheduled on that prompt's event loop so it doesn't interleave with rendering. A pending
title is written when the prompt finishes in case the loop stops before running the callback.

This drops the _LockedOutput proxy, the main_session property setter, and the forced per-Cmd output driver,
restoring prompt-toolkit's dumb-terminal fallback, the ambient AppSession output when stdout is sys.stdout,
and isinstance checks on the renderer's output.
@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.75%. Comparing base (57c3a66) to head (4472a0a).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1790   +/-   ##
=======================================
  Coverage   99.75%   99.75%           
=======================================
  Files          23       23           
  Lines        6503     6574   +71     
=======================================
+ Hits         6487     6558   +71     
  Misses         16       16           
Flag Coverage Δ
unittests 99.75% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

The loop thread can write the title before the worker checks the stream, which happens on free-threaded Python.
Only a call on the loop thread is guaranteed to have written it before returning.
- Read the select() AppSession and its app once in _get_prompt_loop(), since the main thread clears them when
  select() finishes.
- Ignore output errors when a title is written on a prompt's event loop or during prompt and select() cleanup, so
  they can't disrupt the prompt or replace its result or exception.
- Use a reentrant lock so a signal handler which sets the title while one is being written doesn't deadlock.
- Write titles to a terminal stdout when stdin isn't a terminal and the main session uses a DummyOutput.
- Document that a title set from another thread appears once the prompt's loop runs its next callback.
- When a signal handler sets the title while this thread is writing one, only record it. The outer call writes
  pending titles until none remain, so the latest title wins and writes never nest in the same stream.
- Ignore ValueError from a closed stream as well as OSError when writing a title quietly.
- Write a title left pending by a prompt after patch_stdout() exits, so it can't race with its writer thread.
- Create the fallback terminal output the first time a title needs it, from the stdout captured when the app was
  created. This also covers a main_session replaced later by one with a DummyOutput, and titles set while output
  is redirected.
- Share the list of terminal initialization errors between the main session and the title fallback.
- Treat a prompt as running while its application's loop is set, which covers prompt-toolkit's teardown after it
  clears is_running but can still write to the terminal.
- Schedule the title on the prompt's loop even when called on that loop, so a signal handler can't nest a title
  write inside prompt-toolkit's own flush.
- Check whether the fallback stdout is a terminal the first time a title needs it, so constructing a Cmd with a
  stdout that lacks isatty() or is closed works as it did before.
- Describe the stdout fallback in the set_window_title() docstring and the prompt docs.
- Run test workers through futures so their exceptions reach the test without a blind except and noqa.
…t fallback

- While a title callback is queued on a prompt's loop, later titles only replace the pending title, so a burst of
  updates schedules one callback. The callback and prompt cleanup clear the queued loop.
- Keep the stdout this app was created with in its own field, and track whether its fallback output has been
  checked with a flag instead of clearing that field. A cached_property doesn't work here, because
  inspect.getmembers() evaluates properties while a Cmd is constructed.
- Describe all the cases where prompts use a dummy output, and note that a direct write from another thread isn't
  synchronized with other writers to the terminal.
@tleonhardt tleonhardt added the bug label Oct 29, 2026
On a Windows CI runner the default app session output is a Win32Output, which raises NoConsoleScreenBufferError
because there's no console. Run the test inside create_app_session() like the other prompt tests.
@tleonhardt

Copy link
Copy Markdown
Member Author

@KennyMcSimpson @kmvanbrunt If either of you have time to review, I would appreciate it.

At first I thought this would be simple ;-) But when I delved into it, there were a lot of edge cases.

Fundamentally the reason it is complex is some of this should be fixed in prompt-toolkit itself and we are having to kludge around the way prompt-toolkit works.

@tleonhardt
tleonhardt merged commit fe7c2ad into main Oct 30, 2026
32 checks passed
@tleonhardt
tleonhardt deleted the fix/window-title-flush-v2 branch October 30, 2026 00:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

set_window_title() doesn't flush, so a title set during a command appears late

2 participants