Exit prompt
Cause: any answer other than y was treated as no, so your t discarded the changes. Fix: the prompt now only accepts y (save), n (discard) or c (cancel the exit). Anything else asks again, so a typo can't lose data. In my test, t, an empty line, then y saved correctly. Also: I changed the db restore confirmation to y/n only, with no cancel option. Closing stdin still saves automatically. Wait without a time tsk restat <task> wait: works with no time while the task's account is still waiting, and the task just follows that wait. tsk add <task> <acc> wait works the same way. Time required: otherwise it asks for one, with a message saying why. That covers an account that isn't waiting, an account whose wait is already over (Limit reset), and a task with no account. acc wait: it still requires a time. A plain acc wait would have nothing to do, since the account already has a wait. Account panel: there's a new RESET column showing when the account's wait ends. It shows 16:30 for today, tomorrow 06:40 for the next day, or a full date like 2020-01-01 10:00 (which is how an old, already-reset wait appears). Accounts with no wait show -. Since the time is in its own column now, an account with a wait but no task showing it lists just (wait) or (limit reset) in the TASKS column.
This commit is contained in:
1 parent
0af468f47c
commit
35c568333d
6 files changed
+153
-40
No files matched your search
@@ -69,13 +69,15 @@ goes back to `$VISUAL` / `$EDITOR` / nano / vim / vi. The choice is stored in
|
|||||||
|
|
||||||
* Data lives in an in-memory SQLite database. `db save` writes it to the file,
|
* Data lives in an in-memory SQLite database. `db save` writes it to the file,
|
||||||
`db backup` writes a copy, `db restore` loads a file. On `quit` you are asked
|
`db backup` writes a copy, `db restore` loads a file. On `quit` you are asked
|
||||||
to save if there are unsaved changes.
|
to save if there are unsaved changes (`y` save, `n` discard, `c` cancel the
|
||||||
|
exit); any other answer asks again, so a mistyped key never loses data.
|
||||||
* 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); Wait and Limit reset tasks then sort by time (earliest first: when
|
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.
|
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
|
* Account panel: the RESET column is when the account's wait ends (`16:30`,
|
||||||
|
`tomorrow 06:40`, or a full date); `-` if it has none. 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`
|
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
|
(only Done, Frozen or Uploading tasks, or no tasks). The TASKS column lists
|
||||||
the tasks at that level, so a Free account shows its Uploading, Frozen and
|
the tasks at that level, so a Free account shows its Uploading, Frozen and
|
||||||
@@ -90,7 +92,11 @@ goes back to `$VISUAL` / `$EDITOR` / nano / vim / vi. The choice is stored in
|
|||||||
underneath. A wait that no task shows (no tasks, or only Done / Frozen /
|
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
|
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
|
`(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.
|
their own wait.
|
||||||
|
* A time is only optional while the account is already waiting: then
|
||||||
|
`tsk restat <task> wait` (or `tsk add <task> <acc> wait`) just makes the task
|
||||||
|
follow that wait. Otherwise (no account, or its wait is over) a time is
|
||||||
|
required. 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>`.
|
||||||
|
|||||||
+10
-2
@@ -1,6 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <ctime>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -40,12 +42,18 @@ private:
|
|||||||
void setAccountWait(const std::string& account, std::time_t until);
|
void setAccountWait(const std::string& account, std::time_t until);
|
||||||
void noteAccountWait(const std::string& taskName);
|
void noteAccountWait(const std::string& taskName);
|
||||||
void flushMessages();
|
void flushMessages();
|
||||||
void endOfInput();
|
bool endOfInput(); // true if the program should exit
|
||||||
bool confirm(const std::string& question) const;
|
|
||||||
|
enum class Answer { Yes, No, Cancel };
|
||||||
|
// Asks until the reply is valid (y / n, and c when allowed). nullopt if input closed.
|
||||||
|
std::optional<Answer> ask(const std::string& question, bool allowCancel) const;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
static void requireName(const std::string& name);
|
static void requireName(const std::string& name);
|
||||||
static Status parseStatusFrom(const Args& args, std::size_t from);
|
static Status parseStatusFrom(const Args& args, std::size_t from);
|
||||||
|
static bool isBareWait(const Args& args, std::size_t from);
|
||||||
|
std::optional<std::time_t> runningWait(const std::string& account) const;
|
||||||
|
void makeTaskFollowAccount(const TaskRow& task);
|
||||||
|
|
||||||
// Account commands
|
// Account commands
|
||||||
bool cmdAccAdd(const Args& args);
|
bool cmdAccAdd(const Args& args);
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ Status effective(const Status& status, std::time_t now);
|
|||||||
std::string timeToText(std::time_t value);
|
std::string timeToText(std::time_t value);
|
||||||
std::optional<std::time_t> timeFromText(const std::string& text);
|
std::optional<std::time_t> timeFromText(const std::string& text);
|
||||||
std::string clockLabel(std::time_t value); // "16:30"
|
std::string clockLabel(std::time_t value); // "16:30"
|
||||||
|
// "16:30" for today, "tomorrow 06:40" for the next day, otherwise "2026-10-26 16:30".
|
||||||
|
std::string resetLabel(std::time_t value, std::time_t now);
|
||||||
|
|
||||||
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", ...
|
||||||
|
|||||||
+14
-7
@@ -148,12 +148,13 @@ 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 /
|
// The RESET column is when the account's wait ends. A wait that no task is
|
||||||
// Uploading ones) is listed as "(wait HH:MM)" / "(limit reset)" and keeps the
|
// showing (no tasks, or only Done / Frozen / Uploading ones) is listed as
|
||||||
// account Busy.
|
// "(wait)" / "(limit reset)" and keeps the account Busy.
|
||||||
struct AccountView {
|
struct AccountView {
|
||||||
std::string name;
|
std::string name;
|
||||||
AccountLevel level;
|
AccountLevel level;
|
||||||
|
std::string reset; // when its wait ends, "-" if it has none
|
||||||
std::vector<std::string> tasks;
|
std::vector<std::string> tasks;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,14 +181,19 @@ std::string Display::renderAccounts(std::time_t now) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.cooldown && !showsWait) {
|
std::string reset = "-";
|
||||||
|
if (row.cooldown) {
|
||||||
if (std::optional<std::time_t> until = StatusUtil::timeFromText(*row.cooldown)) {
|
if (std::optional<std::time_t> until = StatusUtil::timeFromText(*row.cooldown)) {
|
||||||
busy.push_back(*until > now ? "(wait " + StatusUtil::clockLabel(*until) + ")" : "(limit reset)");
|
reset = StatusUtil::resetLabel(*until, now);
|
||||||
|
if (!showsWait) {
|
||||||
|
busy.push_back(*until > now ? "(wait)" : "(limit reset)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountView entry;
|
AccountView entry;
|
||||||
entry.name = row.id;
|
entry.name = row.id;
|
||||||
|
entry.reset = reset;
|
||||||
if (!busy.empty()) {
|
if (!busy.empty()) {
|
||||||
entry.level = AccountLevel::Busy;
|
entry.level = AccountLevel::Busy;
|
||||||
entry.tasks = busy;
|
entry.tasks = busy;
|
||||||
@@ -211,14 +217,15 @@ std::string Display::renderAccounts(std::time_t now) const {
|
|||||||
return a.level != AccountLevel::Free && a.tasks.size() < b.tasks.size();
|
return a.level != AccountLevel::Free && a.tasks.size() < b.tasks.size();
|
||||||
});
|
});
|
||||||
|
|
||||||
TextTable table({"ACCOUNT", "STATUS", "TASKS"});
|
TextTable table({"ACCOUNT", "STATUS", "RESET", "TASKS"});
|
||||||
for (const AccountView& entry : accounts) {
|
for (const AccountView& entry : accounts) {
|
||||||
if (settings_.hiddenAccountLevels.count(entry.level) > 0) {
|
if (settings_.hiddenAccountLevels.count(entry.level) > 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
std::string status = accountLevelName(entry.level);
|
std::string status = accountLevelName(entry.level);
|
||||||
status[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(status[0])));
|
status[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(status[0])));
|
||||||
table.addRow({entry.name, status, entry.tasks.empty() ? "-" : StringUtil::join(entry.tasks, 0, ", ")});
|
table.addRow({entry.name, status, entry.reset,
|
||||||
|
entry.tasks.empty() ? "-" : StringUtil::join(entry.tasks, 0, ", ")});
|
||||||
}
|
}
|
||||||
return table.render();
|
return table.render();
|
||||||
}
|
}
|
||||||
+98
-28
@@ -77,7 +77,7 @@ void Repl::registerCommands() {
|
|||||||
{ArgKind::Task, ArgKind::None}, &Repl::cmdTskRename);
|
{ArgKind::Task, ArgKind::None}, &Repl::cmdTskRename);
|
||||||
add("tsk reacc", "tsk reacc <name> <new>", "move a task to another account ('-' = none)", 2, 2,
|
add("tsk reacc", "tsk reacc <name> <new>", "move a task to another account ('-' = none)", 2, 2,
|
||||||
{ArgKind::Task, ArgKind::AccountOrDash}, &Repl::cmdTskReacc);
|
{ArgKind::Task, ArgKind::AccountOrDash}, &Repl::cmdTskReacc);
|
||||||
add("tsk restat", "tsk restat <name> <new>", "change a task's status", 2, kUnlimited,
|
add("tsk restat", "tsk restat <name> <new>", "change a task's status (plain 'wait' works while its account is waiting)", 2, kUnlimited,
|
||||||
{ArgKind::Task, ArgKind::Status}, &Repl::cmdTskRestat);
|
{ArgKind::Task, ArgKind::Status}, &Repl::cmdTskRestat);
|
||||||
add("tsk notes", "tsk notes <name>", "edit a task's notes in your editor (see 'editor')", 1, 1,
|
add("tsk notes", "tsk notes <name>", "edit a task's notes in your editor (see 'editor')", 1, 1,
|
||||||
{ArgKind::Task}, &Repl::cmdTskNotes);
|
{ArgKind::Task}, &Repl::cmdTskNotes);
|
||||||
@@ -138,27 +138,35 @@ int Repl::run() {
|
|||||||
std::string line;
|
std::string line;
|
||||||
while (!quit_) {
|
while (!quit_) {
|
||||||
if (!lineEditor_.readLine("> ", line)) {
|
if (!lineEditor_.readLine("> ", line)) {
|
||||||
endOfInput();
|
if (endOfInput()) {
|
||||||
return 0;
|
return 0;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
execute(line);
|
execute(line);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Repl::endOfInput() {
|
bool Repl::endOfInput() {
|
||||||
if (!db_.dirty()) {
|
if (!db_.dirty()) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
if (Terminal::stdinIsInteractive()) {
|
if (!Terminal::stdinIsInteractive()) {
|
||||||
if (confirm("Save changes before exiting?")) {
|
|
||||||
db_.save();
|
|
||||||
std::cout << "Saved to " << db_.path() << ".\n";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
db_.save();
|
db_.save();
|
||||||
std::cout << "Input closed - changes saved to " << db_.path() << ".\n";
|
std::cout << "Input closed - changes saved to " << db_.path() << ".\n";
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::optional<Answer> answer = ask("Save changes before exiting?", true);
|
||||||
|
if (answer == Answer::Cancel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!answer || *answer == Answer::Yes) { // input closed: keep the changes
|
||||||
|
db_.save();
|
||||||
|
std::cout << "Saved to " << db_.path() << ".\n";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Repl::say(const std::string& message) {
|
void Repl::say(const std::string& message) {
|
||||||
@@ -221,14 +229,51 @@ void Repl::refresh() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Repl::confirm(const std::string& question) const {
|
std::optional<Repl::Answer> Repl::ask(const std::string& question, bool allowCancel) const {
|
||||||
std::cout << question << " (y/n): " << std::flush;
|
for (;;) {
|
||||||
std::string answer;
|
std::cout << question << (allowCancel ? " (y/n/c): " : " (y/n): ") << std::flush;
|
||||||
if (!std::getline(std::cin, answer)) {
|
std::string reply;
|
||||||
return false;
|
if (!std::getline(std::cin, reply)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
reply = StringUtil::lower(StringUtil::trim(reply));
|
||||||
|
if (reply == "y" || reply == "yes") {
|
||||||
|
return Answer::Yes;
|
||||||
|
}
|
||||||
|
if (reply == "n" || reply == "no") {
|
||||||
|
return Answer::No;
|
||||||
|
}
|
||||||
|
if (allowCancel && (reply == "c" || reply == "cancel")) {
|
||||||
|
return Answer::Cancel;
|
||||||
|
}
|
||||||
|
std::cout << (allowCancel ? "Please answer y (save), n (discard) or c (cancel).\n"
|
||||||
|
: "Please answer y or n.\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Repl::isBareWait(const Args& args, std::size_t from) {
|
||||||
|
return StringUtil::lower(StringUtil::join(args, from, " ")) == "wait";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The account's wait if it is still running (not yet a Limit reset).
|
||||||
|
std::optional<std::time_t> Repl::runningWait(const std::string& account) const {
|
||||||
|
std::optional<AccountRow> row = db_.getAccount(account);
|
||||||
|
if (!row || !row->cooldown) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
std::optional<std::time_t> until = StatusUtil::timeFromText(*row->cooldown);
|
||||||
|
if (until && *until > std::time(nullptr)) {
|
||||||
|
return until;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt and Testing tasks follow their account's wait; anything else becomes Prompt.
|
||||||
|
void Repl::makeTaskFollowAccount(const TaskRow& task) {
|
||||||
|
std::optional<Status> current = StatusUtil::deserialize(task.status);
|
||||||
|
if (!current || !StatusUtil::followsAccountWait(current->kind)) {
|
||||||
|
db_.setTaskStatus(task.id, StatusUtil::serialize(StatusUtil::make(StatusKind::Prompt)));
|
||||||
}
|
}
|
||||||
answer = StringUtil::lower(StringUtil::trim(answer));
|
|
||||||
return answer == "y" || answer == "yes";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Repl::requireName(const std::string& name) {
|
void Repl::requireName(const std::string& name) {
|
||||||
@@ -354,7 +399,7 @@ bool Repl::cmdTskAdd(const Args& args) {
|
|||||||
++next;
|
++next;
|
||||||
} else if (args[next] == "-") {
|
} else if (args[next] == "-") {
|
||||||
++next;
|
++next;
|
||||||
} else if (!StatusUtil::parse(StringUtil::join(args, next, " "), std::time(nullptr))) {
|
} else if (!isBareWait(args, next) && !StatusUtil::parse(StringUtil::join(args, next, " "), std::time(nullptr))) {
|
||||||
throw std::runtime_error("'" + args[next] + "' is neither an existing account nor a valid status (" +
|
throw std::runtime_error("'" + args[next] + "' is neither an existing account nor a valid status (" +
|
||||||
kStatusHelp + ").");
|
kStatusHelp + ").");
|
||||||
}
|
}
|
||||||
@@ -362,7 +407,14 @@ bool Repl::cmdTskAdd(const Args& args) {
|
|||||||
|
|
||||||
Status status = StatusUtil::make(StatusKind::Prompt);
|
Status status = StatusUtil::make(StatusKind::Prompt);
|
||||||
if (next < args.size()) {
|
if (next < args.size()) {
|
||||||
status = parseStatusFrom(args, next);
|
if (isBareWait(args, next)) {
|
||||||
|
// "wait" without a time: only while the account is already waiting.
|
||||||
|
if (!account || !runningWait(*account)) {
|
||||||
|
throw std::runtime_error("A time is required unless the task's account is already waiting: wait <time>");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
status = parseStatusFrom(args, next);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A wait on a task that has an account is the account's wait; the task itself
|
// A wait on a task that has an account is the account's wait; the task itself
|
||||||
@@ -433,15 +485,26 @@ bool Repl::cmdTskRestat(const Args& args) {
|
|||||||
if (!task) {
|
if (!task) {
|
||||||
throw std::runtime_error("No task named '" + args[0] + "'.");
|
throw std::runtime_error("No task named '" + args[0] + "'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isBareWait(args, 1)) {
|
||||||
|
// "wait" without a time: the task follows its account's running wait.
|
||||||
|
if (!task->account || !runningWait(*task->account)) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
task->account ? "Account '" + *task->account + "' is not waiting, so a time is required: wait <time>"
|
||||||
|
: "Task '" + task->id + "' has no account, so a time is required: wait <time>");
|
||||||
|
}
|
||||||
|
makeTaskFollowAccount(*task);
|
||||||
|
say("Task '" + task->id + "' now follows account '" + *task->account + "'.");
|
||||||
|
noteAccountWait(task->id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Status status = parseStatusFrom(args, 1);
|
Status status = parseStatusFrom(args, 1);
|
||||||
|
|
||||||
if (status.kind == StatusKind::Wait && task->account) {
|
if (status.kind == StatusKind::Wait && task->account) {
|
||||||
// The wait belongs to the account. Keep an active task as it is so it
|
// The wait belongs to the account. Keep an active task as it is so it
|
||||||
// follows the wait; any other status becomes Prompt.
|
// follows the wait; any other status becomes Prompt.
|
||||||
std::optional<Status> current = StatusUtil::deserialize(task->status);
|
makeTaskFollowAccount(*task);
|
||||||
if (!current || !StatusUtil::followsAccountWait(current->kind)) {
|
|
||||||
db_.setTaskStatus(task->id, StatusUtil::serialize(StatusUtil::make(StatusKind::Prompt)));
|
|
||||||
}
|
|
||||||
setAccountWait(*task->account, status.waitUntil);
|
setAccountWait(*task->account, status.waitUntil);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -584,7 +647,7 @@ bool Repl::cmdDbBackup(const Args& args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Repl::cmdDbRestore(const Args& args) {
|
bool Repl::cmdDbRestore(const Args& args) {
|
||||||
if (db_.dirty() && !confirm("Unsaved changes will be replaced. Continue?")) {
|
if (db_.dirty() && ask("Unsaved changes will be replaced. Continue?", false) != Answer::Yes) {
|
||||||
say("Restore cancelled.");
|
say("Restore cancelled.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -640,9 +703,16 @@ bool Repl::cmdHelp(const Args&) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Repl::cmdQuit(const Args&) {
|
bool Repl::cmdQuit(const Args&) {
|
||||||
if (db_.dirty() && confirm("Save changes before exiting?")) {
|
if (db_.dirty()) {
|
||||||
db_.save();
|
const std::optional<Answer> answer = ask("Save changes before exiting?", true);
|
||||||
say("Saved to " + db_.path() + ".");
|
if (answer == Answer::Cancel) {
|
||||||
|
say("Exit cancelled.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!answer || *answer == Answer::Yes) { // input closed: keep the changes
|
||||||
|
db_.save();
|
||||||
|
say("Saved to " + db_.path() + ".");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
quit_ = true;
|
quit_ = true;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -286,6 +286,26 @@ std::string clockLabel(std::time_t value) {
|
|||||||
return formatTime(value, "%H:%M");
|
return formatTime(value, "%H:%M");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string resetLabel(std::time_t value, std::time_t now) {
|
||||||
|
const std::tm day = toLocal(value);
|
||||||
|
const std::tm today = toLocal(now);
|
||||||
|
std::tm next = today;
|
||||||
|
next.tm_mday += 1;
|
||||||
|
next.tm_isdst = -1;
|
||||||
|
std::mktime(&next); // normalizes the date
|
||||||
|
|
||||||
|
auto sameDay = [](const std::tm& a, const std::tm& b) {
|
||||||
|
return a.tm_year == b.tm_year && a.tm_yday == b.tm_yday;
|
||||||
|
};
|
||||||
|
if (sameDay(day, today)) {
|
||||||
|
return clockLabel(value);
|
||||||
|
}
|
||||||
|
if (sameDay(day, next)) {
|
||||||
|
return "tomorrow " + clockLabel(value);
|
||||||
|
}
|
||||||
|
return formatTime(value, "%Y-%m-%d %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