From 21024133f5f3907fee0c40f0e257afaf74b894b1 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur <13575379+dhth@users.noreply.github.com> Date: Fri, 17 Jan 2025 20:50:11 +0530 Subject: [PATCH] refactor: simplify update file (#34) --- .github/workflows/build.yml | 2 - .github/workflows/test.yml | 44 ++ CHANGELOG.md | 33 ++ internal/persistence/queries.go | 4 +- internal/persistence/queries_test.go | 8 +- internal/ui/cmds.go | 22 +- internal/ui/handle.go | 623 +++++++++++++++++++++++++++ internal/ui/model.go | 17 +- internal/ui/msgs.go | 12 +- internal/ui/update.go | 605 ++++---------------------- internal/ui/view.go | 10 +- tests/test.sh | 82 ++++ 12 files changed, 899 insertions(+), 563 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 CHANGELOG.md create mode 100644 internal/ui/handle.go create mode 100755 tests/test.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce8a8ca..ec3ddcb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,8 +30,6 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: go build run: go build -v ./... - - name: go test - run: go test -v ./... - name: run hours run: | go build . diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f94cd26 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +name: test + +on: + push: + branches: ["main"] + pull_request: + paths: + - "go.*" + - "**/*.go" + - ".github/workflows/test.yml" + +env: + GO_VERSION: '1.23.4' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + - name: go test + run: go test -v ./... + + live: + needs: [test] + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + - name: go install + run: go install . + - name: Run live tests + run: | + cd tests + ./test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0584bde --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v0.3.0] - Jun 29, 2024 + +### Added + +- Timestamps in the "Task Log Entry" view can be moved forwards/backwards using + j/k/J/K +- The TUI now shows the start time of an active recording +- An active task log recording can now be cancelled (using ctrl+x) + +### Changed + +- Timestamps in "Task Log" view show up differently based on the end timestamp +- "active" subcommand supports a time placeholder, eg. hours active -t 'working + on {{task}} for {{time}}' + +## [v0.2.0] - Jun 21, 2024 + +### Added + +- Adds the ability to view reports/logs/stats interactively (using the + --interactive/-i flag) +- Adds the "gen" subcommand to allow new users of "hours" to generate dummy data + +[unreleased]: https://github.com/dhth/hours/compare/v0.3.0...HEAD +[v0.3.0]: https://github.com/dhth/hours/compare/v0.2.0...v0.3.0 +[v0.2.0]: https://github.com/dhth/hours/compare/v0.1.0...v0.2.0 diff --git a/internal/persistence/queries.go b/internal/persistence/queries.go index 73f6b05..22ee587 100644 --- a/internal/persistence/queries.go +++ b/internal/persistence/queries.go @@ -490,7 +490,7 @@ LIMIT ?; return tLE, nil } -func DeleteTaskLogEntry(db *sql.DB, entry *types.TaskLogEntry) error { +func DeleteTL(db *sql.DB, entry *types.TaskLogEntry) error { return runInTx(db, func(tx *sql.Tx) error { stmt, err := tx.Prepare(` DELETE from task_log @@ -587,7 +587,7 @@ WHERE id=?; return task, nil } -func fetchTaskLogByID(db *sql.DB, id int) (types.TaskLogEntry, error) { +func fetchTLByID(db *sql.DB, id int) (types.TaskLogEntry, error) { var tl types.TaskLogEntry row := db.QueryRow(` SELECT id, task_id, begin_ts, end_ts, secs_spent, comment diff --git a/internal/persistence/queries_test.go b/internal/persistence/queries_test.go index 88fa033..3a8dc75 100644 --- a/internal/persistence/queries_test.go +++ b/internal/persistence/queries_test.go @@ -76,7 +76,7 @@ func TestRepository(t *testing.T) { // THEN require.NoError(t, err, "failed to update task log") - taskLog, err := fetchTaskLogByID(testDB, tlID) + taskLog, err := fetchTLByID(testDB, tlID) require.NoError(t, err, "failed to fetch task log") taskAfter, err := fetchTaskByID(testDB, taskID) @@ -110,7 +110,7 @@ func TestRepository(t *testing.T) { // THEN require.NoError(t, err, "failed to insert task log") - taskLog, err := fetchTaskLogByID(testDB, tlID) + taskLog, err := fetchTLByID(testDB, tlID) require.NoError(t, err, "failed to fetch task log") taskAfter, err := fetchTaskByID(testDB, taskID) @@ -133,11 +133,11 @@ func TestRepository(t *testing.T) { taskBefore, err := fetchTaskByID(testDB, taskID) require.NoError(t, err, "failed to fetch task") numSecondsBefore := taskBefore.SecsSpent - taskLog, err := fetchTaskLogByID(testDB, tlID) + taskLog, err := fetchTLByID(testDB, tlID) require.NoError(t, err, "failed to fetch task log") // WHEN - err = DeleteTaskLogEntry(testDB, &taskLog) + err = DeleteTL(testDB, &taskLog) // THEN require.NoError(t, err, "failed to insert task log") diff --git a/internal/ui/cmds.go b/internal/ui/cmds.go index 020d771..c3e4163 100644 --- a/internal/ui/cmds.go +++ b/internal/ui/cmds.go @@ -66,10 +66,10 @@ func updateTLBeginTS(db *sql.DB, beginTS time.Time) tea.Cmd { } } -func insertManualEntry(db *sql.DB, taskID int, beginTS time.Time, endTS time.Time, comment string) tea.Cmd { +func insertManualTL(db *sql.DB, taskID int, beginTS time.Time, endTS time.Time, comment string) tea.Cmd { return func() tea.Msg { _, err := pers.InsertManualTL(db, taskID, beginTS, endTS, comment) - return manualTaskLogInserted{taskID, err} + return manualTLInsertedMsg{taskID, err} } } @@ -101,27 +101,27 @@ func updateTaskRep(db *sql.DB, t *types.Task) tea.Cmd { } } -func fetchTaskLogEntries(db *sql.DB) tea.Cmd { +func fetchTLS(db *sql.DB) tea.Cmd { return func() tea.Msg { entries, err := pers.FetchTLEntries(db, true, 50) - return taskLogEntriesFetchedMsg{ + return tLsFetchedMsg{ entries: entries, err: err, } } } -func deleteLogEntry(db *sql.DB, entry *types.TaskLogEntry) tea.Cmd { +func deleteTL(db *sql.DB, entry *types.TaskLogEntry) tea.Cmd { return func() tea.Msg { - err := pers.DeleteTaskLogEntry(db, entry) - return taskLogEntryDeletedMsg{ + err := pers.DeleteTL(db, entry) + return tLDeletedMsg{ entry: entry, err: err, } } } -func deleteActiveTaskLog(db *sql.DB) tea.Cmd { +func deleteActiveTL(db *sql.DB) tea.Cmd { return func() tea.Msg { err := pers.DeleteActiveTL(db) return activeTaskLogDeletedMsg{err} @@ -145,20 +145,20 @@ func updateTask(db *sql.DB, task *types.Task, summary string) tea.Cmd { func updateTaskActiveStatus(db *sql.DB, task *types.Task, active bool) tea.Cmd { return func() tea.Msg { err := pers.UpdateTaskActiveStatus(db, task.ID, active) - return taskActiveStatusUpdated{task, active, err} + return taskActiveStatusUpdatedMsg{task, active, err} } } func fetchTasks(db *sql.DB, active bool) tea.Cmd { return func() tea.Msg { tasks, err := pers.FetchTasks(db, active, 50) - return tasksFetched{tasks, active, err} + return tasksFetchedMsg{tasks, active, err} } } func hideHelp(interval time.Duration) tea.Cmd { return tea.Tick(interval, func(time.Time) tea.Msg { - return HideHelpMsg{} + return hideHelpMsg{} }) } diff --git a/internal/ui/handle.go b/internal/ui/handle.go new file mode 100644 index 0000000..53d7ca0 --- /dev/null +++ b/internal/ui/handle.go @@ -0,0 +1,623 @@ +package ui + +import ( + "fmt" + "strings" + "time" + + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/dhth/hours/internal/types" +) + +const ( + genericErrorMsg = "Something went wrong" + removeFilterMsg = "Remove filter first" +) + +func (m *Model) getCmdToCreateOrUpdateTask() tea.Cmd { + if strings.TrimSpace(m.taskInputs[summaryField].Value()) == "" { + m.message = "Task summary cannot be empty" + return nil + } + + var cmd tea.Cmd + switch m.taskMgmtContext { + case taskCreateCxt: + cmd = createTask(m.db, m.taskInputs[summaryField].Value()) + m.taskInputs[summaryField].SetValue("") + case taskUpdateCxt: + selectedTask, ok := m.activeTasksList.SelectedItem().(*types.Task) + if !ok { + m.message = "Something went wrong" + return nil + } + cmd = updateTask(m.db, selectedTask, m.taskInputs[summaryField].Value()) + m.taskInputs[summaryField].SetValue("") + } + + m.activeView = taskListView + return cmd +} + +func (m *Model) getCmdToUpdateActiveTL() tea.Cmd { + beginTS, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[entryBeginTS].Value(), time.Local) + if err != nil { + m.message = err.Error() + return nil + } + + m.trackingInputs[entryBeginTS].SetValue("") + m.activeView = taskListView + return updateTLBeginTS(m.db, beginTS) +} + +func (m *Model) getCmdToSaveActiveTL() tea.Cmd { + beginTS, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[entryBeginTS].Value(), time.Local) + if err != nil { + m.message = err.Error() + return nil + } + m.activeTLBeginTS = beginTS + + endTS, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[entryEndTS].Value(), time.Local) + if err != nil { + m.message = err.Error() + return nil + } + m.activeTLEndTS = endTS + + if m.activeTLEndTS.Sub(m.activeTLBeginTS).Seconds() <= 0 { + m.message = "time spent needs to be positive" + return nil + } + + if m.trackingInputs[entryComment].Value() == "" { + m.message = "Comment cannot be empty" + return nil + } + + comment := m.trackingInputs[entryComment].Value() + for i := range m.trackingInputs { + m.trackingInputs[i].SetValue("") + } + m.activeView = taskListView + return toggleTracking(m.db, m.activeTaskID, m.activeTLBeginTS, m.activeTLEndTS, comment) +} + +func (m *Model) getCmdToSaveOrUpdateTL() tea.Cmd { + beginTS, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[entryBeginTS].Value(), time.Local) + if err != nil { + m.message = err.Error() + return nil + } + + endTS, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[entryEndTS].Value(), time.Local) + if err != nil { + m.message = err.Error() + return nil + } + + if endTS.Sub(beginTS).Seconds() <= 0 { + m.message = "time spent needs to be positive" + return nil + } + + comment := m.trackingInputs[entryComment].Value() + + if len(comment) == 0 { + m.message = "Comment cannot be empty" + return nil + } + + task, ok := m.activeTasksList.SelectedItem().(*types.Task) + if !ok { + m.message = "Something went wrong" + return nil + } + if m.tasklogSaveType != tasklogInsert { + return nil + } + for i := range m.trackingInputs { + m.trackingInputs[i].SetValue("") + } + + m.activeView = taskListView + return insertManualTL(m.db, task.ID, beginTS, endTS, comment) +} + +func (m *Model) handleEscape() { + switch m.activeView { + case taskInputView: + m.activeView = taskListView + for i := range m.taskInputs { + m.taskInputs[i].SetValue("") + } + case editActiveTLView: + m.taskInputs[entryBeginTS].SetValue("") + m.activeView = taskListView + case saveActiveTLView: + m.activeView = taskListView + m.trackingInputs[entryComment].SetValue("") + case manualTasklogEntryView: + if m.tasklogSaveType == tasklogInsert { + m.activeView = taskListView + } + for i := range m.trackingInputs { + m.trackingInputs[i].SetValue("") + } + } +} + +func (m *Model) goForwardInView() { + switch m.activeView { + case taskListView: + m.activeView = taskLogView + case taskLogView: + m.activeView = inactiveTaskListView + case inactiveTaskListView: + m.activeView = taskListView + case saveActiveTLView, manualTasklogEntryView: + switch m.trackingFocussedField { + case entryBeginTS: + m.trackingFocussedField = entryEndTS + case entryEndTS: + m.trackingFocussedField = entryComment + case entryComment: + m.trackingFocussedField = entryBeginTS + } + for i := range m.trackingInputs { + m.trackingInputs[i].Blur() + } + m.trackingInputs[m.trackingFocussedField].Focus() + } +} + +func (m *Model) goBackwardInView() { + switch m.activeView { + case taskLogView: + m.activeView = taskListView + case taskListView: + m.activeView = inactiveTaskListView + case inactiveTaskListView: + m.activeView = taskLogView + case saveActiveTLView, manualTasklogEntryView: + switch m.trackingFocussedField { + case entryBeginTS: + m.trackingFocussedField = entryComment + case entryEndTS: + m.trackingFocussedField = entryBeginTS + case entryComment: + m.trackingFocussedField = entryEndTS + } + for i := range m.trackingInputs { + m.trackingInputs[i].Blur() + } + m.trackingInputs[m.trackingFocussedField].Focus() + } +} + +func (m *Model) shiftTime(direction types.TimeShiftDirection, duration types.TimeShiftDuration) error { + if m.activeView == editActiveTLView || m.activeView == saveActiveTLView || m.activeView == manualTasklogEntryView { + if m.trackingFocussedField == entryBeginTS || m.trackingFocussedField == entryEndTS { + ts, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[m.trackingFocussedField].Value(), time.Local) + if err != nil { + return err + } + + newTs := types.GetShiftedTime(ts, direction, duration) + + m.trackingInputs[m.trackingFocussedField].SetValue(newTs.Format(timeFormat)) + } + } + return nil +} + +func (m *Model) handleRequestToGoBackOrQuit() bool { + var shouldQuit bool + switch m.activeView { + case taskListView: + fs := m.activeTasksList.FilterState() + if fs == list.Filtering || fs == list.FilterApplied { + m.activeTasksList.ResetFilter() + } else { + shouldQuit = true + } + case taskLogView: + fs := m.taskLogList.FilterState() + if fs == list.Filtering || fs == list.FilterApplied { + m.taskLogList.ResetFilter() + } else { + m.activeView = taskListView + } + case inactiveTaskListView: + fs := m.inactiveTasksList.FilterState() + if fs == list.Filtering || fs == list.FilterApplied { + m.inactiveTasksList.ResetFilter() + } else { + m.activeView = taskListView + } + case helpView: + m.activeView = taskListView + default: + shouldQuit = true + } + + return shouldQuit +} + +func (m *Model) getCmdToReloadData() tea.Cmd { + var cmd tea.Cmd + switch m.activeView { + case taskListView: + cmd = fetchTasks(m.db, true) + case taskLogView: + cmd = fetchTLS(m.db) + m.taskLogList.ResetSelected() + case inactiveTaskListView: + cmd = fetchTasks(m.db, false) + m.inactiveTasksList.ResetSelected() + } + + return cmd +} + +func (m *Model) goToActiveTask() { + if m.activeView != taskListView { + return + } + + if !m.trackingActive { + m.message = "Nothing is being tracked right now" + return + } + + if m.activeTasksList.IsFiltered() { + m.activeTasksList.ResetFilter() + } + activeIndex, ok := m.activeTaskIndexMap[m.activeTaskID] + if !ok { + m.message = genericErrorMsg + return + } + + m.activeTasksList.Select(activeIndex) +} + +func (m *Model) handleRequestToSaveActiveTL() { + m.activeView = editActiveTLView + m.trackingFocussedField = entryBeginTS + m.trackingInputs[entryBeginTS].SetValue(m.activeTLBeginTS.Format(timeFormat)) + m.trackingInputs[m.trackingFocussedField].Focus() +} + +func (m *Model) handleRequestToCreateManualTL() { + m.activeView = manualTasklogEntryView + m.tasklogSaveType = tasklogInsert + m.trackingFocussedField = entryBeginTS + currentTime := time.Now() + currentTimeStr := currentTime.Format(timeFormat) + + m.trackingInputs[entryBeginTS].SetValue(currentTimeStr) + m.trackingInputs[entryEndTS].SetValue(currentTimeStr) + + for i := range m.trackingInputs { + m.trackingInputs[i].Blur() + } + m.trackingInputs[m.trackingFocussedField].Focus() +} + +func (m *Model) getCmdToDeactivateTask() tea.Cmd { + if m.activeTasksList.IsFiltered() { + m.message = removeFilterMsg + return nil + } + + if m.trackingActive { + m.message = "Cannot deactivate a task being tracked; stop tracking and try again." + return nil + } + + task, ok := m.activeTasksList.SelectedItem().(*types.Task) + if !ok { + m.message = msgCouldntSelectATask + return nil + } + + return updateTaskActiveStatus(m.db, task, false) +} + +func (m *Model) getCmdToDeleteTL() tea.Cmd { + entry, ok := m.taskLogList.SelectedItem().(types.TaskLogEntry) + if !ok { + m.message = "Couldn't delete task log entry" + return nil + } + return deleteTL(m.db, &entry) +} + +func (m *Model) getCmdToActivateDeactivatedTask() tea.Cmd { + if m.inactiveTasksList.IsFiltered() { + m.message = removeFilterMsg + return nil + } + + task, ok := m.inactiveTasksList.SelectedItem().(*types.Task) + if !ok { + m.message = genericErrorMsg + return nil + } + + return updateTaskActiveStatus(m.db, task, true) +} + +func (m *Model) getCmdToStartTracking() tea.Cmd { + task, ok := m.activeTasksList.SelectedItem().(*types.Task) + if !ok { + m.message = genericErrorMsg + return nil + } + + m.changesLocked = true + m.activeTLBeginTS = time.Now() + return toggleTracking(m.db, task.ID, m.activeTLBeginTS, m.activeTLEndTS, "") +} + +func (m *Model) handleRequestToStopTracking() { + m.activeView = saveActiveTLView + m.activeTLEndTS = time.Now() + + beginTimeStr := m.activeTLBeginTS.Format(timeFormat) + currentTimeStr := m.activeTLEndTS.Format(timeFormat) + + m.trackingInputs[entryBeginTS].SetValue(beginTimeStr) + m.trackingInputs[entryEndTS].SetValue(currentTimeStr) + m.trackingFocussedField = entryComment + + for i := range m.trackingInputs { + m.trackingInputs[i].Blur() + } + m.trackingInputs[m.trackingFocussedField].Focus() +} + +func (m *Model) handleRequestToCreateTask() { + if m.activeTasksList.IsFiltered() { + m.message = removeFilterMsg + return + } + + m.activeView = taskInputView + m.taskInputFocussedField = summaryField + m.taskInputs[summaryField].Focus() + m.taskMgmtContext = taskCreateCxt +} + +func (m *Model) handleRequestToUpdateTask() { + if m.activeTasksList.IsFiltered() { + m.message = removeFilterMsg + return + } + + task, ok := m.activeTasksList.SelectedItem().(*types.Task) + if !ok { + m.message = genericErrorMsg + return + } + + m.activeView = taskInputView + m.taskInputFocussedField = summaryField + m.taskInputs[summaryField].Focus() + m.taskInputs[summaryField].SetValue(task.Summary) + m.taskMgmtContext = taskUpdateCxt +} + +func (m *Model) handleRequestToScrollVPUp() { + if m.helpVP.AtTop() { + return + } + m.helpVP.LineUp(viewPortMoveLineCount) +} + +func (m *Model) handleRequestToScrollVPDown() { + if m.helpVP.AtBottom() { + return + } + m.helpVP.LineDown(viewPortMoveLineCount) +} + +func (m *Model) handleWindowResizing(msg tea.WindowSizeMsg) { + w, h := listStyle.GetFrameSize() + m.terminalHeight = msg.Height + + m.taskLogList.SetWidth(msg.Width - w) + m.taskLogList.SetHeight(msg.Height - h - 2) + + m.activeTasksList.SetWidth(msg.Width - w) + m.activeTasksList.SetHeight(msg.Height - h - 2) + + m.inactiveTasksList.SetWidth(msg.Width - w) + m.inactiveTasksList.SetHeight(msg.Height - h - 2) + + if !m.helpVPReady { + m.helpVP = viewport.New(w-5, m.terminalHeight-7) + m.helpVP.SetContent(helpText) + m.helpVP.KeyMap.Up.SetEnabled(false) + m.helpVP.KeyMap.Down.SetEnabled(false) + m.helpVPReady = true + } else { + m.helpVP.Height = m.terminalHeight - 7 + m.helpVP.Width = w - 5 + } +} + +func (m *Model) handleTasksFetchedMsg(msg tasksFetchedMsg) tea.Cmd { + if msg.err != nil { + m.message = "error fetching tasks : " + msg.err.Error() + return nil + } + + var cmd tea.Cmd + switch msg.active { + case true: + m.activeTaskMap = make(map[int]*types.Task) + m.activeTaskIndexMap = make(map[int]int) + tasks := make([]list.Item, len(msg.tasks)) + for i, task := range msg.tasks { + task.UpdateTitle() + task.UpdateDesc() + tasks[i] = &task + m.activeTaskMap[task.ID] = &task + m.activeTaskIndexMap[task.ID] = i + } + m.activeTasksList.SetItems(tasks) + m.activeTasksList.Title = "Tasks" + m.tasksFetched = true + cmd = fetchActiveTask(m.db) + + case false: + inactiveTasks := make([]list.Item, len(msg.tasks)) + for i, inactiveTask := range msg.tasks { + inactiveTask.UpdateTitle() + inactiveTask.UpdateDesc() + inactiveTasks[i] = &inactiveTask + } + m.inactiveTasksList.SetItems(inactiveTasks) + } + + return cmd +} + +func (m *Model) handleManualTLInsertedMsg(msg manualTLInsertedMsg) []tea.Cmd { + if msg.err != nil { + m.message = msg.err.Error() + return nil + } + for i := range m.trackingInputs { + m.trackingInputs[i].SetValue("") + } + task, ok := m.activeTaskMap[msg.taskID] + + var cmds []tea.Cmd + if ok { + cmds = append(cmds, updateTaskRep(m.db, task)) + } + cmds = append(cmds, fetchTLS(m.db)) + + return cmds +} + +func (m *Model) handleTLSFetchedMsg(msg tLsFetchedMsg) { + if msg.err != nil { + m.message = msg.err.Error() + return + } + + items := make([]list.Item, len(msg.entries)) + for i, e := range msg.entries { + e.UpdateTitle() + e.UpdateDesc() + items[i] = e + } + m.taskLogList.SetItems(items) +} + +func (m *Model) handleActiveTaskFetchedMsg(msg activeTaskFetchedMsg) { + if msg.err != nil { + m.message = msg.err.Error() + return + } + + if msg.noneActive { + m.lastTrackingChange = trackingFinished + return + } + + m.activeTaskID = msg.activeTaskID + m.lastTrackingChange = trackingStarted + m.activeTLBeginTS = msg.beginTs + activeTask, ok := m.activeTaskMap[m.activeTaskID] + if ok { + activeTask.TrackingActive = true + activeTask.UpdateTitle() + + // go to tracked item on startup + activeIndex, aOk := m.activeTaskIndexMap[msg.activeTaskID] + if aOk { + m.activeTasksList.Select(activeIndex) + } + } + m.trackingActive = true +} + +func (m *Model) handleTrackingToggledMsg(msg trackingToggledMsg) []tea.Cmd { + if msg.err != nil { + m.message = msg.err.Error() + m.trackingActive = false + return nil + } + + m.changesLocked = false + + task, ok := m.activeTaskMap[msg.taskID] + + if !ok { + m.message = genericErrorMsg + return nil + } + + var cmds []tea.Cmd + switch msg.finished { + case true: + m.lastTrackingChange = trackingFinished + task.TrackingActive = false + m.trackingActive = false + m.activeTaskID = -1 + cmds = append(cmds, updateTaskRep(m.db, task)) + cmds = append(cmds, fetchTLS(m.db)) + case false: + m.lastTrackingChange = trackingStarted + task.TrackingActive = true + m.trackingActive = true + m.activeTaskID = msg.taskID + } + + task.UpdateTitle() + + return cmds +} + +func (m *Model) handleTLDeleted(msg tLDeletedMsg) []tea.Cmd { + if msg.err != nil { + m.message = "error deleting entry: " + msg.err.Error() + return nil + } + + var cmds []tea.Cmd + task, ok := m.activeTaskMap[msg.entry.TaskID] + if ok { + cmds = append(cmds, updateTaskRep(m.db, task)) + } + cmds = append(cmds, fetchTLS(m.db)) + + return cmds +} + +func (m *Model) handleActiveTLDeletedMsg(msg activeTaskLogDeletedMsg) { + if msg.err != nil { + m.message = fmt.Sprintf("Error deleting active log entry: %s", msg.err) + return + } + + activeTask, ok := m.activeTaskMap[m.activeTaskID] + if !ok { + m.message = genericErrorMsg + return + } + + activeTask.TrackingActive = false + activeTask.UpdateTitle() + m.lastTrackingChange = trackingFinished + m.trackingActive = false + m.activeTaskID = -1 +} diff --git a/internal/ui/model.go b/internal/ui/model.go index 0516e15..69a0872 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -18,21 +18,21 @@ const ( trackingActive ) -type dBChange uint +type trackingChange uint const ( - insertChange dBChange = iota - updateChange + trackingStarted trackingChange = iota + trackingFinished ) type stateView uint const ( - activeTaskListView stateView = iota + taskListView stateView = iota taskLogView inactiveTaskListView - editStartTsView - askForCommentView + editActiveTLView + saveActiveTLView manualTasklogEntryView taskInputView helpView @@ -102,12 +102,11 @@ type Model struct { taskInputFocussedField taskInputField helpVP viewport.Model helpVPReady bool - lastChange dBChange + lastTrackingChange trackingChange changesLocked bool activeTaskID int tasklogSaveType tasklogSaveType message string - messages []string showHelpIndicator bool terminalHeight int trackingActive bool @@ -117,7 +116,7 @@ func (m Model) Init() tea.Cmd { return tea.Batch( hideHelp(time.Minute*1), fetchTasks(m.db, true), - fetchTaskLogEntries(m.db), + fetchTLS(m.db), fetchTasks(m.db, false), ) } diff --git a/internal/ui/msgs.go b/internal/ui/msgs.go index 8d82e95..8b96d61 100644 --- a/internal/ui/msgs.go +++ b/internal/ui/msgs.go @@ -6,7 +6,7 @@ import ( "github.com/dhth/hours/internal/types" ) -type HideHelpMsg struct{} +type hideHelpMsg struct{} type trackingToggledMsg struct { taskID int @@ -20,7 +20,7 @@ type taskRepUpdatedMsg struct { err error } -type manualTaskLogInserted struct { +type manualTLInsertedMsg struct { taskID int err error } @@ -41,7 +41,7 @@ type activeTaskFetchedMsg struct { err error } -type taskLogEntriesFetchedMsg struct { +type tLsFetchedMsg struct { entries []types.TaskLogEntry err error } @@ -56,18 +56,18 @@ type taskUpdatedMsg struct { err error } -type taskActiveStatusUpdated struct { +type taskActiveStatusUpdatedMsg struct { tsk *types.Task active bool err error } -type taskLogEntryDeletedMsg struct { +type tLDeletedMsg struct { entry *types.TaskLogEntry err error } -type tasksFetched struct { +type tasksFetchedMsg struct { tasks []types.Task active bool err error diff --git a/internal/ui/update.go b/internal/ui/update.go index ae13b22..8f88146 100644 --- a/internal/ui/update.go +++ b/internal/ui/update.go @@ -5,7 +5,6 @@ import ( "time" "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/dhth/hours/internal/types" ) @@ -32,168 +31,28 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch keyMsg.String() { case "enter": + var updateCmd tea.Cmd switch m.activeView { case taskInputView: - m.activeView = activeTaskListView - if m.taskInputs[summaryField].Value() != "" { - switch m.taskMgmtContext { - case taskCreateCxt: - cmds = append(cmds, createTask(m.db, m.taskInputs[summaryField].Value())) - m.taskInputs[summaryField].SetValue("") - case taskUpdateCxt: - selectedTask, ok := m.activeTasksList.SelectedItem().(*types.Task) - if ok { - cmds = append(cmds, updateTask(m.db, selectedTask, m.taskInputs[summaryField].Value())) - m.taskInputs[summaryField].SetValue("") - } - } - return m, tea.Batch(cmds...) - } - case editStartTsView: - beginTS, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[entryBeginTS].Value(), time.Local) - if err != nil { - m.message = err.Error() - return m, tea.Batch(cmds...) - } - - cmds = append(cmds, updateTLBeginTS(m.db, beginTS)) - m.trackingInputs[entryBeginTS].SetValue("") - m.activeView = activeTaskListView - - return m, tea.Batch(cmds...) - case askForCommentView: - beginTS, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[entryBeginTS].Value(), time.Local) - if err != nil { - m.message = err.Error() - return m, tea.Batch(cmds...) - } - m.activeTLBeginTS = beginTS - - endTS, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[entryEndTS].Value(), time.Local) - if err != nil { - m.message = err.Error() - return m, tea.Batch(cmds...) - } - m.activeTLEndTS = endTS - - if m.activeTLEndTS.Sub(m.activeTLBeginTS).Seconds() <= 0 { - m.message = "time spent needs to be positive" - return m, tea.Batch(cmds...) - } - - if m.trackingInputs[entryComment].Value() == "" { - m.message = "Comment cannot be empty" - return m, tea.Batch(cmds...) - } - - cmds = append(cmds, toggleTracking(m.db, m.activeTaskID, m.activeTLBeginTS, m.activeTLEndTS, m.trackingInputs[entryComment].Value())) - m.activeView = activeTaskListView - - for i := range m.trackingInputs { - m.trackingInputs[i].SetValue("") - } - return m, tea.Batch(cmds...) - + updateCmd = m.getCmdToCreateOrUpdateTask() + case editActiveTLView: + updateCmd = m.getCmdToUpdateActiveTL() + case saveActiveTLView: + updateCmd = m.getCmdToSaveActiveTL() case manualTasklogEntryView: - beginTS, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[entryBeginTS].Value(), time.Local) - if err != nil { - m.message = err.Error() - return m, tea.Batch(cmds...) - } - - endTS, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[entryEndTS].Value(), time.Local) - if err != nil { - m.message = err.Error() - return m, tea.Batch(cmds...) - } - - if endTS.Sub(beginTS).Seconds() <= 0 { - m.message = "time spent needs to be positive" - return m, tea.Batch(cmds...) - } - - comment := m.trackingInputs[entryComment].Value() - - if len(comment) == 0 { - m.message = "Comment cannot be empty" - return m, tea.Batch(cmds...) - } - - task, ok := m.activeTasksList.SelectedItem().(*types.Task) - if ok && m.tasklogSaveType == tasklogInsert { - cmds = append(cmds, insertManualEntry(m.db, task.ID, beginTS, endTS, comment)) - m.activeView = activeTaskListView - } - for i := range m.trackingInputs { - m.trackingInputs[i].SetValue("") - } + updateCmd = m.getCmdToSaveOrUpdateTL() + } + if updateCmd != nil { + cmds = append(cmds, updateCmd) return m, tea.Batch(cmds...) } + case "esc": - switch m.activeView { - case taskInputView: - m.activeView = activeTaskListView - for i := range m.taskInputs { - m.taskInputs[i].SetValue("") - } - case editStartTsView: - m.taskInputs[entryBeginTS].SetValue("") - m.activeView = activeTaskListView - case askForCommentView: - m.activeView = activeTaskListView - m.trackingInputs[entryComment].SetValue("") - case manualTasklogEntryView: - if m.tasklogSaveType == tasklogInsert { - m.activeView = activeTaskListView - } - for i := range m.trackingInputs { - m.trackingInputs[i].SetValue("") - } - } + m.handleEscape() case "tab": - switch m.activeView { - case activeTaskListView: - m.activeView = taskLogView - case taskLogView: - m.activeView = inactiveTaskListView - case inactiveTaskListView: - m.activeView = activeTaskListView - case askForCommentView, manualTasklogEntryView: - switch m.trackingFocussedField { - case entryBeginTS: - m.trackingFocussedField = entryEndTS - case entryEndTS: - m.trackingFocussedField = entryComment - case entryComment: - m.trackingFocussedField = entryBeginTS - } - for i := range m.trackingInputs { - m.trackingInputs[i].Blur() - } - m.trackingInputs[m.trackingFocussedField].Focus() - } + m.goForwardInView() case "shift+tab": - switch m.activeView { - case taskLogView: - m.activeView = activeTaskListView - case activeTaskListView: - m.activeView = inactiveTaskListView - case inactiveTaskListView: - m.activeView = taskLogView - case askForCommentView, manualTasklogEntryView: - switch m.trackingFocussedField { - case entryBeginTS: - m.trackingFocussedField = entryComment - case entryEndTS: - m.trackingFocussedField = entryBeginTS - case entryComment: - m.trackingFocussedField = entryEndTS - } - for i := range m.trackingInputs { - m.trackingInputs[i].Blur() - } - m.trackingInputs[m.trackingFocussedField].Focus() - } + m.goBackwardInView() case "k": err := m.shiftTime(types.ShiftBackward, types.ShiftMinute) if err != nil { @@ -234,11 +93,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) } return m, tea.Batch(cmds...) - case editStartTsView: + case editActiveTLView: m.trackingInputs[entryBeginTS], cmd = m.trackingInputs[entryBeginTS].Update(msg) cmds = append(cmds, cmd) return m, tea.Batch(cmds...) - case askForCommentView: + case saveActiveTLView: for i := range m.trackingInputs { m.trackingInputs[i], cmd = m.trackingInputs[i].Update(msg) cmds = append(cmds, cmd) @@ -256,36 +115,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": - switch m.activeView { - case activeTaskListView: - fs := m.activeTasksList.FilterState() - if fs == list.Filtering || fs == list.FilterApplied { - m.activeTasksList.ResetFilter() - } else { - return m, tea.Quit - } - case taskLogView: - fs := m.taskLogList.FilterState() - if fs == list.Filtering || fs == list.FilterApplied { - m.taskLogList.ResetFilter() - } else { - m.activeView = activeTaskListView - } - case inactiveTaskListView: - fs := m.inactiveTasksList.FilterState() - if fs == list.Filtering || fs == list.FilterApplied { - m.inactiveTasksList.ResetFilter() - } else { - m.activeView = activeTaskListView - } - case helpView: - m.activeView = activeTaskListView - default: + shouldQuit := m.handleRequestToGoBackOrQuit() + if shouldQuit { return m, tea.Quit } case "1": - if m.activeView != activeTaskListView { - m.activeView = activeTaskListView + if m.activeView != taskListView { + m.activeView = taskListView } case "2": if m.activeView != taskLogView { @@ -296,218 +132,73 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.activeView = inactiveTaskListView } case "ctrl+r": - switch m.activeView { - case activeTaskListView: - cmds = append(cmds, fetchTasks(m.db, true)) - case taskLogView: - cmds = append(cmds, fetchTaskLogEntries(m.db)) - m.taskLogList.ResetSelected() - case inactiveTaskListView: - cmds = append(cmds, fetchTasks(m.db, false)) - m.inactiveTasksList.ResetSelected() + reloadCmd := m.getCmdToReloadData() + if reloadCmd != nil { + cmds = append(cmds, reloadCmd) } case "ctrl+t": - if m.activeView == activeTaskListView { - if m.trackingActive { - if m.activeTasksList.IsFiltered() { - m.activeTasksList.ResetFilter() - } - activeIndex, ok := m.activeTaskIndexMap[m.activeTaskID] - if ok { - m.activeTasksList.Select(activeIndex) - } - } else { - m.message = "Nothing is being tracked right now" - } - } + m.goToActiveTask() case "ctrl+s": - if m.activeView == activeTaskListView { - _, ok := m.activeTasksList.SelectedItem().(*types.Task) - if !ok { - message := msgCouldntSelectATask - m.message = message - m.messages = append(m.messages, message) - } else { - if m.trackingActive { - m.activeView = editStartTsView - m.trackingFocussedField = entryBeginTS - m.trackingInputs[entryBeginTS].SetValue(m.activeTLBeginTS.Format(timeFormat)) - m.trackingInputs[m.trackingFocussedField].Focus() - } else { - m.activeView = manualTasklogEntryView - m.tasklogSaveType = tasklogInsert - m.trackingFocussedField = entryBeginTS - currentTime := time.Now() - dateString := currentTime.Format("2006/01/02") - currentTimeStr := currentTime.Format(timeFormat) - - m.trackingInputs[entryBeginTS].SetValue(dateString + " ") - m.trackingInputs[entryEndTS].SetValue(currentTimeStr) - - for i := range m.trackingInputs { - m.trackingInputs[i].Blur() - } - m.trackingInputs[m.trackingFocussedField].Focus() - } + if m.activeView == taskListView { + switch m.trackingActive { + case true: + m.handleRequestToSaveActiveTL() + case false: + m.handleRequestToCreateManualTL() } } case "ctrl+d": + var handleCmd tea.Cmd switch m.activeView { - case activeTaskListView: - task, ok := m.activeTasksList.SelectedItem().(*types.Task) - if ok { - if task.TrackingActive { - m.message = "Cannot deactivate a task being tracked; stop tracking and try again." - } else { - cmds = append(cmds, updateTaskActiveStatus(m.db, task, false)) - } - } else { - msg := msgCouldntSelectATask - m.message = msg - m.messages = append(m.messages, msg) - } + case taskListView: + handleCmd = m.getCmdToDeactivateTask() case taskLogView: - entry, ok := m.taskLogList.SelectedItem().(types.TaskLogEntry) - if ok { - cmds = append(cmds, deleteLogEntry(m.db, &entry)) - } else { - msg := "Couldn't delete task log entry" - m.message = msg - m.messages = append(m.messages, msg) - } + handleCmd = m.getCmdToDeleteTL() case inactiveTaskListView: - task, ok := m.inactiveTasksList.SelectedItem().(*types.Task) - if ok { - cmds = append(cmds, updateTaskActiveStatus(m.db, task, true)) - } else { - msg := msgCouldntSelectATask - m.message = msg - m.messages = append(m.messages, msg) - } + handleCmd = m.getCmdToActivateDeactivatedTask() + } + if handleCmd != nil { + cmds = append(cmds, handleCmd) } case "ctrl+x": - if m.activeView == activeTaskListView && m.trackingActive { - cmds = append(cmds, deleteActiveTaskLog(m.db)) + if m.activeView == taskListView && m.trackingActive { + cmds = append(cmds, deleteActiveTL(m.db)) } case "s": - if m.activeView == activeTaskListView { - if m.activeTasksList.FilterState() != list.Filtering { - if m.changesLocked { - message := msgChangesLocked - m.message = message - m.messages = append(m.messages, message) - } - task, ok := m.activeTasksList.SelectedItem().(*types.Task) - if !ok { - message := "Couldn't select a task" - m.message = message - m.messages = append(m.messages, message) - } else { - if m.lastChange == updateChange { - m.changesLocked = true - m.activeTLBeginTS = time.Now() - cmds = append(cmds, toggleTracking(m.db, task.ID, m.activeTLBeginTS, m.activeTLEndTS, "")) - } else if m.lastChange == insertChange { - m.activeView = askForCommentView - m.activeTLEndTS = time.Now() - - beginTimeStr := m.activeTLBeginTS.Format(timeFormat) - currentTimeStr := m.activeTLEndTS.Format(timeFormat) - - m.trackingInputs[entryBeginTS].SetValue(beginTimeStr) - m.trackingInputs[entryEndTS].SetValue(currentTimeStr) - m.trackingFocussedField = entryComment - - for i := range m.trackingInputs { - m.trackingInputs[i].Blur() - } - m.trackingInputs[m.trackingFocussedField].Focus() - } + if m.activeView == taskListView { + switch m.lastTrackingChange { + case trackingFinished: + trackCmd := m.getCmdToStartTracking() + if trackCmd != nil { + cmds = append(cmds, trackCmd) } + case trackingStarted: + m.handleRequestToStopTracking() } } case "a": - if m.activeView == activeTaskListView { - if m.activeTasksList.FilterState() != list.Filtering { - if m.changesLocked { - message := msgChangesLocked - m.message = message - m.messages = append(m.messages, message) - } - m.activeView = taskInputView - m.taskInputFocussedField = summaryField - m.taskInputs[summaryField].Focus() - m.taskMgmtContext = taskCreateCxt - } + if m.activeView == taskListView { + m.handleRequestToCreateTask() } case "u": - if m.activeView == activeTaskListView { - if m.activeTasksList.FilterState() != list.Filtering { - if m.changesLocked { - message := msgChangesLocked - m.message = message - m.messages = append(m.messages, message) - } - task, ok := m.activeTasksList.SelectedItem().(*types.Task) - if ok { - m.activeView = taskInputView - m.taskInputFocussedField = summaryField - m.taskInputs[summaryField].Focus() - m.taskInputs[summaryField].SetValue(task.Summary) - m.taskMgmtContext = taskUpdateCxt - } else { - m.message = "Couldn't select a task" - } - } + if m.activeView == taskListView { + m.handleRequestToUpdateTask() } - case "k": - if m.activeView != helpView { - break + if m.activeView == helpView { + m.handleRequestToScrollVPUp() } - if m.helpVP.AtTop() { - break - } - m.helpVP.LineUp(viewPortMoveLineCount) - case "j": - if m.activeView != helpView { - break + if m.activeView == helpView { + m.handleRequestToScrollVPDown() } - if m.helpVP.AtBottom() { - break - } - m.helpVP.LineDown(viewPortMoveLineCount) - case "?": m.lastView = m.activeView m.activeView = helpView } case tea.WindowSizeMsg: - w, h := listStyle.GetFrameSize() - m.terminalHeight = msg.Height - - m.taskLogList.SetWidth(msg.Width - w) - m.taskLogList.SetHeight(msg.Height - h - 2) - - m.activeTasksList.SetWidth(msg.Width - w) - m.activeTasksList.SetHeight(msg.Height - h - 2) - - m.inactiveTasksList.SetWidth(msg.Width - w) - m.inactiveTasksList.SetHeight(msg.Height - h - 2) - - if !m.helpVPReady { - m.helpVP = viewport.New(w-5, m.terminalHeight-7) - m.helpVP.SetContent(helpText) - m.helpVP.KeyMap.Up.SetEnabled(false) - m.helpVP.KeyMap.Down.SetEnabled(false) - m.helpVPReady = true - } else { - m.helpVP.Height = m.terminalHeight - 7 - m.helpVP.Width = w - 5 - - } + m.handleWindowResizing(msg) case taskCreatedMsg: if msg.err != nil { m.message = fmt.Sprintf("Error creating task: %s", msg.err) @@ -521,128 +212,30 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { msg.tsk.Summary = msg.summary msg.tsk.UpdateTitle() } - case tasksFetched: - if msg.err != nil { - message := "error fetching tasks : " + msg.err.Error() - m.message = message - m.messages = append(m.messages, message) - } else { - if msg.active { - m.activeTaskMap = make(map[int]*types.Task) - m.activeTaskIndexMap = make(map[int]int) - tasks := make([]list.Item, len(msg.tasks)) - for i, task := range msg.tasks { - task.UpdateTitle() - task.UpdateDesc() - tasks[i] = &task - m.activeTaskMap[task.ID] = &task - m.activeTaskIndexMap[task.ID] = i - } - m.activeTasksList.SetItems(tasks) - m.activeTasksList.Title = "Tasks" - m.tasksFetched = true - cmds = append(cmds, fetchActiveTask(m.db)) - } else { - inactiveTasks := make([]list.Item, len(msg.tasks)) - for i, inactiveTask := range msg.tasks { - inactiveTask.UpdateTitle() - inactiveTask.UpdateDesc() - inactiveTasks[i] = &inactiveTask - } - m.inactiveTasksList.SetItems(inactiveTasks) - } + case tasksFetchedMsg: + handleCmd := m.handleTasksFetchedMsg(msg) + if handleCmd != nil { + cmds = append(cmds, handleCmd) } case tlBeginTSUpdatedMsg: if msg.err != nil { - message := msg.err.Error() - m.message = "Error updating begin time: " + message - m.messages = append(m.messages, message) + m.message = msg.err.Error() } else { m.activeTLBeginTS = msg.beginTS } - case manualTaskLogInserted: - if msg.err != nil { - message := msg.err.Error() - m.message = "Error inserting task log: " + message - m.messages = append(m.messages, message) - } else { - for i := range m.trackingInputs { - m.trackingInputs[i].SetValue("") - } - task, ok := m.activeTaskMap[msg.taskID] - - if ok { - cmds = append(cmds, updateTaskRep(m.db, task)) - } - cmds = append(cmds, fetchTaskLogEntries(m.db)) - } - case taskLogEntriesFetchedMsg: - if msg.err != nil { - message := msg.err.Error() - m.message = "Error fetching task log entries: " + message - m.messages = append(m.messages, message) - } else { - var items []list.Item - for _, e := range msg.entries { - e.UpdateTitle() - e.UpdateDesc() - items = append(items, e) - } - m.taskLogList.SetItems(items) + case manualTLInsertedMsg: + handleCmds := m.handleManualTLInsertedMsg(msg) + if handleCmds != nil { + cmds = append(cmds, handleCmds...) } + case tLsFetchedMsg: + m.handleTLSFetchedMsg(msg) case activeTaskFetchedMsg: - if msg.err != nil { - message := msg.err.Error() - m.message = message - m.messages = append(m.messages, message) - } else { - if msg.noneActive { - m.lastChange = updateChange - } else { - m.activeTaskID = msg.activeTaskID - m.lastChange = insertChange - m.activeTLBeginTS = msg.beginTs - activeTask, ok := m.activeTaskMap[m.activeTaskID] - if ok { - activeTask.TrackingActive = true - activeTask.UpdateTitle() - - // go to tracked item on startup - activeIndex, ok := m.activeTaskIndexMap[msg.activeTaskID] - if ok { - m.activeTasksList.Select(activeIndex) - } - } - m.trackingActive = true - } - } + m.handleActiveTaskFetchedMsg(msg) case trackingToggledMsg: - if msg.err != nil { - message := msg.err.Error() - m.message = message - m.messages = append(m.messages, message) - m.trackingActive = false - } else { - m.changesLocked = false - - task, ok := m.activeTaskMap[msg.taskID] - - if ok { - if msg.finished { - m.lastChange = updateChange - task.TrackingActive = false - m.trackingActive = false - m.activeTaskID = -1 - cmds = append(cmds, updateTaskRep(m.db, task)) - cmds = append(cmds, fetchTaskLogEntries(m.db)) - } else { - m.lastChange = insertChange - task.TrackingActive = true - m.trackingActive = true - m.activeTaskID = msg.taskID - } - task.UpdateTitle() - } + updateCmds := m.handleTrackingToggledMsg(msg) + if updateCmds != nil { + cmds = append(cmds, updateCmds...) } case taskRepUpdatedMsg: if msg.err != nil { @@ -650,46 +243,26 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { msg.tsk.UpdateDesc() } - case taskLogEntryDeletedMsg: - if msg.err != nil { - message := "error deleting entry: " + msg.err.Error() - m.message = message - m.messages = append(m.messages, message) - } else { - task, ok := m.activeTaskMap[msg.entry.TaskID] - if ok { - cmds = append(cmds, updateTaskRep(m.db, task)) - } - cmds = append(cmds, fetchTaskLogEntries(m.db)) + case tLDeletedMsg: + updateCmds := m.handleTLDeleted(msg) + if updateCmds != nil { + cmds = append(cmds, updateCmds...) } case activeTaskLogDeletedMsg: + m.handleActiveTLDeletedMsg(msg) + case taskActiveStatusUpdatedMsg: if msg.err != nil { - m.message = fmt.Sprintf("Error deleting active log entry: %s", msg.err) - } else { - activeTask, ok := m.activeTaskMap[m.activeTaskID] - if ok { - activeTask.TrackingActive = false - activeTask.UpdateTitle() - } - m.lastChange = updateChange - m.trackingActive = false - m.activeTaskID = -1 - } - case taskActiveStatusUpdated: - if msg.err != nil { - message := "error updating task's active status: " + msg.err.Error() - m.message = message - m.messages = append(m.messages, message) + m.message = "error updating task's active status: " + msg.err.Error() } else { cmds = append(cmds, fetchTasks(m.db, true)) cmds = append(cmds, fetchTasks(m.db, false)) } - case HideHelpMsg: + case hideHelpMsg: m.showHelpIndicator = false } switch m.activeView { - case activeTaskListView: + case taskListView: m.activeTasksList, cmd = m.activeTasksList.Update(msg) cmds = append(cmds, cmd) case taskLogView: @@ -794,19 +367,3 @@ func (m recordsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, tea.Batch(cmds...) } - -func (m Model) shiftTime(direction types.TimeShiftDirection, duration types.TimeShiftDuration) error { - if m.activeView == editStartTsView || m.activeView == askForCommentView || m.activeView == manualTasklogEntryView { - if m.trackingFocussedField == entryBeginTS || m.trackingFocussedField == entryEndTS { - ts, err := time.ParseInLocation(string(timeFormat), m.trackingInputs[m.trackingFocussedField].Value(), time.Local) - if err != nil { - return err - } - - newTs := types.GetShiftedTime(ts, direction, duration) - - m.trackingInputs[m.trackingFocussedField].SetValue(newTs.Format(timeFormat)) - } - } - return nil -} diff --git a/internal/ui/view.go b/internal/ui/view.go index afdf64d..cd04b58 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -28,7 +28,7 @@ func (m Model) View() string { task, ok := m.activeTaskMap[m.activeTaskID] if ok { taskSummaryMsg = utils.Trim(task.Summary, 50) - if m.activeView != askForCommentView { + if m.activeView != saveActiveTLView { taskStartedSinceMsg = fmt.Sprintf("(since %s)", m.activeTLBeginTS.Format(timeOnlyFormat)) } } @@ -40,7 +40,7 @@ func (m Model) View() string { } switch m.activeView { - case activeTaskListView: + case taskListView: content = listStyle.Render(m.activeTasksList.View()) case taskLogView: content = listStyle.Render(m.taskLogList.View()) @@ -70,7 +70,7 @@ func (m Model) View() string { for i := 0; i < m.terminalHeight-20+10; i++ { content += "\n" } - case askForCommentView: + case saveActiveTLView: formHeadingText := "Saving log entry. Enter the following details." content = fmt.Sprintf( @@ -112,7 +112,7 @@ func (m Model) View() string { for i := 0; i < m.terminalHeight-24; i++ { content += "\n" } - case editStartTsView: + case editActiveTLView: formHeadingText := "Updating log entry. Enter the following details." content = fmt.Sprintf( @@ -197,7 +197,7 @@ func (m Model) View() string { var helpMsg string if m.showHelpIndicator { // first time directions - if m.activeView == activeTaskListView && len(m.activeTasksList.Items()) <= 1 { + if m.activeView == taskListView && len(m.activeTasksList.Items()) <= 1 { if len(m.activeTasksList.Items()) == 0 { helpMsg += " " + initialHelpMsgStyle.Render("Press a to add a task") } else if len(m.taskLogList.Items()) == 0 { diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 0000000..9132c38 --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +cat <<'EOF' + _ + | |__ ___ _ _ _ __ ___ + | '_ \ / _ \| | | | '__/ __| + | | | | (_) | |_| | | \__ \ + |_| |_|\___/ \__,_|_| |___/ + +EOF +pass_count=0 +fail_count=0 + +temp_dir=$(mktemp -d) +db_file_path="${temp_dir}/db.db" + +echo "hours gen -y -d ${db_file_path}" +hours gen -y -d "${db_file_path}" + +tests=( + "log: today|hours -d ${db_file_path} log -p today|0" + "log: yest|hours -d ${db_file_path} log -p yest|0" + "log: 3d|hours -d ${db_file_path} log -p 3d|0" + "log: week|hours -d ${db_file_path} log -p week|0" + "log: date|hours -d ${db_file_path} log -p 2024/06/08|0" + "log: date range|hours -d ${db_file_path} log -p 2024/06/08...2024/06/12|0" + "log: incorrect argument|hours -d ${db_file_path} log -p blah|1" + "log: incorrect date|hours -d ${db_file_path} log -p 2024/0608|1" + "log: incorrect date range|hours -d ${db_file_path} log -p 2024/0608...2024/06/12|1" + "log: date range too large|hours -d ${db_file_path} log -p 2024/06/08...2024/06/15|1" + "report: today|hours -d ${db_file_path} report -p today|0" + "report: yest|hours -d ${db_file_path} report -p yest|0" + "report: 3d|hours -d ${db_file_path} report -p 3d|0" + "report: week|hours -d ${db_file_path} report -p week|0" + "report: date|hours -d ${db_file_path} report -p 2024/06/08|0" + "report: date range|hours -d ${db_file_path} report -p 2024/06/08...2024/06/12|0" + "report: incorrect argument|hours -d ${db_file_path} report -p blah|1" + "report: incorrect date|hours -d ${db_file_path} report -p 2024/0608|1" + "report: incorrect date range|hours -d ${db_file_path} report -p 2024/0608...2024/06/12|1" + "report: date range too large|hours -d ${db_file_path} report -p 2024/06/08...2024/06/15|1" + "stats: today|hours -d ${db_file_path} stats -p today|0" + "stats: yest|hours -d ${db_file_path} stats -p yest|0" + "stats: 3d|hours -d ${db_file_path} stats -p 3d|0" + "stats: week|hours -d ${db_file_path} stats -p week|0" + "stats: date|hours -d ${db_file_path} stats -p 2024/06/08|0" + "stats: date range|hours -d ${db_file_path} stats -p 2024/06/08...2024/06/12|0" + "stats: all|hours -d ${db_file_path} stats -p all|0" + "stats: incorrect argument|hours -d ${db_file_path} stats -p blah|1" + "stats: incorrect date|hours -d ${db_file_path} stats -p 2024/0608|1" + "stats: incorrect date range|hours -d ${db_file_path} stats -p 2024/0608...2024/06/12|1" + "stats: date range too large|hours -d ${db_file_path} stats -p 2024/06/08...2024/06/15|1" +) + +for test in "${tests[@]}"; do + IFS='|' read -r title cmd expected_exit_code <<<"$test" + + echo "> $title" + echo "$cmd" + echo + eval "$cmd" + exit_code=$? + if [ $exit_code -eq $expected_exit_code ]; then + echo "✅ command behaves as expected" + ((pass_count++)) + else + echo "❌ command returned $exit_code, expected $expected_exit_code" + ((fail_count++)) + fi + echo + echo "===============================" + echo +done + +echo "Summary:" +echo "- Passed: $pass_count" +echo "- Failed: $fail_count" + +if [ $fail_count -gt 0 ]; then + exit 1 +else + exit 0 +fi