From 6f6bacdb16e1c1ce6e2cced1abb0f5590967264a Mon Sep 17 00:00:00 2001 From: Timothy Werquin Date: Thu, 19 Oct 2023 21:43:25 +0200 Subject: [PATCH] Yaft2: Unit tests --- apps/yaft2/CMakeLists.txt | 25 +-- apps/yaft2/YaftWidget.cpp | 158 +++++++++++++++++ apps/yaft2/YaftWidget.h | 75 ++++++++ apps/yaft2/config.cpp | 46 ++++- apps/yaft2/config.h | 15 ++ apps/yaft2/keyboard.cpp | 42 ++--- apps/yaft2/keyboard.h | 18 +- apps/yaft2/main.cpp | 210 +---------------------- libs/rMlib/include/UI/Util.h | 2 +- test/CMakeLists.txt | 14 +- test/unit/CMakeLists.txt | 6 +- test/unit/TestYaft.cpp | 208 ++++++++++++++++++++++ test/unit/assets/yaft-aA.png | 3 + test/unit/assets/yaft-hidden.png | 3 + test/unit/assets/yaft-init.png | 3 + test/unit/assets/yaft-keyboard-down.png | 3 + test/unit/assets/yaft-keyboard-down2.png | 3 + test/unit/assets/yaft-keyboard-held.png | 3 + test/unit/assets/yaft-keyboard-stuck.png | 3 + test/unit/assets/yaft-keyboard.png | 3 + test/unit/assets/yaft-landscape.png | 3 + 21 files changed, 585 insertions(+), 261 deletions(-) create mode 100644 apps/yaft2/YaftWidget.cpp create mode 100644 apps/yaft2/YaftWidget.h create mode 100644 test/unit/TestYaft.cpp create mode 100644 test/unit/assets/yaft-aA.png create mode 100644 test/unit/assets/yaft-hidden.png create mode 100644 test/unit/assets/yaft-init.png create mode 100644 test/unit/assets/yaft-keyboard-down.png create mode 100644 test/unit/assets/yaft-keyboard-down2.png create mode 100644 test/unit/assets/yaft-keyboard-held.png create mode 100644 test/unit/assets/yaft-keyboard-stuck.png create mode 100644 test/unit/assets/yaft-keyboard.png create mode 100644 test/unit/assets/yaft-landscape.png diff --git a/apps/yaft2/CMakeLists.txt b/apps/yaft2/CMakeLists.txt index 9d0bdba..fa6f624 100644 --- a/apps/yaft2/CMakeLists.txt +++ b/apps/yaft2/CMakeLists.txt @@ -1,26 +1,27 @@ project(yaft2) -add_executable(${PROJECT_NAME} - main.cpp - keyboard.cpp - screen.cpp - layout.cpp - keymap.cpp - config.cpp) +add_library(yaft_app_lib OBJECT + YaftWidget.cpp keyboard.cpp screen.cpp layout.cpp keymap.cpp config.cpp) +add_library(Yaft::app_lib ALIAS yaft_app_lib) -target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) +target_include_directories(yaft_app_lib PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(${PROJECT_NAME} - PRIVATE +target_link_libraries(yaft_app_lib PUBLIC rMlib libYaft util tomlplusplus::tomlplusplus) if (APPLE) - target_link_libraries(${PROJECT_NAME} PRIVATE linux::mxcfb) + target_link_libraries(yaft_app_lib PUBLIC linux::mxcfb) endif() -install(TARGETS ${PROJECT_NAME} DESTINATION opt/bin) +add_executable(${PROJECT_NAME} main.cpp) +target_link_libraries(${PROJECT_NAME} + PRIVATE + yaft_app_lib + rMlib) + +install(TARGETS ${PROJECT_NAME} DESTINATION opt/bin) install(FILES draft/yaft2.draft DESTINATION opt/etc/draft) diff --git a/apps/yaft2/YaftWidget.cpp b/apps/yaft2/YaftWidget.cpp new file mode 100644 index 0000000..25e2209 --- /dev/null +++ b/apps/yaft2/YaftWidget.cpp @@ -0,0 +1,158 @@ +#include "YaftWidget.h" + +// libYaft +#include "conf.h" +#include "parse.h" +#include "terminal.h" +#include "yaft.h" + +#include "util.h" + +#include + +#include + +using namespace rmlib; + +namespace { +const char* term_name = "yaft-256color"; + +AppContext* globalCtx = nullptr; + +void +sig_handler(int signo) { + if (signo == SIGCHLD) { + if (globalCtx != nullptr) { + globalCtx->stop(); + } + wait(NULL); + } +} + +void +initSignalHandler(AppContext& ctx) { + globalCtx = &ctx; + + struct sigaction sigact; + memset(&sigact, 0, sizeof(struct sigaction)); + sigact.sa_handler = sig_handler; + sigact.sa_flags = SA_RESTART; + sigaction(SIGCHLD, &sigact, NULL); +} + +bool +fork_and_exec(int* master, + const char* cmd, + char* const argv[], + int lines, + int cols) { + pid_t pid; + struct winsize ws; + ws.ws_row = lines; + ws.ws_col = cols; + /* XXX: this variables are UNUSED (man tty_ioctl), + but useful for calculating terminal cell size */ + ws.ws_ypixel = CELL_HEIGHT * lines; + ws.ws_xpixel = CELL_WIDTH * cols; + + pid = eforkpty(master, NULL, NULL, &ws); + if (pid < 0) { + return false; + } else if (pid == 0) { /* child */ + setenv("TERM", term_name, 1); + execvp(cmd, argv); + /* never reach here */ + exit(EXIT_FAILURE); + } + return true; +} +} // namespace + +YaftState::~YaftState() { + if (term) { + term_die(term.get()); + } +} + +void +YaftState::logTerm(std::string_view str) { + parse(term.get(), reinterpret_cast(str.data()), str.size()); +} + +void +YaftState::init(rmlib::AppContext& ctx, const rmlib::BuildContext&) { + term = std::make_unique(); + + // term_init needs the maximum size of the terminal. + int maxSize = std::max(ctx.getFbCanvas().width(), ctx.getFbCanvas().height()); + if (!term_init(term.get(), maxSize, maxSize)) { + std::cout << "Error init term\n"; + ctx.stop(); + return; + } + + if (const auto& err = getWidget().configError; err.has_value()) { + logTerm(err->msg); + } + + initSignalHandler(ctx); + + if (!fork_and_exec(&term->fd, + getWidget().cmd, + getWidget().argv, + term->lines, + term->cols)) { + puts("Failed to fork!"); + std::exit(EXIT_FAILURE); + } + + ctx.listenFd(term->fd, [this] { + std::array buf; + auto size = read(term->fd, &buf[0], buf.size()); + + // Only update if the buffer isn't full. Otherwise more data is comming + // probably. + if (size != buf.size()) { + setState([&](auto& self) { + parse(self.term.get(), reinterpret_cast(&buf[0]), size); + }); + } else { + parse(term.get(), reinterpret_cast(&buf[0]), size); + } + }); + + // listen to stdin in debug. + if constexpr (USE_STDIN) { + ctx.listenFd(STDIN_FILENO, [this] { + std::array buf; + auto size = read(STDIN_FILENO, &buf[0], buf.size()); + if (size > 0) { + write(term->fd, &buf[0], size); + } + }); + } + + checkLandscape(ctx); + ctx.onDeviceUpdate([this, &ctx] { modify().checkLandscape(ctx); }); +} + +void +YaftState::checkLandscape(rmlib::AppContext& ctx) { + const auto& config = getWidget().config; + + if (config.orientation == YaftConfig::Orientation::Auto) { + + // The pogo state updates after a delay, so wait 100 ms before checking. + pogoTimer = ctx.addTimer(std::chrono::milliseconds(100), [this] { + setState( + [](auto& self) { self.isLandscape = device::IsPogoConnected(); }); + }); + } else { + isLandscape = config.orientation == YaftConfig::Orientation::Landscape; + } +} + +YaftState +Yaft::createState() const { + return {}; +} diff --git a/apps/yaft2/YaftWidget.h b/apps/yaft2/YaftWidget.h new file mode 100644 index 0000000..358714a --- /dev/null +++ b/apps/yaft2/YaftWidget.h @@ -0,0 +1,75 @@ +#pragma once + +#include "config.h" +#include "keyboard.h" +#include "layout.h" +#include "screen.h" + +#include + +#include + +class YaftState; + +class Yaft : public rmlib::StatefulWidget { +public: + Yaft(const char* cmd, char* const argv[], YaftConfigAndError config) + : config(std::move(config.config)) + , configError(std::move(config.err)) + , cmd(cmd) + , argv(argv) {} + + YaftState createState() const; + +private: + friend class YaftState; + + YaftConfig config; + std::optional configError; + + const char* cmd; + char* const* argv; +}; + +class YaftState : public rmlib::StateBase { +public: + ~YaftState(); + + /// Logs the given string to the terminal console. + void logTerm(std::string_view str); + + void init(rmlib::AppContext& ctx, const rmlib::BuildContext&); + + void checkLandscape(rmlib::AppContext& ctx); + + auto build(rmlib::AppContext& ctx, + const rmlib::BuildContext& buildCtx) const { + using namespace rmlib; + + const auto& layout = [this]() -> const Layout& { + if (isLandscape) { + return empty_layout; + } + + if (hidden) { + return hidden_layout; + } + + return *getWidget().config.layout; + }(); + + return Column( + Expanded(Screen(term.get(), isLandscape, getWidget().config.autoRefresh)), + Keyboard( + term.get(), { layout, *getWidget().config.keymap }, [this](int num) { + setState([](auto& self) { self.hidden = !self.hidden; }); + })); + } + +private: + std::unique_ptr term; + rmlib::TimerHandle pogoTimer; + + bool hidden = false; + bool isLandscape = false; +}; diff --git a/apps/yaft2/config.cpp b/apps/yaft2/config.cpp index 911ecc3..217dbb9 100644 --- a/apps/yaft2/config.cpp +++ b/apps/yaft2/config.cpp @@ -26,11 +26,21 @@ auto-refresh = 1024 std::filesystem::path getConfigPath() { - const auto* home = getenv("HOME"); - if (home == nullptr || home[0] == 0) { - home = "/home/root"; - } - return std::filesystem::path(home) / ".config" / "yaft" / "config.toml"; + const auto configDir = [] { + if (const auto* xdgCfg = getenv("XDG_CONFIG_HOME"); + xdgCfg != nullptr && xdgCfg[0] != 0) { + return std::filesystem::path(xdgCfg); + } + + const auto* home = getenv("HOME"); + if (home == nullptr || home[0] == 0) { + home = "/home/root"; + } + + return std::filesystem::path(home) / ".config" / "yaft"; + }(); + + return configDir / "config.toml"; } template @@ -84,7 +94,7 @@ getConfig(const toml::table& tbl) { YaftConfig YaftConfig::getDefault() { auto tbl = toml::parse(default_config); - return *getConfig(tbl); + return getConfig(tbl).value(); } ErrorOr @@ -123,3 +133,27 @@ saveDefaultConfig() { return {}; } + +YaftConfigAndError +loadConfigOrMakeDefault() { + auto cfgOrErr = loadConfig().transform([](auto val) { + return YaftConfigAndError{ std::move(val), {} }; + }); + if (cfgOrErr.has_value()) { + return *cfgOrErr; + } + + auto err = cfgOrErr.error(); + if (err.type == YaftConfigError::Missing) { + err.msg = "No config, creating new one\r\n"; + if (const auto optErr = saveDefaultConfig(); !optErr.has_value()) { + err.msg += optErr.error().msg + "\r\n"; + } + } else { + std::stringstream ss; + ss << "Config syntax error: " << err.msg << "\r\n"; + err.msg = ss.str(); + } + + return { YaftConfig::getDefault(), std::move(err) }; +} diff --git a/apps/yaft2/config.h b/apps/yaft2/config.h index 4896ea8..a897a5f 100644 --- a/apps/yaft2/config.h +++ b/apps/yaft2/config.h @@ -1,6 +1,8 @@ #pragma once #include "Error.h" + +#include #include #include "keymap.h" @@ -29,9 +31,22 @@ struct YaftConfigError { std::string msg; }; +struct YaftConfigAndError { + YaftConfig config; + + std::optional err; +}; + /// Load the config from the `~/.config/yaft/config.toml` location. ErrorOr loadConfig(); OptError<> saveDefaultConfig(); + +/// Always returns a config, either the default one or the one on the file +/// system. Will also make a new config file if it didn't exist. +/// +/// If any error occured during the loading of the config, it's also returned. +YaftConfigAndError +loadConfigOrMakeDefault(); diff --git a/apps/yaft2/keyboard.cpp b/apps/yaft2/keyboard.cpp index fd8f324..189b2ce 100644 --- a/apps/yaft2/keyboard.cpp +++ b/apps/yaft2/keyboard.cpp @@ -8,9 +8,6 @@ using namespace rmlib; namespace { -constexpr auto repeat_delay = std::chrono::seconds(1); -constexpr auto repeat_time = std::chrono::milliseconds(100); - constexpr auto pen_slot = 0x1000; constexpr char esc_char = '\x1b'; @@ -154,7 +151,7 @@ KeyboardRenderObject::KeyboardRenderObject(const Keyboard& keyboard) void KeyboardRenderObject::update(const Keyboard& keyboard) { - bool needsReLayout = &keyboard.layout != &widget->layout; + bool needsReLayout = &keyboard.params.layout != &widget->params.layout; widget = &keyboard; if (needsReLayout) { @@ -166,21 +163,22 @@ KeyboardRenderObject::update(const Keyboard& keyboard) { void KeyboardRenderObject::doRebuild(rmlib::AppContext& ctx, const rmlib::BuildContext&) { - const auto duration = repeat_time / 10; - std::cout << "Rebuild\n"; + const auto duration = getWidget().params.repeatTime / 10; repeatTimer = ctx.addTimer( duration, [this]() { updateRepeat(); }, duration); } rmlib::Size KeyboardRenderObject::doLayout(const rmlib::Constraints& constraints) { - if (constraints.isBounded()) { + const auto numRows = widget->params.layout.numRows(); + const auto numCols = widget->params.layout.numCols(); + + if (constraints.isBounded() && numRows != 0 && numCols != 0) { + keyHeight = float(constraints.max.height) / numRows; + keyWidth = float(constraints.max.width) / numCols; return constraints.max; } - const auto numRows = widget->layout.numRows(); - const auto numCols = widget->layout.numCols(); - if (constraints.hasBoundedHeight() && numRows != 0) { keyHeight = float(constraints.max.height) / numRows; keyWidth = keyHeight / Keyboard::key_height * Keyboard::key_width; @@ -193,6 +191,8 @@ KeyboardRenderObject::doLayout(const rmlib::Constraints& constraints) { return Size{ int(keyWidth * numCols), int(keyHeight * numRows) }; } + keyHeight = Keyboard::key_height; + keyWidth = Keyboard::key_width; return constraints.min; } @@ -201,7 +201,7 @@ KeyboardRenderObject::doDraw(rmlib::Rect rect, rmlib::Canvas& canvas) { Point pos = rect.topLeft; UpdateRegion result; - for (const auto& row : widget->layout.rows) { + for (const auto& row : widget->params.layout.rows) { pos.x = rect.topLeft.x; for (const auto& key : row) { @@ -239,6 +239,7 @@ KeyboardRenderObject::handleInput(const rmlib::input::Event& ev) { void KeyboardRenderObject::updateRepeat() { const auto time = time_source::now(); + const auto repeatTime = getWidget().params.repeatTime; for (auto& [key, state] : keyState) { if (state.slot == -1) { @@ -257,7 +258,7 @@ KeyboardRenderObject::updateRepeat() { // Don't continue to repeat keys with special actions on longpress if (key->longPressCode == 0) { - state.nextRepeat += repeat_time; + state.nextRepeat += repeatTime; } else { state.nextRepeat += std::chrono::hours(5); } @@ -274,7 +275,7 @@ KeyboardRenderObject::updateRepeat() { sendKeyDown(*key, /* repeat */ true); } - state.nextRepeat += repeat_time; + state.nextRepeat += repeatTime; } } } @@ -288,7 +289,7 @@ KeyboardRenderObject::updateLayout() { ctrlKey = nullptr; shiftKey = nullptr; - for (const auto& row : widget->layout.rows) { + for (const auto& row : widget->params.layout.rows) { for (const auto& key : row) { if (key.code == ModifierKeys::Alt) { assert(altKey == nullptr); @@ -309,8 +310,6 @@ KeyboardRenderObject::sendKeyDown(int scancode, bool shift, bool alt, bool ctrl) { - bool appCursor = (widget->term->mode & MODE_APP_CURSOR) != 0; - if (isCallback(scancode)) { if (widget->callback) { widget->callback(getCallback(scancode)); @@ -318,6 +317,7 @@ KeyboardRenderObject::sendKeyDown(int scancode, return; } + bool appCursor = (widget->term->mode & MODE_APP_CURSOR) != 0; const auto* code = getKeyCodeStr(scancode, shift, alt, ctrl, appCursor); if (code != nullptr) { write(widget->term->fd, code, strlen(code)); @@ -390,7 +390,7 @@ KeyboardRenderObject::getKey(const rmlib::Point& point) { const auto rowIdx = int((point.y - getRect().topLeft.y) / keyHeight); const auto columnIdx = int((point.x - getRect().topLeft.x) / keyWidth); - const auto& row = widget->layout.rows[rowIdx]; + const auto& row = widget->params.layout.rows[rowIdx]; int keyCounter = 0; for (auto& key : row) { @@ -412,7 +412,7 @@ KeyboardRenderObject::clearSticky() { auto& state = keyState[key]; // Prevent key being held when multi touch typing. - state.nextRepeat = time_source::now() + repeat_delay; + state.nextRepeat = time_source::now() + getWidget().params.repeatDelay; if (state.stuck) { state.stuck = false; @@ -435,7 +435,7 @@ KeyboardRenderObject::handleTouchEvent(const Ev& ev) { auto& state = keyState[key]; state.dirty = true; state.slot = getSlot(ev); - state.nextRepeat = time_source::now() + repeat_delay; + state.nextRepeat = time_source::now() + getWidget().params.repeatDelay; if (isModifier(key->code)) { if (!state.held) { @@ -464,7 +464,7 @@ KeyboardRenderObject::handleTouchEvent(const Ev& ev) { void KeyboardRenderObject::handleKeyEvent(const rmlib::input::KeyEvent& ev) { - const auto& keymap = widget->keymap; + const auto& keymap = widget->params.keymap; auto it = keymap.find(ev.keyCode); if (it == keymap.end()) { @@ -476,7 +476,7 @@ KeyboardRenderObject::handleKeyEvent(const rmlib::input::KeyEvent& ev) { if (ev.type == input::KeyEvent::Press) { state.down = true; - state.nextRepeat = time_source::now() + repeat_delay; + state.nextRepeat = time_source::now() + getWidget().params.repeatDelay; sendKeyDown(key); } else if (ev.type == input::KeyEvent::Release) { state.down = false; diff --git a/apps/yaft2/keyboard.h b/apps/yaft2/keyboard.h index 5612aab..f9140e7 100644 --- a/apps/yaft2/keyboard.h +++ b/apps/yaft2/keyboard.h @@ -14,6 +14,14 @@ class KeyboardRenderObject; using KeyboardCallback = std::function; +struct KeyboardParams { + const Layout& layout; + const KeyMap& keymap; + + std::chrono::milliseconds repeatDelay = std::chrono::seconds(1); + std::chrono::milliseconds repeatTime = std::chrono::milliseconds(100); +}; + /// Keyboard widget, displays a virtual keyboard of the given layout. /// Also interprets physical key presses. class Keyboard : public rmlib::Widget { @@ -22,11 +30,8 @@ class Keyboard : public rmlib::Widget { constexpr static int key_height = 64; constexpr static int key_width = 128; - Keyboard(struct terminal_t* term, - const Layout& layout, - const KeyMap& keymap, - KeyboardCallback cb) - : term(term), layout(layout), keymap(keymap), callback(std::move(cb)) {} + Keyboard(struct terminal_t* term, KeyboardParams params, KeyboardCallback cb) + : term(term), params(params), callback(std::move(cb)) {} std::unique_ptr createRenderObject() const; @@ -34,8 +39,7 @@ class Keyboard : public rmlib::Widget { friend class KeyboardRenderObject; struct terminal_t* term; - const Layout& layout; - const KeyMap& keymap; + KeyboardParams params; KeyboardCallback callback; }; diff --git a/apps/yaft2/main.cpp b/apps/yaft2/main.cpp index c49f810..f8b03ec 100644 --- a/apps/yaft2/main.cpp +++ b/apps/yaft2/main.cpp @@ -1,223 +1,19 @@ -// libYaft -#include "conf.h" -#include "parse.h" -#include "terminal.h" -#include "yaft.h" - -#include "util.h" - // yaft(2) +#include "YaftWidget.h" #include "config.h" -#include "keyboard.h" -#include "layout.h" -#include "screen.h" // rmLib -#include #include // stdlib -#include #include using namespace rmlib; namespace { -const char* term_name = "yaft-256color"; const char* shell_cmd = "/bin/bash"; - -AppContext* globalCtx = nullptr; - -void -sig_handler(int signo) { - if (signo == SIGCHLD) { - if (globalCtx != nullptr) { - globalCtx->stop(); - } - wait(NULL); - } -} - -void -initSignalHandler(AppContext& ctx) { - globalCtx = &ctx; - - struct sigaction sigact; - memset(&sigact, 0, sizeof(struct sigaction)); - sigact.sa_handler = sig_handler; - sigact.sa_flags = SA_RESTART; - sigaction(SIGCHLD, &sigact, NULL); -} - -bool -fork_and_exec(int* master, - const char* cmd, - char* const argv[], - int lines, - int cols) { - pid_t pid; - struct winsize ws; - ws.ws_row = lines; - ws.ws_col = cols; - /* XXX: this variables are UNUSED (man tty_ioctl), - but useful for calculating terminal cell size */ - ws.ws_ypixel = CELL_HEIGHT * lines; - ws.ws_xpixel = CELL_WIDTH * cols; - - pid = eforkpty(master, NULL, NULL, &ws); - if (pid < 0) { - return false; - } else if (pid == 0) { /* child */ - setenv("TERM", term_name, 1); - execvp(cmd, argv); - /* never reach here */ - exit(EXIT_FAILURE); - } - return true; -} - -class YaftState; - -class Yaft : public StatefulWidget { -public: - Yaft(const char* cmd, char* const argv[]) : cmd(cmd), argv(argv) {} - - YaftState createState() const; - -private: - friend class YaftState; - const char* cmd; - char* const* argv; -}; - -class YaftState : public StateBase { -public: - /// Logs the given string to the terminal console. - void logTerm(std::string_view str) { - parse(term.get(), reinterpret_cast(str.data()), str.size()); - } - - YaftConfig getConfig() { - auto cfgOrErr = loadConfig(); - - if (!cfgOrErr.has_value()) { - const auto& err = cfgOrErr.error(); - if (err.type == YaftConfigError::Missing) { - logTerm("No config, creating new one\r\n"); - saveDefaultConfig(); - } else { - std::stringstream ss; - ss << "Config syntax error: " << err.msg << "\r\n"; - logTerm(ss.str()); - } - - return YaftConfig::getDefault(); - } - - return *cfgOrErr; - } - - void init(AppContext& ctx, const BuildContext&) { - term = std::make_unique(); - - // term_init needs the maximum size of the terminal. - int maxSize = - std::max(ctx.getFbCanvas().width(), ctx.getFbCanvas().height()); - if (!term_init(term.get(), maxSize, maxSize)) { - std::cout << "Error init term\n"; - ctx.stop(); - return; - } - - config = getConfig(); - - initSignalHandler(ctx); - - if (!fork_and_exec(&term->fd, - getWidget().cmd, - getWidget().argv, - term->lines, - term->cols)) { - puts("Failed to fork!"); - std::exit(EXIT_FAILURE); - } - - ctx.listenFd(term->fd, [this] { - std::array buf; - auto size = read(term->fd, &buf[0], buf.size()); - - // Only update if the buffer isn't full. Otherwise more data is comming - // probably. - if (size != buf.size()) { - setState([&](auto& self) { - parse(self.term.get(), reinterpret_cast(&buf[0]), size); - }); - } else { - parse(term.get(), reinterpret_cast(&buf[0]), size); - } - }); - - // listen to stdin in debug. - if constexpr (USE_STDIN) { - ctx.listenFd(STDIN_FILENO, [this] { - std::array buf; - auto size = read(STDIN_FILENO, &buf[0], buf.size()); - if (size > 0) { - write(term->fd, &buf[0], size); - } - }); - } - - if (config.orientation == YaftConfig::Orientation::Auto) { - isLandscape = device::IsPogoConnected(); - ctx.onDeviceUpdate([this, &ctx] { - // The pogo state updates after a delay, so wait 100 ms before checking. - pogoTimer = ctx.addTimer(std::chrono::milliseconds(100), [this] { - setState( - [](auto& self) { self.isLandscape = device::IsPogoConnected(); }); - }); - }); - } else { - isLandscape = config.orientation == YaftConfig::Orientation::Landscape; - } - } - - auto build(AppContext& ctx, const BuildContext& buildCtx) const { - const auto& layout = [this]() -> const Layout& { - if (isLandscape) { - return empty_layout; - } - - if (hidden) { - return hidden_layout; - } - - return *config.layout; - }(); - - return Column(Expanded(Screen(term.get(), isLandscape, config.autoRefresh)), - Keyboard(term.get(), layout, *config.keymap, [this](int num) { - setState([](auto& self) { self.hidden = !self.hidden; }); - })); - } - -private: - std::unique_ptr term; - TimerHandle pogoTimer; - - YaftConfig config; - - bool hidden = false; - bool isLandscape = false; -}; - -YaftState -Yaft::createState() const { - return {}; } -} // namespace - int main(int argc, char* argv[]) { static const char* shell_args[3] = { shell_cmd, "-l", NULL }; @@ -236,6 +32,8 @@ main(int argc, char* argv[]) { args = const_cast(shell_args); } - runApp(Yaft(cmd, args)); + auto cfg = loadConfigOrMakeDefault(); + + runApp(Yaft(cmd, args, std::move(cfg))); return 0; } diff --git a/libs/rMlib/include/UI/Util.h b/libs/rMlib/include/UI/Util.h index cfa07d7..6e4bc44 100644 --- a/libs/rMlib/include/UI/Util.h +++ b/libs/rMlib/include/UI/Util.h @@ -45,7 +45,7 @@ struct Constraints { constexpr bool hasBoundedWidth() const { return max.width != unbound; } constexpr bool hasBoundedHeight() const { return max.height != unbound; } constexpr bool isBounded() const { - return hasBoundedHeight() && hasFiniteWidth(); + return hasBoundedHeight() && hasBoundedWidth(); } constexpr bool hasFiniteWidth() const { return min.width != unbound; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8f643bc..a80ff7f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,28 +3,30 @@ add_subdirectory(unit) if (COVERAGE) set(COVERAGE_OUT "${CMAKE_BINARY_DIR}/coverage") - set(IGNORE_REGEX "'build/|stb_.*|test/unit/|tilem/emu'") + set(IGNORE_REGEX "'build/|stb_.*|test/unit/|tilem/emu|libYaft'") + + set(OBJECTS "$") add_custom_target(make-coverage COMMAND rm -rf "${COVERAGE_OUT}" COMMAND mkdir "${COVERAGE_OUT}" - COMMAND ctest - COMMAND llvm-profdata merge -o merged.profdata - "${CMAKE_BINARY_DIR}/test/unit/default.profraw" + + COMMAND env LLVM_PROFILE_FILE="${COVERAGE_OUT}/%m.profraw" ctest + COMMAND llvm-profdata merge -o merged.profdata "${COVERAGE_OUT}" COMMAND llvm-cov show --format html -output-dir "${COVERAGE_OUT}" -ignore-filename-regex "${IGNORE_REGEX}" --instr-profile merged.profdata -Xdemangler c++filt -Xdemangler -n - "${CMAKE_BINARY_DIR}/test/unit/unit-tests" + ${OBJECTS} COMMAND llvm-cov export --format lcov -ignore-filename-regex "${IGNORE_REGEX}" --instr-profile merged.profdata -Xdemangler c++filt -Xdemangler -n - "${CMAKE_BINARY_DIR}/test/unit/unit-tests" > "${CMAKE_BINARY_DIR}/report.lcov" + ${OBJECTS} > "${CMAKE_BINARY_DIR}/report.lcov" DEPENDS unit-tests ) diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 5067b17..7a4f1cc 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -11,7 +11,8 @@ FetchContent_MakeAvailable(catch2) add_executable(${PROJECT_NAME} TestUnistdpp.cpp TestRMLib.cpp - TestTilem.cpp) + TestTilem.cpp + TestYaft.cpp) target_compile_definitions(${PROJECT_NAME} PRIVATE ASSETS_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets") @@ -21,6 +22,7 @@ target_link_libraries(${PROJECT_NAME} Catch2::Catch2WithMain unistdpp rMlib - tilem::lib) + tilem::lib + Yaft::app_lib) add_test(NAME "unit" COMMAND ${PROJECT_NAME}) diff --git a/test/unit/TestYaft.cpp b/test/unit/TestYaft.cpp new file mode 100644 index 0000000..f2c6e60 --- /dev/null +++ b/test/unit/TestYaft.cpp @@ -0,0 +1,208 @@ +#include + +#include "rMLibTestHelper.h" + +#include "YaftWidget.h" + +using namespace rmlib; + +namespace { + +const Layout test_layout = { { + { { "a", makeCallback(1) }, + { "b", makeCallback(2), "", 0, /* width */ 2 }, + { "c", makeCallback(3) } }, + + { + { "ctrl", Ctrl }, + { "alt", Alt }, + { ":shift", Shift }, + { "<>", makeCallback(4), "^", makeCallback(5), 1, makeCallback(6) }, + }, +} }; + +template +void +doKey(TestContext& ctx, + const FindResult& ros, + std::string_view name, + bool down, + const Layout& layout = test_layout) { + + bool found = false; + int rowIdx = 0; + int colIdx = 0; + for (const auto& row : layout.rows) { + for (const auto& key : row) { + if (key.name == name) { + found = true; + break; + } + + colIdx += key.width; + } + + if (found) { + break; + } + + rowIdx++; + colIdx = 0; + } + + INFO("Key " << name << " not found"); + REQUIRE(found); + + float x = (float(colIdx) + 0.5f) / layout.numCols(); + float y = (float(rowIdx) + 0.5f) / layout.numRows(); + + ctx.sendInput(down, ros, { x, y }); +} + +template +void +tapKey(TestContext& ctx, + const FindResult& ros, + std::string_view name, + const Layout& layout = test_layout) { + doKey(ctx, ros, name, true, layout); + ctx.pump(); + doKey(ctx, ros, name, false, layout); +} + +} // namespace + +TEST_CASE("Keyboard", "[yaft][ui]") { + auto ctx = TestContext::make(); + + int lastCallback = -1; + int callbackCount = 0; + const auto updateCb = [&](auto cb) { + if (lastCallback == cb) { + callbackCount++; + } + lastCallback = cb; + }; + + const auto params = KeyboardParams{ + .layout = test_layout, + .keymap = qwerty_keymap, + .repeatDelay = std::chrono::milliseconds(50), + .repeatTime = std::chrono::milliseconds(10), + }; + + ctx.pumpWidget(Center(Sized(Keyboard(nullptr, params, updateCb), 300, 200))); + + auto kbd = ctx.findByType(); + + REQUIRE_THAT(kbd, ctx.matchesGolden("yaft-keyboard.png")); + + // Test key feedback when pressing. + doKey(ctx, kbd, "a", true); + ctx.pump(params.repeatDelay + 2 * params.repeatTime + params.repeatTime / 2); + REQUIRE_THAT(kbd, ctx.matchesGolden("yaft-keyboard-down.png")); + doKey(ctx, kbd, "a", false); + REQUIRE(lastCallback == 1); + + // Make sure repeat works. + CHECK(callbackCount == 3); + + // Also make sure a wide key gets updated correctly. + doKey(ctx, kbd, "b", true); + ctx.pump(); + REQUIRE_THAT(kbd, ctx.matchesGolden("yaft-keyboard-down2.png")); + doKey(ctx, kbd, "b", false); + REQUIRE(lastCallback == 2); + + tapKey(ctx, kbd, "c"); + REQUIRE(lastCallback == 3); + + tapKey(ctx, kbd, ":shift"); + REQUIRE_THAT(kbd, ctx.matchesGolden("yaft-keyboard-held.png")); + tapKey(ctx, kbd, "<>"); + REQUIRE(lastCallback == 5); + + tapKey(ctx, kbd, "<>"); + REQUIRE(lastCallback == 4); + + doKey(ctx, kbd, ":shift", true); + ctx.pump(params.repeatDelay + params.repeatTime / 2); + REQUIRE_THAT(kbd, ctx.matchesGolden("yaft-keyboard-stuck.png")); + doKey(ctx, kbd, ":shift", false); + + tapKey(ctx, kbd, "<>"); + REQUIRE(lastCallback == 5); + + tapKey(ctx, kbd, "<>"); + REQUIRE(lastCallback == 5); + tapKey(ctx, kbd, ":shift"); + + tapKey(ctx, kbd, "<>"); + REQUIRE(lastCallback == 4); + + doKey(ctx, kbd, "<>", true); + ctx.pump(params.repeatDelay + params.repeatTime / 2); + doKey(ctx, kbd, "<>", false); + REQUIRE(lastCallback == 6); +} + +TEST_CASE("Yaft", "[yaft][ui]") { + auto ctx = TestContext::make(); + + std::string program = "/bin/cat"; + std::vector args = { program.data(), nullptr }; + + YaftConfigAndError cfgAndErr; + cfgAndErr.config = YaftConfig::getDefault(); + + SECTION("landscape") { + cfgAndErr.config.orientation = YaftConfig::Orientation::Landscape; + cfgAndErr.err = YaftConfigError{ YaftConfigError::Missing, "Error test!" }; + + ctx.pumpWidget(Yaft(args.front(), args.data(), cfgAndErr)); + ctx.pump(); + + auto yaft = ctx.findByType(); + REQUIRE_THAT(yaft, ctx.matchesGolden("yaft-landscape.png")); + } + + SECTION("portrait") { + cfgAndErr.config.orientation = YaftConfig::Orientation::Protrait; + + ctx.pumpWidget(Yaft(args.front(), args.data(), cfgAndErr)); + ctx.pump(); + + auto yaft = ctx.findByType(); + auto kbd = ctx.findByType(); + REQUIRE_THAT(yaft, ctx.matchesGolden("yaft-init.png")); + + const auto down = [&](auto name) { + doKey(ctx, kbd, name, true, qwerty_layout); + }; + const auto up = [&](auto name) { + doKey(ctx, kbd, name, false, qwerty_layout); + }; + const auto tap = [&](auto name) { tapKey(ctx, kbd, name, qwerty_layout); }; + + tap("a"); + tap(":shift"); + tap("a"); + tap(":enter"); + ctx.pump(); + REQUIRE_THAT(yaft, ctx.matchesGolden("yaft-aA.png")); + + SECTION("Hide") { + down("esc"); + ctx.pump(std::chrono::milliseconds(1200)); + up("esc"); + REQUIRE_THAT(yaft, ctx.matchesGolden("yaft-hidden.png")); + } + + SECTION("Exit") { + tap(":enter"); + tap("ctrl"); + tap("d"); + REQUIRE(ctx.shouldStop()); + } + } +} diff --git a/test/unit/assets/yaft-aA.png b/test/unit/assets/yaft-aA.png new file mode 100644 index 0000000..6031eb6 --- /dev/null +++ b/test/unit/assets/yaft-aA.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d6e72e02b74314ad5348037db3a7292f3b1d2c6dcbd4289560283281a3162f8 +size 40428 diff --git a/test/unit/assets/yaft-hidden.png b/test/unit/assets/yaft-hidden.png new file mode 100644 index 0000000..21dfdb0 --- /dev/null +++ b/test/unit/assets/yaft-hidden.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a749fba922ffaf653b9eaa2e712f614bff053d4cb4d9b92a76e8d99d44499f5 +size 31171 diff --git a/test/unit/assets/yaft-init.png b/test/unit/assets/yaft-init.png new file mode 100644 index 0000000..8d3ca1b --- /dev/null +++ b/test/unit/assets/yaft-init.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2a7a94185b041e90d764e4170dcfaff759f0befb56c435c3c7e286de923b565 +size 40213 diff --git a/test/unit/assets/yaft-keyboard-down.png b/test/unit/assets/yaft-keyboard-down.png new file mode 100644 index 0000000..a302cbc --- /dev/null +++ b/test/unit/assets/yaft-keyboard-down.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c44a28f738f1450cf246318d57f393c9bb097ee4290f676d0be9252e8b97221b +size 2593 diff --git a/test/unit/assets/yaft-keyboard-down2.png b/test/unit/assets/yaft-keyboard-down2.png new file mode 100644 index 0000000..b04125c --- /dev/null +++ b/test/unit/assets/yaft-keyboard-down2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36e9350b3b1989ddaf599bf68ba96869a0d75fcfaa95711c02a78ff7ab7b74e0 +size 2584 diff --git a/test/unit/assets/yaft-keyboard-held.png b/test/unit/assets/yaft-keyboard-held.png new file mode 100644 index 0000000..6deeca1 --- /dev/null +++ b/test/unit/assets/yaft-keyboard-held.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85673b328715b12686bb68528b496aecc41835778294582fc9268f469dc5a4eb +size 2577 diff --git a/test/unit/assets/yaft-keyboard-stuck.png b/test/unit/assets/yaft-keyboard-stuck.png new file mode 100644 index 0000000..8fe1f2c --- /dev/null +++ b/test/unit/assets/yaft-keyboard-stuck.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2aebbf6d540e0353dc41f177f8a236d2b8a4a39f407e405aee5d2dc21b1297b +size 2583 diff --git a/test/unit/assets/yaft-keyboard.png b/test/unit/assets/yaft-keyboard.png new file mode 100644 index 0000000..f7756ea --- /dev/null +++ b/test/unit/assets/yaft-keyboard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcee0c35c6a417b599669cd4f71be1cb38ece353086ed61546fa89e55a4d1a65 +size 2560 diff --git a/test/unit/assets/yaft-landscape.png b/test/unit/assets/yaft-landscape.png new file mode 100644 index 0000000..0b38bab --- /dev/null +++ b/test/unit/assets/yaft-landscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3f79062794a8606ad8af4e185affafffa2e4b682d2efc7a633489b759547d44 +size 28276