1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package note
6
7import (
8 "encoding/json"
9 "errors"
10 "fmt"
11
12 "git.secluded.site/go-lunatask"
13 "git.secluded.site/lune/internal/client"
14 "git.secluded.site/lune/internal/completion"
15 "git.secluded.site/lune/internal/config"
16 "git.secluded.site/lune/internal/ui"
17 "github.com/charmbracelet/lipgloss"
18 "github.com/charmbracelet/lipgloss/table"
19 "github.com/spf13/cobra"
20)
21
22// ListCmd lists notes. Exported for potential use by shortcuts.
23var ListCmd = &cobra.Command{
24 Use: "list",
25 Short: "List notes",
26 Long: `List notes from Lunatask.
27
28Note: Due to end-to-end encryption, note names and content
29are not available through the API. Only metadata is shown.`,
30 RunE: runList,
31}
32
33func init() {
34 ListCmd.Flags().StringP("notebook", "b", "", "Filter by notebook key")
35 ListCmd.Flags().String("source", "", "Filter by source")
36 ListCmd.Flags().String("source-id", "", "Filter by source ID")
37 ListCmd.Flags().Bool("json", false, "Output as JSON")
38
39 _ = ListCmd.RegisterFlagCompletionFunc("notebook", completion.Notebooks)
40}
41
42func runList(cmd *cobra.Command, _ []string) error {
43 apiClient, err := client.New()
44 if err != nil {
45 return err
46 }
47
48 opts := buildListOptions(cmd)
49
50 notes, err := ui.Spin("Fetching notesโฆ", func() ([]lunatask.Note, error) {
51 return apiClient.ListNotes(cmd.Context(), opts)
52 })
53 if err != nil {
54 return err
55 }
56
57 notebookID, err := resolveNotebookFilter(cmd)
58 if err != nil {
59 return err
60 }
61
62 if notebookID != "" {
63 notes = filterByNotebook(notes, notebookID)
64 }
65
66 if len(notes) == 0 {
67 fmt.Fprintln(cmd.OutOrStdout(), "No notes found")
68
69 return nil
70 }
71
72 if mustGetBoolFlag(cmd, "json") {
73 return outputJSON(cmd, notes)
74 }
75
76 return outputTable(cmd, notes)
77}
78
79func buildListOptions(cmd *cobra.Command) *lunatask.ListNotesOptions {
80 source, _ := cmd.Flags().GetString("source")
81 sourceID, _ := cmd.Flags().GetString("source-id")
82
83 if source == "" && sourceID == "" {
84 return nil
85 }
86
87 opts := &lunatask.ListNotesOptions{}
88 if source != "" {
89 opts.Source = &source
90 }
91
92 if sourceID != "" {
93 opts.SourceID = &sourceID
94 }
95
96 return opts
97}
98
99func resolveNotebookFilter(cmd *cobra.Command) (string, error) {
100 notebookKey := mustGetStringFlag(cmd, "notebook")
101 if notebookKey == "" {
102 return "", nil
103 }
104
105 cfg, err := config.Load()
106 if err != nil {
107 if errors.Is(err, config.ErrNotFound) {
108 return "", err
109 }
110
111 return "", err
112 }
113
114 notebook := cfg.NotebookByKey(notebookKey)
115 if notebook == nil {
116 return "", fmt.Errorf("%w: %s", ErrUnknownNotebook, notebookKey)
117 }
118
119 return notebook.ID, nil
120}
121
122func filterByNotebook(notes []lunatask.Note, notebookID string) []lunatask.Note {
123 filtered := make([]lunatask.Note, 0, len(notes))
124
125 for _, note := range notes {
126 if note.NotebookID != nil && *note.NotebookID == notebookID {
127 filtered = append(filtered, note)
128 }
129 }
130
131 return filtered
132}
133
134func mustGetStringFlag(cmd *cobra.Command, name string) string {
135 f := cmd.Flags().Lookup(name)
136 if f == nil {
137 panic("flag not defined: " + name)
138 }
139
140 return f.Value.String()
141}
142
143func mustGetBoolFlag(cmd *cobra.Command, name string) bool {
144 f := cmd.Flags().Lookup(name)
145 if f == nil {
146 panic("flag not defined: " + name)
147 }
148
149 return f.Value.String() == "true"
150}
151
152func outputJSON(cmd *cobra.Command, notes []lunatask.Note) error {
153 enc := json.NewEncoder(cmd.OutOrStdout())
154 enc.SetIndent("", " ")
155
156 if err := enc.Encode(notes); err != nil {
157 return fmt.Errorf("encoding JSON: %w", err)
158 }
159
160 return nil
161}
162
163func outputTable(cmd *cobra.Command, notes []lunatask.Note) error {
164 cfg, _ := config.Load()
165 rows := make([][]string, 0, len(notes))
166
167 for _, note := range notes {
168 notebook := "-"
169 if note.NotebookID != nil {
170 notebook = *note.NotebookID
171 if cfg != nil {
172 if nb := cfg.NotebookByID(*note.NotebookID); nb != nil {
173 notebook = nb.Key
174 }
175 }
176 }
177
178 dateOn := "-"
179 if note.DateOn != nil {
180 dateOn = ui.FormatDate(note.DateOn.Time)
181 }
182
183 pinned := ""
184 if note.Pinned {
185 pinned = "๐"
186 }
187
188 created := ui.FormatDate(note.CreatedAt)
189
190 rows = append(rows, []string{note.ID, notebook, dateOn, pinned, created})
191 }
192
193 tbl := table.New().
194 Headers("ID", "NOTEBOOK", "DATE", "๐", "CREATED").
195 Rows(rows...).
196 StyleFunc(func(row, col int) lipgloss.Style {
197 if row == table.HeaderRow {
198 return ui.TableHeaderStyle()
199 }
200
201 return lipgloss.NewStyle()
202 }).
203 Border(ui.TableBorder())
204
205 fmt.Fprintln(cmd.OutOrStdout(), tbl.Render())
206
207 return nil
208}