cmd2 4.3.1__tar.gz → 4.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cmd2-4.3.1 → cmd2-4.4.0}/.pre-commit-config.yaml +1 -1
- {cmd2-4.3.1 → cmd2-4.4.0}/CHANGELOG.md +59 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/PKG-INFO +1 -1
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/cmd2.py +223 -51
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/history.py +110 -47
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/PKG-INFO +1 -1
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/SOURCES.txt +2 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/scm_file_list.json +3 -0
- cmd2-4.4.0/cmd2.egg-info/scm_version.json +8 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/examples/embedding_in_textual.md +0 -3
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/history.md +36 -6
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/prompt.md +20 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/textual_tty_embed.py +0 -4
- cmd2-4.4.0/tests/test_history.py +1822 -0
- cmd2-4.4.0/tests/test_history_mutations.py +316 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_pipeline_job_control.py +34 -7
- cmd2-4.4.0/tests/test_window_title.py +828 -0
- cmd2-4.3.1/cmd2.egg-info/scm_version.json +0 -8
- cmd2-4.3.1/tests/test_history.py +0 -1002
- {cmd2-4.3.1 → cmd2-4.4.0}/.prettierignore +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/.prettierrc +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/.typos.toml +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/AGENTS.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/CLAUDE.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/GEMINI.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/LICENSE +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/MANIFEST.in +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/Makefile +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/README.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/__init__.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/annotated.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/argparse_completer.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/argparse_utils.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/clipboard.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/colors.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/command_set.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/completion.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/constants.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/decorators.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/exceptions.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/parsing.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/plugin.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/pt_utils.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/py.typed +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/py_bridge.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/rich_utils.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/string_utils.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/styles.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/theme.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/types.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/utils.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/dependency_links.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/requires.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/top_level.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/codecov.yml +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/annotated.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/argparse_completer.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/argparse_utils.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/clipboard.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/cmd.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/colors.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/command_set.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/completion.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/constants.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/decorators.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/exceptions.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/history.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/index.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/parsing.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/plugin.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/pt_utils.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/py_bridge.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/rich_utils.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/string_utils.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/styles.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/theme.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/utils.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/assets/cmd2-logo.png +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/assets/cmd2-readme-demo.gif +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/assets/getting-started-demo.gif +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/doc_conventions.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/examples/alternate_event_loops.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/examples/examples.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/examples/getting_started.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/examples/index.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/annotated.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/argument_processing.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/async_commands.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/builtin_commands.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/clipboard.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/commands.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/completion.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/disable_commands.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/embedded_python_shells.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/generating_output.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/help.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/hooks.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/index.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/initialization.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/misc.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/modular_commands.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/multiline_commands.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/os.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/packaging.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/plugins.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/redirection.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/scripting.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/settings.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/shortcuts_aliases_macros.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/startup_commands.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/table_creation.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/theme.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/index.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/javascripts/readthedocs.js +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/migrating/incompatibilities.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/migrating/index.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/migrating/minimum.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/migrating/next_steps.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/migrating/why.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/mixins/index.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/mixins/mixin_template.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/overrides/main.html +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/overview/alternatives.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/overview/index.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/overview/installation.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/overview/integrating.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/overview/resources.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/stylesheets/cmd2.css +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/testing.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/docs/upgrades.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/.cmd2rc +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/README.md +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/annotated_example.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/argparse_completion.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/argparse_example.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/async_call.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/async_commands.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/async_printing.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/basic_completion.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/cmd2_history.dat +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/cmd_as_argument.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/color.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/command_sets.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/completion_item_choices.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/custom_parser.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/custom_types.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/default_categories.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/dynamic_commands.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/environment.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/event_loops.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/exit_code.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/getting_started.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/hello_cmd2.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/help_categories.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/hooks.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/migrating.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/mixin.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/modular_commands/__init__.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/modular_commands/commandset_basic.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/modular_commands/commandset_complex.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/modular_commands/commandset_custominit.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/modular_commandsets.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/paged_output.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/persistent_history.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/pretty_print.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/python_scripting.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/read_input.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/remove_builtin_commands.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/remove_settable.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/rich_tables.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/rich_theme.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/arg_printer.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/conditional.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/nested.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/quit.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/save_help_text.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/script.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/script.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/tmux_launch.sh +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/tmux_split.sh +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/examples/unicode_commands.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/mkdocs.yml +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/package.json +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/pyproject.toml +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/ruff.toml +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/scripts/validate_tag.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/setup.cfg +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/.cmd2rc +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/__init__.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/conftest.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/echo.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/environment.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/help.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/py_locals.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/pyscript_dir.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/raises_exception.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/recursive.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/self_in_py.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/stop.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/test_print.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/relative_multiple.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/script.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/script.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/binary.bin +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/empty.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/help.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/nested.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/one_down.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/postcmds.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/precmds.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/utf8.txt +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_annotated.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_argparse.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_argparse_completer.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_argparse_subcommands.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_argparse_utils.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_categories.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_cmd2.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_commandset.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_completion.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_dynamic_complete_style.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_future_annotations.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_parsing.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_plugin.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_pt_utils.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_py_completion.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_rich_utils.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_run_pyscript.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_string_utils.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_suite_environment.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_theme.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_utils.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_utils_defining_class.py +0 -0
- {cmd2-4.3.1 → cmd2-4.4.0}/ty.toml +0 -0
|
@@ -1,3 +1,62 @@
|
|
|
1
|
+
## 4.4.0 (October 31, 2026) 🎃
|
|
2
|
+
|
|
3
|
+
- Breaking Changes
|
|
4
|
+
- `set_window_title()` is now an instance method. Call `app.set_window_title(title)` or
|
|
5
|
+
`self.set_window_title(title)` so title changes use that application's terminal.
|
|
6
|
+
- `History.get()` and `History.span()` now raise `HistoryIndexError`, an `IndexError` subclass,
|
|
7
|
+
for invalid indices and bounded ranges selecting no history items. Unbounded ranges (`:` and
|
|
8
|
+
`..`) can still return an empty dictionary. Starts before the beginning of history are
|
|
9
|
+
rejected rather than clamped. Malformed ranges, including zero endpoints, raise
|
|
10
|
+
`HistoryFormatError`, a `ValueError` subclass. The `history` command reports these selection
|
|
11
|
+
errors without a traceback hint, and index zero includes the reminder that the first command
|
|
12
|
+
is command 1.
|
|
13
|
+
- End-only ranges now start at command 1 in the full history, including persisted commands, just
|
|
14
|
+
like explicit-start ranges and single indices. Bare `history` and unbounded ranges still
|
|
15
|
+
select only the current session unless `--all` is supplied.
|
|
16
|
+
- Replaying or editing a selection containing persisted commands now requires `--all`. This
|
|
17
|
+
includes single indices and bounded ranges. Displaying or saving these selections does not
|
|
18
|
+
require `--all`, and command numbers are unchanged.
|
|
19
|
+
- Running, editing, or saving an empty history selection now fails before replay, opening an
|
|
20
|
+
editor, or creating or overwriting a file. This includes `history -o FILE` and `history -e`
|
|
21
|
+
with no selector in an empty session, and actions on searches with no matches. Empty unbounded
|
|
22
|
+
selections report `No history items in range`, while search misses report
|
|
23
|
+
`No matching history items`; displaying them still succeeds without output. The no-argument
|
|
24
|
+
`history --run` safety refusal is unchanged.
|
|
25
|
+
- `History` now records which entries were loaded before the session started instead of keeping
|
|
26
|
+
a plain start index. Entries added after `start_session()` belong to the current session
|
|
27
|
+
wherever they are placed, including entries inserted at the front of the history.
|
|
28
|
+
`session_start_index` is now computed as the index just past the last persisted entry.
|
|
29
|
+
Assigning it marks only the entries already in the list, so assign it or call
|
|
30
|
+
`start_session()` after loading entries. After the list is reordered,
|
|
31
|
+
`history[history.session_start_index:]` can omit current-session entries; use
|
|
32
|
+
`History.is_persisted()` to check an entry instead.
|
|
33
|
+
- Bug Fixes
|
|
34
|
+
- Fixed history ranges starting past the end selecting the last command. Invalid indices and
|
|
35
|
+
bounded ranges selecting no items report `No history items in range` before running, editing,
|
|
36
|
+
or saving commands.
|
|
37
|
+
- Negative single-index history selections now display and return the positive command number.
|
|
38
|
+
- History regular expressions containing range separators are now handled as regex searches.
|
|
39
|
+
Other selectors are treated as numbers or ranges only when they match the supported syntax;
|
|
40
|
+
strings such as URLs, `a:b`, `+1`, and `1_0` remain literal searches.
|
|
41
|
+
- Fixed `History.truncate()` leaving the current session's start index unchanged. Code calling
|
|
42
|
+
it directly would then have retained current-session entries hidden from unbounded ranges and
|
|
43
|
+
searches. Interactive `history` use was unaffected, since `cmd2` only truncates when
|
|
44
|
+
persisting history at exit.
|
|
45
|
+
- Fixed changing a `History` list in place moving commands between the persisted history and the
|
|
46
|
+
current session. Deleting, inserting, assigning, reordering (such as `reverse()` and
|
|
47
|
+
`sort()`), or repeating entries no longer shows persisted commands as current or hides
|
|
48
|
+
current-session commands, and `history --run` and `history --edit` still require `--all` for a
|
|
49
|
+
moved persisted command. Equal commands from different sessions remain separate entries.
|
|
50
|
+
- Fixed `set_window_title()` leaving title changes buffered until the next prompt render. The
|
|
51
|
+
title is now flushed before the method returns when no prompt is running. While a prompt or
|
|
52
|
+
`select()` menu is running, it is written on that prompt's event loop so it doesn't interleave
|
|
53
|
+
with rendering, and output errors are ignored so they can't disrupt the prompt. Titles are
|
|
54
|
+
still written to a terminal stdout when prompts use a dummy output, such as when stdin isn't a
|
|
55
|
+
terminal.
|
|
56
|
+
- Enhancements
|
|
57
|
+
- Added `History.is_persisted()` to check whether a history entry was loaded from a previous
|
|
58
|
+
session.
|
|
59
|
+
|
|
1
60
|
## 4.3.1 (October 24, 2026)
|
|
2
61
|
|
|
3
62
|
- Bug Fixes
|
|
@@ -29,6 +29,7 @@ Documentation: https://cmd2.readthedocs.io/
|
|
|
29
29
|
# import this module, many of these imports are lazy-loaded
|
|
30
30
|
# i.e. we only import the module when we use it.
|
|
31
31
|
import argparse
|
|
32
|
+
import asyncio
|
|
32
33
|
import contextlib
|
|
33
34
|
import copy
|
|
34
35
|
import dataclasses
|
|
@@ -71,7 +72,7 @@ from prompt_toolkit import (
|
|
|
71
72
|
filters,
|
|
72
73
|
print_formatted_text,
|
|
73
74
|
)
|
|
74
|
-
from prompt_toolkit.application import create_app_session, get_app
|
|
75
|
+
from prompt_toolkit.application import Application, AppSession, create_app_session, get_app
|
|
75
76
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
76
77
|
from prompt_toolkit.completion import Completer, DummyCompleter
|
|
77
78
|
from prompt_toolkit.formatted_text import ANSI, AnyFormattedText
|
|
@@ -82,9 +83,9 @@ from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent
|
|
|
82
83
|
from prompt_toolkit.keys import Keys
|
|
83
84
|
from prompt_toolkit.layout import Window, walk
|
|
84
85
|
from prompt_toolkit.layout.containers import ConditionalContainer, FloatContainer
|
|
85
|
-
from prompt_toolkit.output import DummyOutput, create_output
|
|
86
|
+
from prompt_toolkit.output import DummyOutput, Output, create_output
|
|
86
87
|
from prompt_toolkit.patch_stdout import patch_stdout
|
|
87
|
-
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, choice
|
|
88
|
+
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, choice
|
|
88
89
|
from prompt_toolkit.styles import DynamicStyle
|
|
89
90
|
from rich.console import (
|
|
90
91
|
Group,
|
|
@@ -153,7 +154,11 @@ from .exceptions import (
|
|
|
153
154
|
SkipPostcommandHooks,
|
|
154
155
|
)
|
|
155
156
|
from .history import (
|
|
157
|
+
NO_HISTORY_ITEMS,
|
|
158
|
+
NO_MATCHING_HISTORY_ITEMS,
|
|
156
159
|
History,
|
|
160
|
+
HistoryFormatError,
|
|
161
|
+
HistoryIndexError,
|
|
157
162
|
HistoryItem,
|
|
158
163
|
)
|
|
159
164
|
from .parsing import (
|
|
@@ -216,6 +221,11 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
216
221
|
from prompt_toolkit.buffer import Buffer
|
|
217
222
|
|
|
218
223
|
|
|
224
|
+
# Errors raised when prompt-toolkit can't use a terminal. For example, GitHub Actions on Windows reports
|
|
225
|
+
# isatty() as True but has no real console.
|
|
226
|
+
_TERMINAL_INIT_ERRORS = (NoConsoleScreenBufferError, AttributeError, ValueError)
|
|
227
|
+
|
|
228
|
+
|
|
219
229
|
class _SavedCmd2Env:
|
|
220
230
|
"""cmd2 environment settings that are backed up when entering an interactive Python shell."""
|
|
221
231
|
|
|
@@ -685,6 +695,26 @@ class Cmd:
|
|
|
685
695
|
self._alert_thread: threading.Thread | None = None
|
|
686
696
|
self._alert_prompt_timestamp: float = 0.0 # Uses time.monotonic()
|
|
687
697
|
|
|
698
|
+
# Members related to setting the window title. The lock is reentrant so a signal handler which sets the title
|
|
699
|
+
# while the main thread is writing one doesn't deadlock. Such a nested call only records its title, and the
|
|
700
|
+
# outer call writes it next.
|
|
701
|
+
self._window_title_lock = threading.RLock()
|
|
702
|
+
self._pending_window_title: str | None = None
|
|
703
|
+
self._writing_window_title = False
|
|
704
|
+
|
|
705
|
+
# The prompt loop on which a title callback is queued but hasn't run yet. Later titles set while it's queued
|
|
706
|
+
# only replace the pending title, which that callback writes.
|
|
707
|
+
self._window_title_callback_loop: asyncio.AbstractEventLoop | None = None
|
|
708
|
+
|
|
709
|
+
# The stdout this app was created with. When the main session uses a DummyOutput, titles are written to it if
|
|
710
|
+
# it's a terminal, including while a command's output is redirected. See _get_window_title_fallback_output().
|
|
711
|
+
self._window_title_stdout: TextIO = self.stdout
|
|
712
|
+
self._window_title_fallback_checked = False
|
|
713
|
+
self._window_title_fallback_output: Output | None = None
|
|
714
|
+
|
|
715
|
+
# The AppSession of a select() menu while it is displayed, so window titles can find its event loop.
|
|
716
|
+
self._select_app_session: AppSession | None = None
|
|
717
|
+
|
|
688
718
|
# Add functions decorated to be subcommands
|
|
689
719
|
self._register_subcommands(self)
|
|
690
720
|
|
|
@@ -834,7 +864,7 @@ class Cmd:
|
|
|
834
864
|
kwargs["output"] = create_output(stdout=self.stdout)
|
|
835
865
|
return PromptSession(**kwargs)
|
|
836
866
|
|
|
837
|
-
except
|
|
867
|
+
except _TERMINAL_INIT_ERRORS:
|
|
838
868
|
# Fallback to dummy input/output if PromptSession initialization fails.
|
|
839
869
|
# This can happen in some CI environments (like GitHub Actions on Windows)
|
|
840
870
|
# where isatty() is True but there is no real console.
|
|
@@ -3655,14 +3685,19 @@ class Cmd:
|
|
|
3655
3685
|
if not callable(prompt):
|
|
3656
3686
|
prompt = pt_filter_style(prompt)
|
|
3657
3687
|
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3688
|
+
try:
|
|
3689
|
+
with patch_stdout():
|
|
3690
|
+
try:
|
|
3691
|
+
# Set this session as the active one for UI/completion logic.
|
|
3692
|
+
self.active_session = session
|
|
3693
|
+
return session.prompt(prompt, **prompt_kwargs)
|
|
3694
|
+
finally:
|
|
3695
|
+
# Revert back to the main session.
|
|
3696
|
+
self.active_session = self.main_session
|
|
3697
|
+
finally:
|
|
3698
|
+
# Write any title whose callback the prompt's event loop didn't run. Do this after patch_stdout()
|
|
3699
|
+
# exits so the write can't race with its writer thread on the same output.
|
|
3700
|
+
self._flush_pending_window_title_quietly()
|
|
3666
3701
|
|
|
3667
3702
|
# We're not at a terminal, so we're likely reading from a file or a pipe.
|
|
3668
3703
|
prompt_obj = prompt if isinstance(prompt, (ANSI, str)) else prompt()
|
|
@@ -4790,8 +4825,13 @@ class Cmd:
|
|
|
4790
4825
|
if self._is_tty_session(self.main_session):
|
|
4791
4826
|
try:
|
|
4792
4827
|
while True:
|
|
4793
|
-
with create_app_session(input=self.main_session.input, output=self.main_session.output):
|
|
4794
|
-
|
|
4828
|
+
with create_app_session(input=self.main_session.input, output=self.main_session.output) as app_session:
|
|
4829
|
+
self._select_app_session = app_session
|
|
4830
|
+
try:
|
|
4831
|
+
result = choice(message=prompt, options=fulloptions)
|
|
4832
|
+
finally:
|
|
4833
|
+
self._select_app_session = None
|
|
4834
|
+
self._flush_pending_window_title_quietly()
|
|
4795
4835
|
if result is not None:
|
|
4796
4836
|
return result
|
|
4797
4837
|
except KeyboardInterrupt:
|
|
@@ -5446,13 +5486,14 @@ class Cmd:
|
|
|
5446
5486
|
"-a",
|
|
5447
5487
|
"--all",
|
|
5448
5488
|
action="store_true",
|
|
5449
|
-
help="
|
|
5489
|
+
help="include persisted commands; required to run or edit commands from previous sessions",
|
|
5450
5490
|
)
|
|
5451
5491
|
|
|
5452
5492
|
history_arg_help = (
|
|
5453
|
-
"empty all
|
|
5454
|
-
"a one
|
|
5455
|
-
"a..b, a:b, a:, ..b
|
|
5493
|
+
"empty current session (--all includes persisted commands)\n"
|
|
5494
|
+
"a one item by number in the full history\n"
|
|
5495
|
+
"a..b, a:b, a:, ..b inclusive ranges in the full history\n"
|
|
5496
|
+
":, .. current session (--all includes persisted commands)\n"
|
|
5456
5497
|
"string items containing string\n"
|
|
5457
5498
|
"/regex/ items matching regular expression"
|
|
5458
5499
|
)
|
|
@@ -5498,18 +5539,33 @@ class Cmd:
|
|
|
5498
5539
|
|
|
5499
5540
|
return None
|
|
5500
5541
|
|
|
5542
|
+
if args.run and not args.arg:
|
|
5543
|
+
self.perror("Cowardly refusing to run all previously entered commands.")
|
|
5544
|
+
self.perror("If this is what you want to do, specify '1:' as the range of history.")
|
|
5545
|
+
return None
|
|
5546
|
+
|
|
5501
5547
|
# If an argument was supplied, then retrieve partial contents of the history, otherwise retrieve it all
|
|
5502
|
-
|
|
5548
|
+
try:
|
|
5549
|
+
history = self._get_history(args)
|
|
5550
|
+
except (HistoryIndexError, HistoryFormatError) as ex:
|
|
5551
|
+
self.perror(str(ex))
|
|
5552
|
+
return None
|
|
5553
|
+
|
|
5554
|
+
# Selection is independent of the requested action. Empty displays are harmless.
|
|
5555
|
+
if not history and (args.run or args.edit or args.output_file):
|
|
5556
|
+
is_search = args.arg and not self.history.spanpattern.fullmatch(args.arg)
|
|
5557
|
+
self.perror(NO_MATCHING_HISTORY_ITEMS if is_search else NO_HISTORY_ITEMS)
|
|
5558
|
+
return None
|
|
5559
|
+
|
|
5560
|
+
if (args.run or args.edit) and not args.all and any(self.history.is_persisted(item) for item in history.values()):
|
|
5561
|
+
self.perror("Use --all to run or edit commands from previous sessions.")
|
|
5562
|
+
return None
|
|
5503
5563
|
|
|
5504
5564
|
if args.run:
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
stop = self.runcmds_plus_hooks(list(history.values()))
|
|
5510
|
-
self.last_result = True
|
|
5511
|
-
return stop
|
|
5512
|
-
elif args.edit:
|
|
5565
|
+
stop = self.runcmds_plus_hooks(list(history.values()))
|
|
5566
|
+
self.last_result = True
|
|
5567
|
+
return stop
|
|
5568
|
+
if args.edit:
|
|
5513
5569
|
fd, fname = tempfile.mkstemp(suffix=".txt", text=True)
|
|
5514
5570
|
fobj: TextIO
|
|
5515
5571
|
with os.fdopen(fd, "w") as fobj:
|
|
@@ -5550,26 +5606,28 @@ class Cmd:
|
|
|
5550
5606
|
def _get_history(self, args: argparse.Namespace) -> dict[int, HistoryItem]:
|
|
5551
5607
|
"""If an argument was supplied, then retrieve partial contents of the history; otherwise retrieve entire history.
|
|
5552
5608
|
|
|
5553
|
-
This function returns a dictionary with history items keyed by their 1-based index in ascending order.
|
|
5609
|
+
This function returns a dictionary with history items keyed by their positive 1-based index in ascending order.
|
|
5610
|
+
Index and range errors propagate to the caller. Empty selections return an empty dictionary.
|
|
5554
5611
|
"""
|
|
5555
5612
|
if args.arg:
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5613
|
+
if args.arg.startswith(r"/") and args.arg.endswith(r"/"):
|
|
5614
|
+
return self.history.regex_search(args.arg, args.all)
|
|
5615
|
+
if re.fullmatch(r"-?[0-9]+", args.arg):
|
|
5616
|
+
try:
|
|
5617
|
+
index = int(args.arg)
|
|
5618
|
+
except ValueError:
|
|
5619
|
+
# Python may reject integer strings exceeding its conversion limit.
|
|
5620
|
+
raise HistoryIndexError(NO_HISTORY_ITEMS) from None
|
|
5621
|
+
if index == 0:
|
|
5622
|
+
# Preserve the dedicated index-zero error and command-one reminder.
|
|
5623
|
+
self.history.get(index)
|
|
5624
|
+
return self.history.span(f"{index}:{index}")
|
|
5625
|
+
if self.history.spanpattern.fullmatch(args.arg):
|
|
5626
|
+
return self.history.span(args.arg, args.all)
|
|
5627
|
+
return self.history.str_search(args.arg, args.all)
|
|
5628
|
+
|
|
5629
|
+
# Get a copy of the history so it doesn't get mutated while we are using it.
|
|
5630
|
+
return self.history.span(":", args.all)
|
|
5573
5631
|
|
|
5574
5632
|
def _initialize_history(self, hist_file: str) -> None:
|
|
5575
5633
|
"""Initialize history using history related attributes.
|
|
@@ -5664,8 +5722,10 @@ class Cmd:
|
|
|
5664
5722
|
except ModuleNotFoundError: # pragma: no cover
|
|
5665
5723
|
import bz2 as compress_lib # type: ignore[no-redef]
|
|
5666
5724
|
|
|
5667
|
-
|
|
5668
|
-
|
|
5725
|
+
# Truncate a copy so live history numbering and the session boundary are unchanged.
|
|
5726
|
+
history = History(self.history)
|
|
5727
|
+
history.truncate(self._persistent_history_length)
|
|
5728
|
+
history_json = history.to_json()
|
|
5669
5729
|
compressed_bytes = compress_lib.compress(history_json.encode(encoding="utf-8"))
|
|
5670
5730
|
|
|
5671
5731
|
try:
|
|
@@ -5861,13 +5921,125 @@ class Cmd:
|
|
|
5861
5921
|
self._alert_queue.append(alert)
|
|
5862
5922
|
self._alert_condition.notify_all()
|
|
5863
5923
|
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5924
|
+
def set_window_title(self, title: str) -> None:
|
|
5925
|
+
"""Set the title of this application's terminal.
|
|
5926
|
+
|
|
5927
|
+
The title is written to the output used by the main prompt, custom input prompts, and
|
|
5928
|
+
select() menus. That is a dummy output when stdin or stdout isn't a terminal, or when the
|
|
5929
|
+
terminal couldn't be set up. In that case, the title is written to the stdout this app was
|
|
5930
|
+
created with instead, if that is a terminal. Otherwise, no title is written.
|
|
5931
|
+
|
|
5932
|
+
This can be called from any thread. If no prompt is running, then the title is written and
|
|
5933
|
+
flushed before this returns, and output errors propagate to the caller. Such a direct write
|
|
5934
|
+
from another thread isn't synchronized with other writers to the terminal, such as a prompt
|
|
5935
|
+
which is just starting or finishing.
|
|
5936
|
+
|
|
5937
|
+
While a prompt is running, the title is written on the prompt's event loop, even when this is
|
|
5938
|
+
called on that loop, so it doesn't interleave with rendering. It appears once the loop runs its
|
|
5939
|
+
next callback, which can be delayed while the loop is busy, such as in a slow completer. If
|
|
5940
|
+
several titles are set before then, only the latest is written. Output errors are ignored so
|
|
5941
|
+
they can't disrupt the prompt.
|
|
5867
5942
|
|
|
5868
5943
|
:param title: the new window title
|
|
5869
5944
|
"""
|
|
5870
|
-
|
|
5945
|
+
with self._window_title_lock:
|
|
5946
|
+
self._pending_window_title = title
|
|
5947
|
+
loop = self._get_prompt_loop()
|
|
5948
|
+
if loop is not None:
|
|
5949
|
+
if self._window_title_callback_loop is loop:
|
|
5950
|
+
# A callback queued on this loop hasn't run yet, and it will write this title.
|
|
5951
|
+
return
|
|
5952
|
+
|
|
5953
|
+
try:
|
|
5954
|
+
loop.call_soon_threadsafe(self._flush_pending_window_title_quietly)
|
|
5955
|
+
except RuntimeError:
|
|
5956
|
+
# The loop closed after we found it, so the prompt is no longer rendering.
|
|
5957
|
+
pass
|
|
5958
|
+
else:
|
|
5959
|
+
# If the prompt finishes before the loop runs the callback, then the prompt's
|
|
5960
|
+
# cleanup writes the pending title.
|
|
5961
|
+
self._window_title_callback_loop = loop
|
|
5962
|
+
return
|
|
5963
|
+
|
|
5964
|
+
# Keep ownership until the direct write finishes, so another caller or prompt cleanup cannot consume
|
|
5965
|
+
# this request and receive or suppress its output error.
|
|
5966
|
+
self._flush_pending_window_title()
|
|
5967
|
+
|
|
5968
|
+
def _get_prompt_loop(self) -> asyncio.AbstractEventLoop | None:
|
|
5969
|
+
"""Return the event loop of the prompt-toolkit application drawing on this terminal, if one is running.
|
|
5970
|
+
|
|
5971
|
+
An application's loop is set for its whole run, including startup and teardown, when its is_running flag
|
|
5972
|
+
is already cleared but the loop can still write to the terminal.
|
|
5973
|
+
"""
|
|
5974
|
+
apps: list[Application[Any]] = [self.active_session.app]
|
|
5975
|
+
|
|
5976
|
+
# The main thread clears these when select() finishes, so read each one only once.
|
|
5977
|
+
select_app_session = self._select_app_session
|
|
5978
|
+
if select_app_session is not None:
|
|
5979
|
+
select_app = select_app_session.app
|
|
5980
|
+
if select_app is not None:
|
|
5981
|
+
apps.append(select_app)
|
|
5982
|
+
|
|
5983
|
+
for app in apps:
|
|
5984
|
+
loop = app.loop
|
|
5985
|
+
if loop is not None:
|
|
5986
|
+
return loop
|
|
5987
|
+
return None
|
|
5988
|
+
|
|
5989
|
+
def _get_window_title_fallback_output(self) -> Output | None:
|
|
5990
|
+
"""Return an output for the stdout this app was created with, or None if that isn't a terminal.
|
|
5991
|
+
|
|
5992
|
+
Window titles use it when the main session has a DummyOutput. It's created the first time a title needs it.
|
|
5993
|
+
This isn't a cached_property because inspect.getmembers() evaluates properties while this app is constructed.
|
|
5994
|
+
"""
|
|
5995
|
+
if not self._window_title_fallback_checked:
|
|
5996
|
+
self._window_title_fallback_checked = True
|
|
5997
|
+
|
|
5998
|
+
# A stream without isatty(), or a closed one, raises AttributeError or ValueError here.
|
|
5999
|
+
with contextlib.suppress(*_TERMINAL_INIT_ERRORS):
|
|
6000
|
+
if self._window_title_stdout.isatty():
|
|
6001
|
+
self._window_title_fallback_output = create_output(stdout=self._window_title_stdout)
|
|
6002
|
+
|
|
6003
|
+
return self._window_title_fallback_output
|
|
6004
|
+
|
|
6005
|
+
def _get_window_title_output(self) -> Output:
|
|
6006
|
+
"""Return the output which window titles are written to."""
|
|
6007
|
+
output = self.main_session.output
|
|
6008
|
+
if isinstance(output, DummyOutput) and (fallback := self._get_window_title_fallback_output()) is not None:
|
|
6009
|
+
return fallback
|
|
6010
|
+
return output
|
|
6011
|
+
|
|
6012
|
+
def _flush_pending_window_title(self) -> None:
|
|
6013
|
+
"""Write and flush the most recent title passed to set_window_title(), if it hasn't been written yet."""
|
|
6014
|
+
with self._window_title_lock:
|
|
6015
|
+
if self._writing_window_title:
|
|
6016
|
+
# This thread is already writing a title, such as when a signal handler calls set_window_title().
|
|
6017
|
+
# Writing now would nest inside that write, so leave the title pending for the outer call.
|
|
6018
|
+
return
|
|
6019
|
+
|
|
6020
|
+
self._writing_window_title = True
|
|
6021
|
+
try:
|
|
6022
|
+
# Keep going until no newer title arrived during the last write, so the latest title always wins.
|
|
6023
|
+
while (title := self._pending_window_title) is not None:
|
|
6024
|
+
self._pending_window_title = None
|
|
6025
|
+
output = self._get_window_title_output()
|
|
6026
|
+
output.set_title(title)
|
|
6027
|
+
output.flush()
|
|
6028
|
+
finally:
|
|
6029
|
+
self._writing_window_title = False
|
|
6030
|
+
|
|
6031
|
+
def _flush_pending_window_title_quietly(self) -> None:
|
|
6032
|
+
"""Flush a pending window title on a prompt's event loop or during its cleanup, ignoring output errors.
|
|
6033
|
+
|
|
6034
|
+
An error here would otherwise disrupt the prompt or replace its result. Writing to a closed stream raises
|
|
6035
|
+
ValueError rather than OSError.
|
|
6036
|
+
"""
|
|
6037
|
+
with self._window_title_lock:
|
|
6038
|
+
# Either this is the queued callback, or the prompt finished and its loop won't run that callback.
|
|
6039
|
+
# Either way, the next title set during a prompt needs a new callback.
|
|
6040
|
+
self._window_title_callback_loop = None
|
|
6041
|
+
with contextlib.suppress(OSError, ValueError):
|
|
6042
|
+
self._flush_pending_window_title()
|
|
5871
6043
|
|
|
5872
6044
|
def enable_command(self, command: str) -> None:
|
|
5873
6045
|
"""Enable a command by restoring its functions.
|