TUI: review fixes.

List FTXUI in LICENSES.md. Keep -lua_stdout and startup Lua errors off the
terminal while -tui owns it. Print tables and functions through Lua's
tostring. Take over the terminal on the first update rather than in init,
so a startup failure leaves it alone. Split the layout and key handling
out of init.
This commit is contained in:
Rix
2026-10-03 15:44:00 -07:00
parent 1c3edc7dc1
commit 5afd098215
6 changed files with 89 additions and 60 deletions
+1
View File
@@ -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)
+4
View File
@@ -28,6 +28,10 @@ PCSX::Arguments::Arguments(const CommandLine::args& args) {
m_luaStdoutEnabled = true;
}
if (args.get<bool>("stdout") && !args.get<bool>("tui")) m_stdoutEnabled = true;
if (args.get<bool>("tui")) {
m_tuiEnabled = true;
m_luaStdoutEnabled = false;
}
if (args.get<bool>("no-ui") || args.get<bool>("cli")) m_stdoutEnabled = true;
if (args.get<bool>("testmode") || args.get<bool>("no-gui-log")) m_guiLogsEnabled = false;
if (args.get<bool>("testmode")) m_testModeEnabled = true;
+5 -1
View File
@@ -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;
+1
View File
@@ -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);
+70 -58
View File
@@ -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<std::mutex> 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<void()> applyArguments) {
applyArguments();
finishLoadSettings();
using namespace ftxui;
m_app = std::make_unique<App>(App::Fullscreen());
m_app = std::make_unique<ftxui::App>(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<PCSX::TerminalUI::Line>& lines, int height, int scroll) {
using namespace ftxui;
Elements out;
int end = static_cast<int>(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<ftxui::ComponentBase> 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<void()> applyArguments) {
};
auto input = Input(&m_input, "Lua", inputOption);
auto renderLines = [](const std::deque<Line>& lines, int height, int scroll) {
Elements out;
int end = static_cast<int>(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<std::mutex> 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<void()> 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<std::mutex> 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<int>(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<Loop>(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<std::mutex> 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<int>(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<ftxui::Loop>(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();
+8 -1
View File
@@ -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<Line>& 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<ftxui::ComponentBase> buildLayout();
bool handleEvent(const ftxui::Event& event);
std::mutex m_mutex;
std::deque<Line> 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<ftxui::App> m_app;
std::shared_ptr<ftxui::ComponentBase> m_layout;
std::unique_ptr<ftxui::Loop> m_loop;
};