diff --git a/CMakeLists.txt b/CMakeLists.txt index a6995a3..f26e6b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,9 +12,10 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(GTKMM REQUIRED gtkmm-4.0) pkg_check_modules(LAYERSHELL REQUIRED gtk4-layer-shell-0) pkg_check_modules(WEBKIT REQUIRED webkitgtk-6.0) +pkg_check_modules(SQLITE3 REQUIRED sqlite3) -include_directories(${GTKMM_INCLUDE_DIRS} ${LAYERSHELL_INCLUDE_DIRS} ${WEBKIT_INCLUDE_DIRS}) -link_directories(${GTKMM_LIBRARY_DIRS} ${LAYERSHELL_LIBRARY_DIRS} ${WEBKIT_LIBRARY_DIRS}) +include_directories(${GTKMM_INCLUDE_DIRS} ${LAYERSHELL_INCLUDE_DIRS} ${WEBKIT_INCLUDE_DIRS} ${SQLITE3_INCLUDE_DIRS}) +link_directories(${GTKMM_LIBRARY_DIRS} ${LAYERSHELL_LIBRARY_DIRS} ${WEBKIT_LIBRARY_DIRS} ${SQLITE3_LIBRARY_DIRS}) add_library(bar_lib) target_sources(bar_lib @@ -29,6 +30,7 @@ target_sources(bar_lib src/widgets/webWidget.cpp src/services/todo.cpp + src/services/sqliteTodoAdapter.cpp src/services/hyprland.cpp src/services/tray.cpp src/services/notifications.cpp @@ -45,7 +47,7 @@ include_directories(bar_lib PRIVATE add_executable(bar main.cpp) -target_link_libraries(bar bar_lib ${GTKMM_LIBRARIES} ${LAYERSHELL_LIBRARIES} ${WEBKIT_LIBRARIES}) +target_link_libraries(bar bar_lib ${GTKMM_LIBRARIES} ${LAYERSHELL_LIBRARIES} ${WEBKIT_LIBRARIES} ${SQLITE3_LIBRARIES}) # Copy `resources/bar.css` into the build directory when it changes set(RES_SRC "${CMAKE_CURRENT_SOURCE_DIR}/resources/bar.css") diff --git a/include/components/todoEntry.hpp b/include/components/todoEntry.hpp index 8357968..4a010d8 100644 --- a/include/components/todoEntry.hpp +++ b/include/components/todoEntry.hpp @@ -10,8 +10,10 @@ public: ~TodoEntry() override; int get_id() const { return id; } + std::string get_text() const { return text; } private: int id; + std::string text; sigc::signal signal_dismissed; void on_dismiss_clicked(); diff --git a/include/services/todo.hpp b/include/services/todo.hpp index 2aab7b7..b5eae55 100644 --- a/include/services/todo.hpp +++ b/include/services/todo.hpp @@ -1,8 +1,9 @@ #pragma once #include "components/todoEntry.hpp" +#include "services/todoAdapter.hpp" +#include #include -#include class TodoService { public: @@ -12,11 +13,15 @@ class TodoService { std::map getTodos(); void init(); void removeTodo(int id); - TodoEntry *addTodo(std::string text, bool emitSignal = true); + TodoEntry *addTodo(std::string text, bool emitSignal = true, bool persist = true); void updateTodo(int id, std::string text); private: + void load(); + int nextId = 1; std::map todos; sigc::signal refreshSignal; + + std::unique_ptr adapter; }; \ No newline at end of file diff --git a/include/services/todoAdapter.hpp b/include/services/todoAdapter.hpp new file mode 100644 index 0000000..c1336d1 --- /dev/null +++ b/include/services/todoAdapter.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +struct TodoRecord { + int id; + std::string text; +}; + +class ITodoAdapter { + public: + virtual ~ITodoAdapter() = default; + + virtual bool init() = 0; + virtual std::vector listTodos() = 0; + + virtual int addTodo(const std::string &text) = 0; + virtual bool removeTodo(int id) = 0; + virtual bool updateTodo(int id, const std::string &text) = 0; +}; diff --git a/resources/bar.css b/resources/bar.css index e7e5687..969af68 100644 --- a/resources/bar.css +++ b/resources/bar.css @@ -66,14 +66,6 @@ window { margin-right: 0; } -.minimized { - background-color: rgba(50, 50, 50, 0.5); -} - -.restored { - background-color: transparent; -} - button { padding: 2px 5px; margin: 0 2px; @@ -96,6 +88,9 @@ popover { background-color: rgb(30, 30, 30); color: #ffffff; font-family: "IBMPlexSans-Regular", sans-serif; + padding: 10px; + border-radius: 8px; + border: 1px solid #444444; } tooltip { @@ -111,9 +106,7 @@ tooltip { } .todo-popover-container { - padding: 10px; background-color: #1e1e1e; - border-radius: 6px; } .todo-input-area { diff --git a/src/components/popover.cpp b/src/components/popover.cpp index af03dd4..417ceeb 100644 --- a/src/components/popover.cpp +++ b/src/components/popover.cpp @@ -13,11 +13,6 @@ Popover::Popover(std::string icon, std::string name) { popover = new Gtk::Popover(); popover->set_parent(*this); popover->set_autohide(true); - - popover->signal_closed().connect([this]() { - this->add_css_class("minimized"); - this->remove_css_class("restored"); - }); } Popover::~Popover() { @@ -28,8 +23,6 @@ void Popover::on_toggle_window() { if (popover->get_visible()) { popover->popdown(); } else { - this->remove_css_class("minimized"); - this->add_css_class("restored"); popover->popup(); } } diff --git a/src/components/todoEntry.cpp b/src/components/todoEntry.cpp index f7978c2..89e2f6b 100644 --- a/src/components/todoEntry.cpp +++ b/src/components/todoEntry.cpp @@ -7,6 +7,7 @@ TodoEntry::TodoEntry(int id, std::string text, sigc::signal signal_dismissed) : Gtk::Box(Gtk::Orientation::HORIZONTAL) { this->id = id; + this->text = text; this->signal_dismissed = signal_dismissed; diff --git a/src/services/sqliteTodoAdapter.cpp b/src/services/sqliteTodoAdapter.cpp new file mode 100644 index 0000000..43619bb --- /dev/null +++ b/src/services/sqliteTodoAdapter.cpp @@ -0,0 +1,165 @@ +#include "services/todoAdapter.hpp" + +#include +#include +#include +#include + +namespace { +std::string getDbPath() { + const char *homeDir = getenv("HOME"); + if (!homeDir) { + return "todos.db"; + } + + std::string path = std::string(homeDir) + "/.config/bar"; + if (!std::filesystem::exists(path)) { + std::filesystem::create_directories(path); + } + + return path + "/todos.db"; +} + +class SqliteTodoAdapter final : public ITodoAdapter { + public: + ~SqliteTodoAdapter() override { + if (db_) { + sqlite3_close(db_); + db_ = nullptr; + } + } + + bool init() override { + std::string dbPath = getDbPath(); + std::cout << "Opening database at: " << dbPath << std::endl; + + int rc = sqlite3_open(dbPath.c_str(), &db_); + if (rc != SQLITE_OK) { + std::cerr << "Can't open database: " << sqlite3_errmsg(db_) << std::endl; + return false; + } + + const char *sql = "CREATE TABLE IF NOT EXISTS todos (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "text TEXT NOT NULL);"; + char *zErrMsg = nullptr; + rc = sqlite3_exec(db_, sql, nullptr, nullptr, &zErrMsg); + if (rc != SQLITE_OK) { + std::cerr << "SQL error (create table): " << (zErrMsg ? zErrMsg : "") << std::endl; + sqlite3_free(zErrMsg); + return false; + } + + return true; + } + + std::vector listTodos() override { + std::vector results; + if (!db_) { + return results; + } + + sqlite3_stmt *stmt = nullptr; + const char *sql = "SELECT id, text FROM todos"; + int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) { + std::cerr << "Failed to fetch data: " << sqlite3_errmsg(db_) << std::endl; + return results; + } + + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + int id = sqlite3_column_int(stmt, 0); + const unsigned char *txt = sqlite3_column_text(stmt, 1); + results.push_back({id, txt ? reinterpret_cast(txt) : ""}); + } + + sqlite3_finalize(stmt); + return results; + } + + int addTodo(const std::string &text) override { + if (!db_) { + return -1; + } + + sqlite3_stmt *stmt = nullptr; + const char *sql = "INSERT INTO todos (text) VALUES (?1);"; + int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) { + std::cerr << "SQL prepare error (insert): " << sqlite3_errmsg(db_) << std::endl; + return -1; + } + + sqlite3_bind_text(stmt, 1, text.c_str(), -1, SQLITE_TRANSIENT); + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) { + std::cerr << "SQL step error (insert): " << sqlite3_errmsg(db_) << std::endl; + sqlite3_finalize(stmt); + return -1; + } + + sqlite3_finalize(stmt); + return static_cast(sqlite3_last_insert_rowid(db_)); + } + + bool removeTodo(int id) override { + if (!db_) { + return false; + } + + sqlite3_stmt *stmt = nullptr; + const char *sql = "DELETE FROM todos WHERE id = ?1;"; + int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) { + std::cerr << "SQL prepare error (delete): " << sqlite3_errmsg(db_) << std::endl; + return false; + } + + sqlite3_bind_int(stmt, 1, id); + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) { + std::cerr << "SQL step error (delete): " << sqlite3_errmsg(db_) << std::endl; + sqlite3_finalize(stmt); + return false; + } + + sqlite3_finalize(stmt); + return true; + } + + bool updateTodo(int id, const std::string &text) override { + if (!db_) { + return false; + } + + sqlite3_stmt *stmt = nullptr; + const char *sql = "UPDATE todos SET text = ?1 WHERE id = ?2;"; + int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) { + std::cerr << "SQL prepare error (update): " << sqlite3_errmsg(db_) << std::endl; + return false; + } + + sqlite3_bind_text(stmt, 1, text.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int(stmt, 2, id); + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) { + std::cerr << "SQL step error (update): " << sqlite3_errmsg(db_) << std::endl; + sqlite3_finalize(stmt); + return false; + } + + sqlite3_finalize(stmt); + return true; + } + + private: + sqlite3 *db_ = nullptr; +}; + +} // namespace + +// Factory kept local to this TU for now; TodoService uses it. +std::unique_ptr makeSqliteTodoAdapter() { + return std::make_unique(); +} diff --git a/src/services/todo.cpp b/src/services/todo.cpp index fc66d84..a98a77f 100644 --- a/src/services/todo.cpp +++ b/src/services/todo.cpp @@ -4,27 +4,34 @@ #include #include +std::unique_ptr makeSqliteTodoAdapter(); + TodoService::TodoService(sigc::signal refreshSignal) { this->refreshSignal = refreshSignal; + this->adapter = makeSqliteTodoAdapter(); this->init(); } -TodoService::~TodoService() {} +TodoService::~TodoService() { + this->adapter.reset(); +} std::map TodoService::getTodos() { return this->todos; } void TodoService::init() { - std::vector items = { - "Buy groceries", - "Finish the report", - "Call Alice", - "Schedule dentist appointment"}; - - for (auto item : items) { - this->addTodo(item, false); + if (!this->adapter) { + std::cerr << "Todo adapter not set" << std::endl; + return; } + + if (!this->adapter->init()) { + std::cerr << "Todo adapter init failed" << std::endl; + return; + } + + this->load(); } void TodoService::removeTodo(int id) { @@ -33,24 +40,70 @@ void TodoService::removeTodo(int id) { assert(false); } - todos.erase(id); + if (this->adapter) { + this->adapter->removeTodo(id); + } + todos.erase(id); this->refreshSignal.emit(); } -TodoEntry *TodoService::addTodo(std::string text, bool emitSignal) { +TodoEntry *TodoService::addTodo(std::string text, bool emitSignal, bool persist) { + int id = nextId; + + if (persist) { + if (!this->adapter) { + return nullptr; + } + int newId = this->adapter->addTodo(text); + if (newId < 0) { + return nullptr; + } + id = newId; + } + auto signal = sigc::signal(); signal.connect(sigc::mem_fun(*this, &TodoService::removeTodo)); - TodoEntry *todo = Gtk::make_managed(nextId, text, signal); + TodoEntry *todo = Gtk::make_managed(id, text, signal); - todos[nextId] = todo; + todos[id] = todo; - nextId++; + if (id >= nextId) { + nextId = id + 1; + } if (emitSignal) { this->refreshSignal.emit(); } + return todo; } void TodoService::updateTodo(int id, std::string text) {} + +void TodoService::load() { + if (!this->adapter) { + return; + } + + std::cout << "Loading todos from database..." << std::endl; + auto rows = this->adapter->listTodos(); + + int count = 0; + + for (const auto &row : rows) { + count++; + int id = row.id; + std::string text = row.text; + + auto signal = sigc::signal(); + signal.connect(sigc::mem_fun(*this, &TodoService::removeTodo)); + TodoEntry *todo = Gtk::make_managed(id, text, signal); + + todos[id] = todo; + if (id >= nextId) { + nextId = id + 1; + } + } + std::cout << "Loaded " << count << " todos." << std::endl; +} diff --git a/src/widgets/todoPopover.cpp b/src/widgets/todoPopover.cpp index c680aa0..0eb8c15 100644 --- a/src/widgets/todoPopover.cpp +++ b/src/widgets/todoPopover.cpp @@ -29,6 +29,8 @@ TodoPopover::TodoPopover(std::string icon, std::string title) : Popover(icon, ti std::string text = entry->get_text(); if (!text.empty()) { this->todoService->addTodo(text); + + entry->set_text(""); } });