docs: add design sketch
This commit is contained in:
494
docs/design_sketch.adoc
Normal file
494
docs/design_sketch.adoc
Normal file
@ -0,0 +1,494 @@
|
||||
= lopa: v1 Design Sketch
|
||||
Bertalan Z. Péter <bertalan.peter@bp99.eu>
|
||||
:toc: left
|
||||
:toclevels: 2
|
||||
:sectnums:
|
||||
:sectanchors:
|
||||
:icons: font
|
||||
:source-highlighter: rouge
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
This is a design sketch, not a specification.
|
||||
It exists to pin down v1 scope, data model, file format, and CLI surface before implementation begins.
|
||||
It is expected to evolve during and after v1 use.
|
||||
====
|
||||
|
||||
|
||||
== 💡 Purpose and Motivation
|
||||
|
||||
`lopa` is a single-user CLI tool that aims to do *two* things well: task management and day planning.
|
||||
It was built to scratch a specific itch: existing tools (eg todo.txt, TaskWarrior, SuperProductivity) each solve part of this problem but none solve all of it well for the author.
|
||||
|
||||
.Key motivations, in order
|
||||
. Efficient **CLI**-first interaction (TaskWarrior does this well)
|
||||
. *Day scheduling:* assigning tasks to concrete time slots on a given day (SuperProductivity does this reasonably well; TaskWarrior does not)
|
||||
. **Plaintext**-first storage, so tasks can be viewed and edited on any device with a text editor, without a dedicated client (neither motivating tool does this)
|
||||
. *Simple* by default: no urgency calculation, no forced project hierarchy
|
||||
. *Nothing is deleted:* tasks are permanent records; if you want to get a task out of your view, close it with the appropriate reason
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
It should be emphasized that besides pure task management, _day scheduling_ is also a key feature of `lopa`.
|
||||
Doing this type of scheduling well was a strong factor when I decided to develop _yet another_ task manager.
|
||||
In my view, none of the existing perform adequately in this regard (SuperProductivity comes closest).
|
||||
====
|
||||
|
||||
|
||||
== 🚧 Scope
|
||||
|
||||
.In scope for v1
|
||||
* Fundamentals
|
||||
** Create and edit tasks
|
||||
** Close tasks (as done or with a different reason: ‘wontfix’, invalid, duplicate)
|
||||
** Reopen closed tasks
|
||||
* Metadata
|
||||
** Tag tasks (tags subsume the notion of ‘project’, at least for now)
|
||||
** Add freeform notes to tasks
|
||||
* Scheduling
|
||||
** Create and edit day plans: ordered lists of tasks planned for a specific date with optional start times and durations
|
||||
** Reorder tasks manually within day plans and the backlog
|
||||
* Views
|
||||
** Today’s plan (default)
|
||||
** Other day’s plans
|
||||
** Backlog
|
||||
** Filtered task lists (including closed task archive)
|
||||
|
||||
.Out of scope for v1
|
||||
* Bulk maintenance operations (including task deletion; to deal with the ever-growing task list)
|
||||
* Priorities
|
||||
* Subtasks
|
||||
* Blocking relationships between tasks
|
||||
* Recurring tasks
|
||||
* Undo (git on the task directory serves this role)
|
||||
* Sync protocol (external file-sync tools handle this)
|
||||
* Long-running daemon
|
||||
* REPL, TUI, alternative frontends
|
||||
* Urgency calculation
|
||||
* Other date fields on tasks (e.g. due dates). Distinct from day plan scheduling: day plans express intent to work on a specific date; a due date would express a deadline
|
||||
* Notifications and reminders
|
||||
* Natural-language date parsing
|
||||
* Time tracking (start/stop timers)
|
||||
* Calendar integration
|
||||
* Import from other tools
|
||||
* Plugins
|
||||
|
||||
.Explicit non-goals
|
||||
* *Not* a multiuser system
|
||||
* *Not* a networked system
|
||||
* *Not* a GUI-focused application.
|
||||
The primary interface is the CLI.
|
||||
Additional frontends (potentially including a first-party GUI) may consume the same core library in future versions.
|
||||
* *Not* a collaboration tool
|
||||
|
||||
|
||||
== 🧠 Design Principles
|
||||
|
||||
. *Plaintext first.*
|
||||
The task database must be readable and editable with any text editor on any device.
|
||||
The tool is a convenience layer, not a gatekeeper.
|
||||
Anybody must be able to develop their own alternative tooling over the same task database.
|
||||
. *Tasks are permanent records.*
|
||||
`lopa` does not delete tasks.
|
||||
A task created has either an `open` state or some type of _closed_ state (`done`, `wontfix`, `invalid`, `duplicate` in v1).
|
||||
. *Tasks and plans are separate artefacts.*
|
||||
A task describes work that exists; a day plan describes intent for a specific date.
|
||||
. *One thing per file.*
|
||||
Editing one task or one plan must not require rewriting the whole database.
|
||||
This also makes file-level sync tools (Syncthing, git, rsync) work correctly out of the box.
|
||||
. *Boring, standard formats.*
|
||||
Prefer widely-parsed formats over custom syntax.
|
||||
Others should be able to write their own processors without implementing our parser.
|
||||
. *Small, composable commands.*
|
||||
Each subcommand does one thing.
|
||||
Complex operations compose from simple ones.
|
||||
. *Core logic as a library.*
|
||||
The CLI is one frontend among possible others.
|
||||
Task and plan operations live in a library package; the CLI is a thin shell over it.
|
||||
The library is designed to serve the CLI’s actual needs – abstractions are not added in anticipation of hypothetical future consumers.
|
||||
. *Defer complexity.*
|
||||
Features earn their place through real use, not through anticipation.
|
||||
. *Design for the daemon we might build, but do not build it.*
|
||||
Keep business logic separable from CLI plumbing so a future daemon or alternate frontend does not require a rewrite.
|
||||
|
||||
|
||||
== 💽 Data Model
|
||||
|
||||
We have two artefact types: *tasks* and *day plans.*
|
||||
Tasks describe work to be done and day plans describe your intent to do certain tasks on a specific date.
|
||||
Day plans reference tasks by their IDs.
|
||||
|
||||
=== The task
|
||||
|
||||
A task has the following fields in v1:
|
||||
|
||||
[cols="2,1,1,4",options="header"]
|
||||
|===
|
||||
| Field | Type | Required | Notes
|
||||
|
||||
| `id`
|
||||
| string
|
||||
| yes
|
||||
| Short generated identifier, 5-character base32.
|
||||
Short enough to type, long enough to avoid collisions at expected scale.
|
||||
|
||||
| `title`
|
||||
| string
|
||||
| yes
|
||||
| One-line summary of the task.
|
||||
|
||||
| `status`
|
||||
| enum
|
||||
| yes
|
||||
| One of `open`, `done`, `wontfix`, `invalid`, or `duplicate`.
|
||||
Defaults to `open` on creation and `done` when closed without any specific reason.
|
||||
|
||||
| `created`
|
||||
| datetime (ISO 8601)
|
||||
| yes
|
||||
| Auto-set on creation.
|
||||
|
||||
| `tags`
|
||||
| list of strings
|
||||
| no
|
||||
| Free-form labels.
|
||||
Also used to model what other tools call ‘projects.’
|
||||
|
||||
| `order`
|
||||
| integer
|
||||
| no
|
||||
| Manual ordering within the backlog.
|
||||
Higher values sort higher.
|
||||
|
||||
| `notes`
|
||||
| string (multiline)
|
||||
| no
|
||||
| Freeform text attached to the task.
|
||||
Stored as a multiline TOML string.
|
||||
|===
|
||||
|
||||
|
||||
=== The day plan
|
||||
|
||||
A day plan has the following fields in v1:
|
||||
|
||||
[cols="2,1,1,4",options="header"]
|
||||
|===
|
||||
| Field | Type | Required | Notes
|
||||
|
||||
| `date`
|
||||
| date (ISO 8601)
|
||||
| yes
|
||||
| The date this plan covers.
|
||||
Matches the filename; stored in-file for self-containment.
|
||||
|
||||
| `entries`
|
||||
| array of tables
|
||||
| no
|
||||
| Ordered list of planned entries.
|
||||
Empty array is valid (a plan can exist without content).
|
||||
Ordering is by array position in the file; reordering means editing the array.
|
||||
|===
|
||||
|
||||
Each entry within `entries` has:
|
||||
|
||||
[cols="2,1,1,4",options="header"]
|
||||
|===
|
||||
| Field | Type | Required | Notes
|
||||
|
||||
| `task`
|
||||
| string
|
||||
| yes
|
||||
| ID of a task file.
|
||||
|
||||
| `start`
|
||||
| time (HH:MM)
|
||||
| no
|
||||
| Start time within the plan’s date.
|
||||
Absence means ‘on the list, no specific slot.’
|
||||
|
||||
| `duration`
|
||||
| integer (minutes)
|
||||
| no
|
||||
| Expected duration.
|
||||
Required when `start` is set; ignored when `start` is absent.
|
||||
|===
|
||||
|
||||
|
||||
== 🤯 Design Notes
|
||||
|
||||
=== Tasks vs Day Plans
|
||||
|
||||
In `lopa`, tasks are timeless artefacts.
|
||||
Day plans form a mechanism to schedule a task to a specific time.
|
||||
|
||||
.Consequences
|
||||
* Initially, a special ‘today’ feature was considered that would be used at the start of a day to add a set of tasks that are planned for today.
|
||||
However, since more long-term scheduling was also desired, this would mean managing two schedules in parallel.
|
||||
It could be done but would be convoluted.
|
||||
Instead, the simple, sane, and logical choice was to not treat ‘today’ differently than other dates.
|
||||
Tasks for ‘today’ are those that are scheduled for today’s date in a day plan.
|
||||
* Historical plans are preserved; for example, `plans/2026-03-15.toml` is a record of what the user intended to do on that day and remains readable indefinitely.
|
||||
|
||||
|
||||
=== Task Lifecycle
|
||||
|
||||
It is a rather unorthodox choice, but in `lopa`, tasks live forever and are never meant to be deleted.
|
||||
If deletion were possible, we would have to think about what to do when a task is deleted that was referenced in a day plan but that becomes a non-issue.
|
||||
|
||||
The state change of a task never implies any change to the day plans.
|
||||
For example, even if a task is marked `done`, it remains in the day plans it was added to.
|
||||
In practice, the default `lopa view` and `lopa ls` output hides closed tasks; passing `--all` shows them.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Unlike tasks, day plans *can* be deleted.
|
||||
These plans represent intent and can be reasonably discarded.
|
||||
Of course, deleting a plan does not affect the tasks that were referenced in that plan in any way.
|
||||
====
|
||||
|
||||
|
||||
=== IDs
|
||||
|
||||
* Generated at task creation.
|
||||
* 5-character base32 (alphabet: `abcdefghijkmnpqrstuvwxyz23456789`, omitting visually ambiguous characters such as `l` and `o`).
|
||||
* Case-insensitive on input, canonicalized to lowercase on write.
|
||||
* If collision, retry.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Because tasks are never deleted, the ID space accumulates over time.
|
||||
For power users, this _might_ become an issue after prolonged use.
|
||||
The future plan to handle this problem is a supported mechanism for wiping the ‘archive,’ possibly after an export.
|
||||
====
|
||||
|
||||
|
||||
== 🗄️ File Format
|
||||
|
||||
=== One file per artefact
|
||||
|
||||
Each task lives in its own file named `<id>.toml`.
|
||||
Each day plan lives in its own file named `<date>.toml`.
|
||||
|
||||
.Rationale
|
||||
* Editing one task or plan rewrites only that file.
|
||||
Sync tools handle per-file merges cleanly.
|
||||
Concurrent edits on different devices only conflict when the same file is edited on both.
|
||||
* `git diff` on the data directory shows exactly what changed.
|
||||
* `grep`, `find`, etc work across the directory without any custom tooling.
|
||||
* On a phone with a text editor, opening one task means opening one file, not scrolling through thousands of lines.
|
||||
* This pattern is well-precedented (Maildir for email, Jekyll/Hugo for static sites, Obsidian for notes).
|
||||
|
||||
=== File structure
|
||||
|
||||
Each task file is a valid TOML document.
|
||||
|
||||
.Example task file (eg `4a2fk.toml`)
|
||||
[source,toml]
|
||||
----
|
||||
id = "4a2fk"
|
||||
title = "Write project spec"
|
||||
status = "open"
|
||||
created = 2026-07-02T09:15:00Z
|
||||
tags = ["work", "writing"]
|
||||
order = 100
|
||||
notes = """
|
||||
Freeform notes go here.
|
||||
|
||||
Multiple paragraphs are fine. Markdown, code blocks, whatever the user
|
||||
wants. The tool treats this string as opaque.
|
||||
"""
|
||||
----
|
||||
|
||||
Each day plan file is also a valid TOML document.
|
||||
Entries are represented as an array of tables.
|
||||
|
||||
.Example day plan file (eg `2026-07-13.toml`)
|
||||
[source,toml]
|
||||
----
|
||||
date = 2026-07-13
|
||||
|
||||
[[entries]]
|
||||
task = "4a2fk"
|
||||
start = "11:00"
|
||||
duration = 90
|
||||
|
||||
[[entries]]
|
||||
task = "7b3cm"
|
||||
start = "13:00"
|
||||
duration = 10
|
||||
|
||||
[[entries]]
|
||||
task = "9k4mp"
|
||||
# no start time – planned for the day but no fixed slot
|
||||
----
|
||||
|
||||
The order of entries in the file is the display order.
|
||||
Reordering means moving the `[[entries]]` blocks up or down in the file.
|
||||
|
||||
.Rationale for TOML over alternatives
|
||||
* *JSON*: 👎 hostile to hand-editing (no comments, quoted keys, trailing-comma paranoia).
|
||||
* *YAML*: 👎 whitespace-significant editing on a phone is error-prone.
|
||||
* *TOML*: 👍 unambiguous, comments allowed, native date/time types, wide parser support in every serious language.
|
||||
* *Custom format*: would require documenting and maintaining a parser, and would raise the barrier for third-party tooling.
|
||||
|
||||
|
||||
== 📁 Directory Layout
|
||||
|
||||
.Default layout under XDG base directories
|
||||
----
|
||||
$XDG_DATA_HOME/lopa/ (default: ~/.local/share/lopa/)
|
||||
tasks/
|
||||
4a2fk.toml
|
||||
7b3cm.toml
|
||||
...
|
||||
plans/
|
||||
2026-07-13.toml
|
||||
2026-07-14.toml
|
||||
...
|
||||
archive/
|
||||
9k4mp.toml
|
||||
...
|
||||
|
||||
$XDG_CONFIG_HOME/lopa/ (default: ~/.config/lopa/)
|
||||
config.toml
|
||||
----
|
||||
|
||||
.Design decisions
|
||||
* Three flat directories: `tasks/` for open tasks, `plans/` for day plans, `archive/` for closed tasks.
|
||||
* Plan files are named by the date they cover (`YYYY-MM-DD.toml`).
|
||||
* Tasks whose `status` is not `open` (i.e. `done`, `wontfix`, `invalid`, or `duplicate`) move to `archive/` via rename.
|
||||
Rationale: keeps the active `tasks/` directory focused on live work, cleaner git diffs for daily activity.
|
||||
* Reopening a closed task moves it back to `tasks/`.
|
||||
* Plan files are never archived – they accumulate as a permanent journal of intent.
|
||||
* No index file. The directory listings are the index.
|
||||
A cache can be added later without changing the on-disk format.
|
||||
|
||||
|
||||
== ⌨️ CLI Surface
|
||||
|
||||
=== Command shape
|
||||
|
||||
Subcommand style.
|
||||
Every mutating command takes a task ID.
|
||||
|
||||
.Command surface for v1
|
||||
[cols="2,3",options="header"]
|
||||
|===
|
||||
| Command | Effect
|
||||
|
||||
| `lopa`
|
||||
| Default view: today’s scheduled tasks.
|
||||
|
||||
| `lopa add "title" [flags]`
|
||||
| Create a task.
|
||||
Flags: `--tag`, `--plan <when>`, `--note`.
|
||||
|
||||
| `lopa ls [filters]`
|
||||
| List tasks.
|
||||
Filters: `--today`, `--tag <t>`, `--status <s>`, `--planned`, `--unplanned`, `--planned-on <d>`, `--all` (include closed).
|
||||
Composable.
|
||||
|
||||
| `lopa show <id>`
|
||||
| Show full detail of one task, including notes and referencing day plans.
|
||||
|
||||
| `lopa edit <id>`
|
||||
| Open the raw task file in `$EDITOR`.
|
||||
Re-parsed and validated on save.
|
||||
|
||||
| `lopa close <id> [reason]`
|
||||
| Close a task, optionally with a specific reason (`done` is the default).
|
||||
|
||||
| `lopa reopen <id>`
|
||||
| Reopen a closed task.
|
||||
|
||||
| `lopa view [when]`
|
||||
| Show the day plan for `<when>` (an ISO 8601 date) or today by default.
|
||||
Closed tasks are hidden unless `--all` is passed.
|
||||
|
||||
| `lopa plan <id> [when]`
|
||||
| Add a task to a day plan (today by default).
|
||||
|
||||
| `lopa unplan <id> [when]`
|
||||
| Remove a task from a day plan (today by default).
|
||||
An `--all` flag requests removal from all day plans.
|
||||
|
||||
| `lopa edit-plan [when]`
|
||||
| Open the raw plan in `$EDITOR` for today or the specified date.
|
||||
|
||||
| `lopa mv <id> <where> [--on <date>\|--backlog]`
|
||||
| Reorder a task using anchored positioning.
|
||||
`<where>` is one of: `top`, `bottom`, `before <other_id>`, `after <other_id>`.
|
||||
By default, operates on today’s plan.
|
||||
`--on <date>` targets a specific day’s plan.
|
||||
`--backlog` adjusts the task’s `order` field to fit the specified position.
|
||||
|
||||
| `lopa tag <id> +foo -bar`
|
||||
| Add or remove tags.
|
||||
|===
|
||||
|
||||
=== Date arguments
|
||||
|
||||
In v1, all date arguments to CLI commands must be in ISO 8601 (`YYYY-MM-DD`) format.
|
||||
Human-readable date grammars (`today`, `tomorrow`, `next mon`) are deferred to a later version.
|
||||
Commands that accept a date default to today’s date when the argument is omitted.
|
||||
|
||||
=== Output
|
||||
|
||||
* Default output is human-readable, formatted for terminal width.
|
||||
* `--json` flag on `ls`, `show`, and `view` produces machine-readable output.
|
||||
This is the first seed of the future protocol/API, but is a byproduct of the CLI, not a separate design effort.
|
||||
|
||||
=== Design notes on the command surface
|
||||
|
||||
* *Verbs, not `set field=value`.*
|
||||
`lopa close`, `lopa plan`, `lopa tag` express intent more clearly than something like `lopa set 4a2fk status=done`.
|
||||
* *`lopa edit` as escape hatch.*
|
||||
Anything the flags do not cover can be done by editing the file directly.
|
||||
This lowers the bar for the tool itself: it does not need to support every possible mutation as a flag.
|
||||
* *`lopa add` accepts common fields at creation.*
|
||||
Avoids the add-then-edit friction for the common case.
|
||||
|
||||
|
||||
== 🛠️ Implementation Notes
|
||||
|
||||
These are guidance for implementation, not final decisions.
|
||||
|
||||
* *Language*: Go, provisionally.
|
||||
Rationale: fast to write, good stdlib for filesystem and encoding work, single static binary, easy cross-compilation for future device targets, mature TOML libraries.
|
||||
* *Dependencies*: minimize.
|
||||
Curated libraries allowed where they replace significant plumbing (TOML parsing, CLI arg parsing).
|
||||
Zero deps is not a goal.
|
||||
* *Architecture*: the core lives in a library package (task and plan CRUD, filtering, scheduling logic).
|
||||
The CLI is a thin frontend over this library.
|
||||
This keeps the door open to a first-party GUI, a daemon, a REPL, a TUI, or any other frontend without rewriting core logic.
|
||||
Design the library API to serve the CLI’s needs; do not speculatively add abstractions for consumers that do not yet exist.
|
||||
* *No daemon in v1.*
|
||||
Each invocation opens the data directory, does its work, and exits.
|
||||
|
||||
|
||||
== ❓ Open Questions
|
||||
|
||||
Questions to resolve before or during implementation:
|
||||
|
||||
* Whether closing a task should also remove its references from *future* plan files.
|
||||
The current proposal is to leave them.
|
||||
* How `--json` output represents null/unset optional fields.
|
||||
* Whether to include a `--dry-run` flag on mutating commands.
|
||||
* How `lopa mv --backlog` computes `order` values when inserting between existing tasks (renumber vs. fractional gaps vs. large-integer gap policy). Implementation detail, but affects long-term stability of manual ordering.
|
||||
|
||||
|
||||
== ✅ What v1 "Done" Looks Like
|
||||
|
||||
.v1 is done when the author can
|
||||
. Add tasks with tags from the CLI.
|
||||
. Create day plans and add tasks to them with or without specific time slots.
|
||||
. Put the same task on multiple day plans.
|
||||
. See the day’s plan and other days’ plans with a single command.
|
||||
. Reorder tasks manually within a plan and within the backlog.
|
||||
. Edit any field of any task, and any plan file, directly in `$EDITOR`.
|
||||
. Close tasks with a reason.
|
||||
. Sync the whole data directory across machines with Syncthing and have it _Just Work™._
|
||||
. Use the tool daily for at least two weeks without reaching for another tried-and-tested tool.
|
||||
|
||||
The last item is the real acceptance test.
|
||||
Reference in New Issue
Block a user