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 
33 {
34 }
35 
37  : line(line_.str())
38  , chunks(1, {rgb, 0})
39 {
40 }
41 
42 void ConsoleLine::addChunk(string_ref text, unsigned rgb)
43 {
44  chunks.emplace_back(rgb, line.size());
45  line.append(text.data(), text.size());
46 }
47 
48 unsigned ConsoleLine::numChars() const
49 {
50  return unsigned(utf8::unchecked::size(line));
51 }
52 
53 unsigned ConsoleLine::chunkColor(unsigned i) const
54 {
55  assert(i < chunks.size());
56  return chunks[i].first;
57 }
58 
60 {
61  assert(i < chunks.size());
62  auto pos = chunks[i].second;
63  auto len = ((i + 1) == chunks.size())
65  : chunks[i + 1].second - pos;
66  return string_ref(line).substr(pos, len);
67 }
68 
69 ConsoleLine ConsoleLine::substr(unsigned pos, unsigned len) const
70 {
71  ConsoleLine result;
72  if (chunks.empty()) {
73  assert(line.empty());
74  assert(pos == 0);
75  return result;
76  }
77 
78  auto b = begin(line);
80  auto e = b;
81  while (len-- && (e != end(line))) {
83  }
84  result.line.assign(b, e);
85 
86  unsigned bpos = b - begin(line);
87  unsigned bend = e - begin(line);
88  unsigned i = 1;
89  while ((i < chunks.size()) && (chunks[i].second <= bpos)) {
90  ++i;
91  }
92  result.chunks.emplace_back(chunks[i - 1].first, 0);
93  while ((i < chunks.size()) && (chunks[i].second < bend)) {
94  result.chunks.emplace_back(chunks[i].first,
95  chunks[i].second - bpos);
96  ++i;
97  }
98  return result;
99 }
100 
101 // class CommandConsole
102 
103 static const char* const PROMPT_NEW = "> ";
104 static const char* const PROMPT_CONT = "| ";
105 static const char* const PROMPT_BUSY = "*busy*";
106 
108  GlobalCommandController& commandController_,
109  EventDistributor& eventDistributor_,
110  Display& display_)
111  : commandController(commandController_)
112  , eventDistributor(eventDistributor_)
113  , display(display_)
114  , consoleSetting(
115  commandController, "console",
116  "turns console display on/off", false, Setting::DONT_SAVE)
117  , historySizeSetting(
118  commandController, "console_history_size",
119  "amount of commands kept in console history", 100, 0, 10000)
120  , removeDoublesSetting(
121  commandController, "console_remove_doubles",
122  "don't add the command to history if it's the same as the previous one",
123  true)
124  , history(std::max(1, historySizeSetting.getInt()))
125  , executingCommand(false)
126 {
127  resetScrollBack();
128  prompt = PROMPT_NEW;
129  newLineConsole(prompt);
130  loadHistory();
131  putPrompt();
132  Completer::setOutput(this);
133 
134  const auto& fullVersion = Version::full();
135  print(fullVersion);
136  print(string(fullVersion.size(), '-'));
137  print("\n"
138  "General information about openMSX is available at "
139  "http://www.openmsx.org.\n"
140  "\n"
141  "Type 'help' to see a list of available commands "
142  "(use <PgUp>/<PgDn> to scroll).\n"
143  "Or read the Console Command Reference in the manual.\n"
144  "\n");
145 
146  commandController.getInterpreter().setOutput(this);
147  eventDistributor.registerEventListener(
149  // also listen to KEY_UP events, so that we can consume them
150  eventDistributor.registerEventListener(
152 }
153 
155 {
156  eventDistributor.unregisterEventListener(OPENMSX_KEY_DOWN_EVENT, *this);
157  eventDistributor.unregisterEventListener(OPENMSX_KEY_UP_EVENT, *this);
158  commandController.getInterpreter().setOutput(nullptr);
159  Completer::setOutput(nullptr);
160 }
161 
162 void CommandConsole::saveHistory()
163 {
164  try {
165  std::ofstream outputfile;
166  FileOperations::openofstream(outputfile,
167  userFileContext("console").resolveCreate("history.txt"));
168  if (!outputfile) {
169  throw FileException(
170  "Error while saving the console history.");
171  }
172  for (auto& s : history) {
173  outputfile << string_ref(s).substr(prompt.size()) << '\n';
174  }
175  } catch (FileException& e) {
176  commandController.getCliComm().printWarning(e.getMessage());
177  }
178 }
179 
180 void CommandConsole::loadHistory()
181 {
182  try {
183  std::ifstream inputfile(
184  userFileContext("console").
185  resolveCreate("history.txt").c_str());
186  string line;
187  while (inputfile) {
188  getline(inputfile, line);
189  if (!line.empty()) {
190  putCommandHistory(prompt + line);
191  }
192  }
193  } catch (FileException&) {
194  // Error while loading the console history, ignore
195  }
196 }
197 
198 void CommandConsole::getCursorPosition(unsigned& xPosition, unsigned& yPosition) const
199 {
200  xPosition = cursorPosition % getColumns();
201  unsigned num = lines[0].numChars() / getColumns();
202  yPosition = num - (cursorPosition / getColumns());
203 }
204 
206 {
207  unsigned count = 0;
208  for (auto buf : xrange(lines.size())) {
209  count += (lines[buf].numChars() / getColumns()) + 1;
210  if (count > line) {
211  return lines[buf].substr(
212  (count - line - 1) * getColumns(),
213  getColumns());
214  }
215  }
216  return ConsoleLine();
217 }
218 
219 int CommandConsole::signalEvent(const std::shared_ptr<const Event>& event)
220 {
221  auto& keyEvent = checked_cast<const KeyEvent&>(*event);
222  if (!consoleSetting.getBoolean()) return 0;
223 
224  // If the console is open then don't pass the event to the MSX
225  // (whetever the (keyboard) event is). If the event has a meaning for
226  // the console, then also don't pass the event to the hotkey system.
227  // For example PgUp, PgDown are keys that have both a meaning in the
228  // console and are used by standard key bindings.
229  if (event->getType() == OPENMSX_KEY_DOWN_EVENT) {
230  if (!executingCommand) {
231  if (handleEvent(keyEvent)) {
232  // event was used
233  display.repaintDelayed(40000); // 25fps
234  return EventDistributor::HOTKEY; // block HOTKEY and MSX
235  }
236  } else {
237  // For commands that take a long time to execute (e.g.
238  // a loadstate that needs to create a filepool index),
239  // we also send events during the execution (so that
240  // we can show progress on the OSD). In that case
241  // ignore extra input events.
242  }
243  } else {
244  assert(event->getType() == OPENMSX_KEY_UP_EVENT);
245  }
246  return EventDistributor::MSX; // block MSX
247 }
248 
249 bool CommandConsole::handleEvent(const KeyEvent& keyEvent)
250 {
251  auto keyCode = keyEvent.getKeyCode();
252  int key = keyCode & Keys::K_MASK;
253  int mod = keyCode & ~Keys::K_MASK;
254 
255  switch (mod) {
256  case Keys::KM_CTRL:
257  switch (key) {
258  case Keys::K_H:
259  backspace();
260  return true;
261  case Keys::K_A:
262  cursorPosition = unsigned(prompt.size());
263  return true;
264  case Keys::K_E:
265  cursorPosition = lines[0].numChars();
266  return true;
267  case Keys::K_C:
268  clearCommand();
269  return true;
270  }
271  break;
272  case Keys::KM_SHIFT:
273  switch (key) {
274  case Keys::K_PAGEUP:
275  scroll(max<int>(getRows() - 1, 1));
276  return true;
277  case Keys::K_PAGEDOWN:
278  scroll(-max<int>(getRows() - 1, 1));
279  return true;
280  }
281  break;
282  case 0: // no modifier
283  switch (key) {
284  case Keys::K_PAGEUP:
285  scroll(1);
286  return true;
287  case Keys::K_PAGEDOWN:
288  scroll(-1);
289  return true;
290  case Keys::K_UP:
291  prevCommand();
292  return true;
293  case Keys::K_DOWN:
294  nextCommand();
295  return true;
296  case Keys::K_BACKSPACE:
297  backspace();
298  return true;
299  case Keys::K_DELETE:
300  delete_key();
301  return true;
302  case Keys::K_TAB:
303  tabCompletion();
304  return true;
305  case Keys::K_RETURN:
306  case Keys::K_KP_ENTER:
307  commandExecute();
308  cursorPosition = unsigned(prompt.size());
309  return true;
310  case Keys::K_LEFT:
311  if (cursorPosition > prompt.size()) {
312  --cursorPosition;
313  }
314  return true;
315  case Keys::K_RIGHT:
316  if (cursorPosition < lines[0].numChars()) {
317  ++cursorPosition;
318  }
319  return true;
320  case Keys::K_HOME:
321  cursorPosition = unsigned(prompt.size());
322  return true;
323  case Keys::K_END:
324  cursorPosition = lines[0].numChars();
325  return true;
326  }
327  break;
328  }
329 
330  uint16_t unicode = keyEvent.getUnicode();
331  if (!unicode || (mod & Keys::KM_META)) {
332  // Disallow META modifer for 'normal' key presses because on
333  // MacOSX Cmd+L is used as a hotkey to toggle the console.
334  // Hopefully there are no systems that require META to type
335  // normal keys. However there _are_ systems that require the
336  // following modifiers, some examples:
337  // MODE: to type '1-9' on a N900
338  // ALT: to type | [ ] on a azerty keyboard layout
339  // CTRL+ALT: to type '#' on a spanish keyboard layout (windows)
340  //
341  // Event was not used by the console, allow the other
342  // subsystems to process it. E.g. F10, or Cmd+L to close the
343  // console.
344  return false;
345  }
346 
347  if (unicode >= 0x20) {
348  normalKey(unicode);
349  } else {
350  // Skip CTRL-<X> combinations, but still return true.
351  }
352  return true;
353 }
354 
355 void CommandConsole::output(string_ref text)
356 {
357  print(text);
358 }
359 
360 unsigned CommandConsole::getOutputColumns() const
361 {
362  return getColumns();
363 }
364 
365 void CommandConsole::print(string_ref text, unsigned rgb)
366 {
367  while (true) {
368  auto pos = text.find('\n');
369  newLineConsole(ConsoleLine(text.substr(0, pos), rgb));
370  if (pos == string_ref::npos) return;
371  text = text.substr(pos + 1); // skip newline
372  if (text.empty()) return;
373  }
374 }
375 
376 void CommandConsole::newLineConsole(string_ref line)
377 {
378  newLineConsole(ConsoleLine(line));
379 }
380 
381 void CommandConsole::newLineConsole(ConsoleLine line)
382 {
383  if (lines.isFull()) {
384  lines.removeBack();
385  }
386  ConsoleLine tmp = lines[0];
387  lines[0] = line;
388  lines.addFront(tmp);
389 }
390 
391 void CommandConsole::putCommandHistory(const string& command)
392 {
393  // TODO don't store PROMPT as part of history
394  if (command == prompt) {
395  return;
396  }
397  if (removeDoublesSetting.getBoolean() && !history.empty()
398  && (history.back() == command)) {
399  return;
400  }
401 
402  if (history.full()) history.pop_front();
403  history.push_back(command);
404 
405 }
406 
407 void CommandConsole::commandExecute()
408 {
409  resetScrollBack();
410  putCommandHistory(lines[0].str());
411  saveHistory(); // save at this point already, so that we don't lose history in case of a crash
412 
413  commandBuffer += lines[0].str().substr(prompt.size()) + '\n';
414  newLineConsole(lines[0]);
415  if (commandController.isComplete(commandBuffer)) {
416  // Normally the busy promt is NOT shown (not even very briefly
417  // because the screen is not redrawn), though for some commands
418  // that potentially take a long time to execute, we explictly
419  // send events, see also comment in handleEvent().
420  prompt = PROMPT_BUSY;
421  putPrompt();
422 
423  try {
424  ScopedAssign<bool> sa(executingCommand, true);
425  auto resultObj = commandController.executeCommand(
426  commandBuffer);
427  auto result = resultObj.getString();
428  if (!result.empty()) {
429  print(result);
430  }
431  } catch (CommandException& e) {
432  print(e.getMessage(), 0xff0000);
433  }
434  commandBuffer.clear();
435  prompt = PROMPT_NEW;
436  } else {
437  prompt = PROMPT_CONT;
438  }
439  putPrompt();
440 }
441 
442 ConsoleLine CommandConsole::highLight(string_ref line)
443 {
444  assert(line.starts_with(prompt));
445  string_ref command = line.substr(prompt.size());
446  ConsoleLine result;
447  result.addChunk(prompt, 0xffffff);
448 
449  TclParser parser = commandController.getInterpreter().parse(command);
450  string colors = parser.getColors();
451  assert(colors.size() == command.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(command.substr(pos2, pos - pos2), rgb);
472  }
473  return result;
474 }
475 
476 void CommandConsole::putPrompt()
477 {
478  commandScrollBack = unsigned(history.size());
479  currentLine = prompt;
480  lines[0] = highLight(currentLine);
481  cursorPosition = unsigned(prompt.size());
482 }
483 
484 void CommandConsole::tabCompletion()
485 {
486  resetScrollBack();
487  unsigned pl = unsigned(prompt.size());
488  string_ref front = utf8::unchecked::substr(lines[0].str(), pl, cursorPosition - pl);
489  string_ref back = utf8::unchecked::substr(lines[0].str(), cursorPosition);
490  string newFront = commandController.tabCompletion(front);
491  cursorPosition = pl + unsigned(utf8::unchecked::size(newFront));
492  currentLine = prompt + 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 = 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 = lines[0].numChars();
541 }
542 
543 void CommandConsole::clearCommand()
544 {
545  resetScrollBack();
546  commandBuffer.clear();
547  prompt = PROMPT_NEW;
548  currentLine = prompt;
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  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  lines[0] = highLight(currentLine);
579  }
580 }
581 
582 void CommandConsole::normalKey(uint16_t chr)
583 {
584  assert(chr);
585  resetScrollBack();
586  currentLine = lines[0].str();
587  auto pos = begin(currentLine);
588  utf8::unchecked::advance(pos, cursorPosition);
589  utf8::unchecked::append(uint32_t(chr), inserter(currentLine, pos));
590  lines[0] = highLight(currentLine);
591  ++cursorPosition;
592 }
593 
594 void CommandConsole::resetScrollBack()
595 {
596  consoleScrollBack = 0;
597 }
598 
599 } // namespace openmsx
ConsoleLine()
Construct empty line.
string_ref::const_iterator end(const string_ref &x)
Definition: string_ref.hh:150
octet_iterator append(uint32_t cp, octet_iterator result)
Represents the output window/screen of openMSX.
Definition: Display.hh:31
void registerEventListener(EventType type, EventListener &listener, Priority priority=OTHER)
Registers a given object to receive certain events.
string_ref getString() const
Definition: TclObject.cc:139
vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:257
void getCursorPosition(unsigned &xPosition, unsigned &yPosition) const
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.
unsigned getRows() const
void printWarning(string_ref message)
Definition: CliComm.cc:28
uint32_t next(octet_iterator &it)
bool starts_with(string_ref x) const
Definition: string_ref.cc:138
STL namespace.
size_type find(string_ref s) const
Definition: string_ref.cc:60
This class implements a subset of the proposal for std::string_ref (proposed for the next c++ standar...
Definition: string_ref.hh:18
vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:266
size_type size() const
Definition: string_ref.hh:55
This class represents a single text line in the console.
ConsoleLine substr(unsigned pos, unsigned len) const
Get a part of total line.
string_ref substr(string_ref utf8, string_ref::size_type first=0, string_ref::size_type len=string_ref::npos)
const char * data() const
Definition: string_ref.hh:68
bool startsWith(string_ref total, string_ref part)
Definition: StringOp.cc:242
CommandConsole(GlobalCommandController &commandController, EventDistributor &eventDistributor, Display &display)
bool isComplete(const std::string &command)
Returns true iff the command is complete (all braces, quotes etc.
unsigned numChars() const
Get the number of UTF8 characters in this line.
static void setOutput(InterpreterOutput *output_)
Definition: Completer.hh:46
void repaintDelayed(uint64_t delta)
Definition: Display.cc:357
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
static const size_type npos
Definition: string_ref.hh:26
std::string tabCompletion(string_ref command)
Complete the given command.
FileContext userFileContext(string_ref savePath)
Definition: FileContext.cc:161
void addChunk(string_ref text, unsigned rgb)
Append a chunk with a (different) color.
unsigned getColumns() const
void advance(octet_iterator &it, distance_type n)
static std::string full()
Definition: Version.cc:7
void setOutput(InterpreterOutput *output_)
Definition: Interpreter.hh:25
string_ref chunkText(unsigned i) const
Get the text for the i-th chunk.
string_ref substr(size_type pos, size_type n=npos) const
Definition: string_ref.cc:54
ConsoleLine getLine(unsigned line) const
size_t size(string_ref utf8)
string_ref::const_iterator begin(const string_ref &x)
Definition: string_ref.hh:149
unsigned chunkColor(unsigned i) const
Get the color for the i-th chunk.
TclParser parse(string_ref command)
Definition: Interpreter.cc:425
bool empty() const
Definition: string_ref.hh:56
TclObject executeCommand(const std::string &command, CliConnection *connection=nullptr) override
Execute the given command.
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
XRange< T > xrange(T e)
Definition: xrange.hh:98
Assign new value to some variable and restore the original value when this object goes out of scope...
Definition: ScopedAssign.hh:7