Skip to content
Open
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
20 changes: 16 additions & 4 deletions cmd/break.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,25 @@ func breakCmd(cmd *cobra.Command, args []string) error {
}
}

if err := hook.Run(client, "break"); err != nil {
if err := hook.Run(client, hook.Params{
Name: "break",
Command: "break",
Args: getCommandArgs(cmd),
BreakDuration: d,
}); err != nil {
Comment on lines +29 to +34
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PomodoroID field is missing from the hook.Params struct, unlike other commands that include it. This inconsistency means break hooks won't have access to the pomodoro ID environment variable, which could be useful for tracking which pomodoro the break is associated with.

Copilot uses AI. Check for mistakes.
return err
}

if err := wait(d); err != nil {
return err
if shouldWait(cmd, true) {
if err := wait(d); err != nil {
return err
}
}

return hook.Run(client, "stop")
return hook.Run(client, hook.Params{
Name: "stop",
Command: "break",
Args: getCommandArgs(cmd),
BreakDuration: d,
})
Comment on lines +44 to +49
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PomodoroID field is missing from the hook.Params struct. This will result in an empty POMODORO_ID environment variable being passed to the stop hook, which is inconsistent with other commands that properly set this field.

Copilot uses AI. Check for mistakes.
}
13 changes: 12 additions & 1 deletion cmd/cancel.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"github.com/open-pomodoro/go-openpomodoro"
"github.com/open-pomodoro/openpomodoro-cli/hook"
"github.com/spf13/cobra"
)
Expand All @@ -16,7 +17,17 @@ func init() {
}

