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 := apiClient.ListNotes(cmd.Context(), opts)
51 if err != nil {
52 return err
53 }
54
55 notebookID, err := resolveNotebookFilter(cmd)
56 if err != nil {
57 return err
58 }
59
60 if notebookID != "" {
61 notes = filterByNotebook(notes, notebookID)
62 }
63
64 if len(notes) == 0 {
65 fmt.Fprintln(cmd.OutOrStdout(), "No notes found")
66
67 return nil
68 }
69
70 if mustGetBoolFlag(cmd, "json") {
71 return outputJSON(cmd, notes)
72 }
73
74 return outputTable(cmd, notes)
75}
76
77func buildListOptions(cmd *cobra.Command) *lunatask.ListNotesOptions {
78 source, _ := cmd.Flags().GetString("source")
79 sourceID, _ := cmd.Flags().GetString("source-id")
80
81 if source == "" && sourceID == "" {
82 return nil
83 }
84
85 opts := &lunatask.ListNotesOptions{}
86 if source != "" {
87 opts.Source = &source
88 }
89
90 if sourceID != "" {
91 opts.SourceID = &sourceID
92 }
93
94 return opts
95}
96
97func resolveNotebookFilter(cmd *cobra.Command) (string, error) {
98 notebookKey := mustGetStringFlag(cmd, "notebook")
99 if notebookKey == "" {
100 return "", nil
101 }
102
103 cfg, err := config.Load()
104 if err != nil {
105 if errors.Is(err, config.ErrNotFound) {
106 return "", err
107 }
108
109 return "", err
110 }
111
112 notebook := cfg.NotebookByKey(notebookKey)
113 if notebook == nil {
114 return "", fmt.Errorf("%w: %s", ErrUnknownNotebook, notebookKey)
115 }
116
117 return notebook.ID, nil
118}
119
120func filterByNotebook(notes []lunatask.Note, notebookID string) []lunatask.Note {
121 filtered := make([]lunatask.Note, 0, len(notes))
122
123 for _, note := range notes {
124 if note.NotebookID != nil && *note.NotebookID == notebookID {
125 filtered = append(filtered, note)
126 }
127 }
128
129 return filtered
130}
131
132func mustGetStringFlag(cmd *cobra.Command, name string) string {
133 f := cmd.Flags().Lookup(name)
134 if f == nil {
135 panic("flag not defined: " + name)
136 }
137
138 return f.Value.String()
139}
140
141func mustGetBoolFlag(cmd *cobra.Command, name string) bool {
142 f := cmd.Flags().Lookup(name)
143 if f == nil {
144 panic("flag not defined: " + name)
145 }
146
147 return f.Value.String() == "true"
148}
149
150func outputJSON(cmd *cobra.Command, notes []lunatask.Note) error {
151 enc := json.NewEncoder(cmd.OutOrStdout())
152 enc.SetIndent("", " ")
153
154 if err := enc.Encode(notes); err != nil {
155 return fmt.Errorf("encoding JSON: %w", err)
156 }
157
158 return nil
159}
160
161func outputTable(cmd *cobra.Command, notes []lunatask.Note) error {
162 cfg, _ := config.Load()
163 rows := make([][]string, 0, len(notes))
164
165 for _, note := range notes {
166 notebook := "-"
167 if note.NotebookID != nil {
168 notebook = *note.NotebookID
169 if cfg != nil {
170 if nb := cfg.NotebookByID(*note.NotebookID); nb != nil {
171 notebook = nb.Key
172 }
173 }
174 }
175
176 dateOn := "-"
177 if note.DateOn != nil {
178 dateOn = ui.FormatDate(note.DateOn.Time)
179 }
180
181 pinned := ""
182 if note.Pinned {
183 pinned = "📌"
184 }
185
186 created := ui.FormatDate(note.CreatedAt)
187
188 rows = append(rows, []string{note.ID, notebook, dateOn, pinned, created})
189 }
190
191 tbl := table.New().
192 Headers("ID", "NOTEBOOK", "DATE", "📌", "CREATED").
193 Rows(rows...).
194 StyleFunc(func(row, col int) lipgloss.Style {
195 if row == table.HeaderRow {
196 return ui.Bold
197 }
198
199 return lipgloss.NewStyle()
200 }).
201 Border(lipgloss.HiddenBorder())
202
203 fmt.Fprintln(cmd.OutOrStdout(), tbl.Render())
204
205 return nil
206}