diff options
author | Edwin van Leeuwen <edwinvanl@tuta.io> | 2022-05-02 14:12:03 +0100 |
---|---|---|
committer | Edwin van Leeuwen <edwinvanl@tuta.io> | 2022-05-02 14:12:03 +0100 |
commit | 21b9d2a9a35551229af05e43bd6895a3441094ed (patch) | |
tree | bb7c4b1695acdb0019921ec4afb529c0616ca2f0 | |
parent | 729a105ca8ee464a74f37a87787bf97a39ef9446 (diff) | |
parent | 99234a30439b0f7ff6d8587dcee329b0efd4093a (diff) |
Merge branch 'release/0.8.8'v0.8.8
-rw-r--r-- | src/config.hpp | 24 | ||||
-rw-r--r-- | src/main.cpp | 24 | ||||
-rw-r--r-- | src/prompt.hpp | 72 | ||||
-rw-r--r-- | src/reddit.hpp | 2 | ||||
-rw-r--r-- | src/ui.hpp | 95 | ||||
-rw-r--r-- | src/view.hpp | 79 | ||||
-rw-r--r-- | test/catch_prompt.cpp | 61 | ||||
-rw-r--r-- | test/catch_ui.cpp | 9 |
8 files changed, 250 insertions, 116 deletions
diff --git a/src/config.hpp b/src/config.hpp index b381dec..d4aa750 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -90,8 +90,7 @@ inline std::filesystem::path get_data_path() { } template<typename T> -nlohmann::json load_item_view_states_or(const T &default_states) { - std::filesystem::path fn = "item_view_state.json"; +nlohmann::json load_data_from_file_or(const T &default_states, const std::filesystem::path &fn) { auto path = get_data_path(); if (!std::filesystem::exists(path)) { std::filesystem::create_directory(path); @@ -110,8 +109,7 @@ nlohmann::json load_item_view_states_or(const T &default_states) { return nlohmann::json::parse(p_file); } -bool save_item_view_states(const nlohmann::json &json) { - std::filesystem::path fn = "item_view_state.json"; +bool save_to_data_path(const nlohmann::json &json, const std::filesystem::path &fn) { auto path = get_data_path() / fn; std::ofstream file; file.open(path); @@ -119,6 +117,24 @@ bool save_item_view_states(const nlohmann::json &json) { file.close(); return true; } + +template<typename T> +nlohmann::json load_item_view_states_or(const T &default_states) { + return load_data_from_file_or(default_states, "item_view_state.json"); +} + +bool save_item_view_states(const nlohmann::json &json) { + return save_to_data_path(json, "item_view_state.json"); +} + +template<typename T> +nlohmann::json load_prompt_state_or(const T &default_state) { + return load_data_from_file_or(default_state, "prompt_state.json"); +} + +bool save_prompt_state(const nlohmann::json &json) { + return save_to_data_path(json, "prompt_state.json"); +} } // namespace config } // namespace rttt diff --git a/src/main.cpp b/src/main.cpp index 8a85632..58728aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #include "rttt.hpp" #include "storage.hpp" #include "ui.hpp" +#include "view.hpp" // Figure out why this is needed #define EMSCRIPTEN_KEEPALIVE @@ -78,7 +79,7 @@ ui::WindowData switch_path(ui::WindowData &¤t_window, // Change default view if we switch to feed mode if (current_window.path.mode == rttt::list_mode::feed && prev_window_mode != rttt::list_mode::feed) - current_window.scroll_state.ui_view_mode = ui::view_mode::text; + current_window.scroll_state.ui_view_mode = view::mode::text; path_cache.insert({current_window.path.name, current_window}); } @@ -136,7 +137,7 @@ auto dispatch_drawing_comments(ui::WindowData &&window) { constexpr auto dig = [](auto &item) -> std::vector<ItemVariant> * { auto maybe_id = try_id_string(item); if (!maybe_id.has_value() || - ui::is_collapsed(state_ui.item_view_states, maybe_id.value())) + view::is_collapsed(state_ui.item_view_states, maybe_id.value())) return nullptr; if (std::holds_alternative<reddit::Story>(item)) @@ -157,7 +158,7 @@ auto dispatch_drawing_comments(ui::WindowData &&window) { std::vector<int> item_ids{id}; auto dig = [](auto &m_id) -> std::vector<int> * { if (!state_hn.items.contains(m_id) || - ui::is_collapsed(state_ui.item_view_states, std::to_string(m_id))) + view::is_collapsed(state_ui.item_view_states, std::to_string(m_id))) return nullptr; auto &item = state_hn.items.at(m_id); if (std::holds_alternative<hackernews::Story>(item)) { @@ -356,7 +357,7 @@ also change the number of panels/windows rttt::try_id_string(window.scroll_state.highlighted_item); // When we go back to the main window we mark the item as read if (maybe_id) - state_ui.item_view_states = ui::mark_read( + state_ui.item_view_states = view::mark_read( std::move(state_ui.item_view_states), maybe_id.value()); } @@ -364,7 +365,7 @@ also change the number of panels/windows auto maybe_id = rttt::try_id_string(window.scroll_state.highlighted_item); if (maybe_id.has_value()) { - state_ui.item_view_states = ui::toggle_collapsed( + state_ui.item_view_states = view::toggle_collapsed( std::move(state_ui.item_view_states), maybe_id.value()); } } @@ -467,9 +468,7 @@ also change the number of panels/windows std::move(state_ui.windows[state_ui.hoveredWindowId]), state_ui.windows[state_ui.hoveredWindowId].path.name, path_name); - state_prompt.history.push_back(path_name); - state_prompt.history_id = state_prompt.history.size(); - state_prompt.completer.matches.insert(path_name); + state_prompt = prompt::push(std::move(state_prompt), path_name); } if (ImGui::IsKeyPressed(ImGui::GetIO().KeyMap[ImGuiKey_Escape], false)) { @@ -589,6 +588,8 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { state_reddit = rttt::reddit::retrieve_access_token(std::move(state_reddit), config); + // TODO: only load these if we don't have state saved in + // load_prompt_state_or() auto opml_path = rttt::config::getPath() / "subscriptions.opml"; if (std::filesystem::exists(opml_path)) { state_rss = rttt::rss::state(opml_path); @@ -597,10 +598,11 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { state_prompt.completer.matches.insert(known_paths.begin(), known_paths.end()); } + state_prompt = config::load_prompt_state_or(std::move(state_prompt)); state_ui.item_view_states = config::load_item_view_states_or( - rttt::active_storage<std::string, ui::item_view_state>(0, 10, - 3 * 24 * 3600)); + rttt::active_storage<std::string, view::item_state>(0, 10, + 3 * 24 * 3600)); path_cache = rttt::active_storage<std::string, ui::WindowData>( 300, 5, 3700, @@ -631,6 +633,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { ++heartbeat; if (heartbeat % 5000 == 0) { config::save_item_view_states(state_ui.item_view_states); + config::save_prompt_state(state_prompt); } if (render_frame() == false) break; @@ -641,5 +644,6 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char **argv) { ImGui::DestroyContext(); config::save_item_view_states(state_ui.item_view_states); + config::save_prompt_state(state_prompt); return 0; } diff --git a/src/prompt.hpp b/src/prompt.hpp index 104d932..bcbc2ae 100644 --- a/src/prompt.hpp +++ b/src/prompt.hpp @@ -1,6 +1,7 @@ #pragma once #include <cstddef> +#include <regex> #include <set> #include <string> #include <tuple> @@ -9,6 +10,8 @@ #include "imgui/imgui.h" #include "imtui/imtui.h" +#include <nlohmann/json.hpp> + namespace ImGui { // Inspired by https://eliasdaler.github.io/using-imgui-with-sfml-pt2/ @@ -26,25 +29,40 @@ bool InputTextPrompt(const char *label, char *buf, size_t buf_size, namespace rttt { namespace prompt { +struct compare_lower_case { + bool operator()(const std::string &a, const std::string &b) const { + for (size_t i = 0; i < std::min(a.size(), b.size()); ++i) { + auto la = std::tolower(a[i]); + auto lb = std::tolower(b[i]); + if (la == lb) + continue; + return la < lb; + } + return a.size() < b.size(); + } +}; + struct completer { - std::set<std::string> matches; + std::set<std::string, prompt::compare_lower_case> matches; - std::string_view next(const std::string_view &partial) { + std::string_view next(const std::string &partial) { static decltype(matches)::iterator it; + std::regex re("^" + partial, std::regex::icase); // Check for size in case new matches have been added - if (std::get<0>(current_state) != partial || std::get<2>(current_state) != matches.size()) { - it = std::find_if(matches.begin(), matches.end(), - [&partial](std::string_view match) { - return match.substr(0, partial.size()) == partial; - }); + if (std::get<0>(current_state) != partial || + std::get<2>(current_state) != matches.size()) { + it = std::find_if( + matches.begin(), matches.end(), + [&re](std::string match) { return std::regex_search(match, re); }); current_state = - std::tuple<std::string, decltype(matches)::iterator, size_t>{partial, it, matches.size()}; + std::tuple<std::string, decltype(matches)::iterator, size_t>{ + partial, it, matches.size()}; if (it == matches.end()) return {}; return (*it); } else { ++it; - if (it == matches.end() || it->substr(0, partial.size()) != partial) { + if (it == matches.end() || !std::regex_search((*it), re)) { // Restart at the beginning it = std::get<1>(current_state); if (it == matches.end()) @@ -60,16 +78,48 @@ private: std::tuple<std::string, decltype(matches)::iterator, size_t> current_state; }; +void to_json(nlohmann::json &j, const completer &s) { + j = nlohmann::json{{"matches", s.matches}}; +} + +void from_json(const nlohmann::json &j, completer &s) { + j.at("matches").get_to(s.matches); +} + struct state { std::vector<std::string> history; size_t history_id = 0; prompt::completer completer; }; +state push(state &&state, const std::string &path) { + auto it = std::remove_if( + state.history.begin(), state.history.end(), + [&path](const auto &element) -> bool { return element == path; }); + state.history.erase(it, state.history.end()); + state.history.push_back(path); + state.history_id = state.history.size(); + state.completer.matches.insert(path); + return std::move(state); +} + +void to_json(nlohmann::json &j, const state &s) { + j = nlohmann::json{{"history", s.history}, + {"history_id", s.history_id}, + {"completer", s.completer}}; +} + +void from_json(const nlohmann::json &j, state &s) { + j.at("history").get_to(s.history); + j.at("history_id").get_to(s.history_id); + j.at("completer").get_to(s.completer); +} + std::tuple<ImGuiInputTextCallbackData *, state> handle_input(ImGuiInputTextCallbackData *data, state &&state) { if (data->EventKey == ImGuiKey_Tab) { - auto m = state.completer.next(std::string(data->Buf).substr(0, data->CursorPos)); + auto m = + state.completer.next(std::string(data->Buf).substr(0, data->CursorPos)); if (!m.empty()) { strcpy(data->Buf, m.data()); data->BufTextLen = m.size(); @@ -81,7 +131,7 @@ handle_input(ImGuiInputTextCallbackData *data, state &&state) { } else if (data->EventKey == ImGuiKey_DownArrow && state.history_id < state.history.size() - 1) { ++state.history_id; - } + } if (!state.history.empty()) { strcpy(data->Buf, state.history[state.history_id].c_str()); diff --git a/src/reddit.hpp b/src/reddit.hpp index fe48b4a..2981e0d 100644 --- a/src/reddit.hpp +++ b/src/reddit.hpp @@ -24,7 +24,7 @@ namespace rttt { namespace reddit { struct credentials { - cpr::Header header = cpr::Header{{"User-Agent", "rttt/0.8.7"}}; + cpr::Header header = cpr::Header{{"User-Agent", "rttt/0.8.8"}}; std::string client_id; std::string device_id; std::string refresh_token; @@ -7,19 +7,13 @@ #include "logger.hpp" #include "rttt.hpp" +#include "view.hpp" #include "imgui/imgui.h" namespace rttt { namespace ui { -enum class view_mode : int { - normal, - text, - debug, - COUNT, -}; - enum class ColorScheme : int { Default, Dark, @@ -40,74 +34,9 @@ struct scroll_window_state { float large_item_offset = 0; float highlighted_item_size = 0; - view_mode ui_view_mode = ui::view_mode::normal; + view::mode ui_view_mode = view::mode::normal; }; -struct item_view_state { - bool collapsed = false; - bool read = false; -}; - -void to_json(nlohmann::json &j, const item_view_state &s) { - j = nlohmann::json{{"collapsed", s.collapsed}, {"read", s.read}}; -} - -void from_json(const nlohmann::json &j, item_view_state &s) { - j.at("collapsed").get_to(s.collapsed); - j.at("read").get_to(s.read); -} - -bool is_collapsed( - const rttt::active_storage<std::string, item_view_state> &state, - const std::string &id) { - if (state.contains(id)) - return state.at(id).collapsed; - return false; -} - -bool is_read(const rttt::active_storage<std::string, item_view_state> &state, - const std::string &id) { - if (state.contains(id)) - return state.at(id).read; - return false; -} - -rttt::active_storage<std::string, item_view_state> -toggle_collapsed(rttt::active_storage<std::string, item_view_state> &&state, - const std::string &id) { - if (state.contains(id)) { - auto &v = state.at(id); - v.collapsed = !v.collapsed; - if (v.read == false && v.collapsed == false) - state.erase(id); - } else { - state.insert({id, item_view_state{true, false}}); - } - return state; -} - -rttt::active_storage<std::string, item_view_state> -toggle_read(rttt::active_storage<std::string, item_view_state> &&state, - const std::string &id) { - if (state.contains(id)) { - auto &v = state.at(id); - v.read = !v.read; - if (v.read == false && v.collapsed == false) - state.erase(id); - } else { - state.insert({id, item_view_state{false, true}}); - } - return state; -} - -rttt::active_storage<std::string, item_view_state> -mark_read(rttt::active_storage<std::string, item_view_state> &&state, - const std::string &id) { - if (!is_read(state, id)) - return toggle_read(std::move(state), id); - return std::move(state); -} - struct WindowData { rttt::Path path; rttt::ui::scroll_window_state scroll_state; @@ -136,7 +65,7 @@ struct state { // Keeping this around for a long time. Should check how that is for // performance. Note should only add it when read/or collapsed. Ideally remove // if/when neither again - rttt::active_storage<std::string, item_view_state> item_view_states; + rttt::active_storage<std::string, view::item_state> item_view_states; void changeColorScheme(bool inc = true) { if (inc) { @@ -305,7 +234,7 @@ inline void drawHighlightBar(float top) { } template <typename T> -void drawStoryData(const T &data, size_t id, bool isHighlighted, view_mode mode, +void drawStoryData(const T &data, size_t id, bool isHighlighted, view::mode mode, bool read) { auto top = ImGui::GetCursorPosY(); ImGui::Indent(1); @@ -339,7 +268,7 @@ void drawStoryData(const T &data, size_t id, bool isHighlighted, view_mode mode, data.descendants); } - if (mode == view_mode::text && !data.text.empty()) { + if (mode == view::mode::text && !data.text.empty()) { ImGui::NewLine(); ImGui::PushTextWrapPos(ImGui::GetContentRegionAvailWidth() - 2); ImGui::TextDisabled("%s", data.text.c_str()); @@ -422,8 +351,8 @@ void commentModeDrawComment(const T &comment, int indent, bool isHighlighted, */ inline void drawItem(const ItemVariant &item, size_t id, size_t indent, bool isHighlighted, rttt::list_mode mode, - ui::view_mode view_mode, - const ui::item_view_state item_view_state) { + view::mode view_mode, + const view::item_state item_view_state) { if (std::holds_alternative<unknown_type>(item)) { logger::push_back("Unknown item"); return; @@ -465,7 +394,7 @@ inline void drawItem(const ItemVariant &item, size_t id, size_t indent, template <typename T> scroll_window_state drawItems( scroll_window_state &&state, T &items, const rttt::list_mode &list_mode, - rttt::active_storage<std::string, ui::item_view_state> &item_view_states) { + rttt::active_storage<std::string, view::item_state> &item_view_states) { size_t counter = 0; ImGui::SetScrollY(state.scroll_top); @@ -525,7 +454,7 @@ scroll_window_state drawItems( item_view_states.mark_active(maybe_id.value()); } else { drawItem(item, counter, indent, isHighlighted, list_mode, - state.ui_view_mode, ui::item_view_state()); + state.ui_view_mode, view::item_state()); } if (isHighlighted) { @@ -565,7 +494,7 @@ scroll_window_state drawItems( auto maybe_id = try_id_string(state.highlighted_item); if (maybe_id) item_view_states = - ui::toggle_read(std::move(item_view_states), maybe_id.value()); + view::toggle_read(std::move(item_view_states), maybe_id.value()); } // Bind keys @@ -620,8 +549,8 @@ scroll_window_state drawItems( // TODO: Should this really be here? This function is really about the scrolling etc... // While the key is more about window status if (ImGui::IsKeyReleased('v')) { - state.ui_view_mode = (ui::view_mode)(((int)(state.ui_view_mode) + 1) % - ((int)(ui::view_mode::COUNT))); + state.ui_view_mode = (view::mode)(((int)(state.ui_view_mode) + 1) % + ((int)(view::mode::COUNT))); state.large_item_mode = false; state.large_item_offset = 0; } diff --git a/src/view.hpp b/src/view.hpp new file mode 100644 index 0000000..45ce3f9 --- /dev/null +++ b/src/view.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include "nlohmann/json.hpp" + +#include "storage.hpp" + +namespace view { +enum class mode : int { + normal, + text, + debug, + COUNT, +}; + +struct item_state { + bool collapsed = false; + bool read = false; +}; + +void to_json(nlohmann::json &j, const item_state &s) { + j = nlohmann::json{{"collapsed", s.collapsed}, {"read", s.read}}; +} + +void from_json(const nlohmann::json &j, item_state &s) { + j.at("collapsed").get_to(s.collapsed); + j.at("read").get_to(s.read); +} + +bool is_collapsed( + const rttt::active_storage<std::string, item_state> &state, + const std::string &id) { + if (state.contains(id)) + return state.at(id).collapsed; + return false; +} + +bool is_read(const rttt::active_storage<std::string, item_state> &state, + const std::string &id) { + if (state.contains(id)) + return state.at(id).read; + return false; +} + +rttt::active_storage<std::string, item_state> +toggle_collapsed(rttt::active_storage<std::string, item_state> &&state, + const std::string &id) { + if (state.contains(id)) { + auto &v = state.at(id); + v.collapsed = !v.collapsed; + if (v.read == false && v.collapsed == false) + state.erase(id); + } else { + state.insert({id, item_state{true, false}}); + } + return state; +} + +rttt::active_storage<std::string, item_state> +toggle_read(rttt::active_storage<std::string, item_state> &&state, + const std::string &id) { + if (state.contains(id)) { + auto &v = state.at(id); + v.read = !v.read; + if (v.read == false && v.collapsed == false) + state.erase(id); + } else { + state.insert({id, item_state{false, true}}); + } + return state; +} + +rttt::active_storage<std::string, item_state> +mark_read(rttt::active_storage<std::string, item_state> &&state, + const std::string &id) { + if (!is_read(state, id)) + return toggle_read(std::move(state), id); + return std::move(state); +} +} diff --git a/test/catch_prompt.cpp b/test/catch_prompt.cpp index 795811b..ba85fd4 100644 --- a/test/catch_prompt.cpp +++ b/test/catch_prompt.cpp @@ -2,6 +2,8 @@ #include "catch2/catch.hpp" +#include <nlohmann/json.hpp> + #include "prompt.hpp" #include <set> @@ -30,3 +32,62 @@ SCENARIO("Completer works as expected") { REQUIRE(cmpl.next("zz") == ""); } + +SCENARIO("Completer works correctly with lower and uppercase") { + prompt::completer cmpl; + REQUIRE(cmpl.next("a") == ""); + + cmpl.matches = {"aaa", "abb", "Aba", "aab", "baa"}; + + REQUIRE(cmpl.next("a") == "aaa"); + REQUIRE(cmpl.next("a") == "aab"); + REQUIRE(cmpl.next("a") == "Aba"); + REQUIRE(cmpl.next("a") == "abb"); + REQUIRE(cmpl.next("a") == "aaa"); + + REQUIRE(cmpl.next("aa") == "aaa"); + REQUIRE(cmpl.next("aa") == "aab"); + REQUIRE(cmpl.next("aa") == "aaa"); + + REQUIRE(cmpl.next("b") == "baa"); + REQUIRE(cmpl.next("b") == "baa"); + + // Should have reset after the previous match + REQUIRE(cmpl.next("aa") == "aaa"); + + REQUIRE(cmpl.next("aB") == "Aba"); + + REQUIRE(cmpl.next("zz") == ""); +} + +SCENARIO("We can convert the state to and from json") { + prompt::state state; + state.history = {"a", "b"}; + state.completer.matches = {"a", "b", "c"}; + state.history_id = 2; + nlohmann::json j = state; + REQUIRE(j["history"].size() == 2); + REQUIRE(j["completer"]["matches"].size() == 3); + REQUIRE(j["history_id"] == 2); + + prompt::state state_from_json = j; + + REQUIRE(state_from_json.history.size() == 2); + REQUIRE(state_from_json.completer.matches.size() == 3); + REQUIRE(state_from_json.history_id == 2); +} + +SCENARIO("History only saves the last entry of a duplicate") { + prompt::state state; + state.history = {"a", "b"}; + state.completer.matches = {"a", "b", "c"}; + state.history_id = 2; + state = prompt::push(std::move(state), "c"); + REQUIRE(state.history.size() == 3); + REQUIRE(state.history[state.history_id - 1] == "c"); + + state = prompt::push(std::move(state), "a"); + REQUIRE(state.history.size() == 3); + REQUIRE(state.history[state.history_id - 1] == "a"); +} + diff --git a/test/catch_ui.cpp b/test/catch_ui.cpp index 6b8708f..bd75c71 100644 --- a/test/catch_ui.cpp +++ b/test/catch_ui.cpp @@ -1,18 +1,13 @@ -/*struct item_view_state { - bool collapsed = false; - bool read = false; -};*/ - #define CATCH_CONFIG_MAIN #include "catch2/catch.hpp" -#include "ui.hpp" +#include "view.hpp" using namespace rttt; SCENARIO("We can convert item_view_state to and from json") { - ui::item_view_state vs(true, false); + view::item_state vs(true, false); nlohmann::json j = vs; REQUIRE(j["collapsed"] == true); REQUIRE(j["read"] == false); |