feat(command): expose pane consume and expel
- commit
- ef2479eb95c12a15304867926de3824bc32335f8
- author
- brice · · 2026-07-27 14:00 UTC
- parent
- 3894536
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
14 files · +386 -53
M internal/app/cmd_test.go +13 -3
| @@ -25,6 +25,8 @@ func TestParseCmdArgs(t *testing.T) { | |||
| 25 | 25 | {"slug help", []string{"cmd", "split-right", "--help"}, cmdInvocation{slug: "split-right", help: true}, false}, | |
| 26 | 26 | {"simple", []string{"cmd", "split-right"}, cmdInvocation{slug: "split-right"}, false}, | |
| 27 | 27 | {"resize one-shot", []string{"cmd", "grow-pane-width"}, cmdInvocation{slug: "grow-pane-width"}, false}, | |
| 28 | + | {"consume or expel pane left", []string{"cmd", "consume-or-expel-pane-left"}, cmdInvocation{slug: "consume-or-expel-pane-left"}, false}, | |
| 29 | + | {"consume or expel pane right", []string{"cmd", "consume-or-expel-pane-right"}, cmdInvocation{slug: "consume-or-expel-pane-right"}, false}, | |
| 28 | 30 | {"resize modal is not scriptable", []string{"cmd", "resize-pane"}, cmdInvocation{}, true}, | |
| 29 | 31 | {"session flag", []string{"cmd", "-s", "dev", "new-tab"}, cmdInvocation{slug: "new-tab", session: "dev"}, false}, | |
| 30 | 32 | {"json flag", []string{"cmd", "list-panes", "--json"}, cmdInvocation{slug: "list-panes", jsonOut: true}, false}, | |
| @@ -93,7 +95,7 @@ func TestCmdHelpUsesRegistryWithoutDialing(t *testing.T) { | |||
| 93 | 95 | if called { | |
| 94 | 96 | t.Fatal("help dialed daemon") | |
| 95 | 97 | } | |
| 96 | - | for _, want := range []string{"usage: vev cmd", "split-right", "grow-pane-width", "shrink-pane-width", "grow-pane-height", "shrink-pane-height", "equalize-panes", "list-panes"} { | |
| 98 | + | for _, want := range []string{"usage: vev cmd", "split-right", "consume-or-expel-pane-left", "consume-or-expel-pane-right", "grow-pane-width", "shrink-pane-width", "grow-pane-height", "shrink-pane-height", "equalize-panes", "list-panes"} { | |
| 97 | 99 | if !strings.Contains(out.String(), want) { | |
| 98 | 100 | t.Fatalf("help %q missing %q", out.String(), want) | |
| 99 | 101 | } | |
| @@ -105,8 +107,16 @@ func TestCmdHelpUsesRegistryWithoutDialing(t *testing.T) { | |||
| 105 | 107 | } | |
| 106 | 108 | } | |
| 107 | 109 | ||
| 108 | - | func TestResizeOneShotCmdsParseAndUseSelfTarget(t *testing.T) { | |
| 109 | - | for _, slug := range []string{"grow-pane-width", "shrink-pane-width", "grow-pane-height", "shrink-pane-height", "equalize-panes"} { | |
| 110 | + | func TestTargetPaneCmdsParseAndUseSelfTarget(t *testing.T) { | |
| 111 | + | for _, slug := range []string{ | |
| 112 | + | "grow-pane-width", | |
| 113 | + | "shrink-pane-width", | |
| 114 | + | "grow-pane-height", | |
| 115 | + | "shrink-pane-height", | |
| 116 | + | "equalize-panes", | |
| 117 | + | "consume-or-expel-pane-left", | |
| 118 | + | "consume-or-expel-pane-right", | |
| 119 | + | } { | |
| 110 | 120 | t.Run(slug, func(t *testing.T) { | |
| 111 | 121 | invocation, err := parseArgs([]string{"cmd", "--self", slug}) | |
| 112 | 122 | require.NoError(t, err) | |
| @@ -25,6 +25,8 @@ func TestParseCmdArgs(t *testing.T) { | |||||
| 25 | {"slug help", []string{"cmd", "split-right", "--help"}, cmdInvocation{slug: "split-right", help: true}, false}, | 25 | {"slug help", []string{"cmd", "split-right", "--help"}, cmdInvocation{slug: "split-right", help: true}, false}, | ||
| 26 | {"simple", []string{"cmd", "split-right"}, cmdInvocation{slug: "split-right"}, false}, | 26 | {"simple", []string{"cmd", "split-right"}, cmdInvocation{slug: "split-right"}, false}, | ||
| 27 | {"resize one-shot", []string{"cmd", "grow-pane-width"}, cmdInvocation{slug: "grow-pane-width"}, false}, | 27 | {"resize one-shot", []string{"cmd", "grow-pane-width"}, cmdInvocation{slug: "grow-pane-width"}, false}, | ||
| 28 | + | {"consume or expel pane left", []string{"cmd", "consume-or-expel-pane-left"}, cmdInvocation{slug: "consume-or-expel-pane-left"}, false}, | |||
| 29 | + | {"consume or expel pane right", []string{"cmd", "consume-or-expel-pane-right"}, cmdInvocation{slug: "consume-or-expel-pane-right"}, false}, | |||
| 28 | {"resize modal is not scriptable", []string{"cmd", "resize-pane"}, cmdInvocation{}, true}, | 30 | {"resize modal is not scriptable", []string{"cmd", "resize-pane"}, cmdInvocation{}, true}, | ||
| 29 | {"session flag", []string{"cmd", "-s", "dev", "new-tab"}, cmdInvocation{slug: "new-tab", session: "dev"}, false}, | 31 | {"session flag", []string{"cmd", "-s", "dev", "new-tab"}, cmdInvocation{slug: "new-tab", session: "dev"}, false}, | ||
| 30 | {"json flag", []string{"cmd", "list-panes", "--json"}, cmdInvocation{slug: "list-panes", jsonOut: true}, false}, | 32 | {"json flag", []string{"cmd", "list-panes", "--json"}, cmdInvocation{slug: "list-panes", jsonOut: true}, false}, | ||
| @@ -93,7 +95,7 @@ func TestCmdHelpUsesRegistryWithoutDialing(t *testing.T) { | |||||
| 93 | if called { | 95 | if called { | ||
| 94 | t.Fatal("help dialed daemon") | 96 | t.Fatal("help dialed daemon") | ||
| 95 | } | 97 | } | ||
| 96 | - | for _, want := range []string{"usage: vev cmd", "split-right", "grow-pane-width", "shrink-pane-width", "grow-pane-height", "shrink-pane-height", "equalize-panes", "list-panes"} { | 98 | + | for _, want := range []string{"usage: vev cmd", "split-right", "consume-or-expel-pane-left", "consume-or-expel-pane-right", "grow-pane-width", "shrink-pane-width", "grow-pane-height", "shrink-pane-height", "equalize-panes", "list-panes"} { |
| 97 | if !strings.Contains(out.String(), want) { | 99 | if !strings.Contains(out.String(), want) { | ||
| 98 | t.Fatalf("help %q missing %q", out.String(), want) | 100 | t.Fatalf("help %q missing %q", out.String(), want) | ||
| 99 | } | 101 | } | ||
| @@ -105,8 +107,16 @@ func TestCmdHelpUsesRegistryWithoutDialing(t *testing.T) { | |||||
| 105 | } | 107 | } | ||
| 106 | } | 108 | } | ||
| 107 | 109 | ||||
| 108 | - | func TestResizeOneShotCmdsParseAndUseSelfTarget(t *testing.T) { | 110 | + | func TestTargetPaneCmdsParseAndUseSelfTarget(t *testing.T) { |
| 109 | - | for _, slug := range []string{"grow-pane-width", "shrink-pane-width", "grow-pane-height", "shrink-pane-height", "equalize-panes"} { | 111 | + | for _, slug := range []string{ |
| 112 | + | "grow-pane-width", | |||
| 113 | + | "shrink-pane-width", | |||
| 114 | + | "grow-pane-height", | |||
| 115 | + | "shrink-pane-height", | |||
| 116 | + | "equalize-panes", | |||
| 117 | + | "consume-or-expel-pane-left", | |||
| 118 | + | "consume-or-expel-pane-right", | |||
| 119 | + | } { | |||
| 110 | t.Run(slug, func(t *testing.T) { | 120 | t.Run(slug, func(t *testing.T) { | ||
| 111 | invocation, err := parseArgs([]string{"cmd", "--self", slug}) | 121 | invocation, err := parseArgs([]string{"cmd", "--self", slug}) | ||
| 112 | require.NoError(t, err) | 122 | require.NoError(t, err) | ||
M internal/usecase/command/command.go +4 -0
| @@ -44,6 +44,8 @@ type Context interface { | |||
| 44 | 44 | SplitLeft() error | |
| 45 | 45 | SplitUp() error | |
| 46 | 46 | SplitDown() error | |
| 47 | + | ConsumeOrExpelPaneLeft() error | |
| 48 | + | ConsumeOrExpelPaneRight() error | |
| 47 | 49 | StackPane() error | |
| 48 | 50 | ToggleStack() error | |
| 49 | 51 | ToggleFloatingPane() error | |
| @@ -117,6 +119,8 @@ type ControlContext interface { | |||
| 117 | 119 | SplitLeft() error | |
| 118 | 120 | SplitUp() error | |
| 119 | 121 | SplitDown() error | |
| 122 | + | ConsumeOrExpelPaneLeft() error | |
| 123 | + | ConsumeOrExpelPaneRight() error | |
| 120 | 124 | StackPane() error | |
| 121 | 125 | ToggleStack() error | |
| 122 | 126 | GrowPaneWidth() error | |
| @@ -44,6 +44,8 @@ type Context interface { | |||||
| 44 | SplitLeft() error | 44 | SplitLeft() error | ||
| 45 | SplitUp() error | 45 | SplitUp() error | ||
| 46 | SplitDown() error | 46 | SplitDown() error | ||
| 47 | + | ConsumeOrExpelPaneLeft() error | |||
| 48 | + | ConsumeOrExpelPaneRight() error | |||
| 47 | StackPane() error | 49 | StackPane() error | ||
| 48 | ToggleStack() error | 50 | ToggleStack() error | ||
| 49 | ToggleFloatingPane() error | 51 | ToggleFloatingPane() error | ||
| @@ -117,6 +119,8 @@ type ControlContext interface { | |||||
| 117 | SplitLeft() error | 119 | SplitLeft() error | ||
| 118 | SplitUp() error | 120 | SplitUp() error | ||
| 119 | SplitDown() error | 121 | SplitDown() error | ||
| 122 | + | ConsumeOrExpelPaneLeft() error | |||
| 123 | + | ConsumeOrExpelPaneRight() error | |||
| 120 | StackPane() error | 124 | StackPane() error | ||
| 121 | ToggleStack() error | 125 | ToggleStack() error | ||
| 122 | GrowPaneWidth() error | 126 | GrowPaneWidth() error | ||
M internal/usecase/command/context_mock_test.go +88 -0
| @@ -167,6 +167,94 @@ func (_c *MockContext_CloseTab_Call) RunAndReturn(run func() error) *MockContext | |||
| 167 | 167 | return _c | |
| 168 | 168 | } | |
| 169 | 169 | ||
| 170 | + | // ConsumeOrExpelPaneLeft provides a mock function for the type MockContext | |
| 171 | + | func (_mock *MockContext) ConsumeOrExpelPaneLeft() error { | |
| 172 | + | ret := _mock.Called() | |
| 173 | + | ||
| 174 | + | if len(ret) == 0 { | |
| 175 | + | panic("no return value specified for ConsumeOrExpelPaneLeft") | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | var r0 error | |
| 179 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |
| 180 | + | r0 = returnFunc() | |
| 181 | + | } else { | |
| 182 | + | r0 = ret.Error(0) | |
| 183 | + | } | |
| 184 | + | return r0 | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | // MockContext_ConsumeOrExpelPaneLeft_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConsumeOrExpelPaneLeft' | |
| 188 | + | type MockContext_ConsumeOrExpelPaneLeft_Call struct { | |
| 189 | + | *mock.Call | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | // ConsumeOrExpelPaneLeft is a helper method to define mock.On call | |
| 193 | + | func (_e *MockContext_Expecter) ConsumeOrExpelPaneLeft() *MockContext_ConsumeOrExpelPaneLeft_Call { | |
| 194 | + | return &MockContext_ConsumeOrExpelPaneLeft_Call{Call: _e.mock.On("ConsumeOrExpelPaneLeft")} | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | func (_c *MockContext_ConsumeOrExpelPaneLeft_Call) Run(run func()) *MockContext_ConsumeOrExpelPaneLeft_Call { | |
| 198 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 199 | + | run() | |
| 200 | + | }) | |
| 201 | + | return _c | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | func (_c *MockContext_ConsumeOrExpelPaneLeft_Call) Return(err error) *MockContext_ConsumeOrExpelPaneLeft_Call { | |
| 205 | + | _c.Call.Return(err) | |
| 206 | + | return _c | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | func (_c *MockContext_ConsumeOrExpelPaneLeft_Call) RunAndReturn(run func() error) *MockContext_ConsumeOrExpelPaneLeft_Call { | |
| 210 | + | _c.Call.Return(run) | |
| 211 | + | return _c | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | // ConsumeOrExpelPaneRight provides a mock function for the type MockContext | |
| 215 | + | func (_mock *MockContext) ConsumeOrExpelPaneRight() error { | |
| 216 | + | ret := _mock.Called() | |
| 217 | + | ||
| 218 | + | if len(ret) == 0 { | |
| 219 | + | panic("no return value specified for ConsumeOrExpelPaneRight") | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | var r0 error | |
| 223 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |
| 224 | + | r0 = returnFunc() | |
| 225 | + | } else { | |
| 226 | + | r0 = ret.Error(0) | |
| 227 | + | } | |
| 228 | + | return r0 | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | // MockContext_ConsumeOrExpelPaneRight_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConsumeOrExpelPaneRight' | |
| 232 | + | type MockContext_ConsumeOrExpelPaneRight_Call struct { | |
| 233 | + | *mock.Call | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // ConsumeOrExpelPaneRight is a helper method to define mock.On call | |
| 237 | + | func (_e *MockContext_Expecter) ConsumeOrExpelPaneRight() *MockContext_ConsumeOrExpelPaneRight_Call { | |
| 238 | + | return &MockContext_ConsumeOrExpelPaneRight_Call{Call: _e.mock.On("ConsumeOrExpelPaneRight")} | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | func (_c *MockContext_ConsumeOrExpelPaneRight_Call) Run(run func()) *MockContext_ConsumeOrExpelPaneRight_Call { | |
| 242 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 243 | + | run() | |
| 244 | + | }) | |
| 245 | + | return _c | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | func (_c *MockContext_ConsumeOrExpelPaneRight_Call) Return(err error) *MockContext_ConsumeOrExpelPaneRight_Call { | |
| 249 | + | _c.Call.Return(err) | |
| 250 | + | return _c | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | func (_c *MockContext_ConsumeOrExpelPaneRight_Call) RunAndReturn(run func() error) *MockContext_ConsumeOrExpelPaneRight_Call { | |
| 254 | + | _c.Call.Return(run) | |
| 255 | + | return _c | |
| 256 | + | } | |
| 257 | + | ||
| 170 | 258 | // CreateSession provides a mock function for the type MockContext | |
| 171 | 259 | func (_mock *MockContext) CreateSession() error { | |
| 172 | 260 | ret := _mock.Called() | |
| @@ -167,6 +167,94 @@ func (_c *MockContext_CloseTab_Call) RunAndReturn(run func() error) *MockContext | |||||
| 167 | return _c | 167 | return _c | ||
| 168 | } | 168 | } | ||
| 169 | 169 | ||||
| 170 | + | // ConsumeOrExpelPaneLeft provides a mock function for the type MockContext | |||
| 171 | + | func (_mock *MockContext) ConsumeOrExpelPaneLeft() error { | |||
| 172 | + | ret := _mock.Called() | |||
| 173 | + | ||||
| 174 | + | if len(ret) == 0 { | |||
| 175 | + | panic("no return value specified for ConsumeOrExpelPaneLeft") | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | var r0 error | |||
| 179 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |||
| 180 | + | r0 = returnFunc() | |||
| 181 | + | } else { | |||
| 182 | + | r0 = ret.Error(0) | |||
| 183 | + | } | |||
| 184 | + | return r0 | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | // MockContext_ConsumeOrExpelPaneLeft_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConsumeOrExpelPaneLeft' | |||
| 188 | + | type MockContext_ConsumeOrExpelPaneLeft_Call struct { | |||
| 189 | + | *mock.Call | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | // ConsumeOrExpelPaneLeft is a helper method to define mock.On call | |||
| 193 | + | func (_e *MockContext_Expecter) ConsumeOrExpelPaneLeft() *MockContext_ConsumeOrExpelPaneLeft_Call { | |||
| 194 | + | return &MockContext_ConsumeOrExpelPaneLeft_Call{Call: _e.mock.On("ConsumeOrExpelPaneLeft")} | |||
| 195 | + | } | |||
| 196 | + | ||||
| 197 | + | func (_c *MockContext_ConsumeOrExpelPaneLeft_Call) Run(run func()) *MockContext_ConsumeOrExpelPaneLeft_Call { | |||
| 198 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 199 | + | run() | |||
| 200 | + | }) | |||
| 201 | + | return _c | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | func (_c *MockContext_ConsumeOrExpelPaneLeft_Call) Return(err error) *MockContext_ConsumeOrExpelPaneLeft_Call { | |||
| 205 | + | _c.Call.Return(err) | |||
| 206 | + | return _c | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | func (_c *MockContext_ConsumeOrExpelPaneLeft_Call) RunAndReturn(run func() error) *MockContext_ConsumeOrExpelPaneLeft_Call { | |||
| 210 | + | _c.Call.Return(run) | |||
| 211 | + | return _c | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | // ConsumeOrExpelPaneRight provides a mock function for the type MockContext | |||
| 215 | + | func (_mock *MockContext) ConsumeOrExpelPaneRight() error { | |||
| 216 | + | ret := _mock.Called() | |||
| 217 | + | ||||
| 218 | + | if len(ret) == 0 { | |||
| 219 | + | panic("no return value specified for ConsumeOrExpelPaneRight") | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | var r0 error | |||
| 223 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |||
| 224 | + | r0 = returnFunc() | |||
| 225 | + | } else { | |||
| 226 | + | r0 = ret.Error(0) | |||
| 227 | + | } | |||
| 228 | + | return r0 | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | // MockContext_ConsumeOrExpelPaneRight_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConsumeOrExpelPaneRight' | |||
| 232 | + | type MockContext_ConsumeOrExpelPaneRight_Call struct { | |||
| 233 | + | *mock.Call | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | // ConsumeOrExpelPaneRight is a helper method to define mock.On call | |||
| 237 | + | func (_e *MockContext_Expecter) ConsumeOrExpelPaneRight() *MockContext_ConsumeOrExpelPaneRight_Call { | |||
| 238 | + | return &MockContext_ConsumeOrExpelPaneRight_Call{Call: _e.mock.On("ConsumeOrExpelPaneRight")} | |||
| 239 | + | } | |||
| 240 | + | ||||
| 241 | + | func (_c *MockContext_ConsumeOrExpelPaneRight_Call) Run(run func()) *MockContext_ConsumeOrExpelPaneRight_Call { | |||
| 242 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 243 | + | run() | |||
| 244 | + | }) | |||
| 245 | + | return _c | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | func (_c *MockContext_ConsumeOrExpelPaneRight_Call) Return(err error) *MockContext_ConsumeOrExpelPaneRight_Call { | |||
| 249 | + | _c.Call.Return(err) | |||
| 250 | + | return _c | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | func (_c *MockContext_ConsumeOrExpelPaneRight_Call) RunAndReturn(run func() error) *MockContext_ConsumeOrExpelPaneRight_Call { | |||
| 254 | + | _c.Call.Return(run) | |||
| 255 | + | return _c | |||
| 256 | + | } | |||
| 257 | + | ||||
| 170 | // CreateSession provides a mock function for the type MockContext | 258 | // CreateSession provides a mock function for the type MockContext | ||
| 171 | func (_mock *MockContext) CreateSession() error { | 259 | func (_mock *MockContext) CreateSession() error { | ||
| 172 | ret := _mock.Called() | 260 | ret := _mock.Called() | ||
M internal/usecase/command/registry.go +2 -0
| @@ -12,6 +12,8 @@ func Registry() []Command { | |||
| 12 | 12 | paletteControl("split-left", "SPL", "Split left", "Split the focused pane to the left", TargetPane, func(ctx Context, _ []string) error { return ctx.SplitLeft() }, func(ctx ControlContext) error { return ctx.SplitLeft() }), | |
| 13 | 13 | paletteControl("split-up", "SPU", "Split up", "Split the focused pane upward", TargetPane, func(ctx Context, _ []string) error { return ctx.SplitUp() }, func(ctx ControlContext) error { return ctx.SplitUp() }), | |
| 14 | 14 | paletteControl("split-down", "SPD", "Split down", "Split the focused pane downward", TargetPane, func(ctx Context, _ []string) error { return ctx.SplitDown() }, func(ctx ControlContext) error { return ctx.SplitDown() }), | |
| 15 | + | paletteControl("consume-or-expel-pane-left", "CEL", "Consume or expel pane left", "Move the focused pane into or out of the column on the left", TargetPane, func(ctx Context, _ []string) error { return ctx.ConsumeOrExpelPaneLeft() }, func(ctx ControlContext) error { return ctx.ConsumeOrExpelPaneLeft() }), | |
| 16 | + | paletteControl("consume-or-expel-pane-right", "CER", "Consume or expel pane right", "Move the focused pane into or out of the column on the right", TargetPane, func(ctx Context, _ []string) error { return ctx.ConsumeOrExpelPaneRight() }, func(ctx ControlContext) error { return ctx.ConsumeOrExpelPaneRight() }), | |
| 15 | 17 | paletteControl("stack-pane", "STP", "Stack pane", "Create a new pane in a stack", TargetPane, func(ctx Context, _ []string) error { return ctx.StackPane() }, func(ctx ControlContext) error { return ctx.StackPane() }), | |
| 16 | 18 | paletteControl("toggle-stack", "TST", "Toggle stack", "Toggle the focused pane stack", TargetPane, func(ctx Context, _ []string) error { return ctx.ToggleStack() }, func(ctx ControlContext) error { return ctx.ToggleStack() }), | |
| 17 | 19 | paletteOnly("toggle-floating-pane", "FLT", "Toggle floating pane", "Toggle the floating pane", func(ctx Context, _ []string) error { return ctx.ToggleFloatingPane() }), | |
| @@ -12,6 +12,8 @@ func Registry() []Command { | |||||
| 12 | paletteControl("split-left", "SPL", "Split left", "Split the focused pane to the left", TargetPane, func(ctx Context, _ []string) error { return ctx.SplitLeft() }, func(ctx ControlContext) error { return ctx.SplitLeft() }), | 12 | paletteControl("split-left", "SPL", "Split left", "Split the focused pane to the left", TargetPane, func(ctx Context, _ []string) error { return ctx.SplitLeft() }, func(ctx ControlContext) error { return ctx.SplitLeft() }), | ||
| 13 | paletteControl("split-up", "SPU", "Split up", "Split the focused pane upward", TargetPane, func(ctx Context, _ []string) error { return ctx.SplitUp() }, func(ctx ControlContext) error { return ctx.SplitUp() }), | 13 | paletteControl("split-up", "SPU", "Split up", "Split the focused pane upward", TargetPane, func(ctx Context, _ []string) error { return ctx.SplitUp() }, func(ctx ControlContext) error { return ctx.SplitUp() }), | ||
| 14 | paletteControl("split-down", "SPD", "Split down", "Split the focused pane downward", TargetPane, func(ctx Context, _ []string) error { return ctx.SplitDown() }, func(ctx ControlContext) error { return ctx.SplitDown() }), | 14 | paletteControl("split-down", "SPD", "Split down", "Split the focused pane downward", TargetPane, func(ctx Context, _ []string) error { return ctx.SplitDown() }, func(ctx ControlContext) error { return ctx.SplitDown() }), | ||
| 15 | + | paletteControl("consume-or-expel-pane-left", "CEL", "Consume or expel pane left", "Move the focused pane into or out of the column on the left", TargetPane, func(ctx Context, _ []string) error { return ctx.ConsumeOrExpelPaneLeft() }, func(ctx ControlContext) error { return ctx.ConsumeOrExpelPaneLeft() }), | |||
| 16 | + | paletteControl("consume-or-expel-pane-right", "CER", "Consume or expel pane right", "Move the focused pane into or out of the column on the right", TargetPane, func(ctx Context, _ []string) error { return ctx.ConsumeOrExpelPaneRight() }, func(ctx ControlContext) error { return ctx.ConsumeOrExpelPaneRight() }), | |||
| 15 | paletteControl("stack-pane", "STP", "Stack pane", "Create a new pane in a stack", TargetPane, func(ctx Context, _ []string) error { return ctx.StackPane() }, func(ctx ControlContext) error { return ctx.StackPane() }), | 17 | paletteControl("stack-pane", "STP", "Stack pane", "Create a new pane in a stack", TargetPane, func(ctx Context, _ []string) error { return ctx.StackPane() }, func(ctx ControlContext) error { return ctx.StackPane() }), | ||
| 16 | paletteControl("toggle-stack", "TST", "Toggle stack", "Toggle the focused pane stack", TargetPane, func(ctx Context, _ []string) error { return ctx.ToggleStack() }, func(ctx ControlContext) error { return ctx.ToggleStack() }), | 18 | paletteControl("toggle-stack", "TST", "Toggle stack", "Toggle the focused pane stack", TargetPane, func(ctx Context, _ []string) error { return ctx.ToggleStack() }, func(ctx ControlContext) error { return ctx.ToggleStack() }), | ||
| 17 | paletteOnly("toggle-floating-pane", "FLT", "Toggle floating pane", "Toggle the floating pane", func(ctx Context, _ []string) error { return ctx.ToggleFloatingPane() }), | 19 | paletteOnly("toggle-floating-pane", "FLT", "Toggle floating pane", "Toggle the floating pane", func(ctx Context, _ []string) error { return ctx.ToggleFloatingPane() }), | ||
M internal/usecase/command/registry_test.go +11 -2
| @@ -9,7 +9,7 @@ import ( | |||
| 9 | 9 | ||
| 10 | 10 | func TestRegistryCodesAndSlugsAreUniqueInOrder(t *testing.T) { | |
| 11 | 11 | commands := PaletteRegistry() | |
| 12 | - | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "FLT", "CLP", "FPL", "FPR", "FPU", "FPD", "RSZ", "GPW", "SPW", "GPH", "SPH", "EQP", "NXT", "PVT", "BSK", "JRS", "SSP", "NTC", "YLN", "VIS", "RNS", "RNT", "DET"} | |
| 12 | + | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "CEL", "CER", "STP", "TST", "FLT", "CLP", "FPL", "FPR", "FPU", "FPD", "RSZ", "GPW", "SPW", "GPH", "SPH", "EQP", "NXT", "PVT", "BSK", "JRS", "SSP", "NTC", "YLN", "VIS", "RNS", "RNT", "DET"} | |
| 13 | 13 | ||
| 14 | 14 | if len(commands) != len(wantCodes) { | |
| 15 | 15 | t.Fatalf("Registry() returned %d commands, want %d", len(commands), len(wantCodes)) | |
| @@ -45,6 +45,8 @@ func TestBySlugExactMatch(t *testing.T) { | |||
| 45 | 45 | ok bool | |
| 46 | 46 | }{ | |
| 47 | 47 | {slug: "split-right", ok: true}, | |
| 48 | + | {slug: "consume-or-expel-pane-left", ok: true}, | |
| 49 | + | {slug: "consume-or-expel-pane-right", ok: true}, | |
| 48 | 50 | {slug: "SPLIT-RIGHT", ok: false}, | |
| 49 | 51 | {slug: "toast", ok: true}, | |
| 50 | 52 | {slug: "list-panes", ok: true}, | |
| @@ -62,7 +64,8 @@ func TestBySlugExactMatch(t *testing.T) { | |||
| 62 | 64 | func TestRegistryControlMetadata(t *testing.T) { | |
| 63 | 65 | scriptable := map[string]TargetKind{ | |
| 64 | 66 | "split-right": TargetPane, "split-left": TargetPane, "split-up": TargetPane, | |
| 65 | - | "split-down": TargetPane, "stack-pane": TargetPane, "toggle-stack": TargetPane, | |
| 67 | + | "split-down": TargetPane, "consume-or-expel-pane-left": TargetPane, | |
| 68 | + | "consume-or-expel-pane-right": TargetPane, "stack-pane": TargetPane, "toggle-stack": TargetPane, | |
| 66 | 69 | "close-pane": TargetPane, "focus-pane-left": TargetPane, "focus-pane-right": TargetPane, | |
| 67 | 70 | "focus-pane-up": TargetPane, "focus-pane-down": TargetPane, | |
| 68 | 71 | "new-tab": TargetSession, "close-tab": TargetTab, "next-tab": TargetSession, | |
| @@ -146,6 +149,8 @@ func TestControlHandlersValidateAndDelegate(t *testing.T) { | |||
| 146 | 149 | }{ | |
| 147 | 150 | {name: "zero argument mutation delegates", slug: "split-right", wantCall: "split-right"}, | |
| 148 | 151 | {name: "zero argument mutation rejects args", slug: "split-right", args: []string{"extra"}, wantErr: ErrInvalidArguments}, | |
| 152 | + | {name: "consume or expel left delegates", slug: "consume-or-expel-pane-left", wantCall: "consume-or-expel-pane-left"}, | |
| 153 | + | {name: "consume or expel right delegates", slug: "consume-or-expel-pane-right", wantCall: "consume-or-expel-pane-right"}, | |
| 149 | 154 | {name: "grow width delegates", slug: "grow-pane-width", wantCall: "grow-pane-width"}, | |
| 150 | 155 | {name: "shrink width delegates", slug: "shrink-pane-width", wantCall: "shrink-pane-width"}, | |
| 151 | 156 | {name: "grow height delegates", slug: "grow-pane-height", wantCall: "grow-pane-height"}, | |
| @@ -192,6 +197,8 @@ func (s *controlSpy) SplitRight() error { return s.record("split | |||
| 192 | 197 | func (s *controlSpy) SplitLeft() error { return s.record("split-left") } | |
| 193 | 198 | func (s *controlSpy) SplitUp() error { return s.record("split-up") } | |
| 194 | 199 | func (s *controlSpy) SplitDown() error { return s.record("split-down") } | |
| 200 | + | func (s *controlSpy) ConsumeOrExpelPaneLeft() error { return s.record("consume-or-expel-pane-left") } | |
| 201 | + | func (s *controlSpy) ConsumeOrExpelPaneRight() error { return s.record("consume-or-expel-pane-right") } | |
| 195 | 202 | func (s *controlSpy) StackPane() error { return s.record("stack-pane") } | |
| 196 | 203 | func (s *controlSpy) ToggleStack() error { return s.record("toggle-stack") } | |
| 197 | 204 | func (s *controlSpy) GrowPaneWidth() error { return s.record("grow-pane-width") } | |
| @@ -285,6 +292,8 @@ func TestCommandRunCallsMatchingContextMethod(t *testing.T) { | |||
| 285 | 292 | {code: "SPL", expect: func(ctx *MockContext) { ctx.EXPECT().SplitLeft().Return(nil).Once() }}, | |
| 286 | 293 | {code: "SPU", expect: func(ctx *MockContext) { ctx.EXPECT().SplitUp().Return(nil).Once() }}, | |
| 287 | 294 | {code: "SPD", expect: func(ctx *MockContext) { ctx.EXPECT().SplitDown().Return(nil).Once() }}, | |
| 295 | + | {code: "CEL", expect: func(ctx *MockContext) { ctx.EXPECT().ConsumeOrExpelPaneLeft().Return(nil).Once() }}, | |
| 296 | + | {code: "CER", expect: func(ctx *MockContext) { ctx.EXPECT().ConsumeOrExpelPaneRight().Return(nil).Once() }}, | |
| 288 | 297 | {code: "STP", expect: func(ctx *MockContext) { ctx.EXPECT().StackPane().Return(nil).Once() }}, | |
| 289 | 298 | {code: "TST", expect: func(ctx *MockContext) { ctx.EXPECT().ToggleStack().Return(nil).Once() }}, | |
| 290 | 299 | {code: "FLT", expect: func(ctx *MockContext) { ctx.EXPECT().ToggleFloatingPane().Return(nil).Once() }}, | |
| @@ -9,7 +9,7 @@ import ( | |||||
| 9 | 9 | ||||
| 10 | func TestRegistryCodesAndSlugsAreUniqueInOrder(t *testing.T) { | 10 | func TestRegistryCodesAndSlugsAreUniqueInOrder(t *testing.T) { | ||
| 11 | commands := PaletteRegistry() | 11 | commands := PaletteRegistry() | ||
| 12 | - | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "FLT", "CLP", "FPL", "FPR", "FPU", "FPD", "RSZ", "GPW", "SPW", "GPH", "SPH", "EQP", "NXT", "PVT", "BSK", "JRS", "SSP", "NTC", "YLN", "VIS", "RNS", "RNT", "DET"} | 12 | + | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "CEL", "CER", "STP", "TST", "FLT", "CLP", "FPL", "FPR", "FPU", "FPD", "RSZ", "GPW", "SPW", "GPH", "SPH", "EQP", "NXT", "PVT", "BSK", "JRS", "SSP", "NTC", "YLN", "VIS", "RNS", "RNT", "DET"} |
| 13 | 13 | ||||
| 14 | if len(commands) != len(wantCodes) { | 14 | if len(commands) != len(wantCodes) { | ||
| 15 | t.Fatalf("Registry() returned %d commands, want %d", len(commands), len(wantCodes)) | 15 | t.Fatalf("Registry() returned %d commands, want %d", len(commands), len(wantCodes)) | ||
| @@ -45,6 +45,8 @@ func TestBySlugExactMatch(t *testing.T) { | |||||
| 45 | ok bool | 45 | ok bool | ||
| 46 | }{ | 46 | }{ | ||
| 47 | {slug: "split-right", ok: true}, | 47 | {slug: "split-right", ok: true}, | ||
| 48 | + | {slug: "consume-or-expel-pane-left", ok: true}, | |||
| 49 | + | {slug: "consume-or-expel-pane-right", ok: true}, | |||
| 48 | {slug: "SPLIT-RIGHT", ok: false}, | 50 | {slug: "SPLIT-RIGHT", ok: false}, | ||
| 49 | {slug: "toast", ok: true}, | 51 | {slug: "toast", ok: true}, | ||
| 50 | {slug: "list-panes", ok: true}, | 52 | {slug: "list-panes", ok: true}, | ||
| @@ -62,7 +64,8 @@ func TestBySlugExactMatch(t *testing.T) { | |||||
| 62 | func TestRegistryControlMetadata(t *testing.T) { | 64 | func TestRegistryControlMetadata(t *testing.T) { | ||
| 63 | scriptable := map[string]TargetKind{ | 65 | scriptable := map[string]TargetKind{ | ||
| 64 | "split-right": TargetPane, "split-left": TargetPane, "split-up": TargetPane, | 66 | "split-right": TargetPane, "split-left": TargetPane, "split-up": TargetPane, | ||
| 65 | - | "split-down": TargetPane, "stack-pane": TargetPane, "toggle-stack": TargetPane, | 67 | + | "split-down": TargetPane, "consume-or-expel-pane-left": TargetPane, |
| 68 | + | "consume-or-expel-pane-right": TargetPane, "stack-pane": TargetPane, "toggle-stack": TargetPane, | |||
| 66 | "close-pane": TargetPane, "focus-pane-left": TargetPane, "focus-pane-right": TargetPane, | 69 | "close-pane": TargetPane, "focus-pane-left": TargetPane, "focus-pane-right": TargetPane, | ||
| 67 | "focus-pane-up": TargetPane, "focus-pane-down": TargetPane, | 70 | "focus-pane-up": TargetPane, "focus-pane-down": TargetPane, | ||
| 68 | "new-tab": TargetSession, "close-tab": TargetTab, "next-tab": TargetSession, | 71 | "new-tab": TargetSession, "close-tab": TargetTab, "next-tab": TargetSession, | ||
| @@ -146,6 +149,8 @@ func TestControlHandlersValidateAndDelegate(t *testing.T) { | |||||
| 146 | }{ | 149 | }{ | ||
| 147 | {name: "zero argument mutation delegates", slug: "split-right", wantCall: "split-right"}, | 150 | {name: "zero argument mutation delegates", slug: "split-right", wantCall: "split-right"}, | ||
| 148 | {name: "zero argument mutation rejects args", slug: "split-right", args: []string{"extra"}, wantErr: ErrInvalidArguments}, | 151 | {name: "zero argument mutation rejects args", slug: "split-right", args: []string{"extra"}, wantErr: ErrInvalidArguments}, | ||
| 152 | + | {name: "consume or expel left delegates", slug: "consume-or-expel-pane-left", wantCall: "consume-or-expel-pane-left"}, | |||
| 153 | + | {name: "consume or expel right delegates", slug: "consume-or-expel-pane-right", wantCall: "consume-or-expel-pane-right"}, | |||
| 149 | {name: "grow width delegates", slug: "grow-pane-width", wantCall: "grow-pane-width"}, | 154 | {name: "grow width delegates", slug: "grow-pane-width", wantCall: "grow-pane-width"}, | ||
| 150 | {name: "shrink width delegates", slug: "shrink-pane-width", wantCall: "shrink-pane-width"}, | 155 | {name: "shrink width delegates", slug: "shrink-pane-width", wantCall: "shrink-pane-width"}, | ||
| 151 | {name: "grow height delegates", slug: "grow-pane-height", wantCall: "grow-pane-height"}, | 156 | {name: "grow height delegates", slug: "grow-pane-height", wantCall: "grow-pane-height"}, | ||
| @@ -192,6 +197,8 @@ func (s *controlSpy) SplitRight() error { return s.record("split | |||||
| 192 | func (s *controlSpy) SplitLeft() error { return s.record("split-left") } | 197 | func (s *controlSpy) SplitLeft() error { return s.record("split-left") } | ||
| 193 | func (s *controlSpy) SplitUp() error { return s.record("split-up") } | 198 | func (s *controlSpy) SplitUp() error { return s.record("split-up") } | ||
| 194 | func (s *controlSpy) SplitDown() error { return s.record("split-down") } | 199 | func (s *controlSpy) SplitDown() error { return s.record("split-down") } | ||
| 200 | + | func (s *controlSpy) ConsumeOrExpelPaneLeft() error { return s.record("consume-or-expel-pane-left") } | |||
| 201 | + | func (s *controlSpy) ConsumeOrExpelPaneRight() error { return s.record("consume-or-expel-pane-right") } | |||
| 195 | func (s *controlSpy) StackPane() error { return s.record("stack-pane") } | 202 | func (s *controlSpy) StackPane() error { return s.record("stack-pane") } | ||
| 196 | func (s *controlSpy) ToggleStack() error { return s.record("toggle-stack") } | 203 | func (s *controlSpy) ToggleStack() error { return s.record("toggle-stack") } | ||
| 197 | func (s *controlSpy) GrowPaneWidth() error { return s.record("grow-pane-width") } | 204 | func (s *controlSpy) GrowPaneWidth() error { return s.record("grow-pane-width") } | ||
| @@ -285,6 +292,8 @@ func TestCommandRunCallsMatchingContextMethod(t *testing.T) { | |||||
| 285 | {code: "SPL", expect: func(ctx *MockContext) { ctx.EXPECT().SplitLeft().Return(nil).Once() }}, | 292 | {code: "SPL", expect: func(ctx *MockContext) { ctx.EXPECT().SplitLeft().Return(nil).Once() }}, | ||
| 286 | {code: "SPU", expect: func(ctx *MockContext) { ctx.EXPECT().SplitUp().Return(nil).Once() }}, | 293 | {code: "SPU", expect: func(ctx *MockContext) { ctx.EXPECT().SplitUp().Return(nil).Once() }}, | ||
| 287 | {code: "SPD", expect: func(ctx *MockContext) { ctx.EXPECT().SplitDown().Return(nil).Once() }}, | 294 | {code: "SPD", expect: func(ctx *MockContext) { ctx.EXPECT().SplitDown().Return(nil).Once() }}, | ||
| 295 | + | {code: "CEL", expect: func(ctx *MockContext) { ctx.EXPECT().ConsumeOrExpelPaneLeft().Return(nil).Once() }}, | |||
| 296 | + | {code: "CER", expect: func(ctx *MockContext) { ctx.EXPECT().ConsumeOrExpelPaneRight().Return(nil).Once() }}, | |||
| 288 | {code: "STP", expect: func(ctx *MockContext) { ctx.EXPECT().StackPane().Return(nil).Once() }}, | 297 | {code: "STP", expect: func(ctx *MockContext) { ctx.EXPECT().StackPane().Return(nil).Once() }}, | ||
| 289 | {code: "TST", expect: func(ctx *MockContext) { ctx.EXPECT().ToggleStack().Return(nil).Once() }}, | 298 | {code: "TST", expect: func(ctx *MockContext) { ctx.EXPECT().ToggleStack().Return(nil).Once() }}, | ||
| 290 | {code: "FLT", expect: func(ctx *MockContext) { ctx.EXPECT().ToggleFloatingPane().Return(nil).Once() }}, | 299 | {code: "FLT", expect: func(ctx *MockContext) { ctx.EXPECT().ToggleFloatingPane().Return(nil).Once() }}, | ||
M internal/usecase/daemon/control.go +9 -0
| @@ -409,6 +409,15 @@ func (e controlExec) SplitRight() error { return e.split(layout.Right) } | |||
| 409 | 409 | func (e controlExec) SplitLeft() error { return e.split(layout.Left) } | |
| 410 | 410 | func (e controlExec) SplitUp() error { return e.split(layout.Up) } | |
| 411 | 411 | func (e controlExec) SplitDown() error { return e.split(layout.Down) } | |
| 412 | + | func (e controlExec) consumeOrExpelPane(direction layout.Direction) error { | |
| 413 | + | return e.runAction(daemonActionRequest{kind: daemonActionConsumeOrExpelPane, direction: direction}) | |
| 414 | + | } | |
| 415 | + | func (e controlExec) ConsumeOrExpelPaneLeft() error { | |
| 416 | + | return e.consumeOrExpelPane(layout.Left) | |
| 417 | + | } | |
| 418 | + | func (e controlExec) ConsumeOrExpelPaneRight() error { | |
| 419 | + | return e.consumeOrExpelPane(layout.Right) | |
| 420 | + | } | |
| 412 | 421 | func (e controlExec) StackPane() error { | |
| 413 | 422 | return e.runAction(daemonActionRequest{kind: daemonActionStackPane}) | |
| 414 | 423 | } | |
| @@ -409,6 +409,15 @@ func (e controlExec) SplitRight() error { return e.split(layout.Right) } | |||||
| 409 | func (e controlExec) SplitLeft() error { return e.split(layout.Left) } | 409 | func (e controlExec) SplitLeft() error { return e.split(layout.Left) } | ||
| 410 | func (e controlExec) SplitUp() error { return e.split(layout.Up) } | 410 | func (e controlExec) SplitUp() error { return e.split(layout.Up) } | ||
| 411 | func (e controlExec) SplitDown() error { return e.split(layout.Down) } | 411 | func (e controlExec) SplitDown() error { return e.split(layout.Down) } | ||
| 412 | + | func (e controlExec) consumeOrExpelPane(direction layout.Direction) error { | |||
| 413 | + | return e.runAction(daemonActionRequest{kind: daemonActionConsumeOrExpelPane, direction: direction}) | |||
| 414 | + | } | |||
| 415 | + | func (e controlExec) ConsumeOrExpelPaneLeft() error { | |||
| 416 | + | return e.consumeOrExpelPane(layout.Left) | |||
| 417 | + | } | |||
| 418 | + | func (e controlExec) ConsumeOrExpelPaneRight() error { | |||
| 419 | + | return e.consumeOrExpelPane(layout.Right) | |||
| 420 | + | } | |||
| 412 | func (e controlExec) StackPane() error { | 421 | func (e controlExec) StackPane() error { | ||
| 413 | return e.runAction(daemonActionRequest{kind: daemonActionStackPane}) | 422 | return e.runAction(daemonActionRequest{kind: daemonActionStackPane}) | ||
| 414 | } | 423 | } | ||
M internal/usecase/daemon/control_test.go +92 -21
| @@ -16,28 +16,99 @@ import ( | |||
| 16 | 16 | ) | |
| 17 | 17 | ||
| 18 | 18 | func TestPaletteAndControlShareExplicitDaemonActionTarget(t *testing.T) { | |
| 19 | - | d := newTestDaemon(t, nil, stubClock{}) | |
| 20 | - | sess := addControlSession(d, "work", "t_work", "p_work") | |
| 21 | - | tb := sess.activeTab() | |
| 22 | - | tb.mu.Lock() | |
| 23 | - | pane := tb.focusedPane() | |
| 24 | - | tb.mu.Unlock() | |
| 25 | - | spy := &actionRunnerSpy{} | |
| 26 | - | ||
| 27 | - | require.NoError(t, (paletteExec{d: d, sess: sess, actions: spy}).SplitRight()) | |
| 28 | - | require.NoError(t, (controlExec{ | |
| 29 | - | d: d, sess: sess, tab: tb, actions: spy, | |
| 30 | - | target: daemonActionTarget{session: sess, tab: tb, pane: pane}, | |
| 31 | - | }).SplitRight()) | |
| 32 | - | ||
| 33 | - | require.Len(t, spy.requests, 2) | |
| 34 | - | for _, request := range spy.requests { | |
| 35 | - | require.Equal(t, daemonActionSplitPane, request.kind) | |
| 36 | - | require.Same(t, sess, request.target.session) | |
| 37 | - | require.Same(t, tb, request.target.tab) | |
| 38 | - | require.Same(t, pane, request.target.pane) | |
| 39 | - | require.Equal(t, layout.Right, request.direction) | |
| 19 | + | tests := []struct { | |
| 20 | + | name string | |
| 21 | + | palette func(paletteExec) error | |
| 22 | + | control func(controlExec) error | |
| 23 | + | kind daemonActionKind | |
| 24 | + | direction layout.Direction | |
| 25 | + | }{ | |
| 26 | + | {name: "split right", palette: func(e paletteExec) error { return e.SplitRight() }, control: func(e controlExec) error { return e.SplitRight() }, kind: daemonActionSplitPane, direction: layout.Right}, | |
| 27 | + | {name: "consume or expel left", palette: func(e paletteExec) error { return e.ConsumeOrExpelPaneLeft() }, control: func(e controlExec) error { return e.ConsumeOrExpelPaneLeft() }, kind: daemonActionConsumeOrExpelPane, direction: layout.Left}, | |
| 28 | + | {name: "consume or expel right", palette: func(e paletteExec) error { return e.ConsumeOrExpelPaneRight() }, control: func(e controlExec) error { return e.ConsumeOrExpelPaneRight() }, kind: daemonActionConsumeOrExpelPane, direction: layout.Right}, | |
| 40 | 29 | } | |
| 30 | + | ||
| 31 | + | for _, tt := range tests { | |
| 32 | + | t.Run(tt.name, func(t *testing.T) { | |
| 33 | + | d := newTestDaemon(t, nil, stubClock{}) | |
| 34 | + | sess := addControlSession(d, "work", "t_work", "p_work") | |
| 35 | + | tb := sess.activeTab() | |
| 36 | + | tb.mu.Lock() | |
| 37 | + | pane := tb.focusedPane() | |
| 38 | + | tb.mu.Unlock() | |
| 39 | + | spy := &actionRunnerSpy{} | |
| 40 | + | target := daemonActionTarget{session: sess, tab: tb, pane: pane} | |
| 41 | + | ||
| 42 | + | require.NoError(t, tt.palette(paletteExec{d: d, sess: sess, actions: spy})) | |
| 43 | + | require.NoError(t, tt.control(controlExec{d: d, sess: sess, tab: tb, actions: spy, target: target})) | |
| 44 | + | ||
| 45 | + | require.Len(t, spy.requests, 2) | |
| 46 | + | for _, request := range spy.requests { | |
| 47 | + | require.Equal(t, tt.kind, request.kind) | |
| 48 | + | require.Same(t, sess, request.target.session) | |
| 49 | + | require.Same(t, tb, request.target.tab) | |
| 50 | + | require.Same(t, pane, request.target.pane) | |
| 51 | + | require.Equal(t, tt.direction, request.direction) | |
| 52 | + | } | |
| 53 | + | }) | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | func TestConsumeOrExpelControlSelfTargetsNonFocusedPane(t *testing.T) { | |
| 58 | + | h := newPaneRearrangeHarness(t, domain.Size{Cols: 80, Rows: 22}, threeColumnTree()) | |
| 59 | + | h.tab.mu.Lock() | |
| 60 | + | h.tab.tree.Focus = "pane-1" | |
| 61 | + | beforeGeneration := h.tab.layoutGeneration | |
| 62 | + | tabStableID := h.tab.stableID | |
| 63 | + | targetStableID := h.panes["pane-3"].stableID | |
| 64 | + | h.tab.mu.Unlock() | |
| 65 | + | ||
| 66 | + | result := sendCommand(t, h.daemon, ports.CommandRequest{ | |
| 67 | + | Slug: "consume-or-expel-pane-left", | |
| 68 | + | Self: true, | |
| 69 | + | TargetSession: "work", | |
| 70 | + | TargetTab: tabStableID, | |
| 71 | + | TargetPane: targetStableID, | |
| 72 | + | }) | |
| 73 | + | ||
| 74 | + | require.True(t, result.OK, result.Text) | |
| 75 | + | require.Empty(t, result.Output) | |
| 76 | + | h.session.mu.Lock() | |
| 77 | + | require.Zero(t, h.session.active, "--self must not change the active tab") | |
| 78 | + | h.session.mu.Unlock() | |
| 79 | + | h.tab.mu.Lock() | |
| 80 | + | require.Equal(t, layout.PaneID("pane-3"), h.tab.tree.Focus, "a moved explicit target becomes focused") | |
| 81 | + | require.Equal(t, beforeGeneration+1, h.tab.layoutGeneration) | |
| 82 | + | require.Len(t, h.tab.tree.Root.Children, 2) | |
| 83 | + | h.tab.mu.Unlock() | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | func TestConsumeOrExpelControlEdgeNoopReturnsOKAndPreservesFocus(t *testing.T) { | |
| 87 | + | h := newPaneRearrangeHarness(t, domain.Size{Cols: 80, Rows: 22}, threeColumnTree()) | |
| 88 | + | ac := &attachedClient{} | |
| 89 | + | ac.setSession(h.session) | |
| 90 | + | h.session.mu.Lock() | |
| 91 | + | h.session.client = ac | |
| 92 | + | h.session.mu.Unlock() | |
| 93 | + | invalidations := make(chan renderInvalidation, 1) | |
| 94 | + | rc := newRenderCoordinator(renderCoordinatorOptions{onInvalidate: func(inv renderInvalidation) { invalidations <- inv }}) | |
| 95 | + | rc.attach(ac) | |
| 96 | + | h.session.installRenderCoordinator(rc) | |
| 97 | + | before := h.snapshot() | |
| 98 | + | ||
| 99 | + | result := sendCommand(t, h.daemon, ports.CommandRequest{ | |
| 100 | + | Slug: "consume-or-expel-pane-left", | |
| 101 | + | Self: true, | |
| 102 | + | TargetSession: "work", | |
| 103 | + | TargetTab: h.tab.stableID, | |
| 104 | + | TargetPane: h.panes["pane-1"].stableID, | |
| 105 | + | }) | |
| 106 | + | ||
| 107 | + | require.True(t, result.OK, result.Text) | |
| 108 | + | require.Empty(t, result.Output) | |
| 109 | + | require.Equal(t, before, h.snapshot()) | |
| 110 | + | require.Empty(t, h.daemon.notices.history()) | |
| 111 | + | requireNoInvalidation(t, invalidations) | |
| 41 | 112 | } | |
| 42 | 113 | ||
| 43 | 114 | type actionRunnerSpy struct { | |
| @@ -16,28 +16,99 @@ import ( | |||||
| 16 | ) | 16 | ) | ||
| 17 | 17 | ||||
| 18 | func TestPaletteAndControlShareExplicitDaemonActionTarget(t *testing.T) { | 18 | func TestPaletteAndControlShareExplicitDaemonActionTarget(t *testing.T) { | ||
| 19 | - | d := newTestDaemon(t, nil, stubClock{}) | 19 | + | tests := []struct { |
| 20 | - | sess := addControlSession(d, "work", "t_work", "p_work") | 20 | + | name string |
| 21 | - | tb := sess.activeTab() | 21 | + | palette func(paletteExec) error |
| 22 | - | tb.mu.Lock() | 22 | + | control func(controlExec) error |
| 23 | - | pane := tb.focusedPane() | 23 | + | kind daemonActionKind |
| 24 | - | tb.mu.Unlock() | 24 | + | direction layout.Direction |
| 25 | - | spy := &actionRunnerSpy{} | 25 | + | }{ |
| 26 | - | 26 | + | {name: "split right", palette: func(e paletteExec) error { return e.SplitRight() }, control: func(e controlExec) error { return e.SplitRight() }, kind: daemonActionSplitPane, direction: layout.Right}, | |
| 27 | - | require.NoError(t, (paletteExec{d: d, sess: sess, actions: spy}).SplitRight()) | 27 | + | {name: "consume or expel left", palette: func(e paletteExec) error { return e.ConsumeOrExpelPaneLeft() }, control: func(e controlExec) error { return e.ConsumeOrExpelPaneLeft() }, kind: daemonActionConsumeOrExpelPane, direction: layout.Left}, |
| 28 | - | require.NoError(t, (controlExec{ | 28 | + | {name: "consume or expel right", palette: func(e paletteExec) error { return e.ConsumeOrExpelPaneRight() }, control: func(e controlExec) error { return e.ConsumeOrExpelPaneRight() }, kind: daemonActionConsumeOrExpelPane, direction: layout.Right}, |
| 29 | - | d: d, sess: sess, tab: tb, actions: spy, | |||
| 30 | - | target: daemonActionTarget{session: sess, tab: tb, pane: pane}, | |||
| 31 | - | }).SplitRight()) | |||
| 32 | - | ||||
| 33 | - | require.Len(t, spy.requests, 2) | |||
| 34 | - | for _, request := range spy.requests { | |||
| 35 | - | require.Equal(t, daemonActionSplitPane, request.kind) | |||
| 36 | - | require.Same(t, sess, request.target.session) | |||
| 37 | - | require.Same(t, tb, request.target.tab) | |||
| 38 | - | require.Same(t, pane, request.target.pane) | |||
| 39 | - | require.Equal(t, layout.Right, request.direction) | |||
| 40 | } | 29 | } | ||
| 30 | + | ||||
| 31 | + | for _, tt := range tests { | |||
| 32 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 33 | + | d := newTestDaemon(t, nil, stubClock{}) | |||
| 34 | + | sess := addControlSession(d, "work", "t_work", "p_work") | |||
| 35 | + | tb := sess.activeTab() | |||
| 36 | + | tb.mu.Lock() | |||
| 37 | + | pane := tb.focusedPane() | |||
| 38 | + | tb.mu.Unlock() | |||
| 39 | + | spy := &actionRunnerSpy{} | |||
| 40 | + | target := daemonActionTarget{session: sess, tab: tb, pane: pane} | |||
| 41 | + | ||||
| 42 | + | require.NoError(t, tt.palette(paletteExec{d: d, sess: sess, actions: spy})) | |||
| 43 | + | require.NoError(t, tt.control(controlExec{d: d, sess: sess, tab: tb, actions: spy, target: target})) | |||
| 44 | + | ||||
| 45 | + | require.Len(t, spy.requests, 2) | |||
| 46 | + | for _, request := range spy.requests { | |||
| 47 | + | require.Equal(t, tt.kind, request.kind) | |||
| 48 | + | require.Same(t, sess, request.target.session) | |||
| 49 | + | require.Same(t, tb, request.target.tab) | |||
| 50 | + | require.Same(t, pane, request.target.pane) | |||
| 51 | + | require.Equal(t, tt.direction, request.direction) | |||
| 52 | + | } | |||
| 53 | + | }) | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | func TestConsumeOrExpelControlSelfTargetsNonFocusedPane(t *testing.T) { | |||
| 58 | + | h := newPaneRearrangeHarness(t, domain.Size{Cols: 80, Rows: 22}, threeColumnTree()) | |||
| 59 | + | h.tab.mu.Lock() | |||
| 60 | + | h.tab.tree.Focus = "pane-1" | |||
| 61 | + | beforeGeneration := h.tab.layoutGeneration | |||
| 62 | + | tabStableID := h.tab.stableID | |||
| 63 | + | targetStableID := h.panes["pane-3"].stableID | |||
| 64 | + | h.tab.mu.Unlock() | |||
| 65 | + | ||||
| 66 | + | result := sendCommand(t, h.daemon, ports.CommandRequest{ | |||
| 67 | + | Slug: "consume-or-expel-pane-left", | |||
| 68 | + | Self: true, | |||
| 69 | + | TargetSession: "work", | |||
| 70 | + | TargetTab: tabStableID, | |||
| 71 | + | TargetPane: targetStableID, | |||
| 72 | + | }) | |||
| 73 | + | ||||
| 74 | + | require.True(t, result.OK, result.Text) | |||
| 75 | + | require.Empty(t, result.Output) | |||
| 76 | + | h.session.mu.Lock() | |||
| 77 | + | require.Zero(t, h.session.active, "--self must not change the active tab") | |||
| 78 | + | h.session.mu.Unlock() | |||
| 79 | + | h.tab.mu.Lock() | |||
| 80 | + | require.Equal(t, layout.PaneID("pane-3"), h.tab.tree.Focus, "a moved explicit target becomes focused") | |||
| 81 | + | require.Equal(t, beforeGeneration+1, h.tab.layoutGeneration) | |||
| 82 | + | require.Len(t, h.tab.tree.Root.Children, 2) | |||
| 83 | + | h.tab.mu.Unlock() | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | func TestConsumeOrExpelControlEdgeNoopReturnsOKAndPreservesFocus(t *testing.T) { | |||
| 87 | + | h := newPaneRearrangeHarness(t, domain.Size{Cols: 80, Rows: 22}, threeColumnTree()) | |||
| 88 | + | ac := &attachedClient{} | |||
| 89 | + | ac.setSession(h.session) | |||
| 90 | + | h.session.mu.Lock() | |||
| 91 | + | h.session.client = ac | |||
| 92 | + | h.session.mu.Unlock() | |||
| 93 | + | invalidations := make(chan renderInvalidation, 1) | |||
| 94 | + | rc := newRenderCoordinator(renderCoordinatorOptions{onInvalidate: func(inv renderInvalidation) { invalidations <- inv }}) | |||
| 95 | + | rc.attach(ac) | |||
| 96 | + | h.session.installRenderCoordinator(rc) | |||
| 97 | + | before := h.snapshot() | |||
| 98 | + | ||||
| 99 | + | result := sendCommand(t, h.daemon, ports.CommandRequest{ | |||
| 100 | + | Slug: "consume-or-expel-pane-left", | |||
| 101 | + | Self: true, | |||
| 102 | + | TargetSession: "work", | |||
| 103 | + | TargetTab: h.tab.stableID, | |||
| 104 | + | TargetPane: h.panes["pane-1"].stableID, | |||
| 105 | + | }) | |||
| 106 | + | ||||
| 107 | + | require.True(t, result.OK, result.Text) | |||
| 108 | + | require.Empty(t, result.Output) | |||
| 109 | + | require.Equal(t, before, h.snapshot()) | |||
| 110 | + | require.Empty(t, h.daemon.notices.history()) | |||
| 111 | + | requireNoInvalidation(t, invalidations) | |||
| 41 | } | 112 | } | ||
| 42 | 113 | ||||
| 43 | type actionRunnerSpy struct { | 114 | type actionRunnerSpy struct { | ||
M internal/usecase/daemon/input.go +10 -6
| @@ -292,7 +292,7 @@ func (h daemonKeyHandler) Action(action keys.Action) { | |||
| 292 | 292 | if sess == nil { | |
| 293 | 293 | return | |
| 294 | 294 | } | |
| 295 | - | runResizeAction := func(request daemonActionRequest) { | |
| 295 | + | runAction := func(request daemonActionRequest) { | |
| 296 | 296 | request.target = resolveDaemonActionTarget(sess) | |
| 297 | 297 | runner := h.actions | |
| 298 | 298 | if runner == nil { | |
| @@ -338,15 +338,19 @@ func (h daemonKeyHandler) Action(action keys.Action) { | |||
| 338 | 338 | h.d.reportError(sess, err) | |
| 339 | 339 | } | |
| 340 | 340 | case keys.ActionGrowPaneWidth: | |
| 341 | - | runResizeAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Width, delta: resizeStepCols}) | |
| 341 | + | runAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Width, delta: resizeStepCols}) | |
| 342 | 342 | case keys.ActionShrinkPaneWidth: | |
| 343 | - | runResizeAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Width, delta: -resizeStepCols}) | |
| 343 | + | runAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Width, delta: -resizeStepCols}) | |
| 344 | 344 | case keys.ActionGrowPaneHeight: | |
| 345 | - | runResizeAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Height, delta: resizeStepRows}) | |
| 345 | + | runAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Height, delta: resizeStepRows}) | |
| 346 | 346 | case keys.ActionShrinkPaneHeight: | |
| 347 | - | runResizeAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Height, delta: -resizeStepRows}) | |
| 347 | + | runAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Height, delta: -resizeStepRows}) | |
| 348 | 348 | case keys.ActionEqualizePanes: | |
| 349 | - | runResizeAction(daemonActionRequest{kind: daemonActionEqualizePanes}) | |
| 349 | + | runAction(daemonActionRequest{kind: daemonActionEqualizePanes}) | |
| 350 | + | case keys.ActionConsumeOrExpelPaneLeft: | |
| 351 | + | runAction(daemonActionRequest{kind: daemonActionConsumeOrExpelPane, direction: layout.Left}) | |
| 352 | + | case keys.ActionConsumeOrExpelPaneRight: | |
| 353 | + | runAction(daemonActionRequest{kind: daemonActionConsumeOrExpelPane, direction: layout.Right}) | |
| 350 | 354 | case keys.ActionSwitchTab1, keys.ActionSwitchTab2, keys.ActionSwitchTab3, | |
| 351 | 355 | keys.ActionSwitchTab4, keys.ActionSwitchTab5, keys.ActionSwitchTab6, | |
| 352 | 356 | keys.ActionSwitchTab7, keys.ActionSwitchTab8, keys.ActionSwitchTab9: | |
| @@ -292,7 +292,7 @@ func (h daemonKeyHandler) Action(action keys.Action) { | |||||
| 292 | if sess == nil { | 292 | if sess == nil { | ||
| 293 | return | 293 | return | ||
| 294 | } | 294 | } | ||
| 295 | - | runResizeAction := func(request daemonActionRequest) { | 295 | + | runAction := func(request daemonActionRequest) { |
| 296 | request.target = resolveDaemonActionTarget(sess) | 296 | request.target = resolveDaemonActionTarget(sess) | ||
| 297 | runner := h.actions | 297 | runner := h.actions | ||
| 298 | if runner == nil { | 298 | if runner == nil { | ||
| @@ -338,15 +338,19 @@ func (h daemonKeyHandler) Action(action keys.Action) { | |||||
| 338 | h.d.reportError(sess, err) | 338 | h.d.reportError(sess, err) | ||
| 339 | } | 339 | } | ||
| 340 | case keys.ActionGrowPaneWidth: | 340 | case keys.ActionGrowPaneWidth: | ||
| 341 | - | runResizeAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Width, delta: resizeStepCols}) | 341 | + | runAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Width, delta: resizeStepCols}) |
| 342 | case keys.ActionShrinkPaneWidth: | 342 | case keys.ActionShrinkPaneWidth: | ||
| 343 | - | runResizeAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Width, delta: -resizeStepCols}) | 343 | + | runAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Width, delta: -resizeStepCols}) |
| 344 | case keys.ActionGrowPaneHeight: | 344 | case keys.ActionGrowPaneHeight: | ||
| 345 | - | runResizeAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Height, delta: resizeStepRows}) | 345 | + | runAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Height, delta: resizeStepRows}) |
| 346 | case keys.ActionShrinkPaneHeight: | 346 | case keys.ActionShrinkPaneHeight: | ||
| 347 | - | runResizeAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Height, delta: -resizeStepRows}) | 347 | + | runAction(daemonActionRequest{kind: daemonActionResizePane, axis: layout.Height, delta: -resizeStepRows}) |
| 348 | case keys.ActionEqualizePanes: | 348 | case keys.ActionEqualizePanes: | ||
| 349 | - | runResizeAction(daemonActionRequest{kind: daemonActionEqualizePanes}) | 349 | + | runAction(daemonActionRequest{kind: daemonActionEqualizePanes}) |
| 350 | + | case keys.ActionConsumeOrExpelPaneLeft: | |||
| 351 | + | runAction(daemonActionRequest{kind: daemonActionConsumeOrExpelPane, direction: layout.Left}) | |||
| 352 | + | case keys.ActionConsumeOrExpelPaneRight: | |||
| 353 | + | runAction(daemonActionRequest{kind: daemonActionConsumeOrExpelPane, direction: layout.Right}) | |||
| 350 | case keys.ActionSwitchTab1, keys.ActionSwitchTab2, keys.ActionSwitchTab3, | 354 | case keys.ActionSwitchTab1, keys.ActionSwitchTab2, keys.ActionSwitchTab3, | ||
| 351 | keys.ActionSwitchTab4, keys.ActionSwitchTab5, keys.ActionSwitchTab6, | 355 | keys.ActionSwitchTab4, keys.ActionSwitchTab5, keys.ActionSwitchTab6, | ||
| 352 | keys.ActionSwitchTab7, keys.ActionSwitchTab8, keys.ActionSwitchTab9: | 356 | keys.ActionSwitchTab7, keys.ActionSwitchTab8, keys.ActionSwitchTab9: | ||
M internal/usecase/daemon/input_test.go +70 -0
| @@ -26,6 +26,76 @@ import ( | |||
| 26 | 26 | ||
| 27 | 27 | // --- test doubles ----------------------------------------------------------- | |
| 28 | 28 | ||
| 29 | + | func TestConfiguredConsumeOrExpelActionsRouteThroughDaemonInput(t *testing.T) { | |
| 30 | + | tests := []struct { | |
| 31 | + | name string | |
| 32 | + | binding string | |
| 33 | + | key byte | |
| 34 | + | focus layout.PaneID | |
| 35 | + | wantTarget layout.PaneID | |
| 36 | + | }{ | |
| 37 | + | {name: "left", binding: "consume-or-expel-pane-left", key: 'H', focus: "pane-3", wantTarget: "pane-3"}, | |
| 38 | + | {name: "right", binding: "consume-or-expel-pane-right", key: 'L', focus: "pane-1", wantTarget: "pane-1"}, | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | for _, tt := range tests { | |
| 42 | + | t.Run(tt.name, func(t *testing.T) { | |
| 43 | + | h := newPaneRearrangeHarness(t, domain.Size{Cols: 80, Rows: 22}, threeColumnTree()) | |
| 44 | + | h.tab.mu.Lock() | |
| 45 | + | h.tab.tree.Focus = tt.focus | |
| 46 | + | beforeGeneration := h.tab.layoutGeneration | |
| 47 | + | h.tab.mu.Unlock() | |
| 48 | + | bindings, warnings := keys.BuildBindings(map[string]string{tt.binding: "alt+" + string(tt.key)}) | |
| 49 | + | require.Empty(t, warnings) | |
| 50 | + | h.daemon.bindings.Store(bindings) | |
| 51 | + | ac := &attachedClient{} | |
| 52 | + | ac.setSession(h.session) | |
| 53 | + | ac.keys = keys.NewRouter(h.daemon.clock, daemonKeyHandler{d: h.daemon, ac: ac}, &h.daemon.bindings) | |
| 54 | + | h.session.mu.Lock() | |
| 55 | + | h.session.client = ac | |
| 56 | + | h.session.mu.Unlock() | |
| 57 | + | invalidations := make(chan renderInvalidation, 1) | |
| 58 | + | rc := newRenderCoordinator(renderCoordinatorOptions{onInvalidate: func(inv renderInvalidation) { invalidations <- inv }}) | |
| 59 | + | rc.attach(ac) | |
| 60 | + | h.session.installRenderCoordinator(rc) | |
| 61 | + | ||
| 62 | + | h.daemon.handleInput(h.session, ac, []byte{keys.ESC, tt.key}) | |
| 63 | + | ||
| 64 | + | h.tab.mu.Lock() | |
| 65 | + | require.Equal(t, tt.wantTarget, h.tab.tree.Focus) | |
| 66 | + | require.Equal(t, beforeGeneration+1, h.tab.layoutGeneration) | |
| 67 | + | require.Len(t, h.tab.tree.Root.Children, 2) | |
| 68 | + | h.tab.mu.Unlock() | |
| 69 | + | awaitInvalidation(t, invalidations) | |
| 70 | + | requireNoInvalidation(t, invalidations) | |
| 71 | + | }) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func TestConfiguredConsumeOrExpelEdgeActionIsSilent(t *testing.T) { | |
| 76 | + | h := newPaneRearrangeHarness(t, domain.Size{Cols: 80, Rows: 22}, threeColumnTree()) | |
| 77 | + | bindings, warnings := keys.BuildBindings(map[string]string{"consume-or-expel-pane-left": "alt+H"}) | |
| 78 | + | require.Empty(t, warnings) | |
| 79 | + | h.daemon.bindings.Store(bindings) | |
| 80 | + | ac := &attachedClient{} | |
| 81 | + | ac.setSession(h.session) | |
| 82 | + | ac.keys = keys.NewRouter(h.daemon.clock, daemonKeyHandler{d: h.daemon, ac: ac}, &h.daemon.bindings) | |
| 83 | + | h.session.mu.Lock() | |
| 84 | + | h.session.client = ac | |
| 85 | + | h.session.mu.Unlock() | |
| 86 | + | invalidations := make(chan renderInvalidation, 1) | |
| 87 | + | rc := newRenderCoordinator(renderCoordinatorOptions{onInvalidate: func(inv renderInvalidation) { invalidations <- inv }}) | |
| 88 | + | rc.attach(ac) | |
| 89 | + | h.session.installRenderCoordinator(rc) | |
| 90 | + | before := h.snapshot() | |
| 91 | + | ||
| 92 | + | h.daemon.handleInput(h.session, ac, []byte{keys.ESC, 'H'}) | |
| 93 | + | ||
| 94 | + | require.Equal(t, before, h.snapshot()) | |
| 95 | + | require.Empty(t, h.daemon.notices.history()) | |
| 96 | + | requireNoInvalidation(t, invalidations) | |
| 97 | + | } | |
| 98 | + | ||
| 29 | 99 | func TestResizeActionAdaptersSubmitEquivalentRequests(t *testing.T) { | |
| 30 | 100 | tests := []struct { | |
| 31 | 101 | name string | |
| @@ -26,6 +26,76 @@ import ( | |||||
| 26 | 26 | ||||
| 27 | // --- test doubles ----------------------------------------------------------- | 27 | // --- test doubles ----------------------------------------------------------- | ||
| 28 | 28 | ||||
| 29 | + | func TestConfiguredConsumeOrExpelActionsRouteThroughDaemonInput(t *testing.T) { | |||
| 30 | + | tests := []struct { | |||
| 31 | + | name string | |||
| 32 | + | binding string | |||
| 33 | + | key byte | |||
| 34 | + | focus layout.PaneID | |||
| 35 | + | wantTarget layout.PaneID | |||
| 36 | + | }{ | |||
| 37 | + | {name: "left", binding: "consume-or-expel-pane-left", key: 'H', focus: "pane-3", wantTarget: "pane-3"}, | |||
| 38 | + | {name: "right", binding: "consume-or-expel-pane-right", key: 'L', focus: "pane-1", wantTarget: "pane-1"}, | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | for _, tt := range tests { | |||
| 42 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 43 | + | h := newPaneRearrangeHarness(t, domain.Size{Cols: 80, Rows: 22}, threeColumnTree()) | |||
| 44 | + | h.tab.mu.Lock() | |||
| 45 | + | h.tab.tree.Focus = tt.focus | |||
| 46 | + | beforeGeneration := h.tab.layoutGeneration | |||
| 47 | + | h.tab.mu.Unlock() | |||
| 48 | + | bindings, warnings := keys.BuildBindings(map[string]string{tt.binding: "alt+" + string(tt.key)}) | |||
| 49 | + | require.Empty(t, warnings) | |||
| 50 | + | h.daemon.bindings.Store(bindings) | |||
| 51 | + | ac := &attachedClient{} | |||
| 52 | + | ac.setSession(h.session) | |||
| 53 | + | ac.keys = keys.NewRouter(h.daemon.clock, daemonKeyHandler{d: h.daemon, ac: ac}, &h.daemon.bindings) | |||
| 54 | + | h.session.mu.Lock() | |||
| 55 | + | h.session.client = ac | |||
| 56 | + | h.session.mu.Unlock() | |||
| 57 | + | invalidations := make(chan renderInvalidation, 1) | |||
| 58 | + | rc := newRenderCoordinator(renderCoordinatorOptions{onInvalidate: func(inv renderInvalidation) { invalidations <- inv }}) | |||
| 59 | + | rc.attach(ac) | |||
| 60 | + | h.session.installRenderCoordinator(rc) | |||
| 61 | + | ||||
| 62 | + | h.daemon.handleInput(h.session, ac, []byte{keys.ESC, tt.key}) | |||
| 63 | + | ||||
| 64 | + | h.tab.mu.Lock() | |||
| 65 | + | require.Equal(t, tt.wantTarget, h.tab.tree.Focus) | |||
| 66 | + | require.Equal(t, beforeGeneration+1, h.tab.layoutGeneration) | |||
| 67 | + | require.Len(t, h.tab.tree.Root.Children, 2) | |||
| 68 | + | h.tab.mu.Unlock() | |||
| 69 | + | awaitInvalidation(t, invalidations) | |||
| 70 | + | requireNoInvalidation(t, invalidations) | |||
| 71 | + | }) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func TestConfiguredConsumeOrExpelEdgeActionIsSilent(t *testing.T) { | |||
| 76 | + | h := newPaneRearrangeHarness(t, domain.Size{Cols: 80, Rows: 22}, threeColumnTree()) | |||
| 77 | + | bindings, warnings := keys.BuildBindings(map[string]string{"consume-or-expel-pane-left": "alt+H"}) | |||
| 78 | + | require.Empty(t, warnings) | |||
| 79 | + | h.daemon.bindings.Store(bindings) | |||
| 80 | + | ac := &attachedClient{} | |||
| 81 | + | ac.setSession(h.session) | |||
| 82 | + | ac.keys = keys.NewRouter(h.daemon.clock, daemonKeyHandler{d: h.daemon, ac: ac}, &h.daemon.bindings) | |||
| 83 | + | h.session.mu.Lock() | |||
| 84 | + | h.session.client = ac | |||
| 85 | + | h.session.mu.Unlock() | |||
| 86 | + | invalidations := make(chan renderInvalidation, 1) | |||
| 87 | + | rc := newRenderCoordinator(renderCoordinatorOptions{onInvalidate: func(inv renderInvalidation) { invalidations <- inv }}) | |||
| 88 | + | rc.attach(ac) | |||
| 89 | + | h.session.installRenderCoordinator(rc) | |||
| 90 | + | before := h.snapshot() | |||
| 91 | + | ||||
| 92 | + | h.daemon.handleInput(h.session, ac, []byte{keys.ESC, 'H'}) | |||
| 93 | + | ||||
| 94 | + | require.Equal(t, before, h.snapshot()) | |||
| 95 | + | require.Empty(t, h.daemon.notices.history()) | |||
| 96 | + | requireNoInvalidation(t, invalidations) | |||
| 97 | + | } | |||
| 98 | + | ||||
| 29 | func TestResizeActionAdaptersSubmitEquivalentRequests(t *testing.T) { | 99 | func TestResizeActionAdaptersSubmitEquivalentRequests(t *testing.T) { | ||
| 30 | tests := []struct { | 100 | tests := []struct { | ||
| 31 | name string | 101 | name string | ||
M internal/usecase/daemon/palette.go +9 -0
| @@ -407,6 +407,15 @@ func (e paletteExec) SplitRight() error { return e.split(layout.Right) } | |||
| 407 | 407 | func (e paletteExec) SplitLeft() error { return e.split(layout.Left) } | |
| 408 | 408 | func (e paletteExec) SplitUp() error { return e.split(layout.Up) } | |
| 409 | 409 | func (e paletteExec) SplitDown() error { return e.split(layout.Down) } | |
| 410 | + | func (e paletteExec) consumeOrExpelPane(direction layout.Direction) error { | |
| 411 | + | return e.runAction(daemonActionRequest{kind: daemonActionConsumeOrExpelPane, direction: direction}) | |
| 412 | + | } | |
| 413 | + | func (e paletteExec) ConsumeOrExpelPaneLeft() error { | |
| 414 | + | return e.consumeOrExpelPane(layout.Left) | |
| 415 | + | } | |
| 416 | + | func (e paletteExec) ConsumeOrExpelPaneRight() error { | |
| 417 | + | return e.consumeOrExpelPane(layout.Right) | |
| 418 | + | } | |
| 410 | 419 | ||
| 411 | 420 | func (e paletteExec) StackPane() error { | |
| 412 | 421 | return e.runAction(daemonActionRequest{kind: daemonActionStackPane}) | |
| @@ -407,6 +407,15 @@ func (e paletteExec) SplitRight() error { return e.split(layout.Right) } | |||||
| 407 | func (e paletteExec) SplitLeft() error { return e.split(layout.Left) } | 407 | func (e paletteExec) SplitLeft() error { return e.split(layout.Left) } | ||
| 408 | func (e paletteExec) SplitUp() error { return e.split(layout.Up) } | 408 | func (e paletteExec) SplitUp() error { return e.split(layout.Up) } | ||
| 409 | func (e paletteExec) SplitDown() error { return e.split(layout.Down) } | 409 | func (e paletteExec) SplitDown() error { return e.split(layout.Down) } | ||
| 410 | + | func (e paletteExec) consumeOrExpelPane(direction layout.Direction) error { | |||
| 411 | + | return e.runAction(daemonActionRequest{kind: daemonActionConsumeOrExpelPane, direction: direction}) | |||
| 412 | + | } | |||
| 413 | + | func (e paletteExec) ConsumeOrExpelPaneLeft() error { | |||
| 414 | + | return e.consumeOrExpelPane(layout.Left) | |||
| 415 | + | } | |||
| 416 | + | func (e paletteExec) ConsumeOrExpelPaneRight() error { | |||
| 417 | + | return e.consumeOrExpelPane(layout.Right) | |||
| 418 | + | } | |||
| 410 | 419 | ||||
| 411 | func (e paletteExec) StackPane() error { | 420 | func (e paletteExec) StackPane() error { | ||
| 412 | return e.runAction(daemonActionRequest{kind: daemonActionStackPane}) | 421 | return e.runAction(daemonActionRequest{kind: daemonActionStackPane}) | ||
A internal/usecase/keys/bindings_test.go +51 -0
| @@ -0,0 +1,51 @@ | |||
| 1 | + | package keys | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/require" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestConsumeOrExpelPaneActionsBindWithoutDefaults(t *testing.T) { | |
| 10 | + | tests := []struct { | |
| 11 | + | name string | |
| 12 | + | action Action | |
| 13 | + | key rune | |
| 14 | + | }{ | |
| 15 | + | {name: "consume-or-expel-pane-left", action: ActionConsumeOrExpelPaneLeft, key: 'H'}, | |
| 16 | + | {name: "consume-or-expel-pane-right", action: ActionConsumeOrExpelPaneRight, key: 'L'}, | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | for _, tt := range tests { | |
| 20 | + | t.Run(tt.name, func(t *testing.T) { | |
| 21 | + | bindings, warnings := BuildBindings(map[string]string{ | |
| 22 | + | tt.name: "alt+" + string(tt.key), | |
| 23 | + | }) | |
| 24 | + | ||
| 25 | + | require.Empty(t, warnings) | |
| 26 | + | assertAltRuneAction(t, bindings, tt.key, tt.action) | |
| 27 | + | }) | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | func TestConsumeOrExpelPaneActionsAreAbsentFromAllDefaultBindings(t *testing.T) { | |
| 32 | + | tests := []struct { | |
| 33 | + | name string | |
| 34 | + | action Action | |
| 35 | + | }{ | |
| 36 | + | {name: "left", action: ActionConsumeOrExpelPaneLeft}, | |
| 37 | + | {name: "right", action: ActionConsumeOrExpelPaneRight}, | |
| 38 | + | } | |
| 39 | + | defaults := DefaultBindings() | |
| 40 | + | ||
| 41 | + | for _, tt := range tests { | |
| 42 | + | t.Run(tt.name, func(t *testing.T) { | |
| 43 | + | for _, bound := range defaults.altRunes { | |
| 44 | + | require.NotEqual(t, tt.action, bound) | |
| 45 | + | } | |
| 46 | + | for _, bound := range defaults.altArrows { | |
| 47 | + | require.NotEqual(t, tt.action, bound) | |
| 48 | + | } | |
| 49 | + | }) | |
| 50 | + | } | |
| 51 | + | } | |
| @@ -0,0 +1,51 @@ | |||||
| 1 | + | package keys | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/require" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestConsumeOrExpelPaneActionsBindWithoutDefaults(t *testing.T) { | |||
| 10 | + | tests := []struct { | |||
| 11 | + | name string | |||
| 12 | + | action Action | |||
| 13 | + | key rune | |||
| 14 | + | }{ | |||
| 15 | + | {name: "consume-or-expel-pane-left", action: ActionConsumeOrExpelPaneLeft, key: 'H'}, | |||
| 16 | + | {name: "consume-or-expel-pane-right", action: ActionConsumeOrExpelPaneRight, key: 'L'}, | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | for _, tt := range tests { | |||
| 20 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 21 | + | bindings, warnings := BuildBindings(map[string]string{ | |||
| 22 | + | tt.name: "alt+" + string(tt.key), | |||
| 23 | + | }) | |||
| 24 | + | ||||
| 25 | + | require.Empty(t, warnings) | |||
| 26 | + | assertAltRuneAction(t, bindings, tt.key, tt.action) | |||
| 27 | + | }) | |||
| 28 | + | } | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | func TestConsumeOrExpelPaneActionsAreAbsentFromAllDefaultBindings(t *testing.T) { | |||
| 32 | + | tests := []struct { | |||
| 33 | + | name string | |||
| 34 | + | action Action | |||
| 35 | + | }{ | |||
| 36 | + | {name: "left", action: ActionConsumeOrExpelPaneLeft}, | |||
| 37 | + | {name: "right", action: ActionConsumeOrExpelPaneRight}, | |||
| 38 | + | } | |||
| 39 | + | defaults := DefaultBindings() | |||
| 40 | + | ||||
| 41 | + | for _, tt := range tests { | |||
| 42 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 43 | + | for _, bound := range defaults.altRunes { | |||
| 44 | + | require.NotEqual(t, tt.action, bound) | |||
| 45 | + | } | |||
| 46 | + | for _, bound := range defaults.altArrows { | |||
| 47 | + | require.NotEqual(t, tt.action, bound) | |||
| 48 | + | } | |||
| 49 | + | }) | |||
| 50 | + | } | |||
| 51 | + | } | |||
M internal/usecase/keys/keyspec.go +23 -21
| @@ -49,27 +49,29 @@ func ParseKeySpec(spec string) (KeySpec, error) { | |||
| 49 | 49 | } | |
| 50 | 50 | ||
| 51 | 51 | var actionNames = map[Action]string{ | |
| 52 | - | ActionOpenPalette: "open-palette", | |
| 53 | - | ActionToggleFloatingPane: "toggle-floating-pane", | |
| 54 | - | ActionJumpAttention: "jump-attention", | |
| 55 | - | ActionFocusPaneLeft: "focus-pane-left", | |
| 56 | - | ActionFocusPaneRight: "focus-pane-right", | |
| 57 | - | ActionFocusPaneUp: "focus-pane-up", | |
| 58 | - | ActionFocusPaneDown: "focus-pane-down", | |
| 59 | - | ActionSwitchTab1: "switch-tab-1", | |
| 60 | - | ActionSwitchTab2: "switch-tab-2", | |
| 61 | - | ActionSwitchTab3: "switch-tab-3", | |
| 62 | - | ActionSwitchTab4: "switch-tab-4", | |
| 63 | - | ActionSwitchTab5: "switch-tab-5", | |
| 64 | - | ActionSwitchTab6: "switch-tab-6", | |
| 65 | - | ActionSwitchTab7: "switch-tab-7", | |
| 66 | - | ActionSwitchTab8: "switch-tab-8", | |
| 67 | - | ActionSwitchTab9: "switch-tab-9", | |
| 68 | - | ActionGrowPaneWidth: "grow-pane-width", | |
| 69 | - | ActionShrinkPaneWidth: "shrink-pane-width", | |
| 70 | - | ActionGrowPaneHeight: "grow-pane-height", | |
| 71 | - | ActionShrinkPaneHeight: "shrink-pane-height", | |
| 72 | - | ActionEqualizePanes: "equalize-panes", | |
| 52 | + | ActionOpenPalette: "open-palette", | |
| 53 | + | ActionToggleFloatingPane: "toggle-floating-pane", | |
| 54 | + | ActionJumpAttention: "jump-attention", | |
| 55 | + | ActionFocusPaneLeft: "focus-pane-left", | |
| 56 | + | ActionFocusPaneRight: "focus-pane-right", | |
| 57 | + | ActionFocusPaneUp: "focus-pane-up", | |
| 58 | + | ActionFocusPaneDown: "focus-pane-down", | |
| 59 | + | ActionSwitchTab1: "switch-tab-1", | |
| 60 | + | ActionSwitchTab2: "switch-tab-2", | |
| 61 | + | ActionSwitchTab3: "switch-tab-3", | |
| 62 | + | ActionSwitchTab4: "switch-tab-4", | |
| 63 | + | ActionSwitchTab5: "switch-tab-5", | |
| 64 | + | ActionSwitchTab6: "switch-tab-6", | |
| 65 | + | ActionSwitchTab7: "switch-tab-7", | |
| 66 | + | ActionSwitchTab8: "switch-tab-8", | |
| 67 | + | ActionSwitchTab9: "switch-tab-9", | |
| 68 | + | ActionGrowPaneWidth: "grow-pane-width", | |
| 69 | + | ActionShrinkPaneWidth: "shrink-pane-width", | |
| 70 | + | ActionGrowPaneHeight: "grow-pane-height", | |
| 71 | + | ActionShrinkPaneHeight: "shrink-pane-height", | |
| 72 | + | ActionEqualizePanes: "equalize-panes", | |
| 73 | + | ActionConsumeOrExpelPaneLeft: "consume-or-expel-pane-left", | |
| 74 | + | ActionConsumeOrExpelPaneRight: "consume-or-expel-pane-right", | |
| 73 | 75 | } | |
| 74 | 76 | ||
| 75 | 77 | var actionsByName = func() map[string]Action { | |
| @@ -49,27 +49,29 @@ func ParseKeySpec(spec string) (KeySpec, error) { | |||||
| 49 | } | 49 | } | ||
| 50 | 50 | ||||
| 51 | var actionNames = map[Action]string{ | 51 | var actionNames = map[Action]string{ | ||
| 52 | - | ActionOpenPalette: "open-palette", | 52 | + | ActionOpenPalette: "open-palette", |
| 53 | - | ActionToggleFloatingPane: "toggle-floating-pane", | 53 | + | ActionToggleFloatingPane: "toggle-floating-pane", |
| 54 | - | ActionJumpAttention: "jump-attention", | 54 | + | ActionJumpAttention: "jump-attention", |
| 55 | - | ActionFocusPaneLeft: "focus-pane-left", | 55 | + | ActionFocusPaneLeft: "focus-pane-left", |
| 56 | - | ActionFocusPaneRight: "focus-pane-right", | 56 | + | ActionFocusPaneRight: "focus-pane-right", |
| 57 | - | ActionFocusPaneUp: "focus-pane-up", | 57 | + | ActionFocusPaneUp: "focus-pane-up", |
| 58 | - | ActionFocusPaneDown: "focus-pane-down", | 58 | + | ActionFocusPaneDown: "focus-pane-down", |
| 59 | - | ActionSwitchTab1: "switch-tab-1", | 59 | + | ActionSwitchTab1: "switch-tab-1", |
| 60 | - | ActionSwitchTab2: "switch-tab-2", | 60 | + | ActionSwitchTab2: "switch-tab-2", |
| 61 | - | ActionSwitchTab3: "switch-tab-3", | 61 | + | ActionSwitchTab3: "switch-tab-3", |
| 62 | - | ActionSwitchTab4: "switch-tab-4", | 62 | + | ActionSwitchTab4: "switch-tab-4", |
| 63 | - | ActionSwitchTab5: "switch-tab-5", | 63 | + | ActionSwitchTab5: "switch-tab-5", |
| 64 | - | ActionSwitchTab6: "switch-tab-6", | 64 | + | ActionSwitchTab6: "switch-tab-6", |
| 65 | - | ActionSwitchTab7: "switch-tab-7", | 65 | + | ActionSwitchTab7: "switch-tab-7", |
| 66 | - | ActionSwitchTab8: "switch-tab-8", | 66 | + | ActionSwitchTab8: "switch-tab-8", |
| 67 | - | ActionSwitchTab9: "switch-tab-9", | 67 | + | ActionSwitchTab9: "switch-tab-9", |
| 68 | - | ActionGrowPaneWidth: "grow-pane-width", | 68 | + | ActionGrowPaneWidth: "grow-pane-width", |
| 69 | - | ActionShrinkPaneWidth: "shrink-pane-width", | 69 | + | ActionShrinkPaneWidth: "shrink-pane-width", |
| 70 | - | ActionGrowPaneHeight: "grow-pane-height", | 70 | + | ActionGrowPaneHeight: "grow-pane-height", |
| 71 | - | ActionShrinkPaneHeight: "shrink-pane-height", | 71 | + | ActionShrinkPaneHeight: "shrink-pane-height", |
| 72 | - | ActionEqualizePanes: "equalize-panes", | 72 | + | ActionEqualizePanes: "equalize-panes", |
| 73 | + | ActionConsumeOrExpelPaneLeft: "consume-or-expel-pane-left", | |||
| 74 | + | ActionConsumeOrExpelPaneRight: "consume-or-expel-pane-right", | |||
| 73 | } | 75 | } | ||
| 74 | 76 | ||||
| 75 | var actionsByName = func() map[string]Action { | 77 | var actionsByName = func() map[string]Action { | ||
M internal/usecase/keys/keyspec_test.go +2 -0
| @@ -84,6 +84,8 @@ func TestActionNamesAreCanonical(t *testing.T) { | |||
| 84 | 84 | {ActionGrowPaneHeight, "grow-pane-height"}, | |
| 85 | 85 | {ActionShrinkPaneHeight, "shrink-pane-height"}, | |
| 86 | 86 | {ActionEqualizePanes, "equalize-panes"}, | |
| 87 | + | {ActionConsumeOrExpelPaneLeft, "consume-or-expel-pane-left"}, | |
| 88 | + | {ActionConsumeOrExpelPaneRight, "consume-or-expel-pane-right"}, | |
| 87 | 89 | } | |
| 88 | 90 | ||
| 89 | 91 | for _, tc := range cases { | |
| @@ -84,6 +84,8 @@ func TestActionNamesAreCanonical(t *testing.T) { | |||||
| 84 | {ActionGrowPaneHeight, "grow-pane-height"}, | 84 | {ActionGrowPaneHeight, "grow-pane-height"}, | ||
| 85 | {ActionShrinkPaneHeight, "shrink-pane-height"}, | 85 | {ActionShrinkPaneHeight, "shrink-pane-height"}, | ||
| 86 | {ActionEqualizePanes, "equalize-panes"}, | 86 | {ActionEqualizePanes, "equalize-panes"}, | ||
| 87 | + | {ActionConsumeOrExpelPaneLeft, "consume-or-expel-pane-left"}, | |||
| 88 | + | {ActionConsumeOrExpelPaneRight, "consume-or-expel-pane-right"}, | |||
| 87 | } | 89 | } | ||
| 88 | 90 | ||||
| 89 | for _, tc := range cases { | 91 | for _, tc := range cases { | ||
M internal/usecase/keys/router.go +2 -0
| @@ -43,6 +43,8 @@ const ( | |||
| 43 | 43 | ActionGrowPaneHeight | |
| 44 | 44 | ActionShrinkPaneHeight | |
| 45 | 45 | ActionEqualizePanes | |
| 46 | + | ActionConsumeOrExpelPaneLeft | |
| 47 | + | ActionConsumeOrExpelPaneRight | |
| 46 | 48 | ) | |
| 47 | 49 | ||
| 48 | 50 | // Handler receives router outputs. Forward is called only for bytes that should | |
| @@ -43,6 +43,8 @@ const ( | |||||
| 43 | ActionGrowPaneHeight | 43 | ActionGrowPaneHeight | ||
| 44 | ActionShrinkPaneHeight | 44 | ActionShrinkPaneHeight | ||
| 45 | ActionEqualizePanes | 45 | ActionEqualizePanes | ||
| 46 | + | ActionConsumeOrExpelPaneLeft | |||
| 47 | + | ActionConsumeOrExpelPaneRight | |||
| 46 | ) | 48 | ) | ||
| 47 | 49 | ||||
| 48 | // Handler receives router outputs. Forward is called only for bytes that should | 50 | // Handler receives router outputs. Forward is called only for bytes that should | ||