Status system reworked

This commit is contained in:
alejo committed 2026-10-29 01:02:06 -03:00
1 parent 33567db5a3
commit a29ba6e4d2
18 files changed
+315 -334

No files matched your search

+26 -31
View File
@@ -75,8 +75,8 @@ goes back to `$VISUAL` / `$EDITOR` / nano / vim / vi. The choice is stored in
`db backup` writes a copy, `db restore` loads a file. On `quit` you are asked `db backup` writes a copy, `db restore` loads a file. On `quit` you are asked
to save if there are unsaved changes (`y` save, `n` discard, `c` cancel the to save if there are unsaved changes (`y` save, `n` discard, `c` cancel the
exit); any other answer asks again, so a mistyped key never loses data. exit); any other answer asks again, so a mistyped key never loses data.
* The stored status is never shown directly: a `wait` task displays as * The stored status is not always what is shown: a `wait` task displays as
`Wait HH:MM` until that time passes, then as `Limit reset`. Tasks are sorted `Wait HH:MM` until its account's wait ends, then as `Limit reset`. Tasks are sorted
by computed status (Limit reset, Testing, Prompt, Wait, Uploading, Frozen, by computed status (Limit reset, Testing, Prompt, Wait, Uploading, Frozen,
Done); Wait and Limit reset tasks then sort by time (earliest first: when Done); Wait and Limit reset tasks then sort by time (earliest first: when
the wait ends / ended), then account A-Z (unassigned last), then name. The panels are cleared and redrawn after changes. the wait ends / ended), then account A-Z (unassigned last), then name. The panels are cleared and redrawn after changes.
@@ -88,29 +88,22 @@ goes back to `$VISUAL` / `$EDITOR` / nano / vim / vi. The choice is stored in
Done tasks (`-` if it has none). Rows are sorted most free first: Free, Taken, Busy; Done tasks (`-` if it has none). Rows are sorted most free first: Free, Taken, Busy;
within Taken and Busy fewer tasks first, ties by name (see `levelOf` in `Display.cpp` and within Taken and Busy fewer tasks first, ties by name (see `levelOf` in `Display.cpp` and
`StatusUtil::isBusy` / `isTaken` to change the rules). `StatusUtil::isBusy` / `isTaken` to change the rules).
* Waits belong to the account: `acc wait <name> <time>` (or `tsk restat <task> wait * Waits: an account hits its limit until a time (`acc wait <name> <time>`). A task
<time>`, which sets its account's wait). Until the time the account's Prompt that is **set to `wait`** is waiting for that: `tsk restat <task> wait <time>`
and Testing tasks show `Wait HH:MM`; after it they show `Limit reset`, until sets its account's wait to the time and marks the task Wait (several waiting
you run `acc unwait <name>`. Uploading, Frozen and Done tasks are not tasks share the account's time), and `tsk restat <task> wait` with no time marks
affected, and the tasks keep their own stored status underneath. A wait that it Wait while the account is already waiting (otherwise a time is required).
no task shows (no tasks, or only Done / Frozen / Uploading ones) is listed in Tasks without an account keep their own wait time.
the account row as `(wait)` or `(limit reset)` and keeps the account Busy. * When the time passes the account stops waiting by itself, and every task that
Tasks without an account keep their own wait. was Wait now shows `Limit reset`: the conversation can be continued. Those
* A task you set explicitly to `testing` or `prompt` (`tsk restat <task> tasks stay Limit reset until you set them to something else. `acc unwait
testing`, `tsk add <task> <acc> testing`) shows that status, even while its <name>` ends the wait early, with the same result.
account is waiting: that task ignores the wait, and the account's other tasks * Prompt and Testing tasks are not waiting: they show as they are, with
keep following it. `tsk restat <task> wait` (no time) makes it follow the `(account-waiting)` added while their account waits, e.g. `Testing
wait again, and a new `acc wait` or `acc unwait` applies to every task again. (account-waiting)`. The account row shows when the wait ends in the RESET
Adding a task without a status leaves it following the account. column, and stays Busy while it waits; if none of its tasks is set to Wait
* Auto unwait: once an account's limit has reset (its tasks show `Limit reset`), (e.g. you deleted them) its TASKS column lists `(wait)`.
setting a task on it explicitly to `testing` or `prompt` clears that finished * Old databases with per-task waits are converted on load.
wait, because the account is in use again, and its tasks go back to their own
status. 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 <task> wait` (or `tsk add <task> <acc> wait`) just makes the task
follow that wait. Otherwise (no account, or its wait is over) a time is
required. Old databases with per-task waits are converted on load.
* Deleting an account leaves its tasks unassigned (`-`). * Deleting an account leaves its tasks unassigned (`-`).
* Names are case-insensitive; the original spelling is kept. * 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 <name>`. * 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 <name>`.
@@ -129,13 +122,13 @@ against it behaves exactly like typing the commands. Build it with the project
lls_account_add(db, "Acrux"); lls_account_add(db, "Acrux");
lls_task_add(db, "Breeze A", "Acrux", "testing"); lls_task_add(db, "Breeze A", "Acrux", "testing");
lls_account_set_wait(db, "Acrux", "4:30pm"); /* the account hit its limit */ lls_task_add(db, "Breeze B", "Acrux", "wait 4:30pm"); /* Acrux hit its limit; Breeze B waits for it */
lls_task_list* tasks; lls_task_list* tasks;
lls_tasks(db, &tasks); /* display order */ lls_tasks(db, &tasks); /* display order */
for (int i = 0; i < lls_task_list_count(tasks); ++i) { for (int i = 0; i < lls_task_list_count(tasks); ++i) {
const lls_task_info* t = lls_task_list_get(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" */ printf("%s %s\n", t->name, t->status_label); /* "Breeze A Testing (account-waiting)" ... */
} }
lls_task_list_free(tasks); lls_task_list_free(tasks);
@@ -148,9 +141,11 @@ against it behaves exactly like typing the commands. Build it with the project
* After a change, `lls_last_messages(db)` holds what the prompt would have * 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. printed ("Account 'A' is back in use: ..."). Queries do not reset it.
* Statuses are the text you would type: `"prompt"`, `"testing"`, `"wait 4:30pm"`, * Statuses are the text you would type: `"prompt"`, `"testing"`, `"wait 4:30pm"`,
`"wait"` (only while the account is waiting)... Waits belong to the account; `"wait"` (only while the account is waiting)... A task set to wait waits for
a task set to `"testing"` / `"prompt"` while it waits ignores the wait its account; when the time passes the account stops waiting by itself and
(`overrides_wait` is 1) and `"wait"` makes it follow the account again. those tasks show Limit reset. Prompt / Testing tasks keep their status, with
`(account-waiting)` in `status_label` (and `account_waiting` = 1) while the
account waits.
* `lls_tasks` / `lls_accounts` return lists in the order of the panels, with the * `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 computed status (`status`, `status_label`, `until`) and the stored one underneath
(`stored_status`). Strings in a list are valid until `lls_task_list_free`. (`stored_status`). Strings in a list are valid until `lls_task_list_free`.
@@ -165,7 +160,7 @@ against it behaves exactly like typing the commands. Build it with the project
with lls.Database("tasks.db") as db: with lls.Database("tasks.db") as db:
db.add_account("Acrux") db.add_account("Acrux")
db.add_task("Breeze A", account="Acrux", status="testing") db.add_task("Breeze A", account="Acrux", status="testing")
db.set_account_wait("Acrux", "4:30pm") db.add_task("Breeze B", account="Acrux", status="wait 4:30pm") # sets Acrux's wait
for task in db.tasks(): for task in db.tasks():
print(task.name, task.account, task.status_label, task.until) print(task.name, task.account, task.status_label, task.until)
for account in db.accounts(): for account in db.accounts():
+5 -4
View File
@@ -27,8 +27,9 @@ int main(int argc, char** argv) {
lls_task_add(db, "Breeze B", "Acrux", NULL); /* NULL = prompt */ lls_task_add(db, "Breeze B", "Acrux", NULL); /* NULL = prompt */
lls_task_add(db, "Breeze C", "Beta", "done"); lls_task_add(db, "Breeze C", "Beta", "done");
/* The wait belongs to the account: Acrux's Prompt / Testing tasks now show Wait. */ /* Acrux hit its limit: "Breeze B" waits for it (Breeze A stays Testing, with a note). */
if (!check(db, lls_account_set_wait(db, "Acrux", "11:59pm"), "set_wait")) { if (!check(db, lls_account_set_wait(db, "Acrux", "11:59pm"), "set_wait") ||
!check(db, lls_task_set_status(db, "Breeze B", "wait"), "set_status")) {
lls_close(db); lls_close(db);
return 1; return 1;
} }
@@ -36,10 +37,10 @@ int main(int argc, char** argv) {
lls_task_list* tasks = NULL; lls_task_list* tasks = NULL;
if (check(db, lls_tasks(db, &tasks), "tasks")) { if (check(db, lls_tasks(db, &tasks), "tasks")) {
printf("%-10s %-8s %-12s %s\n", "TASK", "ACCOUNT", "STATUS", "NOTES"); printf("%-10s %-8s %-26s %s\n", "TASK", "ACCOUNT", "STATUS", "NOTES");
for (int i = 0; i < lls_task_list_count(tasks); ++i) { for (int i = 0; i < lls_task_list_count(tasks); ++i) {
const lls_task_info* t = lls_task_list_get(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, printf("%-10s %-8s %-26s %d lines\n", t->name, t->account ? t->account : "-", t->status_label,
t->notes_lines); t->notes_lines);
} }
lls_task_list_free(tasks); lls_task_list_free(tasks);
-2
View File
@@ -46,8 +46,6 @@ public:
void setTaskAccount(const std::string& name, const std::optional<std::string>& account); void setTaskAccount(const std::string& name, const std::optional<std::string>& account);
void setTaskStatus(const std::string& name, const std::string& status); void setTaskStatus(const std::string& name, const std::string& status);
void setTaskNotes(const std::string& name, const std::string& notes); void setTaskNotes(const std::string& name, const std::string& notes);
void setTaskWaitOverride(const std::string& name, bool value);
void clearWaitOverrides(const std::string& account); // every task of the account follows its wait again
private: private:
using Param = std::optional<std::string>; using Param = std::optional<std::string>;
-1
View File
@@ -8,7 +8,6 @@ struct TaskRow {
std::optional<std::string> account; std::optional<std::string> account;
std::string status; // serialized, see StatusUtil::serialize std::string status; // serialized, see StatusUtil::serialize
std::string notes; // free text, any length (stored as SQLite TEXT) std::string notes; // free text, any length (stored as SQLite TEXT)
bool waitOverride = false; // set to Testing / Prompt while its account waits: ignores that wait
}; };
struct AccountRow { struct AccountRow {
+9 -7
View File
@@ -26,14 +26,16 @@ std::optional<Status> parse(const std::string& text, std::time_t now);
std::string serialize(const Status& status); std::string serialize(const Status& status);
std::optional<Status> deserialize(const std::string& text); std::optional<Status> deserialize(const std::string& text);
// Prompt and Testing tasks follow their account's wait; Uploading, Frozen and // Prompt and Testing show as they are, with "(account-waiting)" appended while
// Done tasks do not. // their account waits. Other statuses are shown without a note.
bool followsAccountWait(StatusKind kind); bool showsAccountWaiting(StatusKind kind);
// The status to show for a task. If its account has a wait, the tasks that // The status to show for a task. A task set to Wait on an account waits for the
// follow it show Wait until then and Limit reset after; otherwise the stored // account: Wait until the account's wait ends, Limit reset after that (or if the
// status is used (see effective). // account is not waiting). `accountWait` is when the account's wait ends (ended).
Status resolve(const Status& stored, const std::optional<std::time_t>& accountWait, std::time_t now); // Everything else shows its stored status (see effective).
Status resolve(const Status& stored, bool hasAccount, const std::optional<std::time_t>& accountWait,
std::time_t now);
// The status shown to the user: an expired wait becomes LimitReset. // The status shown to the user: an expired wait becomes LimitReset.
Status effective(const Status& status, std::time_t now); Status effective(const Status& status, std::time_t now);
+18 -10
View File
@@ -22,10 +22,15 @@ private:
ErrorCode code_; ErrorCode code_;
}; };
// All the rules for changing accounts and tasks (waits, auto-unwait, status // All the rules for changing accounts and tasks. The prompt and the C API are
// handling...). The prompt and the C API are both thin layers over this class. // both thin layers over this class. Each change appends human-readable notes
// Each change appends human-readable notes (what the prompt prints), which // (what the prompt prints), which takeMessages() hands over.
// takeMessages() hands over. //
// How waits work: an account hits its limit until a time ("acc wait"). A task set
// to Wait is waiting for that: it shows Wait until then, and when the time passes
// the account stops waiting by itself and the task shows Limit reset (the
// conversation can be continued). Prompt / Testing tasks are not waiting: they
// show as they are, with "(account-waiting)" appended while the account waits.
class Tracker { class Tracker {
public: public:
explicit Tracker(Database& db); explicit Tracker(Database& db);
@@ -43,16 +48,19 @@ public:
void deleteAccount(const std::string& name); void deleteAccount(const std::string& name);
void renameAccount(const std::string& name, const std::string& newName); 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", // The account hit its limit until `until` (or a clock time such as "16:30",
// "4:30pm"; a time that has passed today means tomorrow). // "4:30pm"; a time that has passed today means tomorrow). Tasks set to Wait
// follow the new time.
void setAccountWait(const std::string& name, std::time_t until); void setAccountWait(const std::string& name, std::time_t until);
void setAccountWaitText(const std::string& name, const std::string& timeText); void setAccountWaitText(const std::string& name, const std::string& timeText);
// Returns false if the account was not waiting. // Ends the wait now: its Wait tasks become Limit reset. Returns false if the
// account was not waiting.
bool clearAccountWait(const std::string& name); bool clearAccountWait(const std::string& name);
// ---- Tasks -------------------------------------------------------------- // ---- Tasks --------------------------------------------------------------
// `statusText` is what the user would type: "testing", "wait 4pm", "wait"... // `statusText` is what the user would type: "testing", "wait 4pm", "wait"...
// nullopt = Prompt, without the auto-unwait that an explicit status triggers. // nullopt = Prompt. "wait <time>" sets the account's wait too; "wait" alone
// joins the account's running wait (and is an error if it is not waiting).
void addTask(const std::string& name, const std::optional<std::string>& account, void addTask(const std::string& name, const std::optional<std::string>& account,
const std::optional<std::string>& statusText); const std::optional<std::string>& statusText);
void deleteTask(const std::string& name); void deleteTask(const std::string& name);
@@ -72,8 +80,8 @@ private:
std::string existingAccount(const std::string& name) const; std::string existingAccount(const std::string& name) const;
TaskRow existingTask(const std::string& name) const; TaskRow existingTask(const std::string& name) const;
std::optional<std::time_t> runningWait(const std::string& account) const; std::optional<std::time_t> runningWait(const std::string& account) const;
void taskInUse(const std::string& taskName, const std::string& accountName); void settle();
void makeTaskFollowAccount(const TaskRow& task); int endAccountWait(const std::string& account, std::time_t endedAt);
void note(const std::string& message); void note(const std::string& message);
Database& db_; Database& db_;
+13 -9
View File
@@ -12,34 +12,38 @@
// How taken an account is, judged by its tasks: // How taken an account is, judged by its tasks:
// Free - only Done, Frozen or Uploading tasks, or no tasks // Free - only Done, Frozen or Uploading tasks, or no tasks
// Taken - has a Prompt task (and nothing Busy) // Taken - has a Prompt task (and nothing Busy)
// Busy - has a Testing, Limit reset or Wait task, or a wait nothing else shows // Busy - has a Testing, Limit reset or Wait task, or is waiting itself
enum class AccountLevel { Busy, Taken, Free }; enum class AccountLevel { Busy, Taken, Free };
std::string accountLevelName(AccountLevel level); // "busy", "taken", "free" std::string accountLevelName(AccountLevel level); // "busy", "taken", "free"
enum class WaitState { None, Waiting, LimitReset }; // An account waits from the moment it hits its limit until the wait time; after
// that it is not waiting any more (its Wait tasks are then Limit reset).
enum class WaitState { None, Waiting };
// A task as the user sees it: the status is computed from the stored status, // A task as the user sees it: the status is computed from the stored status,
// the account's wait and the current time. // the account's wait and the current time.
struct TaskInfo { struct TaskInfo {
std::string name; std::string name;
std::optional<std::string> account; std::optional<std::string> account;
Status status; // as shown (Wait / Limit reset come from the account) Status status; // as shown (a Wait task becomes Limit reset when its account's wait ends)
Status storedStatus; // what is saved underneath Status storedStatus; // what is saved underneath
// What the panel shows: "Wait 16:30", "Limit reset", "Testing (account-waiting)"...
std::string statusLabel;
// Prompt / Testing while its account waits: shown as they are, with
// "(account-waiting)" appended.
bool accountWaiting = false;
std::string notes; std::string notes;
std::size_t noteLines = 0; std::size_t noteLines = 0;
// Set to Testing / Prompt while its account waits: it shows its own status
// and ignores the wait, while the account's other tasks keep following it.
bool overridesWait = false;
}; };
struct AccountInfo { struct AccountInfo {
std::string name; std::string name;
AccountLevel level = AccountLevel::Free; AccountLevel level = AccountLevel::Free;
WaitState waitState = WaitState::None; WaitState waitState = WaitState::None;
std::time_t waitUntil = 0; // when the wait ends (or ended); 0 if none std::time_t waitUntil = 0; // when the wait ends; 0 if it is not waiting
// True when the account has a wait that none of its tasks shows (it has no // True when the account is waiting but none of its tasks is set to Wait
// tasks, or only Done / Frozen / Uploading ones). It still makes the account Busy. // (e.g. it has no tasks). It still makes the account Busy.
bool waitListed = false; bool waitListed = false;
std::vector<std::string> tasks; // tasks at the account's level (Free: its Uploading/Frozen/Done ones) std::vector<std::string> tasks; // tasks at the account's level (Free: its Uploading/Frozen/Done ones)
}; };
+15 -16
View File
@@ -14,13 +14,13 @@
* - Statuses are the text you would type at the prompt: "prompt", "testing", * - Statuses are the text you would type at the prompt: "prompt", "testing",
* "uploading", "frozen", "done", "limit-reset", "wait 4:30pm", "wait 16:30" * "uploading", "frozen", "done", "limit-reset", "wait 4:30pm", "wait 16:30"
* or just "wait" (only while the task's account is already waiting). * or just "wait" (only while the task's account is already waiting).
* - A wait belongs to the account: setting a task to "wait <time>" sets its * - Waits: an account hits its limit until a time. A task set to "wait" is waiting
* account's wait, and the account's Prompt / Testing tasks then show Wait * for that: "wait <time>" sets the account's wait and marks the task Wait; "wait"
* until that time and Limit reset after it. A task you explicitly set to * alone marks it Wait while the account is already waiting. When the time passes
* "testing" or "prompt" while the wait is running shows that status instead * the account stops waiting by itself and its Wait tasks show Limit reset (the
* (overrides_wait = 1) and the other tasks keep waiting; "wait" makes it follow * conversation can be continued). Prompt / Testing tasks are not waiting: they
* the account again. If the wait is already over (Limit reset), setting a task * keep their status, with "(account-waiting)" added to the label while the
* to testing / prompt clears it: the account is in use again (see the README). * account waits (account_waiting = 1).
* - Times are Unix seconds (UTC); clock-time text like "4:30pm" is read in the * - Times are Unix seconds (UTC); clock-time text like "4:30pm" is read in the
* local time zone, and a time that has passed today means tomorrow. * local time zone, and a time that has passed today means tomorrow.
* *
@@ -75,8 +75,7 @@ extern "C" {
/* ---- Account wait states (lls_account_info.wait_state) -------------------- */ /* ---- Account wait states (lls_account_info.wait_state) -------------------- */
#define LLS_WAIT_NONE 0 #define LLS_WAIT_NONE 0
#define LLS_WAIT_WAITING 1 /* wait_until is in the future */ #define LLS_WAIT_WAITING 1 /* waiting until wait_until; after that it is NONE again (automatically) */
#define LLS_WAIT_LIMIT_RESET 2 /* wait_until has passed; stays until cleared */
typedef struct lls_db lls_db; typedef struct lls_db lls_db;
typedef struct lls_task_list lls_task_list; typedef struct lls_task_list lls_task_list;
@@ -86,13 +85,13 @@ typedef struct lls_account_list lls_account_list;
typedef struct lls_task_info { typedef struct lls_task_info {
const char* name; const char* name;
const char* account; /* NULL when the task has no account */ const char* account; /* NULL when the task has no account */
int status; /* LLS_STATUS_*, as shown (Wait / Limit reset come from the account) */ int status; /* LLS_STATUS_*, as shown (Wait becomes Limit reset when the account's wait ends) */
const char* status_label; /* "Prompt", "Limit reset", "Wait 16:30", ... */ const char* status_label; /* "Prompt", "Limit reset", "Wait 16:30", "Testing (account-waiting)", ... */
int stored_status; /* LLS_STATUS_*, what is saved underneath */ int stored_status; /* LLS_STATUS_*, what is saved underneath */
int64_t until; /* for Wait / Limit reset: when the wait ends (ended); else 0 */ int64_t until; /* for Wait / Limit reset: when the wait ends (ended); else 0 */
const char* notes; /* the full text, "" if none (any length) */ const char* notes; /* the full text, "" if none (any length) */
int notes_lines; /* number of lines in the notes */ int notes_lines; /* number of lines in the notes */
int overrides_wait; /* 1 if its account waits but this task ignores it (set to Testing / Prompt) */ int account_waiting; /* 1 if it is Prompt / Testing while its account waits (the label says so) */
} lls_task_info; } lls_task_info;
/* An account with its computed state. */ /* An account with its computed state. */
@@ -101,8 +100,8 @@ typedef struct lls_account_info {
int level; /* LLS_LEVEL_* */ int level; /* LLS_LEVEL_* */
const char* level_label; /* "Busy", "Taken", "Free" */ const char* level_label; /* "Busy", "Taken", "Free" */
int wait_state; /* LLS_WAIT_* */ int wait_state; /* LLS_WAIT_* */
int64_t wait_until; /* when the wait ends (ended); 0 if none */ int64_t wait_until; /* when the wait ends; 0 if it is not waiting */
int wait_listed; /* 1 if the wait is not shown by any task (it still makes the account Busy) */ int wait_listed; /* 1 if it is waiting but no task is set to Wait (it is still Busy) */
int task_count; int task_count;
const char* const* tasks; /* the tasks at the account's level (a Free account: its Uploading/Frozen/Done ones) */ const char* const* tasks; /* the tasks at the account's level (a Free account: its Uploading/Frozen/Done ones) */
} lls_account_info; } lls_account_info;
@@ -152,9 +151,9 @@ LLS_API int lls_account_add(lls_db* db, const char* name);
LLS_API int lls_account_delete(lls_db* db, const char* name); /* its tasks become unassigned */ LLS_API int lls_account_delete(lls_db* db, const char* name); /* its tasks become unassigned */
LLS_API int lls_account_rename(lls_db* db, const char* name, const char* new_name); LLS_API int lls_account_rename(lls_db* db, const char* name, const char* new_name);
/* The account hit its limit until a clock time ("16:30", "4:30pm") / until a Unix time. */ /* The account hit its limit until a clock time ("16:30", "4:30pm") / until a Unix time. */
LLS_API int lls_account_set_wait(lls_db* db, const char* name, const char* time_text); LLS_API int lls_account_set_wait(lls_db* db, const char* name, const char* time_text); /* tasks set to Wait follow it */
LLS_API int lls_account_set_wait_at(lls_db* db, const char* name, int64_t until); LLS_API int lls_account_set_wait_at(lls_db* db, const char* name, int64_t until);
/* Clears its wait / limit reset. Sets *was_waiting (if not NULL) to 1 or 0. */ /* Ends the wait now: its Wait tasks become Limit reset. Sets *was_waiting (if not NULL) to 1 or 0. */
LLS_API int lls_account_clear_wait(lls_db* db, const char* name, int* was_waiting); LLS_API int lls_account_clear_wait(lls_db* db, const char* name, int* was_waiting);
/* ---- Tasks ---------------------------------------------------------------- */ /* ---- Tasks ---------------------------------------------------------------- */
Binary file not shown.
+9 -9
View File
@@ -54,7 +54,6 @@ class Level(IntEnum):
class WaitState(IntEnum): class WaitState(IntEnum):
NONE = 0 NONE = 0
WAITING = 1 WAITING = 1
LIMIT_RESET = 2
# ---- Errors ------------------------------------------------------------------------ # ---- Errors ------------------------------------------------------------------------
@@ -92,13 +91,13 @@ _ERRORS = {E_INVALID: InvalidArgument, E_NOT_FOUND: NotFound, E_EXISTS: AlreadyE
class Task: class Task:
name: str name: str
account: Optional[str] account: Optional[str]
status: Status # as shown: Wait / Limit reset come from the account status: Status # as shown (Wait becomes Limit reset when the account's wait ends)
status_label: str # "Prompt", "Limit reset", "Wait 16:30", ... status_label: str # "Prompt", "Limit reset", "Wait 16:30", "Testing (account-waiting)"...
stored_status: Status # what is saved underneath stored_status: Status # what is saved underneath
until: Optional[datetime] # for Wait / Limit reset: when the wait ends (ended) until: Optional[datetime] # for Wait / Limit reset: when the wait ends (ended)
notes: str notes: str
notes_lines: int notes_lines: int
overrides_wait: bool # its account waits, but this task (set to Testing / Prompt) ignores it account_waiting: bool # Prompt / Testing while its account waits (the label says so)
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -108,7 +107,7 @@ class Account:
level_label: str # "Busy", "Taken", "Free" level_label: str # "Busy", "Taken", "Free"
wait_state: WaitState wait_state: WaitState
wait_until: Optional[datetime] wait_until: Optional[datetime]
wait_listed: bool # the wait is not shown by any task (account is still Busy) wait_listed: bool # waiting, but no task is set to Wait (the account is still Busy)
tasks: Tuple[str, ...] # tasks at the account's level tasks: Tuple[str, ...] # tasks at the account's level
@@ -124,7 +123,7 @@ class _TaskInfo(ctypes.Structure):
("until", ctypes.c_int64), ("until", ctypes.c_int64),
("notes", ctypes.c_char_p), ("notes", ctypes.c_char_p),
("notes_lines", ctypes.c_int), ("notes_lines", ctypes.c_int),
("overrides_wait", ctypes.c_int), ("account_waiting", ctypes.c_int),
] ]
@@ -348,14 +347,15 @@ class Database:
def set_account_wait(self, name: str, until: Union[str, datetime, int, float]) -> None: def set_account_wait(self, name: str, until: Union[str, datetime, int, float]) -> None:
"""The account hit its limit until `until`: a clock time such as "16:30" / "4:30pm" """The account hit its limit until `until`: a clock time such as "16:30" / "4:30pm"
(a time that has passed today means tomorrow), a datetime, or Unix seconds.""" (a time that has passed today means tomorrow), a datetime, or Unix seconds. Tasks
set to "wait" follow it; when the time passes they become Limit reset."""
if isinstance(until, str): if isinstance(until, str):
self._call("lls_account_set_wait", _enc(name), _enc(until)) self._call("lls_account_set_wait", _enc(name), _enc(until))
else: else:
self._call("lls_account_set_wait_at", _enc(name), _seconds(until)) self._call("lls_account_set_wait_at", _enc(name), _seconds(until))
def clear_account_wait(self, name: str) -> bool: def clear_account_wait(self, name: str) -> bool:
"""Clears its wait / limit reset. Returns False if it was not waiting.""" """Ends the wait now (its Wait tasks become Limit reset). False if it was not waiting."""
was_waiting = ctypes.c_int(0) was_waiting = ctypes.c_int(0)
self._call("lls_account_clear_wait", _enc(name), ctypes.byref(was_waiting)) self._call("lls_account_clear_wait", _enc(name), ctypes.byref(was_waiting))
return bool(was_waiting.value) return bool(was_waiting.value)
@@ -402,7 +402,7 @@ class Database:
until=_when(info.until), until=_when(info.until),
notes=_dec(info.notes), notes=_dec(info.notes),
notes_lines=info.notes_lines, notes_lines=info.notes_lines,
overrides_wait=bool(info.overrides_wait), account_waiting=bool(info.account_waiting),
)) ))
return result return result
finally: finally:
+83 -125
View File
@@ -115,13 +115,16 @@ class OrderingTests(Base):
for account in ("A", "B", "C"): for account in ("A", "B", "C"):
self.db.add_account(account) self.db.add_account(account)
self.db.add_task("task-" + account, account) self.db.add_task("task-" + account, account)
self.db.set_account_wait("A", "6pm") self.db.set_task_status("task-A", "wait 6pm")
self.db.set_account_wait("B", "2pm") self.db.set_task_status("task-B", "wait 2pm")
self.db.set_account_wait("C", "4pm") self.db.set_task_status("task-C", "wait 4pm")
self.assertEqual([t.name for t in self.db.tasks()], ["task-B", "task-C", "task-A"]) self.assertEqual([t.name for t in self.db.tasks()], ["task-B", "task-C", "task-A"])
self.at(17) # B and C are over now: Limit reset first, ordered by when they ended self.at(17) # B and C are over now: Limit reset first, ordered by when they ended
self.assertEqual([t.status_label for t in self.db.tasks()], ["Limit reset", "Limit reset", "Wait 18:00"]) self.assertEqual([t.status_label for t in self.db.tasks()], ["Limit reset", "Limit reset", "Wait 18:00"])
self.assertEqual([t.name for t in self.db.tasks()], ["task-B", "task-C", "task-A"]) self.assertEqual([t.name for t in self.db.tasks()], ["task-B", "task-C", "task-A"])
self.db.add_account("Z") # any change saves what time did: same order afterwards
self.assertEqual([t.name for t in self.db.tasks()], ["task-B", "task-C", "task-A"])
self.assertEqual(self.db.task("task-B").stored_status, lls.Status.LIMIT_RESET)
def test_accounts_most_free_first(self): def test_accounts_most_free_first(self):
for account in ("Busy1", "Taken1", "Free1", "Free2", "Busy2"): for account in ("Busy1", "Taken1", "Free1", "Free2", "Busy2"):
@@ -150,40 +153,81 @@ class WaitTests(Base):
self.db.add_task("a-done", "A", "done") self.db.add_task("a-done", "A", "done")
self.db.add_task("b-prompt", "B") self.db.add_task("b-prompt", "B")
def test_wait_belongs_to_the_account(self): def test_only_tasks_set_to_wait_wait(self):
self.db.set_account_wait("A", "4:30pm") self.db.set_task_status("a-prompt", "wait 4:30pm")
self.assertEqual(self.statuses(), {"a-testing": "Wait 16:30", "a-prompt": "Wait 16:30", self.assertEqual(self.statuses(), {"a-testing": "Testing (account-waiting)", "a-prompt": "Wait 16:30",
"a-done": "Done", "b-prompt": "Prompt"}) "a-done": "Done", "b-prompt": "Prompt"})
self.assertEqual(self.db.task("a-testing").stored_status, lls.Status.TESTING) # kept underneath
self.assertEqual(self.db.task("a-testing").until, NOON.replace(hour=16, minute=30).astimezone())
account = self.db.account("A") account = self.db.account("A")
self.assertEqual((account.level, account.wait_state), (lls.Level.BUSY, lls.WaitState.WAITING)) self.assertEqual((account.level, account.wait_state), (lls.Level.BUSY, lls.WaitState.WAITING))
self.assertEqual(self.db.task("a-prompt").until, NOON.replace(hour=16, minute=30).astimezone())
self.at(17) # the wait is over -> Limit reset until cleared def test_prompt_and_testing_show_as_they_are_with_a_note(self):
self.assertEqual(self.db.task("a-prompt").status, lls.Status.LIMIT_RESET) self.db.set_account_wait("A", "6pm") # nothing is set to wait
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.LIMIT_RESET) testing = self.db.task("a-testing")
self.assertEqual((testing.status, testing.status_label, testing.account_waiting),
(lls.Status.TESTING, "Testing (account-waiting)", True))
prompt = self.db.task("a-prompt")
self.assertEqual((prompt.status, prompt.status_label), (lls.Status.PROMPT, "Prompt (account-waiting)"))
self.assertEqual(self.db.task("a-done").status_label, "Done") # no note on other statuses
self.assertFalse(self.db.task("b-prompt").account_waiting) # another account
account = self.db.account("A")
self.assertEqual((account.level, account.wait_listed), (lls.Level.BUSY, True))
self.db.set_task_status("a-testing", "testing") # setting it again changes nothing
self.assertEqual(self.db.task("a-testing").status_label, "Testing (account-waiting)")
self.assertEqual(self.db.messages, ["Task 'a-testing' set to Testing."])
def test_the_account_stops_waiting_by_itself_and_waiting_tasks_become_limit_reset(self):
self.db.add_task("w1", "A", "wait 4pm")
self.db.add_task("w2", "A", "wait") # joins the running wait
self.assertEqual(self.db.task("w2").status_label, "Wait 16:00")
self.at(17) # the time has passed
self.assertEqual(self.statuses()["w1"], "Limit reset")
self.assertEqual(self.statuses()["w2"], "Limit reset")
self.assertEqual(self.statuses()["a-testing"], "Testing") # no note any more
self.assertEqual(self.statuses()["a-prompt"], "Prompt")
account = self.db.account("A")
self.assertEqual((account.wait_state, account.wait_until), (lls.WaitState.NONE, None))
self.assertEqual(account.tasks[:2], ("w1", "w2")) # Limit reset keeps it Busy
self.db.add_account("Z") # a change saves what time did
self.assertEqual(self.db.task("w1").stored_status, lls.Status.LIMIT_RESET)
self.db.set_task_status("w1", "testing") # handled: leaves Limit reset
self.assertEqual(self.db.task("w1").status_label, "Testing")
self.assertEqual(self.db.task("w2").status_label, "Limit reset")
def test_a_new_wait_leaves_limit_reset_tasks_alone(self):
self.db.add_task("w1", "A", "wait 1pm")
self.at(14)
self.db.add_task("w2", "A", "wait 8pm") # the account hit its limit again
self.assertEqual(self.statuses()["w1"], "Limit reset")
self.assertEqual(self.statuses()["w2"], "Wait 20:00")
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.WAITING)
def test_unwait_ends_the_wait_now(self):
self.db.add_task("w1", "A", "wait 6pm")
self.assertTrue(self.db.clear_account_wait("A")) self.assertTrue(self.db.clear_account_wait("A"))
self.assertFalse(self.db.clear_account_wait("A")) self.assertIn("1 task now Limit reset", self.db.messages[0])
self.assertEqual(self.db.task("a-testing").status, lls.Status.TESTING) self.assertEqual(self.statuses()["w1"], "Limit reset")
self.assertEqual(self.db.task("a-prompt").status, lls.Status.PROMPT) self.assertEqual(self.db.account("A").wait_state, lls.WaitState.NONE)
self.assertEqual(self.db.task("a-testing").status_label, "Testing")
self.assertFalse(self.db.clear_account_wait("A")) # it was not waiting any more
def test_changing_the_account_wait_moves_waiting_tasks(self):
self.db.add_task("w1", "A", "wait 4pm")
self.db.set_account_wait("A", "7pm")
self.assertEqual(self.statuses()["w1"], "Wait 19:00")
def test_time_in_the_past_means_tomorrow(self): def test_time_in_the_past_means_tomorrow(self):
self.db.set_account_wait("A", "9am") # it is noon self.db.set_account_wait("A", "9am") # it is noon
until = self.db.account("A").wait_until until = self.db.account("A").wait_until
self.assertEqual((until.day, until.hour), (16, 9)) self.assertEqual((until.day, until.hour), (16, 9))
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.WAITING)
def test_set_wait_with_datetime_and_seconds(self): def test_set_wait_with_datetime_and_seconds(self):
self.db.set_account_wait("A", NOON.replace(hour=15)) self.db.set_account_wait("A", NOON.replace(hour=15))
self.assertEqual(self.db.task("a-prompt").status_label, "Wait 15:00") self.assertEqual(self.db.account("A").wait_until, NOON.replace(hour=15).astimezone())
self.db.set_account_wait("A", int(NOON.replace(hour=20).timestamp())) self.db.set_account_wait("A", int(NOON.replace(hour=20).timestamp()))
self.assertEqual(self.db.task("a-prompt").status_label, "Wait 20:00") self.assertEqual(self.db.account("A").wait_until, NOON.replace(hour=20).astimezone())
def test_task_wait_sets_the_account_wait(self):
self.db.set_task_status("b-prompt", "wait 3pm")
self.assertEqual(self.db.account("B").wait_state, lls.WaitState.WAITING)
self.assertEqual(self.db.task("b-prompt").status_label, "Wait 15:00")
def test_wait_without_time_only_while_account_is_waiting(self): def test_wait_without_time_only_while_account_is_waiting(self):
with self.assertRaises(lls.InvalidArgument): with self.assertRaises(lls.InvalidArgument):
@@ -196,120 +240,34 @@ class WaitTests(Base):
self.db.set_account_wait("B", "4pm") self.db.set_account_wait("B", "4pm")
self.db.add_task("follower", "B", "wait") # fine now self.db.add_task("follower", "B", "wait") # fine now
self.assertEqual(self.db.task("follower").status_label, "Wait 16:00") self.assertEqual(self.db.task("follower").status_label, "Wait 16:00")
self.db.set_task_status("b-prompt", "wait")
self.assertEqual(self.db.task("b-prompt").status_label, "Wait 16:00")
self.at(17) # over: a time is required again self.at(17) # over: a time is required again
with self.assertRaises(lls.InvalidArgument): with self.assertRaises(lls.InvalidArgument):
self.db.set_task_status("b-prompt", "wait") self.db.add_task("late", "B", "wait")
def test_explicit_testing_or_prompt_clears_a_finished_wait(self): def test_a_task_without_an_account_keeps_its_own_wait(self):
self.db.set_account_wait("A", "1pm") self.db.add_task("loose", None, "wait 4pm")
self.at(14) self.assertEqual(self.db.task("loose").status_label, "Wait 16:00")
self.assertEqual(self.db.task("a-prompt").status, lls.Status.LIMIT_RESET) self.at(17)
self.db.add_task("implicit", "A") # no status given: nothing cleared self.assertEqual(self.db.task("loose").status_label, "Limit reset")
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.LIMIT_RESET)
self.db.set_task_status("a-testing", "uploading") # not an "in use" status
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.LIMIT_RESET)
self.db.add_task("resumed", "A", "testing") # explicit: the account is in use again
self.assertTrue(any("back in use" in m for m in self.db.messages))
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.NONE) # (queries keep the messages)
self.assertEqual(self.db.task("resumed").status, lls.Status.TESTING)
self.assertEqual(self.db.task("a-prompt").status, lls.Status.PROMPT)
self.assertTrue(any("back in use" in m for m in self.db.messages))
def test_explicit_testing_or_prompt_shows_that_status_while_waiting(self): def test_deleting_the_waiting_tasks_keeps_the_account_waiting(self):
self.db.set_account_wait("A", "6pm") self.db.add_task("w1", "A", "wait 4pm")
self.db.set_task_status("a-testing", "testing") # explicit self.db.delete_task("w1")
task = self.db.task("a-testing")
self.assertEqual((task.status, task.status_label, task.overrides_wait), (lls.Status.TESTING, "Testing", True))
self.assertTrue(any("ignores it" in m for m in self.db.messages))
self.db.set_task_status("a-prompt", "prompt")
self.assertEqual(self.db.task("a-prompt").status_label, "Prompt")
self.db.add_task("n1", "A", "testing")
self.assertEqual(self.db.task("n1").status, lls.Status.TESTING)
self.db.add_task("n2", "A") # no status given: follows the wait
self.assertEqual(self.db.task("n2").status, lls.Status.WAIT)
self.assertFalse(self.db.task("n2").overrides_wait)
# the account itself is still waiting, nothing was cleared
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.WAITING)
def test_other_tasks_keep_waiting_and_wait_makes_a_task_follow_again(self):
self.db.set_account_wait("A", "6pm")
self.db.add_task("a-other", "A")
self.db.set_task_status("a-testing", "testing")
self.assertEqual(self.db.task("a-other").status, lls.Status.WAIT)
self.assertEqual(self.db.task("a-prompt").status, lls.Status.WAIT)
self.db.set_task_status("a-testing", "wait") # no time: follow the running wait again
task = self.db.task("a-testing")
self.assertEqual((task.status, task.overrides_wait), (lls.Status.WAIT, False))
self.assertIn("now follows account 'A'", self.db.messages[0])
def test_a_new_wait_applies_to_every_task_again(self):
self.db.set_account_wait("A", "6pm")
self.db.set_task_status("a-testing", "testing")
self.db.set_account_wait("A", "7pm") # the account hit its limit again
task = self.db.task("a-testing")
self.assertEqual((task.status_label, task.overrides_wait), ("Wait 19:00", False))
def test_unwait_clears_the_overrides_too(self):
self.db.set_account_wait("A", "6pm")
self.db.set_task_status("a-testing", "testing")
self.assertTrue(self.db.clear_account_wait("A"))
self.assertFalse(self.db.task("a-testing").overrides_wait)
self.db.set_account_wait("A", "8pm")
self.assertEqual(self.db.task("a-testing").status_label, "Wait 20:00")
def test_an_override_survives_the_wait_ending(self):
self.db.set_account_wait("A", "1pm")
self.at(12, 30)
self.db.set_task_status("a-testing", "testing") # pinned while the wait runs
self.at(14) # the wait is over
self.assertEqual(self.db.task("a-testing").status, lls.Status.TESTING)
self.assertEqual(self.db.task("a-prompt").status, lls.Status.LIMIT_RESET)
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.LIMIT_RESET)
# resuming another task after the reset clears the finished wait for everyone
self.db.set_task_status("a-prompt", "prompt")
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.NONE)
self.assertEqual(self.db.task("a-testing").status, lls.Status.TESTING)
self.assertEqual(self.db.task("a-prompt").status, lls.Status.PROMPT)
def test_the_override_ends_when_the_status_changes_or_the_task_moves(self):
self.db.set_account_wait("A", "6pm")
self.db.set_task_status("a-testing", "testing")
self.db.set_task_status("a-testing", "uploading")
self.assertFalse(self.db.task("a-testing").overrides_wait)
self.db.set_task_status("a-testing", "testing")
self.assertTrue(self.db.task("a-testing").overrides_wait)
self.db.set_task_account("a-testing", "B") # B is not waiting
task = self.db.task("a-testing")
self.assertEqual((task.account, task.status, task.overrides_wait), ("B", lls.Status.TESTING, False))
def test_a_wait_no_task_shows_when_every_task_ignores_it(self):
self.db.set_account_wait("A", "6pm")
self.db.set_task_status("a-testing", "testing")
self.db.set_task_status("a-prompt", "prompt") # a-done is Done
account = self.db.account("A") account = self.db.account("A")
self.assertEqual((account.level, account.wait_state, account.wait_listed), self.assertEqual((account.level, account.wait_state, account.wait_listed),
(lls.Level.BUSY, lls.WaitState.WAITING, True)) (lls.Level.BUSY, lls.WaitState.WAITING, True))
self.assertEqual(self.db.task("a-prompt").status_label, "Prompt (account-waiting)")
def test_overrides_are_saved(self): def test_account_wait_with_no_waiting_tasks(self):
self.db.set_account_wait("A", "6pm")
self.db.set_task_status("a-testing", "testing")
self.db.save()
with lls.Database(self.path) as other:
other.set_clock(NOON)
self.assertTrue(other.task("a-testing").overrides_wait)
self.assertEqual(other.task("a-prompt").status, lls.Status.WAIT)
def test_account_wait_nothing_shows(self):
self.db.add_account("Empty") self.db.add_account("Empty")
self.db.set_account_wait("Empty", "5pm") self.db.set_account_wait("Empty", "5pm")
account = self.db.account("Empty") account = self.db.account("Empty")
self.assertEqual((account.level, account.wait_listed, account.tasks), (lls.Level.BUSY, True, ())) self.assertEqual((account.level, account.wait_listed, account.tasks), (lls.Level.BUSY, True, ()))
self.db.add_task("only-done", "Empty", "done") self.at(18) # over: nothing left to show
self.assertTrue(self.db.account("Empty").wait_listed) account = self.db.account("Empty")
self.assertEqual(self.db.account("Empty").tasks, ()) self.assertEqual((account.level, account.wait_state), (lls.Level.FREE, lls.WaitState.NONE))
def test_messages(self): def test_messages(self):
self.db.set_account_wait("A", "4:30pm") self.db.set_account_wait("A", "4:30pm")
@@ -338,7 +296,7 @@ class NotesAndPersistenceTests(Base):
self.db.add_account("A") self.db.add_account("A")
self.db.add_task("t", "A", "testing") self.db.add_task("t", "A", "testing")
self.db.set_task_notes("t", "one\ntwo") self.db.set_task_notes("t", "one\ntwo")
self.db.set_account_wait("A", "11pm") self.db.set_task_status("t", "wait 11pm")
self.assertTrue(self.db.dirty) self.assertTrue(self.db.dirty)
self.db.save() self.db.save()
self.assertFalse(self.db.dirty) self.assertFalse(self.db.dirty)
+11 -26
View File
@@ -3,6 +3,7 @@
#include <sqlite3.h> #include <sqlite3.h>
#include <cstdio> #include <cstdio>
#include <cstdlib>
#include <ctime> #include <ctime>
#include <iomanip> #include <iomanip>
#include <memory> #include <memory>
@@ -20,8 +21,7 @@ const char* const kSchema =
" id TEXT NOT NULL PRIMARY KEY COLLATE NOCASE," " id TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,"
" account TEXT COLLATE NOCASE REFERENCES Account(id) ON UPDATE CASCADE ON DELETE SET NULL," " account TEXT COLLATE NOCASE REFERENCES Account(id) ON UPDATE CASCADE ON DELETE SET NULL,"
" status TEXT NOT NULL DEFAULT 'prompt'," " status TEXT NOT NULL DEFAULT 'prompt',"
" notes TEXT NOT NULL DEFAULT ''," " notes TEXT NOT NULL DEFAULT ''"
" wait_override INTEGER NOT NULL DEFAULT 0"
");" ");"
"CREATE INDEX IF NOT EXISTS idx_task_account ON Task(account);"; "CREATE INDEX IF NOT EXISTS idx_task_account ON Task(account);";
@@ -109,23 +109,18 @@ void Database::createSchema() {
exec("ALTER TABLE Account ADD COLUMN cooldown TEXT;"); exec("ALTER TABLE Account ADD COLUMN cooldown TEXT;");
} }
// Files from before a task could ignore its account's wait have no such column. // Waits used to be set on each task. They belong to the account now: the
bool hasOverride = false; // account takes the latest of its tasks' waits (they stay Wait and follow it).
for (const Row& row : query("PRAGMA table_info(Task)")) { // Done once per file (PRAGMA user_version), not on every load.
hasOverride = hasOverride || (row.size() > 1 && row[1] && *row[1] == "wait_override"); const std::vector<Row> version = query("PRAGMA user_version");
} if (version.empty() || !version[0][0] || std::atoi(version[0][0]->c_str()) < 2) {
if (!hasOverride) {
exec("ALTER TABLE Task ADD COLUMN wait_override INTEGER NOT NULL DEFAULT 0;");
}
// Waits used to be set on each task. They belong to the account now: move
// them over (the latest one wins) and let those tasks follow the account.
exec( exec(
"UPDATE Account SET cooldown = max(coalesce(cooldown, ''), (" "UPDATE Account SET cooldown = max(coalesce(cooldown, ''), ("
" SELECT max(substr(Task.status, 6)) FROM Task" " SELECT max(substr(Task.status, 6)) FROM Task"
" WHERE Task.account = Account.id AND Task.status LIKE 'wait %'))" " WHERE Task.account = Account.id AND Task.status LIKE 'wait %'))"
" WHERE EXISTS (SELECT 1 FROM Task WHERE Task.account = Account.id AND Task.status LIKE 'wait %');" " WHERE EXISTS (SELECT 1 FROM Task WHERE Task.account = Account.id AND Task.status LIKE 'wait %');"
"UPDATE Task SET status = 'prompt' WHERE account IS NOT NULL AND status LIKE 'wait %';"); "PRAGMA user_version = 2;");
}
} }
void Database::exec(const std::string& sql) { void Database::exec(const std::string& sql) {
@@ -298,7 +293,7 @@ std::optional<std::string> Database::findTask(const std::string& name) const {
} }
std::optional<TaskRow> Database::getTask(const std::string& name) const { std::optional<TaskRow> Database::getTask(const std::string& name) const {
std::vector<Row> rows = query("SELECT id, account, status, notes, wait_override FROM Task WHERE id = ?1", {name}); std::vector<Row> rows = query("SELECT id, account, status, notes FROM Task WHERE id = ?1", {name});
if (rows.empty()) { if (rows.empty()) {
return std::nullopt; return std::nullopt;
} }
@@ -307,19 +302,17 @@ std::optional<TaskRow> Database::getTask(const std::string& name) const {
task.account = rows[0][1]; task.account = rows[0][1];
task.status = *rows[0][2]; task.status = *rows[0][2];
task.notes = *rows[0][3]; task.notes = *rows[0][3];
task.waitOverride = rows[0][4] && *rows[0][4] != "0";
return task; return task;
} }
std::vector<TaskRow> Database::listTasks() const { std::vector<TaskRow> Database::listTasks() const {
std::vector<TaskRow> tasks; std::vector<TaskRow> tasks;
for (const Row& row : query("SELECT id, account, status, notes, wait_override FROM Task ORDER BY id")) { for (const Row& row : query("SELECT id, account, status, notes FROM Task ORDER BY id")) {
TaskRow task; TaskRow task;
task.id = *row[0]; task.id = *row[0];
task.account = row[1]; task.account = row[1];
task.status = *row[2]; task.status = *row[2];
task.notes = *row[3]; task.notes = *row[3];
task.waitOverride = row[4] && *row[4] != "0";
tasks.push_back(task); tasks.push_back(task);
} }
return tasks; return tasks;
@@ -346,14 +339,6 @@ void Database::setTaskStatus(const std::string& name, const std::string& status)
run("UPDATE Task SET status = ?2 WHERE id = ?1", {name, status}); run("UPDATE Task SET status = ?2 WHERE id = ?1", {name, status});
} }
void Database::setTaskWaitOverride(const std::string& name, bool value) {
run("UPDATE Task SET wait_override = ?2 WHERE id = ?1", {name, std::string(value ? "1" : "0")});
}
void Database::clearWaitOverrides(const std::string& account) {
run("UPDATE Task SET wait_override = 0 WHERE account = ?1", {account});
}
void Database::setTaskNotes(const std::string& name, const std::string& notes) { void Database::setTaskNotes(const std::string& name, const std::string& notes) {
run("UPDATE Task SET notes = ?2 WHERE id = ?1", {name, notes}); run("UPDATE Task SET notes = ?2 WHERE id = ?1", {name, notes});
} }
+4 -4
View File
@@ -37,15 +37,15 @@ std::string Display::renderTasks(std::time_t now) const {
} }
table.addRow({task.name, table.addRow({task.name,
task.account ? *task.account : "-", task.account ? *task.account : "-",
StatusUtil::label(task.status), task.statusLabel,
notesSummary(task.noteLines)}); notesSummary(task.noteLines)});
} }
return table.render(); return table.render();
} }
std::string Display::renderAccounts(std::time_t now) const { std::string Display::renderAccounts(std::time_t now) const {
// The TASKS column lists the tasks at the account's level. A wait that no // The TASKS column lists the tasks at the account's level. An account that is
// task is showing is listed as "(wait)" / "(limit reset)". // waiting while none of its tasks is set to Wait is listed as "(wait)".
TextTable table({"ACCOUNT", "STATUS", "RESET", "TASKS"}); TextTable table({"ACCOUNT", "STATUS", "RESET", "TASKS"});
for (const AccountInfo& account : buildAccounts(db_, now)) { for (const AccountInfo& account : buildAccounts(db_, now)) {
if (settings_.hiddenAccountLevels.count(account.level) > 0) { if (settings_.hiddenAccountLevels.count(account.level) > 0) {
@@ -54,7 +54,7 @@ std::string Display::renderAccounts(std::time_t now) const {
std::vector<std::string> entries = account.tasks; std::vector<std::string> entries = account.tasks;
if (account.waitListed) { if (account.waitListed) {
entries.push_back(account.waitState == WaitState::Waiting ? "(wait)" : "(limit reset)"); entries.push_back("(wait)");
} }
std::string status = accountLevelName(account.level); std::string status = accountLevelName(account.level);
+3 -3
View File
@@ -59,9 +59,9 @@ void Repl::registerCommands() {
add("acc rename", "acc rename <name> <new>", "rename an account", 2, 2, add("acc rename", "acc rename <name> <new>", "rename an account", 2, 2,
{ArgKind::Account, ArgKind::None}, &Repl::cmdAccRename); {ArgKind::Account, ArgKind::None}, &Repl::cmdAccRename);
add("acc wait", "acc wait <name> <time>", add("acc wait", "acc wait <name> <time>",
"the account hit its limit until <time>: its Prompt/Testing tasks show Wait, then Limit reset", "the account hit its limit until <time>; tasks set to 'wait' wait for it, then show Limit reset",
2, kUnlimited, {ArgKind::Account, ArgKind::None}, &Repl::cmdAccWait); 2, kUnlimited, {ArgKind::Account, ArgKind::None}, &Repl::cmdAccWait);
add("acc unwait", "acc unwait <name>", "clear an account's wait / limit reset (its tasks show their own status again)", add("acc unwait", "acc unwait <name>", "end an account's wait now (its Wait tasks become Limit reset)",
1, 1, {ArgKind::Account}, &Repl::cmdAccUnwait); 1, 1, {ArgKind::Account}, &Repl::cmdAccUnwait);
add("acc ls", "acc ls", "show the account table", 0, 0, {}, &Repl::cmdAccLs); add("acc ls", "acc ls", "show the account table", 0, 0, {}, &Repl::cmdAccLs);
@@ -73,7 +73,7 @@ void Repl::registerCommands() {
{ArgKind::Task, ArgKind::None}, &Repl::cmdTskRename); {ArgKind::Task, ArgKind::None}, &Repl::cmdTskRename);
add("tsk reacc", "tsk reacc <name> <new>", "move a task to another account ('-' = none)", 2, 2, add("tsk reacc", "tsk reacc <name> <new>", "move a task to another account ('-' = none)", 2, 2,
{ArgKind::Task, ArgKind::AccountOrDash}, &Repl::cmdTskReacc); {ArgKind::Task, ArgKind::AccountOrDash}, &Repl::cmdTskReacc);
add("tsk restat", "tsk restat <name> <new>", "change a task's status (testing/prompt ignore the account's wait; plain 'wait' follows it again)", 2, kUnlimited, add("tsk restat", "tsk restat <name> <new>", "change a task's status ('wait <time>' sets its account's wait; plain 'wait' joins a running one)", 2, kUnlimited,
{ArgKind::Task, ArgKind::Status}, &Repl::cmdTskRestat); {ArgKind::Task, ArgKind::Status}, &Repl::cmdTskRestat);
add("tsk notes", "tsk notes <name>", "edit a task's notes in your editor (see 'editor')", 1, 1, add("tsk notes", "tsk notes <name>", "edit a task's notes in your editor (see 'editor')", 1, 1,
{ArgKind::Task}, &Repl::cmdTskNotes); {ArgKind::Task}, &Repl::cmdTskNotes);
+19 -8
View File
@@ -212,8 +212,8 @@ std::optional<Status> parse(const std::string& text, std::time_t now) {
} }
std::string serialize(const Status& status) { std::string serialize(const Status& status) {
if (status.kind == StatusKind::Wait) { if (status.kind == StatusKind::Wait || (status.kind == StatusKind::LimitReset && status.waitUntil != 0)) {
return "wait " + formatTime(status.waitUntil, "%Y-%m-%dT%H:%M"); return kindName(status.kind) + " " + formatTime(status.waitUntil, "%Y-%m-%dT%H:%M");
} }
return kindName(status.kind); return kindName(status.kind);
} }
@@ -228,9 +228,13 @@ std::optional<Status> deserialize(const std::string& text) {
if (!kind) { if (!kind) {
return std::nullopt; return std::nullopt;
} }
if (*kind != StatusKind::Wait) { const bool hasTime = (*kind == StatusKind::Wait || *kind == StatusKind::LimitReset);
if (!hasTime) {
return make(*kind); return make(*kind);
} }
if (argument.empty() && *kind == StatusKind::LimitReset) {
return make(*kind); // a Limit reset without a time
}
std::tm parts{}; std::tm parts{};
std::istringstream timeIn(argument); std::istringstream timeIn(argument);
@@ -240,21 +244,28 @@ std::optional<Status> deserialize(const std::string& text) {
} }
parts.tm_isdst = -1; parts.tm_isdst = -1;
Status status = make(StatusKind::Wait); Status status = make(*kind);
status.waitUntil = std::mktime(&parts); status.waitUntil = std::mktime(&parts);
return status; return status;
} }
bool followsAccountWait(StatusKind kind) { bool showsAccountWaiting(StatusKind kind) {
return kind == StatusKind::Prompt || kind == StatusKind::Testing; return kind == StatusKind::Prompt || kind == StatusKind::Testing;
} }
Status resolve(const Status& stored, const std::optional<std::time_t>& accountWait, std::time_t now) { Status resolve(const Status& stored, bool hasAccount, const std::optional<std::time_t>& accountWait,
if (accountWait && followsAccountWait(stored.kind)) { std::time_t now) {
Status shown = make(now < *accountWait ? StatusKind::Wait : StatusKind::LimitReset); if (stored.kind == StatusKind::Wait && hasAccount) {
if (accountWait && now < *accountWait) {
Status shown = make(StatusKind::Wait);
shown.waitUntil = *accountWait; shown.waitUntil = *accountWait;
return shown; return shown;
} }
// The account is not waiting any more: the conversation can be continued.
Status shown = make(StatusKind::LimitReset);
shown.waitUntil = accountWait ? *accountWait : stored.waitUntil;
return shown;
}
return effective(stored, now); return effective(stored, now);
} }
+87 -67
View File
@@ -2,6 +2,22 @@
#include "StringUtil.h" #include "StringUtil.h"
namespace {
Status waitStatus(std::time_t until) {
Status status = StatusUtil::make(StatusKind::Wait);
status.waitUntil = until;
return status;
}
Status limitResetStatus(std::time_t endedAt) {
Status status = StatusUtil::make(StatusKind::LimitReset);
status.waitUntil = endedAt;
return status;
}
} // namespace
TrackerError::TrackerError(ErrorCode code, const std::string& message) TrackerError::TrackerError(ErrorCode code, const std::string& message)
: std::runtime_error(message), code_(code) {} : std::runtime_error(message), code_(code) {}
@@ -65,7 +81,7 @@ TaskRow Tracker::existingTask(const std::string& name) const {
return *found; return *found;
} }
// The account's wait if it is still running (not yet a Limit reset). // The account's wait if it is still running.
std::optional<std::time_t> Tracker::runningWait(const std::string& account) const { std::optional<std::time_t> Tracker::runningWait(const std::string& account) const {
std::optional<AccountRow> row = db_.getAccount(account); std::optional<AccountRow> row = db_.getAccount(account);
if (!row || !row->cooldown) { if (!row || !row->cooldown) {
@@ -78,44 +94,44 @@ std::optional<std::time_t> Tracker::runningWait(const std::string& account) cons
return std::nullopt; return std::nullopt;
} }
// A task was explicitly set to Testing or Prompt. If its account's limit has reset // The account stops waiting: its Wait tasks become Limit reset (remembering when
// (a Limit reset), the account is in use again: the finished wait is cleared and // the wait ended, which keeps them in order). Returns how many tasks changed.
// the account's tasks go back to their own status. If the wait is still running, int Tracker::endAccountWait(const std::string& account, std::time_t endedAt) {
// only this task ignores it (it shows the status you gave it); the account's other int changed = 0;
// tasks keep following the wait. for (const TaskRow& task : db_.listTasks()) {
void Tracker::taskInUse(const std::string& taskName, const std::string& accountName) { if (!task.account || !StringUtil::equalsIgnoreCase(*task.account, account)) {
std::optional<AccountRow> account = db_.getAccount(accountName); continue;
if (!account || !account->cooldown) {
return;
} }
std::optional<Status> stored = StatusUtil::deserialize(task.status);
const std::optional<std::time_t> until = StatusUtil::timeFromText(*account->cooldown); if (stored && stored->kind == StatusKind::Wait) {
if (until && *until > now()) { db_.setTaskStatus(task.id, StatusUtil::serialize(limitResetStatus(endedAt)));
db_.setTaskWaitOverride(taskName, true); ++changed;
note("Account '" + account->id + "' is waiting until " + StatusUtil::clockLabel(*until) + "; '" + taskName +
"' ignores it (tsk restat " + taskName + " wait to follow it again).");
return;
} }
}
db_.setAccountCooldown(account->id, std::nullopt); db_.setAccountCooldown(account, std::nullopt);
db_.clearWaitOverrides(account->id); return changed;
note("Account '" + account->id + "' is back in use: its finished wait (Limit reset) was cleared.");
} }
// Prompt and Testing tasks follow their account's wait; anything else becomes Prompt. // Time passing ends waits by itself. The panels already show the result (see
void Tracker::makeTaskFollowAccount(const TaskRow& task) { // Views.cpp); this saves it, and runs before every change.
std::optional<Status> current = StatusUtil::deserialize(task.status); void Tracker::settle() {
if (!current || !StatusUtil::followsAccountWait(current->kind)) { const std::time_t current = now();
db_.setTaskStatus(task.id, StatusUtil::serialize(StatusUtil::make(StatusKind::Prompt))); for (const AccountRow& account : db_.listAccountRows()) {
if (!account.cooldown) {
continue;
} }
if (task.waitOverride) { const std::optional<std::time_t> until = StatusUtil::timeFromText(*account.cooldown);
db_.setTaskWaitOverride(task.id, false); if (until && *until > current) {
continue; // still waiting
}
endAccountWait(account.id, until ? *until : current);
} }
} }
// ---- Accounts ----------------------------------------------------------------- // ---- Accounts -----------------------------------------------------------------
void Tracker::addAccount(const std::string& name) { void Tracker::addAccount(const std::string& name) {
settle();
requireName(name); requireName(name);
if (db_.findAccount(name)) { if (db_.findAccount(name)) {
throw TrackerError(ErrorCode::Exists, "Account '" + name + "' already exists."); throw TrackerError(ErrorCode::Exists, "Account '" + name + "' already exists.");
@@ -125,12 +141,14 @@ void Tracker::addAccount(const std::string& name) {
} }
void Tracker::deleteAccount(const std::string& name) { void Tracker::deleteAccount(const std::string& name) {
settle();
const std::string account = existingAccount(name); const std::string account = existingAccount(name);
db_.removeAccount(account); db_.removeAccount(account);
note("Account '" + account + "' deleted."); note("Account '" + account + "' deleted.");
} }
void Tracker::renameAccount(const std::string& name, const std::string& newName) { void Tracker::renameAccount(const std::string& name, const std::string& newName) {
settle();
const std::string account = existingAccount(name); const std::string account = existingAccount(name);
requireName(newName); requireName(newName);
if (!StringUtil::equalsIgnoreCase(account, newName) && db_.findAccount(newName)) { if (!StringUtil::equalsIgnoreCase(account, newName) && db_.findAccount(newName)) {
@@ -141,11 +159,22 @@ void Tracker::renameAccount(const std::string& name, const std::string& newName)
} }
void Tracker::setAccountWait(const std::string& name, std::time_t until) { void Tracker::setAccountWait(const std::string& name, std::time_t until) {
settle();
const std::string account = existingAccount(name); const std::string account = existingAccount(name);
db_.setAccountCooldown(account, StatusUtil::timeToText(until)); db_.setAccountCooldown(account, StatusUtil::timeToText(until));
db_.clearWaitOverrides(account); // a new limit applies to every task of the account
// The tasks waiting on the account follow the new time.
for (const TaskRow& task : db_.listTasks()) {
if (!task.account || !StringUtil::equalsIgnoreCase(*task.account, account)) {
continue;
}
std::optional<Status> stored = StatusUtil::deserialize(task.status);
if (stored && stored->kind == StatusKind::Wait) {
db_.setTaskStatus(task.id, StatusUtil::serialize(waitStatus(until)));
}
}
note("Account '" + account + "' waits until " + StatusUtil::clockLabel(until) + note("Account '" + account + "' waits until " + StatusUtil::clockLabel(until) +
": its Prompt and Testing tasks show Wait, then Limit reset (acc unwait " + account + " to clear)."); ": tasks set to wait show Wait, then Limit reset (acc unwait " + account + " to end it now).");
} }
void Tracker::setAccountWaitText(const std::string& name, const std::string& timeText) { void Tracker::setAccountWaitText(const std::string& name, const std::string& timeText) {
@@ -158,6 +187,7 @@ void Tracker::setAccountWaitText(const std::string& name, const std::string& tim
} }
bool Tracker::clearAccountWait(const std::string& name) { bool Tracker::clearAccountWait(const std::string& name) {
settle();
std::optional<AccountRow> account = db_.getAccount(name); std::optional<AccountRow> account = db_.getAccount(name);
if (!account) { if (!account) {
throw TrackerError(ErrorCode::NotFound, "No account named '" + name + "'."); throw TrackerError(ErrorCode::NotFound, "No account named '" + name + "'.");
@@ -166,9 +196,11 @@ bool Tracker::clearAccountWait(const std::string& name) {
note("Account '" + account->id + "' is not waiting."); note("Account '" + account->id + "' is not waiting.");
return false; return false;
} }
db_.setAccountCooldown(account->id, std::nullopt); const int changed = endAccountWait(account->id, now());
db_.clearWaitOverrides(account->id); note("Wait ended on '" + account->id + "'" +
note("Wait cleared on '" + account->id + "'."); (changed > 0 ? " (" + std::to_string(changed) + (changed == 1 ? " task" : " tasks") + " now Limit reset)"
: std::string()) +
".");
return true; return true;
} }
@@ -176,6 +208,7 @@ bool Tracker::clearAccountWait(const std::string& name) {
void Tracker::addTask(const std::string& name, const std::optional<std::string>& accountName, void Tracker::addTask(const std::string& name, const std::optional<std::string>& accountName,
const std::optional<std::string>& statusText) { const std::optional<std::string>& statusText) {
settle();
requireName(name); requireName(name);
if (db_.findTask(name)) { if (db_.findTask(name)) {
throw TrackerError(ErrorCode::Exists, "Task '" + name + "' already exists."); throw TrackerError(ErrorCode::Exists, "Task '" + name + "' already exists.");
@@ -187,40 +220,38 @@ void Tracker::addTask(const std::string& name, const std::optional<std::string>&
} }
Status status = StatusUtil::make(StatusKind::Prompt); Status status = StatusUtil::make(StatusKind::Prompt);
const bool bareWait = statusText && StatusUtil::isBareWait(*statusText); bool setsAccountWait = false;
if (statusText) { if (statusText) {
if (bareWait) { if (StatusUtil::isBareWait(*statusText)) {
// "wait" without a time: only while the account is already waiting. // "wait" without a time: join the account's running wait.
if (!account || !runningWait(*account)) { const std::optional<std::time_t> until = account ? runningWait(*account) : std::nullopt;
if (!until) {
throw TrackerError(ErrorCode::Invalid, throw TrackerError(ErrorCode::Invalid,
"A time is required unless the task's account is already waiting: wait <time>"); "A time is required unless the task's account is already waiting: wait <time>");
} }
status = waitStatus(*until);
} else { } else {
status = parseStatus(*statusText); status = parseStatus(*statusText);
setsAccountWait = (status.kind == StatusKind::Wait && account);
} }
} }
// A wait on a task that has an account is the account's wait; the task itself db_.addTask(name, account, StatusUtil::serialize(status));
// stays Prompt and follows it.
const bool accountWait = (status.kind == StatusKind::Wait && account);
const Status stored = accountWait ? StatusUtil::make(StatusKind::Prompt) : status;
db_.addTask(name, account, StatusUtil::serialize(stored));
note("Task '" + name + "' added."); note("Task '" + name + "' added.");
if (accountWait) { if (setsAccountWait) {
setAccountWait(*account, status.waitUntil); setAccountWait(*account, status.waitUntil); // a wait on a task is its account's wait
} else if (statusText && !bareWait && account && StatusUtil::followsAccountWait(status.kind)) {
taskInUse(name, *account); // an explicit Testing / Prompt
} }
} }
void Tracker::deleteTask(const std::string& name) { void Tracker::deleteTask(const std::string& name) {
settle();
const TaskRow task = existingTask(name); const TaskRow task = existingTask(name);
db_.removeTask(task.id); db_.removeTask(task.id);
note("Task '" + task.id + "' deleted."); note("Task '" + task.id + "' deleted.");
} }
void Tracker::renameTask(const std::string& name, const std::string& newName) { void Tracker::renameTask(const std::string& name, const std::string& newName) {
settle();
const TaskRow task = existingTask(name); const TaskRow task = existingTask(name);
requireName(newName); requireName(newName);
if (!StringUtil::equalsIgnoreCase(task.id, newName) && db_.findTask(newName)) { if (!StringUtil::equalsIgnoreCase(task.id, newName) && db_.findTask(newName)) {
@@ -231,23 +262,22 @@ void Tracker::renameTask(const std::string& name, const std::string& newName) {
} }
void Tracker::setTaskAccount(const std::string& name, const std::optional<std::string>& accountName) { void Tracker::setTaskAccount(const std::string& name, const std::optional<std::string>& accountName) {
settle();
const TaskRow task = existingTask(name); const TaskRow task = existingTask(name);
std::optional<std::string> account; std::optional<std::string> account;
if (accountName) { if (accountName) {
account = existingAccount(*accountName); account = existingAccount(*accountName);
} }
db_.setTaskAccount(task.id, account); db_.setTaskAccount(task.id, account);
if (task.waitOverride) {
db_.setTaskWaitOverride(task.id, false);
}
note("Task '" + task.id + "' now on account " + (account ? "'" + *account + "'" : std::string("(none)")) + "."); note("Task '" + task.id + "' now on account " + (account ? "'" + *account + "'" : std::string("(none)")) + ".");
} }
void Tracker::setTaskStatus(const std::string& name, const std::string& statusText) { void Tracker::setTaskStatus(const std::string& name, const std::string& statusText) {
settle();
const TaskRow task = existingTask(name); const TaskRow task = existingTask(name);
if (StatusUtil::isBareWait(statusText)) { if (StatusUtil::isBareWait(statusText)) {
// "wait" without a time: the task follows its account's running wait again. // "wait" without a time: the task waits on its account's running wait.
const std::optional<std::time_t> until = task.account ? runningWait(*task.account) : std::nullopt; const std::optional<std::time_t> until = task.account ? runningWait(*task.account) : std::nullopt;
if (!until) { if (!until) {
throw TrackerError( throw TrackerError(
@@ -255,33 +285,23 @@ void Tracker::setTaskStatus(const std::string& name, const std::string& statusTe
task.account ? "Account '" + *task.account + "' is not waiting, so a time is required: wait <time>" task.account ? "Account '" + *task.account + "' is not waiting, so a time is required: wait <time>"
: "Task '" + task.id + "' has no account, so a time is required: wait <time>"); : "Task '" + task.id + "' has no account, so a time is required: wait <time>");
} }
makeTaskFollowAccount(task); db_.setTaskStatus(task.id, StatusUtil::serialize(waitStatus(*until)));
note("Task '" + task.id + "' now follows account '" + *task.account + "' (waiting until " + note("Task '" + task.id + "' now waits on account '" + *task.account + "' (until " +
StatusUtil::clockLabel(*until) + ")."); StatusUtil::clockLabel(*until) + ").");
return; return;
} }
const Status status = parseStatus(statusText); const Status status = parseStatus(statusText);
db_.setTaskStatus(task.id, StatusUtil::serialize(status));
note("Task '" + task.id + "' set to " + StatusUtil::label(status) + ".");
if (status.kind == StatusKind::Wait && task.account) { if (status.kind == StatusKind::Wait && task.account) {
// The wait belongs to the account. Keep an active task as it is so it setAccountWait(*task.account, status.waitUntil); // a wait on a task is its account's wait
// follows the wait; any other status becomes Prompt.
makeTaskFollowAccount(task);
setAccountWait(*task.account, status.waitUntil);
return;
}
db_.setTaskStatus(task.id, StatusUtil::serialize(status));
if (task.waitOverride) {
db_.setTaskWaitOverride(task.id, false);
}
note("Task '" + task.id + "' set to " + StatusUtil::label(status) + ".");
if (task.account && StatusUtil::followsAccountWait(status.kind)) {
taskInUse(task.id, *task.account); // an explicit Testing / Prompt
} }
} }
bool Tracker::setTaskNotes(const std::string& name, const std::string& notes) { bool Tracker::setTaskNotes(const std::string& name, const std::string& notes) {
settle();
const TaskRow task = existingTask(name); const TaskRow task = existingTask(name);
if (task.notes == notes) { if (task.notes == notes) {
note("Notes unchanged."); note("Notes unchanged.");
+11 -6
View File
@@ -96,8 +96,9 @@ std::vector<TaskInfo> buildTasks(const Database& db, std::time_t now) {
accountWait = found->second; accountWait = found->second;
} }
} }
info.overridesWait = row.waitOverride && accountWait && StatusUtil::followsAccountWait(info.storedStatus.kind); info.status = StatusUtil::resolve(info.storedStatus, row.account.has_value(), accountWait, now);
info.status = StatusUtil::resolve(info.storedStatus, info.overridesWait ? std::nullopt : accountWait, now); info.accountWaiting = accountWait && *accountWait > now && StatusUtil::showsAccountWaiting(info.status.kind);
info.statusLabel = StatusUtil::label(info.status) + (info.accountWaiting ? " (account-waiting)" : "");
tasks.push_back(info); tasks.push_back(info);
} }
@@ -123,22 +124,26 @@ std::vector<AccountInfo> buildAccounts(const Database& db, std::time_t now) {
case AccountLevel::Taken: taken.push_back(task.name); break; case AccountLevel::Taken: taken.push_back(task.name); break;
case AccountLevel::Free: free.push_back(task.name); break; case AccountLevel::Free: free.push_back(task.name); break;
} }
if (task.status.kind == StatusKind::Wait || task.status.kind == StatusKind::LimitReset) { if (task.status.kind == StatusKind::Wait) {
showsWait = true; showsWait = true;
} }
} }
// The account waits until its wait time; after that it has stopped waiting
// by itself (its Wait tasks show Limit reset), whether or not the old value
// is still stored.
AccountInfo info; AccountInfo info;
info.name = row.id; info.name = row.id;
if (row.cooldown) { if (row.cooldown) {
if (std::optional<std::time_t> until = StatusUtil::timeFromText(*row.cooldown)) { std::optional<std::time_t> until = StatusUtil::timeFromText(*row.cooldown);
if (until && *until > now) {
info.waitState = WaitState::Waiting;
info.waitUntil = *until; info.waitUntil = *until;
info.waitState = (*until > now) ? WaitState::Waiting : WaitState::LimitReset;
info.waitListed = !showsWait; info.waitListed = !showsWait;
} }
} }
if (!busy.empty() || info.waitListed) { if (!busy.empty() || info.waitState == WaitState::Waiting) {
info.level = AccountLevel::Busy; info.level = AccountLevel::Busy;
info.tasks = busy; info.tasks = busy;
} else if (!taken.empty()) { } else if (!taken.empty()) {
+2 -6
View File
@@ -29,7 +29,6 @@ lls_db::lls_db() : database(), tracker(database) {}
struct lls_task_list { struct lls_task_list {
std::vector<TaskInfo> items; std::vector<TaskInfo> items;
std::vector<std::string> labels;
std::vector<lls_task_info> infos; std::vector<lls_task_info> infos;
}; };
@@ -52,7 +51,6 @@ static_assert(static_cast<int>(AccountLevel::Taken) == LLS_LEVEL_TAKEN, "level n
static_assert(static_cast<int>(AccountLevel::Free) == LLS_LEVEL_FREE, "level numbering"); static_assert(static_cast<int>(AccountLevel::Free) == LLS_LEVEL_FREE, "level numbering");
static_assert(static_cast<int>(WaitState::None) == LLS_WAIT_NONE, "wait state numbering"); static_assert(static_cast<int>(WaitState::None) == LLS_WAIT_NONE, "wait state numbering");
static_assert(static_cast<int>(WaitState::Waiting) == LLS_WAIT_WAITING, "wait state numbering"); static_assert(static_cast<int>(WaitState::Waiting) == LLS_WAIT_WAITING, "wait state numbering");
static_assert(static_cast<int>(WaitState::LimitReset) == LLS_WAIT_LIMIT_RESET, "wait state numbering");
namespace { namespace {
@@ -309,24 +307,22 @@ int lls_tasks(lls_db* db, lls_task_list** out_list) {
std::unique_ptr<lls_task_list> list(new lls_task_list); std::unique_ptr<lls_task_list> list(new lls_task_list);
list->items = db->tracker.tasks(); list->items = db->tracker.tasks();
const std::size_t count = list->items.size(); const std::size_t count = list->items.size();
list->labels.resize(count);
list->infos.resize(count); // sized once: the pointers below stay valid list->infos.resize(count); // sized once: the pointers below stay valid
for (std::size_t i = 0; i < count; ++i) { for (std::size_t i = 0; i < count; ++i) {
const TaskInfo& task = list->items[i]; const TaskInfo& task = list->items[i];
list->labels[i] = StatusUtil::label(task.status);
lls_task_info& info = list->infos[i]; lls_task_info& info = list->infos[i];
info.name = task.name.c_str(); info.name = task.name.c_str();
info.account = task.account ? task.account->c_str() : nullptr; info.account = task.account ? task.account->c_str() : nullptr;
info.status = static_cast<int>(task.status.kind); info.status = static_cast<int>(task.status.kind);
info.status_label = list->labels[i].c_str(); info.status_label = task.statusLabel.c_str();
info.stored_status = static_cast<int>(task.storedStatus.kind); info.stored_status = static_cast<int>(task.storedStatus.kind);
const bool timed = task.status.kind == StatusKind::Wait || task.status.kind == StatusKind::LimitReset; const bool timed = task.status.kind == StatusKind::Wait || task.status.kind == StatusKind::LimitReset;
info.until = timed ? static_cast<int64_t>(task.status.waitUntil) : 0; info.until = timed ? static_cast<int64_t>(task.status.waitUntil) : 0;
info.notes = task.notes.c_str(); info.notes = task.notes.c_str();
info.notes_lines = static_cast<int>(task.noteLines); info.notes_lines = static_cast<int>(task.noteLines);
info.overrides_wait = task.overridesWait ? 1 : 0; info.account_waiting = task.accountWaiting ? 1 : 0;
} }
*out_list = list.release(); *out_list = list.release();
}, true); }, true);