Initial commit
This commit is contained in:
commit
746cebf509
26 changed files with 757 additions and 0 deletions
19
Makefile
Normal file
19
Makefile
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
BINARY_NAME=worktime
|
||||
VERSION=$(shell git describe --tags $(shel git rev-list --tags --max-count=1) || echo "v0.0.0")
|
||||
COMMIT=$(shell git rev-parse --short HEAD || echo "?")
|
||||
|
||||
all: build test
|
||||
|
||||
build:
|
||||
go build -ldflags "-X 'git.zervo.org/zervo/worktime/internal/cli/commands/version.Version=$(VERSION)' -X 'git.zervo.org/zervo/worktime/internal/cli/commands/version.Commit=$(COMMIT)'" -o $(BINARY_NAME) cmd/worktime/main.go
|
||||
|
||||
test:
|
||||
go test -v cmd/worktime/main.go
|
||||
|
||||
run:
|
||||
go build -ldflags "-X 'git.zervo.org/zervo/worktime/internal/cli/commands/version.Version=$(VERSION)' -X 'git.zervo.org/zervo/worktime/internal/cli/commands/version.Commit=$(COMMIT)'" -o $(BINARY_NAME) cmd/worktime/main.go
|
||||
./$(BINARY_NAME) $(ARGS)
|
||||
|
||||
clean:
|
||||
go clean
|
||||
rm $(BINARY_NAME)
|
||||
9
cmd/worktime/main.go
Normal file
9
cmd/worktime/main.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"git.zervo.org/zervo/worktime/internal/cli/commands"
|
||||
)
|
||||
|
||||
func main() {
|
||||
commands.Execute()
|
||||
}
|
||||
19
go.mod
Normal file
19
go.mod
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
module git.zervo.org/zervo/worktime
|
||||
|
||||
go 1.25.1
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
)
|
||||
25
go.sum
Normal file
25
go.sum
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
60
internal/cli/color/color.go
Normal file
60
internal/cli/color/color.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package color
|
||||
|
||||
const (
|
||||
// Reset all attributes
|
||||
Reset = "\033[0m"
|
||||
|
||||
// Regular Colors
|
||||
Black = "\033[30m"
|
||||
Red = "\033[31m"
|
||||
Green = "\033[32m"
|
||||
Yellow = "\033[33m"
|
||||
Blue = "\033[34m"
|
||||
Magenta = "\033[35m"
|
||||
Cyan = "\033[36m"
|
||||
White = "\033[37m"
|
||||
|
||||
// Bright Colors
|
||||
BrightBlack = "\033[90m"
|
||||
BrightRed = "\033[91m"
|
||||
BrightGreen = "\033[92m"
|
||||
BrightYellow = "\033[93m"
|
||||
BrightBlue = "\033[94m"
|
||||
BrightMagenta = "\033[95m"
|
||||
BrightCyan = "\033[96m"
|
||||
BrightWhite = "\033[97m"
|
||||
|
||||
// Background Colors
|
||||
BgBlack = "\033[40m"
|
||||
BgRed = "\033[41m"
|
||||
BgGreen = "\033[42m"
|
||||
BgYellow = "\033[43m"
|
||||
BgBlue = "\033[44m"
|
||||
BgMagenta = "\033[45m"
|
||||
BgCyan = "\033[46m"
|
||||
BgWhite = "\033[47m"
|
||||
|
||||
// Bright Background Colors
|
||||
BgBrightBlack = "\033[100m"
|
||||
BgBrightRed = "\033[101m"
|
||||
BgBrightGreen = "\033[102m"
|
||||
BgBrightYellow = "\033[103m"
|
||||
BgBrightBlue = "\033[104m"
|
||||
BgBrightMagenta = "\033[105m"
|
||||
BgBrightCyan = "\033[106m"
|
||||
BgBrightWhite = "\033[107m"
|
||||
|
||||
// Text styles
|
||||
Bold = "\033[1m"
|
||||
Dim = "\033[2m"
|
||||
Italic = "\033[3m"
|
||||
Underline = "\033[4m"
|
||||
Blink = "\033[5m"
|
||||
Reverse = "\033[7m"
|
||||
Hidden = "\033[8m"
|
||||
Strike = "\033[9m"
|
||||
)
|
||||
|
||||
func Colorize(s string, color string) string {
|
||||
return color + s + Reset
|
||||
}
|
||||
52
internal/cli/commands/group/add.go
Normal file
52
internal/cli/commands/group/add.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.zervo.org/zervo/worktime/internal/database"
|
||||
"git.zervo.org/zervo/worktime/internal/database/models"
|
||||
"github.com/spf13/cobra"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add [flags] (name) [description]",
|
||||
Short: "Add a group",
|
||||
Long: `Create a new locally stored project group`,
|
||||
Args: cobra.MatchAll(cobra.MinimumNArgs(1), cobra.MaximumNArgs(2)),
|
||||
RunE: addExecute,
|
||||
}
|
||||
|
||||
func addExecute(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
desc := ""
|
||||
|
||||
if len(args) > 1 {
|
||||
desc = args[1]
|
||||
}
|
||||
|
||||
if len(name) > 255 {
|
||||
return fmt.Errorf("given name is too long (%d): maximum allowed length is 255", len(name))
|
||||
}
|
||||
|
||||
db := database.DB()
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := gorm.G[models.ProjectGroup](db).Where("name = ?", name).First(ctx)
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return fmt.Errorf("group with this name already exists")
|
||||
}
|
||||
|
||||
group := models.ProjectGroup{
|
||||
Name: name,
|
||||
Description: desc,
|
||||
}
|
||||
|
||||
err = gorm.G[models.ProjectGroup](db).Create(ctx, &group)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add group: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
57
internal/cli/commands/group/edit.go
Normal file
57
internal/cli/commands/group/edit.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.zervo.org/zervo/worktime/internal/database"
|
||||
"git.zervo.org/zervo/worktime/internal/database/models"
|
||||
"github.com/spf13/cobra"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var editCmd = &cobra.Command{
|
||||
Use: "edit [flags] (group) (field) (value)",
|
||||
Short: "Edit details for a group",
|
||||
Long: `Edit details, such as name or description, for a locally stored project group.
|
||||
Specify group by ID or name, which can be found with 'group list'.
|
||||
Specify either 'name' or 'description' as field.
|
||||
Specify new desired value as value.`,
|
||||
Args: cobra.ExactArgs(3),
|
||||
RunE: editExecute,
|
||||
}
|
||||
|
||||
func editExecute(cmd *cobra.Command, args []string) error {
|
||||
groupId := args[0]
|
||||
field := args[1]
|
||||
value := args[2]
|
||||
|
||||
if len(value) > 255 {
|
||||
return fmt.Errorf("given value is too long (%d): maximum allowed length is 255", len(value))
|
||||
}
|
||||
|
||||
db := database.DB()
|
||||
ctx := context.Background()
|
||||
|
||||
group, err := gorm.G[models.ProjectGroup](db).Where("id = ?", groupId).First(ctx)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
group, err = gorm.G[models.ProjectGroup](db).Where("name = ?", groupId).First(ctx)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return fmt.Errorf("no group with given name or ID found")
|
||||
}
|
||||
}
|
||||
|
||||
switch field {
|
||||
case "name":
|
||||
group.Name = value
|
||||
case "description":
|
||||
group.Description = value
|
||||
default:
|
||||
return fmt.Errorf("uknown field '%s': expected 'name' or 'description'", field)
|
||||
}
|
||||
|
||||
db.Save(&group)
|
||||
|
||||
fmt.Println("Edited project!")
|
||||
return nil
|
||||
}
|
||||
18
internal/cli/commands/group/group.go
Normal file
18
internal/cli/commands/group/group.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "group",
|
||||
Short: "Manage project groups",
|
||||
Long: `List, edit, add or remove logical groups used to organize projects`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.AddCommand(listCmd)
|
||||
Cmd.AddCommand(addCmd)
|
||||
Cmd.AddCommand(removeCmd)
|
||||
Cmd.AddCommand(editCmd)
|
||||
}
|
||||
32
internal/cli/commands/group/list.go
Normal file
32
internal/cli/commands/group/list.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.zervo.org/zervo/worktime/internal/database"
|
||||
"git.zervo.org/zervo/worktime/internal/database/models"
|
||||
"github.com/spf13/cobra"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all groups",
|
||||
Long: `List all locally stored project groups`,
|
||||
RunE: listExecute,
|
||||
}
|
||||
|
||||
func listExecute(cmd *cobra.Command, args []string) error {
|
||||
db := database.DB()
|
||||
ctx := context.Background()
|
||||
|
||||
groups, err := gorm.G[models.ProjectGroup](db).Find(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("aaa")
|
||||
}
|
||||
|
||||
fmt.Printf("Amount: %d\n", len(groups))
|
||||
|
||||
return nil
|
||||
}
|
||||
18
internal/cli/commands/group/remove.go
Normal file
18
internal/cli/commands/group/remove.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var removeCmd = &cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "Remove a group",
|
||||
Long: `Remove a locally stored project group`,
|
||||
Run: removeExecute,
|
||||
}
|
||||
|
||||
func removeExecute(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Removing!")
|
||||
}
|
||||
41
internal/cli/commands/root.go
Normal file
41
internal/cli/commands/root.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
cmd_group "git.zervo.org/zervo/worktime/internal/cli/commands/group"
|
||||
cmd_version "git.zervo.org/zervo/worktime/internal/cli/commands/version"
|
||||
"git.zervo.org/zervo/worktime/internal/util"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
verbose bool
|
||||
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "worktime",
|
||||
Short: "WorkTime is a CLI project time management utility",
|
||||
Long: `WorkTime is a CLI time management utility for keeping track of time spent on different projects.
|
||||
Can associate git directories and user-provided summaries with sessions. Exports to CSVs or directly to PDFs.`,
|
||||
}
|
||||
)
|
||||
|
||||
// Execute will execute the root command of the application
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.SetErrPrefix("ERROR:")
|
||||
rootCmd.SetErr(util.NewErrorWriter())
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "V", false, "increase output verbosity")
|
||||
|
||||
rootCmd.AddCommand(cmd_version.Cmd)
|
||||
rootCmd.AddCommand(cmd_group.Cmd)
|
||||
}
|
||||
21
internal/cli/commands/version/version.go
Normal file
21
internal/cli/commands/version/version.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
Version string = "dev"
|
||||
Commit string = "none"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version information",
|
||||
Long: `Print the WorkTime version information of this WorkTime binary`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("WorkTime. Version: %s, Commit: %s\n", Version, Commit)
|
||||
},
|
||||
}
|
||||
34
internal/cli/pretty/builder.go
Normal file
34
internal/cli/pretty/builder.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package pretty
|
||||
|
||||
import "strings"
|
||||
|
||||
type PrettyBuilder struct {
|
||||
lines []string
|
||||
padding int
|
||||
border bool
|
||||
}
|
||||
|
||||
// NewBuilder creates and returns a new empty builder
|
||||
func NewBuilder() *PrettyBuilder {
|
||||
return &PrettyBuilder{
|
||||
lines: []string{""},
|
||||
}
|
||||
}
|
||||
|
||||
// NewBuilderFromString creates and returns a new builder based on the string
|
||||
func NewBuilderFromString(s string) *PrettyBuilder {
|
||||
lines := strings.Split(strings.Trim(s, "\n"), "\n")
|
||||
return &PrettyBuilder{
|
||||
lines: lines,
|
||||
}
|
||||
}
|
||||
|
||||
// AppendString adds a string to the builder line buffer
|
||||
func (pb *PrettyBuilder) AppendString(s string) *PrettyBuilder {
|
||||
if strings.HasSuffix(s, "\n") {
|
||||
pb.lines = append(pb.lines, strings.Trim(s, "\n"))
|
||||
return pb
|
||||
}
|
||||
pb.lines[len(pb.lines)-1] = pb.lines[len(pb.lines)-1] + s
|
||||
return pb
|
||||
}
|
||||
56
internal/database/db.go
Normal file
56
internal/database/db.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"git.zervo.org/zervo/worktime/internal/database/models"
|
||||
"git.zervo.org/zervo/worktime/internal/util"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
var migrations []any = []any{
|
||||
&models.ProjectGroup{},
|
||||
&models.Project{},
|
||||
&models.Session{},
|
||||
&models.SessionPause{},
|
||||
&models.SyncTarget{},
|
||||
}
|
||||
|
||||
// migrate migrates the given database
|
||||
func migrate(db *gorm.DB) error {
|
||||
for _, model := range migrations {
|
||||
if err := db.AutoMigrate(model); err != nil {
|
||||
return fmt.Errorf("migrate %T: %v", model, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// connect will open and prepare the local sqlite database
|
||||
func connect() {
|
||||
dbPath := filepath.Join(util.GetWorkDir(), "data.db")
|
||||
|
||||
var err error = nil
|
||||
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
|
||||
err = migrate(db)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to migrate database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// DB returns the global database connection instance
|
||||
func DB() *gorm.DB {
|
||||
if db == nil {
|
||||
connect()
|
||||
}
|
||||
return db
|
||||
}
|
||||
20
internal/database/models/base.go
Normal file
20
internal/database/models/base.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
ID string `gorm:"column:id;<-:create;type:uuid;primaryKey;unique" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
||||
}
|
||||
|
||||
func (b *Base) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
b.ID = uuid.NewString()
|
||||
return
|
||||
}
|
||||
8
internal/database/models/project.go
Normal file
8
internal/database/models/project.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package models
|
||||
|
||||
type Project struct {
|
||||
Base
|
||||
GroupID string `gorm:"column:group_id;<-;type:uuid" json:"group_id"`
|
||||
Name string `gorm:"column:name;<-;size:255;not null;unique" json:"name"`
|
||||
Description string `gorm:"column:description;<-" json:"description"`
|
||||
}
|
||||
7
internal/database/models/project_group.go
Normal file
7
internal/database/models/project_group.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package models
|
||||
|
||||
type ProjectGroup struct {
|
||||
Base
|
||||
Name string `gorm:"column:name;<-;size:255;not null;unique" json:"name"`
|
||||
Description string `gorm:"column:description;<-" json:"description"`
|
||||
}
|
||||
17
internal/database/models/session.go
Normal file
17
internal/database/models/session.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.zervo.org/zervo/worktime/internal/types"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Base
|
||||
ProjectID string `gorm:"column:project_id;<-:create;type:uuid" json:"project_id"`
|
||||
State types.SessionState `gorm:"column:state;<-;type:int" json:"state"`
|
||||
Summary string `gorm:"column:summary;<-" json:"summary"`
|
||||
Commits types.StringSlice `gorm:"column:commits;<-;type:json" json:"commits"`
|
||||
FinishedAt time.Time `gorm:"column:finished_at;<-" json:"finished_at"`
|
||||
PauseIDs types.StringSlice `gorm:"column:pause_ids;<-;type:json" json:"pause_ids"`
|
||||
}
|
||||
13
internal/database/models/session_pause.go
Normal file
13
internal/database/models/session_pause.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type SessionPause struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ProjectID string `gorm:"column:session_id;<-:create;type:uuid" json:"session_id"`
|
||||
Resumed bool `gorm:"<-" json:"resumed"`
|
||||
PausedAt time.Time `gorm:"<-;autoCreateTime" json:"paused_at"`
|
||||
ResumedAt time.Time `gorm:"<-" json:"resumed_at"`
|
||||
}
|
||||
8
internal/database/models/sync_target.go
Normal file
8
internal/database/models/sync_target.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package models
|
||||
|
||||
type SyncTarget struct {
|
||||
Base
|
||||
Endpoint string `gorm:"<-;type:string" json:"endpoint"`
|
||||
Token string `gorm:"<-;type:string" json:"token"`
|
||||
Included []Project `gorm:"many2many:synced_projects;"`
|
||||
}
|
||||
54
internal/types/object_reference.go
Normal file
54
internal/types/object_reference.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ObjectReference struct {
|
||||
ID string
|
||||
Kind ObjectType
|
||||
}
|
||||
|
||||
func (or *ObjectReference) Scan(value any) error {
|
||||
if value == nil {
|
||||
*or = ObjectReference{ID: "", Kind: UndefinedObject}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
parts := bytes.Split([]byte(v), []byte{'-'})
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid format for ObjectReference: %s", v)
|
||||
}
|
||||
id := string(parts[0])
|
||||
kind, err := binary.ReadUvarint(bytes.NewReader(parts[1]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
or.ID = id
|
||||
or.Kind = ObjectType(kind)
|
||||
return nil
|
||||
|
||||
case []byte:
|
||||
vStr := string(v)
|
||||
return or.Scan(vStr)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unexpected type for ObjectReference: %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (or ObjectReference) Value() (driver.Value, error) {
|
||||
if or.ID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var kindBuf bytes.Buffer
|
||||
if err := binary.Write(&kindBuf, binary.LittleEndian, int64(or.Kind)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", or.ID, kindBuf.Bytes()), nil
|
||||
}
|
||||
44
internal/types/object_type.go
Normal file
44
internal/types/object_type.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ObjectType int
|
||||
|
||||
const (
|
||||
UndefinedObject ObjectType = iota
|
||||
ProjectGroupObject
|
||||
ProjectObject
|
||||
SessionObject
|
||||
)
|
||||
|
||||
var objectTypeNames = map[ObjectType]string{
|
||||
ProjectGroupObject: "project group object",
|
||||
ProjectObject: "project object",
|
||||
SessionObject: "session object",
|
||||
}
|
||||
|
||||
func (ot ObjectType) String() string {
|
||||
return objectTypeNames[ot]
|
||||
}
|
||||
|
||||
func (ot *ObjectType) Scan(value any) error {
|
||||
if value == nil {
|
||||
*ot = UndefinedObject
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case int64:
|
||||
*ot = ObjectType(v)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected type for ObjectType: %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (ot ObjectType) Value() (driver.Value, error) {
|
||||
return int64(ot), nil
|
||||
}
|
||||
43
internal/types/session_state.go
Normal file
43
internal/types/session_state.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type SessionState int
|
||||
|
||||
const (
|
||||
StateActive SessionState = iota
|
||||
StatePaused
|
||||
StateFinished
|
||||
)
|
||||
|
||||
var sessionStateNames = map[SessionState]string{
|
||||
StateActive: "active",
|
||||
StatePaused: "paused",
|
||||
StateFinished: "finished",
|
||||
}
|
||||
|
||||
func (ss SessionState) String() string {
|
||||
return sessionStateNames[ss]
|
||||
}
|
||||
|
||||
func (ss *SessionState) Scan(value any) error {
|
||||
if value == nil {
|
||||
*ss = StateActive
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case int64:
|
||||
*ss = SessionState(v)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected type for SessionState: %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (ss SessionState) Value() (driver.Value, error) {
|
||||
return int64(ss), nil
|
||||
}
|
||||
31
internal/types/string_slice.go
Normal file
31
internal/types/string_slice.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type StringSlice []string
|
||||
|
||||
func (s *StringSlice) Scan(value any) error {
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("failed to unmarshal JSONB value")
|
||||
}
|
||||
|
||||
result := StringSlice{}
|
||||
if err := json.Unmarshal(bytes, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*s = result
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s StringSlice) Value() (driver.Value, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(s)
|
||||
}
|
||||
23
internal/util/dir.go
Normal file
23
internal/util/dir.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// GetWorkDir returns the data directory for worktime
|
||||
func GetWorkDir() string {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get user home directory: %v", err)
|
||||
}
|
||||
|
||||
workDir := filepath.Join(homeDir, ".myworktime")
|
||||
|
||||
if err := os.MkdirAll(workDir, 0755); err != nil {
|
||||
log.Fatalf("Failed to create worktime directory: %v", err)
|
||||
}
|
||||
|
||||
return workDir
|
||||
}
|
||||
28
internal/util/writer.go
Normal file
28
internal/util/writer.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"git.zervo.org/zervo/worktime/internal/cli/color"
|
||||
)
|
||||
|
||||
type ErrorWriter struct {
|
||||
w io.Writer
|
||||
color string
|
||||
reset string
|
||||
}
|
||||
|
||||
func NewErrorWriter() *ErrorWriter {
|
||||
return &ErrorWriter{
|
||||
w: os.Stderr,
|
||||
color: color.Red,
|
||||
reset: color.Reset,
|
||||
}
|
||||
}
|
||||
|
||||
func (ew *ErrorWriter) Write(p []byte) (n int, err error) {
|
||||
colored := fmt.Appendf(nil, "%s%s%s", ew.color, p, ew.reset)
|
||||
return ew.w.Write(colored)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue