feat(daemon): overflow vertical focus across sessions
- commit
- 777c969057d0f6175af5f443b523c750ac52f626
- author
- brice · · 2026-07-25 05:20 UTC
- parent
- ca3316c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +176 -1
M internal/usecase/daemon/navigation_overflow.go +51 -0
| @@ -1,8 +1,11 @@ | |||
| 1 | 1 | package daemon | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "sort" | |
| 5 | + | ||
| 4 | 6 | "github.com/bnema/vev/internal/domain" | |
| 5 | 7 | "github.com/bnema/vev/internal/usecase/layout" | |
| 8 | + | "github.com/bnema/vev/internal/usecase/picker" | |
| 6 | 9 | ) | |
| 7 | 10 | ||
| 8 | 11 | type overflowKind uint8 | |
| @@ -47,6 +50,54 @@ func resolveOverflow(dir layout.Direction, cfg domain.NavConfig, position, count | |||
| 47 | 50 | return step | |
| 48 | 51 | } | |
| 49 | 52 | ||
| 53 | + | type sessionOverflowSnapshot struct { | |
| 54 | + | session *session | |
| 55 | + | id domain.SessionID | |
| 56 | + | name string | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // prepareSessionOverflow snapshots only the live registry entries, then sorts | |
| 60 | + | // immutable names exactly like the picker. No daemon or session lock survives | |
| 61 | + | // the snapshot, sorting, or the later switchToTarget call. | |
| 62 | + | func (d *Daemon) prepareSessionOverflow(current *session, dir layout.Direction, cfg domain.NavConfig) (picker.Target, bool) { | |
| 63 | + | if current == nil || !cfg.OverflowSessions || (dir != layout.Up && dir != layout.Down) { | |
| 64 | + | return picker.Target{}, false | |
| 65 | + | } | |
| 66 | + | current.mu.Lock() | |
| 67 | + | currentID := current.id | |
| 68 | + | current.mu.Unlock() | |
| 69 | + | ||
| 70 | + | d.mu.Lock() | |
| 71 | + | live := make([]*session, 0, len(d.sessions)) | |
| 72 | + | for _, sess := range d.sessions { | |
| 73 | + | live = append(live, sess) | |
| 74 | + | } | |
| 75 | + | d.mu.Unlock() | |
| 76 | + | ||
| 77 | + | snapshots := make([]sessionOverflowSnapshot, 0, len(live)) | |
| 78 | + | for _, sess := range live { | |
| 79 | + | sess.mu.Lock() | |
| 80 | + | snapshot := sessionOverflowSnapshot{session: sess, id: sess.id, name: sess.name} | |
| 81 | + | sess.mu.Unlock() | |
| 82 | + | snapshots = append(snapshots, snapshot) | |
| 83 | + | } | |
| 84 | + | sort.Slice(snapshots, func(i, j int) bool { return snapshots[i].name < snapshots[j].name }) | |
| 85 | + | ||
| 86 | + | position := -1 | |
| 87 | + | for i, snapshot := range snapshots { | |
| 88 | + | if snapshot.session == current && snapshot.id == currentID { | |
| 89 | + | position = i | |
| 90 | + | break | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | step := resolveOverflow(dir, cfg, position, len(snapshots)) | |
| 94 | + | if step.kind != overflowSessions { | |
| 95 | + | return picker.Target{}, false | |
| 96 | + | } | |
| 97 | + | target := snapshots[position+step.delta] | |
| 98 | + | return picker.Target{Session: target.id, TabIndex: -1}, true | |
| 99 | + | } | |
| 100 | + | ||
| 50 | 101 | type tabOverflowCandidate struct { | |
| 51 | 102 | source *tab | |
| 52 | 103 | target *tab | |
| @@ -1,8 +1,11 @@ | |||||
| 1 | package daemon | 1 | package daemon | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "sort" | |||
| 5 | + | ||||
| 4 | "github.com/bnema/vev/internal/domain" | 6 | "github.com/bnema/vev/internal/domain" | ||
| 5 | "github.com/bnema/vev/internal/usecase/layout" | 7 | "github.com/bnema/vev/internal/usecase/layout" | ||
| 8 | + | "github.com/bnema/vev/internal/usecase/picker" | |||
| 6 | ) | 9 | ) | ||
| 7 | 10 | ||||
| 8 | type overflowKind uint8 | 11 | type overflowKind uint8 | ||
| @@ -47,6 +50,54 @@ func resolveOverflow(dir layout.Direction, cfg domain.NavConfig, position, count | |||||
| 47 | return step | 50 | return step | ||
| 48 | } | 51 | } | ||
| 49 | 52 | ||||
| 53 | + | type sessionOverflowSnapshot struct { | |||
| 54 | + | session *session | |||
| 55 | + | id domain.SessionID | |||
| 56 | + | name string | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // prepareSessionOverflow snapshots only the live registry entries, then sorts | |||
| 60 | + | // immutable names exactly like the picker. No daemon or session lock survives | |||
| 61 | + | // the snapshot, sorting, or the later switchToTarget call. | |||
| 62 | + | func (d *Daemon) prepareSessionOverflow(current *session, dir layout.Direction, cfg domain.NavConfig) (picker.Target, bool) { | |||
| 63 | + | if current == nil || !cfg.OverflowSessions || (dir != layout.Up && dir != layout.Down) { | |||
| 64 | + | return picker.Target{}, false | |||
| 65 | + | } | |||
| 66 | + | current.mu.Lock() | |||
| 67 | + | currentID := current.id | |||
| 68 | + | current.mu.Unlock() | |||
| 69 | + | ||||
| 70 | + | d.mu.Lock() | |||
| 71 | + | live := make([]*session, 0, len(d.sessions)) | |||
| 72 | + | for _, sess := range d.sessions { | |||
| 73 | + | live = append(live, sess) | |||
| 74 | + | } | |||
| 75 | + | d.mu.Unlock() | |||
| 76 | + | ||||
| 77 | + | snapshots := make([]sessionOverflowSnapshot, 0, len(live)) | |||
| 78 | + | for _, sess := range live { | |||
| 79 | + | sess.mu.Lock() | |||
| 80 | + | snapshot := sessionOverflowSnapshot{session: sess, id: sess.id, name: sess.name} | |||
| 81 | + | sess.mu.Unlock() | |||
| 82 | + | snapshots = append(snapshots, snapshot) | |||
| 83 | + | } | |||
| 84 | + | sort.Slice(snapshots, func(i, j int) bool { return snapshots[i].name < snapshots[j].name }) | |||
| 85 | + | ||||
| 86 | + | position := -1 | |||
| 87 | + | for i, snapshot := range snapshots { | |||
| 88 | + | if snapshot.session == current && snapshot.id == currentID { | |||
| 89 | + | position = i | |||
| 90 | + | break | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | step := resolveOverflow(dir, cfg, position, len(snapshots)) | |||
| 94 | + | if step.kind != overflowSessions { | |||
| 95 | + | return picker.Target{}, false | |||
| 96 | + | } | |||
| 97 | + | target := snapshots[position+step.delta] | |||
| 98 | + | return picker.Target{Session: target.id, TabIndex: -1}, true | |||
| 99 | + | } | |||
| 100 | + | ||||
| 50 | type tabOverflowCandidate struct { | 101 | type tabOverflowCandidate struct { | ||
| 51 | source *tab | 102 | source *tab | ||
| 52 | target *tab | 103 | target *tab | ||
M internal/usecase/daemon/navigation_overflow_test.go +116 -0
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package daemon | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "sync" | |
| 4 | 5 | "testing" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/stretchr/testify/require" | |
| @@ -76,6 +77,121 @@ func TestKeyboardHorizontalOverflowLandsOnFacingEdge(t *testing.T) { | |||
| 76 | 77 | } | |
| 77 | 78 | } | |
| 78 | 79 | ||
| 80 | + | func TestKeyboardVerticalOverflowSwitchesOnlyAcrossAlphabeticalLiveSessions(t *testing.T) { | |
| 81 | + | d, alpha, ac, _ := newManualSessionWithPTYs(t, nil, nil) | |
| 82 | + | alpha.mu.Lock() | |
| 83 | + | alpha.name = "alpha" | |
| 84 | + | alpha.mu.Unlock() | |
| 85 | + | ||
| 86 | + | newSession := func(id, name string, active int, focus layout.PaneID) *session { | |
| 87 | + | tabs := []*tab{newTab(nil, domain.Size{Cols: 41, Rows: 10}), newTab(nil, domain.Size{Cols: 41, Rows: 10})} | |
| 88 | + | target := tabs[active] | |
| 89 | + | target.mu.Lock() | |
| 90 | + | target.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: focus} | |
| 91 | + | target.panes["pane-2"] = newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10}) | |
| 92 | + | target.mu.Unlock() | |
| 93 | + | return &session{id: domain.SessionID(id), name: name, ctx: t.Context(), cancel: func() {}, tabs: tabs, active: active} | |
| 94 | + | } | |
| 95 | + | charlie := newSession("live-charlie", "charlie", 1, "pane-2") | |
| 96 | + | echo := newSession("live-echo", "echo", 1, "pane-2") | |
| 97 | + | ||
| 98 | + | // Deliberately register the live sessions out of alphabetical order. The | |
| 99 | + | // stopped name sorts between alpha and charlie but must never be resumed. | |
| 100 | + | d.mu.Lock() | |
| 101 | + | delete(d.sessions, alpha.id) | |
| 102 | + | d.sessions[echo.id] = echo | |
| 103 | + | d.sessions[alpha.id] = alpha | |
| 104 | + | d.sessions[charlie.id] = charlie | |
| 105 | + | d.stopped["bravo"] = stoppedSession{name: "bravo", cwd: "/tmp/bravo"} | |
| 106 | + | d.mu.Unlock() | |
| 107 | + | d.ApplyConfig(domain.Config{Nav: domain.NavConfig{OverflowSessions: true}}) | |
| 108 | + | handler := daemonKeyHandler{d: d, ac: ac} | |
| 109 | + | ||
| 110 | + | moves := []struct { | |
| 111 | + | name string | |
| 112 | + | action keys.Action | |
| 113 | + | want *session | |
| 114 | + | }{ | |
| 115 | + | {name: "down to charlie", action: keys.ActionFocusPaneDown, want: charlie}, | |
| 116 | + | {name: "down to echo", action: keys.ActionFocusPaneDown, want: echo}, | |
| 117 | + | {name: "down wall", action: keys.ActionFocusPaneDown, want: echo}, | |
| 118 | + | {name: "up to charlie", action: keys.ActionFocusPaneUp, want: charlie}, | |
| 119 | + | {name: "up to alpha", action: keys.ActionFocusPaneUp, want: alpha}, | |
| 120 | + | {name: "up wall", action: keys.ActionFocusPaneUp, want: alpha}, | |
| 121 | + | } | |
| 122 | + | for _, move := range moves { | |
| 123 | + | t.Run(move.name, func(t *testing.T) { | |
| 124 | + | handler.Action(move.action) | |
| 125 | + | require.Same(t, move.want, ac.currentSession()) | |
| 126 | + | }) | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | for _, target := range []*session{charlie, echo} { | |
| 130 | + | require.Equal(t, 1, activeTabIndex(target), "switch preserves the target active tab") | |
| 131 | + | target.tabs[1].mu.Lock() | |
| 132 | + | require.Equal(t, layout.PaneID("pane-2"), target.tabs[1].tree.Focus, "switch preserves the target pane focus") | |
| 133 | + | target.tabs[1].mu.Unlock() | |
| 134 | + | } | |
| 135 | + | d.mu.Lock() | |
| 136 | + | _, stopped := d.stopped["bravo"] | |
| 137 | + | var bravoLive bool | |
| 138 | + | for _, candidate := range d.sessions { | |
| 139 | + | candidate.mu.Lock() | |
| 140 | + | bravoLive = bravoLive || candidate.name == "bravo" | |
| 141 | + | candidate.mu.Unlock() | |
| 142 | + | } | |
| 143 | + | d.mu.Unlock() | |
| 144 | + | alpha.mu.Lock() | |
| 145 | + | attached := alpha.client | |
| 146 | + | alpha.mu.Unlock() | |
| 147 | + | require.True(t, stopped, "vertical overflow leaves the stopped session stopped") | |
| 148 | + | require.False(t, bravoLive, "the stopped session is absent from the live registry") | |
| 149 | + | require.Same(t, ac, attached, "the source session owns the genuinely attached client after returning") | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | func TestVerticalOverflowIsRaceFreeDuringSessionRename(t *testing.T) { | |
| 153 | + | d, alpha, ac, _ := newManualSessionWithPTYs(t, nil) | |
| 154 | + | alpha.mu.Lock() | |
| 155 | + | alpha.name = "alpha" | |
| 156 | + | alpha.mu.Unlock() | |
| 157 | + | charlie := &session{id: "live-charlie", name: "charlie", ctx: t.Context(), cancel: func() {}, tabs: []*tab{newTab(nil, domain.Size{Cols: 41, Rows: 10})}} | |
| 158 | + | echo := &session{id: "live-echo", name: "echo", ctx: t.Context(), cancel: func() {}, tabs: []*tab{newTab(nil, domain.Size{Cols: 41, Rows: 10})}} | |
| 159 | + | d.mu.Lock() | |
| 160 | + | d.sessions[echo.id] = echo | |
| 161 | + | d.sessions[charlie.id] = charlie | |
| 162 | + | d.mu.Unlock() | |
| 163 | + | d.ApplyConfig(domain.Config{Nav: domain.NavConfig{OverflowSessions: true}}) | |
| 164 | + | ||
| 165 | + | start := make(chan struct{}) | |
| 166 | + | renameErrors := make(chan error, 40) | |
| 167 | + | var wg sync.WaitGroup | |
| 168 | + | wg.Go(func() { | |
| 169 | + | <-start | |
| 170 | + | for i := range 40 { | |
| 171 | + | name := "charlie" | |
| 172 | + | if i%2 == 0 { | |
| 173 | + | name = "delta" | |
| 174 | + | } | |
| 175 | + | if err := d.renameSession(charlie, name); err != nil { | |
| 176 | + | renameErrors <- err | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | }) | |
| 180 | + | ||
| 181 | + | close(start) | |
| 182 | + | handler := daemonKeyHandler{d: d, ac: ac} | |
| 183 | + | for range 20 { | |
| 184 | + | handler.Action(keys.ActionFocusPaneDown) | |
| 185 | + | handler.Action(keys.ActionFocusPaneUp) | |
| 186 | + | } | |
| 187 | + | wg.Wait() | |
| 188 | + | close(renameErrors) | |
| 189 | + | for err := range renameErrors { | |
| 190 | + | require.NoError(t, err) | |
| 191 | + | } | |
| 192 | + | require.NotNil(t, ac.currentSession()) | |
| 193 | + | } | |
| 194 | + | ||
| 79 | 195 | func TestKeyboardHorizontalOverflowRespectsDefaultsWallsFailedEntryAndFloating(t *testing.T) { | |
| 80 | 196 | tests := []struct { | |
| 81 | 197 | name string | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package daemon | 1 | package daemon | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "sync" | |||
| 4 | "testing" | 5 | "testing" | ||
| 5 | 6 | ||||
| 6 | "github.com/stretchr/testify/require" | 7 | "github.com/stretchr/testify/require" | ||
| @@ -76,6 +77,121 @@ func TestKeyboardHorizontalOverflowLandsOnFacingEdge(t *testing.T) { | |||||
| 76 | } | 77 | } | ||
| 77 | } | 78 | } | ||
| 78 | 79 | ||||
| 80 | + | func TestKeyboardVerticalOverflowSwitchesOnlyAcrossAlphabeticalLiveSessions(t *testing.T) { | |||
| 81 | + | d, alpha, ac, _ := newManualSessionWithPTYs(t, nil, nil) | |||
| 82 | + | alpha.mu.Lock() | |||
| 83 | + | alpha.name = "alpha" | |||
| 84 | + | alpha.mu.Unlock() | |||
| 85 | + | ||||
| 86 | + | newSession := func(id, name string, active int, focus layout.PaneID) *session { | |||
| 87 | + | tabs := []*tab{newTab(nil, domain.Size{Cols: 41, Rows: 10}), newTab(nil, domain.Size{Cols: 41, Rows: 10})} | |||
| 88 | + | target := tabs[active] | |||
| 89 | + | target.mu.Lock() | |||
| 90 | + | target.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: focus} | |||
| 91 | + | target.panes["pane-2"] = newPane("pane-2", nil, domain.Size{Cols: 20, Rows: 10}) | |||
| 92 | + | target.mu.Unlock() | |||
| 93 | + | return &session{id: domain.SessionID(id), name: name, ctx: t.Context(), cancel: func() {}, tabs: tabs, active: active} | |||
| 94 | + | } | |||
| 95 | + | charlie := newSession("live-charlie", "charlie", 1, "pane-2") | |||
| 96 | + | echo := newSession("live-echo", "echo", 1, "pane-2") | |||
| 97 | + | ||||
| 98 | + | // Deliberately register the live sessions out of alphabetical order. The | |||
| 99 | + | // stopped name sorts between alpha and charlie but must never be resumed. | |||
| 100 | + | d.mu.Lock() | |||
| 101 | + | delete(d.sessions, alpha.id) | |||
| 102 | + | d.sessions[echo.id] = echo | |||
| 103 | + | d.sessions[alpha.id] = alpha | |||
| 104 | + | d.sessions[charlie.id] = charlie | |||
| 105 | + | d.stopped["bravo"] = stoppedSession{name: "bravo", cwd: "/tmp/bravo"} | |||
| 106 | + | d.mu.Unlock() | |||
| 107 | + | d.ApplyConfig(domain.Config{Nav: domain.NavConfig{OverflowSessions: true}}) | |||
| 108 | + | handler := daemonKeyHandler{d: d, ac: ac} | |||
| 109 | + | ||||
| 110 | + | moves := []struct { | |||
| 111 | + | name string | |||
| 112 | + | action keys.Action | |||
| 113 | + | want *session | |||
| 114 | + | }{ | |||
| 115 | + | {name: "down to charlie", action: keys.ActionFocusPaneDown, want: charlie}, | |||
| 116 | + | {name: "down to echo", action: keys.ActionFocusPaneDown, want: echo}, | |||
| 117 | + | {name: "down wall", action: keys.ActionFocusPaneDown, want: echo}, | |||
| 118 | + | {name: "up to charlie", action: keys.ActionFocusPaneUp, want: charlie}, | |||
| 119 | + | {name: "up to alpha", action: keys.ActionFocusPaneUp, want: alpha}, | |||
| 120 | + | {name: "up wall", action: keys.ActionFocusPaneUp, want: alpha}, | |||
| 121 | + | } | |||
| 122 | + | for _, move := range moves { | |||
| 123 | + | t.Run(move.name, func(t *testing.T) { | |||
| 124 | + | handler.Action(move.action) | |||
| 125 | + | require.Same(t, move.want, ac.currentSession()) | |||
| 126 | + | }) | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | for _, target := range []*session{charlie, echo} { | |||
| 130 | + | require.Equal(t, 1, activeTabIndex(target), "switch preserves the target active tab") | |||
| 131 | + | target.tabs[1].mu.Lock() | |||
| 132 | + | require.Equal(t, layout.PaneID("pane-2"), target.tabs[1].tree.Focus, "switch preserves the target pane focus") | |||
| 133 | + | target.tabs[1].mu.Unlock() | |||
| 134 | + | } | |||
| 135 | + | d.mu.Lock() | |||
| 136 | + | _, stopped := d.stopped["bravo"] | |||
| 137 | + | var bravoLive bool | |||
| 138 | + | for _, candidate := range d.sessions { | |||
| 139 | + | candidate.mu.Lock() | |||
| 140 | + | bravoLive = bravoLive || candidate.name == "bravo" | |||
| 141 | + | candidate.mu.Unlock() | |||
| 142 | + | } | |||
| 143 | + | d.mu.Unlock() | |||
| 144 | + | alpha.mu.Lock() | |||
| 145 | + | attached := alpha.client | |||
| 146 | + | alpha.mu.Unlock() | |||
| 147 | + | require.True(t, stopped, "vertical overflow leaves the stopped session stopped") | |||
| 148 | + | require.False(t, bravoLive, "the stopped session is absent from the live registry") | |||
| 149 | + | require.Same(t, ac, attached, "the source session owns the genuinely attached client after returning") | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | func TestVerticalOverflowIsRaceFreeDuringSessionRename(t *testing.T) { | |||
| 153 | + | d, alpha, ac, _ := newManualSessionWithPTYs(t, nil) | |||
| 154 | + | alpha.mu.Lock() | |||
| 155 | + | alpha.name = "alpha" | |||
| 156 | + | alpha.mu.Unlock() | |||
| 157 | + | charlie := &session{id: "live-charlie", name: "charlie", ctx: t.Context(), cancel: func() {}, tabs: []*tab{newTab(nil, domain.Size{Cols: 41, Rows: 10})}} | |||
| 158 | + | echo := &session{id: "live-echo", name: "echo", ctx: t.Context(), cancel: func() {}, tabs: []*tab{newTab(nil, domain.Size{Cols: 41, Rows: 10})}} | |||
| 159 | + | d.mu.Lock() | |||
| 160 | + | d.sessions[echo.id] = echo | |||
| 161 | + | d.sessions[charlie.id] = charlie | |||
| 162 | + | d.mu.Unlock() | |||
| 163 | + | d.ApplyConfig(domain.Config{Nav: domain.NavConfig{OverflowSessions: true}}) | |||
| 164 | + | ||||
| 165 | + | start := make(chan struct{}) | |||
| 166 | + | renameErrors := make(chan error, 40) | |||
| 167 | + | var wg sync.WaitGroup | |||
| 168 | + | wg.Go(func() { | |||
| 169 | + | <-start | |||
| 170 | + | for i := range 40 { | |||
| 171 | + | name := "charlie" | |||
| 172 | + | if i%2 == 0 { | |||
| 173 | + | name = "delta" | |||
| 174 | + | } | |||
| 175 | + | if err := d.renameSession(charlie, name); err != nil { | |||
| 176 | + | renameErrors <- err | |||
| 177 | + | } | |||
| 178 | + | } | |||
| 179 | + | }) | |||
| 180 | + | ||||
| 181 | + | close(start) | |||
| 182 | + | handler := daemonKeyHandler{d: d, ac: ac} | |||
| 183 | + | for range 20 { | |||
| 184 | + | handler.Action(keys.ActionFocusPaneDown) | |||
| 185 | + | handler.Action(keys.ActionFocusPaneUp) | |||
| 186 | + | } | |||
| 187 | + | wg.Wait() | |||
| 188 | + | close(renameErrors) | |||
| 189 | + | for err := range renameErrors { | |||
| 190 | + | require.NoError(t, err) | |||
| 191 | + | } | |||
| 192 | + | require.NotNil(t, ac.currentSession()) | |||
| 193 | + | } | |||
| 194 | + | ||||
| 79 | func TestKeyboardHorizontalOverflowRespectsDefaultsWallsFailedEntryAndFloating(t *testing.T) { | 195 | func TestKeyboardHorizontalOverflowRespectsDefaultsWallsFailedEntryAndFloating(t *testing.T) { | ||
| 80 | tests := []struct { | 196 | tests := []struct { | ||
| 81 | name string | 197 | name string | ||
M internal/usecase/daemon/pane_actions.go +9 -1
| @@ -320,10 +320,18 @@ func (d *Daemon) focusDir(sess *session, ac *attachedClient, dir layout.Directio | |||
| 320 | 320 | return err | |
| 321 | 321 | } | |
| 322 | 322 | ||
| 323 | + | cfg := d.currentNavConfig() | |
| 324 | + | if sessionTarget, ok := d.prepareSessionOverflow(sess, dir, cfg); ok { | |
| 325 | + | if ac == nil { | |
| 326 | + | return errNoNeighbor | |
| 327 | + | } | |
| 328 | + | return d.switchToTarget(sess, ac, sessionTarget) | |
| 329 | + | } | |
| 330 | + | ||
| 323 | 331 | sess.mu.Lock() | |
| 324 | 332 | position, count := sess.active, len(sess.tabs) | |
| 325 | 333 | sess.mu.Unlock() | |
| 326 | - | step := resolveOverflow(dir, d.currentNavConfig(), position, count) | |
| 334 | + | step := resolveOverflow(dir, cfg, position, count) | |
| 327 | 335 | if step.kind != overflowTabs { | |
| 328 | 336 | return err | |
| 329 | 337 | } | |
| @@ -320,10 +320,18 @@ func (d *Daemon) focusDir(sess *session, ac *attachedClient, dir layout.Directio | |||||
| 320 | return err | 320 | return err | ||
| 321 | } | 321 | } | ||
| 322 | 322 | ||||
| 323 | + | cfg := d.currentNavConfig() | |||
| 324 | + | if sessionTarget, ok := d.prepareSessionOverflow(sess, dir, cfg); ok { | |||
| 325 | + | if ac == nil { | |||
| 326 | + | return errNoNeighbor | |||
| 327 | + | } | |||
| 328 | + | return d.switchToTarget(sess, ac, sessionTarget) | |||
| 329 | + | } | |||
| 330 | + | ||||
| 323 | sess.mu.Lock() | 331 | sess.mu.Lock() | ||
| 324 | position, count := sess.active, len(sess.tabs) | 332 | position, count := sess.active, len(sess.tabs) | ||
| 325 | sess.mu.Unlock() | 333 | sess.mu.Unlock() | ||
| 326 | - | step := resolveOverflow(dir, d.currentNavConfig(), position, count) | 334 | + | step := resolveOverflow(dir, cfg, position, count) |
| 327 | if step.kind != overflowTabs { | 335 | if step.kind != overflowTabs { | ||
| 328 | return err | 336 | return err | ||
| 329 | } | 337 | } | ||