Detailed changes
@@ -5,34 +5,172 @@
package note
import (
+ "bufio"
+ "errors"
"fmt"
+ "os"
+ "strings"
+ "git.secluded.site/go-lunatask"
+ "git.secluded.site/lune/internal/client"
"git.secluded.site/lune/internal/completion"
+ "git.secluded.site/lune/internal/config"
+ "git.secluded.site/lune/internal/ui"
"github.com/spf13/cobra"
)
+// ErrUnknownNotebook indicates the specified notebook key was not found in config.
+var ErrUnknownNotebook = errors.New("unknown notebook key")
+
+// ErrNoInput indicates no input was provided on stdin.
+var ErrNoInput = errors.New("no input provided on stdin")
+
// AddCmd creates a new note. Exported for potential use by shortcuts.
var AddCmd = &cobra.Command{
Use: "add [NAME]",
Short: "Create a new note",
Long: `Create a new note in Lunatask.
+The note name is optional. Use flags to set additional properties.
Use "-" as content flag value to read from stdin.`,
- RunE: func(cmd *cobra.Command, args []string) error {
- // TODO: implement note creation
- name := ""
- if len(args) > 0 {
- name = args[0]
- }
- fmt.Fprintf(cmd.OutOrStdout(), "Creating note: %s (not yet implemented)\n", name)
-
- return nil
- },
+ RunE: runAdd,
}
func init() {
AddCmd.Flags().StringP("notebook", "b", "", "Notebook key (from config)")
AddCmd.Flags().StringP("content", "c", "", "Note content (use - for stdin)")
+ AddCmd.Flags().String("source", "", "Source identifier")
+ AddCmd.Flags().String("source-id", "", "Source ID")
_ = AddCmd.RegisterFlagCompletionFunc("notebook", completion.Notebooks)
}
+
+func runAdd(cmd *cobra.Command, args []string) error {
+ apiClient, err := client.New()
+ if err != nil {
+ return err
+ }
+
+ builder := apiClient.NewNote()
+
+ if len(args) > 0 {
+ name, err := resolveName(args[0])
+ if err != nil {
+ return err
+ }
+
+ builder.WithName(name)
+ }
+
+ if err := applyNotebook(cmd, builder); err != nil {
+ return err
+ }
+
+ if err := applyContent(cmd, builder); err != nil {
+ return err
+ }
+
+ applySource(cmd, builder)
+
+ note, err := builder.Create(cmd.Context())
+ if err != nil {
+ return err
+ }
+
+ if note == nil {
+ fmt.Fprintln(cmd.OutOrStdout(), ui.Warning.Render("Note already exists (duplicate source)"))
+
+ return nil
+ }
+
+ fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Created note: "+note.ID))
+
+ return nil
+}
+
+func resolveName(arg string) (string, error) {
+ if arg != "-" {
+ return arg, nil
+ }
+
+ scanner := bufio.NewScanner(os.Stdin)
+ if scanner.Scan() {
+ return strings.TrimSpace(scanner.Text()), nil
+ }
+
+ if err := scanner.Err(); err != nil {
+ return "", fmt.Errorf("reading stdin: %w", err)
+ }
+
+ return "", ErrNoInput
+}
+
+func applyNotebook(cmd *cobra.Command, builder *lunatask.NoteBuilder) error {
+ notebookKey, _ := cmd.Flags().GetString("notebook")
+ if notebookKey == "" {
+ cfg, err := config.Load()
+ if err != nil && !errors.Is(err, config.ErrNotFound) {
+ return err
+ }
+
+ if cfg != nil {
+ notebookKey = cfg.Defaults.Notebook
+ }
+ }
+
+ if notebookKey == "" {
+ return nil
+ }
+
+ cfg, err := config.Load()
+ if err != nil {
+ return err
+ }
+
+ notebook := cfg.NotebookByKey(notebookKey)
+ if notebook == nil {
+ return fmt.Errorf("%w: %s", ErrUnknownNotebook, notebookKey)
+ }
+
+ builder.InNotebook(notebook.ID)
+
+ return nil
+}
+
+func applyContent(cmd *cobra.Command, builder *lunatask.NoteBuilder) error {
+ content, _ := cmd.Flags().GetString("content")
+ if content == "" {
+ return nil
+ }
+
+ resolved, err := resolveContent(content)
+ if err != nil {
+ return err
+ }
+
+ builder.WithContent(resolved)
+
+ return nil
+}
+
+func resolveContent(content string) (string, error) {
+ if content != "-" {
+ return content, nil
+ }
+
+ data, err := os.ReadFile("/dev/stdin")
+ if err != nil {
+ return "", fmt.Errorf("reading stdin: %w", err)
+ }
+
+ return strings.TrimSpace(string(data)), nil
+}
+
+func applySource(cmd *cobra.Command, builder *lunatask.NoteBuilder) {
+ source, _ := cmd.Flags().GetString("source")
+ sourceID, _ := cmd.Flags().GetString("source-id")
+
+ if source != "" && sourceID != "" {
+ builder.FromSource(source, sourceID)
+ }
+}
@@ -7,6 +7,7 @@ package note
import (
"fmt"
+ "git.secluded.site/lune/internal/client"
"git.secluded.site/lune/internal/ui"
"git.secluded.site/lune/internal/validate"
"github.com/spf13/cobra"
@@ -16,29 +17,43 @@ import (
var DeleteCmd = &cobra.Command{
Use: "delete ID",
Short: "Delete a note",
- Args: cobra.ExactArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- id, err := validate.Reference(args[0])
- if err != nil {
- return err
- }
+ Long: `Delete a note from Lunatask.
+
+Accepts a UUID or lunatask:// deep link.`,
+ Args: cobra.ExactArgs(1),
+ RunE: runDelete,
+}
+
+func init() {
+ DeleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
+}
+
+func runDelete(cmd *cobra.Command, args []string) error {
+ id, err := validate.Reference(args[0])
+ if err != nil {
+ return err
+ }
- force, _ := cmd.Flags().GetBool("force")
- if !force {
- if !ui.Confirm(fmt.Sprintf("Delete note %s?", id)) {
- fmt.Fprintln(cmd.OutOrStdout(), "Cancelled")
+ force, _ := cmd.Flags().GetBool("force")
+ if !force {
+ if !ui.Confirm(fmt.Sprintf("Delete note %s?", id)) {
+ fmt.Fprintln(cmd.OutOrStdout(), "Cancelled")
- return nil
- }
+ return nil
}
+ }
- // TODO: implement note deletion
- fmt.Fprintf(cmd.OutOrStdout(), "Deleting note %s (not yet implemented)\n", id)
+ apiClient, err := client.New()
+ if err != nil {
+ return err
+ }
- return nil
- },
-}
+ note, err := apiClient.DeleteNote(cmd.Context(), id)
+ if err != nil {
+ return err
+ }
-func init() {
- DeleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
+ fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Deleted note: "+note.ID))
+
+ return nil
}
@@ -1,34 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package note
-
-import (
- "fmt"
-
- "git.secluded.site/lune/internal/validate"
- "github.com/spf13/cobra"
-)
-
-// GetCmd retrieves a note by ID. Exported for potential use by shortcuts.
-var GetCmd = &cobra.Command{
- Use: "get ID",
- Short: "Get a note by ID or reference",
- Args: cobra.ExactArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- id, err := validate.Reference(args[0])
- if err != nil {
- return err
- }
-
- // TODO: implement note get
- fmt.Fprintf(cmd.OutOrStdout(), "Getting note %s (not yet implemented)\n", id)
-
- return nil
- },
-}
-
-func init() {
- GetCmd.Flags().Bool("json", false, "Output as JSON")
-}
@@ -5,9 +5,17 @@
package note
import (
+ "encoding/json"
+ "errors"
"fmt"
+ "git.secluded.site/go-lunatask"
+ "git.secluded.site/lune/internal/client"
"git.secluded.site/lune/internal/completion"
+ "git.secluded.site/lune/internal/config"
+ "git.secluded.site/lune/internal/ui"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/lipgloss/table"
"github.com/spf13/cobra"
)
@@ -19,17 +27,180 @@ var ListCmd = &cobra.Command{
Note: Due to end-to-end encryption, note names and content
are not available through the API. Only metadata is shown.`,
- RunE: func(cmd *cobra.Command, _ []string) error {
- // TODO: implement note listing
- fmt.Fprintln(cmd.OutOrStdout(), "Note listing not yet implemented")
-
- return nil
- },
+ RunE: runList,
}
func init() {
ListCmd.Flags().StringP("notebook", "b", "", "Filter by notebook key")
+ ListCmd.Flags().String("source", "", "Filter by source")
+ ListCmd.Flags().String("source-id", "", "Filter by source ID")
ListCmd.Flags().Bool("json", false, "Output as JSON")
_ = ListCmd.RegisterFlagCompletionFunc("notebook", completion.Notebooks)
}
+
+func runList(cmd *cobra.Command, _ []string) error {
+ apiClient, err := client.New()
+ if err != nil {
+ return err
+ }
+
+ opts := buildListOptions(cmd)
+
+ notes, err := apiClient.ListNotes(cmd.Context(), opts)
+ if err != nil {
+ return err
+ }
+
+ notebookID, err := resolveNotebookFilter(cmd)
+ if err != nil {
+ return err
+ }
+
+ if notebookID != "" {
+ notes = filterByNotebook(notes, notebookID)
+ }
+
+ if len(notes) == 0 {
+ fmt.Fprintln(cmd.OutOrStdout(), "No notes found")
+
+ return nil
+ }
+
+ if mustGetBoolFlag(cmd, "json") {
+ return outputJSON(cmd, notes)
+ }
+
+ return outputTable(cmd, notes)
+}
+
+func buildListOptions(cmd *cobra.Command) *lunatask.ListNotesOptions {
+ source, _ := cmd.Flags().GetString("source")
+ sourceID, _ := cmd.Flags().GetString("source-id")
+
+ if source == "" && sourceID == "" {
+ return nil
+ }
+
+ opts := &lunatask.ListNotesOptions{}
+ if source != "" {
+ opts.Source = &source
+ }
+
+ if sourceID != "" {
+ opts.SourceID = &sourceID
+ }
+
+ return opts
+}
+
+func resolveNotebookFilter(cmd *cobra.Command) (string, error) {
+ notebookKey := mustGetStringFlag(cmd, "notebook")
+ if notebookKey == "" {
+ return "", nil
+ }
+
+ cfg, err := config.Load()
+ if err != nil {
+ if errors.Is(err, config.ErrNotFound) {
+ return "", err
+ }
+
+ return "", err
+ }
+
+ notebook := cfg.NotebookByKey(notebookKey)
+ if notebook == nil {
+ return "", fmt.Errorf("%w: %s", ErrUnknownNotebook, notebookKey)
+ }
+
+ return notebook.ID, nil
+}
+
+func filterByNotebook(notes []lunatask.Note, notebookID string) []lunatask.Note {
+ filtered := make([]lunatask.Note, 0, len(notes))
+
+ for _, note := range notes {
+ if note.NotebookID != nil && *note.NotebookID == notebookID {
+ filtered = append(filtered, note)
+ }
+ }
+
+ return filtered
+}
+
+func mustGetStringFlag(cmd *cobra.Command, name string) string {
+ f := cmd.Flags().Lookup(name)
+ if f == nil {
+ panic("flag not defined: " + name)
+ }
+
+ return f.Value.String()
+}
+
+func mustGetBoolFlag(cmd *cobra.Command, name string) bool {
+ f := cmd.Flags().Lookup(name)
+ if f == nil {
+ panic("flag not defined: " + name)
+ }
+
+ return f.Value.String() == "true"
+}
+
+func outputJSON(cmd *cobra.Command, notes []lunatask.Note) error {
+ enc := json.NewEncoder(cmd.OutOrStdout())
+ enc.SetIndent("", " ")
+
+ if err := enc.Encode(notes); err != nil {
+ return fmt.Errorf("encoding JSON: %w", err)
+ }
+
+ return nil
+}
+
+func outputTable(cmd *cobra.Command, notes []lunatask.Note) error {
+ cfg, _ := config.Load()
+ rows := make([][]string, 0, len(notes))
+
+ for _, note := range notes {
+ notebook := "-"
+ if note.NotebookID != nil {
+ notebook = *note.NotebookID
+ if cfg != nil {
+ if nb := cfg.NotebookByID(*note.NotebookID); nb != nil {
+ notebook = nb.Key
+ }
+ }
+ }
+
+ dateOn := "-"
+ if note.DateOn != nil {
+ dateOn = ui.FormatDate(note.DateOn.Time)
+ }
+
+ pinned := ""
+ if note.Pinned {
+ pinned = "📌"
+ }
+
+ created := ui.FormatDate(note.CreatedAt)
+
+ rows = append(rows, []string{note.ID, notebook, dateOn, pinned, created})
+ }
+
+ tbl := table.New().
+ Headers("ID", "NOTEBOOK", "DATE", "📌", "CREATED").
+ Rows(rows...).
+ StyleFunc(func(row, col int) lipgloss.Style {
+ if row == table.HeaderRow {
+ return ui.Bold
+ }
+
+ return lipgloss.NewStyle()
+ }).
+ Border(lipgloss.HiddenBorder())
+
+ fmt.Fprintln(cmd.OutOrStdout(), tbl.Render())
+
+ return nil
+}
@@ -17,7 +17,7 @@ var Cmd = &cobra.Command{
func init() {
Cmd.AddCommand(AddCmd)
Cmd.AddCommand(ListCmd)
- Cmd.AddCommand(GetCmd)
+ Cmd.AddCommand(ShowCmd)
Cmd.AddCommand(UpdateCmd)
Cmd.AddCommand(DeleteCmd)
}
@@ -0,0 +1,118 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package note
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "git.secluded.site/go-lunatask"
+ "git.secluded.site/lune/internal/client"
+ "git.secluded.site/lune/internal/config"
+ "git.secluded.site/lune/internal/deeplink"
+ "git.secluded.site/lune/internal/ui"
+ "git.secluded.site/lune/internal/validate"
+ "github.com/spf13/cobra"
+)
+
+// ShowCmd displays a note by ID. Exported for potential use by shortcuts.
+var ShowCmd = &cobra.Command{
+ Use: "show ID",
+ Short: "Show note details",
+ Long: `Show detailed information for a note.
+
+Accepts a UUID or lunatask:// deep link.
+
+Note: Due to end-to-end encryption, note name and content
+are not available through the API. Only metadata is shown.`,
+ Args: cobra.ExactArgs(1),
+ RunE: runShow,
+}
+
+func init() {
+ ShowCmd.Flags().Bool("json", false, "Output as JSON")
+}
+
+func runShow(cmd *cobra.Command, args []string) error {
+ id, err := validate.Reference(args[0])
+ if err != nil {
+ return err
+ }
+
+ apiClient, err := client.New()
+ if err != nil {
+ return err
+ }
+
+ note, err := apiClient.GetNote(cmd.Context(), id)
+ if err != nil {
+ return err
+ }
+
+ if mustGetBoolFlag(cmd, "json") {
+ return outputNoteJSON(cmd, note)
+ }
+
+ return printNoteDetails(cmd, note)
+}
+
+func outputNoteJSON(cmd *cobra.Command, note *lunatask.Note) error {
+ enc := json.NewEncoder(cmd.OutOrStdout())
+ enc.SetIndent("", " ")
+
+ if err := enc.Encode(note); err != nil {
+ return fmt.Errorf("encoding JSON: %w", err)
+ }
+
+ return nil
+}
+
+func printNoteDetails(cmd *cobra.Command, note *lunatask.Note) error {
+ link, _ := deeplink.Build(deeplink.Note, note.ID)
+
+ fmt.Fprintf(cmd.OutOrStdout(), "%s\n", ui.H1.Render("Note"))
+ fmt.Fprintf(cmd.OutOrStdout(), " ID: %s\n", note.ID)
+ fmt.Fprintf(cmd.OutOrStdout(), " Link: %s\n", link)
+
+ printNotebook(cmd, note)
+
+ if note.DateOn != nil {
+ fmt.Fprintf(cmd.OutOrStdout(), " Date: %s\n", ui.FormatDate(note.DateOn.Time))
+ }
+
+ if note.Pinned {
+ fmt.Fprintf(cmd.OutOrStdout(), " Pinned: yes\n")
+ }
+
+ if len(note.Sources) > 0 {
+ fmt.Fprintf(cmd.OutOrStdout(), " Sources:\n")
+
+ for _, src := range note.Sources {
+ fmt.Fprintf(cmd.OutOrStdout(), " - %s: %s\n", src.Source, src.SourceID)
+ }
+ }
+
+ fmt.Fprintf(cmd.OutOrStdout(), " Created: %s\n", ui.FormatDate(note.CreatedAt))
+ fmt.Fprintf(cmd.OutOrStdout(), " Updated: %s\n", ui.FormatDate(note.UpdatedAt))
+
+ return nil
+}
+
+func printNotebook(cmd *cobra.Command, note *lunatask.Note) {
+ if note.NotebookID == nil {
+ return
+ }
+
+ notebookDisplay := *note.NotebookID
+ cfg, _ := config.Load()
+
+ if cfg != nil {
+ if notebook := cfg.NotebookByID(*note.NotebookID); notebook != nil {
+ notebookDisplay = fmt.Sprintf("%s (%s)", notebook.Name, notebook.Key)
+ }
+ }
+
+ fmt.Fprintf(cmd.OutOrStdout(), " Notebook: %s\n", notebookDisplay)
+}
@@ -7,7 +7,12 @@ package note
import (
"fmt"
+ "git.secluded.site/go-lunatask"
+ "git.secluded.site/lune/internal/client"
"git.secluded.site/lune/internal/completion"
+ "git.secluded.site/lune/internal/config"
+ "git.secluded.site/lune/internal/dateutil"
+ "git.secluded.site/lune/internal/ui"
"git.secluded.site/lune/internal/validate"
"github.com/spf13/cobra"
)
@@ -16,18 +21,12 @@ import (
var UpdateCmd = &cobra.Command{
Use: "update ID",
Short: "Update a note",
- Args: cobra.ExactArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- id, err := validate.Reference(args[0])
- if err != nil {
- return err
- }
+ Long: `Update an existing note in Lunatask.
- // TODO: implement note update
- fmt.Fprintf(cmd.OutOrStdout(), "Updating note %s (not yet implemented)\n", id)
-
- return nil
- },
+Accepts a UUID or lunatask:// deep link.
+Only specified flags are modified; other fields remain unchanged.`,
+ Args: cobra.ExactArgs(1),
+ RunE: runUpdate,
}
func init() {
@@ -38,3 +37,111 @@ func init() {
_ = UpdateCmd.RegisterFlagCompletionFunc("notebook", completion.Notebooks)
}
+
+func runUpdate(cmd *cobra.Command, args []string) error {
+ id, err := validate.Reference(args[0])
+ if err != nil {
+ return err
+ }
+
+ apiClient, err := client.New()
+ if err != nil {
+ return err
+ }
+
+ builder := apiClient.NewNoteUpdate(id)
+
+ if err := applyUpdateName(cmd, builder); err != nil {
+ return err
+ }
+
+ if err := applyUpdateNotebook(cmd, builder); err != nil {
+ return err
+ }
+
+ if err := applyUpdateContent(cmd, builder); err != nil {
+ return err
+ }
+
+ if err := applyUpdateDate(cmd, builder); err != nil {
+ return err
+ }
+
+ note, err := builder.Update(cmd.Context())
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Updated note: "+note.ID))
+
+ return nil
+}
+
+func applyUpdateName(cmd *cobra.Command, builder *lunatask.NoteUpdateBuilder) error {
+ name, _ := cmd.Flags().GetString("name")
+ if name == "" {
+ return nil
+ }
+
+ resolved, err := resolveName(name)
+ if err != nil {
+ return err
+ }
+
+ builder.WithName(resolved)
+
+ return nil
+}
+
+func applyUpdateNotebook(cmd *cobra.Command, builder *lunatask.NoteUpdateBuilder) error {
+ notebookKey, _ := cmd.Flags().GetString("notebook")
+ if notebookKey == "" {
+ return nil
+ }
+
+ cfg, err := config.Load()
+ if err != nil {
+ return err
+ }
+
+ notebook := cfg.NotebookByKey(notebookKey)
+ if notebook == nil {
+ return fmt.Errorf("%w: %s", ErrUnknownNotebook, notebookKey)
+ }
+
+ builder.InNotebook(notebook.ID)
+
+ return nil
+}
+
+func applyUpdateContent(cmd *cobra.Command, builder *lunatask.NoteUpdateBuilder) error {
+ content, _ := cmd.Flags().GetString("content")
+ if content == "" {
+ return nil
+ }
+
+ resolved, err := resolveContent(content)
+ if err != nil {
+ return err
+ }
+
+ builder.WithContent(resolved)
+
+ return nil
+}
+
+func applyUpdateDate(cmd *cobra.Command, builder *lunatask.NoteUpdateBuilder) error {
+ dateStr, _ := cmd.Flags().GetString("date")
+ if dateStr == "" {
+ return nil
+ }
+
+ date, err := dateutil.Parse(dateStr)
+ if err != nil {
+ return err
+ }
+
+ builder.OnDate(date)
+
+ return nil
+}
@@ -223,3 +223,14 @@ func (c *Config) GoalByID(id string) *GoalMatch {
return nil
}
+
+// NotebookByID finds a notebook by its ID.
+func (c *Config) NotebookByID(id string) *Notebook {
+ for i := range c.Notebooks {
+ if c.Notebooks[i].ID == id {
+ return &c.Notebooks[i]
+ }
+ }
+
+ return nil
+}