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