1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package lunatask
6
7import (
8 "context"
9 "time"
10)
11
12// Task is a task in Lunatask. Name and Note are encrypted client-side
13// and will be null when read back from the API.
14type Task struct {
15 ID string `json:"id"`
16 AreaID *string `json:"area_id"`
17 GoalID *string `json:"goal_id"`
18 Name *string `json:"name"`
19 Note *string `json:"note"`
20 Status *TaskStatus `json:"status"`
21 PreviousStatus *TaskStatus `json:"previous_status"`
22 Estimate *int `json:"estimate"`
23 Priority *Priority `json:"priority"`
24 Progress *int `json:"progress"`
25 Motivation *Motivation `json:"motivation"`
26 Eisenhower *Eisenhower `json:"eisenhower"`
27 Sources []Source `json:"sources"`
28 ScheduledOn *Date `json:"scheduled_on"`
29 CompletedAt *time.Time `json:"completed_at"`
30 CreatedAt time.Time `json:"created_at"`
31 UpdatedAt time.Time `json:"updated_at"`
32}
33
34// createTaskRequest defines a new task for JSON serialization.
35type createTaskRequest struct {
36 Name string `json:"name"`
37 AreaID *string `json:"area_id,omitempty"`
38 GoalID *string `json:"goal_id,omitempty"`
39 Note *string `json:"note,omitempty"`
40 Status *TaskStatus `json:"status,omitempty"`
41 Motivation *Motivation `json:"motivation,omitempty"`
42 Estimate *int `json:"estimate,omitempty"`
43 Priority *Priority `json:"priority,omitempty"`
44 Eisenhower *Eisenhower `json:"eisenhower,omitempty"`
45 ScheduledOn *Date `json:"scheduled_on,omitempty"`
46 CompletedAt *time.Time `json:"completed_at,omitempty"`
47 Source *string `json:"source,omitempty"`
48 SourceID *string `json:"source_id,omitempty"`
49}
50
51// updateTaskRequest specifies which fields to change on a task.
52type updateTaskRequest struct {
53 Name *string `json:"name,omitempty"`
54 AreaID *string `json:"area_id,omitempty"`
55 GoalID *string `json:"goal_id,omitempty"`
56 Note *string `json:"note,omitempty"`
57 Status *TaskStatus `json:"status,omitempty"`
58 Motivation *Motivation `json:"motivation,omitempty"`
59 Estimate *int `json:"estimate,omitempty"`
60 Priority *Priority `json:"priority,omitempty"`
61 Eisenhower *Eisenhower `json:"eisenhower,omitempty"`
62 ScheduledOn *Date `json:"scheduled_on,omitempty"`
63 CompletedAt *time.Time `json:"completed_at,omitempty"`
64}
65
66// taskResponse wraps a single task from the API.
67type taskResponse struct {
68 Task Task `json:"task"`
69}
70
71// tasksResponse wraps a list of tasks from the API.
72type tasksResponse struct {
73 Tasks []Task `json:"tasks"`
74}
75
76// ListTasksOptions filters tasks by source integration.
77type ListTasksOptions struct {
78 Source *string
79 SourceID *string
80}
81
82// GetSource implements [SourceFilter].
83func (o *ListTasksOptions) GetSource() *string { return o.Source }
84
85// GetSourceID implements [SourceFilter].
86func (o *ListTasksOptions) GetSourceID() *string { return o.SourceID }
87
88// ListTasks returns all tasks, optionally filtered. Pass nil for all.
89func (c *Client) ListTasks(ctx context.Context, opts *ListTasksOptions) ([]Task, error) {
90 var filter SourceFilter
91 if opts != nil {
92 filter = opts
93 }
94
95 return list(ctx, c, "/tasks", filter, func(r tasksResponse) []Task { return r.Tasks })
96}
97
98// GetTask fetches a task by ID. Name and Note will be null (E2EE).
99func (c *Client) GetTask(ctx context.Context, taskID string) (*Task, error) {
100 return get(ctx, c, "/tasks", taskID, "task", func(r taskResponse) Task { return r.Task })
101}
102
103// DeleteTask removes a task and returns its final state.
104func (c *Client) DeleteTask(ctx context.Context, taskID string) (*Task, error) {
105 return del(ctx, c, "/tasks", taskID, "task", func(r taskResponse) Task { return r.Task })
106}
107
108// TaskBuilder constructs and creates a task via method chaining.
109//
110// task, err := client.NewTask("Review PR").
111// InArea(areaID).
112// WithStatus(lunatask.StatusNext).
113// WithEstimate(30).
114// Create(ctx)
115type TaskBuilder struct {
116 client *Client
117 req createTaskRequest
118 important *bool
119 urgent *bool
120}
121
122// NewTask starts building a task with the given name.
123func (c *Client) NewTask(name string) *TaskBuilder {
124 return &TaskBuilder{client: c, req: createTaskRequest{Name: name}}
125}
126
127// InArea assigns the task to an area. IDs are in the area's settings in the app.
128func (b *TaskBuilder) InArea(areaID string) *TaskBuilder {
129 b.req.AreaID = &areaID
130
131 return b
132}
133
134// InGoal assigns the task to a goal. IDs are in the goal's settings in the app.
135func (b *TaskBuilder) InGoal(goalID string) *TaskBuilder {
136 b.req.GoalID = &goalID
137
138 return b
139}
140
141// WithNote attaches a Markdown note to the task.
142func (b *TaskBuilder) WithNote(note string) *TaskBuilder {
143 b.req.Note = ¬e
144
145 return b
146}
147
148// WithStatus sets the workflow status.
149// Use one of the Status* constants (e.g., [StatusNext]).
150func (b *TaskBuilder) WithStatus(status TaskStatus) *TaskBuilder {
151 b.req.Status = &status
152
153 return b
154}
155
156// WithMotivation sets why this task matters.
157// Use one of the Motivation* constants (e.g., [MotivationMust]).
158func (b *TaskBuilder) WithMotivation(motivation Motivation) *TaskBuilder {
159 b.req.Motivation = &motivation
160
161 return b
162}
163
164// WithEstimate sets the expected duration in minutes (0–720).
165func (b *TaskBuilder) WithEstimate(minutes int) *TaskBuilder {
166 b.req.Estimate = &minutes
167
168 return b
169}
170
171// Priority sets the priority level. Use Priority* constants (e.g., [PriorityHigh]).
172func (b *TaskBuilder) Priority(p Priority) *TaskBuilder {
173 b.req.Priority = &p
174
175 return b
176}
177
178// WithEisenhower sets the Eisenhower matrix quadrant directly.
179// Prefer [TaskBuilder.Important] and [TaskBuilder.Urgent] for a more readable API.
180func (b *TaskBuilder) WithEisenhower(eisenhower Eisenhower) *TaskBuilder {
181 b.req.Eisenhower = &eisenhower
182
183 return b
184}
185
186// Important marks the task as important. Produces [EisenhowerDoLater] alone,
187// or [EisenhowerDoNow] when combined with [TaskBuilder.Urgent].
188func (b *TaskBuilder) Important() *TaskBuilder {
189 t := true
190 b.important = &t
191
192 return b
193}
194
195// NotImportant marks the task as not important. Use with [TaskBuilder.NotUrgent]
196// to explicitly set [EisenhowerEliminate], or with [TaskBuilder.Urgent] for [EisenhowerDelegate].
197// Calling neither Important nor NotImportant leaves Eisenhower unset.
198func (b *TaskBuilder) NotImportant() *TaskBuilder {
199 f := false
200 b.important = &f
201
202 return b
203}
204
205// Urgent marks the task as urgent. Produces [EisenhowerDelegate] alone,
206// or [EisenhowerDoNow] when combined with [TaskBuilder.Important].
207func (b *TaskBuilder) Urgent() *TaskBuilder {
208 t := true
209 b.urgent = &t
210
211 return b
212}
213
214// NotUrgent marks the task as not urgent. Use with [TaskBuilder.NotImportant]
215// to explicitly set [EisenhowerEliminate], or with [TaskBuilder.Important] for [EisenhowerDoLater].
216// Calling neither Urgent nor NotUrgent leaves Eisenhower unset.
217func (b *TaskBuilder) NotUrgent() *TaskBuilder {
218 f := false
219 b.urgent = &f
220
221 return b
222}
223
224// ScheduledOn sets when the task should appear on your schedule.
225func (b *TaskBuilder) ScheduledOn(date Date) *TaskBuilder {
226 b.req.ScheduledOn = &date
227
228 return b
229}
230
231// CompletedAt marks the task completed at a specific time.
232func (b *TaskBuilder) CompletedAt(t time.Time) *TaskBuilder {
233 b.req.CompletedAt = &t
234
235 return b
236}
237
238// FromSource tags the task with a free-form origin identifier, useful for
239// tracking tasks created by scripts or external integrations.
240func (b *TaskBuilder) FromSource(source, sourceID string) *TaskBuilder {
241 b.req.Source = &source
242 b.req.SourceID = &sourceID
243
244 return b
245}
246
247// Create sends the task to Lunatask. Returns (nil, nil) if a not-completed
248// task already exists in the same area with matching source/source_id.
249func (b *TaskBuilder) Create(ctx context.Context) (*Task, error) {
250 if b.important != nil || b.urgent != nil {
251 important := b.important != nil && *b.important
252 urgent := b.urgent != nil && *b.urgent
253 e := NewEisenhower(important, urgent)
254 b.req.Eisenhower = &e
255 }
256
257 return create(ctx, b.client, "/tasks", b.req, func(r taskResponse) Task { return r.Task })
258}
259
260// TaskUpdateBuilder constructs and updates a task via method chaining.
261// Only fields you set will be modified; others remain unchanged.
262//
263// task, err := client.NewTaskUpdate(taskID).
264// WithStatus(lunatask.StatusCompleted).
265// CompletedAt(time.Now()).
266// Update(ctx)
267type TaskUpdateBuilder struct {
268 client *Client
269 taskID string
270 req updateTaskRequest
271 important *bool
272 urgent *bool
273}
274
275// NewTaskUpdate starts building a task update for the given task ID.
276func (c *Client) NewTaskUpdate(taskID string) *TaskUpdateBuilder {
277 return &TaskUpdateBuilder{client: c, taskID: taskID}
278}
279
280// Name changes the task's name.
281func (b *TaskUpdateBuilder) Name(name string) *TaskUpdateBuilder {
282 b.req.Name = &name
283
284 return b
285}
286
287// InArea moves the task to an area. IDs are in the area's settings in the app.
288func (b *TaskUpdateBuilder) InArea(areaID string) *TaskUpdateBuilder {
289 b.req.AreaID = &areaID
290
291 return b
292}
293
294// InGoal moves the task to a goal. IDs are in the goal's settings in the app.
295func (b *TaskUpdateBuilder) InGoal(goalID string) *TaskUpdateBuilder {
296 b.req.GoalID = &goalID
297
298 return b
299}
300
301// WithNote replaces the task's Markdown note.
302func (b *TaskUpdateBuilder) WithNote(note string) *TaskUpdateBuilder {
303 b.req.Note = ¬e
304
305 return b
306}
307
308// WithStatus sets the workflow status.
309// Use one of the Status* constants (e.g., [StatusNext]).
310func (b *TaskUpdateBuilder) WithStatus(status TaskStatus) *TaskUpdateBuilder {
311 b.req.Status = &status
312
313 return b
314}
315
316// WithMotivation sets why this task matters.
317// Use one of the Motivation* constants (e.g., [MotivationMust]).
318func (b *TaskUpdateBuilder) WithMotivation(motivation Motivation) *TaskUpdateBuilder {
319 b.req.Motivation = &motivation
320
321 return b
322}
323
324// WithEstimate sets the expected duration in minutes (0–720).
325func (b *TaskUpdateBuilder) WithEstimate(minutes int) *TaskUpdateBuilder {
326 b.req.Estimate = &minutes
327
328 return b
329}
330
331// Priority sets the priority level. Use Priority* constants (e.g., [PriorityHigh]).
332func (b *TaskUpdateBuilder) Priority(p Priority) *TaskUpdateBuilder {
333 b.req.Priority = &p
334
335 return b
336}
337
338// WithEisenhower sets the Eisenhower matrix quadrant directly.
339// Prefer [TaskUpdateBuilder.Important] and [TaskUpdateBuilder.Urgent] for a more readable API.
340func (b *TaskUpdateBuilder) WithEisenhower(eisenhower Eisenhower) *TaskUpdateBuilder {
341 b.req.Eisenhower = &eisenhower
342
343 return b
344}
345
346// Important marks the task as important. Produces [EisenhowerDoLater] alone,
347// or [EisenhowerDoNow] when combined with [TaskUpdateBuilder.Urgent].
348func (b *TaskUpdateBuilder) Important() *TaskUpdateBuilder {
349 t := true
350 b.important = &t
351
352 return b
353}
354
355// NotImportant marks the task as not important. Use with [TaskUpdateBuilder.NotUrgent]
356// to explicitly set [EisenhowerEliminate], or with [TaskUpdateBuilder.Urgent] for [EisenhowerDelegate].
357// Calling neither Important nor NotImportant leaves Eisenhower unset.
358func (b *TaskUpdateBuilder) NotImportant() *TaskUpdateBuilder {
359 f := false
360 b.important = &f
361
362 return b
363}
364
365// Urgent marks the task as urgent. Produces [EisenhowerDelegate] alone,
366// or [EisenhowerDoNow] when combined with [TaskUpdateBuilder.Important].
367func (b *TaskUpdateBuilder) Urgent() *TaskUpdateBuilder {
368 t := true
369 b.urgent = &t
370
371 return b
372}
373
374// NotUrgent marks the task as not urgent. Use with [TaskUpdateBuilder.NotImportant]
375// to explicitly set [EisenhowerEliminate], or with [TaskUpdateBuilder.Important] for [EisenhowerDoLater].
376// Calling neither Urgent nor NotUrgent leaves Eisenhower unset.
377func (b *TaskUpdateBuilder) NotUrgent() *TaskUpdateBuilder {
378 f := false
379 b.urgent = &f
380
381 return b
382}
383
384// ScheduledOn sets when the task should appear on your schedule.
385func (b *TaskUpdateBuilder) ScheduledOn(date Date) *TaskUpdateBuilder {
386 b.req.ScheduledOn = &date
387
388 return b
389}
390
391// CompletedAt marks the task completed at a specific time.
392func (b *TaskUpdateBuilder) CompletedAt(t time.Time) *TaskUpdateBuilder {
393 b.req.CompletedAt = &t
394
395 return b
396}
397
398// Update sends the changes to Lunatask.
399func (b *TaskUpdateBuilder) Update(ctx context.Context) (*Task, error) {
400 if b.important != nil || b.urgent != nil {
401 important := b.important != nil && *b.important
402 urgent := b.urgent != nil && *b.urgent
403 e := NewEisenhower(important, urgent)
404 b.req.Eisenhower = &e
405 }
406
407 return update(ctx, b.client, "/tasks", b.taskID, "task", b.req, func(r taskResponse) Task { return r.Task })
408}