diff --git a/.doc/doki/doc/plugin.md b/.doc/doki/doc/plugin.md index dc04cb9..b8bc717 100644 --- a/.doc/doki/doc/plugin.md +++ b/.doc/doki/doc/plugin.md @@ -8,7 +8,7 @@ how Backlog is defined: foreground: "#5fff87" background: "#005f00" key: "F3" - panes: + lanes: - name: Backlog columns: 4 filter: status = 'backlog' @@ -18,7 +18,7 @@ how Backlog is defined: action: status = 'ready' sort: Priority, ID ``` -that translates to - show all tikis in the status `backlog`, sort by priority and then by ID arranged visually in 4 columns in a single pane. +that translates to - show all tikis in the status `backlog`, sort by priority and then by ID arranged visually in 4 columns in a single lane. The `actions` section defines a keyboard shortcut `b` that moves the selected tiki to the board by setting its status to `ready` You define the name, caption colors, hotkey, tiki filter and sorting. Save this into a yaml file and add this line: @@ -44,12 +44,12 @@ Likewise the documentation is just a plugin: that translates to - show `index.md` file located under `.doc/doki` installed in the same way -## Multi-pane plugin +## Multi-lane plugin -Backlog is a pretty simple plugin in that it displays all tikis in a single pane. Multi-pane tiki plugins offer functionality -similar to that of the board. You can define multiple panes per view and move tikis around with Shift-Left/Shift-Right -much like in the board. You can create a multi-pane plugin by defining multiple panes in its definition and assigning -actions to each pane. An action defines what happens when you move a tiki into the pane. Here is a multi-pane plugin +Backlog is a pretty simple plugin in that it displays all tikis in a single lane. Multi-lane tiki plugins offer functionality +similar to that of the board. You can define multiple lanes per view and move tikis around with Shift-Left/Shift-Right +much like in the board. You can create a multi-lane plugin by defining multiple lanes in its definition and assigning +actions to each lane. An action defines what happens when you move a tiki into the lane. Here is a multi-lane plugin definition that roughly mimics the board: ```yaml @@ -58,7 +58,7 @@ foreground: "#5fff87" background: "#005f00" key: "F4" sort: Priority, Title -panes: +lanes: - name: Ready columns: 1 filter: status = 'ready' @@ -79,7 +79,7 @@ panes: ## Plugin actions -In addition to pane actions that trigger when moving tikis between panes, you can define plugin-level actions +In addition to lane actions that trigger when moving tikis between lanes, you can define plugin-level actions that apply to the currently selected tiki via a keyboard shortcut. These shortcuts are displayed in the header when the plugin is active. ```yaml @@ -95,14 +95,14 @@ actions: Each action has: - `key` - a single printable character used as the keyboard shortcut - `label` - description shown in the header -- `action` - an action expression (same syntax as pane actions, see below) +- `action` - an action expression (same syntax as lane actions, see below) When the shortcut key is pressed, the action is applied to the currently selected tiki. For example, pressing `b` in the Backlog plugin changes the selected tiki's status to `ready`, effectively moving it to the board. ## Action expression -The `action: status = 'backlog'` statement in a plugin is an action to be run when a tiki is moved into the pane. Here `=` +The `action: status = 'backlog'` statement in a plugin is an action to be run when a tiki is moved into the lane. Here `=` means `assign` so status is assigned `backlog` when the tiki is moved. Likewise you can manipulate tags using `+-` (add) or `-=` (remove) expressions. For example, `tags += [idea, UI]` adds `idea` and `UI` tags to a tiki diff --git a/controller/plugin.go b/controller/plugin.go index d7179cf..bb7f350 100644 --- a/controller/plugin.go +++ b/controller/plugin.go @@ -133,13 +133,13 @@ func (pc *PluginController) HandleAction(actionID ActionID) bool { } func (pc *PluginController) handleNav(direction string) bool { - pane := pc.pluginConfig.GetSelectedPane() - tasks := pc.GetFilteredTasksForPane(pane) + lane := pc.pluginConfig.GetSelectedLane() + tasks := pc.GetFilteredTasksForLane(lane) if direction == "left" || direction == "right" { if pc.pluginConfig.MoveSelection(direction, len(tasks)) { return true } - return pc.handlePaneSwitch(direction) + return pc.handleLaneSwitch(direction) } return pc.pluginConfig.MoveSelection(direction, len(tasks)) } @@ -156,38 +156,38 @@ func (pc *PluginController) handleOpenTask() bool { return true } -func (pc *PluginController) handlePaneSwitch(direction string) bool { - currentPane := pc.pluginConfig.GetSelectedPane() - nextPane := currentPane +func (pc *PluginController) handleLaneSwitch(direction string) bool { + currentLane := pc.pluginConfig.GetSelectedLane() + nextLane := currentLane switch direction { case "left": - nextPane-- + nextLane-- case "right": - nextPane++ + nextLane++ default: return false } - for nextPane >= 0 && nextPane < len(pc.pluginDef.Panes) { - tasks := pc.GetFilteredTasksForPane(nextPane) + for nextLane >= 0 && nextLane < len(pc.pluginDef.Lanes) { + tasks := pc.GetFilteredTasksForLane(nextLane) if len(tasks) > 0 { - pc.pluginConfig.SetSelectedPane(nextPane) + pc.pluginConfig.SetSelectedLane(nextLane) // Select the task at top of viewport (scroll offset) rather than keeping stale index - scrollOffset := pc.pluginConfig.GetScrollOffsetForPane(nextPane) + scrollOffset := pc.pluginConfig.GetScrollOffsetForLane(nextLane) if scrollOffset >= len(tasks) { scrollOffset = len(tasks) - 1 } if scrollOffset < 0 { scrollOffset = 0 } - pc.pluginConfig.SetSelectedIndexForPane(nextPane, scrollOffset) + pc.pluginConfig.SetSelectedIndexForLane(nextLane, scrollOffset) return true } switch direction { case "left": - nextPane-- + nextLane-- case "right": - nextPane++ + nextLane++ } } return false @@ -249,7 +249,7 @@ func (pc *PluginController) handlePluginAction(r rune) bool { } currentUser := getCurrentUserName(pc.taskStore) - updated, err := plugin.ApplyPaneAction(taskItem, pa.Action, currentUser) + updated, err := plugin.ApplyLaneAction(taskItem, pa.Action, currentUser) if err != nil { slog.Error("failed to apply plugin action", "task_id", taskID, "key", string(r), "error", err) return false @@ -271,13 +271,13 @@ func (pc *PluginController) handleMoveTask(offset int) bool { return false } - if pc.pluginDef == nil || len(pc.pluginDef.Panes) == 0 { + if pc.pluginDef == nil || len(pc.pluginDef.Lanes) == 0 { return false } - currentPane := pc.pluginConfig.GetSelectedPane() - targetPane := currentPane + offset - if targetPane < 0 || targetPane >= len(pc.pluginDef.Panes) { + currentLane := pc.pluginConfig.GetSelectedLane() + targetLane := currentLane + offset + if targetLane < 0 || targetLane >= len(pc.pluginDef.Lanes) { return false } @@ -287,19 +287,19 @@ func (pc *PluginController) handleMoveTask(offset int) bool { } currentUser := getCurrentUserName(pc.taskStore) - updated, err := plugin.ApplyPaneAction(taskItem, pc.pluginDef.Panes[targetPane].Action, currentUser) + updated, err := plugin.ApplyLaneAction(taskItem, pc.pluginDef.Lanes[targetLane].Action, currentUser) if err != nil { - slog.Error("failed to apply pane action", "task_id", taskID, "error", err) + slog.Error("failed to apply lane action", "task_id", taskID, "error", err) return false } if err := pc.taskStore.UpdateTask(updated); err != nil { - slog.Error("failed to update task after pane move", "task_id", taskID, "error", err) + slog.Error("failed to update task after lane move", "task_id", taskID, "error", err) return false } pc.ensureSearchResultIncludesTask(updated) - pc.selectTaskInPane(targetPane, taskID) + pc.selectTaskInLane(targetLane, taskID) return true } @@ -313,7 +313,7 @@ func (pc *PluginController) HandleSearch(query string) { // Save current position pc.pluginConfig.SavePreSearchState() - // Search across all tasks; pane membership is decided per pane + // Search across all tasks; lane membership is decided per lane results := pc.taskStore.Search(query, nil) if len(results) == 0 { pc.pluginConfig.SetSearchResults([]task.SearchResult{}, query) @@ -321,28 +321,28 @@ func (pc *PluginController) HandleSearch(query string) { } pc.pluginConfig.SetSearchResults(results, query) - if pc.selectFirstNonEmptyPane() { + if pc.selectFirstNonEmptyLane() { return } } // getSelectedTaskID returns the ID of the currently selected task func (pc *PluginController) getSelectedTaskID() string { - pane := pc.pluginConfig.GetSelectedPane() - tasks := pc.GetFilteredTasksForPane(pane) - idx := pc.pluginConfig.GetSelectedIndexForPane(pane) + lane := pc.pluginConfig.GetSelectedLane() + tasks := pc.GetFilteredTasksForLane(lane) + idx := pc.pluginConfig.GetSelectedIndexForLane(lane) if idx < 0 || idx >= len(tasks) { return "" } return tasks[idx].ID } -// GetFilteredTasksForPane returns tasks filtered and sorted for a specific pane. -func (pc *PluginController) GetFilteredTasksForPane(pane int) []*task.Task { +// GetFilteredTasksForLane returns tasks filtered and sorted for a specific lane. +func (pc *PluginController) GetFilteredTasksForLane(lane int) []*task.Task { if pc.pluginDef == nil { return nil } - if pane < 0 || pane >= len(pc.pluginDef.Panes) { + if lane < 0 || lane >= len(pc.pluginDef.Lanes) { return nil } @@ -359,8 +359,8 @@ func (pc *PluginController) GetFilteredTasksForPane(pane int) []*task.Task { // Apply filter var filtered []*task.Task for _, task := range allTasks { - paneFilter := pc.pluginDef.Panes[pane].Filter - if paneFilter == nil || paneFilter.Evaluate(task, now, currentUser) { + laneFilter := pc.pluginDef.Lanes[lane].Filter + if laneFilter == nil || laneFilter.Evaluate(task, now, currentUser) { filtered = append(filtered, task) } } @@ -381,12 +381,12 @@ func (pc *PluginController) GetFilteredTasksForPane(pane int) []*task.Task { return filtered } -func (pc *PluginController) selectTaskInPane(pane int, taskID string) { - if pane < 0 || pane >= len(pc.pluginDef.Panes) { +func (pc *PluginController) selectTaskInLane(lane int, taskID string) { + if lane < 0 || lane >= len(pc.pluginDef.Lanes) { return } - tasks := pc.GetFilteredTasksForPane(pane) + tasks := pc.GetFilteredTasksForLane(lane) targetIndex := 0 for i, task := range tasks { if task.ID == taskID { @@ -395,33 +395,33 @@ func (pc *PluginController) selectTaskInPane(pane int, taskID string) { } } - pc.pluginConfig.SetSelectedPane(pane) - pc.pluginConfig.SetSelectedIndexForPane(pane, targetIndex) + pc.pluginConfig.SetSelectedLane(lane) + pc.pluginConfig.SetSelectedIndexForLane(lane, targetIndex) } -func (pc *PluginController) selectFirstNonEmptyPane() bool { - for pane := range pc.pluginDef.Panes { - tasks := pc.GetFilteredTasksForPane(pane) +func (pc *PluginController) selectFirstNonEmptyLane() bool { + for lane := range pc.pluginDef.Lanes { + tasks := pc.GetFilteredTasksForLane(lane) if len(tasks) > 0 { - pc.pluginConfig.SetSelectedPaneAndIndex(pane, 0) + pc.pluginConfig.SetSelectedLaneAndIndex(lane, 0) return true } } return false } -func (pc *PluginController) EnsureFirstNonEmptyPaneSelection() bool { +func (pc *PluginController) EnsureFirstNonEmptyLaneSelection() bool { if pc.pluginDef == nil { return false } - currentPane := pc.pluginConfig.GetSelectedPane() - if currentPane >= 0 && currentPane < len(pc.pluginDef.Panes) { - tasks := pc.GetFilteredTasksForPane(currentPane) + currentLane := pc.pluginConfig.GetSelectedLane() + if currentLane >= 0 && currentLane < len(pc.pluginDef.Lanes) { + tasks := pc.GetFilteredTasksForLane(currentLane) if len(tasks) > 0 { return false } } - return pc.selectFirstNonEmptyPane() + return pc.selectFirstNonEmptyLane() } func (pc *PluginController) ensureSearchResultIncludesTask(updated *task.Task) { diff --git a/controller/plugin_selection_test.go b/controller/plugin_selection_test.go index 57920d3..d90e82b 100644 --- a/controller/plugin_selection_test.go +++ b/controller/plugin_selection_test.go @@ -11,7 +11,7 @@ import ( "github.com/boolean-maybe/tiki/task" ) -func TestEnsureFirstNonEmptyPaneSelectionSelectsFirstTask(t *testing.T) { +func TestEnsureFirstNonEmptyLaneSelectionSelectsFirstTask(t *testing.T) { taskStore := store.NewInMemoryStore() if err := taskStore.CreateTask(&task.Task{ ID: "T-1", @@ -43,28 +43,28 @@ func TestEnsureFirstNonEmptyPaneSelectionSelectsFirstTask(t *testing.T) { BasePlugin: plugin.BasePlugin{ Name: "TestPlugin", }, - Panes: []plugin.TikiPane{ + Lanes: []plugin.TikiLane{ {Name: "Empty", Columns: 1, Filter: emptyFilter}, {Name: "Todo", Columns: 1, Filter: todoFilter}, }, } pluginConfig := model.NewPluginConfig("TestPlugin") - pluginConfig.SetPaneLayout([]int{1, 1}) - pluginConfig.SetSelectedPane(0) - pluginConfig.SetSelectedIndexForPane(0, 1) + pluginConfig.SetLaneLayout([]int{1, 1}) + pluginConfig.SetSelectedLane(0) + pluginConfig.SetSelectedIndexForLane(0, 1) pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil) - pc.EnsureFirstNonEmptyPaneSelection() + pc.EnsureFirstNonEmptyLaneSelection() - if pluginConfig.GetSelectedPane() != 1 { - t.Fatalf("expected selected pane 1, got %d", pluginConfig.GetSelectedPane()) + if pluginConfig.GetSelectedLane() != 1 { + t.Fatalf("expected selected lane 1, got %d", pluginConfig.GetSelectedLane()) } - if pluginConfig.GetSelectedIndexForPane(1) != 0 { - t.Fatalf("expected selected index 0, got %d", pluginConfig.GetSelectedIndexForPane(1)) + if pluginConfig.GetSelectedIndexForLane(1) != 0 { + t.Fatalf("expected selected index 0, got %d", pluginConfig.GetSelectedIndexForLane(1)) } } -func TestEnsureFirstNonEmptyPaneSelectionKeepsCurrentPane(t *testing.T) { +func TestEnsureFirstNonEmptyLaneSelectionKeepsCurrentLane(t *testing.T) { taskStore := store.NewInMemoryStore() if err := taskStore.CreateTask(&task.Task{ ID: "T-1", @@ -84,28 +84,28 @@ func TestEnsureFirstNonEmptyPaneSelectionKeepsCurrentPane(t *testing.T) { BasePlugin: plugin.BasePlugin{ Name: "TestPlugin", }, - Panes: []plugin.TikiPane{ + Lanes: []plugin.TikiLane{ {Name: "First", Columns: 1, Filter: todoFilter}, {Name: "Second", Columns: 1, Filter: todoFilter}, }, } pluginConfig := model.NewPluginConfig("TestPlugin") - pluginConfig.SetPaneLayout([]int{1, 1}) - pluginConfig.SetSelectedPane(1) - pluginConfig.SetSelectedIndexForPane(1, 0) + pluginConfig.SetLaneLayout([]int{1, 1}) + pluginConfig.SetSelectedLane(1) + pluginConfig.SetSelectedIndexForLane(1, 0) pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil) - pc.EnsureFirstNonEmptyPaneSelection() + pc.EnsureFirstNonEmptyLaneSelection() - if pluginConfig.GetSelectedPane() != 1 { - t.Fatalf("expected selected pane 1, got %d", pluginConfig.GetSelectedPane()) + if pluginConfig.GetSelectedLane() != 1 { + t.Fatalf("expected selected lane 1, got %d", pluginConfig.GetSelectedLane()) } - if pluginConfig.GetSelectedIndexForPane(1) != 0 { - t.Fatalf("expected selected index 0, got %d", pluginConfig.GetSelectedIndexForPane(1)) + if pluginConfig.GetSelectedIndexForLane(1) != 0 { + t.Fatalf("expected selected index 0, got %d", pluginConfig.GetSelectedIndexForLane(1)) } } -func TestEnsureFirstNonEmptyPaneSelectionNoTasks(t *testing.T) { +func TestEnsureFirstNonEmptyLaneSelectionNoTasks(t *testing.T) { taskStore := store.NewInMemoryStore() emptyFilter, err := filter.ParseFilter("status = 'done'") if err != nil { @@ -116,30 +116,30 @@ func TestEnsureFirstNonEmptyPaneSelectionNoTasks(t *testing.T) { BasePlugin: plugin.BasePlugin{ Name: "TestPlugin", }, - Panes: []plugin.TikiPane{ + Lanes: []plugin.TikiLane{ {Name: "Empty", Columns: 1, Filter: emptyFilter}, {Name: "StillEmpty", Columns: 1, Filter: emptyFilter}, }, } pluginConfig := model.NewPluginConfig("TestPlugin") - pluginConfig.SetPaneLayout([]int{1, 1}) - pluginConfig.SetSelectedPane(1) - pluginConfig.SetSelectedIndexForPane(1, 2) + pluginConfig.SetLaneLayout([]int{1, 1}) + pluginConfig.SetSelectedLane(1) + pluginConfig.SetSelectedIndexForLane(1, 2) pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil) - pc.EnsureFirstNonEmptyPaneSelection() + pc.EnsureFirstNonEmptyLaneSelection() - if pluginConfig.GetSelectedPane() != 1 { - t.Fatalf("expected selected pane 1, got %d", pluginConfig.GetSelectedPane()) + if pluginConfig.GetSelectedLane() != 1 { + t.Fatalf("expected selected lane 1, got %d", pluginConfig.GetSelectedLane()) } - if pluginConfig.GetSelectedIndexForPane(1) != 2 { - t.Fatalf("expected selected index 2, got %d", pluginConfig.GetSelectedIndexForPane(1)) + if pluginConfig.GetSelectedIndexForLane(1) != 2 { + t.Fatalf("expected selected index 2, got %d", pluginConfig.GetSelectedIndexForLane(1)) } } -func TestPaneSwitchSelectsTopOfViewport(t *testing.T) { +func TestLaneSwitchSelectsTopOfViewport(t *testing.T) { taskStore := store.NewInMemoryStore() - // Create tasks for two panes + // Create tasks for two lanes for i := 1; i <= 10; i++ { status := task.StatusReady if i > 5 { @@ -168,40 +168,40 @@ func TestPaneSwitchSelectsTopOfViewport(t *testing.T) { BasePlugin: plugin.BasePlugin{ Name: "TestPlugin", }, - Panes: []plugin.TikiPane{ + Lanes: []plugin.TikiLane{ {Name: "Ready", Columns: 1, Filter: readyFilter}, {Name: "InProgress", Columns: 1, Filter: inProgressFilter}, }, } pluginConfig := model.NewPluginConfig("TestPlugin") - pluginConfig.SetPaneLayout([]int{1, 1}) + pluginConfig.SetLaneLayout([]int{1, 1}) - // Start in pane 0 (Ready), with selection at index 2 - pluginConfig.SetSelectedPane(0) - pluginConfig.SetSelectedIndexForPane(0, 2) + // Start in lane 0 (Ready), with selection at index 2 + pluginConfig.SetSelectedLane(0) + pluginConfig.SetSelectedIndexForLane(0, 2) - // Simulate that pane 1 has been scrolled to offset 3 - pluginConfig.SetScrollOffsetForPane(1, 3) + // Simulate that lane 1 has been scrolled to offset 3 + pluginConfig.SetScrollOffsetForLane(1, 3) pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil) - // Navigate right to pane 1 + // Navigate right to lane 1 pc.HandleAction(ActionNavRight) - // Should be in pane 1 - if pluginConfig.GetSelectedPane() != 1 { - t.Fatalf("expected selected pane 1, got %d", pluginConfig.GetSelectedPane()) + // Should be in lane 1 + if pluginConfig.GetSelectedLane() != 1 { + t.Fatalf("expected selected lane 1, got %d", pluginConfig.GetSelectedLane()) } // Selection should be at scroll offset (top of viewport), not stale index - if pluginConfig.GetSelectedIndexForPane(1) != 3 { - t.Errorf("expected selection at scroll offset 3, got %d", pluginConfig.GetSelectedIndexForPane(1)) + if pluginConfig.GetSelectedIndexForLane(1) != 3 { + t.Errorf("expected selection at scroll offset 3, got %d", pluginConfig.GetSelectedIndexForLane(1)) } } -func TestPaneSwitchClampsScrollOffsetToTaskCount(t *testing.T) { +func TestLaneSwitchClampsScrollOffsetToTaskCount(t *testing.T) { taskStore := store.NewInMemoryStore() - // Create 3 tasks in pane 1 only + // Create 3 tasks in lane 1 only for i := 1; i <= 3; i++ { if err := taskStore.CreateTask(&task.Task{ ID: fmt.Sprintf("T-%d", i), @@ -213,7 +213,7 @@ func TestPaneSwitchClampsScrollOffsetToTaskCount(t *testing.T) { } } - // Pane 0 is empty, pane 1 has 3 tasks + // Lane 0 is empty, lane 1 has 3 tasks emptyFilter, err := filter.ParseFilter("status = 'ready'") if err != nil { t.Fatalf("parse filter: %v", err) @@ -227,37 +227,37 @@ func TestPaneSwitchClampsScrollOffsetToTaskCount(t *testing.T) { BasePlugin: plugin.BasePlugin{ Name: "TestPlugin", }, - Panes: []plugin.TikiPane{ + Lanes: []plugin.TikiLane{ {Name: "Empty", Columns: 1, Filter: emptyFilter}, {Name: "InProgress", Columns: 1, Filter: inProgressFilter}, }, } pluginConfig := model.NewPluginConfig("TestPlugin") - pluginConfig.SetPaneLayout([]int{1, 1}) + pluginConfig.SetLaneLayout([]int{1, 1}) - // Start in pane 1 - pluginConfig.SetSelectedPane(1) - pluginConfig.SetSelectedIndexForPane(1, 0) + // Start in lane 1 + pluginConfig.SetSelectedLane(1) + pluginConfig.SetSelectedIndexForLane(1, 0) // Set a stale scroll offset that exceeds the task count - pluginConfig.SetScrollOffsetForPane(1, 10) + pluginConfig.SetScrollOffsetForLane(1, 10) pc := NewPluginController(taskStore, pluginConfig, pluginDef, nil) - // Navigate left (to empty pane, will skip to... well, nowhere) + // Navigate left (to empty lane, will skip to... well, nowhere) // Then try to go right from a fresh setup - pluginConfig.SetSelectedPane(0) - pluginConfig.SetScrollOffsetForPane(1, 10) // stale offset > task count + pluginConfig.SetSelectedLane(0) + pluginConfig.SetScrollOffsetForLane(1, 10) // stale offset > task count pc.HandleAction(ActionNavRight) - // Should be in pane 1 - if pluginConfig.GetSelectedPane() != 1 { - t.Fatalf("expected selected pane 1, got %d", pluginConfig.GetSelectedPane()) + // Should be in lane 1 + if pluginConfig.GetSelectedLane() != 1 { + t.Fatalf("expected selected lane 1, got %d", pluginConfig.GetSelectedLane()) } // Selection should be clamped to last valid index (2, since 3 tasks) - selectedIdx := pluginConfig.GetSelectedIndexForPane(1) + selectedIdx := pluginConfig.GetSelectedIndexForLane(1) if selectedIdx < 0 || selectedIdx >= 3 { t.Errorf("expected selection clamped to valid range [0,2], got %d", selectedIdx) } diff --git a/integration/pane_action_test.go b/integration/lane_action_test.go similarity index 96% rename from integration/pane_action_test.go rename to integration/lane_action_test.go index 49f854d..81d3a14 100644 --- a/integration/pane_action_test.go +++ b/integration/lane_action_test.go @@ -12,13 +12,13 @@ import ( "github.com/spf13/viper" ) -func TestPluginView_MoveTaskAppliesPaneAction(t *testing.T) { +func TestPluginView_MoveTaskAppliesLaneAction(t *testing.T) { originalPlugins := viper.Get("plugins") viper.Set("plugins", []plugin.PluginRef{ { Name: "ActionTest", Key: "F4", - Panes: []plugin.PluginPaneConfig{ + Lanes: []plugin.PluginLaneConfig{ { Name: "Backlog", Columns: 1, diff --git a/integration/task_deletion_test.go b/integration/task_deletion_test.go index c91840f..f01d8a9 100644 --- a/integration/task_deletion_test.go +++ b/integration/task_deletion_test.go @@ -126,8 +126,8 @@ func TestTaskDeletion_SelectionMoves(t *testing.T) { } } -// TestTaskDeletion_LastTaskInPane verifies deleting last task resets selection -func TestTaskDeletion_LastTaskInPane(t *testing.T) { +// TestTaskDeletion_LastTaskInLane verifies deleting last task resets selection +func TestTaskDeletion_LastTaskInLane(t *testing.T) { ta := testutil.NewTestApp(t) defer ta.Cleanup() @@ -136,7 +136,7 @@ func TestTaskDeletion_LastTaskInPane(t *testing.T) { t.Fatalf("failed to load plugins: %v", err) } - // Create only one task in todo pane + // Create only one task in todo lane if err := testutil.CreateTestTask(ta.TaskDir, "TIKI-1", "Only Task", taskpkg.StatusReady, taskpkg.TypeStory); err != nil { t.Fatalf("failed to create task: %v", err) } @@ -175,7 +175,7 @@ func TestTaskDeletion_LastTaskInPane(t *testing.T) { t.Errorf("selection should reset to 0 after deleting last task, got %d", kanbanConfig.GetSelectedIndex()) } - // Verify no crash occurred (pane is empty) + // Verify no crash occurred (lane is empty) // This is implicit - if we got here without panic, test passes } @@ -232,8 +232,8 @@ func TestTaskDeletion_MultipleSequential(t *testing.T) { } } -// TestTaskDeletion_FromDifferentPane verifies deleting from non-todo pane -func TestTaskDeletion_FromDifferentPane(t *testing.T) { +// TestTaskDeletion_FromDifferentLane verifies deleting from non-todo lane +func TestTaskDeletion_FromDifferentLane(t *testing.T) { ta := testutil.NewTestApp(t) defer ta.Cleanup() @@ -242,7 +242,7 @@ func TestTaskDeletion_FromDifferentPane(t *testing.T) { t.Fatalf("failed to load plugins: %v", err) } - // Create task in in_progress pane + // Create task in in_progress lane if err := testutil.CreateTestTask(ta.TaskDir, "TIKI-1", "In Progress Task", taskpkg.StatusInProgress, taskpkg.TypeStory); err != nil { t.Fatalf("failed to create task: %v", err) } @@ -254,14 +254,14 @@ func TestTaskDeletion_FromDifferentPane(t *testing.T) { ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil) ta.Draw() - // Move to in_progress pane (Right arrow) + // Move to in_progress lane (Right arrow) ta.SendKey(tcell.KeyRight, 0, tcell.ModNone) // Verify TIKI-1 visible found, _, _ := ta.FindText("TIKI-1") if !found { ta.DumpScreen() - t.Fatalf("TIKI-1 should be visible in in_progress pane") + t.Fatalf("TIKI-1 should be visible in in_progress lane") } // Delete task @@ -326,8 +326,8 @@ func TestTaskDeletion_CannotDeleteFromTaskDetail(t *testing.T) { } } -// TestTaskDeletion_WithMultiplePanes verifies deletion doesn't affect other panes -func TestTaskDeletion_WithMultiplePanes(t *testing.T) { +// TestTaskDeletion_WithMultipleLanes verifies deletion doesn't affect other lanes +func TestTaskDeletion_WithMultipleLanes(t *testing.T) { ta := testutil.NewTestApp(t) defer ta.Cleanup() @@ -336,7 +336,7 @@ func TestTaskDeletion_WithMultiplePanes(t *testing.T) { t.Fatalf("failed to load plugins: %v", err) } - // Create tasks in different panes + // Create tasks in different lanes if err := testutil.CreateTestTask(ta.TaskDir, "TIKI-1", "Todo Task", taskpkg.StatusReady, taskpkg.TypeStory); err != nil { t.Fatalf("failed to create task: %v", err) } @@ -354,7 +354,7 @@ func TestTaskDeletion_WithMultiplePanes(t *testing.T) { ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil) ta.Draw() - // Delete TIKI-1 from todo pane + // Delete TIKI-1 from todo lane ta.SendKey(tcell.KeyRune, 'd', tcell.ModNone) // Reload @@ -367,11 +367,11 @@ func TestTaskDeletion_WithMultiplePanes(t *testing.T) { t.Errorf("TIKI-1 should be deleted") } - // Verify TIKI-2 and TIKI-3 still exist (in other panes) + // Verify TIKI-2 and TIKI-3 still exist (in other lanes) if ta.TaskStore.GetTask("TIKI-2") == nil { - t.Errorf("TIKI-2 (in different pane) should still exist") + t.Errorf("TIKI-2 (in different lane) should still exist") } if ta.TaskStore.GetTask("TIKI-3") == nil { - t.Errorf("TIKI-3 (in different pane) should still exist") + t.Errorf("TIKI-3 (in different lane) should still exist") } } diff --git a/integration/task_detail_view_test.go b/integration/task_detail_view_test.go index a16c487..baa6720 100644 --- a/integration/task_detail_view_test.go +++ b/integration/task_detail_view_test.go @@ -382,11 +382,11 @@ func TestTaskDetailView_AllStatuses(t *testing.T) { // For each status, navigate to first task with that status and verify detail view for i, status := range statuses { - // Find the task on board (may need to navigate between panes) + // Find the task on board (may need to navigate between lanes) taskID := fmt.Sprintf("TIKI-%d", i+1) - // Navigate to correct pane based on status - // For simplicity, we'll just open first task in todo pane for this test + // Navigate to correct lane based on status + // For simplicity, we'll just open first task in todo lane for this test if status == taskpkg.StatusReady { ta.SendKey(tcell.KeyEnter, 0, tcell.ModNone) diff --git a/internal/bootstrap/plugins.go b/internal/bootstrap/plugins.go index ba951c6..3ea2d8f 100644 --- a/internal/bootstrap/plugins.go +++ b/internal/bootstrap/plugins.go @@ -50,11 +50,11 @@ func BuildPluginConfigsAndDefs(plugins []plugin.Plugin) (map[string]*model.Plugi if tp.ViewMode == "expanded" { pc.SetViewMode("expanded") } - columns := make([]int, len(tp.Panes)) - for i, pane := range tp.Panes { - columns[i] = pane.Columns + columns := make([]int, len(tp.Lanes)) + for i, lane := range tp.Lanes { + columns[i] = lane.Columns } - pc.SetPaneLayout(columns) + pc.SetLaneLayout(columns) } pluginConfigs[p.GetName()] = pc diff --git a/model/plugin_config.go b/model/plugin_config.go index b3505c9..48d7a8e 100644 --- a/model/plugin_config.go +++ b/model/plugin_config.go @@ -23,11 +23,11 @@ type PluginSelectionListener func() type PluginConfig struct { mu sync.RWMutex pluginName string - selectedPane int + selectedLane int selectedIndices []int - paneColumns []int - scrollOffsets []int // per-pane viewport position (top visible row) - preSearchPane int + laneColumns []int + scrollOffsets []int // per-lane viewport position (top visible row) + preSearchLane int preSearchIndices []int viewMode ViewMode // compact or expanded display configIndex int // index in config.yaml plugins array (-1 if embedded/not in config) @@ -45,7 +45,7 @@ func NewPluginConfig(name string) *PluginConfig { listeners: make(map[int]PluginSelectionListener), nextListenerID: 1, // Start at 1 to avoid conflict with zero-value sentinel } - pc.SetPaneLayout([]int{4}) + pc.SetLaneLayout([]int{4}) return pc } @@ -61,103 +61,103 @@ func (pc *PluginConfig) GetPluginName() string { return pc.pluginName } -// SetPaneLayout configures pane columns and resets selection state as needed. -func (pc *PluginConfig) SetPaneLayout(columns []int) { +// SetLaneLayout configures lane columns and resets selection state as needed. +func (pc *PluginConfig) SetLaneLayout(columns []int) { pc.mu.Lock() defer pc.mu.Unlock() - pc.paneColumns = normalizePaneColumns(columns) - pc.selectedIndices = ensureSelectionLength(pc.selectedIndices, len(pc.paneColumns)) - pc.preSearchIndices = ensureSelectionLength(pc.preSearchIndices, len(pc.paneColumns)) - pc.scrollOffsets = ensureSelectionLength(pc.scrollOffsets, len(pc.paneColumns)) + pc.laneColumns = normalizeLaneColumns(columns) + pc.selectedIndices = ensureSelectionLength(pc.selectedIndices, len(pc.laneColumns)) + pc.preSearchIndices = ensureSelectionLength(pc.preSearchIndices, len(pc.laneColumns)) + pc.scrollOffsets = ensureSelectionLength(pc.scrollOffsets, len(pc.laneColumns)) - if pc.selectedPane < 0 || pc.selectedPane >= len(pc.paneColumns) { - pc.selectedPane = 0 + if pc.selectedLane < 0 || pc.selectedLane >= len(pc.laneColumns) { + pc.selectedLane = 0 } } -// GetPaneCount returns the number of panes. -func (pc *PluginConfig) GetPaneCount() int { +// GetLaneCount returns the number of lanes. +func (pc *PluginConfig) GetLaneCount() int { pc.mu.RLock() defer pc.mu.RUnlock() - return len(pc.paneColumns) + return len(pc.laneColumns) } -// GetSelectedPane returns the selected pane index. -func (pc *PluginConfig) GetSelectedPane() int { +// GetSelectedLane returns the selected lane index. +func (pc *PluginConfig) GetSelectedLane() int { pc.mu.RLock() defer pc.mu.RUnlock() - return pc.selectedPane + return pc.selectedLane } -// SetSelectedPane sets the selected pane index. -func (pc *PluginConfig) SetSelectedPane(pane int) { +// SetSelectedLane sets the selected lane index. +func (pc *PluginConfig) SetSelectedLane(lane int) { pc.mu.Lock() - if pane < 0 || pane >= len(pc.paneColumns) { + if lane < 0 || lane >= len(pc.laneColumns) { pc.mu.Unlock() return } - changed := pc.selectedPane != pane - pc.selectedPane = pane + changed := pc.selectedLane != lane + pc.selectedLane = lane pc.mu.Unlock() if changed { pc.notifyListeners() } } -// GetSelectedIndex returns the selected task index for the current pane. +// GetSelectedIndex returns the selected task index for the current lane. func (pc *PluginConfig) GetSelectedIndex() int { pc.mu.RLock() defer pc.mu.RUnlock() - return pc.indexForPane(pc.selectedPane) + return pc.indexForLane(pc.selectedLane) } -// GetSelectedIndexForPane returns the selected index for a pane. -func (pc *PluginConfig) GetSelectedIndexForPane(pane int) int { +// GetSelectedIndexForLane returns the selected index for a lane. +func (pc *PluginConfig) GetSelectedIndexForLane(lane int) int { pc.mu.RLock() defer pc.mu.RUnlock() - return pc.indexForPane(pane) + return pc.indexForLane(lane) } -// SetSelectedIndex sets the selected task index for the current pane. +// SetSelectedIndex sets the selected task index for the current lane. func (pc *PluginConfig) SetSelectedIndex(idx int) { pc.mu.Lock() - pc.setIndexForPane(pc.selectedPane, idx) + pc.setIndexForLane(pc.selectedLane, idx) pc.mu.Unlock() pc.notifyListeners() } -// SetSelectedIndexForPane sets the selected index for a specific pane. -func (pc *PluginConfig) SetSelectedIndexForPane(pane int, idx int) { +// SetSelectedIndexForLane sets the selected index for a specific lane. +func (pc *PluginConfig) SetSelectedIndexForLane(lane int, idx int) { pc.mu.Lock() - pc.setIndexForPane(pane, idx) + pc.setIndexForLane(lane, idx) pc.mu.Unlock() pc.notifyListeners() } -// GetScrollOffsetForPane returns the scroll offset (top visible row) for a pane. -func (pc *PluginConfig) GetScrollOffsetForPane(pane int) int { +// GetScrollOffsetForLane returns the scroll offset (top visible row) for a lane. +func (pc *PluginConfig) GetScrollOffsetForLane(lane int) int { pc.mu.RLock() defer pc.mu.RUnlock() - if pane < 0 || pane >= len(pc.scrollOffsets) { + if lane < 0 || lane >= len(pc.scrollOffsets) { return 0 } - return pc.scrollOffsets[pane] + return pc.scrollOffsets[lane] } -// SetScrollOffsetForPane sets the scroll offset for a specific pane. -func (pc *PluginConfig) SetScrollOffsetForPane(pane int, offset int) { +// SetScrollOffsetForLane sets the scroll offset for a specific lane. +func (pc *PluginConfig) SetScrollOffsetForLane(lane int, offset int) { pc.mu.Lock() defer pc.mu.Unlock() - if pane < 0 || pane >= len(pc.scrollOffsets) { + if lane < 0 || lane >= len(pc.scrollOffsets) { return } - pc.scrollOffsets[pane] = offset + pc.scrollOffsets[lane] = offset } -func (pc *PluginConfig) SetSelectedPaneAndIndex(pane int, idx int) { +func (pc *PluginConfig) SetSelectedLaneAndIndex(lane int, idx int) { pc.mu.Lock() - if pane < 0 || pane >= len(pc.selectedIndices) { + if lane < 0 || lane >= len(pc.selectedIndices) { pc.mu.Unlock() return } @@ -165,9 +165,9 @@ func (pc *PluginConfig) SetSelectedPaneAndIndex(pane int, idx int) { pc.mu.Unlock() return } - changed := pc.selectedPane != pane || pc.selectedIndices[pane] != idx - pc.selectedPane = pane - pc.selectedIndices[pane] = idx + changed := pc.selectedLane != lane || pc.selectedIndices[lane] != idx + pc.selectedLane = lane + pc.selectedIndices[lane] = idx pc.mu.Unlock() if changed { @@ -175,11 +175,11 @@ func (pc *PluginConfig) SetSelectedPaneAndIndex(pane int, idx int) { } } -// GetColumnsForPane returns the number of grid columns for a pane. -func (pc *PluginConfig) GetColumnsForPane(pane int) int { +// GetColumnsForLane returns the number of grid columns for a lane. +func (pc *PluginConfig) GetColumnsForLane(lane int) int { pc.mu.RLock() defer pc.mu.RUnlock() - return pc.columnsForPane(pane) + return pc.columnsForLane(lane) } // AddSelectionListener registers a callback for selection changes @@ -212,16 +212,16 @@ func (pc *PluginConfig) notifyListeners() { } } -// MoveSelection moves selection in a direction within the current pane. +// MoveSelection moves selection in a direction within the current lane. func (pc *PluginConfig) MoveSelection(direction string, taskCount int) bool { if taskCount == 0 { return false } pc.mu.Lock() - pane := pc.selectedPane - columns := pc.columnsForPane(pane) - oldIndex := pc.indexForPane(pane) + lane := pc.selectedLane + columns := pc.columnsForLane(lane) + oldIndex := pc.indexForLane(lane) row := oldIndex / columns col := oldIndex % columns numRows := (taskCount + columns - 1) / columns @@ -229,24 +229,24 @@ func (pc *PluginConfig) MoveSelection(direction string, taskCount int) bool { switch direction { case "up": if row > 0 { - pc.setIndexForPane(pane, oldIndex-columns) + pc.setIndexForLane(lane, oldIndex-columns) } case "down": newIdx := oldIndex + columns if row < numRows-1 && newIdx < taskCount { - pc.setIndexForPane(pane, newIdx) + pc.setIndexForLane(lane, newIdx) } case "left": if col > 0 { - pc.setIndexForPane(pane, oldIndex-1) + pc.setIndexForLane(lane, oldIndex-1) } case "right": if col < columns-1 && oldIndex+1 < taskCount { - pc.setIndexForPane(pane, oldIndex+1) + pc.setIndexForLane(lane, oldIndex+1) } } - moved := pc.indexForPane(pane) != oldIndex + moved := pc.indexForLane(lane) != oldIndex pc.mu.Unlock() if moved { @@ -255,16 +255,16 @@ func (pc *PluginConfig) MoveSelection(direction string, taskCount int) bool { return moved } -// ClampSelection ensures selection is within bounds for the current pane. +// ClampSelection ensures selection is within bounds for the current lane. func (pc *PluginConfig) ClampSelection(taskCount int) { pc.mu.Lock() - pane := pc.selectedPane - index := pc.indexForPane(pane) + lane := pc.selectedLane + index := pc.indexForLane(lane) if index >= taskCount { - pc.setIndexForPane(pane, taskCount-1) + pc.setIndexForLane(lane, taskCount-1) } - if pc.indexForPane(pane) < 0 { - pc.setIndexForPane(pane, 0) + if pc.indexForLane(lane) < 0 { + pc.setIndexForLane(lane, 0) } pc.mu.Unlock() } @@ -312,10 +312,10 @@ func (pc *PluginConfig) SetViewMode(mode string) { // SavePreSearchState saves current selection for later restoration func (pc *PluginConfig) SavePreSearchState() { pc.mu.Lock() - pc.preSearchPane = pc.selectedPane - pc.preSearchIndices = ensureSelectionLength(pc.preSearchIndices, len(pc.paneColumns)) + pc.preSearchLane = pc.selectedLane + pc.preSearchIndices = ensureSelectionLength(pc.preSearchIndices, len(pc.laneColumns)) copy(pc.preSearchIndices, pc.selectedIndices) - selectedIndex := pc.indexForPane(pc.selectedPane) + selectedIndex := pc.indexForLane(pc.selectedLane) pc.mu.Unlock() pc.searchState.SavePreSearchState(selectedIndex) } @@ -330,13 +330,13 @@ func (pc *PluginConfig) SetSearchResults(results []task.SearchResult, query stri func (pc *PluginConfig) ClearSearchResults() { pc.searchState.ClearSearchResults() pc.mu.Lock() - if len(pc.preSearchIndices) == len(pc.paneColumns) { - pc.selectedIndices = ensureSelectionLength(pc.selectedIndices, len(pc.paneColumns)) + if len(pc.preSearchIndices) == len(pc.laneColumns) { + pc.selectedIndices = ensureSelectionLength(pc.selectedIndices, len(pc.laneColumns)) copy(pc.selectedIndices, pc.preSearchIndices) - pc.selectedPane = pc.preSearchPane + pc.selectedLane = pc.preSearchLane } else if len(pc.selectedIndices) > 0 { - pc.selectedPane = 0 - pc.setIndexForPane(0, 0) + pc.selectedLane = 0 + pc.setIndexForLane(0, 0) } pc.mu.Unlock() pc.notifyListeners() @@ -357,40 +357,40 @@ func (pc *PluginConfig) GetSearchQuery() string { return pc.searchState.GetSearchQuery() } -func (pc *PluginConfig) indexForPane(pane int) int { +func (pc *PluginConfig) indexForLane(lane int) int { if len(pc.selectedIndices) == 0 { return 0 } - if pane < 0 || pane >= len(pc.selectedIndices) { - slog.Warn("pane index out of range", "pane", pane, "count", len(pc.selectedIndices)) + if lane < 0 || lane >= len(pc.selectedIndices) { + slog.Warn("lane index out of range", "lane", lane, "count", len(pc.selectedIndices)) return 0 } - return pc.selectedIndices[pane] + return pc.selectedIndices[lane] } -func (pc *PluginConfig) setIndexForPane(pane int, idx int) { +func (pc *PluginConfig) setIndexForLane(lane int, idx int) { if len(pc.selectedIndices) == 0 { return } - if pane < 0 || pane >= len(pc.selectedIndices) { - slog.Warn("pane index out of range", "pane", pane, "count", len(pc.selectedIndices)) + if lane < 0 || lane >= len(pc.selectedIndices) { + slog.Warn("lane index out of range", "lane", lane, "count", len(pc.selectedIndices)) return } - pc.selectedIndices[pane] = idx + pc.selectedIndices[lane] = idx } -func (pc *PluginConfig) columnsForPane(pane int) int { - if len(pc.paneColumns) == 0 { +func (pc *PluginConfig) columnsForLane(lane int) int { + if len(pc.laneColumns) == 0 { return 1 } - if pane < 0 || pane >= len(pc.paneColumns) { - slog.Warn("pane columns out of range", "pane", pane, "count", len(pc.paneColumns)) + if lane < 0 || lane >= len(pc.laneColumns) { + slog.Warn("lane columns out of range", "lane", lane, "count", len(pc.laneColumns)) return 1 } - return pc.paneColumns[pane] + return pc.laneColumns[lane] } -func normalizePaneColumns(columns []int) []int { +func normalizeLaneColumns(columns []int) []int { if len(columns) == 0 { return []int{1} } diff --git a/model/plugin_config_test.go b/model/plugin_config_test.go index ea16d92..d0abef7 100644 --- a/model/plugin_config_test.go +++ b/model/plugin_config_test.go @@ -23,8 +23,8 @@ func TestNewPluginConfig(t *testing.T) { t.Errorf("initial GetSelectedIndex() = %d, want 0", pc.GetSelectedIndex()) } - if pc.GetColumnsForPane(0) != 4 { - t.Errorf("GetColumnsForPane(0) = %d, want 4", pc.GetColumnsForPane(0)) + if pc.GetColumnsForLane(0) != 4 { + t.Errorf("GetColumnsForLane(0) = %d, want 4", pc.GetColumnsForLane(0)) } if pc.GetViewMode() != ViewModeCompact { @@ -628,72 +628,72 @@ func TestPluginConfig_GridNavigation_AllCorners(t *testing.T) { func TestPluginConfig_ScrollOffset(t *testing.T) { pc := NewPluginConfig("test") - pc.SetPaneLayout([]int{1, 1, 1}) // 3 panes + pc.SetLaneLayout([]int{1, 1, 1}) // 3 lanes // Initial scroll offsets should be 0 - for pane := 0; pane < 3; pane++ { - if offset := pc.GetScrollOffsetForPane(pane); offset != 0 { - t.Errorf("initial GetScrollOffsetForPane(%d) = %d, want 0", pane, offset) + for lane := 0; lane < 3; lane++ { + if offset := pc.GetScrollOffsetForLane(lane); offset != 0 { + t.Errorf("initial GetScrollOffsetForLane(%d) = %d, want 0", lane, offset) } } // Set scroll offset for pane 1 - pc.SetScrollOffsetForPane(1, 5) - if offset := pc.GetScrollOffsetForPane(1); offset != 5 { - t.Errorf("GetScrollOffsetForPane(1) = %d, want 5", offset) + pc.SetScrollOffsetForLane(1, 5) + if offset := pc.GetScrollOffsetForLane(1); offset != 5 { + t.Errorf("GetScrollOffsetForLane(1) = %d, want 5", offset) } // Other panes should be unaffected - if offset := pc.GetScrollOffsetForPane(0); offset != 0 { - t.Errorf("GetScrollOffsetForPane(0) = %d, want 0", offset) + if offset := pc.GetScrollOffsetForLane(0); offset != 0 { + t.Errorf("GetScrollOffsetForLane(0) = %d, want 0", offset) } - if offset := pc.GetScrollOffsetForPane(2); offset != 0 { - t.Errorf("GetScrollOffsetForPane(2) = %d, want 0", offset) + if offset := pc.GetScrollOffsetForLane(2); offset != 0 { + t.Errorf("GetScrollOffsetForLane(2) = %d, want 0", offset) } // Set scroll offset for pane 2 - pc.SetScrollOffsetForPane(2, 10) - if offset := pc.GetScrollOffsetForPane(2); offset != 10 { - t.Errorf("GetScrollOffsetForPane(2) = %d, want 10", offset) + pc.SetScrollOffsetForLane(2, 10) + if offset := pc.GetScrollOffsetForLane(2); offset != 10 { + t.Errorf("GetScrollOffsetForLane(2) = %d, want 10", offset) } } func TestPluginConfig_ScrollOffset_OutOfBounds(t *testing.T) { pc := NewPluginConfig("test") - pc.SetPaneLayout([]int{1, 1}) // 2 panes + pc.SetLaneLayout([]int{1, 1}) // 2 lanes // Getting out of bounds should return 0 - if offset := pc.GetScrollOffsetForPane(-1); offset != 0 { - t.Errorf("GetScrollOffsetForPane(-1) = %d, want 0", offset) + if offset := pc.GetScrollOffsetForLane(-1); offset != 0 { + t.Errorf("GetScrollOffsetForLane(-1) = %d, want 0", offset) } - if offset := pc.GetScrollOffsetForPane(5); offset != 0 { - t.Errorf("GetScrollOffsetForPane(5) = %d, want 0", offset) + if offset := pc.GetScrollOffsetForLane(5); offset != 0 { + t.Errorf("GetScrollOffsetForLane(5) = %d, want 0", offset) } // Setting out of bounds should be a no-op (not panic) - pc.SetScrollOffsetForPane(-1, 10) - pc.SetScrollOffsetForPane(5, 10) + pc.SetScrollOffsetForLane(-1, 10) + pc.SetScrollOffsetForLane(5, 10) // Valid panes should still be 0 - if offset := pc.GetScrollOffsetForPane(0); offset != 0 { - t.Errorf("GetScrollOffsetForPane(0) = %d after out-of-bounds set, want 0", offset) + if offset := pc.GetScrollOffsetForLane(0); offset != 0 { + t.Errorf("GetScrollOffsetForLane(0) = %d after out-of-bounds set, want 0", offset) } } func TestPluginConfig_ScrollOffset_PreservedOnLayoutChange(t *testing.T) { pc := NewPluginConfig("test") - pc.SetPaneLayout([]int{1, 1, 1}) + pc.SetLaneLayout([]int{1, 1, 1}) // Set scroll offsets - pc.SetScrollOffsetForPane(0, 3) - pc.SetScrollOffsetForPane(1, 7) + pc.SetScrollOffsetForLane(0, 3) + pc.SetScrollOffsetForLane(1, 7) // Change layout to same size - should preserve offsets - pc.SetPaneLayout([]int{2, 2, 2}) - if offset := pc.GetScrollOffsetForPane(0); offset != 3 { - t.Errorf("GetScrollOffsetForPane(0) after same-size layout change = %d, want 3", offset) + pc.SetLaneLayout([]int{2, 2, 2}) + if offset := pc.GetScrollOffsetForLane(0); offset != 3 { + t.Errorf("GetScrollOffsetForLane(0) after same-size layout change = %d, want 3", offset) } - if offset := pc.GetScrollOffsetForPane(1); offset != 7 { - t.Errorf("GetScrollOffsetForPane(1) after same-size layout change = %d, want 7", offset) + if offset := pc.GetScrollOffsetForLane(1); offset != 7 { + t.Errorf("GetScrollOffsetForLane(1) after same-size layout change = %d, want 7", offset) } } diff --git a/plugin/action.go b/plugin/action.go index 9bfa4e7..f147674 100644 --- a/plugin/action.go +++ b/plugin/action.go @@ -8,13 +8,13 @@ import ( "github.com/boolean-maybe/tiki/task" ) -// PaneAction represents parsed pane actions. -type PaneAction struct { - Ops []PaneActionOp +// LaneAction represents parsed lane actions. +type LaneAction struct { + Ops []LaneActionOp } -// PaneActionOp represents a single action operation. -type PaneActionOp struct { +// LaneActionOp represents a single action operation. +type LaneActionOp struct { Field ActionField Operator ActionOperator StrValue string @@ -43,103 +43,103 @@ const ( ActionOperatorRemove ActionOperator = "-=" ) -// ParsePaneAction parses a pane action string into operations. -func ParsePaneAction(input string) (PaneAction, error) { +// ParseLaneAction parses a lane action string into operations. +func ParseLaneAction(input string) (LaneAction, error) { input = strings.TrimSpace(input) if input == "" { - return PaneAction{}, nil + return LaneAction{}, nil } parts, err := splitTopLevelCommas(input) if err != nil { - return PaneAction{}, err + return LaneAction{}, err } - ops := make([]PaneActionOp, 0, len(parts)) + ops := make([]LaneActionOp, 0, len(parts)) for _, part := range parts { if part == "" { - return PaneAction{}, fmt.Errorf("empty action segment") + return LaneAction{}, fmt.Errorf("empty action segment") } field, op, value, err := parseActionSegment(part) if err != nil { - return PaneAction{}, err + return LaneAction{}, err } switch field { case ActionFieldTags: if op == ActionOperatorAssign { - return PaneAction{}, fmt.Errorf("tags action only supports += or -=") + return LaneAction{}, fmt.Errorf("tags action only supports += or -=") } tags, err := parseTagsValue(value) if err != nil { - return PaneAction{}, err + return LaneAction{}, err } - ops = append(ops, PaneActionOp{ + ops = append(ops, LaneActionOp{ Field: field, Operator: op, Tags: tags, }) case ActionFieldPriority, ActionFieldPoints: if op != ActionOperatorAssign { - return PaneAction{}, fmt.Errorf("%s action only supports =", field) + return LaneAction{}, fmt.Errorf("%s action only supports =", field) } intValue, err := parseIntValue(value) if err != nil { - return PaneAction{}, err + return LaneAction{}, err } if field == ActionFieldPriority && !task.IsValidPriority(intValue) { - return PaneAction{}, fmt.Errorf("priority value out of range: %d", intValue) + return LaneAction{}, fmt.Errorf("priority value out of range: %d", intValue) } if field == ActionFieldPoints && !task.IsValidPoints(intValue) { - return PaneAction{}, fmt.Errorf("points value out of range: %d", intValue) + return LaneAction{}, fmt.Errorf("points value out of range: %d", intValue) } - ops = append(ops, PaneActionOp{ + ops = append(ops, LaneActionOp{ Field: field, Operator: op, IntValue: intValue, }) case ActionFieldStatus: if op != ActionOperatorAssign { - return PaneAction{}, fmt.Errorf("%s action only supports =", field) + return LaneAction{}, fmt.Errorf("%s action only supports =", field) } strValue, err := parseStringValue(value) if err != nil { - return PaneAction{}, err + return LaneAction{}, err } if _, ok := task.ParseStatus(strValue); !ok { - return PaneAction{}, fmt.Errorf("invalid status value %q", strValue) + return LaneAction{}, fmt.Errorf("invalid status value %q", strValue) } - ops = append(ops, PaneActionOp{ + ops = append(ops, LaneActionOp{ Field: field, Operator: op, StrValue: strValue, }) case ActionFieldType: if op != ActionOperatorAssign { - return PaneAction{}, fmt.Errorf("%s action only supports =", field) + return LaneAction{}, fmt.Errorf("%s action only supports =", field) } strValue, err := parseStringValue(value) if err != nil { - return PaneAction{}, err + return LaneAction{}, err } if _, ok := task.ParseType(strValue); !ok { - return PaneAction{}, fmt.Errorf("invalid type value %q", strValue) + return LaneAction{}, fmt.Errorf("invalid type value %q", strValue) } - ops = append(ops, PaneActionOp{ + ops = append(ops, LaneActionOp{ Field: field, Operator: op, StrValue: strValue, }) default: if op != ActionOperatorAssign { - return PaneAction{}, fmt.Errorf("%s action only supports =", field) + return LaneAction{}, fmt.Errorf("%s action only supports =", field) } strValue, err := parseStringValue(value) if err != nil { - return PaneAction{}, err + return LaneAction{}, err } - ops = append(ops, PaneActionOp{ + ops = append(ops, LaneActionOp{ Field: field, Operator: op, StrValue: strValue, @@ -147,11 +147,11 @@ func ParsePaneAction(input string) (PaneAction, error) { } } - return PaneAction{Ops: ops}, nil + return LaneAction{Ops: ops}, nil } -// ApplyPaneAction applies a parsed action to a task clone. -func ApplyPaneAction(src *task.Task, action PaneAction, currentUser string) (*task.Task, error) { +// ApplyLaneAction applies a parsed action to a task clone. +func ApplyLaneAction(src *task.Task, action LaneAction, currentUser string) (*task.Task, error) { if src == nil { return nil, fmt.Errorf("task is nil") } diff --git a/plugin/action_test.go b/plugin/action_test.go index ab4fb1e..4b58504 100644 --- a/plugin/action_test.go +++ b/plugin/action_test.go @@ -74,8 +74,8 @@ func TestSplitTopLevelCommas(t *testing.T) { } } -func TestParsePaneAction(t *testing.T) { - action, err := ParsePaneAction("status=done, type=bug, priority=2, points=3, assignee='Alice', tags+=[frontend,'needs review']") +func TestParseLaneAction(t *testing.T) { + action, err := ParseLaneAction("status=done, type=bug, priority=2, points=3, assignee='Alice', tags+=[frontend,'needs review']") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -123,7 +123,7 @@ func TestParsePaneAction(t *testing.T) { } } -func TestParsePaneAction_Errors(t *testing.T) { +func TestParseLaneAction_Errors(t *testing.T) { tests := []struct { name string input string @@ -183,7 +183,7 @@ func TestParsePaneAction_Errors(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - _, err := ParsePaneAction(tc.input) + _, err := ParseLaneAction(tc.input) if err == nil { t.Fatalf("expected error containing %q", tc.wantErr) } @@ -194,7 +194,7 @@ func TestParsePaneAction_Errors(t *testing.T) { } } -func TestApplyPaneAction(t *testing.T) { +func TestApplyLaneAction(t *testing.T) { base := &task.Task{ ID: "TASK-1", Title: "Task", @@ -206,12 +206,12 @@ func TestApplyPaneAction(t *testing.T) { Assignee: "Bob", } - action, err := ParsePaneAction("status=done, type=bug, priority=2, points=3, assignee=Alice, tags+=[moved]") + action, err := ParseLaneAction("status=done, type=bug, priority=2, points=3, assignee=Alice, tags+=[moved]") if err != nil { t.Fatalf("unexpected parse error: %v", err) } - updated, err := ApplyPaneAction(base, action, "") + updated, err := ApplyLaneAction(base, action, "") if err != nil { t.Fatalf("unexpected apply error: %v", err) } @@ -242,7 +242,7 @@ func TestApplyPaneAction(t *testing.T) { } } -func TestApplyPaneAction_InvalidResult(t *testing.T) { +func TestApplyLaneAction_InvalidResult(t *testing.T) { base := &task.Task{ ID: "TASK-1", Title: "Task", @@ -252,8 +252,8 @@ func TestApplyPaneAction_InvalidResult(t *testing.T) { Points: 1, } - action := PaneAction{ - Ops: []PaneActionOp{ + action := LaneAction{ + Ops: []LaneActionOp{ { Field: ActionFieldPriority, Operator: ActionOperatorAssign, @@ -262,13 +262,13 @@ func TestApplyPaneAction_InvalidResult(t *testing.T) { }, } - _, err := ApplyPaneAction(base, action, "") + _, err := ApplyLaneAction(base, action, "") if err == nil { t.Fatalf("expected validation error") } } -func TestApplyPaneAction_AssigneeCurrentUser(t *testing.T) { +func TestApplyLaneAction_AssigneeCurrentUser(t *testing.T) { base := &task.Task{ ID: "TASK-1", Title: "Task", @@ -279,12 +279,12 @@ func TestApplyPaneAction_AssigneeCurrentUser(t *testing.T) { Assignee: "Bob", } - action, err := ParsePaneAction("assignee=CURRENT_USER") + action, err := ParseLaneAction("assignee=CURRENT_USER") if err != nil { t.Fatalf("unexpected parse error: %v", err) } - updated, err := ApplyPaneAction(base, action, "Alex") + updated, err := ApplyLaneAction(base, action, "Alex") if err != nil { t.Fatalf("unexpected apply error: %v", err) } @@ -293,7 +293,7 @@ func TestApplyPaneAction_AssigneeCurrentUser(t *testing.T) { } } -func TestApplyPaneAction_AssigneeCurrentUserMissing(t *testing.T) { +func TestApplyLaneAction_AssigneeCurrentUserMissing(t *testing.T) { base := &task.Task{ ID: "TASK-1", Title: "Task", @@ -303,12 +303,12 @@ func TestApplyPaneAction_AssigneeCurrentUserMissing(t *testing.T) { Points: 1, } - action, err := ParsePaneAction("assignee=CURRENT_USER") + action, err := ParseLaneAction("assignee=CURRENT_USER") if err != nil { t.Fatalf("unexpected parse error: %v", err) } - _, err = ApplyPaneAction(base, action, "") + _, err = ApplyLaneAction(base, action, "") if err == nil { t.Fatalf("expected error for missing current user") } diff --git a/plugin/definition.go b/plugin/definition.go index 709d4f4..413c13f 100644 --- a/plugin/definition.go +++ b/plugin/definition.go @@ -61,7 +61,7 @@ func (p *BasePlugin) GetType() string { // TikiPlugin is a task-based plugin (like default Kanban board) type TikiPlugin struct { BasePlugin - Panes []TikiPane // pane definitions for this plugin + Lanes []TikiLane // lane definitions for this plugin Sort []SortRule // parsed sort rules (nil = default sort) ViewMode string // default view mode: "compact" or "expanded" (empty = compact) Actions []PluginAction // shortcut actions applied to the selected task @@ -86,23 +86,23 @@ type PluginActionConfig struct { type PluginAction struct { Rune rune Label string - Action PaneAction + Action LaneAction } -// PluginPaneConfig represents a pane in YAML or config definitions. -type PluginPaneConfig struct { +// PluginLaneConfig represents a lane in YAML or config definitions. +type PluginLaneConfig struct { Name string `yaml:"name" mapstructure:"name"` Columns int `yaml:"columns" mapstructure:"columns"` Filter string `yaml:"filter" mapstructure:"filter"` Action string `yaml:"action" mapstructure:"action"` } -// TikiPane represents a parsed pane definition. -type TikiPane struct { +// TikiLane represents a parsed lane definition. +type TikiLane struct { Name string Columns int Filter filter.FilterExpr - Action PaneAction + Action LaneAction } // PluginRef is the entry in config.yaml that references a plugin file or defines it inline @@ -122,6 +122,6 @@ type PluginRef struct { Fetcher string `mapstructure:"fetcher"` Text string `mapstructure:"text"` URL string `mapstructure:"url"` - Panes []PluginPaneConfig `mapstructure:"panes"` + Lanes []PluginLaneConfig `mapstructure:"lanes"` Actions []PluginActionConfig `mapstructure:"actions"` } diff --git a/plugin/embed/backlog.yaml b/plugin/embed/backlog.yaml index f122430..e82e8bd 100644 --- a/plugin/embed/backlog.yaml +++ b/plugin/embed/backlog.yaml @@ -2,7 +2,7 @@ name: Backlog foreground: "#5fff87" background: "#0b3d2e" key: "F3" -panes: +lanes: - name: Backlog columns: 4 filter: status = 'backlog' and type != 'epic' diff --git a/plugin/embed/kanban.yaml b/plugin/embed/kanban.yaml index f9fc40a..5d35024 100644 --- a/plugin/embed/kanban.yaml +++ b/plugin/embed/kanban.yaml @@ -2,7 +2,7 @@ name: Kanban foreground: "#87ceeb" background: "#25496a" key: "F1" -panes: +lanes: - name: Ready filter: status = 'ready' and type != 'epic' action: status = 'ready' diff --git a/plugin/embed/recent.yaml b/plugin/embed/recent.yaml index 19b7c85..a8f3289 100644 --- a/plugin/embed/recent.yaml +++ b/plugin/embed/recent.yaml @@ -2,7 +2,7 @@ name: Recent foreground: "#f4d6a6" background: "#5a3d1b" key: Ctrl-R -panes: +lanes: - name: Recent columns: 4 filter: NOW - UpdatedAt < 24hours diff --git a/plugin/embed/roadmap.yaml b/plugin/embed/roadmap.yaml index 427d2b7..e514aba 100644 --- a/plugin/embed/roadmap.yaml +++ b/plugin/embed/roadmap.yaml @@ -2,7 +2,7 @@ name: Roadmap foreground: "#e2e8f0" background: "#2a5f5a" key: "F4" -panes: +lanes: - name: Now columns: 1 filter: type = 'epic' AND status = 'ready' diff --git a/plugin/integration_test.go b/plugin/integration_test.go index d3a582f..61928d6 100644 --- a/plugin/integration_test.go +++ b/plugin/integration_test.go @@ -14,7 +14,7 @@ name: UI Tasks foreground: "#ffffff" background: "#0000ff" key: U -panes: +lanes: - name: UI filter: tags IN ['ui', 'ux', 'design'] ` @@ -33,8 +33,8 @@ panes: t.Fatalf("Expected TikiPlugin, got %T", def) } - if len(tp.Panes) != 1 || tp.Panes[0].Filter == nil { - t.Fatal("Expected pane filter to be parsed") + if len(tp.Lanes) != 1 || tp.Lanes[0].Filter == nil { + t.Fatal("Expected lane filter to be parsed") } // Test filter evaluation with matching tasks @@ -45,7 +45,7 @@ panes: Status: task.StatusReady, } - if !tp.Panes[0].Filter.Evaluate(matchingTask, time.Now(), "testuser") { + if !tp.Lanes[0].Filter.Evaluate(matchingTask, time.Now(), "testuser") { t.Error("Expected filter to match task with 'ui' and 'design' tags") } @@ -57,7 +57,7 @@ panes: Status: task.StatusReady, } - if tp.Panes[0].Filter.Evaluate(nonMatchingTask, time.Now(), "testuser") { + if tp.Lanes[0].Filter.Evaluate(nonMatchingTask, time.Now(), "testuser") { t.Error("Expected filter to NOT match task with 'backend' and 'api' tags") } @@ -69,7 +69,7 @@ panes: Status: task.StatusReady, } - if !tp.Panes[0].Filter.Evaluate(partialMatchTask, time.Now(), "testuser") { + if !tp.Lanes[0].Filter.Evaluate(partialMatchTask, time.Now(), "testuser") { t.Error("Expected filter to match task with 'ux' tag") } } @@ -79,7 +79,7 @@ func TestPluginWithComplexInFilter(t *testing.T) { pluginYAML := ` name: Active Work key: A -panes: +lanes: - name: Active filter: tags IN ['ui', 'backend'] AND status NOT IN ['done', 'cancelled'] ` @@ -101,7 +101,7 @@ panes: Status: task.StatusReady, } - if !tp.Panes[0].Filter.Evaluate(matchingTask, time.Now(), "testuser") { + if !tp.Lanes[0].Filter.Evaluate(matchingTask, time.Now(), "testuser") { t.Error("Expected filter to match active UI task") } @@ -112,7 +112,7 @@ panes: Status: task.StatusDone, } - if tp.Panes[0].Filter.Evaluate(doneTask, time.Now(), "testuser") { + if tp.Lanes[0].Filter.Evaluate(doneTask, time.Now(), "testuser") { t.Error("Expected filter to NOT match done UI task") } @@ -123,7 +123,7 @@ panes: Status: task.StatusInProgress, } - if tp.Panes[0].Filter.Evaluate(noTagsTask, time.Now(), "testuser") { + if tp.Lanes[0].Filter.Evaluate(noTagsTask, time.Now(), "testuser") { t.Error("Expected filter to NOT match task without matching tags") } } @@ -133,7 +133,7 @@ func TestPluginWithStatusInFilter(t *testing.T) { pluginYAML := ` name: In Progress Work key: W -panes: +lanes: - name: Active filter: status IN ['ready', 'in_progress', 'in_progress'] ` @@ -167,7 +167,7 @@ panes: Status: tc.status, } - result := tp.Panes[0].Filter.Evaluate(task, time.Now(), "testuser") + result := tp.Lanes[0].Filter.Evaluate(task, time.Now(), "testuser") if result != tc.expect { t.Errorf("Expected %v for status %s, got %v", tc.expect, tc.status, result) } diff --git a/plugin/loader.go b/plugin/loader.go index 14d6ec6..4eaa2fe 100644 --- a/plugin/loader.go +++ b/plugin/loader.go @@ -141,7 +141,7 @@ func loadPluginFromRef(ref PluginRef) (Plugin, error) { Fetcher: ref.Fetcher, Text: ref.Text, URL: ref.URL, - Panes: ref.Panes, + Lanes: ref.Lanes, } source = "inline:" + ref.Name } diff --git a/plugin/loader_test.go b/plugin/loader_test.go index 18948d0..2564d05 100644 --- a/plugin/loader_test.go +++ b/plugin/loader_test.go @@ -15,7 +15,7 @@ func TestLoadPluginFromRef_FullyInline(t *testing.T) { Foreground: "#ffffff", Background: "#000000", Key: "I", - Panes: []PluginPaneConfig{ + Lanes: []PluginLaneConfig{ {Name: "Todo", Filter: "status = 'ready'"}, }, Sort: "Priority DESC", @@ -44,8 +44,8 @@ func TestLoadPluginFromRef_FullyInline(t *testing.T) { t.Errorf("Expected view mode 'expanded', got '%s'", tp.ViewMode) } - if len(tp.Panes) != 1 || tp.Panes[0].Filter == nil { - t.Fatal("Expected pane filter to be parsed") + if len(tp.Lanes) != 1 || tp.Lanes[0].Filter == nil { + t.Fatal("Expected lane filter to be parsed") } if len(tp.Sort) != 1 || tp.Sort[0].Field != "priority" || !tp.Sort[0].Descending { @@ -58,7 +58,7 @@ func TestLoadPluginFromRef_FullyInline(t *testing.T) { Status: taskpkg.StatusReady, } - if !tp.Panes[0].Filter.Evaluate(task, time.Now(), "testuser") { + if !tp.Lanes[0].Filter.Evaluate(task, time.Now(), "testuser") { t.Error("Expected filter to match todo task") } } @@ -66,7 +66,7 @@ func TestLoadPluginFromRef_FullyInline(t *testing.T) { func TestLoadPluginFromRef_InlineMinimal(t *testing.T) { ref := PluginRef{ Name: "Minimal", - Panes: []PluginPaneConfig{ + Lanes: []PluginLaneConfig{ {Name: "Bugs", Filter: "type = 'bug'"}, }, } @@ -85,8 +85,8 @@ func TestLoadPluginFromRef_InlineMinimal(t *testing.T) { t.Errorf("Expected name 'Minimal', got '%s'", tp.Name) } - if len(tp.Panes) != 1 || tp.Panes[0].Filter == nil { - t.Error("Expected pane filter to be parsed") + if len(tp.Lanes) != 1 || tp.Lanes[0].Filter == nil { + t.Error("Expected lane filter to be parsed") } } @@ -98,7 +98,7 @@ func TestLoadPluginFromRef_FileBased(t *testing.T) { foreground: "#ff0000" background: "#0000ff" key: T -panes: +lanes: - name: In Progress filter: status = 'in_progress' sort: Priority, UpdatedAt DESC @@ -143,7 +143,7 @@ func TestLoadPluginFromRef_Hybrid(t *testing.T) { foreground: "#ff0000" background: "#0000ff" key: L -panes: +lanes: - name: Todo filter: status = 'ready' sort: Priority @@ -175,8 +175,8 @@ view: compact t.Errorf("Expected name 'Base Plugin', got '%s'", tp.Name) } - if len(tp.Panes) != 1 || tp.Panes[0].Filter == nil { - t.Error("Expected pane filter from file") + if len(tp.Lanes) != 1 || tp.Lanes[0].Filter == nil { + t.Error("Expected lane filter from file") } // Overridden fields should be from inline @@ -197,7 +197,7 @@ func TestLoadPluginFromRef_HybridMultipleOverrides(t *testing.T) { foreground: "#ffffff" background: "#000000" key: M -panes: +lanes: - name: Todo filter: status = 'ready' sort: Priority @@ -211,7 +211,7 @@ view: compact ref := PluginRef{ File: pluginFile, // Use absolute path Key: "X", - Panes: []PluginPaneConfig{ + Lanes: []PluginLaneConfig{ {Name: "In Progress", Filter: "status = 'in_progress'"}, }, Sort: "UpdatedAt DESC", @@ -243,10 +243,10 @@ view: compact ID: "TIKI-1", Status: taskpkg.StatusInProgress, } - if len(tp.Panes) != 1 || tp.Panes[0].Filter == nil { - t.Fatal("Expected overridden pane filter") + if len(tp.Lanes) != 1 || tp.Lanes[0].Filter == nil { + t.Fatal("Expected overridden lane filter") } - if !tp.Panes[0].Filter.Evaluate(task, time.Now(), "testuser") { + if !tp.Lanes[0].Filter.Evaluate(task, time.Now(), "testuser") { t.Error("Expected overridden filter to match in_progress task") } @@ -254,7 +254,7 @@ view: compact ID: "TIKI-2", Status: taskpkg.StatusReady, } - if tp.Panes[0].Filter.Evaluate(todoTask, time.Now(), "testuser") { + if tp.Lanes[0].Filter.Evaluate(todoTask, time.Now(), "testuser") { t.Error("Expected overridden filter to NOT match todo task") } } @@ -277,7 +277,7 @@ func TestLoadPluginFromRef_MissingFile(t *testing.T) { func TestLoadPluginFromRef_NoName(t *testing.T) { // Inline plugin without name ref := PluginRef{ - Panes: []PluginPaneConfig{ + Lanes: []PluginLaneConfig{ {Name: "Todo", Filter: "status = 'ready'"}, }, } diff --git a/plugin/merger.go b/plugin/merger.go index 180c8b8..765ce3f 100644 --- a/plugin/merger.go +++ b/plugin/merger.go @@ -19,7 +19,7 @@ type pluginFileConfig struct { Fetcher string `yaml:"fetcher"` Text string `yaml:"text"` URL string `yaml:"url"` - Panes []PluginPaneConfig `yaml:"panes"` + Lanes []PluginLaneConfig `yaml:"lanes"` Actions []PluginActionConfig `yaml:"actions"` } @@ -61,8 +61,8 @@ func mergePluginConfigs(base pluginFileConfig, overrides PluginRef) pluginFileCo if overrides.URL != "" { result.URL = overrides.URL } - if len(overrides.Panes) > 0 { - result.Panes = overrides.Panes + if len(overrides.Lanes) > 0 { + result.Lanes = overrides.Lanes } if len(overrides.Actions) > 0 { result.Actions = overrides.Actions @@ -91,7 +91,7 @@ func mergePluginDefinitions(base Plugin, override Plugin) Plugin { ConfigIndex: overrideTiki.ConfigIndex, // Use override's config index Type: baseTiki.Type, }, - Panes: baseTiki.Panes, + Lanes: baseTiki.Lanes, Sort: baseTiki.Sort, ViewMode: baseTiki.ViewMode, Actions: baseTiki.Actions, @@ -109,8 +109,8 @@ func mergePluginDefinitions(base Plugin, override Plugin) Plugin { if overrideTiki.Background != tcell.ColorDefault { result.Background = overrideTiki.Background } - if len(overrideTiki.Panes) > 0 { - result.Panes = overrideTiki.Panes + if len(overrideTiki.Lanes) > 0 { + result.Lanes = overrideTiki.Lanes } if overrideTiki.Sort != nil { result.Sort = overrideTiki.Sort @@ -148,7 +148,7 @@ func validatePluginRef(ref PluginRef) error { ref.Sort != "" || ref.Foreground != "" || ref.Background != "" || ref.View != "" || ref.Type != "" || ref.Fetcher != "" || ref.Text != "" || ref.URL != "" || - len(ref.Panes) > 0 || len(ref.Actions) > 0 + len(ref.Lanes) > 0 || len(ref.Actions) > 0 if !hasContent { return fmt.Errorf("inline plugin '%s' has no configuration fields", ref.Name) diff --git a/plugin/merger_test.go b/plugin/merger_test.go index 8be5d1d..84c75c5 100644 --- a/plugin/merger_test.go +++ b/plugin/merger_test.go @@ -14,7 +14,7 @@ func TestMergePluginConfigs(t *testing.T) { Foreground: "#ff0000", Background: "#0000ff", Key: "L", - Panes: []PluginPaneConfig{ + Lanes: []PluginLaneConfig{ {Name: "Todo", Filter: "status = 'ready'"}, }, Sort: "Priority", @@ -32,8 +32,8 @@ func TestMergePluginConfigs(t *testing.T) { if result.Name != "Base" { t.Errorf("Expected name 'Base', got '%s'", result.Name) } - if len(result.Panes) != 1 || result.Panes[0].Filter != "status = 'ready'" { - t.Errorf("Expected panes from base, got %+v", result.Panes) + if len(result.Lanes) != 1 || result.Lanes[0].Filter != "status = 'ready'" { + t.Errorf("Expected lanes from base, got %+v", result.Lanes) } if result.Foreground != "#ff0000" { t.Errorf("Expected foreground from base, got '%s'", result.Foreground) @@ -54,7 +54,7 @@ func TestMergePluginConfigs_AllOverrides(t *testing.T) { Foreground: "#ff0000", Background: "#0000ff", Key: "L", - Panes: []PluginPaneConfig{ + Lanes: []PluginLaneConfig{ {Name: "Todo", Filter: "status = 'ready'"}, }, Sort: "Priority", @@ -66,7 +66,7 @@ func TestMergePluginConfigs_AllOverrides(t *testing.T) { Foreground: "#00ff00", Background: "#000000", Key: "O", - Panes: []PluginPaneConfig{ + Lanes: []PluginLaneConfig{ {Name: "Done", Filter: "status = 'done'"}, }, Sort: "UpdatedAt DESC", @@ -88,8 +88,8 @@ func TestMergePluginConfigs_AllOverrides(t *testing.T) { if result.Key != "O" { t.Errorf("Expected key 'O', got '%s'", result.Key) } - if len(result.Panes) != 1 || result.Panes[0].Filter != "status = 'done'" { - t.Errorf("Expected pane filter 'status = 'done'', got %+v", result.Panes) + if len(result.Lanes) != 1 || result.Lanes[0].Filter != "status = 'done'" { + t.Errorf("Expected lane filter 'status = 'done'', got %+v", result.Lanes) } if result.Sort != "UpdatedAt DESC" { t.Errorf("Expected sort 'UpdatedAt DESC', got '%s'", result.Sort) @@ -125,7 +125,7 @@ func TestValidatePluginRef_Hybrid(t *testing.T) { func TestValidatePluginRef_InlineValid(t *testing.T) { ref := PluginRef{ Name: "Test", - Panes: []PluginPaneConfig{ + Lanes: []PluginLaneConfig{ {Name: "Todo", Filter: "status = 'ready'"}, }, } @@ -138,7 +138,7 @@ func TestValidatePluginRef_InlineValid(t *testing.T) { func TestValidatePluginRef_InlineNoName(t *testing.T) { ref := PluginRef{ - Panes: []PluginPaneConfig{ + Lanes: []PluginLaneConfig{ {Name: "Todo", Filter: "status = 'ready'"}, }, } @@ -183,7 +183,7 @@ func TestMergePluginDefinitions_TikiToTiki(t *testing.T) { Background: tcell.ColorBlue, Type: "tiki", }, - Panes: []TikiPane{ + Lanes: []TikiLane{ {Name: "Todo", Columns: 1, Filter: baseFilter}, }, Sort: baseSort, @@ -203,7 +203,7 @@ func TestMergePluginDefinitions_TikiToTiki(t *testing.T) { ConfigIndex: 1, Type: "tiki", }, - Panes: []TikiPane{ + Lanes: []TikiLane{ {Name: "Bugs", Columns: 1, Filter: overrideFilter}, }, Sort: nil, @@ -229,8 +229,8 @@ func TestMergePluginDefinitions_TikiToTiki(t *testing.T) { if resultTiki.ViewMode != "expanded" { t.Errorf("Expected expanded view, got %q", resultTiki.ViewMode) } - if len(resultTiki.Panes) != 1 || resultTiki.Panes[0].Filter == nil { - t.Error("Expected pane filter to be overridden") + if len(resultTiki.Lanes) != 1 || resultTiki.Lanes[0].Filter == nil { + t.Error("Expected lane filter to be overridden") } // Check that base sort is kept when override has nil @@ -253,7 +253,7 @@ func TestMergePluginDefinitions_PreservesModifier(t *testing.T) { Background: tcell.ColorDefault, Type: "tiki", }, - Panes: []TikiPane{ + Lanes: []TikiLane{ {Name: "Todo", Columns: 1, Filter: baseFilter}, }, } diff --git a/plugin/parser.go b/plugin/parser.go index 7770163..12ab83b 100644 --- a/plugin/parser.go +++ b/plugin/parser.go @@ -52,8 +52,8 @@ func parsePluginConfig(cfg pluginFileConfig, source string) (Plugin, error) { if cfg.View != "" { return nil, fmt.Errorf("doki plugin cannot have 'view'") } - if len(cfg.Panes) > 0 { - return nil, fmt.Errorf("doki plugin cannot have 'panes'") + if len(cfg.Lanes) > 0 { + return nil, fmt.Errorf("doki plugin cannot have 'lanes'") } if len(cfg.Actions) > 0 { return nil, fmt.Errorf("doki plugin cannot have 'actions'") @@ -90,35 +90,35 @@ func parsePluginConfig(cfg pluginFileConfig, source string) (Plugin, error) { if cfg.Filter != "" { return nil, fmt.Errorf("tiki plugin cannot have 'filter'") } - if len(cfg.Panes) == 0 { - return nil, fmt.Errorf("tiki plugin requires 'panes'") + if len(cfg.Lanes) == 0 { + return nil, fmt.Errorf("tiki plugin requires 'lanes'") } - if len(cfg.Panes) > 10 { - return nil, fmt.Errorf("tiki plugin has too many panes (%d), max is 10", len(cfg.Panes)) + if len(cfg.Lanes) > 10 { + return nil, fmt.Errorf("tiki plugin has too many lanes (%d), max is 10", len(cfg.Lanes)) } - panes := make([]TikiPane, 0, len(cfg.Panes)) - for i, pane := range cfg.Panes { - if pane.Name == "" { - return nil, fmt.Errorf("pane %d missing name", i) + lanes := make([]TikiLane, 0, len(cfg.Lanes)) + for i, lane := range cfg.Lanes { + if lane.Name == "" { + return nil, fmt.Errorf("lane %d missing name", i) } - columns := pane.Columns + columns := lane.Columns if columns == 0 { columns = 1 } if columns < 0 { - return nil, fmt.Errorf("pane %q has invalid columns %d", pane.Name, columns) + return nil, fmt.Errorf("lane %q has invalid columns %d", lane.Name, columns) } - filterExpr, err := filter.ParseFilter(pane.Filter) + filterExpr, err := filter.ParseFilter(lane.Filter) if err != nil { - return nil, fmt.Errorf("parsing filter for pane %q: %w", pane.Name, err) + return nil, fmt.Errorf("parsing filter for lane %q: %w", lane.Name, err) } - action, err := ParsePaneAction(pane.Action) + action, err := ParseLaneAction(lane.Action) if err != nil { - return nil, fmt.Errorf("parsing action for pane %q: %w", pane.Name, err) + return nil, fmt.Errorf("parsing action for lane %q: %w", lane.Name, err) } - panes = append(panes, TikiPane{ - Name: pane.Name, + lanes = append(lanes, TikiLane{ + Name: lane.Name, Columns: columns, Filter: filterExpr, Action: action, @@ -139,7 +139,7 @@ func parsePluginConfig(cfg pluginFileConfig, source string) (Plugin, error) { return &TikiPlugin{ BasePlugin: base, - Panes: panes, + Lanes: lanes, Sort: sortRules, ViewMode: cfg.View, Actions: actions, @@ -185,7 +185,7 @@ func parsePluginActions(configs []PluginActionConfig) ([]PluginAction, error) { return nil, fmt.Errorf("action %d (key %q) missing 'action'", i, cfg.Key) } - action, err := ParsePaneAction(cfg.Action) + action, err := ParseLaneAction(cfg.Action) if err != nil { return nil, fmt.Errorf("parsing action %d (key %q): %w", i, cfg.Key, err) } diff --git a/plugin/parser_test.go b/plugin/parser_test.go index dfa1f87..24cf1f6 100644 --- a/plugin/parser_test.go +++ b/plugin/parser_test.go @@ -164,7 +164,7 @@ func TestParsePluginConfig_DefaultTikiType(t *testing.T) { cfg := pluginFileConfig{ Name: "Test", Key: "T", - Panes: []PluginPaneConfig{ + Lanes: []PluginLaneConfig{ {Name: "Todo", Filter: "status='ready'"}, }, // Type not specified, should default to "tiki" @@ -277,7 +277,7 @@ func TestParsePluginYAML_ValidTiki(t *testing.T) { name: Test Plugin key: T type: tiki -panes: +lanes: - name: Todo columns: 4 filter: status = 'ready' @@ -305,12 +305,12 @@ background: "#0000ff" t.Errorf("Expected view mode 'expanded', got %q", tikiPlugin.ViewMode) } - if len(tikiPlugin.Panes) != 1 { - t.Fatalf("Expected 1 pane, got %d", len(tikiPlugin.Panes)) + if len(tikiPlugin.Lanes) != 1 { + t.Fatalf("Expected 1 lane, got %d", len(tikiPlugin.Lanes)) } - if tikiPlugin.Panes[0].Columns != 4 { - t.Errorf("Expected pane columns 4, got %d", tikiPlugin.Panes[0].Columns) + if tikiPlugin.Lanes[0].Columns != 4 { + t.Errorf("Expected lane columns 4, got %d", tikiPlugin.Lanes[0].Columns) } } @@ -429,7 +429,7 @@ func TestParsePluginYAML_TikiWithActions(t *testing.T) { yamlData := []byte(` name: Test key: T -panes: +lanes: - name: Backlog filter: status = 'backlog' actions: diff --git a/testutil/integration_helpers.go b/testutil/integration_helpers.go index f2ae80c..27beb95 100644 --- a/testutil/integration_helpers.go +++ b/testutil/integration_helpers.go @@ -303,11 +303,11 @@ func (ta *TestApp) LoadPlugins() error { // Create appropriate controller based on plugin type if tp, ok := p.(*plugin.TikiPlugin); ok { - columns := make([]int, len(tp.Panes)) - for i, pane := range tp.Panes { - columns[i] = pane.Columns + columns := make([]int, len(tp.Lanes)) + for i, lane := range tp.Lanes { + columns[i] = lane.Columns } - pc.SetPaneLayout(columns) + pc.SetLaneLayout(columns) pluginControllers[p.GetName()] = controller.NewPluginController( ta.TaskStore, pc, tp, ta.NavController, ) diff --git a/view/factory.go b/view/factory.go index 4f7983b..62b7ac6 100644 --- a/view/factory.go +++ b/view/factory.go @@ -87,8 +87,8 @@ func (f *ViewFactory) CreateView(viewID model.ViewID, params map[string]interfac f.taskStore, pluginConfig, tikiPlugin, - tikiController.GetFilteredTasksForPane, - tikiController.EnsureFirstNonEmptyPaneSelection, + tikiController.GetFilteredTasksForLane, + tikiController.EnsureFirstNonEmptyLaneSelection, tikiController.GetActionRegistry(), ) } else { diff --git a/view/gradient_caption_row.go b/view/gradient_caption_row.go index f2b15c9..17bc1fe 100644 --- a/view/gradient_caption_row.go +++ b/view/gradient_caption_row.go @@ -8,47 +8,47 @@ import ( "github.com/rivo/tview" ) -// GradientCaptionRow is a tview primitive that renders multiple pane captions +// GradientCaptionRow is a tview primitive that renders multiple lane captions // with a continuous horizontal background gradient spanning the entire screen width type GradientCaptionRow struct { *tview.Box - paneNames []string + laneNames []string bgColor tcell.Color // original background color from plugin gradient config.Gradient // computed gradient (for truecolor/256-color terminals) textColor tcell.Color } // NewGradientCaptionRow creates a new gradient caption row widget -func NewGradientCaptionRow(paneNames []string, bgColor tcell.Color, textColor tcell.Color) *GradientCaptionRow { +func NewGradientCaptionRow(laneNames []string, bgColor tcell.Color, textColor tcell.Color) *GradientCaptionRow { return &GradientCaptionRow{ Box: tview.NewBox(), - paneNames: paneNames, + laneNames: laneNames, bgColor: bgColor, gradient: computeCaptionGradient(bgColor), textColor: textColor, } } -// Draw renders all pane captions with a screen-wide gradient background +// Draw renders all lane captions with a screen-wide gradient background func (gcr *GradientCaptionRow) Draw(screen tcell.Screen) { gcr.DrawForSubclass(screen, gcr) x, y, width, height := gcr.GetInnerRect() - if width <= 0 || height <= 0 || len(gcr.paneNames) == 0 { + if width <= 0 || height <= 0 || len(gcr.laneNames) == 0 { return } - // Calculate pane width (equal distribution) - numPanes := len(gcr.paneNames) - paneWidth := width / numPanes + // Calculate lane width (equal distribution) + numLanes := len(gcr.laneNames) + laneWidth := width / numLanes - // Convert all pane names to runes for Unicode handling - paneRunes := make([][]rune, numPanes) - for i, name := range gcr.paneNames { - paneRunes[i] = []rune(name) + // Convert all lane names to runes for Unicode handling + laneRunes := make([][]rune, numLanes) + for i, name := range gcr.laneNames { + laneRunes[i] = []rune(name) } - // Render each pane position across the screen + // Render each lane position across the screen for col := 0; col < width; col++ { // Calculate gradient color based on screen position (edges to center gradient) // Distance from center: 0.0 at center, 1.0 at edges @@ -75,34 +75,34 @@ func (gcr *GradientCaptionRow) Draw(screen tcell.Screen) { bgColor = gradient.InterpolateColor(gcr.gradient, 1.0) } - // Determine which pane this position belongs to - paneIndex := col / paneWidth - if paneIndex >= numPanes { - paneIndex = numPanes - 1 + // Determine which lane this position belongs to + laneIndex := col / laneWidth + if laneIndex >= numLanes { + laneIndex = numLanes - 1 } - // Calculate position within this pane - paneStartX := paneIndex * paneWidth - paneEndX := paneStartX + paneWidth - if paneIndex == numPanes-1 { - paneEndX = width // Last pane extends to screen edge + // Calculate position within this lane + laneStartX := laneIndex * laneWidth + laneEndX := laneStartX + laneWidth + if laneIndex == numLanes-1 { + laneEndX = width // Last lane extends to screen edge } - currentPaneWidth := paneEndX - paneStartX - posInPane := col - paneStartX + currentLaneWidth := laneEndX - laneStartX + posInLane := col - laneStartX - // Get the text for this pane - textRunes := paneRunes[paneIndex] + // Get the text for this lane + textRunes := laneRunes[laneIndex] textWidth := len(textRunes) - // Calculate centered text position within pane + // Calculate centered text position within lane textStartPos := 0 - if textWidth < currentPaneWidth { - textStartPos = (currentPaneWidth - textWidth) / 2 + if textWidth < currentLaneWidth { + textStartPos = (currentLaneWidth - textWidth) / 2 } // Determine if we should render a character at this position char := ' ' - textIndex := posInPane - textStartPos + textIndex := posInLane - textStartPos if textIndex >= 0 && textIndex < textWidth { char = textRunes[textIndex] } diff --git a/view/help/custom.md b/view/help/custom.md index cc13bf0..71914e8 100644 --- a/view/help/custom.md +++ b/view/help/custom.md @@ -11,7 +11,7 @@ how Backlog is defined: foreground: "#5fff87" background: "#005f00" key: "F3" - panes: + lanes: - name: Backlog columns: 4 filter: status = 'backlog' @@ -21,7 +21,7 @@ how Backlog is defined: action: status = 'ready' sort: Priority, ID ``` -that translates to - show all tikis in the status `backlog`, sort by priority and then by ID arranged visually in 4 columns in a single pane. +that translates to - show all tikis in the status `backlog`, sort by priority and then by ID arranged visually in 4 columns in a single lane. The `actions` section defines a keyboard shortcut `b` that moves the selected tiki to the board by setting its status to `ready` You define the name, caption colors, hotkey, tiki filter and sorting. Save this into a yaml file and add this line: @@ -47,12 +47,12 @@ Likewise the documentation is just a plugin: that translates to - show `index.md` file located under `.doc/doki` installed in the same way -## Multi-pane plugin +## Multi-lane plugin -Backlog is a pretty simple plugin in that it displays all tikis in a single pane. Multi-pane tiki plugins offer functionality -similar to that of the board. You can define multiple panes per view and move tikis around with Shift-Left/Shift-Right -much like in the board. You can create a multi-pane plugin by defining multiple panes in its definition and assigning -actions to each pane. An action defines what happens when you move a tiki into the pane. Here is a multi-pane plugin +Backlog is a pretty simple plugin in that it displays all tikis in a single lane. Multi-lane tiki plugins offer functionality +similar to that of the board. You can define multiple lanes per view and move tikis around with Shift-Left/Shift-Right +much like in the board. You can create a multi-lane plugin by defining multiple lanes in its definition and assigning +actions to each lane. An action defines what happens when you move a tiki into the lane. Here is a multi-lane plugin definition that roughly mimics the board: ```yaml @@ -61,7 +61,7 @@ foreground: "#5fff87" background: "#005f00" key: "F4" sort: Priority, Title -panes: +lanes: - name: Ready columns: 1 filter: status = 'ready' @@ -82,7 +82,7 @@ panes: ## Plugin actions -In addition to pane actions that trigger when moving tikis between panes, you can define plugin-level actions +In addition to lane actions that trigger when moving tikis between lanes, you can define plugin-level actions that apply to the currently selected tiki via a keyboard shortcut. These shortcuts are displayed in the header when the plugin is active. ```yaml @@ -98,7 +98,7 @@ actions: Each action has: - `key` - a single printable character used as the keyboard shortcut - `label` - description shown in the header -- `action` - an action expression (same syntax as pane actions, see below) +- `action` - an action expression (same syntax as lane actions, see below) When the shortcut key is pressed, the action is applied to the currently selected tiki. For example, pressing `b` in the Backlog plugin changes the selected tiki's status to `ready`, @@ -106,7 +106,7 @@ effectively moving it to the board. ## Action expression -The `action: status = 'backlog'` statement in a plugin is an action to be run when a tiki is moved into the pane. Here `=` +The `action: status = 'backlog'` statement in a plugin is an action to be run when a tiki is moved into the lane. Here `=` means `assign` so status is assigned `backlog` when the tiki is moved. Likewise you can manipulate tags using `+-` (add) or `-=` (remove) expressions. For example, `tags += [idea, UI]` adds `idea` and `UI` tags to a tiki diff --git a/view/tiki_plugin_view.go b/view/tiki_plugin_view.go index 9a6e283..4720176 100644 --- a/view/tiki_plugin_view.go +++ b/view/tiki_plugin_view.go @@ -16,20 +16,20 @@ import ( // Note: tcell import is still used for pv.pluginDef.Background/Foreground checks -// PluginView renders a filtered/sorted list of tasks across panes +// PluginView renders a filtered/sorted list of tasks across lanes type PluginView struct { root *tview.Flex titleBar tview.Primitive searchHelper *SearchHelper - panes *tview.Flex - paneBoxes []*ScrollableList + lanes *tview.Flex + laneBoxes []*ScrollableList taskStore store.Store pluginConfig *model.PluginConfig pluginDef *plugin.TikiPlugin registry *controller.ActionRegistry storeListenerID int selectionListenerID int - getPaneTasks func(pane int) []*task.Task // injected from controller + getLaneTasks func(lane int) []*task.Task // injected from controller ensureSelection func() bool // injected from controller } @@ -38,7 +38,7 @@ func NewPluginView( taskStore store.Store, pluginConfig *model.PluginConfig, pluginDef *plugin.TikiPlugin, - getPaneTasks func(pane int) []*task.Task, + getLaneTasks func(lane int) []*task.Task, ensureSelection func() bool, registry *controller.ActionRegistry, ) *PluginView { @@ -47,7 +47,7 @@ func NewPluginView( pluginConfig: pluginConfig, pluginDef: pluginDef, registry: registry, - getPaneTasks: getPaneTasks, + getLaneTasks: getLaneTasks, ensureSelection: ensureSelection, } @@ -62,18 +62,18 @@ func (pv *PluginView) build() { if pv.pluginDef.Foreground != tcell.ColorDefault { textColor = pv.pluginDef.Foreground } - paneNames := make([]string, len(pv.pluginDef.Panes)) - for i, pane := range pv.pluginDef.Panes { - paneNames[i] = pane.Name + laneNames := make([]string, len(pv.pluginDef.Lanes)) + for i, lane := range pv.pluginDef.Lanes { + laneNames[i] = lane.Name } - pv.titleBar = NewGradientCaptionRow(paneNames, pv.pluginDef.Background, textColor) + pv.titleBar = NewGradientCaptionRow(laneNames, pv.pluginDef.Background, textColor) - // panes container (rows) - pv.panes = tview.NewFlex().SetDirection(tview.FlexColumn) - pv.paneBoxes = make([]*ScrollableList, 0, len(pv.pluginDef.Panes)) + // lanes container (rows) + pv.lanes = tview.NewFlex().SetDirection(tview.FlexColumn) + pv.laneBoxes = make([]*ScrollableList, 0, len(pv.pluginDef.Lanes)) - // search helper - focus returns to panes container - pv.searchHelper = NewSearchHelper(pv.panes) + // search helper - focus returns to lanes container + pv.searchHelper = NewSearchHelper(pv.lanes) pv.searchHelper.SetCancelHandler(func() { pv.HideSearch() }) @@ -95,9 +95,9 @@ func (pv *PluginView) rebuildLayout() { query := pv.pluginConfig.GetSearchQuery() pv.searchHelper.ShowSearch(query) pv.root.AddItem(pv.searchHelper.GetSearchBox(), config.SearchBoxHeight, 0, false) - pv.root.AddItem(pv.panes, 0, 1, false) + pv.root.AddItem(pv.lanes, 0, 1, false) } else { - pv.root.AddItem(pv.panes, 0, 1, true) + pv.root.AddItem(pv.lanes, 0, 1, true) } } @@ -112,36 +112,36 @@ func (pv *PluginView) refresh() { if viewMode == model.ViewModeExpanded { itemHeight = config.TaskBoxHeightExpanded } - selectedPane := pv.pluginConfig.GetSelectedPane() + selectedLane := pv.pluginConfig.GetSelectedLane() - if len(pv.paneBoxes) != len(pv.pluginDef.Panes) { - pv.paneBoxes = make([]*ScrollableList, 0, len(pv.pluginDef.Panes)) - for range pv.pluginDef.Panes { - pv.paneBoxes = append(pv.paneBoxes, NewScrollableList()) + if len(pv.laneBoxes) != len(pv.pluginDef.Lanes) { + pv.laneBoxes = make([]*ScrollableList, 0, len(pv.pluginDef.Lanes)) + for range pv.pluginDef.Lanes { + pv.laneBoxes = append(pv.laneBoxes, NewScrollableList()) } } - pv.panes.Clear() + pv.lanes.Clear() - for paneIdx := range pv.pluginDef.Panes { - paneContainer := pv.paneBoxes[paneIdx] - paneContainer.SetItemHeight(itemHeight) - paneContainer.Clear() + for laneIdx := range pv.pluginDef.Lanes { + laneContainer := pv.laneBoxes[laneIdx] + laneContainer.SetItemHeight(itemHeight) + laneContainer.Clear() - isSelectedPane := paneIdx == selectedPane - pv.panes.AddItem(paneContainer, 0, 1, isSelectedPane) + isSelectedLane := laneIdx == selectedLane + pv.lanes.AddItem(laneContainer, 0, 1, isSelectedLane) - tasks := pv.getPaneTasks(paneIdx) - if isSelectedPane { + tasks := pv.getLaneTasks(laneIdx) + if isSelectedLane { pv.pluginConfig.ClampSelection(len(tasks)) } if len(tasks) == 0 { - paneContainer.SetSelection(-1) + laneContainer.SetSelection(-1) continue } - columns := pv.pluginConfig.GetColumnsForPane(paneIdx) - selectedIndex := pv.pluginConfig.GetSelectedIndexForPane(paneIdx) + columns := pv.pluginConfig.GetColumnsForLane(laneIdx) + selectedIndex := pv.pluginConfig.GetSelectedIndexForLane(laneIdx) selectedRow := selectedIndex / columns numRows := (len(tasks) + columns - 1) / columns @@ -151,7 +151,7 @@ func (pv *PluginView) refresh() { idx := row*columns + col if idx < len(tasks) { task := tasks[idx] - isSelected := isSelectedPane && idx == selectedIndex + isSelected := isSelectedLane && idx == selectedIndex var taskBox *tview.Frame if viewMode == model.ViewModeCompact { taskBox = CreateCompactTaskBox(task, isSelected, config.GetColors()) @@ -164,17 +164,17 @@ func (pv *PluginView) refresh() { rowFlex.AddItem(spacer, 0, 1, false) } } - paneContainer.AddItem(rowFlex) + laneContainer.AddItem(rowFlex) } - if isSelectedPane { - paneContainer.SetSelection(selectedRow) + if isSelectedLane { + laneContainer.SetSelection(selectedRow) } else { - paneContainer.SetSelection(-1) + laneContainer.SetSelection(-1) } - // Sync scroll offset from view to model for later pane navigation - pv.pluginConfig.SetScrollOffsetForPane(paneIdx, paneContainer.GetScrollOffset()) + // Sync scroll offset from view to model for later lane navigation + pv.pluginConfig.SetScrollOffsetForLane(laneIdx, laneContainer.GetScrollOffset()) } } @@ -208,9 +208,9 @@ func (pv *PluginView) OnBlur() { // GetSelectedID returns the selected task ID func (pv *PluginView) GetSelectedID() string { - pane := pv.pluginConfig.GetSelectedPane() - tasks := pv.getPaneTasks(pane) - idx := pv.pluginConfig.GetSelectedIndexForPane(pane) + lane := pv.pluginConfig.GetSelectedLane() + tasks := pv.getLaneTasks(lane) + idx := pv.pluginConfig.GetSelectedIndexForLane(lane) if idx >= 0 && idx < len(tasks) { return tasks[idx].ID } @@ -219,12 +219,12 @@ func (pv *PluginView) GetSelectedID() string { // SetSelectedID sets the selection to a task func (pv *PluginView) SetSelectedID(id string) { - for pane := range pv.pluginDef.Panes { - tasks := pv.getPaneTasks(pane) + for lane := range pv.pluginDef.Lanes { + tasks := pv.getLaneTasks(lane) for i, t := range tasks { if t.ID == id { - pv.pluginConfig.SetSelectedPane(pane) - pv.pluginConfig.SetSelectedIndexForPane(pane, i) + pv.pluginConfig.SetSelectedLane(lane) + pv.pluginConfig.SetSelectedIndexForLane(lane, i) return } } @@ -244,7 +244,7 @@ func (pv *PluginView) ShowSearch() tview.Primitive { pv.root.Clear() pv.root.AddItem(pv.titleBar, 1, 0, false) pv.root.AddItem(pv.searchHelper.GetSearchBox(), config.SearchBoxHeight, 0, true) - pv.root.AddItem(pv.panes, 0, 1, false) + pv.root.AddItem(pv.lanes, 0, 1, false) return searchBox } @@ -263,7 +263,7 @@ func (pv *PluginView) HideSearch() { // Rebuild layout without search box pv.root.Clear() pv.root.AddItem(pv.titleBar, 1, 0, false) - pv.root.AddItem(pv.panes, 0, 1, true) + pv.root.AddItem(pv.lanes, 0, 1, true) } // IsSearchVisible returns whether the search box is currently visible @@ -289,8 +289,8 @@ func (pv *PluginView) SetFocusSetter(setter func(p tview.Primitive)) { // GetStats returns stats for the header (Total count of filtered tasks) func (pv *PluginView) GetStats() []store.Stat { total := 0 - for pane := range pv.pluginDef.Panes { - tasks := pv.getPaneTasks(pane) + for lane := range pv.pluginDef.Lanes { + tasks := pv.getLaneTasks(lane) total += len(tasks) } return []store.Stat{ diff --git a/view/tiki_plugin_view_test.go b/view/tiki_plugin_view_test.go index 858e8e7..344c370 100644 --- a/view/tiki_plugin_view_test.go +++ b/view/tiki_plugin_view_test.go @@ -15,14 +15,14 @@ import ( func TestPluginViewRefreshPreservesScrollOffset(t *testing.T) { taskStore := store.NewInMemoryStore() pluginConfig := model.NewPluginConfig("TestPlugin") - pluginConfig.SetPaneLayout([]int{1}) + pluginConfig.SetLaneLayout([]int{1}) pluginDef := &plugin.TikiPlugin{ BasePlugin: plugin.BasePlugin{ Name: "TestPlugin", }, - Panes: []plugin.TikiPane{ - {Name: "Pane", Columns: 1}, + Lanes: []plugin.TikiLane{ + {Name: "Lane", Columns: 1}, }, } @@ -36,35 +36,35 @@ func TestPluginViewRefreshPreservesScrollOffset(t *testing.T) { } } - pv := NewPluginView(taskStore, pluginConfig, pluginDef, func(pane int) []*task.Task { + pv := NewPluginView(taskStore, pluginConfig, pluginDef, func(lane int) []*task.Task { return tasks }, nil, controller.PluginViewActions()) - if len(pv.paneBoxes) != 1 { - t.Fatalf("expected 1 pane box, got %d", len(pv.paneBoxes)) + if len(pv.laneBoxes) != 1 { + t.Fatalf("expected 1 lane box, got %d", len(pv.laneBoxes)) } - pane := pv.paneBoxes[0] + lane := pv.laneBoxes[0] itemHeight := config.TaskBoxHeight - pane.SetRect(0, 0, 80, itemHeight*5) + lane.SetRect(0, 0, 80, itemHeight*5) - pluginConfig.SetSelectedIndexForPane(0, len(tasks)-1) + pluginConfig.SetSelectedIndexForLane(0, len(tasks)-1) pv.refresh() expectedScrollOffset := len(tasks) - 5 - if pane.scrollOffset != expectedScrollOffset { - t.Fatalf("expected scrollOffset %d, got %d", expectedScrollOffset, pane.scrollOffset) + if lane.scrollOffset != expectedScrollOffset { + t.Fatalf("expected scrollOffset %d, got %d", expectedScrollOffset, lane.scrollOffset) } - paneBefore := pane - pluginConfig.SetSelectedIndexForPane(0, len(tasks)-2) + laneBefore := lane + pluginConfig.SetSelectedIndexForLane(0, len(tasks)-2) pv.refresh() - if pv.paneBoxes[0] != paneBefore { - t.Fatalf("expected pane list to be reused across refresh") + if pv.laneBoxes[0] != laneBefore { + t.Fatalf("expected lane list to be reused across refresh") } - if pane.scrollOffset != expectedScrollOffset { - t.Fatalf("expected scrollOffset to remain %d, got %d", expectedScrollOffset, pane.scrollOffset) + if lane.scrollOffset != expectedScrollOffset { + t.Fatalf("expected scrollOffset to remain %d, got %d", expectedScrollOffset, lane.scrollOffset) } }