diff --git a/client/todo/add.go b/client/todo/add.go index 9b4464a..13eb421 100644 --- a/client/todo/add.go +++ b/client/todo/add.go @@ -3,6 +3,8 @@ package main import ( "context" "fmt" + "strings" + "time" "gitea.kleinsense.nl/DariusKlein/kleinTodo/common" "github.com/urfave/cli/v3" @@ -43,13 +45,19 @@ func addAction(context context.Context, c *cli.Command) error { } func createNewTodo() common.Todo { + + labelInput := common.AskUserString("Labels (comma separated):\n") + labelSlice := strings.Split(labelInput, ",") + for i, l := range labelSlice { + labelSlice[i] = strings.TrimSpace(l) + } return common.Todo{ Name: common.AskUserString("Name:\n"), Description: common.AskUserString("Description:\n"), - Status: common.AskUserString(fmt.Sprintf("Status(%s, %s, %s, %s, %s, %s):\n", - common.NotStarted, common.Done, common.WIP, common.Pending, common.Blocked, common.Failed, - ), - ), - Owner: cfg.Server.Credentials.Username, + Status: common.AskUserString(fmt.Sprintf("Status (%s, %s, %s, %s, %s, %s):\n", + common.NotStarted, common.Done, common.WIP, common.Pending, common.Blocked, common.Failed)), + Labels: labelSlice, + Owner: cfg.Server.Credentials.Username, + LastModified: time.Now(), } } diff --git a/client/todo/go.mod b/client/todo/go.mod index 0f4e4b6..ae62613 100644 --- a/client/todo/go.mod +++ b/client/todo/go.mod @@ -2,7 +2,12 @@ module gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo go 1.24.4 -require github.com/urfave/cli/v3 v3.4.1 +require ( + gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260111200537-716a65f01944 + github.com/BurntSushi/toml v1.6.0 + github.com/google/uuid v1.6.0 + github.com/urfave/cli/v3 v3.4.1 +) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect @@ -17,5 +22,7 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sys v0.30.0 // indirect + go.etcd.io/bbolt v1.4.3 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/sys v0.35.0 // indirect ) diff --git a/client/todo/go.sum b/client/todo/go.sum index a489104..8c140b3 100644 --- a/client/todo/go.sum +++ b/client/todo/go.sum @@ -1,3 +1,7 @@ +gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260111200537-716a65f01944 h1:uF+AelkGdfP7WH0luZM3fbxRRuOLx+9hCh012HGLqVs= +gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260111200537-716a65f01944/go.mod h1:bHquapurFm/eUTtrl9mGLEdAYc5cOeueHFvqjommp44= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= @@ -12,6 +16,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -31,8 +37,14 @@ github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/client/todo/main.go b/client/todo/main.go index a4a25dc..51c6a62 100644 --- a/client/todo/main.go +++ b/client/todo/main.go @@ -27,7 +27,7 @@ func main() { Name: "Todo", Usage: "kleinTodo client", UsageText: "Todo [command] [arguments...]", - Version: "v0.1.0", + Version: "v0.9.0", HideVersion: true, Authors: []any{ mail.Address{ diff --git a/common/askUser.go b/common/askUser.go index 758704f..b705420 100644 --- a/common/askUser.go +++ b/common/askUser.go @@ -4,6 +4,9 @@ import ( "bufio" "fmt" "os" + "strings" + + "github.com/charmbracelet/lipgloss" ) func AskUserBool(question string) bool { @@ -18,8 +21,10 @@ func AskUserBool(question string) bool { } func AskUserString(question string) string { - fmt.Printf(question) + promptStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) + fmt.Printf("%s %s", promptStyle.Render(">"), question) + reader := bufio.NewReader(os.Stdin) input, _, _ := reader.ReadLine() - return string(input) + return strings.TrimSpace(string(input)) } diff --git a/common/todo.go b/common/todo.go index cebd6f0..ccb9025 100644 --- a/common/todo.go +++ b/common/todo.go @@ -3,6 +3,8 @@ package common import ( "encoding/json" "fmt" + "github.com/charmbracelet/lipgloss" + "github.com/google/uuid" "hash/fnv" "strings" "time" @@ -12,16 +14,21 @@ func (todo Todo) Store(store *BoltStore, user string) error { if todo.Owner != user { return fmt.Errorf("unauthorized user") } + if todo.Id == "" { + todo.Id = uuid.New().String() + } + todo.Id = uuid.New().String() todo.LastModified = time.Now() todoJson, err := json.Marshal(todo) if err != nil { return err } - return store.SaveValueToBucket(user, todo.Name, string(todoJson)) + return store.SaveValueToBucket(user, todo.Id, string(todoJson)) } func (todoRequest StoreTodoRequest) Store(store *BoltStore, user string) error { todo := Todo{ + Id: uuid.New().String(), Name: todoRequest.Name, Description: todoRequest.Description, Status: todoRequest.Status, @@ -32,7 +39,7 @@ func (todoRequest StoreTodoRequest) Store(store *BoltStore, user string) error { if err != nil { return err } - return store.SaveValueToBucket(user, todo.Name, string(todoJson)) + return store.SaveValueToBucket(user, todo.Id, string(todoJson)) } func (todoList TodoList) FindByName(name string) (Todo, bool) { @@ -65,33 +72,42 @@ func (todo Todo) PrintIndexed(index int) { statusColor = ColorWhite // No color for unknown statuses } - for _, label := range []string{"test", "example", "example-test", "High prio"} { - bg := getLabelColor(label) - fmt.Printf(" %s %s %s", bg, label, ColorReset) + var renderedLabels []string + + for _, l := range todo.Labels { + h := fnv.New32a() + h.Write([]byte(l)) + colorID := fmt.Sprint(1 + (h.Sum32() % 6)) + + style := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("0")). + Background(lipgloss.Color(colorID)). + Padding(0, 1). + MarginRight(1). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color(colorID)). + BorderBackground(lipgloss.Color("")) + + renderedLabels = append(renderedLabels, style.Render(l)) } - fmt.Printf("\n%s%d)%s %s%-20s%s", ColorCyan, index, ColorReset, ColorWhite, todo.Name, ColorReset) + fmt.Println(lipgloss.JoinHorizontal(lipgloss.Top, renderedLabels...)) + + fmt.Printf("%s%d)%s %s%-20s%s", ColorCyan, index, ColorReset, ColorWhite, todo.Name, ColorReset) fmt.Printf(" %s %s%s\n", statusColor, strings.ToUpper(todo.Status), ColorReset) // 5. Description (Indented and slightly dimmer if possible, or standard) fmt.Printf(" %s\n", todo.Description) - - // 6. Footer (Timestamp in Gray to de-emphasize) + uuidStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) lastMod := todo.LastModified.Format("2006-01-02 15:04") - fmt.Printf(" %sLast Modified: %s%s\n", ColorGray, lastMod, ColorReset) + metaLine := fmt.Sprintf(" Last Modified: %s • ID: %s", lastMod, todo.Id) + fmt.Println(uuidStyle.Render(metaLine)) - // 7. Separator (Optional, but helps readability) fmt.Printf(" %s----------------------------%s\n", ColorGray, ColorReset) } -func getLabelColor(label string) string { - h := fnv.New32a() - h.Write([]byte(label)) - idx := h.Sum32() % uint32(len(LabelColors)) - return LabelColors[idx] -} - func (t Todo) IsEqual(other Todo) bool { return t.Name == other.Name && t.Description == other.Description && diff --git a/common/types.go b/common/types.go index 9b496f7..fed8e8f 100644 --- a/common/types.go +++ b/common/types.go @@ -8,10 +8,12 @@ type Credentials struct { } type Todo struct { + Id string `json:"id" toml:"id"` Name string `json:"name"` Description string `json:"description"` Status string `json:"status"` Owner string `json:"owner"` + Labels []string `json:"labels"` LastModified time.Time `json:"last_modified"` Deleted bool `json:"deleted"` } diff --git a/go.work.sum b/go.work.sum index e9c1289..6d9c65e 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,3 +1,7 @@ +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -13,14 +17,14 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA= go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= diff --git a/server/handler/syncHandler.go b/server/handler/syncHandler.go index 27dffb4..69b7e5c 100644 --- a/server/handler/syncHandler.go +++ b/server/handler/syncHandler.go @@ -33,7 +33,7 @@ func SyncHandler(w http.ResponseWriter, r *http.Request) { } for _, clientTodo := range todoList.Todos { - serverTodo, exists := serverTodos[clientTodo.Name] + serverTodo, exists := serverTodos[clientTodo.Id] if !exists { if clientTodo.Deleted { @@ -43,16 +43,16 @@ func SyncHandler(w http.ResponseWriter, r *http.Request) { if handleError(w, http.StatusInternalServerError, err) { return } - serverTodos[clientTodo.Name] = clientTodo + serverTodos[clientTodo.Id] = clientTodo continue } if clientTodo.Deleted { if clientTodo.LastModified.After(serverTodo.LastModified) { - err = store.RemoveValueFromBucket(user, clientTodo.Name) + err = store.RemoveValueFromBucket(user, clientTodo.Id) if handleError(w, http.StatusInternalServerError, err) { return } - delete(serverTodos, clientTodo.Name) + delete(serverTodos, clientTodo.Id) } } else if !serverTodo.IsEqual(clientTodo) { if serverTodo.IsEqualIgnoringStatus(clientTodo) && clientTodo.LastModified.After(serverTodo.LastModified) { @@ -60,7 +60,7 @@ func SyncHandler(w http.ResponseWriter, r *http.Request) { if handleError(w, http.StatusInternalServerError, err) { return } - serverTodos[clientTodo.Name] = clientTodo + serverTodos[clientTodo.Id] = clientTodo } else { response.MisMatchingTodos = append(response.MisMatchingTodos, common.MisMatchingTodo{ ServerTodo: serverTodo,