From d10badce0e9c12aa341c2de247523b7a494761fc Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 21 Sep 2018 20:40:00 -0400 Subject: First version of async printing example --- examples/async_printing.py | 121 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100755 examples/async_printing.py (limited to 'examples/async_printing.py') diff --git a/examples/async_printing.py b/examples/async_printing.py new file mode 100755 index 00000000..65e83bbe --- /dev/null +++ b/examples/async_printing.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# coding=utf-8 +"""A simple example demonstrating an application that asynchronously prints alerts and updates the prompt""" + +import random +import threading +import time +from typing import List + +import cmd2 + +ALERTS = ["Mail server is down", + "Blockage detected in sector 4", + "Ants have overrun the break room", + "Your next appointment has arrived", + "Christmas bonuses are cancelled", + "Mandatory overtime this weekend", + "Jimmy quit", + "Jody got married", + "Is there anyone on board who knows how to fly a plane?", + "Gentlemen, you can't fight in here. This is the War Room!", + "Your mom goes to college!"] + + +def get_alerts() -> List[str]: + """ Randomly generates alerts for testing purposes """ + + rand_num = random.randint(1, 20) + if rand_num > 3: + return [] + + alerts = [] + for i in range(0, rand_num): + cur_alert = random.choice(ALERTS) + if cur_alert in alerts: + i -= 1 + else: + alerts.append(cur_alert) + + return alerts + + +def get_alert_str() -> str: + """ + Combines alerts into one string that can be printed to the terminal + :return: the alert string + """ + alert_str = '' + alerts = get_alerts() + + longest_alert = max(ALERTS, key=len) + num_astericks = len(longest_alert) + 8 + + for i, cur_alert in enumerate(alerts): + # Use padding to center the alert + padding = ' ' * int((num_astericks - len(cur_alert)) / 2) + + if i > 0: + alert_str += '\n' + alert_str += '*' * num_astericks + '\n' + alert_str += padding + cur_alert + padding + '\n' + alert_str += '*' * num_astericks + '\n' + + return alert_str + + +class AlerterApp(cmd2.Cmd): + """ An app that shows off async_alert() and async_update_prompt() """ + + def __init__(self, *args, **kwargs) -> None: + """ Initializer """ + + super().__init__(*args, **kwargs) + + self.prompt = "(APR)> " + + # The thread that will asynchronously alert the user of events + self._stop_thread = False + self._alerter_thread = threading.Thread(name='alerter', target=self._alerter_thread_func) + + # Create some hooks to handle the starting and stopping of our thread + self.register_preloop_hook(self._preloop_hook) + self.register_postloop_hook(self._postloop_hook) + + def _preloop_hook(self) -> None: + """ Start the alerter thread """ + + # This function runs after cmdloop() locks _terminal_lock, which will be locked until the prompt appears. + # Therefore it is safe to start our thread since there is no risk of it alerting before the prompt is displayed. + self._stop_thread = False + self._alerter_thread.start() + + def _postloop_hook(self) -> None: + """ Stops the alerter thread """ + self._stop_thread = True + if self._alerter_thread.is_alive(): + self._alerter_thread.join() + + def _alerter_thread_func(self) -> None: + """ Prints alerts and updates the prompt any time the prompt is showing """ + + while not self._stop_thread: + # Always acquire _terminal_lock before printing alerts or updating the prompt + # To keep the app responsive, do not block on this call + if self._terminal_lock.acquire(blocking=False): + + # We have the terminal lock. See if any alerts need to be printed. + alert_str = get_alert_str() + + if alert_str: + self.async_alert(alert_str) + + # Don't forget to release the lock + self._terminal_lock.release() + + time.sleep(0.5) + + +if __name__ == '__main__': + app = AlerterApp() + app.cmdloop() -- cgit v1.2.1 From 3f57b97df8cd5576547bddea98cc72238f0c6072 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 21 Sep 2018 21:21:21 -0400 Subject: Updating prompt in example app --- examples/async_printing.py | 50 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) (limited to 'examples/async_printing.py') diff --git a/examples/async_printing.py b/examples/async_printing.py index 65e83bbe..69880ee7 100755 --- a/examples/async_printing.py +++ b/examples/async_printing.py @@ -8,6 +8,7 @@ import time from typing import List import cmd2 +from colorama import Fore ALERTS = ["Mail server is down", "Blockage detected in sector 4", @@ -23,8 +24,10 @@ ALERTS = ["Mail server is down", def get_alerts() -> List[str]: - """ Randomly generates alerts for testing purposes """ - + """ + Randomly reports alerts + :return: the list of alerts + """ rand_num = random.randint(1, 20) if rand_num > 3: return [] @@ -40,7 +43,7 @@ def get_alerts() -> List[str]: return alerts -def get_alert_str() -> str: +def generate_alert_str() -> str: """ Combines alerts into one string that can be printed to the terminal :return: the alert string @@ -72,7 +75,12 @@ class AlerterApp(cmd2.Cmd): super().__init__(*args, **kwargs) - self.prompt = "(APR)> " + self._orig_prompt = "(APR)> " + self.prompt = self._orig_prompt + + self.intro = "\nWatch as this application prints alerts and updates the prompt.\n" + self.intro += "This will only happen when the prompt is present. Notice how it doesn't\n" + self.intro += "interfere with your typing.\n" # The thread that will asynchronously alert the user of events self._stop_thread = False @@ -96,6 +104,28 @@ class AlerterApp(cmd2.Cmd): if self._alerter_thread.is_alive(): self._alerter_thread.join() + def _generate_new_prompt(self) -> str: + """ + Randomly generates a new prompt + :return: the new prompt + """ + rand_num = random.randint(1, 20) + + status_color = Fore.RESET + + if rand_num == 1: + status_color = Fore.LIGHTRED_EX + elif rand_num == 2: + status_color = Fore.LIGHTYELLOW_EX + elif rand_num == 3: + status_color = Fore.CYAN + elif rand_num == 4: + status_color = Fore.LIGHTGREEN_EX + elif rand_num == 5: + status_color = Fore.LIGHTBLUE_EX + + return status_color + self._orig_prompt + Fore.RESET + def _alerter_thread_func(self) -> None: """ Prints alerts and updates the prompt any time the prompt is showing """ @@ -104,11 +134,16 @@ class AlerterApp(cmd2.Cmd): # To keep the app responsive, do not block on this call if self._terminal_lock.acquire(blocking=False): - # We have the terminal lock. See if any alerts need to be printed. - alert_str = get_alert_str() + # Get any alerts that need to be printed + alert_str = generate_alert_str() + + # Generate a new prompt + new_prompt = self._generate_new_prompt() if alert_str: - self.async_alert(alert_str) + self._async_alert(alert_str, new_prompt) + elif new_prompt != self.prompt: + self._async_update_prompt(new_prompt) # Don't forget to release the lock self._terminal_lock.release() @@ -118,4 +153,5 @@ class AlerterApp(cmd2.Cmd): if __name__ == '__main__': app = AlerterApp() + app.set_window_title("Asynchronous Printer Test") app.cmdloop() -- cgit v1.2.1 From 831435c445803da76a6e46061417c6708bbfa9d7 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 22 Sep 2018 13:58:04 -0400 Subject: Recreating the thread in preloop to support multiple calls of cmdloop --- examples/async_printing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'examples/async_printing.py') diff --git a/examples/async_printing.py b/examples/async_printing.py index 69880ee7..981d39e3 100755 --- a/examples/async_printing.py +++ b/examples/async_printing.py @@ -84,7 +84,7 @@ class AlerterApp(cmd2.Cmd): # The thread that will asynchronously alert the user of events self._stop_thread = False - self._alerter_thread = threading.Thread(name='alerter', target=self._alerter_thread_func) + self._alerter_thread = threading.Thread() # Create some hooks to handle the starting and stopping of our thread self.register_preloop_hook(self._preloop_hook) @@ -96,6 +96,8 @@ class AlerterApp(cmd2.Cmd): # This function runs after cmdloop() locks _terminal_lock, which will be locked until the prompt appears. # Therefore it is safe to start our thread since there is no risk of it alerting before the prompt is displayed. self._stop_thread = False + + self._alerter_thread = threading.Thread(name='alerter', target=self._alerter_thread_func) self._alerter_thread.start() def _postloop_hook(self) -> None: -- cgit v1.2.1 From c5a667d42acc09693b1c5ebe70f3c92d7e23e20e Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 22 Sep 2018 14:49:09 -0400 Subject: Added more alerts and updated documentation --- examples/async_printing.py | 51 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 12 deletions(-) (limited to 'examples/async_printing.py') diff --git a/examples/async_printing.py b/examples/async_printing.py index 981d39e3..db2485fe 100755 --- a/examples/async_printing.py +++ b/examples/async_printing.py @@ -16,11 +16,39 @@ ALERTS = ["Mail server is down", "Your next appointment has arrived", "Christmas bonuses are cancelled", "Mandatory overtime this weekend", - "Jimmy quit", - "Jody got married", + "Jimmy quit. Jody got married.", "Is there anyone on board who knows how to fly a plane?", "Gentlemen, you can't fight in here. This is the War Room!", - "Your mom goes to college!"] + "Your mom goes to college!", + "I'm an ugly stinking llama. Llama face!", + "That's some bad hat, Harry", + "I love the smell of napalm in the morning", + "You're gonna need a bigger boat", + "Snakes, why did it have to be snakes?", + "No, I am your father", + "I'm Batman", + "Goonies never say die", + "You're killing me Smalls", + "It's alive!! It's alive!!", + "Houston, we have a problem!", + "These go to 11", + "Soylent Green is people!!", + "You shall not pass!!", + "KHAAN!!", + "Get to tha choppa!", + "I've heard it both ways", + "Yo Adrian, I did it!!", + "Just what do you think you're doing, Dave?", + "Welcome... to the real world", + "See this? This... is my boomstick!", + "Shop smart. Shop S-MART!", + "It's not the years, it's the mileage", + "I am not an elephant! I am not an animal! I am a human being!", + "Great Scott!!", + "This is heavy", + "Do... or do not. There is no try.", + "Game over man, GAME OVER!", + "SHALL WE PLAY A GAME?"] def get_alerts() -> List[str]: @@ -29,16 +57,12 @@ def get_alerts() -> List[str]: :return: the list of alerts """ rand_num = random.randint(1, 20) - if rand_num > 3: + if rand_num > 2: return [] alerts = [] for i in range(0, rand_num): - cur_alert = random.choice(ALERTS) - if cur_alert in alerts: - i -= 1 - else: - alerts.append(cur_alert) + alerts.append(random.choice(ALERTS)) return alerts @@ -92,9 +116,9 @@ class AlerterApp(cmd2.Cmd): def _preloop_hook(self) -> None: """ Start the alerter thread """ - - # This function runs after cmdloop() locks _terminal_lock, which will be locked until the prompt appears. - # Therefore it is safe to start our thread since there is no risk of it alerting before the prompt is displayed. + # This runs after cmdloop() acquires self._terminal_lock, which will be locked until the prompt appears. + # Therefore this is the best place to start the alerter thread since there is no risk of it alerting + # before the prompt is displayed. self._stop_thread = False self._alerter_thread = threading.Thread(name='alerter', target=self._alerter_thread_func) @@ -102,6 +126,9 @@ class AlerterApp(cmd2.Cmd): def _postloop_hook(self) -> None: """ Stops the alerter thread """ + + # After this function returns, cmdloop() releases self._terminal_lock which could make the alerter + # thread think the prompt is on screen. Therefore this is the best place to stop the alerter thread. self._stop_thread = True if self._alerter_thread.is_alive(): self._alerter_thread.join() -- cgit v1.2.1 From dc4b313ac20ac538f663e563d955c18c13a9d92b Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 23 Sep 2018 17:00:35 -0400 Subject: Made demo more instructive --- examples/async_printing.py | 195 ++++++++++++++++++++++++--------------------- 1 file changed, 103 insertions(+), 92 deletions(-) (limited to 'examples/async_printing.py') diff --git a/examples/async_printing.py b/examples/async_printing.py index db2485fe..23147865 100755 --- a/examples/async_printing.py +++ b/examples/async_printing.py @@ -7,88 +7,20 @@ import threading import time from typing import List -import cmd2 from colorama import Fore -ALERTS = ["Mail server is down", - "Blockage detected in sector 4", - "Ants have overrun the break room", - "Your next appointment has arrived", - "Christmas bonuses are cancelled", - "Mandatory overtime this weekend", - "Jimmy quit. Jody got married.", - "Is there anyone on board who knows how to fly a plane?", - "Gentlemen, you can't fight in here. This is the War Room!", - "Your mom goes to college!", - "I'm an ugly stinking llama. Llama face!", - "That's some bad hat, Harry", - "I love the smell of napalm in the morning", - "You're gonna need a bigger boat", - "Snakes, why did it have to be snakes?", - "No, I am your father", - "I'm Batman", - "Goonies never say die", - "You're killing me Smalls", - "It's alive!! It's alive!!", - "Houston, we have a problem!", - "These go to 11", - "Soylent Green is people!!", - "You shall not pass!!", - "KHAAN!!", - "Get to tha choppa!", - "I've heard it both ways", - "Yo Adrian, I did it!!", - "Just what do you think you're doing, Dave?", - "Welcome... to the real world", - "See this? This... is my boomstick!", - "Shop smart. Shop S-MART!", - "It's not the years, it's the mileage", - "I am not an elephant! I am not an animal! I am a human being!", - "Great Scott!!", - "This is heavy", - "Do... or do not. There is no try.", - "Game over man, GAME OVER!", - "SHALL WE PLAY A GAME?"] - - -def get_alerts() -> List[str]: - """ - Randomly reports alerts - :return: the list of alerts - """ - rand_num = random.randint(1, 20) - if rand_num > 2: - return [] - - alerts = [] - for i in range(0, rand_num): - alerts.append(random.choice(ALERTS)) - - return alerts - - -def generate_alert_str() -> str: - """ - Combines alerts into one string that can be printed to the terminal - :return: the alert string - """ - alert_str = '' - alerts = get_alerts() - - longest_alert = max(ALERTS, key=len) - num_astericks = len(longest_alert) + 8 - - for i, cur_alert in enumerate(alerts): - # Use padding to center the alert - padding = ' ' * int((num_astericks - len(cur_alert)) / 2) - - if i > 0: - alert_str += '\n' - alert_str += '*' * num_astericks + '\n' - alert_str += padding + cur_alert + padding + '\n' - alert_str += '*' * num_astericks + '\n' - - return alert_str +import cmd2 + +ALERTS = ["Watch as this application prints alerts and updates the prompt", + "This will only happen when the prompt is present", + "Notice how it doesn't interfere with your typing or cursor location", + "Go ahead and type some stuff and move the cursor throughout the line", + "Keep typing...", + "Move that cursor...", + "Pretty seamless, eh?", + "You can stop and start the alerts by typing stop_alerts and start_alerts", + "This demo will now continue to print alerts at random intervals" + ] class AlerterApp(cmd2.Cmd): @@ -99,16 +31,13 @@ class AlerterApp(cmd2.Cmd): super().__init__(*args, **kwargs) - self._orig_prompt = "(APR)> " - self.prompt = self._orig_prompt - - self.intro = "\nWatch as this application prints alerts and updates the prompt.\n" - self.intro += "This will only happen when the prompt is present. Notice how it doesn't\n" - self.intro += "interfere with your typing.\n" + self.prompt = "(APR)> " # The thread that will asynchronously alert the user of events self._stop_thread = False self._alerter_thread = threading.Thread() + self._alert_count = 0 + self._next_alert_time = 0 # Create some hooks to handle the starting and stopping of our thread self.register_preloop_hook(self._preloop_hook) @@ -118,7 +47,8 @@ class AlerterApp(cmd2.Cmd): """ Start the alerter thread """ # This runs after cmdloop() acquires self._terminal_lock, which will be locked until the prompt appears. # Therefore this is the best place to start the alerter thread since there is no risk of it alerting - # before the prompt is displayed. + # before the prompt is displayed. You can also start it via a command if its not something that should + # be running during the entire application. See do_start_alerts(). self._stop_thread = False self._alerter_thread = threading.Thread(name='alerter', target=self._alerter_thread_func) @@ -129,13 +59,87 @@ class AlerterApp(cmd2.Cmd): # After this function returns, cmdloop() releases self._terminal_lock which could make the alerter # thread think the prompt is on screen. Therefore this is the best place to stop the alerter thread. + # You can also stop it via a command. See do_stop_alerts(). + self._stop_thread = True + if self._alerter_thread.is_alive(): + self._alerter_thread.join() + + def do_start_alerts(self, _): + """ Starts the alerter thread """ + if self._alerter_thread.is_alive(): + print("The alert thread is already started") + else: + self._stop_thread = False + self._alerter_thread = threading.Thread(name='alerter', target=self._alerter_thread_func) + self._alerter_thread.start() + + def do_stop_alerts(self, _): + """ Stops the alerter thread """ self._stop_thread = True if self._alerter_thread.is_alive(): self._alerter_thread.join() + else: + print("The alert thread is already stopped") - def _generate_new_prompt(self) -> str: + def _get_alerts(self) -> List[str]: + """ + Reports alerts + :return: the list of alerts """ - Randomly generates a new prompt + global ALERTS + + cur_time = time.monotonic() + if cur_time < self._next_alert_time: + return [] + + alerts = [] + + if self._alert_count < len(ALERTS): + alerts.append(ALERTS[self._alert_count]) + self._alert_count += 1 + self._next_alert_time = cur_time + 4 + + else: + rand_num = random.randint(1, 20) + if rand_num > 2: + return [] + + for i in range(0, rand_num): + alerts.append("Alert {}".format(self._alert_count)) + self._alert_count += 1 + + self._next_alert_time = 0 + + return alerts + + def _generate_alert_str(self) -> str: + """ + Combines alerts into one string that can be printed to the terminal + :return: the alert string + """ + global ALERTS + + alert_str = '' + alerts = self._get_alerts() + + longest_alert = max(ALERTS, key=len) + num_astericks = len(longest_alert) + 8 + + for i, cur_alert in enumerate(alerts): + # Use padding to center the alert + padding = ' ' * int((num_astericks - len(cur_alert)) / 2) + + if i > 0: + alert_str += '\n' + alert_str += '*' * num_astericks + '\n' + alert_str += padding + cur_alert + padding + '\n' + alert_str += '*' * num_astericks + '\n' + + return alert_str + + def _generate_colored_prompt(self) -> str: + """ + Randomly generates a colored the prompt :return: the new prompt """ rand_num = random.randint(1, 20) @@ -153,24 +157,31 @@ class AlerterApp(cmd2.Cmd): elif rand_num == 5: status_color = Fore.LIGHTBLUE_EX - return status_color + self._orig_prompt + Fore.RESET + return status_color + self.visible_prompt + Fore.RESET def _alerter_thread_func(self) -> None: """ Prints alerts and updates the prompt any time the prompt is showing """ + self._alert_count = 0 + self._next_alert_time = 0 + while not self._stop_thread: # Always acquire _terminal_lock before printing alerts or updating the prompt # To keep the app responsive, do not block on this call if self._terminal_lock.acquire(blocking=False): # Get any alerts that need to be printed - alert_str = generate_alert_str() + alert_str = self._generate_alert_str() # Generate a new prompt - new_prompt = self._generate_new_prompt() + new_prompt = self._generate_colored_prompt() + # Check if we have alerts to print if alert_str: + # new_prompt is an optional parameter to _async_alert() self._async_alert(alert_str, new_prompt) + + # No alerts needed to be printed, check if the prompt changed elif new_prompt != self.prompt: self._async_update_prompt(new_prompt) -- cgit v1.2.1 From 10dd79e9dee5f0286cd413d513645083e7c44e8a Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 25 Sep 2018 00:19:20 -0400 Subject: Made async stuff public --- examples/async_printing.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'examples/async_printing.py') diff --git a/examples/async_printing.py b/examples/async_printing.py index 23147865..feda24c0 100755 --- a/examples/async_printing.py +++ b/examples/async_printing.py @@ -45,7 +45,7 @@ class AlerterApp(cmd2.Cmd): def _preloop_hook(self) -> None: """ Start the alerter thread """ - # This runs after cmdloop() acquires self._terminal_lock, which will be locked until the prompt appears. + # This runs after cmdloop() acquires self.terminal_lock, which will be locked until the prompt appears. # Therefore this is the best place to start the alerter thread since there is no risk of it alerting # before the prompt is displayed. You can also start it via a command if its not something that should # be running during the entire application. See do_start_alerts(). @@ -57,7 +57,7 @@ class AlerterApp(cmd2.Cmd): def _postloop_hook(self) -> None: """ Stops the alerter thread """ - # After this function returns, cmdloop() releases self._terminal_lock which could make the alerter + # After this function returns, cmdloop() releases self.terminal_lock which could make the alerter # thread think the prompt is on screen. Therefore this is the best place to stop the alerter thread. # You can also stop it via a command. See do_stop_alerts(). self._stop_thread = True @@ -166,9 +166,9 @@ class AlerterApp(cmd2.Cmd): self._next_alert_time = 0 while not self._stop_thread: - # Always acquire _terminal_lock before printing alerts or updating the prompt + # Always acquire terminal_lock before printing alerts or updating the prompt # To keep the app responsive, do not block on this call - if self._terminal_lock.acquire(blocking=False): + if self.terminal_lock.acquire(blocking=False): # Get any alerts that need to be printed alert_str = self._generate_alert_str() @@ -178,15 +178,15 @@ class AlerterApp(cmd2.Cmd): # Check if we have alerts to print if alert_str: - # new_prompt is an optional parameter to _async_alert() - self._async_alert(alert_str, new_prompt) + # new_prompt is an optional parameter to async_alert() + self.async_alert(alert_str, new_prompt) # No alerts needed to be printed, check if the prompt changed elif new_prompt != self.prompt: - self._async_update_prompt(new_prompt) + self.async_update_prompt(new_prompt) # Don't forget to release the lock - self._terminal_lock.release() + self.terminal_lock.release() time.sleep(0.5) -- cgit v1.2.1 From ecbaf000ca0384b5caafb0d79725a127e65bb2d5 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 25 Sep 2018 17:57:52 -0400 Subject: Added more examples and documentation --- examples/async_printing.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'examples/async_printing.py') diff --git a/examples/async_printing.py b/examples/async_printing.py index feda24c0..a4165ae8 100755 --- a/examples/async_printing.py +++ b/examples/async_printing.py @@ -1,6 +1,9 @@ #!/usr/bin/env python # coding=utf-8 -"""A simple example demonstrating an application that asynchronously prints alerts and updates the prompt""" +""" +A simple example demonstrating an application that asynchronously prints alerts, updates the prompt +and changes the window title +""" import random import threading @@ -18,6 +21,7 @@ ALERTS = ["Watch as this application prints alerts and updates the prompt", "Keep typing...", "Move that cursor...", "Pretty seamless, eh?", + "Feedback can also be given in the window title. Notice the arg count up there?", "You can stop and start the alerts by typing stop_alerts and start_alerts", "This demo will now continue to print alerts at random intervals" ] @@ -105,8 +109,8 @@ class AlerterApp(cmd2.Cmd): return [] for i in range(0, rand_num): - alerts.append("Alert {}".format(self._alert_count)) self._alert_count += 1 + alerts.append("Alert {}".format(self._alert_count)) self._next_alert_time = 0 @@ -180,6 +184,8 @@ class AlerterApp(cmd2.Cmd): if alert_str: # new_prompt is an optional parameter to async_alert() self.async_alert(alert_str, new_prompt) + new_title = "Alerts Printed: {}".format(self._alert_count) + self.set_window_title(new_title) # No alerts needed to be printed, check if the prompt changed elif new_prompt != self.prompt: -- cgit v1.2.1