summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/tui/app.go14
-rw-r--r--internal/tui/commands.go9
-rw-r--r--internal/tui/form.go45
-rw-r--r--internal/tui/history_box.go25
-rw-r--r--internal/tui/keys.go37
-rw-r--r--internal/tui/modal.go5
-rw-r--r--internal/tui/shared.go6
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 {