list.go

  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}