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