gtd

package module
v0.0.0-...-10ffa72 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 3, 2026 License: MIT Imports: 2 Imported by: 0

README

gtd-tui

A personal, single-user productivity app for terminal users, built around the Getting Things Done methodology. Tasks, projects, and notes live in one cross-linked, navigable interface — no app switching, no maintenance overhead, no friction between thinking and capturing.

Status: early development. The Tasks and Projects vertical slice is usable; Inbox, References, Meetings, Comments, and Timelines are specified but not yet implemented. See Roadmap.

Why

Most GTD tools fail the same way: the system needs more upkeep than the work it's tracking. gtd-tui is built around the opposite premise — capture should take seconds, navigation should be one keystroke, and every entity should carry its own history so you can answer "what happened here, and why?" without hunting through separate logs.

The product principles live in openspec/specs/product-vision/spec.md:

  • Low ceremony — capture is a single low-friction interaction.
  • One pane of glass — tasks, projects, and notes are views of the same data.
  • Easy linking — relationships are first-class; cross-links are easy to create and follow.
  • Timeline as context — every entity has a chronological history.

Out of scope: team collaboration, time tracking, calendar replacement.

Install

Requires Go 1.26 or newer. No CGO, no native dependencies.

go install github.com/qualidafial/gtd-tui/cmd/gtd@latest

Or build from source:

git clone https://github.com/qualidafial/gtd-tui
cd gtd-tui
go build -o gtd ./cmd/gtd

Run

gtd

The database is created on first run at $XDG_CONFIG_HOME/gtd/gtd.db (typically ~/.config/gtd/gtd.db). Migrations are applied automatically.

Keybindings

Global:

Key Action
? Toggle help
tab Next tab
shift+tab Previous tab
esc Back / cancel
ctrl+c Quit

Task list:

Key Action
+ / insert New task
enter Edit task
space Toggle complete
delete Drop task
p Jump to project
shift+↑/↓ Reorder (within current filter)
/ Filter

Project list:

Key Action
+ / insert New project
e Edit project
enter Open project view
space Toggle complete
delete Drop project
s Park (set someday)
shift+↑/↓ Reorder (within current filter)
/ Filter

Filter syntax

The / query bar accepts a small DSL for narrowing the visible list. Multiple clauses combine with AND; the last value for a repeated key wins.

Examples:

  • status:open — only open items (default)
  • status:done — completed items
  • status:dropped — dropped items
  • status:someday — parked projects
  • due:2026-06-01 — items due on a specific date
  • tomato — free-text title match

Invalid clauses are highlighted in the bar and don't apply until corrected.

Concepts

The domain model is captured in openspec/specs/domain-model/spec.md. Briefly:

  • Item — an unprocessed inbox capture. Clarified into a Task, Project, or Reference (lineage preserved via soft-delete).
  • Task — a single actionable item. Kind is next_action or delegated; Status is open, done, or dropped. Belongs to zero or one Project.
  • Project — a multi-step outcome. Status is open, someday (parked), done, or dropped. Terminal transitions cascade or detach open tasks; the invariant "no open tasks under a closed project" is enforced.
  • Reference — standalone markdown content kept for retrieval.
  • Meeting — title, time slot, attendees, markdown body. Action items captured during a meeting flow to the inbox with a link back.
  • Comment — short, event-shaped text attached to a Task or Project; recorded implicitly on edits and explicitly via the comment API.

The clarify workflow (Capture → Clarify → Organize → Engage → Reflect) and the five clarify outcomes (Discard, Incubate, FileAsReference, ClarifyAsTask, ClarifyAsProject) are in openspec/specs/gtd-workflows/spec.md, which also includes a Mermaid diagram of the decision flow.

Project layout

Follows Ben Johnson's Go application structure:

.                       Root package — domain types and service interfaces only
├── cmd/gtd/            CLI entry point
├── service/            Cross-store orchestration (transactional)
├── sqlite/             SQLite implementation
│   └── migrations/     Embedded SQL migrations, applied in order
├── tui/                Bubbletea v2 UI
│   └── pages/          One package per top-level page (tasks, projects, …)
├── internal/set/       Internal generic Set type
└── openspec/           Specifications and proposed changes
    ├── specs/          Authoritative current behavior
    └── changes/        Proposed and archived changes

