OXYGENHUB
◆ main openmsx / src / imgui / ImGuiConsole.cc
22.7 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "ImGuiConsole.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiManager.hh"
5
#include "ImGuiUtils.hh"
6
7
#include "BooleanSetting.hh"
8
#include "CliComm.hh"
9
#include "Completer.hh"
10
#include "FileContext.hh"
11
#include "FileException.hh"
12
#include "FileOperations.hh"
13
#include "GlobalCommandController.hh"
14
#include "Interpreter.hh"
15
#include "Reactor.hh"
16
#include "TclParser.hh"
17
#include "Version.hh"
18
19
#include "narrow.hh"
20
#include "strCat.hh"
21
#include "utf8_unchecked.hh"
22
#include "xrange.hh"
23
24
#include <imgui.h>
25
#include <imgui_internal.h> /**/ // Hack: see below
26
#include <imgui_stdlib.h>
27
28
#include <fstream>
29
#include <ranges>
30
31
namespace openmsx {
32
33
using namespace std::literals;
34
35
static constexpr std::string_view PROMPT_NEW  = "> ";
36
static constexpr std::string_view PROMPT_CONT = "| ";
37
static constexpr std::string_view PROMPT_BUSY = "*busy*";
38
39
ImGuiConsole::ImGuiConsole(ImGuiManager& manager_)
40
	: ImGuiPart(manager_)
41
	, consoleSetting(
42
		manager.getReactor().getCommandController(), "console",
43
		"turns console display on/off", false, Setting::Save::NO)
44
	, history(1000)
45
	, lines(1000)
46
	, prompt(PROMPT_NEW)
47
{
48
	loadHistory();
49
50
	Completer::setOutput(this);
51
	manager.getInterpreter().setOutput(this);
52
	consoleSetting.attach(*this);
53
54
	const auto& fullVersion = Version::full();
55
	print(fullVersion);
56
	print(std::string(fullVersion.size(), '-'));
57
	print("\n"
58
	      "General information about openMSX is available at http://openmsx.org.\n"
59
	      "\n"
60
	      "Type 'help' to see a list of available commands.\n"
61
	      "Or read the Console Command Reference in the manual.\n"
62
	      "\n");
63
}
64
65
ImGuiConsole::~ImGuiConsole()
66
{
67
	consoleSetting.detach(*this);
68
}
69
70
void ImGuiConsole::save(ImGuiTextBuffer& buf)
71
{
72
	savePersistent(buf, *this, persistentElements);
73
}
74
75
void ImGuiConsole::loadLine(std::string_view name, zstring_view value)
76
{
77
	loadOnePersistent(name, value, *this, persistentElements);
78
}
79
80
void ImGuiConsole::print(std::string_view text, imColor color)
81
{
82
	do {
83
		auto pos = text.find('\n');
84
		newLineConsole(ConsoleLine(std::string(text.substr(0, pos)), color));
85
		if (pos == std::string_view::npos) break;
86
		text.remove_prefix(pos + 1); // skip newline
87
	} while (!text.empty());
88
}
89
90
void ImGuiConsole::newLineConsole(ConsoleLine line)
91
{
92
	auto addLine = [&](ConsoleLine&& l) {
93
		if (lines.full()) lines.pop_front();
94
		lines.push_back(std::move(l));
95
	};
96
97
	if (wrap) {
98
		do {
99
			auto rest = line.splitAtColumn(columns);
100
			addLine(std::move(line));
101
			line = std::move(rest);
102
		} while (!line.str().empty());
103
	} else {
104
		addLine(std::move(line));
105
	}
106
107
	scrollToBottom = true;
108
}
109
110
static void drawLine(const ConsoleLine& line)
111
{
112
	auto n = line.numChunks();
113
	for (auto i : xrange(n)) {
114
		im::StyleColor(ImGuiCol_Text, getColor(line.chunkColor(i)), [&]{
115
			ImGui::TextUnformatted(line.chunkText(i));
116
			if (i != (n - 1)) ImGui::SameLine(0.0f, 0.0f);
117
		});
118
	}
119
}
120
121
static ImGuiConsole::CompletionPopupLayout calcPopupLayout(
122
	gl::vec2 maxSize, std::span<const CompletionCandidate> completions)
123
{
124
	const auto& style = ImGui::GetStyle();
125
	auto itemHeight = ImGui::GetTextLineHeight() + 2 * style.CellPadding.y;
126
	auto fitRows = int(maxSize.y / itemHeight);
127
128
	// measure item widths
129
	assert(!completions.empty());
130
	auto N = narrow<int>(completions.size());
131
	auto padding = 2.0f * style.CellPadding.x;
132
	auto itemWidths = to_vector(std::views::transform(completions, [&](const auto& c){
133
		return ImGui::CalcTextSize(c.display().c_str()).x + padding;
134
	}));
135
	auto effectiveMaxWidth = maxSize.x - (N > fitRows ? style.ScrollbarSize : 0.0f);
136
	auto maxItemWidth = std::ranges::max(itemWidths);
137
	if (maxItemWidth > effectiveMaxWidth) {
138
		// even a single column is too wide
139
		auto height = float(N) * itemHeight;
140
		return {.columnWidths = {maxItemWidth},
141
			.size{maxSize.x, std::min(height, maxSize.y)},
142
			.needXScrollBar = true, .needYScrollBar = height > maxSize.y};
143
	}
144
145
	// Iterator over column-numbers 1..N, calculate layout and remember the 'best' one.
146
	// The best one is:
147
	// * doesn't need scroll bars
148
	// * not too wide, not too narrow, prefer closer to square
149
	ImGuiConsole::CompletionPopupLayout bestLayout{.needYScrollBar = true};
150
	float bestRatio = 0.0f; // worst ratio
151
	for (int cols = 1; cols <= N; ++cols) {
152
		int rows = (N + cols - 1) / cols;
153
		// E.g. when you place 9 items in 7 columns, column-major fill-order,
154
		// you'll end up with 2 rows and 4 fully filled, 1 partially filled
155
		// and 3 empty columns. So effectively only 5 columns instead of 7.
156
		int c2 = (N + rows - 1) / rows; // actual number of columns
157
		assert(1 <= c2);
158
		assert(c2 <= cols);
159
		if (c2 != cols) continue; // already tested
160
161
		std::vector<float> colWidths;
162
		colWidths.reserve(cols);
163
		float width = 0.0f;
164
		for (int c = 0; c < cols; ++c) {
165
			int from = c * rows;
166
			int to   = std::min(from + rows, N);
167
			assert(from < to);
168
			float w = std::ranges::max(subspan(itemWidths, from, to - from));
169
			width += w;
170
			colWidths.push_back(w);
171
		}
172
		effectiveMaxWidth = maxSize.x - (rows > fitRows ? style.ScrollbarSize : 0.0f);
173
		if (width > effectiveMaxWidth) {
174
			// Heuristic: if this is too wide, then using more
175
			// columns will also be too wide. Mathematically that's
176
			// not always true.
177
			break;
178
		}
179
180
		auto height = float(rows) * itemHeight;
181
		auto layout = ImGuiConsole::CompletionPopupLayout{
182
			.columnWidths = std::move(colWidths),
183
			.size{width, std::min(height, maxSize.y)},
184
			.needXScrollBar = false, .needYScrollBar = height > maxSize.y};
185
		auto ratio = width < height ? width / height : height / width; // 0..1
186
		if ((bestLayout.needYScrollBar && !layout.needYScrollBar) || // no scrollbar is always better
187
		    (ratio > bestRatio)) { // ratio closer to 1.0
188
			bestLayout = std::move(layout);
189
			bestRatio = ratio;
190
		}
191
	}
192
	assert(bestRatio != 0.0f);
193
	return bestLayout;
194
}
195
196
void ImGuiConsole::paint(MSXMotherBoard* /*motherBoard*/)
197
{
198
	if (!replayInput.empty()) {
199
		// completion popup was closed with text input, replay that for
200
		// the text input line.
201
		auto& io = ImGui::GetIO();
202
		for (auto ch : replayInput) {
203
			io.AddInputCharacter(ch);
204
		}
205
		replayInput.clear();
206
	}
207
208
	bool reclaimFocus = show && !wasShown; // window appears
209
	wasShown = show;
210
	if (!show) return;
211
212
	ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver);
213
	im::Window("Console", &show, [&]{
214
		im::ScopedFont sf(manager.fontMono);
215
216
		// Reserve enough left-over height for 1 separator + 1 input text
217
		const auto& style = ImGui::GetStyle();
218
		const float footerHeightToReserve = style.ItemSpacing.y +
219
						ImGui::GetFrameHeightWithSpacing();
220
221
		bool scrollUp   = ImGui::Shortcut(ImGuiKey_PageUp);
222
		bool scrollDown = ImGui::Shortcut(ImGuiKey_PageDown);
223
		gl::vec2 scrollRegionSize; // remember size/pos for completion popup
224
		gl::vec2 scrollRegionPos;
225
		im::Child("ScrollingRegion", ImVec2(0, -footerHeightToReserve), 0,
226
		          ImGuiWindowFlags_HorizontalScrollbar, [&]{
227
			scrollRegionSize = ImGui::GetWindowSize();
228
			scrollRegionPos = ImGui::GetWindowPos();
229
			im::PopupContextWindow([&]{
230
				if (ImGui::Selectable("Clear")) {
231
					lines.clear();
232
				}
233
				ImGui::Checkbox("Wrap (new) output", &wrap);
234
			});
235
236
			im::StyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1), [&]{ // Tighten spacing
237
				im::ListClipper(lines.size(), [&](int i) {
238
					drawLine(lines[i]);
239
				});
240
			});
241
			ImGui::Spacing();
242
243
			// Keep up at the bottom of the scroll region if we were already
244
			// at the bottom at the beginning of the frame.
245
			if (scrollToBottom || (ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) {
246
				scrollToBottom = false;
247
				ImGui::SetScrollHereY(1.0f);
248
			}
249
250
			auto scrollDelta = ImGui::GetWindowHeight() * 0.5f;
251
			if (scrollUp) {
252
				ImGui::SetScrollY(std::max(ImGui::GetScrollY() - scrollDelta, 0.0f));
253
			}
254
			if (scrollDown) {
255
				ImGui::SetScrollY(std::min(ImGui::GetScrollY() + scrollDelta, ImGui::GetScrollMaxY()));
256
			}
257
258
			// recalculate the number of columns
259
			auto width = ImGui::GetContentRegionAvail().x;
260
			auto charWidth = ImGui::CalcTextSize("M"sv).x;
261
			columns = narrow_cast<unsigned>(width / charWidth);
262
		});
263
		ImGui::Separator();
264
265
		// tab-completion popup
266
		bool doCenter = false;
267
		if (!completions.empty() && !completionPopupOpen) {
268
			// just opened popup
269
			ImGui::OpenPopup("MessagePopup");
270
			completionIndex = 0;
271
			doCenter = true;
272
273
			popupLayout = calcPopupLayout(scrollRegionSize * 0.95f, completions);
274
			popupLayout.size += 2.0f * (gl::vec2(style.WindowPadding) + gl::vec2(style.WindowBorderSize));
275
276
			// Screen-space coordinates of the scroll region
277
			auto scrollMin = scrollRegionPos;
278
			auto scrollMax = scrollMin + scrollRegionSize;
279
			// Desired horizontal position: center above text cursor
280
			float x = std::clamp(textCursorScrnPosX - 0.5f * popupLayout.size.x,
281
			                     scrollMin.x, scrollMax.x - popupLayout.size.x);
282
			// Align to the bottom of scroll region
283
			float y = scrollMax.y - popupLayout.size.y;
284
			ImGui::SetNextWindowPos(gl::vec2{x, y});
285
		}
286
		auto closeCleanup = [&] {
287
			completions.clear();
288
			completionPopupOpen = false;
289
		};
290
		auto close = [&]{
291
			closeCleanup();
292
			ImGui::CloseCurrentPopup();
293
		};
294
295
		ImGui::SetNextWindowSize(popupLayout.size);
296
		int wFlags = ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove;
297
		bool openNow = im::Popup("MessagePopup", wFlags, [&]{
298
			completionPopupOpen = true;
299
			auto N = narrow<int>(completions.size());
300
			auto C = narrow<int>(popupLayout.columnWidths.size());
301
			auto R = (N + C - 1) / C;
302
303
			auto& io = ImGui::GetIO();
304
			if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter) ||
305
			    ImGui::IsKeyPressed(ImGuiKey_Space)) {
306
				// select completion
307
				completionReplacement = completions[completionIndex];
308
				io.ClearInputKeys();
309
			} else if (io.InputQueueCharacters.Size != 0) {
310
				replayInput.assign(io.InputQueueCharacters.begin(), io.InputQueueCharacters.end());
311
			} else if (ImGui::IsKeyPressed(ImGuiKey_Backspace)) {
312
				replayBackspace = true;
313
			} else if (ImGui::IsKeyPressed(ImGuiKey_Tab)) {
314
				if (io.KeyShift) {
315
					completionIndex = (completionIndex == 0) ? (N - 1) : (completionIndex - 1);
316
				} else {
317
					completionIndex = (completionIndex == (N - 1)) ? 0 : (completionIndex + 1);
318
				}
319
				doCenter = true;
320
			} else {
321
				bool up    = ImGui::IsKeyPressed(ImGuiKey_UpArrow);
322
				bool down  = ImGui::IsKeyPressed(ImGuiKey_DownArrow);
323
				bool left  = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
324
				bool right = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
325
				if (up || down || left || right) {
326
					int c = completionIndex / R;
327
					int r = completionIndex % R;
328
					int lastR = N - (C - 1) * R;
329
					int nr = (c == (C - 1)) ? lastR : R;
330
					int nc = (r >= lastR) ? (C - 1) : C;
331
332
					if (up) {
333
						r = (r > 0) ? (r - 1) : (nr - 1);
334
					} else if (down) {
335
						r = (r < (nr - 1)) ? (r + 1) : 0;
336
					} else if (left) {
337
						c = (c > 0) ? (c - 1) : (nc - 1);
338
					} else if (right) {
339
						c = (c < (nc - 1)) ? (c + 1) : 0;
340
					}
341
342
					completionIndex = c * R + r;
343
					assert(completionIndex < N);
344
					doCenter = true;
345
				}
346
			}
347
			im::Child("list", {}, [&]{
348
				int tFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX
349
				          | (popupLayout.needXScrollBar ? ImGuiTableFlags_ScrollX : 0)
350
				          | (popupLayout.needYScrollBar ? ImGuiTableFlags_ScrollY : 0);
351
				im::Table("completionTable", C, tFlags, [&]{
352
					for (const auto& w : popupLayout.columnWidths) {
353
						ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, w);
354
					}
355
					int currentRow = completionIndex % R;
356
					im::ListClipper(R, currentRow, [&](int r) {
357
						for (int c = 0; c < C; ++c) {
358
							if (ImGui::TableNextColumn()) {
359
								int i = c * R + r;
360
								if (i >= N) break;
361
								bool selected = i == completionIndex;
362
								if (ImGui::Selectable(completions[i].display().c_str(), selected)) {
363
									completionReplacement = completions[i];
364
								}
365
								if (selected && doCenter) ImGui::SetScrollHereY(0.5f);
366
							}
367
						}
368
					});
369
				});
370
			});
371
			if (!completionReplacement.text.empty() || !replayInput.empty() || replayBackspace) {
372
				close();
373
			}
374
		});
375
		if (!openNow && completionPopupOpen) {
376
			// just closed popup  (by some ImGui action, e.g. click outside)
377
			closeCleanup();
378
			// Clear ESC key state so InputText doesn't see it as pressed
379
			ImGui::GetIO().ClearInputKeys();
380
		}
381
382
		// Command-line
383
		ImGui::AlignTextToFramePadding();
384
		ImGui::TextUnformatted(prompt);
385
		ImGui::SameLine(0.0f, 0.0f);
386
387
		ImGui::SetNextItemWidth(-FLT_MIN); // full window width
388
		/**/ // Hack: see below
389
		/**/ auto cursorScrnPos = ImGui::GetCursorScreenPos();
390
		/**/ auto itemWidth = ImGui::CalcItemWidth();
391
392
		ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue |
393
					ImGuiInputTextFlags_EscapeClearsAll |
394
					ImGuiInputTextFlags_CallbackEdit |
395
					ImGuiInputTextFlags_CallbackCompletion |
396
					ImGuiInputTextFlags_CallbackHistory |
397
					ImGuiInputTextFlags_CallbackAlways;
398
		bool enter = false;
399
		im::StyleColor(ImGuiCol_Text, 0x00000000, [&]{ // transparent, see HACK below
400
			enter = ImGui::InputTextWithHint("##Input", "enter command", &inputBuf, flags, &textEditCallbackStub, this);
401
			if (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Escape)) {
402
				commandBuffer.clear();
403
				prompt = PROMPT_NEW;
404
			}
405
		});
