Skip to content

Commit

Permalink
feat: Implement editor pane skeletally
Browse files Browse the repository at this point in the history
  • Loading branch information
ja-he committed Nov 11, 2023
1 parent cee9dd6 commit bf95e8c
Show file tree
Hide file tree
Showing 31 changed files with 1,002 additions and 769 deletions.
143 changes: 65 additions & 78 deletions internal/control/cli/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"fmt"
"math"
"os"
"path"
"strconv"
Expand All @@ -13,7 +14,8 @@ import (

"github.com/ja-he/dayplan/internal/control"
"github.com/ja-he/dayplan/internal/control/action"
"github.com/ja-he/dayplan/internal/control/editor"
"github.com/ja-he/dayplan/internal/control/edit"
"github.com/ja-he/dayplan/internal/control/edit/editors"
"github.com/ja-he/dayplan/internal/input"
"github.com/ja-he/dayplan/internal/input/processors"
"github.com/ja-he/dayplan/internal/model"
Expand Down Expand Up @@ -82,6 +84,36 @@ func NewController(
) *Controller {
controller := Controller{}

inputConfig := input.InputConfig{

Editor: map[input.Keyspec]input.Actionspec{
"h": "next-field",
"j": "prev-field",
"i": "enter-subeditor",
"<CR>": "commit",
"<ESC>": "abort",
},

StringEditor: input.ModedSpec{
Normal: map[input.Keyspec]input.Actionspec{
"h": "move-cursor-rune-left",
"l": "move-cursor-rune-right",
"<left>": "move-cursor-rune-left",
"<right>": "move-cursor-rune-right",
"0": "move-cursor-to-beginning",
"$": "move-cursor-to-end",
"<CR>": "commit",
"<ESC>": "abort",
"i": "swap-mode-insert",
},
Insert: map[input.Keyspec]input.Actionspec{
"<left>": "move-cursor-rune-left",
"<right>": "move-cursor-rune-right",
"<ESC>": "swap-mode-normal",
},
},
}

categoryGetter := func(name string) model.Category {
cat, ok := categoryStyling.GetKnownCategoriesByName()[name]
if ok {
Expand Down Expand Up @@ -345,21 +377,9 @@ func NewController(
}
},
func() int { return timelineWidth },
func() control.EventEditMode { return controller.data.EventEditMode },
func() edit.EventEditMode { return controller.data.EventEditMode },
)

var stringEditorAbort, stringEditorCommitAndExit, stringEditorMoveCursorLeft, stringEditorMoveCursorRight, stringEditorInsertMode func()
stringsEditorInputTree, err := input.ConstructInputTree(map[string]action.Action{
"<esc>": action.NewSimple(func() string { return "cancel editing" }, func() { stringEditorAbort() }),
"<cr>": action.NewSimple(func() string { return "cancel editing" }, func() { stringEditorCommitAndExit() }),
"h": action.NewSimple(func() string { return "move cursor left" }, func() { stringEditorMoveCursorLeft() }),
"l": action.NewSimple(func() string { return "move cursor left" }, func() { stringEditorMoveCursorRight() }),
"i": action.NewSimple(func() string { return "insert mode" }, func() { stringEditorInsertMode() }),
})
if err != nil {
stderrLogger.Fatal().Err(err).Msgf("could not create strings pane input tree")
}

var currentTask *model.Task
setCurrentTask := func(t *model.Task) { currentTask = t }
backlogViewParams := ui.BacklogViewParams{
Expand Down Expand Up @@ -539,61 +559,28 @@ func NewController(
log.Warn().Msg("apparently, task editor was still active when a new one was activated, unexpected / error")
}
var err error
controller.data.TaskEditor, err = editor.ConstructEditor(currentTask, nil)
controller.data.TaskEditor, err = editors.ConstructEditor(currentTask, nil)
if err != nil {
log.Error().Err(err).Interface("current-task", currentTask).Msg("was not able to construct editor for current task")
return
}

nameStringEditor := controller.data.TaskEditor.GetEditor("name")
if nameStringEditor == nil {
log.Warn().Msgf("task editor for '%s' has no view 'name'", currentTask.Name)
return
}
stringEditorAbort = func() {
controller.rootPane.PopSubpane()
}
stringEditorMoveCursorLeft = nameStringEditor.MoveCursorLeft
stringEditorMoveCursorRight = nameStringEditor.MoveCursorRight
stringEditorPane := panes.NewStringEditorPane(
func() (int, int, int, int) { return 0, 0, 60, 1 },
taskEditorPane, err := controller.data.TaskEditor.GetPane(
tui.NewConstrainedRenderer(renderer, func() (x, y, w, h int) {
screenWidth, screenHeight := screenSize()
taskEditorBoxWidth := int(math.Min(80, float64(screenWidth)))
taskEditorBoxHeight := int(math.Min(20, float64(screenHeight)))
return (screenWidth / 2) - (taskEditorBoxWidth / 2), (screenHeight / 2) - (taskEditorBoxHeight / 2), taskEditorBoxWidth, taskEditorBoxHeight
}),
func() bool { return true },
processors.NewModalInputProcessor(stringsEditorInputTree),
tui.NewConstrainedRenderer(renderer, func() (int, int, int, int) { return 0, 0, 60, 1 }),
nameStringEditor,
inputConfig,
stylesheet,
renderer,
)
editorInsertMode := processors.NewTextInputProcessor( // TODO rename
map[input.Key]action.Action{{Key: tcell.KeyESC}: action.NewSimple(func() string { return "exit insert mode" }, func() {
stringEditorPane.PopModalOverlay()
nameStringEditor.SetMode(input.TextEditModeNormal) // TODO: probably want to move away from this model of having the editor store the mode? maybe?
})},
nameStringEditor.AddRune,
)
stringEditorInsertMode = func() {
stringEditorPane.ApplyModalOverlay(editorInsertMode)
nameStringEditor.SetMode(input.TextEditModeInsert) // TODO: probably want to move away from this model of having the editor store the mode? maybe?
}
stringEditorCommitAndExit = func() {
nameStringEditor.Commit()
controller.rootPane.PopSubpane()
}
controller.rootPane.PushSubpane(stringEditorPane)
}),
"w": action.NewSimple(func() string { return "store backlog to file" }, func() {
writer, err := os.OpenFile(backlogFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Error().Err(err).Msgf("unable to write open backlog file '%s' for writing", backlogFilePath)
return
}
defer writer.Close()
err = backlog.Write(writer)
if err != nil {
log.Error().Err(err).Msg("unable to write backlog to writer")
log.Error().Err(err).Msgf("could not construct task editor pane")
return
}
log.Info().Msgf("wrote backlog to '%s' sucessfully", backlogFilePath)
controller.rootPane.PushSubpane(taskEditorPane)
}),
},
)
Expand Down Expand Up @@ -832,16 +819,16 @@ func NewController(
ensureEventsPaneTimestampVisible(controller.data.GetCurrentDay().Current.Start)
}
}),
"M": action.NewSimple(func() string { return "exit move mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = control.EventEditModeNormal }),
"<esc>": action.NewSimple(func() string { return "exit move mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = control.EventEditModeNormal }),
"M": action.NewSimple(func() string { return "exit move mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = edit.EventEditModeNormal }),
"<esc>": action.NewSimple(func() string { return "exit move mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = edit.EventEditModeNormal }),
// TODO(ja-he): mode switching
},
)
if err != nil {
panic(err.Error())
}
dayEventsPane.ApplyModalOverlay(input.CapturingOverlayWrap(overlay))
controller.data.EventEditMode = control.EventEditModeMove
controller.data.EventEditMode = edit.EventEditModeMove
}
ensureBacklogTaskVisible = func(t *model.Task) {
viewportLB, viewportUB := tasksPane.GetTaskVisibilityBounds()
Expand Down Expand Up @@ -945,15 +932,15 @@ func NewController(
)
ensureEventsPaneTimestampVisible(current.Start)
}),
"m": action.NewSimple(func() string { return "exit move mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = control.EventEditModeNormal }),
"<esc>": action.NewSimple(func() string { return "exit move mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = control.EventEditModeNormal }),
"m": action.NewSimple(func() string { return "exit move mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = edit.EventEditModeNormal }),
"<esc>": action.NewSimple(func() string { return "exit move mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = edit.EventEditModeNormal }),
},
)
if err != nil {
panic(err.Error())
}
dayEventsPane.ApplyModalOverlay(input.CapturingOverlayWrap(eventMoveOverlay))
controller.data.EventEditMode = control.EventEditModeMove
controller.data.EventEditMode = edit.EventEditModeMove
})}
dayViewEventsPaneInputTree.Root.Children[input.Key{Key: tcell.KeyRune, Ch: 'r'}] = &input.Node{Action: action.NewSimple(func() string { return "enter event resize mode" }, func() {
if controller.data.GetCurrentDay().Current == nil {
Expand Down Expand Up @@ -999,15 +986,15 @@ func NewController(
)
ensureEventsPaneTimestampVisible(current.End)
}),
"r": action.NewSimple(func() string { return "exit resize mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = control.EventEditModeNormal }),
"<esc>": action.NewSimple(func() string { return "exit resize mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = control.EventEditModeNormal }),
"r": action.NewSimple(func() string { return "exit resize mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = edit.EventEditModeNormal }),
"<esc>": action.NewSimple(func() string { return "exit resize mode" }, func() { dayEventsPane.PopModalOverlay(); controller.data.EventEditMode = edit.EventEditModeNormal }),
},
)
if err != nil {
stderrLogger.Fatal().Err(err).Msg("failed to construct input tree for event pane's resize mode")
}
dayEventsPane.ApplyModalOverlay(input.CapturingOverlayWrap(eventResizeOverlay))
controller.data.EventEditMode = control.EventEditModeResize
controller.data.EventEditMode = edit.EventEditModeResize
})}

var helpContentRegister func()
Expand Down Expand Up @@ -1399,7 +1386,7 @@ func NewController(
}

controller.data.EventEditor.SetMode(input.TextEditModeNormal)
controller.data.EventEditMode = control.EventEditModeNormal
controller.data.EventEditMode = edit.EventEditModeNormal

coordinatesProvided := (envData.Latitude != "" && envData.Longitude != "")
owmApiKeyProvided := (envData.OwmApiKey != "")
Expand Down Expand Up @@ -1463,7 +1450,7 @@ func NewController(
controller.initializedScreen = renderer
controller.syncer = renderer

controller.data.MouseEditState = control.MouseEditStateNone
controller.data.MouseEditState = edit.MouseEditStateNone

return &controller
}
Expand Down Expand Up @@ -1496,14 +1483,14 @@ func (t *Controller) ScrollBottom() {
}

func (t *Controller) abortEdit() {
t.data.MouseEditState = control.MouseEditStateNone
t.data.MouseEditState = edit.MouseEditStateNone
t.data.MouseEditedEvent = nil
t.data.EventEditor.Active = false
t.rootPane.PopSubpane()
}

func (t *Controller) endEdit() {
t.data.MouseEditState = control.MouseEditStateNone
t.data.MouseEditState = edit.MouseEditStateNone
t.data.MouseEditedEvent = nil
if t.data.EventEditor.Active {
t.data.EventEditor.Active = false
Expand All @@ -1515,13 +1502,13 @@ func (t *Controller) endEdit() {
}

func (t *Controller) startMouseMove(eventsInfo *ui.EventsPanePositionInfo) {
t.data.MouseEditState = control.MouseEditStateMoving
t.data.MouseEditState = edit.MouseEditStateMoving
t.data.MouseEditedEvent = eventsInfo.Event
t.data.CurrentMoveStartingOffsetMinutes = eventsInfo.Event.Start.DurationInMinutesUntil(eventsInfo.Time)
}

func (t *Controller) startMouseResize(eventsInfo *ui.EventsPanePositionInfo) {
t.data.MouseEditState = control.MouseEditStateResizing
t.data.MouseEditState = edit.MouseEditStateResizing
t.data.MouseEditedEvent = eventsInfo.Event
}

Expand All @@ -1543,7 +1530,7 @@ func (t *Controller) startMouseEventCreation(info *ui.EventsPanePositionInfo) {
log.Error().Err(err).Interface("event", e).Msg("error occurred adding event")
} else {
t.data.MouseEditedEvent = &e
t.data.MouseEditState = control.MouseEditStateResizing
t.data.MouseEditState = edit.MouseEditStateResizing
}
}

Expand Down Expand Up @@ -1867,7 +1854,7 @@ func (t *Controller) Run() {
switch e := ev.(type) {
case *tcell.EventKey:
t.data.MouseMode = false
t.data.MouseEditState = control.MouseEditStateNone
t.data.MouseEditState = edit.MouseEditStateNone

key := input.KeyFromTcellEvent(e)
inputApplied := t.rootPane.ProcessInput(key)
Expand All @@ -1883,11 +1870,11 @@ func (t *Controller) Run() {
t.updateCursorPos(x, y)

switch t.data.MouseEditState {
case control.MouseEditStateNone:
case edit.MouseEditStateNone:
t.handleMouseNoneEditEvent(e)
case control.MouseEditStateResizing:
case edit.MouseEditStateResizing:
t.handleMouseResizeEditEvent(ev)
case control.MouseEditStateMoving:
case edit.MouseEditStateMoving:
t.handleMouseMoveEditEvent(ev)
}

Expand Down
33 changes: 6 additions & 27 deletions internal/control/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package control
import (
"sync"

"github.com/ja-he/dayplan/internal/control/editor"
"github.com/ja-he/dayplan/internal/control/edit"
"github.com/ja-he/dayplan/internal/control/edit/editors"
"github.com/ja-he/dayplan/internal/model"
"github.com/ja-he/dayplan/internal/styling"
"github.com/ja-he/dayplan/internal/ui"
Expand Down Expand Up @@ -81,8 +82,8 @@ type ControlData struct {
CurrentDate model.Date
Weather weather.Handler

EventEditor editor.EventEditor
TaskEditor editor.Editor
EventEditor editors.EventEditor
TaskEditor edit.Editor

ShowLog bool
ShowHelp bool
Expand All @@ -97,35 +98,13 @@ type ControlData struct {
EventProcessingTimes util.MetricsHandler

MouseMode bool
EventEditMode EventEditMode
EventEditMode edit.EventEditMode

MouseEditState MouseEditState
MouseEditState edit.MouseEditState
MouseEditedEvent *model.Event
CurrentMoveStartingOffsetMinutes int
}

type MouseEditState int

const (
_ MouseEditState = iota
MouseEditStateNone
MouseEditStateMoving
MouseEditStateResizing
)

func (s MouseEditState) toString() string {
return "TODO"
}

type EventEditMode = int

const (
_ EventEditMode = iota
EventEditModeNormal
EventEditModeMove
EventEditModeResize
)

type DaysData struct {
daysMutex sync.RWMutex
days map[model.Date]DayWithInfo
Expand Down
29 changes: 29 additions & 0 deletions internal/control/edit/editor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Package edit implements generic interfaces for editing of objects (by the
// user).
package edit

import (
"github.com/ja-he/dayplan/internal/input"
"github.com/ja-he/dayplan/internal/styling"
"github.com/ja-he/dayplan/internal/ui"
)

// Editor...
type Editor interface {
GetName() string

// Commit the state of the editor.
Commit()

// Abort the editors actions, discarding state.
Abort()

// GetPane returns a pane that represents this editor.
GetPane(
renderer ui.ConstrainedRenderer,
visible func() bool,
inputConfig input.InputConfig,
stylesheet styling.Stylesheet,
cursorController ui.TextCursorController,
) (ui.Pane, error)
}
Loading

0 comments on commit bf95e8c

Please sign in to comment.