openMSX
ImGuiConsole.cc
Go to the documentation of this file.
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"
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
30namespace openmsx {
31
32using namespace std::literals;
33
34static constexpr std::string_view PROMPT_NEW = "> ";
35static constexpr std::string_view PROMPT_CONT = "| ";
36static constexpr std::string_view PROMPT_BUSY = "*busy*";
37
39 : ImGuiPart(manager_)
40 , consoleSetting(
41 manager.getReactor().getCommandController(), "console",
42 "turns console display on/off", false, Setting::Save::NO)
43 , history(1000)
44 , lines(1000)
45 , prompt(PROMPT_NEW)
46{
47 loadHistory();
48
51 consoleSetting.attach(*this);
52
53 const auto& fullVersion = Version::full();
54 print(fullVersion);
55 print(std::string(fullVersion.size(), '-'));
56 print("\n"
57 "General information about openMSX is available at http://openmsx.org.\n"
58 "\n"
59 "Type 'help' to see a list of available commands.\n"
60 "Or read the Console Command Reference in the manual.\n"
61 "\n");
62}
63
65{
66 consoleSetting.detach(*this);
67}
68
69void ImGuiConsole::save(ImGuiTextBuffer& buf)
70{
71 savePersistent(buf, *this, persistentElements);
72}
73
74void ImGuiConsole::loadLine(std::string_view name, zstring_view value)
75{
76 loadOnePersistent(name, value, *this, persistentElements);
77}
78
79void ImGuiConsole::print(std::string_view text, imColor color)
80{
81 do {
82 auto pos = text.find('\n');
83 newLineConsole(ConsoleLine(std::string(text.substr(0, pos)), color));
84 if (pos == std::string_view::npos) break;
85 text.remove_prefix(pos + 1); // skip newline
86 } while (!text.empty());
87}
88
89void ImGuiConsole::newLineConsole(ConsoleLine line)
90{
91 auto addLine = [&](ConsoleLine&& l) {
92 if (lines.full()) lines.pop_front();
93 lines.push_back(std::move(l));
94 };
95
96 if (wrap) {
97 do {
98 auto rest = line.splitAtColumn(columns);
99 addLine(std::move(line));
100 line = std::move(rest);
101 } while (!line.str().empty());
102 } else {
103 addLine(std::move(line));
104 }
105
106 scrollToBottom = true;
107}
108
109static void drawLine(const ConsoleLine& line)
110{
111 auto n = line.numChunks();
112 for (auto i : xrange(n)) {
113 im::StyleColor(ImGuiCol_Text, getColor(line.chunkColor(i)), [&]{
114 ImGui::TextUnformatted(line.chunkText(i));
115 if (i != (n - 1)) ImGui::SameLine(0.0f, 0.0f);
116 });
117 }
118}
119
121{
122 bool reclaimFocus = show && !wasShown; // window appears
123 wasShown = show;
124 if (!show) return;
125
126 ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver);
127 im::Window("Console", &show, [&]{
129
130 // Reserve enough left-over height for 1 separator + 1 input text
131 const auto& style = ImGui::GetStyle();
132 const float footerHeightToReserve = style.ItemSpacing.y +
133 ImGui::GetFrameHeightWithSpacing();
134
135 bool scrollUp = ImGui::Shortcut(ImGuiKey_PageUp);
136 bool scrollDown = ImGui::Shortcut(ImGuiKey_PageDown);
137 im::Child("ScrollingRegion", ImVec2(0, -footerHeightToReserve), 0,
138 ImGuiWindowFlags_HorizontalScrollbar, [&]{
140 if (ImGui::Selectable("Clear")) {
141 lines.clear();
142 }
143 ImGui::Checkbox("Wrap (new) output", &wrap);
144 });
145
146 im::StyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1), [&]{ // Tighten spacing
147 im::ListClipper(lines.size(), [&](int i) {
148 drawLine(lines[i]);
149 });
150 });
151 ImGui::Spacing();
152
153 // Keep up at the bottom of the scroll region if we were already
154 // at the bottom at the beginning of the frame.
155 if (scrollToBottom || (ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) {
156 scrollToBottom = false;
157 ImGui::SetScrollHereY(1.0f);
158 }
159
160 auto scrollDelta = ImGui::GetWindowHeight() * 0.5f;
161 if (scrollUp) {
162 ImGui::SetScrollY(std::max(ImGui::GetScrollY() - scrollDelta, 0.0f));
163 }
164 if (scrollDown) {
165 ImGui::SetScrollY(std::min(ImGui::GetScrollY() + scrollDelta, ImGui::GetScrollMaxY()));
166 }
167
168 // recalculate the number of columns
169 auto width = ImGui::GetContentRegionAvail().x;
170 auto charWidth = ImGui::CalcTextSize("M"sv).x;
171 columns = narrow_cast<unsigned>(width / charWidth);
172 });
173 ImGui::Separator();
174
175 // Command-line
176 ImGui::AlignTextToFramePadding();
178 ImGui::SameLine(0.0f, 0.0f);
179
180 ImGui::SetNextItemWidth(-FLT_MIN); // full window width
181 // Hack: see below
182 auto cursorScrnPos = ImGui::GetCursorScreenPos();
183 auto itemWidth = ImGui::CalcItemWidth();
184
185 ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue |
186 ImGuiInputTextFlags_EscapeClearsAll |
187 ImGuiInputTextFlags_CallbackCompletion |
188 ImGuiInputTextFlags_CallbackHistory;
189 bool enter = false;
190 im::StyleColor(ImGuiCol_Text, 0x00000000, [&]{ // transparent, see HACK below
191 enter = ImGui::InputTextWithHint("##Input", "enter command", &inputBuf, flags, &textEditCallbackStub, this);
192 if (ImGui::IsItemEdited()) {
193 historyBackupLine = inputBuf;
194 historyPos = -1;
195 colorize(inputBuf);
196 }
197 if (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Escape)) {
198 commandBuffer.clear();
199 prompt = PROMPT_NEW;
200 }
201 });
202 if (enter && (prompt != PROMPT_BUSY)) {
203 // print command in output buffer, with prompt prepended
204 ConsoleLine cmdLine(prompt);
205 cmdLine.addLine(coloredInputBuf);
206 newLineConsole(std::move(cmdLine));
207
208 // append (partial) command to a possibly multi-line command
209 strAppend(commandBuffer, inputBuf, '\n');
210
211 putHistory(std::move(inputBuf));
212 saveHistory(); // save at this point already, so that we don't lose history in case of a crash
213 inputBuf.clear();
214 coloredInputBuf.clear();
215 historyPos = -1;
216 historyBackupLine.clear();
217
218 auto& commandController = manager.getReactor().getGlobalCommandController();
219 if (commandController.isComplete(commandBuffer)) {
220 // Normally the busy prompt is NOT shown (not even briefly
221 // because the screen is not redrawn), though for some commands
222 // that potentially take a long time to execute, we explicitly
223 // do redraw.
224 prompt = PROMPT_BUSY;
225
226 manager.executeDelayed(TclObject(commandBuffer),
227 [this](const TclObject& result) {
228 if (const auto& s = result.getString(); !s.empty()) {
229 this->print(s);
230 }
231 prompt = PROMPT_NEW;
232 },
233 [this](const std::string& error) {
234 this->print(error, imColor::ERROR);
235 prompt = PROMPT_NEW;
236 });
237 commandBuffer.clear();
238 } else {
239 prompt = PROMPT_CONT;
240 }
241 reclaimFocus = true;
242 }
243 ImGui::SetItemDefaultFocus();
244
245 if (reclaimFocus ||
246 (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) &&
247 !ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId) &&
248 !ImGui::IsAnyItemActive() && !ImGui::IsMouseClicked(0) && !ImGui::IsMouseClicked(1))) {
249 ImGui::SetKeyboardFocusHere(-1); // focus the InputText widget
250 }
251
252 // Hack: currently ImGui::InputText() does not support colored text.
253 // Though there are plans to extend this. See:
254 // https://github.com/ocornut/imgui/pull/3130
255 // https://github.com/ocornut/imgui/issues/902
256 // To work around this limitation, we use ImGui::InputText() as-is,
257 // but then overdraw the text using the correct colors. This works,
258 // but it's fragile because it depends on some internal implementation
259 // details. More specifically: the scroll-position. And obtaining this
260 // information required stuff from <imgui_internal.h>.
261 auto* font = ImGui::GetFont();
262 auto fontSize = ImGui::GetFontSize();
263 gl::vec2 frameSize(itemWidth, fontSize + style.FramePadding.y * 2.0f);
264 gl::vec2 topLeft = cursorScrnPos;
265 gl::vec2 bottomRight = topLeft + frameSize;
266 gl::vec2 drawPos = topLeft + gl::vec2(style.FramePadding);
267 ImVec4 clipRect = gl::vec4(topLeft, bottomRight);
268 auto* drawList = ImGui::GetWindowDrawList();
269 auto charWidth = ImGui::GetFont()->GetCharAdvance('A'); // assumes fixed-width font
270 if (ImGui::IsItemActive()) {
271 auto id = ImGui::GetID("##Input");
272 if (const auto* state = ImGui::GetInputTextState(id)) { // Internal API !!!
273 // adjust for scroll
274 drawPos.x -= state->Scroll.x;
275 // redraw cursor (it was drawn transparent before)
276 bool cursorIsVisible = (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
277 if (cursorIsVisible) {
278 // This assumes a single line and fixed-width font
279 gl::vec2 cursorOffset(float(state->GetCursorPos()) * charWidth, 0.0f);
280 gl::vec2 cursorScreenPos = ImTrunc(drawPos + cursorOffset);
281 ImRect cursorScreenRect(cursorScreenPos.x, cursorScreenPos.y - 0.5f, cursorScreenPos.x + 1.0f, cursorScreenPos.y + fontSize - 1.5f);
282 if (cursorScreenRect.Overlaps(clipRect)) {
283 drawList->AddLine(cursorScreenRect.Min, cursorScreenRect.GetBL(), getColor(imColor::TEXT));
284 }
285 }
286 }
287 }
288 for (auto i : xrange(coloredInputBuf.numChunks())) {
289 auto text = coloredInputBuf.chunkText(i);
290 auto rgba = getColor(coloredInputBuf.chunkColor(i));
291 const char* begin = text.data();
292 const char* end = begin + text.size();
293 drawList->AddText(font, fontSize, drawPos, rgba, begin, end, 0.0f, &clipRect);
294 // avoid ImGui::CalcTextSize(): it's off-by-one for sizes >= 256 pixels
295 drawPos.x += charWidth * float(utf8::unchecked::distance(begin, end));
296 }
297 });
298}
299
300int ImGuiConsole::textEditCallbackStub(ImGuiInputTextCallbackData* data)
301{
302 auto* console = static_cast<ImGuiConsole*>(data->UserData);
303 return console->textEditCallback(data);
304}
305
306int ImGuiConsole::textEditCallback(ImGuiInputTextCallbackData* data)
307{
308 switch (data->EventFlag) {
309 case ImGuiInputTextFlags_CallbackCompletion: {
310 std::string_view oldLine{data->Buf, narrow<size_t>(data->BufTextLen)};
311 std::string_view front = utf8::unchecked::substr(oldLine, 0, data->CursorPos);
312 std::string_view back = utf8::unchecked::substr(oldLine, data->CursorPos);
313
314 auto& commandController = manager.getReactor().getGlobalCommandController();
315 std::string newFront = commandController.tabCompletion(front);
316 historyBackupLine = strCat(std::move(newFront), back);
317 historyPos = -1;
318
319 data->DeleteChars(0, data->BufTextLen);
320 data->InsertChars(0, historyBackupLine.c_str());
321
322 colorize(historyBackupLine);
323 break;
324 }
325 case ImGuiInputTextFlags_CallbackHistory: {
326 bool match = false;
327 if (data->EventKey == ImGuiKey_UpArrow) {
328 while (!match && (historyPos < narrow<int>(history.size() - 1))) {
329 ++historyPos;
330 match = history[historyPos].starts_with(historyBackupLine);
331 }
332 } else if ((data->EventKey == ImGuiKey_DownArrow) && (historyPos != -1)) {
333 while (!match) {
334 if (--historyPos == -1) break;
335 match = history[historyPos].starts_with(historyBackupLine);
336 }
337 }
338 if (match || (historyPos == -1)) {
339 const auto& historyStr = (historyPos >= 0) ? history[historyPos] : historyBackupLine;
340 data->DeleteChars(0, data->BufTextLen);
341 data->InsertChars(0, historyStr.c_str());
342 colorize(std::string_view{data->Buf, narrow<size_t>(data->BufTextLen)});
343 }
344 break;
345 }
346 }
347 return 0;
348}
349
350void ImGuiConsole::colorize(std::string_view line)
351{
352 TclParser parser = manager.getInterpreter().parse(line);
353 const auto& colors = parser.getColors();
354 assert(colors.size() == line.size());
355
356 coloredInputBuf.clear();
357 size_t pos = 0;
358 while (pos != colors.size()) {
359 char col = colors[pos];
360 size_t pos2 = pos++;
361 while ((pos != colors.size()) && (colors[pos] == col)) {
362 ++pos;
363 }
364 imColor color = [&] {
365 switch (col) {
366 using enum imColor;
367 case 'E': return ERROR;
368 case 'c': return COMMENT;
369 case 'v': return VARIABLE;
370 case 'l': return LITERAL;
371 case 'p': return PROC;
372 case 'o': return OPERATOR;
373 default: return TEXT; // other
374 }
375 }();
376 coloredInputBuf.addChunk(line.substr(pos2, pos - pos2), color);
377 }
378}
379
380void ImGuiConsole::putHistory(std::string command)
381{
382 if (command.empty()) return;
383 if (!history.empty() && (history.front() == command)) {
384 return;
385 }
386 if (history.full()) history.pop_back();
387 history.push_front(std::move(command));
388}
389
390void ImGuiConsole::saveHistory()
391{
392 try {
393 std::ofstream outputFile;
395 userFileContext("console").resolveCreate("history.txt"));
396 if (!outputFile) {
397 throw FileException("Error while saving the console history.");
398 }
399 for (const auto& s : view::reverse(history)) {
400 outputFile << s << '\n';
401 }
402 } catch (FileException& e) {
403 manager.getCliComm().printWarning(e.getMessage());
404 }
405}
406
407void ImGuiConsole::loadHistory()
408{
409 try {
410 std::ifstream inputFile(
411 userFileContext("console").resolveCreate("history.txt"));
412 std::string line;
413 while (inputFile) {
414 getline(inputFile, line);
415 putHistory(line);
416 }
417 } catch (FileException&) {
418 // Error while loading the console history, ignore
419 }
420}
421
422void ImGuiConsole::output(std::string_view text)
423{
424 print(text);
425}
426
427unsigned ImGuiConsole::getOutputColumns() const
428{
429 return columns;
430}
431
432void ImGuiConsole::update(const Setting& /*setting*/) noexcept
433{
434 show = consoleSetting.getBoolean();
435 if (!show) {
436 // Close the console via the 'console' setting. Typically this
437 // means via the F10 hotkey (or possibly by typing 'set console
438 // off' in the console).
439 //
440 // Give focus to the main openMSX window.
441 //
442 // This makes the following scenario work:
443 // * You were controlling the MSX, e.g. playing a game.
444 // * You press F10 to open the console.
445 // * You type a command (e.g. swap a game disk, for some people
446 // the console is still more convenient and/or faster than the
447 // new media menu).
448 // * You press F10 again to close the console
449 // * At this point the focus should go back to the main openMSX
450 // window (so that MSX input works).
451 SDL_SetWindowInputFocus(SDL_GetWindowFromID(WindowEvent::getMainWindowId()));
452 ImGui::SetWindowFocus(nullptr);
453 }
454}
455
456} // namespace openmsx
const std::string & getColors() const
Ouput: a string of equal length of the input command where each character indicates the type of the c...
Definition TclParser.hh:29
void push_front(T2 &&t)
size_t size() const
void printWarning(std::string_view message)
Definition CliComm.cc:12
static void setOutput(InterpreterOutput *output_)
Definition Completer.hh:72
This class represents a single text line in the console.
void addLine(const ConsoleLine &ln)
Append another line (possibly containing multiple chunks).
std::string_view chunkText(size_t i) const
Get the text for the i-th chunk.
imColor chunkColor(size_t i) const
Get the color for the i-th chunk.
void addChunk(std::string_view text, imColor color=imColor::TEXT)
Append a chunk with a (different) color.
void clear()
Reinitialize to an empty line.
size_t numChunks() const
Get the number of different chunks.
std::string tabCompletion(std::string_view command)
Complete the given command.
void paint(MSXMotherBoard *motherBoard) override
void save(ImGuiTextBuffer &buf) override
ImGuiConsole(ImGuiManager &manager)
void loadLine(std::string_view name, zstring_view value) override
Interpreter & getInterpreter()
void executeDelayed(std::function< void()> action)
ImGuiManager & manager
Definition ImGuiPart.hh:30
void setOutput(InterpreterOutput *output_)
TclParser parse(std::string_view command)
GlobalCommandController & getGlobalCommandController()
Definition Reactor.hh:91
void detach(Observer< T > &observer)
Definition Subject.hh:60
void attach(Observer< T > &observer)
Definition Subject.hh:54
zstring_view getString() const
Definition TclObject.cc:141
static std::string full()
Definition Version.cc:8
static uint32_t getMainWindowId()
Definition Event.hh:218
Like std::string_view, but with the extra guarantee that it refers to a zero-terminated string.
auto CalcTextSize(std::string_view str)
Definition ImGuiUtils.hh:39
void TextUnformatted(const std::string &str)
Definition ImGuiUtils.hh:26
constexpr double e
Definition Math.hh:21
vecN< 2, float > vec2
Definition gl_vec.hh:382
vecN< 4, float > vec4
Definition gl_vec.hh:384
void Window(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:63
void StyleVar(ImGuiStyleVar idx, float val, std::invocable<> auto next)
Definition ImGuiCpp.hh:190
void PopupContextWindow(const char *str_id, ImGuiPopupFlags popup_flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:438
void StyleColor(bool active, Args &&...args)
Definition ImGuiCpp.hh:175
void Child(const char *str_id, const ImVec2 &size, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:110
void ListClipper(size_t count, int forceIndex, float lineHeight, std::invocable< int > auto next)
Definition ImGuiCpp.hh:538
void openOfStream(std::ofstream &stream, zstring_view filename)
Open an ofstream in a platform-independent manner.
This file implemented 3 utility functions:
Definition Autofire.cc:11
bool loadOnePersistent(std::string_view name, zstring_view value, C &c, const std::tuple< Elements... > &tup)
void savePersistent(ImGuiTextBuffer &buf, C &c, const std::tuple< Elements... > &tup)
std::optional< bool > match(const BooleanInput &binding, const Event &event, function_ref< int(JoystickId)> getJoyDeadZone)
ImU32 getColor(imColor col)
const FileContext & userFileContext()
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)
auto distance(octet_iterator first, octet_iterator last)
Definition view.hh:15
constexpr auto reverse(Range &&range)
Definition view.hh:435
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)