Files
geek-life/app/task_detail.go
Phanupong Janthapoon 0dedb1a7d9 [FIX] Misspell (#46)
2023-12-10 01:08:00 +06:00

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