6 Commits
0.1.0 ... 1.0.0

13 changed files with 324 additions and 169 deletions

View File

@@ -1,92 +1,87 @@
# GOTASK # GOTASK
Tiny project aiming at creating a CLI task tracker in Golang Tiny project aiming to create a CLI task tracker in Golang
## Purpose ## Purpose
1. Practice Golang 1. Practice Golang
1. Keep track of tasks 1. Keep track of tasks
## Roadmap
1. ~~Add~~
2. ~~List all~~
3. Progress
4. Regress
5. List by
## Features ## Features
### Add tasks ### Add tasks
```console ```console
[user@host]$ gotask add "Publish code" [user@host]$ gotask-cli add "Publish code"
New task "Publish code" added to do @ id=4 New task "Publish code" added to do @ id=4
``` ```
### List all tasks ### List all tasks
```console ```console
[user@host]$ gotask list [user@host]$ gotask-cli list
+---------------------------------------------------------------------+ +----+--------------+--------+---------------------+---------+
|id| Description | Status | Created | Updated | | ID | DESCRIPTION | STATUS | CREATED | UPDATED |
|--|--------------|-------------|------------------|------------------| +----+--------------+--------+---------------------+---------+
| 1| Update doc | todo | 01-03-2026 09:23 | | # Never updated | 1 | Update doc | todo | 2026-04-03 22:34:40 | |
| 2| Write code | doing | 04-03-2026 10:13 | 05-03-2026 17:45 | # Set in progress at UpdateAt | 2 | Write code | todo | 2026-04-03 22:38:26 | |
| 3| Write tests | done | 05-03-2026 11:34 | 01-04-2026 19:43 | # Done at UpdateAt | 3 | Write tests | todo | 2026-04-03 22:38:45 | |
| 4| Publish code | todo | 01-04-2026 09:13 | | | 4 | Publish code | todo | 2026-04-03 22:38:56 | |
+---------------------------------------------------------------------+ +----+--------------+--------+---------------------+---------+
``` ```
### Mark progress ### Mark progress
```console ```console
[user@host]$ gotask progress 4 [user@host]$ gotask-cli progress 4
[todo] task "Publish code" progress to [doing] [todo] task "Publish code" progress to [doing]
``` ```
### List tasks by status ### List tasks by status
```console ```console
[user@host]$ gotask list doing [user@host]$ gotask-cli list doing
+---------------------------------------------------------------------+ +----+--------------+--------+---------------------+---------------------+
|id| Description | Status | Created | Updated | | ID | DESCRIPTION | STATUS | CREATED | UPDATED |
|--|--------------|-------------|------------------|------------------| +----+--------------+--------+---------------------+---------------------+
| 2| Write code | doing | 04-03-2026 10:13 | 05-03-2026 17:45 | | 4 | Publish code | doing | 2026-04-03 22:38:56 | 2026-04-03 22:44:36 |
| 4| Publish code | doing | 01-04-2026 09:13 | 01-04-2026 10:30 | +----+--------------+--------+---------------------+---------------------+
+---------------------------------------------------------------------+
``` ```
### Mark regress ### Mark regress
```console ```console
[user@host]$ gotask regress 4 [user@host]$ gotask-cli regress 4
[doing] task "Publish code" regress to [todo] [doing] task "Publish code" regress to [todo]
``` ```
### List task by id
```console
[user@host]$ gotask list 4
+---------------------------------------------------------------------+
|id| Description | Status | Created | Updated |
|--|--------------|-------------|------------------|------------------|
| 4| Publish code | todo | 01-04-2026 09:13 | 01-04-2026 10:33 |
+---------------------------------------------------------------------+
```
## Storing data ## Storing data
For convenience reasons, the data will be stored in a JSON file with the following structure. For convenience reasons, the data will be stored in a JSON file with the following structure.
```json ```json
[ [
{ {
"id": 1, "Id": 1,
"Description": "Update doc", "Description": "Update doc",
"Status": 0, "Status": 0,
"Created": 1772353380, "Created": 1775248480,
"UpdatedAt": -1 "Updated": -1
}, },
{ {
"id": 2, "Id": 2,
"Description": "Write code", "Description": "Write code",
"Status": 1, "Status": 0,
"Created": 1772615580, "Created": 1775248706,
"UpdatedAt": 1772729100 "Updated": -1
} },
{
"Id": 3,
"Description": "Write tests",
"Status": 0,
"Created": 1775248725,
"Updated": -1
},
{
"Id": 4,
"Description": "Publish code",
"Status": 0,
"Created": 1775248736,
"Updated": 1775249113
}
] ]
``` ```

20
commands/add_task.go Normal file
View File

@@ -0,0 +1,20 @@
package commands
import (
"fmt"
"gotask-cli/models"
"time"
)
func AddTask(desc string, backlog []models.Task) []models.Task {
newtask := models.Task{
Id: uint(len(backlog)) + 1,
Description: desc,
Status: 0,
Created: time.Now().Unix(),
Updated: -1}
fmt.Printf("New task \"%s\" added to do @ id=%d", newtask.Description, newtask.Id)
backlog = append(backlog, newtask)
return backlog
}

