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