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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,5 +13,6 @@
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
builds/
|
||||
/geek-life
|
||||
/geek-life.db
|
||||
|
||||
137
README.md
137
README.md
@@ -1,2 +1,135 @@
|
||||
# geek-task
|
||||
Todo List Manager for Geeks
|
||||
geek-life - The CLI Task Manager for Geeks :technologist:
|
||||
=========
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](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!
|
||||
|
||||

|
||||
|
||||
### 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)
|
||||
|
||||
35
app/cli.go
35
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,
|
||||
|
||||
50
app/project_detail.go
Normal file
50
app/project_detail.go
Normal 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))
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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("<space> 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
|
||||
|
||||
34
app/tasks.go
34
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"
|
||||
}
|
||||
|
||||
|
||||
14
app/util.go
14
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)
|
||||
|
||||
5
build.sh
Normal file
5
build.sh
Normal 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
0
builds/.gitkeep
Normal file
2
go.mod
2
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
|
||||
)
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
BIN
screens/geek-life_v1.gif
Normal file
BIN
screens/geek-life_v1.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1020 KiB |
33
util/util.go
33
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, ".")
|
||||
|
||||
Reference in New Issue
Block a user