main f2d166b

activation.go RawHistoryFolder

1package wayland
2
3import (
4	"crypto/rand"
5	"time"
6
7	"github.com/bnema/go-wayland-bindings/server/wayland"
8	"github.com/bnema/go-wayland-bindings/server/xdgactivation"
9	"github.com/bnema/neferwl/internal/ports"
10	"github.com/bnema/purego-libwayland/server"
11)
12
13// xdg-activation: a client that has the user's attention (keyboard focus,
14// or the last key or button press) gets a token, and the client it hands
15// the token to (a link opened in the browser, a notification action) may
16// bring its window forward with it. Other tokens are issued but inert, so
17// a background app cannot steal the focus.
18
19// tokenLifetime is how long a token can activate a window, and how recent
20// a press must be to earn one.
21const tokenLifetime = 10 * time.Second
22
23// maxTokens bounds the valid tokens kept; the oldest goes first.
24const maxTokens = 32
25
26// activationToken is an issued valid token. Tokens of a client die when
27// the focus moves away from it, even on an automatic focus change such as
28// a new window mapping: a hand-off racing a transient window then fails
29// safe, without stealing focus.
30type activationToken struct {
31	client server.Client
32	at     time.Time
33}
34
35func registerActivation(d *server.Display, s *Server) error {
36	return xdgactivation.NewActivationV1Global(d, 1, func(c server.Client, v, id uint32) {
37		_, _ = xdgactivation.NewActivationV1(c, int32(v), id, activation{s})
38	})
39}
40
41type activation struct{ server *Server }
42
43func (activation) Destroy(*xdgactivation.ActivationV1) {}
44
45func (a activation) GetActivationToken(r *xdgactivation.ActivationV1, id uint32) {
46	_, _ = xdgactivation.NewActivationTokenV1(r.Client(), r.Version(), id, &tokenRequest{server: a.server})
47}
48
49// Activate focuses the toplevel of surface when the token is valid. The
50// token is used up either way.
51func (a activation) Activate(_ *xdgactivation.ActivationV1, token string, surf *wayland.Surface) {
52	s := a.server
53	t, ok := s.tokens[token]
54	delete(s.tokens, token)
55	state := s.surfaceOf(surf)
56	reason := ""
57	switch {
58	case s.protected():
59		reason = "session_protected"
60	case !ok:
61		reason = "unknown_token"
62	case time.Since(t.at) > tokenLifetime:
63		reason = "expired"
64	case state == nil:
65		reason = "no_surface"
66	}
67	var w *window
68	if reason == "" {
69		if w = s.windows[state.root().windowID()]; w == nil || w.toplevel == nil || !w.mapped {
70			reason = "not_toplevel"
71		}
72	}
73	if reason != "" {
74		s.log.Debug().Str("reason", reason).Msg("activation refused")
75		return
76	}
77	s.log.Info().Uint64("id", uint64(w.id)).Msg("activation")
78	s.emit(ports.WindowActivate{ID: w.id})
79}
80
81// tokenRequest collects a token's serial and surface until commit.
82type tokenRequest struct {
83	server    *Server
84	serial    uint32
85	hasSerial bool
86	used      bool
87}
88
89func (t *tokenRequest) SetSerial(_ *xdgactivation.ActivationTokenV1, serial uint32, _ *wayland.Seat) {
90	t.serial, t.hasSerial = serial, true
91}
92func (*tokenRequest) SetAppId(*xdgactivation.ActivationTokenV1, string)             {}
93func (*tokenRequest) SetSurface(*xdgactivation.ActivationTokenV1, *wayland.Surface) {}
94func (*tokenRequest) Destroy(*xdgactivation.ActivationTokenV1)                      {}
95
96// Commit issues the token. It is valid when the requesting client has the
97// keyboard focus, or made the last press, recent and not followed by a
98// focus change, that it names by serial. Invalid tokens are not kept.
99func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) {
100	if t.used {
101		r.PostError(uint32(xdgactivation.ActivationTokenV1ErrorAlreadyUsed), "token already committed")
102		return
103	}
104	t.used = true
105	s := t.server
106	c := r.Client()
107	fresh := time.Since(s.seat.pressAt) <= tokenLifetime && !s.seat.pressAt.Before(s.seat.focusAt)
108	valid := s.focusClient() == c || (t.hasSerial && t.serial == s.seat.press && s.seat.pressClient == c && fresh)
109	token := rand.Text()
110	if valid && !s.protected() {
111		s.pruneTokens()
112		s.tokens[token] = activationToken{client: c, at: time.Now()}
113	}
114	r.SendDone(token)
115}
116
117// focusClient is the client with the keyboard focus.
118func (s *Server) focusClient() server.Client {
119	if w := s.windows[s.seat.focused]; w != nil && w.xdg.resource.Resource.Alive() {
120		return w.xdg.resource.Client()
121	}
122	if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() {
123		return l.resource.Client()
124	}
125	return server.Client{}
126}
127
128// pruneTokens forgets expired tokens and, past maxTokens, the oldest.
129func (s *Server) pruneTokens() {
130	oldest := ""
131	for k, t := range s.tokens {
132		if time.Since(t.at) > tokenLifetime {
133			delete(s.tokens, k)
134		} else if oldest == "" || t.at.Before(s.tokens[oldest].at) {
135			oldest = k
136		}
137	}
138	if len(s.tokens) >= maxTokens {
139		delete(s.tokens, oldest)
140	}
141}
142
143// dropTokens forgets the tokens of a client that lost the focus.
144func (s *Server) dropTokens(c server.Client) {
145	for k, t := range s.tokens {
146		if t.client == c {
147			delete(s.tokens, k)
148		}
149	}
150}

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