Skip to content

Commit 2caa254

Browse files
committed
Merged alert_printer stuff from master and resolved conflicts
2 parents 7ea8d3b + dcd79f3 commit 2caa254

File tree

7 files changed

+534
-55
lines changed

7 files changed

+534
-55
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
for formatting help/description text
1111
* Aliases are now sorted alphabetically
1212
* The **set** command now tab-completes settable parameter names
13+
* Added ``async_alert``, ``async_update_prompt``, and ``set_window_title`` functions
14+
* These allow you to provide feedback to the user in an asychronous fashion, meaning alerts can
15+
display when the user is still entering text at the prompt. See [async_printing.py](https://github.com/python-cmd2/cmd2/blob/master/examples/async_printing.py)
16+
for an example.
1317
* Cross-platform colored output support
1418
* ``colorama`` gets initialized properly in ``Cmd.__init()``
1519
* The ``Cmd.colors`` setting is no longer platform dependent and now has three values:

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Main Features
4040
- Trivial to provide built-in help for all commands
4141
- Built-in regression testing framework for your applications (transcript-based testing)
4242
- Transcripts for use with built-in regression can be automatically generated from `history -t`
43+
- Alerts that seamlessly print while user enters text at prompt
4344

4445
Python 2.7 support is EOL
4546
-------------------------

cmd2/cmd2.py

Lines changed: 180 additions & 49 deletions
Large diffs are not rendered by default.

cmd2/rl_utils.py

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,54 @@ class RlType(Enum):
2626

2727

2828
# Check what implementation of readline we are using
29-
3029
rl_type = RlType.NONE
3130

31+
# Tells if the terminal we are running in supports vt100 control characters
32+
vt100_support = False
33+
3234
# The order of this check matters since importing pyreadline will also show readline in the modules list
3335
if 'pyreadline' in sys.modules:
3436
rl_type = RlType.PYREADLINE
3537

38+
from ctypes import byref
39+
from ctypes.wintypes import DWORD, HANDLE
40+
import atexit
41+
42+
# Check if we are running in a terminal
43+
if sys.stdout.isatty(): # pragma: no cover
44+
# noinspection PyPep8Naming
45+
def enable_win_vt100(handle: HANDLE) -> bool:
46+
"""
47+
Enables VT100 character sequences in a Windows console
48+
This only works on Windows 10 and up
49+
:param handle: the handle on which to enable vt100
50+
:return: True if vt100 characters are enabled for the handle
51+
"""
52+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
53+
54+
# Get the current mode for this handle in the console
55+
cur_mode = DWORD(0)
56+
readline.rl.console.GetConsoleMode(handle, byref(cur_mode))
57+
58+
retVal = False
59+
60+
# Check if ENABLE_VIRTUAL_TERMINAL_PROCESSING is already enabled
61+
if (cur_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0:
62+
retVal = True
63+
64+
elif readline.rl.console.SetConsoleMode(handle, cur_mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING):
65+
# Restore the original mode when we exit
66+
atexit.register(readline.rl.console.SetConsoleMode, handle, cur_mode)
67+
retVal = True
68+
69+
return retVal
70+
71+
# Enable VT100 sequences for stdout and stderr
72+
STD_OUT_HANDLE = -11
73+
STD_ERROR_HANDLE = -12
74+
vt100_support = (enable_win_vt100(readline.rl.console.GetStdHandle(STD_OUT_HANDLE)) and
75+
enable_win_vt100(readline.rl.console.GetStdHandle(STD_ERROR_HANDLE)))
76+
3677
############################################################################################################
3778
# pyreadline is incomplete in terms of the Python readline API. Add the missing functions we need.
3879
############################################################################################################
@@ -74,9 +115,13 @@ def pyreadline_remove_history_item(pos: int) -> None:
74115
import ctypes
75116
readline_lib = ctypes.CDLL(readline.__file__)
76117

118+
# Check if we are running in a terminal
119+
if sys.stdout.isatty():
120+
vt100_support = True
121+
77122

78123
# noinspection PyProtectedMember
79-
def rl_force_redisplay() -> None:
124+
def rl_force_redisplay() -> None: # pragma: no cover
80125
"""
81126
Causes readline to display the prompt and input text wherever the cursor is and start
82127
reading input from this location. This is the proper way to restore the input line after
@@ -85,14 +130,77 @@ def rl_force_redisplay() -> None:
85130
if not sys.stdout.isatty():
86131
return
87132

88-
if rl_type == RlType.GNU: # pragma: no cover
133+
if rl_type == RlType.GNU:
89134
readline_lib.rl_forced_update_display()
90135

91136
# After manually updating the display, readline asks that rl_display_fixed be set to 1 for efficiency
92137
display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed")
93138
display_fixed.value = 1
94139

95-
elif rl_type == RlType.PYREADLINE: # pragma: no cover
140+
elif rl_type == RlType.PYREADLINE:
96141
# Call _print_prompt() first to set the new location of the prompt
97142
readline.rl.mode._print_prompt()
98143
readline.rl.mode._update_line()
144+
145+
146+
# noinspection PyProtectedMember
147+
def rl_get_point() -> int: # pragma: no cover
148+
"""
149+
Returns the offset of the current cursor position in rl_line_buffer
150+
"""
151+
if rl_type == RlType.GNU:
152+
return ctypes.c_int.in_dll(readline_lib, "rl_point").value
153+
154+
elif rl_type == RlType.PYREADLINE:
155+
return readline.rl.mode.l_buffer.point
156+
157+
else:
158+
return 0
159+
160+
161+
# noinspection PyProtectedMember
162+
def rl_set_prompt(prompt: str) -> None: # pragma: no cover
163+
"""
164+
Sets readline's prompt
165+
:param prompt: the new prompt value
166+
"""
167+
safe_prompt = rl_make_safe_prompt(prompt)
168+
169+
if rl_type == RlType.GNU:
170+
encoded_prompt = bytes(safe_prompt, encoding='utf-8')
171+
readline_lib.rl_set_prompt(encoded_prompt)
172+
173+
elif rl_type == RlType.PYREADLINE:
174+
readline.rl._set_prompt(safe_prompt)
175+
176+
177+
def rl_make_safe_prompt(prompt: str) -> str: # pragma: no cover
178+
"""Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes.
179+
180+
:param prompt: original prompt
181+
:return: prompt safe to pass to GNU Readline
182+
"""
183+
if rl_type == RlType.GNU:
184+
# start code to tell GNU Readline about beginning of invisible characters
185+
start = "\x01"
186+
187+
# end code to tell GNU Readline about end of invisible characters
188+
end = "\x02"
189+
190+
escaped = False
191+
result = ""
192+
193+
for c in prompt:
194+
if c == "\x1b" and not escaped:
195+
result += start + c
196+
escaped = True
197+
elif c.isalpha() and escaped:
198+
result += c + end
199+
escaped = False
200+
else:
201+
result += c
202+
203+
return result
204+
205+
else:
206+
return prompt

docs/unfreefeatures.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,33 @@ Exit code to shell
209209
The ``self.exit_code`` attribute of your ``cmd2`` application controls
210210
what exit code is sent to the shell when your application exits from
211211
``cmdloop()``.
212+
213+
214+
Asynchronous Feedback
215+
=====================
216+
``cmd2`` provides two functions to provide asynchronous feedback to the user without interfering with
217+
the command line. This means the feedback is provided to the user when they are still entering text at
218+
the prompt. To use this functionality, the application must be running in a terminal that supports
219+
VT100 control characters and readline. Linux, Mac, and Windows 10 and greater all support these.
220+
221+
async_alert()
222+
Used to display an important message to the user while they are at the prompt in between commands.
223+
To the user it appears as if an alert message is printed above the prompt and their current input
224+
text and cursor location is left alone.
225+
226+
async_update_prompt()
227+
Updates the prompt while the user is still typing at it. This is good for alerting the user to system
228+
changes dynamically in between commands. For instance you could alter the color of the prompt to indicate
229+
a system status or increase a counter to report an event.
230+
231+
``cmd2`` also provides a function to change the title of the terminal window. This feature requires the
232+
application be running in a terminal that supports VT100 control characters. Linux, Mac, and Windows 10 and
233+
greater all support these.
234+
235+
set_window_title()
236+
Sets the terminal window title
237+
238+
239+
The easiest way to understand these functions is to see the AsyncPrinting_ example for a demonstration.
240+
241+
.. _AsyncPrinting: https://github.com/python-cmd2/cmd2/blob/master/examples/async_printing.py

0 commit comments

Comments
 (0)