mirror of
https://github.com/boolean-maybe/tiki
synced 2026-04-21 13:37:20 +00:00
add statusline hints
This commit is contained in:
parent
8a9477d3fb
commit
0fa71de797
23 changed files with 375 additions and 108 deletions
|
|
@ -139,6 +139,11 @@ func (re *RecurrenceEdit) CycleNext() {
|
|||
re.cycleNext()
|
||||
}
|
||||
|
||||
// IsValueFocused returns true when the value part (weekday/day) is active.
|
||||
func (re *RecurrenceEdit) IsValueFocused() bool {
|
||||
return re.activePart == 1
|
||||
}
|
||||
|
||||
// MovePartLeft moves the active part to frequency (part 0).
|
||||
func (re *RecurrenceEdit) MovePartLeft() {
|
||||
re.activePart = 0
|
||||
|
|
|
|||
|
|
@ -88,13 +88,15 @@ type ColorConfig struct {
|
|||
HeaderActionViewLabelColor string // tview color string for view action labels
|
||||
|
||||
// Statusline colors (bottom bar, powerline style)
|
||||
StatuslineBg string // hex color for stat segment background, e.g. "#3a3a5c"
|
||||
StatuslineFg string // hex color for stat segment text, e.g. "#cccccc"
|
||||
StatuslineAccentBg string // hex color for accent segment background (first segment), e.g. "#5f87af"
|
||||
StatuslineAccentFg string // hex color for accent segment text, e.g. "#1c1c2e"
|
||||
StatuslineMessageFg string // hex color for right-section message text, e.g. "#ff8787"
|
||||
StatuslineMessageBg string // hex color for right-section message background, e.g. "#3a3a3a"
|
||||
StatuslineFillBg string // hex color for empty statusline area between segments
|
||||
StatuslineBg string // hex color for stat segment background, e.g. "#3a3a5c"
|
||||
StatuslineFg string // hex color for stat segment text, e.g. "#cccccc"
|
||||
StatuslineAccentBg string // hex color for accent segment background (first segment), e.g. "#5f87af"
|
||||
StatuslineAccentFg string // hex color for accent segment text, e.g. "#1c1c2e"
|
||||
StatuslineInfoFg string // hex color for info message text
|
||||
StatuslineInfoBg string // hex color for info message background
|
||||
StatuslineErrorFg string // hex color for error message text
|
||||
StatuslineErrorBg string // hex color for error message background
|
||||
StatuslineFillBg string // hex color for empty statusline area between segments
|
||||
}
|
||||
|
||||
// DefaultColors returns the default color configuration
|
||||
|
|
@ -195,13 +197,15 @@ func DefaultColors() *ColorConfig {
|
|||
HeaderActionViewLabelColor: "#808080", // gray for view-specific labels
|
||||
|
||||
// Statusline (Nord theme)
|
||||
StatuslineBg: "#434c5e", // Nord polar night 3
|
||||
StatuslineFg: "#d8dee9", // Nord snow storm 1
|
||||
StatuslineAccentBg: "#5e81ac", // Nord frost blue
|
||||
StatuslineAccentFg: "#2e3440", // Nord polar night 1
|
||||
StatuslineMessageFg: "#bf616a", // Nord aurora red
|
||||
StatuslineMessageBg: "#3b4252", // Nord polar night 2
|
||||
StatuslineFillBg: "#3b4252", // Nord polar night 2
|
||||
StatuslineBg: "#434c5e", // Nord polar night 3
|
||||
StatuslineFg: "#d8dee9", // Nord snow storm 1
|
||||
StatuslineAccentBg: "#5e81ac", // Nord frost blue
|
||||
StatuslineAccentFg: "#2e3440", // Nord polar night 1
|
||||
StatuslineInfoFg: "#a3be8c", // Nord aurora green
|
||||
StatuslineInfoBg: "#3b4252", // Nord polar night 2
|
||||
StatuslineErrorFg: "#bf616a", // Nord aurora red
|
||||
StatuslineErrorBg: "#3b4252", // Nord polar night 2
|
||||
StatuslineFillBg: "#3b4252", // Nord polar night 2
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ func NewDepsController(
|
|||
pluginConfig *model.PluginConfig,
|
||||
pluginDef *plugin.TikiPlugin,
|
||||
navController *NavigationController,
|
||||
statusline *model.StatuslineConfig,
|
||||
) *DepsController {
|
||||
return &DepsController{
|
||||
pluginBase: pluginBase{
|
||||
|
|
@ -37,6 +38,7 @@ func NewDepsController(
|
|||
pluginConfig: pluginConfig,
|
||||
pluginDef: pluginDef,
|
||||
navController: navController,
|
||||
statusline: statusline,
|
||||
registry: DepsViewActions(),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ func newDepsTestEnv(t *testing.T) (*DepsController, store.Store) {
|
|||
pluginConfig.SetLaneLayout([]int{1, 2, 1}, nil)
|
||||
|
||||
nav := newMockNavigationController()
|
||||
dc := NewDepsController(taskStore, pluginConfig, pluginDef, nav)
|
||||
dc := NewDepsController(taskStore, pluginConfig, pluginDef, nav, nil)
|
||||
return dc, taskStore
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"github.com/boolean-maybe/tiki/model"
|
||||
"github.com/boolean-maybe/tiki/plugin"
|
||||
)
|
||||
|
||||
|
|
@ -9,6 +10,7 @@ import (
|
|||
type DokiController struct {
|
||||
pluginDef *plugin.DokiPlugin
|
||||
navController *NavigationController
|
||||
statusline *model.StatuslineConfig
|
||||
registry *ActionRegistry
|
||||
}
|
||||
|
||||
|
|
@ -16,10 +18,12 @@ type DokiController struct {
|
|||
func NewDokiController(
|
||||
pluginDef *plugin.DokiPlugin,
|
||||
navController *NavigationController,
|
||||
statusline *model.StatuslineConfig,
|
||||
) *DokiController {
|
||||
return &DokiController{
|
||||
pluginDef: pluginDef,
|
||||
navController: navController,
|
||||
statusline: statusline,
|
||||
registry: DokiViewActions(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ type InputRouter struct {
|
|||
pluginControllers map[string]PluginControllerInterface // keyed by plugin name
|
||||
globalActions *ActionRegistry
|
||||
taskStore store.Store
|
||||
statusline *model.StatuslineConfig
|
||||
registerPlugin func(name string, cfg *model.PluginConfig, def plugin.Plugin, ctrl PluginControllerInterface)
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ func NewInputRouter(
|
|||
taskController *TaskController,
|
||||
pluginControllers map[string]PluginControllerInterface,
|
||||
taskStore store.Store,
|
||||
statusline *model.StatuslineConfig,
|
||||
) *InputRouter {
|
||||
return &InputRouter{
|
||||
navController: navController,
|
||||
|
|
@ -63,6 +65,7 @@ func NewInputRouter(
|
|||
pluginControllers: pluginControllers,
|
||||
globalActions: DefaultGlobalActions(),
|
||||
taskStore: taskStore,
|
||||
statusline: statusline,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +238,7 @@ func (ir *InputRouter) openDepsEditor(taskID string) bool {
|
|||
pluginConfig.SetViewMode(vm)
|
||||
}
|
||||
|
||||
ctrl := NewDepsController(ir.taskStore, pluginConfig, pluginDef, ir.navController)
|
||||
ctrl := NewDepsController(ir.taskStore, pluginConfig, pluginDef, ir.navController, ir.statusline)
|
||||
|
||||
if ir.registerPlugin != nil {
|
||||
ir.registerPlugin(name, pluginConfig, pluginDef, ctrl)
|
||||
|
|
|
|||
|
|
@ -269,6 +269,7 @@ type RecurrenceEditableView interface {
|
|||
type RecurrencePartNavigable interface {
|
||||
MoveRecurrencePartLeft() bool
|
||||
MoveRecurrencePartRight() bool
|
||||
IsRecurrenceValueFocused() bool
|
||||
}
|
||||
|
||||
// ViewInfoProvider is a view that provides its name and description for the header info section
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ func NewPluginController(
|
|||
pluginConfig *model.PluginConfig,
|
||||
pluginDef *plugin.TikiPlugin,
|
||||
navController *NavigationController,
|
||||
statusline *model.StatuslineConfig,
|
||||
) *PluginController {
|
||||
pc := &PluginController{
|
||||
pluginBase: pluginBase{
|
||||
|
|
@ -31,6 +32,7 @@ func NewPluginController(
|
|||
pluginConfig: pluginConfig,
|
||||
pluginDef: pluginDef,
|
||||
navController: navController,
|
||||
statusline: statusline,
|
||||
registry: PluginViewActions(),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ type pluginBase struct {
|
|||
pluginConfig *model.PluginConfig
|
||||
pluginDef *plugin.TikiPlugin
|
||||
navController *NavigationController
|
||||
statusline *model.StatuslineConfig
|
||||
registry *ActionRegistry
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ func TestEnsureFirstNonEmptyLaneSelectionSelectsFirstTask(t *testing.T) {
|
|||
pluginConfig.SetSelectedLane(0)
|
||||
pluginConfig.SetSelectedIndexForLane(0, 1)
|
||||
|
||||
pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil)
|
||||
pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil, nil)
|
||||
pc.EnsureFirstNonEmptyLaneSelection()
|
||||
|
||||
if pluginConfig.GetSelectedLane() != 1 {
|
||||
|
|
@ -94,7 +94,7 @@ func TestEnsureFirstNonEmptyLaneSelectionKeepsCurrentLane(t *testing.T) {
|
|||
pluginConfig.SetSelectedLane(1)
|
||||
pluginConfig.SetSelectedIndexForLane(1, 0)
|
||||
|
||||
pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil)
|
||||
pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil, nil)
|
||||
pc.EnsureFirstNonEmptyLaneSelection()
|
||||
|
||||
if pluginConfig.GetSelectedLane() != 1 {
|
||||
|
|
@ -126,7 +126,7 @@ func TestEnsureFirstNonEmptyLaneSelectionNoTasks(t *testing.T) {
|
|||
pluginConfig.SetSelectedLane(1)
|
||||
pluginConfig.SetSelectedIndexForLane(1, 2)
|
||||
|
||||
pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil)
|
||||
pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil, nil)
|
||||
pc.EnsureFirstNonEmptyLaneSelection()
|
||||
|
||||
if pluginConfig.GetSelectedLane() != 1 {
|
||||
|
|
@ -183,7 +183,7 @@ func TestLaneSwitchSelectsTopOfViewport(t *testing.T) {
|
|||
// Simulate that lane 1 has been scrolled to offset 3
|
||||
pluginConfig.SetScrollOffsetForLane(1, 3)
|
||||
|
||||
pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil)
|
||||
pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil, nil)
|
||||
|
||||
// Navigate right to lane 1
|
||||
pc.HandleAction(ActionNavRight)
|
||||
|
|
@ -242,7 +242,7 @@ func TestLaneSwitchClampsScrollOffsetToTaskCount(t *testing.T) {
|
|||
// Set a stale scroll offset that exceeds the task count
|
||||
pluginConfig.SetScrollOffsetForLane(1, 10)
|
||||
|
||||
pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil)
|
||||
pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil, nil)
|
||||
|
||||
// Navigate left (to empty lane, will skip to... well, nowhere)
|
||||
// Then try to go right from a fresh setup
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
type TaskController struct {
|
||||
taskStore store.Store
|
||||
navController *NavigationController
|
||||
statusline *model.StatuslineConfig
|
||||
currentTaskID string
|
||||
draftTask *taskpkg.Task // For new task creation only
|
||||
editingTask *taskpkg.Task // In-memory copy being edited (existing tasks)
|
||||
|
|
@ -34,10 +35,12 @@ type TaskController struct {
|
|||
func NewTaskController(
|
||||
taskStore store.Store,
|
||||
navController *NavigationController,
|
||||
statusline *model.StatuslineConfig,
|
||||
) *TaskController {
|
||||
return &TaskController{
|
||||
taskStore: taskStore,
|
||||
navController: navController,
|
||||
statusline: statusline,
|
||||
registry: TaskDetailViewActions(),
|
||||
editRegistry: TaskEditViewActions(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ func init() {
|
|||
func TestTaskController_SetDraft(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
draft := newTestTask()
|
||||
tc.SetDraft(draft)
|
||||
|
|
@ -44,7 +44,7 @@ func TestTaskController_SetDraft(t *testing.T) {
|
|||
func TestTaskController_ClearDraft(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
tc.SetDraft(newTestTask())
|
||||
tc.ClearDraft()
|
||||
|
|
@ -57,7 +57,7 @@ func TestTaskController_ClearDraft(t *testing.T) {
|
|||
func TestTaskController_StartEditSession(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
// Create a task in the store
|
||||
original := newTestTask()
|
||||
|
|
@ -87,7 +87,7 @@ func TestTaskController_StartEditSession(t *testing.T) {
|
|||
func TestTaskController_StartEditSession_NonExistent(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
editingTask := tc.StartEditSession("NONEXISTENT")
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ func TestTaskController_StartEditSession_NonExistent(t *testing.T) {
|
|||
func TestTaskController_CancelEditSession(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
// Start an edit session
|
||||
original := newTestTask()
|
||||
|
|
@ -183,7 +183,7 @@ func TestTaskController_SaveStatus(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
tt.setupTask(tc, taskStore)
|
||||
|
||||
|
|
@ -259,7 +259,7 @@ func TestTaskController_SaveType(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
tt.setupTask(tc, taskStore)
|
||||
|
||||
|
|
@ -342,7 +342,7 @@ func TestTaskController_SavePriority(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
tt.setupTask(tc, taskStore)
|
||||
|
||||
|
|
@ -418,7 +418,7 @@ func TestTaskController_SaveAssignee(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
tt.setupTask(tc, taskStore)
|
||||
|
||||
|
|
@ -493,7 +493,7 @@ func TestTaskController_SavePoints(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
tt.setupTask(tc, taskStore)
|
||||
|
||||
|
|
@ -572,7 +572,7 @@ func TestTaskController_SaveTitle(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
tt.setupTask(tc, taskStore)
|
||||
|
||||
|
|
@ -639,7 +639,7 @@ func TestTaskController_SaveDescription(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
tt.setupTask(tc, taskStore)
|
||||
|
||||
|
|
@ -669,7 +669,7 @@ func TestTaskController_SaveDescription(t *testing.T) {
|
|||
func TestTaskController_CommitEditSession_Draft(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
draft := newTestTaskWithID()
|
||||
draft.Title = "Draft Title"
|
||||
|
|
@ -699,7 +699,7 @@ func TestTaskController_CommitEditSession_Draft(t *testing.T) {
|
|||
func TestTaskController_CommitEditSession_DraftValidationFailure(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
draft := newTestTaskWithID()
|
||||
draft.Title = "" // Invalid - empty title
|
||||
|
|
@ -721,7 +721,7 @@ func TestTaskController_CommitEditSession_DraftValidationFailure(t *testing.T) {
|
|||
func TestTaskController_CommitEditSession_Existing(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
// Create original task
|
||||
original := newTestTask()
|
||||
|
|
@ -755,7 +755,7 @@ func TestTaskController_CommitEditSession_Existing(t *testing.T) {
|
|||
func TestTaskController_CommitEditSession_NoActiveSession(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
err := tc.CommitEditSession()
|
||||
if err != nil {
|
||||
|
|
@ -768,7 +768,7 @@ func TestTaskController_CommitEditSession_NoActiveSession(t *testing.T) {
|
|||
func TestTaskController_GetCurrentTask(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
// Create task
|
||||
original := newTestTask()
|
||||
|
|
@ -790,7 +790,7 @@ func TestTaskController_GetCurrentTask(t *testing.T) {
|
|||
func TestTaskController_GetCurrentTask_Empty(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
current := tc.GetCurrentTask()
|
||||
if current != nil {
|
||||
|
|
@ -801,7 +801,7 @@ func TestTaskController_GetCurrentTask_Empty(t *testing.T) {
|
|||
func TestTaskController_GetCurrentTask_NonExistent(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
tc.SetCurrentTask("NONEXISTENT")
|
||||
|
||||
|
|
@ -816,7 +816,7 @@ func TestTaskController_GetCurrentTask_NonExistent(t *testing.T) {
|
|||
func TestTaskController_GetActionRegistry(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
registry := tc.GetActionRegistry()
|
||||
if registry == nil {
|
||||
|
|
@ -833,7 +833,7 @@ func TestTaskController_GetActionRegistry(t *testing.T) {
|
|||
func TestTaskController_GetEditActionRegistry(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
registry := tc.GetEditActionRegistry()
|
||||
if registry == nil {
|
||||
|
|
@ -852,7 +852,7 @@ func TestTaskController_GetEditActionRegistry(t *testing.T) {
|
|||
func TestTaskController_FocusedField(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
// Initially should be empty
|
||||
if tc.GetFocusedField() != "" {
|
||||
|
|
@ -927,7 +927,7 @@ func TestTaskController_SaveDue(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
tt.setupTask(tc, taskStore)
|
||||
|
||||
|
|
@ -1029,7 +1029,7 @@ func TestTaskController_SaveTags(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
navController := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, navController)
|
||||
tc := NewTaskController(taskStore, navController, nil)
|
||||
|
||||
tt.setupTask(tc, taskStore)
|
||||
|
||||
|
|
|
|||
|
|
@ -65,12 +65,18 @@ func (c *TaskEditCoordinator) HandleKey(activeView View, event *tcell.EventKey)
|
|||
return c.FocusPrevField(activeView)
|
||||
case tcell.KeyLeft:
|
||||
if nav, ok := activeView.(RecurrencePartNavigable); ok {
|
||||
return nav.MoveRecurrencePartLeft()
|
||||
if nav.MoveRecurrencePartLeft() {
|
||||
c.updateFieldHint(activeView)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case tcell.KeyRight:
|
||||
if nav, ok := activeView.(RecurrencePartNavigable); ok {
|
||||
return nav.MoveRecurrencePartRight()
|
||||
if nav.MoveRecurrencePartRight() {
|
||||
c.updateFieldHint(activeView)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case tcell.KeyEscape:
|
||||
|
|
@ -89,7 +95,9 @@ func (c *TaskEditCoordinator) FocusNextField(activeView View) bool {
|
|||
if !ok {
|
||||
return false
|
||||
}
|
||||
return fieldFocusable.FocusNextField()
|
||||
result := fieldFocusable.FocusNextField()
|
||||
c.updateFieldHint(activeView)
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *TaskEditCoordinator) FocusPrevField(activeView View) bool {
|
||||
|
|
@ -97,7 +105,9 @@ func (c *TaskEditCoordinator) FocusPrevField(activeView View) bool {
|
|||
if !ok {
|
||||
return false
|
||||
}
|
||||
return fieldFocusable.FocusPrevField()
|
||||
result := fieldFocusable.FocusPrevField()
|
||||
c.updateFieldHint(activeView)
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *TaskEditCoordinator) CycleFieldValueUp(activeView View) bool {
|
||||
|
|
@ -118,6 +128,7 @@ func (c *TaskEditCoordinator) CommitAndClose(activeView View) bool {
|
|||
if !c.commit(activeView) {
|
||||
return false
|
||||
}
|
||||
c.clearFieldHint()
|
||||
c.navController.HandleBack()
|
||||
return true
|
||||
}
|
||||
|
|
@ -142,6 +153,7 @@ func (c *TaskEditCoordinator) CancelAndClose() bool {
|
|||
// Cancel edit session (discards changes) and clear any draft.
|
||||
c.taskController.CancelEditSession()
|
||||
c.taskController.ClearDraft()
|
||||
c.clearFieldHint()
|
||||
c.navController.HandleBack()
|
||||
return true
|
||||
}
|
||||
|
|
@ -171,6 +183,38 @@ func (c *TaskEditCoordinator) commit(activeView View) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// updateFieldHint shows or clears a statusline hint based on the focused field.
|
||||
func (c *TaskEditCoordinator) updateFieldHint(activeView View) {
|
||||
sl := c.taskController.statusline
|
||||
if sl == nil {
|
||||
return
|
||||
}
|
||||
fieldFocusable, ok := activeView.(FieldFocusableView)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
switch fieldFocusable.GetFocusedField() {
|
||||
case model.EditFieldStatus, model.EditFieldType, model.EditFieldPriority,
|
||||
model.EditFieldAssignee, model.EditFieldPoints, model.EditFieldDue:
|
||||
sl.SetMessage("↑↓ change value", model.MessageLevelInfo, false)
|
||||
case model.EditFieldRecurrence:
|
||||
if nav, ok := activeView.(RecurrencePartNavigable); ok && nav.IsRecurrenceValueFocused() {
|
||||
sl.SetMessage("← edit pattern ↑↓ change value", model.MessageLevelInfo, false)
|
||||
} else {
|
||||
sl.SetMessage("↑↓ change pattern → edit value", model.MessageLevelInfo, false)
|
||||
}
|
||||
default:
|
||||
sl.ClearMessage()
|
||||
}
|
||||
}
|
||||
|
||||
// clearFieldHint removes any statusline hint set by updateFieldHint.
|
||||
func (c *TaskEditCoordinator) clearFieldHint() {
|
||||
if sl := c.taskController.statusline; sl != nil {
|
||||
sl.ClearMessage()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TaskEditCoordinator) prepareView(activeView View, focus model.EditField) {
|
||||
app := c.navController.GetApp()
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,28 @@ func (m *mockTaskEditView) GetEditedTitle() string { return m.title
|
|||
func (m *mockTaskEditView) GetEditedDescription() string { return m.description }
|
||||
func (m *mockTaskEditView) GetEditedTags() []string { return m.tags }
|
||||
|
||||
// mockFieldFocusableView implements FieldFocusableView + RecurrencePartNavigable for hint tests.
|
||||
type mockFieldFocusableView struct {
|
||||
mockTaskEditView
|
||||
focusedField model.EditField
|
||||
valueFocused bool
|
||||
}
|
||||
|
||||
func (m *mockFieldFocusableView) SetFocusedField(field model.EditField) { m.focusedField = field }
|
||||
func (m *mockFieldFocusableView) GetFocusedField() model.EditField { return m.focusedField }
|
||||
func (m *mockFieldFocusableView) FocusNextField() bool {
|
||||
m.focusedField = model.NextField(m.focusedField)
|
||||
return true
|
||||
}
|
||||
func (m *mockFieldFocusableView) FocusPrevField() bool {
|
||||
m.focusedField = model.PrevField(m.focusedField)
|
||||
return true
|
||||
}
|
||||
func (m *mockFieldFocusableView) IsEditFieldFocused() bool { return true }
|
||||
func (m *mockFieldFocusableView) MoveRecurrencePartLeft() bool { m.valueFocused = false; return true }
|
||||
func (m *mockFieldFocusableView) MoveRecurrencePartRight() bool { m.valueFocused = true; return true }
|
||||
func (m *mockFieldFocusableView) IsRecurrenceValueFocused() bool { return m.valueFocused }
|
||||
|
||||
// mockNonEditView implements only View (not TaskEditView).
|
||||
type mockNonEditView struct{}
|
||||
|
||||
|
|
@ -68,7 +90,7 @@ func TestTaskEditCoordinator_HandleKey_TagsOnly_Backtab(t *testing.T) {
|
|||
func TestTaskEditCoordinator_HandleKey_Escape(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
nav := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, nav)
|
||||
tc := NewTaskController(taskStore, nav, nil)
|
||||
tc.SetDraft(newTestTask())
|
||||
|
||||
coord := NewTaskEditCoordinator(nav, tc)
|
||||
|
|
@ -89,7 +111,7 @@ func TestTaskEditCoordinator_HandleKey_Escape(t *testing.T) {
|
|||
func TestTaskEditCoordinator_Commit_SavesTags(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
nav := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, nav)
|
||||
tc := NewTaskController(taskStore, nav, nil)
|
||||
|
||||
draft := newTestTask()
|
||||
draft.Title = "Tagged Task"
|
||||
|
|
@ -119,7 +141,7 @@ func TestTaskEditCoordinator_Commit_SavesTags(t *testing.T) {
|
|||
|
||||
func TestTaskEditCoordinator_Commit_NonEditView(t *testing.T) {
|
||||
nav := newMockNavigationController()
|
||||
tc := NewTaskController(store.NewInMemoryStore(), nav)
|
||||
tc := NewTaskController(store.NewInMemoryStore(), nav, nil)
|
||||
coord := NewTaskEditCoordinator(nav, tc)
|
||||
|
||||
got := coord.commit(&mockNonEditView{})
|
||||
|
|
@ -131,7 +153,7 @@ func TestTaskEditCoordinator_Commit_NonEditView(t *testing.T) {
|
|||
func TestTaskEditCoordinator_Commit_ValidationFails(t *testing.T) {
|
||||
taskStore := store.NewInMemoryStore()
|
||||
nav := newMockNavigationController()
|
||||
tc := NewTaskController(taskStore, nav)
|
||||
tc := NewTaskController(taskStore, nav, nil)
|
||||
tc.SetDraft(newTestTask())
|
||||
|
||||
coord := NewTaskEditCoordinator(nav, tc)
|
||||
|
|
@ -149,3 +171,110 @@ func TestTaskEditCoordinator_Commit_ValidationFails(t *testing.T) {
|
|||
t.Error("commit() should return false when IsValid() returns false")
|
||||
}
|
||||
}
|
||||
|
||||
// --- field hint tests ---
|
||||
|
||||
func TestTaskEditCoordinator_FieldHint_RecurrencePatternFocused(t *testing.T) {
|
||||
sl := model.NewStatuslineConfig()
|
||||
nav := newMockNavigationController()
|
||||
tc := NewTaskController(store.NewInMemoryStore(), nav, sl)
|
||||
|
||||
coord := NewTaskEditCoordinator(nav, tc)
|
||||
view := &mockFieldFocusableView{focusedField: model.EditFieldRecurrence, valueFocused: false}
|
||||
|
||||
coord.updateFieldHint(view)
|
||||
|
||||
msg, level, _ := sl.GetMessage()
|
||||
if msg != "\u2191\u2193 change pattern \u2192 edit value" {
|
||||
t.Errorf("message = %q, want pattern hint", msg)
|
||||
}
|
||||
if level != model.MessageLevelInfo {
|
||||
t.Errorf("level = %q, want %q", level, model.MessageLevelInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskEditCoordinator_FieldHint_RecurrenceValueFocused(t *testing.T) {
|
||||
sl := model.NewStatuslineConfig()
|
||||
nav := newMockNavigationController()
|
||||
tc := NewTaskController(store.NewInMemoryStore(), nav, sl)
|
||||
|
||||
coord := NewTaskEditCoordinator(nav, tc)
|
||||
view := &mockFieldFocusableView{focusedField: model.EditFieldRecurrence, valueFocused: true}
|
||||
|
||||
coord.updateFieldHint(view)
|
||||
|
||||
msg, level, _ := sl.GetMessage()
|
||||
if msg != "\u2190 edit pattern \u2191\u2193 change value" {
|
||||
t.Errorf("message = %q, want value hint", msg)
|
||||
}
|
||||
if level != model.MessageLevelInfo {
|
||||
t.Errorf("level = %q, want %q", level, model.MessageLevelInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskEditCoordinator_FieldHint_NonRecurrenceClearsHint(t *testing.T) {
|
||||
sl := model.NewStatuslineConfig()
|
||||
nav := newMockNavigationController()
|
||||
tc := NewTaskController(store.NewInMemoryStore(), nav, sl)
|
||||
|
||||
coord := NewTaskEditCoordinator(nav, tc)
|
||||
|
||||
// set a hint first
|
||||
sl.SetMessage("some hint", model.MessageLevelInfo, false)
|
||||
|
||||
view := &mockFieldFocusableView{focusedField: model.EditFieldTitle}
|
||||
coord.updateFieldHint(view)
|
||||
|
||||
msg, _, _ := sl.GetMessage()
|
||||
if msg != "" {
|
||||
t.Errorf("message = %q, want empty after focusing non-recurrence field", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskEditCoordinator_FieldHint_FocusNextSetsHint(t *testing.T) {
|
||||
sl := model.NewStatuslineConfig()
|
||||
nav := newMockNavigationController()
|
||||
tc := NewTaskController(store.NewInMemoryStore(), nav, sl)
|
||||
|
||||
coord := NewTaskEditCoordinator(nav, tc)
|
||||
// Due is right before Recurrence in navigation order
|
||||
view := &mockFieldFocusableView{focusedField: model.EditFieldDue}
|
||||
|
||||
coord.FocusNextField(view)
|
||||
|
||||
if view.focusedField != model.EditFieldRecurrence {
|
||||
t.Fatalf("expected recurrence, got %q", view.focusedField)
|
||||
}
|
||||
msg, _, _ := sl.GetMessage()
|
||||
if msg != "\u2191\u2193 change pattern \u2192 edit value" {
|
||||
t.Errorf("message = %q, want hint after FocusNextField to recurrence", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskEditCoordinator_FieldHint_CancelClearsHint(t *testing.T) {
|
||||
sl := model.NewStatuslineConfig()
|
||||
nav := newMockNavigationController()
|
||||
tc := NewTaskController(store.NewInMemoryStore(), nav, sl)
|
||||
|
||||
coord := NewTaskEditCoordinator(nav, tc)
|
||||
sl.SetMessage("some hint", model.MessageLevelInfo, false)
|
||||
|
||||
coord.CancelAndClose()
|
||||
|
||||
msg, _, _ := sl.GetMessage()
|
||||
if msg != "" {
|
||||
t.Errorf("message = %q, want empty after CancelAndClose", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskEditCoordinator_FieldHint_NilStatuslineNoOp(t *testing.T) {
|
||||
nav := newMockNavigationController()
|
||||
tc := NewTaskController(store.NewInMemoryStore(), nav, nil)
|
||||
|
||||
coord := NewTaskEditCoordinator(nav, tc)
|
||||
view := &mockFieldFocusableView{focusedField: model.EditFieldRecurrence}
|
||||
|
||||
// should not panic
|
||||
coord.updateFieldHint(view)
|
||||
coord.clearFieldHint()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ func BuildControllers(
|
|||
taskStore store.Store,
|
||||
plugins []plugin.Plugin,
|
||||
pluginConfigs map[string]*model.PluginConfig,
|
||||
statuslineConfig *model.StatuslineConfig,
|
||||
) *Controllers {
|
||||
navController := controller.NewNavigationController(app)
|
||||
taskController := controller.NewTaskController(taskStore, navController)
|
||||
taskController := controller.NewTaskController(taskStore, navController, statuslineConfig)
|
||||
|
||||
pluginControllers := make(map[string]controller.PluginControllerInterface)
|
||||
for _, p := range plugins {
|
||||
|
|
@ -34,11 +35,12 @@ func BuildControllers(
|
|||
pluginConfigs[p.GetName()],
|
||||
tp,
|
||||
navController,
|
||||
statuslineConfig,
|
||||
)
|
||||
continue
|
||||
}
|
||||
if dp, ok := p.(*plugin.DokiPlugin); ok {
|
||||
pluginControllers[p.GetName()] = controller.NewDokiController(dp, navController)
|
||||
pluginControllers[p.GetName()] = controller.NewDokiController(dp, navController, statuslineConfig)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ func Bootstrap(tikiSkillContent, dokiSkillContent string) (*Result, error) {
|
|||
taskStore,
|
||||
plugins,
|
||||
pluginConfigs,
|
||||
statuslineConfig,
|
||||
)
|
||||
|
||||
// Phase 8: Input routing
|
||||
|
|
@ -126,6 +127,7 @@ func Bootstrap(tikiSkillContent, dokiSkillContent string) (*Result, error) {
|
|||
controllers.Task,
|
||||
controllers.Plugins,
|
||||
taskStore,
|
||||
statuslineConfig,
|
||||
)
|
||||
|
||||
// Phase 9: View factory and layout
|
||||
|
|
|
|||
10
model/message_level.go
Normal file
10
model/message_level.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package model
|
||||
|
||||
// MessageLevel identifies the severity of a statusline message.
|
||||
// Determines which color pair is used when rendering the message.
|
||||
type MessageLevel string
|
||||
|
||||
const (
|
||||
MessageLevelInfo MessageLevel = "info"
|
||||
MessageLevelError MessageLevel = "error"
|
||||
)
|
||||
|
|
@ -19,6 +19,7 @@ type StatuslineConfig struct {
|
|||
|
||||
// right section: transient message
|
||||
message string
|
||||
level MessageLevel
|
||||
autoHide bool // if true, entire bar hides on next keypress
|
||||
|
||||
// visibility
|
||||
|
|
@ -35,6 +36,7 @@ func NewStatuslineConfig() *StatuslineConfig {
|
|||
leftStats: make(map[string]StatValue),
|
||||
viewStats: make(map[string]StatValue),
|
||||
rightViewStats: make(map[string]StatValue),
|
||||
level: MessageLevelInfo,
|
||||
visible: true,
|
||||
listeners: make(map[int]func()),
|
||||
nextListener: 1,
|
||||
|
|
@ -120,23 +122,24 @@ func (sc *StatuslineConfig) SetViewStats(stats map[string]StatValue) {
|
|||
sc.notifyListeners()
|
||||
}
|
||||
|
||||
// SetMessage sets the right-section message. If autoHide is true, the entire
|
||||
// statusline will hide on the next keypress (via DismissAutoHide).
|
||||
// SetMessage sets the right-section message with a severity level. If autoHide
|
||||
// is true, the entire statusline will hide on the next keypress (via DismissAutoHide).
|
||||
// Setting a message makes the statusline visible.
|
||||
func (sc *StatuslineConfig) SetMessage(text string, autoHide bool) {
|
||||
func (sc *StatuslineConfig) SetMessage(text string, level MessageLevel, autoHide bool) {
|
||||
sc.mu.Lock()
|
||||
sc.message = text
|
||||
sc.level = level
|
||||
sc.autoHide = autoHide
|
||||
sc.visible = true
|
||||
sc.mu.Unlock()
|
||||
sc.notifyListeners()
|
||||
}
|
||||
|
||||
// GetMessage returns the current message and whether auto-hide is active
|
||||
func (sc *StatuslineConfig) GetMessage() (string, bool) {
|
||||
// GetMessage returns the current message, its level, and whether auto-hide is active
|
||||
func (sc *StatuslineConfig) GetMessage() (string, MessageLevel, bool) {
|
||||
sc.mu.RLock()
|
||||
defer sc.mu.RUnlock()
|
||||
return sc.message, sc.autoHide
|
||||
return sc.message, sc.level, sc.autoHide
|
||||
}
|
||||
|
||||
// ClearMessage clears the right-section message
|
||||
|
|
@ -144,6 +147,7 @@ func (sc *StatuslineConfig) ClearMessage() {
|
|||
sc.mu.Lock()
|
||||
changed := sc.message != ""
|
||||
sc.message = ""
|
||||
sc.level = MessageLevelInfo
|
||||
sc.autoHide = false
|
||||
sc.mu.Unlock()
|
||||
if changed {
|
||||
|
|
@ -162,6 +166,7 @@ func (sc *StatuslineConfig) DismissAutoHide() bool {
|
|||
}
|
||||
sc.autoHide = false
|
||||
sc.message = ""
|
||||
sc.level = MessageLevelInfo
|
||||
sc.visible = false
|
||||
sc.mu.Unlock()
|
||||
sc.notifyListeners()
|
||||
|
|
|
|||
|
|
@ -20,10 +20,13 @@ func TestNewStatuslineConfig(t *testing.T) {
|
|||
t.Error("initial GetLeftStats() should be empty")
|
||||
}
|
||||
|
||||
msg, autoHide := sc.GetMessage()
|
||||
msg, level, autoHide := sc.GetMessage()
|
||||
if msg != "" {
|
||||
t.Errorf("initial message = %q, want empty", msg)
|
||||
}
|
||||
if level != MessageLevelInfo {
|
||||
t.Errorf("initial level = %q, want %q", level, MessageLevelInfo)
|
||||
}
|
||||
if autoHide {
|
||||
t.Error("initial autoHide = true, want false")
|
||||
}
|
||||
|
|
@ -101,12 +104,15 @@ func TestStatuslineConfig_ClearViewStats(t *testing.T) {
|
|||
func TestStatuslineConfig_Message(t *testing.T) {
|
||||
sc := NewStatuslineConfig()
|
||||
|
||||
sc.SetMessage("error occurred", false)
|
||||
sc.SetMessage("error occurred", MessageLevelError, false)
|
||||
|
||||
msg, autoHide := sc.GetMessage()
|
||||
msg, level, autoHide := sc.GetMessage()
|
||||
if msg != "error occurred" {
|
||||
t.Errorf("message = %q, want %q", msg, "error occurred")
|
||||
}
|
||||
if level != MessageLevelError {
|
||||
t.Errorf("level = %q, want %q", level, MessageLevelError)
|
||||
}
|
||||
if autoHide {
|
||||
t.Error("autoHide = true, want false")
|
||||
}
|
||||
|
|
@ -115,9 +121,9 @@ func TestStatuslineConfig_Message(t *testing.T) {
|
|||
func TestStatuslineConfig_MessageAutoHide(t *testing.T) {
|
||||
sc := NewStatuslineConfig()
|
||||
|
||||
sc.SetMessage("transient error", true)
|
||||
sc.SetMessage("transient error", MessageLevelInfo, true)
|
||||
|
||||
msg, autoHide := sc.GetMessage()
|
||||
msg, _, autoHide := sc.GetMessage()
|
||||
if msg != "transient error" {
|
||||
t.Errorf("message = %q, want %q", msg, "transient error")
|
||||
}
|
||||
|
|
@ -134,7 +140,7 @@ func TestStatuslineConfig_MessageMakesVisible(t *testing.T) {
|
|||
t.Fatal("precondition: should be invisible")
|
||||
}
|
||||
|
||||
sc.SetMessage("hello", false)
|
||||
sc.SetMessage("hello", MessageLevelInfo, false)
|
||||
if !sc.IsVisible() {
|
||||
t.Error("SetMessage should make statusline visible")
|
||||
}
|
||||
|
|
@ -143,13 +149,16 @@ func TestStatuslineConfig_MessageMakesVisible(t *testing.T) {
|
|||
func TestStatuslineConfig_ClearMessage(t *testing.T) {
|
||||
sc := NewStatuslineConfig()
|
||||
|
||||
sc.SetMessage("error", true)
|
||||
sc.SetMessage("error", MessageLevelError, true)
|
||||
sc.ClearMessage()
|
||||
|
||||
msg, autoHide := sc.GetMessage()
|
||||
msg, level, autoHide := sc.GetMessage()
|
||||
if msg != "" {
|
||||
t.Errorf("message after clear = %q, want empty", msg)
|
||||
}
|
||||
if level != MessageLevelInfo {
|
||||
t.Errorf("level after clear = %q, want %q", level, MessageLevelInfo)
|
||||
}
|
||||
if autoHide {
|
||||
t.Error("autoHide should be false after ClearMessage")
|
||||
}
|
||||
|
|
@ -178,7 +187,7 @@ func TestStatuslineConfig_DismissAutoHide(t *testing.T) {
|
|||
}
|
||||
|
||||
// set auto-hide message
|
||||
sc.SetMessage("will hide", true)
|
||||
sc.SetMessage("will hide", MessageLevelInfo, true)
|
||||
if !sc.IsVisible() {
|
||||
t.Fatal("precondition: should be visible after SetMessage")
|
||||
}
|
||||
|
|
@ -194,10 +203,13 @@ func TestStatuslineConfig_DismissAutoHide(t *testing.T) {
|
|||
}
|
||||
|
||||
// message should be cleared
|
||||
msg, autoHide := sc.GetMessage()
|
||||
msg, level, autoHide := sc.GetMessage()
|
||||
if msg != "" {
|
||||
t.Errorf("message after dismiss = %q, want empty", msg)
|
||||
}
|
||||
if level != MessageLevelInfo {
|
||||
t.Errorf("level after dismiss = %q, want %q", level, MessageLevelInfo)
|
||||
}
|
||||
if autoHide {
|
||||
t.Error("autoHide should be false after dismiss")
|
||||
}
|
||||
|
|
@ -214,7 +226,7 @@ func TestStatuslineConfig_DismissAutoHideNotifiesListeners(t *testing.T) {
|
|||
callCount := 0
|
||||
sc.AddListener(func() { callCount++ })
|
||||
|
||||
sc.SetMessage("hide me", true)
|
||||
sc.SetMessage("hide me", MessageLevelInfo, true)
|
||||
callCount = 0 // reset after SetMessage notification
|
||||
|
||||
sc.DismissAutoHide()
|
||||
|
|
@ -274,7 +286,7 @@ func TestStatuslineConfig_ListenerNotification(t *testing.T) {
|
|||
{"SetLeftStat", func() { sc.SetLeftStat("k", "v", 1) }},
|
||||
{"SetViewStat", func() { sc.SetViewStat("k", "v", 1) }},
|
||||
{"ClearViewStats", func() { sc.ClearViewStats() }},
|
||||
{"SetMessage", func() { sc.SetMessage("msg", false) }},
|
||||
{"SetMessage", func() { sc.SetMessage("msg", MessageLevelInfo, false) }},
|
||||
{"SetVisible", func() { sc.SetVisible(false); sc.SetVisible(true) }},
|
||||
}
|
||||
|
||||
|
|
@ -368,7 +380,7 @@ func TestStatuslineConfig_ConcurrentAccess(t *testing.T) {
|
|||
|
||||
go func() {
|
||||
for i := range 50 {
|
||||
sc.SetMessage("msg", i%2 == 0)
|
||||
sc.SetMessage("msg", MessageLevelInfo, i%2 == 0)
|
||||
if i%5 == 0 {
|
||||
sc.ClearMessage()
|
||||
}
|
||||
|
|
@ -398,7 +410,7 @@ func TestStatuslineConfig_ConcurrentAccess(t *testing.T) {
|
|||
for range 100 {
|
||||
_ = sc.GetLeftStats()
|
||||
_ = sc.GetRightViewStats()
|
||||
_, _ = sc.GetMessage()
|
||||
_, _, _ = sc.GetMessage()
|
||||
_ = sc.IsVisible()
|
||||
}
|
||||
done <- true
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ type TestApp struct {
|
|||
PluginControllers map[string]controller.PluginControllerInterface
|
||||
PluginDefs []plugin.Plugin
|
||||
taskController *controller.TaskController
|
||||
statuslineConfig *model.StatuslineConfig
|
||||
headerConfig *model.HeaderConfig
|
||||
layoutModel *model.LayoutModel
|
||||
}
|
||||
|
|
@ -82,8 +83,9 @@ func NewTestApp(t *testing.T) *TestApp {
|
|||
app.SetScreen(screen)
|
||||
|
||||
// 5. Initialize Controller Layer
|
||||
statuslineConfig := model.NewStatuslineConfig()
|
||||
navController := controller.NewNavigationController(app)
|
||||
taskController := controller.NewTaskController(taskStore, navController)
|
||||
taskController := controller.NewTaskController(taskStore, navController, statuslineConfig)
|
||||
// Empty plugin controllers map for tests (no plugins configured by default)
|
||||
pluginControllers := make(map[string]controller.PluginControllerInterface)
|
||||
inputRouter := controller.NewInputRouter(
|
||||
|
|
@ -91,6 +93,7 @@ func NewTestApp(t *testing.T) *TestApp {
|
|||
taskController,
|
||||
pluginControllers,
|
||||
taskStore,
|
||||
statuslineConfig,
|
||||
)
|
||||
|
||||
// 6. Initialize View Layer
|
||||
|
|
@ -98,7 +101,6 @@ func NewTestApp(t *testing.T) *TestApp {
|
|||
|
||||
// 7. Create header widget, statusline, and RootLayout
|
||||
headerWidget := header.NewHeaderWidget(headerConfig)
|
||||
statuslineConfig := model.NewStatuslineConfig()
|
||||
statuslineWidget := statusline.NewStatuslineWidget(statuslineConfig)
|
||||
rootLayout := view.NewRootLayout(view.RootLayoutOpts{
|
||||
Header: headerWidget,
|
||||
|
|
@ -153,17 +155,18 @@ func NewTestApp(t *testing.T) *TestApp {
|
|||
// Note: Do NOT call app.Run() - we use app.Draw() + screen.Show() for synchronous testing
|
||||
|
||||
ta := &TestApp{
|
||||
App: app,
|
||||
Screen: screen,
|
||||
RootLayout: rootLayout,
|
||||
TaskStore: taskStore,
|
||||
NavController: navController,
|
||||
InputRouter: inputRouter,
|
||||
TaskDir: taskDir,
|
||||
t: t,
|
||||
taskController: taskController,
|
||||
headerConfig: headerConfig,
|
||||
layoutModel: layoutModel,
|
||||
App: app,
|
||||
Screen: screen,
|
||||
RootLayout: rootLayout,
|
||||
TaskStore: taskStore,
|
||||
NavController: navController,
|
||||
InputRouter: inputRouter,
|
||||
TaskDir: taskDir,
|
||||
t: t,
|
||||
taskController: taskController,
|
||||
statuslineConfig: statuslineConfig,
|
||||
headerConfig: headerConfig,
|
||||
layoutModel: layoutModel,
|
||||
}
|
||||
|
||||
// 11. Auto-load plugins since all views are now plugins
|
||||
|
|
@ -328,11 +331,11 @@ func (ta *TestApp) LoadPlugins() error {
|
|||
}
|
||||
pc.SetLaneLayout(columns, widths)
|
||||
pluginControllers[p.GetName()] = controller.NewPluginController(
|
||||
ta.TaskStore, pc, tp, ta.NavController,
|
||||
ta.TaskStore, pc, tp, ta.NavController, ta.statuslineConfig,
|
||||
)
|
||||
} else if dp, ok := p.(*plugin.DokiPlugin); ok {
|
||||
pluginControllers[p.GetName()] = controller.NewDokiController(
|
||||
dp, ta.NavController,
|
||||
dp, ta.NavController, ta.statuslineConfig,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -361,6 +364,7 @@ func (ta *TestApp) LoadPlugins() error {
|
|||
ta.taskController,
|
||||
pluginControllers,
|
||||
ta.TaskStore,
|
||||
ta.statuslineConfig,
|
||||
)
|
||||
|
||||
// Update global input capture to handle plugin switching keys
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ func (sw *StatuslineWidget) render(width int) {
|
|||
rightStatsLen := segmentsVisibleLen(rightSegments)
|
||||
|
||||
// message (between left and right)
|
||||
msg, _ := sw.config.GetMessage()
|
||||
msgRendered := sw.renderMessage(msg, colors)
|
||||
msg, level, _ := sw.config.GetMessage()
|
||||
msgRendered := sw.renderMessage(msg, level, colors)
|
||||
msgLen := visibleLen(msg)
|
||||
|
||||
// pad to fill the width with the fill background color
|
||||
|
|
@ -104,7 +104,7 @@ func (sw *StatuslineWidget) render(width int) {
|
|||
}
|
||||
padding := fmt.Sprintf("[-:%s]%s[-:-]", colors.StatuslineFillBg, strings.Repeat(" ", padLen))
|
||||
|
||||
sw.SetText(left + padding + msgRendered + rightStats)
|
||||
sw.SetText(left + msgRendered + padding + rightStats)
|
||||
}
|
||||
|
||||
// renderLeftSegments builds the powerline left section.
|
||||
|
|
@ -173,12 +173,23 @@ func segmentColors(index int, colors *config.ColorConfig) (string, string) {
|
|||
return colors.StatuslineBg, colors.StatuslineFg
|
||||
}
|
||||
|
||||
// renderMessage builds the message section
|
||||
func (sw *StatuslineWidget) renderMessage(msg string, colors *config.ColorConfig) string {
|
||||
// renderMessage builds the message section with level-specific colors
|
||||
func (sw *StatuslineWidget) renderMessage(msg string, level model.MessageLevel, colors *config.ColorConfig) string {
|
||||
if msg == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("[%s:%s] %s [-:-]", colors.StatuslineMessageFg, colors.StatuslineMessageBg, msg)
|
||||
fg, bg := messageColors(level, colors)
|
||||
return fmt.Sprintf("[%s:%s] %s [-:-]", fg, bg, msg)
|
||||
}
|
||||
|
||||
// messageColors returns (fg, bg) for the given message level
|
||||
func messageColors(level model.MessageLevel, colors *config.ColorConfig) (string, string) {
|
||||
switch level {
|
||||
case model.MessageLevelError:
|
||||
return colors.StatuslineErrorFg, colors.StatuslineErrorBg
|
||||
default:
|
||||
return colors.StatuslineInfoFg, colors.StatuslineInfoBg
|
||||
}
|
||||
}
|
||||
|
||||
// sortedSegments converts a stat map to a sorted slice of segments
|
||||
|
|
|
|||
|
|
@ -11,13 +11,15 @@ import (
|
|||
|
||||
func testColors() *config.ColorConfig {
|
||||
return &config.ColorConfig{
|
||||
StatuslineBg: "#normal_bg",
|
||||
StatuslineFg: "#normal_fg",
|
||||
StatuslineAccentBg: "#accent_bg",
|
||||
StatuslineAccentFg: "#accent_fg",
|
||||
StatuslineMessageFg: "#msg_fg",
|
||||
StatuslineMessageBg: "#msg_bg",
|
||||
StatuslineFillBg: "#fill_bg",
|
||||
StatuslineBg: "#normal_bg",
|
||||
StatuslineFg: "#normal_fg",
|
||||
StatuslineAccentBg: "#accent_bg",
|
||||
StatuslineAccentFg: "#accent_fg",
|
||||
StatuslineInfoFg: "#info_fg",
|
||||
StatuslineInfoBg: "#info_bg",
|
||||
StatuslineErrorFg: "#error_fg",
|
||||
StatuslineErrorBg: "#error_bg",
|
||||
StatuslineFillBg: "#fill_bg",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -217,19 +219,32 @@ func TestRenderRightSegments_threeSegments(t *testing.T) {
|
|||
|
||||
func TestRenderMessage_empty(t *testing.T) {
|
||||
sw := newTestWidget()
|
||||
result := sw.renderMessage("", testColors())
|
||||
result := sw.renderMessage("", model.MessageLevelInfo, testColors())
|
||||
if result != "" {
|
||||
t.Errorf("got %q, want empty", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderMessage_nonEmpty(t *testing.T) {
|
||||
func TestRenderMessage_info(t *testing.T) {
|
||||
sw := newTestWidget()
|
||||
colors := testColors()
|
||||
result := sw.renderMessage("error occurred", colors)
|
||||
result := sw.renderMessage("task saved", model.MessageLevelInfo, colors)
|
||||
|
||||
if !strings.Contains(result, "[#msg_fg:#msg_bg] error occurred ") {
|
||||
t.Errorf("message should use message colors, got %q", result)
|
||||
if !strings.Contains(result, "[#info_fg:#info_bg] task saved ") {
|
||||
t.Errorf("info message should use info colors, got %q", result)
|
||||
}
|
||||
if !strings.HasSuffix(result, "[-:-]") {
|
||||
t.Errorf("should end with color reset, got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderMessage_error(t *testing.T) {
|
||||
sw := newTestWidget()
|
||||
colors := testColors()
|
||||
result := sw.renderMessage("validation failed", model.MessageLevelError, colors)
|
||||
|
||||
if !strings.Contains(result, "[#error_fg:#error_bg] validation failed ") {
|
||||
t.Errorf("error message should use error colors, got %q", result)
|
||||
}
|
||||
if !strings.HasSuffix(result, "[-:-]") {
|
||||
t.Errorf("should end with color reset, got %q", result)
|
||||
|
|
|
|||
|
|
@ -239,6 +239,14 @@ func (ev *TaskEditView) MoveRecurrencePartRight() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// IsRecurrenceValueFocused returns true when the recurrence field's value part is active.
|
||||
func (ev *TaskEditView) IsRecurrenceValueFocused() bool {
|
||||
if ev.focusedField != model.EditFieldRecurrence || ev.recurrenceInput == nil {
|
||||
return false
|
||||
}
|
||||
return ev.recurrenceInput.IsValueFocused()
|
||||
}
|
||||
|
||||
// UpdateHeaderForField updates the registry with field-specific actions
|
||||
func (ev *TaskEditView) UpdateHeaderForField(field model.EditField) {
|
||||
if ev.descOnly {
|
||||
|
|
|
|||
Loading…
Reference in a new issue