Add source code.
This commit is contained in:
1 parent
dab19ace02
commit
0b8a7b3cdc
31 files changed
+2809
No files matched your search
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// What kind of value an argument holds (drives tab completion).
|
||||
enum class ArgKind {
|
||||
None,
|
||||
Task,
|
||||
Account,
|
||||
AccountOrDash,
|
||||
AccountOrStatus,
|
||||
Status,
|
||||
Visibility,
|
||||
TaskLevel,
|
||||
AccountLevel,
|
||||
File,
|
||||
Editor
|
||||
};
|
||||
|
||||
struct CommandSpec {
|
||||
std::string key; // e.g. "tsk add"
|
||||
std::string usage; // e.g. "tsk add <name> [acc] [status]"
|
||||
std::string help;
|
||||
std::size_t minArgs = 0;
|
||||
std::size_t maxArgs = 0;
|
||||
std::vector<ArgKind> argKinds;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "CommandSpec.h"
|
||||
#include "Completion.h"
|
||||
#include "Database.h"
|
||||
|
||||
// Works out tab-completion candidates for the text typed so far: command
|
||||
// words first, then arguments (task names, account names, statuses, ...).
|
||||
class Completer {
|
||||
public:
|
||||
Completer(const Database& db, const std::vector<CommandSpec>& specs);
|
||||
|
||||
Completion complete(const std::string& textBeforeCursor) const;
|
||||
|
||||
private:
|
||||
std::vector<std::string> valuesFor(ArgKind kind, const std::string& current) const;
|
||||
|
||||
const Database& db_;
|
||||
const std::vector<CommandSpec>& specs_;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Result of a tab-completion request: the text from `start` up to the cursor
|
||||
// can be replaced by any of `candidates`.
|
||||
struct Completion {
|
||||
std::size_t start = 0;
|
||||
std::vector<std::string> candidates;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
// User preferences stored outside the database, in
|
||||
// $XDG_CONFIG_HOME/task_tracker (or ~/.config/task_tracker, or %APPDATA%).
|
||||
class Config {
|
||||
public:
|
||||
Config();
|
||||
|
||||
void load();
|
||||
void save() const;
|
||||
|
||||
const std::string& editor() const; // empty = use the default
|
||||
void setEditor(const std::string& command);
|
||||
|
||||
std::string historyPath() const;
|
||||
|
||||
static std::string directory();
|
||||
|
||||
private:
|
||||
std::string directory_;
|
||||
std::string editor_;
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Models.h"
|
||||
|
||||
struct sqlite3;
|
||||
|
||||
// Works on an in-memory SQLite database that is loaded from, and saved to, a
|
||||
// file on request (db save / backup / restore).
|
||||
class Database {
|
||||
public:
|
||||
Database();
|
||||
~Database();
|
||||
Database(const Database&) = delete;
|
||||
Database& operator=(const Database&) = delete;
|
||||
|
||||
// Loads `path` into memory if it exists, otherwise starts empty.
|
||||
void open(const std::string& path);
|
||||
void save();
|
||||
std::string backup(const std::string& file); // empty = timestamped name; returns the file used
|
||||
void restore(const std::string& file);
|
||||
|
||||
bool dirty() const;
|
||||
const std::string& path() const;
|
||||
|
||||
// Accounts
|
||||
std::optional<std::string> findAccount(const std::string& name) const; // canonical spelling
|
||||
std::vector<std::string> listAccounts() const;
|
||||
void addAccount(const std::string& name);
|
||||
void removeAccount(const std::string& name);
|
||||
void renameAccount(const std::string& name, const std::string& newName);
|
||||
|
||||
// Tasks
|
||||
std::optional<std::string> findTask(const std::string& name) const; // canonical spelling
|
||||
std::optional<TaskRow> getTask(const std::string& name) const;
|
||||
std::vector<TaskRow> listTasks() const;
|
||||
void addTask(const std::string& name, const std::optional<std::string>& account, const std::string& status);
|
||||
void removeTask(const std::string& name);
|
||||
void renameTask(const std::string& name, const std::string& newName);
|
||||
void setTaskAccount(const std::string& name, const std::optional<std::string>& account);
|
||||
void setTaskStatus(const std::string& name, const std::string& status);
|
||||
void setTaskNotes(const std::string& name, const std::string& notes);
|
||||
|
||||
private:
|
||||
using Param = std::optional<std::string>;
|
||||
using Row = std::vector<std::optional<std::string>>;
|
||||
|
||||
void createSchema();
|
||||
void exec(const std::string& sql);
|
||||
std::vector<Row> query(const std::string& sql, const std::vector<Param>& params = {}) const;
|
||||
void run(const std::string& sql, const std::vector<Param>& params);
|
||||
|
||||
sqlite3* db_;
|
||||
std::string path_;
|
||||
bool dirty_;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <ctime>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "Database.h"
|
||||
#include "Status.h"
|
||||
|
||||
enum class AccountLevel { Busy, Free };
|
||||
|
||||
struct DisplaySettings {
|
||||
bool showTasks = true; // auto-print the task table after each change
|
||||
bool showAccounts = true; // auto-print the account table after each change
|
||||
std::set<StatusKind> hiddenTaskLevels;
|
||||
std::set<AccountLevel> hiddenAccountLevels;
|
||||
};
|
||||
|
||||
// Builds the computed views. The status shown is derived from the stored
|
||||
// status and the current time, never copied straight from the database.
|
||||
class Display {
|
||||
public:
|
||||
explicit Display(const Database& db);
|
||||
|
||||
DisplaySettings& settings();
|
||||
const DisplaySettings& settings() const;
|
||||
|
||||
std::string renderTasks(std::time_t now) const;
|
||||
std::string renderAccounts(std::time_t now) const;
|
||||
|
||||
private:
|
||||
const Database& db_;
|
||||
DisplaySettings settings_;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ExternalEditor {
|
||||
|
||||
// $VISUAL, then $EDITOR, then the first of nano/vim/vi found on PATH.
|
||||
// Empty if nothing was found.
|
||||
std::string defaultCommand();
|
||||
|
||||
// Is the first word of `command` an executable we can find?
|
||||
bool commandExists(const std::string& command);
|
||||
|
||||
// Well-known editors that are installed (used for tab completion).
|
||||
std::vector<std::string> installedEditors();
|
||||
|
||||
// Opens `initialText` in the editor and returns the edited text.
|
||||
// Throws std::runtime_error if the editor cannot be run or exits with an error.
|
||||
std::string edit(const std::string& command, const std::string& initialText);
|
||||
|
||||
} // namespace ExternalEditor
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Completion.h"
|
||||
|
||||
// Reads one line with cursor editing, history (up/down) and tab completion.
|
||||
// Falls back to plain std::getline when stdin/stdout are not a terminal.
|
||||
class LineEditor {
|
||||
public:
|
||||
using CompletionFn = std::function<Completion(const std::string& textBeforeCursor)>;
|
||||
|
||||
LineEditor();
|
||||
|
||||
void setCompleter(const CompletionFn& completer);
|
||||
void setHistoryFile(const std::string& path); // loads it and appends new entries
|
||||
void addHistory(const std::string& line);
|
||||
|
||||
// Returns false on end of input (Ctrl-D on an empty line, or closed stdin).
|
||||
bool readLine(const std::string& prompt, std::string& line);
|
||||
|
||||
private:
|
||||
bool readPlain(const std::string& prompt, std::string& line);
|
||||
bool readInteractive(const std::string& prompt, std::string& line);
|
||||
|
||||
CompletionFn completer_;
|
||||
std::vector<std::string> history_;
|
||||
std::string historyFile_;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
struct TaskRow {
|
||||
std::string id;
|
||||
std::optional<std::string> account;
|
||||
std::string status; // serialized, see StatusUtil::serialize
|
||||
std::string notes; // free text, any length (stored as SQLite TEXT)
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "CommandSpec.h"
|
||||
#include "Completer.h"
|
||||
#include "Config.h"
|
||||
#include "Database.h"
|
||||
#include "Display.h"
|
||||
#include "LineEditor.h"
|
||||
#include "Status.h"
|
||||
|
||||
// Interactive prompt: commands are typed live, one per line.
|
||||
class Repl {
|
||||
public:
|
||||
Repl(Database& db, Display& display, Config& config);
|
||||
|
||||
int run();
|
||||
|
||||
private:
|
||||
using Args = std::vector<std::string>;
|
||||
using Handler = bool (Repl::*)(const Args&); // returns true if the panels should be redrawn
|
||||
|
||||
struct Command {
|
||||
CommandSpec spec;
|
||||
Handler handler;
|
||||
};
|
||||
|
||||
void registerCommands();
|
||||
void add(const std::string& key, const std::string& usage, const std::string& help,
|
||||
std::size_t minArgs, std::size_t maxArgs, const std::vector<ArgKind>& argKinds,
|
||||
Handler handler);
|
||||
const Command* find(const Args& tokens, std::size_t& consumed) const;
|
||||
|
||||
void execute(const std::string& line);
|
||||
void refresh() const;
|
||||
void say(const std::string& message);
|
||||
void flushMessages();
|
||||
void endOfInput();
|
||||
bool confirm(const std::string& question) const;
|
||||
|
||||
// Helpers
|
||||
static void requireName(const std::string& name);
|
||||
static Status parseStatusFrom(const Args& args, std::size_t from);
|
||||
|
||||
// Account commands
|
||||
bool cmdAccAdd(const Args& args);
|
||||
bool cmdAccDel(const Args& args);
|
||||
bool cmdAccRename(const Args& args);
|
||||
bool cmdAccLs(const Args& args);
|
||||
|
||||
// Task commands
|
||||
bool cmdTskAdd(const Args& args);
|
||||
bool cmdTskDel(const Args& args);
|
||||
bool cmdTskLs(const Args& args);
|
||||
bool cmdTskRename(const Args& args);
|
||||
bool cmdTskReacc(const Args& args);
|
||||
bool cmdTskRestat(const Args& args);
|
||||
bool cmdTskNotes(const Args& args);
|
||||
|
||||
// Display commands
|
||||
bool cmdDisp(const Args& args);
|
||||
bool cmdDispTsk(const Args& args);
|
||||
bool cmdDispTskLevel(const Args& args);
|
||||
bool cmdDispAcc(const Args& args);
|
||||
bool cmdDispAccLevel(const Args& args);
|
||||
|
||||
// Database commands
|
||||
bool cmdDbSave(const Args& args);
|
||||
bool cmdDbBackup(const Args& args);
|
||||
bool cmdDbRestore(const Args& args);
|
||||
|
||||
// Misc
|
||||
bool cmdEditor(const Args& args);
|
||||
bool cmdHelp(const Args& args);
|
||||
bool cmdQuit(const Args& args);
|
||||
|
||||
Database& db_;
|
||||
Display& display_;
|
||||
Config& config_;
|
||||
std::vector<Command> commands_;
|
||||
std::vector<CommandSpec> specs_;
|
||||
Completer completer_;
|
||||
LineEditor lineEditor_;
|
||||
std::vector<std::string> messages_;
|
||||
bool quit_;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <ctime>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// 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 {
|
||||
StatusKind kind = StatusKind::Prompt;
|
||||
std::time_t waitUntil = 0; // only meaningful when kind == Wait
|
||||
};
|
||||
|
||||
namespace StatusUtil {
|
||||
|
||||
Status make(StatusKind kind);
|
||||
|
||||
// User input such as "testing" or "wait 16:30". A wait time that has already
|
||||
// passed today is taken to mean tomorrow.
|
||||
std::optional<Status> parse(const std::string& text, std::time_t now);
|
||||
|
||||
// Database representation, e.g. "testing" or "wait 2026-10-17T16:30".
|
||||
std::string serialize(const Status& status);
|
||||
std::optional<Status> deserialize(const std::string& text);
|
||||
|
||||
// The status shown to the user: an expired wait becomes LimitReset.
|
||||
Status effective(const Status& status, std::time_t now);
|
||||
|
||||
std::string label(const Status& status); // "Prompt", "Limit reset", "Wait 16:30", ...
|
||||
std::string kindName(StatusKind kind); // lowercase token: "prompt", "limit-reset", ...
|
||||
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
|
||||
bool isBusy(StatusKind kind); // does this status hold the account?
|
||||
|
||||
} // namespace StatusUtil
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace StringUtil {
|
||||
|
||||
std::string lower(const std::string& text);
|
||||
std::string trim(const std::string& text);
|
||||
bool equalsIgnoreCase(const std::string& a, const std::string& b);
|
||||
bool startsWithIgnoreCase(const std::string& text, const std::string& prefix);
|
||||
std::string join(const std::vector<std::string>& parts, std::size_t from, const std::string& separator);
|
||||
|
||||
// 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);
|
||||
|
||||
} // namespace StringUtil
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class TextTable {
|
||||
public:
|
||||
explicit TextTable(const std::vector<std::string>& headers);
|
||||
|
||||
void addRow(const std::vector<std::string>& cells);
|
||||
std::size_t rowCount() const;
|
||||
std::string render() const;
|
||||
|
||||
private:
|
||||
std::vector<std::string> headers_;
|
||||
std::vector<std::vector<std::string>> rows_;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
namespace Terminal {
|
||||
|
||||
bool stdinIsInteractive();
|
||||
bool stdoutIsInteractive();
|
||||
|
||||
// Clears the screen. Does nothing when stdout is not a terminal.
|
||||
void clearScreen();
|
||||
|
||||
} // namespace Terminal
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Tokenizer {
|
||||
|
||||
// Splits a command line on whitespace. Double quotes group words into a single
|
||||
// token ("Breeze A"). Throws std::invalid_argument on an unterminated quote.
|
||||
std::vector<std::string> split(const std::string& line);
|
||||
|
||||
// Lenient split for tab completion: completed words, plus the word being typed.
|
||||
struct PartialLine {
|
||||
std::vector<std::string> words; // completed words (quotes removed)
|
||||
std::string current; // word being typed (quotes removed, may be empty)
|
||||
std::size_t start = 0; // index in the text where that word begins
|
||||
};
|
||||
|
||||
PartialLine splitPartial(const std::string& text);
|
||||
|
||||
} // namespace Tokenizer
|
||||
Reference in new issue
Block a user