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
20 changes: 11 additions & 9 deletions buffer.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package pulse

import "time"

// Buffer represents a buffer that has been opened during an active coding session.
type Buffer struct {
OpenedClosed []int64
OpenedClosed []time.Time
Filename string
Repository string
Filepath string
Filetype string
}

// NewBuffer creates a new buffer.
func NewBuffer(filename, repo, filetype, filepath string, openedAt int64) Buffer {
func NewBuffer(filename, repo, filetype, filepath string, openedAt time.Time) Buffer {
return Buffer{
OpenedClosed: []int64{openedAt},
OpenedClosed: []time.Time{openedAt},
Filename: filename,
Repository: repo,
Filetype: filetype,
Expand All @@ -21,7 +23,7 @@ func NewBuffer(filename, repo, filetype, filepath string, openedAt int64) Buffer
}

// Open should be called when a buffer is opened.
func (b *Buffer) Open(time int64) {
func (b *Buffer) Open(time time.Time) {
b.OpenedClosed = append(b.OpenedClosed, time)
}

Expand All @@ -31,20 +33,20 @@ func (b *Buffer) IsOpen() bool {
}

// LastOpened returns the last time the buffer was opened.
func (b *Buffer) LastOpened() int64 {
func (b *Buffer) LastOpened() time.Time {
return b.OpenedClosed[len(b.OpenedClosed)-1]
}

// Close should be called when a buffer is closed.
func (b *Buffer) Close(time int64) {
func (b *Buffer) Close(time time.Time) {
b.OpenedClosed = append(b.OpenedClosed, time)
}

// Duration returns the total duration that the buffer has been open.
func (b *Buffer) Duration() int64 {
var duration int64
func (b *Buffer) Duration() time.Duration {
var duration time.Duration
for i := 0; i < len(b.OpenedClosed); i += 2 {
duration += b.OpenedClosed[i+1] - b.OpenedClosed[i]
duration += b.OpenedClosed[i+1].Sub(b.OpenedClosed[i])
}
return duration
}
2 changes: 1 addition & 1 deletion buffer_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (b *bufferStack) files() Files {
sortOrder = append(sortOrder, buffer.Filepath)
pathFile[buffer.Filepath] = fileFromBuffer(buffer)
} else {
file.DurationMs += buffer.Duration()
file.Duration += buffer.Duration()
pathFile[buffer.Filepath] = file
}
}
Expand Down
11 changes: 0 additions & 11 deletions clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ type Clock interface {
Now() time.Time
NewTicker(d time.Duration) (<-chan time.Time, func())
NewTimer(d time.Duration) (<-chan time.Time, func() bool)
GetTime() int64
}

type RealClock struct{}
Expand All @@ -34,10 +33,6 @@ func (c *RealClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) {
return t.C, t.Stop
}

func (c *RealClock) GetTime() int64 {
return time.Now().UTC().UnixMilli()
}

type testTimer struct {
deadline time.Time
ch chan time.Time
Expand Down Expand Up @@ -109,12 +104,6 @@ func (c *TestClock) Now() time.Time {
return c.time
}

func (c *TestClock) GetTime() int64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.time.UnixMilli()
}

func (c *TestClock) NewTicker(d time.Duration) (<-chan time.Time, func()) {
c.mu.Lock()
defer c.mu.Unlock()
Expand Down
38 changes: 20 additions & 18 deletions coding_session.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
package pulse

import "time"

// CodingSession represents an ongoing coding session.
type CodingSession struct {
// startStops is a slice of timestamps representing the start and stop times of
// an ongoing coding session. This ensures accurate time tracking when switching
// between multiple editor processes. We only count time for one at a time.
startStops []int64
startStops []time.Time
bufStack *bufferStack
EditorID string
StartedAt int64
StartedAt time.Time
OS string
Editor string
}

// StartSession creates a new coding session.
func StartSession(editorID string, startedAt int64, os, editor string) *CodingSession {
func StartSession(editorID string, startedAt time.Time, os, editor string) *CodingSession {
return &CodingSession{
startStops: []int64{startedAt},
startStops: []time.Time{startedAt},
bufStack: newBufferStack(),
EditorID: editorID,
StartedAt: startedAt,
Expand All @@ -26,23 +28,23 @@ func StartSession(editorID string, startedAt int64, os, editor string) *CodingSe
}

// Pause should be called when another editor process gains focus.
func (s *CodingSession) Pause(time int64) {
func (s *CodingSession) Pause(time time.Time) {
if currentBuffer := s.bufStack.peek(); currentBuffer != nil {
currentBuffer.Close(time)
}
s.startStops = append(s.startStops, time)
}

// PauseTime is used to determine which coding session to resume.
func (s *CodingSession) PauseTime() int64 {
func (s *CodingSession) PauseTime() time.Time {
if len(s.startStops) == 0 {
return 0
return time.Time{}
}
return s.startStops[len(s.startStops)-1]
}

// Resume should be called when the editor regains focus.
func (s *CodingSession) Resume(time int64) {
func (s *CodingSession) Resume(time time.Time) {
if currentBuffer := s.bufStack.peek(); currentBuffer != nil {
currentBuffer.Open(time)
}
Expand All @@ -64,10 +66,10 @@ func (s *CodingSession) HasBuffers() bool {
}

// Duration returns the total duration of the coding session.
func (s *CodingSession) Duration() int64 {
var duration int64
func (s *CodingSession) Duration() time.Duration {
var duration time.Duration
for i := 0; i < len(s.startStops); i += 2 {
duration += s.startStops[i+1] - s.startStops[i]
duration += s.startStops[i+1].Sub(s.startStops[i])
}
return duration
}
Expand All @@ -79,7 +81,7 @@ func (s *CodingSession) Active() bool {

// End ends the active coding sessions. It sets the total duration in
// milliseconds, and turns the stack of buffers into a slice of files.
func (s *CodingSession) End(endedAt int64) Session {
func (s *CodingSession) End(endedAt time.Time) Session {
if currentBuffer := s.bufStack.peek(); currentBuffer != nil && currentBuffer.IsOpen() {
currentBuffer.Close(endedAt)
}
Expand All @@ -89,11 +91,11 @@ func (s *CodingSession) End(endedAt int64) Session {
}

return Session{
StartedAt: s.StartedAt,
EndedAt: endedAt,
DurationMs: s.Duration(),
OS: s.OS,
Editor: s.Editor,
Files: s.bufStack.files(),
StartedAt: s.StartedAt,
EndedAt: endedAt,
Duration: s.Duration(),
OS: s.OS,
Editor: s.Editor,
Files: s.bufStack.files(),
}
}
39 changes: 24 additions & 15 deletions coding_session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,58 @@ package pulse_test

import (
"testing"
"time"

"github.com/creativecreature/pulse"
)

func TestActiveSession(t *testing.T) {
t.Parallel()

mockClock := pulse.NewTestClock(time.Now())

// Start a new coding session
activeSession := pulse.StartSession("1337", 100, "linux", "nvim")
activeSession := pulse.StartSession("1337", mockClock.Now(), "linux", "nvim")

// Open the first buffer
// Open the first buffer, and wait 400ms.
bufferOne := pulse.NewBuffer(
"init.lua",
"dotfiles",
"lua",
"dotfiles/editors/nvim/init.lua",
101,
mockClock.Now(),
)
activeSession.PushBuffer(bufferOne)
mockClock.Add(400 * time.Millisecond)

// Open a second buffer.
// Open a second buffer, and wait 200ms.
bufferTwo := pulse.NewBuffer(
"plugins.lua",
"dotfiles",
"lua",
"dotfiles/editors/nvim/plugins.lua",
301,
mockClock.Now(),
)
activeSession.PushBuffer(bufferTwo)
mockClock.Add(200 * time.Millisecond)

// Open the same file as buffer one. The total duration for these
// Open the first buffer again. The total duration for these
// buffers should be merged when we end the coding session.
bufferThree := pulse.NewBuffer(
"init.lua",
"dotfiles",
"lua",
"dotfiles/editors/nvim/init.lua",
611,
mockClock.Now(),
)
activeSession.PushBuffer(bufferThree)
mockClock.Add(time.Millisecond * 100)

endedAt := int64(700)
finishedSession := activeSession.End(endedAt)
finishedSession := activeSession.End(mockClock.Now())

// Assert that the duration of the session was set correctly.
if finishedSession.DurationMs != 600 {
t.Errorf("Expected the session duration to be 600, got %d", finishedSession.DurationMs)
if finishedSession.Duration.Milliseconds() != 700 {
t.Errorf("Expected the session duration to be 600, got %d", finishedSession.Duration.Milliseconds())
}

// Assert that the buffers have been merged into files.
Expand All @@ -55,11 +62,13 @@ func TestActiveSession(t *testing.T) {
}

// Assert that the merged buffers has both durations.
if finishedSession.Files[0].DurationMs != 289 {
t.Errorf("Expected the merged duration for init.lua to be 289, got %d", finishedSession.Files[0].DurationMs)
initLuaDuration := finishedSession.Files[0].Duration.Milliseconds()
if initLuaDuration != 500 {
t.Errorf("Expected the merged duration for init.lua to be 500, got %d", initLuaDuration)
}

if finishedSession.Files[1].DurationMs != 310 {
t.Errorf("Expected the duration for plugins.lua to be 310, got %d", finishedSession.Files[1].DurationMs)
pluginsLuaDuration := finishedSession.Files[1].Duration.Milliseconds()
if pluginsLuaDuration != 200 {
t.Errorf("Expected the duration for plugins.lua to be 200, got %d", pluginsLuaDuration)
}
}
4 changes: 2 additions & 2 deletions disk/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ func (s *Storage) dayDir() (string, error) {

// filename returns the name we'll use when writing the session to disk.
func (s *Storage) filename(session pulse.Session) string {
startTime := time.UnixMilli(session.StartedAt).Format("15:04:05.000")
endTime := time.UnixMilli(session.EndedAt).Format("15:04:05.000")
startTime := session.StartedAt.Format("15:04:05.000")
endTime := session.EndedAt.Format("15:04:05.000")
return fmt.Sprintf("%s-%s.json", startTime, endTime)
}

Expand Down
24 changes: 12 additions & 12 deletions disk/disk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,34 +47,34 @@ func TestStorageReadWriteClean(t *testing.T) {
now := time.Now()
sessions := pulse.Sessions{
pulse.Session{
StartedAt: now.UnixMilli(),
EndedAt: now.Add(time.Hour).UnixMilli(),
DurationMs: time.Hour.Milliseconds(),
OS: "linux",
Editor: "nvim",
StartedAt: now,
EndedAt: now.Add(time.Hour),
Duration: time.Hour,
OS: "linux",
Editor: "nvim",
Files: pulse.Files{
pulse.File{
Name: "main.go",
Path: "/cmd/main.go",
Repository: "pulse",
Filetype: "go",
DurationMs: time.Hour.Milliseconds(),
Duration: time.Hour,
},
},
},
pulse.Session{
StartedAt: now.Add(time.Minute).UnixMilli(),
EndedAt: now.Add(time.Minute * 11).UnixMilli(),
DurationMs: time.Hour.Milliseconds(),
OS: "linux",
Editor: "nvim",
StartedAt: now.Add(time.Minute),
EndedAt: now.Add(time.Minute * 11),
Duration: time.Hour,
OS: "linux",
Editor: "nvim",
Files: pulse.Files{
pulse.File{
Name: "main.go",
Path: "/cmd/main.go",
Repository: "pulse",
Filetype: "go",
DurationMs: time.Minute.Milliseconds() * 10,
Duration: time.Minute * 10,
},
},
},
Expand Down
14 changes: 8 additions & 6 deletions file.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package pulse

import "time"

// File represents a file that has been opened during a coding session.
type File struct {
Name string `json:"name"`
Path string `json:"path"`
Repository string `json:"repository"`
Filetype string `json:"filetype"`
DurationMs int64 `json:"duration_ms"`
Name string `json:"name"`
Path string `json:"path"`
Repository string `json:"repository"`
Filetype string `json:"filetype"`
Duration time.Duration `json:"duration"`
}

// fileFromBuffer turns a code buffer into a file.
Expand All @@ -16,7 +18,7 @@ func fileFromBuffer(b Buffer) File {
Path: b.Filepath,
Repository: b.Repository,
Filetype: b.Filetype,
DurationMs: b.Duration(),
Duration: b.Duration(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func repositoryPathFile(sessions []Session) map[string]map[string]AggregatedFile
Name: file.Name,
Path: file.Path,
Filetype: file.Filetype,
DurationMs: file.DurationMs,
DurationMs: file.Duration.Milliseconds(),
}

// Check if it is the first time we're seeing a repository
Expand Down
Loading