commit 2fbc5eb1861dba9b1915de64b62fade04cf7248f
Author: Tomas Nemec <nemi@skaut.cz>
Date: Mon, 5 Dec 2022 06:12:41 +0100
init
Diffstat:
A | go.mod | | | 3 | +++ |
A | main.go | | | 207 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | tme/entry.go | | | 126 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | tme/entry_test.go | | | 84 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | tme/group.go | | | 97 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | tme/time.go | | | 45 | +++++++++++++++++++++++++++++++++++++++++++++ |
A | tme_test.go | | | 166 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
7 files changed, 728 insertions(+), 0 deletions(-)
diff --git a/go.mod b/go.mod
@@ -0,0 +1,3 @@
+module gtms.dev/tme
+
+go 1.19
diff --git a/main.go b/main.go
@@ -0,0 +1,207 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+
+ "gtms.dev/tme/tme"
+)
+
+const (
+ activeFile = "active"
+)
+
+func main() {
+ basePath := os.Getenv("TME_DIR")
+ if basePath == "" {
+ fmt.Fprintln(os.Stderr, "TME_DIR environment variable is not set.")
+ os.Exit(1)
+ }
+
+ if _, err := os.Stat(basePath); os.IsNotExist(err) {
+ fmt.Fprintf(os.Stderr, "%s does not exist\n", basePath)
+ os.Exit(1)
+ }
+
+ args := os.Args[1:]
+ if len(args) == 0 {
+ os.Exit(0)
+ }
+
+ cmd := args[0]
+ args = args[1:] // shift
+
+ entryTime := tme.NewTime()
+ command := Command{basePath, args, entryTime}
+ if cmd == "add" {
+ command.add()
+ } else if cmd == "start" {
+ command.start()
+ } else if cmd == "stop" {
+ command.stop()
+ } else if cmd == "ls" {
+ command.ls()
+ }
+}
+
+type Command struct {
+ basePath string
+ args []string
+ entryTime *tme.Time
+}
+
+func (c *Command) nextArg() (string, error) {
+ if len(c.args) == 0 {
+ return "", errors.New("No more arguments")
+ }
+
+ if len(c.args) == 1 {
+ first := (c.args)[0]
+ c.args = make([]string, 0)
+ return first, nil
+ }
+
+ first := (c.args)[0]
+ c.args = (c.args)[1:]
+ return first, nil
+}
+
+func (c Command) add() {
+ if len(c.args) != 3 {
+ fmt.Fprintln(os.Stderr, "add <path> <start> <stop>")
+ os.Exit(1)
+ }
+
+ groupArg, _ := c.nextArg()
+ startArg, _ := c.nextArg()
+ stopArg, _ := c.nextArg()
+
+ start, err := c.entryTime.ParseArg(startArg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "[start] time (%s) could not be parsed\n", startArg)
+ os.Exit(1)
+ }
+
+ stop, err := c.entryTime.ParseArg(stopArg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "[stop] time (%s) could not be parsed\n", stopArg)
+ os.Exit(1)
+ }
+
+ group := tme.NewGroup(c.basePath, groupArg)
+ // TODO(tms) 22.10.22: maybe ask if user want create new folder (fe: typo)
+ group.Create()
+
+ entry, err := tme.NewEntry(start, stop)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ if entry.Exists(group) {
+ fmt.Fprintln(os.Stderr, "entry already created")
+ os.Exit(1)
+ }
+
+ group.Add(entry)
+}
+
+func (c Command) start() {
+ if len(c.args) != 2 {
+ fmt.Fprintln(os.Stderr, "start <path> <start>")
+ os.Exit(1)
+ }
+
+ groupArg, _ := c.nextArg()
+ startArg, _ := c.nextArg()
+
+ start, err := c.entryTime.ParseArg(startArg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "[start] time (%s) could not be parsed\n", startArg)
+ os.Exit(1)
+ }
+
+ group := tme.NewGroup(c.basePath, groupArg)
+ // TODO(tms) 22.10.22: maybe ask if user want create new folder (fe: typo)
+ group.Create()
+
+ entry := tme.NewStartEntry(start)
+
+ if entry.Exists(group) {
+ fmt.Fprintln(os.Stderr, "entry in this group already started")
+ os.Exit(1)
+ }
+
+ group.Add(entry)
+}
+
+func (c Command) stop() {
+ if len(c.args) != 2 {
+ fmt.Fprintln(os.Stderr, "stop <path> <stop>")
+ os.Exit(1)
+ }
+
+ groupArg, _ := c.nextArg()
+ stopArg, _ := c.nextArg()
+
+ stop, err := c.entryTime.ParseArg(stopArg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "[start] time (%s) could not be parsed\n", stopArg)
+ os.Exit(1)
+ }
+
+ group := tme.NewGroup(c.basePath, groupArg)
+
+ startEntry, err := tme.NewStartEntryFromGroup(group, c.entryTime)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "no entry running in %q\n", groupArg)
+ os.Exit(1)
+ }
+
+ entry, err := startEntry.Stop(stop)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ if entry.Exists(group) {
+ fmt.Fprintln(os.Stderr, "entry already created")
+ os.Exit(1)
+ }
+
+ err = group.Add(entry)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ err = group.Remove(startEntry)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+func (c Command) ls() {
+ if len(c.args) > 1 {
+ fmt.Fprintln(os.Stderr, "ls [<path>]")
+ os.Exit(1)
+ }
+
+ rootPath := ""
+ if len(c.args) == 1 {
+ rootPath, _ = c.nextArg()
+ }
+
+ group := tme.NewGroup(c.basePath, rootPath)
+ files, err := group.List()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ for _, file := range files {
+ fmt.Println(file)
+ }
+}
diff --git a/tme/entry.go b/tme/entry.go
@@ -0,0 +1,126 @@
+package tme
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path"
+ "strings"
+ "time"
+)
+
+type Entry interface {
+ Data() string
+ FileName() string
+ FullPath(group Group) string
+}
+
+type FinalEntry struct {
+ Start time.Time
+ Stop time.Time
+}
+
+func NewEntry(start time.Time, stop time.Time) (FinalEntry, error) {
+ if start.After(stop) {
+ return FinalEntry{}, errors.New("duration must be positive")
+ }
+
+ return FinalEntry{
+ Start: start,
+ Stop: stop,
+ }, nil
+}
+
+func NewEntryFromPath(path string, entryTime *Time) (FinalEntry, error) {
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ return FinalEntry{}, err
+ }
+
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return FinalEntry{}, err
+ }
+
+ // TODO(tms) 11.11.22: format check
+
+ lines := strings.Split(string(data), "\n")
+ start, _ := entryTime.ParseEntry(lines[0])
+ stop, _ := entryTime.ParseEntry(lines[1])
+
+ return FinalEntry{
+ Start: start,
+ Stop: stop,
+ }, nil
+}
+
+func (e FinalEntry) Data() string {
+ return fmt.Sprintf("%s\n%s\n", e.Start.Format(DateTimeLayout), e.Stop.Format(DateTimeLayout))
+}
+
+func (e FinalEntry) FileName() string {
+ return strings.Join([]string{e.Start.Format(FileNameLayout), e.Stop.Format(FileNameLayout)}, "_")
+}
+
+func (e FinalEntry) FullPath(group Group) string {
+ return path.Join(group.FullPath(), e.FileName())
+}
+
+func (e FinalEntry) Exists(group Group) bool {
+ if _, err := os.Stat(e.FullPath(group)); !os.IsNotExist(err) {
+ return true
+ }
+ return false
+}
+
+type StartEntry struct {
+ Start time.Time
+}
+
+func NewStartEntry(start time.Time) StartEntry {
+ return StartEntry{
+ Start: start,
+ }
+}
+
+func NewStartEntryFromGroup(group Group, entryTime *Time) (StartEntry, error) {
+ startEntryPath := group.ActivePath()
+ if _, err := os.Stat(startEntryPath); os.IsNotExist(err) {
+ return StartEntry{}, err
+ }
+
+ data, err := os.ReadFile(startEntryPath)
+ if err != nil {
+ return StartEntry{}, err
+ }
+
+ firstLine := strings.Split(string(data), "\n")[0]
+ start, _ := entryTime.ParseEntry(firstLine)
+
+ return StartEntry{
+ Start: start,
+ }, nil
+
+}
+
+func (e StartEntry) Data() string {
+ return fmt.Sprintf("%s\n", e.Start.Format(DateTimeLayout))
+}
+
+func (e StartEntry) FileName() string {
+ return "active"
+}
+
+func (e StartEntry) FullPath(group Group) string {
+ return path.Join(group.FullPath(), e.FileName())
+}
+
+func (e StartEntry) Exists(group Group) bool {
+ if _, err := os.Stat(e.FullPath(group)); !os.IsNotExist(err) {
+ return true
+ }
+ return false
+}
+
+func (e StartEntry) Stop(stop time.Time) (FinalEntry, error) {
+ return NewEntry(e.Start, stop)
+}
diff --git a/tme/entry_test.go b/tme/entry_test.go
@@ -0,0 +1,84 @@
+package tme
+
+import (
+ "os"
+ "testing"
+)
+
+func TestEntry(t *testing.T) {
+ entryTime := NewTime()
+
+ t.Run("positive duration is ok", func(b *testing.T) {
+ start, _ := entryTime.ParseArg("5:00")
+ stop, _ := entryTime.ParseArg("6:00")
+ _, err := NewEntry(start, stop)
+ if err != nil {
+ b.Error(err)
+ }
+ })
+
+ t.Run("same duration is kind of ok?", func(b *testing.T) {
+ start, _ := entryTime.ParseArg("6:00")
+ stop, _ := entryTime.ParseArg("6:00")
+ _, err := NewEntry(start, stop)
+ if err != nil {
+ b.Error(err)
+ }
+ })
+
+ t.Run("negative duration should fail", func(b *testing.T) {
+ start, _ := entryTime.ParseArg("6:00")
+ stop, _ := entryTime.ParseArg("5:00")
+ _, err := NewEntry(start, stop)
+ if err == nil {
+ b.Error(err)
+ }
+ })
+}
+
+func TestStartStopEntry(t *testing.T) {
+ basePath := setUp()
+ entryTime := NewTime()
+ t.Cleanup(func() { tearDown(basePath) })
+
+ start, _ := entryTime.ParseArg("6:00")
+ startEntry := NewStartEntry(start)
+
+ group := NewGroup(basePath, "project")
+ group.Create()
+
+ group.Add(startEntry)
+ if _, err := os.Stat(startEntry.FullPath(group)); os.IsNotExist(err) {
+ t.Fatalf(`want %q to exist`, startEntry.FullPath(group))
+ }
+
+ stop, _ := entryTime.ParseArg("7:00")
+ finalEntry, err := startEntry.Stop(stop)
+ if err != nil {
+ t.Error(err)
+ }
+
+ group.Remove(startEntry)
+ if _, err := os.Stat(startEntry.FullPath(group)); !os.IsNotExist(err) {
+ t.Fatalf(`want %q to be deleted`, startEntry.FullPath(group))
+ }
+
+ group.Add(finalEntry)
+ if _, err := os.Stat(finalEntry.FullPath(group)); os.IsNotExist(err) {
+ t.Fatalf(`want %q to exist`, finalEntry.FullPath(group))
+ }
+}
+
+func setUp() string {
+ tempDir, err := os.MkdirTemp("", "tme_*")
+ if err != nil {
+ panic(err)
+ }
+
+ os.Setenv("TME_DIR", tempDir)
+ return tempDir
+}
+
+func tearDown(tempDir string) {
+ os.RemoveAll(tempDir)
+}
diff --git a/tme/group.go b/tme/group.go
@@ -0,0 +1,97 @@
+package tme
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path"
+ "path/filepath"
+)
+
+type Group struct {
+ basePath string
+ Path string
+}
+
+func NewGroup(basePath, path string) Group {
+ return Group{
+ basePath: basePath,
+ Path: path,
+ }
+}
+
+func (g Group) Create() error {
+ if err := os.MkdirAll(g.FullPath(), os.ModePerm); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (g Group) FullPath() string {
+ return path.Join(g.basePath, g.Path)
+}
+
+func (g Group) EntryPath(entry Entry) string {
+ return path.Join(g.Path, entry.FileName())
+}
+
+func (g Group) ActivePath() string {
+ return path.Join(g.FullPath(), "active")
+}
+
+func (g Group) Exists() bool {
+ if _, err := os.Stat(g.FullPath()); !os.IsNotExist(err) {
+ return true
+ }
+ return false
+}
+
+func (g Group) Add(entry Entry) error {
+ err := os.WriteFile(entry.FullPath(g), []byte(entry.Data()), os.ModePerm)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (g Group) Remove(entry Entry) error {
+ return os.Remove(entry.FullPath(g))
+}
+
+func (g Group) List() ([]string, error) {
+ if !g.Exists() {
+ return []string{}, errors.New("Group does not exist")
+ }
+
+ var files []string
+ err := filepath.WalkDir(g.FullPath(), func(path string, d fs.DirEntry, err error) error {
+ if d.IsDir() {
+ return nil
+ }
+ files = append(files, path)
+ return nil
+ })
+ if err != nil {
+ return []string{}, err
+ }
+
+ return files, nil
+}
+
+func (g Group) FormatList(entryTime *Time) ([]string, error) {
+ filePaths, err := g.List()
+ if err != nil {
+ return []string{}, err
+ }
+
+ var lines []string
+ for _, path := range filePaths {
+ entry, err := NewEntryFromPath(path, entryTime)
+ if err != nil {
+ return []string{}, err
+ }
+ lines = append(lines, fmt.Sprintf("%s %s %s", g.Path, entry.Start.Format(TimeLayout), entry.Stop.Format(TimeLayout)))
+ }
+ return lines, nil
+}
diff --git a/tme/time.go b/tme/time.go
@@ -0,0 +1,45 @@
+package tme
+
+import "time"
+
+const (
+ TimeLayout = "15:04"
+ FileNameLayout = "0601021504"
+ DateTimeLayout = time.RFC3339
+)
+
+type Time struct {
+ context time.Time
+}
+
+func NewTime() *Time {
+ return &Time{time.Now()}
+}
+
+func (et *Time) ParseArg(raw string) (time.Time, error) {
+ t, err := time.Parse(TimeLayout, raw)
+ return time.Date(
+ et.context.Year(),
+ et.context.Month(),
+ et.context.Day(),
+ t.Hour(),
+ t.Minute(),
+ 0,
+ 0,
+ et.context.Location(),
+ ), err
+}
+
+func (et *Time) ParseEntry(raw string) (time.Time, error) {
+ t, err := time.Parse(DateTimeLayout, raw)
+ return time.Date(
+ t.Year(),
+ t.Month(),
+ t.Day(),
+ t.Hour(),
+ t.Minute(),
+ 0,
+ 0,
+ et.context.Location(),
+ ), err
+}
diff --git a/tme_test.go b/tme_test.go
@@ -0,0 +1,166 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "gtms.dev/tme/tme"
+)
+
+func TestAdd(t *testing.T) {
+ basePath := setUp()
+ t.Cleanup(func() { tearDown(basePath) })
+ entryTime := tme.NewTime()
+ group := createAndCheckGroup(t, basePath, "project")
+
+ start, _ := entryTime.ParseArg("10:00")
+ stop, _ := entryTime.ParseArg("11:00")
+ tme, err := tme.NewEntry(start, stop)
+ if err != nil {
+ t.Error(err)
+ }
+
+ group.Add(tme)
+ if data, err := os.ReadFile(tme.FullPath(group)); err != nil {
+ fmt.Printf("%v", err)
+ } else {
+ checkEntryFile(start, stop, data, entryTime, t)
+ }
+
+}
+
+func TestStart(t *testing.T) {
+ basePath := setUp()
+ t.Cleanup(func() { tearDown(basePath) })
+ entryDir := "project"
+ fullEntryDir := strings.Join([]string{basePath, entryDir}, "/")
+ entryTime := tme.NewTime()
+
+ group := createAndCheckGroup(t, basePath, "project")
+
+ start, _ := entryTime.ParseArg("10:00")
+ entry := tme.NewStartEntry(start)
+
+ group.Add(entry)
+ if _, err := os.Stat(fullEntryDir); os.IsNotExist(err) {
+ t.Fatalf(`want "%v" to exist`, entryDir)
+ }
+
+ if data, err := os.ReadFile(entry.FullPath(group)); err != nil {
+ fmt.Printf("%v", err)
+ } else {
+ checkStartEntryFile(start, data, entryTime, t)
+ }
+}
+
+func TestStop(t *testing.T) {
+ basePath := setUp()
+ t.Cleanup(func() { tearDown(basePath) })
+ entryTime := tme.NewTime()
+
+ group := tme.NewGroup(basePath, "project")
+ group.Create()
+
+ start, _ := entryTime.ParseArg("10:00")
+ startEntry := tme.NewStartEntry(start)
+ group.Add(startEntry)
+
+ stop, _ := entryTime.ParseArg("11:00")
+ entry, err := tme.NewEntry(startEntry.Start, stop)
+ if err != nil {
+ t.Error(err)
+ }
+
+ group.Add(entry)
+ if data, err := os.ReadFile(entry.FullPath(group)); err != nil {
+ t.Errorf("%v", err)
+ } else {
+ checkEntryFile(start, stop, data, entryTime, t)
+ }
+}
+
+func ExampleList() {
+ basePath := setUp()
+ entryTime := tme.NewTime()
+
+ group := tme.NewGroup(basePath, "tme")
+ group.Create()
+
+ start, _ := entryTime.ParseArg("10:00")
+ stop, _ := entryTime.ParseArg("11:00")
+ entry, _ := tme.NewEntry(start, stop)
+ group.Add(entry)
+
+ lines, err := group.FormatList(entryTime)
+ if err != nil {
+ fmt.Print(err)
+ }
+
+ for _, line := range lines {
+ fmt.Println(line)
+ }
+
+ // Output:
+ // tme 10:00 11:00
+
+ tearDown(basePath)
+}
+
+func checkEntryFile(start time.Time, stop time.Time, data []byte, entryTime *tme.Time, t *testing.T) {
+ lines := strings.Split(string(data), "\n")
+ expectLines := 3
+ if len(lines) != expectLines {
+ t.Fatalf("want %d lines, got %d lines", expectLines, len(lines))
+ }
+
+ startLine := lines[0]
+ lineStart, _ := entryTime.ParseEntry(startLine)
+ if start != lineStart {
+ t.Fatalf("time mismatch! %v != %v", start, lineStart)
+ }
+
+ stopLine := lines[1]
+ lineStop, _ := entryTime.ParseEntry(stopLine)
+ if stop != lineStop {
+ t.Fatalf("time mismatch! %v != %v", stop, lineStop)
+ }
+}
+
+func checkStartEntryFile(start time.Time, data []byte, entryTime *tme.Time, t *testing.T) {
+ lines := strings.Split(string(data), "\n")
+ expectLines := 2
+ if len(lines) != expectLines {
+ t.Fatalf("want %d lines, got %d lines", expectLines, len(lines))
+ }
+
+ startLine := lines[0]
+ lineStart, _ := entryTime.ParseEntry(startLine)
+ if start != lineStart {
+ t.Fatalf("time mismatch! %v != %v", start, lineStart)
+ }
+}
+func createAndCheckGroup(t *testing.T, basePath, groupName string) tme.Group {
+ group := tme.NewGroup(basePath, groupName)
+ group.Create()
+ if _, err := os.Stat(group.FullPath()); os.IsNotExist(err) {
+ t.Fatalf(`want %q to exist`, group.Path)
+ }
+ return group
+}
+
+func setUp() string {
+ tempDir, err := os.MkdirTemp("", "tme_*")
+ if err != nil {
+ panic(err)
+ }
+
+ os.Setenv("TME_DIR", tempDir)
+ return tempDir
+}
+
+func tearDown(tempDir string) {
+ os.RemoveAll(tempDir)
+}