Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Track time effortlessly with automatic project detection and simple commands.`,
// Tracking
cmd.AddCommand(tracking.StartCmd())
cmd.AddCommand(tracking.StopCmd())
cmd.AddCommand(tracking.CancelCmd())
cmd.AddCommand(tracking.PauseCmd())
cmd.AddCommand(tracking.ResumeCmd())
cmd.AddCommand(tracking.StatusCmd())
Expand Down
60 changes: 60 additions & 0 deletions cmd/tracking/cancel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package tracking

import (
"fmt"
"os"

"github.com/DylanDevelops/tmpo/internal/storage"
"github.com/DylanDevelops/tmpo/internal/ui"
"github.com/spf13/cobra"
)

func CancelCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "cancel",
Short: "Cancel the running time entry",
Long: "Stops and cancels the running time tracking session.",
Run: func(cmd *cobra.Command, args []string) {
ui.NewlineAbove()

db, err := storage.Initialize()
if err != nil {
ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err))
os.Exit(1)
}

defer db.Close()

running, err := db.GetRunningEntry()
if err != nil {
ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err))
os.Exit(1)
}

if running == nil {
ui.PrintWarning(ui.EmojiWarning, "No active time tracking session.")
ui.NewlineBelow()
os.Exit(0)
}

err = db.CancelEntry(running.ID)
if err != nil {
ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err))
os.Exit(1)
}

db.SaveLastAction(storage.UndoAction{
Type: storage.ActionCancel,
ProjectName: running.ProjectName,
Entry: running,
})

ui.PrintSuccess(ui.EmojiCancel, fmt.Sprintf("Cancelled tracking %s", ui.Bold(running.ProjectName)))
ui.PrintMuted(4, "If this was a mistake, run `tmpo undo`.")

ui.NewlineBelow()
},
}

return cmd
}
1 change: 1 addition & 0 deletions cmd/tracking/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func StopCmd() *cobra.Command {

if running == nil {
ui.PrintWarning(ui.EmojiWarning, "No active time tracking session.")
ui.NewlineBelow()
os.Exit(0)
}

Expand Down
28 changes: 21 additions & 7 deletions cmd/utilities/undo.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

var actionDescriptions = map[storage.ActionType]string{
storage.ActionStop: "Stopped tracking",
storage.ActionCancel: "Cancelled tracking",
storage.ActionPause: "Paused tracking",
storage.ActionStart: "Started tracking",
storage.ActionResume: "Resumed tracking",
Expand Down Expand Up @@ -87,22 +88,24 @@ func undoActionDescription(action *storage.UndoAction) string {
func applyUndo(db *storage.Database, action *storage.UndoAction) error {
switch action.Type {
case storage.ActionStop, storage.ActionPause:
running, err := db.GetRunningEntry()
if err != nil {
return fmt.Errorf("checking for running entry: %w", err)
}
if running != nil {
return fmt.Errorf("a timer is already running for %s — stop it first with 'tmpo stop'", running.ProjectName)
if err := ensureNoRunningTimer(db); err != nil {
return err
}
return db.UncompleteEntry(action.EntryID)

case storage.ActionStart, storage.ActionResume, storage.ActionManual:
return db.DeleteTimeEntry(action.EntryID)

case storage.ActionDelete:
case storage.ActionDelete, storage.ActionCancel:
if action.Entry == nil {
return fmt.Errorf("no entry snapshot available to restore")
}

if action.Entry.EndTime == nil {
if err := ensureNoRunningTimer(db); err != nil {
return err
}
}
return db.RestoreDeletedEntry(action.Entry)

case storage.ActionEdit:
Expand All @@ -115,3 +118,14 @@ func applyUndo(db *storage.Database, action *storage.UndoAction) error {
return fmt.Errorf("unknown action type: %s", action.Type)
}
}

func ensureNoRunningTimer(db *storage.Database) error {
running, err := db.GetRunningEntry()
if err != nil {
return fmt.Errorf("checking for running entry: %w", err)
}
if running != nil {
return fmt.Errorf("a timer is already running for %s — stop it first with 'tmpo stop'", running.ProjectName)
}
return nil
}
8 changes: 8 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ Stop the currently running time entry.
tmpo stop
```

### `tmpo cancel`

Cancel the currently running time entry.

```bash
tmpo cancel
```

### `tmpo pause`

Pause the currently running time entry. This is useful for taking quick breaks without losing context. The paused session can be resumed with `tmpo resume`.
Expand Down
10 changes: 10 additions & 0 deletions internal/storage/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,16 @@ func (d *Database) StopEntry(id int64) error {
return nil
}

func (d *Database) CancelEntry(id int64) error {
err := d.DeleteTimeEntry(id)

if err != nil {
return fmt.Errorf("failed to cancel time entry: %w", err)
}

return nil
}

func (d *Database) GetEntry(id int64) (*TimeEntry, error) {
var entry TimeEntry
var endTime sql.NullTime
Expand Down
17 changes: 17 additions & 0 deletions internal/storage/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,23 @@ func TestStopEntry(t *testing.T) {
assert.NotNil(t, stopped.EndTime)
}

func TestCancelEntry(t *testing.T) {
db := setupTestDB(t)
defer db.Close()

entry, err := db.CreateEntry("test-project", "test", nil, nil)
assert.NoError(t, err)
assert.NotNil(t, entry)

err = db.CancelEntry(entry.ID)
assert.NoError(t, err)

// Verify entry was cancelled
cancelled, err := db.GetEntry(entry.ID)
assert.Error(t, err)
assert.Nil(t, cancelled)
}

func TestGetEntry(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
Expand Down
1 change: 1 addition & 0 deletions internal/storage/undo.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type ActionType string

const (
ActionStop ActionType = "stop"
ActionCancel ActionType = "cancel"
ActionStart ActionType = "start"
ActionPause ActionType = "pause"
ActionResume ActionType = "resume"
Expand Down
1 change: 1 addition & 0 deletions internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const (
const (
EmojiStart = "✨"
EmojiStop = "🛑"
EmojiCancel = "🚫"
EmojiStatus = "⏱️"
EmojiStats = "📊"
EmojiLog = "📝"
Expand Down
Loading