Architectural rules — value semantics, modernc.org/sqlite driver, squirrel for queries, CHECK constraints, WAL + foreign keys, service-level transactions — are in openspec/specs/architecture/spec.md.

Development

go test ./...        # full suite, uses in-memory SQLite
go build ./...
openspec validate --specs  # validate all specs

Specs are the source of truth. Behavioral changes start with a proposal under openspec/changes/; see existing changes for the format. The opsx:propose / opsx:apply / opsx:archive slash commands automate the workflow.

Roadmap

Implemented:

  • Tasks (CRUD, status transitions, ordering, filtering)
  • Projects (CRUD, status transitions including someday/park, ordering, filtering, project view with linked tasks, project picker overlay)
  • Shared query bar with live-preview validation
  • TUI view stack with overlay support

Specified, not yet implemented (see openspec/changes/):

  • Inbox — Item entity + four clarify operations
  • References — Reference entity + FileAsReference
  • Meetings — Meeting + MeetingLink + AddActionItem
  • Comments — edit-with-comment + standalone comments
  • Timelines — activity history per entity + global Reflect view

License

MIT © 2026 Matthew Hall

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type DatePredicate

type DatePredicate struct {
	Kind DatePredicateKind
	Time time.Time
}

DatePredicate constrains a date column. Time-based kinds (OnOrBefore, AvailableAsOf, After) carry a resolved UTC time; IsNull/IsNotNull ignore Time.

type DatePredicateKind

type DatePredicateKind int

DatePredicateKind discriminates how a DatePredicate constrains a date column.

const (
	// OnOrBefore matches `column IS NOT NULL AND column <= Time` (used by Due).
	OnOrBefore DatePredicateKind = iota
	// AvailableAsOf matches `column IS NULL OR column <= Time` (used by Ready).
	AvailableAsOf
	// After matches `column > Time` (used by Defer).
	After
	// IsNull matches `column IS NULL`.
	IsNull
	// IsNotNull matches `column IS NOT NULL`.
	IsNotNull
)

type InboxService

type InboxService interface {
	Create(ctx context.Context, item Item) (Item, error)
	List(ctx context.Context) ([]Item, error)
	Get(ctx context.Context, id int64) (Item, error)

	Discard(ctx context.Context, itemID int64) (Item, error)
	Incubate(ctx context.Context, itemID int64, project Project) (Project, Item, error)
	ClarifyAsTask(ctx context.Context, itemID int64, task Task) (Task, Item, error)
	ClarifyAsProject(ctx context.Context, itemID int64, project Project, firstTask Task) (Project, Task, Item, error)
}

InboxService is the full inbox surface: capture (Create/List/Get) plus the four clarify operations. The concrete implementation lives in the service package; the SQLite layer satisfies the capture-side methods and the service-layer wraps them with the cross-store transactional clarify ops.

type Item

type Item struct {
	ID                     int64
	Title                  string
	Description            string
	CreatedAt              time.Time
	UpdatedAt              time.Time
	ClarifiedIntoTaskID    *int64
	ClarifiedIntoProjectID *int64
	Discarded              bool
}

Item is an unprocessed inbox capture. It is the entry point for the GTD clarify workflow: every Item is eventually clarified into a Task or Project, or discarded. At most one of ClarifiedIntoTaskID / ClarifiedIntoProjectID is non-nil, and Discarded is false whenever either is set. The Incubate and ClarifyAsProject operations both target ClarifiedIntoProjectID; the project's status distinguishes the two outcomes.

type Project

type Project struct {
	ID          int64
	Title       string
	Outcome     string
	Description string
	Due         *time.Time
	Status      ProjectStatus
	CreatedAt   time.Time
	UpdatedAt   time.Time
	// StatusChangedAt records when the project last entered its current status:
	// equal to CreatedAt on creation (the transition into open), then
	// overwritten by the supplied instant on every Complete/Drop/Park/Reopen.
	StatusChangedAt time.Time
}