func cancelCmd(cmd *cobra.Command, args []string) error {
if err := hook.Run(client, "stop"); err != nil {
p, err := client.Pomodoro()
if err != nil {
return err
}

if err := hook.Run(client, hook.Params{
Name: "stop",
PomodoroID: p.StartTime.Format(openpomodoro.TimeFormat),
Command: "cancel",
Args: getCommandArgs(cmd),
}); err != nil {
return err
}

Expand Down
13 changes: 12 additions & 1 deletion cmd/clear.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"github.com/open-pomodoro/go-openpomodoro"
"github.com/open-pomodoro/openpomodoro-cli/hook"
"github.com/spf13/cobra"
)
Expand All @@ -16,7 +17,17 @@ func init() {
}

func clearCmd(cmd *cobra.Command, args []string) error {
if err := hook.Run(client, "stop"); err != nil {
p, err := client.Pomodoro()
if err != nil {
return err
}

if err := hook.Run(client, hook.Params{
Name: "stop",
PomodoroID: p.StartTime.Format(openpomodoro.TimeFormat),
Command: "clear",
Args: getCommandArgs(cmd),
}); err != nil {
return err
}

Expand Down
29 changes: 24 additions & 5 deletions cmd/finish.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"
"time"

"github.com/open-pomodoro/go-openpomodoro"
"github.com/open-pomodoro/openpomodoro-cli/format"
"github.com/open-pomodoro/openpomodoro-cli/hook"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -34,7 +35,12 @@ func finishCmd(cmd *cobra.Command, args []string) error {
d := time.Now().Sub(p.StartTime)
fmt.Println(format.DurationAsTime(d))

if err := hook.Run(client, "stop"); err != nil {
if err := hook.Run(client, hook.Params{
Name: "stop",
PomodoroID: p.StartTime.Format(openpomodoro.TimeFormat),
Command: "finish",
Args: getCommandArgs(cmd),
}); err != nil {
return err
}

Expand All @@ -54,15 +60,28 @@ func finishCmd(cmd *cobra.Command, args []string) error {
}
}

if err := hook.Run(client, "break"); err != nil {
if err := hook.Run(client, hook.Params{
Name: "break",
PomodoroID: p.StartTime.Format(openpomodoro.TimeFormat),
Command: "finish",
Args: getCommandArgs(cmd),
BreakDuration: breakDuration,
}); err != nil {
return err
}

if err := wait(breakDuration); err != nil {
return err
if shouldWait(cmd, true) {
if err := wait(breakDuration); err != nil {
return err
}
}

return hook.Run(client, "stop")
return hook.Run(client, hook.Params{
Name: "stop",
PomodoroID: p.StartTime.Format(openpomodoro.TimeFormat),
Command: "finish",
Args: getCommandArgs(cmd),
})
}

return nil
Expand Down
13 changes: 12 additions & 1 deletion cmd/repeat.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"time"

"github.com/open-pomodoro/go-openpomodoro"
"github.com/open-pomodoro/openpomodoro-cli/hook"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -46,7 +47,17 @@ func repeatCmd(cmd *cobra.Command, args []string) error {
return err
}

if err := hook.Run(client, "start"); err != nil {
current, err := client.Pomodoro()
if err != nil {
return err
}

if err := hook.Run(client, hook.Params{
Name: "start",
PomodoroID: current.StartTime.Format(openpomodoro.TimeFormat),
Command: "repeat",
Args: getCommandArgs(cmd),
}); err != nil {
return err
}

Expand Down
12 changes: 11 additions & 1 deletion cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,17 @@ func startCmd(cmd *cobra.Command, args []string) error {
return err
}

if err := hook.Run(client, "start"); err != nil {
current, err := client.Pomodoro()
if err != nil {
return err
}

if err := hook.Run(client, hook.Params{
Name: "start",
PomodoroID: current.StartTime.Format(openpomodoro.TimeFormat),
Command: "start",
Args: getCommandArgs(cmd),
}); err != nil {
return err
}

Expand Down
23 changes: 23 additions & 0 deletions cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package cmd
import (
"encoding/json"
"fmt"
"os"
"strconv"
"time"

"github.com/justincampbell/go-countdown"
"github.com/justincampbell/go-countdown/format"
"github.com/open-pomodoro/go-openpomodoro"
"github.com/spf13/cobra"
)

// wait displays a countdown timer for the specified duration
Expand All @@ -23,6 +25,15 @@ func wait(d time.Duration) error {
return err
}

// shouldWait determines if we should wait based on the wait flag and command context
// For break commands, wait by default unless --no-wait is explicitly set
func shouldWait(cmd *cobra.Command, defaultWait bool) bool {
if cmd.Flags().Changed("wait") {
return waitFlag
}
return defaultWait
}

// parseDurationMinutes parses a duration string, defaulting to minutes if no unit is specified
func parseDurationMinutes(s string) (time.Duration, error) {
if _, err := strconv.Atoi(s); err == nil {
Expand All @@ -47,3 +58,15 @@ func isPomodoroCompleted(p *openpomodoro.Pomodoro) bool {
current, _ := client.Pomodoro()
return current.IsInactive() || !current.Matches(p)
}

// getCommandArgs extracts the command-specific arguments from os.Args
func getCommandArgs(cmd *cobra.Command) []string {
for i, arg := range os.Args {
if arg == cmd.Name() || (i > 0 && os.Args[i-1] == "pomodoro") {
if arg == cmd.Name() {
return os.Args[i+1:]
}
}
}
return cmd.Flags().Args()
}
Comment on lines +62 to +72
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getCommandArgs function has complex logic that may not handle all edge cases correctly. The condition (i > 0 && os.Args[i-1] == "pomodoro") on line 65 doesn't result in any action, making it redundant. Consider simplifying this logic or adding comments to explain the intended behavior for different argument patterns.

Copilot uses AI. Check for mistakes.
35 changes: 32 additions & 3 deletions hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@ import (
"os"
"os/exec"
"path"
"strings"
"time"

"github.com/open-pomodoro/go-openpomodoro"
)

// Params holds the parameters for running a hook.
type Params struct {
Name string
PomodoroID string
Command string
Args []string
BreakDuration time.Duration
}

// Run runs a hook with the given name.
func Run(client *openpomodoro.Client, name string) error {
filename := path.Join(client.Directory, "hooks", name)
func Run(client *openpomodoro.Client, params Params) error {
filename := path.Join(client.Directory, "hooks", params.Name)

if _, err := os.Stat(filename); os.IsNotExist(err) {
return nil
Expand All @@ -21,10 +32,28 @@ func Run(client *openpomodoro.Client, name string) error {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env,
fmt.Sprintf("POMODORO_ID=%s", params.PomodoroID),
fmt.Sprintf("POMODORO_DIRECTORY=%s", client.Directory),
fmt.Sprintf("POMODORO_COMMAND=%s", params.Command),
fmt.Sprintf("POMODORO_ARGS=%s", joinArgs(params.Args)),
)

if params.BreakDuration > 0 {
cmd.Env = append(cmd.Env,
fmt.Sprintf("POMODORO_BREAK_DURATION_MINUTES=%d", int(params.BreakDuration.Minutes())),
fmt.Sprintf("POMODORO_BREAK_DURATION_SECONDS=%d", int(params.BreakDuration.Seconds())),
)
}

if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Hook %q failed:\n\n", name)
fmt.Fprintf(os.Stderr, "Hook %q failed:\n\n", params.Name)
return err
}

return nil
}

func joinArgs(args []string) string {
return strings.Join(args, " ")
}
74 changes: 74 additions & 0 deletions test/hooks.bats
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,77 @@ load test_helper
assert_hook_contains "START_EXECUTED"
assert_hook_contains "STOP_EXECUTED"
}

@test "hook receives POMODORO_ID environment variable" {
create_hook "start" 'echo "ID=$POMODORO_ID" >> "$TEST_DIR/hook_log"'

run pomodoro start "Test task" --ago 5m
assert_success

run grep -E 'ID=[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}' "$TEST_DIR/hook_log"
assert_success
}

@test "hook receives POMODORO_DIRECTORY environment variable" {
create_hook "start" 'echo "DIR=$POMODORO_DIRECTORY" >> "$TEST_DIR/hook_log"'

run pomodoro start "Test task"
assert_success

assert_hook_contains "DIR=$TEST_DIR"
}

@test "hook receives POMODORO_COMMAND environment variable" {
create_hook "start" 'echo "CMD=$POMODORO_COMMAND" >> "$TEST_DIR/hook_log"'

run pomodoro start "Test task"
assert_success

assert_hook_contains "CMD=start"
}

@test "hook receives POMODORO_ARGS environment variable" {
create_hook "start" 'echo "ARGS=$POMODORO_ARGS" >> "$TEST_DIR/hook_log"'

run pomodoro start "Test task" --tags "urgent,work" --duration 30
assert_success

assert_hook_contains 'ARGS=Test task --tags urgent,work --duration 30'
}

@test "hook can use POMODORO_ID with show command" {
create_hook "start" 'echo "ID_SET=${POMODORO_ID:+yes}" >> "$TEST_DIR/hook_log"'

run pomodoro start "My test description"
assert_success

assert_hook_contains "ID_SET=yes"
}

@test "break hook receives POMODORO_BREAK_DURATION_MINUTES" {
create_hook "break" 'echo "MINS=$POMODORO_BREAK_DURATION_MINUTES" >> "$TEST_DIR/hook_log"'

run pomodoro break 15 --wait=false
assert_success

assert_hook_contains "MINS=15"
}

@test "break hook receives POMODORO_BREAK_DURATION_SECONDS" {
create_hook "break" 'echo "SECS=$POMODORO_BREAK_DURATION_SECONDS" >> "$TEST_DIR/hook_log"'

run pomodoro break 5 --wait=false
assert_success

assert_hook_contains "SECS=300"
}

@test "finish --break hook receives break duration" {
create_hook "break" 'echo "MINS=$POMODORO_BREAK_DURATION_MINUTES SECS=$POMODORO_BREAK_DURATION_SECONDS" >> "$TEST_DIR/hook_log"'

pomodoro start "Task" --ago 5m
run pomodoro finish --break=10 --wait=false
assert_success

assert_hook_contains "MINS=10 SECS=600"
}