detached 844b5d1

feat(ports): add cast sink and cast messages

commit
844b5d1ece3122765e7a015734721de019ec0852
author
brice · · 2026-10-14 18:42 UTC
committer
brice · · 2026-10-14 21:39 UTC
parent
df4b807
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +227 -1
M .hexcheck.yaml +1 -1
@@ -25,7 +25,7 @@ allow:
2525 ruleSettings:
2626 missing-generated-mock-for-port:
2727 # Sealed message unions are channel data, not ports; tested by sending messages (ADR 005).
28- excludePaths: [internal/ports/client_events.go, internal/ports/client_commands.go, internal/ports/input_events.go, internal/ports/output_events.go, internal/ports/lease.go, internal/ports/session_lock_messages.go, internal/ports/session_backend.go]
28+ excludePaths: [internal/ports/client_events.go, internal/ports/client_commands.go, internal/ports/input_events.go, internal/ports/output_events.go, internal/ports/lease.go, internal/ports/session_lock_messages.go, internal/ports/screencast_messages.go, internal/ports/session_backend.go]
2929 suspicious-business-logic-in-adapter:
3030 # app is the composition root; starting core and wiring channels is its job.
3131 excludePaths: [internal/app/app.go, internal/app/session.go]
@@ -25,7 +25,7 @@ allow:
25 ruleSettings:25 ruleSettings:
26 missing-generated-mock-for-port:26 missing-generated-mock-for-port:
27 # Sealed message unions are channel data, not ports; tested by sending messages (ADR 005).27 # Sealed message unions are channel data, not ports; tested by sending messages (ADR 005).
28- excludePaths: [internal/ports/client_events.go, internal/ports/client_commands.go, internal/ports/input_events.go, internal/ports/output_events.go, internal/ports/lease.go, internal/ports/session_lock_messages.go, internal/ports/session_backend.go]28+ excludePaths: [internal/ports/client_events.go, internal/ports/client_commands.go, internal/ports/input_events.go, internal/ports/output_events.go, internal/ports/lease.go, internal/ports/session_lock_messages.go, internal/ports/screencast_messages.go, internal/ports/session_backend.go]
29 suspicious-business-logic-in-adapter:29 suspicious-business-logic-in-adapter:
30 # app is the composition root; starting core and wiring channels is its job.30 # app is the composition root; starting core and wiring channels is its job.
31 excludePaths: [internal/app/app.go, internal/app/session.go]31 excludePaths: [internal/app/app.go, internal/app/session.go]
M .mockery.yml +1 -0
@@ -14,6 +14,7 @@ packages:
1414 filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go'
1515 interfaces:
1616 CaptureFrame:
17+ CastSink:
1718 Clock:
1819 ContentReturn:
1920 Notifier:
@@ -14,6 +14,7 @@ packages:
14 filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go'14 filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go'
15 interfaces:15 interfaces:
16 CaptureFrame:16 CaptureFrame:
17+ CastSink:
17 Clock:18 Clock:
18 ContentReturn:19 ContentReturn:
19 Notifier:20 Notifier:
A internal/mocks/ports/screencast_mock.go +126 -0
@@ -0,0 +1,126 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "github.com/bnema/neferwl/internal/ports"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// NewMockCastSink creates a new instance of MockCastSink. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func NewMockCastSink(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *MockCastSink {
18+ if helper, ok := t.(interface{ Helper() }); ok {
19+ helper.Helper()
20+ }
21+
22+ mock := &MockCastSink{}
23+ mock.Mock.Test(t)
24+
25+ t.Cleanup(func() {
26+ if helper, ok := t.(interface{ Helper() }); ok {
27+ helper.Helper()
28+ }
29+ mock.AssertExpectations(t)
30+ })
31+
32+ return mock
33+}
34+
35+// MockCastSink is an autogenerated mock type for the CastSink type
36+type MockCastSink struct {
37+ mock.Mock
38+}
39+
40+type MockCastSink_Expecter struct {
41+ mock *mock.Mock
42+}
43+
44+func (_m *MockCastSink) EXPECT() *MockCastSink_Expecter {
45+ return &MockCastSink_Expecter{mock: &_m.Mock}
46+}
47+
48+// Inventory provides a mock function for the type MockCastSink
49+func (_mock *MockCastSink) Inventory(castInventory ports.CastInventory) {
50+ _mock.Called(castInventory)
51+ return
52+}
53+
54+// MockCastSink_Inventory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Inventory'
55+type MockCastSink_Inventory_Call struct {
56+ *mock.Call
57+}
58+
59+// Inventory is a helper method to define mock.On call
60+// - castInventory ports.CastInventory
61+func (_e *MockCastSink_Expecter) Inventory(castInventory any) *MockCastSink_Inventory_Call {
62+ return &MockCastSink_Inventory_Call{Call: _e.mock.On("Inventory", castInventory)}
63+}
64+
65+func (_c *MockCastSink_Inventory_Call) Run(run func(castInventory ports.CastInventory)) *MockCastSink_Inventory_Call {
66+ _c.Call.Run(func(args mock.Arguments) {
67+ var arg0 ports.CastInventory
68+ if args[0] != nil {
69+ arg0 = args[0].(ports.CastInventory)
70+ }
71+ run(
72+ arg0,
73+ )
74+ })
75+ return _c
76+}
77+
78+func (_c *MockCastSink_Inventory_Call) Return() *MockCastSink_Inventory_Call {
79+ _c.Call.Return()
80+ return _c
81+}
82+
83+func (_c *MockCastSink_Inventory_Call) RunAndReturn(run func(castInventory ports.CastInventory)) *MockCastSink_Inventory_Call {
84+ _c.Run(run)
85+ return _c
86+}
87+
88+// Push provides a mock function for the type MockCastSink
89+func (_mock *MockCastSink) Push(castEvent ports.CastEvent) {
90+ _mock.Called(castEvent)
91+ return
92+}
93+
94+// MockCastSink_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push'
95+type MockCastSink_Push_Call struct {
96+ *mock.Call
97+}
98+
99+// Push is a helper method to define mock.On call
100+// - castEvent ports.CastEvent
101+func (_e *MockCastSink_Expecter) Push(castEvent any) *MockCastSink_Push_Call {
102+ return &MockCastSink_Push_Call{Call: _e.mock.On("Push", castEvent)}
103+}
104+
105+func (_c *MockCastSink_Push_Call) Run(run func(castEvent ports.CastEvent)) *MockCastSink_Push_Call {
106+ _c.Call.Run(func(args mock.Arguments) {
107+ var arg0 ports.CastEvent
108+ if args[0] != nil {
109+ arg0 = args[0].(ports.CastEvent)
110+ }
111+ run(
112+ arg0,
113+ )
114+ })
115+ return _c
116+}
117+
118+func (_c *MockCastSink_Push_Call) Return() *MockCastSink_Push_Call {
119+ _c.Call.Return()
120+ return _c
121+}
122+
123+func (_c *MockCastSink_Push_Call) RunAndReturn(run func(castEvent ports.CastEvent)) *MockCastSink_Push_Call {
124+ _c.Run(run)
125+ return _c
126+}
@@ -0,0 +1,126 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "github.com/bnema/neferwl/internal/ports"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// NewMockCastSink creates a new instance of MockCastSink. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func NewMockCastSink(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *MockCastSink {
18+ if helper, ok := t.(interface{ Helper() }); ok {
19+ helper.Helper()
20+ }
21+
22+ mock := &MockCastSink{}
23+ mock.Mock.Test(t)
24+
25+ t.Cleanup(func() {
26+ if helper, ok := t.(interface{ Helper() }); ok {
27+ helper.Helper()
28+ }
29+ mock.AssertExpectations(t)
30+ })
31+
32+ return mock
33+}
34+
35+// MockCastSink is an autogenerated mock type for the CastSink type
36+type MockCastSink struct {
37+ mock.Mock
38+}
39+
40+type MockCastSink_Expecter struct {
41+ mock *mock.Mock
42+}
43+
44+func (_m *MockCastSink) EXPECT() *MockCastSink_Expecter {
45+ return &MockCastSink_Expecter{mock: &_m.Mock}
46+}
47+
48+// Inventory provides a mock function for the type MockCastSink
49+func (_mock *MockCastSink) Inventory(castInventory ports.CastInventory) {
50+ _mock.Called(castInventory)
51+ return
52+}
53+
54+// MockCastSink_Inventory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Inventory'
55+type MockCastSink_Inventory_Call struct {
56+ *mock.Call
57+}
58+
59+// Inventory is a helper method to define mock.On call
60+// - castInventory ports.CastInventory
61+func (_e *MockCastSink_Expecter) Inventory(castInventory any) *MockCastSink_Inventory_Call {
62+ return &MockCastSink_Inventory_Call{Call: _e.mock.On("Inventory", castInventory)}
63+}
64+
65+func (_c *MockCastSink_Inventory_Call) Run(run func(castInventory ports.CastInventory)) *MockCastSink_Inventory_Call {
66+ _c.Call.Run(func(args mock.Arguments) {
67+ var arg0 ports.CastInventory
68+ if args[0] != nil {
69+ arg0 = args[0].(ports.CastInventory)
70+ }
71+ run(
72+ arg0,
73+ )
74+ })
75+ return _c
76+}
77+
78+func (_c *MockCastSink_Inventory_Call) Return() *MockCastSink_Inventory_Call {
79+ _c.Call.Return()
80+ return _c
81+}
82+
83+func (_c *MockCastSink_Inventory_Call) RunAndReturn(run func(castInventory ports.CastInventory)) *MockCastSink_Inventory_Call {
84+ _c.Run(run)
85+ return _c
86+}
87+
88+// Push provides a mock function for the type MockCastSink
89+func (_mock *MockCastSink) Push(castEvent ports.CastEvent) {
90+ _mock.Called(castEvent)
91+ return
92+}
93+
94+// MockCastSink_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push'
95+type MockCastSink_Push_Call struct {
96+ *mock.Call
97+}
98+
99+// Push is a helper method to define mock.On call
100+// - castEvent ports.CastEvent
101+func (_e *MockCastSink_Expecter) Push(castEvent any) *MockCastSink_Push_Call {
102+ return &MockCastSink_Push_Call{Call: _e.mock.On("Push", castEvent)}
103+}
104+
105+func (_c *MockCastSink_Push_Call) Run(run func(castEvent ports.CastEvent)) *MockCastSink_Push_Call {
106+ _c.Call.Run(func(args mock.Arguments) {
107+ var arg0 ports.CastEvent
108+ if args[0] != nil {
109+ arg0 = args[0].(ports.CastEvent)
110+ }
111+ run(
112+ arg0,
113+ )
114+ })
115+ return _c
116+}
117+
118+func (_c *MockCastSink_Push_Call) Return() *MockCastSink_Push_Call {
119+ _c.Call.Return()
120+ return _c
121+}
122+
123+func (_c *MockCastSink_Push_Call) RunAndReturn(run func(castEvent ports.CastEvent)) *MockCastSink_Push_Call {
124+ _c.Run(run)
125+ return _c
126+}
A internal/ports/screencast.go +11 -0
@@ -0,0 +1,11 @@
1+package ports
2+
3+// CastSink receives wayland → screencast messages. Push never blocks:
4+// the screencast adapter queues the event for its owner goroutine, and drops
5+// nothing that releases a buffer or ends a cast (CastFrameDone, CastStopped).
6+// Inventory publishes the current outputs and windows after each change; the
7+// value and its slices belong to the receiver afterwards.
8+type CastSink interface {
9+ Push(CastEvent)
10+ Inventory(CastInventory)
11+}
@@ -0,0 +1,11 @@
1+package ports
2+
3+// CastSink receives wayland → screencast messages. Push never blocks:
4+// the screencast adapter queues the event for its owner goroutine, and drops
5+// nothing that releases a buffer or ends a cast (CastFrameDone, CastStopped).
6+// Inventory publishes the current outputs and windows after each change; the
7+// value and its slices belong to the receiver afterwards.
8+type CastSink interface {
9+ Push(CastEvent)
10+ Inventory(CastInventory)
11+}
A internal/ports/screencast_messages.go +88 -0
@@ -0,0 +1,88 @@
1+package ports
2+
3+import "time"
4+
5+// The cast messages are concrete structs, not sealed interfaces, so a send
6+// on a channel never boxes them (zero allocation per frame).
7+
8+// CastCommandKind selects the CastCommand fields in use.
9+type CastCommandKind uint8
10+
11+const (
12+ // CastOpen starts cast ID on Output or Window.
13+ CastOpen CastCommandKind = iota + 1
14+ // CastFrame asks for one frame into buffer Buf.
15+ CastFrame
16+ // CastClose ends cast ID; wayland answers with CastStopped.
17+ CastClose
18+)
19+
20+// CastCommand carries screencast → wayland: open or close a cast, or request
21+// one frame into a PipeWire buffer.
22+type CastCommand struct {
23+ Kind CastCommandKind
24+ ID uint64
25+ // CastOpen: the target. A window cast sets Window and leaves Output empty.
26+ Output string
27+ Window WindowID
28+ Cursor bool
29+ // CastFrame: Buf is the buffer slot. Data is the buffer's mapping,
30+ // borrowed until the CastFrameDone of Buf: the sender neither reuses nor
31+ // unmaps it before then.
32+ Buf int
33+ Data []byte
34+ Stride int
35+ Width, Height int
36+ Format uint32 // wl_shm format
37+}
38+
39+// CastEventKind selects the CastEvent fields in use.
40+type CastEventKind uint8
41+
42+const (
43+ // CastSize: the target is now Width x Height; the stream must renegotiate.
44+ CastSize CastEventKind = iota + 1
45+ // CastDamage: the target changed and a frame may be requested. Time is
46+ // the flip time.
47+ CastDamage
48+ // CastFrameDone: the frame into Buf finished. Err is nil when Data holds
49+ // a complete image; Time is the capture time.
50+ CastFrameDone
51+ // CastStopped: the cast ended (Reason, Err); no event follows.
52+ CastStopped
53+)
54+
55+// CastEvent carries wayland → screencast through CastSink.Push.
56+type CastEvent struct {
57+ Kind CastEventKind
58+ ID uint64
59+ Width, Height int
60+ Buf int
61+ Err error
62+ Time time.Time
63+ Reason CaptureReason
64+}
65+
66+// CastOutput is one output of the inventory.
67+type CastOutput struct {
68+ Info OutputInfo
69+ Modes []OutputMode
70+ Current int // index into Modes, -1 when none
71+ // X, Y, Width, Height are the logical placement.
72+ X, Y, Width, Height int
73+ Scale float64
74+ Transform BufferTransform
75+}
76+
77+// CastWindow is one window of the inventory.
78+type CastWindow struct {
79+ ID WindowID
80+ AppID, Title string
81+}
82+
83+// CastInventory is the set a cast target is picked from.
84+type CastInventory struct {
85+ Outputs []CastOutput
86+ Windows []CastWindow
87+ Focused string // name of the focused output
88+}
@@ -0,0 +1,88 @@
1+package ports
2+
3+import "time"
4+
5+// The cast messages are concrete structs, not sealed interfaces, so a send
6+// on a channel never boxes them (zero allocation per frame).
7+
8+// CastCommandKind selects the CastCommand fields in use.
9+type CastCommandKind uint8
10+
11+const (
12+ // CastOpen starts cast ID on Output or Window.
13+ CastOpen CastCommandKind = iota + 1
14+ // CastFrame asks for one frame into buffer Buf.
15+ CastFrame
16+ // CastClose ends cast ID; wayland answers with CastStopped.
17+ CastClose
18+)
19+
20+// CastCommand carries screencast → wayland: open or close a cast, or request
21+// one frame into a PipeWire buffer.
22+type CastCommand struct {
23+ Kind CastCommandKind
24+ ID uint64
25+ // CastOpen: the target. A window cast sets Window and leaves Output empty.
26+ Output string
27+ Window WindowID
28+ Cursor bool
29+ // CastFrame: Buf is the buffer slot. Data is the buffer's mapping,
30+ // borrowed until the CastFrameDone of Buf: the sender neither reuses nor
31+ // unmaps it before then.
32+ Buf int
33+ Data []byte
34+ Stride int
35+ Width, Height int
36+ Format uint32 // wl_shm format
37+}
38+
39+// CastEventKind selects the CastEvent fields in use.
40+type CastEventKind uint8
41+
42+const (
43+ // CastSize: the target is now Width x Height; the stream must renegotiate.
44+ CastSize CastEventKind = iota + 1
45+ // CastDamage: the target changed and a frame may be requested. Time is
46+ // the flip time.
47+ CastDamage
48+ // CastFrameDone: the frame into Buf finished. Err is nil when Data holds
49+ // a complete image; Time is the capture time.
50+ CastFrameDone
51+ // CastStopped: the cast ended (Reason, Err); no event follows.
52+ CastStopped
53+)
54+
55+// CastEvent carries wayland → screencast through CastSink.Push.
56+type CastEvent struct {
57+ Kind CastEventKind
58+ ID uint64
59+ Width, Height int
60+ Buf int
61+ Err error
62+ Time time.Time
63+ Reason CaptureReason
64+}
65+
66+// CastOutput is one output of the inventory.
67+type CastOutput struct {
68+ Info OutputInfo
69+ Modes []OutputMode
70+ Current int // index into Modes, -1 when none
71+ // X, Y, Width, Height are the logical placement.
72+ X, Y, Width, Height int
73+ Scale float64
74+ Transform BufferTransform
75+}
76+
77+// CastWindow is one window of the inventory.
78+type CastWindow struct {
79+ ID WindowID
80+ AppID, Title string
81+}
82+
83+// CastInventory is the set a cast target is picked from.
84+type CastInventory struct {
85+ Outputs []CastOutput
86+ Windows []CastWindow
87+ Focused string // name of the focused output
88+}

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 25

Tags 10

↑↓ moveenter openesc close