diff --git a/cmd/note_edit.go b/cmd/note_edit.go index 3a77b7f..9559528 100644 --- a/cmd/note_edit.go +++ b/cmd/note_edit.go @@ -5,6 +5,8 @@ import ( "strconv" "time" + tea "github.com/charmbracelet/bubbletea" + "github.com/deadpyxel/workday/cmd/ui" "github.com/deadpyxel/workday/internal/journal" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -48,6 +50,22 @@ func editNoteInCurrentDay(cmd *cobra.Command, args []string) error { return fmt.Errorf("The index provided is not valid for the existing notes: %d", noteIdx) } + p := tea.NewProgram(ui.NewEditNoteModel(&entries[idx])) + if _, err := p.Run(); err != nil { + return err + } + + editState := ui.NewEditNoteState(&entries[idx].Notes[noteIdx]) + p = tea.NewProgram(editState) + if _, err := p.Run(); err != nil { + return err + } + + if !editState.Finished { + fmt.Println("Edit cancelled.") + return nil + } + entries[idx].Notes[noteIdx] = journal.Note{Contents: newNote} err = journal.SaveEntries(entries, journalPath) diff --git a/cmd/ui/models.go b/cmd/ui/models.go new file mode 100644 index 0000000..cfc42d8 --- /dev/null +++ b/cmd/ui/models.go @@ -0,0 +1,41 @@ +package ui + +import ( + "strings" + + "github.com/charmbracelet/bubbles/textinput" + "github.com/deadpyxel/workday/internal/journal" +) + +type EditNoteModel struct { + choices []journal.Note + cursor int + selected map[int]struct{} +} + +func NewEditNoteModel(entry *journal.JournalEntry) *EditNoteModel { + return &EditNoteModel{choices: entry.Notes, selected: make(map[int]struct{})} +} + +type EditNoteState struct { + Note *journal.Note + NewNote textinput.Model + NewTags textinput.Model + Finished bool +} + +func NewEditNoteState(note *journal.Note) *EditNoteState { + contentInput := textinput.New() + contentInput.SetValue(note.Contents) + contentInput.Placeholder = "Enter note contents" + + tagsInput := textinput.New() + tagsInput.SetValue(strings.Join(note.Tags, ",")) + tagsInput.Placeholder = "Enter tags, comma separated" + + return &EditNoteState{ + Note: note, + NewNote: contentInput, + NewTags: tagsInput, + } +} diff --git a/cmd/ui/views.go b/cmd/ui/views.go new file mode 100644 index 0000000..a00aa01 --- /dev/null +++ b/cmd/ui/views.go @@ -0,0 +1,108 @@ +package ui + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +func (m EditNoteModel) Init() tea.Cmd { + return nil +} + +func (m EditNoteModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + // Is it a key press? + case tea.KeyMsg: + + // Cool, what was the actual key pressed? + switch msg.String() { + + // These keys should exit the program. + case "ctrl+c", "q": + return m, tea.Quit + + // The "up" and "k" keys move the cursor up + case "up", "k": + if m.cursor > 0 { + m.cursor-- + } + + // The "down" and "j" keys move the cursor down + case "down", "j": + if m.cursor < len(m.choices)-1 { + m.cursor++ + } + + // The "enter" key and the spacebar (a literal space) toggle + // the selected state for the item that the cursor is pointing at. + case "enter", " ": + _, ok := m.selected[m.cursor] + if ok { + delete(m.selected, m.cursor) + } else { + m.selected[m.cursor] = struct{}{} + } + } + } + + // Return the updated model to the Bubble Tea runtime for processing. + // Note that we're not returning a command. + return m, nil +} + +func (m EditNoteModel) View() string { + s := "Which Note do you want to edit?\n\n" + for i, note := range m.choices { + // Is the cursor pointing at this choice? + cursor := " " // No cursor + if m.cursor == i { + cursor = ">" // Render cursor + } + + // Is this choice selected? + checked := " " + if _, ok := m.selected[i]; ok { + checked = "x" + } + + // Render the row + s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, ¬e) + } + + s += "\nPress q to quit.\n" + + return s +} + +func (s *EditNoteState) Init() tea.Cmd { + return textinput.Blink +} + +func (s *EditNoteState) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return s, tea.Quit + case "enter": + s.Finished = true + return s, tea.Quit + } + default: + var cmd tea.Cmd + s.NewNote, cmd = s.NewNote.Update(msg) + // s.NewTags, cmd = s.NewTags.Update(msg) + return s, cmd + } + return s, nil +} + +func (s *EditNoteState) View() string { + if s.Finished { + return "" + } + return fmt.Sprintf("Editing note:\n\n%s\n\nTags: %s\n\nPress Enter to save, Ctrl+C to cancel.", s.NewNote.View(), s.NewTags.View()) +} diff --git a/internal/journal/utils_test.go b/internal/journal/utils_test.go index 13ef59c..d16604e 100644 --- a/internal/journal/utils_test.go +++ b/internal/journal/utils_test.go @@ -125,7 +125,7 @@ func TestCalculateTotalTime(t *testing.T) { t.Errorf("Expected %v, but got %v", expected, result) } }) - t.Run("When slice contains valid entries, returns expected result with no errors", func(t *testing.T) { + t.Run("When slice contains valid entries returns expected result with no errors", func(t *testing.T) { entries := []JournalEntry{ {StartTime: time.Date(2021, time.January, 1, 10, 0, 0, 0, time.UTC), EndTime: time.Date(2021, time.January, 1, 12, 0, 0, 0, time.UTC)}, {StartTime: time.Date(2021, time.January, 1, 14, 0, 0, 0, time.UTC), EndTime: time.Date(2021, time.January, 1, 16, 0, 0, 0, time.UTC)}, @@ -140,7 +140,7 @@ func TestCalculateTotalTime(t *testing.T) { } }) - t.Run("When slice contains invalid entries, returns 0 with error", func(t *testing.T) { + t.Run("When slice contains invalid entries returns 0 with error", func(t *testing.T) { entries := []JournalEntry{ {StartTime: time.Date(2021, time.January, 1, 10, 0, 0, 0, time.UTC), EndTime: time.Date(2021, time.January, 1, 9, 0, 0, 0, time.UTC)}, }