├── .github └── assets │ ├── todo.gif │ └── todo.png ├── .gitignore ├── go.mod ├── pkg ├── pprint │ └── pprint.go ├── file │ └── file.go └── todo │ ├── todo.go │ └── op.go ├── go.sum ├── cmd └── todo │ └── main.go └── README.md /.github/assets/todo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HxX2/todocli/HEAD/.github/assets/todo.gif -------------------------------------------------------------------------------- /.github/assets/todo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HxX2/todocli/HEAD/.github/assets/todo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #-----------------------# 2 | # Todocli Ignores # 3 | #-----------------------# 4 | 5 | #build 6 | /todo 7 | 8 | #other 9 | *.old 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/HxX2/todo 2 | 3 | go 1.22.4 4 | 5 | require ( 6 | github.com/fatih/color v1.17.0 7 | ) 8 | 9 | require ( 10 | github.com/mattn/go-colorable v0.1.13 // indirect 11 | github.com/mattn/go-isatty v0.0.20 // indirect 12 | golang.org/x/sys v0.21.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/pprint/pprint.go: -------------------------------------------------------------------------------- 1 | package pprint 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | ) 8 | 9 | func Print(s string, colors ...color.Attribute) { 10 | color.Set(colors...) 11 | fmt.Print(s) 12 | color.Set(color.Reset) 13 | } 14 | 15 | func Error(msg string) { 16 | Print("Error: ", color.FgRed, color.Bold) 17 | Print(msg) 18 | } 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 2 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 3 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 4 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 5 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 6 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 7 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 8 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 9 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 10 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 11 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 12 | -------------------------------------------------------------------------------- /cmd/todo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/HxX2/todo/pkg/todo" 7 | ) 8 | 9 | func main() { 10 | t := todo.Init() 11 | 12 | addPtr := flag.String("a", "", "add a task") 13 | remPtr := flag.Int("r", 0, "remove a task") 14 | togglePtr := flag.Int("t", 0, "toggle done for a task") 15 | editPtr := flag.Bool("e", false, "edite todo file") 16 | listDonePtr := flag.Bool("ld", false, "list done tasks") 17 | listUndonePtr := flag.Bool("lu", false, "list undone tasks") 18 | hideProgressPtr := flag.Bool("hp", false, "hide progress bar") 19 | initPtr := flag.Bool("i", false, "initialize todo file in git project") 20 | 21 | flag.Parse() 22 | 23 | remTaskNum := *remPtr 24 | newTask := *addPtr 25 | toggleTaskNum := *togglePtr 26 | editFlag := *editPtr 27 | initFlag := *initPtr 28 | 29 | t.ListDone = !*listUndonePtr 30 | t.ListUndone = !*listDonePtr 31 | t.ShowProgress = !*hideProgressPtr 32 | 33 | switch { 34 | case remTaskNum != 0: 35 | t.RemTask(remTaskNum) 36 | case newTask != "": 37 | t.AddTask(newTask) 38 | case toggleTaskNum != 0: 39 | t.ToggleTask(toggleTaskNum) 40 | case editFlag: 41 | t.OpenEditor() 42 | case initFlag: 43 | t.ProjectInit() 44 | case t.ListUndone: 45 | t.PrintList() 46 | case t.ListDone: 47 | t.PrintList() 48 | default: 49 | t.PrintList() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/HxX2/todo/pkg/pprint" 9 | ) 10 | 11 | func Open(filePath string) *os.File { 12 | file, err := os.Open(filePath) 13 | 14 | if err != nil { 15 | pprint.Error(fmt.Sprintf("Can't open %s", filePath)) 16 | } 17 | 18 | return file 19 | } 20 | 21 | func Write(filePath string, content string, flag int) { 22 | file, err := os.OpenFile(filePath, flag|os.O_WRONLY|os.O_CREATE, 0644) 23 | defer file.Close() 24 | if err != nil { 25 | pprint.Error(fmt.Sprintf("Can't open %s", filePath)) 26 | return 27 | } 28 | 29 | _, err = file.WriteString(content) 30 | if err != nil { 31 | pprint.Error(fmt.Sprintf("Can't write in %s", filePath)) 32 | return 33 | } 34 | } 35 | 36 | func Size(filepath string) int64 { 37 | file, err := os.Open(filepath) 38 | if err != nil { 39 | pprint.Error(fmt.Sprintf("Can't open %s", filepath)) 40 | } 41 | defer file.Close() 42 | 43 | fileStat, err := file.Stat() 44 | if err != nil { 45 | pprint.Error(fmt.Sprintf("Can't get file stat %s", filepath)) 46 | } 47 | 48 | return fileStat.Size() 49 | } 50 | 51 | func GetGitRoot() string { 52 | output, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() 53 | if err != nil { 54 | return "" 55 | } 56 | 57 | gitRoot := string(output[:len(output)-1]) 58 | 59 | return gitRoot 60 | } 61 | -------------------------------------------------------------------------------- /pkg/todo/todo.go: -------------------------------------------------------------------------------- 1 | package todo 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/HxX2/todo/pkg/file" 9 | "github.com/HxX2/todo/pkg/pprint" 10 | ) 11 | 12 | type Todo struct { 13 | filePath string 14 | listName string 15 | doneCount float64 16 | undoneCount float64 17 | 18 | ListDone bool 19 | ListUndone bool 20 | ShowProgress bool 21 | } 22 | 23 | func Init() *Todo { 24 | todo := new(Todo) 25 | 26 | configDir := filepath.Join(os.Getenv("HOME"), ".config", "todo") 27 | filePath := filepath.Join(configDir, "todo.txt") 28 | 29 | _, err := os.Stat(filePath) 30 | if os.IsNotExist(err) { 31 | err = os.MkdirAll(configDir, 0755) 32 | if err != nil { 33 | pprint.Error(fmt.Sprintf("Can't create config directory\n%s\n", err)) 34 | return nil 35 | } 36 | 37 | file, err := os.Create(filePath) 38 | defer file.Close() 39 | if err != nil { 40 | pprint.Error(fmt.Sprintf("Can't create todo.txt file\n%s\n", err)) 41 | return nil 42 | } 43 | 44 | } else if err != nil { 45 | pprint.Error(fmt.Sprintf("Can't check file\n%s\n", err)) 46 | return nil 47 | } 48 | 49 | gitRoot := file.GetGitRoot() 50 | if gitRoot != "" { 51 | gitFilePath := filepath.Join(gitRoot, "todo.txt") 52 | 53 | _, err := os.Stat(gitFilePath) 54 | if !os.IsNotExist(err) { 55 | filePath = gitFilePath 56 | todo.listName = "󰊢 " + filepath.Base(gitRoot) 57 | } 58 | } 59 | 60 | todo.filePath = filePath 61 | todo.ListDone = true 62 | todo.ListUndone = true 63 | todo.ShowProgress = true 64 | todo.doneCount = 0 65 | todo.undoneCount = 0 66 | 67 | return todo 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Todo CLI 2 | ============ 3 | 4 | 5 | ![prompt](https://raw.githubusercontent.com/HxX2/todocli/v1.0.0/.github/assets/todo.gif) 6 | 7 | 8 | **Table of Contents** 9 | 10 | 11 | 12 | - [About](#about) 13 | * [Installing](#installing) 14 | * [Uninstalling](#uninstalling) 15 | * [Build From Source](#build-from-source) 16 | * [Usage](#usage) 17 | 18 | 19 | 20 | ## About 21 | 22 | Todo CLI is a simple to do list to manage your tasks. 23 | Written in GO and styled with [Nerd Fonts](https://www.nerdfonts.com/) 24 | 25 | ### Installing 26 | 27 | ```console 28 | GOBIN= go install ./cmd/todo 29 | ``` 30 | 31 | ### Uninstalling 32 | 33 | ```console 34 | rm -rf /todo 35 | ``` 36 | 37 | ### Build From Source 38 | 39 | Install Go and build with this command: 40 | 41 | ```console 42 | go build ./cmd/todo 43 | ``` 44 | 45 | ### Usage 46 | 47 | To init a todo list in the current git directory 48 | ```console 49 | todo -i 50 | ``` 51 | 52 | To add a task to the list 53 | 54 | ```console 55 | todo -a 56 | ``` 57 | Toggle a task as done or undone 58 | 59 | ```console 60 | todo -t 61 | ``` 62 | Remove a Task from the list 63 | 64 | ```console 65 | todo -r 66 | ``` 67 | Opens editor to edite the raw file of the list (it uses the $EDITOR env var) 68 | 69 | ```console 70 | todo -e 71 | ``` 72 | 73 | List done tasks 74 | 75 | ```console 76 | todo -ld 77 | ``` 78 | 79 | List undone tasks 80 | 81 | ```console 82 | todo -lu 83 | ``` 84 | 85 | Hide Progress bar (can be used with other options) 86 | 87 | ```console 88 | todo -hp 89 | ``` 90 | -------------------------------------------------------------------------------- /pkg/todo/op.go: -------------------------------------------------------------------------------- 1 | package todo 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/fatih/color" 12 | 13 | "github.com/HxX2/todo/pkg/file" 14 | "github.com/HxX2/todo/pkg/pprint" 15 | ) 16 | 17 | func (t Todo) PrintList() { 18 | file := file.Open(t.filePath) 19 | defer file.Close() 20 | 21 | scanner := bufio.NewScanner(file) 22 | 23 | pprint.Print("", color.FgMagenta) 24 | pprint.Print(" "+t.listName+" ToDo List ", color.BgMagenta, color.FgBlack, color.Bold) 25 | pprint.Print("\n\n", color.FgMagenta) 26 | 27 | i := 1 28 | for scanner.Scan() { 29 | line := scanner.Text() 30 | if strings.Contains(line, "[X]") && t.ListDone { 31 | pprint.Print(fmt.Sprintf("%2d ", i), color.FgWhite, color.Faint) 32 | pprint.Print(" ", color.FgGreen) 33 | pprint.Print(fmt.Sprintf("%s\n", line[4:]), color.Bold, color.CrossedOut) 34 | t.doneCount++ 35 | } else if strings.Contains(line, "[]") && t.ListUndone { 36 | pprint.Print(fmt.Sprintf("%2d ", i), color.FgWhite, color.Faint) 37 | pprint.Print(" ") 38 | pprint.Print(fmt.Sprintf("%s\n", line[3:]), color.Bold) 39 | t.undoneCount++ 40 | } 41 | i++ 42 | } 43 | 44 | t.printProgress() 45 | 46 | if err := scanner.Err(); err != nil { 47 | pprint.Error("Can't read file") 48 | } 49 | } 50 | 51 | func (t Todo) RemTask(taskId int) { 52 | newLines := make([]string, 0) 53 | todoFile := file.Open(t.filePath) 54 | defer todoFile.Close() 55 | 56 | i := 1 57 | scanner := bufio.NewScanner(todoFile) 58 | for scanner.Scan() { 59 | line := scanner.Text() 60 | if i != taskId { 61 | newLines = append(newLines, line) 62 | } 63 | i++ 64 | } 65 | 66 | if err := scanner.Err(); err != nil { 67 | pprint.Error(fmt.Sprintf("Can't read %s", t.filePath)) 68 | } 69 | 70 | file.Write(t.filePath, strings.Join(newLines, "\n"), os.O_TRUNC) 71 | 72 | t.PrintList() 73 | } 74 | 75 | func (t Todo) AddTask(task string) { 76 | var ftask string 77 | 78 | if file.Size(t.filePath) == 0 { 79 | ftask = fmt.Sprintf("[] %s", task) 80 | } else { 81 | ftask = fmt.Sprintf("\n[] %s", task) 82 | } 83 | 84 | file.Write(t.filePath, ftask, os.O_APPEND) 85 | 86 | t.PrintList() 87 | } 88 | 89 | func (t Todo) ToggleTask(taskId int) { 90 | newLines := make([]string, 0) 91 | todoFile := file.Open(t.filePath) 92 | defer todoFile.Close() 93 | 94 | i := 1 95 | scanner := bufio.NewScanner(todoFile) 96 | for scanner.Scan() { 97 | line := scanner.Text() 98 | if i == taskId { 99 | if strings.Contains(line, "[X]") { 100 | line = strings.Replace(line, "[X", "[", 1) 101 | } else if strings.Contains(line, "[]") { 102 | line = strings.Replace(line, "[", "[X", 1) 103 | } 104 | } 105 | newLines = append(newLines, line) 106 | i++ 107 | } 108 | 109 | if err := scanner.Err(); err != nil { 110 | pprint.Error(fmt.Sprintf("Can't read %s", t.filePath)) 111 | } 112 | 113 | file.Write(t.filePath, strings.Join(newLines, "\n"), os.O_TRUNC) 114 | 115 | t.PrintList() 116 | } 117 | 118 | func (t Todo) OpenEditor() { 119 | editor := os.Getenv("EDITOR") 120 | 121 | if editor == "" { 122 | pprint.Error("Can't open editor [no $EDITOR env]") 123 | return 124 | } 125 | 126 | cmd := exec.Command(editor, t.filePath) 127 | cmd.Stdin = os.Stdin 128 | cmd.Stdout = os.Stdout 129 | cmd.Stderr = os.Stderr 130 | 131 | err := cmd.Run() 132 | 133 | if err != nil { 134 | pprint.Error(fmt.Sprintf("Failed to open editor\n%s\n", err)) 135 | return 136 | } 137 | } 138 | 139 | func (t Todo) ProjectInit() { 140 | gitRoot := file.GetGitRoot() 141 | if gitRoot == "" { 142 | pprint.Error("Not in a git repository\n") 143 | return 144 | } 145 | 146 | todoFile := filepath.Join(gitRoot, "todo.txt") 147 | 148 | _, err := os.Stat(todoFile) 149 | if os.IsNotExist(err) { 150 | file, err := os.Create(todoFile) 151 | defer file.Close() 152 | if err != nil { 153 | pprint.Error(fmt.Sprintf("Can't create todo.txt file\n%s\n", err)) 154 | return 155 | } 156 | } 157 | 158 | pprint.Print("todo file created successfully in 󰊢 root \n", color.FgGreen, color.Bold) 159 | 160 | } 161 | 162 | func (t Todo) printProgress() { 163 | if !t.ShowProgress || !t.ListDone || !t.ListUndone || (t.doneCount == 0 && t.undoneCount == 0) { 164 | return 165 | } 166 | 167 | lineWidth := 50.0 168 | total := t.doneCount + t.undoneCount 169 | doneWidth := lineWidth * (t.doneCount / total) 170 | undoneWidth := (lineWidth - doneWidth) - 1 171 | percentage := (t.doneCount / total) * 100 172 | 173 | fmt.Print("\n") 174 | for i := 0; i < int(doneWidth); i++ { 175 | pprint.Print("━", color.FgGreen) 176 | } 177 | 178 | if t.undoneCount != 0 { 179 | pprint.Print("╺", color.FgWhite, color.Faint) 180 | } 181 | 182 | for i := 0; i < int(undoneWidth); i++ { 183 | pprint.Print("━", color.FgWhite, color.Faint) 184 | } 185 | 186 | fmt.Printf(" %d%% Done\n", int(percentage)) 187 | } 188 | --------------------------------------------------------------------------------