v0.29.0 21b3150

webview_audio_test.go RawHistoryFolder

1package cef
2
3import (
4	"bytes"
5	"context"
6	"errors"
7	"testing"
8	"time"
9
10	"github.com/bnema/dumber/internal/application/port"
11	"github.com/bnema/dumber/internal/infrastructure/audio/null"
12	"github.com/bnema/dumber/internal/logging"
13	purecef "github.com/bnema/purego-cef/cef"
14	"github.com/rs/zerolog"
15	"github.com/stretchr/testify/assert"
16	"github.com/stretchr/testify/require"
17)
18
19// TestEngine_WiresAudioOutputFactory verifies that the audio output factory
20// is properly injected through engine initialization.
21func TestEngine_WiresAudioOutputFactory(t *testing.T) {
22	// This test verifies the wiring pattern. Since we can't easily
23	// initialize a full CEF engine in unit tests, we verify the
24	// webViewFactoryOptions struct accepts the audio factory.
25	audioFactory := &null.Factory{}
26
27	// Verify webViewFactoryOptions can hold the audio factory
28	opts := webViewFactoryOptions{
29		windowlessFrameRate: 60,
30		audioOutputFactory:  audioFactory,
31	}
32
33	require.NotNil(t, opts.audioOutputFactory)
34	assert.Equal(t, audioFactory, opts.audioOutputFactory)
35
36	// Verify the factory is passed through
37	factory := newWebViewFactory(&Engine{}, opts)
38	require.NotNil(t, factory.audioOutputFactory)
39}
40
41// TestWebViewFactory_AudioFactoryIsOptional verifies that nil audio factory
42// is handled gracefully (no panic, no audio output).
43func TestWebViewFactory_AudioFactoryIsOptional(t *testing.T) {
44	// Arrange
45	opts := webViewFactoryOptions{
46		windowlessFrameRate: 60,
47		audioOutputFactory:  nil,
48	}
49
50	// Act
51	factory := newWebViewFactory(&Engine{}, opts)
52
53	// Assert
54	require.NotNil(t, factory)
55	assert.Nil(t, factory.audioOutputFactory)
56}
57
58// TestHandlerSet_ReceivesAudioFactory verifies the handler set receives
59// the audio factory from the WebView.
60func TestHandlerSet_ReceivesAudioFactory(t *testing.T) {
61	// This test verifies that handlerSet can access the audio factory
62	// through its associated WebView.
63	audioFactory := &null.Factory{}
64
65	wv := &WebView{
66		ctx:                context.Background(),
67		audioOutputFactory: audioFactory,
68	}
69
70	handlers := &handlerSet{wv: wv}
71
72	// Verify the handler can access the audio factory through the WebView
73	require.NotNil(t, handlers.wv)
74	assert.Equal(t, audioFactory, handlers.wv.audioOutputFactory)
75}
76
77// TestWebView_AudioStreamLifecycleFieldsExist verifies the WebView has
78// the necessary fields for audio stream lifecycle management.
79func TestWebView_AudioStreamLifecycleFieldsExist(t *testing.T) {
80	wv := &WebView{
81		ctx:                context.Background(),
82		audioOutputFactory: &null.Factory{},
83	}
84
85	// Verify the fields exist and can be set
86	wv.activeAudioStream = &mockAudioStream{}
87
88	assert.NotNil(t, wv.audioOutputFactory)
89	assert.NotNil(t, wv.activeAudioStream)
90}
91
92// mockAudioStream is a test double for port.AudioOutputStream
93type mockAudioStream struct {
94	writeCalled  bool
95	closeCalled  bool
96	writeSamples [][]float32
97}
98
99func (m *mockAudioStream) Write(samples [][]float32) error {
100	m.writeCalled = true
101	m.writeSamples = samples
102	return nil
103}
104
105func (m *mockAudioStream) Close() error {
106	m.closeCalled = true
107	return nil
108}
109
110// Verify mock implements the interface
111var _ port.AudioOutputStream = (*mockAudioStream)(nil)
112
113// racyMockAudioStream blocks inside Write until doneCh is closed, allowing
114// tests to verify that the caller holds a lock across the Write call.
115type racyMockAudioStream struct {
116	writeCh     chan struct{} // closed when Write is entered
117	doneCh      chan struct{} // Write blocks until this is closed
118	closeCalled bool
119}
120
121func (m *racyMockAudioStream) Write(_ [][]float32) error {
122	close(m.writeCh) // signal that Write has been entered
123	<-m.doneCh       // block until test unblocks us
124	return nil
125}
126
127func (m *racyMockAudioStream) Close() error {
128	m.closeCalled = true
129	return nil
130}
131
132var _ port.AudioOutputStream = (*racyMockAudioStream)(nil)
133
134// ============================================================================
135// Task 6: Audio Stream Lifecycle Handling Tests
136// ============================================================================
137
138// TestHandlerSet_OnAudioStreamStarted_CreatesStream verifies that
139// OnAudioStreamStarted creates a new audio stream from the factory.
140func TestHandlerSet_OnAudioStreamStarted_CreatesStream(t *testing.T) {
141	ctx := context.Background()
142	mockStream := &mockAudioStream{}
143	mockFactory := &mockAudioFactory{stream: mockStream}
144
145	wv := &WebView{
146		ctx:                ctx,
147		audioOutputFactory: mockFactory,
148	}
149
150	handlers := &handlerSet{wv: wv}
151
152	params := &purecef.AudioParameters{
153		SampleRate:      48000,
154		ChannelLayout:   purecef.ChannelLayoutStereo,
155		FramesPerBuffer: 512,
156	}
157
158	// Act — third arg is channels, not framesPerBuffer
159	handlers.OnAudioStreamStarted(nil, params, 2)
160
161	// Assert
162	require.NotNil(t, wv.activeAudioStream)
163	assert.Equal(t, mockStream, wv.activeAudioStream)
164	assert.True(t, wv.audioPlaying.Load())
165
166	// Verify factory was called with correct format
167	require.NotNil(t, mockFactory.lastFormat)
168	assert.Equal(t, 48000, mockFactory.lastFormat.SampleRate)
169	assert.Equal(t, 2, mockFactory.lastFormat.ChannelCount)
170	assert.Equal(t, 512, mockFactory.lastFormat.FramesPerBuffer)
171}
172
173// TestHandlerSet_OnAudioStreamStarted_NoFactory_DoesNotPanic verifies
174// that OnAudioStreamStarted handles nil factory gracefully.
175func TestHandlerSet_OnAudioStreamStarted_NoFactory_DoesNotPanic(t *testing.T) {
176	wv := &WebView{
177		ctx:                context.Background(),
178		audioOutputFactory: nil,
179	}
180
181	handlers := &handlerSet{wv: wv}
182
183	params := &purecef.AudioParameters{
184		SampleRate:      48000,
185		ChannelLayout:   purecef.ChannelLayoutStereo,
186		FramesPerBuffer: 512,
187	}
188
189	// Should not panic
190	assert.NotPanics(t, func() {
191		handlers.OnAudioStreamStarted(nil, params, 512)
192	})
193
194	assert.False(t, wv.audioPlaying.Load())
195}
196
197// TestHandlerSet_OnAudioStreamStarted_ClosesExistingStream verifies
198// that starting a new stream closes any existing one.
199func TestHandlerSet_OnAudioStreamStarted_ClosesExistingStream(t *testing.T) {
200	ctx := context.Background()
201	oldStream := &mockAudioStream{}
202	newStream := &mockAudioStream{}
203	mockFactory := &mockAudioFactory{stream: newStream}
204
205	wv := &WebView{
206		ctx:                ctx,
207		audioOutputFactory: mockFactory,
208		activeAudioStream:  oldStream,
209	}
210
211	handlers := &handlerSet{wv: wv}
212
213	params := &purecef.AudioParameters{
214		SampleRate:      44100,
215		ChannelLayout:   purecef.ChannelLayoutMono,
216		FramesPerBuffer: 256,
217	}
218
219	// Act — third arg is channels (1 for mono)
220	handlers.OnAudioStreamStarted(nil, params, 1)
221
222	// Assert
223	assert.True(t, oldStream.closeCalled)
224	assert.Equal(t, newStream, wv.activeAudioStream)
225}
226
227// TestHandlerSet_OnAudioStreamStarted_FactoryError_HandlesGracefully verifies
228// that factory errors are handled without panic and audioPlaying is reset to false.
229func TestHandlerSet_OnAudioStreamStarted_FactoryError_HandlesGracefully(t *testing.T) {
230	ctx := context.Background()
231	mockFactory := &mockAudioFactory{err: errors.New("factory error")}
232
233	wv := &WebView{
234		ctx:                ctx,
235		audioOutputFactory: mockFactory,
236	}
237
238	handlers := &handlerSet{wv: wv}
239
240	params := &purecef.AudioParameters{
241		SampleRate:      48000,
242		ChannelLayout:   purecef.ChannelLayoutStereo,
243		FramesPerBuffer: 512,
244	}
245
246	// Should not panic
247	assert.NotPanics(t, func() {
248		handlers.OnAudioStreamStarted(nil, params, 512)
249	})
250
251	// audioPlaying must be false when NewStream fails — if it stays true,
252	// the UI/state layer incorrectly believes audio is active.
253	assert.False(t, wv.audioPlaying.Load(), "audioPlaying must be false after NewStream failure")
254	assert.Nil(t, wv.activeAudioStream, "activeAudioStream must be nil after NewStream failure")
255}
256
257// ============================================================================
258// Task 7: PCM Packet Forwarding Tests
259// ============================================================================
260
261// TestHandlerSet_OnAudioStreamPacket_CopiesAndWrites verifies that
262// OnAudioStreamPacket copies samples before writing to avoid data races.
263func TestHandlerSet_OnAudioStreamPacket_CopiesAndWrites(t *testing.T) {
264	ctx := context.Background()
265	mockStream := &mockAudioStream{}
266
267	wv := &WebView{
268		ctx:               ctx,
269		activeAudioStream: mockStream,
270	}
271
272	handlers := &handlerSet{wv: wv}
273
274	// Create sample data: 2 channels, 512 frames each
275	originalData := [][]float32{
276		make([]float32, 512), // Channel 0
277		make([]float32, 512), // Channel 1
278	}
279	originalData[0][0] = 0.5
280	originalData[1][0] = -0.5
281
282	// Act
283	handlers.OnAudioStreamPacket(nil, originalData, 512, 0)
284
285	// Assert
286	require.True(t, mockStream.writeCalled)
287	require.Len(t, mockStream.writeSamples, 2)
288	require.Len(t, mockStream.writeSamples[0], 512)
289
290	// Verify data was copied (values match)
291	assert.InDelta(t, 0.5, mockStream.writeSamples[0][0], 0.000001)
292	assert.InDelta(t, -0.5, mockStream.writeSamples[1][0], 0.000001)
293
294	// Verify it's a copy (different underlying array)
295	assert.NotSame(t, &originalData[0][0], &mockStream.writeSamples[0][0])
296}
297
298// TestHandlerSet_OnAudioStreamPacket_NoActiveStream_DoesNothing verifies
299// that packets are silently discarded when no stream is active.
300func TestHandlerSet_OnAudioStreamPacket_NoActiveStream_DoesNothing(t *testing.T) {
301	wv := &WebView{
302		ctx:               context.Background(),
303		activeAudioStream: nil,
304	}
305
306	handlers := &handlerSet{wv: wv}
307
308	data := [][]float32{
309		{0.1, 0.2, 0.3},
310		{0.4, 0.5, 0.6},
311	}
312
313	// Should not panic
314	assert.NotPanics(t, func() {
315		handlers.OnAudioStreamPacket(nil, data, 3, 0)
316	})
317}
318
319// TestHandlerSet_OnAudioStreamPacket_NilData_DoesNotPanic verifies
320// that nil data is handled gracefully.
321func TestHandlerSet_OnAudioStreamPacket_NilData_DoesNotPanic(t *testing.T) {
322	mockStream := &mockAudioStream{}
323	wv := &WebView{
324		ctx:               context.Background(),
325		activeAudioStream: mockStream,
326	}
327
328	handlers := &handlerSet{wv: wv}
329
330	// Should not panic with nil data
331	assert.NotPanics(t, func() {
332		handlers.OnAudioStreamPacket(nil, nil, 0, 0)
333	})
334}
335
336// ============================================================================
337// Task 8: Stream Cleanup Tests
338// ============================================================================
339
340// TestHandlerSet_OnAudioStreamStopped_ClosesStream verifies that
341// OnAudioStreamStopped closes the active stream.
342func TestHandlerSet_OnAudioStreamStopped_ClosesStream(t *testing.T) {
343	mockStream := &mockAudioStream{}
344	wv := &WebView{
345		ctx:               context.Background(),
346		activeAudioStream: mockStream,
347	}
348	wv.audioPlaying.Store(true)
349
350	handlers := &handlerSet{wv: wv}
351
352	// Act
353	handlers.OnAudioStreamStopped(nil)
354
355	// Assert
356	assert.True(t, mockStream.closeCalled)
357	assert.Nil(t, wv.activeAudioStream)
358	assert.False(t, wv.audioPlaying.Load())
359}
360
361// TestHandlerSet_OnAudioStreamStopped_NoActiveStream_DoesNotPanic verifies
362// that stopping when no stream exists doesn't panic.
363func TestHandlerSet_OnAudioStreamStopped_NoActiveStream_DoesNotPanic(t *testing.T) {
364	wv := &WebView{
365		ctx:               context.Background(),
366		activeAudioStream: nil,
367	}
368	wv.audioPlaying.Store(true)
369
370	handlers := &handlerSet{wv: wv}
371
372	// Should not panic
373	assert.NotPanics(t, func() {
374		handlers.OnAudioStreamStopped(nil)
375	})
376
377	assert.False(t, wv.audioPlaying.Load())
378}
379
380// TestHandlerSet_OnAudioStreamError_ClosesStream verifies that
381// OnAudioStreamError closes the active stream.
382func TestHandlerSet_OnAudioStreamError_ClosesStream(t *testing.T) {
383	mockStream := &mockAudioStream{}
384	wv := &WebView{
385		ctx:               context.Background(),
386		activeAudioStream: mockStream,
387	}
388	wv.audioPlaying.Store(true)
389
390	handlers := &handlerSet{wv: wv}
391
392	// Act
393	handlers.OnAudioStreamError(nil, "test error")
394
395	// Assert
396	assert.True(t, mockStream.closeCalled)
397	assert.Nil(t, wv.activeAudioStream)
398	assert.False(t, wv.audioPlaying.Load())
399}
400
401// TestHandlerSet_OnAudioStreamError_NoActiveStream_DoesNotPanic verifies
402// that error handling when no stream exists doesn't panic.
403func TestHandlerSet_OnAudioStreamError_NoActiveStream_DoesNotPanic(t *testing.T) {
404	wv := &WebView{
405		ctx:               context.Background(),
406		activeAudioStream: nil,
407	}
408	wv.audioPlaying.Store(true)
409
410	handlers := &handlerSet{wv: wv}
411
412	// Should not panic
413	assert.NotPanics(t, func() {
414		handlers.OnAudioStreamError(nil, "test error")
415	})
416
417	assert.False(t, wv.audioPlaying.Load())
418}
419
420func TestHandlerSet_OnAudioStreamError_LogsWarnForNormalError(t *testing.T) {
421	var logs bytes.Buffer
422	logger := zerolog.New(&logs).Level(zerolog.DebugLevel)
423	ctx := logging.WithContext(context.Background(), logger)
424
425	wv := &WebView{ctx: ctx}
426	handlers := &handlerSet{wv: wv}
427
428	handlers.OnAudioStreamError(nil, "test error")
429
430	output := logs.String()
431	assert.Contains(t, output, `"level":"warn"`)
432	assert.Contains(t, output, `"message":"cef: audio stream error"`)
433	assert.Contains(t, output, `"error":"test error"`)
434}
435
436func TestHandlerSet_OnAudioStreamError_ShutdownSocketCloseLogsDebug(t *testing.T) {
437	var logs bytes.Buffer
438	logger := zerolog.New(&logs).Level(zerolog.DebugLevel)
439	ctx := logging.WithContext(context.Background(), logger)
440
441	wv := &WebView{ctx: ctx}
442	wv.destroyed.Store(true)
443	handlers := &handlerSet{wv: wv}
444
445	handlers.OnAudioStreamError(nil, "Socket closed unexpectedly")
446
447	output := logs.String()
448	assert.Contains(t, output, `"level":"debug"`)
449	assert.NotContains(t, output, `"level":"warn"`)
450	assert.Contains(t, output, `"message":"cef: audio stream closed during shutdown"`)
451	assert.Contains(t, output, `"error":"Socket closed unexpectedly"`)
452}
453
454// TestWebView_Destroy_ClosesAudioStream verifies that destroying
455// the WebView closes any active audio stream.
456func TestWebView_Destroy_ClosesAudioStream(t *testing.T) {
457	mockStream := &mockAudioStream{}
458	wv := &WebView{
459		ctx:               context.Background(),
460		activeAudioStream: mockStream,
461	}
462	wv.audioPlaying.Store(true)
463
464	// Act — Destroy must close the audio stream as part of teardown.
465	wv.Destroy()
466
467	// Assert
468	assert.True(t, mockStream.closeCalled, "Destroy must close the active audio stream")
469	assert.Nil(t, wv.activeAudioStream)
470}
471
472// TestWebView_Destroy_NoAudioStream_DoesNotPanic verifies that Destroy
473// handles the case where no audio stream is active.
474func TestWebView_Destroy_NoAudioStream_DoesNotPanic(t *testing.T) {
475	wv := &WebView{
476		ctx: context.Background(),
477	}
478
479	assert.NotPanics(t, func() {
480		wv.Destroy()
481	})
482}
483
484// ============================================================================
485// Callback parameter mapping tests
486// ============================================================================
487
488// TestBuildAudioStreamFormat_UsesParamsFramesPerBuffer verifies that
489// buildAudioStreamFormat reads FramesPerBuffer from the AudioParameters struct,
490// NOT from the callback's third argument (which is channels).
491func TestBuildAudioStreamFormat_UsesParamsFramesPerBuffer(t *testing.T) {
492	wv := &WebView{ctx: context.Background()}
493	handlers := &handlerSet{wv: wv}
494
495	params := &purecef.AudioParameters{
496		SampleRate:      48000,
497		ChannelLayout:   purecef.ChannelLayoutStereo,
498		FramesPerBuffer: 1024,
499	}
500	channels := int32(2) // third callback arg is channels, not framesPerBuffer
501
502	format := handlers.buildAudioStreamFormat(params, channels)
503
504	assert.Equal(t, 48000, format.SampleRate, "sample rate should come from params")
505	assert.Equal(t, 2, format.ChannelCount, "channel count should match callback channels arg")
506	assert.Equal(t, 1024, format.FramesPerBuffer, "frames_per_buffer should come from params.FramesPerBuffer, not the channels arg")
507}
508
509// TestBuildAudioStreamFormat_ChannelsArgDiffersFromLayout verifies correct
510// behavior when the callback's channels arg and the layout-derived count differ.
511// The channels callback arg is the authoritative channel count.
512func TestBuildAudioStreamFormat_ChannelsArgDiffersFromLayout(t *testing.T) {
513	wv := &WebView{ctx: context.Background()}
514	handlers := &handlerSet{wv: wv}
515
516	params := &purecef.AudioParameters{
517		SampleRate:      44100,
518		ChannelLayout:   purecef.ChannelLayoutStereo, // layout says 2
519		FramesPerBuffer: 512,
520	}
521	channels := int32(1) // but CEF says 1 channel
522
523	format := handlers.buildAudioStreamFormat(params, channels)
524
525	assert.Equal(t, 44100, format.SampleRate)
526	assert.Equal(t, 1, format.ChannelCount, "should use callback channels arg, not layout-derived count")
527	assert.Equal(t, 512, format.FramesPerBuffer, "should use params.FramesPerBuffer")
528}
529
530// TestOnAudioStreamStarted_CorrectParameterMapping verifies end-to-end that
531// OnAudioStreamStarted passes the correct values to the factory:
532// - channels from the callback arg (not from params.FramesPerBuffer)
533// - framesPerBuffer from params.FramesPerBuffer
534func TestOnAudioStreamStarted_CorrectParameterMapping(t *testing.T) {
535	ctx := context.Background()
536	mockStream := &mockAudioStream{}
537	mockFactory := &mockAudioFactory{stream: mockStream}
538
539	wv := &WebView{
540		ctx:                ctx,
541		audioOutputFactory: mockFactory,
542	}
543
544	handlers := &handlerSet{wv: wv}
545
546	params := &purecef.AudioParameters{
547		SampleRate:      48000,
548		ChannelLayout:   purecef.ChannelLayoutStereo,
549		FramesPerBuffer: 1024,
550	}
551
552	// Third arg is channels=2, NOT framesPerBuffer.
553	handlers.OnAudioStreamStarted(nil, params, 2)
554
555	require.NotNil(t, mockFactory.lastFormat)
556	assert.Equal(t, 48000, mockFactory.lastFormat.SampleRate)
557	assert.Equal(t, 2, mockFactory.lastFormat.ChannelCount, "channel count should be 2 (from callback arg)")
558	assert.Equal(t, 1024, mockFactory.lastFormat.FramesPerBuffer, "frames_per_buffer should be 1024 (from params.FramesPerBuffer)")
559}
560
561// ============================================================================
562// Race-window tests (write/close interleave)
563// ============================================================================
564
565// TestOnAudioStreamPacket_WriteHoldsLock verifies that closeAudioStream cannot
566// interleave between the stream snapshot and Write. If the lock is NOT held
567// across Write, the goroutine calling closeAudioStream will Close the stream
568// while Write is in-flight, producing a write-after-close.
569func TestOnAudioStreamPacket_WriteHoldsLock(t *testing.T) {
570	ctx := context.Background()
571
572	// racyStream blocks inside Write until we signal, giving closeAudioStream
573	// a window to race if the mutex isn't held.
574	stream := &racyMockAudioStream{
575		writeCh: make(chan struct{}),
576		doneCh:  make(chan struct{}),
577	}
578
579	wv := &WebView{
580		ctx:               ctx,
581		activeAudioStream: stream,
582	}
583	handlers := &handlerSet{wv: wv}
584
585	data := [][]float32{{0.1, 0.2}, {0.3, 0.4}}
586
587	// Start a packet write — it will block inside Write.
588	go handlers.OnAudioStreamPacket(nil, data, 2, 0)
589
590	// Wait for Write to be entered.
591	<-stream.writeCh
592
593	// Now try to close. If the implementation holds the lock across Write,
594	// this will block until Write returns. If not, Close races with Write.
595	closeDone := make(chan struct{})
596	go func() {
597		wv.closeAudioStream()
598		close(closeDone)
599	}()
600
601	// Close should NOT have completed yet because Write holds the lock.
602	// Give the goroutine time to acquire the lock if it can.
603	time.Sleep(50 * time.Millisecond)
604	select {
605	case <-closeDone:
606		t.Fatal("closeAudioStream completed while Write was in-flight — lock not held across Write")
607	default:
608		// expected: closeAudioStream is blocked
609	}
610
611	// Unblock Write.
612	close(stream.doneCh)
613
614	// Now closeAudioStream should complete.
615	<-closeDone
616
617	assert.True(t, stream.closeCalled)
618	assert.Nil(t, wv.activeAudioStream)
619}
620
621// TestOnAudioStreamPacket_StreamClosedBetweenPackets verifies that when a
622// stream is closed between two OnAudioStreamPacket calls, the second call
623// sees nil and silently drops the packet.
624func TestOnAudioStreamPacket_StreamClosedBetweenPackets(t *testing.T) {
625	ctx := context.Background()
626	stream := &mockAudioStream{}
627
628	wv := &WebView{
629		ctx:               ctx,
630		activeAudioStream: stream,
631	}
632	handlers := &handlerSet{wv: wv}
633	data := [][]float32{{0.1, 0.2}, {0.3, 0.4}}
634
635	// First packet succeeds.
636	handlers.OnAudioStreamPacket(nil, data, 2, 0)
637	assert.True(t, stream.writeCalled)
638
639	// Close the stream.
640	wv.closeAudioStream()
641
642	// Second packet should be silently dropped.
643	assert.NotPanics(t, func() {
644		handlers.OnAudioStreamPacket(nil, data, 2, 1)
645	})
646}
647
648// ============================================================================
649// Mock Factories
650// ============================================================================
651
652// mockAudioFactory is a test double for port.AudioOutputFactory
653type mockAudioFactory struct {
654	stream     port.AudioOutputStream
655	err        error
656	lastFormat *port.AudioStreamFormat
657	lastCtx    context.Context
658}
659
660func (m *mockAudioFactory) NewStream(ctx context.Context, format port.AudioStreamFormat) (port.AudioOutputStream, error) {
661	m.lastCtx = ctx
662	m.lastFormat = &format
663	return m.stream, m.err
664}
665
666// Verify mock implements the interface
667var _ port.AudioOutputFactory = (*mockAudioFactory)(nil)

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 8

Tags 42

↑↓ moveenter openesc close