From 10badb282c94fbd1e3a6283e20f0b4f398214456 Mon Sep 17 00:00:00 2001 From: Acrux Development Date: Wed, 28 Oct 2026 01:02:10 -0300 Subject: [PATCH] C and Python API + Smarter account-based wait times --- CMakeLists.txt | 55 +++- README.md | 84 ++++- examples/example.c | 65 ++++ include/Display.h | 13 +- include/Repl.h | 11 +- include/Status.h | 6 + include/Tracker.h | 83 +++++ include/Views.h | 51 +++ include/lls_api.h | 185 +++++++++++ python/__pycache__/lls.cpython-312.pyc | Bin 0 -> 26287 bytes python/lls.py | 439 +++++++++++++++++++++++++ python/test_lls.py | 314 ++++++++++++++++++ src/Display.cpp | 209 ++---------- src/Repl.cpp | 244 ++------------ src/Status.cpp | 13 + src/Tracker.cpp | 300 +++++++++++++++++ src/Views.cpp | 162 +++++++++ src/capi.cpp | 399 ++++++++++++++++++++++ 18 files changed, 2204 insertions(+), 429 deletions(-) create mode 100644 examples/example.c create mode 100644 include/Tracker.h create mode 100644 include/Views.h create mode 100644 include/lls_api.h create mode 100644 python/__pycache__/lls.cpython-312.pyc create mode 100644 python/lls.py create mode 100644 python/test_lls.py create mode 100644 src/Tracker.cpp create mode 100644 src/Views.cpp create mode 100644 src/capi.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b383ab..f2d9fc7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,34 +25,67 @@ if(NOT SQLite3_FOUND) find_package(Threads REQUIRED) add_library(sqlite3_local STATIC ${sqlite3_amalgamation_SOURCE_DIR}/sqlite3.c) + set_target_properties(sqlite3_local PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) target_include_directories(sqlite3_local PUBLIC ${sqlite3_amalgamation_SOURCE_DIR}) target_link_libraries(sqlite3_local PUBLIC Threads::Threads ${CMAKE_DL_LIBS}) add_library(SQLite::SQLite3 ALIAS sqlite3_local) endif() -# --- Application ------------------------------------------------------------- +# --- Warnings ---------------------------------------------------------------- +function(lls_warnings target) + if(MSVC) + target_compile_options(${target} PRIVATE /W4) + else() + target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic) + endif() +endfunction() + +# --- Core: the rules, shared by the prompt and the C API --------------------- +add_library(lls_core STATIC + src/Database.cpp + src/Status.cpp + src/StringUtil.cpp + src/Tracker.cpp + src/Views.cpp +) +target_include_directories(lls_core PUBLIC include) +target_link_libraries(lls_core PUBLIC SQLite::SQLite3) +set_target_properties(lls_core PROPERTIES + POSITION_INDEPENDENT_CODE ON + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON) +lls_warnings(lls_core) + +# --- C API: liblls_api.so / lls_api.dll (include/lls_api.h) ------------------ +add_library(lls_api SHARED src/capi.cpp) +target_link_libraries(lls_api PRIVATE lls_core) +target_compile_definitions(lls_api PRIVATE LLS_BUILDING_API) +set_target_properties(lls_api PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON) +lls_warnings(lls_api) + +# --- Application: the interactive prompt ------------------------------------- add_executable(lls src/main.cpp src/Completer.cpp src/Config.cpp - src/Database.cpp src/Display.cpp src/ExternalEditor.cpp src/LineEditor.cpp src/Repl.cpp - src/Status.cpp - src/StringUtil.cpp src/Table.cpp src/Terminal.cpp src/Tokenizer.cpp src/HomeDirectory.cpp ) +target_link_libraries(lls PRIVATE lls_core) +lls_warnings(lls) -target_include_directories(lls PRIVATE include) -target_link_libraries(lls PRIVATE SQLite::SQLite3) - -if(MSVC) - target_compile_options(lls PRIVATE /W4) -else() - target_compile_options(lls PRIVATE -Wall -Wextra -Wpedantic) +# --- Example: the C API from C ----------------------------------------------- +option(LLS_BUILD_EXAMPLES "Build the C API example" ON) +if(LLS_BUILD_EXAMPLES) + add_executable(lls_example examples/example.c) + target_link_libraries(lls_example PRIVATE lls_api) + target_include_directories(lls_example PRIVATE include) endif() diff --git a/README.md b/README.md index d1d8330..4dd563e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ installed; otherwise CMake downloads the amalgamation (needs network once). cmake --build build ./build/lls [database-file] # default: tasks.db +This builds three things: the prompt (`lls`), the C API as a shared library +(`liblls_api.so` / `lls_api.dll`, header `include/lls_api.h`) and a C example +(`lls_example`). + ## Commands acc add tsk add [acc] [status] @@ -93,6 +97,11 @@ goes back to `$VISUAL` / `$EDITOR` / nano / vim / vi. The choice is stored in Uploading ones) is listed in the account row as `(wait HH:MM)` or `(limit reset)` and keeps the account Busy. Tasks without an account keep their own wait. +* Auto unwait: once an account's limit has reset (its tasks show `Limit reset`), + explicitly setting a task on it to `testing` or `prompt` (`tsk add + testing`, `tsk restat testing`) clears that finished wait, because the + account is in use again. Adding a task without a status does not, and a wait + that is still running is never cleared automatically (use `acc unwait`). * A time is only optional while the account is already waiting: then `tsk restat wait` (or `tsk add wait`) just makes the task follow that wait. Otherwise (no account, or its wait is over) a time is @@ -101,7 +110,78 @@ goes back to `$VISUAL` / `$EDITOR` / nano / vim / vi. The choice is stored in * Names are case-insensitive; the original spelling is kept. * Notes live in the database (SQLite TEXT, binary-safe, SQLite's default limit is ~1 GB). The task table shows `-` when a task has no notes, otherwise the number of lines (e.g. `3 lines`); open them with `tsk notes `. +## C API and Python + +`include/lls_api.h` is a plain C API over the same database and the same rules +as the prompt (it is built on the same `Tracker` class), so a program written +against it behaves exactly like typing the commands. Build it with the project +(`liblls_api.so`), link against it from C/C++, or load it from Python. + + #include "lls_api.h" + + lls_db* db; + if (lls_open("tasks.db", &db) != LLS_OK) { /* lls_last_error(NULL) */ } + + lls_account_add(db, "Acrux"); + lls_task_add(db, "Breeze A", "Acrux", "testing"); + lls_account_set_wait(db, "Acrux", "4:30pm"); /* the account hit its limit */ + + lls_task_list* tasks; + lls_tasks(db, &tasks); /* display order */ + for (int i = 0; i < lls_task_list_count(tasks); ++i) { + const lls_task_info* t = lls_task_list_get(tasks, i); + printf("%s %s\n", t->name, t->status_label); /* "Breeze A Wait 16:30" */ + } + lls_task_list_free(tasks); + + lls_save(db); /* nothing is saved automatically */ + lls_close(db); + +* Every call that can fail returns `LLS_OK` or an error code (`LLS_E_INVALID`, + `LLS_E_NOT_FOUND`, `LLS_E_EXISTS`, `LLS_E_DATABASE`) with the message in + `lls_last_error(db)`. No C++ exception crosses the API. +* After a change, `lls_last_messages(db)` holds what the prompt would have + printed ("Account 'A' is back in use: ..."). Queries do not reset it. +* Statuses are the text you would type: `"prompt"`, `"testing"`, `"wait 4:30pm"`, + `"wait"` (only while the account is waiting)... Waits belong to the account. +* `lls_tasks` / `lls_accounts` return lists in the order of the panels, with the + computed status (`status`, `status_label`, `until`) and the stored one underneath + (`stored_status`). Strings in a list are valid until `lls_task_list_free`. +* `lls_set_clock(db, unix_seconds)` pretends it is another time, handy for + tests and simulations; 0 goes back to the real clock. +* A `lls_db` must not be shared between threads without your own locking. + +`python/lls.py` is a ctypes binding (no compiler needed): + + import lls # python/ is on sys.path; LLS_LIBRARY=build/liblls_api.so + + with lls.Database("tasks.db") as db: + db.add_account("Acrux") + db.add_task("Breeze A", account="Acrux", status="testing") + db.set_account_wait("Acrux", "4:30pm") + for task in db.tasks(): + print(task.name, task.account, task.status_label, task.until) + for account in db.accounts(): + print(account.name, account.level.name, account.tasks) + print(db.messages) + db.save() + +Errors are exceptions (`lls.NotFound`, `lls.AlreadyExists`, `lls.InvalidArgument`, +`lls.DatabaseError`, all `lls.LLSError`). The library is found through the +`LLS_LIBRARY` environment variable, `../build` next to the module, or the +current directory's `build`. Run the tests with + + LLS_LIBRARY=build/liblls_api.so python3 python/test_lls.py -v + ## Layout - include/ headers (declarations only, no inline definitions) - src/ implementations + include/ headers (declarations only, no inline definitions); lls_api.h is the public C header + src/ implementations + examples/ example.c - the C API from C + python/ lls.py (ctypes binding) and test_lls.py + + Core (static library, shared by the prompt and the C API): + Database SQLite storage Tracker all the rules; the only thing that changes data + Status statuses, times Views computed task / account views and their order + Prompt only: Repl, LineEditor, Completer, Display, Table, Config, ExternalEditor... + C API: capi.cpp (wraps Tracker + Views) diff --git a/examples/example.c b/examples/example.c new file mode 100644 index 0000000..36b7426 --- /dev/null +++ b/examples/example.c @@ -0,0 +1,65 @@ +/* Minimal use of the lls C API: add accounts and tasks, set a wait, print the panels. */ +#include + +#include "lls_api.h" + +static int check(lls_db* db, int code, const char* what) { + if (code != LLS_OK) { + fprintf(stderr, "%s failed (%d): %s\n", what, code, lls_last_error(db)); + return 0; + } + return 1; +} + +int main(int argc, char** argv) { + const char* path = (argc > 1) ? argv[1] : "example.db"; + + lls_db* db = NULL; + if (lls_open(path, &db) != LLS_OK) { + fprintf(stderr, "open failed: %s\n", lls_last_error(NULL)); + return 1; + } + + /* Ignore "already exists" so the example can be run twice. */ + lls_account_add(db, "Acrux"); + lls_account_add(db, "Beta"); + lls_task_add(db, "Breeze A", "Acrux", "testing"); + lls_task_add(db, "Breeze B", "Acrux", NULL); /* NULL = prompt */ + lls_task_add(db, "Breeze C", "Beta", "done"); + + /* The wait belongs to the account: Acrux's Prompt / Testing tasks now show Wait. */ + if (!check(db, lls_account_set_wait(db, "Acrux", "11:59pm"), "set_wait")) { + lls_close(db); + return 1; + } + printf("%s\n\n", lls_last_messages(db)); + + lls_task_list* tasks = NULL; + if (check(db, lls_tasks(db, &tasks), "tasks")) { + printf("%-10s %-8s %-12s %s\n", "TASK", "ACCOUNT", "STATUS", "NOTES"); + for (int i = 0; i < lls_task_list_count(tasks); ++i) { + const lls_task_info* t = lls_task_list_get(tasks, i); + printf("%-10s %-8s %-12s %d lines\n", t->name, t->account ? t->account : "-", t->status_label, + t->notes_lines); + } + lls_task_list_free(tasks); + } + + lls_account_list* accounts = NULL; + if (check(db, lls_accounts(db, &accounts), "accounts")) { + printf("\n%-8s %-6s %s\n", "ACCOUNT", "LEVEL", "TASKS"); + for (int i = 0; i < lls_account_list_count(accounts); ++i) { + const lls_account_info* a = lls_account_list_get(accounts, i); + printf("%-8s %-6s", a->name, a->level_label); + for (int k = 0; k < a->task_count; ++k) { + printf(" %s", a->tasks[k]); + } + printf("\n"); + } + lls_account_list_free(accounts); + } + + check(db, lls_save(db), "save"); /* nothing is saved automatically */ + lls_close(db); + return 0; +} diff --git a/include/Display.h b/include/Display.h index 18f97fa..9dbe2c5 100644 --- a/include/Display.h +++ b/include/Display.h @@ -6,14 +6,7 @@ #include "Database.h" #include "Status.h" - -// How taken an account is, judged by its tasks: -// Free - only Done, Frozen or Uploading tasks, or no tasks -// Taken - has a Prompt task (and nothing Busy) -// Busy - has a Testing, Limit reset or Wait task -enum class AccountLevel { Busy, Taken, Free }; - -std::string accountLevelName(AccountLevel level); // "busy", "taken", "free" +#include "Views.h" struct DisplaySettings { bool showTasks = true; // auto-print the task table after each change @@ -22,8 +15,8 @@ struct DisplaySettings { std::set hiddenAccountLevels; }; -// Builds the computed views. The status shown is derived from the stored -// status and the current time, never copied straight from the database. +// Renders the task and account panels as text tables, from the computed views +// (see Views.h), honouring the hidden levels in the settings. class Display { public: explicit Display(const Database& db); diff --git a/include/Repl.h b/include/Repl.h index 9b10a09..0e0f284 100644 --- a/include/Repl.h +++ b/include/Repl.h @@ -12,6 +12,7 @@ #include "Database.h" #include "Display.h" #include "LineEditor.h" +#include "Tracker.h" #include "Status.h" // Interactive prompt: commands are typed live, one per line. @@ -39,8 +40,6 @@ private: void execute(const std::string& line); void refresh() const; void say(const std::string& message); - void setAccountWait(const std::string& account, std::time_t until); - void noteAccountWait(const std::string& taskName); void flushMessages(); bool endOfInput(); // true if the program should exit @@ -48,13 +47,6 @@ private: // Asks until the reply is valid (y / n, and c when allowed). nullopt if input closed. std::optional ask(const std::string& question, bool allowCancel) const; - // Helpers - static void requireName(const std::string& name); - static Status parseStatusFrom(const Args& args, std::size_t from); - static bool isBareWait(const Args& args, std::size_t from); - std::optional runningWait(const std::string& account) const; - void makeTaskFollowAccount(const TaskRow& task); - // Account commands bool cmdAccAdd(const Args& args); bool cmdAccDel(const Args& args); @@ -92,6 +84,7 @@ private: Database& db_; Display& display_; Config& config_; + Tracker tracker_; std::vector commands_; std::vector specs_; Completer completer_; diff --git a/include/Status.h b/include/Status.h index f2dcb92..defe1a0 100644 --- a/include/Status.h +++ b/include/Status.h @@ -45,6 +45,12 @@ std::string clockLabel(std::time_t value); // "16:30" // "16:30" for today, "tomorrow 06:40" for the next day, otherwise "2026-10-26 16:30". std::string resetLabel(std::time_t value, std::time_t now); +// True when the text is just "wait" (no time given). +bool isBareWait(const std::string& text); + +// One-line list of the accepted statuses, for error messages and help. +std::string helpText(); + std::string label(const Status& status); // "Prompt", "Limit reset", "Wait 16:30", ... std::string kindName(StatusKind kind); // lowercase token: "prompt", "limit-reset", ... std::vector allKinds(); diff --git a/include/Tracker.h b/include/Tracker.h new file mode 100644 index 0000000..af97b0a --- /dev/null +++ b/include/Tracker.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Database.h" +#include "Views.h" + +enum class ErrorCode { Invalid, NotFound, Exists, Database }; + +// What went wrong, with a message that can be shown to the user as it is. +class TrackerError : public std::runtime_error { +public: + TrackerError(ErrorCode code, const std::string& message); + + ErrorCode code() const; + +private: + ErrorCode code_; +}; + +// All the rules for changing accounts and tasks (waits, auto-unwait, status +// handling...). The prompt and the C API are both thin layers over this class. +// Each change appends human-readable notes (what the prompt prints), which +// takeMessages() hands over. +class Tracker { +public: + explicit Tracker(Database& db); + + const Database& database() const; + + // For tests and simulations: pretend the time is `fixedNow`. 0 = real clock. + void setClock(std::time_t fixedNow); + std::time_t now() const; + + std::vector takeMessages(); + + // ---- Accounts ----------------------------------------------------------- + void addAccount(const std::string& name); + void deleteAccount(const std::string& name); + void renameAccount(const std::string& name, const std::string& newName); + + // The account hits its limit until `until` (or a clock time such as "16:30", + // "4:30pm"; a time that has passed today means tomorrow). + void setAccountWait(const std::string& name, std::time_t until); + void setAccountWaitText(const std::string& name, const std::string& timeText); + // Returns false if the account was not waiting. + bool clearAccountWait(const std::string& name); + + // ---- Tasks -------------------------------------------------------------- + // `statusText` is what the user would type: "testing", "wait 4pm", "wait"... + // nullopt = Prompt, without the auto-unwait that an explicit status triggers. + void addTask(const std::string& name, const std::optional& account, + const std::optional& statusText); + void deleteTask(const std::string& name); + void renameTask(const std::string& name, const std::string& newName); + void setTaskAccount(const std::string& name, const std::optional& account); + void setTaskStatus(const std::string& name, const std::string& statusText); + // Returns false (and changes nothing) if the notes are the same already. + bool setTaskNotes(const std::string& name, const std::string& notes); + + // ---- Computed views ----------------------------------------------------- + std::vector tasks() const; + std::vector accounts() const; + +private: + static void requireName(const std::string& name); + Status parseStatus(const std::string& text) const; + std::string existingAccount(const std::string& name) const; + TaskRow existingTask(const std::string& name) const; + std::optional runningWait(const std::string& account) const; + void resumeAccount(const std::string& account); + void makeTaskFollowAccount(const TaskRow& task); + void noteAccountWait(const std::string& taskName); + void note(const std::string& message); + + Database& db_; + std::time_t fixedNow_; + std::vector messages_; +}; diff --git a/include/Views.h b/include/Views.h new file mode 100644 index 0000000..a040014 --- /dev/null +++ b/include/Views.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Database.h" +#include "Status.h" + +// How taken an account is, judged by its tasks: +// Free - only Done, Frozen or Uploading tasks, or no tasks +// Taken - has a Prompt task (and nothing Busy) +// Busy - has a Testing, Limit reset or Wait task, or a wait nothing else shows +enum class AccountLevel { Busy, Taken, Free }; + +std::string accountLevelName(AccountLevel level); // "busy", "taken", "free" + +enum class WaitState { None, Waiting, LimitReset }; + +// A task as the user sees it: the status is computed from the stored status, +// the account's wait and the current time. +struct TaskInfo { + std::string name; + std::optional account; + Status status; // as shown (Wait / Limit reset come from the account) + Status storedStatus; // what is saved underneath + std::string notes; + std::size_t noteLines = 0; +}; + +struct AccountInfo { + std::string name; + AccountLevel level = AccountLevel::Free; + WaitState waitState = WaitState::None; + std::time_t waitUntil = 0; // when the wait ends (or ended); 0 if none + // True when the account has a wait that none of its tasks shows (it has no + // tasks, or only Done / Frozen / Uploading ones). It still makes the account Busy. + bool waitListed = false; + std::vector tasks; // tasks at the account's level (Free: its Uploading/Frozen/Done ones) +}; + +// Tasks in display order: status priority (Limit reset, Testing, Prompt, Wait, +// Uploading, Frozen, Done); for Wait and Limit reset then time, earliest first; +// then account A-Z (none last); then task name. +std::vector buildTasks(const Database& db, std::time_t now); + +// Accounts in display order, most free first: Free, Taken, Busy; within Taken +// and Busy fewer entries first; ties by name. +std::vector buildAccounts(const Database& db, std::time_t now); diff --git a/include/lls_api.h b/include/lls_api.h new file mode 100644 index 0000000..a7e9656 --- /dev/null +++ b/include/lls_api.h @@ -0,0 +1,185 @@ +/* + * lls C API - query and change the task / account database from C, or from + * Python through ctypes / cffi (see python/lls.py). + * + * Model + * - lls_open() loads the database file into memory (or starts empty). Changes + * stay in memory until lls_save(); lls_is_dirty() tells whether there is + * anything to save. Nothing is saved automatically. + * - Every function that can fail returns LLS_OK (0) or an error code, and the + * message is available from lls_last_error(). Changes also leave human + * readable notes in lls_last_messages() (what the interactive prompt prints, + * e.g. "Account 'A' is back in use: its finished wait was cleared."). + * - Names are case-insensitive but keep their original spelling. + * - Statuses are the text you would type at the prompt: "prompt", "testing", + * "uploading", "frozen", "done", "limit-reset", "wait 4:30pm", "wait 16:30" + * or just "wait" (only while the task's account is already waiting). + * - A wait belongs to the account: setting a task to "wait