summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEdwin van Leeuwen <edwinvanl@tuta.io>2022-08-21 12:32:12 +0100
committerEdwin van Leeuwen <edwinvanl@tuta.io>2022-08-21 12:32:12 +0100
commit6d0d40cc6901237b6908b85133cf24d40879563a (patch)
treeea224f6012cdadd2db2d60c3fa6bbc90db696beb
parentc5b87af882a6e8ecff61d97882c73c94adb114d3 (diff)
parentbae499ead5c7a01f3348e0e789b0271341c7db5c (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--.gitignore4
-rw-r--r--.gitlab-ci.yml8
-rw-r--r--CMakeLists.txt21
-rw-r--r--README.md8
-rw-r--r--aur/PKGBUILD2
-rw-r--r--src/main.cpp135
-rw-r--r--src/rttt-send.cpp17
-rw-r--r--src/rttt.hpp343
-rw-r--r--src/rttt/command.hpp211
-rw-r--r--src/rttt/config.hpp28
-rw-r--r--src/rttt/functional.hpp141
-rw-r--r--src/rttt/hackernews.hpp4
-rw-r--r--src/rttt/reddit.hpp69
-rw-r--r--src/rttt/rss.hpp204
-rw-r--r--src/rttt/thing.hpp62
-rw-r--r--src/rttt/twitter.hpp4
-rw-r--r--src/rttt/ui_ftxui.hpp101
-rw-r--r--test/catch_command.cpp193
-rw-r--r--test/catch_functional.cpp171
-rw-r--r--test/catch_rss.cpp69
20 files changed, 1429 insertions, 366 deletions
diff --git a/.gitignore b/.gitignore
index 3c8bee4..cac06db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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)
diff --git a/README.md b/README.md
index a1a2ea2..fef9509 100644
--- a/README.md
+++ b/README.md
@@ -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, "&#x2F;", "/");
+ replaceAll(res, "&#x27;", "'");
+ replaceAll(res, "&gt;", ">");
+ replaceAll(res, "–", "-");
+ replaceAll(res, "“", "\"");
+ replaceAll(res, "”", "\"");
+ replaceAll(res, "‘", "'");
+ replaceAll(res, "’", "'");
+ replaceAll(res, "„", "'");
+ replaceAll(res, "&quot;", "\"");
+ replaceAll(res, "&amp;", "&");
+ replaceAll(res, "—", "-");
+
+ return res;
}
- replaceAll(res, "&#x2F;", "/");
- replaceAll(res, "&#x27;", "'");
- replaceAll(res, "&gt;", ">");
- replaceAll(res, "–", "-");
- replaceAll(res, "“", "\"");
- replaceAll(res, "”", "\"");
- replaceAll(res, "‘", "'");
- replaceAll(res, "’", "'");
- replaceAll(res, "„", "'");
- replaceAll(res, "&quot;", "\"");
- replaceAll(res, "&amp;", "&");
- 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;