Skip to content

Conversation

@tleonhardt
Copy link
Member

@tleonhardt tleonhardt commented Dec 29, 2025

This PR migrates from using GNU Readline for user input, tab-completion, and history to using prompt-toolkit for the same purpose.

prompt-toolkit is a pure-Python readline replacement that is fully cross-platform compatible and doesn't rely on the presence of underlying C dynamic libraries. It also opens the door to some advanced features in the future.

All use of readline is removed and the cmd2.rl_utils module has been deleted. There is a new cmd2.pt_utils module in its place. Currently all linting is passing and the documentation builds. This is essentially final draft that needs more testing. There are likely a some things that need to be considered related to asynicio support, signal handlers, and blocking popen calls.

All tests are passing on all platforms. I have done some manual testing on both macOS and Linux. But I don't have a Windows VM to do any manual testing on Windows however.

NOTE FOR REVIEWERS: The substantive code changes are in the 3 following files (all in the cmd2 directory):

  • argparse_completer.py (modified)
  • cmd2.py (modified)
  • pt_utils.py (new)

TODO:

  • Fix so tab-completion hints shown up above or below the prompt but not in the the bottom bar
  • Fix tab completion for for command/sub-command names and arguments so it uses prompt-toolkit completion
  • Fix subcommand suggestion for alias create <TAB> - currently showing nothing
  • Increaes code coverage for new code
  • Modify getting_started.py example to show how to use the optional bottom toolbar feature
  • Update documentation
  • Self review and code / comment cleeanup
  • Check type-hints for new/modified methods and functions
  • Verify that all arguments to cmd2.Cmd.read_input work like they did previously - test using examples/read_input.py
  • Do a bunch of manual testing related to edge cases on all platforms
    • Windows
    • macOS
    • Linux
  • Think about interactions with signal handlers
    • Ensure <Ctrl>+c works for SIGINT when using shell command - need to fix how prompt is redrawn after
    • Ensure terminated processes still properly handle SIGHUP/SIGTERM so that cmd2 exit handler runs to save persistent history and such
    • Should we worry about SIGKILL? - used to work fine and works normally now, but not anymore if inside ppaged (get traceback with termios.error)
  • Thank about blocking calls to things like popen
    • Ensure output redirection works, to both file and clipboard
    • Ensure piping command output to shell command works
    • Ensure ppaged works properly like it did before - works, but doesn't handle SIGKILL correctly - get traceback
  • Provide a flag to use readline-style completions instead of the prompt-toolkit style
  • Dynamically switch from CompleteStyle.COLUMN to CompleteStyle.MULTI_COLUMN when there are a lot of completions - based on a settable which defaults to 7; switching disabled when user specifies READLINE_LIKE style
  • Test in embedded py and ipy shells
    • Make sure locals self still works correctly in ipy
    • Make sure locals self still work correctly in py
    • See if we can get tab-completion working in embedded Python py shell for systems with LibEdit
  • Think about whether there might be a race condition risk with self._in_prompt - I added a threadling.lock object to protect access to self._in_prompt to be safe
  • Make sure we are happy with test coverage for new code
  • Think about asyncio cases - prompt-toolkit natively uses asycnio and starts an event loop
    • Fix examples/async_call.py example to account for prompt-toolkit starting an asyncio event loop
    • Investigate whether cmd2 can use asyncio to replace things like self.terminal_lock, async_alert, and async_update_prompt - removed self.terminal_lock and updated sync_alert, and other stuff as well as async_printing.py example; also removed unused async_refresh_prompt and need_prompt_refresh methods
    • Can we remove the requirement for cmd2 apps to run in the main thread? - I don't think so. From what I can see, prompt-toolkit needs to run in the main thread for various reasons, though it does have good support for handling backkground tasks in other threads
    • Review example for how include async do_* commands is in examples/async_commands.py - if we are happy with it, we can move the decorator there into cmd2/decorators.py and the utility function into cmd2.utils.pyand add some unit tests
  • Make sure changes are well documented in both CHANGELOG.md and in the Zensical documentation

