refactor page/list movement

Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-05-19 23:01:04 +02:00
parent 746f1afd1b
commit 924cb73afb
8 changed files with 115 additions and 254 deletions
+4 -16
View File
@@ -4,6 +4,7 @@ import (
"charm.land/bubbles/v2/key" "charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2" tea "charm.land/bubbletea/v2"
"github.com/anotherhadi/spilltea/internal/keys" "github.com/anotherhadi/spilltea/internal/keys"
"github.com/anotherhadi/spilltea/internal/util"
) )
func (e Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (e Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -12,12 +13,7 @@ func (e Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.MouseWheelMsg: case tea.MouseWheelMsg:
switch msg.Button { util.HandleMouseWheel(msg, &e.viewport)
case tea.MouseWheelUp:
e.viewport.SetYOffset(e.viewport.YOffset() - 1)
case tea.MouseWheelDown:
e.viewport.SetYOffset(e.viewport.YOffset() + 1)
}
case tea.KeyPressMsg: case tea.KeyPressMsg:
if e.searching { if e.searching {
@@ -61,17 +57,9 @@ func (e Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, g.Down): case key.Matches(msg, g.Down):
e.viewport.SetYOffset(e.viewport.YOffset() + 1) e.viewport.SetYOffset(e.viewport.YOffset() + 1)
case key.Matches(msg, g.ScrollUp): case key.Matches(msg, g.ScrollUp):
step := e.viewport.Height() / 2 util.ScrollViewport(&e.viewport, -1)
if step < 1 {
step = 1
}
e.viewport.SetYOffset(e.viewport.YOffset() - step)
case key.Matches(msg, g.ScrollDown): case key.Matches(msg, g.ScrollDown):
step := e.viewport.Height() / 2 util.ScrollViewport(&e.viewport, 1)
if step < 1 {
step = 1
}
e.viewport.SetYOffset(e.viewport.YOffset() + step)
case key.Matches(msg, g.Help): case key.Matches(msg, g.Help):
e.help.ShowAll = !e.help.ShowAll e.help.ShowAll = !e.help.ShowAll
e.SetSize(e.width, e.height) e.SetSize(e.width, e.height)
+8 -39
View File
@@ -6,6 +6,7 @@ import (
"charm.land/bubbles/v2/key" "charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2" tea "charm.land/bubbletea/v2"
"github.com/anotherhadi/spilltea/internal/keys" "github.com/anotherhadi/spilltea/internal/keys"
"github.com/anotherhadi/spilltea/internal/util"
) )
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -42,12 +43,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
case tea.MouseWheelMsg: case tea.MouseWheelMsg:
switch msg.Button { util.HandleMouseWheel(msg, &m.bodyViewport)
case tea.MouseWheelUp:
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() - 1)
case tea.MouseWheelDown:
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() + 1)
}
return m, nil return m, nil
case tea.KeyPressMsg: case tea.KeyPressMsg:
@@ -82,17 +78,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, RefreshCmd(m.database) return m, RefreshCmd(m.database)
} }
case key.Matches(msg, g.ScrollUp): case key.Matches(msg, g.ScrollUp):
step := m.bodyViewport.Height() / 2 util.ScrollViewport(&m.bodyViewport, -1)
if step < 1 {
step = 1
}
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() - step)
case key.Matches(msg, g.ScrollDown): case key.Matches(msg, g.ScrollDown):
step := m.bodyViewport.Height() / 2 util.ScrollViewport(&m.bodyViewport, 1)
if step < 1 {
step = 1
}
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() + step)
case key.Matches(msg, g.GotoTop): case key.Matches(msg, g.GotoTop):
m.cursor = 0 m.cursor = 0
m.pager.Page = 0 m.pager.Page = 0
@@ -100,38 +88,19 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.refreshBody() m.refreshBody()
case key.Matches(msg, g.GotoBottom): case key.Matches(msg, g.GotoBottom):
if len(m.findings) > 0 { m.cursor = util.CursorGotoBottom(len(m.findings))
m.cursor = len(m.findings) - 1 m.pager.Page = util.CursorGotoBottom(m.pager.TotalPages)
m.pager.Page = m.pager.TotalPages - 1
m.refreshListViewport() m.refreshListViewport()
m.refreshBody() m.refreshBody()
}
case key.Matches(msg, g.PrevPage): case key.Matches(msg, g.PrevPage):
step := m.pager.PerPage m.cursor = util.CursorMovePage(m.cursor, len(m.findings), m.pager.PerPage, false)
if step < 1 {
step = 1
}
m.cursor -= step
if m.cursor < 0 {
m.cursor = 0
}
m.pager.Page = m.cursor / m.pager.PerPage m.pager.Page = m.cursor / m.pager.PerPage
m.refreshListViewport() m.refreshListViewport()
m.refreshBody() m.refreshBody()
case key.Matches(msg, g.NextPage): case key.Matches(msg, g.NextPage):
step := m.pager.PerPage m.cursor = util.CursorMovePage(m.cursor, len(m.findings), m.pager.PerPage, true)
if step < 1 {
step = 1
}
m.cursor += step
if m.cursor >= len(m.findings) {
m.cursor = len(m.findings) - 1
if m.cursor < 0 {
m.cursor = 0
}
}
m.pager.Page = m.cursor / m.pager.PerPage m.pager.Page = m.cursor / m.pager.PerPage
m.refreshListViewport() m.refreshListViewport()
m.refreshBody() m.refreshBody()
+6 -51
View File
@@ -93,24 +93,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.bodyViewport.SetXOffset(0) m.bodyViewport.SetXOffset(0)
case tea.MouseWheelMsg: case tea.MouseWheelMsg:
switch msg.Button { util.HandleMouseWheel(msg, &m.bodyViewport)
case tea.MouseWheelUp:
if msg.Mod.Contains(tea.ModShift) {
m.bodyViewport.ScrollLeft(6)
} else {
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() - 1)
}
case tea.MouseWheelDown:
if msg.Mod.Contains(tea.ModShift) {
m.bodyViewport.ScrollRight(6)
} else {
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() + 1)
}
case tea.MouseWheelLeft:
m.bodyViewport.ScrollLeft(6)
case tea.MouseWheelRight:
m.bodyViewport.ScrollRight(6)
}
case tea.KeyPressMsg: case tea.KeyPressMsg:
h := keys.Keys.History h := keys.Keys.History
@@ -276,18 +259,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.clearSearch() return m, m.clearSearch()
case key.Matches(msg, g.ScrollUp): case key.Matches(msg, g.ScrollUp):
step := m.bodyViewport.Height() / 2 util.ScrollViewport(&m.bodyViewport, -1)
if step < 1 {
step = 1
}
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() - step)
case key.Matches(msg, g.ScrollDown): case key.Matches(msg, g.ScrollDown):
step := m.bodyViewport.Height() / 2 util.ScrollViewport(&m.bodyViewport, 1)
if step < 1 {
step = 1
}
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() + step)
case key.Matches(msg, g.Left): case key.Matches(msg, g.Left):
m.bodyViewport.ScrollLeft(6) m.bodyViewport.ScrollLeft(6)
@@ -304,41 +279,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.bodyViewport.SetXOffset(0) m.bodyViewport.SetXOffset(0)
case key.Matches(msg, g.GotoBottom): case key.Matches(msg, g.GotoBottom):
if len(m.entries) > 0 { m.cursor = util.CursorGotoBottom(len(m.entries))
m.cursor = len(m.entries) - 1
m.pager.Page = m.pager.TotalPages - 1
m.refreshListViewport() m.refreshListViewport()
m.refreshBody() m.refreshBody()
m.bodyViewport.SetYOffset(0) m.bodyViewport.SetYOffset(0)
m.bodyViewport.SetXOffset(0) m.bodyViewport.SetXOffset(0)
}
case key.Matches(msg, g.PrevPage): case key.Matches(msg, g.PrevPage):
step := m.pager.PerPage m.cursor = util.CursorMovePage(m.cursor, len(m.entries), m.pager.PerPage, false)
if step < 1 {
step = 1
}
m.cursor -= step
if m.cursor < 0 {
m.cursor = 0
}
m.refreshListViewport() m.refreshListViewport()
m.refreshBody() m.refreshBody()
m.bodyViewport.SetYOffset(0) m.bodyViewport.SetYOffset(0)
m.bodyViewport.SetXOffset(0) m.bodyViewport.SetXOffset(0)
case key.Matches(msg, g.NextPage): case key.Matches(msg, g.NextPage):
step := m.pager.PerPage m.cursor = util.CursorMovePage(m.cursor, len(m.entries), m.pager.PerPage, true)
if step < 1 {
step = 1
}
m.cursor += step
if m.cursor >= len(m.entries) {
m.cursor = len(m.entries) - 1
if m.cursor < 0 {
m.cursor = 0
}
}
m.refreshListViewport() m.refreshListViewport()
m.refreshBody() m.refreshBody()
m.bodyViewport.SetYOffset(0) m.bodyViewport.SetYOffset(0)
+9 -72
View File
@@ -52,24 +52,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.MouseWheelMsg: case tea.MouseWheelMsg:
if !m.editing { if !m.editing {
switch msg.Button { util.HandleMouseWheel(msg, &m.bodyViewport)
case tea.MouseWheelUp:
if msg.Mod.Contains(tea.ModShift) {
m.bodyViewport.ScrollLeft(6)
} else {
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() - 1)
}
case tea.MouseWheelDown:
if msg.Mod.Contains(tea.ModShift) {
m.bodyViewport.ScrollRight(6)
} else {
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() + 1)
}
case tea.MouseWheelLeft:
m.bodyViewport.ScrollLeft(6)
case tea.MouseWheelRight:
m.bodyViewport.ScrollRight(6)
}
} }
case tea.KeyPressMsg: case tea.KeyPressMsg:
@@ -127,18 +110,10 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg, cmds *[]tea.Cmd) (tea.Model
} }
case key.Matches(msg, keys.Keys.Global.ScrollUp): case key.Matches(msg, keys.Keys.Global.ScrollUp):
step := m.bodyViewport.Height() / 2 util.ScrollViewport(&m.bodyViewport, -1)
if step < 1 {
step = 1
}
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() - step)
case key.Matches(msg, keys.Keys.Global.ScrollDown): case key.Matches(msg, keys.Keys.Global.ScrollDown):
step := m.bodyViewport.Height() / 2 util.ScrollViewport(&m.bodyViewport, 1)
if step < 1 {
step = 1
}
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() + step)
case key.Matches(msg, keys.Keys.Global.Left): case key.Matches(msg, keys.Keys.Global.Left):
m.bodyViewport.ScrollLeft(6) m.bodyViewport.ScrollLeft(6)
@@ -278,13 +253,9 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg, cmds *[]tea.Cmd) (tea.Model
case key.Matches(msg, keys.Keys.Global.GotoBottom): case key.Matches(msg, keys.Keys.Global.GotoBottom):
if onResponses { if onResponses {
if len(m.responseQueue) > 0 { m.responseCursor = util.CursorGotoBottom(len(m.responseQueue))
m.responseCursor = len(m.responseQueue) - 1
}
} else { } else {
if len(m.queue) > 0 { m.cursor = util.CursorGotoBottom(len(m.queue))
m.cursor = len(m.queue) - 1
}
} }
m.refreshListViewport() m.refreshListViewport()
m.refreshResponseListViewport() m.refreshResponseListViewport()
@@ -292,23 +263,9 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg, cmds *[]tea.Cmd) (tea.Model
case key.Matches(msg, keys.Keys.Global.PrevPage): case key.Matches(msg, keys.Keys.Global.PrevPage):
if onResponses { if onResponses {
step := m.responsePager.PerPage m.responseCursor = util.CursorMovePage(m.responseCursor, len(m.responseQueue), m.responsePager.PerPage, false)
if step < 1 {
step = 1
}
m.responseCursor -= step
if m.responseCursor < 0 {
m.responseCursor = 0
}
} else { } else {
step := m.pager.PerPage m.cursor = util.CursorMovePage(m.cursor, len(m.queue), m.pager.PerPage, false)
if step < 1 {
step = 1
}
m.cursor -= step
if m.cursor < 0 {
m.cursor = 0
}
} }
m.refreshListViewport() m.refreshListViewport()
m.refreshResponseListViewport() m.refreshResponseListViewport()
@@ -316,29 +273,9 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg, cmds *[]tea.Cmd) (tea.Model
case key.Matches(msg, keys.Keys.Global.NextPage): case key.Matches(msg, keys.Keys.Global.NextPage):
if onResponses { if onResponses {
step := m.responsePager.PerPage m.responseCursor = util.CursorMovePage(m.responseCursor, len(m.responseQueue), m.responsePager.PerPage, true)
if step < 1 {
step = 1
}
m.responseCursor += step
if m.responseCursor >= len(m.responseQueue) {
m.responseCursor = len(m.responseQueue) - 1
if m.responseCursor < 0 {
m.responseCursor = 0
}
}
} else { } else {
step := m.pager.PerPage m.cursor = util.CursorMovePage(m.cursor, len(m.queue), m.pager.PerPage, true)
if step < 1 {
step = 1
}
m.cursor += step
if m.cursor >= len(m.queue) {
m.cursor = len(m.queue) - 1
if m.cursor < 0 {
m.cursor = 0
}
}
} }
m.refreshListViewport() m.refreshListViewport()
m.refreshResponseListViewport() m.refreshResponseListViewport()
+6 -35
View File
@@ -4,6 +4,7 @@ import (
"charm.land/bubbles/v2/key" "charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2" tea "charm.land/bubbletea/v2"
"github.com/anotherhadi/spilltea/internal/keys" "github.com/anotherhadi/spilltea/internal/keys"
"github.com/anotherhadi/spilltea/internal/util"
) )
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -20,12 +21,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.MouseWheelMsg: case tea.MouseWheelMsg:
if !m.editing { if !m.editing {
switch msg.Button { util.HandleMouseWheel(msg, &m.detailViewport)
case tea.MouseWheelUp:
m.detailViewport.SetYOffset(m.detailViewport.YOffset() - 1)
case tea.MouseWheelDown:
m.detailViewport.SetYOffset(m.detailViewport.YOffset() + 1)
}
} }
case tea.KeyPressMsg: case tea.KeyPressMsg:
@@ -129,47 +125,22 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
case key.Matches(msg, g.PrevPage): case key.Matches(msg, g.PrevPage):
step := m.pager.PerPage m.cursor = util.CursorMovePage(m.cursor, len(m.filtered), m.pager.PerPage, false)
if step < 1 {
step = 1
}
m.cursor -= step
if m.cursor < 0 {
m.cursor = 0
}
m.recalcSizes() m.recalcSizes()
m.syncTextarea() m.syncTextarea()
m.detailViewport.GotoTop() m.detailViewport.GotoTop()
case key.Matches(msg, g.NextPage): case key.Matches(msg, g.NextPage):
step := m.pager.PerPage m.cursor = util.CursorMovePage(m.cursor, len(m.filtered), m.pager.PerPage, true)
if step < 1 {
step = 1
}
m.cursor += step
if m.cursor >= len(m.filtered) {
m.cursor = len(m.filtered) - 1
if m.cursor < 0 {
m.cursor = 0
}
}
m.recalcSizes() m.recalcSizes()
m.syncTextarea() m.syncTextarea()
m.detailViewport.GotoTop() m.detailViewport.GotoTop()
case key.Matches(msg, g.ScrollUp): case key.Matches(msg, g.ScrollUp):
step := m.detailViewport.Height() / 2 util.ScrollViewport(&m.detailViewport, -1)
if step < 1 {
step = 1
}
m.detailViewport.SetYOffset(m.detailViewport.YOffset() - step)
case key.Matches(msg, g.ScrollDown): case key.Matches(msg, g.ScrollDown):
step := m.detailViewport.Height() / 2 util.ScrollViewport(&m.detailViewport, 1)
if step < 1 {
step = 1
}
m.detailViewport.SetYOffset(m.detailViewport.YOffset() + step)
case key.Matches(msg, g.Help): case key.Matches(msg, g.Help):
m.help.ShowAll = !m.help.ShowAll m.help.ShowAll = !m.help.ShowAll
+5 -33
View File
@@ -191,20 +191,12 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, g.ScrollUp): case key.Matches(msg, g.ScrollUp):
vp := m.focusedViewport() vp := m.focusedViewport()
step := vp.Height() / 2 util.ScrollViewport(&vp, -1)
if step < 1 {
step = 1
}
vp.SetYOffset(vp.YOffset() - step)
m.setFocusedViewport(vp) m.setFocusedViewport(vp)
case key.Matches(msg, g.ScrollDown): case key.Matches(msg, g.ScrollDown):
vp := m.focusedViewport() vp := m.focusedViewport()
step := vp.Height() / 2 util.ScrollViewport(&vp, 1)
if step < 1 {
step = 1
}
vp.SetYOffset(vp.YOffset() + step)
m.setFocusedViewport(vp) m.setFocusedViewport(vp)
case key.Matches(msg, g.Left): case key.Matches(msg, g.Left):
@@ -247,37 +239,17 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
m.refreshBody() m.refreshBody()
case key.Matches(msg, keys.Keys.Global.GotoBottom): case key.Matches(msg, keys.Keys.Global.GotoBottom):
if len(m.entries) > 0 { m.cursor = util.CursorGotoBottom(len(m.entries))
m.cursor = len(m.entries) - 1
m.pager.Page = m.pager.TotalPages - 1
m.refreshListViewport() m.refreshListViewport()
m.refreshBody() m.refreshBody()
}
case key.Matches(msg, keys.Keys.Global.PrevPage): case key.Matches(msg, keys.Keys.Global.PrevPage):
step := m.pager.PerPage m.cursor = util.CursorMovePage(m.cursor, len(m.entries), m.pager.PerPage, false)
if step < 1 {
step = 1
}
m.cursor -= step
if m.cursor < 0 {
m.cursor = 0
}
m.refreshListViewport() m.refreshListViewport()
m.refreshBody() m.refreshBody()
case key.Matches(msg, keys.Keys.Global.NextPage): case key.Matches(msg, keys.Keys.Global.NextPage):
step := m.pager.PerPage m.cursor = util.CursorMovePage(m.cursor, len(m.entries), m.pager.PerPage, true)
if step < 1 {
step = 1
}
m.cursor += step
if m.cursor >= len(m.entries) {
m.cursor = len(m.entries) - 1
if m.cursor < 0 {
m.cursor = 0
}
}
m.refreshListViewport() m.refreshListViewport()
m.refreshBody() m.refreshBody()
+30
View File
@@ -0,0 +1,30 @@
package util
// CursorMovePage moves cursor forward or backward by one page (perPage items),
// clamped to [0, total-1].
func CursorMovePage(cursor, total, perPage int, forward bool) int {
step := perPage
if step < 1 {
step = 1
}
if forward {
cursor += step
} else {
cursor -= step
}
if cursor < 0 || total <= 0 {
return 0
}
if cursor >= total {
return total - 1
}
return cursor
}
// CursorGotoBottom returns the last valid cursor index for a list of total items.
func CursorGotoBottom(total int) int {
if total <= 0 {
return 0
}
return total - 1
}
+39
View File
@@ -0,0 +1,39 @@
package util
import (
"charm.land/bubbles/v2/viewport"
tea "charm.land/bubbletea/v2"
)
// ScrollViewport scrolls vp vertically by half its height.
// delta should be -1 for up, +1 for down.
func ScrollViewport(vp *viewport.Model, delta int) {
step := vp.Height() / 2
if step < 1 {
step = 1
}
vp.SetYOffset(vp.YOffset() + delta*step)
}
// HandleMouseWheel applies standard mouse wheel scrolling to vp.
// Vertical: one line at a time. Shift+vertical or horizontal: scroll 6 columns.
func HandleMouseWheel(msg tea.MouseWheelMsg, vp *viewport.Model) {
switch msg.Button {
case tea.MouseWheelUp:
if msg.Mod.Contains(tea.ModShift) {
vp.ScrollLeft(6)
} else {
vp.SetYOffset(vp.YOffset() - 1)
}
case tea.MouseWheelDown:
if msg.Mod.Contains(tea.ModShift) {
vp.ScrollRight(6)
} else {
vp.SetYOffset(vp.YOffset() + 1)
}
case tea.MouseWheelLeft:
vp.ScrollLeft(6)
case tea.MouseWheelRight:
vp.ScrollRight(6)
}
}