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