openMSX
CommandConsole.cc
Go to the documentation of this file.
1 #include "CommandConsole.hh"
2 #include "CommandException.hh"
4 #include "Completer.hh"
5 #include "Interpreter.hh"
6 #include "Keys.hh"
7 #include "FileContext.hh"
8 #include "FileException.hh"
9 #include "FileOperations.hh"
10 #include "CliComm.hh"
11 #include "SettingsConfig.hh"
12 #include "XMLElement.hh"
13 #include "InputEvents.hh"
14 #include "Display.hh"
15 #include "EventDistributor.hh"
16 #include "GlobalSettings.hh"
17 #include "BooleanSetting.hh"
18 #include "IntegerSetting.hh"
19 #include "Version.hh"
20 #include "checked_cast.hh"
21 #include "utf8_unchecked.hh"
22 #include "StringOp.hh"
23 #include "ScopedAssign.hh"
24 #include "memory.hh"
25 #include "xrange.hh"
26 #include <algorithm>
27 #include <fstream>
28 #include <cassert>
29 
30 using std::min;
31 using std::max;
32 using std::string;
33 using std::make_pair;
34 
35 namespace openmsx {
36 
37 // class ConsoleLine
38 
40 {
41 }
42 
44  : line(line_.str())
45  , chunks(1, make_pair(rgb, 0))
46 {
47 }
48 
49 void ConsoleLine::addChunk(string_ref text, unsigned rgb)
50 {
51  chunks.push_back(make_pair(rgb, line.size()));
52  line.append(text.data(), text.size());
53 }
54 
55 unsigned ConsoleLine::numChars() const
56 {
57  return unsigned(utf8::unchecked::size(line));
58 }
59 
60 const string& ConsoleLine::str() const
61 {
62  return line;
63 }
64 
65 unsigned ConsoleLine::numChunks() const
66 {
67  return unsigned(chunks.size());
68 }
69 
70 unsigned ConsoleLine::chunkColor(unsigned i) const
71 {
72  assert(i < chunks.size());
73  return chunks[i].first;
74 }
75 
77 {
78  assert(i < chunks.size());
79  auto pos = chunks[i].second;
80  auto len = ((i + 1) == chunks.size())
82  : chunks[i + 1].second - pos;
83  return string_ref(line).substr(pos, len);
84 }
85 
86 ConsoleLine ConsoleLine::substr(unsigned pos, unsigned len) const
87 {
88  ConsoleLine result;
89  if (chunks.empty()) {
90  assert(line.empty());
91  assert(pos == 0);
92  return result;
93  }
94 
95  auto begin = line.begin();
96  utf8::unchecked::advance(begin, pos);
97  auto end = begin;
98  while (len-- && (end != line.end())) {
100  }
101  result.line.assign(begin, end);
102 
103  unsigned bpos = begin - line.begin();
104  unsigned bend = end - line.begin();
105  unsigned i = 1;
106  while ((i < chunks.size()) && (chunks[i].second <= bpos)) {
107  ++i;
108  }
109  result.chunks.push_back(make_pair(chunks[i - 1].first, 0));
110  while ((i < chunks.size()) && (chunks[i].second < bend)) {
111  result.chunks.push_back(make_pair(chunks[i].first,
112  chunks[i].second - bpos));
113  ++i;
114  }
115  return result;
116 }
117 
118 // class CommandConsole
119 
120 static const char* const PROMPT_NEW = "> ";
121 static const char* const PROMPT_CONT = "| ";
122 static const char* const PROMPT_BUSY = "*busy*";
123 
125  GlobalCommandController& commandController_,
126  EventDistributor& eventDistributor_,
127  Display& display_)
128  : commandController(commandController_)
129  , eventDistributor(eventDistributor_)
130  , display(display_)
131  , consoleSetting(make_unique<BooleanSetting>(
132  commandController, "console",
133  "turns console display on/off", false, Setting::DONT_SAVE))
134  , historySizeSetting(make_unique<IntegerSetting>(
135  commandController, "console_history_size",
136  "amount of commands kept in console history", 100, 0, 10000))
137  , removeDoublesSetting(make_unique<BooleanSetting>(
138  commandController, "console_remove_doubles",
139  "don't add the command to history if it's the same as the previous one",
140  true))
141  , executingCommand(false)
142 {
143  resetScrollBack();
144  prompt = PROMPT_NEW;
145  newLineConsole(prompt);
146  putPrompt();
147  loadHistory();
148  Completer::setOutput(this);
149 
150  const auto& fullVersion = Version::full();
151  print(fullVersion);
152  print(string(fullVersion.size(), '-'));
153  print("\n"
154  "General information about openMSX is available at "
155  "http://www.openmsx.org.\n"
156  "\n"
157  "Type 'help' to see a list of available commands "
158  "(use <PgUp>/<PgDn> to scroll).\n"
159  "Or read the Console Command Reference in the manual.\n"
160  "\n");
161 
162  commandController.getInterpreter().setOutput(this);
163  eventDistributor.registerEventListener(
165  // also listen to KEY_UP events, so that we can consume them
166  eventDistributor.registerEventListener(
168 }
169 
171 {
172  eventDistributor.unregisterEventListener(OPENMSX_KEY_DOWN_EVENT, *this);
173  eventDistributor.unregisterEventListener(OPENMSX_KEY_UP_EVENT, *this);
174  commandController.getInterpreter().setOutput(nullptr);
175  Completer::setOutput(nullptr);
176 }
177 
178 void CommandConsole::saveHistory()
179 {
180  try {
181  UserFileContext context("console");
182  std::ofstream outputfile;
183  FileOperations::openofstream(outputfile,
184  context.resolveCreate("history.txt"));
185  if (!outputfile) {
186  throw FileException(
187  "Error while saving the console history.");
188  }
189  for (auto& s : history) {
190  outputfile << string_ref(s).substr(prompt.size()) << '\n';
191  }
192  } catch (FileException& e) {
193  commandController.getCliComm().printWarning(e.getMessage());
194  }
195 }
196 
197 void CommandConsole::loadHistory()
198 {
199  try {
200  UserFileContext context("console");
201  std::ifstream inputfile(
202  context.resolveCreate("history.txt").c_str());
203  if (!inputfile) {
204  throw FileException(
205  "Error while loading the console history.");
206  }
207  string line;
208  while (inputfile) {
209  getline(inputfile, line);
210  if (!line.empty()) {
211  putCommandHistory(prompt + line);
212  }
213  }
214  } catch (FileException& e) {
215  PRT_DEBUG(e.getMessage());
216  (void)&e; // Prevent warning
217  }
218 }
219 
221 {
222  return *consoleSetting.get();
223 }
224 
225 void CommandConsole::getCursorPosition(unsigned& xPosition, unsigned& yPosition) const
226 {
227  xPosition = cursorPosition % getColumns();
228  unsigned num = lines[0].numChars() / getColumns();
229  yPosition = num - (cursorPosition / getColumns());
230 }
231 
233 {
234  return consoleScrollBack;
235 }
236 
238 {
239  unsigned count = 0;
240  for (auto buf : xrange(lines.size())) {
241  count += (lines[buf].numChars() / getColumns()) + 1;
242  if (count > line) {
243  return lines[buf].substr(
244  (count - line - 1) * getColumns(),
245  getColumns());
246  }
247  }
248  return ConsoleLine();
249 }
250 
251 int CommandConsole::signalEvent(const std::shared_ptr<const Event>& event)
252 {
253  auto& keyEvent = checked_cast<const KeyEvent&>(*event);
254  if (!consoleSetting->getValue()) {
255  return 0;
256  }
257 
258  // If the console is open then don't pass the event to the MSX
259  // (whetever the (keyboard) event is). If the event has a meaning for
260  // the console, then also don't pass the event to the hotkey system.
261  // For example PgUp, PgDown are keys that have both a meaning in the
262  // console and are used by standard key bindings.
263  if (event->getType() == OPENMSX_KEY_DOWN_EVENT) {
264  if (!executingCommand) {
265  if (handleEvent(keyEvent)) {
266  // event was used
267  display.repaintDelayed(40000); // 25fps
268  return EventDistributor::HOTKEY |
270  }
271  } else {
272  // For commands that take a long time to execute (e.g.
273  // a loadstate that needs to create a filepool index),
274  // we also send events during the execution (so that
275  // we can show progress on the OSD). In that case
276  // ignore extra input events.
277  }
278  } else {
279  assert(event->getType() == OPENMSX_KEY_UP_EVENT);
280  }
281  return EventDistributor::MSX;
282 }
283 
284 bool CommandConsole::handleEvent(const KeyEvent& keyEvent)
285 {
286  auto keyCode = keyEvent.getKeyCode();
287  int key = keyCode & Keys::K_MASK;
288  int mod = keyCode & ~Keys::K_MASK;
289  word chr = keyEvent.getUnicode();
290 
291  bool used = true;
292  switch (mod) {
293  case Keys::KM_CTRL:
294  switch (key) {
295  case Keys::K_H:
296  backspace();
297  break;
298  case Keys::K_A:
299  cursorPosition = unsigned(prompt.size());
300  break;
301  case Keys::K_E:
302  cursorPosition = lines[0].numChars();
303  break;
304  case Keys::K_C:
305  clearCommand();
306  break;
307  default:
308  used = false;
309  break;
310  }
311  break;
312  case Keys::KM_SHIFT:
313  switch (key) {
314  case Keys::K_PAGEUP:
315  scroll(max<int>(getRows() - 1, 1));
316  break;
317  case Keys::K_PAGEDOWN:
318  scroll(-max<int>(getRows() - 1, 1));
319  break;
320  default:
321  if (chr) {
322  normalKey(chr);
323  } else {
324  used = false;
325  }
326  break;
327  }
328  break;
329  case Keys::KM_MODE: // e.g. to type '1-9' on a N900
330  case Keys::KM_ALT: // e.g. to type | [ ] on a azerty keyboard layout
331  // Don't add the META modifier here because that will break the Cmd+L
332  // hotkey on MacOSX to toggle the console.
333  case 0:
334  switch (key) {
335  case Keys::K_PAGEUP:
336  scroll(1);
337  break;
338  case Keys::K_PAGEDOWN:
339  scroll(-1);
340  break;
341  case Keys::K_UP:
342  prevCommand();
343  break;
344  case Keys::K_DOWN:
345  nextCommand();
346  break;
347  case Keys::K_BACKSPACE:
348  backspace();
349  break;
350  case Keys::K_DELETE:
351  delete_key();
352  break;
353  case Keys::K_TAB:
354  tabCompletion();
355  break;
356  case Keys::K_RETURN:
357  case Keys::K_KP_ENTER:
358  commandExecute();
359  cursorPosition = unsigned(prompt.size());
360  break;
361  case Keys::K_LEFT:
362  if (cursorPosition > prompt.size()) {
363  --cursorPosition;
364  }
365  break;
366  case Keys::K_RIGHT:
367  if (cursorPosition < lines[0].numChars()) {
368  ++cursorPosition;
369  }
370  break;
371  case Keys::K_HOME:
372  cursorPosition = unsigned(prompt.size());
373  break;
374  case Keys::K_END:
375  cursorPosition = lines[0].numChars();
376  break;
377  default:
378  if (chr) {
379  normalKey(chr);
380  } else {
381  used = false;
382  }
383  break;
384  }
385  break;
386  default:
387  used = false;
388  }
389  return used;
390 }
391 
392 void CommandConsole::setColumns(unsigned columns_)
393 {
394  columns = columns_;
395 }
396 
398 {
399  return columns;
400 }
401 
402 void CommandConsole::setRows(unsigned rows_)
403 {
404  rows = rows_;
405 }
406 
407 unsigned CommandConsole::getRows() const
408 {
409  return rows;
410 }
411 
412 void CommandConsole::output(string_ref text)
413 {
414  print(text);
415 }
416 
417 unsigned CommandConsole::getOutputColumns() const
418 {
419  return getColumns();
420 }
421 
422 void CommandConsole::print(string_ref text, unsigned rgb)
423 {
424  while (true) {
425  auto pos = text.find('\n');
426  newLineConsole(ConsoleLine(text.substr(0, pos), rgb));
427  if (pos == string_ref::npos) return;
428  text = text.substr(pos + 1); // skip newline
429  if (text.empty()) return;
430  }
431 }
432 
433 void CommandConsole::newLineConsole(string_ref line)
434 {
435  newLineConsole(ConsoleLine(line));
436 }
437 
438 void CommandConsole::newLineConsole(ConsoleLine line)
439 {
440  if (lines.isFull()) {
441  lines.removeBack();
442  }
443  ConsoleLine tmp = lines[0];
444  lines[0] = line;
445  lines.addFront(tmp);
446 }
447 
448 void CommandConsole::putCommandHistory(const string& command)
449 {
450  // TODO don't store PROMPT as part of history
451  if (command == prompt) {
452  return;
453  }
454  if (removeDoublesSetting->getValue() && !history.empty()
455  && (history.back() == command)) {
456  return;
457  }
458 
459  history.push_back(command);
460 
461  // if necessary, shrink history to the desired (smaller) size
462  while (history.size() > unsigned(historySizeSetting->getValue())) {
463  history.pop_front();
464  }
465 }
466 
467 void CommandConsole::commandExecute()
468 {
469  resetScrollBack();
470  putCommandHistory(lines[0].str());
471  saveHistory(); // save at this point already, so that we don't lose history in case of a crash
472 
473  commandBuffer += lines[0].str().substr(prompt.size()) + '\n';
474  newLineConsole(lines[0]);
475  if (commandController.isComplete(commandBuffer)) {
476  // Normally the busy promt is NOT shown (not even very briefly
477  // because the screen is not redrawn), though for some commands
478  // that potentially take a long time to execute, we explictly
479  // send events, see also comment in handleEvent().
480  prompt = PROMPT_BUSY;
481  putPrompt();
482 
483  try {
484  ScopedAssign<bool> sa(executingCommand, true);
485  string result = commandController.executeCommand(
486  commandBuffer);
487  if (!result.empty()) {
488  print(result);
489  }
490  } catch (CommandException& e) {
491  print(e.getMessage(), 0xff0000);
492  }
493  commandBuffer.clear();
494  prompt = PROMPT_NEW;
495  } else {
496  prompt = PROMPT_CONT;
497  }
498  putPrompt();
499 }
500 
501 ConsoleLine CommandConsole::highLight(string_ref line)
502 {
503  assert(line.starts_with(prompt));
504  string_ref command = line.substr(prompt.size());
505  ConsoleLine result;
506  result.addChunk(prompt, 0xffffff);
507 
508  TclParser parser = commandController.getInterpreter().parse(command);
509  string colors = parser.getColors();
510  assert(colors.size() == command.size());
511 
512  unsigned pos = 0;
513  while (pos != colors.size()) {
514  char col = colors[pos];
515  unsigned pos2 = pos++;
516  while ((pos != colors.size()) && (colors[pos] == col)) {
517  ++pos;
518  }
519  // TODO make these color configurable?
520  unsigned rgb;
521  switch (col) {
522  case 'E': rgb = 0xff0000; break; // error
523  case 'c': rgb = 0x5c5cff; break; // comment
524  case 'v': rgb = 0x00ffff; break; // variable
525  case 'l': rgb = 0xff00ff; break; // literal
526  case 'p': rgb = 0xcdcd00; break; // proc
527  case 'o': rgb = 0x00cdcd; break; // operator
528  default: rgb = 0xffffff; break; // other
529  }
530  result.addChunk(command.substr(pos2, pos - pos2), rgb);
531  }
532  return result;
533 }
534 
535 void CommandConsole::putPrompt()
536 {
537  commandScrollBack = history.end();
538  currentLine = prompt;
539  lines[0] = highLight(currentLine);
540  cursorPosition = unsigned(prompt.size());
541 }
542 
543 void CommandConsole::tabCompletion()
544 {
545  resetScrollBack();
546  unsigned pl = unsigned(prompt.size());
547  string_ref front = utf8::unchecked::substr(lines[0].str(), pl, cursorPosition - pl);
548  string_ref back = utf8::unchecked::substr(lines[0].str(), cursorPosition);
549  string newFront = commandController.tabCompletion(front);
550  cursorPosition = pl + unsigned(utf8::unchecked::size(newFront));
551  currentLine = prompt + newFront + back;
552  lines[0] = highLight(currentLine);
553 }
554 
555 void CommandConsole::scroll(int delta)
556 {
557  consoleScrollBack = min(max(consoleScrollBack + delta, 0),
558  int(lines.size()));
559 }
560 
561 void CommandConsole::prevCommand()
562 {
563  resetScrollBack();
564  if (history.empty()) {
565  return; // no elements
566  }
567  bool match = false;
568  auto tempScrollBack = commandScrollBack;
569  while ((tempScrollBack != history.begin()) && !match) {
570  --tempScrollBack;
571  match = StringOp::startsWith(*tempScrollBack, currentLine);
572  }
573  if (match) {
574  commandScrollBack = tempScrollBack;
575  lines[0] = highLight(*commandScrollBack);
576  cursorPosition = lines[0].numChars();
577  }
578 }
579 
580 void CommandConsole::nextCommand()
581 {
582  resetScrollBack();
583  if (commandScrollBack == history.end()) {
584  return; // don't loop !
585  }
586  bool match = false;
587  auto tempScrollBack = commandScrollBack;
588  while ((++tempScrollBack != history.end()) && !match) {
589  match = StringOp::startsWith(*tempScrollBack, currentLine);
590  }
591  if (match) {
592  --tempScrollBack; // one time to many
593  commandScrollBack = tempScrollBack;
594  lines[0] = highLight(*commandScrollBack);
595  } else {
596  commandScrollBack = history.end();
597  lines[0] = highLight(currentLine);
598  }
599  cursorPosition = lines[0].numChars();
600 }
601 
602 void CommandConsole::clearCommand()
603 {
604  resetScrollBack();
605  commandBuffer.clear();
606  prompt = PROMPT_NEW;
607  currentLine = prompt;
608  lines[0] = highLight(currentLine);
609  cursorPosition = unsigned(prompt.size());
610 }
611 
612 void CommandConsole::backspace()
613 {
614  resetScrollBack();
615  if (cursorPosition > prompt.size()) {
616  currentLine = lines[0].str();
617  auto begin = currentLine.begin();
618  utf8::unchecked::advance(begin, cursorPosition - 1);
619  auto end = begin;
620  utf8::unchecked::advance(end, 1);
621  currentLine.erase(begin, end);
622  lines[0] = highLight(currentLine);
623  --cursorPosition;
624  }
625 }
626 
627 void CommandConsole::delete_key()
628 {
629  resetScrollBack();
630  if (lines[0].numChars() > cursorPosition) {
631  currentLine = lines[0].str();
632  auto begin = currentLine.begin();
633  utf8::unchecked::advance(begin, cursorPosition);
634  auto end = begin;
635  utf8::unchecked::advance(end, 1);
636  currentLine.erase(begin, end);
637  lines[0] = highLight(currentLine);
638  }
639 }
640 
641 void CommandConsole::normalKey(word chr)
642 {
643  assert(chr);
644  resetScrollBack();
645  currentLine = lines[0].str();
646  auto pos = currentLine.begin();
647  utf8::unchecked::advance(pos, cursorPosition);
648  utf8::unchecked::append(uint32_t(chr), inserter(currentLine, pos));
649  lines[0] = highLight(currentLine);
650  ++cursorPosition;
651 }
652 
653 void CommandConsole::resetScrollBack()
654 {
655  consoleScrollBack = 0;
656 }
657 
658 } // namespace openmsx