summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py70
-rw-r--r--cmd2/rl_utils.py45
2 files changed, 113 insertions, 2 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 888d9531..874fdf6c 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -50,7 +50,7 @@ from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .parsing import StatementParser, Statement
# Set up readline
-from .rl_utils import rl_type, RlType
+from .rl_utils import rl_type, RlType, rl_get_point
if rl_type == RlType.NONE: # pragma: no cover
rl_warning = "Readline features including tab completion have been disabled since no \n" \
"supported version of readline was found. To resolve this, install \n" \
@@ -3191,6 +3191,74 @@ Script should contain one command per line, just like command would be typed in
runner = unittest.TextTestRunner()
runner.run(testcase)
+ def print_alert(self, alert_msg: str) -> None:
+ """
+ Used to display an important message to the user while they are at the prompt.
+ The current prompt and input lines are erased, then the alert is printed in their place, and
+ finally the prompt and input lines restored below the alert. To the user it appears as if
+ an alert message is printed above the prompt.
+
+ :param alert_msg: the message to display to the user
+ """
+ if rl_type == RlType.NONE:
+ return
+
+ import shutil
+ import colorama.ansi as ansi
+ from colorama import Cursor
+
+ visible_prompt = self.visible_prompt
+
+ # Get the size of the terminal
+ terminal_size = shutil.get_terminal_size()
+
+ # Figure out how many lines the prompt and user input take up
+ total_str_size = len(visible_prompt) + len(readline.get_line_buffer())
+ num_input_lines = int(total_str_size / terminal_size.columns) + 1
+
+ # Get the cursor's offset from the beginning of the first input line
+ cursor_input_offset = len(visible_prompt) + rl_get_point()
+
+ # Calculate what input line the cursor is on
+ cursor_input_line = int(cursor_input_offset / terminal_size.columns) + 1
+
+ # Create a string that will clear all input lines and print the alert
+ terminal_str = ''
+
+ # Move the cursor down to the last input line
+ if cursor_input_line != num_input_lines:
+ terminal_str += Cursor.DOWN(num_input_lines - cursor_input_line)
+
+ # Clear each input line from the bottom up so that the cursor ends up on the original first input line
+ terminal_str += (ansi.clear_line() + Cursor.UP(1)) * (num_input_lines - 1)
+ terminal_str += ansi.clear_line()
+
+ # Print the alert
+ terminal_str += alert_msg + '\n'
+
+ # Write the string to the terminal
+ if rl_type == RlType.GNU:
+ sys.stdout.write(terminal_str)
+ elif rl_type == RlType.PYREADLINE:
+ readline.rl.mode.console.write(terminal_str)
+
+ # Redraw the prompt and input line
+ rl_force_redisplay()
+
+ def worker(self):
+ import time
+ while True:
+ alert_msg = "\n***********************************************\n" \
+ " Message failed to send\n" \
+ "***********************************************"
+ time.sleep(5)
+ self.print_alert(alert_msg)
+
+ def do_alert(self, args):
+ import threading
+ printer = threading.Thread(target=self.worker, daemon=True)
+ printer.start()
+
def cmdloop(self, intro: Optional[str]=None) -> None:
"""This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.
diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py
index 7e49ea47..634f0c5e 100644
--- a/cmd2/rl_utils.py
+++ b/cmd2/rl_utils.py
@@ -26,13 +26,41 @@ class RlType(Enum):
# Check what implementation of readline we are using
-
rl_type = RlType.NONE
# The order of this check matters since importing pyreadline will also show readline in the modules list
if 'pyreadline' in sys.modules:
rl_type = RlType.PYREADLINE
+ from ctypes import byref
+ from ctypes.wintypes import DWORD, HANDLE
+ import atexit
+
+ # noinspection PyPep8Naming
+ def enable_win_vt100(handle: HANDLE) -> None:
+ """
+ Enables VT100 character sequences in a Windows console
+ This only works on Windows 10 and up
+ """
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
+
+ # Get the current mode for this handle in the console
+ cur_mode = DWORD(0)
+ readline.rl.console.GetConsoleMode(handle, byref(cur_mode))
+
+ # If ENABLE_VIRTUAL_TERMINAL_PROCESSING is not enabled, then enable it now
+ if (cur_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0:
+ readline.rl.console.SetConsoleMode(handle, cur_mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+
+ # Restore the original mode when we exit
+ atexit.register(readline.rl.console.SetConsoleMode, handle, cur_mode)
+
+ # Enable VT100 sequences for stdout and stderr
+ STD_OUT_HANDLE = -11
+ STD_ERROR_HANDLE = -12
+ enable_win_vt100(readline.rl.console.GetStdHandle(STD_OUT_HANDLE))
+ enable_win_vt100(readline.rl.console.GetStdHandle(STD_ERROR_HANDLE))
+
############################################################################################################
# pyreadline is incomplete in terms of the Python readline API. Add the missing functions we need.
############################################################################################################
@@ -96,3 +124,18 @@ def rl_force_redisplay() -> None:
# Call _print_prompt() first to set the new location of the prompt
readline.rl.mode._print_prompt()
readline.rl.mode._update_line()
+
+
+# noinspection PyProtectedMember
+def rl_get_point() -> int:
+ """
+ Returns the offset of the current cursor position in rl_line_buffer
+ """
+ if rl_type == RlType.GNU: # pragma: no cover
+ return ctypes.c_int.in_dll(readline_lib, "rl_point").value
+
+ elif rl_type == RlType.PYREADLINE: # pragma: no cover
+ return readline.rl.mode.l_buffer.point
+
+ else: # pragma: no cover
+ return 0