Tons of tests failing and some even getting stuck.
…f the correct arguments to app.complete()

This fixes all of the test_argparse_comleter.py tests.

There are still failing tests in test_cmd2.py, test_history.py, and test_run_pyscript.py as well as a test in test_cmd2.py that gets stuck.
TODO:
- prompt-toolkit history isn't properly initialized with history from a persistent history file, as shown by the remaining failing history test
… of mocking the built-in input function.

There are still 3 failing and 1 skipped test in test_cmd2.py

Additionally, some tests in test_run_pyscript.py are getting stuck.All tests in other files are passing.
Also:
- Fixed make clean so it cleans up code coverage file artifacts
Also added a bottom toolboar for displaying these type hints.
@tleonhardt tleonhardt added this to the 4.0.0 milestone Dec 29, 2025
@tleonhardt tleonhardt added enhancement major dependencies Pull requests that update a dependency file labels Dec 29, 2025
@github-actions
Copy link
Contributor

🤖 Hi @tleonhardt, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@codecov
Copy link

codecov bot commented Dec 29, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.17%. Comparing base (cb0c75e) to head (d6117d5).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1553      +/-   ##
==========================================
+ Coverage   98.94%   99.17%   +0.22%     
==========================================
  Files          21       21              
  Lines        4942     4940       -2     
==========================================
+ Hits         4890     4899       +9     
+ Misses         52       41      -11     
Flag Coverage Δ
unittests 99.17% <100.00%> (+0.22%) ⬆️

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

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

@github-actions
Copy link
Contributor

🤖 I'm sorry @tleonhardt, but I was unable to process your request. Please see the logs for more details.

@tleonhardt tleonhardt mentioned this pull request Dec 29, 2025
13 tasks
Ensure terminal settings and foreground process group are restored after
the pager process exits. This prevents 'termios.error' crashes in
prompt_toolkit when the pager is killed abnormally (e.g. SIGKILL),
which can leave the terminal in an inconsistent state.
Split the try/except block in 'ppaged' to handle 'ImportError' for 'termios' separately.
This prevents 'UnboundLocalError' when accessing 'termios.error' in the 'except' clause
on platforms where 'termios' is not available (like Windows).
Replaced 'threading.RLock' 'terminal_lock' with 'asyncio.call_soon_threadsafe'
and 'self._in_prompt' flag for 'async_alert'.
Updated 'examples/async_printing.py' to remove lock usage.
Updated tests to mock event loop and '_in_prompt' state.
Updated 'examples/async_printing.py' to use 'asyncio' tasks and 'create_background_task'
instead of 'threading'.
Added 'pre_prompt' hook to 'cmd2.Cmd' and integrated it into 'read_input'
to support starting background tasks when the prompt loop starts.
Correctly initialize a new PromptSession with pipe_input in the test
to avoid AttributeError since the 'input' property is read-only.
Also:
- Move where pre_prompt is defined so it is near other hook methods
- Document pre_prompt hook in Hooks feature docuemntation
This defaults to the columnar way.  But a user can set it to CompleteStyle.READLINE_LIKE to get readline style tab completion.

Also:
- Renamed `include_bottom_toolbar` flag to just `bottom_toolbar`
- Renamed `_bottom_toolbar` method to `get_bottom_toolbar` which is a better name for it
- Updated `examples/hello_cmd2.py` to use readline style tab-completion
- Added max_column_completion_items setting (default 7) to configure the threshold.
- Updated Cmd.complete to switch between CompleteStyle.COLUMN and CompleteStyle.MULTI_COLUMN.
- Added test case to verify the behavior.
Also:
- Disable dynamic complete_style switching when READLINE_LIKE is specified
Hoping to clear up some type errors on windows
Also:
- Add test for get_bottom_toolbar with a very narrow terminal
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file enhancement major

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants