Skip to content

Commit 8d1a333

Browse files
authored
Merge pull request #4 from jms-guy/waka_integration
Waka integration
2 parents e2c71b8 + 4736993 commit 8d1a333

34 files changed

Lines changed: 691 additions & 156 deletions

.github/workflows/CD.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ jobs:
1515
- name: Build binaries
1616
run: |
1717
# Service binaries
18-
GOOS=windows go build -o timekeep-service.exe ./cmd/service
19-
GOOS=linux go build -o timekeepd ./cmd/service
18+
GOOS=windows go build -ldflags "-X github.com/jms-guy/timekeep/cmd/service/internal/events.Version=${{ github.ref_name }}" -o timekeep-service.exe ./cmd/service
19+
GOOS=linux go build -ldflags "-X github.com/jms-guy/timekeep/cmd/service/internal/events.Version=${{ github.ref_name }}" -o timekeepd ./cmd/service
2020
2121
# CLI binaries
22-
GOOS=windows go build -o timekeep.exe ./cmd/cli
23-
GOOS=linux go build -o timekeep ./cmd/cli
22+
GOOS=windows go build -ldflags "-X main.Version=${{ github.ref_name }}" -o timekeep.exe ./cmd/cli
23+
GOOS=linux go build -ldflags "-X main.Version=${{ github.ref_name }}" -o timekeep ./cmd/cli
2424
2525
- name: Prepare release assets
2626
run: |

README.md

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ A process activity tracker, it runs as a background service recording start/stop
1212
- [How It Works](#how-it-works)
1313
- [Usage](#usage)
1414
- [Installation](#installation)
15+
- [WakaTime](#wakatime)
16+
- [File Locations](#file-locations)
1517
- [Current Limitations](#current-limitations)
1618
- [To-Do](#to-do)
1719
- [Contributing & Issues](#contributing--issues)
@@ -38,16 +40,13 @@ A process activity tracker, it runs as a background service recording start/stop
3840
```powershell
3941
timekeep add notepad.exe # Add notepad
4042
timekeep ls # List currently tracked programs
41-
Programs currently being tracked:
4243
• notepad.exe
4344
timekeep info notepad.exe # Basic info for program sessions
44-
Statistics for notepad.exe:
4545
• Current Lifetime: 19h 41m
4646
• Total sessions to date: 4
4747
• Last Session: 2025-09-26 11:25 - 2025-09-26 11:26 (21 seconds)
4848
• Average session length: 4h 55m
4949
timekeep history notepad.exe # Session history for program
50-
Session history for notepad.exe:
5150
notepad.exe | 2025-09-26 11:25 - 2025-09-26 11:26 | Duration: 21 seconds
5251
notepad.exe | 2025-09-24 13:49 - 2025-09-24 13:50 | Duration: 39 seconds
5352
notepad.exe | 2025-09-23 11:18 - 2025-09-23 11:19 | Duration: 56 seconds
@@ -79,7 +78,7 @@ GOOS=windows go build -o timekeep-service.exe ./cmd/service
7978
GOOS=windows go build -o timekeep.exe ./cmd/cli
8079
8180
# Install and start service (Run as Administrator)
82-
sc.exe create timekeep binPath= "C:\Program Files\Timekeep\timekeep-service.exe" start= auto
81+
sc.exe create timekeep binPath= "C:\Program Files\Timekeep\timekeep-service.exe" start= auto # Assuming this is the location of service binary
8382
sc.exe start timekeep
8483
8584
# Verify service is running
@@ -165,6 +164,58 @@ sudo rm /usr/local/bin/timekeepd /usr/local/bin/timekeep
165164
sudo systemctl daemon-reload
166165
```
167166

167+
## WakaTime
168+
Timekeep now integrates with [WakaTime](https://wakatime.com), allowing users to track external program usage alongside their IDE and web-browsing stats. To enable WakaTime integration, users must:
169+
1. Have a WakaTime account
170+
2. Have [wakatime-cli](https://github.com/wakatime/wakatime-cli) installed on their machine
171+
172+
Enable integration through timekeep. Set your WakaTime API key and wakatime-cli path either directly in the Timekeep [config](https://github.com/jms-guy/timekeep/blob/waka_integration/README.md#file-locations) file, or provide them through flags:
173+
`timekeep wakatime enable --api-key "KEY" --set-path "PATH"`
174+
175+
```json
176+
{
177+
"wakatime": {
178+
"enabled": true,
179+
"api_key": "APIKEY",
180+
"cli_path": "PATH"
181+
}
182+
}
183+
```
184+
185+
**The wakatime-cli path must be an absolute path.**
186+
187+
After enabling, wakatime-cli heartbeats will be sent containing tracking data for given programs. Note, that only programs added to Timekeep with a given category will have data sent to WakaTime.
188+
189+
`timekeep add notepad.exe --category notes`
190+
191+
If no category is set for a program, it will still be tracked locally, but no data for it will be sent out.
192+
193+
List of categories accepted(defined [here](https://github.com/wakatime/wakatime-cli/blob/75ed1c3d905fc77a5039817458298c9ac44853a3/cmd/root.go#L74)):
194+
```bash
195+
"Category of this heartbeat activity. Can be \"coding\", \"ai coding\","+
196+
" \"building\", \"indexing\", \"debugging\", \"learning\", \"notes\","+
197+
" \"meeting\", \"planning\", \"researching\", \"communicating\", \"supporting\","+
198+
" \"advising\", \"running tests\", \"writing tests\", \"manual testing\","+
199+
" \"writing docs\", \"code reviewing\", \"browsing\","+
200+
" \"translating\", or \"designing\".
201+
```
202+
203+
Disable integration with:
204+
`timekeep wakatime disable`
205+
206+
## File Locations
207+
- **Logs**
208+
- **Windows**: *C:\ProgramData\Timekeep\logs*
209+
- **Linux**: */var/log/timekeep*
210+
211+
- **Config**
212+
- **Windows**: *C:\ProgramData\Timekeep\config*
213+
- **Linux**: *~/.local/config/timekeep*
214+
215+
- **Database**
216+
- **Windows**: *C:\ProgramData\Timekeep*
217+
- **Linux**: *~/.local/share/timekeep*
218+
168219
## Current Limitations
169220
- Linux - Very short-lived processes can be missed by polling (poll interval currently default 1s)
170221
- Linux - Program basenames may collide (different binaries with same name are treated as same program)

cmd/cli/cli_setup.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
package main
22

33
import (
4+
"github.com/jms-guy/timekeep/internal/config"
45
"github.com/jms-guy/timekeep/internal/repository"
56
mysql "github.com/jms-guy/timekeep/sql"
67
)
78

9+
var Version = "dev"
10+
811
type CLIService struct {
912
PrRepo repository.ProgramRepository
1013
AsRepo repository.ActiveRepository
1114
HsRepo repository.HistoryRepository
1215
ServiceCmd ServiceCommander
1316
CmdExe CommandExecutor
17+
Config *config.Config
1418
Version string
1519
}
1620

17-
var currentVersion = "v1.0.0"
18-
1921
// Creates new CLI service instance
2022
func CreateCLIService(pr repository.ProgramRepository, ar repository.ActiveRepository, hr repository.HistoryRepository, sc ServiceCommander, cmdE CommandExecutor) *CLIService {
2123
return &CLIService{
@@ -24,7 +26,7 @@ func CreateCLIService(pr repository.ProgramRepository, ar repository.ActiveRepos
2426
HsRepo: hr,
2527
ServiceCmd: sc,
2628
CmdExe: cmdE,
27-
Version: currentVersion,
29+
Version: Version,
2830
}
2931
}
3032

@@ -38,6 +40,13 @@ func CLIServiceSetup() (*CLIService, error) {
3840

3941
service := CreateCLIService(store, store, store, &realServiceCommander{}, &realCommandExecutor{})
4042

43+
config, err := config.Load()
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
service.Config = config
49+
4150
return service, nil
4251
}
4352

cmd/cli/commands.go

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,27 @@ import (
1111
)
1212

1313
// Adds programs into the database, and sends communication to service to being tracking them
14-
func (s *CLIService) AddPrograms(ctx context.Context, args []string) error {
15-
var addedPrograms []string
14+
func (s *CLIService) AddPrograms(ctx context.Context, args []string, category string) error {
15+
categoryNull := sql.NullString{
16+
String: category,
17+
Valid: category != "",
18+
}
19+
1620
for _, program := range args {
17-
err := s.PrRepo.AddProgram(ctx, strings.ToLower(program))
21+
err := s.PrRepo.AddProgram(ctx, database.AddProgramParams{
22+
Name: strings.ToLower(program),
23+
Category: categoryNull,
24+
})
1825
if err != nil {
1926
return fmt.Errorf("error adding program %s: %w", program, err)
2027
}
21-
addedPrograms = append(addedPrograms, program)
2228
}
2329

2430
err := s.ServiceCmd.WriteToService()
2531
if err != nil {
2632
return fmt.Errorf("programs added but failed to notify service: %w", err)
2733
}
2834

29-
fmt.Printf("Added %d program(s) to track\n", len(addedPrograms))
3035
return nil
3136
}
3237

@@ -43,25 +48,21 @@ func (s *CLIService) RemovePrograms(ctx context.Context, args []string, all bool
4348
return fmt.Errorf("error alerting service of program removal: %w", err)
4449
}
4550

46-
fmt.Println("All programs removed from tracking")
4751
return nil
4852
}
4953

50-
var removedPrograms []string
5154
for _, program := range args {
5255
err := s.PrRepo.RemoveProgram(ctx, strings.ToLower(program))
5356
if err != nil {
5457
return fmt.Errorf("error removing program %s: %w", program, err)
5558
}
56-
removedPrograms = append(removedPrograms, program)
5759
}
5860

5961
err := s.ServiceCmd.WriteToService()
6062
if err != nil {
6163
return fmt.Errorf("programs removed but failed to notify service: %w", err)
6264
}
6365

64-
fmt.Printf("Removed %d program(s) from tracking\n", len(removedPrograms))
6566
return nil
6667
}
6768

@@ -73,11 +74,9 @@ func (s *CLIService) GetList(ctx context.Context) error {
7374
}
7475

7576
if len(programs) == 0 {
76-
fmt.Println("No programs are currently being tracked")
7777
return nil
7878
}
7979

80-
fmt.Println("Programs currently being tracked:")
8180
for _, program := range programs {
8281
fmt.Printf(" • %s\n", program)
8382
}
@@ -93,7 +92,6 @@ func (s *CLIService) GetAllInfo(ctx context.Context) error {
9392
}
9493

9594
if len(programs) == 0 {
96-
fmt.Println("No programs are currently being tracked")
9795
return nil
9896
}
9997

@@ -126,7 +124,7 @@ func (s *CLIService) GetInfo(ctx context.Context, args []string) error {
126124
lastSession, err := s.HsRepo.GetLastSessionForProgram(ctx, program.Name)
127125
if err != nil {
128126
if err == sql.ErrNoRows {
129-
fmt.Printf("Statistics for %s:\n", program.Name)
127+
fmt.Printf(" • Category: %s", program.Category.String)
130128
s.formatDuration(" • Current Lifetime: ", duration)
131129
fmt.Printf(" • Total sessions to date: 0\n")
132130
fmt.Printf(" • Last Session: No sessions recorded yet\n")
@@ -141,7 +139,7 @@ func (s *CLIService) GetInfo(ctx context.Context, args []string) error {
141139
return fmt.Errorf("error getting history count for %s: %w", program.Name, err)
142140
}
143141

144-
fmt.Printf("Statistics for %s:\n", program.Name)
142+
fmt.Printf(" • Category: %s", program.Category.String)
145143
s.formatDuration(" • Current Lifetime: ", duration)
146144
fmt.Printf(" • Total sessions to date: %d\n", sessionCount)
147145

@@ -184,16 +182,9 @@ func (s *CLIService) GetSessionHistory(ctx context.Context, args []string, date,
184182
}
185183

186184
if len(history) == 0 {
187-
fmt.Println("No session history present")
188185
return nil
189186
}
190187

191-
if programName != "" {
192-
fmt.Printf("Session history for %s: \n", programName)
193-
} else {
194-
fmt.Println("Session history: ")
195-
}
196-
197188
for _, session := range history {
198189
printSession(session)
199190
}
@@ -208,7 +199,6 @@ func (s *CLIService) ResetStats(ctx context.Context, args []string, all bool) er
208199
if err != nil {
209200
return err
210201
}
211-
fmt.Println("All session records reset")
212202

213203
} else {
214204
if len(args) == 0 {
@@ -223,12 +213,11 @@ func (s *CLIService) ResetStats(ctx context.Context, args []string, all bool) er
223213
}
224214
}
225215

226-
fmt.Printf("Session records for %d programs reset\n", len(args))
227216
}
228217

229218
err := s.ServiceCmd.WriteToService()
230219
if err != nil {
231-
fmt.Printf("Warning: Failed to notify service of reset: %v\n", err)
220+
fmt.Printf("Warning: Failed to notify service: %v\n", err)
232221
}
233222

234223
return nil
@@ -279,11 +268,9 @@ func (s *CLIService) GetActiveSessions(ctx context.Context) error {
279268
return fmt.Errorf("error getting active sessions: %w", err)
280269
}
281270
if len(activeSessions) == 0 {
282-
fmt.Println("No active sessions.")
283271
return nil
284272
}
285273

286-
fmt.Println("Active sessions: ")
287274
for _, session := range activeSessions {
288275
duration := time.Since(session.StartTime)
289276
sessionDetails := fmt.Sprintf(" • %s - ", session.ProgramName)
@@ -299,3 +286,61 @@ func (s *CLIService) GetVersion() error {
299286
fmt.Println(s.Version)
300287
return nil
301288
}
289+
290+
// Changes config to enable WakaTime with API key
291+
func (s *CLIService) EnableWakaTime(apiKey, path string) error {
292+
if s.Config.WakaTime.Enabled {
293+
return nil
294+
}
295+
296+
if apiKey != "" {
297+
s.Config.WakaTime.APIKey = apiKey
298+
}
299+
300+
if s.Config.WakaTime.APIKey == "" {
301+
return fmt.Errorf("WakaTime API key required. Use flag: --api-key <key>")
302+
}
303+
304+
if path != "" {
305+
s.Config.WakaTime.CLIPath = path
306+
}
307+
308+
if s.Config.WakaTime.CLIPath == "" {
309+
return fmt.Errorf("wakatime-cli path required. Use flag: --set-path <path>")
310+
}
311+
312+
s.Config.WakaTime.Enabled = true
313+
314+
if err := s.saveAndNotify(); err != nil {
315+
return err
316+
}
317+
318+
return nil
319+
}
320+
321+
// Disables WakaTime in config
322+
func (s *CLIService) DisableWakaTime() error {
323+
if !s.Config.WakaTime.Enabled {
324+
return nil
325+
}
326+
327+
s.Config.WakaTime.Enabled = false
328+
329+
if err := s.saveAndNotify(); err != nil {
330+
return err
331+
}
332+
333+
return nil
334+
}
335+
336+
// Sets wakatime-cli file path
337+
func (s *CLIService) SetCLIPath(args []string) error {
338+
newPath := args[0]
339+
s.Config.WakaTime.CLIPath = newPath
340+
341+
if err := s.saveAndNotify(); err != nil {
342+
return err
343+
}
344+
345+
return nil
346+
}

cmd/cli/commands_helpers.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,14 @@ func printSession(session database.SessionHistory) {
141141
fmt.Printf("%dh %dm\n", hours, minutes)
142142
}
143143
}
144+
145+
// Helper to save config and send refresh command to service
146+
func (s *CLIService) saveAndNotify() error {
147+
if err := s.Config.Save(); err != nil {
148+
return fmt.Errorf("failed to save config: %w", err)
149+
}
150+
if err := s.ServiceCmd.WriteToService(); err != nil {
151+
return fmt.Errorf("config saved but failed to notify service: %w", err)
152+
}
153+
return nil
154+
}

0 commit comments

Comments
 (0)