406
		if (enter && (prompt != PROMPT_BUSY)) {
407
			// print command in output buffer, with prompt prepended
408
			ConsoleLine cmdLine(prompt);
409
			cmdLine.addLine(coloredInputBuf);
410
			newLineConsole(std::move(cmdLine));
411
412
			// append (partial) command to a possibly multi-line command
413
			strAppend(commandBuffer, inputBuf, '\n');
414
415
			putHistory(std::move(inputBuf));
416
			saveHistory(); // save at this point already, so that we don't lose history in case of a crash
417
			inputBuf.clear();
418
			coloredInputBuf.clear();
419
			historyPos = -1;
420
			historyBackupLine.clear();
421
422
			auto& commandController = manager.getReactor().getGlobalCommandController();
423
			if (commandController.isComplete(commandBuffer)) {
424
				// Normally the busy prompt is NOT shown (not even briefly
425
				// because the screen is not redrawn), though for some commands
426
				// that potentially take a long time to execute, we explicitly
427
				// do redraw.
428
				prompt = PROMPT_BUSY;
429
430
				manager.executeDelayed(TclObject(commandBuffer),
431
					[this](const TclObject& result) {
432
						if (const auto& s = result.getString(); !s.empty()) {
433
							this->print(s);
434
						}
435
						prompt = PROMPT_NEW;
436
					},
437
					[this](const std::string& error) {
438
						this->print(error, imColor::ERROR);
439
						prompt = PROMPT_NEW;
440
					});
441
				commandBuffer.clear();
442
			} else {
443
				prompt = PROMPT_CONT;
444
			}
445
			reclaimFocus = true;
446
		}
