From bd68c478d7384f5f5e03bbbd0f46398546dff61e Mon Sep 17 00:00:00 2001 From: penrods Date: Sat, 25 Feb 2017 21:53:43 -0800 Subject: [PATCH] First check-in of enhanced command line interface (CLI): * Uses curses * Displays a "chat history" with requests and responses * Shows filtered logs from mycroft-skills.log, mycroft-voice.log * Start of framework for special ":" commands (for log searching, etc) --- mycroft/client/text/main.py | 277 ++++++++++++++++++++++++++++++++---- 1 file changed, 249 insertions(+), 28 deletions(-) diff --git a/mycroft/client/text/main.py b/mycroft/client/text/main.py index 43a757e086..fea1580d8c 100644 --- a/mycroft/client/text/main.py +++ b/mycroft/client/text/main.py @@ -18,73 +18,294 @@ import sys import time +import subprocess +from cStringIO import StringIO from threading import Thread, Lock +import curses +import curses.ascii + from mycroft.messagebus.client.ws import WebsocketClient from mycroft.messagebus.message import Message from mycroft.tts import TTSFactory from mycroft.util.log import getLogger -tts = TTSFactory.create() +tts = None ws = None mutex = Lock() logger = getLogger("CLIClient") +utterances = [] +chat = [] +mergedLog = [] +line = "What time is it" +bQuiet = '--quiet' in sys.argv +scr = None + +############################################################################## +# Helper functions + +def clamp(n, smallest, largest): + return max(smallest, min(n, largest)) + + +def stripNonAscii(text): + return ''.join([i if ord(i) < 128 else ' ' for i in text]) + + +############################################################################## +# Log file monitoring + +class LogMonitorThread(Thread): + def __init__(self, filename): + Thread.__init__(self) + self.filename = filename + + def run(self): + global mergedLog + + proc = subprocess.Popen(["tail", "-f", self.filename], + stdout=subprocess.PIPE) + while True: + output = proc.stdout.readline().strip() + if output == "" and proc.poll() is not None: + break + + # TODO: Filter log output (black and white listing lines) + if "enclosure.mouth.viseme" in output: + continue + + if output: + mergedLog.append(output) + draw_screen() + + +def startLogMonitor(filename): + thread = LogMonitorThread(filename) + thread.setDaemon(True) # this thread won't prevent prog from exiting + thread.start() + + +############################################################################## +# Capturing output from Mycroft def handle_speak(event): + global chat mutex.acquire() - ws.emit(Message("recognizer_loop:audio_output_start")) + if not bQuiet: + ws.emit(Message("recognizer_loop:audio_output_start")) try: utterance = event.data.get('utterance') - print(">> " + utterance) - tts.execute(utterance) + chat.append(">> " + utterance) + draw_screen() + if not bQuiet: + if not tts: + tts = TTSFactory.create() + tts.init(ws) + tts.execute(utterance) finally: mutex.release() - ws.emit(Message("recognizer_loop:audio_output_end")) - - -def handle_quiet(event): - try: - utterance = event.data.get('utterance') - print(">> " + utterance) - finally: - pass + if not bQuiet: + ws.emit(Message("recognizer_loop:audio_output_end")) def connect(): + # Once the websocket has connected, just watch it for speak events ws.run_forever() +############################################################################## +# Screen handling -def main(): - global ws - ws = WebsocketClient() - tts.init(ws) - if '--quiet' in sys.argv: - ws.on('speak', handle_quiet) +def init_screen(): + global CLR_CHAT_HEADING + global CLR_CHAT_RESP + global CLR_CHAT_QUERY + global CLR_CMDLINE + global CLR_INPUT + global CLR_LOG + global CLR_LOG_DEBUG + + if curses.has_colors(): + bg = curses.COLOR_BLACK + for i in range(0, curses.COLORS): + curses.init_pair(i + 1, i, bg) + + # Colors + # 1 = black on black + # 2 = dk red + # 3 = dk green + # 4 = dk yellow + # 5 = dk blue + # 6 = dk purple + # 7 = dk cyan + # 8 = lt gray + # 9 = dk gray + # 10= red + # 11= green + # 12= yellow + # 13= blue + # 14= purple + # 15= cyan + # 16= white + + CLR_CHAT_HEADING = curses.color_pair(3) + CLR_CHAT_RESP = curses.color_pair(7) + CLR_CHAT_QUERY = curses.color_pair(8) + CLR_CMDLINE = curses.color_pair(15) + CLR_INPUT = curses.color_pair(16) + CLR_LOG = curses.color_pair(8) + CLR_LOG_DEBUG = curses.color_pair(4) + +def draw_screen(): + global scr + scr.clear() + + # Display log output at the top + scr.addstr(0, 0, "Log Output", curses.A_REVERSE) + scr.addstr(1, 0, "=" * (curses.COLS-1), CLR_LOG) + cLogLines = curses.LINES-13 + + cLogs = len(mergedLog) + y = 2 + for i in range(clamp(cLogs-cLogLines, 0, cLogs-1), cLogs): + log = mergedLog[i] + log = log[26:] # skip date/time at the front of log line + + # Categorize log line + if "Skills - DEBUG - " in log: + log = log.replace("Skills - DEBUG - ", "") + clr = CLR_LOG_DEBUG + else: + clr = CLR_LOG + + # limit line to screen width (show tail end) + log = ("..."+log[-(curses.COLS-3):]) if len(log) > curses.COLS else log + scr.addstr(y, 0, log, clr) + y += 1 + + # Log legend in the lower-right + scr.addstr(curses.LINES-10, curses.COLS/2 + 2, "Log Output Legend", curses.A_REVERSE) + scr.addstr(curses.LINES-9, curses.COLS/2 + 2, "=" * (curses.COLS/2 - 4)) + scr.addstr(curses.LINES-8, curses.COLS/2 + 2, "mycroft-skills.log, debug info", CLR_LOG_DEBUG) + scr.addstr(curses.LINES-7, curses.COLS/2 + 2, "mycroft-skills.log, non debug", CLR_LOG) + scr.addstr(curses.LINES-6, curses.COLS/2 + 2, "mycroft-voice.log", CLR_LOG) + + # History log in the middle + scr.addstr(curses.LINES-10, 0, "History", CLR_CHAT_HEADING) + scr.addstr(curses.LINES-9, 0, "=" * (curses.COLS/2), CLR_CHAT_HEADING) + + cChat = len(chat) + if cChat: + y = curses.LINES-8 + for i in range(cChat-clamp(cChat, 1,5), cChat): + chat_line = chat[i] + if chat_line.startswith(">> "): + clr = CLR_CHAT_RESP + else: + clr = CLR_CHAT_QUERY + scr.addstr(y, 0, stripNonAscii(chat_line), clr) + y += 1 + + # Command line at the bottom + l = line + if len(line) > 0 and line[0] == ":": + scr.addstr(curses.LINES-2, 0, "Command ('help' for options):", CLR_CMDLINE) + scr.addstr(curses.LINES-1, 0, ":", CLR_CMDLINE) + l = line[1:] else: - ws.on('speak', handle_speak) + scr.addstr(curses.LINES-2, 0, "Input (Ctrl+C to quit):", CLR_CMDLINE) + scr.addstr(curses.LINES-1, 0, ">", CLR_CMDLINE) + scr.addstr(curses.LINES-1, 2, l, CLR_INPUT) + scr.refresh() + + +############################################################################## +# + +def handle_cmd(cmd): + if "show" in cmd and "log" in cmd: + pass + elif "errors" in cmd: + # Look in all logs for error messages, print here + pass + + +def main(stdscr): + global scr + global ws + global line + + scr = stdscr + init_screen() + + ws = WebsocketClient() + + ws.on('speak', handle_speak) event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() + + history = [] + hist_idx = -1 # index, from the bottom try: + input = "" while True: + draw_screen() # TODO: Change this mechanism # Sleep for a while so all the output that results # from the previous command finishes before we print. - time.sleep(1.5) - print("Input (Ctrl+C to quit):") - line = sys.stdin.readline() - ws.emit( - Message("recognizer_loop:utterance", - {'utterances': [line.strip()]})) + # time.sleep(1.5) + + # print("Input (Ctrl+C to quit):") + c = scr.getch() + if c == curses.KEY_ENTER or c == 10 or c == 13: + if line == "": + continue + + if line[:1] == ":": + handle_cmd(line[1:]) + else: + history.append(line) + chat.append(line) + ws.emit( + Message("recognizer_loop:utterance", + {'utterances': [line.strip()]})) + hist_idx = -1 + line = "" + elif c == curses.KEY_UP: + hist_idx = clamp(hist_idx+1, -1, len(history)-1) + if hist_idx >= 0: + line = history[len(history)-hist_idx-1] + else: + line = "" + elif c == curses.KEY_DOWN: + hist_idx = clamp(hist_idx-1, -1, len(history)-1) + if hist_idx >= 0: + line = history[len(history)-hist_idx-1] + else: + line = "" + elif curses.ascii.isascii(c): + line += chr(c) + elif c == curses.KEY_BACKSPACE: + line = line[:-1] + else: + line += str(c) + pass + # if line.startswith("*"): + # handle_cmd(line.strip("*")) + # else: + except KeyboardInterrupt, e: # User hit Ctrl+C to quit - print("") + pass except KeyboardInterrupt, e: logger.exception(e) event_thread.exit() sys.exit() +startLogMonitor("scripts/logs/mycroft-skills.log") +startLogMonitor("scripts/logs/mycroft-voice.log") if __name__ == "__main__": - main() + curses.wrapper(main) +