├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── build.sh ├── screenshot.png ├── task.go ├── task_test.go ├── taskmanager ├── taskmanager.go └── taskmanager_test.go └── vendor └── vendor.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | */.DS_Store 4 | task 5 | .task.json 6 | vendor/* 7 | !vendor/vendor.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - go: 1.4 7 | - go: 1.5 8 | - go: 1.6 9 | - go: 1.7 10 | - go: 1.8 11 | - go: 1.9 12 | - go: tip 13 | allow_failures: 14 | - go: tip 15 | 16 | script: 17 | - go get -t -v ./... 18 | - diff -u <(echo -n) <(gofmt -d .) 19 | - go vet $(go list ./... | grep -v /vendor/) 20 | - go test -v -race ./... 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Saddam H 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Task 2 | ========= 3 | 4 | [![Build Status](https://travis-ci.org/thedevsaddam/task.svg?branch=master)](https://travis-ci.org/thedevsaddam/task) 5 | ![Project status](https://img.shields.io/badge/version-1.0-green.svg) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/thedevsaddam/task)](https://goreportcard.com/report/github.com/thedevsaddam/task) 7 | 8 | Terminal tasks todo tool for geek 9 | 10 | Buy Me A Coffee 11 | 12 | ![Task screenshot](https://raw.githubusercontent.com/thedevsaddam/task/master/screenshot.png) 13 | 14 | ### [Download Binary](https://github.com/thedevsaddam/task_binaries) 15 | 16 | Mac/Linux download the binary 17 | ```bash 18 | $ cp task /usr/local/bin/task 19 | $ sudo chmod +x /usr/local/bin/task 20 | ``` 21 | For windows download the binary and set environment variable so that you can access the binary from terminal 22 | 23 | ### Custom File Path 24 | If you are interested to sync the task in Dropbox/Google drive, you can set a custom path. To set a custom path 25 | open your `.bashrc` or `.bash_profile` and add this line `export TASK_DB_FILE_PATH=Your file path` 26 | 27 | Example File path 28 | ```bash 29 | export TASK_DB_FILE_PATH=/home/thedevsaddam/Dropbox # default file name will be .task.json 30 | export TASK_DB_FILE_PATH=/home/thedevsaddam/Dropbox/mytasks.json 31 | ``` 32 | 33 | ### Usage 34 | * List all the tasks 35 | ```bash 36 | $ task 37 | ``` 38 | * Add a new task to list 39 | ```bash 40 | $ task a Pirates of the Caribbean: Dead Men Tell No Tales 41 | ``` 42 | * Add a **reminder** task to list 43 | ```bash 44 | $ task reminder Meeting with Jane next wednesday at 2:30pm 45 | ``` 46 | * List all pending tasks 47 | ```bash 48 | $ task p 49 | ``` 50 | * Show a task details 51 | ```bash 52 | $ task s ID 53 | ``` 54 | * Mark a task as completed 55 | ```bash 56 | $ task c ID 57 | ``` 58 | * Mark a task as pending 59 | ```bash 60 | $ task p ID 61 | ``` 62 | * Modify a task task 63 | ```bash 64 | $ task m ID Watch Game of Thrones 65 | ``` 66 | * Delete latest task 67 | ```bash 68 | $ task del 69 | ``` 70 | * Remove a specific task by id 71 | ```bash 72 | $ task r ID 73 | ``` 74 | * Flush/Delete all the tasks 75 | ```bash 76 | $ task flush 77 | ``` 78 | * To start the program as service (Note: Must use as service if you are using **reminder**) 79 | ```bash 80 | $ task service-start # Start service 81 | $ task service-force-start # Forcefully start service 82 | $ task service-stop #stop service 83 | ``` 84 | 85 | ##### Examples of reminder 86 | ```bash 87 | $ task remind Take a cup of coffee in 30min 88 | $ task remind Watch game of thrones season 7 today 8:30pm 89 | $ task remind Watch despicable me 3 next friday at 3pm 90 | $ task remind Bug fix of the docker and send PR next thursday 91 | ``` 92 | 93 | ### Build yourself 94 | 95 | Go to your $GOPATH/src and get the package 96 | ```bash 97 | $ go get github.com/thedevsaddam/task 98 | ``` 99 | 100 | Install dependency management tool go [govendor](https://github.com/kardianos/govendor) 101 | ```bash 102 | $ go get -u github.com/kardianos/govendor 103 | ``` 104 | 105 | To install dependencies go to project root and `$ cd vendor` 106 | ```bash 107 | $ govendor sync 108 | ``` 109 | 110 | In unix system use 111 | ```bash 112 | $ ./build 113 | ``` 114 | 115 | ### Some awesome packages are used to make this awesome task :) 116 | * [Notifier](https://github.com/0xAX/notificator) 117 | * [Auto start/service](https://github.com/ProtonMail/go-autostart) 118 | * [Color](https://github.com/fatih/color) 119 | * [Natural date parser](https://github.com/olebedev/when) 120 | * [Table writter](https://github.com/olekukonko/tablewriter) 121 | * [Go prompt](https://github.com/segmentio/go-prompt) 122 | * [Task manager](https://github.com/thedevsaddam/task/taskmanager) 123 | 124 | ### Contribution 125 | There are some tasks that need to be done. I have tried to make a minimal setup, need more code refactoring, review, bug fixing and adding features. 126 | If you are interested to make this application better please send pull requests. 127 | 128 | ### **License** 129 | The **task** is a open-source software licensed under the [MIT License](LICENSE.md). 130 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #MAC OS 4 | export GOARCH="386" 5 | export GOOS="darwin" 6 | export CGO_ENABLED=1 7 | go build -o mac_386 -v 8 | 9 | export GOARCH="amd64" 10 | export GOOS="darwin" 11 | export CGO_ENABLED=1 12 | go build -o mac_amd64 -v 13 | 14 | #LINUX 15 | export GOARCH="amd64" 16 | export GOOS="linux" 17 | export CGO_ENABLED=0 18 | go build -o linux_amd64 -v 19 | 20 | export GOARCH="386" 21 | export GOOS="linux" 22 | export CGO_ENABLED=0 23 | go build -o linux_386 -v 24 | 25 | #FREEBSD 26 | export GOARCH="amd64" 27 | export GOOS="freebsd" 28 | export CGO_ENABLED=0 29 | go build -o freebsd_amd64 -v 30 | 31 | export GOARCH="386" 32 | export GOOS="freebsd" 33 | export CGO_ENABLED=1 34 | go build -o freebsd_386 -v 35 | 36 | #OPENBSD 37 | export GOARCH="amd64" 38 | export GOOS="openbsd" 39 | export CGO_ENABLED=0 40 | go build -o freebsd_amd64 -v 41 | 42 | export GOARCH="386" 43 | export GOOS="openbsd" 44 | export CGO_ENABLED=0 45 | go build -o freebsd_386 -v 46 | 47 | #WINDOWS 48 | export GOARCH="386" 49 | export GOOS="windows" 50 | export CGO_ENABLED=0 51 | go build -o windows_386.exe -v 52 | 53 | export GOARCH="amd64" 54 | export GOOS="windows" 55 | export CGO_ENABLED=0 56 | go build -o windows_amd64.exe -v -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevsaddam/task/f1e40bb4cc6ea7628a8a5b0ee1e400488971d79f/screenshot.png -------------------------------------------------------------------------------- /task.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/0xAX/notificator" 13 | "github.com/ProtonMail/go-autostart" 14 | "github.com/fatih/color" 15 | "github.com/olebedev/when" 16 | "github.com/olebedev/when/rules/common" 17 | "github.com/olebedev/when/rules/en" 18 | "github.com/olekukonko/tablewriter" 19 | "github.com/segmentio/go-prompt" 20 | "github.com/thedevsaddam/task/taskmanager" 21 | ) 22 | 23 | const usage = `Usage: 24 | Name: 25 | Terminal Task 26 | Description: 27 | Your favorite terminal task manager and reminder! 28 | Version: 29 | 1.0.0 30 | $ task 31 | Show all tasks 32 | $ task p 33 | Show all pending tasks 34 | $ task a Watch Games of thrones 35 | Add a new task [Watch Games of thrones] to list 36 | $ task remind Meeting with John tomorrow at 10:30pm 37 | This will send you a desktop notification 38 | $ task del 39 | Remove latest task from list 40 | $ task rm ID 41 | Remove task of ID from list 42 | $ task s ID 43 | Show detail view task of ID 44 | $ task c ID 45 | Mark task of ID as completed 46 | $ task m ID Pirates of the Caribbean 47 | Modify a task 48 | $ task p ID 49 | Mark task of ID as pending 50 | $ task flush 51 | Flush the database! 52 | $ task service-start 53 | Run task as service if you are using reminder 54 | $ task service-stop 55 | Unregister Task from service! 56 | ` 57 | 58 | const ( 59 | completedSign = "\u2713" 60 | pendingSign = "\u2613" 61 | dateTimeLayout = "2006-01-02 15:04" 62 | refreshRate = 40 63 | ) 64 | 65 | var ( 66 | //task manager instance 67 | tm = taskmanager.New() 68 | //notifier 69 | notify *notificator.Notificator 70 | //run a service 71 | service = autostart.App{ 72 | Name: "thedevsaddam_terminal_task", 73 | DisplayName: "Task", 74 | Exec: []string{"/usr/local/bin/task", "listen-reminder-queue"}, 75 | } 76 | ) 77 | 78 | func main() { 79 | 80 | flag.Usage = func() { 81 | fmt.Fprint(os.Stderr, usage) 82 | flag.PrintDefaults() 83 | } 84 | flag.Parse() 85 | cmd, args, argsLen := flag.Arg(0), flag.Args(), len(flag.Args()) 86 | 87 | switch { 88 | case cmd == "" || cmd == "l" || cmd == "ls" && argsLen == 1: 89 | showTasksInTable(tm.GetAllTasks()) 90 | case cmd == "a" || cmd == "add" && argsLen >= 1: 91 | if len(args[1:]) <= 0 { 92 | warningText(" Task description can not be empty \n") 93 | return 94 | } 95 | tm.Add(strings.Join(args[1:], " "), "", "") 96 | successText(" Added to list: " + strings.Join(args[1:], " ") + " ") 97 | case cmd == "reminder" || cmd == "remind" || cmd == "remind-me" && argsLen >= 1: 98 | if len(args[1:]) <= 0 { 99 | warningText(" Task/Reminder description can not be empty \n") 100 | return 101 | } 102 | reminder := strings.Join(args[1:], " ") 103 | action, actionWhen := parseReminder(reminder) 104 | tm.Add(action, "", actionWhen) 105 | successText(" Reminder Added: " + action + " ") 106 | case cmd == "p" || cmd == "pending" && argsLen == 1: 107 | showTasksInTable(tm.GetPendingTasks()) 108 | case cmd == "del" || cmd == "delete" && argsLen == 1: 109 | p := prompt.Choose("Do you want to delete latest task?", []string{"yes", "no"}) 110 | if p == 1 { 111 | warningText(" Task delete aboarted! ") 112 | return 113 | } 114 | err := tm.RemoveTask(tm.GetLastId()) 115 | if err != nil { 116 | errorText(err.Error()) 117 | return 118 | } 119 | successText(" Removed latest task ") 120 | case cmd == "r" || cmd == "rm" && argsLen == 2: 121 | id, _ := strconv.Atoi(flag.Arg(1)) 122 | p := prompt.Choose("Do you want to delete task of id "+flag.Arg(1)+" ?", []string{"yes", "no"}) 123 | if p == 1 { 124 | warningText(" Task delete aboarted! ") 125 | return 126 | } 127 | err := tm.RemoveTask(id) 128 | if err != nil { 129 | errorText(err.Error()) 130 | return 131 | } 132 | successText(" Task " + strconv.Itoa(id) + " removed! ") 133 | case cmd == "e" || cmd == "m" || cmd == "u" && argsLen >= 2: 134 | id, _ := strconv.Atoi(flag.Arg(1)) 135 | ok, _ := tm.UpdateTask(id, strings.Join(args[2:], " ")) 136 | successText(ok) 137 | case cmd == "c" || cmd == "d" || cmd == "done" && argsLen >= 2: 138 | id, _ := strconv.Atoi(flag.Arg(1)) 139 | task, err := tm.MarkAsCompleteTask(id) 140 | if err != nil { 141 | errorText(err.Error()) 142 | return 143 | } 144 | successText(" " + completedSign + " " + task.Description) 145 | case cmd == "i" || cmd == "p" || cmd == "pending" && argsLen >= 2: 146 | id, _ := strconv.Atoi(flag.Arg(1)) 147 | task, err := tm.MarkAsPendingTask(id) 148 | if err != nil { 149 | errorText(err.Error()) 150 | return 151 | } 152 | successText(" " + pendingMark() + " " + task.Description) 153 | case cmd == "s" && argsLen == 2: 154 | id, _ := strconv.Atoi(flag.Arg(1)) 155 | task, err := tm.GetTask(id) 156 | if err != nil { 157 | errorText(err.Error()) 158 | return 159 | } 160 | showTask(task) 161 | case cmd == "flush": 162 | p := prompt.Choose("Do you want to delete all tasks?", []string{"yes", "no"}) 163 | if p == 1 { 164 | warningText(" Flush aborted! ") 165 | return 166 | } 167 | err := tm.FlushDB() 168 | if err != nil { 169 | errorText(err.Error()) 170 | return 171 | } 172 | successText(" Database flushed successfully! ") 173 | case cmd == "service-start" && argsLen == 1: 174 | serviceStart() 175 | case cmd == "service-force-start" && argsLen == 1: 176 | serviceForceStart() 177 | case cmd == "service-stop" && argsLen == 1: 178 | serviceStop() 179 | case cmd == "listen-reminder-queue" && argsLen == 1: 180 | listenReminderQueue() 181 | case cmd == "h" || cmd == "v": 182 | fmt.Fprint(os.Stderr, usage) 183 | default: 184 | errorText(" [No command found by " + cmd + "] ") 185 | fmt.Fprint(os.Stderr, "\n"+usage) 186 | } 187 | 188 | } 189 | 190 | //show tasks list in table 191 | func showTasksInTable(tasks taskmanager.Tasks) { 192 | fmt.Fprintln(os.Stdout, "") 193 | table := tablewriter.NewWriter(os.Stdout) 194 | table.SetHeader([]string{"ID", "Description", completedSign + "/" + pendingMark(), "Created"}) 195 | table.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: false}) 196 | table.SetFooter([]string{"", "Total: " + strconv.Itoa(tm.TotalTask()), "", "Pending: " + strconv.Itoa(tm.PendingTask())}) 197 | table.SetCenterSeparator("|") 198 | table.SetRowLine(true) 199 | for _, task := range tasks { 200 | //set completed icon 201 | status := pendingSign 202 | if task.Completed != "" { 203 | status = completedSign 204 | } else { 205 | status = pendingMark() 206 | } 207 | table.Append([]string{ 208 | strconv.Itoa(task.Id), 209 | task.Description, 210 | status, 211 | task.Created, 212 | }) 213 | } 214 | table.Render() 215 | fmt.Fprintln(os.Stdout, "") 216 | } 217 | 218 | //show a single tasks 219 | func showTask(task taskmanager.Task) { 220 | fmt.Fprintln(os.Stdout, "") 221 | printText("Task Details view") 222 | printText("--------------------------------") 223 | printText("ID: " + strconv.Itoa(task.Id)) 224 | printText("UID: " + task.UID) 225 | printText("Description: " + task.Description) 226 | printText("Tag: " + task.Tag) 227 | printText("Created: " + task.Created) 228 | printText("Updated: " + task.Updated) 229 | fmt.Fprintln(os.Stdout, "") 230 | } 231 | 232 | func printText(str string) { 233 | fmt.Fprintf(os.Stdout, str+"\n") 234 | } 235 | 236 | func printBoldText(str string) { 237 | if runtime.GOOS == "windows" { 238 | fmt.Fprintf(os.Stdout, str+"\n") 239 | } else { 240 | bold := color.New(color.Bold).FprintlnFunc() 241 | bold(os.Stdout, str) 242 | } 243 | } 244 | 245 | func successText(str string) { 246 | if runtime.GOOS == "windows" { 247 | fmt.Fprintf(color.Output, color.GreenString(str)) 248 | } else { 249 | success := color.New(color.Bold, color.BgGreen, color.FgWhite).FprintlnFunc() 250 | success(os.Stdout, str) 251 | } 252 | } 253 | 254 | func warningText(str string) { 255 | if runtime.GOOS == "windows" { 256 | fmt.Fprintf(color.Output, color.YellowString(str)) 257 | } else { 258 | warning := color.New(color.Bold, color.BgYellow, color.FgBlack).FprintlnFunc() 259 | warning(os.Stdout, str) 260 | } 261 | } 262 | 263 | func errorText(str string) { 264 | if runtime.GOOS == "windows" { 265 | fmt.Fprintf(color.Output, color.RedString(str)) 266 | } else { 267 | errColor := color.New(color.Bold, color.BgRed, color.FgWhite).FprintlnFunc() 268 | errColor(os.Stdout, str) 269 | } 270 | } 271 | 272 | func pendingMark() string { 273 | pending := pendingSign 274 | if runtime.GOOS == "windows" { 275 | pending = "x" 276 | } 277 | return pending 278 | } 279 | 280 | //parse reminder 281 | func parseReminder(reminder string) (string, string) { 282 | defer func() { 283 | if r := recover(); r != nil { 284 | errorText(" Your reminder does not contain any date time reference! ") 285 | os.Exit(1) 286 | } 287 | }() 288 | w := when.New(nil) 289 | w.Add(en.All...) 290 | w.Add(common.All...) 291 | r, _ := w.Parse(reminder, time.Now()) 292 | action := strings.Replace(reminder, reminder[r.Index:r.Index+len(r.Text)], "", -1) 293 | actionTime := r.Time.Format(dateTimeLayout) 294 | return action, actionTime 295 | } 296 | 297 | //listen for reminder queue 298 | func listenReminderQueue() { 299 | for { 300 | rm := taskmanager.New() 301 | reminderList := rm.GetReminderTasks() 302 | now := time.Now().Format(dateTimeLayout) 303 | for _, r := range reminderList { 304 | if r.RemindAt == now { 305 | desktopNotifier("Task Reminder!", r.Description) 306 | rm.MarkAsCompleteTask(r.Id) 307 | } 308 | } 309 | time.Sleep(time.Second * refreshRate) 310 | } 311 | } 312 | 313 | //send desktop notification 314 | func desktopNotifier(title, body string) { 315 | notify = notificator.New(notificator.Options{ 316 | DefaultIcon: "default.png", 317 | AppName: "Terminal Task", 318 | }) 319 | notify.Push(title, body, "default.png", notificator.UR_NORMAL) 320 | } 321 | 322 | //enable auto start 323 | func serviceStart() { 324 | if service.IsEnabled() { 325 | warningText("Task is already enabled as service!") 326 | } else { 327 | if err := service.Enable(); err != nil { 328 | errorText(err.Error()) 329 | } 330 | successText("Task has been registered as service!") 331 | } 332 | } 333 | 334 | //disable auto start 335 | func serviceStop() { 336 | if service.IsEnabled() { 337 | if err := service.Disable(); err != nil { 338 | errorText(err.Error()) 339 | } 340 | successText("Task has been removed from service!") 341 | } else { 342 | warningText("Task was not registered as service!") 343 | } 344 | } 345 | 346 | //force start 347 | func serviceForceStart() { 348 | serviceStop() 349 | serviceStart() 350 | } 351 | -------------------------------------------------------------------------------- /task_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/thedevsaddam/task/taskmanager" 5 | ) 6 | 7 | func Example_showTask() { 8 | showTask(taskmanager.Task{ 9 | Id: 1, 10 | UID: "213e9bb0-79e8-4647-8902-8421271e1809", 11 | Description: "Watch Pirates of the Caribbean: Dead Men Tell No Tales", 12 | Tag: "low", 13 | Created: "Fri, 07/21/17, 12:13PM", 14 | Updated: "Fri, 07/21/17, 12:15PM", 15 | Completed: "Fri, 07/21/17, 24:10PM", 16 | }) 17 | //output: 18 | // 19 | //Task Details view 20 | //-------------------------------- 21 | //ID: 1 22 | //UID: 213e9bb0-79e8-4647-8902-8421271e1809 23 | //Description: Watch Pirates of the Caribbean: Dead Men Tell No Tales 24 | //Tag: low 25 | //Created: Fri, 07/21/17, 12:13PM 26 | //Updated: Fri, 07/21/17, 12:15PM 27 | // 28 | } 29 | -------------------------------------------------------------------------------- /taskmanager/taskmanager.go: -------------------------------------------------------------------------------- 1 | package taskmanager 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "os/user" 12 | "path/filepath" 13 | "sort" 14 | "strconv" 15 | "strings" 16 | "sync" 17 | "time" 18 | ) 19 | 20 | type ( 21 | // Task describes a task object 22 | Task struct { 23 | Id int `json:"id"` 24 | UID string `json:"uid"` 25 | Description string `json:"description"` 26 | Tag string `json:"tag"` 27 | Created string `json:"created"` 28 | Updated string `json:"updated"` 29 | RemindAt string `json:"remind_at"` 30 | Completed string `json:"completed"` 31 | } 32 | 33 | // Tasks represents a list of Task object 34 | Tasks []Task 35 | ) 36 | 37 | const ( 38 | // dbFileName is the default storage file path 39 | dbFileName = ".task.json" 40 | // timeLayout default time layout for task application 41 | timeLayout = "Mon, 01/02/06, 03:04PM" 42 | ) 43 | 44 | var mutex sync.Mutex 45 | 46 | // New return a Task list instance 47 | func New() Tasks { 48 | return readDBFile() 49 | } 50 | 51 | //Add create a new task 52 | func (t *Tasks) Add(description, tag string, remind string) Task { 53 | _t := Task{Id: t.GetNextId(), UID: uid(), Description: description, Tag: tag, Created: time.Now().Format(timeLayout), RemindAt: remind, Completed: ""} 54 | *t = append(*t, _t) 55 | writeDBFile(*t) 56 | return _t 57 | } 58 | 59 | //GetAllTasks fetch all tasks 60 | func (t Tasks) GetAllTasks() Tasks { 61 | sort.Sort(t) 62 | return t 63 | } 64 | 65 | //GetCompletedTasks fetch all completed tasks 66 | func (t Tasks) GetCompletedTasks() Tasks { 67 | var completedTasks Tasks 68 | for _, item := range t { 69 | if item.Completed != "" { 70 | completedTasks = append(completedTasks, item) 71 | } 72 | } 73 | sort.Sort(completedTasks) 74 | return completedTasks 75 | } 76 | 77 | //GetPendingTasks fetch all pending tasks 78 | func (t Tasks) GetPendingTasks() Tasks { 79 | var pendingTasks Tasks 80 | for _, item := range t { 81 | if item.Completed == "" { 82 | pendingTasks = append(pendingTasks, item) 83 | } 84 | } 85 | sort.Sort(pendingTasks) 86 | return pendingTasks 87 | } 88 | 89 | //GetReminderTasks fetch all the reminder tasks 90 | func (t Tasks) GetReminderTasks() Tasks { 91 | var reminderList Tasks 92 | for _, item := range t { 93 | if item.RemindAt != "" && item.Completed == "" { 94 | reminderList = append(reminderList, item) //only uncompleted reminder 95 | } 96 | } 97 | return reminderList 98 | } 99 | 100 | //GetTask fetch a single task 101 | func (t Tasks) GetTask(id int) (Task, error) { 102 | if err := t.isValidId(id); err != nil { 103 | return Task{}, err 104 | } 105 | i, err := t.getIndexIdNo(id) 106 | if err != nil { 107 | return Task{}, err 108 | } 109 | return t[i], nil 110 | } 111 | 112 | //UpdateTask update a task by id 113 | func (t *Tasks) UpdateTask(id int, description string) (string, error) { 114 | if err := t.isValidId(id); err != nil { 115 | return fmt.Sprintf("Unable to update %s", description), err 116 | } 117 | i, err := t.getIndexIdNo(id) 118 | if err != nil { 119 | return "", err 120 | } 121 | oldDescription := (*t)[i].Description 122 | (*t)[i].Description = description 123 | (*t)[i].Updated = time.Now().Format(timeLayout) 124 | writeDBFile(*t) 125 | return fmt.Sprintf("Task Updated: %s --> %s", oldDescription, description), nil 126 | } 127 | 128 | //UpdateTaskTag update a task's tag by id 129 | func (t *Tasks) UpdateTaskTag(id int, tag string) (string, error) { 130 | if err := t.isValidId(id); err != nil { 131 | return fmt.Sprintf("Unable to update %s", tag), err 132 | } 133 | i, err := t.getIndexIdNo(id) 134 | if err != nil { 135 | return "", nil 136 | } 137 | oldTag := (*t)[i].Tag 138 | (*t)[i].Tag = tag 139 | (*t)[i].Updated = time.Now().Format(timeLayout) 140 | writeDBFile(*t) 141 | return fmt.Sprintf("Task Updated: %s --> %s", oldTag, tag), nil 142 | } 143 | 144 | //MarkAsCompleteTask mark a task as completed by id 145 | func (t *Tasks) MarkAsCompleteTask(id int) (Task, error) { 146 | if err := t.isValidId(id); err != nil { 147 | return Task{}, err 148 | } 149 | i, err := t.getIndexIdNo(id) 150 | if err != nil { 151 | return Task{}, err 152 | } 153 | (*t)[i].Completed = time.Now().Format(timeLayout) 154 | writeDBFile(*t) 155 | return (*t)[i], nil 156 | } 157 | 158 | //MarkAsPendingTask mark a task as pending by id 159 | func (t *Tasks) MarkAsPendingTask(id int) (Task, error) { 160 | if err := t.isValidId(id); err != nil { 161 | return Task{}, err 162 | } 163 | i, err := t.getIndexIdNo(id) 164 | if err != nil { 165 | return Task{}, err 166 | } 167 | (*t)[i].Completed = "" 168 | writeDBFile(*t) 169 | return (*t)[i], nil 170 | } 171 | 172 | //RemoveTask delete a task by id 173 | func (t *Tasks) RemoveTask(id int) error { 174 | if err := t.isValidId(id); err != nil { 175 | return err 176 | } 177 | i, err := t.getIndexIdNo(id) 178 | if err != nil { 179 | return err 180 | } 181 | *t = append((*t)[:i], (*t)[i+1:]...) 182 | writeDBFile(*t) 183 | return nil 184 | } 185 | 186 | //TotalTask return total task count 187 | func (t Tasks) TotalTask() int { 188 | return len(t) 189 | } 190 | 191 | //CompletedTask return total completed task count 192 | func (t Tasks) CompletedTask() int { 193 | completedTask := 0 194 | for _, i := range t { 195 | if i.Completed != "" { 196 | completedTask++ 197 | } 198 | } 199 | return completedTask 200 | } 201 | 202 | //PendingTask return total pending task count 203 | func (t Tasks) PendingTask() int { 204 | return len(t) - t.CompletedTask() 205 | } 206 | 207 | //GetLastId return last inserted id 208 | func (t Tasks) GetLastId() int { 209 | if t.TotalTask() <= 0 { 210 | return 0 211 | } 212 | maxId := t[0].Id 213 | for _, item := range t { 214 | if item.Id >= maxId { 215 | maxId = item.Id 216 | } 217 | } 218 | return maxId 219 | } 220 | 221 | //GetNextId return next id 222 | func (t Tasks) GetNextId() int { 223 | return t.GetLastId() + 1 224 | } 225 | 226 | //check if id valid 227 | func (t Tasks) isValidId(id int) error { 228 | if id < 0 || id == 0 { 229 | return errors.New("Negative id not accepted!") 230 | } 231 | index, err := t.getIndexIdNo(id) 232 | if err != nil { 233 | return err 234 | } 235 | if index > len(t) { 236 | return errors.New("Id " + strconv.Itoa(id) + " not exist!") 237 | } 238 | return nil 239 | } 240 | 241 | // get indexIdNo from id 242 | func (t Tasks) getIndexIdNo(id int) (int, error) { 243 | for i, task := range t { 244 | if task.Id == id { 245 | return i, nil 246 | } 247 | } 248 | return 0, errors.New("Invalid Id!") 249 | } 250 | 251 | //FlushDB flush task database 252 | func (t *Tasks) FlushDB() error { 253 | *t = Tasks{} 254 | removeDBFileIfExist() 255 | createDBFileIfNotExist() 256 | return nil 257 | } 258 | 259 | //implement the sort interface 260 | // Len return total length of task list 261 | func (t Tasks) Len() int { 262 | return len(t) 263 | } 264 | 265 | // Less order the task as descending order 266 | func (t Tasks) Less(i, j int) bool { 267 | return t[i].Id > t[j].Id 268 | } 269 | 270 | // Swap tasks 271 | func (t Tasks) Swap(i, j int) { 272 | t[i], t[j] = t[j], t[i] 273 | } 274 | 275 | //===========================helpers 276 | //generate a uid 277 | func uid() string { 278 | uuid := make([]byte, 16) 279 | n, err := io.ReadFull(rand.Reader, uuid) 280 | if n != len(uuid) || err != nil { 281 | return "" 282 | } 283 | // variant bits; see section 4.1.1 284 | uuid[8] = uuid[8]&^0xc0 | 0x80 285 | // version 4 (pseudo-random); see section 4.1.3 286 | uuid[6] = uuid[6]&^0xf0 | 0x40 287 | return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]) 288 | } 289 | 290 | //get file path 291 | func dbFile() string { 292 | env := os.Getenv("TASK_DB_FILE_PATH") 293 | if env != "" { 294 | if strings.HasSuffix(env, ".json") { 295 | return env 296 | } 297 | return filepath.Join(filepath.Clean(env), dbFileName) 298 | } 299 | 300 | usr, err := user.Current() 301 | if err != nil { 302 | panic(err) 303 | 304 | } 305 | return filepath.Join(usr.HomeDir, dbFileName) 306 | } 307 | 308 | //load database 309 | func readDBFile() Tasks { 310 | //load the json to task 311 | mutex.Lock() 312 | defer mutex.Unlock() 313 | file, e := ioutil.ReadFile(dbFile()) 314 | if e != nil { 315 | fmt.Printf("File error: %v\n", e) 316 | os.Exit(1) 317 | } 318 | var tasks Tasks 319 | json.Unmarshal(file, &tasks) 320 | return tasks 321 | } 322 | 323 | //write to json 324 | func writeDBFile(tasks Tasks) { 325 | mutex.Lock() 326 | defer mutex.Unlock() 327 | removeDBFileIfExist() 328 | taskJson, _ := json.Marshal(tasks) 329 | e := ioutil.WriteFile(dbFile(), taskJson, 0644) 330 | if e != nil { 331 | fmt.Printf("File error: %v\n", e) 332 | os.Exit(1) 333 | } 334 | } 335 | 336 | //create a db file if not exist 337 | func createDBFileIfNotExist() { 338 | if _, err := os.Stat(dbFile()); os.IsNotExist(err) { 339 | os.Create(dbFile()) 340 | } 341 | } 342 | 343 | //delete a db file if exist 344 | func removeDBFileIfExist() { 345 | if _, err := os.Stat(dbFile()); !os.IsNotExist(err) { 346 | os.Remove(dbFile()) 347 | } 348 | } 349 | 350 | func init() { 351 | //create .task.json file if not exist 352 | createDBFileIfNotExist() 353 | } 354 | -------------------------------------------------------------------------------- /taskmanager/taskmanager_test.go: -------------------------------------------------------------------------------- 1 | package taskmanager 2 | 3 | import ( 4 | "os/user" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | var tasksList = []struct { 10 | description string 11 | uuid string 12 | tag string 13 | remindAt string 14 | }{ 15 | { 16 | description: "Go to store", tag: "low", remindAt: "", 17 | }, 18 | { 19 | description: "Learn golang testing", tag: "high", remindAt: "", 20 | }, 21 | { 22 | description: "Watch Pirates of the carribean", tag: "medium", remindAt: "", 23 | }, 24 | } 25 | 26 | var tm Tasks 27 | 28 | func TestMain(m *testing.M) { 29 | createDBFileIfNotExist() 30 | tm = New() 31 | m.Run() 32 | removeDBFileIfExist() 33 | } 34 | 35 | func TestTasks_Add(t *testing.T) { 36 | for _, t := range tasksList { 37 | tm.Add(t.description, t.tag, t.remindAt) 38 | } 39 | if 3 != len(tasksList) { 40 | t.Error("Task count does not matched!") 41 | } 42 | } 43 | 44 | func TestTasks_GetAllTasks(t *testing.T) { 45 | tasks := tm.GetAllTasks() 46 | if len(tasks) != 3 { 47 | t.Error("Failed to get all tasks!") 48 | } 49 | } 50 | 51 | func TestTasks_GetTask(t *testing.T) { 52 | taskId := 3 53 | task, err := tm.GetTask(taskId) 54 | t.Log(task) 55 | if err != nil { 56 | t.Error("Failed to get task by id") 57 | } 58 | if task.Id != taskId { 59 | t.Error("Task id does not match in get task by id") 60 | } 61 | } 62 | 63 | func TestTasks_UpdateTask(t *testing.T) { 64 | task, err := tm.UpdateTask(1, "Go to USA") 65 | t.Log(task) 66 | if err != nil { 67 | t.Error("Unable to update task") 68 | } 69 | } 70 | 71 | func TestTasks_UpdateTaskTag(t *testing.T) { 72 | tag, err := tm.UpdateTaskTag(1, "important") 73 | t.Log(tag) 74 | if err != nil { 75 | t.Error("Unable to Update task tag") 76 | } 77 | } 78 | 79 | func TestTasks_MarkAsCompleteTask(t *testing.T) { 80 | taskId := 2 81 | task, err := tm.MarkAsCompleteTask(taskId) 82 | t.Log(task) 83 | if err != nil { 84 | t.Error("Unable to mark task as complete") 85 | } 86 | if task.Id != taskId { 87 | t.Error("Task id does not match in mark as complete") 88 | } 89 | } 90 | 91 | func TestTasks_GetCompletedTasks(t *testing.T) { 92 | tasks := tm.GetCompletedTasks() 93 | t.Log(tasks) 94 | if len(tasks) != 1 { 95 | t.Error("Failed to match number of completed tasks") 96 | } 97 | if tasks[0].Id != 2 { 98 | t.Error("Failed to match the completed task id") 99 | } 100 | for _, _task := range tasks { 101 | if len(_task.Completed) == 0 { 102 | t.Error("Failed match completed tasks status") 103 | } 104 | } 105 | } 106 | 107 | func TestTasks_GetReminderTasks(t *testing.T) { 108 | reminders := tm.GetReminderTasks() 109 | for _, r := range reminders { 110 | if len(r.RemindAt) <= 0 { 111 | t.Error("Failed to get reminder tasks!") 112 | } 113 | } 114 | } 115 | 116 | func TestTasks_MarkAsPendingTask(t *testing.T) { 117 | taskId := 1 118 | task, err := tm.MarkAsPendingTask(taskId) 119 | t.Log(task) 120 | if err != nil { 121 | t.Error("Unable to mark task as pending") 122 | } 123 | if task.Id != taskId { 124 | t.Error("Task id does not match in mark as pending") 125 | } 126 | } 127 | 128 | func TestTasks_GetPendingTasks(t *testing.T) { 129 | tasks := tm.GetPendingTasks() 130 | if len(tasks) != 2 { 131 | t.Error("Failed to match the pending tasks total count!") 132 | } 133 | 134 | } 135 | 136 | func TestTasks_GetLastId(t *testing.T) { 137 | if tm.GetLastId() != 3 { 138 | t.Error("Last inserted id does not correct!") 139 | } 140 | } 141 | 142 | func TestTasks_GetNextId(t *testing.T) { 143 | if tm.GetNextId() != 4 { 144 | t.Error("Next id does not correct!") 145 | } 146 | } 147 | 148 | func TestTasks_TotalTask(t *testing.T) { 149 | if tm.TotalTask() != 3 { 150 | t.Error("Failed to count total task!") 151 | } 152 | } 153 | 154 | func TestTasks_Len(t *testing.T) { 155 | if tm.Len() != 3 { 156 | t.Error("Failed to count let of tasks") 157 | } 158 | } 159 | 160 | func TestTasks_RemoveTask(t *testing.T) { 161 | err := tm.RemoveTask(3) 162 | if err != nil { 163 | t.Error("Failed to remove task") 164 | } 165 | if tm.TotalTask() != 2 { 166 | t.Error("Task did not remove properly!") 167 | } 168 | } 169 | 170 | func TestTasks_FlushDB(t *testing.T) { 171 | err := tm.FlushDB() 172 | if err != nil { 173 | t.Error("Failed to flush database!") 174 | } 175 | } 176 | 177 | func TestTasks_dbFile(t *testing.T) { 178 | usr, _ := user.Current() 179 | if dbFile() != filepath.Join(filepath.Clean(usr.HomeDir), ".task.json") { 180 | t.Error("Task file path incorrect!") 181 | } 182 | } 183 | 184 | func BenchmarkTasks_Add(b *testing.B) { 185 | for n := 0; n < b.N; n++ { 186 | for _, task := range tasksList { 187 | tm.Add(task.description, task.tag, task.remindAt) 188 | } 189 | } 190 | } 191 | 192 | func BenchmarkTasks_GetAllTasks(b *testing.B) { 193 | for n := 0; n < b.N; n++ { 194 | tm.GetAllTasks() 195 | } 196 | } 197 | 198 | func BenchmarkTasks_GetReminderTasks(b *testing.B) { 199 | for n := 0; n < b.N; n++ { 200 | tm.GetReminderTasks() 201 | } 202 | } 203 | 204 | func BenchmarkTasks_GetCompletedTasks(b *testing.B) { 205 | for n := 0; n < b.N; n++ { 206 | tm.GetCompletedTasks() 207 | } 208 | } 209 | 210 | func BenchmarkTasks_GetPendingTasks(b *testing.B) { 211 | for n := 0; n < b.N; n++ { 212 | tm.GetPendingTasks() 213 | } 214 | } 215 | 216 | func BenchmarkTasks_GetTask(b *testing.B) { 217 | for n := 0; n < b.N; n++ { 218 | tm.GetTask(2) 219 | } 220 | } 221 | 222 | func BenchmarkTasks_MarkAsCompleteTask(b *testing.B) { 223 | for n := 0; n < b.N; n++ { 224 | tm.MarkAsCompleteTask(1) 225 | } 226 | } 227 | 228 | func BenchmarkTasks_MarkAsPendingTask(b *testing.B) { 229 | for n := 0; n < b.N; n++ { 230 | tm.MarkAsPendingTask(3) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "path": "get", 7 | "revision": "" 8 | }, 9 | { 10 | "checksumSHA1": "aFYuxZ9WmM4Ywp3wCIVox9g3bew=", 11 | "path": "github.com/0xAX/notificator", 12 | "revision": "6bcea42e61381c13704c7fb6eb2438950f335832", 13 | "revisionTime": "2017-05-28T18:13:08Z" 14 | }, 15 | { 16 | "checksumSHA1": "ErMS+JDb3UuJQiCNovvknd/xeJ4=", 17 | "path": "github.com/AlekSi/pointer", 18 | "revision": "08a25bac605b3fcb6cc27f3917b2c2c87451963d", 19 | "revisionTime": "2017-01-05T16:04:40Z" 20 | }, 21 | { 22 | "checksumSHA1": "NrWs7vLYZAcDq117/eb2K/3H7Yc=", 23 | "path": "github.com/ProtonMail/go-autostart", 24 | "revision": "a62cac4ac4691cf6e94acff3b86d958ac5630448", 25 | "revisionTime": "2017-01-30T18:36:13Z" 26 | }, 27 | { 28 | "checksumSHA1": "AANTVr9CVVyzsgviODY6Wi2thuM=", 29 | "path": "github.com/fatih/color", 30 | "revision": "62e9147c64a1ed519147b62a56a14e83e2be02c1", 31 | "revisionTime": "2017-05-23T20:24:04Z" 32 | }, 33 | { 34 | "checksumSHA1": "K6exl2ouL7d8cR2i378EzZOdRVI=", 35 | "path": "github.com/howeyc/gopass", 36 | "revision": "bf9dde6d0d2c004a008c27aaee91170c786f6db8", 37 | "revisionTime": "2017-01-09T16:22:49Z" 38 | }, 39 | { 40 | "checksumSHA1": "bfGiF5iraNvFHCCK3KVvITsPCok=", 41 | "path": "github.com/mattn/go-colorable", 42 | "revision": "3fa8c76f9daed4067e4a806fb7e4dc86455c6d6a", 43 | "revisionTime": "2017-07-11T10:06:59Z" 44 | }, 45 | { 46 | "checksumSHA1": "U6lX43KDDlNOn+Z0Yyww+ZzHfFo=", 47 | "path": "github.com/mattn/go-isatty", 48 | "revision": "fc9e8d8ef48496124e79ae0df75490096eccf6fe", 49 | "revisionTime": "2017-03-22T23:44:13Z" 50 | }, 51 | { 52 | "checksumSHA1": "cJE7dphDlam/i7PhnsyosNWtbd4=", 53 | "path": "github.com/mattn/go-runewidth", 54 | "revision": "97311d9f7767e3d6f422ea06661bc2c7a19e8a5d", 55 | "revisionTime": "2017-05-10T07:48:58Z" 56 | }, 57 | { 58 | "checksumSHA1": "1cD/Rsg7Fuxk8AtSlkFbVXy6Z74=", 59 | "path": "github.com/olebedev/when", 60 | "revision": "175108fec17fb14cc3d4f0ec49e55fee9eaeadd8", 61 | "revisionTime": "2017-05-19T06:39:12Z" 62 | }, 63 | { 64 | "checksumSHA1": "sBD0ICrArfbjm1XfbwqhRRBBKRg=", 65 | "path": "github.com/olebedev/when/rules", 66 | "revision": "175108fec17fb14cc3d4f0ec49e55fee9eaeadd8", 67 | "revisionTime": "2017-05-19T06:39:12Z" 68 | }, 69 | { 70 | "checksumSHA1": "9M6aBZjAmfvpTd0sdi6NhxW8TOA=", 71 | "path": "github.com/olebedev/when/rules/common", 72 | "revision": "175108fec17fb14cc3d4f0ec49e55fee9eaeadd8", 73 | "revisionTime": "2017-05-19T06:39:12Z" 74 | }, 75 | { 76 | "checksumSHA1": "4jB+2fpFPYoNcim/AxCUeO8OaOE=", 77 | "path": "github.com/olebedev/when/rules/en", 78 | "revision": "175108fec17fb14cc3d4f0ec49e55fee9eaeadd8", 79 | "revisionTime": "2017-05-19T06:39:12Z" 80 | }, 81 | { 82 | "checksumSHA1": "xr9Zja3ua4NblzIUgWu2wqFuNL8=", 83 | "path": "github.com/olebedev/when/rules/ru", 84 | "revision": "175108fec17fb14cc3d4f0ec49e55fee9eaeadd8", 85 | "revisionTime": "2017-05-19T06:39:12Z" 86 | }, 87 | { 88 | "checksumSHA1": "3DXd47ue3PGGyFBGXqXSOI/iIeI=", 89 | "path": "github.com/olekukonko/tablewriter", 90 | "revision": "be5337e7b39e64e5f91445ce7e721888dbab7387", 91 | "revisionTime": "2017-07-19T10:10:40Z" 92 | }, 93 | { 94 | "checksumSHA1": "rJab1YdNhQooDiBWNnt7TLWPyBU=", 95 | "path": "github.com/pkg/errors", 96 | "revision": "c605e284fe17294bda444b34710735b29d1a9d90", 97 | "revisionTime": "2017-05-05T04:36:39Z" 98 | }, 99 | { 100 | "checksumSHA1": "llmzhtIUy63V3Pl65RuEn18ck5g=", 101 | "path": "github.com/segmentio/go-prompt", 102 | "revision": "f0d19b6901ade831d5a3204edc0d6a7d6457fbb2", 103 | "revisionTime": "2016-10-17T23:32:05Z" 104 | }, 105 | { 106 | "path": "go", 107 | "revision": "" 108 | }, 109 | { 110 | "checksumSHA1": "ZaU56svwLgiJD0y8JOB3+/mpYBA=", 111 | "path": "golang.org/x/crypto/ssh/terminal", 112 | "revision": "6914964337150723782436d56b3f21610a74ce7b", 113 | "revisionTime": "2017-07-20T17:50:53Z" 114 | }, 115 | { 116 | "checksumSHA1": "clYzF13hcnUKF7X/o5TgvjrCLdM=", 117 | "path": "golang.org/x/sys/unix", 118 | "revision": "7a4fde3fda8ef580a89dbae8138c26041be14299", 119 | "revisionTime": "2017-06-29T20:26:00Z" 120 | } 121 | ], 122 | "rootPath": "github.com/thedevsaddam/task" 123 | } 124 | --------------------------------------------------------------------------------