From 07c6cebbabbbbe4153ca7048f3cd33b8ef9b56fa Mon Sep 17 00:00:00 2001 From: Lucas CHOLLET Date: Wed, 8 Mar 2023 16:13:42 -0500 Subject: [PATCH] Applets/ClipboardHistory: Add persistent storage Clipboard entries are now preserved upon reboot :^). Unfortunately, it only supports data with the mimetype "text/". This is done by writing all entries as a JSON object in a file located in ~/.data. Co-authored-by: Sagittarius-a --- .../ClipboardHistoryModel.cpp | 97 ++++++++++++++++++- .../ClipboardHistory/ClipboardHistoryModel.h | 10 ++ Userland/Applets/ClipboardHistory/main.cpp | 16 ++- Userland/Libraries/LibGUI/Clipboard.cpp | 23 +++++ Userland/Libraries/LibGUI/Clipboard.h | 4 + 5 files changed, 143 insertions(+), 7 deletions(-) diff --git a/Userland/Applets/ClipboardHistory/ClipboardHistoryModel.cpp b/Userland/Applets/ClipboardHistory/ClipboardHistoryModel.cpp index ecb6f5453c..f29f52285d 100644 --- a/Userland/Applets/ClipboardHistory/ClipboardHistoryModel.cpp +++ b/Userland/Applets/ClipboardHistory/ClipboardHistoryModel.cpp @@ -7,6 +7,7 @@ */ #include "ClipboardHistoryModel.h" +#include #include #include #include @@ -117,29 +118,40 @@ void ClipboardHistoryModel::clipboard_content_did_change(DeprecatedString const& add_item(data_and_type); } +ErrorOr ClipboardHistoryModel::invalidate_model_and_file(bool rewrite_all) +{ + invalidate(); + + TRY(write_to_file(rewrite_all)); + return {}; +} + void ClipboardHistoryModel::add_item(const GUI::Clipboard::DataAndType& item) { + bool has_deleted_an_item = false; m_history_items.remove_first_matching([&](ClipboardItem& existing) { return existing.data_and_type.data == item.data && existing.data_and_type.mime_type == item.mime_type; }); - if (m_history_items.size() == m_history_limit) + if (m_history_items.size() == m_history_limit) { m_history_items.take_last(); + has_deleted_an_item = true; + } m_history_items.prepend({ item, Core::DateTime::now() }); - invalidate(); + invalidate_model_and_file(has_deleted_an_item).release_value_but_fixme_should_propagate_errors(); } void ClipboardHistoryModel::remove_item(int index) { m_history_items.remove(index); - invalidate(); + invalidate_model_and_file(true).release_value_but_fixme_should_propagate_errors(); } void ClipboardHistoryModel::clear() { m_history_items.clear(); - invalidate(); + invalidate_model_and_file(true).release_value_but_fixme_should_propagate_errors(); } void ClipboardHistoryModel::config_string_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, DeprecatedString const& value_string) @@ -155,9 +167,84 @@ void ClipboardHistoryModel::config_string_did_change(DeprecatedString const& dom auto value = value_or_error.value(); if (value < (int)m_history_items.size()) { m_history_items.remove(value, m_history_items.size() - value); - invalidate(); + invalidate_model_and_file(false).release_value_but_fixme_should_propagate_errors(); } m_history_limit = value; return; } } + +ErrorOr ClipboardHistoryModel::ClipboardItem::from_json(JsonObject const& object) +{ + if (!object.has("data_and_type"sv) && !object.has("time"sv)) + return Error::from_string_literal("JsonObject does not contain necessary fields"); + + ClipboardItem result; + result.data_and_type = TRY(GUI::Clipboard::DataAndType::from_json(*object.get_object("data_and_type"sv))); + result.time = Core::DateTime::from_timestamp(*object.get_integer("time"sv)); + + return result; +} + +ErrorOr ClipboardHistoryModel::ClipboardItem::to_json() const +{ + JsonObject object; + + object.set("data_and_type", TRY(data_and_type.to_json())); + object.set("time", time.timestamp()); + + return object; +} + +ErrorOr ClipboardHistoryModel::read_from_file(DeprecatedString const& path) +{ + m_path = path; + + auto read_from_file_impl = [this]() -> ErrorOr { + auto file = TRY(Core::File::open(m_path, Core::File::OpenMode::Read)); + auto buffered_file = TRY(Core::BufferedFile::create(move(file))); + + auto buffer = TRY(ByteBuffer::create_uninitialized(PAGE_SIZE)); + + while (TRY(buffered_file->can_read_line())) { + auto line = TRY(buffered_file->read_line(buffer)); + auto object = TRY(JsonParser { line }.parse()).as_object(); + TRY(m_history_items.try_append(TRY(ClipboardItem::from_json(object)))); + } + return {}; + }; + + auto maybe_error = read_from_file_impl(); + if (maybe_error.is_error()) + dbgln("Unable to load clipboard history: {}", maybe_error.release_error()); + + return {}; +} + +ErrorOr ClipboardHistoryModel::write_to_file(bool rewrite_all) +{ + if (m_history_items.is_empty()) { + // This will proceed to empty the file + rewrite_all = true; + } + + auto const write_element = [](Core::File& file, ClipboardItem const& item) -> ErrorOr { + if (!item.data_and_type.mime_type.starts_with("text/"sv)) + return {}; + TRY(file.write_until_depleted(TRY(item.to_json()).to_deprecated_string().bytes())); + TRY(file.write_until_depleted("\n"sv.bytes())); + return {}; + }; + + if (!rewrite_all) { + auto file = TRY(Core::File::open(m_path, Core::File::OpenMode::Write | Core::File::OpenMode::Append)); + TRY(write_element(*file, m_history_items.first())); + } else { + auto file = TRY(Core::File::open(m_path, Core::File::OpenMode::Write | Core::File::OpenMode::Truncate)); + for (auto const& item : m_history_items) { + TRY(write_element(*file, item)); + } + } + + return {}; +} diff --git a/Userland/Applets/ClipboardHistory/ClipboardHistoryModel.h b/Userland/Applets/ClipboardHistory/ClipboardHistoryModel.h index 0a28dd1ab1..344548b7cc 100644 --- a/Userland/Applets/ClipboardHistory/ClipboardHistoryModel.h +++ b/Userland/Applets/ClipboardHistory/ClipboardHistoryModel.h @@ -31,6 +31,9 @@ public: struct ClipboardItem { GUI::Clipboard::DataAndType data_and_type; Core::DateTime time; + + static ErrorOr from_json(JsonObject const& object); + ErrorOr to_json() const; }; virtual ~ClipboardHistoryModel() override = default; @@ -41,6 +44,11 @@ public: void clear(); bool is_empty() { return m_history_items.is_empty(); } + ErrorOr read_from_file(DeprecatedString const& path); + ErrorOr write_to_file(bool rewrite_all); + + ErrorOr invalidate_model_and_file(bool rewrite_all); + // ^GUI::Model virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override; @@ -60,4 +68,6 @@ private: Vector m_history_items; size_t m_history_limit; + + DeprecatedString m_path; }; diff --git a/Userland/Applets/ClipboardHistory/main.cpp b/Userland/Applets/ClipboardHistory/main.cpp index 95a16d105b..c4bf072c21 100644 --- a/Userland/Applets/ClipboardHistory/main.cpp +++ b/Userland/Applets/ClipboardHistory/main.cpp @@ -6,6 +6,8 @@ #include "ClipboardHistoryModel.h" #include +#include +#include #include #include #include @@ -17,14 +19,22 @@ ErrorOr serenity_main(Main::Arguments arguments) { - TRY(Core::System::pledge("stdio recvfd sendfd rpath unix")); + TRY(Core::System::pledge("stdio recvfd sendfd rpath unix cpath wpath")); auto app = TRY(GUI::Application::create(arguments)); + auto clipboard_config = TRY(Core::ConfigFile::open_for_app("ClipboardHistory")); + + auto const default_path = DeprecatedString::formatted("{}/{}", Core::StandardPaths::data_directory(), "Clipboard/ClipboardHistory.json"sv); + auto const clipboard_file_path = clipboard_config->read_entry("Clipboard", "ClipboardFilePath", default_path); + auto const parent_path = LexicalPath(clipboard_file_path); + TRY(Core::Directory::create(parent_path.dirname(), Core::Directory::CreateDirectories::Yes)); Config::pledge_domain("ClipboardHistory"); Config::monitor_domain("ClipboardHistory"); - TRY(Core::System::pledge("stdio recvfd sendfd rpath")); + TRY(Core::System::pledge("stdio recvfd sendfd rpath cpath wpath")); TRY(Core::System::unveil("/res", "r")); + TRY(Core::System::unveil(parent_path.dirname(), "rwc"sv)); + TRY(Core::System::unveil(nullptr, nullptr)); auto app_icon = TRY(GUI::Icon::try_create_default_icon("edit-copy"sv)); @@ -36,6 +46,8 @@ ErrorOr serenity_main(Main::Arguments arguments) auto table_view = TRY(main_window->set_main_widget()); auto model = ClipboardHistoryModel::create(); + TRY(model->read_from_file(clipboard_file_path)); + auto data_and_type = GUI::Clipboard::the().fetch_data_and_type(); if (!(data_and_type.data.is_empty() && data_and_type.mime_type.is_empty() && data_and_type.metadata.is_empty())) model->add_item(data_and_type); diff --git a/Userland/Libraries/LibGUI/Clipboard.cpp b/Userland/Libraries/LibGUI/Clipboard.cpp index 54e1cdcc59..348287620f 100644 --- a/Userland/Libraries/LibGUI/Clipboard.cpp +++ b/Userland/Libraries/LibGUI/Clipboard.cpp @@ -124,6 +124,29 @@ RefPtr Clipboard::DataAndType::as_bitmap() const return bitmap; } +ErrorOr Clipboard::DataAndType::from_json(JsonObject const& object) +{ + if (!object.has("data"sv) && !object.has("mime_type"sv)) + return Error::from_string_literal("JsonObject does not contain necessary fields"); + + DataAndType result; + result.data = object.get_deprecated_string("data"sv)->to_byte_buffer(); + result.mime_type = *object.get_deprecated_string("mime_type"sv); + // FIXME: Also read metadata + + return result; +} + +ErrorOr Clipboard::DataAndType::to_json() const +{ + JsonObject object; + object.set("data", TRY(DeprecatedString::from_utf8(data.bytes()))); + object.set("mime_type", mime_type); + // FIXME: Also write metadata + + return object; +} + void Clipboard::set_data(ReadonlyBytes data, DeprecatedString const& type, HashMap const& metadata) { if (data.is_empty()) { diff --git a/Userland/Libraries/LibGUI/Clipboard.h b/Userland/Libraries/LibGUI/Clipboard.h index 7b2e8a3718..95dc00b8b2 100644 --- a/Userland/Libraries/LibGUI/Clipboard.h +++ b/Userland/Libraries/LibGUI/Clipboard.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -34,6 +35,9 @@ public: HashMap metadata; RefPtr as_bitmap() const; + + static ErrorOr from_json(JsonObject const& object); + ErrorOr to_json() const; }; static ErrorOr initialize(Badge);