Show system_info.txt, console.txt, json file, userreport_hw.txt paths in the terminal...
[0ad.git] / source / ps / CConsole.cpp
blob966de9a90ba9a6705db294ebb79d822b6a0e6f75
1 /* Copyright (C) 2022 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include "CConsole.h"
22 #include "graphics/Canvas2D.h"
23 #include "graphics/FontMetrics.h"
24 #include "graphics/TextRenderer.h"
25 #include "gui/CGUI.h"
26 #include "gui/GUIManager.h"
27 #include "lib/code_generation.h"
28 #include "lib/ogl.h"
29 #include "lib/timer.h"
30 #include "lib/utf8.h"
31 #include "maths/MathUtil.h"
32 #include "ps/CLogger.h"
33 #include "ps/ConfigDB.h"
34 #include "ps/CStrInternStatic.h"
35 #include "ps/Filesystem.h"
36 #include "ps/GameSetup/Config.h"
37 #include "ps/Globals.h"
38 #include "ps/Hotkey.h"
39 #include "ps/Profile.h"
40 #include "ps/Pyrogenesis.h"
41 #include "ps/VideoMode.h"
42 #include "scriptinterface/ScriptInterface.h"
43 #include "scriptinterface/JSON.h"
45 #include <vector>
46 #include <wctype.h>
48 namespace
51 // For text being typed into the console.
52 constexpr int CONSOLE_BUFFER_SIZE = 1024;
54 const char* CONSOLE_FONT = "mono-10";
56 } // anonymous namespace
58 CConsole* g_Console = 0;
60 CConsole::CConsole()
62 m_Toggle = false;
63 m_Visible = false;
65 m_VisibleFrac = 0.0f;
67 m_Buffer = std::make_unique<wchar_t[]>(CONSOLE_BUFFER_SIZE);
68 FlushBuffer();
70 m_MsgHistPos = 1;
71 m_CharsPerPage = 0;
73 m_PrevTime = 0.0;
74 m_CursorVisState = true;
75 m_CursorBlinkRate = 0.5;
77 m_QuitHotkeyWasShown = false;
79 InsertMessage("[ 0 A.D. Console v0.15 ]");
80 InsertMessage("");
83 CConsole::~CConsole() = default;
85 void CConsole::Init()
87 // Initialise console history file
88 m_MaxHistoryLines = 200;
89 CFG_GET_VAL("console.history.size", m_MaxHistoryLines);
91 m_HistoryFile = L"config/console.txt";
92 LoadHistory();
94 UpdateScreenSize(g_xres, g_yres);
96 // Calculate and store the line spacing
97 const CFontMetrics font{CStrIntern(CONSOLE_FONT)};
98 m_FontHeight = font.GetLineSpacing();
99 m_FontWidth = font.GetCharacterWidth(L'C');
100 m_CharsPerPage = static_cast<size_t>(g_xres / m_FontWidth);
101 // Offset by an arbitrary amount, to make it fit more nicely
102 m_FontOffset = 7;
104 m_CursorBlinkRate = 0.5;
105 CFG_GET_VAL("gui.cursorblinkrate", m_CursorBlinkRate);
108 void CConsole::UpdateScreenSize(int w, int h)
110 m_X = 0;
111 m_Y = 0;
112 float height = h * 0.6f;
113 m_Width = w / g_VideoMode.GetScale();
114 m_Height = height / g_VideoMode.GetScale();
117 void CConsole::ShowQuitHotkeys()
119 if (m_QuitHotkeyWasShown)
120 return;
122 std::string str;
123 for (const std::pair<const SDL_Scancode_, KeyMapping>& key : g_HotkeyMap)
124 if (key.second.front().name == "console.toggle")
125 str += (str.empty() ? "Press " : " / ") + FindScancodeName(static_cast<SDL_Scancode>(key.first));
127 if (!str.empty())
128 InsertMessage(str + " to quit.");
130 m_QuitHotkeyWasShown = true;
133 void CConsole::ToggleVisible()
135 m_Toggle = true;
136 m_Visible = !m_Visible;
138 // TODO: this should be based on input focus, not visibility
139 if (m_Visible)
141 ShowQuitHotkeys();
142 SDL_StartTextInput();
143 return;
145 SDL_StopTextInput();
148 void CConsole::SetVisible(bool visible)
150 if (visible != m_Visible)
151 m_Toggle = true;
152 m_Visible = visible;
153 if (visible)
155 m_PrevTime = 0.0;
156 m_CursorVisState = false;
160 void CConsole::FlushBuffer()
162 // Clear the buffer and set the cursor and length to 0
163 memset(m_Buffer.get(), '\0', sizeof(wchar_t) * CONSOLE_BUFFER_SIZE);
164 m_BufferPos = m_BufferLength = 0;
167 void CConsole::Update(const float deltaRealTime)
169 if (m_Toggle)
171 const float AnimateTime = .30f;
172 const float Delta = deltaRealTime / AnimateTime;
173 if (m_Visible)
175 m_VisibleFrac += Delta;
176 if (m_VisibleFrac > 1.0f)
178 m_VisibleFrac = 1.0f;
179 m_Toggle = false;
182 else
184 m_VisibleFrac -= Delta;
185 if (m_VisibleFrac < 0.0f)
187 m_VisibleFrac = 0.0f;
188 m_Toggle = false;
194 void CConsole::Render(CCanvas2D& canvas)
196 if (!(m_Visible || m_Toggle))
197 return;
199 PROFILE3_GPU("console");
201 DrawWindow(canvas);
203 CTextRenderer textRenderer;
204 textRenderer.SetCurrentFont(CStrIntern(CONSOLE_FONT));
205 // Animation: slide in from top of screen.
206 const float deltaY = (1.0f - m_VisibleFrac) * m_Height;
207 textRenderer.Translate(m_X, m_Y - deltaY);
209 DrawHistory(textRenderer);
210 DrawBuffer(textRenderer);
212 canvas.DrawText(textRenderer);
215 void CConsole::DrawWindow(CCanvas2D& canvas)
217 std::vector<CVector2D> points =
219 CVector2D{m_Width, 0.0f},
220 CVector2D{1.0f, 0.0f},
221 CVector2D{1.0f, m_Height - 1.0f},
222 CVector2D{m_Width, m_Height - 1.0f},
223 CVector2D{m_Width, 0.0f}
225 for (CVector2D& point : points)
226 point += CVector2D{m_X, m_Y - (1.0f - m_VisibleFrac) * m_Height};
228 canvas.DrawRect(CRect(points[1], points[3]), CColor(0.0f, 0.0f, 0.5f, 0.6f));
229 canvas.DrawLine(points, 1.0f, CColor(0.5f, 0.5f, 0.0f, 0.6f));
231 if (m_Height > m_FontHeight + 4)
233 points = {
234 CVector2D{0.0f, m_Height - static_cast<float>(m_FontHeight) - 4.0f},
235 CVector2D{m_Width, m_Height - static_cast<float>(m_FontHeight) - 4.0f}
237 for (CVector2D& point : points)
238 point += CVector2D{m_X, m_Y - (1.0f - m_VisibleFrac) * m_Height};
239 canvas.DrawLine(points, 1.0f, CColor(0.5f, 0.5f, 0.0f, 0.6f));
243 void CConsole::DrawHistory(CTextRenderer& textRenderer)
245 int i = 1;
247 std::deque<std::wstring>::iterator it; //History iterator
249 std::lock_guard<std::mutex> lock(m_Mutex); // needed for safe access to m_deqMsgHistory
251 textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
253 for (it = m_MsgHistory.begin();
254 it != m_MsgHistory.end()
255 && (((i - m_MsgHistPos + 1) * m_FontHeight) < m_Height);
256 ++it)
258 if (i >= m_MsgHistPos)
260 textRenderer.Put(
261 9.0f,
262 m_Height - static_cast<float>(m_FontOffset) - static_cast<float>(m_FontHeight) * (i - m_MsgHistPos + 1),
263 it->c_str());
266 i++;
270 // Renders the buffer to the screen.
271 void CConsole::DrawBuffer(CTextRenderer& textRenderer)
273 if (m_Height < m_FontHeight)
274 return;
276 const CVector2D savedTranslate = textRenderer.GetTranslate();
278 textRenderer.Translate(2.0f, m_Height - static_cast<float>(m_FontOffset) + 1.0f);
280 textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 0.0f, 1.0f));
281 textRenderer.PutAdvance(L"]");
283 textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
285 if (m_BufferPos == 0)
286 DrawCursor(textRenderer);
288 for (int i = 0; i < m_BufferLength; ++i)
290 textRenderer.PrintfAdvance(L"%lc", m_Buffer[i]);
291 if (m_BufferPos - 1 == i)
292 DrawCursor(textRenderer);
295 textRenderer.ResetTranslate(savedTranslate);
298 void CConsole::DrawCursor(CTextRenderer& textRenderer)
300 if (m_CursorBlinkRate > 0.0)
302 // check if the cursor visibility state needs to be changed
303 double currTime = timer_Time();
304 if ((currTime - m_PrevTime) >= m_CursorBlinkRate)
306 m_CursorVisState = !m_CursorVisState;
307 m_PrevTime = currTime;
310 else
312 // Should always be visible
313 m_CursorVisState = true;
316 if(m_CursorVisState)
318 // Slightly translucent yellow
319 textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 0.0f, 0.8f));
321 // Cursor character is chosen to be an underscore
322 textRenderer.Put(0.0f, 0.0f, L"_");
324 // Revert to the standard text color
325 textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
329 bool CConsole::IsEOB() const
331 return m_BufferPos == m_BufferLength;
334 bool CConsole::IsBOB() const
336 return m_BufferPos == 0;
339 bool CConsole::IsFull() const
341 return m_BufferLength == CONSOLE_BUFFER_SIZE;
344 bool CConsole::IsEmpty() const
346 return m_BufferLength == 0;
349 //Inserts a character into the buffer.
350 void CConsole::InsertChar(const int szChar, const wchar_t cooked)
352 static int historyPos = -1;
354 if (!m_Visible) return;
356 switch (szChar)
358 case SDLK_RETURN:
359 historyPos = -1;
360 m_MsgHistPos = 1;
361 ProcessBuffer(m_Buffer.get());
362 FlushBuffer();
363 return;
365 case SDLK_TAB:
366 // Auto Complete
367 return;
369 case SDLK_BACKSPACE:
370 if (IsEmpty() || IsBOB()) return;
372 if (m_BufferPos == m_BufferLength)
373 m_Buffer[m_BufferPos - 1] = '\0';
374 else
376 for (int j = m_BufferPos-1; j < m_BufferLength - 1; ++j)
377 m_Buffer[j] = m_Buffer[j + 1]; // move chars to left
378 m_Buffer[m_BufferLength-1] = '\0';
381 m_BufferPos--;
382 m_BufferLength--;
383 return;
385 case SDLK_DELETE:
386 if (IsEmpty() || IsEOB())
387 return;
389 if (m_BufferPos == m_BufferLength - 1)
391 m_Buffer[m_BufferPos] = '\0';
392 m_BufferLength--;
394 else
396 if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL])
398 // Make Ctrl-Delete delete up to end of line
399 m_Buffer[m_BufferPos] = '\0';
400 m_BufferLength = m_BufferPos;
402 else
404 // Delete just one char and move the others left
405 for(int j = m_BufferPos; j < m_BufferLength - 1; ++j)
406 m_Buffer[j] = m_Buffer[j + 1];
407 m_Buffer[m_BufferLength - 1] = '\0';
408 m_BufferLength--;
412 return;
414 case SDLK_HOME:
415 if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL])
417 std::lock_guard<std::mutex> lock(m_Mutex); // needed for safe access to m_deqMsgHistory
419 const int linesShown = static_cast<int>(m_Height / m_FontHeight) - 4;
420 m_MsgHistPos = Clamp(static_cast<int>(m_MsgHistory.size()) - linesShown, 1, static_cast<int>(m_MsgHistory.size()));
422 else
424 m_BufferPos = 0;
426 return;
428 case SDLK_END:
429 if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL])
431 m_MsgHistPos = 1;
433 else
435 m_BufferPos = m_BufferLength;
437 return;
439 case SDLK_LEFT:
440 if (m_BufferPos)
441 m_BufferPos--;
442 return;
444 case SDLK_RIGHT:
445 if (m_BufferPos != m_BufferLength)
446 m_BufferPos++;
447 return;
449 // BEGIN: Buffer History Lookup
450 case SDLK_UP:
451 if (m_BufHistory.size() && historyPos != static_cast<int>(m_BufHistory.size()) - 1)
453 historyPos++;
454 SetBuffer(m_BufHistory.at(historyPos).c_str());
455 m_BufferPos = m_BufferLength;
457 return;
459 case SDLK_DOWN:
460 if (m_BufHistory.size())
462 if (historyPos > 0)
464 historyPos--;
465 SetBuffer(m_BufHistory.at(historyPos).c_str());
466 m_BufferPos = m_BufferLength;
468 else if (historyPos == 0)
470 historyPos--;
471 FlushBuffer();
474 return;
475 // END: Buffer History Lookup
477 // BEGIN: Message History Lookup
478 case SDLK_PAGEUP:
480 std::lock_guard<std::mutex> lock(m_Mutex); // needed for safe access to m_deqMsgHistory
482 if (m_MsgHistPos != static_cast<int>(m_MsgHistory.size()))
483 m_MsgHistPos++;
484 return;
487 case SDLK_PAGEDOWN:
488 if (m_MsgHistPos != 1)
489 m_MsgHistPos--;
490 return;
491 // END: Message History Lookup
493 default: //Insert a character
494 if (IsFull() || cooked == 0)
495 return;
497 if (IsEOB()) //are we at the end of the buffer?
498 m_Buffer[m_BufferPos] = cooked; //cat char onto end
499 else
500 { //we need to insert
501 int i;
502 for (i = m_BufferLength; i > m_BufferPos; --i)
503 m_Buffer[i] = m_Buffer[i - 1]; // move chars to right
504 m_Buffer[i] = cooked;
507 m_BufferPos++;
508 m_BufferLength++;
510 return;
515 void CConsole::InsertMessage(const std::string& message)
517 // (TODO: this text-wrapping is rubbish since we now use variable-width fonts)
519 //Insert newlines to wraparound text where needed
520 std::wstring wrapAround = wstring_from_utf8(message.c_str());
521 std::wstring newline(L"\n");
522 size_t oldNewline=0;
523 size_t distance;
525 //make sure everything has been initialized
526 if (m_CharsPerPage != 0)
528 while (oldNewline + m_CharsPerPage < wrapAround.length())
530 distance = wrapAround.find(newline, oldNewline) - oldNewline;
531 if (distance > m_CharsPerPage)
533 oldNewline += m_CharsPerPage;
534 wrapAround.insert(oldNewline++, newline);
536 else
537 oldNewline += distance+1;
540 // Split into lines and add each one individually
541 oldNewline = 0;
544 std::lock_guard<std::mutex> lock(m_Mutex); // needed for safe access to m_deqMsgHistory
546 while ( (distance = wrapAround.find(newline, oldNewline)) != wrapAround.npos)
548 distance -= oldNewline;
549 m_MsgHistory.push_front(wrapAround.substr(oldNewline, distance));
550 oldNewline += distance+1;
552 m_MsgHistory.push_front(wrapAround.substr(oldNewline));
556 const wchar_t* CConsole::GetBuffer()
558 m_Buffer[m_BufferLength] = 0;
559 return m_Buffer.get();
562 void CConsole::SetBuffer(const wchar_t* szMessage)
564 int oldBufferPos = m_BufferPos; // remember since FlushBuffer will set it to 0
566 FlushBuffer();
568 wcsncpy(m_Buffer.get(), szMessage, CONSOLE_BUFFER_SIZE);
569 m_Buffer[CONSOLE_BUFFER_SIZE-1] = 0;
570 m_BufferLength = static_cast<int>(wcslen(m_Buffer.get()));
571 m_BufferPos = std::min(oldBufferPos, m_BufferLength);
574 void CConsole::ProcessBuffer(const wchar_t* szLine)
576 if (!szLine || wcslen(szLine) <= 0)
577 return;
579 ENSURE(wcslen(szLine) < CONSOLE_BUFFER_SIZE);
581 m_BufHistory.push_front(szLine);
582 SaveHistory(); // Do this each line for the moment; if a script causes
583 // a crash it's a useful record.
585 // Process it as JavaScript
586 std::shared_ptr<ScriptInterface> pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface();
587 ScriptRequest rq(*pScriptInterface);
589 JS::RootedValue rval(rq.cx);
590 pScriptInterface->Eval(CStrW(szLine).ToUTF8().c_str(), &rval);
591 if (!rval.isUndefined())
592 InsertMessage(Script::ToString(rq, &rval));
595 void CConsole::LoadHistory()
597 // note: we don't care if this file doesn't exist or can't be read;
598 // just don't load anything in that case.
600 // do this before LoadFile to avoid an error message if file not found.
601 if (!VfsFileExists(m_HistoryFile))
602 return;
604 std::shared_ptr<u8> buf; size_t buflen;
605 if (g_VFS->LoadFile(m_HistoryFile, buf, buflen) < 0)
606 return;
608 CStr bytes ((char*)buf.get(), buflen);
610 CStrW str (bytes.FromUTF8());
611 size_t pos = 0;
612 while (pos != CStrW::npos)
614 pos = str.find('\n');
615 if (pos != CStrW::npos)
617 if (pos > 0)
618 m_BufHistory.push_front(str.Left(str[pos-1] == '\r' ? pos - 1 : pos));
619 str = str.substr(pos + 1);
621 else if (str.length() > 0)
622 m_BufHistory.push_front(str);
626 void CConsole::SaveHistory()
628 WriteBuffer buffer;
629 const int linesToSkip = static_cast<int>(m_BufHistory.size()) - m_MaxHistoryLines;
630 std::deque<std::wstring>::reverse_iterator it = m_BufHistory.rbegin();
631 if(linesToSkip > 0)
632 std::advance(it, linesToSkip);
633 for (; it != m_BufHistory.rend(); ++it)
635 CStr8 line = CStrW(*it).ToUTF8();
636 buffer.Append(line.data(), line.length());
637 static const char newline = '\n';
638 buffer.Append(&newline, 1);
641 if (g_VFS->CreateFile(m_HistoryFile, buffer.Data(), buffer.Size()) == INFO::OK)
642 ONCE(debug_printf("FILES| Console command history written to %s\n", m_HistoryFile.string8().c_str()));
643 else
644 debug_printf("FILES| Failed to write console command history to %s\n", m_HistoryFile.string8().c_str());
647 static bool isUnprintableChar(SDL_Keysym key)
649 switch (key.sym)
651 // We want to allow some, which are handled specially
652 case SDLK_RETURN: case SDLK_TAB:
653 case SDLK_BACKSPACE: case SDLK_DELETE:
654 case SDLK_HOME: case SDLK_END:
655 case SDLK_LEFT: case SDLK_RIGHT:
656 case SDLK_UP: case SDLK_DOWN:
657 case SDLK_PAGEUP: case SDLK_PAGEDOWN:
658 return true;
660 // Ignore the others
661 default:
662 return false;
666 InReaction conInputHandler(const SDL_Event_* ev)
668 if (!g_Console)
669 return IN_PASS;
671 if (static_cast<int>(ev->ev.type) == SDL_HOTKEYPRESS)
673 std::string hotkey = static_cast<const char*>(ev->ev.user.data1);
675 if (hotkey == "console.toggle")
677 ResetActiveHotkeys();
678 g_Console->ToggleVisible();
679 return IN_HANDLED;
681 else if (g_Console->IsActive() && hotkey == "copy")
683 std::string text = utf8_from_wstring(g_Console->GetBuffer());
684 SDL_SetClipboardText(text.c_str());
685 return IN_HANDLED;
687 else if (g_Console->IsActive() && hotkey == "paste")
689 char* utf8_text = SDL_GetClipboardText();
690 if (!utf8_text)
691 return IN_HANDLED;
693 std::wstring text = wstring_from_utf8(utf8_text);
694 SDL_free(utf8_text);
696 for (wchar_t c : text)
697 g_Console->InsertChar(0, c);
699 return IN_HANDLED;
703 if (!g_Console->IsActive())
704 return IN_PASS;
706 // In SDL2, we no longer get Unicode wchars via SDL_Keysym
707 // we use text input events instead and they provide UTF-8 chars
708 if (ev->ev.type == SDL_TEXTINPUT)
710 // TODO: this could be more efficient with an interface to insert UTF-8 strings directly
711 std::wstring wstr = wstring_from_utf8(ev->ev.text.text);
712 for (size_t i = 0; i < wstr.length(); ++i)
713 g_Console->InsertChar(0, wstr[i]);
714 return IN_HANDLED;
716 // TODO: text editing events for IME support
718 if (ev->ev.type != SDL_KEYDOWN && ev->ev.type != SDL_KEYUP)
719 return IN_PASS;
721 int sym = ev->ev.key.keysym.sym;
723 // Stop unprintable characters (ctrl+, alt+ and escape).
724 if (ev->ev.type == SDL_KEYDOWN && isUnprintableChar(ev->ev.key.keysym) &&
725 !HotkeyIsPressed("console.toggle"))
727 g_Console->InsertChar(sym, 0);
728 return IN_HANDLED;
731 // We have a probably printable key - we should return HANDLED so it can't trigger hotkeys.
732 // However, if Ctrl/Meta modifiers are active (or it's escape), just pass it through instead,
733 // assuming that we are indeed trying to trigger hotkeys (e.g. copy/paste).
734 // Also ignore the key if we are trying to toggle the console off.
735 // See also similar logic in CInput.cpp
736 if (EventWillFireHotkey(ev, "console.toggle") ||
737 g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL] ||
738 g_scancodes[SDL_SCANCODE_LGUI] || g_scancodes[SDL_SCANCODE_RGUI])
739 return IN_PASS;
741 return IN_HANDLED;