CLI Updates: Restore VT100 support, help, etc. (#1667)

* Restored VT100 function key support
* ESC now clears the entry line
* Spoken commands now become part of the history
* Enhanced help screen with auto word-wrap
* Bugfix: "Show meter" setting didn't persist, now it does
pull/1671/head
Steve Penrod 2018-07-12 02:38:29 -05:00 committed by Åke
parent bcb1e795ef
commit 223c88b4dd
1 changed files with 195 additions and 109 deletions

View File

@ -61,6 +61,7 @@ ws = None
mutex = Lock()
utterances = []
history = []
chat = [] # chat history, oldest at the lowest index
line = ""
bSimple = '--simple' in sys.argv
@ -92,6 +93,7 @@ meter_cur = -1
meter_thresh = -1
screen_mode = 0 # 0 = main, 1 = help, others in future?
subscreen = 0 # for help pages, etc.
FULL_REDRAW_FREQUENCY = 10 # seconds between full redraws
last_full_redraw = time.time()-(FULL_REDRAW_FREQUENCY-1) # seed for 1s redraw
screen_lock = Lock()
@ -142,6 +144,7 @@ def load_settings():
global cy_chat_area
global show_last_key
global max_log_lines
global show_meter
try:
with io.open(config_file, 'r') as f:
@ -315,6 +318,7 @@ def add_log_message(message):
def clear_log():
global filteredLog
global mergedLog
global log_line_offset
mergedLog = []
filteredLog = []
@ -359,6 +363,18 @@ def handle_speak(event):
draw_screen()
def handle_utterance(event):
global chat
global history
utterance = event.data.get('utterances')[0]
history.append(utterance)
if bSimple:
print(utterance)
else:
chat.append(utterance)
draw_screen()
def connect():
# Once the websocket has connected, just watch it for speak events
ws.run_forever()
@ -504,16 +520,16 @@ def draw_screen():
if not scr:
return
if not screen_mode == 0:
return
# Use a lock to prevent screen corruption when drawing
# from multiple threads
with screen_lock:
_do_drawing(scr)
if screen_mode == 0:
do_draw_main(scr)
elif screen_mode == 1:
do_draw_help(scr)
def _do_drawing(scr):
def do_draw_main(scr):
global log_line_offset
global longest_visible_line
global last_full_redraw
@ -557,7 +573,7 @@ def _do_drawing(scr):
scr.addstr(0, 0, "Log Output:" + " " * (curses.COLS - 31) +
str(start) + "-" + str(end) + " of " + str(cLogs),
CLR_HEADING)
ver = " mycroft-core "+mycroft.version.CORE_VERSION_STR+" ==="
ver = " mycroft-core " + mycroft.version.CORE_VERSION_STR + " ==="
scr.addstr(1, 0, "=" * (curses.COLS-1-len(ver)), CLR_HEADING)
scr.addstr(1, curses.COLS-1-len(ver), ver, CLR_HEADING)
@ -686,53 +702,63 @@ def _do_drawing(scr):
def make_titlebar(title, bar_length):
return title + " " + ("=" * (bar_length - 1 - len(title)))
##############################################################################
# Help system
help_struct = [
('Keyboard shortcuts',
[("Ctrl+N / Ctrl+P", "scroll thru query history"),
("Up/Down/PgUp/PgDn", "scroll thru log history"),
("Ctrl+T / Ctrl+PgUp", "scroll to top (oldest)"),
("Ctrl+B / Ctrl+PgDn", "scroll to bottom (newest)"),
("Left / Right", "scroll long lines left/right"),
("Home", "scroll to start of long lines"),
("End", "scroll to end of long lines")
]
),
("Commands (type ':' to enter command mode)",
[(":help", "this screen"),
(":quit or :exit", "exit the program"),
(":meter (show|hide)", "display of microphone level"),
(":filter [remove] 'str'", "adds or removes a log filter"),
(
'Log Scrolling shortcuts',
[
("Up / Down / PgUp / PgDn", "scroll thru history"),
("Ctrl+T / Ctrl+PgUp", "scroll to top of logs (jump to oldest)"),
("Ctrl+B / Ctrl+PgDn", "scroll to bottom of logs" +
"(jump to newest)"),
("Left / Right", "scroll long lines left/right"),
("Home / End", "scroll to start/end of long lines")
]
),
(
"Query History shortcuts",
[
("Ctrl+N / Ctrl+Right", "previous query"),
("Ctrl+P / Ctrl+Left", "next query")
]
),
(
"General Commands (type ':' to enter command mode)",
[
(":quit or :exit", "exit the program"),
(":meter (show|hide)", "display the microphone level"),
(":keycode (show|hide)", "display typed key codes (mainly debugging)"),
(":history (# lines)", "set size of visible history buffer"),
(":clear log", "flush the logs")
]
),
(
"Log Manipulation Commands",
[
(":filter 'STR'", "adds a log filter (optional quotes)"),
(":filter remove 'STR'", "removes a log filter"),
(":filter (clear|reset)", "reset filters"),
(":filter (show|list)", "display current filters"),
(":history (# lines)", "set number of history lines"),
(":find 'str'", "show logs containing 'str'"),
(":keycode (show|hide)", "display keyboard codes"),
(":clear log", "flush the logs"),
(":skills", "list installed skills"),
(":activate 'skill'", "activate skill"),
(":deactivate 'skill'", "deactivate skill"),
(":keep 'skill'", ("deactivate all skills except",
"the indicated skill"))
]
)
(":filter (show|list)", "display current filters"),
(":find 'STR'", "show logs containing 'str'")
]
),
(
"Skill Debugging Commands",
[
(":skills", "list installed skills"),
(":activate SKILL", "activate skill, e.g. 'activate skill-wiki'"),
(":deactivate SKILL", "deactivate skill"),
(":keep SKILL", "deactivate all skills except " +
"the indicated skill")
]
)
]
def help_new_page():
# Header
scr.erase()
scr.addstr(0, 0, center(25) + "Mycroft Command Line Help",
CLR_CMDLINE)
scr.addstr(1, 0, "=" * (curses.COLS - 1), CLR_CMDLINE)
def help_footer(page, total):
global scr
text = "Page {} of {} [ Press any key to continue ]".format(page, total)
scr.addstr(curses.LINES - 1, 0, center(len(text)) + text, CLR_HEADING)
scr.refresh()
scr.get_wch() # blocks
help_longest = 0
for s in help_struct:
for ent in s[1]:
help_longest = max(help_longest, len(ent[0]))
def num_help_pages():
@ -742,42 +768,81 @@ def num_help_pages():
return ceil(lines / (curses.LINES - 4))
def show_help():
global scr
global screen_mode
def do_draw_help(scr):
if not scr:
return
screen_mode = 1 # showing help (prevents overwrite by log updates)
def render_header():
scr.erase()
scr.addstr(0, 0, center(25) + "Mycroft Command Line Help", CLR_HEADING)
scr.addstr(1, 0, "=" * (curses.COLS - 1), CLR_HEADING)
help_new_page()
y_pos = 2
page = 1
def render_help(txt, y_pos, i, first_line, last_line, clr):
if i >= first_line and i < last_line:
scr.addstr(y_pos, 0, txt, clr)
y_pos += 1
return y_pos
def render_footer(page, total):
text = "Page {} of {} [ Any key to continue ]".format(page, total)
scr.addstr(curses.LINES - 1, 0, center(len(text)) + text, CLR_HEADING)
render_header()
y = 2
page = subscreen + 1
first = subscreen * (curses.LINES - 7) # account for header
last = first + (curses.LINES - 7) # account for header/footer
i = 0
for section in help_struct:
scr.addstr(y_pos, 0, section[0], CLR_CMDLINE)
y_pos += 1
scr.addstr(y_pos, 0, "=" * (curses.COLS - 1), CLR_CMDLINE)
y_pos += 1
y = render_help(section[0], y, i, first, last, CLR_HEADING)
i += 1
y = render_help("=" * (curses.COLS - 1), y, i, first, last,
CLR_HEADING)
i += 1
for line in section[1]:
scr.addstr(y_pos, 0, line[0])
if isinstance(line[1], tuple):
explaination = line[1]
else:
explaination = (line[1],)
for e in explaination:
scr.addstr(y_pos, 40, e)
y_pos += 1
if y_pos >= curses.LINES - 2:
help_footer(page, num_help_pages())
page += 1
help_new_page()
y_pos = 2
words = line[1].split()
ln = line[0].ljust(help_longest + 1)
for w in words:
if len(ln) + 1 + len(w) < curses.COLS:
ln += " "+w
else:
y = render_help(ln, y, i, first, last, CLR_CMDLINE)
ln = " ".ljust(help_longest + 2) + w
y = render_help(ln, y, i, first, last, CLR_CMDLINE)
i += 1
if y_pos != 2: # If we didn't just have a page break pause and show footer
help_footer(page, num_help_pages())
screen_mode = 0 # back to main screen
draw_screen()
y = render_help(" ", y, i, first, last, CLR_CMDLINE)
i += 1
if i > last:
break
render_footer(page, num_help_pages())
def show_help():
global screen_mode
global subscreen
if screen_mode != 1:
screen_mode = 1
subscreen = 0
draw_screen()
def show_next_help():
global screen_mode
global subscreen
if screen_mode == 1:
subscreen += 1
if subscreen >= num_help_pages():
screen_mode = 0
draw_screen()
##############################################################################
# Skill debugging
def show_skills(skills):
"""
@ -949,6 +1014,7 @@ def gui_main(stdscr):
global longest_visible_line
global find_str
global last_key
global history
scr = stdscr
init_screen()
@ -956,23 +1022,30 @@ def gui_main(stdscr):
ws = WebsocketClient()
ws.on('speak', handle_speak)
ws.on('message', handle_message)
ws.on('recognizer_loop:utterance', handle_utterance)
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()
c = scr.get_wch()
c = scr.get_wch() # unicode char or int for special keys
if isinstance(c, int):
code = c
else:
code = ord(c)
# Convert VT100 ESC codes generated by some terminals
if c == 27:
c1 = scr.get_wch()
c2 = scr.get_wch()
if code == 27:
scr.timeout(100)
c1 = scr.getch()
if c1 != -1:
c2 = scr.getch()
scr.timeout(-1)
if c1 == 79 and c2 == 120:
c = curses.KEY_UP
elif c1 == 79 and c2 == 116:
@ -990,12 +1063,35 @@ def gui_main(stdscr):
elif c1 == 79 and c2 == 113:
c = curses.KEY_END
else:
c = c2
last_key = str(c)+",ESC+"+str(c1)+"+"+str(c2)
else:
last_key = str(c)
c = c1
if c == '\n' or c == 10 or c == 13:
if c1 != -1:
last_key = str(c) + ",ESC+" + str(c1) + "+" + str(c2)
code = c
else:
last_key = "ESC"
else:
if code < 33:
last_key = str(code)
else:
last_key = str(code)
if code == 27: # Hitting ESC twice clears the entry line
hist_idx = -1
line = ""
elif c == curses.KEY_RESIZE:
# Generated by Curses when window/screen has been resized
y, x = scr.getmaxyx()
curses.resizeterm(y, x)
# resizeterm() causes another curses.KEY_RESIZE, so
# we need to capture that to prevent a loop of resizes
c = scr.get_wch()
elif screen_mode == 1:
# in Help mode, any key goes to next page
show_next_help()
continue
elif c == '\n' or code == 10 or code == 13 or code == 343:
# ENTER sends the typed line to be processed by Mycroft
if line == "":
continue
@ -1006,21 +1102,19 @@ def gui_main(stdscr):
break
else:
# Treat this as an utterance
history.append(line)
chat.append(line)
ws.emit(Message("recognizer_loop:utterance",
{'utterances': [line.strip()],
'lang': 'en-us'}))
hist_idx = -1
line = ""
elif c == 16 or c == 545: # Ctrl+P or Ctrl+Left (Previous)
elif code == 16 or code == 545: # Ctrl+P or Ctrl+Left (Previous)
# Move up the history stack
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 == 14 or c == 560: # Ctrl+N or Ctrl+Right (Next)
elif code == 14 or code == 560: # Ctrl+N or Ctrl+Right (Next)
# Move down the history stack
hist_idx = clamp(hist_idx - 1, -1, len(history) - 1)
if hist_idx >= 0:
@ -1051,32 +1145,24 @@ def gui_main(stdscr):
elif c == curses.KEY_PPAGE: # aka PgUp
# PgUp to go up a page in the logs
scroll_log(False)
elif c == 2 or c == 550: # Ctrl+B or Ctrl+PgDn
elif code == 2 or code == 550: # Ctrl+B or Ctrl+PgDn
scroll_log(True, max_log_lines)
elif c == 20 or c == 555: # Ctrl+T or Ctrl+PgUp
elif code == 20 or code == 555: # Ctrl+T or Ctrl+PgUp
scroll_log(False, max_log_lines)
elif c == curses.KEY_RESIZE:
# Generated by Curses when window/screen has been resized
y, x = scr.getmaxyx()
curses.resizeterm(y, x)
# resizeterm() causes another curses.KEY_RESIZE, so
# we need to capture that to prevent a loop of resizes
c = scr.get_wch()
elif c == curses.KEY_BACKSPACE or c == 127:
elif code == curses.KEY_BACKSPACE or code == 127:
# Backspace to erase a character in the utterance
line = line[:-1]
elif c == 6: # Ctrl+F (Find)
elif code == 6: # Ctrl+F (Find)
line = ":find "
elif c == 18: # Ctrl+R (Redraw)
elif code == 18: # Ctrl+R (Redraw)
scr.erase()
scr.refresh()
elif c == 24: # Ctrl+X (Exit)
elif code == 24: # Ctrl+X (Exit)
if find_str:
# End the find session
find_str = None
rebuild_filtered_log()
elif isinstance(c, str):
elif code > 31 and isinstance(c, str):
# Accept typed character in the utterance
line += c