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 := 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.Bold
199			}
200
201			return lipgloss.NewStyle()
202		}).
203		Border(lipgloss.HiddenBorder())
204
205	fmt.Fprintln(cmd.OutOrStdout(), tbl.Render())
206
207	return nil
208}