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