9
commands/help.go Normal file
View File

@@ -0,0 +1,9 @@
package commands
import (
"fmt"
)
func Help() {
fmt.Println("Hang in there, will do the help stuff some day !")
}

10
commands/list_tasks.go Normal file
View File

@@ -0,0 +1,10 @@
package commands
import (
"gotask-cli/models"
"gotask-cli/utils"
)
func ListTasks(backlog []models.Task) {
utils.PrintTasksAsATable(backlog)
}

28
commands/list_tasks_by.go Normal file
View File

@@ -0,0 +1,28 @@
package commands
import (
"fmt"
"gotask-cli/models"
"gotask-cli/utils"
)
func ListTasksBy(status string, backlog []models.Task) {
var backlogToDisplay []models.Task
var statusID models.Status
for i, v := range models.StatusName {
if v == status {
statusID = i
}
}
for _, v := range backlog {
if v.Status == models.Status(statusID) {
backlogToDisplay = append(backlogToDisplay, v)
}
}
if len(backlogToDisplay) > 0 {
utils.PrintTasksAsATable(backlogToDisplay)
} else {
fmt.Printf("No tasks with the status [%s]", status)
}
}

18
commands/progress_task.go Normal file
View File

@@ -0,0 +1,18 @@
package commands
import (
"fmt"
"gotask-cli/models"
"time"
)
func ProgressTask(id uint, backlog []models.Task) []models.Task {
if backlog[id].Status >= 0 && backlog[id].Status < 2 {
backlog[id].Status++
backlog[id].Updated = time.Now().Unix()
fmt.Printf("[%s] task \"%s\" progress to [%s]", models.StatusName[backlog[id].Status-1], backlog[id].Description, models.StatusName[backlog[id].Status])
} else {
fmt.Printf("Can't progress task %s", backlog[id].Description)
}
return backlog
}

18
commands/regress_task.go Normal file
View File

@@ -0,0 +1,18 @@
package commands
import (
"fmt"
"gotask-cli/models"
"time"
)
func RegressTask(id uint, backlog []models.Task) []models.Task {
if backlog[id].Status >= 1 && backlog[id].Status <= 2 {
backlog[id].Status--
backlog[id].Updated = time.Now().Unix()
fmt.Printf("[%s] task \"%s\" regress to [%s]", models.StatusName[backlog[id].Status+1], backlog[id].Description, models.StatusName[backlog[id].Status])
} else {
fmt.Printf("Can't regress task %s", backlog[id].Description)
}
return backlog
}

View File

