Task sorting
- Time: Wait tasks sort by when the wait ends, earliest first. Limit reset tasks sort by when their wait ended, so the one that has been ready longest comes first. The time is kept when a wait expires, which is what makes the second sort possible. - Order: I put time before account for these two statuses, so the full order is status, then time, then account, then name. Other statuses still go status, account, name. If you'd rather have account before time, swap the two checks in taskLess in Display.cpp. - Manual limit reset: a task you set to limit-reset by hand has no wait time, so it sorts first among limit resets. Account status: an account takes the most taken level among its tasks. - Busy: it has a Testing, Limit reset or Wait task. The TASKS column lists those tasks. - Taken: it has a Prompt task and nothing Busy. The TASKS column lists the Prompt tasks. - Free: it only has Done, Frozen or Uploading tasks, or no tasks. The TASKS column shows Changes to be committed: modified: README.md modified: include/Display.h modified: include/Status.h modified: src/Completer.cpp modified: src/Display.cpp modified: src/Repl.cpp modified: src/Status.cpp
This commit is contained in:
1 parent
04fa2b1b20
commit
6cb5a8611f
7 files changed
+107
-36
No files matched your search
@@ -29,7 +29,7 @@ installed; otherwise CMake downloads the amalgamation (needs network once).
|
|||||||
disp tsk (hide|show)
|
disp tsk (hide|show)
|
||||||
disp tsk level (hide|show) (prompt|limit-reset|testing|wait|uploading|frozen|done|all)
|
disp tsk level (hide|show) (prompt|limit-reset|testing|wait|uploading|frozen|done|all)
|
||||||
disp acc (hide|show)
|
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)
|
disp (show current settings)
|
||||||
|
|
||||||
db save | db backup [file] | db restore <file>
|
db save | db backup [file] | db restore <file>
|
||||||
@@ -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
|
* 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
|
`Wait HH:MM` until that time passes, 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), then account A-Z (unassigned last), then wait time, then name. The panels are cleared and redrawn after changes.
|
Done); Wait and Limit reset tasks then sort by time (earliest first: when
|
||||||
* Account panel: one row per account, sorted from most free to least free.
|
the wait ends / ended), then account A-Z (unassigned last), then name. The panels are cleared and redrawn after changes.
|
||||||
`Busy` while any of its tasks is Testing or Wait (those tasks are listed),
|
* Account panel: one row per account, judged by its tasks: `Busy` if any is
|
||||||
otherwise `Free`; fewer busy tasks sorts first, ties by name
|
Testing, Limit reset or Wait; else `Taken` if any is Prompt; else `Free`
|
||||||
(see `Display::renderAccounts` / `StatusUtil::isBusy` to change the rules).
|
(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 (`-`).
|
* 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>`.
|
||||||
|
|||||||
+7
-1
@@ -7,7 +7,13 @@
|
|||||||
#include "Database.h"
|
#include "Database.h"
|
||||||
#include "Status.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 {
|
struct DisplaySettings {
|
||||||
bool showTasks = true; // auto-print the task table after each change
|
bool showTasks = true; // auto-print the task table after each change
|
||||||
|
|||||||
+3
-2
@@ -11,7 +11,7 @@ enum class StatusKind { Prompt, LimitReset, Testing, Wait, Uploading, Frozen, Do
|
|||||||
|
|
||||||
struct Status {
|
struct Status {
|
||||||
StatusKind kind = StatusKind::Prompt;
|
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 {
|
namespace StatusUtil {
|
||||||
@@ -36,6 +36,7 @@ std::vector<StatusKind> allKinds();
|
|||||||
// old name for "prompt".
|
// old name for "prompt".
|
||||||
std::optional<StatusKind> kindFromName(const std::string& name);
|
std::optional<StatusKind> kindFromName(const std::string& name);
|
||||||
int rank(StatusKind kind); // task sort priority, lowest first
|
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
|
} // namespace StatusUtil
|
||||||
+1
-1
@@ -101,7 +101,7 @@ std::vector<std::string> Completer::valuesFor(ArgKind kind, const std::string& c
|
|||||||
values.push_back("all");
|
values.push_back("all");
|
||||||
break;
|
break;
|
||||||
case ArgKind::AccountLevel:
|
case ArgKind::AccountLevel:
|
||||||
values = {"busy", "free", "all"};
|
values = {"busy", "taken", "free", "all"};
|
||||||
break;
|
break;
|
||||||
case ArgKind::File:
|
case ArgKind::File:
|
||||||
values = fileNames(current);
|
values = fileNames(current);
|
||||||
|
|||||||
+73
-20
@@ -1,6 +1,7 @@
|
|||||||
#include "Display.h"
|
#include "Display.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -30,14 +31,19 @@ std::vector<TaskView> loadViews(const Database& db, std::time_t now) {
|
|||||||
return views;
|
return views;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task order: status priority (see StatusUtil::rank), then account A-Z
|
// Task order: status priority (see StatusUtil::rank); then, for Wait and Limit
|
||||||
// (tasks without an account last), then earliest wait time, then task name.
|
// 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) {
|
bool taskLess(const TaskView& a, const TaskView& b) {
|
||||||
const int rankA = StatusUtil::rank(a.status.kind);
|
const int rankA = StatusUtil::rank(a.status.kind);
|
||||||
const int rankB = StatusUtil::rank(b.status.kind);
|
const int rankB = StatusUtil::rank(b.status.kind);
|
||||||
if (rankA != rankB) {
|
if (rankA != rankB) {
|
||||||
return 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()) {
|
if (a.account.empty() != b.account.empty()) {
|
||||||
return !a.account.empty();
|
return !a.account.empty();
|
||||||
}
|
}
|
||||||
@@ -46,12 +52,29 @@ bool taskLess(const TaskView& a, const TaskView& b) {
|
|||||||
if (accountA != accountB) {
|
if (accountA != accountB) {
|
||||||
return 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);
|
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.
|
// "-" when there are no notes, otherwise how many lines they have.
|
||||||
std::string notesSummary(const std::string& notes) {
|
std::string notesSummary(const std::string& notes) {
|
||||||
const std::size_t lines = StringUtil::lineCount(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();
|
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::string Display::renderAccounts(std::time_t now) const {
|
||||||
std::vector<TaskView> views = loadViews(db_, now);
|
std::vector<TaskView> views = loadViews(db_, now);
|
||||||
std::sort(views.begin(), views.end(), taskLess);
|
std::sort(views.begin(), views.end(), taskLess);
|
||||||
|
|
||||||
// One row per account. It is Busy while any of its tasks is Testing or
|
// One row per account. Its level is the most taken of its tasks
|
||||||
// Wait (those tasks are listed); otherwise it is Free.
|
// (Busy > Taken > Free); the TASKS column lists the tasks at that level.
|
||||||
struct AccountView {
|
struct AccountView {
|
||||||
std::string name;
|
std::string name;
|
||||||
std::vector<std::string> busyTasks;
|
AccountLevel level;
|
||||||
|
std::vector<std::string> tasks;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<AccountView> accounts;
|
std::vector<AccountView> accounts;
|
||||||
for (const std::string& account : db_.listAccounts()) {
|
for (const std::string& account : db_.listAccounts()) {
|
||||||
|
std::vector<std::string> busy;
|
||||||
|
std::vector<std::string> 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;
|
AccountView entry;
|
||||||
entry.name = account;
|
entry.name = account;
|
||||||
for (const TaskView& view : views) {
|
if (!busy.empty()) {
|
||||||
if (view.account == account && StatusUtil::isBusy(view.status.kind)) {
|
entry.level = AccountLevel::Busy;
|
||||||
entry.busyTasks.push_back(view.name);
|
entry.tasks = busy;
|
||||||
}
|
} else if (!taken.empty()) {
|
||||||
|
entry.level = AccountLevel::Taken;
|
||||||
|
entry.tasks = taken;
|
||||||
|
} else {
|
||||||
|
entry.level = AccountLevel::Free;
|
||||||
}
|
}
|
||||||
accounts.push_back(entry);
|
accounts.push_back(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Most free first: fewer busy tasks first, then by name (listAccounts is
|
// Most free first: Free, then Taken, then Busy; within a level fewer tasks
|
||||||
// already alphabetical and stable_sort keeps that order for ties).
|
// first. listAccounts is alphabetical and stable_sort keeps that 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) {
|
||||||
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"});
|
TextTable table({"ACCOUNT", "STATUS", "TASKS"});
|
||||||
for (const AccountView& entry : accounts) {
|
for (const AccountView& entry : accounts) {
|
||||||
const AccountLevel level = entry.busyTasks.empty() ? AccountLevel::Free : AccountLevel::Busy;
|
if (settings_.hiddenAccountLevels.count(entry.level) > 0) {
|
||||||
if (settings_.hiddenAccountLevels.count(level) > 0) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
table.addRow({entry.name,
|
std::string status = accountLevelName(entry.level);
|
||||||
level == AccountLevel::Busy ? "Busy" : "Free",
|
status[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(status[0])));
|
||||||
entry.busyTasks.empty() ? "-" : StringUtil::join(entry.busyTasks, 0, ", ")});
|
table.addRow({entry.name, status, entry.tasks.empty() ? "-" : StringUtil::join(entry.tasks, 0, ", ")});
|
||||||
}
|
}
|
||||||
return table.render();
|
return table.render();
|
||||||
}
|
}
|
||||||
+6
-4
@@ -85,7 +85,7 @@ void Repl::registerCommands() {
|
|||||||
{ArgKind::Visibility, ArgKind::TaskLevel}, &Repl::cmdDispTskLevel);
|
{ArgKind::Visibility, ArgKind::TaskLevel}, &Repl::cmdDispTskLevel);
|
||||||
add("disp acc", "disp acc (hide|show)", "auto-print the account table after changes", 1, 1,
|
add("disp acc", "disp acc (hide|show)", "auto-print the account table after changes", 1, 1,
|
||||||
{ArgKind::Visibility}, &Repl::cmdDispAcc);
|
{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,
|
"hide or show account rows by status", 2, 2,
|
||||||
{ArgKind::Visibility, ArgKind::AccountLevel}, &Repl::cmdDispAccLevel);
|
{ArgKind::Visibility, ArgKind::AccountLevel}, &Repl::cmdDispAccLevel);
|
||||||
|
|
||||||
@@ -421,7 +421,7 @@ bool Repl::cmdDisp(const Args&) {
|
|||||||
std::cout << " none";
|
std::cout << " none";
|
||||||
}
|
}
|
||||||
for (AccountLevel level : settings.hiddenAccountLevels) {
|
for (AccountLevel level : settings.hiddenAccountLevels) {
|
||||||
std::cout << (level == AccountLevel::Busy ? " busy" : " free");
|
std::cout << " " << accountLevelName(level);
|
||||||
}
|
}
|
||||||
std::cout << "\n";
|
std::cout << "\n";
|
||||||
return false;
|
return false;
|
||||||
@@ -471,13 +471,15 @@ bool Repl::cmdDispAccLevel(const Args& args) {
|
|||||||
std::set<AccountLevel>& hidden = display_.settings().hiddenAccountLevels;
|
std::set<AccountLevel>& hidden = display_.settings().hiddenAccountLevels;
|
||||||
std::vector<AccountLevel> targets;
|
std::vector<AccountLevel> targets;
|
||||||
if (level == "all") {
|
if (level == "all") {
|
||||||
targets = {AccountLevel::Busy, AccountLevel::Free};
|
targets = {AccountLevel::Busy, AccountLevel::Taken, AccountLevel::Free};
|
||||||
} else if (level == "busy") {
|
} else if (level == "busy") {
|
||||||
targets = {AccountLevel::Busy};
|
targets = {AccountLevel::Busy};
|
||||||
|
} else if (level == "taken") {
|
||||||
|
targets = {AccountLevel::Taken};
|
||||||
} else if (level == "free") {
|
} else if (level == "free") {
|
||||||
targets = {AccountLevel::Free};
|
targets = {AccountLevel::Free};
|
||||||
} else {
|
} 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) {
|
for (AccountLevel target : targets) {
|
||||||
|
|||||||
+8
-2
@@ -153,7 +153,11 @@ int rank(StatusKind kind) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isBusy(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<Status> parse(const std::string& text, std::time_t now) {
|
std::optional<Status> parse(const std::string& text, std::time_t now) {
|
||||||
@@ -243,7 +247,9 @@ std::optional<Status> deserialize(const std::string& text) {
|
|||||||
|
|
||||||
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) {
|
||||||
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;
|
return status;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user