feat(session): add recent navigation commands
- commit
- 097cba32c619e2c92921a7b64cf270ecdc6502cd
- author
- brice · · 2026-07-05 13:46 UTC
- parent
- ca585a7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +271 -4
M internal/usecase/command/command.go +2 -0
| @@ -18,6 +18,8 @@ type Context interface { | |||
| 18 | 18 | FocusPaneDown() error | |
| 19 | 19 | NextTab() error | |
| 20 | 20 | PrevTab() error | |
| 21 | + | BackSession() error | |
| 22 | + | ForwardSession() error | |
| 21 | 23 | Detach() error | |
| 22 | 24 | EnterVisualMode() error | |
| 23 | 25 | RenameSession() error | |
| @@ -18,6 +18,8 @@ type Context interface { | |||||
| 18 | FocusPaneDown() error | 18 | FocusPaneDown() error | ||
| 19 | NextTab() error | 19 | NextTab() error | ||
| 20 | PrevTab() error | 20 | PrevTab() error | ||
| 21 | + | BackSession() error | |||
| 22 | + | ForwardSession() error | |||
| 21 | Detach() error | 23 | Detach() error | ||
| 22 | EnterVisualMode() error | 24 | EnterVisualMode() error | ||
| 23 | RenameSession() error | 25 | RenameSession() error | ||
M internal/usecase/command/context_mock_test.go +88 -0
| @@ -35,6 +35,50 @@ func (_m *MockContext) EXPECT() *MockContext_Expecter { | |||
| 35 | 35 | return &MockContext_Expecter{mock: &_m.Mock} | |
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | + | // BackSession provides a mock function for the type MockContext | |
| 39 | + | func (_mock *MockContext) BackSession() error { | |
| 40 | + | ret := _mock.Called() | |
| 41 | + | ||
| 42 | + | if len(ret) == 0 { | |
| 43 | + | panic("no return value specified for BackSession") | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | var r0 error | |
| 47 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |
| 48 | + | r0 = returnFunc() | |
| 49 | + | } else { | |
| 50 | + | r0 = ret.Error(0) | |
| 51 | + | } | |
| 52 | + | return r0 | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // MockContext_BackSession_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BackSession' | |
| 56 | + | type MockContext_BackSession_Call struct { | |
| 57 | + | *mock.Call | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | // BackSession is a helper method to define mock.On call | |
| 61 | + | func (_e *MockContext_Expecter) BackSession() *MockContext_BackSession_Call { | |
| 62 | + | return &MockContext_BackSession_Call{Call: _e.mock.On("BackSession")} | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | func (_c *MockContext_BackSession_Call) Run(run func()) *MockContext_BackSession_Call { | |
| 66 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 67 | + | run() | |
| 68 | + | }) | |
| 69 | + | return _c | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | func (_c *MockContext_BackSession_Call) Return(err error) *MockContext_BackSession_Call { | |
| 73 | + | _c.Call.Return(err) | |
| 74 | + | return _c | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | func (_c *MockContext_BackSession_Call) RunAndReturn(run func() error) *MockContext_BackSession_Call { | |
| 78 | + | _c.Call.Return(run) | |
| 79 | + | return _c | |
| 80 | + | } | |
| 81 | + | ||
| 38 | 82 | // ClosePane provides a mock function for the type MockContext | |
| 39 | 83 | func (_mock *MockContext) ClosePane() error { | |
| 40 | 84 | ret := _mock.Called() | |
| @@ -475,6 +519,50 @@ func (_c *MockContext_FocusPaneUp_Call) RunAndReturn(run func() error) *MockCont | |||
| 475 | 519 | return _c | |
| 476 | 520 | } | |
| 477 | 521 | ||
| 522 | + | // ForwardSession provides a mock function for the type MockContext | |
| 523 | + | func (_mock *MockContext) ForwardSession() error { | |
| 524 | + | ret := _mock.Called() | |
| 525 | + | ||
| 526 | + | if len(ret) == 0 { | |
| 527 | + | panic("no return value specified for ForwardSession") | |
| 528 | + | } | |
| 529 | + | ||
| 530 | + | var r0 error | |
| 531 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |
| 532 | + | r0 = returnFunc() | |
| 533 | + | } else { | |
| 534 | + | r0 = ret.Error(0) | |
| 535 | + | } | |
| 536 | + | return r0 | |
| 537 | + | } | |
| 538 | + | ||
| 539 | + | // MockContext_ForwardSession_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ForwardSession' | |
| 540 | + | type MockContext_ForwardSession_Call struct { | |
| 541 | + | *mock.Call | |
| 542 | + | } | |
| 543 | + | ||
| 544 | + | // ForwardSession is a helper method to define mock.On call | |
| 545 | + | func (_e *MockContext_Expecter) ForwardSession() *MockContext_ForwardSession_Call { | |
| 546 | + | return &MockContext_ForwardSession_Call{Call: _e.mock.On("ForwardSession")} | |
| 547 | + | } | |
| 548 | + | ||
| 549 | + | func (_c *MockContext_ForwardSession_Call) Run(run func()) *MockContext_ForwardSession_Call { | |
| 550 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 551 | + | run() | |
| 552 | + | }) | |
| 553 | + | return _c | |
| 554 | + | } | |
| 555 | + | ||
| 556 | + | func (_c *MockContext_ForwardSession_Call) Return(err error) *MockContext_ForwardSession_Call { | |
| 557 | + | _c.Call.Return(err) | |
| 558 | + | return _c | |
| 559 | + | } | |
| 560 | + | ||
| 561 | + | func (_c *MockContext_ForwardSession_Call) RunAndReturn(run func() error) *MockContext_ForwardSession_Call { | |
| 562 | + | _c.Call.Return(run) | |
| 563 | + | return _c | |
| 564 | + | } | |
| 565 | + | ||
| 478 | 566 | // NextTab provides a mock function for the type MockContext | |
| 479 | 567 | func (_mock *MockContext) NextTab() error { | |
| 480 | 568 | ret := _mock.Called() | |
| @@ -35,6 +35,50 @@ func (_m *MockContext) EXPECT() *MockContext_Expecter { | |||||
| 35 | return &MockContext_Expecter{mock: &_m.Mock} | 35 | return &MockContext_Expecter{mock: &_m.Mock} | ||
| 36 | } | 36 | } | ||
| 37 | 37 | ||||
| 38 | + | // BackSession provides a mock function for the type MockContext | |||
| 39 | + | func (_mock *MockContext) BackSession() error { | |||
| 40 | + | ret := _mock.Called() | |||
| 41 | + | ||||
| 42 | + | if len(ret) == 0 { | |||
| 43 | + | panic("no return value specified for BackSession") | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | var r0 error | |||
| 47 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |||
| 48 | + | r0 = returnFunc() | |||
| 49 | + | } else { | |||
| 50 | + | r0 = ret.Error(0) | |||
| 51 | + | } | |||
| 52 | + | return r0 | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // MockContext_BackSession_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BackSession' | |||
| 56 | + | type MockContext_BackSession_Call struct { | |||
| 57 | + | *mock.Call | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | // BackSession is a helper method to define mock.On call | |||
| 61 | + | func (_e *MockContext_Expecter) BackSession() *MockContext_BackSession_Call { | |||
| 62 | + | return &MockContext_BackSession_Call{Call: _e.mock.On("BackSession")} | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | func (_c *MockContext_BackSession_Call) Run(run func()) *MockContext_BackSession_Call { | |||
| 66 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 67 | + | run() | |||
| 68 | + | }) | |||
| 69 | + | return _c | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | func (_c *MockContext_BackSession_Call) Return(err error) *MockContext_BackSession_Call { | |||
| 73 | + | _c.Call.Return(err) | |||
| 74 | + | return _c | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | func (_c *MockContext_BackSession_Call) RunAndReturn(run func() error) *MockContext_BackSession_Call { | |||
| 78 | + | _c.Call.Return(run) | |||
| 79 | + | return _c | |||
| 80 | + | } | |||
| 81 | + | ||||
| 38 | // ClosePane provides a mock function for the type MockContext | 82 | // ClosePane provides a mock function for the type MockContext | ||
| 39 | func (_mock *MockContext) ClosePane() error { | 83 | func (_mock *MockContext) ClosePane() error { | ||
| 40 | ret := _mock.Called() | 84 | ret := _mock.Called() | ||
| @@ -475,6 +519,50 @@ func (_c *MockContext_FocusPaneUp_Call) RunAndReturn(run func() error) *MockCont | |||||
| 475 | return _c | 519 | return _c | ||
| 476 | } | 520 | } | ||
| 477 | 521 | ||||
| 522 | + | // ForwardSession provides a mock function for the type MockContext | |||
| 523 | + | func (_mock *MockContext) ForwardSession() error { | |||
| 524 | + | ret := _mock.Called() | |||
| 525 | + | ||||
| 526 | + | if len(ret) == 0 { | |||
| 527 | + | panic("no return value specified for ForwardSession") | |||
| 528 | + | } | |||
| 529 | + | ||||
| 530 | + | var r0 error | |||
| 531 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |||
| 532 | + | r0 = returnFunc() | |||
| 533 | + | } else { | |||
| 534 | + | r0 = ret.Error(0) | |||
| 535 | + | } | |||
| 536 | + | return r0 | |||
| 537 | + | } | |||
| 538 | + | ||||
| 539 | + | // MockContext_ForwardSession_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ForwardSession' | |||
| 540 | + | type MockContext_ForwardSession_Call struct { | |||
| 541 | + | *mock.Call | |||
| 542 | + | } | |||
| 543 | + | ||||
| 544 | + | // ForwardSession is a helper method to define mock.On call | |||
| 545 | + | func (_e *MockContext_Expecter) ForwardSession() *MockContext_ForwardSession_Call { | |||
| 546 | + | return &MockContext_ForwardSession_Call{Call: _e.mock.On("ForwardSession")} | |||
| 547 | + | } | |||
| 548 | + | ||||
| 549 | + | func (_c *MockContext_ForwardSession_Call) Run(run func()) *MockContext_ForwardSession_Call { | |||
| 550 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 551 | + | run() | |||
| 552 | + | }) | |||
| 553 | + | return _c | |||
| 554 | + | } | |||
| 555 | + | ||||
| 556 | + | func (_c *MockContext_ForwardSession_Call) Return(err error) *MockContext_ForwardSession_Call { | |||
| 557 | + | _c.Call.Return(err) | |||
| 558 | + | return _c | |||
| 559 | + | } | |||
| 560 | + | ||||
| 561 | + | func (_c *MockContext_ForwardSession_Call) RunAndReturn(run func() error) *MockContext_ForwardSession_Call { | |||
| 562 | + | _c.Call.Return(run) | |||
| 563 | + | return _c | |||
| 564 | + | } | |||
| 565 | + | ||||
| 478 | // NextTab provides a mock function for the type MockContext | 566 | // NextTab provides a mock function for the type MockContext | ||
| 479 | func (_mock *MockContext) NextTab() error { | 567 | func (_mock *MockContext) NextTab() error { | ||
| 480 | ret := _mock.Called() | 568 | ret := _mock.Called() | ||
M internal/usecase/command/registry.go +2 -0
| @@ -21,6 +21,8 @@ func Registry() []Command { | |||
| 21 | 21 | {Code: "FPD", Name: "Focus pane down", Desc: "Focus the pane below", Run: func(ctx Context) error { return ctx.FocusPaneDown() }}, | |
| 22 | 22 | {Code: "NXT", Name: "Next tab", Desc: "Switch to the next tab", Run: func(ctx Context) error { return ctx.NextTab() }}, | |
| 23 | 23 | {Code: "PVT", Name: "Previous tab", Desc: "Switch to the previous tab", Run: func(ctx Context) error { return ctx.PrevTab() }}, | |
| 24 | + | {Code: "BSK", Name: "Back session", Desc: "Switch back through recent sessions", Run: func(ctx Context) error { return ctx.BackSession() }}, | |
| 25 | + | {Code: "FSK", Name: "Forward session", Desc: "Switch forward through recent sessions", Run: func(ctx Context) error { return ctx.ForwardSession() }}, | |
| 24 | 26 | {Code: "SSP", Name: "Session picker", Desc: "Open the session picker", Run: func(ctx Context) error { return ctx.OpenSessionPicker() }}, | |
| 25 | 27 | {Code: "VIS", Name: "Visual mode", Desc: "Enter visual mode", Run: func(ctx Context) error { return ctx.EnterVisualMode() }}, | |
| 26 | 28 | {Code: "RNS", Name: "Rename session", Desc: "Rename the session (an ephemeral session becomes named)", Run: func(ctx Context) error { return ctx.RenameSession() }}, | |
| @@ -21,6 +21,8 @@ func Registry() []Command { | |||||
| 21 | {Code: "FPD", Name: "Focus pane down", Desc: "Focus the pane below", Run: func(ctx Context) error { return ctx.FocusPaneDown() }}, | 21 | {Code: "FPD", Name: "Focus pane down", Desc: "Focus the pane below", Run: func(ctx Context) error { return ctx.FocusPaneDown() }}, | ||
| 22 | {Code: "NXT", Name: "Next tab", Desc: "Switch to the next tab", Run: func(ctx Context) error { return ctx.NextTab() }}, | 22 | {Code: "NXT", Name: "Next tab", Desc: "Switch to the next tab", Run: func(ctx Context) error { return ctx.NextTab() }}, | ||
| 23 | {Code: "PVT", Name: "Previous tab", Desc: "Switch to the previous tab", Run: func(ctx Context) error { return ctx.PrevTab() }}, | 23 | {Code: "PVT", Name: "Previous tab", Desc: "Switch to the previous tab", Run: func(ctx Context) error { return ctx.PrevTab() }}, | ||
| 24 | + | {Code: "BSK", Name: "Back session", Desc: "Switch back through recent sessions", Run: func(ctx Context) error { return ctx.BackSession() }}, | |||
| 25 | + | {Code: "FSK", Name: "Forward session", Desc: "Switch forward through recent sessions", Run: func(ctx Context) error { return ctx.ForwardSession() }}, | |||
| 24 | {Code: "SSP", Name: "Session picker", Desc: "Open the session picker", Run: func(ctx Context) error { return ctx.OpenSessionPicker() }}, | 26 | {Code: "SSP", Name: "Session picker", Desc: "Open the session picker", Run: func(ctx Context) error { return ctx.OpenSessionPicker() }}, | ||
| 25 | {Code: "VIS", Name: "Visual mode", Desc: "Enter visual mode", Run: func(ctx Context) error { return ctx.EnterVisualMode() }}, | 27 | {Code: "VIS", Name: "Visual mode", Desc: "Enter visual mode", Run: func(ctx Context) error { return ctx.EnterVisualMode() }}, | ||
| 26 | {Code: "RNS", Name: "Rename session", Desc: "Rename the session (an ephemeral session becomes named)", Run: func(ctx Context) error { return ctx.RenameSession() }}, | 28 | {Code: "RNS", Name: "Rename session", Desc: "Rename the session (an ephemeral session becomes named)", Run: func(ctx Context) error { return ctx.RenameSession() }}, | ||
M internal/usecase/command/registry_test.go +3 -1
| @@ -7,7 +7,7 @@ import ( | |||
| 7 | 7 | ||
| 8 | 8 | func TestRegistryCodesAreUniqueUppercaseThreeLettersInOrder(t *testing.T) { | |
| 9 | 9 | commands := Registry() | |
| 10 | - | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "SSP", "VIS", "RNS", "DET"} | |
| 10 | + | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "DET"} | |
| 11 | 11 | ||
| 12 | 12 | if len(commands) != len(wantCodes) { | |
| 13 | 13 | t.Fatalf("Registry() returned %d commands, want %d", len(commands), len(wantCodes)) | |
| @@ -66,6 +66,8 @@ func TestCommandRunCallsMatchingContextMethod(t *testing.T) { | |||
| 66 | 66 | {code: "FPD", expect: func(ctx *MockContext) { ctx.EXPECT().FocusPaneDown().Return(nil).Once() }}, | |
| 67 | 67 | {code: "NXT", expect: func(ctx *MockContext) { ctx.EXPECT().NextTab().Return(nil).Once() }}, | |
| 68 | 68 | {code: "PVT", expect: func(ctx *MockContext) { ctx.EXPECT().PrevTab().Return(nil).Once() }}, | |
| 69 | + | {code: "BSK", expect: func(ctx *MockContext) { ctx.EXPECT().BackSession().Return(nil).Once() }}, | |
| 70 | + | {code: "FSK", expect: func(ctx *MockContext) { ctx.EXPECT().ForwardSession().Return(nil).Once() }}, | |
| 69 | 71 | {code: "SSP", expect: func(ctx *MockContext) { ctx.EXPECT().OpenSessionPicker().Return(nil).Once() }}, | |
| 70 | 72 | {code: "VIS", expect: func(ctx *MockContext) { ctx.EXPECT().EnterVisualMode().Return(nil).Once() }}, | |
| 71 | 73 | {code: "RNS", expect: func(ctx *MockContext) { ctx.EXPECT().RenameSession().Return(nil).Once() }}, | |
| @@ -7,7 +7,7 @@ import ( | |||||
| 7 | 7 | ||||
| 8 | func TestRegistryCodesAreUniqueUppercaseThreeLettersInOrder(t *testing.T) { | 8 | func TestRegistryCodesAreUniqueUppercaseThreeLettersInOrder(t *testing.T) { | ||
| 9 | commands := Registry() | 9 | commands := Registry() | ||
| 10 | - | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "SSP", "VIS", "RNS", "DET"} | 10 | + | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "DET"} |
| 11 | 11 | ||||
| 12 | if len(commands) != len(wantCodes) { | 12 | if len(commands) != len(wantCodes) { | ||
| 13 | t.Fatalf("Registry() returned %d commands, want %d", len(commands), len(wantCodes)) | 13 | t.Fatalf("Registry() returned %d commands, want %d", len(commands), len(wantCodes)) | ||
| @@ -66,6 +66,8 @@ func TestCommandRunCallsMatchingContextMethod(t *testing.T) { | |||||
| 66 | {code: "FPD", expect: func(ctx *MockContext) { ctx.EXPECT().FocusPaneDown().Return(nil).Once() }}, | 66 | {code: "FPD", expect: func(ctx *MockContext) { ctx.EXPECT().FocusPaneDown().Return(nil).Once() }}, | ||
| 67 | {code: "NXT", expect: func(ctx *MockContext) { ctx.EXPECT().NextTab().Return(nil).Once() }}, | 67 | {code: "NXT", expect: func(ctx *MockContext) { ctx.EXPECT().NextTab().Return(nil).Once() }}, | ||
| 68 | {code: "PVT", expect: func(ctx *MockContext) { ctx.EXPECT().PrevTab().Return(nil).Once() }}, | 68 | {code: "PVT", expect: func(ctx *MockContext) { ctx.EXPECT().PrevTab().Return(nil).Once() }}, | ||
| 69 | + | {code: "BSK", expect: func(ctx *MockContext) { ctx.EXPECT().BackSession().Return(nil).Once() }}, | |||
| 70 | + | {code: "FSK", expect: func(ctx *MockContext) { ctx.EXPECT().ForwardSession().Return(nil).Once() }}, | |||
| 69 | {code: "SSP", expect: func(ctx *MockContext) { ctx.EXPECT().OpenSessionPicker().Return(nil).Once() }}, | 71 | {code: "SSP", expect: func(ctx *MockContext) { ctx.EXPECT().OpenSessionPicker().Return(nil).Once() }}, | ||
| 70 | {code: "VIS", expect: func(ctx *MockContext) { ctx.EXPECT().EnterVisualMode().Return(nil).Once() }}, | 72 | {code: "VIS", expect: func(ctx *MockContext) { ctx.EXPECT().EnterVisualMode().Return(nil).Once() }}, | ||
| 71 | {code: "RNS", expect: func(ctx *MockContext) { ctx.EXPECT().RenameSession().Return(nil).Once() }}, | 73 | {code: "RNS", expect: func(ctx *MockContext) { ctx.EXPECT().RenameSession().Return(nil).Once() }}, | ||
M internal/usecase/daemon/client.go +1 -0
| @@ -56,6 +56,7 @@ type attachedClient struct { | |||
| 56 | 56 | themeMu sync.Mutex | |
| 57 | 57 | theme themeui.Theme | |
| 58 | 58 | lastCursor cursorOut | |
| 59 | + | recentNav recentSessionNavigator | |
| 59 | 60 | linkMu sync.Mutex | |
| 60 | 61 | sendMu sync.Mutex | |
| 61 | 62 | } | |
| @@ -56,6 +56,7 @@ type attachedClient struct { | |||||
| 56 | themeMu sync.Mutex | 56 | themeMu sync.Mutex | ||
| 57 | theme themeui.Theme | 57 | theme themeui.Theme | ||
| 58 | lastCursor cursorOut | 58 | lastCursor cursorOut | ||
| 59 | + | recentNav recentSessionNavigator | |||
| 59 | linkMu sync.Mutex | 60 | linkMu sync.Mutex | ||
| 60 | sendMu sync.Mutex | 61 | sendMu sync.Mutex | ||
| 61 | } | 62 | } | ||
M internal/usecase/daemon/input_test.go +82 -0
| @@ -236,6 +236,88 @@ func TestPaletteNextPreviousSwitchActiveTab(t *testing.T) { | |||
| 236 | 236 | } | |
| 237 | 237 | } | |
| 238 | 238 | ||
| 239 | + | func TestPaletteBackForwardSession(t *testing.T) { | |
| 240 | + | d, current, ac, _, releases := newRecentNavigationTestSessions(t) | |
| 241 | + | defer releaseAll(releases) | |
| 242 | + | recent := d.sessions[domain.SessionID("recent")] | |
| 243 | + | older := d.sessions[domain.SessionID("older")] | |
| 244 | + | ||
| 245 | + | runPaletteCommand(t, d, current, ac, "BSK") | |
| 246 | + | require.Same(t, recent, ac.currentSession()) | |
| 247 | + | ||
| 248 | + | runPaletteCommand(t, d, recent, ac, "BSK") | |
| 249 | + | require.Same(t, older, ac.currentSession()) | |
| 250 | + | ||
| 251 | + | runPaletteCommand(t, d, older, ac, "FSK") | |
| 252 | + | require.Same(t, recent, ac.currentSession()) | |
| 253 | + | ||
| 254 | + | runPaletteCommand(t, d, recent, ac, "FSK") | |
| 255 | + | require.Same(t, current, ac.currentSession()) | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | func TestPaletteBackForwardSessionBoundaries(t *testing.T) { | |
| 259 | + | t.Run("forward from current does not wrap", func(t *testing.T) { | |
| 260 | + | d, current, ac, _, releases := newRecentNavigationTestSessions(t) | |
| 261 | + | defer releaseAll(releases) | |
| 262 | + | ||
| 263 | + | runPaletteCommand(t, d, current, ac, "FSK") | |
| 264 | + | require.Same(t, current, ac.currentSession()) | |
| 265 | + | }) | |
| 266 | + | ||
| 267 | + | t.Run("back with no MRU does not move", func(t *testing.T) { | |
| 268 | + | p, release := newBlockingPTY(t) | |
| 269 | + | defer release() | |
| 270 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, p) | |
| 271 | + | ||
| 272 | + | runPaletteCommand(t, d, sess, ac, "BSK") | |
| 273 | + | require.Same(t, sess, ac.currentSession()) | |
| 274 | + | }) | |
| 275 | + | ||
| 276 | + | t.Run("back at oldest does not wrap", func(t *testing.T) { | |
| 277 | + | d, current, ac, _, releases := newRecentNavigationTestSessions(t) | |
| 278 | + | defer releaseAll(releases) | |
| 279 | + | older := d.sessions[domain.SessionID("older")] | |
| 280 | + | ||
| 281 | + | runPaletteCommand(t, d, current, ac, "BSK") | |
| 282 | + | runPaletteCommand(t, d, ac.currentSession(), ac, "BSK") | |
| 283 | + | require.Same(t, older, ac.currentSession()) | |
| 284 | + | ||
| 285 | + | runPaletteCommand(t, d, older, ac, "BSK") | |
| 286 | + | require.Same(t, older, ac.currentSession()) | |
| 287 | + | }) | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | func newRecentNavigationTestSessions(t *testing.T) (*Daemon, *session, *attachedClient, chan ports.Frame, []func()) { | |
| 291 | + | t.Helper() | |
| 292 | + | p1, release1 := newBlockingPTY(t) | |
| 293 | + | p2, release2 := newBlockingPTY(t) | |
| 294 | + | p3, release3 := newBlockingPTY(t) | |
| 295 | + | d, current, ac, sends := newManualSessionWithPTYs(t, p1) | |
| 296 | + | current.id = "current" | |
| 297 | + | delete(d.sessions, domain.SessionID("manual")) | |
| 298 | + | d.sessions[current.id] = current | |
| 299 | + | recent := &session{id: "recent", name: "recent", ctx: current.ctx, cancel: func() {}, tabs: []*tab{newTab(p2, domain.Size{Cols: 80, Rows: 23})}} | |
| 300 | + | older := &session{id: "older", name: "older", ctx: current.ctx, cancel: func() {}, tabs: []*tab{newTab(p3, domain.Size{Cols: 80, Rows: 23})}} | |
| 301 | + | d.sessions[recent.id] = recent | |
| 302 | + | d.sessions[older.id] = older | |
| 303 | + | current.mruAt.Store(30) | |
| 304 | + | recent.mruAt.Store(20) | |
| 305 | + | older.mruAt.Store(10) | |
| 306 | + | return d, current, ac, sends, []func(){release1, release2, release3} | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | func runPaletteCommand(t *testing.T, d *Daemon, sess *session, ac *attachedClient, code string) { | |
| 310 | + | t.Helper() | |
| 311 | + | d.handleInput(sess, ac, []byte("\x1b ")) | |
| 312 | + | d.handleInput(sess, ac, []byte(code+"\r")) | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | func releaseAll(releases []func()) { | |
| 316 | + | for _, release := range releases { | |
| 317 | + | release() | |
| 318 | + | } | |
| 319 | + | } | |
| 320 | + | ||
| 239 | 321 | func TestAltXClosesActiveTabAndSelectsRemaining(t *testing.T) { | |
| 240 | 322 | writes := make(chan []byte, 1) | |
| 241 | 323 | p1, releasePTY1 := newBlockingPTY(t) | |
| @@ -236,6 +236,88 @@ func TestPaletteNextPreviousSwitchActiveTab(t *testing.T) { | |||||
| 236 | } | 236 | } | ||
| 237 | } | 237 | } | ||
| 238 | 238 | ||||
| 239 | + | func TestPaletteBackForwardSession(t *testing.T) { | |||
| 240 | + | d, current, ac, _, releases := newRecentNavigationTestSessions(t) | |||
| 241 | + | defer releaseAll(releases) | |||
| 242 | + | recent := d.sessions[domain.SessionID("recent")] | |||
| 243 | + | older := d.sessions[domain.SessionID("older")] | |||
| 244 | + | ||||
| 245 | + | runPaletteCommand(t, d, current, ac, "BSK") | |||
| 246 | + | require.Same(t, recent, ac.currentSession()) | |||
| 247 | + | ||||
| 248 | + | runPaletteCommand(t, d, recent, ac, "BSK") | |||
| 249 | + | require.Same(t, older, ac.currentSession()) | |||
| 250 | + | ||||
| 251 | + | runPaletteCommand(t, d, older, ac, "FSK") | |||
| 252 | + | require.Same(t, recent, ac.currentSession()) | |||
| 253 | + | ||||
| 254 | + | runPaletteCommand(t, d, recent, ac, "FSK") | |||
| 255 | + | require.Same(t, current, ac.currentSession()) | |||
| 256 | + | } | |||
| 257 | + | ||||
| 258 | + | func TestPaletteBackForwardSessionBoundaries(t *testing.T) { | |||
| 259 | + | t.Run("forward from current does not wrap", func(t *testing.T) { | |||
| 260 | + | d, current, ac, _, releases := newRecentNavigationTestSessions(t) | |||
| 261 | + | defer releaseAll(releases) | |||
| 262 | + | ||||
| 263 | + | runPaletteCommand(t, d, current, ac, "FSK") | |||
| 264 | + | require.Same(t, current, ac.currentSession()) | |||
| 265 | + | }) | |||
| 266 | + | ||||
| 267 | + | t.Run("back with no MRU does not move", func(t *testing.T) { | |||
| 268 | + | p, release := newBlockingPTY(t) | |||
| 269 | + | defer release() | |||
| 270 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, p) | |||
| 271 | + | ||||
| 272 | + | runPaletteCommand(t, d, sess, ac, "BSK") | |||
| 273 | + | require.Same(t, sess, ac.currentSession()) | |||
| 274 | + | }) | |||
| 275 | + | ||||
| 276 | + | t.Run("back at oldest does not wrap", func(t *testing.T) { | |||
| 277 | + | d, current, ac, _, releases := newRecentNavigationTestSessions(t) | |||
| 278 | + | defer releaseAll(releases) | |||
| 279 | + | older := d.sessions[domain.SessionID("older")] | |||
| 280 | + | ||||
| 281 | + | runPaletteCommand(t, d, current, ac, "BSK") | |||
| 282 | + | runPaletteCommand(t, d, ac.currentSession(), ac, "BSK") | |||
| 283 | + | require.Same(t, older, ac.currentSession()) | |||
| 284 | + | ||||
| 285 | + | runPaletteCommand(t, d, older, ac, "BSK") | |||
| 286 | + | require.Same(t, older, ac.currentSession()) | |||
| 287 | + | }) | |||
| 288 | + | } | |||
| 289 | + | ||||
| 290 | + | func newRecentNavigationTestSessions(t *testing.T) (*Daemon, *session, *attachedClient, chan ports.Frame, []func()) { | |||
| 291 | + | t.Helper() | |||
| 292 | + | p1, release1 := newBlockingPTY(t) | |||
| 293 | + | p2, release2 := newBlockingPTY(t) | |||
| 294 | + | p3, release3 := newBlockingPTY(t) | |||
| 295 | + | d, current, ac, sends := newManualSessionWithPTYs(t, p1) | |||
| 296 | + | current.id = "current" | |||
| 297 | + | delete(d.sessions, domain.SessionID("manual")) | |||
| 298 | + | d.sessions[current.id] = current | |||
| 299 | + | recent := &session{id: "recent", name: "recent", ctx: current.ctx, cancel: func() {}, tabs: []*tab{newTab(p2, domain.Size{Cols: 80, Rows: 23})}} | |||
| 300 | + | older := &session{id: "older", name: "older", ctx: current.ctx, cancel: func() {}, tabs: []*tab{newTab(p3, domain.Size{Cols: 80, Rows: 23})}} | |||
| 301 | + | d.sessions[recent.id] = recent | |||
| 302 | + | d.sessions[older.id] = older | |||
| 303 | + | current.mruAt.Store(30) | |||
| 304 | + | recent.mruAt.Store(20) | |||
| 305 | + | older.mruAt.Store(10) | |||
| 306 | + | return d, current, ac, sends, []func(){release1, release2, release3} | |||
| 307 | + | } | |||
| 308 | + | ||||
| 309 | + | func runPaletteCommand(t *testing.T, d *Daemon, sess *session, ac *attachedClient, code string) { | |||
| 310 | + | t.Helper() | |||
| 311 | + | d.handleInput(sess, ac, []byte("\x1b ")) | |||
| 312 | + | d.handleInput(sess, ac, []byte(code+"\r")) | |||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | func releaseAll(releases []func()) { | |||
| 316 | + | for _, release := range releases { | |||
| 317 | + | release() | |||
| 318 | + | } | |||
| 319 | + | } | |||
| 320 | + | ||||
| 239 | func TestAltXClosesActiveTabAndSelectsRemaining(t *testing.T) { | 321 | func TestAltXClosesActiveTabAndSelectsRemaining(t *testing.T) { | ||
| 240 | writes := make(chan []byte, 1) | 322 | writes := make(chan []byte, 1) | ||
| 241 | p1, releasePTY1 := newBlockingPTY(t) | 323 | p1, releasePTY1 := newBlockingPTY(t) | ||
M internal/usecase/daemon/palette.go +10 -0
| @@ -215,6 +215,16 @@ func (e paletteExec) PrevTab() error { | |||
| 215 | 215 | return nil | |
| 216 | 216 | } | |
| 217 | 217 | ||
| 218 | + | func (e paletteExec) BackSession() error { | |
| 219 | + | e.d.navigateRecentSession(e.sess, e.ac, 1) | |
| 220 | + | return nil | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | func (e paletteExec) ForwardSession() error { | |
| 224 | + | e.d.navigateRecentSession(e.sess, e.ac, -1) | |
| 225 | + | return nil | |
| 226 | + | } | |
| 227 | + | ||
| 218 | 228 | func (e paletteExec) Detach() error { | |
| 219 | 229 | e.d.clientGone(e.sess, e.ac, e.ac.transport(), true) | |
| 220 | 230 | return nil | |
| @@ -215,6 +215,16 @@ func (e paletteExec) PrevTab() error { | |||||
| 215 | return nil | 215 | return nil | ||
| 216 | } | 216 | } | ||
| 217 | 217 | ||||
| 218 | + | func (e paletteExec) BackSession() error { | |||
| 219 | + | e.d.navigateRecentSession(e.sess, e.ac, 1) | |||
| 220 | + | return nil | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | func (e paletteExec) ForwardSession() error { | |||
| 224 | + | e.d.navigateRecentSession(e.sess, e.ac, -1) | |||
| 225 | + | return nil | |||
| 226 | + | } | |||
| 227 | + | ||||
| 218 | func (e paletteExec) Detach() error { | 228 | func (e paletteExec) Detach() error { | ||
| 219 | e.d.clientGone(e.sess, e.ac, e.ac.transport(), true) | 229 | e.d.clientGone(e.sess, e.ac, e.ac.transport(), true) | ||
| 220 | return nil | 230 | return nil | ||
M internal/usecase/daemon/palette_test.go +2 -2
| @@ -112,12 +112,12 @@ func TestPaletteRecentCommandsNewestFirstThenRegistryOrder(t *testing.T) { | |||
| 112 | 112 | for i, cmd := range commands { | |
| 113 | 113 | codes[i] = cmd.Code | |
| 114 | 114 | } | |
| 115 | - | require.Equal(t, []string{"SSP", "NXT", "CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "PVT", "VIS", "RNS", "DET"}, codes) | |
| 115 | + | require.Equal(t, []string{"SSP", "NXT", "CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "PVT", "BSK", "FSK", "VIS", "RNS", "DET"}, codes) | |
| 116 | 116 | } | |
| 117 | 117 | ||
| 118 | 118 | func TestPaletteRecencyCanBeUpdatedConcurrently(t *testing.T) { | |
| 119 | 119 | d := &Daemon{} | |
| 120 | - | codes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "SSP", "VIS", "RNS", "DET"} | |
| 120 | + | codes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "DET"} | |
| 121 | 121 | ||
| 122 | 122 | var wg sync.WaitGroup | |
| 123 | 123 | for range 50 { | |
| @@ -112,12 +112,12 @@ func TestPaletteRecentCommandsNewestFirstThenRegistryOrder(t *testing.T) { | |||||
| 112 | for i, cmd := range commands { | 112 | for i, cmd := range commands { | ||
| 113 | codes[i] = cmd.Code | 113 | codes[i] = cmd.Code | ||
| 114 | } | 114 | } | ||
| 115 | - | require.Equal(t, []string{"SSP", "NXT", "CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "PVT", "VIS", "RNS", "DET"}, codes) | 115 | + | require.Equal(t, []string{"SSP", "NXT", "CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "PVT", "BSK", "FSK", "VIS", "RNS", "DET"}, codes) |
| 116 | } | 116 | } | ||
| 117 | 117 | ||||
| 118 | func TestPaletteRecencyCanBeUpdatedConcurrently(t *testing.T) { | 118 | func TestPaletteRecencyCanBeUpdatedConcurrently(t *testing.T) { | ||
| 119 | d := &Daemon{} | 119 | d := &Daemon{} | ||
| 120 | - | codes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "SSP", "VIS", "RNS", "DET"} | 120 | + | codes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "DET"} |
| 121 | 121 | ||||
| 122 | var wg sync.WaitGroup | 122 | var wg sync.WaitGroup | ||
| 123 | for range 50 { | 123 | for range 50 { | ||
A internal/usecase/daemon/recent_nav.go +78 -0
| @@ -0,0 +1,78 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/vev/internal/domain" | |
| 5 | + | "github.com/bnema/vev/internal/usecase/picker" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | type recentSessionNavigator struct { | |
| 9 | + | ids []domain.SessionID | |
| 10 | + | index int | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | func recentSessionTrail(cur *session, state barState) []domain.SessionID { | |
| 14 | + | if cur == nil { | |
| 15 | + | return nil | |
| 16 | + | } | |
| 17 | + | ids := make([]domain.SessionID, 0, 1+len(state.mru)) | |
| 18 | + | ids = append(ids, cur.id) | |
| 19 | + | for _, entry := range state.mru { | |
| 20 | + | if entry.id != "" { | |
| 21 | + | ids = append(ids, entry.id) | |
| 22 | + | } | |
| 23 | + | } | |
| 24 | + | return ids | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | func (n *recentSessionNavigator) reset(ids []domain.SessionID) { | |
| 28 | + | n.ids = append(n.ids[:0], ids...) | |
| 29 | + | n.index = 0 | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func (d *Daemon) navigateRecentSession(sess *session, ac *attachedClient, delta int) { | |
| 33 | + | if d == nil || sess == nil || ac == nil || delta == 0 { | |
| 34 | + | return | |
| 35 | + | } | |
| 36 | + | trail := recentSessionTrail(sess, d.barStateFor(sess, "")) | |
| 37 | + | if len(trail) == 0 { | |
| 38 | + | d.paint(sess, ac, true) | |
| 39 | + | return | |
| 40 | + | } | |
| 41 | + | if !d.recentNavValid(&ac.recentNav) || ac.recentNav.index < 0 || ac.recentNav.index >= len(ac.recentNav.ids) || ac.recentNav.ids[ac.recentNav.index] != sess.id { | |
| 42 | + | ac.recentNav.reset(trail) | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | next := ac.recentNav.index + delta | |
| 46 | + | if next < 0 || next >= len(ac.recentNav.ids) { | |
| 47 | + | d.paint(sess, ac, true) | |
| 48 | + | return | |
| 49 | + | } | |
| 50 | + | targetID := ac.recentNav.ids[next] | |
| 51 | + | if d.sessionByID(targetID) == nil { | |
| 52 | + | ac.recentNav.reset(trail) | |
| 53 | + | next = ac.recentNav.index + delta | |
| 54 | + | if next < 0 || next >= len(ac.recentNav.ids) { | |
| 55 | + | d.paint(sess, ac, true) | |
| 56 | + | return | |
| 57 | + | } | |
| 58 | + | targetID = ac.recentNav.ids[next] | |
| 59 | + | if d.sessionByID(targetID) == nil { | |
| 60 | + | d.paint(sess, ac, true) | |
| 61 | + | return | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ac.recentNav.index = next | |
| 65 | + | d.switchToTarget(sess, ac, picker.Target{Session: targetID, TabIndex: -1}) | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func (d *Daemon) recentNavValid(n *recentSessionNavigator) bool { | |
| 69 | + | if n == nil || len(n.ids) == 0 { | |
| 70 | + | return false | |
| 71 | + | } | |
| 72 | + | for _, id := range n.ids { | |
| 73 | + | if d.sessionByID(id) == nil { | |
| 74 | + | return false | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | return true | |
| 78 | + | } | |
| @@ -0,0 +1,78 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/vev/internal/domain" | |||
| 5 | + | "github.com/bnema/vev/internal/usecase/picker" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | type recentSessionNavigator struct { | |||
| 9 | + | ids []domain.SessionID | |||
| 10 | + | index int | |||
| 11 | + | } | |||
| 12 | + | ||||
| 13 | + | func recentSessionTrail(cur *session, state barState) []domain.SessionID { | |||
| 14 | + | if cur == nil { | |||
| 15 | + | return nil | |||
| 16 | + | } | |||
| 17 | + | ids := make([]domain.SessionID, 0, 1+len(state.mru)) | |||
| 18 | + | ids = append(ids, cur.id) | |||
| 19 | + | for _, entry := range state.mru { | |||
| 20 | + | if entry.id != "" { | |||
| 21 | + | ids = append(ids, entry.id) | |||
| 22 | + | } | |||
| 23 | + | } | |||
| 24 | + | return ids | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | func (n *recentSessionNavigator) reset(ids []domain.SessionID) { | |||
| 28 | + | n.ids = append(n.ids[:0], ids...) | |||
| 29 | + | n.index = 0 | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func (d *Daemon) navigateRecentSession(sess *session, ac *attachedClient, delta int) { | |||
| 33 | + | if d == nil || sess == nil || ac == nil || delta == 0 { | |||
| 34 | + | return | |||
| 35 | + | } | |||
| 36 | + | trail := recentSessionTrail(sess, d.barStateFor(sess, "")) | |||
| 37 | + | if len(trail) == 0 { | |||
| 38 | + | d.paint(sess, ac, true) | |||
| 39 | + | return | |||
| 40 | + | } | |||
| 41 | + | if !d.recentNavValid(&ac.recentNav) || ac.recentNav.index < 0 || ac.recentNav.index >= len(ac.recentNav.ids) || ac.recentNav.ids[ac.recentNav.index] != sess.id { | |||
| 42 | + | ac.recentNav.reset(trail) | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | next := ac.recentNav.index + delta | |||
| 46 | + | if next < 0 || next >= len(ac.recentNav.ids) { | |||
| 47 | + | d.paint(sess, ac, true) | |||
| 48 | + | return | |||
| 49 | + | } | |||
| 50 | + | targetID := ac.recentNav.ids[next] | |||
| 51 | + | if d.sessionByID(targetID) == nil { | |||
| 52 | + | ac.recentNav.reset(trail) | |||
| 53 | + | next = ac.recentNav.index + delta | |||
| 54 | + | if next < 0 || next >= len(ac.recentNav.ids) { | |||
| 55 | + | d.paint(sess, ac, true) | |||
| 56 | + | return | |||
| 57 | + | } | |||
| 58 | + | targetID = ac.recentNav.ids[next] | |||
| 59 | + | if d.sessionByID(targetID) == nil { | |||
| 60 | + | d.paint(sess, ac, true) | |||
| 61 | + | return | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | ac.recentNav.index = next | |||
| 65 | + | d.switchToTarget(sess, ac, picker.Target{Session: targetID, TabIndex: -1}) | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func (d *Daemon) recentNavValid(n *recentSessionNavigator) bool { | |||
| 69 | + | if n == nil || len(n.ids) == 0 { | |||
| 70 | + | return false | |||
| 71 | + | } | |||
| 72 | + | for _, id := range n.ids { | |||
| 73 | + | if d.sessionByID(id) == nil { | |||
| 74 | + | return false | |||
| 75 | + | } | |||
| 76 | + | } | |||
| 77 | + | return true | |||
| 78 | + | } | |||
M internal/usecase/daemon/status.go +3 -1
| @@ -29,6 +29,7 @@ import ( | |||
| 29 | 29 | "strconv" | |
| 30 | 30 | "time" | |
| 31 | 31 | ||
| 32 | + | "github.com/bnema/vev/internal/domain" | |
| 32 | 33 | themeui "github.com/bnema/vev/internal/usecase/theme" | |
| 33 | 34 | "github.com/bnema/vev/pkg/renderer" | |
| 34 | 35 | ) | |
| @@ -122,6 +123,7 @@ type barState struct { | |||
| 122 | 123 | } | |
| 123 | 124 | ||
| 124 | 125 | type mruSession struct { | |
| 126 | + | id domain.SessionID | |
| 125 | 127 | name string | |
| 126 | 128 | ephemeral bool | |
| 127 | 129 | attention bool | |
| @@ -174,7 +176,7 @@ func (d *Daemon) barStateFor(cur *session, copyFeedback string) barState { | |||
| 174 | 176 | } | |
| 175 | 177 | at := sess.mruAt.Load() | |
| 176 | 178 | sess.mu.Lock() | |
| 177 | - | entry := mruSession{name: sess.name, ephemeral: sess.ephemeral, mruAt: at} | |
| 179 | + | entry := mruSession{id: sess.id, name: sess.name, ephemeral: sess.ephemeral, mruAt: at} | |
| 178 | 180 | for _, tb := range sess.tabs { | |
| 179 | 181 | if tb.attention { | |
| 180 | 182 | entry.attention = true | |
| @@ -29,6 +29,7 @@ import ( | |||||
| 29 | "strconv" | 29 | "strconv" | ||
| 30 | "time" | 30 | "time" | ||
| 31 | 31 | ||||
| 32 | + | "github.com/bnema/vev/internal/domain" | |||
| 32 | themeui "github.com/bnema/vev/internal/usecase/theme" | 33 | themeui "github.com/bnema/vev/internal/usecase/theme" | ||
| 33 | "github.com/bnema/vev/pkg/renderer" | 34 | "github.com/bnema/vev/pkg/renderer" | ||
| 34 | ) | 35 | ) | ||
| @@ -122,6 +123,7 @@ type barState struct { | |||||
| 122 | } | 123 | } | ||
| 123 | 124 | ||||
| 124 | type mruSession struct { | 125 | type mruSession struct { | ||
| 126 | + | id domain.SessionID | |||
| 125 | name string | 127 | name string | ||
| 126 | ephemeral bool | 128 | ephemeral bool | ||
| 127 | attention bool | 129 | attention bool | ||
| @@ -174,7 +176,7 @@ func (d *Daemon) barStateFor(cur *session, copyFeedback string) barState { | |||||
| 174 | } | 176 | } | ||
| 175 | at := sess.mruAt.Load() | 177 | at := sess.mruAt.Load() | ||
| 176 | sess.mu.Lock() | 178 | sess.mu.Lock() | ||
| 177 | - | entry := mruSession{name: sess.name, ephemeral: sess.ephemeral, mruAt: at} | 179 | + | entry := mruSession{id: sess.id, name: sess.name, ephemeral: sess.ephemeral, mruAt: at} |
| 178 | for _, tb := range sess.tabs { | 180 | for _, tb := range sess.tabs { | ||
| 179 | if tb.attention { | 181 | if tb.attention { | ||
| 180 | entry.attention = true | 182 | entry.attention = true | ||