openMSX
GlobalCommandController.cc
Go to the documentation of this file.
2 #include "Reactor.hh"
3 #include "Setting.hh"
4 #include "ProxyCommand.hh"
5 #include "ProxySetting.hh"
6 #include "LocalFileReference.hh"
7 #include "GlobalCliComm.hh"
8 #include "CliConnection.hh"
9 #include "CommandException.hh"
10 #include "SettingsManager.hh"
11 #include "TclObject.hh"
12 #include "Version.hh"
13 #include "ScopedAssign.hh"
14 #include "join.hh"
15 #include "outer.hh"
16 #include "ranges.hh"
17 #include "stl.hh"
18 #include "view.hh"
19 #include "xrange.hh"
20 #include <cassert>
21 #include <memory>
22 
23 using std::string;
24 using std::vector;
25 
26 namespace openmsx {
27 
29  EventDistributor& eventDistributor,
30  GlobalCliComm& cliComm_, Reactor& reactor_)
31  : cliComm(cliComm_)
32  , connection(nullptr)
33  , reactor(reactor_)
34  , openMSXInfoCommand(*this, "openmsx_info")
35  , hotKey(reactor.getRTScheduler(), *this, eventDistributor)
36  , settingsConfig(*this, hotKey)
37  , helpCmd(*this)
38  , tabCompletionCmd(*this)
39  , updateCmd(*this)
40  , platformInfo(getOpenMSXInfoCommand())
41  , versionInfo (getOpenMSXInfoCommand())
42  , romInfoTopic(getOpenMSXInfoCommand())
43 {
44 }
45 
47 
49 {
50  // GlobalCommandController destructor must have run before
51  // we can check this.
52  assert(commands.empty());
53  assert(commandCompleters.empty());
54 }
55 
57 {
58  auto it = proxyCommandMap.find(name);
59  if (it == end(proxyCommandMap)) {
60  it = proxyCommandMap.emplace_noDuplicateCheck(
61  0, std::make_unique<ProxyCmd>(reactor, name));
62  }
63  ++it->first;
64 }
65 
67 {
68  auto it = proxyCommandMap.find(name);
69  assert(it != end(proxyCommandMap));
70  assert(it->first > 0);
71  --it->first;
72  if (it->first == 0) {
73  proxyCommandMap.erase(it);
74  }
75 }
76 
77 GlobalCommandController::ProxySettings::iterator
78 GlobalCommandController::findProxySetting(string_view name)
79 {
80  return ranges::find_if(proxySettings, [&](auto& v) {
81  return v.first->getFullName() == name;
82  });
83 }
84 
86 {
87  const auto& name = setting.getBaseNameObj();
88  auto it = findProxySetting(name.getString());
89  if (it == end(proxySettings)) {
90  // first occurrence
91  auto proxy = std::make_unique<ProxySetting>(reactor, name);
94  proxySettings.emplace_back(std::move(proxy), 1);
95  } else {
96  // was already registered
97  ++(it->second);
98  }
99 }
100 
102 {
103  auto it = findProxySetting(setting.getBaseName());
104  assert(it != end(proxySettings));
105  assert(it->second);
106  --(it->second);
107  if (it->second == 0) {
108  auto& proxy = *it->first;
111  move_pop_back(proxySettings, it);
112  }
113 }
114 
116 {
117  return cliComm;
118 }
119 
121 {
122  return interpreter;
123 }
124 
126  Command& command, const string& str)
127 {
128  assert(!commands.contains(str));
129  commands.emplace_noDuplicateCheck(str, &command);
130  interpreter.registerCommand(str, command);
131 }
132 
134  Command& command, string_view str)
135 {
136  assert(commands.contains(str));
137  assert(commands[str.str()] == &command);
138  commands.erase(str);
139  interpreter.unregisterCommand(command);
140 }
141 
143  CommandCompleter& completer, string_view str)
144 {
145  if (str.starts_with("::")) str.remove_prefix(2); // drop leading ::
146  assert(!commandCompleters.contains(str));
147  commandCompleters.emplace_noDuplicateCheck(str.str(), &completer);
148 }
149 
151  CommandCompleter& completer, string_view str)
152 {
153  if (str.starts_with("::")) str.remove_prefix(2); // drop leading ::
154  assert(commandCompleters.contains(str));
155  assert(commandCompleters[str.str()] == &completer); (void)completer;
156  commandCompleters.erase(str);
157 }
158 
160 {
162  interpreter.registerSetting(setting);
163 }
164 
166 {
167  interpreter.unregisterSetting(setting);
169 }
170 
172 {
173  return commands.contains(command);
174 }
175 
176 void GlobalCommandController::split(string_view str, vector<string>& tokens,
177  const char delimiter)
178 {
179  enum ParseState {Alpha, BackSlash, Quote};
180  ParseState state = Alpha;
181 
182  for (auto chr : str) {
183  switch (state) {
184  case Alpha:
185  if (tokens.empty()) {
186  tokens.emplace_back();
187  }
188  if (chr == delimiter) {
189  // token done, start new token
190  tokens.emplace_back();
191  } else {
192  tokens.back() += chr;
193  if (chr == '\\') {
194  state = BackSlash;
195  } else if (chr == '"') {
196  state = Quote;
197  }
198  }
199  break;
200  case Quote:
201  tokens.back() += chr;
202  if (chr == '"') {
203  state = Alpha;
204  }
205  break;
206  case BackSlash:
207  tokens.back() += chr;
208  state = Alpha;
209  break;
210  }
211  }
212 }
213 
214 string GlobalCommandController::removeEscaping(const string& str)
215 {
216  enum ParseState {Alpha, BackSlash, Quote};
217  ParseState state = Alpha;
218 
219  string result;
220  for (auto chr : str) {
221  switch (state) {
222  case Alpha:
223  if (chr == '\\') {
224  state = BackSlash;
225  } else if (chr == '"') {
226  state = Quote;
227  } else {
228  result += chr;
229  }
230  break;
231  case Quote:
232  if (chr == '"') {
233  state = Alpha;
234  } else {
235  result += chr;
236  }
237  break;
238  case BackSlash:
239  result += chr;
240  state = Alpha;
241  break;
242  }
243  }
244  return result;
245 }
246 
247 vector<string> GlobalCommandController::removeEscaping(
248  const vector<string>& input, bool keepLastIfEmpty)
249 {
250  vector<string> result;
251  for (auto& s : input) {
252  if (!s.empty()) {
253  result.push_back(removeEscaping(s));
254  }
255  }
256  if (keepLastIfEmpty && (input.empty() || input.back().empty())) {
257  result.emplace_back();
258  }
259  return result;
260 }
261 
262 static string escapeChars(const string& str, const string& chars)
263 {
264  string result;
265  for (auto chr : str) {
266  if (chars.find(chr) != string::npos) {
267  result += '\\';
268  }
269  result += chr;
270 
271  }
272  return result;
273 }
274 
275 string GlobalCommandController::addEscaping(const string& str, bool quote,
276  bool finished)
277 {
278  if (str.empty() && finished) {
279  quote = true;
280  }
281  string result = escapeChars(str, "$[]");
282  if (quote) {
283  result.insert(result.begin(), '"');
284  if (finished) {
285  result += '"';
286  }
287  } else {
288  result = escapeChars(result, " ");
289  }
290  return result;
291 }
292 
293 bool GlobalCommandController::isComplete(const string& command)
294 {
295  return interpreter.isComplete(command);
296 }
297 
299  const string& command, CliConnection* connection_)
300 {
301  ScopedAssign<CliConnection*> sa(connection, connection_);
302  return interpreter.execute(command);
303 }
304 
305 void GlobalCommandController::source(const string& script)
306 {
307  try {
308  LocalFileReference file(script);
309  interpreter.executeFile(file.getFilename());
310  } catch (CommandException& e) {
312  "While executing ", script, ": ", e.getMessage());
313  }
314 }
315 
317 {
318  // split on 'active' command (the command that should actually be
319  // completed). Some examples:
320  // if {[debug rea<tab> <-- should complete the 'debug' command
321  // instead of the 'if' command
322  // bind F6 { cycl<tab> <-- should complete 'cycle' instead of 'bind'
323  TclParser parser = interpreter.parse(command);
324  int last = parser.getLast();
325  string_view pre = command.substr(0, last);
326  string_view post = command.substr(last);
327 
328  // split command string in tokens
329  vector<string> originalTokens;
330  split(post, originalTokens, ' ');
331  if (originalTokens.empty()) {
332  originalTokens.emplace_back();
333  }
334 
335  // complete last token
336  auto tokens = removeEscaping(originalTokens, true);
337  auto oldNum = tokens.size();
338  tabCompletion(tokens);
339  auto newNum = tokens.size();
340  bool tokenFinished = oldNum != newNum;
341 
342  // replace last token
343  string& original = originalTokens.back();
344  string& completed = tokens[oldNum - 1];
345  if (!completed.empty()) {
346  bool quote = !original.empty() && (original[0] == '"');
347  original = addEscaping(completed, quote, tokenFinished);
348  }
349  if (tokenFinished) {
350  assert(newNum == (oldNum + 1));
351  assert(tokens.back().empty());
352  originalTokens.emplace_back();
353  }
354 
355  // rebuild command string
356  return strCat(pre, join(originalTokens, ' '));
357 }
358 
359 void GlobalCommandController::tabCompletion(vector<string>& tokens)
360 {
361  if (tokens.empty()) {
362  // nothing typed yet
363  return;
364  }
365  if (tokens.size() == 1) {
366  string_view cmd = tokens[0];
367  string_view leadingNs;
368  // remove leading ::
369  if (cmd.starts_with("::")) {
370  cmd.remove_prefix(2);
371  leadingNs = "::";
372  }
373  // get current (typed) namespace
374  auto p1 = cmd.rfind("::");
375  string_view ns = (p1 == string_view::npos) ? cmd : cmd.substr(0, p1 + 2);
376 
377  // build a list of all command strings
378  TclObject names = interpreter.getCommandNames();
379  vector<string> names2;
380  names2.reserve(names.size());
381  for (string_view n1 : names) {
382  // remove leading ::
383  if (n1.starts_with("::")) n1.remove_prefix(2);
384  // initial namespace part must match
385  if (!n1.starts_with(ns)) continue;
386  // the part following the initial namespace
387  string_view n2 = n1.substr(ns.size());
388  // only keep upto the next namespace portion,
389  auto p2 = n2.find("::");
390  auto n3 = (p2 == string_view::npos) ? n1 : n1.substr(0, ns.size() + p2 + 2);
391  // don't care about adding the same string multiple times
392  names2.push_back(strCat(leadingNs, n3));
393  }
394  Completer::completeString(tokens, names2);
395  } else {
396  string_view cmd = tokens.front();
397  if (cmd.starts_with("::")) cmd.remove_prefix(2); // drop leading ::
398 
399  if (auto v = lookup(commandCompleters, cmd)) {
400  (*v)->tabCompletion(tokens);
401  } else {
402  TclObject command = makeTclList("openmsx::tabcompletion");
403  command.addListElements(tokens);
404  try {
405  TclObject list = command.executeCommand(interpreter);
406  bool sensitive = true;
407  auto begin = list.begin();
408  auto end = list.end();
409  if (begin != end) {
410  auto it2 = end; --it2;
411  auto back = *it2;
412  if (back == "false") {
413  end = it2;
414  sensitive = false;
415  } else if (back == "true") {
416  end = it2;
417  sensitive = true;
418  }
419  }
420  Completer::completeString(tokens, begin, end, sensitive);
421  } catch (CommandException& e) {
422  cliComm.printWarning(
423  "Error while executing tab-completion "
424  "proc: ", e.getMessage());
425  }
426  }
427  }
428 }
429 
430 
431 // Help Command
432 
433 GlobalCommandController::HelpCmd::HelpCmd(GlobalCommandController& controller_)
434  : Command(controller_, "help")
435 {
436 }
437 
438 void GlobalCommandController::HelpCmd::execute(
439  span<const TclObject> tokens, TclObject& result)
440 {
441  auto& controller = OUTER(GlobalCommandController, helpCmd);
442  switch (tokens.size()) {
443  case 1: {
444  string text =
445  "Use 'help [command]' to get help for a specific command\n"
446  "The following commands exist:\n";
447  auto cmds = to_vector<string_view>(
448  view::keys(controller.commandCompleters));
449  ranges::sort(cmds);
450  for (auto& line : formatListInColumns(cmds)) {
451  strAppend(text, line, '\n');
452  }
453  result = text;
454  break;
455  }
456  default: {
457  if (auto v = lookup(controller.commandCompleters, tokens[1].getString())) {
458  auto tokens2 = to_vector(view::transform(
459  view::drop(tokens, 1),
460  [](auto& t) { return t.getString().str(); }));
461  result = (*v)->help(tokens2);
462  } else {
463  TclObject command = makeTclList("openmsx::help");
464  command.addListElements(view::drop(tokens, 1));
465  result = command.executeCommand(getInterpreter());
466  }
467  break;
468  }
469  }
470 }
471 
472 string GlobalCommandController::HelpCmd::help(const vector<string>& /*tokens*/) const
473 {
474  return "prints help information for commands\n";
475 }
476 
477 void GlobalCommandController::HelpCmd::tabCompletion(vector<string>& tokens) const
478 {
479  string front = std::move(tokens.front());
480  tokens.erase(begin(tokens));
481  auto& controller = OUTER(GlobalCommandController, helpCmd);
482  controller.tabCompletion(tokens);
483  tokens.insert(begin(tokens), std::move(front));
484 }
485 
486 
487 // TabCompletionCmd Command
488 
489 GlobalCommandController::TabCompletionCmd::TabCompletionCmd(
490  GlobalCommandController& controller_)
491  : Command(controller_, "tabcompletion")
492 {
493 }
494 
495 void GlobalCommandController::TabCompletionCmd::execute(
496  span<const TclObject> tokens, TclObject& result)
497 {
498  checkNumArgs(tokens, 2, "commandstring");
499  // TODO this prints list of possible completions in the console
500  auto& controller = OUTER(GlobalCommandController, tabCompletionCmd);
501  result = controller.tabCompletion(tokens[1].getString());
502 }
503 
504 string GlobalCommandController::TabCompletionCmd::help(const vector<string>& /*tokens*/) const
505 {
506  return "!!! This command will change in the future !!!\n"
507  "Tries to completes the given argument as if it were typed in "
508  "the console. This command is only useful to provide "
509  "tabcompletion to external console interfaces.";
510 }
511 
512 
513 // class UpdateCmd
514 
515 GlobalCommandController::UpdateCmd::UpdateCmd(CommandController& commandController_)
516  : Command(commandController_, "openmsx_update")
517 {
518 }
519 
520 static GlobalCliComm::UpdateType getType(const TclObject& name)
521 {
522  auto updateStr = CliComm::getUpdateStrings();
523  for (auto i : xrange(updateStr.size())) {
524  if (updateStr[i] == name) {
525  return static_cast<CliComm::UpdateType>(i);
526  }
527  }
528  throw CommandException("No such update type: ", name.getString());
529 }
530 
531 CliConnection& GlobalCommandController::UpdateCmd::getConnection()
532 {
533  auto& controller = OUTER(GlobalCommandController, updateCmd);
534  if (auto* c = controller.getConnection()) {
535  return *c;
536  }
537  throw CommandException("This command only makes sense when "
538  "it's used from an external application.");
539 }
540 
541 void GlobalCommandController::UpdateCmd::execute(
542  span<const TclObject> tokens, TclObject& /*result*/)
543 {
544  checkNumArgs(tokens, 3, "enable|disable type");
545  if (tokens[1] == "enable") {
546  getConnection().setUpdateEnable(getType(tokens[2]), true);
547  } else if (tokens[1] == "disable") {
548  getConnection().setUpdateEnable(getType(tokens[2]), false);
549  } else {
550  throw SyntaxError();
551  }
552 }
553 
554 string GlobalCommandController::UpdateCmd::help(const vector<string>& /*tokens*/) const
555 {
556  static const string helpText = "Enable or disable update events for external applications. See doc/openmsx-control-xml.txt.";
557  return helpText;
558 }
559 
560 void GlobalCommandController::UpdateCmd::tabCompletion(vector<string>& tokens) const
561 {
562  switch (tokens.size()) {
563  case 2: {
564  static const char* const ops[] = { "enable", "disable" };
565  completeString(tokens, ops);
566  break;
567  }
568  case 3:
569  completeString(tokens, CliComm::getUpdateStrings());
570  break;
571  }
572 }
573 
574 
575 // Platform info
576 
577 GlobalCommandController::PlatformInfo::PlatformInfo(InfoCommand& openMSXInfoCommand_)
578  : InfoTopic(openMSXInfoCommand_, "platform")
579 {
580 }
581 
582 void GlobalCommandController::PlatformInfo::execute(
583  span<const TclObject> /*tokens*/, TclObject& result) const
584 {
585  result = TARGET_PLATFORM;
586 }
587 
588 string GlobalCommandController::PlatformInfo::help(const vector<string>& /*tokens*/) const
589 {
590  return "Prints openMSX platform.";
591 }
592 
593 // Version info
594 
595 GlobalCommandController::VersionInfo::VersionInfo(InfoCommand& openMSXInfoCommand_)
596  : InfoTopic(openMSXInfoCommand_, "version")
597 {
598 }
599 
600 void GlobalCommandController::VersionInfo::execute(
601  span<const TclObject> /*tokens*/, TclObject& result) const
602 {
603  result = Version::full();
604 }
605 
606 string GlobalCommandController::VersionInfo::help(const vector<string>& /*tokens*/) const
607 {
608  return "Prints openMSX version.";
609 }
610 
611 } // namespace openmsx
auto transform(Range &&range, UnaryOp op)
Definition: view.hh:312
Contains the main loop of openMSX.
Definition: Reactor.hh:64
hash_map< std::string, Command *, XXHasher > commands
bool starts_with(string_view x) const
Definition: string_view.cc:116
const std::string & getMessage() const &
Definition: MSXException.hh:23
void registerCommand(const std::string &name, Command &command)
Definition: Interpreter.cc:145
auto xrange(T e)
Definition: xrange.hh:170
GlobalCommandController(const GlobalCommandController &)=delete
TclObject executeFile(const std::string &filename)
Definition: Interpreter.cc:215
void registerProxyCommand(const std::string &name)
const std::string & getFilename() const
Returns path to a local uncompressed version of this file.
auto begin() const
Definition: TclObject.hh:160
void unregisterSetting(BaseSetting &setting)
Definition: span.hh:34
const Value * lookup(const hash_map< Key, Value, Hasher, Equal > &map, const Key2 &key)
Definition: hash_map.hh:91
mat3 n3(vec3(1, 0, 3), vec3(4, 5, 6), vec3(7, 8, 9))
string_view getString() const
Definition: TclObject.cc:102
void unregisterCommand(Command &command, string_view str) override
void registerSetting(Setting &setting) override
TODO.
void unregisterCompleter(CommandCompleter &completer, string_view str) override
const TclObject & getBaseNameObj() const
Definition: Setting.hh:33
void move_pop_back(VECTOR &v, typename VECTOR::iterator it)
Erase the pointed to element from the given vector.
Definition: stl.hh:191
int getLast() const
Get Start of the last subcommand.
Definition: TclParser.hh:31
void strAppend(std::string &result, Ts &&...ts)
Definition: strCat.hh:648
TclParser parse(string_view command)
Definition: Interpreter.cc:447
auto find_if(InputRange &&range, UnaryPredicate pred)
Definition: ranges.hh:113
static const size_type npos
Definition: string_view.hh:24
auto begin(const string_view &x)
Definition: string_view.hh:151
bool hasCommand(string_view command) const override
Does a command with this name already exist?
void unregisterProxyCommand(string_view name)
unsigned size() const
Definition: TclObject.hh:158
void registerSetting(BaseSetting &setting)
void remove_prefix(size_type n)
Definition: string_view.hh:65
bool isComplete(const std::string &command)
Returns true iff the command is complete (all braces, quotes etc.
auto keys(Map &&map)
Definition: view.hh:317
size_type find(string_view s) const
Definition: string_view.cc:38
void registerCompleter(CommandCompleter &completer, string_view str) override
(Un)register a command completer, used to complete build-in Tcl cmds
void sort(RandomAccessRange &&range)
Definition: ranges.hh:35
static span< const char *const > getUpdateStrings()
Definition: CliComm.hh:81
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
void unregisterSetting(Setting &setting) override
size_type rfind(string_view s) const
Definition: string_view.cc:65
static void completeString(std::vector< std::string > &tokens, ITER begin, ITER end, bool caseSensitive=true)
Definition: Completer.hh:124
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.
std::string str() const
Definition: string_view.cc:12
void unregisterCommand(Command &command)
Definition: Interpreter.cc:153
void registerCommand(Command &command, const std::string &str) override
(Un)register a command
TclObject getCommandNames()
Definition: Interpreter.cc:196
string_view substr(size_type pos, size_type n=npos) const
Definition: string_view.cc:32
TclObject execute(const std::string &command)
Definition: Interpreter.cc:206
hash_map< std::string, CommandCompleter *, XXHasher > commandCompleters
static std::string full()
Definition: Version.cc:8
string_view getBaseName() const
Definition: Setting.hh:35
auto drop(Range &&range, size_t n)
Definition: view.hh:294
std::string strCat(Ts &&...ts)
Definition: strCat.hh:577
size_type size() const
Definition: string_view.hh:44
auto end() const
Definition: TclObject.hh:161
#define OUTER(type, member)
Definition: outer.hh:38
void source(const std::string &script)
Executes all defined auto commands.
void registerSetting(BaseSetting &variable)
Definition: Interpreter.cc:261
Helper class to use files in APIs other than openmsx::File.
void addListElements(ITER first, ITER last)
Definition: TclObject.hh:122
TclObject makeTclList(Args &&... args)
Definition: TclObject.hh:280
TclObject executeCommand(Interpreter &interp, bool compile=false)
Interpret this TclObject as a command and execute it.
Definition: TclObject.cc:172
detail::Joiner< Collection, Separator > join(Collection &&col, Separator &&sep)
Definition: join.hh:60
TclObject t
void printWarning(string_view message)
Definition: CliComm.cc:20
auto to_vector(Range &&range) -> std::vector< detail::ToVectorType< T, decltype(std::begin(range))>>
Definition: stl.hh:325
TclObject executeCommand(const std::string &command, CliConnection *connection=nullptr) override
Execute the given command.
void unregisterSetting(BaseSetting &variable)
Definition: Interpreter.cc:313
auto end(const string_view &x)
Definition: string_view.hh:152
void setUpdateEnable(CliComm::UpdateType type, bool value)
Assign new value to some variable and restore the original value when this object goes out of scope...
Definition: ScopedAssign.hh:7
bool isComplete(const std::string &command) const
Definition: Interpreter.cc:201