From 532d44256d1907570db03e8702992057e882207d Mon Sep 17 00:00:00 2001 From: Kevin Mulholland Date: Tue, 14 Apr 2026 10:02:40 -0400 Subject: [PATCH 1/2] feat: add copy entry ability --- internal/messages/messages.go | 4 +++ internal/ui/app.go | 6 ++++ internal/ui/components/entryform/entryform.go | 36 +++++++++++++++++++ internal/ui/components/help/keybindings.go | 5 +++ internal/ui/components/modal/modal.go | 19 ++++++++++ internal/ui/views/entries/entries.go | 9 +++++ 6 files changed, 79 insertions(+) diff --git a/internal/messages/messages.go b/internal/messages/messages.go index 91a9df1..e326c57 100644 --- a/internal/messages/messages.go +++ b/internal/messages/messages.go @@ -84,6 +84,10 @@ type EntryUpdatedMsg struct { Entry models.Entry } +type EntryCopyStartedMsg struct { + Entry models.Entry +} + // ===================================== // Modal messages // ===================================== diff --git a/internal/ui/app.go b/internal/ui/app.go index 88f4dcb..2e3d83d 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -350,6 +350,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.SetContent(m.renderContent()) return m, nil + case messages.EntryCopyStartedMsg: + m.showModal = true + m.modal = modal.CopyEntryForm(m.config, m.projects, msg.Entry) + m.viewport.SetContent(m.renderContent()) + return m, nil + case messages.EntryDeleteStartedMsg: m.showModal = true m.modal = modal.NewDeleteConfirmation(msg.EntryId) diff --git a/internal/ui/components/entryform/entryform.go b/internal/ui/components/entryform/entryform.go index 1243390..732a264 100644 --- a/internal/ui/components/entryform/entryform.go +++ b/internal/ui/components/entryform/entryform.go @@ -114,6 +114,42 @@ func New(cfg *config.Config, projects []models.Project) Model { } } +func (m Model) CopyEntry(entry models.Entry) Model { + m.editing = false + + m.selectedEntry = entry + + m.description.SetValue(entry.Description) + + // Setup Calendar + m.calendar.SetSelectedDay(entry.TimeInterval.Start.In(time.Local)) + + // Pre-fill time inputs + startStr := entry.TimeInterval.Start.In(time.Local).Format("3:04 PM") + endStr := entry.TimeInterval.End.In(time.Local).Format("3:04 PM") + m.timeStart.SetValue(startStr) + m.timeEnd.SetValue(endStr) + + // Find and select the project + for i, proj := range m.projects { + if proj.ID == entry.ProjectID { + m.selectedProj = proj + m.selectedProjID = i + break + } + } + + // Find the cursor position for the project + for i, proj := range m.projects { + if proj.ID == entry.ProjectID { + m.cursor = i + break + } + } + + return m +} + func (m Model) UpdateEntry(entry models.Entry) Model { m.editing = true diff --git a/internal/ui/components/help/keybindings.go b/internal/ui/components/help/keybindings.go index 55fed58..51d9622 100644 --- a/internal/ui/components/help/keybindings.go +++ b/internal/ui/components/help/keybindings.go @@ -49,6 +49,7 @@ var Global = GlobalKeyMap{ type EntryKeyMap struct { Delete key.Binding Edit key.Binding + Copy key.Binding New key.Binding Up key.Binding Down key.Binding @@ -86,6 +87,10 @@ var Entry = EntryKeyMap{ key.WithKeys("e"), key.WithHelp("e", "Edit entry"), ), + Copy: key.NewBinding( + key.WithKeys("c"), + key.WithHelp("c", "Copy entry"), + ), Delete: key.NewBinding( key.WithKeys("d"), key.WithHelp("d", "Delete entry"), diff --git a/internal/ui/components/modal/modal.go b/internal/ui/components/modal/modal.go index f65162c..ec12b36 100644 --- a/internal/ui/components/modal/modal.go +++ b/internal/ui/components/modal/modal.go @@ -71,6 +71,25 @@ func UpdateEntryForm(cfg *config.Config, projects []models.Project, entry models } } +func CopyEntryForm(cfg *config.Config, projects []models.Project, entry models.Entry) *Model { + form := entryform.New(cfg, projects) + form = form.CopyEntry(entry) + viewport := viewport.New(0, styles.ModalHeight) + viewport.SetContent(form.View()) + + if viewport.Height > viewport.TotalLineCount() { + viewport.Height = viewport.TotalLineCount() + viewport.SetContent(form.View()) + } + + return &Model{ + modalType: EntryModal, + entryForm: &form, + viewport: viewport, + title: "Copy Entry", + } +} + func NewDeleteConfirmation(entryId string) *Model { deleteConfirmation := confirmation.New(entryId, "entry") viewport := viewport.New(0, 4) diff --git a/internal/ui/views/entries/entries.go b/internal/ui/views/entries/entries.go index 817afff..4329839 100644 --- a/internal/ui/views/entries/entries.go +++ b/internal/ui/views/entries/entries.go @@ -81,6 +81,15 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return messages.EntryDeleteStartedMsg{EntryId: selectedEntry.ID} } } + case "c": + // Copy the selected entry + if len(m.entries) > 0 { + selectedEntry := m.entries[m.list.Index()] + // Open the edit modal + return m, func() tea.Msg { + return messages.EntryCopyStartedMsg{Entry: selectedEntry} + } + } } case messages.EntriesLoadedMsg: From 39418e9b5d8f3fb5183f305075675d13690a7517 Mon Sep 17 00:00:00 2001 From: Kevin Mulholland Date: Tue, 14 Apr 2026 10:04:34 -0400 Subject: [PATCH 2/2] chore: format files --- internal/ui/app.go | 2 +- internal/ui/views/entries/entries.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ui/app.go b/internal/ui/app.go index 2e3d83d..e186fdd 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -351,7 +351,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case messages.EntryCopyStartedMsg: - m.showModal = true + m.showModal = true m.modal = modal.CopyEntryForm(m.config, m.projects, msg.Entry) m.viewport.SetContent(m.renderContent()) return m, nil diff --git a/internal/ui/views/entries/entries.go b/internal/ui/views/entries/entries.go index 4329839..ab17159 100644 --- a/internal/ui/views/entries/entries.go +++ b/internal/ui/views/entries/entries.go @@ -82,7 +82,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } } case "c": - // Copy the selected entry + // Copy the selected entry if len(m.entries) > 0 { selectedEntry := m.entries[m.list.Index()] // Open the edit modal