summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEdwin van Leeuwen <edwinvanl@tuta.io>2022-05-02 14:12:03 +0100
committerEdwin van Leeuwen <edwinvanl@tuta.io>2022-05-02 14:12:03 +0100
commit21b9d2a9a35551229af05e43bd6895a3441094ed (patch)
treebb7c4b1695acdb0019921ec4afb529c0616ca2f0
parent729a105ca8ee464a74f37a87787bf97a39ef9446 (diff)
parent99234a30439b0f7ff6d8587dcee329b0efd4093a (diff)
Merge branch 'release/0.8.8'v0.8.8
-rw-r--r--src/config.hpp24
-rw-r--r--src/main.cpp24
-rw-r--r--src/prompt.hpp72
-rw-r--r--src/reddit.hpp2
-rw-r--r--src/ui.hpp95
-rw-r--r--src/view.hpp79
-rw-r--r--test/catch_prompt.cpp61
-rw-r--r--test/catch_ui.cpp9
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 &&current_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;
diff --git a/src/ui.hpp b/src/ui.hpp
index 4b82f68..de77614 100644
--- a/src/ui.hpp
+++ b/src/ui.hpp
@@ -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);