type ProjectFilter

type ProjectFilter struct {
	Status *ProjectStatus
	Search []string
}

func (ProjectFilter) WithStatus

func (f ProjectFilter) WithStatus(s ProjectStatus) ProjectFilter

type ProjectService

type ProjectService interface {
	GetProject(ctx context.Context, id int64) (Project, error)
	ListProjects(ctx context.Context, filter ProjectFilter) ([]Project, error)
	CreateProject(ctx context.Context, project Project) (Project, error)
	UpdateProject(ctx context.Context, project Project) (Project, error)
	// CompleteProject transitions the project to done. When cascade is true,
	// pending tasks are marked done; when false, they are detached (ProjectID
	// set to nil). The at instant stamps the project's StatusChangedAt and any
	// cascaded task's StatusChangedAt.
	CompleteProject(ctx context.Context, id int64, cascade bool, at time.Time) (Project, error)
	// DropProject transitions the project to dropped, with the same cascade
	// semantics as CompleteProject.
	DropProject(ctx context.Context, id int64, cascade bool, at time.Time) (Project, error)
	// ParkProject transitions the project to someday without changing task
	// statuses; tasks are filtered from default views by query logic.
	ParkProject(ctx context.Context, id int64, at time.Time) (Project, error)
	// ReopenProject restores a someday/done/dropped project to open without
	// changing task statuses. Mirrors ReopenTask.
	ReopenProject(ctx context.Context, id int64, at time.Time) (Project, error)
	// MoveProjectUp / MoveProjectDown shift a project one slot within projects
	// of the same status that also match filter. The moving project's status
	// group is always the universe; filter narrows further.
	MoveProjectUp(ctx context.Context, id int64, filter ProjectFilter) error
	MoveProjectDown(ctx context.Context, id int64, filter ProjectFilter) error
	CountTasksByProjects(ctx context.Context, projectIDs []int64) (map[int64]ProjectTaskCounts, error)
}

type ProjectStatus

type ProjectStatus string
const (
	ProjectStatusOpen    ProjectStatus = "open"
	ProjectStatusSomeday ProjectStatus = "someday"
	ProjectStatusDone    ProjectStatus = "done"
	ProjectStatusDropped ProjectStatus = "dropped"
)

type ProjectTaskCounts

type ProjectTaskCounts struct {
	Complete int // done tasks (non-dropped, non-pending)
	Total    int // non-dropped tasks
}

type Task

type Task struct {
	ID          int64
	Title       string
	Description string
	Status      TaskStatus
	Assignee    *string
	ProjectID   *int64
	Due         *time.Time
	DeferUntil  *time.Time
	CreatedAt   time.Time
	UpdatedAt   time.Time
	// StatusChangedAt records when the task last entered its current status:
	// equal to CreatedAt on creation (the transition into open), then
	// overwritten by the supplied instant on every Complete/Drop/Reopen.
	StatusChangedAt time.Time
}

type TaskFilter

type TaskFilter struct {
	Status    *TaskStatus
	Assignee  *string
	ProjectID *int64
	Due       *DatePredicate
	Ready     *DatePredicate
	Defer     *DatePredicate
	Search    []string
	TaskIDs   []int64
	// IncludeSomedayProjects keeps tasks whose project is parked (someday) in
	// the results. When false (default), someday-project tasks are excluded.
	IncludeSomedayProjects bool
}

func (TaskFilter) WithAssignee

func (f TaskFilter) WithAssignee(a string) TaskFilter

func (TaskFilter) WithProjectID

func (f TaskFilter) WithProjectID(id int64) TaskFilter

func (TaskFilter) WithSearch

func (f TaskFilter) WithSearch(terms ...string) TaskFilter

func (TaskFilter) WithStatus

func (f TaskFilter) WithStatus(s TaskStatus) TaskFilter

func (TaskFilter) WithTaskIDs

func (f TaskFilter) WithTaskIDs(ids ...int64) TaskFilter

type TaskService

