Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/roborev/tui/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ func (m model) handleMouseMsg(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
}
switch msg.Button {
case tea.MouseButtonWheelUp:
if m.currentView == viewColumnOptions {
if m.colOptionsIdx > 0 {
m.colOptionsIdx--
}
return m, nil
}
if m.currentView == viewTasks {
if m.fixSelectedIdx > 0 {
m.fixSelectedIdx--
Expand All @@ -52,6 +58,12 @@ func (m model) handleMouseMsg(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
}
return m.handleUpKey()
case tea.MouseButtonWheelDown:
if m.currentView == viewColumnOptions {
if m.colOptionsIdx < len(m.colOptionsList)-1 {
m.colOptionsIdx++
}
return m, nil
}
if m.currentView == viewTasks {
if m.fixSelectedIdx < len(m.fixJobs)-1 {
m.fixSelectedIdx++
Expand All @@ -65,6 +77,8 @@ func (m model) handleMouseMsg(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
m.handleQueueMouseClick(msg.X, msg.Y)
case viewTasks:
m.handleTasksMouseClick(msg.Y)
case viewColumnOptions:
return m.handleColumnOptionsMouseClick(msg.Y)
}
return m, nil
default:
Expand Down
109 changes: 76 additions & 33 deletions cmd/roborev/tui/handlers_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,45 +254,88 @@ func (m model) handleColumnOptionsInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
return m, nil
case " ", "enter":
if m.colOptionsIdx >= 0 && m.colOptionsIdx < len(m.colOptionsList) {
opt := &m.colOptionsList[m.colOptionsIdx]
if opt.id == colOptionBorders {
opt.enabled = !opt.enabled
m.colBordersOn = opt.enabled
m.colOptionsDirty = true
m.queueColGen++
m.taskColGen++
} else if opt.id == colOptionMouse {
opt.enabled = !opt.enabled
m.mouseEnabled = opt.enabled
m.colOptionsDirty = true
return m, mouseCaptureCmd(m.currentView, m.mouseEnabled)
} else if opt.id == colOptionTasksWorkflow {
opt.enabled = !opt.enabled
m.tasksEnabled = opt.enabled
m.colOptionsDirty = true
} else if m.colOptionsReturnView == viewTasks {
// Tasks view: no visibility toggle (all columns always shown)
return m, nil
} else {
opt.enabled = !opt.enabled
if opt.enabled {
delete(m.hiddenColumns, opt.id)
} else {
if m.hiddenColumns == nil {
m.hiddenColumns = map[int]bool{}
}
m.hiddenColumns[opt.id] = true
}
m.colOptionsDirty = true
m.queueColGen++
return m.toggleColumnOption(m.colOptionsIdx)
}
return m, nil
}

// toggleColumnOption toggles the option at the given index.
func (m model) toggleColumnOption(idx int) (tea.Model, tea.Cmd) {
if idx < 0 || idx >= len(m.colOptionsList) {
return m, nil
}
opt := &m.colOptionsList[idx]
if opt.id == colOptionBorders {
opt.enabled = !opt.enabled
m.colBordersOn = opt.enabled
m.colOptionsDirty = true
m.queueColGen++
m.taskColGen++
} else if opt.id == colOptionMouse {
opt.enabled = !opt.enabled
m.mouseEnabled = opt.enabled
m.colOptionsDirty = true
return m, mouseCaptureCmd(m.currentView, m.mouseEnabled)
} else if opt.id == colOptionTasksWorkflow {
opt.enabled = !opt.enabled
m.tasksEnabled = opt.enabled
m.colOptionsDirty = true
} else if m.colOptionsReturnView == viewTasks {
// Tasks view: no visibility toggle (all columns always shown)
return m, nil
} else {
opt.enabled = !opt.enabled
if opt.enabled {
delete(m.hiddenColumns, opt.id)
} else {
if m.hiddenColumns == nil {
m.hiddenColumns = map[int]bool{}
}
m.hiddenColumns[opt.id] = true
}
return m, nil
m.colOptionsDirty = true
m.queueColGen++
}
return m, nil
}

// handleColumnOptionsMouseClick handles mouse clicks in the column options modal.
// The layout is: title (line 0), blank (line 1), then one line per option,
// with a separator blank line inserted before the first sentinel option (borders).
func (m model) handleColumnOptionsMouseClick(y int) (tea.Model, tea.Cmd) {
// Find the index of the separator (blank line before borders toggle).
separatorAt := -1
for i, opt := range m.colOptionsList {
if opt.id == colOptionBorders && i > 0 {
separatorAt = i
break
}
}

// Options start at row 2 (after title + blank line).
row := y - 2
if row < 0 {
return m, nil
}

// Adjust for the separator line.
if separatorAt >= 0 {
if row == separatorAt {
return m, nil // clicked the blank separator line
}
if row > separatorAt {
row-- // account for the separator blank line
}
}

if row < 0 || row >= len(m.colOptionsList) {
return m, nil
}

m.colOptionsIdx = row
return m.toggleColumnOption(row)
}

// syncColumnOrderFromOptions updates m.columnOrder or m.taskColumnOrder
// from the current colOptionsList (excluding the borders toggle).
func (m *model) syncColumnOrderFromOptions() {
Expand Down
90 changes: 90 additions & 0 deletions cmd/roborev/tui/queue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1739,6 +1739,96 @@ func TestColumnOptionsToggle(t *testing.T) {
assert.False(t, m.hiddenColumns[colRef], "expected colRef removed from hiddenColumns")
}

func TestColumnOptionsMouseClick(t *testing.T) {
m := newTuiModel("localhost:7373")
m.jobs = []storage.ReviewJob{makeJob(1)}
m.currentView = viewQueue
m.hiddenColumns = map[int]bool{}
m.mouseEnabled = true

// Open column options modal.
m, _ = updateModel(t, m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'o'}})
require.Equal(t, viewColumnOptions, m.currentView)

// First option is at row 2 (title=0, blank=1, first option=2).
firstOpt := m.colOptionsList[0]
assert.True(t, firstOpt.enabled, "expected first column enabled initially")

// Click on the first option row to toggle it off.
m, _ = updateModel(t, m, mouseLeftClick(5, 2))
assert.False(t, m.colOptionsList[0].enabled, "expected first column disabled after click")
assert.Equal(t, 0, m.colOptionsIdx, "expected cursor on clicked row")

// Click again to toggle it back on.
m, _ = updateModel(t, m, mouseLeftClick(5, 2))
assert.True(t, m.colOptionsList[0].enabled, "expected first column re-enabled after second click")

// Click on the second option.
m, _ = updateModel(t, m, mouseLeftClick(5, 3))
assert.Equal(t, 1, m.colOptionsIdx, "expected cursor moved to second row")
}

func TestColumnOptionsMouseClickSentinel(t *testing.T) {
m := newTuiModel("localhost:7373")
m.jobs = []storage.ReviewJob{makeJob(1)}
m.currentView = viewQueue
m.hiddenColumns = map[int]bool{}
m.mouseEnabled = true

m, _ = updateModel(t, m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'o'}})
require.Equal(t, viewColumnOptions, m.currentView)

// Find the borders option (first sentinel, has a separator line before it).
bordersIdx := -1
for i, opt := range m.colOptionsList {
if opt.id == colOptionBorders {
bordersIdx = i
break
}
}
require.NotEqual(t, -1, bordersIdx)

// Borders is at row = 2 + bordersIdx + 1 (separator line).
bordersRow := 2 + bordersIdx + 1
initialBorders := m.colBordersOn

// Click the separator line (row just before borders) — should be a no-op.
separatorRow := 2 + bordersIdx
prevIdx := m.colOptionsIdx
prevLastEnabled := m.colOptionsList[bordersIdx-1].enabled
m, _ = updateModel(t, m, mouseLeftClick(5, separatorRow))
assert.Equal(t, prevIdx, m.colOptionsIdx, "separator click should not move cursor")
assert.Equal(t, prevLastEnabled, m.colOptionsList[bordersIdx-1].enabled,
"separator click should not toggle adjacent option")

// Click the actual borders row.
m, _ = updateModel(t, m, mouseLeftClick(5, bordersRow))
assert.Equal(t, bordersIdx, m.colOptionsIdx)
assert.NotEqual(t, initialBorders, m.colBordersOn, "expected borders toggled")
}

func TestColumnOptionsMouseWheel(t *testing.T) {
m := newTuiModel("localhost:7373")
m.jobs = []storage.ReviewJob{makeJob(1)}
m.currentView = viewQueue
m.hiddenColumns = map[int]bool{}
m.mouseEnabled = true

m, _ = updateModel(t, m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'o'}})
require.Equal(t, viewColumnOptions, m.currentView)
assert.Equal(t, 0, m.colOptionsIdx)

m, _ = updateModel(t, m, mouseWheelDown())
assert.Equal(t, 1, m.colOptionsIdx)

m, _ = updateModel(t, m, mouseWheelUp())
assert.Equal(t, 0, m.colOptionsIdx)

// Wheel up at top should stay at 0.
m, _ = updateModel(t, m, mouseWheelUp())
assert.Equal(t, 0, m.colOptionsIdx)
}

func TestMouseDisabledIgnoresQueueMouseInput(t *testing.T) {
m := newTuiModel("http://localhost")
m.currentView = tuiViewQueue
Expand Down
Loading