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
+98
-28
@@ -77,7 +77,7 @@ void Repl::registerCommands() {
|
||||
{ArgKind::Task, ArgKind::None}, &Repl::cmdTskRename);
|
||||
add("tsk reacc", "tsk reacc <name> <new>", "move a task to another account ('-' = none)", 2, 2,
|
||||
{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);
|
||||
add("tsk notes", "tsk notes <name>", "edit a task's notes in your editor (see 'editor')", 1, 1,
|
||||
{ArgKind::Task}, &Repl::cmdTskNotes);
|
||||
@@ -138,27 +138,35 @@ int Repl::run() {
|
||||
std::string line;
|
||||
while (!quit_) {
|
||||
if (!lineEditor_.readLine("> ", line)) {
|
||||
endOfInput();
|
||||
return 0;
|
||||
if (endOfInput()) {
|
||||
return 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
execute(line);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Repl::endOfInput() {
|
||||
bool Repl::endOfInput() {
|
||||
if (!db_.dirty()) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if (Terminal::stdinIsInteractive()) {
|
||||
if (confirm("Save changes before exiting?")) {
|
||||
db_.save();
|
||||
std::cout << "Saved to " << db_.path() << ".\n";
|
||||
}
|
||||
} else {
|
||||
if (!Terminal::stdinIsInteractive()) {
|
||||
db_.save();
|
||||
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) {
|
||||
@@ -221,14 +229,51 @@ void Repl::refresh() const {
|
||||
}
|
||||
}
|
||||
|
||||
bool Repl::confirm(const std::string& question) const {
|
||||
std::cout << question << " (y/n): " << std::flush;
|
||||
std::string answer;
|
||||
if (!std::getline(std::cin, answer)) {
|
||||
return false;
|
||||
std::optional<Repl::Answer> Repl::ask(const std::string& question, bool allowCancel) const {
|
||||
for (;;) {
|
||||
std::cout << question << (allowCancel ? " (y/n/c): " : " (y/n): ") << std::flush;
|
||||
std::string reply;
|
||||
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) {
|
||||
@@ -354,7 +399,7 @@ bool Repl::cmdTskAdd(const Args& args) {
|
||||
++next;
|
||||
} else if (args[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 (" +
|
||||
kStatusHelp + ").");
|
||||
}
|
||||
@@ -362,7 +407,14 @@ bool Repl::cmdTskAdd(const Args& args) {
|
||||
|
||||
Status status = StatusUtil::make(StatusKind::Prompt);
|
||||
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
|
||||
@@ -433,15 +485,26 @@ bool Repl::cmdTskRestat(const Args& args) {
|
||||
if (!task) {
|
||||
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);
|
||||
|
||||
if (status.kind == StatusKind::Wait && task->account) {
|
||||
// The wait belongs to the account. Keep an active task as it is so it
|
||||
// follows the wait; any other status becomes Prompt.
|
||||
std::optional<Status> current = StatusUtil::deserialize(task->status);
|
||||
if (!current || !StatusUtil::followsAccountWait(current->kind)) {
|
||||
db_.setTaskStatus(task->id, StatusUtil::serialize(StatusUtil::make(StatusKind::Prompt)));
|
||||
}
|
||||
makeTaskFollowAccount(*task);
|
||||
setAccountWait(*task->account, status.waitUntil);
|
||||
return true;
|
||||
}
|
||||
@@ -584,7 +647,7 @@ bool Repl::cmdDbBackup(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.");
|
||||
return false;
|
||||
}
|
||||
@@ -640,9 +703,16 @@ bool Repl::cmdHelp(const Args&) {
|
||||
}
|
||||
|
||||
bool Repl::cmdQuit(const Args&) {
|
||||
if (db_.dirty() && confirm("Save changes before exiting?")) {
|
||||
db_.save();
|
||||
say("Saved to " + db_.path() + ".");
|
||||
if (db_.dirty()) {
|
||||
const std::optional<Answer> answer = ask("Save changes before exiting?", true);
|
||||
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;
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user