On branch main

Your branch is up to date with 'origin/main'.

Accounts:
the account panel now sorts from most free to least free. Fewer Busy tasks come first, and ties go alphabetically.

Tasks:
they now sort by status, then account A–Z, then wait time, then task name.

Status order:
Limit reset, Testing, Prompt, Wait, Uploading, Frozen, Done.

Account order: it ignores case. Tasks with no account come last in their status group.
Wait time: it only matters among tasks that have the same status and the same account.

Changes to be committed:
	modified:   README.md
	modified:   include/Status.h
	modified:   include/StringUtil.h
	modified:   src/Display.cpp
	modified:   src/Status.cpp
	modified:   src/StringUtil.cpp
This commit is contained in:
alejo committed 2026-10-17 11:59:35 -03:00
1 parent 4c69c164e4
commit 04fa2b1b20
6 files changed
+69 -53

No files matched your search

+6 -4
View File
@@ -72,13 +72,15 @@ goes back to `$VISUAL` / `$EDITOR` / nano / vim / vi. The choice is stored in
to save if there are unsaved changes.
* 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 the computed status. The panels are cleared and redrawn after changes.
* Account panel: one row per account. `Busy` while any of its tasks is
Testing or Wait (those tasks are listed), otherwise `Free`
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).
* 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). Multi-line notes show on one line in the table, in full.
* 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>`.
## Layout
+1 -2
View File
@@ -7,7 +7,6 @@
// Prompt: free to accept prompts.
// LimitReset: computed - a Wait whose time has passed (can also be set by hand).
// The enum order is the sort order of the panels.
enum class StatusKind { Prompt, LimitReset, Testing, Wait, Uploading, Frozen, Done };
struct Status {
@@ -36,7 +35,7 @@ std::vector<StatusKind> allKinds();
// Accepts "limit-reset", "limit_reset", "limitreset", "reset"; "available" is an
// old name for "prompt".
std::optional<StatusKind> kindFromName(const std::string& name);
int rank(StatusKind kind); // sort order
int rank(StatusKind kind); // task sort priority, lowest first
bool isBusy(StatusKind kind); // does this status hold the account?
} // namespace StatusUtil
+2 -3
View File
@@ -15,8 +15,7 @@ std::string join(const std::vector<std::string>& parts, std::size_t from, const
// Number of characters (UTF-8 code points) in the text.
std::size_t displayWidth(const std::string& text);
// Collapses line breaks into " / ". If `maxChars` > 0 the text is cut to that
// many characters; 0 means no limit.
std::string singleLine(const std::string& text, std::size_t maxChars);
// Number of lines in the text, ignoring trailing blank lines (0 for empty text).
std::size_t lineCount(const std::string& text);
} // namespace StringUtil
+44 -9
View File
@@ -30,18 +30,37 @@ std::vector<TaskView> 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.
bool taskLess(const TaskView& a, const TaskView& b) {
int rankA = StatusUtil::rank(a.status.kind);
int rankB = StatusUtil::rank(b.status.kind);
const int rankA = StatusUtil::rank(a.status.kind);
const int rankB = StatusUtil::rank(b.status.kind);
if (rankA != rankB) {
return rankA < rankB;
}
if (a.account.empty() != b.account.empty()) {
return !a.account.empty();
}
const std::string accountA = StringUtil::lower(a.account);
const std::string accountB = StringUtil::lower(b.account);
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);
}
// "-" 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);
if (lines == 0) {
return "-";
}
return std::to_string(lines) + (lines == 1 ? " line" : " lines");
}
} // namespace
Display::Display(const Database& db) : db_(db) {}
@@ -66,7 +85,7 @@ std::string Display::renderTasks(std::time_t now) const {
table.addRow({view.name,
view.account.empty() ? "-" : view.account,
StatusUtil::label(view.status),
view.notes.empty() ? "-" : StringUtil::singleLine(view.notes, 0)});
notesSummary(view.notes)});
}
return table.render();
}
@@ -77,22 +96,38 @@ std::string Display::renderAccounts(std::time_t now) const {
// One row per account. It is Busy while any of its tasks is Testing or
// Wait (those tasks are listed); otherwise it is Free.
TextTable table({"ACCOUNT", "STATUS", "TASKS"});
for (const std::string& account : db_.listAccounts()) {
struct AccountView {
std::string name;
std::vector<std::string> busyTasks;
};
std::vector<AccountView> accounts;
for (const std::string& account : db_.listAccounts()) {
AccountView entry;
entry.name = account;
for (const TaskView& view : views) {
if (view.account == account && StatusUtil::isBusy(view.status.kind)) {
busyTasks.push_back(view.name);
entry.busyTasks.push_back(view.name);
}
}
accounts.push_back(entry);
}
const AccountLevel level = busyTasks.empty() ? AccountLevel::Free : AccountLevel::Busy;
// Most free first: fewer busy tasks first, then by name (listAccounts is
// already alphabetical and stable_sort keeps that order for ties).
std::stable_sort(accounts.begin(), accounts.end(), [](const AccountView& a, const AccountView& b) {
return a.busyTasks.size() < b.busyTasks.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) {
continue;
}
table.addRow({account,
table.addRow({entry.name,
level == AccountLevel::Busy ? "Busy" : "Free",
busyTasks.empty() ? "-" : StringUtil::join(busyTasks, 0, ", ")});
entry.busyTasks.empty() ? "-" : StringUtil::join(entry.busyTasks, 0, ", ")});
}
return table.render();
}
+11 -1
View File
@@ -138,8 +138,18 @@ std::optional<StatusKind> kindFromName(const std::string& name) {
return std::nullopt;
}
// Priority in the task table: Limit reset, Testing, Prompt, Wait, Uploading, Frozen, Done.
int rank(StatusKind kind) {
return static_cast<int>(kind);
switch (kind) {
case StatusKind::LimitReset: return 0;
case StatusKind::Testing: return 1;
case StatusKind::Prompt: return 2;
case StatusKind::Wait: return 3;
case StatusKind::Uploading: return 4;
case StatusKind::Frozen: return 5;
case StatusKind::Done: return 6;
}
return 6;
}
bool isBusy(StatusKind kind) {
+5 -34
View File
@@ -53,41 +53,12 @@ std::size_t displayWidth(const std::string& text) {
return width;
}
std::string singleLine(const std::string& text, std::size_t maxChars) {
std::string out;
bool pendingBreak = false;
for (char c : text) {
if (c == '\n' || c == '\r') {
pendingBreak = true;
continue;
std::size_t lineCount(const std::string& text) {
const std::string body = trim(text);
if (body.empty()) {
return 0;
}
if (pendingBreak) {
if (!out.empty()) {
out += " / ";
}
pendingBreak = false;
}
out.push_back(c == '\t' ? ' ' : c);
}
if (maxChars == 0) {
return out;
}
std::size_t chars = 0;
std::size_t cut = 0;
for (; cut < out.size(); ++cut) {
if ((static_cast<unsigned char>(out[cut]) & 0xC0) != 0x80) {
if (chars == maxChars) {
break;
}
++chars;
}
}
if (cut < out.size()) {
out = out.substr(0, cut) + "...";
}
return out;
return static_cast<std::size_t>(std::count(body.begin(), body.end(), '\n')) + 1;
}
} // namespace StringUtil