Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48a43dbc98 | |||
| 078c6d8bd5 | |||
| 12de0f1b6c | |||
| 10aabc8c0a | |||
| 5325c18c4e | |||
| be1dd05d9b |
99
README.md
99
README.md
@@ -1,92 +1,87 @@
|
||||
# GOTASK
|
||||
|
||||
Tiny project aiming at creating a CLI task tracker in Golang
|
||||
Tiny project aiming to create a CLI task tracker in Golang
|
||||
|
||||
## Purpose
|
||||
|
||||
1. Practice Golang
|
||||
1. Keep track of tasks
|
||||
|
||||
## Roadmap
|
||||
1. ~~Add~~
|
||||
2. ~~List all~~
|
||||
3. Progress
|
||||
4. Regress
|
||||
5. List by
|
||||
|
||||
## Features
|
||||
### Add tasks
|
||||
```console
|
||||
[user@host]$ gotask add "Publish code"
|
||||
[user@host]$ gotask-cli add "Publish code"
|
||||
New task "Publish code" added to do @ id=4
|
||||
```
|
||||
|
||||
### List all tasks
|
||||
```console
|
||||
[user@host]$ gotask list
|
||||
+---------------------------------------------------------------------+
|
||||
|id| Description | Status | Created | Updated |
|
||||
|--|--------------|-------------|------------------|------------------|
|
||||
| 1| Update doc | todo | 01-03-2026 09:23 | | # Never updated
|
||||
| 2| Write code | doing | 04-03-2026 10:13 | 05-03-2026 17:45 | # Set in progress at UpdateAt
|
||||
| 3| Write tests | done | 05-03-2026 11:34 | 01-04-2026 19:43 | # Done at UpdateAt
|
||||
| 4| Publish code | todo | 01-04-2026 09:13 | |
|
||||
+---------------------------------------------------------------------+
|
||||
[user@host]$ gotask-cli list
|
||||
+----+--------------+--------+---------------------+---------+
|
||||
| ID | DESCRIPTION | STATUS | CREATED | UPDATED |
|
||||
+----+--------------+--------+---------------------+---------+
|
||||
| 1 | Update doc | todo | 2026-04-03 22:34:40 | |
|
||||
| 2 | Write code | todo | 2026-04-03 22:38:26 | |
|
||||
| 3 | Write tests | todo | 2026-04-03 22:38:45 | |
|
||||
| 4 | Publish code | todo | 2026-04-03 22:38:56 | |
|
||||
+----+--------------+--------+---------------------+---------+
|
||||
```
|
||||
|
||||
### Mark progress
|
||||
```console
|
||||
[user@host]$ gotask progress 4
|
||||
[user@host]$ gotask-cli progress 4
|
||||
[todo] task "Publish code" progress to [doing]
|
||||
```
|
||||
|
||||
### List tasks by status
|
||||
```console
|
||||
[user@host]$ gotask list doing
|
||||
+---------------------------------------------------------------------+
|
||||
|id| Description | Status | Created | Updated |
|
||||
|--|--------------|-------------|------------------|------------------|
|
||||
| 2| Write code | doing | 04-03-2026 10:13 | 05-03-2026 17:45 |
|
||||
| 4| Publish code | doing | 01-04-2026 09:13 | 01-04-2026 10:30 |
|
||||
+---------------------------------------------------------------------+
|
||||
[user@host]$ gotask-cli list doing
|
||||
+----+--------------+--------+---------------------+---------------------+
|
||||
| ID | DESCRIPTION | STATUS | CREATED | UPDATED |
|
||||
+----+--------------+--------+---------------------+---------------------+
|
||||
| 4 | Publish code | doing | 2026-04-03 22:38:56 | 2026-04-03 22:44:36 |
|
||||
+----+--------------+--------+---------------------+---------------------+
|
||||
```
|
||||
|
||||
### Mark regress
|
||||
```console
|
||||
[user@host]$ gotask regress 4
|
||||
[user@host]$ gotask-cli regress 4
|
||||
[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
|
||||
For convenience reasons, the data will be stored in a JSON file with the following structure.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"Description": "Update doc",
|
||||
"Status": 0,
|
||||
"Created": 1772353380,
|
||||
"UpdatedAt": -1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"Description": "Write code",
|
||||
"Status": 1,
|
||||
"Created": 1772615580,
|
||||
"UpdatedAt": 1772729100
|
||||
}
|
||||
{
|
||||
"Id": 1,
|
||||
"Description": "Update doc",
|
||||
"Status": 0,
|
||||
"Created": 1775248480,
|
||||
"Updated": -1
|
||||
},
|
||||
{
|
||||
"Id": 2,
|
||||
"Description": "Write code",
|
||||
"Status": 0,
|
||||
"Created": 1775248706,
|
||||
"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
20
commands/add_task.go
Normal 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
9
commands/help.go
Normal 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
10
commands/list_tasks.go
Normal 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
28
commands/list_tasks_by.go
Normal 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
18
commands/progress_task.go
Normal 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
18
commands/regress_task.go
Normal 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
|
||||
}
|
||||
168
gotask-cli.go
168
gotask-cli.go
@@ -1,143 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
"strconv"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
type Status int
|
||||
|
||||
const (
|
||||
todo Status = iota
|
||||
doing
|
||||
done
|
||||
"gotask-cli/commands"
|
||||
"gotask-cli/utils"
|
||||
)
|
||||
|
||||
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() {
|
||||
initBacklog()
|
||||
backlog := utils.InitBacklog(path)
|
||||
if len(os.Args[1:]) >= 1 {
|
||||
switch os.Args[1] {
|
||||
case "help":
|
||||
fmt.Println("S.O.S!")
|
||||
commands.Help()
|
||||
case "add":
|
||||
if len(os.Args[1:]) >= 2 {
|
||||
addTask(os.Args[2])
|
||||
if len(os.Args[1:]) == 2 {
|
||||
backlog = commands.AddTask(os.Args[2], backlog)
|
||||
} else {
|
||||
fmt.Println("Missing argument")
|
||||
commands.Help()
|
||||
}
|
||||
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:
|
||||
fmt.Println("NO !")
|
||||
}
|
||||
saveBacklog()
|
||||
utils.SaveBacklog(backlog, path)
|
||||
} else {
|
||||
fmt.Println("Missing argument")
|
||||
commands.Help()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
23
models/task.go
Normal file
23
models/task.go
Normal 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
7
utils/err_checker.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package utils
|
||||
|
||||
func Check(e error) {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
33
utils/read_backlog.go
Normal file
33
utils/read_backlog.go
Normal 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
31
utils/table_render.go
Normal 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
29
utils/write_backlog.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user