447
		ImGui::SetItemDefaultFocus();
448
449
		if (reclaimFocus ||
450
		(ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) &&
451
		!ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId) &&
452
		!ImGui::IsAnyItemActive() && !ImGui::IsMouseClicked(0) && !ImGui::IsMouseClicked(1))) {
453
			ImGui::SetKeyboardFocusHere(-1); // focus the InputText widget
454
		}
455
456
		/**/ // Hack: currently ImGui::InputText() does not support colored text.
457
		/**/ // Though there are plans to extend this. See:
458
		/**/ //     https://github.com/ocornut/imgui/pull/3130
459
		/**/ //     https://github.com/ocornut/imgui/issues/902
460
		/**/ // To work around this limitation, we use ImGui::InputText() as-is,
461
		/**/ // but then overdraw the text using the correct colors. This works,
462
		/**/ // but it's fragile because it depends on some internal implementation
463
		/**/ // details. More specifically: the scroll-position. And obtaining this
464
		/**/ // information required stuff from <imgui_internal.h>.
465
		/**/ auto* font = ImGui::GetFont();
466
		/**/ auto fontSize = ImGui::GetFontSize();
467
		/**/ gl::vec2 frameSize(itemWidth, fontSize + style.FramePadding.y * 2.0f);
468
		/**/ gl::vec2 topLeft = cursorScrnPos;
