diff --git a/.gitignore b/.gitignore index 05e0274..839d465 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,6 @@ # Dependency directories (remove the comment below to include it) # vendor/ +builds/ /geek-life /geek-life.db diff --git a/README.md b/README.md index b196374..8ffc48f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,135 @@ -# geek-task -Todo List Manager for Geeks +geek-life - The CLI Task Manager for Geeks :technologist: +========= + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Go Report Card](https://goreportcard.com/badge/github.com/ajaxray/geek-life)](https://goreportcard.com/report/github.com/ajaxray/geek-life) + + +:superhero: Command Line hero? +:computer: Live with the dark terminal? +:memo: Think in Markdown? + +**Finally!** A full featured task manager for YOU! + +![Geek-life overview](screens/geek-life_v1.gif "Geek-life overview") + +### Highlights + +- For ninjas - do things faster with keyboard shortcuts +- Markdown lovers, feel at :house:! You'll see markdown everywhere. +- Full featured (almost) - Projects, Tasks, due-dates, task notes... +- A <4MB app that takes <1% CPU and ~7MB memory 1 - how much lighter you can think? +- Task note editor with markdown syntax highlighting2 +- Full mouse support + +### Roadmap +- [x] Create Project +- [x] Delete Project +- [ ] Edit Project +- [x] Create Task (under project) +- [x] Set Task due date (as `dd-mm-yyyy`) with shortcut +- [x] Set Task due date with quick input buttons (today, +1 day, -1 day) +- [x] Tasklist items should indicate status (done, pending, overdue) using colors +- [x] Shortcut for Adding new Project and Task +- [x] Global shortcuts for jumping to Projects or Tasks panel anytime +- [x] Cleanup all completed tasks of project +- [x] Task note editor should syntax highlight (markdown) and line numbers +- [x] Status bar for common shortcuts +- [x] Status bar should display success/error message of actions +- [x] Status bar may display quick tips based on focused element +- [ ] Dynamic lists + - Today - Due Today and overdue + - Upcoming - Due in one week + - Someday - No due date +- [ ] [Havitica](https://habitica.com/)3 integration - Use it as Habitica client or use Habitica for cloud backup +- [ ] Time tracking + +### Ready for action (installing and running) + +It's just a single binary file, **no external dependencies**. +Just download the appropriate version of [executable from latest release](https://github.com/ajaxray/geek-life/releases) for your OS. +Then rename and give it permission to execute. For example +```bash +mv geek-life_linux-amd64 geek-life +sudo chmod +x geek-life +``` + +If you want to install it globally (run from any directory of your system), put it in your systems $PATH directory. +```bash +sudo mv geek-life /usr/local/bin/geek-life +``` +Done! + +## Keyboard shortcuts + +Some shortcuts are global, some are contextual. +Contextual shortcuts will be applied according to focused pane/element. +You'll see a currently focused pane bordered with double line. + +In case writing in a text input (e,g, new project/task, due date), you have to `Enter` to submit/save. + +| Context | Shortcut | Action | +|---|:---:|---| +| Global | `p` | Go to Project list | +| Global | `t` | Go to Task list | +| Projects | `n` | New Project | +| Tasks | `n` | New Task | +| Tasks | `Esc` | Go back to Projects Pane | +| Task Detail | `Esc` | Go back to Tasks Pane | +| Task Detail | `Space` | Toggle task as done/pending | +| Task Detail | `d` | Set Due date | +| Task Detail | `↓`/`↑` | Scroll Up/Down the note editor | +| Task Detail | `e` | Activate note editor for modification | +| Active Note Editor | `Esc` | Deactivate note editor and save content | + +*Tips about using shortcuts efficiently:* + +- `Esc` will bring you a step back - to previous pane in most cases. +- When you're in Project or Task list, use `↓`/`↑` to navigate the list. +- When you're in Project or Task list `Enter` will load currently selected Project/Task. +- After creating new Project, focus will automatically move to Tasks. Start adding tasks immediately by pressing `n`. +- After creating new Task, focus will stay in "new task" input. So that you can add tasks quickly one after another. +- After creating new Task, Press `Esc` when you're done creating tasks. + +## Building blocks + +- Made with :love: and [golang](https://golang.org/) 1.14 *(you don't need golang to run it)* +- Designed with [tview](https://github.com/rivo/tview) - interactive widgets for terminal-based UI +- Task Note editor made with [femto](https://github.com/pgavlin/femto) +- Datastore is [storm](https://github.com/asdine/storm) - a powerful toolkit for [BoltDB](https://github.com/etcd-io/bbolt) + +### Contribute + +If you fix a bug or want to add/improve a feature, +and it's alligned with the focus (merging with ease) of this tool, +I will be glad to accept your PR. :) + +## You may ask... + +#### Where is the data stored? Can I change the location? + +By default, it will try to create a db file in you home directory. + +But as a geek, you may try to put it different location (e,g, in you dropbox for syncing). +In that case, just mention `DB_FILE` as an environment variable. + +```bash +DB_FILE=~/dropbox/geek-life/default.db geek-life +``` + +#### How can I suggest a feature? + +Just [post an issue](https://github.com/ajaxray/geek-life/issues/new) describing your desired feature/enhancement +and select `feature` label. + +Also, incomplete features in the current roadmap will be found in issue list. +You may :thumbsup: issues if you want to increase priority of a feature. + +--- +### Footnotes +1. In my Macbook Air, 1.6 GHz Dual-Core Intel Core i5, RAM: 8 GB 1600 MHz DDR3 +2. Use [monakai](https://github.com/sickill/vim-monokai) color scheme for markdown syntax +3. Habitica is a free habit and productivity app that treats your real life like a game + +--- +> "This is the Book about which there is no doubt, a guidance for those conscious of Allah" - [Al-Quran](http://quran.com) diff --git a/app/cli.go b/app/cli.go index d73b71c..d8d9418 100644 --- a/app/cli.go +++ b/app/cli.go @@ -14,21 +14,22 @@ import ( ) var ( - app *tview.Application - newProject, newTask *tview.InputField - projectList, taskList *tview.List - projectPane, taskPane, detailPane *tview.Flex - layout, contents *tview.Flex - statusBar *tview.Pages - message *tview.TextView - shortcutsPage, messagePage string = "shortcuts", "message" + app *tview.Application + newProject, newTask *tview.InputField + projectList, taskList *tview.List + projectPane, projectDetailPane *tview.Flex + taskPane, taskDetailPane *tview.Flex + layout, contents *tview.Flex + statusBar *tview.Pages + message *tview.TextView + shortcutsPage, messagePage string = "shortcuts", "message" db *storm.DB projectRepo repository.ProjectRepository taskRepo repository.TaskRepository projects []model.Project - currentProject model.Project + currentProject *model.Project ) func main() { @@ -40,7 +41,7 @@ func main() { projectRepo = repo.NewProjectRepository(db) taskRepo = repo.NewTaskRepository(db) - titleText := tview.NewTextView().SetText("[lime::b]Geek-life [::-]- life management for geeks!").SetDynamicColors(true) + titleText := tview.NewTextView().SetText("[lime::b]Geek-life [::-]- Task Manager for geeks!").SetDynamicColors(true) cloudStatus := tview.NewTextView().SetText("[::d]Cloud Sync: off").SetTextAlign(tview.AlignRight).SetDynamicColors(true) titleBar := tview.NewFlex(). @@ -48,6 +49,7 @@ func main() { AddItem(cloudStatus, 0, 1, false) prepareProjectPane() + prepareProjectDetail() prepareTaskPane() prepareStatusBar() prepareDetailPane() @@ -55,21 +57,20 @@ func main() { contents = tview.NewFlex(). AddItem(projectPane, 25, 1, true). AddItem(taskPane, 0, 2, false) - //AddItem(detailPane, 0, 3, true) layout = tview.NewFlex().SetDirection(tview.FlexRow). AddItem(titleBar, 2, 1, false). AddItem(contents, 0, 2, true). AddItem(statusBar, 1, 1, false) - setKeyboardShortcuts(projectPane, taskPane) + setKeyboardShortcuts() if err := app.SetRoot(layout, true).EnableMouse(true).Run(); err != nil { panic(err) } } -func setKeyboardShortcuts(projectPane *tview.Flex, taskPane *tview.Flex) *tview.Application { +func setKeyboardShortcuts() *tview.Application { return app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if ignoreKeyEvt() { return event @@ -81,7 +82,7 @@ func setKeyboardShortcuts(projectPane *tview.Flex, taskPane *tview.Flex) *tview. event = handleProjectPaneShortcuts(event) case taskPane.HasFocus(): event = handleTaskPaneShortcuts(event) - case detailPane.HasFocus(): + case taskDetailPane.HasFocus(): event = handleDetailPaneShortcuts(event) } @@ -110,9 +111,9 @@ func prepareStatusBar() { tview.NewGrid(). SetColumns(0, 0, 0, 0). SetRows(0). - AddItem(tview.NewTextView().SetText("Shortcuts: Alt+.(dot)"), 0, 0, 1, 1, 0, 0, false). - AddItem(tview.NewTextView().SetText("New Project: n").SetTextAlign(tview.AlignCenter), 0, 1, 1, 1, 0, 0, false). - AddItem(tview.NewTextView().SetText("New Task: t").SetTextAlign(tview.AlignCenter), 0, 2, 1, 1, 0, 0, false). + AddItem(tview.NewTextView().SetText("Navigate List: ↓/↑"), 0, 0, 1, 1, 0, 0, false). + AddItem(tview.NewTextView().SetText("New Task/Project: n").SetTextAlign(tview.AlignCenter), 0, 1, 1, 1, 0, 0, false). + AddItem(tview.NewTextView().SetText("Step back: Esc").SetTextAlign(tview.AlignCenter), 0, 2, 1, 1, 0, 0, false). AddItem(tview.NewTextView().SetText("Quit: Ctrl+C").SetTextAlign(tview.AlignRight), 0, 3, 1, 1, 0, 0, false), true, true, diff --git a/app/project_detail.go b/app/project_detail.go new file mode 100644 index 0000000..4aa50f1 --- /dev/null +++ b/app/project_detail.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + + "github.com/gdamore/tcell" + "github.com/rivo/tview" +) + +func prepareProjectDetail() { + deleteBtn := makeButton("Delete Project", deleteCurrentProject) + clearBtn := makeButton("Clear Completed Tasks", clearCompletedTasks) + + deleteBtn.SetBackgroundColor(tcell.ColorRed) + projectDetailPane = tview.NewFlex().SetDirection(tview.FlexRow). + // AddItem(activeProjectName, 1, 1, false). + // AddItem(makeHorizontalLine(tcell.RuneS3, tcell.ColorGray), 1, 1, false). + AddItem(deleteBtn, 3, 1, false). + AddItem(blankCell, 1, 1, false). + AddItem(clearBtn, 3, 1, false). + AddItem(blankCell, 0, 1, false) + + projectDetailPane.SetBorder(true).SetTitle("[::u]A[::-]ctions") +} + +func deleteCurrentProject() { + if currentProject != nil && projectRepo.Delete(currentProject) == nil { + for i, _ := range tasks { + taskRepo.Delete(&tasks[i]) + } + + showMessage("Removed Project: " + currentProject.Title) + removeThirdCol() + taskList.Clear() + projectList.Clear() + + loadProjectList() + } +} + +func clearCompletedTasks() { + count := 0 + for i, task := range tasks { + if task.Completed && taskRepo.Delete(&tasks[i]) == nil { + taskList.RemoveItem(i) + count++ + } + } + showMessage(fmt.Sprintf("[yellow]%d tasks cleared!", count)) +} diff --git a/app/projects.go b/app/projects.go index 1348fb6..6b328f1 100644 --- a/app/projects.go +++ b/app/projects.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strings" "github.com/asdine/storm/v3" "github.com/gdamore/tcell" @@ -9,17 +10,8 @@ import ( ) func prepareProjectPane() { - var err error - projects, err = projectRepo.GetAll() - if err != nil { - showMessage("Could not load Projects: " + err.Error()) - } - projectList = tview.NewList().ShowSecondaryText(false) - - for i := range projects { - addProjectToList(i, false) - } + loadProjectList() newProject = makeLightTextInput("+[New Project]"). SetDoneFunc(func(key tcell.Key) { @@ -29,7 +21,7 @@ func prepareProjectPane() { if err != nil { showMessage("[red::]Failed to create Project:" + err.Error()) } else { - showMessage(fmt.Sprintf("[green::]Project %s created. Press n to start adding new tasks.", newProject.GetText())) + showMessage(fmt.Sprintf("[yellow::]Project %s created. Press n to start adding new tasks.", newProject.GetText())) projects = append(projects, project) addProjectToList(len(projects)-1, true) newProject.SetText("") @@ -46,6 +38,30 @@ func prepareProjectPane() { projectPane.SetBorder(true).SetTitle("[::u]P[::-]rojects") } +func loadProjectList() { + var err error + projects, err = projectRepo.GetAll() + if err != nil { + showMessage("Could not load Projects: " + err.Error()) + return + } + + projectList.AddItem("[::d]Dynamic Lists", "", 0, nil) + projectList.AddItem("[::d]"+strings.Repeat(string(tcell.RuneS3), 25), "", 0, nil) + projectList.AddItem("- Today", "", 0, yetToImplement("Today's Tasks")) + projectList.AddItem("- Upcoming", "", 0, yetToImplement("Upcoming Tasks")) + projectList.AddItem("- No Due Date", "", 0, yetToImplement("Unscheduled Tasks")) + projectList.AddItem("", "", 0, nil) + projectList.AddItem("[::d]Projects", "", 0, nil) + projectList.AddItem("[::d]"+strings.Repeat(string(tcell.RuneS3), 25), "", 0, nil) + + for i := range projects { + addProjectToList(i, false) + } + + projectList.SetCurrentItem(6) // Select Projects, as dynamic lists are not ready +} + func addProjectToList(i int, selectItem bool) { // To avoid overriding of loop variables - https://www.calhoun.io/gotchas-and-common-mistakes-with-closures-in-go/ projectList.AddItem("- "+projects[i].Title, "", 0, func(idx int) func() { @@ -53,28 +69,28 @@ func addProjectToList(i int, selectItem bool) { }(i)) if selectItem { - projectList.SetCurrentItem(i) + projectList.SetCurrentItem(projectList.GetItemCount() - 1) loadProject(i) } } func loadProject(idx int) { - currentProject = projects[idx] + currentProject = &projects[idx] taskList.Clear() app.SetFocus(taskPane) var err error - if tasks, err = taskRepo.GetAllByProject(currentProject); err != nil && err != storm.ErrNotFound { + if tasks, err = taskRepo.GetAllByProject(*currentProject); err != nil && err != storm.ErrNotFound { showMessage("[red::]Error: " + err.Error()) } for i, task := range tasks { - taskList.AddItem(makeTaskListingTitle(task), "", 0, func(taskidx int) func() { - return func() { loadTask(taskidx) } - }(i)) + addTaskToList(task, i) } - contents.RemoveItem(detailPane) + removeThirdCol() + projectDetailPane.SetTitle("[::b]" + currentProject.Title) + contents.AddItem(projectDetailPane, 25, 0, false) } func handleProjectPaneShortcuts(event *tcell.EventKey) *tcell.EventKey { diff --git a/app/task_detail.go b/app/task_detail.go index 08cceb7..f7656db 100644 --- a/app/task_detail.go +++ b/app/task_detail.go @@ -12,10 +12,12 @@ import ( var ( taskName, taskDateDisplay *tview.TextView + editorHint *tview.TextView taskDate *tview.InputField taskDetailView *femto.View taskStatusToggle *tview.Button colorscheme femto.Colorscheme + blankCell = tview.NewTextView() ) const dateLayoutISO = "2006-01-02" @@ -23,51 +25,40 @@ const dateLayoutHuman = "02 Jan, Monday" func prepareDetailPane() { taskName = tview.NewTextView().SetDynamicColors(true) - hr := makeHorizontalLine(tview.BoxDrawingsLightHorizontal) prepareDetailsEditor() - taskStatusToggle = makeButton("Complete", func() {}).SetLabelColor(tcell.ColorLightGray) + taskStatusToggle = makeButton("Complete", toggleActiveTaskStatus).SetLabelColor(tcell.ColorLightGray) - hint := tview.NewTextView().SetTextColor(tcell.ColorYellow). - SetText("press Enter to save changes, Esc to ignore") + toggleHint := tview.NewTextView().SetTextColor(tcell.ColorDimGray).SetText(" to toggle") - detailPane = tview.NewFlex().SetDirection(tview.FlexRow). + editorLabel := tview.NewFlex(). + AddItem(tview.NewTextView().SetText("Task Not[::u]e[::-]:").SetDynamicColors(true), 0, 1, false). + AddItem(makeButton("edit", func() { activateEditor() }), 6, 0, false) + + editorHint = tview.NewTextView(). + SetText(" e to edit, ↓↑ to scroll"). + SetTextColor(tcell.ColorDimGray) + editorHelp := tview.NewFlex(). + AddItem(editorHint, 0, 1, false). + AddItem(tview.NewTextView().SetTextAlign(tview.AlignRight). + SetText("syntax:markdown theme:monakai"). + SetTextColor(tcell.ColorDimGray), 0, 1, false) + + taskDetailPane = tview.NewFlex().SetDirection(tview.FlexRow). AddItem(taskName, 2, 1, true). - AddItem(hr, 1, 1, false). - AddItem(nil, 1, 1, false). + AddItem(makeHorizontalLine(tcell.RuneS3, tcell.ColorGray), 1, 1, false). + AddItem(blankCell, 1, 1, false). AddItem(makeDateRow(), 1, 1, true). + AddItem(blankCell, 1, 1, false). + AddItem(editorLabel, 1, 1, false). AddItem(taskDetailView, 15, 4, false). - AddItem(tview.NewTextView(), 1, 1, false). - AddItem(hint, 1, 1, false). - AddItem(nil, 0, 1, false). + AddItem(editorHelp, 1, 1, false). + AddItem(blankCell, 0, 1, false). + AddItem(toggleHint, 1, 1, false). AddItem(taskStatusToggle, 3, 1, false) - detailPane.SetBorder(true).SetTitle("Detail") - - // taskName is the default focus attracting child of detailPane - taskName.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - - switch event.Key() { - case tcell.KeyEsc: - app.SetFocus(taskPane) - case tcell.KeyDown: - taskDetailView.ScrollDown(1) - case tcell.KeyUp: - taskDetailView.ScrollUp(1) - case tcell.KeyRune: - // switch event.Rune() { - // case 'n': - // app.SetFocus(projectPane) - // case 'e': - // if detailPane.HasFocus() { - // activateEditor() - // } - // } - } - - return event - }) + taskDetailPane.SetBorder(true).SetTitle("Task Detail") } func makeDateRow() *tview.Flex { @@ -83,6 +74,7 @@ func makeDateRow() *tview.Flex { case tcell.KeyEsc: setTaskDate(currentTask.DueDate, false) } + app.SetFocus(taskDetailPane) }) todaySelector := func() { @@ -100,31 +92,28 @@ func makeDateRow() *tview.Flex { return tview.NewFlex(). AddItem(taskDateDisplay, 0, 2, true). AddItem(taskDate, 14, 0, true). - AddItem(nil, 1, 0, false). - AddItem(nil, 1, 0, false). + AddItem(blankCell, 1, 0, false). AddItem(makeButton("today", todaySelector), 8, 1, false). - AddItem(nil, 1, 0, false). + AddItem(blankCell, 1, 0, false). AddItem(makeButton("+1", nextDaySelector), 4, 1, false). - AddItem(nil, 1, 0, false). + AddItem(blankCell, 1, 0, false). AddItem(makeButton("-1", prevDaySelector), 4, 1, false) } -func setStatusToggle(idx int) { - action := func(i int, label string, color tcell.Color, status bool) { - taskStatusToggle.SetLabel(label).SetBackgroundColor(color) - taskStatusToggle.SetSelectedFunc(func() { - if taskRepo.UpdateField(currentTask, "Completed", status) == nil { - currentTask.Completed = status - loadTask(i) - taskList.SetItemText(i, makeTaskListingTitle(*currentTask), "") - } - }) - } - +func setStatusToggle() { if currentTask.Completed { - action(idx, "Resume", tcell.ColorMaroon, false) + taskStatusToggle.SetLabel("Resume").SetBackgroundColor(tcell.ColorMaroon) } else { - action(idx, "Complete", tcell.ColorDarkGreen, true) + taskStatusToggle.SetLabel("Complete").SetBackgroundColor(tcell.ColorDarkGreen) + } +} + +func toggleActiveTaskStatus() { + status := !currentTask.Completed + if taskRepo.UpdateField(currentTask, "Completed", status) == nil { + currentTask.Completed = status + loadTask(currentTaskIdx) + taskList.SetItemText(currentTaskIdx, makeTaskListingTitle(*currentTask), "") } } @@ -202,21 +191,34 @@ func makeBufferFromString(content string) *femto.Buffer { func activateEditor() { taskDetailView.Readonly = false taskDetailView.SetBorderColor(tcell.ColorDarkOrange) + editorHint.SetText(" Esc to save changes") app.SetFocus(taskDetailView) } func deactivateEditor() { taskDetailView.Readonly = true taskDetailView.SetBorderColor(tcell.ColorLightSlateGray) - app.SetFocus(detailPane) + editorHint.SetText(" e to edit, ↓↑ to scroll") + app.SetFocus(taskDetailPane) } func handleDetailPaneShortcuts(event *tcell.EventKey) *tcell.EventKey { - switch event.Rune() { - case 'e': - activateEditor() - case 'd': - app.SetFocus(taskDate) + switch event.Key() { + case tcell.KeyEsc: + app.SetFocus(taskPane) + case tcell.KeyDown: + taskDetailView.ScrollDown(1) + case tcell.KeyUp: + taskDetailView.ScrollUp(1) + case tcell.KeyRune: + switch event.Rune() { + case 'e': + activateEditor() + case 'd': + app.SetFocus(taskDate) + case ' ': + toggleActiveTaskStatus() + } } return event diff --git a/app/tasks.go b/app/tasks.go index 5016291..5c551d5 100644 --- a/app/tasks.go +++ b/app/tasks.go @@ -11,8 +11,9 @@ import ( ) var ( - tasks []model.Task - currentTask *model.Task + tasks []model.Task + currentTask *model.Task + currentTaskIdx int ) func prepareTaskPane() { @@ -25,12 +26,13 @@ func prepareTaskPane() { SetDoneFunc(func(key tcell.Key) { switch key { case tcell.KeyEnter: - task, err := taskRepo.Create(currentProject, newTask.GetText(), "", "", time.Now().Unix()) + task, err := taskRepo.Create(*currentProject, newTask.GetText(), "", "", 0) if err != nil { showMessage("[red::]Could not create Task:" + err.Error()) } - taskList.AddItem(task.Title, "", 0, nil) + tasks = append(tasks, task) + addTaskToList(task, len(tasks)-1) newTask.SetText("") case tcell.KeyEsc: app.SetFocus(taskPane) @@ -45,26 +47,38 @@ func prepareTaskPane() { taskPane.SetBorder(true).SetTitle("[::u]T[::-]asks") } +func addTaskToList(task model.Task, i int) *tview.List { + return taskList.AddItem(makeTaskListingTitle(task), "", 0, func(taskidx int) func() { + return func() { loadTask(taskidx) } + }(i)) +} + func loadTask(idx int) { - contents.RemoveItem(detailPane) - currentTask = &tasks[idx] + removeThirdCol() + currentTaskIdx = idx + currentTask = &tasks[currentTaskIdx] taskName.SetText(fmt.Sprintf("[%s::b]# %s", getTaskTitleColor(*currentTask), currentTask.Title)) taskDetailView.Buf = makeBufferFromString(currentTask.Details) taskDetailView.SetColorscheme(colorscheme) taskDetailView.Start() setTaskDate(currentTask.DueDate, false) + setStatusToggle() - contents.AddItem(detailPane, 0, 3, false) - setStatusToggle(idx) + contents.AddItem(taskDetailPane, 0, 3, false) deactivateEditor() } +func removeThirdCol() { + contents.RemoveItem(taskDetailPane) + contents.RemoveItem(projectDetailPane) +} + func getTaskTitleColor(task model.Task) string { - colorName := "whitesmoke" + colorName := "olive" if task.Completed { colorName = "lime" - } else if task.DueDate != 0 && task.DueDate < time.Now().Unix() { + } else if task.DueDate != 0 && task.DueDate < time.Now().Truncate(24*time.Hour).Unix() { colorName = "red" } diff --git a/app/util.go b/app/util.go index c8c8e45..bac57f9 100644 --- a/app/util.go +++ b/app/util.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "reflect" "time" @@ -10,11 +11,11 @@ import ( "github.com/ajaxray/geek-life/util" ) -func makeHorizontalLine(lineChar rune) *tview.TextView { +func makeHorizontalLine(lineChar rune, color tcell.Color) *tview.TextView { hr := tview.NewTextView() hr.SetDrawFunc(func(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) { // Draw a horizontal line across the middle of the box. - style := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorBlack) + style := tcell.StyleDefault.Foreground(color).Background(tcell.ColorBlack) centerY := y + height/2 for cx := x; cx < x+width; cx++ { screen.SetContent(cx, centerY, lineChar, nil, style) @@ -30,7 +31,7 @@ func makeHorizontalLine(lineChar rune) *tview.TextView { func makeLightTextInput(placeholder string) *tview.InputField { return tview.NewInputField(). SetPlaceholder(placeholder). - SetPlaceholderTextColor(tcell.ColorLightSlateGray). + SetPlaceholderTextColor(tcell.ColorYellow). SetFieldTextColor(tcell.ColorBlack). SetFieldBackgroundColor(tcell.ColorGray) } @@ -49,13 +50,18 @@ func showMessage(text string) { statusBar.SwitchToPage(messagePage) go func() { + time.Sleep(time.Second * 5) app.QueueUpdateDraw(func() { - time.Sleep(time.Second * 5) statusBar.SwitchToPage(shortcutsPage) }) }() } +func yetToImplement(feature string) func() { + message := fmt.Sprintf("[yellow]%s is yet to implement. Please Check in next version.", feature) + return func() { showMessage(message) } +} + func makeButton(label string, handler func()) *tview.Button { btn := tview.NewButton(label).SetSelectedFunc(handler). SetLabelColor(tcell.ColorWhite) diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..726bec4 --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +env GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o builds/geek-life_darwin-amd64 ./app +env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o builds/geek-life_linux-amd64 ./app +env GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o builds/geek-life_linux-arm64 ./app +env GOOS=windows GOARCH=386 go build -ldflags="-s -w" -o builds/geek-life_windows-386.exe ./app +upx builds/geek-life_* \ No newline at end of file diff --git a/builds/.gitkeep b/builds/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod index ad23b4b..9ec9c81 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/asdine/storm/v3 v3.2.0 github.com/gdamore/tcell v1.3.0 github.com/golang/protobuf v1.3.3 // indirect + github.com/mitchellh/go-homedir v1.1.0 github.com/pgavlin/femto v0.0.0-20191028012355-31a9964a50b5 github.com/rivo/tview v0.0.0-20200507165325-823f280c5426 - github.com/stretchr/testify v1.4.0 // indirect golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect ) diff --git a/go.sum b/go.sum index ea8d25b..ac90dde 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.5/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/pgavlin/femto v0.0.0-20191028012355-31a9964a50b5 h1:jDmnr/0bMWhOta4Rk9F0RAeSpdr71SLvI34Ooav/CYM= github.com/pgavlin/femto v0.0.0-20191028012355-31a9964a50b5/go.mod h1:vBSdyXS0eulREXU3VHVmy2BnHekZx8HTO8oEMIe/I+M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/repository/storm/task.go b/repository/storm/task.go index f843232..7031959 100644 --- a/repository/storm/task.go +++ b/repository/storm/task.go @@ -64,5 +64,5 @@ func (t *taskRepository) UpdateField(task *model.Task, field string, value inter } func (t *taskRepository) Delete(task *model.Task) error { - panic("implement me") + return t.DB.DeleteStruct(task) } diff --git a/screens/geek-life_v1.gif b/screens/geek-life_v1.gif new file mode 100644 index 0000000..7e61c7e Binary files /dev/null and b/screens/geek-life_v1.gif differ diff --git a/util/util.go b/util/util.go index 311323b..623455c 100644 --- a/util/util.go +++ b/util/util.go @@ -2,22 +2,51 @@ package util import ( "fmt" + "io/ioutil" "log" + "os" + "path" "strconv" "strings" "time" "github.com/asdine/storm/v3" + "github.com/mitchellh/go-homedir" ) // ConnectStorm Create database connection func ConnectStorm() *storm.DB { - db, err := storm.Open(GetEnvStr("DB_FILE", "geek-life.db")) - FatalIfError(err, "Could not connect Embedded Database File") + dbPath := GetEnvStr("DB_FILE", "") + var err error + + if dbPath == "" { + // Try in home dir + dbPath, err = homedir.Expand("~/.geek-life/default.db") + + // If home dir is not detected, try in system tmp dir + if err != nil { + f, _ := ioutil.TempFile("geek-life", "default.db") + dbPath = f.Name() + } + } + + CreateDirIfNotExist(path.Dir(dbPath)) + + db, openErr := storm.Open(dbPath) + FatalIfError(openErr, "Could not connect Embedded Database File") return db } +func CreateDirIfNotExist(dir string) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + err = os.MkdirAll(dir, 0755) + if err != nil { + panic(err) + } + } +} + // UnixToTime create time.Time from string timestamp func UnixToTime(timestamp string) time.Time { parts := strings.Split(timestamp, ".")