@@ -1,143 +1,77 @@
package main package main
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"time" "strconv"
"github.com/jedib0t/go-pretty/v6/table" "gotask-cli/commands"
) "gotask-cli/utils"
type Status int
const (
todo Status = iota
doing
done
) )
const path string = "./tasks.json" const path string = "./tasks.json"
const tempPath string = "./tasks.save"
var statusName = map[Status]string{
todo: "todo",
doing: "doing",
done: "done",
}
type task struct {
Id uint
Description string
Status Status
Created int64
Updated int64
}
var backlog []task
func initBacklog() {
if _, err := os.Stat(path); err == nil {
loadBacklog()
} else if errors.Is(err, os.ErrNotExist) {
// path does not exist
f, err := os.Create(path)
check(err)
defer f.Close()
loadBacklog()
} else {
check(err)
os.Exit(1)
}
}
func loadBacklog() {
content, err := os.ReadFile(path)
check(err)
err = json.Unmarshal([]byte(content), &backlog)
check(err)
}
func check(e error) {
if e != nil {
panic(e)
}
}
func printTasksAsATable(tasks []task) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Id", "Description", "Status", "Created", "Updated"})
for _, v := range tasks {
var updated string
if v.Updated == -1 {
updated = ""
} else {
updated = time.Unix(v.Updated, 0).Format(time.DateTime)
}
t.AppendRow([]interface{}{
v.Id,
v.Description,
statusName[v.Status],
time.Unix(v.Created, 0).Format(time.DateTime),
updated,
})
}
t.Render()
}
func addTask(desc string) {
newtask := task{
Id: uint(len(backlog)) + 1,
Description: desc,
Status: 0,
Created: time.Now().Unix(),
Updated: -1}
fmt.Printf("New task \"%s\" added to do @ id=%d", newtask.Description, newtask.Id)
backlog = append(backlog, newtask)
}
func saveBacklog() {
if _, err := os.Stat(tempPath); err == nil {
err = os.Remove(tempPath)
} else if errors.Is(err, os.ErrNotExist) {
} else {
check(err)
os.Exit(1)
}
f, err := os.Create(tempPath)
check(err)
defer f.Close()
backlogJSON, _ := json.MarshalIndent(backlog, "", "\t")
f.Write(backlogJSON)
err = os.Rename(tempPath, path)
check(err)
}
func main() { func main() {
initBacklog() backlog := utils.InitBacklog(path)
if len(os.Args[1:]) >= 1 { if len(os.Args[1:]) >= 1 {
switch os.Args[1] { switch os.Args[1] {
case "help": case "help":
fmt.Println("S.O.S!") commands.Help()
case "add": case "add":
if len(os.Args[1:]) >= 2 { if len(os.Args[1:]) == 2 {
addTask(os.Args[2]) backlog = commands.AddTask(os.Args[2], backlog)
} else { } else {
fmt.Println("Missing argument") fmt.Println("Missing argument")
commands.Help()
} }
case "list": case "list":
printTasksAsATable(backlog) switch len(os.Args[1:]) {
case 1:
commands.ListTasks(backlog)
case 2:
switch os.Args[2] {
case "todo", "doing", "done":
commands.ListTasksBy(os.Args[2], backlog)
default:
fmt.Printf("Unknown status %s", os.Args[2])
}
default:
fmt.Println("Too many arguments !")
}
case "progress":
if len(os.Args[1:]) == 2 {
id, err := strconv.ParseUint(os.Args[2], 10, 32)
utils.Check(err)
if id > 0 && id <= uint64(len(backlog)) {
commands.ProgressTask(uint(id)-1, backlog)
} else {
fmt.Printf("No task found with id=%d, try list to get id", id)
}
} else {
fmt.Println("Missing argument")
commands.Help()
}
case "regress":
if len(os.Args[1:]) == 2 {
id, err := strconv.ParseUint(os.Args[2], 10, 32)
utils.Check(err)
if id > 0 && id <= uint64(len(backlog)) {
commands.RegressTask(uint(id)-1, backlog)
} else {
fmt.Printf("No task found with id=%d, try list to get id", id)
}
} else {
fmt.Println("Missing argument")
commands.Help()
}
default: default:
fmt.Println("NO !") fmt.Println("NO !")
} }
saveBacklog() utils.SaveBacklog(backlog, path)
} else { } else {
fmt.Println("Missing argument") fmt.Println("Missing argument")
commands.Help()
} }
} }

23
models/task.go Normal file
View File

@@ -0,0 +1,23 @@
package models
type Status int
const (
todo Status = iota
doing
done
)
var StatusName = map[Status]string{
todo: "todo",
doing: "doing",
done: "done",
}
type Task struct {
Id uint
Description string
Status Status
Created int64
Updated int64
}

7
utils/err_checker.go Normal file
View File

@@ -0,0 +1,7 @@
package utils
func Check(e error) {
if e != nil {
panic(e)
}
}

33
utils/read_backlog.go Normal file
View File

@@ -0,0 +1,33 @@
package utils
import (
"encoding/json"
"errors"
"gotask-cli/models"
"os"
)
func InitBacklog(path string) []models.Task {
if _, err := os.Stat(path); err == nil {
} else if errors.Is(err, os.ErrNotExist) {
f, err := os.Create(path)
Check(err)
defer f.Close()
f.Write([]byte("[]"))
} else {
Check(err)
os.Exit(1)
}
return loadBacklog(path)
}
func loadBacklog(path string) []models.Task {
var backlog []models.Task
content, err := os.ReadFile(path)
Check(err)
err = json.Unmarshal([]byte(content), &backlog)
Check(err)
return backlog
}

31
utils/table_render.go Normal file
View File

@@ -0,0 +1,31 @@
package utils
import (
"gotask-cli/models"
"os"
"time"
"github.com/jedib0t/go-pretty/v6/table"
)
func PrintTasksAsATable(tasks []models.Task) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Id", "Description", "Status", "Created", "Updated"})
for _, v := range tasks {
var updated string
if v.Updated == -1 {
updated = ""
} else {
updated = time.Unix(v.Updated, 0).Format(time.DateTime)
}
t.AppendRow([]interface{}{
v.Id,
v.Description,
models.StatusName[v.Status],
time.Unix(v.Created, 0).Format(time.DateTime),
updated,
})
}
t.Render()
}

29
utils/write_backlog.go Normal file
View File

@@ -0,0 +1,29 @@
package utils
import (
"encoding/json"
"errors"
"os"
"gotask-cli/models"
)
const tempPath string = "./tasks.save"
func SaveBacklog(backlog []models.Task, path string) {
if _, err := os.Stat(tempPath); err == nil {
err = os.Remove(tempPath)
} else if errors.Is(err, os.ErrNotExist) {
} else {
Check(err)
os.Exit(1)
}
f, err := os.Create(tempPath)
Check(err)
defer f.Close()
backlogJSON, _ := json.MarshalIndent(backlog, "", "\t")
f.Write(backlogJSON)
err = os.Rename(tempPath, path)
Check(err)
}