diff options
author | Edwin van Leeuwen <edwinvanl@tuta.io> | 2022-08-21 12:32:12 +0100 |
---|---|---|
committer | Edwin van Leeuwen <edwinvanl@tuta.io> | 2022-08-21 12:32:12 +0100 |
commit | 6d0d40cc6901237b6908b85133cf24d40879563a (patch) | |
tree | ea224f6012cdadd2db2d60c3fa6bbc90db696beb | |
parent | c5b87af882a6e8ecff61d97882c73c94adb114d3 (diff) | |
parent | bae499ead5c7a01f3348e0e789b0271341c7db5c (diff) |
Merge branch 'release/1.1.0'v1.1.0
- Support for commands (e.g. :rss feed add <feed>)
- rttt-send to send commands to rttt
- Stability improvements
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | .gitlab-ci.yml | 8 | ||||
-rw-r--r-- | CMakeLists.txt | 21 | ||||
-rw-r--r-- | README.md | 8 | ||||
-rw-r--r-- | aur/PKGBUILD | 2 | ||||
-rw-r--r-- | src/main.cpp | 135 | ||||
-rw-r--r-- | src/rttt-send.cpp | 17 | ||||
-rw-r--r-- | src/rttt.hpp | 343 | ||||
-rw-r--r-- | src/rttt/command.hpp | 211 | ||||
-rw-r--r-- | src/rttt/config.hpp | 28 | ||||
-rw-r--r-- | src/rttt/functional.hpp | 141 | ||||
-rw-r--r-- | src/rttt/hackernews.hpp | 4 | ||||
-rw-r--r-- | src/rttt/reddit.hpp | 69 | ||||
-rw-r--r-- | src/rttt/rss.hpp | 204 | ||||
-rw-r--r-- | src/rttt/thing.hpp | 62 | ||||
-rw-r--r-- | src/rttt/twitter.hpp | 4 | ||||
-rw-r--r-- | src/rttt/ui_ftxui.hpp | 101 | ||||
-rw-r--r-- | test/catch_command.cpp | 193 | ||||
-rw-r--r-- | test/catch_functional.cpp | 171 | ||||
-rw-r--r-- | test/catch_rss.cpp | 69 |
20 files changed, 1429 insertions, 366 deletions
@@ -31,4 +31,6 @@ CPackConfig.cmake CPackSourceConfig.cmake analysis.svg analysis.txt -gmon.out
\ No newline at end of file +gmon.out +CMakeDoxyfile.in +CMakeDoxygenDefaults.cmake
\ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9f753b7..0102796 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ build:ubuntu: image: gcc stage: build script: - - apt update && apt -y install cmake make + - apt update && apt -y install cmake make libzmq3-dev - cmake . - make artifacts: @@ -13,7 +13,11 @@ build:arch: image: testcab/yay stage: build script: - - yay -Syu --noconfirm cmake git cpr nlohmann-json pugixml fmt + - yay -Sy --noconfirm archlinux-keyring + - sudo pacman-key --init + - sudo pacman-key --populate archlinux + #- sudo pacman-key --refresh-keys + - yay -Syu --noconfirm cmake git cpr nlohmann-json pugixml fmt cppzmq - cd aur # If this does not work, then try CI_COMMIT_REF_SLUG - echo $CI_COMMIT_REF_NAME diff --git a/CMakeLists.txt b/CMakeLists.txt index d664a74..7c76371 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,16 @@ FetchContent_Declare(fmt FetchContent_MakeAvailable(fmt) endif() +set (CPPZMQ_BUILD_TESTS OFF CACHE INTERNAL "Turn off cppzmq tests") +find_package(cppzmq) +if (NOT cppzmq_FOUND) + FetchContent_Declare(cppzmq + GIT_REPOSITORY https://github.com/zeromq/cppzmq + GIT_TAG v4.8.1 + ) + FetchContent_MakeAvailable(cppzmq) +endif() + set(CMAKE_THREAD_PREFER_PTHREAD TRUE) set(THREADS_PREFER_PTHREAD_FLAG TRUE) find_package(Threads REQUIRED) @@ -119,12 +129,23 @@ target_link_libraries(${TARGET} PRIVATE ftxui::dom PRIVATE ftxui::component # No PRIVATE fmt::fmt + PRIVATE cppzmq +) + +# rttt-send +add_executable(rttt-send src/rttt-send.cpp) +target_link_libraries(rttt-send + PRIVATE cppzmq + PRIVATE fmt::fmt ) +# General options target_compile_options(${TARGET} PRIVATE -Wall -Wextra -Wpedantic -Werror) install(TARGETS ${TARGET} RUNTIME DESTINATION bin) +install(TARGETS rttt-send RUNTIME DESTINATION bin) +# Test files FILE(GLOB TESTFILES test/catch_*.cpp) foreach(TESTFILE ${TESTFILES}) get_filename_component(NAME ${TESTFILE} NAME_WE) @@ -18,6 +18,8 @@ yay -S rttt-git ### From source +'rttt' depends on 'zeromq' so make sure that is installed first (e.g. `sudo apt install libzmq3-dev` on ubuntu). After that run: + ```bash git clone https://gitlab.com/BlackEdder/rttt.git cd rttt @@ -33,6 +35,12 @@ To switch to different readable things press '/' and type the various shortcuts By default **rttt** uses **xdg-open** to open urls. This can be changed in the configuration [open_command](https://gitlab.com/BlackEdder/rttt/-/wikis/Custom-open_command). I myself use [sesame](https://github.com/green7ea/sesame), which makes it easy to open images directly in an image viewer, movies in mpv, etc. My configuration for sesame can be found on the [wiki](https://gitlab.com/BlackEdder/rttt/-/wikis/Custom-open_command) +### Commands + +New in 'rttt' is the support for basic commands. For example `:open path /rss` will open your rss subscriptions. To see a list of all the currently available commands type `:help`. Current support is limited, but we hope to start supporting more commands soon. + +We have also added the 'rttt-send' binary which enables you to send commands to your open 'rttt' instance from other processes, e.g.: `rttt-send ":rss feed add <feed>"`. + ## Configuration Configuration for **rttt** is stored in '$XDG_CONFIG_HOME/rttt/config.json'. diff --git a/aur/PKGBUILD b/aur/PKGBUILD index 36b87d1..b99c8bb 100644 --- a/aur/PKGBUILD +++ b/aur/PKGBUILD @@ -7,7 +7,7 @@ pkgdesc="Read-the-things-tui (rttt) lets you read RSS/Atom, hackernews and Reddi arch=("x86_64") url="https://gitlab.com/BlackEdder/rttt" license=('GPL3') -depends=('pugixml' 'curl' 'cpr' 'nlohmann-json' 'fmt') +depends=('pugixml' 'curl' 'cpr' 'nlohmann-json' 'fmt' 'cppzmq') makedepends=('git' 'cmake') provides=("${pkgname%-git}") conflicts=("${pkgname%-git}") diff --git a/src/main.cpp b/src/main.cpp index c5833f0..fd00e29 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,14 +5,18 @@ #include <thread> #include <vector> +#include <zmq.hpp> + +#include "ftxui/component/captured_mouse.hpp" #include "ftxui/component/component.hpp" #include "ftxui/component/screen_interactive.hpp" #include "ftxui/dom/elements.hpp" #include "ftxui/dom/flexbox_config.hpp" -#include "ftxui/component/captured_mouse.hpp" +#include "rttt.hpp" #include "rttt/config.hpp" #include "rttt/fake_thing.hpp" +#include "rttt/functional.hpp" #include "rttt/logger.hpp" #include "rttt/prompt.hpp" #include "rttt/text.hpp" @@ -44,23 +48,28 @@ std::map<std::string, std::string> parse_arguments(int argc, char **argv) { rttt::active_storage<std::string, rttt::ui::scroll_window_state> window_state_cache = - rttt::active_storage<std::string, rttt::ui::scroll_window_state>( - 0, 0, 3500); - -auto switch_path(const rttt::Path &from, const rttt::Path &to, - const rttt::ui::scroll_window_state &window_state) { - if (window_state_cache.contains(from.name)) { - window_state_cache.at(from.name) = window_state; - window_state_cache.mark_active(from.name); - } else { - window_state_cache.insert({from.name, window_state}); - } - if (!window_state_cache.contains(to.name)) { - window_state_cache.insert({to.name, rttt::ui::scroll_window_state()}); - window_state_cache.at(to.name).layout = rttt::thing::default_view_mode(to); - } - window_state_cache.mark_active(to.name); - return std::make_tuple(to, window_state_cache.at(to.name)); + rttt::active_storage<std::string, rttt::ui::scroll_window_state>(0, 0, + 3500); + +auto switch_path(rttt::Path &path, + rttt::ui::scroll_window_state &window_state) { + return try_with([&path, &window_state](const rttt::Path &to) { + if (window_state_cache.contains(path.name)) { + window_state_cache.at(path.name) = window_state; + window_state_cache.mark_active(path.name); + } else { + window_state_cache.insert({path.name, window_state}); + } + if (!window_state_cache.contains(to.name)) { + window_state_cache.insert({to.name, rttt::ui::scroll_window_state()}); + window_state_cache.at(to.name).layout = + rttt::thing::default_view_mode(to); + } + window_state_cache.mark_active(to.name); + window_state = window_state_cache.at(to.name); + path = to; + return 0; + }); } } // namespace @@ -82,10 +91,34 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { } // state setup - prompt_state = rttt::config::load_prompt_state_or(std::move(prompt_state)); auto config = rttt::config::load(); rttt::thing::setup(thing_state, config); - rttt::Path path = rttt::thing::parse_path(config["start_path"]); + rttt::thing::setup_commands(thing_state, ui_state); + auto maybe_path = + rttt::thing::parse_path(config["start_path"].get<std::string>()); + if (!maybe_path) { + std::cerr << fmt::format("Failed parsing path {}", config["start_path"]) + << std::endl; + return 1; + } + + rttt::Path path = maybe_path.value(); + rttt::ui::scroll_window_state window_state; + + thing_state.commands | + rttt::command::push( + "open path", + rttt::command::command() | + rttt::command::function([&path, + &window_state](std::string_view to) { + rttt::thing::parse_path(to) | switch_path(path, window_state); + return true; + }) | + rttt::command::help(" <path>: Open the given path, e.g. /rss")); + + prompt_state = prompt_state | + rttt::config::load_prompt_state_or(prompt_state) | + rttt::thing::completions(thing_state); // Item view states rttt::active_storage<std::string, rttt::view::item_state> item_view_states = @@ -96,8 +129,6 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { // Drawing windows auto screen = ftxui::ScreenInteractive::Fullscreen(); - rttt::ui::scroll_window_state window_state; - auto scroll_window_renderer = ftxui::Renderer([&] { using namespace ftxui; rttt::thing::retrieve_path_view(thing_state, path); @@ -126,13 +157,13 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { }), &ui_state.status_open); - auto popup_renderer = - ftxui::Maybe(ftxui::Renderer([&] { - return ftxui::window(ftxui::text(ui_state.popup.title), - ui_state.popup.content) | - ftxui::clear_under | ftxui::center; - }), - &ui_state.popup_open); + auto popup_renderer = ftxui::Maybe( + ftxui::Renderer([&] { + return ftxui::window(ftxui::text(ui_state.popup.value().title), + ui_state.popup.value().content) | + ftxui::clear_under | ftxui::center; + }), + [&popup = ui_state.popup]() { return popup.has_value(); }); // move these to prompt state std::string input = "/"; @@ -140,10 +171,12 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { int prompt_position = 1; prompt_option.on_enter = [&]() { ui_state.prompt_open = false; - logger::push("Prompt: {}", input); prompt_state = rttt::prompt::push(std::move(prompt_state), input); - std::tie(path, window_state) = - switch_path(path, rttt::thing::parse_path(input), window_state); + if (input[0] == '/') { + rttt::thing::parse_path(input) | switch_path(path, window_state); + } else if (input[0] == ':') { + thing_state | rttt::thing::execute_command(input); + } }; prompt_option.cursor_position = &prompt_position; auto prompt = ftxui::Maybe(ftxui::Input(&input, "/r/front", prompt_option), @@ -163,15 +196,15 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { ftxui::Element document = ftxui::vbox({header, scroll_window_renderer->Render(), messages_renderer->Render(), prompt->Render()}); - if (ui_state.popup_open) { + if (ui_state.popup.has_value()) { document = ftxui::dbox({document | ftxui::dim, popup_renderer->Render()}); } return document; }); auto component = CatchEvent(main_renderer, [&](ftxui::Event event) { - if (ui_state.popup_open) { - return ui_state.popup.event_handler(event); + if (ui_state.popup.has_value()) { + return ui_state.popup.value().event_handler(event); } if (ui_state.prompt_open) { if (event == ftxui::Event::Tab) { @@ -245,7 +278,6 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { auto u = rttt::extractURL(maybe_text.value()); popup_urls.insert(popup_urls.end(), u.begin(), u.end()); } - ui_state.popup_open = true; ui_state.popup = rttt::ui::create_url_popup(ui_state, popup_urls); popup_renderer->TakeFocus(); return true; @@ -254,17 +286,17 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { return true; } else if (event == ftxui::Event::Character('l')) { auto from = path; - path = rttt::thing::focus_path(std::move(path), - window_state.highlighted_item); - std::tie(path, window_state) = switch_path(from, path, window_state); + rttt::thing::focus_path(std::move(from), window_state.highlighted_item) | + switch_path(path, window_state); return true; } else if (event == ftxui::Event::Character('h')) { auto from = path; // Mark read when returning item_view_states = rttt::view::mark_read(std::move(item_view_states), from.id); - path = rttt::thing::unfocus_path(std::move(path)); - std::tie(path, window_state) = switch_path(from, path, window_state); + + rttt::thing::unfocus_path(std::move(from)) | + switch_path(path, window_state); return true; } else if (event == ftxui::Event::Character('v')) { if (window_state.layout == rttt::view::layout::text) @@ -273,7 +305,6 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { window_state.layout |= rttt::view::layout::text; return true; } else if (event == ftxui::Event::Character('?')) { - ui_state.popup_open = true; ui_state.popup = rttt::ui::create_help_popup(ui_state); popup_renderer->TakeFocus(); return true; @@ -286,10 +317,20 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { ui_state.prompt_open = true; prompt->TakeFocus(); return true; + } else if (event == ftxui::Event::Character(':')) { + input = ":"; + prompt_position = 1; + ui_state.prompt_open = true; + prompt->TakeFocus(); + return true; } return false; }); + zmq::context_t ctx; + zmq::socket_t sock(ctx, zmq::socket_type::pull); + sock.bind("ipc:///tmp/rttt.ipc"); + bool refresh_ui_continue = true; std::thread refresh_ui([&] { using namespace std::chrono_literals; @@ -308,6 +349,16 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { rttt::config::save_item_view_states(item_view_states); rttt::config::save_prompt_state(prompt_state); } + + // rttt-send msg + zmq::message_t msg; + auto result = sock.recv(msg, zmq::recv_flags::dontwait); + result | try_with([&]([[maybe_unused]] const auto &value) { + thing_state.commands | rttt::command::execute(msg.to_string()); + logger::push("rttt-send: {}", msg.str()); + screen.PostEvent(ftxui::Event::Custom); + return 0; + }); ++counter; } }); diff --git a/src/rttt-send.cpp b/src/rttt-send.cpp new file mode 100644 index 0000000..c03ccc1 --- /dev/null +++ b/src/rttt-send.cpp @@ -0,0 +1,17 @@ +#include <iostream> + +#include "fmt/format.h" +#include <zmq.hpp> + +int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { + std::string message = ":log Hello from rttt-send"; + if (argc > 1) + message = std::string(argv[1]); + + std::cout << fmt::format("Sending message: {}", message) << std::endl; + + zmq::context_t ctx; + zmq::socket_t sock(ctx, zmq::socket_type::push); + sock.connect("ipc:///tmp/rttt.ipc"); + sock.send(zmq::buffer(message), zmq::send_flags::dontwait); +} diff --git a/src/rttt.hpp b/src/rttt.hpp index fe082dc..d1efc93 100644 --- a/src/rttt.hpp +++ b/src/rttt.hpp @@ -18,193 +18,194 @@ namespace rttt { -// TODO: move to thing::type -enum class SiteType : int { - Unknown, - HN, - Reddit, - RSS, - Twitter, -}; - -// Consider moving this to view.hpp -enum class list_mode : int { - story, - comment, - feed, -}; - -// TODO: should work with constexpr, but might need newer g++/clang++ -inline std::vector<std::string> split(std::string_view strv, - std::string_view delims = " ", size_t no_pieces = 0) { - std::vector<std::string> output; - size_t first = 0; - - while (first < strv.size()) { - const auto second = strv.find_first_of(delims, first); - - if (first != second) - output.emplace_back(strv.substr(first, second - first)); - - if (second == std::string_view::npos) { - break; - } else if (no_pieces > 0 && output.size() >= no_pieces - 1) { - output.emplace_back(strv.substr(second + 1, strv.size())); - break; + // TODO: move to thing::type + enum class SiteType : int { + Unknown, + HN, + Reddit, + RSS, + Twitter, + }; + + // Consider moving this to view.hpp + enum class list_mode : int { + story, + comment, + feed, + }; + + // TODO: should work with constexpr, but might need newer g++/clang++ + inline std::vector<std::string> split(std::string_view strv, + std::string_view delims = " ", + size_t no_pieces = 0) { + std::vector<std::string> output; + size_t first = 0; + + while (first < strv.size()) { + const auto second = strv.find_first_of(delims, first); + + if (first != second) + output.emplace_back(strv.substr(first, second - first)); + + if (second == std::string_view::npos) { + break; + } else if (no_pieces > 0 && output.size() >= no_pieces - 1) { + output.emplace_back(strv.substr(second + 1, strv.size())); + break; + } + + first = second + 1; } - first = second + 1; + return output; } - return output; -} - -inline void replaceAll(std::string &s, const std::string &search, - const std::string &replace) { - for (size_t pos = 0;; pos += replace.length()) { - pos = s.find(search, pos); - if (pos == std::string::npos) - break; - s.erase(pos, search.length()); - s.insert(pos, replace); + inline void replaceAll(std::string & s, const std::string &search, + const std::string &replace) { + for (size_t pos = 0;; pos += replace.length()) { + pos = s.find(search, pos); + if (pos == std::string::npos) + break; + s.erase(pos, search.length()); + s.insert(pos, replace); + } } -} - -inline std::string parse_html(std::string str) { - replaceAll(str, "<p>", "\n"); - - std::string res; - bool inTag = false; - for (auto &ch : str) { - if (ch == '<') { - inTag = true; - } else if (ch == '>') { - inTag = false; - } else { - if (inTag == false) { - res += ch; + + inline std::string parse_html(std::string str) { + replaceAll(str, "<p>", "\n"); + + std::string res; + bool inTag = false; + for (auto &ch : str) { + if (ch == '<') { + inTag = true; + } else if (ch == '>') { + inTag = false; + } else { + if (inTag == false) { + res += ch; + } } } + + replaceAll(res, "/", "/"); + replaceAll(res, "'", "'"); + replaceAll(res, ">", ">"); + replaceAll(res, "–", "-"); + replaceAll(res, "“", "\""); + replaceAll(res, "”", "\""); + replaceAll(res, "‘", "'"); + replaceAll(res, "’", "'"); + replaceAll(res, "„", "'"); + replaceAll(res, """, "\""); + replaceAll(res, "&", "&"); + replaceAll(res, "—", "-"); + + return res; } - replaceAll(res, "/", "/"); - replaceAll(res, "'", "'"); - replaceAll(res, ">", ">"); - replaceAll(res, "–", "-"); - replaceAll(res, "“", "\""); - replaceAll(res, "”", "\""); - replaceAll(res, "‘", "'"); - replaceAll(res, "’", "'"); - replaceAll(res, "„", "'"); - replaceAll(res, """, "\""); - replaceAll(res, "&", "&"); - replaceAll(res, "—", "-"); - - return res; -} - -inline std::tm parse_time(std::string time_string) { - std::tm time = {}; - std::regex e("(\\d{4})-(\\d+)-(\\d+)T(\\d+):(\\d+)"); - std::smatch sm; - std::regex_search(time_string, sm, e); - if (sm.size() > 1) { - time.tm_year = std::stoi(sm[1]) - 1900; - time.tm_mon = std::stoi(sm[2]) - 1; - time.tm_mday = std::stoi(sm[3]); - time.tm_hour = std::stoi(sm[4]); - time.tm_min = std::stoi(sm[5]); + inline std::tm parse_time(std::string time_string) { + std::tm time = {}; + std::regex e("(\\d{4})-(\\d+)-(\\d+)T(\\d+):(\\d+)"); + std::smatch sm; + std::regex_search(time_string, sm, e); + if (sm.size() > 1) { + time.tm_year = std::stoi(sm[1]) - 1900; + time.tm_mon = std::stoi(sm[2]) - 1; + time.tm_mday = std::stoi(sm[3]); + time.tm_hour = std::stoi(sm[4]); + time.tm_min = std::stoi(sm[5]); + } + return time; } - return time; -} - -// TODO: Move to text.hpp and rename -inline std::vector<std::string> extractURL(std::string text) { - std::vector<std::string> v; - - // Currently we assume an url always ends with alphanum, / or _ - // ([^/_[:alnum:]]) If this turns out to be false then we could also try to - // filter out trailing punctuation marks etc specifically ([);:.!?] std::regex - // e("(https?://[A-z0-9$–_.+!*‘(),./?=]+?)[),;:.!?]*(\\s|$)"); - // "(https?://[[:alnum:]$-_.+!*‘(),./?=;&#]+?)[^/_[:alnum:]]*(\\s|$)"); - std::regex e( - "(https?://[[:alnum:]$-_+!*‘,/?=;&#]+?)(\\]\\(|[^/_[:alnum:]]*(\\s|$))"); - std::smatch sm; - while (std::regex_search(text, sm, e)) { - if (sm.size() > 1) - v.push_back(sm[1]); - text = sm.suffix().str(); + + // TODO: Move to text.hpp and rename + inline std::vector<std::string> extractURL(std::string text) { + std::vector<std::string> v; + + // Currently we assume an url always ends with alphanum, / or _ + // ([^/_[:alnum:]]) If this turns out to be false then we could also try to + // filter out trailing punctuation marks etc specifically ([);:.!?] + // std::regex e("(https?://[A-z0-9$–_.+!*‘(),./?=]+?)[),;:.!?]*(\\s|$)"); + // "(https?://[[:alnum:]$-_.+!*‘(),./?=;&#]+?)[^/_[:alnum:]]*(\\s|$)"); + std::regex e("(https?://[[:alnum:]$-_+!*‘,/?=;&#]+?)(\\]\\(|[^/" + "_[:alnum:]]*(\\s|$))"); + std::smatch sm; + while (std::regex_search(text, sm, e)) { + if (sm.size() > 1) + v.push_back(sm[1]); + text = sm.suffix().str(); + } + + return v; } - return v; -} - -struct Path { - SiteType type = SiteType::Unknown; - std::string name; - std::string basename; - list_mode mode = list_mode::story; - std::string id; - std::vector<std::string> parts; -}; - -inline Path parse_path(const std::string &path_name) { - Path path; - path.parts = split(path_name, "/"); - auto &v = path.parts; - if (v[0] == "hn") { - path.type = SiteType::HN; - } else if (v[0] == "r") { - path.type = SiteType::Reddit; - } else if (v[0] == "rss") { - path.type = SiteType::RSS; - path.basename = "/rss"; - if (v.size() == 2 && v[1] != "front") { - path.mode = list_mode::feed; - path.id = v[1]; - path.name = path.basename + "/" + v[1]; - } else { - path.name = path.basename; - path.mode = list_mode::story; + struct Path { + SiteType type = SiteType::Unknown; + std::string name; + std::string basename; + list_mode mode = list_mode::story; + std::string id; + std::vector<std::string> parts; + }; + + inline Path parse_path(std::string_view path_name) { + Path path; + path.parts = split(path_name, "/"); + auto &v = path.parts; + if (v[0] == "hn") { + path.type = SiteType::HN; + } else if (v[0] == "r") { + path.type = SiteType::Reddit; + } else if (v[0] == "rss") { + path.type = SiteType::RSS; + path.basename = "/rss"; + if (v.size() == 2 && v[1] != "front") { + path.mode = list_mode::feed; + path.id = v[1]; + path.name = path.basename + "/" + v[1]; + } else { + path.name = path.basename; + path.mode = list_mode::story; + } + return path; } + if (v.size() == 4) { + if (v[2] == "comments") { + path.mode = list_mode::comment; + } else { + logger::log_ifnot(false); + } + path.name = "/" + v[0] + "/" + v[1] + "/" + v[2] + "/" + v[3]; + path.id = v[3]; + } + path.basename = "/" + v[0] + "/" + v[1]; + if (path.name.empty()) + path.name = path.basename; return path; } - if (v.size() == 4) { - if (v[2] == "comments") { - path.mode = list_mode::comment; - } else { - logger::log_ifnot(false); - } - path.name = "/" + v[0] + "/" + v[1] + "/" + v[2] + "/" + v[3]; - path.id = v[3]; + + inline std::string timeSince(uint64_t t) { + auto delta = current_time() - t; + if (delta < 60) + return std::to_string(delta) + " seconds"; + if (delta < 3600) + return std::to_string(delta / 60) + " minutes"; + if (delta < 24 * 3600) + return std::to_string(delta / 3600) + " hours"; + return std::to_string(delta / 24 / 3600) + " days"; } - path.basename = "/" + v[0] + "/" + v[1]; - if (path.name.empty()) - path.name = path.basename; - return path; -} - -inline std::string timeSince(uint64_t t) { - auto delta = current_time() - t; - if (delta < 60) - return std::to_string(delta) + " seconds"; - if (delta < 3600) - return std::to_string(delta / 60) + " minutes"; - if (delta < 24 * 3600) - return std::to_string(delta / 3600) + " hours"; - return std::to_string(delta / 24 / 3600) + " days"; -} - -inline int openInBrowser(std::string uri) { - auto config = rttt::config::load(); - auto base_cmd = config["open_command"].get<std::string>(); + + inline int openInBrowser(std::string uri) { + auto config = rttt::config::load(); + auto base_cmd = config["open_command"].get<std::string>(); #ifdef __APPLE__ - if (base_command == "xdg-open") - base_cmd = "open"; + if (base_command == "xdg-open") + base_cmd = "open"; #endif - // std::string cmd = "run-mailcap " + uri + " > /dev/null 2>&1"; - std::string cmd = base_cmd + " \"" + uri + "\" > /dev/null 2>&1 &"; - logger::push("EXECUTING: {}", cmd); - return system(cmd.c_str()); -} + // std::string cmd = "run-mailcap " + uri + " > /dev/null 2>&1"; + std::string cmd = base_cmd + " \"" + uri + "\" > /dev/null 2>&1 &"; + logger::push("EXECUTING: {}", cmd); + return system(cmd.c_str()); + } } // namespace rttt diff --git a/src/rttt/command.hpp b/src/rttt/command.hpp new file mode 100644 index 0000000..b7ebae9 --- /dev/null +++ b/src/rttt/command.hpp @@ -0,0 +1,211 @@ +#pragma once + +#include <string> +#include <functional> +#include <optional> +#include <map> +#include <memory> +#include <ranges> + +#include "fmt/format.h" + +#include "rttt/functional.hpp" +#include "rttt/text.hpp" + +namespace rttt { +namespace command { +struct command_state { + std::function<bool()> func; + std::function<bool(const std::string_view url)> func_with_arg; + + std::map<std::string, std::shared_ptr<command_state>> commands; + + std::optional<std::string> help; +}; + +using command = std::shared_ptr<command_state>; +using list = std::map<std::string, command>; + +auto help(const std::string &message) { + return with([message](command &cmd) -> command & { + if (!cmd) { + cmd = std::make_shared<command_state>(); + } + cmd->help = std::move(message); + return cmd; + }); +} + +auto function(const std::function<bool(const std::string_view argument)> &f) { + return with([f](command &cmd) -> command & { + if (!cmd) { + cmd = std::make_shared<command_state>(); + } + cmd->func_with_arg = f; + return cmd; + }); +} + +auto function(const std::function<bool()> &f) { + return with([f](command &cmd) -> command & { + if (!cmd) { + cmd = std::make_shared<command_state>(); + } + cmd->func = f; + return cmd; + }); +} + +void push_helper(list &commands, const std::string &name, command cmd) { + auto names = rttt::text::split(name, " ", 2); + if (names.size() == 1) { + commands.insert({names[0], cmd}); + } else { + if (!commands.contains(names[0])) { + auto new_cmd = std::make_shared<command_state>(); + commands.insert({names[0], new_cmd}); + } + push_helper(commands.at(names[0])->commands, names[1], cmd); + } +} + +auto push(const std::string &name, command cmd) { + return with([&name, cmd](list &commands) -> list & { + push_helper(commands, name, cmd); + return commands; + }); +} + +auto push(const std::string &name, + std::function<bool(const std::string_view argument)> f) { + return with([f, &name](list &commands) -> list & { + auto command = std::make_shared<command_state>() | function(f); + commands | push(name, command); + return commands; + }); +} + +auto push(const std::string &name, std::function<bool()> f) { + return with([f, &name](list &commands) -> list & { + auto command = std::make_shared<command_state>() | function(f); + commands | push(name, command); + return commands; + }); +} + +auto push(const std::string &name, list &subcommands) { + return with([&name, &subcommands](list &commands) -> list & { + auto command = std::make_shared<command_state>(); + command->commands = subcommands; + commands | push(name, command); + return commands; + }); +} + +auto push(const std::string &name, list &&subcommands) { + return with([&name, &subcommands](list &commands) -> list & { + auto command = std::make_shared<command_state>(); + command->commands = std::move(subcommands); + commands | push(name, command); + return commands; + }); +} + +with<std::function<bool(list &)>> +execute(const std::string &name, + const std::optional<std::string_view> arg = std::nullopt) { + std::function<bool(list &)> f = [&name, arg](list &commands) -> bool { + if (name.empty()) + return false; + + auto nm = name; + if (nm[0] == ':') + nm = nm.substr(1); + + if (!commands.contains(nm)) { + if (!arg) { + auto v = rttt::text::split(nm, " ", 2); + if (v.size() > 1) { + return commands | execute(v[0], v[1]); + } + } + return false; + } + + auto &cmd = commands[nm]; + + if (!arg) { + if (cmd->func) + return cmd->func(); + return false; + } + + auto v = rttt::text::split(arg.value(), " ", 2); + if (v.size() == 1 && cmd->commands | execute(v[0])) + return true; |