469
		/**/ gl::vec2 bottomRight = topLeft + frameSize;
470
		/**/ gl::vec2 drawPos = topLeft + gl::vec2(style.FramePadding);
471
		/**/ ImVec4 clipRect = gl::vec4(topLeft, bottomRight);
472
		/**/ auto* drawList = ImGui::GetWindowDrawList();
473
		/**/ if (ImGui::IsItemActive()) {
474
		/**/	auto id = ImGui::GetID("##Input");
475
		/**/	if (const auto* state = ImGui::GetInputTextState(id)) { // Internal API !!!
476
		/**/		// adjust for scroll
477
		/**/		drawPos.x -= state->Scroll.x;
478
		/**/		// redraw cursor (it was drawn transparent before)
479
		/**/		bool cursorIsVisible = (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
480
		/**/		if (cursorIsVisible) {
481
		/**/			// This assumes a single line
482
		/**/			auto strToCursor = zstring_view(coloredInputBuf.str()).substr(0, state->GetCursorPos());
483
		/**/			gl::vec2 cursorOffset(ImGui::CalcTextSize(strToCursor).x, 0.0f);
484
		/**/			gl::vec2 cursorScreenPos = ImTrunc(drawPos + cursorOffset);
485
		/**/			textCursorScrnPosX = cursorScreenPos.x; // remember for completion popup
486
		/**/			ImRect cursorScreenRect(cursorScreenPos.x, cursorScreenPos.y - 0.5f, cursorScreenPos.x + 1.0f, cursorScreenPos.y + fontSize - 1.5f);
487
		/**/			if (cursorScreenRect.Overlaps(clipRect)) {
488
		/**/				drawList->AddLine(cursorScreenRect.Min, cursorScreenRect.GetBL(), getColor(imColor::TEXT));
489
		/**/			}
490
		/**/		}
491
		/**/	}
492
		/**/ }
493
		/**/ for (auto i : xrange(coloredInputBuf.numChunks())) {
494
		/**/ 	auto text = coloredInputBuf.chunkText(i);
495
		/**/ 	auto rgba = getColor(coloredInputBuf.chunkColor(i));
496
		/**/ 	const char* begin = text.data();
497
		/**/ 	const char* end = begin + text.size();
498
		/**/ 	drawList->AddText(font, fontSize, drawPos, rgba, begin, end, 0.0f, &clipRect);
499
		/**/    drawPos.x += ImGui::CalcTextSize(text).x;
500
		/**/ }
501
	});
