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"
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 "build-info.hh"
21#include <cassert>
22#include <memory>
23
24using std::string;
25using std::string_view;
26using std::vector;
27
28namespace openmsx {
29
31 EventDistributor& eventDistributor,
32 GlobalCliComm& cliComm_, Reactor& reactor_)
33 : cliComm(cliComm_)
34 , reactor(reactor_)
35 , openMSXInfoCommand(*this, "openmsx_info")
36 , hotKey(reactor.getRTScheduler(), *this, eventDistributor)
37 , settingsConfig(*this, hotKey)
38 , helpCmd(*this)
39 , tabCompletionCmd(*this)
40 , updateCmd(*this)
41 , platformInfo(getOpenMSXInfoCommand())
42 , versionInfo (getOpenMSXInfoCommand())
43 , romInfoTopic(getOpenMSXInfoCommand())
44{
45}
46
48
50{
51 // GlobalCommandController destructor must have run before
52 // we can check this.
53#ifdef DEBUG
54 assert(commands.empty());
55#endif
56 assert(commandCompleters.empty());
57}
58
60{
61 auto it = proxyCommandMap.find(name);
62 if (it == end(proxyCommandMap)) {
63 it = proxyCommandMap.emplace_noDuplicateCheck(
64 0, std::make_unique<ProxyCmd>(reactor, name));
65 }
66 ++it->first;
67}
68
70{
71 auto it = proxyCommandMap.find(name);
72 assert(it != end(proxyCommandMap));
73 assert(it->first > 0);
74 --it->first;
75 if (it->first == 0) {
76 proxyCommandMap.erase(it);
77 }
78}
79
80GlobalCommandController::ProxySettings::iterator
81GlobalCommandController::findProxySetting(string_view name)
82{
83 return ranges::find(proxySettings, name,
84 [](auto& v) { return v.first->getFullName(); });
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, zstring_view str)
129{
130#ifdef DEBUG
131 assert(!commands.contains(str));
132 commands.emplace_noDuplicateCheck(str, &command);
133#endif
134 interpreter.registerCommand(str, command);
135}
136
138 Command& command, string_view str)
139{
140 (void)str;
141#ifdef DEBUG
142 assert(commands.contains(str));
143 assert(commands[str] == &command);
144 commands.erase(str);
145#endif
146 interpreter.unregisterCommand(command);
147}
148
150 CommandCompleter& completer, string_view str)
151{
152 if (str.starts_with("::")) str.remove_prefix(2); // drop leading ::
153 assert(!commandCompleters.contains(str));
154 commandCompleters.emplace_noDuplicateCheck(str, &completer);
155}
156
158 CommandCompleter& completer, string_view str)
159{
160 if (str.starts_with("::")) str.remove_prefix(2); // drop leading ::
161 assert(commandCompleters.contains(str));
162 assert(commandCompleters[str] == &completer); (void)completer;
163 commandCompleters.erase(str);
164}
165
171
177
178static vector<string> split(string_view str, const char delimiter)
179{
180 vector<string> tokens;
181
182 enum ParseState {Alpha, BackSlash, Quote};
183 ParseState state = Alpha;
184
185 for (auto chr : str) {
186 switch (state) {
187 case Alpha:
188 if (tokens.empty()) {
189 tokens.emplace_back();
190 }
191 if (chr == delimiter) {
192 // token done, start new token
193 tokens.emplace_back();
194 } else {
195 tokens.back() += chr;
196 if (chr == '\\') {
197 state = BackSlash;
198 } else if (chr == '"') {
199 state = Quote;
200 }
201 }
202 break;
203 case Quote:
204 tokens.back() += chr;
205 if (chr == '"') {
206 state = Alpha;
207 }
208 break;
209 case BackSlash:
210 tokens.back() += chr;
211 state = Alpha;
212 break;
213 }
214 }
215 return tokens;
216}
217
218static string removeEscaping(const string& str)
219{
220 enum ParseState {Alpha, BackSlash, Quote};
221 ParseState state = Alpha;
222
223 string result;
224 for (auto chr : str) {
225 switch (state) {
226 case Alpha:
227 if (chr == '\\') {
228 state = BackSlash;
229 } else if (chr == '"') {
230 state = Quote;
231 } else {
232 result += chr;
233 }
234 break;
235 case Quote:
236 if (chr == '"') {
237 state = Alpha;
238 } else {
239 result += chr;
240 }
241 break;
242 case BackSlash:
243 result += chr;
244 state = Alpha;
245 break;
246 }
247 }
248 return result;
249}
250
251static vector<string> removeEscaping(std::span<const string> input, bool keepLastIfEmpty)
252{
253 vector<string> result;
254 for (const auto& s : input) {
255 if (!s.empty()) {
256 result.push_back(removeEscaping(s));
257 }
258 }
259 if (keepLastIfEmpty && (input.empty() || input.back().empty())) {
260 result.emplace_back();
261 }
262 return result;
263}
264
265static string escapeChars(const string& str, string_view chars)
266{
267 string result;
268 for (auto chr : str) {
269 if (chars.find(chr) != string::npos) {
270 result += '\\';
271 }
272 result += chr;
273
274 }
275 return result;
276}
277
278static string addEscaping(const string& str, bool quote, 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
296{
297 return interpreter.isComplete(command);
298}
299
301 zstring_view command, CliConnection* connection_)
302{
303 ScopedAssign sa(connection, connection_);
304 return interpreter.execute(command);
305}
306
307void 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
318string 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 = split(post, ' ');
332 if (originalTokens.empty()) {
333 originalTokens.emplace_back();
334 }
335
336 // complete last token
337 auto tokens = removeEscaping(originalTokens, true);
338 auto oldNum = tokens.size();
339 tabCompletion(tokens);
340 auto newNum = tokens.size();
341 bool tokenFinished = oldNum != newNum;
342
343 // replace last token
344 string& original = originalTokens.back();
345 if (const string& completed = tokens[oldNum - 1]; !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
359void 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
433GlobalCommandController::HelpCmd::HelpCmd(GlobalCommandController& controller_)
434 : Command(controller_, "help")
435{
436}
437
438void GlobalCommandController::HelpCmd::execute(
439 std::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 = concat<string_view>(
448 view::keys(controller.commandCompleters),
449 getInterpreter().execute("openmsx::all_command_names_with_help"));
450 cmds.erase(ranges::remove_if(cmds, [](const auto& c) {
451 return c.find("::") != std::string_view::npos; }),
452 cmds.end());
453 ranges::sort(cmds);
454 for (auto& line : formatListInColumns(cmds)) {
455 strAppend(text, line, '\n');
456 }
457 result = text;
458 break;
459 }
460 default: {
461 if (const auto* v = lookup(controller.commandCompleters, tokens[1].getString())) {
462 result = (*v)->help(tokens.subspan(1));
463 } else {
464 TclObject command = makeTclList("openmsx::help");
465 command.addListElements(view::drop(tokens, 1));
466 result = command.executeCommand(getInterpreter());
467 }
468 break;
469 }
470 }
471}
472
473string GlobalCommandController::HelpCmd::help(std::span<const TclObject> /*tokens*/) const
474{
475 return "prints help information for commands\n";
476}
477
478void GlobalCommandController::HelpCmd::tabCompletion(vector<string>& tokens) const
479{
480 string front = std::move(tokens.front());
481 tokens.erase(begin(tokens));
482 auto& controller = OUTER(GlobalCommandController, helpCmd);
483 controller.tabCompletion(tokens);
484 tokens.insert(begin(tokens), std::move(front));
485}
486
487
488// TabCompletionCmd Command
489
490GlobalCommandController::TabCompletionCmd::TabCompletionCmd(
491 GlobalCommandController& controller_)
492 : Command(controller_, "tabcompletion")
493{
494}
495
496void GlobalCommandController::TabCompletionCmd::execute(
497 std::span<const TclObject> tokens, TclObject& result)
498{
499 checkNumArgs(tokens, 2, "commandstring");
500 // TODO this prints list of possible completions in the console
501 auto& controller = OUTER(GlobalCommandController, tabCompletionCmd);
502 result = controller.tabCompletion(tokens[1].getString());
503}
504
505string GlobalCommandController::TabCompletionCmd::help(std::span<const TclObject> /*tokens*/) const
506{
507 return "!!! This command will change in the future !!!\n"
508 "Tries to completes the given argument as if it were typed in "
509 "the console. This command is only useful to provide "
510 "tabcompletion to external console interfaces.";
511}
512
513
514// class UpdateCmd
515
516GlobalCommandController::UpdateCmd::UpdateCmd(CommandController& commandController_)
517 : Command(commandController_, "openmsx_update")
518{
519}
520
521static GlobalCliComm::UpdateType getType(const TclObject& name)
522{
523 for (auto updateStr = CliComm::getUpdateStrings();
524 auto i : xrange(updateStr.size())) {
525 if (updateStr[i] == name) {
526 return static_cast<CliComm::UpdateType>(i);
527 }
528 }
529 throw CommandException("No such update type: ", name.getString());
530}
531
532CliConnection& GlobalCommandController::UpdateCmd::getConnection()
533{
534 auto& controller = OUTER(GlobalCommandController, updateCmd);
535 if (auto* c = controller.getConnection()) {
536 return *c;
537 }
538 throw CommandException("This command only makes sense when "
539 "it's used from an external application.");
540}
541
542void GlobalCommandController::UpdateCmd::execute(
543 std::span<const TclObject> tokens, TclObject& /*result*/)
544{
545 checkNumArgs(tokens, 3, Prefix{1}, "enable|disable type");
546 if (tokens[1] == "enable") {
547 getConnection().setUpdateEnable(getType(tokens[2]), true);
548 } else if (tokens[1] == "disable") {
549 getConnection().setUpdateEnable(getType(tokens[2]), false);
550 } else {
551 throw SyntaxError();
552 }
553}
554
555string GlobalCommandController::UpdateCmd::help(std::span<const TclObject> /*tokens*/) const
556{
557 return "Enable or disable update events for external applications. See doc/openmsx-control-xml.txt.";
558}
559
560void GlobalCommandController::UpdateCmd::tabCompletion(vector<string>& tokens) const
561{
562 switch (tokens.size()) {
563 case 2: {
564 using namespace std::literals;
565 static constexpr std::array ops = {"enable"sv, "disable"sv};
566 completeString(tokens, ops);
567 break;
568 }
569 case 3:
570 completeString(tokens, CliComm::getUpdateStrings());
571 break;
572 }
573}
574
575
576// Platform info
577
578GlobalCommandController::PlatformInfo::PlatformInfo(InfoCommand& openMSXInfoCommand_)
579 : InfoTopic(openMSXInfoCommand_, "platform")
580{
581}
582
583void GlobalCommandController::PlatformInfo::execute(
584 std::span<const TclObject> /*tokens*/, TclObject& result) const
585{
586 result = TARGET_PLATFORM;
587}
588
589string GlobalCommandController::PlatformInfo::help(std::span<const TclObject> /*tokens*/) const
590{
591 return "Prints openMSX platform.";
592}
593
594// Version info
595
596GlobalCommandController::VersionInfo::VersionInfo(InfoCommand& openMSXInfoCommand_)
597 : InfoTopic(openMSXInfoCommand_, "version")
598{
599}
600
601void GlobalCommandController::VersionInfo::execute(
602 std::span<const TclObject> /*tokens*/, TclObject& result) const
603{
604 result = Version::full();
605}
606
607string GlobalCommandController::VersionInfo::help(std::span<const TclObject> /*tokens*/) const
608{
609 return "Prints openMSX version.";
610}
611
612} // namespace openmsx
BaseSetting * setting
Assign new value to some variable and restore the original value when this object goes out of scope.
int getLast() const
Get Start of the last subcommand.
Definition TclParser.hh:33
void printWarning(std::string_view message)
Definition CliComm.cc:10
static std::span< const std::string_view, NUM_UPDATES > getUpdateStrings()
Definition CliComm.hh:99
static void completeString(std::vector< std::string > &tokens, ITER begin, ITER end, bool caseSensitive=true)
Definition Completer.hh:138
hash_map< std::string, CommandCompleter *, XXHasher > commandCompleters
void registerProxySetting(const Setting &setting)
void unregisterProxySetting(const Setting &setting)
GlobalCommandController(EventDistributor &eventDistributor, GlobalCliComm &cliComm, Reactor &reactor)
void registerSetting(Setting &setting) override
TODO.
std::string tabCompletion(std::string_view command)
Complete the given command.
void registerProxyCommand(std::string_view name)
void unregisterProxyCommand(std::string_view name)
void registerCommand(Command &command, zstring_view str) override
(Un)register a command
void registerCompleter(CommandCompleter &completer, std::string_view str) override
(Un)register a command completer, used to complete build-in Tcl cmds
TclObject executeCommand(zstring_view command, CliConnection *connection=nullptr) override
Execute the given command.
void unregisterSetting(Setting &setting) override
void unregisterCompleter(CommandCompleter &completer, std::string_view str) override
void unregisterCommand(Command &command, std::string_view str) override
bool isComplete(zstring_view command)
Returns true iff the command is complete (all braces, quotes etc.
void source(const std::string &script)
Executes all defined auto commands.
TclObject execute(zstring_view command)
TclObject getCommandNames()
void registerSetting(BaseSetting &variable)
void unregisterCommand(Command &command)
TclParser parse(std::string_view command)
TclObject executeFile(zstring_view filename)
bool isComplete(zstring_view command) const
void unregisterSetting(BaseSetting &variable)
void registerCommand(zstring_view name, Command &command)
Helper class to use files in APIs other than openmsx::File.
const std::string & getFilename() const
Returns path to a local uncompressed version of this file.
Contains the main loop of openMSX.
Definition Reactor.hh:74
void unregisterSetting(BaseSetting &setting)
void registerSetting(BaseSetting &setting)
static std::string full()
Definition Version.cc:8
Like std::string_view, but with the extra guarantee that it refers to a zero-terminated string.
mat3 n3(vec3(1, 0, 3), vec3(4, 5, 6), vec3(7, 8, 9))
const Value * lookup(const hash_map< Key, Value, Hasher, Equal > &map, const Key2 &key)
Definition hash_map.hh:118
detail::Joiner< Collection, Separator > join(Collection &&col, Separator &&sep)
Definition join.hh:60
constexpr double e
Definition Math.hh:21
This file implemented 3 utility functions:
Definition Autofire.cc:11
TclObject makeTclList(Args &&... args)
Definition TclObject.hh:293
auto remove_if(ForwardRange &&range, UnaryPredicate pred)
Definition ranges.hh:287
auto find(InputRange &&range, const T &value)
Definition ranges.hh:160
constexpr void sort(RandomAccessRange &&range)
Definition ranges.hh:49
std::string_view substr(std::string_view utf8, std::string_view::size_type first=0, std::string_view::size_type len=std::string_view::npos)
size_t size(std::string_view utf8)
constexpr auto drop(Range &&range, size_t n)
Definition view.hh:502
constexpr auto keys(Map &&map)
Definition view.hh:525
#define OUTER(type, member)
Definition outer.hh:42
void move_pop_back(VECTOR &v, typename VECTOR::iterator it)
Erase the pointed to element from the given vector.
Definition stl.hh:134
std::string strCat()
Definition strCat.hh:703
void strAppend(std::string &result, Ts &&...ts)
Definition strCat.hh:752
constexpr auto xrange(T e)
Definition xrange.hh:132
constexpr auto begin(const zstring_view &x)
constexpr auto end(const zstring_view &x)