diff --git a/LICENSES.md b/LICENSES.md index 93c3955e..c854e27e 100644 --- a/LICENSES.md +++ b/LICENSES.md @@ -28,6 +28,7 @@ find a link to the vendored version itself. - [flags](https://github.com/sailormoon/flags) ([vendored](https://github.com/grumpycoders/pcsx-redux/blob/main/third_party/flags.h)) - [fmt](https://github.com/fmtlib/fmt) - [FreeType](https://gitlab.freedesktop.org/freetype/freetype.git) + - [FTXUI](https://github.com/ArthurSonzogni/FTXUI) - [gl3w](https://github.com/skaslev/gl3w) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/gl3w/GL)) - [SDL](https://www.libsdl.org) - [googletest](https://github.com/google/googletest) diff --git a/src/core/arguments.cc b/src/core/arguments.cc index 34dfeaf2..8b074467 100644 --- a/src/core/arguments.cc +++ b/src/core/arguments.cc @@ -28,6 +28,10 @@ PCSX::Arguments::Arguments(const CommandLine::args& args) { m_luaStdoutEnabled = true; } if (args.get("stdout") && !args.get("tui")) m_stdoutEnabled = true; + if (args.get("tui")) { + m_tuiEnabled = true; + m_luaStdoutEnabled = false; + } if (args.get("no-ui") || args.get("cli")) m_stdoutEnabled = true; if (args.get("testmode") || args.get("no-gui-log")) m_guiLogsEnabled = false; if (args.get("testmode")) m_testModeEnabled = true; diff --git a/src/core/arguments.h b/src/core/arguments.h index 87e01b92..228d1b2a 100644 --- a/src/core/arguments.h +++ b/src/core/arguments.h @@ -41,9 +41,12 @@ class Arguments { bool isStdoutEnabled() const { return m_stdoutEnabled; } // Returns true if Lua should be displaying its console output to stdout. - // Enabled with the flags -lua_stdout, -no-ui, or -cli. + // Enabled with the flags -lua_stdout, -no-ui, or -cli, but never with -tui. bool isLuaStdoutEnabled() const { return m_luaStdoutEnabled; } + // Enabled with the flag -tui. The terminal belongs to the UI, so nothing may write to it directly. + bool isTuiEnabled() const { return m_tuiEnabled; } + // Returns true if the GUI logs window should be enabled. // Disabled with -testmode or -no-gui-log. bool isGUILogsEnabled() const { return m_guiLogsEnabled; } @@ -85,6 +88,7 @@ class Arguments { private: std::string m_portablePath = ""; bool m_luaStdoutEnabled = false; + bool m_tuiEnabled = false; bool m_stdoutEnabled = false; bool m_guiLogsEnabled = true; bool m_testModeEnabled = false; diff --git a/src/main/main.cc b/src/main/main.cc index a09769ff..fecb7d3f 100644 --- a/src/main/main.cc +++ b/src/main/main.cc @@ -94,6 +94,7 @@ class SystemImpl final : public PCSX::System { if (m_args.isGUILogsEnabled()) { s_ui->addLuaLog(s, error); } + if (m_args.isTuiEnabled()) return; if ((error && m_inStartup) || m_args.isLuaStdoutEnabled()) { if (error) { fputs(s.c_str(), stderr); diff --git a/src/main/terminalui.cc b/src/main/terminalui.cc index b4d5be2f..a70bbf30 100644 --- a/src/main/terminalui.cc +++ b/src/main/terminalui.cc @@ -70,7 +70,7 @@ bool PCSX::TerminalUI::addLog(LogClass logClass, const std::string& msg) { appendLines(m_log, m_logPending, msg, false); while (m_log.size() > c_maxLogLines) m_log.pop_front(); } - if (m_app) m_app->RequestAnimationFrame(); + if (m_loop) m_app->RequestAnimationFrame(); return true; } @@ -81,7 +81,7 @@ void PCSX::TerminalUI::addLuaLog(const std::string& msg, bool error) { appendLines(m_lua, pending, msg + "\n", error); while (m_lua.size() > c_maxLuaLines) m_lua.pop_front(); } - if (m_app) m_app->RequestAnimationFrame(); + if (m_loop) m_app->RequestAnimationFrame(); } void PCSX::TerminalUI::addNotification(const std::string& notification) { @@ -89,7 +89,7 @@ void PCSX::TerminalUI::addNotification(const std::string& notification) { std::lock_guard lock(m_mutex); m_notification = notification; } - if (m_app) m_app->RequestAnimationFrame(); + if (m_loop) m_app->RequestAnimationFrame(); } void PCSX::TerminalUI::execute(const std::string& cmd) { @@ -109,21 +109,31 @@ void PCSX::TerminalUI::execute(const std::string& cmd) { L.load(cmd, "console:", false); } int n = L.pcall(); - for (int i = n; i > 0; i--) { - std::string value; - if (L.type(-i) == LUA_TNUMBER) { - value = fmt::format("{}", L.tonumber(-i)); - } else { - value = L.tostring(-i); - } - addLuaLog(value, false); - } + int first = L.gettop() - n + 1; + for (int i = first; i < first + n; i++) addLuaLog(formatResult(i), false); } catch (std::exception& e) { addLuaLog(e.what(), true); } if (L.gettop() > top) L.pop(L.gettop() - top); } +std::string PCSX::TerminalUI::formatResult(int index) { + auto L = *g_emulator->m_lua; + if (L.type(index) == LUA_TNUMBER) return fmt::format("{}", L.tonumber(index)); + // Tables, functions and userdata have no string form of their own; ask Lua's tostring. + lua_State* s = L.getState(); + lua_getglobal(s, "tostring"); + lua_pushvalue(s, index); + std::string value = "(unprintable)"; + if (lua_pcall(s, 1, 1, 0) == 0 && lua_isstring(s, -1)) { + size_t len; + const char* str = lua_tolstring(s, -1, &len); + value.assign(str, len); + } + lua_pop(s, 1); + return value; +} + void PCSX::TerminalUI::historyMove(int delta) { if (m_history.empty()) return; if (delta < 0 && m_historyPos > 0) m_historyPos--; @@ -136,11 +146,30 @@ void PCSX::TerminalUI::init(std::function applyArguments) { applyArguments(); finishLoadSettings(); - using namespace ftxui; - m_app = std::make_unique(App::Fullscreen()); + m_app = std::make_unique(ftxui::App::Fullscreen()); m_app->TrackMouse(false); m_app->HandlePipedInput(false); + // The terminal is only taken over on the first update(), so a failure during the rest of + // startup leaves it untouched. + m_layout = buildLayout(); +} +namespace { +ftxui::Elements renderLines(const std::deque& lines, int height, int scroll) { + using namespace ftxui; + Elements out; + int end = static_cast(lines.size()) - scroll; + int start = std::max(0, end - height); + for (int i = start; i < end; i++) { + auto t = text(lines[i].text); + out.push_back(lines[i].error ? t | color(Color::Red) : t); + } + return out; +} +} // namespace + +std::shared_ptr PCSX::TerminalUI::buildLayout() { + using namespace ftxui; InputOption inputOption; inputOption.multiline = false; inputOption.on_enter = [this]() { @@ -150,25 +179,12 @@ void PCSX::TerminalUI::init(std::function applyArguments) { }; auto input = Input(&m_input, "Lua", inputOption); - auto renderLines = [](const std::deque& lines, int height, int scroll) { - Elements out; - int end = static_cast(lines.size()) - scroll; - int start = std::max(0, end - height); - for (int i = start; i < end; i++) { - auto t = text(lines[i].text); - out.push_back(lines[i].error ? t | color(Color::Red) : t); - } - return out; - }; - - auto layout = Renderer(input, [this, input, renderLines]() { + auto layout = Renderer(input, [this, input]() { std::lock_guard lock(m_mutex); - auto dim = Terminal::Size(); // Log gets everything left after the Lua pane, the separator, the input line and the status line. - int logHeight = std::max(1, dim.dimy - c_luaPaneHeight - 3); - bool running = g_system->running(); + int logHeight = std::max(1, Terminal::Size().dimy - c_luaPaneHeight - 3); auto status = hbox({ - text(running ? " RUNNING " : " PAUSED ") | inverted, + text(g_system->running() ? " RUNNING " : " PAUSED ") | inverted, text(fmt::format(" pc={:08x} ", g_emulator->m_cpu->m_regs.pc)), text(m_logScroll ? fmt::format(" scrolled -{} ", m_logScroll) : ""), filler(), @@ -183,35 +199,29 @@ void PCSX::TerminalUI::init(std::function applyArguments) { status, }); }); + return CatchEvent(layout, [this](Event event) { return handleEvent(event); }); +} - layout = CatchEvent(layout, [this](Event event) { - if (event == Event::ArrowUp) { - historyMove(-1); - return true; - } - if (event == Event::ArrowDown) { - historyMove(1); - return true; - } - if (event == Event::PageUp || event == Event::PageDown) { - std::lock_guard lock(m_mutex); - int page = std::max(1, Terminal::Size().dimy / 2); - m_logScroll += event == Event::PageUp ? page : -page; - m_logScroll = std::clamp(m_logScroll, 0, std::max(0, static_cast(m_log.size()) - 1)); - return true; - } - if (event == Event::F5) { - if (g_system->running()) { - g_system->pause(); - } else { - g_system->resume(); - } - return true; - } - return false; - }); - - m_loop = std::make_unique(m_app.get(), layout); +bool PCSX::TerminalUI::handleEvent(const ftxui::Event& event) { + using ftxui::Event; + if (event == Event::ArrowUp || event == Event::ArrowDown) { + historyMove(event == Event::ArrowUp ? -1 : 1); + return true; + } + if (event == Event::PageUp || event == Event::PageDown) { + std::lock_guard lock(m_mutex); + int page = std::max(1, ftxui::Terminal::Size().dimy / 2); + m_logScroll += event == Event::PageUp ? page : -page; + m_logScroll = std::clamp(m_logScroll, 0, std::max(0, static_cast(m_log.size()) - 1)); + return true; + } + if (event != Event::F5) return false; + if (g_system->running()) { + g_system->pause(); + } else { + g_system->resume(); + } + return true; } void PCSX::TerminalUI::setLua(Lua L) { setLuaCommon(L); } @@ -219,11 +229,13 @@ void PCSX::TerminalUI::setLua(Lua L) { setLuaCommon(L); } void PCSX::TerminalUI::close() { // The loop owns the terminal state; destroying it restores the screen. m_loop.reset(); + m_layout.reset(); m_app.reset(); } void PCSX::TerminalUI::update(bool vsync) { tick(); + if (!m_loop && m_layout) m_loop = std::make_unique(m_app.get(), m_layout); if (m_loop) { // Nothing else asks for a redraw while the CPU runs, so the status line would sit still. auto now = std::chrono::steady_clock::now(); diff --git a/src/main/terminalui.h b/src/main/terminalui.h index 8f19c1b1..778a461d 100644 --- a/src/main/terminalui.h +++ b/src/main/terminalui.h @@ -30,7 +30,9 @@ namespace ftxui { class App; +class ComponentBase; class Loop; +struct Event; } // namespace ftxui namespace PCSX { @@ -50,14 +52,18 @@ class TerminalUI : public UI { void update(bool vsync = false) override; void addNotification(const std::string& notification) override; - private: struct Line { std::string text; bool error = false; }; + + private: void appendLines(std::deque& dest, std::string& pending, const std::string& msg, bool error); void execute(const std::string& cmd); void historyMove(int delta); + std::string formatResult(int index); + std::shared_ptr buildLayout(); + bool handleEvent(const ftxui::Event& event); std::mutex m_mutex; std::deque m_log; @@ -70,6 +76,7 @@ class TerminalUI : public UI { std::string m_notification; std::chrono::steady_clock::time_point m_lastRedraw; std::unique_ptr m_app; + std::shared_ptr m_layout; std::unique_ptr m_loop; };