detached 383c02c

feat(ports): add ImagePush wire message

Adds MsgImagePush (client-originated type 10) carrying a clipboard
image's MIME type and bytes, plus a ClipboardReader port for the
client-side seam that reads it. Bumps ProtocolVersion to 6 (strict
equality negotiation). Lays the wire groundwork for transferring a
clipboard image to a remote pane as a temp-file path (see
docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md).
commit
383c02c4ba3c41dbe5ea2387a2fe60b52c397d6c
author
brice · · 2026-07-04 14:47 UTC
parent
f87757e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +189 -12
M internal/ports/frame.go +11 -10
@@ -1,7 +1,7 @@
11 package ports
22
33 // ProtocolVersion is the current vev IPC wire protocol version.
4-const ProtocolVersion uint16 = 5
4+const ProtocolVersion uint16 = 6
55
66 // MsgType identifies the kind of payload carried by a Frame.
77 type MsgType uint8
@@ -10,15 +10,16 @@ type MsgType uint8
1010 // through 15, server-originated messages from 16, leaving room for growth in
1111 // each band.
1212 const (
13- MsgHello MsgType = 1
14- MsgInput MsgType = 2
15- MsgResize MsgType = 3
16- MsgDetach MsgType = 4
17- MsgPing MsgType = 5
18- MsgList MsgType = 6
19- MsgKill MsgType = 7
20- MsgTheme MsgType = 8
21- MsgAck MsgType = 9
13+ MsgHello MsgType = 1
14+ MsgInput MsgType = 2
15+ MsgResize MsgType = 3
16+ MsgDetach MsgType = 4
17+ MsgPing MsgType = 5
18+ MsgList MsgType = 6
19+ MsgKill MsgType = 7
20+ MsgTheme MsgType = 8
21+ MsgAck MsgType = 9
22+ MsgImagePush MsgType = 10
2223
2324 MsgWelcome MsgType = 16
2425 MsgError MsgType = 17
@@ -1,7 +1,7 @@
1 package ports1 package ports
2 2
3 // ProtocolVersion is the current vev IPC wire protocol version.3 // ProtocolVersion is the current vev IPC wire protocol version.
4-const ProtocolVersion uint16 = 54+const ProtocolVersion uint16 = 6
5 5
6 // MsgType identifies the kind of payload carried by a Frame.6 // MsgType identifies the kind of payload carried by a Frame.
7 type MsgType uint87 type MsgType uint8
@@ -10,15 +10,16 @@ type MsgType uint8
10 // through 15, server-originated messages from 16, leaving room for growth in10 // through 15, server-originated messages from 16, leaving room for growth in
11 // each band.11 // each band.
12 const (12 const (
13- MsgHello MsgType = 113+ MsgHello MsgType = 1
14- MsgInput MsgType = 214+ MsgInput MsgType = 2
15- MsgResize MsgType = 315+ MsgResize MsgType = 3
16- MsgDetach MsgType = 416+ MsgDetach MsgType = 4
17- MsgPing MsgType = 517+ MsgPing MsgType = 5
18- MsgList MsgType = 618+ MsgList MsgType = 6
19- MsgKill MsgType = 719+ MsgKill MsgType = 7
20- MsgTheme MsgType = 820+ MsgTheme MsgType = 8
21- MsgAck MsgType = 921+ MsgAck MsgType = 9
22+ MsgImagePush MsgType = 10
22 23
23 MsgWelcome MsgType = 1624 MsgWelcome MsgType = 16
24 MsgError MsgType = 1725 MsgError MsgType = 17
M internal/ports/frame_test.go +3 -2
@@ -3,8 +3,8 @@ package ports
33 import "testing"
44
55 func TestProtocolVersion(t *testing.T) {
6- if ProtocolVersion != 5 {
7- t.Fatalf("ProtocolVersion = %d, want 5", ProtocolVersion)
6+ if ProtocolVersion != 6 {
7+ t.Fatalf("ProtocolVersion = %d, want 6", ProtocolVersion)
88 }
99 }
1010
@@ -22,6 +22,7 @@ func TestMsgTypeUnique(t *testing.T) {
2222 {"MsgKill", MsgKill},
2323 {"MsgTheme", MsgTheme},
2424 {"MsgAck", MsgAck},
25+ {"MsgImagePush", MsgImagePush},
2526 {"MsgWelcome", MsgWelcome},
2627 {"MsgError", MsgError},
2728 {"MsgOutput", MsgOutput},
@@ -3,8 +3,8 @@ package ports
3 import "testing"3 import "testing"
4 4
5 func TestProtocolVersion(t *testing.T) {5 func TestProtocolVersion(t *testing.T) {
6- if ProtocolVersion != 5 {6+ if ProtocolVersion != 6 {
7- t.Fatalf("ProtocolVersion = %d, want 5", ProtocolVersion)7+ t.Fatalf("ProtocolVersion = %d, want 6", ProtocolVersion)
8 }8 }
9 }9 }
10 10
@@ -22,6 +22,7 @@ func TestMsgTypeUnique(t *testing.T) {
22 {"MsgKill", MsgKill},22 {"MsgKill", MsgKill},
23 {"MsgTheme", MsgTheme},23 {"MsgTheme", MsgTheme},
24 {"MsgAck", MsgAck},24 {"MsgAck", MsgAck},
25+ {"MsgImagePush", MsgImagePush},
25 {"MsgWelcome", MsgWelcome},26 {"MsgWelcome", MsgWelcome},
26 {"MsgError", MsgError},27 {"MsgError", MsgError},
27 {"MsgOutput", MsgOutput},28 {"MsgOutput", MsgOutput},
M internal/ports/mocks/mocks.go +95 -0
@@ -1833,3 +1833,98 @@ func (_c *MockTimer_Stop_Call) RunAndReturn(run func() bool) *MockTimer_Stop_Cal
18331833 _c.Call.Return(run)
18341834 return _c
18351835 }
1836+
1837+// NewMockClipboardReader creates a new instance of MockClipboardReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
1838+// The first argument is typically a *testing.T value.
1839+func NewMockClipboardReader(t interface {
1840+ mock.TestingT
1841+ Cleanup(func())
1842+}) *MockClipboardReader {
1843+ mock := &MockClipboardReader{}
1844+ mock.Mock.Test(t)
1845+
1846+ t.Cleanup(func() { mock.AssertExpectations(t) })
1847+
1848+ return mock
1849+}
1850+
1851+// MockClipboardReader is an autogenerated mock type for the ClipboardReader type
1852+type MockClipboardReader struct {
1853+ mock.Mock
1854+}
1855+
1856+type MockClipboardReader_Expecter struct {
1857+ mock *mock.Mock
1858+}
1859+
1860+func (_m *MockClipboardReader) EXPECT() *MockClipboardReader_Expecter {
1861+ return &MockClipboardReader_Expecter{mock: &_m.Mock}
1862+}
1863+
1864+// ReadImage provides a mock function for the type MockClipboardReader
1865+func (_mock *MockClipboardReader) ReadImage(ctx context.Context) (string, []byte, error) {
1866+ ret := _mock.Called(ctx)
1867+
1868+ if len(ret) == 0 {
1869+ panic("no return value specified for ReadImage")
1870+ }
1871+
1872+ var r0 string
1873+ var r1 []byte
1874+ var r2 error
1875+ if returnFunc, ok := ret.Get(0).(func(context.Context) (string, []byte, error)); ok {
1876+ return returnFunc(ctx)
1877+ }
1878+ if returnFunc, ok := ret.Get(0).(func(context.Context) string); ok {
1879+ r0 = returnFunc(ctx)
1880+ } else {
1881+ r0 = ret.Get(0).(string)
1882+ }
1883+ if returnFunc, ok := ret.Get(1).(func(context.Context) []byte); ok {
1884+ r1 = returnFunc(ctx)
1885+ } else {
1886+ if ret.Get(1) != nil {
1887+ r1 = ret.Get(1).([]byte)
1888+ }
1889+ }
1890+ if returnFunc, ok := ret.Get(2).(func(context.Context) error); ok {
1891+ r2 = returnFunc(ctx)
1892+ } else {
1893+ r2 = ret.Error(2)
1894+ }
1895+ return r0, r1, r2
1896+}
1897+
1898+// MockClipboardReader_ReadImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadImage'
1899+type MockClipboardReader_ReadImage_Call struct {
1900+ *mock.Call
1901+}
1902+
1903+// ReadImage is a helper method to define mock.On call
1904+// - ctx context.Context
1905+func (_e *MockClipboardReader_Expecter) ReadImage(ctx any) *MockClipboardReader_ReadImage_Call {
1906+ return &MockClipboardReader_ReadImage_Call{Call: _e.mock.On("ReadImage", ctx)}
1907+}
1908+
1909+func (_c *MockClipboardReader_ReadImage_Call) Run(run func(ctx context.Context)) *MockClipboardReader_ReadImage_Call {
1910+ _c.Call.Run(func(args mock.Arguments) {
1911+ var arg0 context.Context
1912+ if args[0] != nil {
1913+ arg0 = args[0].(context.Context)
1914+ }
1915+ run(
1916+ arg0,
1917+ )
1918+ })
1919+ return _c
1920+}
1921+
1922+func (_c *MockClipboardReader_ReadImage_Call) Return(mime string, data []byte, err error) *MockClipboardReader_ReadImage_Call {
1923+ _c.Call.Return(mime, data, err)
1924+ return _c
1925+}
1926+
1927+func (_c *MockClipboardReader_ReadImage_Call) RunAndReturn(run func(ctx context.Context) (string, []byte, error)) *MockClipboardReader_ReadImage_Call {
1928+ _c.Call.Return(run)
1929+ return _c
1930+}
@@ -1833,3 +1833,98 @@ func (_c *MockTimer_Stop_Call) RunAndReturn(run func() bool) *MockTimer_Stop_Cal
1833 _c.Call.Return(run)1833 _c.Call.Return(run)
1834 return _c1834 return _c
1835 }1835 }
1836+
1837+// NewMockClipboardReader creates a new instance of MockClipboardReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
1838+// The first argument is typically a *testing.T value.
1839+func NewMockClipboardReader(t interface {
1840+ mock.TestingT
1841+ Cleanup(func())
1842+}) *MockClipboardReader {
1843+ mock := &MockClipboardReader{}
1844+ mock.Mock.Test(t)
1845+
1846+ t.Cleanup(func() { mock.AssertExpectations(t) })
1847+
1848+ return mock
1849+}
1850+
1851+// MockClipboardReader is an autogenerated mock type for the ClipboardReader type
1852+type MockClipboardReader struct {
1853+ mock.Mock
1854+}
1855+
1856+type MockClipboardReader_Expecter struct {
1857+ mock *mock.Mock
1858+}
1859+
1860+func (_m *MockClipboardReader) EXPECT() *MockClipboardReader_Expecter {
1861+ return &MockClipboardReader_Expecter{mock: &_m.Mock}
1862+}
1863+
1864+// ReadImage provides a mock function for the type MockClipboardReader
1865+func (_mock *MockClipboardReader) ReadImage(ctx context.Context) (string, []byte, error) {
1866+ ret := _mock.Called(ctx)
1867+
1868+ if len(ret) == 0 {
1869+ panic("no return value specified for ReadImage")
1870+ }
1871+
1872+ var r0 string
1873+ var r1 []byte
1874+ var r2 error
1875+ if returnFunc, ok := ret.Get(0).(func(context.Context) (string, []byte, error)); ok {
1876+ return returnFunc(ctx)
1877+ }
1878+ if returnFunc, ok := ret.Get(0).(func(context.Context) string); ok {
1879+ r0 = returnFunc(ctx)
1880+ } else {
1881+ r0 = ret.Get(0).(string)
1882+ }
1883+ if returnFunc, ok := ret.Get(1).(func(context.Context) []byte); ok {
1884+ r1 = returnFunc(ctx)
1885+ } else {
1886+ if ret.Get(1) != nil {
1887+ r1 = ret.Get(1).([]byte)
1888+ }
1889+ }
1890+ if returnFunc, ok := ret.Get(2).(func(context.Context) error); ok {
1891+ r2 = returnFunc(ctx)
1892+ } else {
1893+ r2 = ret.Error(2)
1894+ }
1895+ return r0, r1, r2
1896+}
1897+
1898+// MockClipboardReader_ReadImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadImage'
1899+type MockClipboardReader_ReadImage_Call struct {
1900+ *mock.Call
1901+}
1902+
1903+// ReadImage is a helper method to define mock.On call
1904+// - ctx context.Context
1905+func (_e *MockClipboardReader_Expecter) ReadImage(ctx any) *MockClipboardReader_ReadImage_Call {
1906+ return &MockClipboardReader_ReadImage_Call{Call: _e.mock.On("ReadImage", ctx)}
1907+}
1908+
1909+func (_c *MockClipboardReader_ReadImage_Call) Run(run func(ctx context.Context)) *MockClipboardReader_ReadImage_Call {
1910+ _c.Call.Run(func(args mock.Arguments) {
1911+ var arg0 context.Context
1912+ if args[0] != nil {
1913+ arg0 = args[0].(context.Context)
1914+ }
1915+ run(
1916+ arg0,
1917+ )
1918+ })
1919+ return _c
1920+}
1921+
1922+func (_c *MockClipboardReader_ReadImage_Call) Return(mime string, data []byte, err error) *MockClipboardReader_ReadImage_Call {
1923+ _c.Call.Return(mime, data, err)
1924+ return _c
1925+}
1926+
1927+func (_c *MockClipboardReader_ReadImage_Call) RunAndReturn(run func(ctx context.Context) (string, []byte, error)) *MockClipboardReader_ReadImage_Call {
1928+ _c.Call.Return(run)
1929+ return _c
1930+}
M internal/ports/ports.go +18 -0
@@ -2,12 +2,18 @@ package ports
22
33 import (
44 "context"
5+ "errors"
56 "io"
67 "time"
78
89 "github.com/bnema/vev/internal/domain"
910 )
1011
12+// ErrNoClipboardImage is returned by ClipboardReader.ReadImage when the
13+// clipboard holds no image (or an unsupported type). Callers fall back to
14+// forwarding whatever keystroke triggered the read.
15+var ErrNoClipboardImage = errors.New("ports: no image on clipboard")
16+
1117 // PTY is a running pseudo-terminal-backed child process.
1218 type PTY interface {
1319 io.ReadWriteCloser // Read = child output, Write = child input
@@ -75,3 +81,15 @@ type Timer interface {
7581 Reset(d time.Duration) bool
7682 Stop() bool
7783 }
84+
85+// ClipboardReader reads an image off the CLIENT-side system clipboard, for
86+// forwarding to a remote session's focused pane. Implementations live in
87+// internal/adapters (e.g. a wl-paste-backed one for Wayland); usecases only
88+// see this interface. See
89+// docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md.
90+type ClipboardReader interface {
91+ // ReadImage returns the clipboard's image bytes and MIME type. It returns
92+ // ErrNoClipboardImage when the clipboard holds no image (or an
93+ // unsupported type).
94+ ReadImage(ctx context.Context) (mime string, data []byte, err error)
95+}
@@ -2,12 +2,18 @@ package ports
2 2
3 import (3 import (
4 "context"4 "context"
5+ "errors"
5 "io"6 "io"
6 "time"7 "time"
7 8
8 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
9 )10 )
10 11
12+// ErrNoClipboardImage is returned by ClipboardReader.ReadImage when the
13+// clipboard holds no image (or an unsupported type). Callers fall back to
14+// forwarding whatever keystroke triggered the read.
15+var ErrNoClipboardImage = errors.New("ports: no image on clipboard")
16+
11 // PTY is a running pseudo-terminal-backed child process.17 // PTY is a running pseudo-terminal-backed child process.
12 type PTY interface {18 type PTY interface {
13 io.ReadWriteCloser // Read = child output, Write = child input19 io.ReadWriteCloser // Read = child output, Write = child input
@@ -75,3 +81,15 @@ type Timer interface {
75 Reset(d time.Duration) bool81 Reset(d time.Duration) bool
76 Stop() bool82 Stop() bool
77 }83 }
84+
85+// ClipboardReader reads an image off the CLIENT-side system clipboard, for
86+// forwarding to a remote session's focused pane. Implementations live in
87+// internal/adapters (e.g. a wl-paste-backed one for Wayland); usecases only
88+// see this interface. See
89+// docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md.
90+type ClipboardReader interface {
91+ // ReadImage returns the clipboard's image bytes and MIME type. It returns
92+ // ErrNoClipboardImage when the clipboard holds no image (or an
93+ // unsupported type).
94+ ReadImage(ctx context.Context) (mime string, data []byte, err error)
95+}
M internal/ports/wire.go +28 -0
@@ -87,6 +87,14 @@ type Theme struct {
8787 TrueColor bool
8888 }
8989
90+// ImagePush carries a clipboard image from a remote client, to be written to
91+// a temp file and injected into the focused pane's PTY as a path (see
92+// docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md).
93+type ImagePush struct {
94+ Mime string
95+ Data []byte
96+}
97+
9098 // Detach asks the daemon to detach the current client without killing the
9199 // session.
92100 type Detach struct{}
@@ -357,6 +365,26 @@ func UnmarshalInput(b []byte) (Input, error) {
357365 return Input{InputSeq: seq, Data: r.rest()}, nil
358366 }
359367
368+// MarshalImagePush encodes m into an ImagePush message payload.
369+func MarshalImagePush(m ImagePush) []byte {
370+ w := payloadWriter{}
371+ w.putString(m.Mime)
372+ w.putBytes(m.Data)
373+ return w.b
374+}
375+
376+// UnmarshalImagePush decodes an ImagePush message payload. After the
377+// length-prefixed mime string, the rest of the payload is data; there is no
378+// length prefix for it.
379+func UnmarshalImagePush(b []byte) (ImagePush, error) {
380+ r := payloadReader{b: b}
381+ mime, err := r.getString()
382+ if err != nil {
383+ return ImagePush{}, err
384+ }
385+ return ImagePush{Mime: mime, Data: r.rest()}, nil
386+}
387+
360388 // MarshalResize encodes m into a Resize message payload.
361389 func MarshalResize(m Resize) []byte {
362390 w := payloadWriter{}
@@ -87,6 +87,14 @@ type Theme struct {
87 TrueColor bool87 TrueColor bool
88 }88 }
89 89
90+// ImagePush carries a clipboard image from a remote client, to be written to
91+// a temp file and injected into the focused pane's PTY as a path (see
92+// docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md).
93+type ImagePush struct {
94+ Mime string
95+ Data []byte
96+}
97+
90 // Detach asks the daemon to detach the current client without killing the98 // Detach asks the daemon to detach the current client without killing the
91 // session.99 // session.
92 type Detach struct{}100 type Detach struct{}
@@ -357,6 +365,26 @@ func UnmarshalInput(b []byte) (Input, error) {
357 return Input{InputSeq: seq, Data: r.rest()}, nil365 return Input{InputSeq: seq, Data: r.rest()}, nil
358 }366 }
359 367
368+// MarshalImagePush encodes m into an ImagePush message payload.
369+func MarshalImagePush(m ImagePush) []byte {
370+ w := payloadWriter{}
371+ w.putString(m.Mime)
372+ w.putBytes(m.Data)
373+ return w.b
374+}
375+
376+// UnmarshalImagePush decodes an ImagePush message payload. After the
377+// length-prefixed mime string, the rest of the payload is data; there is no
378+// length prefix for it.
379+func UnmarshalImagePush(b []byte) (ImagePush, error) {
380+ r := payloadReader{b: b}
381+ mime, err := r.getString()
382+ if err != nil {
383+ return ImagePush{}, err
384+ }
385+ return ImagePush{Mime: mime, Data: r.rest()}, nil
386+}
387+
360 // MarshalResize encodes m into a Resize message payload.388 // MarshalResize encodes m into a Resize message payload.
361 func MarshalResize(m Resize) []byte {389 func MarshalResize(m Resize) []byte {
362 w := payloadWriter{}390 w := payloadWriter{}
M internal/ports/wire_test.go +34 -0
@@ -140,6 +140,40 @@ func TestInputGoldenAndRoundTrip(t *testing.T) {
140140 assertAllPrefixesFail(t, full[:8], UnmarshalInput)
141141 }
142142
143+func TestImagePushGoldenAndRoundTrip(t *testing.T) {
144+ tests := []struct {
145+ name string
146+ msg ImagePush
147+ want []byte
148+ }{
149+ {
150+ name: "png data",
151+ msg: ImagePush{Mime: "image/png", Data: []byte{0x01, 0x02, 0x03}},
152+ want: []byte{0x00, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x01, 0x02, 0x03},
153+ },
154+ {name: "empty", msg: ImagePush{}, want: []byte{0x00, 0x00}},
155+ }
156+
157+ for _, tt := range tests {
158+ t.Run(tt.name, func(t *testing.T) {
159+ got := MarshalImagePush(tt.msg)
160+ if !bytes.Equal(got, tt.want) {
161+ t.Fatalf("MarshalImagePush() = %#v, want %#v", got, tt.want)
162+ }
163+ back, err := UnmarshalImagePush(got)
164+ if err != nil {
165+ t.Fatalf("UnmarshalImagePush() error = %v", err)
166+ }
167+ if !reflect.DeepEqual(back, tt.msg) {
168+ t.Fatalf("round trip = %#v, want %#v", back, tt.msg)
169+ }
170+ })
171+ }
172+
173+ full := MarshalImagePush(tests[0].msg)
174+ assertAllPrefixesFail(t, full[:11], UnmarshalImagePush)
175+}
176+
143177 func TestThemeGoldenAndRoundTrip(t *testing.T) {
144178 tests := []struct {
145179 name string
@@ -140,6 +140,40 @@ func TestInputGoldenAndRoundTrip(t *testing.T) {
140 assertAllPrefixesFail(t, full[:8], UnmarshalInput)140 assertAllPrefixesFail(t, full[:8], UnmarshalInput)
141 }141 }
142 142
143+func TestImagePushGoldenAndRoundTrip(t *testing.T) {
144+ tests := []struct {
145+ name string
146+ msg ImagePush
147+ want []byte
148+ }{
149+ {
150+ name: "png data",
151+ msg: ImagePush{Mime: "image/png", Data: []byte{0x01, 0x02, 0x03}},
152+ want: []byte{0x00, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x01, 0x02, 0x03},
153+ },
154+ {name: "empty", msg: ImagePush{}, want: []byte{0x00, 0x00}},
155+ }
156+
157+ for _, tt := range tests {
158+ t.Run(tt.name, func(t *testing.T) {
159+ got := MarshalImagePush(tt.msg)
160+ if !bytes.Equal(got, tt.want) {
161+ t.Fatalf("MarshalImagePush() = %#v, want %#v", got, tt.want)
162+ }
163+ back, err := UnmarshalImagePush(got)
164+ if err != nil {
165+ t.Fatalf("UnmarshalImagePush() error = %v", err)
166+ }
167+ if !reflect.DeepEqual(back, tt.msg) {
168+ t.Fatalf("round trip = %#v, want %#v", back, tt.msg)
169+ }
170+ })
171+ }
172+
173+ full := MarshalImagePush(tests[0].msg)
174+ assertAllPrefixesFail(t, full[:11], UnmarshalImagePush)
175+}
176+
143 func TestThemeGoldenAndRoundTrip(t *testing.T) {177 func TestThemeGoldenAndRoundTrip(t *testing.T) {
144 tests := []struct {178 tests := []struct {
145 name string179 name string

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close