v0.3.0 6800c6a

app.go RawHistoryFolder

1//go:build linux
2
3package session
4
5import (
6	"context"
7	"fmt"
8	"os"
9	"time"
10
11	"github.com/bnema/nefergui/internal/platform/wayland"
12	"github.com/bnema/nefergui/internal/render"
13)
14
15// AppHooks runs entirely on the session owner loop. Draw returns dirty=false
16// when there is no new display list; the session retries a dirty frame until
17// committed without waiting for GPU completion on the loop.
18type AppHooks struct {
19	Wake      <-chan struct{}
20	Input     func([]wayland.Input) error
21	Event     func(wayland.Event) error
22	Draw      func() (render.Frame, bool, error)
23	Committed func(uint64) error
24}
25
26// RunApp drives a UI until close or cancellation. Wayland input is batched after
27// Apply, and wake tokens, repeat deadlines and release points share one select.
28func (s *Session) RunApp(ctx context.Context, app AppHooks) error {
29	if app.Draw == nil {
30		return fmt.Errorf("session: nil app draw")
31	}
32	if s.waitCancel != nil {
33		return fmt.Errorf("session already running")
34	}
35	s.waitCtx, s.waitCancel = context.WithCancel(ctx)
36	s.releases = make(chan releaseEvent, 128)
37	if s.debugDir != "" {
38		fds, _ := os.ReadDir("/proc/self/fd")
39		s.fdStart, s.fdPeak = len(fds), len(fds)
40	}
41	defer func() { s.waitCancel(); s.waiters.Wait() }()
42	wlEvents := s.Window.StartReader()
43	sched := newFrameSchedule()
44	for !s.Window.Closed {
45		if err := ctx.Err(); err != nil {
46			return err
47		}
48		pending, err := s.finishPending()
49		if err != nil {
50			return err
51		}
52		// Resize or scale change (also set before RunApp, or by the event case
53		// below): the prepared frame is stale until tick clears Window.Dirty.
54		if s.Window.Dirty {
55			sched.Invalidate()
56		}
57		if inputs := s.Window.DrainInput(nil); len(inputs) > 0 {
58			sched.Invalidate()
59			if app.Input != nil {
60				if err := app.Input(inputs); err != nil {
61					return err
62				}
63			}
64		}
65		committed, err := sched.Step(s.Window.Ready(), app.Draw, func(prepared render.Frame) (bool, error) {
66			return s.tickFrame(prepared)
67		})
68		if err != nil {
69			return err
70		}
71		if committed {
72			if app.Committed != nil {
73				if err := app.Committed(s.frame); err != nil {
74					return err
75				}
76			}
77			continue
78		}
79		var retry, repeat <-chan time.Time
80		if sched.NeedsRetry(pending || s.framePending, s.Window.Ready()) {
81			retry = time.After(retryInterval)
82		}
83		if adapter := s.Window.InputAdapter(); adapter != nil {
84			if at, ok := adapter.NextRepeat(); ok {
85				repeat = time.After(time.Until(at))
86			}
87		}
88		select {
89		case <-ctx.Done():
90			return ctx.Err()
91		case <-app.Wake:
92			sched.Invalidate()
93		case <-retry:
94		case <-repeat:
95			if adapter := s.Window.InputAdapter(); adapter != nil {
96				if input, ok := adapter.Tick(time.Now()); ok {
97					sched.Invalidate()
98					if app.Input != nil {
99						if err := app.Input([]wayland.Input{input}); err != nil {
100							return err
101						}
102					}
103				}
104			}
105		case ev := <-wlEvents:
106			if ev.Kind == wayland.BufferRelease {
107				if slot := s.Slots[ev.BufferID]; slot != nil {
108					if err := s.Pool.Release(slot.Buffer); err != nil {
109						return err
110					}
111					s.traceState(slot.Buffer, "wl_buffer.release")
112				}
113			} else if err := s.Window.Apply(ev); err != nil {
114				return err
115			}
116			if app.Event != nil {
117				if err := app.Event(ev); err != nil {
118					return err
119				}
120			}
121			// DrainInput is handled at the start of the next iteration.
122		case ev := <-s.releases:
123			if err := s.release(ev); err != nil {
124				return err
125			}
126		}
127	}
128	if s.Device != nil && s.Device.Validation != nil && len(s.Device.Validation.Messages()) > 0 {
129		return fmt.Errorf("%d Vulkan validation errors: %v", len(s.Device.Validation.Messages()), s.Device.Validation.Messages())
130	}
131	return nil
132}

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 1

Tags 12

↑↓ moveenter openesc close