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