Skip to content

Commit c6329dc

Browse files
authored
Merge pull request #7 from topfunky/topfunky/refactor-and-count-down
Refactor `main` function and render progress bar from 100% when counting down
2 parents 2bc6b69 + c8a6966 commit c6329dc

9 files changed

Lines changed: 357 additions & 72 deletions

File tree

Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
.PHONY: build install clean test list release-dry-run install-deps
22

3+
VERSION := $(shell git describe --tags 2>/dev/null || echo "dev")
4+
LDFLAGS := -ldflags "-X main.version=$(VERSION)"
5+
36
# Display available tasks
47
list:
58
@$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | xargs
69

710
# Build the application
811
build:
9-
go build
12+
go build $(LDFLAGS)
1013

1114
# Install the application
1215
install:
13-
go install
16+
go install $(LDFLAGS)
1417

1518
# Clean the build
1619
clean:
20+
@rm -f tiny-timer
1721
go clean -testcache
1822

1923
# Run tests

handlers.go

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/charmbracelet/bubbles/progress"
1010
tea "github.com/charmbracelet/bubbletea"
11+
"tiny-timer/status"
1112
)
1213

1314
// Top level event handler that is called each time the screen is updated
@@ -31,6 +32,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
3132
m.progress = progressModel.(progress.Model)
3233
return m, cmd
3334

35+
case status.InfoMsg, status.ClearStatusMsg:
36+
// Handle status component updates
37+
statusModel, cmd := m.status.Update(msg)
38+
m.status = statusModel
39+
return m, cmd
40+
3441
default:
3542
return m, nil
3643
}
@@ -49,14 +56,18 @@ func updatePercent(m model) (tea.Model, tea.Cmd) {
4956
return m, tea.Batch(tickCmd(), cmd)
5057
}
5158

52-
percentCompleted := float64(elapsed) / float64(m.targetDuration)
59+
// Calculate completion for further evaluation
60+
//
61+
// For count down mode (default), fill progress bar
62+
// and work backwards as time elapses
63+
percentCompleted := float64(m.targetDuration-elapsed) / float64(m.targetDuration)
5364

