diff options
author | T <t@tjp.lol> | 2025-08-13 14:47:24 -0600 |
---|---|---|
committer | T <t@tjp.lol> | 2025-08-13 14:47:55 -0600 |
commit | d6781f3e5b431057c23b2deaa943f273699e37f5 (patch) | |
tree | 269ef61bc44750881068ebd94d643a9b3a57c20c | |
parent | 389b72e55b04ccfc02b04eb81cb8f7bb7a5c8b59 (diff) |
client and project modal field suggestions, ctrl+n to accept
-rw-r--r-- | internal/tui/app.go | 14 | ||||
-rw-r--r-- | internal/tui/commands.go | 9 | ||||
-rw-r--r-- | internal/tui/form.go | 45 | ||||
-rw-r--r-- | internal/tui/history_box.go | 25 | ||||
-rw-r--r-- | internal/tui/keys.go | 37 | ||||
-rw-r--r-- | internal/tui/modal.go | 5 | ||||
-rw-r--r-- | internal/tui/shared.go | 6 |
7 files changed, 128 insertions, 13 deletions
diff --git a/internal/tui/app.go b/internal/tui/app.go index 34310bd..433a5ad 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -153,6 +153,12 @@ func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.historyBox.changeSelection(msg.Forward) } + case selectionToEnd: + switch m.selectedBox { + case HistoryBox: + m.historyBox.changeSelectionToEnd(msg.Top) + } + case drillDownMsg: if m.selectedBox == HistoryBox { m.historyBox.drillDown() @@ -176,7 +182,7 @@ func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case openDeleteConfirmation: if m.selectedBox == HistoryBox && m.historyBox.viewLevel == HistoryLevelDetails { - m.modalBox.activate(ModalTypeDeleteConfirmation, m.historyBox.selectedEntry().ID) + m.modalBox.activate(ModalTypeDeleteConfirmation, m.historyBox.selectedEntry().ID, m) } case recheckBounds: @@ -186,7 +192,7 @@ func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case openCreateClientModal: - m.modalBox.activate(ModalTypeClient, 0) + m.modalBox.activate(ModalTypeClient, 0, m) m.modalBox.form.fields[0].Focus() case openCreateProjectModal: @@ -205,7 +211,7 @@ func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *AppModel) openEntryEditor() { - m.modalBox.activate(ModalTypeEntry, m.historyBox.selectedEntry().ID) + m.modalBox.activate(ModalTypeEntry, m.historyBox.selectedEntry().ID, *m) m.modalBox.form.fields[0].Focus() entry := m.historyBox.selectedEntry() @@ -236,7 +242,7 @@ func (m *AppModel) openEntryEditor() { } func (m *AppModel) openHistoryFilterModal() { - m.modalBox.activate(ModalTypeHistoryFilter, 0) + m.modalBox.activate(ModalTypeHistoryFilter, 0, *m) m.modalBox.form.fields[0].Focus() // Pre-populate form with current filter values diff --git a/internal/tui/commands.go b/internal/tui/commands.go index 040df4b..aa5cc79 100644 --- a/internal/tui/commands.go +++ b/internal/tui/commands.go @@ -11,6 +11,7 @@ import ( type ( navigationMsg struct{ Forward bool } selectionMsg struct{ Forward bool } + selectionToEnd struct{ Top bool } drillDownMsg struct{} drillUpMsg struct{} modalClosed struct{} @@ -77,6 +78,14 @@ func changeSelection(forward bool) tea.Cmd { return func() tea.Msg { return selectionMsg{forward} } } +func changeSelectionToTop() tea.Cmd { + return func() tea.Msg { return selectionToEnd{true} } +} + +func changeSelectionToBottom() tea.Cmd { + return func() tea.Msg { return selectionToEnd{false} } +} + func closeModal() tea.Cmd { return func() tea.Msg { return modalClosed{} } } diff --git a/internal/tui/form.go b/internal/tui/form.go index 7dae012..3a1e5f6 100644 --- a/internal/tui/form.go +++ b/internal/tui/form.go @@ -13,9 +13,18 @@ import ( "github.com/charmbracelet/lipgloss" ) +type suggestionType int + +const ( + noSuggestions suggestionType = iota + suggestClients + suggestProjects +) + type FormField struct { textinput.Model - label string + label string + suggestions suggestionType } func (ff FormField) Update(msg tea.Msg) (FormField, tea.Cmd) { @@ -117,8 +126,8 @@ func NewEntryEditorForm() Form { form := NewForm([]FormField{ newTimestampField("Start time"), newOptionalTimestampField("End time"), - {Model: textinput.New(), label: "Client"}, - {Model: textinput.New(), label: "Project"}, + {Model: textinput.New(), label: "Client", suggestions: suggestClients}, + {Model: textinput.New(), label: "Project", suggestions: suggestProjects}, {Model: textinput.New(), label: "Description"}, newOptionalFloatField("Hourly Rate"), }) @@ -141,7 +150,7 @@ func NewClientForm() Form { func NewProjectForm() Form { form := NewForm([]FormField{ {Model: textinput.New(), label: "Name"}, - {Model: textinput.New(), label: "Client"}, + {Model: textinput.New(), label: "Client", suggestions: suggestClients}, newOptionalFloatField("Hourly Rate"), }) form.SelectedStyle = &modalFocusedInputStyle @@ -152,8 +161,8 @@ func NewProjectForm() Form { func NewHistoryFilterForm() Form { form := NewForm([]FormField{ newDateRangeField("Date Range"), - {Model: textinput.New(), label: "Client (optional)"}, - {Model: textinput.New(), label: "Project (optional)"}, + {Model: textinput.New(), label: "Client (optional)", suggestions: suggestClients}, + {Model: textinput.New(), label: "Project (optional)", suggestions: suggestProjects}, }) form.SelectedStyle = &modalFocusedInputStyle form.UnselectedStyle = &modalBlurredInputStyle @@ -212,6 +221,30 @@ func (ff Form) View() string { return content } +func (f *Form) SetSuggestions(m AppModel) { + for i := range f.fields { + ff := &f.fields[i] + switch ff.suggestions { + case suggestClients: + clients := make([]string, len(m.projectsBox.clients)) + for i, cl := range m.projectsBox.clients { + clients[i] = cl.Name + } + ff.SetSuggestions(clients) + ff.ShowSuggestions = true + case suggestProjects: + projNames := make([]string, 0, 10) + for _, cl := range m.projectsBox.clients { + for _, proj := range m.projectsBox.projects[cl.ID] { + projNames = append(projNames, proj.Name) + } + } + ff.SetSuggestions(projNames) + ff.ShowSuggestions = true + } + } +} + var ( modalFocusedInputStyle = lipgloss.NewStyle(). Border(lipgloss.DoubleBorder()). diff --git a/internal/tui/history_box.go b/internal/tui/history_box.go index c5c045e..01ea59b 100644 --- a/internal/tui/history_box.go +++ b/internal/tui/history_box.go @@ -401,6 +401,15 @@ func (m *HistoryBoxModel) changeSelection(forward bool) { } } +func (m *HistoryBoxModel) changeSelectionToEnd(top bool) { + switch m.viewLevel { + case HistoryLevelSummary: + m.changeSummarySelectionToEnd(top) + case HistoryLevelDetails: + m.changeDetailsSelectionToEnd(top) + } +} + func (m *HistoryBoxModel) changeSummarySelection(forward bool) { newIdx := m.summarySelection if forward { @@ -416,6 +425,14 @@ func (m *HistoryBoxModel) changeSummarySelection(forward bool) { } } +func (m *HistoryBoxModel) changeSummarySelectionToEnd(top bool) { + if top { + m.summarySelection = 0 + } else { + m.summarySelection = len(m.summaryItems) - 1 + } +} + func (m *HistoryBoxModel) changeDetailsSelection(forward bool) { newIdx := m.detailSelection entries := m.selectedEntries() @@ -432,6 +449,14 @@ func (m *HistoryBoxModel) changeDetailsSelection(forward bool) { } } +func (m *HistoryBoxModel) changeDetailsSelectionToEnd(top bool) { + if top { + m.detailSelection = 0 + } else { + m.detailSelection = len(m.selectedEntries()) - 1 + } +} + func (m HistoryBoxModel) selectedEntry() queries.TimeEntry { if m.viewLevel != HistoryLevelDetails { panic("fetching selected entry in history summary level") diff --git a/internal/tui/keys.go b/internal/tui/keys.go index c2f2271..3a7242d 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -164,6 +164,16 @@ var Bindings map[KeyBindingScope]map[string]KeyBinding = map[KeyBindingScope]map Description: func(AppModel) string { return "Filter" }, Result: func(*AppModel) tea.Cmd { return createHistoryFilterModal() }, }, + "g": KeyBinding{ + Key: "g", + Description: func(AppModel) string { return "Top" }, + Result: func(*AppModel) tea.Cmd { return changeSelectionToTop() }, + }, + "G": KeyBinding{ + Key: "G", + Description: func(AppModel) string { return "Bottom" }, + Result: func(*AppModel) tea.Cmd { return changeSelectionToBottom() }, + }, }, ScopeHistoryBoxDetails: { "j": KeyBinding{ @@ -214,6 +224,16 @@ var Bindings map[KeyBindingScope]map[string]KeyBinding = map[KeyBindingScope]map Result: func(*AppModel) tea.Cmd { return backToHistorySummary() }, Hide: true, }, + "g": KeyBinding{ + Key: "g", + Description: func(AppModel) string { return "Top" }, + Result: func(*AppModel) tea.Cmd { return changeSelectionToTop() }, + }, + "G": KeyBinding{ + Key: "G", + Description: func(AppModel) string { return "Bottom" }, + Result: func(*AppModel) tea.Cmd { return changeSelectionToBottom() }, + }, }, ScopeModal: { "enter": KeyBinding{ @@ -231,6 +251,23 @@ var Bindings map[KeyBindingScope]map[string]KeyBinding = map[KeyBindingScope]map Description: func(AppModel) string { return "Close" }, Result: func(*AppModel) tea.Cmd { return closeModal() }, }, + "ctrl+n": KeyBinding{ + Key: "Ctrl+n", + Description: func(m AppModel) string { + if m.modalBox.form.fields[m.modalBox.form.selIdx].suggestions == noSuggestions { + return "" + } else { + return "Accept Suggestion" + } + }, + Result: func(m *AppModel) tea.Cmd { + field := &m.modalBox.form.fields[m.modalBox.form.selIdx] + sugg := field.CurrentSuggestion() + field.SetValue(sugg) + field.CursorEnd() + return nil + }, + }, }, } diff --git a/internal/tui/modal.go b/internal/tui/modal.go index 8277077..badc658 100644 --- a/internal/tui/modal.go +++ b/internal/tui/modal.go @@ -116,7 +116,7 @@ func (m ModalBoxModel) RenderDeleteConfirmation() string { } func (m *ModalBoxModel) activateCreateProjectModal(am AppModel) { - m.activate(ModalTypeProject, 0) + m.activate(ModalTypeProject, 0, am) if am.selectedBox == ProjectsBox && len(am.projectsBox.clients) > 0 { client := am.projectsBox.clients[am.projectsBox.selectedClient] m.form.fields[1].SetValue(client.Name) @@ -124,11 +124,12 @@ func (m *ModalBoxModel) activateCreateProjectModal(am AppModel) { m.form.fields[0].Focus() } -func (m *ModalBoxModel) activate(t ModalType, editedID int64) { +func (m *ModalBoxModel) activate(t ModalType, editedID int64, am AppModel) { m.Active = true m.Type = t m.form = t.newForm() m.editedID = editedID + m.form.SetSuggestions(am) } func (m *ModalBoxModel) deactivate() { diff --git a/internal/tui/shared.go b/internal/tui/shared.go index 769d367..7ef7772 100644 --- a/internal/tui/shared.go +++ b/internal/tui/shared.go @@ -166,11 +166,15 @@ func RenderBottomBar(m AppModel, bindings []KeyBinding, err error) string { if binding.Hide { continue } + desc := binding.Description(m) + if desc == "" { + continue + } if i > 0 { content += sepStyle.Render(" ") } content += keyStyle.Render(fmt.Sprintf("[%s]", binding.Key)) - content += descStyle.Render(fmt.Sprintf(" %s", binding.Description(m))) + content += descStyle.Render(fmt.Sprintf(" %s", desc)) } if err != nil { |