502
}
503
504
int ImGuiConsole::textEditCallbackStub(ImGuiInputTextCallbackData* data)
505
{
506
	auto* console = static_cast<ImGuiConsole*>(data->UserData);
507
	return console->textEditCallback(data);
508
}
509
510
void ImGuiConsole::doEdit(ImGuiInputTextCallbackData* data)
511
{
512
	historyBackupLine.assign(data->Buf, narrow<size_t>(data->BufTextLen));
513
	historyPos = -1;
514
	colorize(historyBackupLine);
515
}
516
517
void ImGuiConsole::tabEdit(ImGuiInputTextCallbackData* data, function_ref<std::string(std::string_view)> action)
518
{
519
	std::string_view oldLine{data->Buf, narrow<size_t>(data->BufTextLen)};
520
	auto front = oldLine.substr(0, data->CursorPos);
521
	auto back  = oldLine.substr(data->CursorPos);
522
523
	std::string newStr = action(front);
524
	auto newPos = narrow<int>(newStr.size());
525
	newStr += back;
526
527
	data->DeleteChars(0, data->BufTextLen);
528
	data->InsertChars(0, newStr.c_str());
529
	data->CursorPos = newPos;
530
531
	doEdit(data);
532
}
533
534
int ImGuiConsole::textEditCallback(ImGuiInputTextCallbackData* data)
535
{
536
	if (data->EventFlag & ImGuiInputTextFlags_CallbackCompletion) {
537
		auto& commandController = manager.getReactor().getGlobalCommandController();
538
		tabEdit(data, [&](std::string_view front) {
539
			return commandController.tabCompletion(front);
540
		});
541
	}
542
	if (data->EventFlag & ImGuiInputTextFlags_CallbackHistory) {
543
		bool match = false;
544
		if (data->EventKey == ImGuiKey_UpArrow) {
545
			while (!match && (historyPos < narrow<int>(history.size() - 1))) {
546
				++historyPos;
547
				match = history[historyPos].starts_with(historyBackupLine);
548
			}
549
		} else if ((data->EventKey == ImGuiKey_DownArrow) && (historyPos != -1)) {
550
			while (!match) {
551
				if (--historyPos == -1) break;
552
				match = history[historyPos].starts_with(historyBackupLine);
553
			}
554
		}
555
		if (match || (historyPos == -1)) {
556
			const auto& historyStr = (historyPos >= 0) ? history[historyPos] : historyBackupLine;
557
			data->DeleteChars(0, data->BufTextLen);
558
			data->InsertChars(0, historyStr.c_str());
559
			colorize(std::string_view{data->Buf, narrow<size_t>(data->BufTextLen)});
560
		}
561
	}
562
	if (data->EventFlag & ImGuiInputTextFlags_CallbackEdit) {
563
		doEdit(data);
564
	}
565
	if (data->EventFlag & ImGuiInputTextFlags_CallbackAlways) {
566
		if (!completionReplacement.text.empty()) {
567
			// completion popup made a selection, use that
568
			auto& commandController = manager.getReactor().getGlobalCommandController();
569
			tabEdit(data, [&](std::string_view front) {
570
				return commandController.tabCompletionReplace(front, completionReplacement);
571
			});
572
			completionReplacement.text.clear();
573
		}
574
		if (replayBackspace) {
575
			// completion popup was closed with backspace, simulate that
576
			replayBackspace = false;
577
			if (data->CursorPos > 0) {
578
				data->DeleteChars(data->CursorPos - 1, 1);
579
				doEdit(data);
580
			}
581
		}
582
	}
583
	return 0;
584
}
585
586
void ImGuiConsole::colorize(std::string_view line)
587
{
588
	TclParser parser = manager.getInterpreter().parse(line);
589
	const auto& colors = parser.getColors();
590
	assert(colors.size() == line.size());
591
592
	coloredInputBuf.clear();
593
	size_t pos = 0;
594
	while (pos != colors.size()) {
595
		char col = colors[pos];
596
		size_t pos2 = pos++;
597
		while ((pos != colors.size()) && (colors[pos] == col)) {
598
			++pos;
599
		}
600
		imColor color = [&] {
601
			switch (col) {
602
			using enum imColor;
603
			case 'E': return ERROR;
604
			case 'c': return COMMENT;
605
			case 'v': return VARIABLE;
606
			case 'l': return LITERAL;
607
			case 'p': return PROC;
608
			case 'o': return OPERATOR;
609
			default:  return TEXT; // other
610
			}
611
		}();
612
		coloredInputBuf.addChunk(line.substr(pos2, pos - pos2), color);
613
	}
614
}
615
616
void ImGuiConsole::putHistory(std::string command)
617
{
618
	if (command.empty()) return;
619
	if (!history.empty() && (history.front() == command)) {
620
		return;
621
	}
622
	if (history.full()) history.pop_back();
623
	history.push_front(std::move(command));
624
}
625
626
void ImGuiConsole::saveHistory()
627
{
628
	try {
629
		std::ofstream outputFile;
630
		FileOperations::openOfStream(outputFile,
631
		        userFileContext("console").resolveCreate("history.txt"));
632
		if (!outputFile) {
633
			throw FileException("Error while saving the console history.");
634
		}
635
		for (const auto& s : std::views::reverse(history)) {
636
			outputFile << s << '\n';
637
		}
638
	} catch (FileException& e) {
639
		manager.getCliComm().printWarning(e.getMessage());
640
	}
641
}
642
643
void ImGuiConsole::loadHistory()
644
{
645
	try {
646
		std::ifstream inputFile(
647
		        userFileContext("console").resolveCreate("history.txt"));
648
		std::string line;
649
		while (inputFile) {
650
			getline(inputFile, line);
651
			putHistory(line);
652
		}
653
	} catch (FileException&) {
654
		// Error while loading the console history, ignore
655
	}
656
}
657
658
void ImGuiConsole::output(std::string_view text)
659
{
660
	print(text);
661
}
662
663
unsigned ImGuiConsole::getOutputColumns() const
664
{
665
	return columns;
666
}
667
668
void ImGuiConsole::setCompletions(std::vector<CompletionCandidate> completions_)
669
{
670
	assert(!completions_.empty());
671
	completions = std::move(completions_);
672
}
673
674
void ImGuiConsole::update(const Setting& /*setting*/) noexcept
675
{
676
	show = consoleSetting.getBoolean();
677
	if (!show) {
678
		// Close the console via the 'console' setting.  Typically this
679
		// means via the F10 hotkey (or possibly by typing 'set console
680
		// off' in the console).
681
		//
682
		// Give focus to the main openMSX window.
683
		//
684
		// This makes the following scenario work:
685
		// * You were controlling the MSX, e.g. playing a game.
686
		// * You press F10 to open the console.
687
		// * You type a command (e.g. swap a game disk, for some people
688
		//   the console is still more convenient and/or faster than the
689
		//   new media menu).
690
		// * You press F10 again to close the console
691
		// * At this point the focus should go back to the main openMSX
692
		//   window (so that MSX input works).
693
		SDL_RaiseWindow(SDL_GetWindowFromID(WindowEvent::getMainWindowId()));
694
		ImGui::SetWindowFocus(nullptr);
695
	}
696
}
697
698
} // namespace openmsx