mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 03:27:45 +00:00
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 <sagittarius-a@users.noreply.github.com>
This commit is contained in:
parent
c09d0c4816
commit
07c6cebbab
5 changed files with 143 additions and 7 deletions
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ClipboardHistoryModel.h"
|
#include "ClipboardHistoryModel.h"
|
||||||
|
#include <AK/JsonParser.h>
|
||||||
#include <AK/NumberFormat.h>
|
#include <AK/NumberFormat.h>
|
||||||
#include <AK/StringBuilder.h>
|
#include <AK/StringBuilder.h>
|
||||||
#include <LibConfig/Client.h>
|
#include <LibConfig/Client.h>
|
||||||
|
@ -117,29 +118,40 @@ void ClipboardHistoryModel::clipboard_content_did_change(DeprecatedString const&
|
||||||
add_item(data_and_type);
|
add_item(data_and_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ErrorOr<void> 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)
|
void ClipboardHistoryModel::add_item(const GUI::Clipboard::DataAndType& item)
|
||||||
{
|
{
|
||||||
|
bool has_deleted_an_item = false;
|
||||||
m_history_items.remove_first_matching([&](ClipboardItem& existing) {
|
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;
|
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();
|
m_history_items.take_last();
|
||||||
|
has_deleted_an_item = true;
|
||||||
|
}
|
||||||
|
|
||||||
m_history_items.prepend({ item, Core::DateTime::now() });
|
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)
|
void ClipboardHistoryModel::remove_item(int index)
|
||||||
{
|
{
|
||||||
m_history_items.remove(index);
|
m_history_items.remove(index);
|
||||||
invalidate();
|
invalidate_model_and_file(true).release_value_but_fixme_should_propagate_errors();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClipboardHistoryModel::clear()
|
void ClipboardHistoryModel::clear()
|
||||||
{
|
{
|
||||||
m_history_items.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)
|
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();
|
auto value = value_or_error.value();
|
||||||
if (value < (int)m_history_items.size()) {
|
if (value < (int)m_history_items.size()) {
|
||||||
m_history_items.remove(value, m_history_items.size() - value);
|
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;
|
m_history_limit = value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ErrorOr<ClipboardHistoryModel::ClipboardItem> 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_t>("time"sv));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorOr<JsonObject> 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<void> ClipboardHistoryModel::read_from_file(DeprecatedString const& path)
|
||||||
|
{
|
||||||
|
m_path = path;
|
||||||
|
|
||||||
|
auto read_from_file_impl = [this]() -> ErrorOr<void> {
|
||||||
|
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<void> 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<void> {
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
|
@ -31,6 +31,9 @@ public:
|
||||||
struct ClipboardItem {
|
struct ClipboardItem {
|
||||||
GUI::Clipboard::DataAndType data_and_type;
|
GUI::Clipboard::DataAndType data_and_type;
|
||||||
Core::DateTime time;
|
Core::DateTime time;
|
||||||
|
|
||||||
|
static ErrorOr<ClipboardItem> from_json(JsonObject const& object);
|
||||||
|
ErrorOr<JsonObject> to_json() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
virtual ~ClipboardHistoryModel() override = default;
|
virtual ~ClipboardHistoryModel() override = default;
|
||||||
|
@ -41,6 +44,11 @@ public:
|
||||||
void clear();
|
void clear();
|
||||||
bool is_empty() { return m_history_items.is_empty(); }
|
bool is_empty() { return m_history_items.is_empty(); }
|
||||||
|
|
||||||
|
ErrorOr<void> read_from_file(DeprecatedString const& path);
|
||||||
|
ErrorOr<void> write_to_file(bool rewrite_all);
|
||||||
|
|
||||||
|
ErrorOr<void> invalidate_model_and_file(bool rewrite_all);
|
||||||
|
|
||||||
// ^GUI::Model
|
// ^GUI::Model
|
||||||
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
|
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
|
||||||
|
|
||||||
|
@ -60,4 +68,6 @@ private:
|
||||||
|
|
||||||
Vector<ClipboardItem> m_history_items;
|
Vector<ClipboardItem> m_history_items;
|
||||||
size_t m_history_limit;
|
size_t m_history_limit;
|
||||||
|
|
||||||
|
DeprecatedString m_path;
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
#include "ClipboardHistoryModel.h"
|
#include "ClipboardHistoryModel.h"
|
||||||
#include <LibConfig/Client.h>
|
#include <LibConfig/Client.h>
|
||||||
|
#include <LibCore/Directory.h>
|
||||||
|
#include <LibCore/StandardPaths.h>
|
||||||
#include <LibCore/System.h>
|
#include <LibCore/System.h>
|
||||||
#include <LibGUI/Action.h>
|
#include <LibGUI/Action.h>
|
||||||
#include <LibGUI/Application.h>
|
#include <LibGUI/Application.h>
|
||||||
|
@ -17,14 +19,22 @@
|
||||||
|
|
||||||
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
ErrorOr<int> 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 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::pledge_domain("ClipboardHistory");
|
||||||
Config::monitor_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("/res", "r"));
|
||||||
|
TRY(Core::System::unveil(parent_path.dirname(), "rwc"sv));
|
||||||
|
|
||||||
TRY(Core::System::unveil(nullptr, nullptr));
|
TRY(Core::System::unveil(nullptr, nullptr));
|
||||||
auto app_icon = TRY(GUI::Icon::try_create_default_icon("edit-copy"sv));
|
auto app_icon = TRY(GUI::Icon::try_create_default_icon("edit-copy"sv));
|
||||||
|
|
||||||
|
@ -36,6 +46,8 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||||
auto table_view = TRY(main_window->set_main_widget<GUI::TableView>());
|
auto table_view = TRY(main_window->set_main_widget<GUI::TableView>());
|
||||||
auto model = ClipboardHistoryModel::create();
|
auto model = ClipboardHistoryModel::create();
|
||||||
|
|
||||||
|
TRY(model->read_from_file(clipboard_file_path));
|
||||||
|
|
||||||
auto data_and_type = GUI::Clipboard::the().fetch_data_and_type();
|
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()))
|
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);
|
model->add_item(data_and_type);
|
||||||
|
|
|
@ -124,6 +124,29 @@ RefPtr<Gfx::Bitmap> Clipboard::DataAndType::as_bitmap() const
|
||||||
return bitmap;
|
return bitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ErrorOr<Clipboard::DataAndType> 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<JsonObject> 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<DeprecatedString, DeprecatedString> const& metadata)
|
void Clipboard::set_data(ReadonlyBytes data, DeprecatedString const& type, HashMap<DeprecatedString, DeprecatedString> const& metadata)
|
||||||
{
|
{
|
||||||
if (data.is_empty()) {
|
if (data.is_empty()) {
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include <AK/DeprecatedString.h>
|
#include <AK/DeprecatedString.h>
|
||||||
#include <AK/Function.h>
|
#include <AK/Function.h>
|
||||||
#include <AK/HashMap.h>
|
#include <AK/HashMap.h>
|
||||||
|
#include <AK/JsonObject.h>
|
||||||
#include <LibGUI/Forward.h>
|
#include <LibGUI/Forward.h>
|
||||||
#include <LibGfx/Forward.h>
|
#include <LibGfx/Forward.h>
|
||||||
|
|
||||||
|
@ -34,6 +35,9 @@ public:
|
||||||
HashMap<DeprecatedString, DeprecatedString> metadata;
|
HashMap<DeprecatedString, DeprecatedString> metadata;
|
||||||
|
|
||||||
RefPtr<Gfx::Bitmap> as_bitmap() const;
|
RefPtr<Gfx::Bitmap> as_bitmap() const;
|
||||||
|
|
||||||
|
static ErrorOr<Clipboard::DataAndType> from_json(JsonObject const& object);
|
||||||
|
ErrorOr<JsonObject> to_json() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
static ErrorOr<void> initialize(Badge<Application>);
|
static ErrorOr<void> initialize(Badge<Application>);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue