pulse

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 28, 2024 License: MIT Imports: 18 Imported by: 0

README

pulse: like a fitness tracker for your coding sessions

Go Reference Go Report Card Test

This repository contains all of the code for gathering the data which I display on my website

Screenshot of website

Screenshot of website

How it works

After spending some time debugging different language servers in Neovim, I felt inspired to write my own server that would simply parse metadata and aggregate statistics about my coding sessions.

I run the server from this repository as a daemon, and it receives remote procedure calls from the neovim plugin pertaining to events such as the opening of buffers, windows gaining focus, the initiation of new nvim processes, etc.

These calls contains the path to the buffer, which the server parses and writes to a log-structured key-value store. The store is a work in progress, but it now includes some core features such as hash indexes, segmentation, and compaction.

The server runs a background job which requests all of the buffers from the KV store, and proceeds to aggregate them to a remote database. I chose this approach primarily because I wanted to avoid surpassing the limits set by the free tier for the MongoDB database.

The only things that aren't included in this repository is the API which retrieves the data and the website that displays it. The website has been the most challenging part so far because I wanted it to have a unique look and feel and to build all of the components from scratch. I'm in the process of making it open source, but there are still a few things that I'd like to clean up first.

Running this project

1. Download the binaries

Download and unpack the server and client binaries from the releases. Next, you'll want to make sure that they are reachable from your $PATH.

2. Create a configuration file

Create a configuration file. It should be located at $HOME/.pulse/config.yaml

server:
  name: "pulse-server"
  hostname: "localhost"
  port: "1122"
  aggregationInterval: "10m"
  segmentationInterval: "5m"
database:
  uri: "mongodb+srv://<USERNAME>:[email protected]/?retryWrites=true"
  name: "pulse"
  collection: "sessions"

3. Launch the server as a daemon

On linux, you can setup a systemd service to run the server, and on macOS you can create a launch daemon.

I'm using a Mac, and my launch daemon configuration looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>

    <key>Label</key>
    <string>dev.conner.pulse.plist</string>

    <key>RunAtLoad</key>
    <true/>

    <key>StandardErrorPath</key>
		<string>/Users/conner/.pulse/logs/stderr.log</string>

    <key>StandardOutPath</key>
		<string>/Users/conner/.pulse/logs/stdout.log</string>

    <key>EnvironmentVariables</key>
    <dict>
      <key>PATH</key>
      <string><![CDATA[/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin]]></string>
    </dict>

    <key>WorkingDirectory</key>
    <string>/Users/conner</string>

    <key>ProgramArguments</key>
    <array>
			<string>/Users/conner/bin/pulse-server</string>
    </array>

		<key>KeepAlive</key>
    <true/>

  </dict>
</plist>

4. Install the neovim plugin

Here is an example using lazy.nvim:

return {
	-- Does not require any configuration.
	{ "creativecreature/pulse" },
}

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Filename

func Filename(index int) string

Filename generates a filename based on the given index.

func Index

func Index(filename string) int

Index extracts an index from a filename.

func NewLogger

func NewLogger() *log.Logger

New wraps the construction of a charmbracelet logger in order to achieve coherent styles and settings.

func TruncateDay

func TruncateDay(timestamp int64) int64

TruncateDay truncates the timestamp to the start of the day.

func TruncateMonth

func TruncateMonth(timestamp int64) int64

TruncateMonth truncates the timestamp to the start of the month.

func TruncateWeek

func TruncateWeek(timestamp int64) int64

TruncateWeek truncates the timestamp to the start of the week.

func TruncateYear

func TruncateYear(timestamp int64) int64

TruncateYear truncates the timestamp to the start of the year.

Types

type Buffer

type Buffer struct {
	OpenedAt   time.Time     `json:"-"`
	ClosedAt   time.Time     `json:"-"`
	Duration   time.Duration `json:"duration"`
	Filename   string        `json:"filename"`
	Filepath   string        `json:"filepath"`
	Filetype   string        `json:"filetype"`
	Repository string        `json:"repository"`
}

Buffer rerpresents a buffer that has been edited during a coding session.

func NewBuffer

func NewBuffer(filename, repo, filetype, filepath string, openedAt time.Time) Buffer

NewBuffer creates a new buffer.

func (*Buffer) Close

func (b *Buffer) Close(closedAt time.Time)

Close should be called when the coding session ends, or another buffer is opened.

func (*Buffer) Key

func (b *Buffer) Key() string

Key returns a unique identifier for the buffer.

func (*Buffer) Merge

func (b *Buffer) Merge(other Buffer) Buffer

Merge takes two buffers, merges them, and returns the result.

type Buffers

type Buffers []Buffer

Buffers represents a slice of buffers that have been edited during a coding session.

func (Buffers) Len

func (b Buffers) Len() int

func (Buffers) Less

func (b Buffers) Less(i, j int) bool

func (Buffers) Swap

func (b Buffers) Swap(i, j int)

type CodingSession

type CodingSession struct {
	ID           string       `bson:"_id,omitempty"`
	Period       Period       `bson:"period"`
	EpochDateMs  int64        `bson:"date"`
	DateString   string       `bson:"date_string"`
	TotalTimeMs  int64        `bson:"total_time_ms"`
	Repositories Repositories `bson:"repositories"`
}

CodingSession represents a coding session that has been aggregated for a given time period (day, week, month, year).

func NewCodingSession

func NewCodingSession(buffers Buffers, now time.Time) CodingSession

type CodingSessions

type CodingSessions []CodingSession

CodingSessions represents a slice of coding sessions.

func (CodingSessions) MergeByDay

func (s CodingSessions) MergeByDay() CodingSessions

MergeByDay merges sessions that occurred the same day.

func (CodingSessions) MergeByMonth

func (s CodingSessions) MergeByMonth() CodingSessions

MergeByWeek merges sessions that occurred the same month.

func (CodingSessions) MergeByWeek

func (s CodingSessions) MergeByWeek() CodingSessions

MergeByWeek merges sessions that occurred the same week.

func (CodingSessions) MergeByYear

func (s CodingSessions) MergeByYear() CodingSessions

MergeByYear merges sessions that occurred the same year.

type Config

type Config struct {
	Server struct {
		Name                 string
		Hostname             string
		Port                 string
		AggregationInterval  time.Duration
		SegmentationInterval time.Duration
		SegmentSizeKB        int
	}
	Database struct {
		Name       string
		URI        string
		Collection string
	}
}

func ParseConfig

func ParseConfig() (*Config, error)

type Event

type Event struct {
	EditorID string
	Path     string
	Editor   string
	OS       string
}

Event represents the events we receive from the editor.

type File

type File struct {
	Name       string `bson:"name"`
	Path       string `bson:"path"`
	Filetype   string `bson:"filetype"`
	DurationMs int64  `bson:"duration_ms"`
}

File represents a file that has been aggregated for a given time period (day, week, month, year).

type Files

type Files []File

Files represents a slice of files that has been aggregated for a given time period (day, week, month, year).

type GitFile

type GitFile struct {
	Name       string
	Filetype   string
	Repository string
	Path       string
}

GitFile represents a file within a git repository.

type HashIndex

type HashIndex map[string]int64

HashIndex is a map of keys to offsets in the segment file.

type LogDB

type LogDB struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

LogDB is a simple key-value store that persists data to a log file.

func NewDB

func NewDB(dirPath string, segmentSizeKB int, c clock.Clock) *LogDB

NewDB creates a new log database.

func (*LogDB) Aggregate

func (db *LogDB) Aggregate() map[string][]byte

Aggregate gathers all the unique key-value pairs in the database, and then removes all the segments and resets the state.

func (*LogDB) Get

func (db *LogDB) Get(key string) ([]byte, bool)

Get retrieves a value from the database.

func (*LogDB) GetAllUnique

func (db *LogDB) GetAllUnique() map[string][]byte

func (*LogDB) MustSet

func (db *LogDB) MustSet(key string, value []byte)

MustSet writes a key-value pair to the log file and panics on error.

func (*LogDB) RunSegmentations

func (db *LogDB) RunSegmentations(ctx context.Context, segmentationInterval time.Duration)

RunSegmentations starts the database's compaction process.

func (*LogDB) Set

func (db *LogDB) Set(key string, value []byte) error

Set writes a key-value pair to the log file.

type Period

type Period int8

Period represents the time period for which the coding sessions have been aggregated.

const (
	Day Period = iota
	Week
	Month
	Year
)

type Record

type Record struct {
	Key   string `json:"key"`
	Value []byte `json:"value"`
}

Record represents a key-value pair in our database.

type RecordWithOffset

type RecordWithOffset struct {
	Record
	Offset int64
}

RecordWithOffset holds a record and its offset in the log file.

type Repositories

type Repositories []Repository

Repositories represents a list of git repositories.

type Repository

type Repository struct {
	Name       string `bson:"name"`
	Files      Files  `bson:"files"`
	DurationMs int64  `bson:"duration_ms"`
}

Repository represents a git repository. A coding session might open files across any number of repos. The files of the coding session are later grouped by repository.

type Segment

type Segment struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

Segment represents a segment in our log database. Each segment has its own file descriptor and hash index.

Directories

Path Synopsis
Package client is used to send remote procedure calls to the server.
Package client is used to send remote procedure calls to the server.
cmd
client command
server command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL