feat(tui): open rows on double click
This commit is contained in:
parent
c462ff5ee2
commit
6327ecdc68
@ -125,6 +125,11 @@ type clusterBrowserModel struct {
|
||||
memberRows []memberRow
|
||||
memberOff int
|
||||
memberIndex int
|
||||
lastClickFocus tuiFocus
|
||||
lastClickIndex int
|
||||
lastClickX int
|
||||
lastClickY int
|
||||
lastClickAt time.Time
|
||||
detailView viewport.Model
|
||||
searchInput textinput.Model
|
||||
detailCache map[int64]store.ClusterDetail
|
||||
@ -148,6 +153,7 @@ type tuiMenuItem struct {
|
||||
}
|
||||
|
||||
const tuiMenuSeparatorAction = "separator"
|
||||
const tuiDoubleClickWindow = 450 * time.Millisecond
|
||||
|
||||
func (item tuiMenuItem) selectable() bool {
|
||||
return item.action != "" && item.action != tuiMenuSeparatorAction
|
||||
@ -1033,6 +1039,7 @@ func (m *clusterBrowserModel) handleMouse(msg tea.MouseMsg) {
|
||||
if msg.Action != tea.MouseActionPress {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
switch {
|
||||
case layout.clusters.contains(msg.X, msg.Y):
|
||||
m.focus = focusClusters
|
||||
@ -1049,6 +1056,7 @@ func (m *clusterBrowserModel) handleMouse(msg tea.MouseMsg) {
|
||||
m.selected = index
|
||||
m.loadSelectedCluster()
|
||||
m.status = fmt.Sprintf("Cluster %d", m.payload.Clusters[m.selected].ID)
|
||||
m.finishRowClick(focusClusters, index, msg.X, msg.Y, now)
|
||||
}
|
||||
case layout.members.contains(msg.X, msg.Y):
|
||||
m.focus = focusMembers
|
||||
@ -1065,6 +1073,7 @@ func (m *clusterBrowserModel) handleMouse(msg tea.MouseMsg) {
|
||||
if !m.memberRows[index].selectable {
|
||||
m.memberIndex = index
|
||||
m.status = m.memberRows[index].label
|
||||
m.clearLastClick()
|
||||
return
|
||||
}
|
||||
previous := m.memberIndex
|
||||
@ -1073,6 +1082,7 @@ func (m *clusterBrowserModel) handleMouse(msg tea.MouseMsg) {
|
||||
m.detailView.GotoTop()
|
||||
}
|
||||
m.status = fmt.Sprintf("Selected #%d", m.memberRows[m.memberIndex].thread().Number)
|
||||
m.finishRowClick(focusMembers, index, msg.X, msg.Y, now)
|
||||
}
|
||||
case layout.detail.contains(msg.X, msg.Y):
|
||||
m.focus = focusDetail
|
||||
@ -1087,6 +1097,32 @@ func (m *clusterBrowserModel) handleMouse(msg tea.MouseMsg) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *clusterBrowserModel) finishRowClick(focus tuiFocus, index, x, y int, now time.Time) {
|
||||
if m.isDoubleClick(focus, index, x, y, now) {
|
||||
m.clearLastClick()
|
||||
m.runAction("open")
|
||||
return
|
||||
}
|
||||
m.lastClickFocus = focus
|
||||
m.lastClickIndex = index
|
||||
m.lastClickX = x
|
||||
m.lastClickY = y
|
||||
m.lastClickAt = now
|
||||
}
|
||||
|
||||
func (m *clusterBrowserModel) isDoubleClick(focus tuiFocus, index, x, y int, now time.Time) bool {
|
||||
return !m.lastClickAt.IsZero() &&
|
||||
m.lastClickFocus == focus &&
|
||||
m.lastClickIndex == index &&
|
||||
m.lastClickX == x &&
|
||||
m.lastClickY == y &&
|
||||
now.Sub(m.lastClickAt) <= tuiDoubleClickWindow
|
||||
}
|
||||
|
||||
func (m *clusterBrowserModel) clearLastClick() {
|
||||
m.lastClickAt = time.Time{}
|
||||
}
|
||||
|
||||
func (m *clusterBrowserModel) handleMenuMouse(layout tuiLayout, msg tea.MouseMsg) {
|
||||
if msg.Action == tea.MouseActionMotion {
|
||||
index, ok := m.menuIndexAtMouse(layout, msg.X, msg.Y)
|
||||
@ -3377,7 +3413,7 @@ func (r memberRow) thread() store.Thread {
|
||||
return r.member.Thread
|
||||
}
|
||||
|
||||
func openURL(url string) error {
|
||||
var openURL = func(url string) error {
|
||||
if strings.TrimSpace(url) == "" {
|
||||
return fmt.Errorf("no URL selected")
|
||||
}
|
||||
|
||||
@ -284,6 +284,77 @@ func TestTUIMouseSelectsClusterRows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIMouseDoubleClickOpensClusterRepresentative(t *testing.T) {
|
||||
model := newClusterBrowserModel(context.Background(), nil, 0, clusterBrowserPayload{
|
||||
Repository: "openclaw/openclaw",
|
||||
Sort: "recent",
|
||||
Clusters: sampleTUIClusters(),
|
||||
})
|
||||
model.width = 140
|
||||
model.height = 32
|
||||
layout := model.layout()
|
||||
restoreOpenURL, opened := captureOpenURL(t)
|
||||
|
||||
msg := tea.MouseMsg{
|
||||
X: layout.clusters.x + 2,
|
||||
Y: layout.clusters.y + 4,
|
||||
Action: tea.MouseActionPress,
|
||||
Button: tea.MouseButtonLeft,
|
||||
}
|
||||
model.handleMouse(msg)
|
||||
if len(*opened) != 0 {
|
||||
t.Fatalf("single click opened URL: %#v", *opened)
|
||||
}
|
||||
model.handleMouse(msg)
|
||||
restoreOpenURL()
|
||||
|
||||
if got := *opened; len(got) != 1 || got[0] != "https://github.com/openclaw/openclaw/issues/11" {
|
||||
t.Fatalf("opened URLs = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIMouseDoubleClickOpensMemberThread(t *testing.T) {
|
||||
model := newClusterBrowserModel(context.Background(), nil, 0, clusterBrowserPayload{
|
||||
Repository: "openclaw/openclaw",
|
||||
Sort: "recent",
|
||||
Clusters: sampleTUIClusters(),
|
||||
})
|
||||
model.width = 140
|
||||
model.height = 32
|
||||
model.memberRows = []memberRow{
|
||||
{label: "ISSUES (1)"},
|
||||
{selectable: true, member: store.ClusterMemberDetail{Thread: store.Thread{
|
||||
ID: 42,
|
||||
Number: 42,
|
||||
Kind: "issue",
|
||||
State: "open",
|
||||
Title: "Selected issue",
|
||||
HTMLURL: "https://github.com/openclaw/openclaw/issues/42",
|
||||
UpdatedAtGitHub: "2026-04-27T10:00:00Z",
|
||||
}}},
|
||||
}
|
||||
model.memberIndex = 0
|
||||
layout := model.layout()
|
||||
restoreOpenURL, opened := captureOpenURL(t)
|
||||
|
||||
msg := tea.MouseMsg{
|
||||
X: layout.members.x + 2,
|
||||
Y: layout.members.y + 4,
|
||||
Action: tea.MouseActionPress,
|
||||
Button: tea.MouseButtonLeft,
|
||||
}
|
||||
model.handleMouse(msg)
|
||||
if len(*opened) != 0 {
|
||||
t.Fatalf("single click opened URL: %#v", *opened)
|
||||
}
|
||||
model.handleMouse(msg)
|
||||
restoreOpenURL()
|
||||
|
||||
if got := *opened; len(got) != 1 || got[0] != "https://github.com/openclaw/openclaw/issues/42" {
|
||||
t.Fatalf("opened URLs = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIMouseSelectsVisibleClusterWindow(t *testing.T) {
|
||||
clusters := make([]store.ClusterSummary, 0, 30)
|
||||
for i := 0; i < 30; i++ {
|
||||
@ -2537,6 +2608,26 @@ func TestTUIPanePositionLabels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func captureOpenURL(t *testing.T) (func(), *[]string) {
|
||||
t.Helper()
|
||||
previous := openURL
|
||||
opened := []string{}
|
||||
openURL = func(url string) error {
|
||||
opened = append(opened, url)
|
||||
return nil
|
||||
}
|
||||
restored := false
|
||||
restore := func() {
|
||||
if restored {
|
||||
return
|
||||
}
|
||||
openURL = previous
|
||||
restored = true
|
||||
}
|
||||
t.Cleanup(restore)
|
||||
return restore, &opened
|
||||
}
|
||||
|
||||
func sampleTUIClusters() []store.ClusterSummary {
|
||||
return []store.ClusterSummary{
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user