diff --git a/README.md b/README.md index ad78a6f..72fa2d4 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ installed; otherwise CMake downloads the amalgamation (needs network once). disp tsk (hide|show) disp tsk level (hide|show) (prompt|limit-reset|testing|wait|uploading|frozen|done|all) disp acc (hide|show) - disp acc level (hide|show) (busy|free|all) + disp acc level (hide|show) (busy|taken|free|all) disp (show current settings) db save | db backup [file] | db restore @@ -73,11 +73,14 @@ goes back to `$VISUAL` / `$EDITOR` / nano / vim / vi. The choice is stored in * The stored status is never shown directly: a `wait` task displays as `Wait HH:MM` until that time passes, then as `Limit reset`. Tasks are sorted by computed status (Limit reset, Testing, Prompt, Wait, Uploading, Frozen, - Done), then account A-Z (unassigned last), then wait time, then name. The panels are cleared and redrawn after changes. -* Account panel: one row per account, sorted from most free to least free. - `Busy` while any of its tasks is Testing or Wait (those tasks are listed), - otherwise `Free`; fewer busy tasks sorts first, ties by name - (see `Display::renderAccounts` / `StatusUtil::isBusy` to change the rules). + 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. +* Account panel: one row per account, judged by its tasks: `Busy` if any is + Testing, Limit reset or Wait; else `Taken` if any is Prompt; else `Free` + (only Done, Frozen or Uploading tasks, or no tasks). The TASKS column lists + the tasks at that level. Rows are sorted most free first: Free, Taken, Busy, + fewer tasks first, ties by name (see `levelOf` in `Display.cpp` and + `StatusUtil::isBusy` / `isTaken` to change the rules). * Deleting an account leaves its tasks unassigned (`-`). * 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 `. diff --git a/include/Display.h b/include/Display.h index 1c5a3cf..18f97fa 100644 --- a/include/Display.h +++ b/include/Display.h @@ -7,7 +7,13 @@ #include "Database.h" #include "Status.h" -enum class AccountLevel { Busy, Free }; +// How taken an account is, judged by its tasks: +// Free - only Done, Frozen or Uploading tasks, or no tasks +// Taken - has a Prompt task (and nothing Busy) +// Busy - has a Testing, Limit reset or Wait task +enum class AccountLevel { Busy, Taken, Free }; + +std::string accountLevelName(AccountLevel level); // "busy", "taken", "free" struct DisplaySettings { bool showTasks = true; // auto-print the task table after each change diff --git a/include/Status.h b/include/Status.h index e7ab8b2..ed17428 100644 --- a/include/Status.h +++ b/include/Status.h @@ -11,7 +11,7 @@ enum class StatusKind { Prompt, LimitReset, Testing, Wait, Uploading, Frozen, Do struct Status { StatusKind kind = StatusKind::Prompt; - std::time_t waitUntil = 0; // only meaningful when kind == Wait + std::time_t waitUntil = 0; // when the wait ends; set for Wait, and kept on the computed LimitReset }; namespace StatusUtil { @@ -36,6 +36,7 @@ std::vector allKinds(); // old name for "prompt". std::optional kindFromName(const std::string& name); int rank(StatusKind kind); // task sort priority, lowest first -bool isBusy(StatusKind kind); // does this status hold the account? +bool isBusy(StatusKind kind); // keeps its account Busy: Testing, Limit reset, Wait +bool isTaken(StatusKind kind); // keeps its account Taken: Prompt } // namespace StatusUtil diff --git a/src/Completer.cpp b/src/Completer.cpp index e4bb00c..6ba421b 100644 --- a/src/Completer.cpp +++ b/src/Completer.cpp @@ -101,7 +101,7 @@ std::vector Completer::valuesFor(ArgKind kind, const std::string& c values.push_back("all"); break; case ArgKind::AccountLevel: - values = {"busy", "free", "all"}; + values = {"busy", "taken", "free", "all"}; break; case ArgKind::File: values = fileNames(current); diff --git a/src/Display.cpp b/src/Display.cpp index 3f9f6b9..71dce66 100644 --- a/src/Display.cpp +++ b/src/Display.cpp @@ -1,6 +1,7 @@ #include "Display.h" #include +#include #include #include @@ -30,14 +31,19 @@ std::vector loadViews(const Database& db, std::time_t now) { return views; } -// Task order: status priority (see StatusUtil::rank), then account A-Z -// (tasks without an account last), then earliest wait time, then task name. +// Task order: status priority (see StatusUtil::rank); then, for Wait and Limit +// reset, time (earliest first: when the wait ends / ended); then account A-Z +// (tasks without an account last); then task name. bool taskLess(const TaskView& a, const TaskView& b) { const int rankA = StatusUtil::rank(a.status.kind); const int rankB = StatusUtil::rank(b.status.kind); if (rankA != rankB) { return rankA < rankB; } + const StatusKind kind = a.status.kind; // same kind as b: ranks are unique per kind + if ((kind == StatusKind::Wait || kind == StatusKind::LimitReset) && a.status.waitUntil != b.status.waitUntil) { + return a.status.waitUntil < b.status.waitUntil; + } if (a.account.empty() != b.account.empty()) { return !a.account.empty(); } @@ -46,12 +52,29 @@ bool taskLess(const TaskView& a, const TaskView& b) { if (accountA != accountB) { return accountA < accountB; } - if (a.status.kind == StatusKind::Wait && a.status.waitUntil != b.status.waitUntil) { - return a.status.waitUntil < b.status.waitUntil; - } return StringUtil::lower(a.name) < StringUtil::lower(b.name); } +AccountLevel levelOf(StatusKind kind) { + if (StatusUtil::isBusy(kind)) { + return AccountLevel::Busy; + } + if (StatusUtil::isTaken(kind)) { + return AccountLevel::Taken; + } + return AccountLevel::Free; +} + +// Free first, then Taken, then Busy. +int levelRank(AccountLevel level) { + switch (level) { + case AccountLevel::Free: return 0; + case AccountLevel::Taken: return 1; + case AccountLevel::Busy: return 2; + } + return 2; +} + // "-" when there are no notes, otherwise how many lines they have. std::string notesSummary(const std::string& notes) { const std::size_t lines = StringUtil::lineCount(notes); @@ -90,44 +113,74 @@ std::string Display::renderTasks(std::time_t now) const { return table.render(); } +std::string accountLevelName(AccountLevel level) { + switch (level) { + case AccountLevel::Busy: return "busy"; + case AccountLevel::Taken: return "taken"; + case AccountLevel::Free: return "free"; + } + return "free"; +} + std::string Display::renderAccounts(std::time_t now) const { std::vector views = loadViews(db_, now); std::sort(views.begin(), views.end(), taskLess); - // One row per account. It is Busy while any of its tasks is Testing or - // Wait (those tasks are listed); otherwise it is Free. + // 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. struct AccountView { std::string name; - std::vector busyTasks; + AccountLevel level; + std::vector tasks; }; std::vector accounts; for (const std::string& account : db_.listAccounts()) { + std::vector busy; + std::vector taken; + for (const TaskView& view : views) { + if (view.account != account) { + continue; + } + const AccountLevel level = levelOf(view.status.kind); + if (level == AccountLevel::Busy) { + busy.push_back(view.name); + } else if (level == AccountLevel::Taken) { + taken.push_back(view.name); + } + } + AccountView entry; entry.name = account; - for (const TaskView& view : views) { - if (view.account == account && StatusUtil::isBusy(view.status.kind)) { - entry.busyTasks.push_back(view.name); - } + if (!busy.empty()) { + entry.level = AccountLevel::Busy; + entry.tasks = busy; + } else if (!taken.empty()) { + entry.level = AccountLevel::Taken; + entry.tasks = taken; + } else { + entry.level = AccountLevel::Free; } accounts.push_back(entry); } - // Most free first: fewer busy tasks first, then by name (listAccounts is - // already alphabetical and stable_sort keeps that order for ties). + // Most free first: Free, then Taken, then Busy; within a level fewer tasks + // first. listAccounts is alphabetical and stable_sort keeps that for ties. std::stable_sort(accounts.begin(), accounts.end(), [](const AccountView& a, const AccountView& b) { - return a.busyTasks.size() < b.busyTasks.size(); + if (a.level != b.level) { + return levelRank(a.level) < levelRank(b.level); + } + return a.tasks.size() < b.tasks.size(); }); TextTable table({"ACCOUNT", "STATUS", "TASKS"}); for (const AccountView& entry : accounts) { - const AccountLevel level = entry.busyTasks.empty() ? AccountLevel::Free : AccountLevel::Busy; - if (settings_.hiddenAccountLevels.count(level) > 0) { + if (settings_.hiddenAccountLevels.count(entry.level) > 0) { continue; } - table.addRow({entry.name, - level == AccountLevel::Busy ? "Busy" : "Free", - entry.busyTasks.empty() ? "-" : StringUtil::join(entry.busyTasks, 0, ", ")}); + std::string status = accountLevelName(entry.level); + status[0] = static_cast(std::toupper(static_cast(status[0]))); + table.addRow({entry.name, status, entry.tasks.empty() ? "-" : StringUtil::join(entry.tasks, 0, ", ")}); } return table.render(); } diff --git a/src/Repl.cpp b/src/Repl.cpp index 7f3c365..cf90c58 100644 --- a/src/Repl.cpp +++ b/src/Repl.cpp @@ -85,7 +85,7 @@ void Repl::registerCommands() { {ArgKind::Visibility, ArgKind::TaskLevel}, &Repl::cmdDispTskLevel); add("disp acc", "disp acc (hide|show)", "auto-print the account table after changes", 1, 1, {ArgKind::Visibility}, &Repl::cmdDispAcc); - add("disp acc level", "disp acc level (hide|show) (busy|free|all)", + add("disp acc level", "disp acc level (hide|show) (busy|taken|free|all)", "hide or show account rows by status", 2, 2, {ArgKind::Visibility, ArgKind::AccountLevel}, &Repl::cmdDispAccLevel); @@ -421,7 +421,7 @@ bool Repl::cmdDisp(const Args&) { std::cout << " none"; } for (AccountLevel level : settings.hiddenAccountLevels) { - std::cout << (level == AccountLevel::Busy ? " busy" : " free"); + std::cout << " " << accountLevelName(level); } std::cout << "\n"; return false; @@ -471,13 +471,15 @@ bool Repl::cmdDispAccLevel(const Args& args) { std::set& hidden = display_.settings().hiddenAccountLevels; std::vector targets; if (level == "all") { - targets = {AccountLevel::Busy, AccountLevel::Free}; + targets = {AccountLevel::Busy, AccountLevel::Taken, AccountLevel::Free}; } else if (level == "busy") { targets = {AccountLevel::Busy}; + } else if (level == "taken") { + targets = {AccountLevel::Taken}; } else if (level == "free") { targets = {AccountLevel::Free}; } else { - throw std::runtime_error("Unknown level '" + args[1] + "'. Use: busy, free, all"); + throw std::runtime_error("Unknown level '" + args[1] + "'. Use: busy, taken, free, all"); } for (AccountLevel target : targets) { diff --git a/src/Status.cpp b/src/Status.cpp index ba8fcd0..ff6d12a 100644 --- a/src/Status.cpp +++ b/src/Status.cpp @@ -153,7 +153,11 @@ int rank(StatusKind kind) { } bool isBusy(StatusKind kind) { - return kind == StatusKind::Testing || kind == StatusKind::Wait; + return kind == StatusKind::Testing || kind == StatusKind::LimitReset || kind == StatusKind::Wait; +} + +bool isTaken(StatusKind kind) { + return kind == StatusKind::Prompt; } std::optional parse(const std::string& text, std::time_t now) { @@ -243,7 +247,9 @@ std::optional deserialize(const std::string& text) { Status effective(const Status& status, std::time_t now) { if (status.kind == StatusKind::Wait && now >= status.waitUntil) { - return make(StatusKind::LimitReset); + Status expired = make(StatusKind::LimitReset); + expired.waitUntil = status.waitUntil; // keep it: Limit reset tasks sort by when the wait ended + return expired; } return status; }