diff --git a/README.md b/README.md index 8844d50..ad78a6f 100644 --- a/README.md +++ b/README.md @@ -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 `. ## Layout diff --git a/include/Status.h b/include/Status.h index f2fc041..e7ab8b2 100644 --- a/include/Status.h +++ b/include/Status.h @@ -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 allKinds(); // Accepts "limit-reset", "limit_reset", "limitreset", "reset"; "available" is an // old name for "prompt". std::optional 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 diff --git a/include/StringUtil.h b/include/StringUtil.h index 46ce758..5c23d41 100644 --- a/include/StringUtil.h +++ b/include/StringUtil.h @@ -15,8 +15,7 @@ std::string join(const std::vector& 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 diff --git a/src/Display.cpp b/src/Display.cpp index 993d97c..3f9f6b9 100644 --- a/src/Display.cpp +++ b/src/Display.cpp @@ -30,18 +30,37 @@ 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. 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 busyTasks; + }; + + std::vector 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(); } diff --git a/src/Status.cpp b/src/Status.cpp index 5ba7fb8..ba8fcd0 100644 --- a/src/Status.cpp +++ b/src/Status.cpp @@ -138,8 +138,18 @@ std::optional 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(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) { diff --git a/src/StringUtil.cpp b/src/StringUtil.cpp index 596e326..6013ce3 100644 --- a/src/StringUtil.cpp +++ b/src/StringUtil.cpp @@ -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; - } - if (pendingBreak) { - if (!out.empty()) { - out += " / "; - } - pendingBreak = false; - } - out.push_back(c == '\t' ? ' ' : c); +std::size_t lineCount(const std::string& text) { + const std::string body = trim(text); + if (body.empty()) { + return 0; } - - if (maxChars == 0) { - return out; - } - - std::size_t chars = 0; - std::size_t cut = 0; - for (; cut < out.size(); ++cut) { - if ((static_cast(out[cut]) & 0xC0) != 0x80) { - if (chars == maxChars) { - break; - } - ++chars; - } - } - if (cut < out.size()) { - out = out.substr(0, cut) + "..."; - } - return out; + return static_cast(std::count(body.begin(), body.end(), '\n')) + 1; } } // namespace StringUtil