text_client.py
Go to the documentation of this file.
1 # Copyright 2017 Mycroft AI Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 import sys
16 import io
17 import signal
18 from math import ceil
19 
20 from .gui_server import start_qml_gui
21 
22 from mycroft.tts import TTS
23 
24 import os
25 import os.path
26 import time
27 import curses
28 import textwrap
29 import json
30 import mycroft.version
31 from threading import Thread, Lock
32 from mycroft.messagebus.client.ws import WebsocketClient
33 from mycroft.messagebus.message import Message
34 from mycroft.util.log import LOG
35 from mycroft.configuration import Configuration
36 
37 import locale
38 # Curses uses LC_ALL to determine how to display chars set it to system
39 # default
40 locale.setlocale(locale.LC_ALL, "") # Set LC_ALL to user default
41 preferred_encoding = locale.getpreferredencoding()
42 
43 bSimple = False
44 bus = None # Mycroft messagebus connection
45 config = {} # Will be populated by the Mycroft configuration
46 event_thread = None
47 history = []
48 chat = [] # chat history, oldest at the lowest index
49 line = ""
50 scr = None
51 log_line_offset = 0 # num lines back in logs to show
52 log_line_lr_scroll = 0 # amount to scroll left/right for long lines
53 longest_visible_line = 0 # for HOME key
54 auto_scroll = True
55 
56 # for debugging odd terminals
57 last_key = ""
58 show_last_key = False
59 show_gui = None # None = not initialized, else True/False
60 gui_text = []
61 
62 log_lock = Lock()
63 max_log_lines = 5000
64 mergedLog = []
65 filteredLog = []
66 default_log_filters = ["mouth.viseme", "mouth.display", "mouth.icon", "DEBUG"]
67 log_filters = list(default_log_filters)
68 log_files = []
69 find_str = None
70 cy_chat_area = 7 # default chat history height (in lines)
71 size_log_area = 0 # max number of visible log lines, calculated during draw
72 
73 
74 # Values used to display the audio meter
75 show_meter = True
76 meter_peak = 20
77 meter_cur = -1
78 meter_thresh = -1
79 
80 SCR_MAIN = 0
81 SCR_HELP = 1
82 SCR_SKILLS = 2
83 screen_mode = SCR_MAIN
84 
85 subscreen = 0 # for help pages, etc.
86 FULL_REDRAW_FREQUENCY = 10 # seconds between full redraws
87 last_full_redraw = time.time()-(FULL_REDRAW_FREQUENCY-1) # seed for 1s redraw
88 screen_lock = Lock()
89 is_screen_dirty = True
90 
91 # Curses color codes (reassigned at runtime)
92 CLR_HEADING = 0
93 CLR_FIND = 0
94 CLR_CHAT_RESP = 0
95 CLR_CHAT_QUERY = 0
96 CLR_CMDLINE = 0
97 CLR_INPUT = 0
98 CLR_LOG1 = 0
99 CLR_LOG2 = 0
100 CLR_LOG_DEBUG = 0
101 CLR_LOG_ERROR = 0
102 CLR_LOG_CMDMESSAGE = 0
103 CLR_METER_CUR = 0
104 CLR_METER = 0
105 
106 # Allow Ctrl+C catching...
107 ctrl_c_was_pressed = False
108 
109 
110 def ctrl_c_handler(signum, frame):
111  global ctrl_c_was_pressed
112  ctrl_c_was_pressed = True
113 
114 
116  global ctrl_c_was_pressed
117  if ctrl_c_was_pressed:
118  ctrl_c_was_pressed = False
119  return True
120  else:
121  return False
122 
123 
124 signal.signal(signal.SIGINT, ctrl_c_handler)
125 
126 
127 ##############################################################################
128 # Helper functions
129 
130 def clamp(n, smallest, largest):
131  """ Force n to be between smallest and largest, inclusive """
132  return max(smallest, min(n, largest))
133 
134 
135 def handleNonAscii(text):
136  """
137  If default locale supports UTF-8 reencode the string otherwise
138  remove the offending characters.
139  """
140  if preferred_encoding == 'ASCII':
141  return ''.join([i if ord(i) < 128 else ' ' for i in text])
142  else:
143  return text.encode(preferred_encoding)
144 
145 
146 ##############################################################################
147 # Settings
148 
149 config_file = os.path.join(os.path.expanduser("~"), ".mycroft_cli.conf")
150 
151 
153  """ Load the mycroft config and connect it to updates over the messagebus.
154  """
155  Configuration.init(bus)
156  return Configuration.get()
157 
158 
160  """ Connect to the mycroft messagebus and load and register config
161  on the bus.
162 
163  Sets the bus and config global variables
164  """
165  global bus
166  global config
167  bus = connect_to_messagebus()
168  config = load_mycroft_config(bus)
169 
170 
172  global log_filters
173  global cy_chat_area
174  global show_last_key
175  global max_log_lines
176  global show_meter
177 
178  try:
179  with io.open(config_file, 'r') as f:
180  config = json.load(f)
181  if "filters" in config:
182  log_filters = config["filters"]
183  if "cy_chat_area" in config:
184  cy_chat_area = config["cy_chat_area"]
185  if "show_last_key" in config:
186  show_last_key = config["show_last_key"]
187  if "max_log_lines" in config:
188  max_log_lines = config["max_log_lines"]
189  if "show_meter" in config:
190  show_meter = config["show_meter"]
191  except Exception as e:
192  LOG.info("Ignoring failed load of settings file")
193 
194 
196  config = {}
197  config["filters"] = log_filters
198  config["cy_chat_area"] = cy_chat_area
199  config["show_last_key"] = show_last_key
200  config["max_log_lines"] = max_log_lines
201  config["show_meter"] = show_meter
202  with io.open(config_file, 'w') as f:
203  f.write(str(json.dumps(config, ensure_ascii=False)))
204 
205 
206 ##############################################################################
207 # Log file monitoring
208 
209 class LogMonitorThread(Thread):
210  def __init__(self, filename, logid):
211  global log_files
212  Thread.__init__(self)
213  self.filename = filename
214  self.st_results = os.stat(filename)
215  self.logid = str(logid)
216  log_files.append(filename)
217 
218  def run(self):
219  while True:
220  try:
221  st_results = os.stat(self.filename)
222 
223  # Check if file has been modified since last read
224  if not st_results.st_mtime == self.st_results.st_mtime:
225  self.read_file_from(self.st_results.st_size)
226  self.st_results = st_results
227 
229  except OSError:
230  # ignore any file IO exceptions, just try again
231  pass
232  time.sleep(0.1)
233 
234  def read_file_from(self, bytefrom):
235  global meter_cur
236  global meter_thresh
237  global filteredLog
238  global mergedLog
239  global log_line_offset
240  global log_lock
241 
242  with io.open(self.filename) as fh:
243  fh.seek(bytefrom)
244  while True:
245  line = fh.readline()
246  if line == "":
247  break
248 
249  # Allow user to filter log output
250  ignore = False
251  if find_str:
252  if find_str not in line:
253  ignore = True
254  else:
255  for filtered_text in log_filters:
256  if filtered_text in line:
257  ignore = True
258  break
259 
260  with log_lock:
261  if ignore:
262  mergedLog.append(self.logid + line.rstrip())
263  else:
264  if bSimple:
265  print(line.rstrip())
266  else:
267  filteredLog.append(self.logid + line.rstrip())
268  mergedLog.append(self.logid + line.rstrip())
269  if not auto_scroll:
270  log_line_offset += 1
271 
272  # Limit log to max_log_lines
273  if len(mergedLog) >= max_log_lines:
274  with log_lock:
275  cToDel = len(mergedLog) - max_log_lines
276  if len(filteredLog) == len(mergedLog):
277  del filteredLog[:cToDel]
278  del mergedLog[:cToDel]
279 
280  # release log_lock before calling to prevent deadlock
281  if len(filteredLog) != len(mergedLog):
283 
284 
285 def start_log_monitor(filename):
286  if os.path.isfile(filename):
287  thread = LogMonitorThread(filename, len(log_files))
288  thread.setDaemon(True) # this thread won't prevent prog from exiting
289  thread.start()
290 
291 
292 class MicMonitorThread(Thread):
293  def __init__(self, filename):
294  Thread.__init__(self)
295  self.filename = filename
296  self.st_results = None
297 
298  def run(self):
299  while True:
300  try:
301  st_results = os.stat(self.filename)
302 
303  if (not self.st_results or
304  not st_results.st_ctime == self.st_results.st_ctime or
305  not st_results.st_mtime == self.st_results.st_mtime):
306  self.read_mic_level()
307  self.st_results = st_results
309  except Exception:
310  # Ignore whatever failure happened and just try again later
311  pass
312  time.sleep(0.2)
313 
314  def read_mic_level(self):
315  global meter_cur
316  global meter_thresh
317 
318  with io.open(self.filename, 'r') as fh:
319  line = fh.readline()
320  # Just adjust meter settings
321  # Ex:Energy: cur=4 thresh=1.5
322  parts = line.split("=")
323  meter_thresh = float(parts[-1])
324  meter_cur = float(parts[-2].split(" ")[0])
325 
326 
327 class ScreenDrawThread(Thread):
328  def __init__(self):
329  Thread.__init__(self)
330 
331  def run(self):
332  global scr
333  global screen_lock
334  global is_screen_dirty
335  global log_lock
336 
337  while scr:
338  try:
339  if is_screen_dirty:
340  # Use a lock to prevent screen corruption when drawing
341  # from multiple threads
342  with screen_lock:
343  is_screen_dirty = False
344 
345  if screen_mode == SCR_MAIN:
346  with log_lock:
347  do_draw_main(scr)
348  elif screen_mode == SCR_HELP:
349  do_draw_help(scr)
350 
351  finally:
352  time.sleep(0.01)
353 
354 
355 def start_mic_monitor(filename):
356  if os.path.isfile(filename):
357  thread = MicMonitorThread(filename)
358  thread.setDaemon(True) # this thread won't prevent prog from exiting
359  thread.start()
360 
361 
362 def add_log_message(message):
363  """ Show a message for the user (mixed in the logs) """
364  global filteredLog
365  global mergedLog
366  global log_line_offset
367  global log_lock
368 
369  with log_lock:
370  message = "@" + message # the first byte is a code
371  filteredLog.append(message)
372  mergedLog.append(message)
373 
374  if log_line_offset != 0:
375  log_line_offset = 0 # scroll so the user can see the message
377 
378 
379 def clear_log():
380  global filteredLog
381  global mergedLog
382  global log_line_offset
383  global log_lock
384 
385  with log_lock:
386  mergedLog = []
387  filteredLog = []
388  log_line_offset = 0
389 
390 
392  global filteredLog
393  global mergedLog
394  global log_lock
395 
396  with log_lock:
397  filteredLog = []
398  for line in mergedLog:
399  # Apply filters
400  ignore = False
401 
402  if find_str and find_str != "":
403  # Searching log
404  if find_str not in line:
405  ignore = True
406  else:
407  # Apply filters
408  for filtered_text in log_filters:
409  if filtered_text and filtered_text in line:
410  ignore = True
411  break
412 
413  if not ignore:
414  filteredLog.append(line)
415 
416 
417 ##############################################################################
418 # Capturing output from Mycroft
419 
420 def handle_speak(event):
421  global chat
422  utterance = event.data.get('utterance')
423  utterance = TTS.remove_ssml(utterance)
424  if bSimple:
425  print(">> " + utterance)
426  else:
427  chat.append(">> " + utterance)
429 
430 
431 def handle_utterance(event):
432  global chat
433  global history
434  utterance = event.data.get('utterances')[0]
435  history.append(utterance)
436  chat.append(utterance)
438 
439 
440 def connect(bus):
441  """ Run the mycroft messagebus referenced by bus.
442 
443  Arguments:
444  bus: Mycroft messagebus instance
445  """
446  bus.run_forever()
447 
448 
449 ##############################################################################
450 # Capturing the messagebus
451 
452 def handle_message(msg):
453  # TODO: Think this thru a little bit -- remove this logging within core?
454  # add_log_message(msg)
455  pass
456 
457 
458 ##############################################################################
459 # "Graphic primitives"
460 def draw(x, y, msg, pad=None, pad_chr=None, clr=None):
461  """Draw a text to the screen
462 
463  Args:
464  x (int): X coordinate (col), 0-based from upper-left
465  y (int): Y coordinate (row), 0-based from upper-left
466  msg (str): string to render to screen
467  pad (bool or int, optional): if int, pads/clips to given length, if
468  True use right edge of the screen.
469  pad_chr (char, optional): pad character, default is space
470  clr (int, optional): curses color, Defaults to CLR_LOG1.
471  """
472  if y < 0 or y > curses.LINES or x < 0 or x > curses.COLS:
473  return
474 
475  if x + len(msg) > curses.COLS:
476  s = msg[:curses.COLS-x]
477  else:
478  s = msg
479  if pad:
480  ch = pad_chr or " "
481  if pad is True:
482  pad = curses.COLS # pad to edge of screen
483  s += ch * (pad-x-len(msg))
484  else:
485  # pad to given length (or screen width)
486  if x+pad > curses.COLS:
487  pad = curses.COLS-x
488  s += ch * (pad-len(msg))
489 
490  if not clr:
491  clr = CLR_LOG1
492 
493  scr.addstr(y, x, s, clr)
494 
495 
496 ##############################################################################
497 # Screen handling
498 
499 
501  global CLR_HEADING
502  global CLR_FIND
503  global CLR_CHAT_RESP
504  global CLR_CHAT_QUERY
505  global CLR_CMDLINE
506  global CLR_INPUT
507  global CLR_LOG1
508  global CLR_LOG2
509  global CLR_LOG_DEBUG
510  global CLR_LOG_ERROR
511  global CLR_LOG_CMDMESSAGE
512  global CLR_METER_CUR
513  global CLR_METER
514 
515  if curses.has_colors():
516  curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
517  bg = curses.COLOR_BLACK
518  for i in range(1, curses.COLORS):
519  curses.init_pair(i + 1, i, bg)
520 
521  # Colors (on black backgound):
522  # 1 = white 5 = dk blue
523  # 2 = dk red 6 = dk purple
524  # 3 = dk green 7 = dk cyan
525  # 4 = dk yellow 8 = lt gray
526  CLR_HEADING = curses.color_pair(1)
527  CLR_CHAT_RESP = curses.color_pair(4)
528  CLR_CHAT_QUERY = curses.color_pair(7)
529  CLR_FIND = curses.color_pair(4)
530  CLR_CMDLINE = curses.color_pair(7)
531  CLR_INPUT = curses.color_pair(7)
532  CLR_LOG1 = curses.color_pair(3)
533  CLR_LOG2 = curses.color_pair(6)
534  CLR_LOG_DEBUG = curses.color_pair(4)
535  CLR_LOG_ERROR = curses.color_pair(2)
536  CLR_LOG_CMDMESSAGE = curses.color_pair(2)
537  CLR_METER_CUR = curses.color_pair(2)
538  CLR_METER = curses.color_pair(4)
539 
540 
541 def scroll_log(up, num_lines=None):
542  global log_line_offset
543 
544  # default to a half-page
545  if not num_lines:
546  num_lines = size_log_area // 2
547 
548  with log_lock:
549  if up:
550  log_line_offset -= num_lines
551  else:
552  log_line_offset += num_lines
553  if log_line_offset > len(filteredLog):
554  log_line_offset = len(filteredLog) - 10
555  if log_line_offset < 0:
556  log_line_offset = 0
558 
559 
560 def _do_meter(height):
561  if not show_meter or meter_cur == -1:
562  return
563 
564  # The meter will look something like this:
565  #
566  # 8.4 *
567  # *
568  # -*- 2.4
569  # *
570  # *
571  # *
572  # Where the left side is the current level and the right side is
573  # the threshold level for 'silence'.
574  global scr
575  global meter_peak
576 
577  if meter_cur > meter_peak:
578  meter_peak = meter_cur + 1
579 
580  scale = meter_peak
581  if meter_peak > meter_thresh * 3:
582  scale = meter_thresh * 3
583  h_cur = clamp(int((float(meter_cur) / scale) * height), 0, height - 1)
584  h_thresh = clamp(
585  int((float(meter_thresh) / scale) * height), 0, height - 1)
586  clr = curses.color_pair(4) # dark yellow
587 
588  str_level = "{0:3} ".format(int(meter_cur)) # e.g. ' 4'
589  str_thresh = "{0:4.2f}".format(meter_thresh) # e.g. '3.24'
590  meter_width = len(str_level) + len(str_thresh) + 4
591  for i in range(0, height):
592  meter = ""
593  if i == h_cur:
594  # current energy level
595  meter = str_level
596  else:
597  meter = " " * len(str_level)
598 
599  if i == h_thresh:
600  # add threshold indicator
601  meter += "--- "
602  else:
603  meter += " "
604 
605  if i == h_thresh:
606  # 'silence' threshold energy level
607  meter += str_thresh
608 
609  # draw the line
610  meter += " " * (meter_width - len(meter))
611  scr.addstr(curses.LINES - 1 - i, curses.COLS -
612  len(meter) - 1, meter, clr)
613 
614  # draw an asterisk if the audio energy is at this level
615  if i <= h_cur:
616  if meter_cur > meter_thresh:
617  clr_bar = curses.color_pair(3) # dark green for loud
618  else:
619  clr_bar = curses.color_pair(5) # dark blue for 'silent'
620  scr.addstr(curses.LINES - 1 - i, curses.COLS - len(str_thresh) - 4,
621  "*", clr_bar)
622 
623 
624 def _do_gui(gui_width):
625  clr = curses.color_pair(2) # dark red
626  x = curses.COLS - gui_width
627  y = 3
628  draw(x, y, " "+make_titlebar("= GUI", gui_width-1)+" ", clr=CLR_HEADING)
629  cnt = len(gui_text)+1
630  if cnt > curses.LINES-15:
631  cnt = curses.LINES-15
632  for i in range(0, cnt):
633  draw(x, y+1+i, " !", clr=CLR_HEADING)
634  if i < len(gui_text):
635  draw(x+2, y+1+i, gui_text[i], pad=gui_width-3)
636  else:
637  draw(x+2, y+1+i, "*"*(gui_width-3))
638  draw(x+(gui_width-1), y+1+i, "!", clr=CLR_HEADING)
639  draw(x, y+cnt, " "+"-"*(gui_width-2)+" ", clr=CLR_HEADING)
640 
641 
643  global is_screen_dirty
644  global screen_lock
645 
646  with screen_lock:
647  is_screen_dirty = True
648 
649 
650 def do_draw_main(scr):
651  global log_line_offset
652  global longest_visible_line
653  global last_full_redraw
654  global auto_scroll
655  global size_log_area
656 
657  if time.time() - last_full_redraw > FULL_REDRAW_FREQUENCY:
658  # Do a full-screen redraw periodically to clear and
659  # noise from non-curses text that get output to the
660  # screen (e.g. modules that do a 'print')
661  scr.clear()
662  last_full_redraw = time.time()
663  else:
664  scr.erase()
665 
666  # Display log output at the top
667  cLogs = len(filteredLog) + 1 # +1 for the '--end--'
668  size_log_area = curses.LINES - (cy_chat_area + 5)
669  start = clamp(cLogs - size_log_area, 0, cLogs - 1) - log_line_offset
670  end = cLogs - log_line_offset
671  if start < 0:
672  end -= start
673  start = 0
674  if end > cLogs:
675  end = cLogs
676 
677  auto_scroll = (end == cLogs)
678 
679  # adjust the line offset (prevents paging up too far)
680  log_line_offset = cLogs - end
681 
682  # Top header and line counts
683  if find_str:
684  scr.addstr(0, 0, "Search Results: ", CLR_HEADING)
685  scr.addstr(0, 16, find_str, CLR_FIND)
686  scr.addstr(0, 16 + len(find_str), " ctrl+X to end" +
687  " " * (curses.COLS - 31 - 12 - len(find_str)) +
688  str(start) + "-" + str(end) + " of " + str(cLogs),
689  CLR_HEADING)
690  else:
691  scr.addstr(0, 0, "Log Output:" + " " * (curses.COLS - 31) +
692  str(start) + "-" + str(end) + " of " + str(cLogs),
693  CLR_HEADING)
694  ver = " mycroft-core " + mycroft.version.CORE_VERSION_STR + " ==="
695  scr.addstr(1, 0, "=" * (curses.COLS-1-len(ver)), CLR_HEADING)
696  scr.addstr(1, curses.COLS-1-len(ver), ver, CLR_HEADING)
697 
698  y = 2
699  len_line = 0
700  for i in range(start, end):
701  if i >= cLogs - 1:
702  log = ' ^--- NEWEST ---^ '
703  else:
704  log = filteredLog[i]
705  logid = log[0]
706  if len(log) > 25 and log[5] == '-' and log[8] == '-':
707  log = log[27:] # skip logid & date/time at the front of log line
708  else:
709  log = log[1:] # just skip the logid
710 
711  # Categorize log line
712  if " - DEBUG - " in log:
713  log = log.replace("Skills ", "")
714  clr = CLR_LOG_DEBUG
715  elif " - ERROR - " in log:
716  clr = CLR_LOG_ERROR
717  else:
718  if logid == "1":
719  clr = CLR_LOG1
720  elif logid == "@":
721  clr = CLR_LOG_CMDMESSAGE
722  else:
723  clr = CLR_LOG2
724 
725  # limit output line to screen width
726  len_line = len(log)
727  if len(log) > curses.COLS:
728  start = len_line - (curses.COLS - 4) - log_line_lr_scroll
729  if start < 0:
730  start = 0
731  end = start + (curses.COLS - 4)
732  if start == 0:
733  log = log[start:end] + "~~~~" # start....
734  elif end >= len_line - 1:
735  log = "~~~~" + log[start:end] # ....end
736  else:
737  log = "~~" + log[start:end] + "~~" # ..middle..
738  if len_line > longest_visible_line:
739  longest_visible_line = len_line
740  scr.addstr(y, 0, handleNonAscii(log), clr)
741  y += 1
742 
743  # Log legend in the lower-right
744  y_log_legend = curses.LINES - (3 + cy_chat_area)
745  scr.addstr(y_log_legend, curses.COLS // 2 + 2,
746  make_titlebar("Log Output Legend", curses.COLS // 2 - 2),
747  CLR_HEADING)
748  scr.addstr(y_log_legend + 1, curses.COLS // 2 + 2,
749  "DEBUG output",
750  CLR_LOG_DEBUG)
751  if len(log_files) > 0:
752  scr.addstr(y_log_legend + 2, curses.COLS // 2 + 2,
753  os.path.basename(log_files[0]) + ", other",
754  CLR_LOG1)
755  if len(log_files) > 1:
756  scr.addstr(y_log_legend + 3, curses.COLS // 2 + 2,
757  os.path.basename(log_files[1]), CLR_LOG2)
758 
759  # Meter
760  y_meter = y_log_legend
761  if show_meter:
762  scr.addstr(y_meter, curses.COLS - 14, " Mic Level ",
763  CLR_HEADING)
764 
765  # History log in the middle
766  y_chat_history = curses.LINES - (3 + cy_chat_area)
767  chat_width = curses.COLS // 2 - 2
768  chat_out = []
769  scr.addstr(y_chat_history, 0, make_titlebar("History", chat_width),
770  CLR_HEADING)
771 
772  # Build a nicely wrapped version of the chat log
773  idx_chat = len(chat) - 1
774  while len(chat_out) < cy_chat_area and idx_chat >= 0:
775  if chat[idx_chat][0] == '>':
776  wrapper = textwrap.TextWrapper(initial_indent="",
777  subsequent_indent=" ",
778  width=chat_width)
779  else:
780  wrapper = textwrap.TextWrapper(width=chat_width)
781 
782  chatlines = wrapper.wrap(chat[idx_chat])
783  for txt in reversed(chatlines):
784  if len(chat_out) >= cy_chat_area:
785  break
786  chat_out.insert(0, txt)
787 
788  idx_chat -= 1
789 
790  # Output the chat
791  y = curses.LINES - (2 + cy_chat_area)
792  for txt in chat_out:
793  if txt.startswith(">> ") or txt.startswith(" "):
794  clr = CLR_CHAT_RESP
795  else:
796  clr = CLR_CHAT_QUERY
797  scr.addstr(y, 1, handleNonAscii(txt), clr)
798  y += 1
799 
800  if show_gui and curses.COLS > 20 and curses.LINES > 20:
801  _do_gui(curses.COLS-20)
802 
803  # Command line at the bottom
804  ln = line
805  if len(line) > 0 and line[0] == ":":
806  scr.addstr(curses.LINES - 2, 0, "Command ('help' for options):",
807  CLR_CMDLINE)
808  scr.addstr(curses.LINES - 1, 0, ":", CLR_CMDLINE)
809  ln = line[1:]
810  else:
811  prompt = "Input (':' for command, Ctrl+C to quit)"
812  if show_last_key:
813  prompt += " === keycode: "+last_key
814  scr.addstr(curses.LINES - 2, 0,
815  make_titlebar(prompt,
816  curses.COLS - 1),
817  CLR_HEADING)
818  scr.addstr(curses.LINES - 1, 0, ">", CLR_HEADING)
819 
820  _do_meter(cy_chat_area + 2)
821  scr.addstr(curses.LINES - 1, 2, ln[-(curses.COLS - 3):], CLR_INPUT)
822 
823  # Curses doesn't actually update the display until refresh() is called
824  scr.refresh()
825 
826 
827 def make_titlebar(title, bar_length):
828  return title + " " + ("=" * (bar_length - 1 - len(title)))
829 
830 ##############################################################################
831 # Help system
832 
833 
834 help_struct = [
835  (
836  'Log Scrolling shortcuts',
837  [
838  ("Up / Down / PgUp / PgDn", "scroll thru history"),
839  ("Ctrl+T / Ctrl+PgUp", "scroll to top of logs (jump to oldest)"),
840  ("Ctrl+B / Ctrl+PgDn", "scroll to bottom of logs" +
841  "(jump to newest)"),
842  ("Left / Right", "scroll long lines left/right"),
843  ("Home / End", "scroll to start/end of long lines")
844  ]
845  ),
846  (
847  "Query History shortcuts",
848  [
849  ("Ctrl+N / Ctrl+Right", "previous query"),
850  ("Ctrl+P / Ctrl+Left", "next query")
851  ]
852  ),
853  (
854  "General Commands (type ':' to enter command mode)",
855  [
856  (":quit or :exit", "exit the program"),
857  (":meter (show|hide)", "display the microphone level"),
858  (":keycode (show|hide)", "display typed key codes (mainly debugging)"),
859  (":history (# lines)", "set size of visible history buffer"),
860  (":clear", "flush the logs")
861  ]
862  ),
863  (
864  "Log Manipulation Commands",
865  [
866  (":filter 'STR'", "adds a log filter (optional quotes)"),
867  (":filter remove 'STR'", "removes a log filter"),
868  (":filter (clear|reset)", "reset filters"),
869  (":filter (show|list)", "display current filters"),
870  (":find 'STR'", "show logs containing 'str'"),
871  (":log level (DEBUG|INFO|ERROR)", "set logging level"),
872  (":log bus (on|off)", "control logging of messagebus messages")
873  ]
874  ),
875  (
876  "Skill Debugging Commands",
877  [
878  (":skills", "list installed skills"),
879  (":activate SKILL", "activate skill, e.g. 'activate skill-wiki'"),
880  (":deactivate SKILL", "deactivate skill"),
881  (":keep SKILL", "deactivate all skills except " +
882  "the indicated skill")
883  ]
884  )
885 ]
886 help_longest = 0
887 for s in help_struct:
888  for ent in s[1]:
889  help_longest = max(help_longest, len(ent[0]))
890 
891 
893  lines = 0
894  for section in help_struct:
895  lines += 2 + len(section[1])
896  return ceil(lines / (curses.LINES - 4))
897 
898 
899 def do_draw_help(scr):
900 
901  def render_header():
902  scr.addstr(0, 0, center(25) + "Mycroft Command Line Help", CLR_HEADING)
903  scr.addstr(1, 0, "=" * (curses.COLS - 1), CLR_HEADING)
904 
905  def render_help(txt, y_pos, i, first_line, last_line, clr):
906  if i >= first_line and i < last_line:
907  scr.addstr(y_pos, 0, txt, clr)
908  y_pos += 1
909  return y_pos
910 
911  def render_footer(page, total):
912  text = "Page {} of {} [ Any key to continue ]".format(page, total)
913  scr.addstr(curses.LINES - 1, 0, center(len(text)) + text, CLR_HEADING)
914 
915  scr.erase()
916  render_header()
917  y = 2
918  page = subscreen + 1
919 
920  first = subscreen * (curses.LINES - 7) # account for header
921  last = first + (curses.LINES - 7) # account for header/footer
922  i = 0
923  for section in help_struct:
924  y = render_help(section[0], y, i, first, last, CLR_HEADING)
925  i += 1
926  y = render_help("=" * (curses.COLS - 1), y, i, first, last,
927  CLR_HEADING)
928  i += 1
929 
930  for line in section[1]:
931  words = line[1].split()
932  ln = line[0].ljust(help_longest + 1)
933  for w in words:
934  if len(ln) + 1 + len(w) < curses.COLS:
935  ln += " "+w
936  else:
937  y = render_help(ln, y, i, first, last, CLR_CMDLINE)
938  ln = " ".ljust(help_longest + 2) + w
939  y = render_help(ln, y, i, first, last, CLR_CMDLINE)
940  i += 1
941 
942  y = render_help(" ", y, i, first, last, CLR_CMDLINE)
943  i += 1
944 
945  if i > last:
946  break
947 
948  render_footer(page, num_help_pages())
949 
950  # Curses doesn't actually update the display until refresh() is called
951  scr.refresh()
952 
953 
954 def show_help():
955  global screen_mode
956  global subscreen
957 
958  if screen_mode != SCR_HELP:
959  screen_mode = SCR_HELP
960  subscreen = 0
962 
963 
965  global screen_mode
966  global subscreen
967 
968  if screen_mode == SCR_HELP:
969  subscreen += 1
970  if subscreen >= num_help_pages():
971  screen_mode = SCR_MAIN
973 
974 
975 ##############################################################################
976 # Skill debugging
977 
978 def show_skills(skills):
979  """
980  Show list of loaded skills in as many column as necessary
981  """
982  global scr
983  global screen_mode
984 
985  if not scr:
986  return
987 
988  screen_mode = SCR_SKILLS
989 
990  row = 2
991  column = 0
992 
993  def prepare_page():
994  global scr
995  nonlocal row
996  nonlocal column
997  scr.erase()
998  scr.addstr(0, 0, center(25) + "Loaded skills", CLR_CMDLINE)
999  scr.addstr(1, 1, "=" * (curses.COLS - 2), CLR_CMDLINE)
1000  row = 2
1001  column = 0
1002 
1003  prepare_page()
1004  col_width = 0
1005  skill_names = sorted(skills.keys())
1006  for skill in skill_names:
1007  if skills[skill]['active']:
1008  color = curses.color_pair(4)
1009  else:
1010  color = curses.color_pair(2)
1011 
1012  scr.addstr(row, column, " {}".format(skill), color)
1013  row += 1
1014  col_width = max(col_width, len(skill))
1015  if row == curses.LINES - 2 and column > 0 and skill != skill_names[-1]:
1016  column = 0
1017  scr.addstr(curses.LINES - 1, 0,
1018  center(23) + "Press any key to continue", CLR_HEADING)
1019  scr.refresh()
1020  scr.get_wch() # blocks
1021  prepare_page()
1022  elif row == curses.LINES - 2:
1023  # Reached bottom of screen, start at top and move output to a
1024  # New column
1025  row = 2
1026  column += col_width + 2
1027  col_width = 0
1028  if column > curses.COLS - 20:
1029  # End of screen
1030  break
1031 
1032  scr.addstr(curses.LINES - 1, 0, center(23) + "Press any key to return",
1033  CLR_HEADING)
1034  scr.refresh()
1035 
1036 
1037 def center(str_len):
1038  # generate number of characters needed to center a string
1039  # of the given length
1040  return " " * ((curses.COLS - str_len) // 2)
1041 
1042 
1043 ##############################################################################
1044 # Main UI lopo
1045 
1046 def _get_cmd_param(cmd, keyword):
1047  # Returns parameter to a command. Will de-quote.
1048  # Ex: find 'abc def' returns: abc def
1049  # find abc def returns: abc def
1050  if isinstance(keyword, list):
1051  for w in keyword:
1052  cmd = cmd.replace(w, "").strip()
1053  else:
1054  cmd = cmd.replace(keyword, "").strip()
1055  if not cmd:
1056  return None
1057 
1058  last_char = cmd[-1]
1059  if last_char == '"' or last_char == "'":
1060  parts = cmd.split(last_char)
1061  return parts[-2]
1062  else:
1063  parts = cmd.split(" ")
1064  return parts[-1]
1065 
1066 
1067 def handle_cmd(cmd):
1068  global show_meter
1069  global screen_mode
1070  global log_filters
1071  global cy_chat_area
1072  global find_str
1073  global show_last_key
1074 
1075  if "show" in cmd and "log" in cmd:
1076  pass
1077  elif "help" in cmd:
1078  show_help()
1079  elif "exit" in cmd or "quit" in cmd:
1080  return 1
1081  elif "keycode" in cmd:
1082  # debugging keyboard
1083  if "hide" in cmd or "off" in cmd:
1084  show_last_key = False
1085  elif "show" in cmd or "on" in cmd:
1086  show_last_key = True
1087  elif "meter" in cmd:
1088  # microphone level meter
1089  if "hide" in cmd or "off" in cmd:
1090  show_meter = False
1091  elif "show" in cmd or "on" in cmd:
1092  show_meter = True
1093  elif "find" in cmd:
1094  find_str = _get_cmd_param(cmd, "find")
1096  elif "filter" in cmd:
1097  if "show" in cmd or "list" in cmd:
1098  # display active filters
1099  add_log_message("Filters: " + str(log_filters))
1100  return
1101 
1102  if "reset" in cmd or "clear" in cmd:
1103  log_filters = list(default_log_filters)
1104  else:
1105  # extract last word(s)
1106  param = _get_cmd_param(cmd, "filter")
1107  if param:
1108  if "remove" in cmd and param in log_filters:
1109  log_filters.remove(param)
1110  else:
1111  log_filters.append(param)
1112 
1114  add_log_message("Filters: " + str(log_filters))
1115  elif "clear" in cmd:
1116  clear_log()
1117  elif "log" in cmd:
1118  # Control logging behavior in all Mycroft processes
1119  if "level" in cmd:
1120  level = _get_cmd_param(cmd, ["log", "level"])
1121  bus.emit(Message("mycroft.debug.log", data={'level': level}))
1122  elif "bus" in cmd:
1123  state = _get_cmd_param(cmd, ["log", "bus"]).lower()
1124  if state in ["on", "true", "yes"]:
1125  bus.emit(Message("mycroft.debug.log", data={'bus': True}))
1126  elif state in ["off", "false", "no"]:
1127  bus.emit(Message("mycroft.debug.log", data={'bus': False}))
1128  elif "history" in cmd:
1129  # extract last word(s)
1130  lines = int(_get_cmd_param(cmd, "history"))
1131  if not lines or lines < 1:
1132  lines = 1
1133  max_chat_area = curses.LINES - 7
1134  if lines > max_chat_area:
1135  lines = max_chat_area
1136  cy_chat_area = lines
1137  elif "skills" in cmd:
1138  # List loaded skill
1139  message = bus.wait_for_response(
1140  Message('skillmanager.list'), reply_type='mycroft.skills.list')
1141 
1142  if message:
1143  show_skills(message.data)
1144  scr.get_wch() # blocks
1145  screen_mode = SCR_MAIN
1147  elif "deactivate" in cmd:
1148  skills = cmd.split()[1:]
1149  if len(skills) > 0:
1150  for s in skills:
1151  bus.emit(Message("skillmanager.deactivate", data={'skill': s}))
1152  else:
1153  add_log_message('Usage :deactivate SKILL [SKILL2] [...]')
1154  elif "keep" in cmd:
1155  s = cmd.split()
1156  if len(s) > 1:
1157  bus.emit(Message("skillmanager.keep", data={'skill': s[1]}))
1158  else:
1159  add_log_message('Usage :keep SKILL')
1160 
1161  elif "activate" in cmd:
1162  skills = cmd.split()[1:]
1163  if len(skills) > 0:
1164  for s in skills:
1165  bus.emit(Message("skillmanager.activate", data={'skill': s}))
1166  else:
1167  add_log_message('Usage :activate SKILL [SKILL2] [...]')
1168 
1169  # TODO: More commands
1170  return 0 # do nothing upon return
1171 
1172 
1174  add_log_message("Connected to Messagebus!")
1175  # start_qml_gui(bus, gui_text)
1176 
1177 
1179  add_log_message("Looking for Messagebus websocket...")
1180 
1181 
1182 def gui_main(stdscr):
1183  global scr
1184  global bus
1185  global line
1186  global log_line_lr_scroll
1187  global longest_visible_line
1188  global find_str
1189  global last_key
1190  global history
1191  global screen_lock
1192  global show_gui
1193  global config
1194 
1195  scr = stdscr
1196  init_screen()
1197  scr.keypad(1)
1198  scr.notimeout(True)
1199 
1200  bus.on('speak', handle_speak)
1201  bus.on('message', handle_message)
1202  bus.on('recognizer_loop:utterance', handle_utterance)
1203  bus.on('connected', handle_is_connected)
1204  bus.on('reconnecting', handle_reconnecting)
1205 
1206  add_log_message("Establishing Mycroft Messagebus connection...")
1207 
1208  gui_thread = ScreenDrawThread()
1209  gui_thread.setDaemon(True) # this thread won't prevent prog from exiting
1210  gui_thread.start()
1211 
1212  hist_idx = -1 # index, from the bottom
1213  c = 0
1214  try:
1215  while True:
1217  c = 0
1218  code = 0
1219 
1220  try:
1221  if ctrl_c_pressed():
1222  # User hit Ctrl+C. treat same as Ctrl+X
1223  c = 24
1224  else:
1225  # Don't block, this allows us to refresh the screen while
1226  # waiting on initial messagebus connection, etc
1227  scr.timeout(1)
1228  c = scr.get_wch() # unicode char or int for special keys
1229  if c == -1:
1230  continue
1231  except curses.error:
1232  # This happens in odd cases, such as when you Ctrl+Z
1233  # the CLI and then resume. Curses fails on get_wch().
1234  continue
1235 
1236  if isinstance(c, int):
1237  code = c
1238  else:
1239  code = ord(c)
1240 
1241  # Convert VT100 ESC codes generated by some terminals
1242  if code == 27:
1243  # NOTE: Not sure exactly why, but the screen can get corrupted
1244  # if we draw to the screen while doing a scr.getch(). So
1245  # lock screen updates until the VT100 sequence has been
1246  # completely read.
1247  with screen_lock:
1248  scr.timeout(0)
1249  c1 = -1
1250  start = time.time()
1251  while c1 == -1:
1252  c1 = scr.getch()
1253  if time.time()-start > 1:
1254  break # 1 second timeout waiting for ESC code
1255 
1256  c2 = -1
1257  while c2 == -1:
1258  c2 = scr.getch()
1259  if time.time()-start > 1: # 1 second timeout
1260  break # 1 second timeout waiting for ESC code
1261 
1262  if c1 == 79 and c2 == 120:
1263  c = curses.KEY_UP
1264  elif c1 == 79 and c2 == 116:
1265  c = curses.KEY_LEFT
1266  elif c1 == 79 and c2 == 114:
1267  c = curses.KEY_DOWN
1268  elif c1 == 79 and c2 == 118:
1269  c = curses.KEY_RIGHT
1270  elif c1 == 79 and c2 == 121:
1271  c = curses.KEY_PPAGE # aka PgUp
1272  elif c1 == 79 and c2 == 115:
1273  c = curses.KEY_NPAGE # aka PgDn
1274  elif c1 == 79 and c2 == 119:
1275  c = curses.KEY_HOME
1276  elif c1 == 79 and c2 == 113:
1277  c = curses.KEY_END
1278  else:
1279  c = c1
1280 
1281  if c1 != -1:
1282  last_key = str(c) + ",ESC+" + str(c1) + "+" + str(c2)
1283  code = c
1284  else:
1285  last_key = "ESC"
1286  else:
1287  if code < 33:
1288  last_key = str(code)
1289  else:
1290  last_key = str(code)
1291 
1292  scr.timeout(-1) # resume blocking
1293  if code == 27: # Hitting ESC twice clears the entry line
1294  hist_idx = -1
1295  line = ""
1296  elif c == curses.KEY_RESIZE:
1297  # Generated by Curses when window/screen has been resized
1298  y, x = scr.getmaxyx()
1299  curses.resizeterm(y, x)
1300 
1301  # resizeterm() causes another curses.KEY_RESIZE, so
1302  # we need to capture that to prevent a loop of resizes
1303  c = scr.get_wch()
1304  elif screen_mode == SCR_HELP:
1305  # in Help mode, any key goes to next page
1306  show_next_help()
1307  continue
1308  elif c == '\n' or code == 10 or code == 13 or code == 343:
1309  # ENTER sends the typed line to be processed by Mycroft
1310  if line == "":
1311  continue
1312 
1313  if line[:1] == ":":
1314  # Lines typed like ":help" are 'commands'
1315  if handle_cmd(line[1:]) == 1:
1316  break
1317  else:
1318  # Treat this as an utterance
1319  bus.emit(Message("recognizer_loop:utterance",
1320  {'utterances': [line.strip()],
1321  'lang': config.get('lang', 'en-us')}))
1322  hist_idx = -1
1323  line = ""
1324  elif code == 16 or code == 545: # Ctrl+P or Ctrl+Left (Previous)
1325  # Move up the history stack
1326  hist_idx = clamp(hist_idx + 1, -1, len(history) - 1)
1327  if hist_idx >= 0:
1328  line = history[len(history) - hist_idx - 1]
1329  else:
1330  line = ""
1331  elif code == 14 or code == 560: # Ctrl+N or Ctrl+Right (Next)
1332  # Move down the history stack
1333  hist_idx = clamp(hist_idx - 1, -1, len(history) - 1)
1334  if hist_idx >= 0:
1335  line = history[len(history) - hist_idx - 1]
1336  else:
1337  line = ""
1338  elif c == curses.KEY_LEFT:
1339  # scroll long log lines left
1340  log_line_lr_scroll += curses.COLS // 4
1341  elif c == curses.KEY_RIGHT:
1342  # scroll long log lines right
1343  log_line_lr_scroll -= curses.COLS // 4
1344  if log_line_lr_scroll < 0:
1345  log_line_lr_scroll = 0
1346  elif c == curses.KEY_HOME:
1347  # HOME scrolls log lines all the way to the start
1348  log_line_lr_scroll = longest_visible_line
1349  elif c == curses.KEY_END:
1350  # END scrolls log lines all the way to the end
1351  log_line_lr_scroll = 0
1352  elif c == curses.KEY_UP:
1353  scroll_log(False, 1)
1354  elif c == curses.KEY_DOWN:
1355  scroll_log(True, 1)
1356  elif c == curses.KEY_NPAGE: # aka PgDn
1357  # PgDn to go down a page in the logs
1358  scroll_log(True)
1359  elif c == curses.KEY_PPAGE: # aka PgUp
1360  # PgUp to go up a page in the logs
1361  scroll_log(False)
1362  elif code == 2 or code == 550: # Ctrl+B or Ctrl+PgDn
1363  scroll_log(True, max_log_lines)
1364  elif code == 20 or code == 555: # Ctrl+T or Ctrl+PgUp
1365  scroll_log(False, max_log_lines)
1366  elif code == curses.KEY_BACKSPACE or code == 127:
1367  # Backspace to erase a character in the utterance
1368  line = line[:-1]
1369  elif code == 6: # Ctrl+F (Find)
1370  line = ":find "
1371  elif code == 7: # Ctrl+G (start GUI)
1372  if show_gui is None:
1373  start_qml_gui(bus, gui_text)
1374  show_gui = not show_gui
1375  elif code == 18: # Ctrl+R (Redraw)
1376  scr.erase()
1377  elif code == 24: # Ctrl+X (Exit)
1378  if find_str:
1379  # End the find session
1380  find_str = None
1382  elif line.startswith(":"):
1383  # cancel command mode
1384  line = ""
1385  else:
1386  # exit CLI
1387  break
1388  elif code > 31 and isinstance(c, str):
1389  # Accept typed character in the utterance
1390  line += c
1391 
1392  finally:
1393  scr.erase()
1394  scr.refresh()
1395  scr = None
1396 
1397 
1399  global bSimple
1400  bSimple = True
1401 
1402  bus.on('speak', handle_speak)
1403  try:
1404  while True:
1405  # Sleep for a while so all the output that results
1406  # from the previous command finishes before we print.
1407  time.sleep(1.5)
1408  print("Input (Ctrl+C to quit):")
1409  line = sys.stdin.readline()
1410  bus.emit(Message("recognizer_loop:utterance",
1411  {'utterances': [line.strip()]}))
1412  except KeyboardInterrupt as e:
1413  # User hit Ctrl+C to quit
1414  print("")
1415  except KeyboardInterrupt as e:
1416  LOG.exception(e)
1417  event_thread.exit()
1418  sys.exit()
1419 
1420 
1422  """ Connect to the mycroft messagebus and launch a thread handling the
1423  connection.
1424 
1425  Returns: WebsocketClient
1426  """
1427  bus = WebsocketClient() # Mycroft messagebus connection
1428 
1429  event_thread = Thread(target=connect, args=[bus])
1430  event_thread.setDaemon(True)
1431  event_thread.start()
1432  return bus
def _get_cmd_param(cmd, keyword)
Main UI lopo.
def make_titlebar(title, bar_length)
Definition: text_client.py:827
def clamp(n, smallest, largest)
Helper functions.
Definition: text_client.py:130
def ctrl_c_handler(signum, frame)
Definition: text_client.py:110
def handle_speak(event)
Capturing output from Mycroft.
Definition: text_client.py:420
def init_screen()
Screen handling.
Definition: text_client.py:500
def draw(x, y, msg, pad=None, pad_chr=None, clr=None)
"Graphic primitives"
Definition: text_client.py:460
def start_qml_gui(messagebus, output_buf)
Definition: gui_server.py:36
def handle_message(msg)
Capturing the messagebus.
Definition: text_client.py:452
def show_skills(skills)
Skill debugging.
Definition: text_client.py:978
def scroll_log(up, num_lines=None)
Definition: text_client.py:541


mycroft_ros
Author(s):
autogenerated on Mon Apr 26 2021 02:35:40