Prepared for first release

- Creating db file in home dir or DB_FILE env variable
- Updated README with usages details
- Added GIF screencast
This commit is contained in:
Anis Ahmad
2020-06-06 02:13:04 +06:00
parent 89624edcba
commit 1cb95b3ce2
15 changed files with 373 additions and 114 deletions

1
.gitignore vendored
View File

@@ -13,5 +13,6 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
builds/
/geek-life /geek-life
/geek-life.db /geek-life.db

137
README.md
View File

@@ -1,2 +1,135 @@
# geek-task geek-life - The CLI Task Manager for Geeks :technologist:
Todo List Manager for Geeks =========
[![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 <sup>1</sup> - how much lighter you can think?
- Task note editor with markdown syntax highlighting<sup>2</sup>
- 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/)<sup>3</sup> 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)

View File

@@ -14,21 +14,22 @@ import (
) )
var ( var (
app *tview.Application app *tview.Application
newProject, newTask *tview.InputField newProject, newTask *tview.InputField
projectList, taskList *tview.List projectList, taskList *tview.List
projectPane, taskPane, detailPane *tview.Flex projectPane, projectDetailPane *tview.Flex
layout, contents *tview.Flex taskPane, taskDetailPane *tview.Flex
statusBar *tview.Pages layout, contents *tview.Flex
message *tview.TextView statusBar *tview.Pages
shortcutsPage, messagePage string = "shortcuts", "message" message *tview.TextView
shortcutsPage, messagePage string = "shortcuts", "message"
db *storm.DB db *storm.DB
projectRepo repository.ProjectRepository projectRepo repository.ProjectRepository
taskRepo repository.TaskRepository taskRepo repository.TaskRepository
projects []model.Project projects []model.Project
currentProject model.Project currentProject *model.Project
) )
func main() { func main() {
@@ -40,7 +41,7 @@ func main() {
projectRepo = repo.NewProjectRepository(db) projectRepo = repo.NewProjectRepository(db)
taskRepo = repo.NewTaskRepository(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) cloudStatus := tview.NewTextView().SetText("[::d]Cloud Sync: off").SetTextAlign(tview.AlignRight).SetDynamicColors(true)
titleBar := tview.NewFlex(). titleBar := tview.NewFlex().
@@ -48,6 +49,7 @@ func main() {
AddItem(cloudStatus, 0, 1, false) AddItem(cloudStatus, 0, 1, false)
prepareProjectPane() prepareProjectPane()
prepareProjectDetail()
prepareTaskPane() prepareTaskPane()
prepareStatusBar() prepareStatusBar()
prepareDetailPane() prepareDetailPane()
@@ -55,21 +57,20 @@ func main() {
contents = tview.NewFlex(). contents = tview.NewFlex().
AddItem(projectPane, 25, 1, true). AddItem(projectPane, 25, 1, true).
AddItem(taskPane, 0, 2, false) AddItem(taskPane, 0, 2, false)
//AddItem(detailPane, 0, 3, true)
layout = tview.NewFlex().SetDirection(tview.FlexRow). layout = tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(titleBar, 2, 1, false). AddItem(titleBar, 2, 1, false).
AddItem(contents, 0, 2, true). AddItem(contents, 0, 2, true).
AddItem(statusBar, 1, 1, false) AddItem(statusBar, 1, 1, false)
setKeyboardShortcuts(projectPane, taskPane) setKeyboardShortcuts()
if err := app.SetRoot(layout, true).EnableMouse(true).Run(); err != nil { if err := app.SetRoot(layout, true).EnableMouse(true).Run(); err != nil {
panic(err) 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 { return app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if ignoreKeyEvt() { if ignoreKeyEvt() {
return event return event
@@ -81,7 +82,7 @@ func setKeyboardShortcuts(projectPane *tview.Flex, taskPane *tview.Flex) *tview.
event = handleProjectPaneShortcuts(event) event = handleProjectPaneShortcuts(event)
case taskPane.HasFocus(): case taskPane.HasFocus():
event = handleTaskPaneShortcuts(event) event = handleTaskPaneShortcuts(event)
case detailPane.HasFocus(): case taskDetailPane.HasFocus():
event = handleDetailPaneShortcuts(event) event = handleDetailPaneShortcuts(event)
} }
@@ -110,9 +111,9 @@ func prepareStatusBar() {
tview.NewGrid(). tview.NewGrid().
SetColumns(0, 0, 0, 0). SetColumns(0, 0, 0, 0).
SetRows(0). SetRows(0).
AddItem(tview.NewTextView().SetText("Shortcuts: Alt+.(dot)"), 0, 0, 1, 1, 0, 0, false). AddItem(tview.NewTextView().SetText("Navigate List: ↓/↑"), 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/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("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), AddItem(tview.NewTextView().SetText("Quit: Ctrl+C").SetTextAlign(tview.AlignRight), 0, 3, 1, 1, 0, 0, false),
true, true,
true, true,

50
app/project_detail.go Normal file
View File

@@ -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))
}

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"strings"
"github.com/asdine/storm/v3" "github.com/asdine/storm/v3"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
@@ -9,17 +10,8 @@ import (
) )
func prepareProjectPane() { func prepareProjectPane() {
var err error
projects, err = projectRepo.GetAll()
if err != nil {
showMessage("Could not load Projects: " + err.Error())
}
projectList = tview.NewList().ShowSecondaryText(false) projectList = tview.NewList().ShowSecondaryText(false)
loadProjectList()
for i := range projects {
addProjectToList(i, false)
}
newProject = makeLightTextInput("+[New Project]"). newProject = makeLightTextInput("+[New Project]").
SetDoneFunc(func(key tcell.Key) { SetDoneFunc(func(key tcell.Key) {
@@ -29,7 +21,7 @@ func prepareProjectPane() {
if err != nil { if err != nil {
showMessage("[red::]Failed to create Project:" + err.Error()) showMessage("[red::]Failed to create Project:" + err.Error())
} else { } 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) projects = append(projects, project)
addProjectToList(len(projects)-1, true) addProjectToList(len(projects)-1, true)
newProject.SetText("") newProject.SetText("")
@@ -46,6 +38,30 @@ func prepareProjectPane() {
projectPane.SetBorder(true).SetTitle("[::u]P[::-]rojects") 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) { func addProjectToList(i int, selectItem bool) {
// To avoid overriding of loop variables - https://www.calhoun.io/gotchas-and-common-mistakes-with-closures-in-go/ // 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() { projectList.AddItem("- "+projects[i].Title, "", 0, func(idx int) func() {
@@ -53,28 +69,28 @@ func addProjectToList(i int, selectItem bool) {
}(i)) }(i))
if selectItem { if selectItem {
projectList.SetCurrentItem(i) projectList.SetCurrentItem(projectList.GetItemCount() - 1)
loadProject(i) loadProject(i)
} }
} }
func loadProject(idx int) { func loadProject(idx int) {
currentProject = projects[idx] currentProject = &projects[idx]
taskList.Clear() taskList.Clear()
app.SetFocus(taskPane) app.SetFocus(taskPane)
var err error 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()) showMessage("[red::]Error: " + err.Error())
} }
for i, task := range tasks { for i, task := range tasks {
taskList.AddItem(makeTaskListingTitle(task), "", 0, func(taskidx int) func() { addTaskToList(task, i)
return func() { loadTask(taskidx) }
}(i))
} }
contents.RemoveItem(detailPane) removeThirdCol()
projectDetailPane.SetTitle("[::b]" + currentProject.Title)
contents.AddItem(projectDetailPane, 25, 0, false)
} }
func handleProjectPaneShortcuts(event *tcell.EventKey) *tcell.EventKey { func handleProjectPaneShortcuts(event *tcell.EventKey) *tcell.EventKey {

View File

@@ -12,10 +12,12 @@ import (
var ( var (
taskName, taskDateDisplay *tview.TextView taskName, taskDateDisplay *tview.TextView
editorHint *tview.TextView
taskDate *tview.InputField taskDate *tview.InputField
taskDetailView *femto.View taskDetailView *femto.View
taskStatusToggle *tview.Button taskStatusToggle *tview.Button
colorscheme femto.Colorscheme colorscheme femto.Colorscheme
blankCell = tview.NewTextView()
) )
const dateLayoutISO = "2006-01-02" const dateLayoutISO = "2006-01-02"
@@ -23,51 +25,40 @@ const dateLayoutHuman = "02 Jan, Monday"
func prepareDetailPane() { func prepareDetailPane() {
taskName = tview.NewTextView().SetDynamicColors(true) taskName = tview.NewTextView().SetDynamicColors(true)
hr := makeHorizontalLine(tview.BoxDrawingsLightHorizontal)
prepareDetailsEditor() prepareDetailsEditor()
taskStatusToggle = makeButton("Complete", func() {}).SetLabelColor(tcell.ColorLightGray) taskStatusToggle = makeButton("Complete", toggleActiveTaskStatus).SetLabelColor(tcell.ColorLightGray)
hint := tview.NewTextView().SetTextColor(tcell.ColorYellow). toggleHint := tview.NewTextView().SetTextColor(tcell.ColorDimGray).SetText("<space> to toggle")
SetText("press Enter to save changes, Esc to ignore")
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(taskName, 2, 1, true).
AddItem(hr, 1, 1, false). AddItem(makeHorizontalLine(tcell.RuneS3, tcell.ColorGray), 1, 1, false).
AddItem(nil, 1, 1, false). AddItem(blankCell, 1, 1, false).
AddItem(makeDateRow(), 1, 1, true). AddItem(makeDateRow(), 1, 1, true).
AddItem(blankCell, 1, 1, false).
AddItem(editorLabel, 1, 1, false).
AddItem(taskDetailView, 15, 4, false). AddItem(taskDetailView, 15, 4, false).
AddItem(tview.NewTextView(), 1, 1, false). AddItem(editorHelp, 1, 1, false).
AddItem(hint, 1, 1, false). AddItem(blankCell, 0, 1, false).
AddItem(nil, 0, 1, false). AddItem(toggleHint, 1, 1, false).
AddItem(taskStatusToggle, 3, 1, false) AddItem(taskStatusToggle, 3, 1, false)
detailPane.SetBorder(true).SetTitle("Detail") taskDetailPane.SetBorder(true).SetTitle("Task 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
})
} }
func makeDateRow() *tview.Flex { func makeDateRow() *tview.Flex {
@@ -83,6 +74,7 @@ func makeDateRow() *tview.Flex {
case tcell.KeyEsc: case tcell.KeyEsc:
setTaskDate(currentTask.DueDate, false) setTaskDate(currentTask.DueDate, false)
} }
app.SetFocus(taskDetailPane)
}) })
todaySelector := func() { todaySelector := func() {
@@ -100,31 +92,28 @@ func makeDateRow() *tview.Flex {
return tview.NewFlex(). return tview.NewFlex().
AddItem(taskDateDisplay, 0, 2, true). AddItem(taskDateDisplay, 0, 2, true).
AddItem(taskDate, 14, 0, true). AddItem(taskDate, 14, 0, true).
AddItem(nil, 1, 0, false). AddItem(blankCell, 1, 0, false).
AddItem(nil, 1, 0, false).
AddItem(makeButton("today", todaySelector), 8, 1, 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(makeButton("+1", nextDaySelector), 4, 1, false).
AddItem(nil, 1, 0, false). AddItem(blankCell, 1, 0, false).
AddItem(makeButton("-1", prevDaySelector), 4, 1, false) AddItem(makeButton("-1", prevDaySelector), 4, 1, false)
} }
func setStatusToggle(idx int) { func setStatusToggle() {
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), "")
}
})
}
if currentTask.Completed { if currentTask.Completed {
action(idx, "Resume", tcell.ColorMaroon, false) taskStatusToggle.SetLabel("Resume").SetBackgroundColor(tcell.ColorMaroon)
} else { } 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() { func activateEditor() {
taskDetailView.Readonly = false taskDetailView.Readonly = false
taskDetailView.SetBorderColor(tcell.ColorDarkOrange) taskDetailView.SetBorderColor(tcell.ColorDarkOrange)
editorHint.SetText(" Esc to save changes")
app.SetFocus(taskDetailView) app.SetFocus(taskDetailView)
} }
func deactivateEditor() { func deactivateEditor() {
taskDetailView.Readonly = true taskDetailView.Readonly = true
taskDetailView.SetBorderColor(tcell.ColorLightSlateGray) taskDetailView.SetBorderColor(tcell.ColorLightSlateGray)
app.SetFocus(detailPane) editorHint.SetText(" e to edit, ↓↑ to scroll")
app.SetFocus(taskDetailPane)
} }
func handleDetailPaneShortcuts(event *tcell.EventKey) *tcell.EventKey { func handleDetailPaneShortcuts(event *tcell.EventKey) *tcell.EventKey {
switch event.Rune() { switch event.Key() {
case 'e': case tcell.KeyEsc:
activateEditor() app.SetFocus(taskPane)
case 'd': case tcell.KeyDown:
app.SetFocus(taskDate) 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 return event

View File

@@ -11,8 +11,9 @@ import (
) )
var ( var (
tasks []model.Task tasks []model.Task
currentTask *model.Task currentTask *model.Task
currentTaskIdx int
) )
func prepareTaskPane() { func prepareTaskPane() {
@@ -25,12 +26,13 @@ func prepareTaskPane() {
SetDoneFunc(func(key tcell.Key) { SetDoneFunc(func(key tcell.Key) {
switch key { switch key {
case tcell.KeyEnter: case tcell.KeyEnter:
task, err := taskRepo.Create(currentProject, newTask.GetText(), "", "", time.Now().Unix()) task, err := taskRepo.Create(*currentProject, newTask.GetText(), "", "", 0)
if err != nil { if err != nil {
showMessage("[red::]Could not create Task:" + err.Error()) 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("") newTask.SetText("")
case tcell.KeyEsc: case tcell.KeyEsc:
app.SetFocus(taskPane) app.SetFocus(taskPane)
@@ -45,26 +47,38 @@ func prepareTaskPane() {
taskPane.SetBorder(true).SetTitle("[::u]T[::-]asks") 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) { func loadTask(idx int) {
contents.RemoveItem(detailPane) removeThirdCol()
currentTask = &tasks[idx] currentTaskIdx = idx
currentTask = &tasks[currentTaskIdx]
taskName.SetText(fmt.Sprintf("[%s::b]# %s", getTaskTitleColor(*currentTask), currentTask.Title)) taskName.SetText(fmt.Sprintf("[%s::b]# %s", getTaskTitleColor(*currentTask), currentTask.Title))
taskDetailView.Buf = makeBufferFromString(currentTask.Details) taskDetailView.Buf = makeBufferFromString(currentTask.Details)
taskDetailView.SetColorscheme(colorscheme) taskDetailView.SetColorscheme(colorscheme)
taskDetailView.Start() taskDetailView.Start()
setTaskDate(currentTask.DueDate, false) setTaskDate(currentTask.DueDate, false)
setStatusToggle()
contents.AddItem(detailPane, 0, 3, false) contents.AddItem(taskDetailPane, 0, 3, false)
setStatusToggle(idx)
deactivateEditor() deactivateEditor()
} }
func removeThirdCol() {
contents.RemoveItem(taskDetailPane)
contents.RemoveItem(projectDetailPane)
}
func getTaskTitleColor(task model.Task) string { func getTaskTitleColor(task model.Task) string {
colorName := "whitesmoke" colorName := "olive"
if task.Completed { if task.Completed {
colorName = "lime" 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" colorName = "red"
} }

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"reflect" "reflect"
"time" "time"
@@ -10,11 +11,11 @@ import (
"github.com/ajaxray/geek-life/util" "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 := tview.NewTextView()
hr.SetDrawFunc(func(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) { 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. // 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 centerY := y + height/2
for cx := x; cx < x+width; cx++ { for cx := x; cx < x+width; cx++ {
screen.SetContent(cx, centerY, lineChar, nil, style) screen.SetContent(cx, centerY, lineChar, nil, style)
@@ -30,7 +31,7 @@ func makeHorizontalLine(lineChar rune) *tview.TextView {
func makeLightTextInput(placeholder string) *tview.InputField { func makeLightTextInput(placeholder string) *tview.InputField {
return tview.NewInputField(). return tview.NewInputField().
SetPlaceholder(placeholder). SetPlaceholder(placeholder).
SetPlaceholderTextColor(tcell.ColorLightSlateGray). SetPlaceholderTextColor(tcell.ColorYellow).
SetFieldTextColor(tcell.ColorBlack). SetFieldTextColor(tcell.ColorBlack).
SetFieldBackgroundColor(tcell.ColorGray) SetFieldBackgroundColor(tcell.ColorGray)
} }
@@ -49,13 +50,18 @@ func showMessage(text string) {
statusBar.SwitchToPage(messagePage) statusBar.SwitchToPage(messagePage)
go func() { go func() {
time.Sleep(time.Second * 5)
app.QueueUpdateDraw(func() { app.QueueUpdateDraw(func() {
time.Sleep(time.Second * 5)
statusBar.SwitchToPage(shortcutsPage) 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 { func makeButton(label string, handler func()) *tview.Button {
btn := tview.NewButton(label).SetSelectedFunc(handler). btn := tview.NewButton(label).SetSelectedFunc(handler).
SetLabelColor(tcell.ColorWhite) SetLabelColor(tcell.ColorWhite)

5
build.sh Normal file
View File

@@ -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_*

0
builds/.gitkeep Normal file
View File

2
go.mod
View File

@@ -6,9 +6,9 @@ require (
github.com/asdine/storm/v3 v3.2.0 github.com/asdine/storm/v3 v3.2.0
github.com/gdamore/tcell v1.3.0 github.com/gdamore/tcell v1.3.0
github.com/golang/protobuf v1.3.3 // indirect 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/pgavlin/femto v0.0.0-20191028012355-31a9964a50b5
github.com/rivo/tview v0.0.0-20200507165325-823f280c5426 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 golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect
) )

2
go.sum
View File

@@ -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.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 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 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 h1:jDmnr/0bMWhOta4Rk9F0RAeSpdr71SLvI34Ooav/CYM=
github.com/pgavlin/femto v0.0.0-20191028012355-31a9964a50b5/go.mod h1:vBSdyXS0eulREXU3VHVmy2BnHekZx8HTO8oEMIe/I+M= 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= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

View File

@@ -64,5 +64,5 @@ func (t *taskRepository) UpdateField(task *model.Task, field string, value inter
} }
func (t *taskRepository) Delete(task *model.Task) error { func (t *taskRepository) Delete(task *model.Task) error {
panic("implement me") return t.DB.DeleteStruct(task)
} }

BIN
screens/geek-life_v1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 KiB

View File

@@ -2,22 +2,51 @@ package util
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os"
"path"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/asdine/storm/v3" "github.com/asdine/storm/v3"
"github.com/mitchellh/go-homedir"
) )
// ConnectStorm Create database connection // ConnectStorm Create database connection
func ConnectStorm() *storm.DB { func ConnectStorm() *storm.DB {
db, err := storm.Open(GetEnvStr("DB_FILE", "geek-life.db")) dbPath := GetEnvStr("DB_FILE", "")
FatalIfError(err, "Could not connect Embedded Database 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 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 // UnixToTime create time.Time from string timestamp
func UnixToTime(timestamp string) time.Time { func UnixToTime(timestamp string) time.Time {
parts := strings.Split(timestamp, ".") parts := strings.Split(timestamp, ".")