C and Python API + Smarter account-based wait times
This commit is contained in:
1 parent
35c568333d
commit
10badb282c
18 files changed
+2204
-429
No files matched your search
+33
-211
@@ -16,10 +16,6 @@ namespace {
|
||||
|
||||
const std::size_t kUnlimited = std::numeric_limits<std::size_t>::max();
|
||||
|
||||
const char* const kStatusHelp =
|
||||
"prompt, limit-reset, testing, wait <time>, uploading, frozen, done "
|
||||
"(time: 16:30, 4:30pm, 4pm)";
|
||||
|
||||
bool parseVisibility(const std::string& word) {
|
||||
const std::string value = StringUtil::lower(word);
|
||||
if (value == "show") {
|
||||
@@ -34,7 +30,7 @@ bool parseVisibility(const std::string& word) {
|
||||
} // namespace
|
||||
|
||||
Repl::Repl(Database& db, Display& display, Config& config)
|
||||
: db_(db), display_(display), config_(config), completer_(db, specs_), quit_(false) {
|
||||
: db_(db), display_(display), config_(config), tracker_(db), completer_(db, specs_), quit_(false) {
|
||||
registerCommands();
|
||||
}
|
||||
|
||||
@@ -203,11 +199,16 @@ void Repl::execute(const std::string& line) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((this->*command->handler)(args)) {
|
||||
const bool redraw = (this->*command->handler)(args);
|
||||
for (const std::string& message : tracker_.takeMessages()) {
|
||||
say(message);
|
||||
}
|
||||
if (redraw) {
|
||||
refresh();
|
||||
}
|
||||
flushMessages();
|
||||
} catch (const std::exception& error) {
|
||||
tracker_.takeMessages();
|
||||
messages_.clear();
|
||||
std::cout << "Error: " << error.what() << "\n";
|
||||
}
|
||||
@@ -219,7 +220,7 @@ void Repl::refresh() const {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::time_t now = std::time(nullptr);
|
||||
const std::time_t now = tracker_.now();
|
||||
Terminal::clearScreen();
|
||||
if (settings.showTasks) {
|
||||
std::cout << display_.renderTasks(now) << "\n";
|
||||
@@ -251,146 +252,43 @@ std::optional<Repl::Answer> Repl::ask(const std::string& question, bool allowCan
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
void Repl::requireName(const std::string& name) {
|
||||
if (name.empty() || name == "-") {
|
||||
throw std::runtime_error("'" + name + "' is not a valid name.");
|
||||
}
|
||||
}
|
||||
|
||||
Status Repl::parseStatusFrom(const Args& args, std::size_t from) {
|
||||
const std::string text = StringUtil::join(args, from, " ");
|
||||
std::optional<Status> status = StatusUtil::parse(text, std::time(nullptr));
|
||||
if (!status) {
|
||||
throw std::runtime_error("Invalid status '" + text + "'. Use: " + kStatusHelp);
|
||||
}
|
||||
return *status;
|
||||
}
|
||||
|
||||
// ---- Accounts ---------------------------------------------------------------
|
||||
|
||||
bool Repl::cmdAccAdd(const Args& args) {
|
||||
requireName(args[0]);
|
||||
if (db_.findAccount(args[0])) {
|
||||
throw std::runtime_error("Account '" + args[0] + "' already exists.");
|
||||
}
|
||||
db_.addAccount(args[0]);
|
||||
say("Account '" + args[0] + "' added.");
|
||||
tracker_.addAccount(args[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Repl::cmdAccDel(const Args& args) {
|
||||
std::optional<std::string> name = db_.findAccount(args[0]);
|
||||
if (!name) {
|
||||
throw std::runtime_error("No account named '" + args[0] + "'.");
|
||||
}
|
||||
db_.removeAccount(*name);
|
||||
say("Account '" + *name + "' deleted.");
|
||||
tracker_.deleteAccount(args[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Repl::cmdAccRename(const Args& args) {
|
||||
std::optional<std::string> name = db_.findAccount(args[0]);
|
||||
if (!name) {
|
||||
throw std::runtime_error("No account named '" + args[0] + "'.");
|
||||
}
|
||||
requireName(args[1]);
|
||||
if (!StringUtil::equalsIgnoreCase(*name, args[1]) && db_.findAccount(args[1])) {
|
||||
throw std::runtime_error("Account '" + args[1] + "' already exists.");
|
||||
}
|
||||
db_.renameAccount(*name, args[1]);
|
||||
say("Account '" + *name + "' renamed to '" + args[1] + "'.");
|
||||
tracker_.renameAccount(args[0], args[1]);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Repl::setAccountWait(const std::string& account, std::time_t until) {
|
||||
db_.setAccountCooldown(account, StatusUtil::timeToText(until));
|
||||
say("Account '" + account + "' waits until " + StatusUtil::clockLabel(until) +
|
||||
": its Prompt and Testing tasks show Wait, then Limit reset (acc unwait " + account + " to clear).");
|
||||
}
|
||||
|
||||
// Tells the user when a task's own status is hidden by its account's wait.
|
||||
void Repl::noteAccountWait(const std::string& taskName) {
|
||||
std::optional<TaskRow> task = db_.getTask(taskName);
|
||||
std::optional<Status> stored = task ? StatusUtil::deserialize(task->status) : std::nullopt;
|
||||
if (!task || !task->account || !stored || !StatusUtil::followsAccountWait(stored->kind)) {
|
||||
return;
|
||||
}
|
||||
std::optional<AccountRow> account = db_.getAccount(*task->account);
|
||||
if (account && account->cooldown) {
|
||||
say("It shows as Wait / Limit reset because account '" + account->id + "' is waiting (acc unwait " +
|
||||
account->id + " to clear).");
|
||||
}
|
||||
}
|
||||
|
||||
bool Repl::cmdAccWait(const Args& args) {
|
||||
std::optional<AccountRow> account = db_.getAccount(args[0]);
|
||||
if (!account) {
|
||||
throw std::runtime_error("No account named '" + args[0] + "'.");
|
||||
}
|
||||
const std::string text = "wait " + StringUtil::join(args, 1, " ");
|
||||
std::optional<Status> status = StatusUtil::parse(text, std::time(nullptr));
|
||||
if (!status) {
|
||||
throw std::runtime_error("Invalid time '" + StringUtil::join(args, 1, " ") +
|
||||
"'. Use e.g. 16:30, 4:30pm or 4pm.");
|
||||
}
|
||||
setAccountWait(account->id, status->waitUntil);
|
||||
tracker_.setAccountWaitText(args[0], StringUtil::join(args, 1, " "));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Repl::cmdAccUnwait(const Args& args) {
|
||||
std::optional<AccountRow> account = db_.getAccount(args[0]);
|
||||
if (!account) {
|
||||
throw std::runtime_error("No account named '" + args[0] + "'.");
|
||||
}
|
||||
if (!account->cooldown) {
|
||||
say("Account '" + account->id + "' is not waiting.");
|
||||
return false;
|
||||
}
|
||||
db_.setAccountCooldown(account->id, std::nullopt);
|
||||
say("Wait cleared on '" + account->id + "'.");
|
||||
return true;
|
||||
return tracker_.clearAccountWait(args[0]);
|
||||
}
|
||||
|
||||
bool Repl::cmdAccLs(const Args&) {
|
||||
Terminal::clearScreen();
|
||||
std::cout << display_.renderAccounts(std::time(nullptr)) << "\n";
|
||||
std::cout << display_.renderAccounts(tracker_.now()) << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Tasks ------------------------------------------------------------------
|
||||
|
||||
bool Repl::cmdTskAdd(const Args& args) {
|
||||
const std::string& name = args[0];
|
||||
requireName(name);
|
||||
if (db_.findTask(name)) {
|
||||
throw std::runtime_error("Task '" + name + "' already exists.");
|
||||
}
|
||||
|
||||
// tsk add <name> [acc] [status]: the second word is an account if one exists
|
||||
// with that name ("-" = none), otherwise it starts the status.
|
||||
std::size_t next = 1;
|
||||
std::optional<std::string> account;
|
||||
if (next < args.size()) {
|
||||
@@ -399,119 +297,50 @@ bool Repl::cmdTskAdd(const Args& args) {
|
||||
++next;
|
||||
} else if (args[next] == "-") {
|
||||
++next;
|
||||
} 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 + ").");
|
||||
}
|
||||
}
|
||||
|
||||
Status status = StatusUtil::make(StatusKind::Prompt);
|
||||
if (next < args.size()) {
|
||||
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);
|
||||
const std::string text = StringUtil::join(args, next, " ");
|
||||
if (!StatusUtil::isBareWait(text) && !StatusUtil::parse(text, tracker_.now())) {
|
||||
throw std::runtime_error("'" + args[next] + "' is neither an existing account nor a valid status (" +
|
||||
StatusUtil::helpText() + ").");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A wait on a task that has an account is the account's wait; the task itself
|
||||
// stays Prompt and follows it.
|
||||
const bool accountWait = (status.kind == StatusKind::Wait && account);
|
||||
const Status stored = accountWait ? StatusUtil::make(StatusKind::Prompt) : status;
|
||||
|
||||
db_.addTask(name, account, StatusUtil::serialize(stored));
|
||||
say("Task '" + name + "' added.");
|
||||
if (accountWait) {
|
||||
setAccountWait(*account, status.waitUntil);
|
||||
} else {
|
||||
noteAccountWait(name);
|
||||
std::optional<std::string> statusText;
|
||||
if (next < args.size()) {
|
||||
statusText = StringUtil::join(args, next, " ");
|
||||
}
|
||||
tracker_.addTask(args[0], account, statusText);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Repl::cmdTskDel(const Args& args) {
|
||||
std::optional<std::string> name = db_.findTask(args[0]);
|
||||
if (!name) {
|
||||
throw std::runtime_error("No task named '" + args[0] + "'.");
|
||||
}
|
||||
db_.removeTask(*name);
|
||||
say("Task '" + *name + "' deleted.");
|
||||
tracker_.deleteTask(args[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Repl::cmdTskLs(const Args&) {
|
||||
Terminal::clearScreen();
|
||||
std::cout << display_.renderTasks(std::time(nullptr)) << "\n";
|
||||
std::cout << display_.renderTasks(tracker_.now()) << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Repl::cmdTskRename(const Args& args) {
|
||||
std::optional<std::string> name = db_.findTask(args[0]);
|
||||
if (!name) {
|
||||
throw std::runtime_error("No task named '" + args[0] + "'.");
|
||||
}
|
||||
requireName(args[1]);
|
||||
if (!StringUtil::equalsIgnoreCase(*name, args[1]) && db_.findTask(args[1])) {
|
||||
throw std::runtime_error("Task '" + args[1] + "' already exists.");
|
||||
}
|
||||
db_.renameTask(*name, args[1]);
|
||||
say("Task '" + *name + "' renamed to '" + args[1] + "'.");
|
||||
tracker_.renameTask(args[0], args[1]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Repl::cmdTskReacc(const Args& args) {
|
||||
std::optional<std::string> name = db_.findTask(args[0]);
|
||||
if (!name) {
|
||||
throw std::runtime_error("No task named '" + args[0] + "'.");
|
||||
}
|
||||
|
||||
std::optional<std::string> account;
|
||||
if (args[1] != "-") {
|
||||
account = db_.findAccount(args[1]);
|
||||
if (!account) {
|
||||
throw std::runtime_error("No account named '" + args[1] + "'.");
|
||||
}
|
||||
account = args[1];
|
||||
}
|
||||
db_.setTaskAccount(*name, account);
|
||||
say("Task '" + *name + "' now on account " + (account ? "'" + *account + "'" : std::string("(none)")) + ".");
|
||||
tracker_.setTaskAccount(args[0], account);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Repl::cmdTskRestat(const Args& args) {
|
||||
std::optional<TaskRow> task = db_.getTask(args[0]);
|
||||
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.
|
||||
makeTaskFollowAccount(*task);
|
||||
setAccountWait(*task->account, status.waitUntil);
|
||||
return true;
|
||||
}
|
||||
|
||||
db_.setTaskStatus(task->id, StatusUtil::serialize(status));
|
||||
say("Task '" + task->id + "' set to " + StatusUtil::label(status) + ".");
|
||||
noteAccountWait(task->id);
|
||||
tracker_.setTaskStatus(args[0], StringUtil::join(args, 1, " "));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -531,14 +360,7 @@ bool Repl::cmdTskNotes(const Args& args) {
|
||||
|
||||
const std::string initial = task->notes.empty() ? "" : task->notes + "\n";
|
||||
const std::string edited = StringUtil::trim(ExternalEditor::edit(command, initial));
|
||||
|
||||
if (edited == StringUtil::trim(task->notes)) {
|
||||
say("Notes unchanged.");
|
||||
return false;
|
||||
}
|
||||
db_.setTaskNotes(task->id, edited);
|
||||
say(edited.empty() ? "Notes cleared." : "Notes updated.");
|
||||
return true;
|
||||
return tracker_.setTaskNotes(task->id, edited);
|
||||
}
|
||||
|
||||
// ---- Display ----------------------------------------------------------------
|
||||
@@ -696,7 +518,7 @@ bool Repl::cmdHelp(const Args&) {
|
||||
}
|
||||
std::cout << " " << command.spec.usage << "\n " << command.spec.help << "\n";
|
||||
}
|
||||
std::cout << "\nStatuses: " << kStatusHelp << "\n"
|
||||
std::cout << "\nStatuses: " << StatusUtil::helpText() << "\n"
|
||||
<< "Keys: Up/Down history, Left/Right edit, Tab completes, Ctrl-A/E start/end, "
|
||||
"Ctrl-U/K/W delete, Ctrl-L clear\n\n";
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user