type TaskService interface {
	GetTask(ctx context.Context, id int64) (Task, error)
	ListTasks(ctx context.Context, filter TaskFilter) ([]Task, error)
	CreateTask(ctx context.Context, task Task) (Task, error)
	UpdateTask(ctx context.Context, task Task) (Task, error)
	CompleteTask(ctx context.Context, id int64, at time.Time) (Task, error)
	DropTask(ctx context.Context, id int64, at time.Time) (Task, error)
	ReopenTask(ctx context.Context, id int64, at time.Time) (Task, error)
	DeleteTask(ctx context.Context, id int64) error
	// MoveTaskUp / MoveTaskDown shift an open task one slot within the open
	// tasks that match filter. The filter scopes the swap to the user's current
	// view so the reorder is immediately visible; status is always forced to
	// open inside the move regardless of filter.Status.
	MoveTaskUp(ctx context.Context, id int64, filter TaskFilter) error
	MoveTaskDown(ctx context.Context, id int64, filter TaskFilter) error
}

type TaskStatus

type TaskStatus string
const (
	TaskStatusOpen    TaskStatus = "open"
	TaskStatusDone    TaskStatus = "done"
	TaskStatusDropped TaskStatus = "dropped"
)

Directories

Path Synopsis
cmd
gtd command
internal
orderkey
Package orderkey produces sortable fractional-indexing keys so list items can be reordered with single-row updates regardless of list length.
Package orderkey produces sortable fractional-indexing keys so list items can be reordered with single-row updates regardless of list length.
projectquery
Package projectquery parses a compact project-list query string into a gtd.ProjectFilter.
Package projectquery parses a compact project-list query string into a gtd.ProjectFilter.
reltime
Package reltime renders timestamps as compact relative-time WHEN strings, providing a single shared vocabulary for the task list's chips and the task editor's status line.
Package reltime renders timestamps as compact relative-time WHEN strings, providing a single shared vocabulary for the task list's chips and the task editor's status line.
set
taskquery
Package taskquery parses a compact task-list query string into a gtd.TaskFilter.
Package taskquery parses a compact task-list query string into a gtd.TaskFilter.
tui
cmds
Package cmds holds small tea.Cmd helpers shared across the TUI.
Package cmds holds small tea.Cmd helpers shared across the TUI.
components/form
Package form provides a small, synchronous form toolkit for TUI overlays.
Package form provides a small, synchronous form toolkit for TUI overlays.
components/form/datefield
Package datefield is a single-line date/time field for use in a form.Form.
Package datefield is a single-line date/time field for use in a form.Form.
components/form/inputfield
Package inputfield is a single-line text field for use in a form.Form.
Package inputfield is a single-line text field for use in a form.Form.
components/form/radiofield
Package radiofield is an inline single-select field for use in a form.Form.
Package radiofield is an inline single-select field for use in a form.Form.
components/form/savefield
Package savefield is a terminal "[ Save ]" button for use as the last field in a form.Form.
Package savefield is a terminal "[ Save ]" button for use as the last field in a form.Form.
components/form/selectfield
Package selectfield is a single-select field for use in a form.Form.
Package selectfield is a single-select field for use in a form.Form.
components/form/textfield
Package textfield is a multi-line text field for use in a form.Form.
Package textfield is a multi-line text field for use in a form.Form.
components/querybar
Package querybar provides a reusable single-line query bar component for TUI screens.
Package querybar provides a reusable single-line query bar component for TUI screens.
internal/keymap
Package keymap is the stack-wide source of truth for keybinding ownership across priority-ordered layers (field → form → overlay → app).
Package keymap is the stack-wide source of truth for keybinding ownership across priority-ordered layers (field → form → overlay → app).
pages/inbox/clarify
Package clarify walks the GTD clarify decision tree for a single inbox item.
Package clarify walks the GTD clarify decision tree for a single inbox item.
pages/inbox/clarify/doitnow
Package doitnow renders the GTD do-it-now confirmation prompt: a small overlay shown after the wizard creates an open task that the user said they would do immediately.
Package doitnow renders the GTD do-it-now confirmation prompt: a small overlay shown after the wizard creates an open task that the user said they would do immediately.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL