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}