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 "InputEvents.hh"
12 #include "Display.hh"
13 #include "EventDistributor.hh"
14 #include "Version.hh"
15 #include "checked_cast.hh"
16 #include "utf8_unchecked.hh"
17 #include "StringOp.hh"
18 #include "ScopedAssign.hh"
19 #include "xrange.hh"
20 #include <algorithm>
21 #include <fstream>
22 #include <cassert>
23 
24 using std::min;
25 using std::max;
26 using std::string;
27 
28 namespace openmsx {
29 
30 // class ConsoleLine
31 
32 ConsoleLine::ConsoleLine(string line_, uint32_t rgb)
33  : line(std::move(line_))
34  , chunks(1, {rgb, 0})
35 {
36 }
37 
38 void ConsoleLine::addChunk(string_view text, uint32_t rgb)
39 {
40  chunks.emplace_back(rgb, line.size());
41  line.append(text.data(), text.size());
42 }
43 
44 size_t ConsoleLine::numChars() const
45 {
46  return utf8::unchecked::size(line);
47 }
48 
49 uint32_t ConsoleLine::chunkColor(size_t i) const
50 {
51  assert(i < chunks.size());
52  return chunks[i].first;
53 }
54 
56 {
57  assert(i < chunks.size());
58  auto pos = chunks[i].second;
59  auto len = ((i + 1) == chunks.size())
61  : chunks[i + 1].second - pos;
62  return string_view(line).substr(pos, len);
63 }
64 
65 ConsoleLine ConsoleLine::substr(size_t pos, size_t len) const
66 {
67  ConsoleLine result;
68  if (chunks.empty()) {
69  assert(line.empty());
70  assert(pos == 0);
71  return result;
72  }
73 
74  auto b = begin(line);
76  auto e = b;
77  while (len-- && (e != end(line))) {
79  }
80  result.line.assign(b, e);
81 
82  unsigned bpos = b - begin(line);
83  unsigned bend = e - begin(line);
84  unsigned i = 1;
85  while ((i < chunks.size()) && (chunks[i].second <= bpos)) {
86  ++i;
87  }
88  result.chunks.emplace_back(chunks[i - 1].first, 0);
89  while ((i < chunks.size()) && (chunks[i].second < bend)) {
90  result.chunks.emplace_back(chunks[i].first,
91  chunks[i].second - bpos);
92  ++i;
93  }
94  return result;
95 }
96 
97 // class CommandConsole
98 
99 static const char* const PROMPT_NEW = "> ";
100 static const char* const PROMPT_CONT = "| ";
101 static const char* const PROMPT_BUSY = "*busy*";
102 
104  GlobalCommandController& commandController_,
105  EventDistributor& eventDistributor_,
106  Display& display_)
107  : commandController(commandController_)
108  , eventDistributor(eventDistributor_)
109  , display(display_)
110  , consoleSetting(
111  commandController, "console",
112  "turns console display on/off", false, Setting::DONT_SAVE)
113  , historySizeSetting(
114  commandController, "console_history_size",
115  "amount of commands kept in console history", 100, 0, 10000)
116  , removeDoublesSetting(
117  commandController, "console_remove_doubles",
118  "don't add the command to history if it's the same as the previous one",
119  true)
120  , history(std::max(1, historySizeSetting.getInt()))
121  , executingCommand(false)
122 {
123  resetScrollBack();
124  prompt = PROMPT_NEW;
125  newLineConsole(prompt);
126  loadHistory();
127  putPrompt();
128  Completer::setOutput(this);
129 
130  const auto& fullVersion = Version::full();
131  print(fullVersion);
132  print(string(fullVersion.size(), '-'));
133  print("\n"
134  "General information about openMSX is available at "
135  "http://openmsx.org.\n"
136  "\n"
137  "Type 'help' to see a list of available commands "
138  "(use <PgUp>/<PgDn> to scroll).\n"
139  "Or read the Console Command Reference in the manual.\n"
140  "\n");
141 
142  commandController.getInterpreter().setOutput(this);
143  eventDistributor.registerEventListener(
145  // also listen to KEY_UP events, so that we can consume them
146  eventDistributor.registerEventListener(
148 }
149 
151 {
152  eventDistributor.unregisterEventListener(OPENMSX_KEY_DOWN_EVENT, *this);
153  eventDistributor.unregisterEventListener(OPENMSX_KEY_UP_EVENT, *this);
154  commandController.getInterpreter().setOutput(nullptr);
155  Completer::setOutput(nullptr);
156 }
157 
158 void CommandConsole::saveHistory()
159 {
160  try {
161  std::ofstream outputfile;
162  FileOperations::openofstream(outputfile,
163  userFileContext("console").resolveCreate("history.txt"));
164  if (!outputfile) {
165  throw FileException(
166  "Error while saving the console history.");
167  }
168  for (auto& s : history) {
169  outputfile << s << '\n';
170  }
171  } catch (FileException& e) {
172  commandController.getCliComm().printWarning(e.getMessage());
173  }
174 }
175 
176 void CommandConsole::loadHistory()
177 {
178  try {
179  std::ifstream inputfile(
180  userFileContext("console").
181  resolveCreate("history.txt").c_str());
182  string line;
183  while (inputfile) {
184  getline(inputfile, line);
185  putCommandHistory(line);
186  }
187  } catch (FileException&) {
188  // Error while loading the console history, ignore
189  }
190 }
191 
192 void CommandConsole::getCursorPosition(unsigned& xPosition, unsigned& yPosition) const
193 {
194  xPosition = cursorPosition % getColumns();
195  auto num = lines[0].numChars() / getColumns();
196  yPosition = unsigned(num - (cursorPosition / getColumns()));
197 }
198 
200 {
201  size_t count = 0;
202  for (auto buf : xrange(lines.size())) {
203  count += (lines[buf].numChars() / getColumns()) + 1;
204  if (count > line) {
205  return lines[buf].substr(
206  (count - line - 1) * getColumns(),
207  getColumns());
208  }
209  }
210  return ConsoleLine();
211 }
212 
213 int CommandConsole::signalEvent(const std::shared_ptr<const Event>& event)
214 {
215  if (!consoleSetting.getBoolean()) return 0;
216  auto& keyEvent = checked_cast<const KeyEvent&>(*event);
217 
218  // If the console is open then don't pass the event to the MSX
219  // (whetever the (keyboard) event is). If the event has a meaning for
220  // the console, then also don't pass the event to the hotkey system.
221  // For example PgUp, PgDown are keys that have both a meaning in the
222  // console and are used by standard key bindings.
223  if (event->getType() == OPENMSX_KEY_DOWN_EVENT) {
224  if (!executingCommand) {
225  if (handleEvent(keyEvent)) {
226  // event was used
227  display.repaintDelayed(40000); // 25fps
228  return EventDistributor::HOTKEY; // block HOTKEY and MSX
229  }
230  } else {
231  // For commands that take a long time to execute (e.g.
232  // a loadstate that needs to create a filepool index),
233  // we also send events during the execution (so that
234  // we can show progress on the OSD). In that case
235  // ignore extra input events.
236  }
237  } else {
238  assert(event->getType() == OPENMSX_KEY_UP_EVENT);
239  }
240  return EventDistributor::MSX; // block MSX
241 }
242 
243 bool CommandConsole::handleEvent(const KeyEvent& keyEvent)
244 {
245  auto keyCode = keyEvent.getKeyCode();
246  int key = keyCode & Keys::K_MASK;
247  int mod = keyCode & ~Keys::K_MASK;
248 
249  switch (mod) {
250  case Keys::KM_CTRL:
251  switch (key) {
252  case Keys::K_H:
253  backspace();
254  return true;
255  case Keys::K_A:
256  cursorPosition = unsigned(prompt.size());
257  return true;
258  case Keys::K_E:
259  cursorPosition = unsigned(lines[0].numChars());
260  return true;
261  case Keys::K_C:
262  clearCommand();
263  return true;
264  }
265  break;
266  case Keys::KM_SHIFT:
267  switch (key) {
268  case Keys::K_PAGEUP:
269  scroll(max<int>(getRows() - 1, 1));
270  return true;
271  case Keys::K_PAGEDOWN:
272  scroll(-max<int>(getRows() - 1, 1));
273  return true;
274  }
275  break;
276  case 0: // no modifier
277  switch (key) {
278  case Keys::K_PAGEUP:
279  scroll(1);
280  return true;
281  case Keys::K_PAGEDOWN:
282  scroll(-1);
283  return true;
284  case Keys::K_UP:
285  prevCommand();
286  return true;
287  case Keys::K_DOWN:
288  nextCommand();
289  return true;
290  case Keys::K_BACKSPACE:
291  backspace();
292  return true;
293  case Keys::K_DELETE:
294  delete_key();
295  return true;
296  case Keys::K_TAB:
297  tabCompletion();
298  return true;
299  case Keys::K_RETURN:
300  case Keys::K_KP_ENTER:
301  commandExecute();
302  cursorPosition = unsigned(prompt.size());
303  return true;
304  case Keys::K_LEFT:
305  if (cursorPosition > prompt.size()) {
306  --cursorPosition;
307  }
308  return true;
309  case Keys::K_RIGHT:
310  if (cursorPosition < lines[0].numChars()) {
311  ++cursorPosition;
312  }
313  return true;
314  case Keys::K_HOME:
315  cursorPosition = unsigned(prompt.size());
316  return true;
317  case Keys::K_END:
318  cursorPosition = unsigned(lines[0].numChars());
319  return true;
320  }
321  break;
322  }
323 
324  auto unicode = keyEvent.getUnicode();
325  if (!unicode || (mod & Keys::KM_META)) {
326  // Disallow META modifer for 'normal' key presses because on
327  // MacOSX Cmd+L is used as a hotkey to toggle the console.
328  // Hopefully there are no systems that require META to type
329  // normal keys. However there _are_ systems that require the
330  // following modifiers, some examples:
331  // MODE: to type '1-9' on a N900
332  // ALT: to type | [ ] on a azerty keyboard layout
333  // CTRL+ALT: to type '#' on a spanish keyboard layout (windows)
334  //
335  // Event was not used by the console, allow the other
336  // subsystems to process it. E.g. F10, or Cmd+L to close the
337  // console.
338  return false;
339  }
340 
341  // Apparently on macOS keyboard events for keys like F1 have a non-zero
342  // unicode field. This confuses the console code (it thinks it's a
343  // printable character) and it prevents those keys from triggering
344  // hotkey bindings. See this bug report:
345  // https://github.com/openMSX/openMSX/issues/1095
346  // As a workaround we ignore chars in the 'Private Use Area' (PUA).
347  // https://en.wikipedia.org/wiki/Private_Use_Areas
348  if (utf8::is_pua(unicode)) {
349  return false;
350  }
351 
352  if (unicode >= 0x20) {
353  normalKey(unicode);
354  } else {
355  // Skip CTRL-<X> combinations, but still return true.
356  }
357  return true;
358 }
359 
360 void CommandConsole::output(string_view text)
361 {
362  print(text);
363 }
364 
365 unsigned CommandConsole::getOutputColumns() const
366 {
367  return getColumns();
368 }
369 
370 void CommandConsole::print(string_view text, unsigned rgb)
371 {
372  while (true) {
373  auto pos = text.find('\n');
374  newLineConsole(ConsoleLine(text.substr(0, pos).str(), rgb));
375  if (pos == string_view::npos) return;
376  text = text.substr(pos + 1); // skip newline
377  if (text.empty()) return;
378  }
379 }
380 
381 void CommandConsole::newLineConsole(string line)
382 {
383  newLineConsole(ConsoleLine(std::move(line)));
384 }
385 
386 void CommandConsole::newLineConsole(ConsoleLine line)
387 {
388  if (lines.isFull()) {
389  lines.removeBack();
390  }
391  ConsoleLine tmp = std::move(lines[0]);
392  lines[0] = std::move(line);
393  lines.addFront(std::move(tmp));
394 }
395 
396 void CommandConsole::putCommandHistory(const string& command)
397 {
398  if (command.empty()) return;
399  if (removeDoublesSetting.getBoolean() && !history.empty() &&
400  (history.back() == command)) {
401  return;
402  }
403  if (history.full()) history.pop_front();
404  history.push_back(command);
405 }
406 
407 void CommandConsole::commandExecute()
408 {
409  resetScrollBack();
410  string cmd0 = lines[0].str().substr(prompt.size());
411  putCommandHistory(cmd0);
412  saveHistory(); // save at this point already, so that we don't lose history in case of a crash
413 
414  strAppend(commandBuffer, cmd0, '\n');
415  newLineConsole(lines[0]);
416  if (commandController.isComplete(commandBuffer)) {
417  // Normally the busy prompt is NOT shown (not even very briefly
418  // because the screen is not redrawn), though for some commands
419  // that potentially take a long time to execute, we explictly
420  // send events, see also comment in signalEvent().
421  prompt = PROMPT_BUSY;
422  putPrompt();
423 
424  try {
425  ScopedAssign<bool> sa(executingCommand, true);
426  auto resultObj = commandController.executeCommand(
427  commandBuffer);
428  auto result = resultObj.getString();
429  if (!result.empty()) {
430  print(result);
431  }
432  } catch (CommandException& e) {
433  print(e.getMessage(), 0xff0000);
434  }
435  commandBuffer.clear();
436  prompt = PROMPT_NEW;
437  } else {
438  prompt = PROMPT_CONT;
439  }
440  putPrompt();
441 }
442 
443 ConsoleLine CommandConsole::highLight(string_view line)
444 {
445  ConsoleLine result;
446  result.addChunk(prompt, 0xffffff);
447 
448  TclParser parser = commandController.getInterpreter().parse(line);
449  string colors = parser.getColors();
450  assert(colors.size() == line.size());
451 
452  unsigned pos = 0;
453  while (pos != colors.size()) {
454  char col = colors[pos];
455  unsigned pos2 = pos++;
456  while ((pos != colors.size()) && (colors[pos] == col)) {
457  ++pos;
458  }
459  // TODO make these color configurable?
460  unsigned rgb;
461  switch (col) {
462  case 'E': rgb = 0xff0000; break; // error
463  case 'c': rgb = 0x5c5cff; break; // comment
464  case 'v': rgb = 0x00ffff; break; // variable
465  case 'l': rgb = 0xff00ff; break; // literal
466  case 'p': rgb = 0xcdcd00; break; // proc
467  case 'o': rgb = 0x00cdcd; break; // operator
468  default: rgb = 0xffffff; break; // other
469  }
470  result.addChunk(line.substr(pos2, pos - pos2), rgb);
471  }
472  return result;
473 }
474 
475 void CommandConsole::putPrompt()
476 {
477  commandScrollBack = unsigned(history.size());
478  currentLine.clear();
479  lines[0] = highLight(currentLine);
480  cursorPosition = unsigned(prompt.size());
481 }
482 
483 void CommandConsole::tabCompletion()
484 {
485  resetScrollBack();
486  auto pl = unsigned(prompt.size());
487  string front = utf8::unchecked::substr(lines[0].str(), pl, cursorPosition - pl).str();
488  string back = utf8::unchecked::substr(lines[0].str(), cursorPosition).str();
489  string newFront = commandController.tabCompletion(front);
490  cursorPosition = pl + unsigned(utf8::unchecked::size(newFront));
491  currentLine = newFront + back;
492  lines[0] = highLight(currentLine);
493 }
494 
495 void CommandConsole::scroll(int delta)
496 {
497  consoleScrollBack = min(max(consoleScrollBack + delta, 0),
498  int(lines.size()));
499 }
500 
501 void CommandConsole::prevCommand()
502 {
503  resetScrollBack();
504  if (history.empty()) {
505  return; // no elements
506  }
507  bool match = false;
508  unsigned tmp = commandScrollBack;
509  while ((tmp != 0) && !match) {
510  --tmp;
511  match = StringOp::startsWith(history[tmp], currentLine);
512  }
513  if (match) {
514  commandScrollBack = tmp;
515  lines[0] = highLight(history[commandScrollBack]);
516  cursorPosition = unsigned(lines[0].numChars());
517  }
518 }
519 
520 void CommandConsole::nextCommand()
521 {
522  resetScrollBack();
523  if (commandScrollBack == history.size()) {
524  return; // don't loop !
525  }
526  bool match = false;
527  auto tmp = commandScrollBack;
528  while ((++tmp != history.size()) && !match) {
529  match = StringOp::startsWith(history[tmp], currentLine);
530  }
531  if (match) {
532  --tmp; // one time to many
533  commandScrollBack = tmp;
534  lines[0] = highLight(history[commandScrollBack]);
535  } else {
536  commandScrollBack = unsigned(history.size());
537  lines[0] = highLight(currentLine);
538  }
539  cursorPosition = unsigned(lines[0].numChars());
540 }
541 
542 void CommandConsole::clearCommand()
543 {
544  resetScrollBack();
545  commandBuffer.clear();
546  prompt = PROMPT_NEW;
547  currentLine.clear();
548  lines[0] = highLight(currentLine);
549  cursorPosition = unsigned(prompt.size());
550 }
551 
552 void CommandConsole::backspace()
553 {
554  resetScrollBack();
555  if (cursorPosition > prompt.size()) {
556  currentLine = lines[0].str();
557  auto b = begin(currentLine);
558  utf8::unchecked::advance(b, cursorPosition - 1);
559  auto e = b;
561  currentLine.erase(b, e);
562  currentLine.erase(0, prompt.size());
563  lines[0] = highLight(currentLine);
564  --cursorPosition;
565  }
566 }
567 
568 void CommandConsole::delete_key()
569 {
570  resetScrollBack();
571  if (lines[0].numChars() > cursorPosition) {
572  currentLine = lines[0].str();
573  auto b = begin(currentLine);
574  utf8::unchecked::advance(b, cursorPosition);
575  auto e = b;
577  currentLine.erase(b, e);
578  currentLine.erase(0, prompt.size());
579  lines[0] = highLight(currentLine);
580  }
581 }
582 
583 void CommandConsole::normalKey(uint32_t chr)
584 {
585  assert(chr);
586  resetScrollBack();
587  currentLine = lines[0].str();
588  auto pos = begin(currentLine);
589  utf8::unchecked::advance(pos, cursorPosition);
590  utf8::unchecked::append(chr, inserter(currentLine, pos));
591  currentLine.erase(0, prompt.size());
592  lines[0] = highLight(currentLine);
593  ++cursorPosition;
594 }
595 
596 void CommandConsole::resetScrollBack()
597 {
598  consoleScrollBack = 0;
599 }
600 
601 } // namespace openmsx
const char * data() const
Definition: string_view.hh:57
void getCursorPosition(unsigned &xPosition, unsigned &yPosition) const
octet_iterator append(uint32_t cp, octet_iterator result)
const std::string & getMessage() const &
Definition: MSXException.hh:23
Represents the output window/screen of openMSX.
Definition: Display.hh:31
string_view chunkText(size_t i) const
Get the text for the i-th chunk.
auto xrange(T e)
Definition: xrange.hh:170
std::string getColors() const
Ouput: a string of equal length of the input command where each character indicates the type of the c...
Definition: TclParser.hh:27
void registerEventListener(EventType type, EventListener &listener, Priority priority=OTHER)
Registers a given object to receive certain events.
vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:269
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
void openofstream(std::ofstream &stream, const std::string &filename)
Open an ofstream in a platform-independent manner.
ConsoleLine getLine(unsigned line) const
uint32_t chunkColor(size_t i) const
Get the color for the i-th chunk.
size_t size() const
bool is_pua(uint32_t cp)
Definition: utf8_core.hh:255
uint32_t next(octet_iterator &it)
bool startsWith(string_view total, string_view part)
Definition: StringOp.cc:69
STL namespace.
string_view getString() const
Definition: TclObject.cc:102
vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:287
This class represents a single text line in the console.
void strAppend(std::string &result, Ts &&...ts)
Definition: strCat.hh:648
TclParser parse(string_view command)
Definition: Interpreter.cc:447
static const size_type npos
Definition: string_view.hh:24
auto begin(const string_view &x)
Definition: string_view.hh:151
size_t size(string_view utf8)
bool getBoolean() const noexcept
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:160
CommandConsole(GlobalCommandController &commandController, EventDistributor &eventDistributor, Display &display)
uint32_t getUnicode() const
Definition: InputEvents.hh:30
bool isComplete(const std::string &command)
Returns true iff the command is complete (all braces, quotes etc.
ConsoleLine substr(size_t pos, size_t len) const
Get a part of total line.
size_type find(string_view s) const
Definition: string_view.cc:38
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
Definition: lz4.cc:207
static void setOutput(InterpreterOutput *output_)
Definition: Completer.hh:64
unsigned getColumns() const
void repaintDelayed(uint64_t delta)
Definition: Display.cc:367
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
size_t numChars() const
Get the number of UTF8 characters in this line.
void addChunk(string_view text, uint32_t rgb)
Append a chunk with a (different) color.
Keys::KeyCode getKeyCode() const
Definition: InputEvents.hh:29
bool empty() const
Definition: string_view.hh:45
This class implements a (close approximation) of the std::string_view class.
Definition: string_view.hh:16
std::string tabCompletion(string_view command)
Complete the given command.
bool empty() const
std::string str() const
Definition: string_view.cc:12
void advance(octet_iterator &it, distance_type n)
ConsoleLine()=default
Construct empty line.
string_view substr(size_type pos, size_type n=npos) const
Definition: string_view.cc:32
static std::string full()
Definition: Version.cc:8
void setOutput(InterpreterOutput *output_)
Definition: Interpreter.hh:25
string_view substr(string_view utf8, string_view::size_type first=0, string_view::size_type len=string_view::npos)
bool full() const
size_type size() const
Definition: string_view.hh:44
void push_back(const T &t)
void printWarning(string_view message)
Definition: CliComm.cc:20
TclObject executeCommand(const std::string &command, CliConnection *connection=nullptr) override
Execute the given command.
auto end(const string_view &x)
Definition: string_view.hh:152
Assign new value to some variable and restore the original value when this object goes out of scope...
Definition: ScopedAssign.hh:7
unsigned getRows() const