mirror of
https://github.com/grumpycoders/pcsx-redux.git
synced 2026-10-11 22:39:54 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user