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.contains(chr)) {
274 result += '\\';
275 }
276 result += chr;
277 }
278 return result;
279}
280
281static string addEscaping(const string& str, bool quote, bool finished)
282{
283 if (str.empty() && finished) {
284 quote = true;
285 }
286 string result = escapeChars(str, "$[]");
287 if (quote) {
288 result.insert(result.begin(), '"');
289 if (finished) {
290 result += '"';
291 }
292 } else {
293 result = escapeChars(result, " ");
294 }
295 return result;
296}
297
299{
300 return interpreter.isComplete(command);
301}
302
304 zstring_view command, CliConnection* connection_)
305{
306 ScopedAssign sa(connection, connection_);
307 return interpreter.execute(command);
308}
309
310void GlobalCommandController::source(const string& script)
311{
312 try {
313 LocalFileReference file(script);
314 interpreter.executeFile(file.getFilename());
315 } catch (CommandException& e) {
317 "While executing ", script, ": ", e.getMessage());
318 }
319}
320
321string GlobalCommandController::tabCompletion(string_view command)
322{
323 // split on 'active' command (the command that should actually be
324 // completed). Some examples:
325 // if {[debug rea<tab> <-- should complete the 'debug' command
326 // instead of the 'if' command
327 // bind F6 { cycl<tab> <-- should complete 'cycle' instead of 'bind'
328 TclParser parser = interpreter.parse(command);
329 int last = parser.getLast();
330 string_view pre = command.substr(0, last);
331 string_view post = command.substr(last);
332
333 // split command string in tokens
334 vector<string> originalTokens = split(post, ' ');
335 if (originalTokens.empty()) {
336 originalTokens.emplace_back();
337 }
338
339 // complete last token
340 auto tokens = removeEscaping(originalTokens, true);
341 auto oldNum = tokens.size();
342 tabCompletion(tokens);
343 auto newNum = tokens.size();
344 bool tokenFinished = oldNum != newNum;
345
346 // replace last token
347 string& original = originalTokens.back();
348 if (const string& completed = tokens[oldNum - 1]; !completed.empty()) {
349 bool quote = !original.empty() && (original[0] == '"');
350 original = addEscaping(completed, quote, tokenFinished);
351 }
352 if (tokenFinished) {
353 assert(newNum == (oldNum + 1));
354 assert(tokens.back().empty());
355 originalTokens.emplace_back();
356 }
357
358 // rebuild command string
359 return strCat(pre, join(originalTokens, ' '));
360}
361
362void GlobalCommandController::tabCompletion(vector<string>& tokens)
363{
364 if (tokens.empty()) {
365 // nothing typed yet
366 return;
367 }
368 if (tokens.size() == 1) {
369 string_view cmd = tokens[0];
370 string_view leadingNs;
371 // remove leading ::
372 if (cmd.starts_with("::")) {
373 cmd.remove_prefix(2);
374 leadingNs = "::";
375 }
376 // get current (typed) namespace
377 auto p1 = cmd.rfind("::");
378 string_view ns = (p1 == string_view::npos) ? cmd : cmd.substr(0, p1 + 2);
379
380 // build a list of all command strings
381 TclObject names = interpreter.getCommandNames();
382 vector<string> names2;
383 names2.reserve(names.size());
384 for (string_view n1 : names) {
385 // remove leading ::
386 if (n1.starts_with("::")) n1.remove_prefix(2);
387 // initial namespace part must match
388 if (!n1.starts_with(ns)) continue;
389 // the part following the initial namespace
390 string_view n2 = n1.substr(ns.size());
391 // only keep upto the next namespace portion,
392 auto p2 = n2.find("::");
393 auto n3 = (p2 == string_view::npos) ? n1 : n1.substr(0, ns.size() + p2 + 2);
394 // don't care about adding the same string multiple times
395 names2.push_back(strCat(leadingNs, n3));
396 }
397 Completer::completeString(tokens, names2);
398 } else {
399 string_view cmd = tokens.front();
400 if (cmd.starts_with("::")) cmd.remove_prefix(2); // drop leading ::
401
402 if (auto* v = lookup(commandCompleters, cmd)) {
403 (*v)->tabCompletion(tokens);
404 } else {
405 TclObject command = makeTclList("openmsx::tabcompletion");
406 command.addListElements(tokens);
407 try {
408 TclObject list = command.executeCommand(interpreter);
409 bool sensitive = true;
410 auto begin = list.begin();
411 auto end = list.end();
412 if (begin != end) {
413 auto it2 = end; --it2;
414 auto back = *it2;
415 if (back == "false") {
416 end = it2;
417 sensitive = false;
418 } else if (back == "true") {
419 end = it2;
420 sensitive = true;
421 }
422 }
423 Completer::completeString(tokens, begin, end, sensitive);
424 } catch (CommandException& e) {
425 cliComm.printWarning(
426 "Error while executing tab-completion "
427 "proc: ", e.getMessage());
428 }
429 }
430 }
431}
432
433
434// Help Command
435
436GlobalCommandController::HelpCmd::HelpCmd(GlobalCommandController& controller_)
437 : Command(controller_, "help")
438{
439}
440
441void GlobalCommandController::HelpCmd::execute(
442 std::span<const TclObject> tokens, TclObject& result)
443{
444 auto& controller = OUTER(GlobalCommandController, helpCmd);
445 switch (tokens.size()) {
446 case 1: {
447 string text =
448 "Use 'help [command]' to get help for a specific command\n"
449 "The following commands exist:\n";
450 auto cmds = concat<string_view>(
451 view::keys(controller.commandCompleters),
452 getInterpreter().execute("openmsx::all_command_names_with_help"));
453 std::erase_if(cmds, [](const auto& c) { return c.contains("::"); });
454 ranges::sort(cmds);
455 for (auto& line : formatListInColumns(cmds)) {
456 strAppend(text, line, '\n');
457 }
458 result = text;
459 break;
460 }
461 default: {
462 if (const auto* v = lookup(controller.commandCompleters, tokens[1].getString())) {
463 result = (*v)->help(tokens.subspan(1));
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
474string GlobalCommandController::HelpCmd::help(std::span<const TclObject> /*tokens*/) const
475{
476 return "prints help information for commands\n";
477}
478
479void 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
491GlobalCommandController::TabCompletionCmd::TabCompletionCmd(
492 GlobalCommandController& controller_)
493 : Command(controller_, "tabcompletion")
494{
495}
496
497void GlobalCommandController::TabCompletionCmd::execute(
498 std::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
506string GlobalCommandController::TabCompletionCmd::help(std::span<const TclObject> /*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
517GlobalCommandController::UpdateCmd::UpdateCmd(CommandController& commandController_)
518 : Command(commandController_, "openmsx_update")
519{
520}
521
522static GlobalCliComm::UpdateType getType(const TclObject& name)
523{
524 for (auto updateStr = CliComm::getUpdateStrings();
525 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
533CliConnection& GlobalCommandController::UpdateCmd::getConnection()
534{
535 const 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
543void GlobalCommandController::UpdateCmd::execute(
544 std::span<const TclObject> tokens, TclObject& /*result*/)
545{
546 checkNumArgs(tokens, 3, Prefix{1}, "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
556string GlobalCommandController::UpdateCmd::help(std::span<const TclObject> /*tokens*/) const
557{
558 return "Enable or disable update events for external applications. See doc/openmsx-control-xml.txt.";
559}
560
561void GlobalCommandController::UpdateCmd::tabCompletion(vector<string>& tokens) const
562{
563 switch (tokens.size()) {
564 case 2: {
565 using namespace std::literals;
566 static constexpr std::array ops = {"enable"sv, "disable"sv};
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
579GlobalCommandController::PlatformInfo::PlatformInfo(InfoCommand& openMSXInfoCommand_)
580 : InfoTopic(openMSXInfoCommand_, "platform")
581{
582}
583
584void GlobalCommandController::PlatformInfo::execute(
585 std::span<const TclObject> /*tokens*/, TclObject& result) const
586{
587 result = TARGET_PLATFORM;
588}
589
590string GlobalCommandController::PlatformInfo::help(std::span<const TclObject> /*tokens*/) const
591{
592 return "Prints openMSX platform.";
593}
594
595// Version info
596
597GlobalCommandController::VersionInfo::VersionInfo(InfoCommand& openMSXInfoCommand_)
598 : InfoTopic(openMSXInfoCommand_, "version")
599{
600}
601
602void GlobalCommandController::VersionInfo::execute(
603 std::span<const TclObject> /*tokens*/, TclObject& result) const
604{
605 result = Version::full();
606}
607
608string GlobalCommandController::VersionInfo::help(std::span<const TclObject> /*tokens*/) const
609{
610 return "Prints openMSX version.";
611}
612
613} // 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 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:423
constexpr auto keys(Map &&map)
Definition view.hh:446
#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:137
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)