diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f2c55f9..5be071d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,10 @@ add_executable(${PROJECT_NAME} Page.cpp stringutil.hpp stringutil.cpp + Tui.cpp + Tui.h + Command.h + Command.cpp ) target_link_libraries(${PROJECT_NAME} PRIVATE LibXml2::LibXml2 cpr::cpr) diff --git a/src/Command.cpp b/src/Command.cpp new file mode 100644 index 0000000..e1f8496 --- /dev/null +++ b/src/Command.cpp @@ -0,0 +1,51 @@ +#include "Command.h" + +#include + + +namespace Command { + Command readCommand() { + if (!std::cin) + return {Exit{}}; + std::string line; + if (!std::getline(std::cin, line)) + return {Exit{}}; + + // Trim leading and trailing whitespaces + constexpr std::string_view TO_TRIM = " \t\n\r"; + const size_t start = line.find_first_not_of(TO_TRIM); + const size_t end = line.find_last_not_of(TO_TRIM); + if (start == std::string::npos) return None{}; + const std::string_view line_trimmed(line.data() + start, end - start + 1); + + using namespace std::string_view_literals; + if (line_trimmed == "r"sv || line_trimmed == "refresh"sv) + return Refresh{}; + if (line_trimmed == "n"sv || line_trimmed == ">"sv || line_trimmed == "next"sv) + return Next{}; + if (line_trimmed == "p"sv || line_trimmed == "<"sv || line_trimmed == "previous"sv) + return Previous{}; + if (line_trimmed == "x"sv || line_trimmed == "exit"sv || line_trimmed == "q"sv || line_trimmed == "quit"sv) + return Exit{}; + if (line_trimmed == "help"sv || line_trimmed == "?"sv || line_trimmed == "?"sv) + return Help{}; + + if (line_trimmed.starts_with("seek") || line.starts_with("s ")) { + const size_t space = line_trimmed.find(' '); + if (space == std::string::npos) + throw NoNumberException(); + int number = 0; + const size_t size = line.size(); + for (size_t i = space + 1; i < size; ++i) { + const char c = line_trimmed[i]; + if (!std::isdigit(c)) + throw NoNumberException(); + number = number * 10 + (c - '0'); + } + + return {Seek{number}}; + } + + return {None{}}; + } +} diff --git a/src/Command.h b/src/Command.h new file mode 100644 index 0000000..84b3196 --- /dev/null +++ b/src/Command.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + + +namespace Command { + class NoNumberException final : public std::exception { + public: + [[nodiscard]] const char *what() const noexcept override { + return "Seek option was used without a number"; + } + }; + + + struct Help { + }; + + struct None { + }; + + struct Next { + }; + + struct Previous { + }; + + struct Refresh { + }; + + struct Exit { + }; + + struct Seek { + int number; + }; + + using Command = std::variant; + + Command readCommand(); +} diff --git a/src/Page.cpp b/src/Page.cpp index d2acaa6..6e98cec 100644 --- a/src/Page.cpp +++ b/src/Page.cpp @@ -74,7 +74,7 @@ Page &Page::operator-=(int) { return *this; } -std::string Page::str() const { +std::string Page::str_pretty() const { std::string ret; std::istringstream stream(m_subpage); diff --git a/src/Page.hpp b/src/Page.hpp index 0a61a16..b514377 100644 --- a/src/Page.hpp +++ b/src/Page.hpp @@ -32,7 +32,7 @@ public: Page &operator-=(int); - [[nodiscard]] std::string str() const; + [[nodiscard]] std::string str_pretty() const; bool refresh(); diff --git a/src/Tui.cpp b/src/Tui.cpp new file mode 100644 index 0000000..a00310a --- /dev/null +++ b/src/Tui.cpp @@ -0,0 +1,81 @@ +#include "Tui.h" + +#include +#include +#include +#include + +#include "Command.h" + +Tui::Tui(Page page) : m_page(std::move(page)) { +} + + +std::optional get_page_number(const std::string_view line) { + const size_t space = line.find_first_of(' '); + if (space == std::string::npos) + return std::nullopt; + + int number = 0; + const size_t size = line.size(); + for (size_t i = space; i < size; i++) { + const char c = line[i]; + if (!std::isdigit(c)) + return std::nullopt; + + number = number * 10 + (c - '0'); + } + + return number; +} + +void printHelp() { + std::cout + << "Available Commands:\n" + << " r, refresh : Refresh the current view.\n" + << " n, >, next : Move to the next item.\n" + << " p, <, previous : Move to the previous item.\n" + << " x, exit, q, quit : Exit the application.\n" + << " seek , s : Seek to the specified number.\n" + << " help, h, ? : Display this help message." + << std::endl; +} + +void Tui::run() { + std::cout << m_page.str_pretty() << std::endl; + for (;;) { + std::cout << "Enter a command: "; + std::flush(std::cout); + const Command::Command command = Command::readCommand(); + + bool should_exit = false; + auto visitor = [this, &should_exit](T0 &&arg) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + std::cout << "Invalid command!" << std::endl; + } else if constexpr (std::is_same_v) { + m_page += 1; + std::cout << m_page.str_pretty() << std::endl; + } else if constexpr (std::is_same_v) { + m_page -= 1; + std::cout << m_page.str_pretty() << std::endl; + } else if constexpr (std::is_same_v) { + m_page.refresh(); + std::cout << m_page.str_pretty() << std::endl; + } else if constexpr (std::is_same_v) { + should_exit = true; + } else if constexpr (std::is_same_v) { + printHelp(); + } else if constexpr (std::is_same_v) { + const Command::Seek &seek = arg; + const int number = seek.number; + m_page = Page(number); + std::cout << m_page.str_pretty() << std::endl; + } + }; + std::visit(visitor, command); + if (should_exit) + return; + } +} diff --git a/src/Tui.h b/src/Tui.h new file mode 100644 index 0000000..4569718 --- /dev/null +++ b/src/Tui.h @@ -0,0 +1,13 @@ +#pragma once +#include "Page.hpp" + + +class Tui { +public: + Tui(Page page); + + void run(); + +private: + Page m_page; +}; diff --git a/src/main.cpp b/src/main.cpp index 451498b..79f642d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,7 @@ -#include - #include "Page.hpp" +#include "Tui.h" int main(int argc, char *argv[]) { - const auto page = Page(); - std::cout << page.str() << std::endl; + Tui tui = Page(); + tui.run(); }