361 lines
9.8 KiB
Go
361 lines
9.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/atotto/clipboard"
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/pgavlin/femto"
|
|
"github.com/pgavlin/femto/runtime"
|
|
"github.com/rivo/tview"
|
|
|
|
"github.com/ajaxray/geek-life/model"
|
|
"github.com/ajaxray/geek-life/repository"
|
|
"github.com/ajaxray/geek-life/util"
|
|
)
|
|
|
|
const dateLayoutISO = "2006-01-02"
|
|
const dateLayoutHuman = "02 Jan, Monday"
|
|
|
|
// TaskDetailPane displays detailed info of a Task
|
|
type TaskDetailPane struct {
|
|
*tview.Flex
|
|
header *TaskDetailHeader
|
|
taskDateDisplay *tview.TextView
|
|
editorHint *tview.TextView
|
|
taskDate *tview.InputField
|
|
taskStatusToggle *tview.Button
|
|
taskDetailView *femto.View
|
|
colorScheme femto.Colorscheme
|
|
taskRepo repository.TaskRepository
|
|
task *model.Task
|
|
}
|
|
|
|
// NewTaskDetailPane initializes and configures a TaskDetailPane
|
|
func NewTaskDetailPane(taskRepo repository.TaskRepository) *TaskDetailPane {
|
|
pane := TaskDetailPane{
|
|
Flex: tview.NewFlex().SetDirection(tview.FlexRow),
|
|
header: NewTaskDetailHeader(taskRepo),
|
|
taskDateDisplay: tview.NewTextView().SetDynamicColors(true),
|
|
taskStatusToggle: makeButton("Complete", nil).SetLabelColor(tcell.ColorLightGray),
|
|
taskRepo: taskRepo,
|
|
}
|
|
|
|
pane.prepareDetailsEditor()
|
|
|
|
toggleHint := tview.NewTextView().SetTextColor(tcell.ColorDimGray).SetText("<space> to toggle")
|
|
pane.taskStatusToggle.SetSelectedFunc(pane.toggleTaskStatus)
|
|
|
|
pane.editorHint = tview.NewTextView().SetText(" e = edit, v = external, ↓↑ = scroll").SetTextColor(tcell.ColorDimGray)
|
|
|
|
// Prepare static (no external interaction) elements
|
|
editorLabel := tview.NewFlex().
|
|
AddItem(tview.NewTextView().SetText("Task Not[::u]e[::-]:").SetDynamicColors(true), 0, 1, false).
|
|
AddItem(makeButton("[::u]e[::-]dit", func() { pane.activateEditor() }), 6, 0, false)
|
|
editorHelp := tview.NewFlex().
|
|
AddItem(pane.editorHint, 0, 1, false).
|
|
AddItem(tview.NewTextView().SetTextAlign(tview.AlignRight).
|
|
SetText("syntax:markdown (monakai)").
|
|
SetTextColor(tcell.ColorDimGray), 0, 1, false)
|
|
|
|
pane.
|
|
AddItem(pane.header, 4, 1, true).
|
|
AddItem(blankCell, 1, 1, false).
|
|
AddItem(pane.makeDateRow(), 1, 1, true).
|
|
AddItem(blankCell, 1, 1, false).
|
|
AddItem(editorLabel, 1, 1, false).
|
|
AddItem(pane.taskDetailView, 15, 4, false).
|
|
AddItem(editorHelp, 1, 1, false).
|
|
AddItem(blankCell, 0, 1, false).
|
|
AddItem(toggleHint, 1, 1, false).
|
|
AddItem(pane.taskStatusToggle, 3, 1, false)
|
|
|
|
pane.SetBorder(true).SetTitle("Task Detail")
|
|
|
|
return &pane
|
|
}
|
|
|
|
func (td *TaskDetailPane) Export() {
|
|
var content bytes.Buffer
|
|
|
|
content.WriteString("# " + td.task.Title + " \n")
|
|
if td.taskDate.GetText() != "" {
|
|
content.WriteString("\n> Due Date: " + td.taskDate.GetText() + " \n")
|
|
}
|
|
content.WriteString("\n" + td.task.Details + " \n")
|
|
|
|
_ = clipboard.WriteAll(content.String())
|
|
app.SetFocus(td)
|
|
statusBar.showForSeconds("Task copied. Try Pasting anywhere.", 5)
|
|
}
|
|
|
|
func (td *TaskDetailPane) makeDateRow() *tview.Flex {
|
|
|
|
td.taskDate = makeLightTextInput("yyyy-mm-dd").
|
|
SetLabel("Set:").
|
|
SetLabelColor(tcell.ColorWhiteSmoke).
|
|
SetFieldWidth(12).
|
|
SetDoneFunc(func(key tcell.Key) {
|
|
switch key {
|
|
case tcell.KeyEnter:
|
|
date := parseDateInputOrCurrent(td.taskDate.GetText())
|
|
td.setTaskDate(date.Unix(), true)
|
|
case tcell.KeyEsc:
|
|
td.setTaskDate(td.task.DueDate, false)
|
|
}
|
|
app.SetFocus(td)
|
|
})
|
|
|
|
return tview.NewFlex().
|
|
AddItem(td.taskDateDisplay, 0, 2, true).
|
|
AddItem(td.taskDate, 14, 0, true).
|
|
AddItem(blankCell, 1, 0, false).
|
|
AddItem(makeButton("t[::u]o[::-]day", td.todaySelector), 8, 1, false).
|
|
AddItem(blankCell, 1, 0, false).
|
|
AddItem(makeButton("[::u]+[::-]1", td.nextDaySelector), 4, 1, false).
|
|
AddItem(blankCell, 1, 0, false).
|
|
AddItem(makeButton("[::u]-[::-]1", td.prevDaySelector), 4, 1, false)
|
|
}
|
|
|
|
func (td *TaskDetailPane) updateToggleDisplay() {
|
|
if td.task.Completed {
|
|
td.taskStatusToggle.SetLabel("Resume").SetBackgroundColor(tcell.ColorMaroon)
|
|
} else {
|
|
td.taskStatusToggle.SetLabel("Complete").SetBackgroundColor(tcell.ColorDarkGreen)
|
|
}
|
|
}
|
|
|
|
func (td *TaskDetailPane) toggleTaskStatus() {
|
|
status := !td.task.Completed
|
|
if taskRepo.UpdateField(td.task, "Completed", status) == nil {
|
|
td.task.Completed = status
|
|
taskPane.ReloadCurrentTask()
|
|
}
|
|
}
|
|
|
|
// Display Task date in detail pane, and update date if asked to
|
|
func (td *TaskDetailPane) setTaskDate(unixDate int64, update bool) {
|
|
if update {
|
|
td.task.DueDate = unixDate
|
|
if err := td.taskRepo.UpdateField(td.task, "DueDate", unixDate); err != nil {
|
|
statusBar.showForSeconds("Could not update due date: "+err.Error(), 5)
|
|
return
|
|
}
|
|
}
|
|
|
|
if unixDate != 0 {
|
|
due := time.Unix(unixDate, 0)
|
|
color := "white"
|
|
humanDate := due.Format(dateLayoutHuman)
|
|
|
|
if due.Before(time.Now()) {
|
|
color = "red"
|
|
}
|
|
td.taskDateDisplay.SetText(fmt.Sprintf("[::u]D[::-]ue: [%s]%s", color, humanDate))
|
|
td.taskDate.SetText(due.Format(dateLayoutISO))
|
|
} else {
|
|
td.taskDate.SetText("")
|
|
td.taskDateDisplay.SetText("[::u]D[::-]ue: [::d]Not Set")
|
|
}
|
|
}
|
|
|
|
func (td *TaskDetailPane) prepareDetailsEditor() {
|
|
|
|
td.taskDetailView = femto.NewView(makeBufferFromString(""))
|
|
td.taskDetailView.SetRuntimeFiles(runtime.Files)
|
|
|
|
// var colorScheme femto.Colorscheme
|
|
if monokai := runtime.Files.FindFile(femto.RTColorscheme, "monokai"); monokai != nil {
|
|
if data, err := monokai.Data(); err == nil {
|
|
td.colorScheme = femto.ParseColorscheme(string(data))
|
|
}
|
|
}
|
|
|
|
td.taskDetailView.SetColorscheme(td.colorScheme)
|
|
td.taskDetailView.SetBorder(true)
|
|
td.taskDetailView.SetBorderColor(tcell.ColorLightSlateGray)
|
|
|
|
td.taskDetailView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyEsc:
|
|
td.updateTaskNote(td.taskDetailView.Buf.String())
|
|
td.deactivateEditor()
|
|
return nil
|
|
}
|
|
|
|
return event
|
|
})
|
|
}
|
|
|
|
func (td *TaskDetailPane) updateTaskNote(note string) {
|
|
td.task.Details = note
|
|
err := taskRepo.Update(td.task)
|
|
if err == nil {
|
|
statusBar.showForSeconds("[lime]Saved task detail", 5)
|
|
} else {
|
|
statusBar.showForSeconds("[red]Could not save: "+err.Error(), 5)
|
|
}
|
|
}
|
|
|
|
func makeBufferFromString(content string) *femto.Buffer {
|
|
buff := femto.NewBufferFromString(content, "")
|
|
// taskDetail.Settings["ruler"] = false
|
|
buff.Settings["filetype"] = "markdown"
|
|
buff.Settings["keepautoindent"] = true
|
|
buff.Settings["statusline"] = false
|
|
buff.Settings["softwrap"] = true
|
|
buff.Settings["scrollbar"] = true
|
|
|
|
return buff
|
|
}
|
|
|
|
func (td *TaskDetailPane) activateEditor() {
|
|
td.taskDetailView.Readonly = false
|
|
td.taskDetailView.SetBorderColor(tcell.ColorDarkOrange)
|
|
td.editorHint.SetText(" Esc to save changes")
|
|
app.SetFocus(td.taskDetailView)
|
|
}
|
|
|
|
func (td *TaskDetailPane) deactivateEditor() {
|
|
td.taskDetailView.Readonly = true
|
|
td.taskDetailView.SetBorderColor(tcell.ColorLightSlateGray)
|
|
td.editorHint.SetText(" e = edit, v = external, ↓↑ = scroll")
|
|
app.SetFocus(td)
|
|
}
|
|
|
|
func (td *TaskDetailPane) editInExternalEditor() {
|
|
|
|
tmpFileName, err := writeToTmpFile(td.task.Details)
|
|
if err != nil {
|
|
statusBar.showForSeconds("[red::]Failed to create tmp file. Try in-app editing by pressing i", 5)
|
|
return
|
|
}
|
|
|
|
var messageToShow, updatedContent string
|
|
app.Suspend(func() {
|
|
cmd := exec.Command(util.GetEnvStr("EDITOR", "vim"), tmpFileName)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
messageToShow = "[red::]Failed to save content. Try in-app editing by pressing e"
|
|
return
|
|
}
|
|
|
|
if content, readErr := ioutil.ReadFile(tmpFileName); readErr == nil {
|
|
updatedContent = string(content)
|
|
} else {
|
|
messageToShow = "[red::]Failed to load external editing. Try in-app editing by pressing e"
|
|
}
|
|
})
|
|
|
|
if messageToShow != "" {
|
|
statusBar.showForSeconds(messageToShow, 10)
|
|
}
|
|
|
|
if updatedContent != "" {
|
|
td.updateTaskNote(updatedContent)
|
|
td.SetTask(td.task)
|
|
}
|
|
|
|
app.EnableMouse(true)
|
|
|
|
_ = os.Remove(tmpFileName)
|
|
|
|
// app.SetFocus(td)
|
|
}
|
|
|
|
// writeToTmpFile writes given content to a tmpFile and returns the filename
|
|
func writeToTmpFile(content string) (string, error) {
|
|
tmpFile, err := ioutil.TempFile("", "geek_life_task_note_*.md")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fileName := tmpFile.Name()
|
|
|
|
if err = ioutil.WriteFile(fileName, []byte(content), 0777); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return fileName, tmpFile.Close()
|
|
}
|
|
|
|
func (td *TaskDetailPane) handleShortcuts(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyEsc:
|
|
removeThirdCol()
|
|
app.SetFocus(taskPane)
|
|
contents.AddItem(projectDetailPane, 25, 0, false)
|
|
return nil
|
|
case tcell.KeyDown:
|
|
td.taskDetailView.ScrollDown(1)
|
|
return nil
|
|
case tcell.KeyUp:
|
|
td.taskDetailView.ScrollUp(1)
|
|
return nil
|
|
case tcell.KeyRune:
|
|
switch unicode.ToLower(event.Rune()) {
|
|
case 'e':
|
|
td.activateEditor()
|
|
return nil
|
|
case 'v':
|
|
td.editInExternalEditor()
|
|
return nil
|
|
case 'd':
|
|
app.SetFocus(td.taskDate)
|
|
return nil
|
|
case 'r':
|
|
td.header.ShowRename()
|
|
return nil
|
|
case ' ':
|
|
td.toggleTaskStatus()
|
|
return nil
|
|
case 'x':
|
|
td.Export()
|
|
return nil
|
|
case 'o':
|
|
td.todaySelector()
|
|
return nil
|
|
case '+':
|
|
td.nextDaySelector()
|
|
return nil
|
|
case '-':
|
|
td.prevDaySelector()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
// SetTask sets a Task to be displayed
|
|
func (td *TaskDetailPane) SetTask(task *model.Task) {
|
|
td.task = task
|
|
|
|
td.header.SetTask(task)
|
|
td.taskDetailView.Buf = makeBufferFromString(td.task.Details)
|
|
td.taskDetailView.SetColorscheme(td.colorScheme)
|
|
td.taskDetailView.Start()
|
|
td.setTaskDate(td.task.DueDate, false)
|
|
td.updateToggleDisplay()
|
|
td.deactivateEditor()
|
|
}
|
|
|
|
func (td *TaskDetailPane) todaySelector() {
|
|
td.setTaskDate(parseDateInputOrCurrent("").Unix(), true)
|
|
}
|
|
|
|
func (td *TaskDetailPane) nextDaySelector() {
|
|
td.setTaskDate(parseDateInputOrCurrent(td.taskDate.GetText()).AddDate(0, 0, 1).Unix(), true)
|
|
}
|
|
|
|
func (td *TaskDetailPane) prevDaySelector() {
|
|
td.setTaskDate(parseDateInputOrCurrent(td.taskDate.GetText()).AddDate(0, 0, -1).Unix(), true)
|
|
}
|