5465
// Check for completion based on actual elapsed time
55-
if percentCompleted >= 1.0 {
56-
// Ensure progress is set to 100% for final display
57-
m.progress.SetPercent(1.0)
66+
if percentCompleted <= 0.0 {
67+
// Ensure progress is set to 0% for final display
68+
m.progress.SetPercent(0.0)
5869

59-
if err := sendNotification("Pomodoro CLI", "Timer has finished"); err != nil {
70+
if err := sendNotification("tiny-timer", "Timer has finished"); err != nil {
6071
fmt.Println("Error sending notification:", err)
6172
}
6273

@@ -67,21 +78,26 @@ func updatePercent(m model) (tea.Model, tea.Cmd) {
6778
return m, nil
6879
}
6980

81+
// Activate normal progress bar update
7082
cmd := m.progress.SetPercent(percentCompleted)
7183
return m, tea.Batch(tickCmd(), cmd)
7284
}
7385

7486
func updateWindowSize(m model, msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) {
7587
m.progress.Width = msg.Width - padding*2 - 4
76-
if m.progress.Width > maxWidth {
77-
m.progress.Width = maxWidth
78-
}
88+
m.progress.Width = min(m.progress.Width, maxWidth)
7989
m.help.Width = msg.Width
80-
return m, nil
90+
91+
// Update status component with window size
92+
s, cmd := m.status.Update(msg)
93+
m.status = s
94+
95+
return m, cmd
8196
}
8297

8398
func handlePromptInput(m model, msg promptMsg) (tea.Model, tea.Cmd) {
8499
m.promptActive = false
100+
var cmds []tea.Cmd
85101

86102
switch m.promptType {
87103
case promptLogAndReset:
@@ -90,7 +106,21 @@ func handlePromptInput(m model, msg promptMsg) (tea.Model, tea.Cmd) {
90106
log.Printf("handlePromptInput: Saving session, elapsed=%d, title=%q", elapsed, msg.title)
91107
if err := saveSessionToDB(elapsed, true, msg.title); err != nil {
92108
log.Printf("handlePromptInput: Error saving session: %v", err)
93-
fmt.Println("Error saving session to DB:", err)
109+
// Show error status
110+
cmds = append(cmds, func() tea.Msg {
111+
return status.InfoMsg{
112+
Type: status.InfoTypeError,
113+
Msg: fmt.Sprintf("Failed to save session: %v", err),
114+
}
115+
})
116+
} else {
117+
// Show success status
118+
cmds = append(cmds, func() tea.Msg {
119+
return status.InfoMsg{
120+
Type: status.InfoTypeSuccess,
121+
Msg: fmt.Sprintf("Saved: %s (%d:%02d)", msg.title, elapsed/60, elapsed%60),
122+
}
123+
})
94124
}
95125
// Refresh history table if we are logging
96126
log.Printf("handlePromptInput: Building table view after save")
@@ -104,7 +134,8 @@ func handlePromptInput(m model, msg promptMsg) (tea.Model, tea.Cmd) {
104134
m.startTime = time.Now().Unix()
105135
cmd := m.progress.SetPercent(0)
106136
m.title = ""
107-
return m, tea.Batch(tickCmd(), cmd)
137+
cmds = append(cmds, tickCmd(), cmd)
138+
return m, tea.Batch(cmds...)
108139
case promptEditTitle:
109140
// Just update title without logging
110141
m.title = msg.title
@@ -129,23 +160,24 @@ func handlePromptInput(m model, msg promptMsg) (tea.Model, tea.Cmd) {
129160

130161
// handlePromptKeyInput handles key input when prompt is active
131162
func handlePromptKeyInput(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) {
132-
if msg.Type == tea.KeyEnter {
163+
switch msg.Type {
164+
case tea.KeyEnter:
133165
return handlePromptInput(m, promptMsg{title: m.inputBuffer, logDB: m.promptType == promptLogAndReset})
134-
} else if msg.Type == tea.KeyEsc {
166+
case tea.KeyEsc:
135167
m.promptActive = false
136168
return m, nil
137-
} else if msg.Type == tea.KeyBackspace {
169+
case tea.KeyBackspace:
138170
if len(m.inputBuffer) > 0 {
139171
m.inputBuffer = m.inputBuffer[:len(m.inputBuffer)-1]
140172
}
141173
return m, nil
142-
} else if msg.Type == tea.KeySpace {
174+
case tea.KeySpace:
143175
// Only allow space for title prompts, not duration
144176
if m.promptType != promptSetDuration {
145177
m.inputBuffer += " "
146178
}
147179
return m, nil
148-
} else if msg.Type == tea.KeyRunes {
180+
case tea.KeyRunes:
149181
for _, r := range msg.Runes {
150182
// For duration prompts, only allow numeric characters
151183
if m.promptType == promptSetDuration {
@@ -162,7 +194,7 @@ func handlePromptKeyInput(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) {
162194
}
163195

164196
// handleTableViewKey handles key input when in table view mode
165-
func handleTableViewKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) {
197+
func handleTableViewKey(m model, _ tea.KeyMsg) (tea.Model, tea.Cmd) {
166198
// Any key exits table view
167199
m.mode = timerView
168200
return m, nil

main.go

Lines changed: 86 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,45 @@ import (
1616
"github.com/charmbracelet/bubbles/key"
1717
"github.com/charmbracelet/bubbles/progress"
1818
tea "github.com/charmbracelet/bubbletea"
19+
"tiny-timer/status"
1920
)
2021

22+
var version = "dev"
23+
2124
func main() {
22-
// Parse CLI flags
25+
title, countUp, clean, debug := parseFlags()
26+
27+
configureLogging(debug)
28+
29+
if clean {
30+
handleCleanFlag()
31+
return
32+
}
33+
34+
if err := initDB(); err != nil {
35+
fmt.Println("Error initializing database:", err)
36+
os.Exit(1)
37+
}
38+
39+
defer closeDBConnection()
40+
41+
targetDuration := calculateTargetDuration(countUp)
42+
keys := createKeyBindings()
43+
m := createModel(title, countUp, targetDuration, keys)
44+
45+
if _, err := tea.NewProgram(m).Run(); err != nil {
46+
fmt.Println("Oh no!", err)
47+
os.Exit(1)
48+
}
49+
}
50+
51+
func parseFlags() (title string, countUp bool, clean bool, debug bool) {
2352
titleFlag := flag.String("title", "", "Optional title for the timer session")
2453
countUpFlag := flag.Bool("count-up", false, "Enable count-up mode (logs task time after completion)")
2554
cleanFlag := flag.Bool("clean", false, "Delete the database and exit")
2655
debugFlag := flag.Bool("debug", false, "Enable debug logging to debug.log")
56+
versionFlag := flag.Bool("version", false, "Print version and exit")
2757

28-
// Customize usage to include positional argument
2958
flag.Usage = func() {
3059
fmt.Fprintf(os.Stderr, "Usage: %s [minutes] [flags]\n\n", os.Args[0])
3160
fmt.Fprintf(os.Stderr, "Positional arguments:\n")
@@ -35,64 +64,62 @@ func main() {
3564
flag.PrintDefaults()
3665
}
3766

38-
// Pre-process arguments to allow positional argument (minutes) before flags
39-
// flag.Parse() stops at the first non-flag argument.
40-
// We check if the first argument is a number and move it to the end if so.
67+
preprocessArgs()
68+
flag.Parse()
69+
70+
if *versionFlag {
71+
fmt.Println("tiny-timer version", version)
72+
os.Exit(0)
73+
}
74+
75+
return *titleFlag, *countUpFlag, *cleanFlag, *debugFlag
76+
}
77+
78+
func preprocessArgs() {
4179
args := os.Args[1:]
4280
if len(args) > 0 {
4381
if _, err := strconv.ParseInt(args[0], 10, 64); err == nil {
44-
// First arg is a number, move it to the end so flag.Parse() can see the flags
4582
minutes := args[0]
4683
newArgs := append(args[1:], minutes)
4784
os.Args = append([]string{os.Args[0]}, newArgs...)
4885
}
4986
}
87+
}
5088

51-
flag.Parse()
52-
53-
// Enable debug logging if flag is set
54-
// tea.LogToFile configures the standard log package to write to debug.log
55-
// All log.Printf() calls throughout the codebase will write to this file
56-
if *debugFlag {
89+
func configureLogging(debug bool) {
90+
if debug {
5791
f, err := tea.LogToFile("debug.log", "debug")
5892
if err != nil {
5993
fmt.Fprintf(os.Stderr, "Warning: Failed to enable debug logging: %v\n", err)
6094
} else {
6195
defer f.Close()
6296
}
6397
} else {
64-
// Silence all log output in normal mode
6598
log.SetOutput(io.Discard)
6699
}
100+
}
67101

68-
if *cleanFlag {
69-
dbPath, err := getDBPath()
70-
if err != nil {
71-
fmt.Println("Error getting database path:", err)
72-
os.Exit(1)
73-
}
74-
if _, err := os.Stat(dbPath); err == nil {
75-
if err := os.Remove(dbPath); err != nil {
76-
fmt.Println("Error deleting database:", err)
77-
os.Exit(1)
78-
}
79-
fmt.Println("Database deleted successfully.")
80-
} else if os.IsNotExist(err) {
81-
fmt.Println("Database does not exist.")
82-
} else {
83-
fmt.Println("Error checking database:", err)
102+
func handleCleanFlag() {
103+
dbPath, err := getDBPath()
104+
if err != nil {
105+
fmt.Println("Error getting database path:", err)
106+
os.Exit(1)
107+
}
108+
if _, err := os.Stat(dbPath); err == nil {
109+
if err := os.Remove(dbPath); err != nil {
110+
fmt.Println("Error deleting database:", err)
84111
os.Exit(1)
85112
}
86-
os.Exit(0)
87-
}
88-
89-
// Initialize database on launch
90-
if err := initDB(); err != nil {
91-
fmt.Println("Error initializing database:", err)
113+
fmt.Println("Database deleted successfully.")
114+
} else if os.IsNotExist(err) {
115+
fmt.Println("Database does not exist.")
116+
} else {
117+
fmt.Println("Error checking database:", err)
92118
os.Exit(1)
93119
}
120+
}
94121

95-
// Read positional arg for duration, or use default
122+
func calculateTargetDuration(countUp bool) int64 {
96123
var targetDurationInMinutes int64 = defaultDurationInMinutes
97124
if flag.NArg() > 0 {
98125
if arg, err := strconv.ParseInt(flag.Arg(0), 10, 64); err == nil && arg > 0 {
@@ -101,11 +128,15 @@ func main() {
101128
}
102129

103130
targetDuration := targetDurationInMinutes * 60
104-
if *countUpFlag && flag.NArg() == 0 {
131+
if countUp && flag.NArg() == 0 {
105132
targetDuration = defaultCountUpDuration
106133
}
107134

108-
keys := keyMap{
135+
return targetDuration
136+
}
137+
138+
func createKeyBindings() keyMap {
139+
return keyMap{
109140
Done: key.NewBinding(
110141
key.WithKeys("d"),
111142
key.WithHelp("d", "done"),
@@ -143,22 +174,27 @@ func main() {
143174
key.WithHelp("backspace", "delete"),
144175
),
145176
}
177+
}
178+
179+
func createModel(title string, countUp bool, targetDuration int64, keys keyMap) model {
180+
prog := progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage())
181+
if countUp {
182+
prog.SetPercent(0)
183+
} else {
184+
prog.SetPercent(1.0)
185+
}
186+
187+
statusCmp := status.NewStatusCmp()
188+
statusCmp.SetKeyMap(keys)
146189

147-
m := model{
148-
progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()),
190+
return model{
191+
progress: prog,
149192
startTime: time.Now().Unix(),
150193
targetDuration: targetDuration,
151-
title: *titleFlag,
152-
countUpMode: *countUpFlag,
194+
title: title,
195+
countUpMode: countUp,
153196
help: newHelpModel(),
154197
keys: keys,
155-
}
156-
157-
// Ensure database connection is closed on exit
158-
defer closeDBConnection()
159-
160-
if _, err := tea.NewProgram(m).Run(); err != nil {
161-
fmt.Println("Oh no!", err)
162-
os.Exit(1)
198+
status: statusCmp,
163199
}
164200
}

model.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/charmbracelet/bubbles/progress"
99
"github.com/charmbracelet/bubbles/table"
1010
tea "github.com/charmbracelet/bubbletea"
11+
"tiny-timer/status"
1112
)
1213

1314
type tickMsg time.Time
@@ -82,6 +83,7 @@ type model struct {
8283
promptType promptType
8384
help help.Model
8485
keys keyMap
86+
status *status.StatusCmp
8587
}
8688

8789
// Start the event loop

0 commit comments

Comments
 (0)