Limits are now account-tied rather than task-tied
This commit is contained in:
1 parent
315a2102a7
commit
0af468f47c
9 files changed
+235
-14
No files matched your search
@@ -21,9 +21,9 @@ installed; otherwise CMake downloads the amalgamation (needs network once).
|
|||||||
acc add <name> tsk add <name> [acc] [status]
|
acc add <name> tsk add <name> [acc] [status]
|
||||||
acc del <name> tsk del <name>
|
acc del <name> tsk del <name>
|
||||||
acc rename <name> <new> tsk rename <name> <new>
|
acc rename <name> <new> tsk rename <name> <new>
|
||||||
acc ls tsk reacc <name> <new|->
|
acc wait <name> <time> tsk reacc <name> <new|->
|
||||||
tsk restat <name> <new>
|
acc unwait <name> tsk restat <name> <new>
|
||||||
tsk notes <name> (opens your editor)
|
acc ls tsk notes <name> (opens your editor)
|
||||||
tsk ls
|
tsk ls
|
||||||
|
|
||||||
disp tsk (hide|show)
|
disp tsk (hide|show)
|
||||||
@@ -82,6 +82,15 @@ 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
|
||||||
|
<time>`, which sets its account's wait). Until the time the account's Prompt
|
||||||
|
and Testing tasks show `Wait HH:MM`; after it they show `Limit reset`, until
|
||||||
|
you run `acc unwait <name>`, which also clears a stale one. Uploading, Frozen
|
||||||
|
and Done tasks are not affected, and the tasks keep their own stored status
|
||||||
|
underneath. A wait that no task shows (no tasks, or only Done / Frozen /
|
||||||
|
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. 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>`.
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ public:
|
|||||||
void addAccount(const std::string& name);
|
void addAccount(const std::string& name);
|
||||||
void removeAccount(const std::string& name);
|
void removeAccount(const std::string& name);
|
||||||
void renameAccount(const std::string& name, const std::string& newName);
|
void renameAccount(const std::string& name, const std::string& newName);
|
||||||
|
std::optional<AccountRow> getAccount(const std::string& name) const;
|
||||||
|
std::vector<AccountRow> listAccountRows() const;
|
||||||
|
void setAccountCooldown(const std::string& name, const std::optional<std::string>& cooldown);
|
||||||
|
|
||||||
// Tasks
|
// Tasks
|
||||||
std::optional<std::string> findTask(const std::string& name) const; // canonical spelling
|
std::optional<std::string> findTask(const std::string& name) const; // canonical spelling
|
||||||
|
|||||||
@@ -9,3 +9,8 @@ struct TaskRow {
|
|||||||
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)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct AccountRow {
|
||||||
|
std::string id;
|
||||||
|
std::optional<std::string> cooldown; // the account's wait: when its limit ends (StatusUtil::timeToText)
|
||||||
|
};
|
||||||
@@ -37,6 +37,8 @@ private:
|
|||||||
void execute(const std::string& line);
|
void execute(const std::string& line);
|
||||||
void refresh() const;
|
void refresh() const;
|
||||||
void say(const std::string& message);
|
void say(const std::string& message);
|
||||||
|
void setAccountWait(const std::string& account, std::time_t until);
|
||||||
|
void noteAccountWait(const std::string& taskName);
|
||||||
void flushMessages();
|
void flushMessages();
|
||||||
void endOfInput();
|
void endOfInput();
|
||||||
bool confirm(const std::string& question) const;
|
bool confirm(const std::string& question) const;
|
||||||
@@ -49,6 +51,8 @@ private:
|
|||||||
bool cmdAccAdd(const Args& args);
|
bool cmdAccAdd(const Args& args);
|
||||||
bool cmdAccDel(const Args& args);
|
bool cmdAccDel(const Args& args);
|
||||||
bool cmdAccRename(const Args& args);
|
bool cmdAccRename(const Args& args);
|
||||||
|
bool cmdAccWait(const Args& args);
|
||||||
|
bool cmdAccUnwait(const Args& args);
|
||||||
bool cmdAccLs(const Args& args);
|
bool cmdAccLs(const Args& args);
|
||||||
|
|
||||||
// Task commands
|
// Task commands
|
||||||
|
|||||||
@@ -26,9 +26,23 @@ 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
|
||||||
|
// Done tasks do not.
|
||||||
|
bool followsAccountWait(StatusKind kind);
|
||||||
|
|
||||||
|
// The status to show for a task. If its account has a wait, the tasks that
|
||||||
|
// follow it show Wait until then and Limit reset after; otherwise the stored
|
||||||
|
// status is used (see effective).
|
||||||
|
Status resolve(const Status& stored, 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);
|
||||||
|
|
||||||
|
// Times kept outside a Status (an account's cooldown): "2026-10-17T16:30".
|
||||||
|
std::string timeToText(std::time_t value);
|
||||||
|
std::optional<std::time_t> timeFromText(const std::string& text);
|
||||||
|
std::string clockLabel(std::time_t value); // "16:30"
|
||||||
|
|
||||||
std::string label(const Status& status); // "Prompt", "Limit reset", "Wait 16:30", ...
|
std::string label(const Status& status); // "Prompt", "Limit reset", "Wait 16:30", ...
|
||||||
std::string kindName(StatusKind kind); // lowercase token: "prompt", "limit-reset", ...
|
std::string kindName(StatusKind kind); // lowercase token: "prompt", "limit-reset", ...
|
||||||
std::vector<StatusKind> allKinds();
|
std::vector<StatusKind> allKinds();
|
||||||
|
|||||||
+46
-1
@@ -13,7 +13,8 @@ namespace {
|
|||||||
|
|
||||||
const char* const kSchema =
|
const char* const kSchema =
|
||||||
"CREATE TABLE IF NOT EXISTS Account ("
|
"CREATE TABLE IF NOT EXISTS Account ("
|
||||||
" id TEXT NOT NULL PRIMARY KEY COLLATE NOCASE"
|
" id TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,"
|
||||||
|
" cooldown TEXT"
|
||||||
");"
|
");"
|
||||||
"CREATE TABLE IF NOT EXISTS Task ("
|
"CREATE TABLE IF NOT EXISTS Task ("
|
||||||
" id TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,"
|
" id TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,"
|
||||||
@@ -97,6 +98,24 @@ void Database::createSchema() {
|
|||||||
exec(kSchema);
|
exec(kSchema);
|
||||||
// "available" was split into "prompt" and "limit-reset"; old files still say "available".
|
// "available" was split into "prompt" and "limit-reset"; old files still say "available".
|
||||||
exec("UPDATE Task SET status = 'prompt' WHERE status = 'available';");
|
exec("UPDATE Task SET status = 'prompt' WHERE status = 'available';");
|
||||||
|
|
||||||
|
// Files from before account cooldowns have no such column.
|
||||||
|
bool hasCooldown = false;
|
||||||
|
for (const Row& row : query("PRAGMA table_info(Account)")) {
|
||||||
|
hasCooldown = hasCooldown || (row.size() > 1 && row[1] && *row[1] == "cooldown");
|
||||||
|
}
|
||||||
|
if (!hasCooldown) {
|
||||||
|
exec("ALTER TABLE Account ADD COLUMN cooldown TEXT;");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
"UPDATE Account SET cooldown = max(coalesce(cooldown, ''), ("
|
||||||
|
" SELECT max(substr(Task.status, 6)) 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 %';");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Database::exec(const std::string& sql) {
|
void Database::exec(const std::string& sql) {
|
||||||
@@ -232,6 +251,32 @@ void Database::renameAccount(const std::string& name, const std::string& newName
|
|||||||
run("UPDATE Account SET id = ?2 WHERE id = ?1", {name, newName});
|
run("UPDATE Account SET id = ?2 WHERE id = ?1", {name, newName});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<AccountRow> Database::getAccount(const std::string& name) const {
|
||||||
|
std::vector<Row> rows = query("SELECT id, cooldown FROM Account WHERE id = ?1", {name});
|
||||||
|
if (rows.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
AccountRow account;
|
||||||
|
account.id = *rows[0][0];
|
||||||
|
account.cooldown = rows[0][1];
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<AccountRow> Database::listAccountRows() const {
|
||||||
|
std::vector<AccountRow> accounts;
|
||||||
|
for (const Row& row : query("SELECT id, cooldown FROM Account ORDER BY id")) {
|
||||||
|
AccountRow account;
|
||||||
|
account.id = *row[0];
|
||||||
|
account.cooldown = row[1];
|
||||||
|
accounts.push_back(account);
|
||||||
|
}
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Database::setAccountCooldown(const std::string& name, const std::optional<std::string>& cooldown) {
|
||||||
|
run("UPDATE Account SET cooldown = ?2 WHERE id = ?1", {name, cooldown});
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Tasks ------------------------------------------------------------------
|
// ---- Tasks ------------------------------------------------------------------
|
||||||
|
|
||||||
std::optional<std::string> Database::findTask(const std::string& name) const {
|
std::optional<std::string> Database::findTask(const std::string& name) const {
|
||||||
|
|||||||
+37
-5
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -18,6 +19,16 @@ struct TaskView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::vector<TaskView> loadViews(const Database& db, std::time_t now) {
|
std::vector<TaskView> loadViews(const Database& db, std::time_t now) {
|
||||||
|
// Each account's wait (when its limit ends), keyed by lowercase name.
|
||||||
|
std::map<std::string, std::time_t> accountWaits;
|
||||||
|
for (const AccountRow& account : db.listAccountRows()) {
|
||||||
|
if (account.cooldown) {
|
||||||
|
if (std::optional<std::time_t> until = StatusUtil::timeFromText(*account.cooldown)) {
|
||||||
|
accountWaits[StringUtil::lower(account.id)] = *until;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<TaskView> views;
|
std::vector<TaskView> views;
|
||||||
for (const TaskRow& row : db.listTasks()) {
|
for (const TaskRow& row : db.listTasks()) {
|
||||||
TaskView view;
|
TaskView view;
|
||||||
@@ -25,7 +36,15 @@ std::vector<TaskView> loadViews(const Database& db, std::time_t now) {
|
|||||||
view.account = row.account.value_or("");
|
view.account = row.account.value_or("");
|
||||||
view.notes = row.notes;
|
view.notes = row.notes;
|
||||||
std::optional<Status> stored = StatusUtil::deserialize(row.status);
|
std::optional<Status> stored = StatusUtil::deserialize(row.status);
|
||||||
view.status = StatusUtil::effective(stored.value_or(StatusUtil::make(StatusKind::Prompt)), now);
|
|
||||||
|
std::optional<std::time_t> accountWait;
|
||||||
|
if (row.account) {
|
||||||
|
auto found = accountWaits.find(StringUtil::lower(*row.account));
|
||||||
|
if (found != accountWaits.end()) {
|
||||||
|
accountWait = found->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.status = StatusUtil::resolve(stored.value_or(StatusUtil::make(StatusKind::Prompt)), accountWait, now);
|
||||||
views.push_back(view);
|
views.push_back(view);
|
||||||
}
|
}
|
||||||
return views;
|
return views;
|
||||||
@@ -129,6 +148,9 @@ std::string Display::renderAccounts(std::time_t now) const {
|
|||||||
// One row per account. Its level is the most taken of its tasks
|
// One row per account. Its level is the most taken of its tasks
|
||||||
// (Busy > Taken > Free); the TASKS column lists the tasks at that level
|
// (Busy > Taken > Free); the TASKS column lists the tasks at that level
|
||||||
// (for a Free account that is its Uploading, Frozen and Done tasks).
|
// (for a Free account that is its Uploading, Frozen and Done tasks).
|
||||||
|
// An account wait that no task is showing (no tasks, or only Done / Frozen /
|
||||||
|
// Uploading ones) is listed as "(wait HH:MM)" / "(limit reset)" and keeps the
|
||||||
|
// account Busy.
|
||||||
struct AccountView {
|
struct AccountView {
|
||||||
std::string name;
|
std::string name;
|
||||||
AccountLevel level;
|
AccountLevel level;
|
||||||
@@ -136,12 +158,13 @@ std::string Display::renderAccounts(std::time_t now) const {
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::vector<AccountView> accounts;
|
std::vector<AccountView> accounts;
|
||||||
for (const std::string& account : db_.listAccounts()) {
|
for (const AccountRow& row : db_.listAccountRows()) {
|
||||||
std::vector<std::string> busy;
|
std::vector<std::string> busy;
|
||||||
std::vector<std::string> taken;
|
std::vector<std::string> taken;
|
||||||
std::vector<std::string> free;
|
std::vector<std::string> free;
|
||||||
|
bool showsWait = false;
|
||||||
for (const TaskView& view : views) {
|
for (const TaskView& view : views) {
|
||||||
if (view.account != account) {
|
if (view.account != row.id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const AccountLevel level = levelOf(view.status.kind);
|
const AccountLevel level = levelOf(view.status.kind);
|
||||||
@@ -152,10 +175,19 @@ std::string Display::renderAccounts(std::time_t now) const {
|
|||||||
} else {
|
} else {
|
||||||
free.push_back(view.name);
|
free.push_back(view.name);
|
||||||
}
|
}
|
||||||
|
if (view.status.kind == StatusKind::Wait || view.status.kind == StatusKind::LimitReset) {
|
||||||
|
showsWait = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.cooldown && !showsWait) {
|
||||||
|
if (std::optional<std::time_t> until = StatusUtil::timeFromText(*row.cooldown)) {
|
||||||
|
busy.push_back(*until > now ? "(wait " + StatusUtil::clockLabel(*until) + ")" : "(limit reset)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountView entry;
|
AccountView entry;
|
||||||
entry.name = account;
|
entry.name = row.id;
|
||||||
if (!busy.empty()) {
|
if (!busy.empty()) {
|
||||||
entry.level = AccountLevel::Busy;
|
entry.level = AccountLevel::Busy;
|
||||||
entry.tasks = busy;
|
entry.tasks = busy;
|
||||||
@@ -170,7 +202,7 @@ std::string Display::renderAccounts(std::time_t now) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Most free first: Free, then Taken, then Busy; within Taken and Busy fewer
|
// Most free first: Free, then Taken, then Busy; within Taken and Busy fewer
|
||||||
// tasks first (Free accounts are all equally free). listAccounts is
|
// tasks first (Free accounts are all equally free). listAccountRows is
|
||||||
// alphabetical and stable_sort keeps that order for ties.
|
// alphabetical and stable_sort keeps that order for ties.
|
||||||
std::stable_sort(accounts.begin(), accounts.end(), [](const AccountView& a, const AccountView& b) {
|
std::stable_sort(accounts.begin(), accounts.end(), [](const AccountView& a, const AccountView& b) {
|
||||||
if (a.level != b.level) {
|
if (a.level != b.level) {
|
||||||
|
|||||||
+82
-5
@@ -62,6 +62,11 @@ void Repl::registerCommands() {
|
|||||||
{ArgKind::Account}, &Repl::cmdAccDel);
|
{ArgKind::Account}, &Repl::cmdAccDel);
|
||||||
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>",
|
||||||
|
"the account hit its limit until <time>: its Prompt/Testing tasks show Wait, then Limit reset",
|
||||||
|
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)",
|
||||||
|
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);
|
||||||
|
|
||||||
add("tsk add", "tsk add <name> [acc] [status]", "create a task", 1, kUnlimited,
|
add("tsk add", "tsk add <name> [acc] [status]", "create a task", 1, kUnlimited,
|
||||||
@@ -277,6 +282,55 @@ bool Repl::cmdAccRename(const Args& args) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Repl::setAccountWait(const std::string& account, std::time_t until) {
|
||||||
|
db_.setAccountCooldown(account, StatusUtil::timeToText(until));
|
||||||
|
say("Account '" + account + "' waits until " + StatusUtil::clockLabel(until) +
|
||||||
|
": its Prompt and Testing tasks show Wait, then Limit reset (acc unwait " + account + " to clear).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tells the user when a task's own status is hidden by its account's wait.
|
||||||
|
void Repl::noteAccountWait(const std::string& taskName) {
|
||||||
|
std::optional<TaskRow> task = db_.getTask(taskName);
|
||||||
|
std::optional<Status> stored = task ? StatusUtil::deserialize(task->status) : std::nullopt;
|
||||||
|
if (!task || !task->account || !stored || !StatusUtil::followsAccountWait(stored->kind)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::optional<AccountRow> account = db_.getAccount(*task->account);
|
||||||
|
if (account && account->cooldown) {
|
||||||
|
say("It shows as Wait / Limit reset because account '" + account->id + "' is waiting (acc unwait " +
|
||||||
|
account->id + " to clear).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Repl::cmdAccWait(const Args& args) {
|
||||||
|
std::optional<AccountRow> account = db_.getAccount(args[0]);
|
||||||
|
if (!account) {
|
||||||
|
throw std::runtime_error("No account named '" + args[0] + "'.");
|
||||||
|
}
|
||||||
|
const std::string text = "wait " + StringUtil::join(args, 1, " ");
|
||||||
|
std::optional<Status> status = StatusUtil::parse(text, std::time(nullptr));
|
||||||
|
if (!status) {
|
||||||
|
throw std::runtime_error("Invalid time '" + StringUtil::join(args, 1, " ") +
|
||||||
|
"'. Use e.g. 16:30, 4:30pm or 4pm.");
|
||||||
|
}
|
||||||
|
setAccountWait(account->id, status->waitUntil);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Repl::cmdAccUnwait(const Args& args) {
|
||||||
|
std::optional<AccountRow> account = db_.getAccount(args[0]);
|
||||||
|
if (!account) {
|
||||||
|
throw std::runtime_error("No account named '" + args[0] + "'.");
|
||||||
|
}
|
||||||
|
if (!account->cooldown) {
|
||||||
|
say("Account '" + account->id + "' is not waiting.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
db_.setAccountCooldown(account->id, std::nullopt);
|
||||||
|
say("Wait cleared on '" + account->id + "'.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool Repl::cmdAccLs(const Args&) {
|
bool Repl::cmdAccLs(const Args&) {
|
||||||
Terminal::clearScreen();
|
Terminal::clearScreen();
|
||||||
std::cout << display_.renderAccounts(std::time(nullptr)) << "\n";
|
std::cout << display_.renderAccounts(std::time(nullptr)) << "\n";
|
||||||
@@ -311,8 +365,18 @@ bool Repl::cmdTskAdd(const Args& args) {
|
|||||||
status = parseStatusFrom(args, next);
|
status = parseStatusFrom(args, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
db_.addTask(name, account, StatusUtil::serialize(status));
|
// A wait on a task that has an account is the account's wait; the task itself
|
||||||
|
// 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));
|
||||||
say("Task '" + name + "' added.");
|
say("Task '" + name + "' added.");
|
||||||
|
if (accountWait) {
|
||||||
|
setAccountWait(*account, status.waitUntil);
|
||||||
|
} else {
|
||||||
|
noteAccountWait(name);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,13 +429,26 @@ bool Repl::cmdTskReacc(const Args& args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Repl::cmdTskRestat(const Args& args) {
|
bool Repl::cmdTskRestat(const Args& args) {
|
||||||
std::optional<std::string> name = db_.findTask(args[0]);
|
std::optional<TaskRow> task = db_.getTask(args[0]);
|
||||||
if (!name) {
|
if (!task) {
|
||||||
throw std::runtime_error("No task named '" + args[0] + "'.");
|
throw std::runtime_error("No task named '" + args[0] + "'.");
|
||||||
}
|
}
|
||||||
Status status = parseStatusFrom(args, 1);
|
Status status = parseStatusFrom(args, 1);
|
||||||
db_.setTaskStatus(*name, StatusUtil::serialize(status));
|
|
||||||
say("Task '" + *name + "' set to " + StatusUtil::label(status) + ".");
|
if (status.kind == StatusKind::Wait && task->account) {
|
||||||
|
// The wait belongs to the account. Keep an active task as it is so it
|
||||||
|
// follows the wait; any other status becomes Prompt.
|
||||||
|
std::optional<Status> current = StatusUtil::deserialize(task->status);
|
||||||
|
if (!current || !StatusUtil::followsAccountWait(current->kind)) {
|
||||||
|
db_.setTaskStatus(task->id, StatusUtil::serialize(StatusUtil::make(StatusKind::Prompt)));
|
||||||
|
}
|
||||||
|
setAccountWait(*task->account, status.waitUntil);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
db_.setTaskStatus(task->id, StatusUtil::serialize(status));
|
||||||
|
say("Task '" + task->id + "' set to " + StatusUtil::label(status) + ".");
|
||||||
|
noteAccountWait(task->id);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -245,6 +245,19 @@ std::optional<Status> deserialize(const std::string& text) {
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool followsAccountWait(StatusKind kind) {
|
||||||
|
return kind == StatusKind::Prompt || kind == StatusKind::Testing;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status resolve(const Status& stored, const std::optional<std::time_t>& accountWait, std::time_t now) {
|
||||||
|
if (accountWait && followsAccountWait(stored.kind)) {
|
||||||
|
Status shown = make(now < *accountWait ? StatusKind::Wait : StatusKind::LimitReset);
|
||||||
|
shown.waitUntil = *accountWait;
|
||||||
|
return shown;
|
||||||
|
}
|
||||||
|
return effective(stored, now);
|
||||||
|
}
|
||||||
|
|
||||||
Status effective(const Status& status, std::time_t now) {
|
Status effective(const Status& status, std::time_t now) {
|
||||||
if (status.kind == StatusKind::Wait && now >= status.waitUntil) {
|
if (status.kind == StatusKind::Wait && now >= status.waitUntil) {
|
||||||
Status expired = make(StatusKind::LimitReset);
|
Status expired = make(StatusKind::LimitReset);
|
||||||
@@ -254,6 +267,25 @@ Status effective(const Status& status, std::time_t now) {
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string timeToText(std::time_t value) {
|
||||||
|
return formatTime(value, "%Y-%m-%dT%H:%M");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::time_t> timeFromText(const std::string& text) {
|
||||||
|
std::tm parts{};
|
||||||
|
std::istringstream in(text);
|
||||||
|
in >> std::get_time(&parts, "%Y-%m-%dT%H:%M");
|
||||||
|
if (in.fail()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
parts.tm_isdst = -1;
|
||||||
|
return std::mktime(&parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string clockLabel(std::time_t value) {
|
||||||
|
return formatTime(value, "%H:%M");
|
||||||
|
}
|
||||||
|
|
||||||
std::string label(const Status& status) {
|
std::string label(const Status& status) {
|
||||||
if (status.kind == StatusKind::Wait) {
|
if (status.kind == StatusKind::Wait) {
|
||||||
return "Wait " + formatTime(status.waitUntil, "%H:%M");
|
return "Wait " + formatTime(status.waitUntil, "%H:%M");
|
||||||
|
|||||||
Reference in new issue
Block a user