feat(input): keyboard shortcuts inhibit and idle inhibit
Wayland reports windows with shortcuts or idle inhibitors to core. Core makes a window's shortcuts inhibitor active while the window has keyboard focus, tells wayland (active/inactive to the client) and forwards every key to it without matching binds; emergency quit and VT switch stay in input. Idle inhibitors appear in the state file per window.
- commit
- dfbf98e6c9e1abef010e1fd05f6cecc2b6f7f895
- author
- brice · · 2026-09-26 12:18 UTC
- parent
- 2bcca8a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +478 -8
M internal/adapters/wayland/globals.go +1 -0
| @@ -21,6 +21,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||
| 21 | 21 | func() error { return registerPresentationConstraints(d, s) }, | |
| 22 | 22 | func() error { return registerPresentation(d, s) }, | |
| 23 | 23 | func() error { return registerSyncobj(d, s) }, | |
| 24 | + | func() error { return registerInhibit(d, s) }, | |
| 24 | 25 | func() error { return registerActivation(d, s) }, | |
| 25 | 26 | func() error { return registerVirtualKeyboard(d, s) }, | |
| 26 | 27 | func() error { | |
| @@ -21,6 +21,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||||
| 21 | func() error { return registerPresentationConstraints(d, s) }, | 21 | func() error { return registerPresentationConstraints(d, s) }, | ||
| 22 | func() error { return registerPresentation(d, s) }, | 22 | func() error { return registerPresentation(d, s) }, | ||
| 23 | func() error { return registerSyncobj(d, s) }, | 23 | func() error { return registerSyncobj(d, s) }, | ||
| 24 | + | func() error { return registerInhibit(d, s) }, | |||
| 24 | func() error { return registerActivation(d, s) }, | 25 | func() error { return registerActivation(d, s) }, | ||
| 25 | func() error { return registerVirtualKeyboard(d, s) }, | 26 | func() error { return registerVirtualKeyboard(d, s) }, | ||
| 26 | func() error { | 27 | func() error { | ||
A internal/adapters/wayland/inhibit.go +163 -0
| @@ -0,0 +1,163 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |
| 5 | + | "github.com/bnema/purego-libwayland/protocol/idleinhibit" | |
| 6 | + | "github.com/bnema/purego-libwayland/protocol/keyboardshortcutsinhibit" | |
| 7 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 8 | + | "github.com/bnema/purego-libwayland/server" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Inhibitors. keyboard-shortcuts-inhibit: a window asks for every key, | |
| 12 | + | // compositor binds included, while it has keyboard focus; core decides | |
| 13 | + | // when it is in effect (ports.ShortcutsInhibitState) and the client hears | |
| 14 | + | // active/inactive. idle-inhibit: a window keeps the session awake while | |
| 15 | + | // it is mapped. Both are reported to core per window. | |
| 16 | + | ||
| 17 | + | // inhibitor is one inhibitor object on a surface. | |
| 18 | + | type inhibitor struct { | |
| 19 | + | surf *surface | |
| 20 | + | shortcut *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1 // nil: idle | |
| 21 | + | window ports.WindowID // reported to core (0: not yet) | |
| 22 | + | active bool // shortcuts: last state sent | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | func registerInhibit(d *server.Display, s *Server) error { | |
| 26 | + | if err := keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 27 | + | _, _ = keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitManagerV1(c, int32(v), id, shortcutsManager{s}) | |
| 28 | + | }); err != nil { | |
| 29 | + | return err | |
| 30 | + | } | |
| 31 | + | return idleinhibit.NewZwpIdleInhibitManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 32 | + | _, _ = idleinhibit.NewZwpIdleInhibitManagerV1(c, int32(v), id, idleManager{s}) | |
| 33 | + | }) | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | type shortcutsManager struct{ server *Server } | |
| 37 | + | ||
| 38 | + | func (shortcutsManager) Destroy(*keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1) {} | |
| 39 | + | ||
| 40 | + | // InhibitShortcuts adds a surface's inhibitor; one per surface and seat | |
| 41 | + | // (there is one seat). | |
| 42 | + | func (m shortcutsManager) InhibitShortcuts(r *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1, id uint32, surf *wayland.Surface, _ *wayland.Seat) { | |
| 43 | + | state := m.server.surfaceOf(surf) | |
| 44 | + | if state == nil { | |
| 45 | + | return | |
| 46 | + | } | |
| 47 | + | for _, in := range m.server.inhibitors { | |
| 48 | + | if in.surf == state && in.shortcut != nil { | |
| 49 | + | r.PostError(uint32(keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited), "surface already inhibits shortcuts") | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | res, err := keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitorV1(r.Client(), r.Version(), id, shortcutsInhibitor{}) | |
| 54 | + | if err != nil { | |
| 55 | + | return | |
| 56 | + | } | |
| 57 | + | in := &inhibitor{surf: state, shortcut: res} | |
| 58 | + | m.server.addInhibitor(in) | |
| 59 | + | res.OnDestroy = func() { m.server.removeInhibitor(in) } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | type shortcutsInhibitor struct{} | |
| 63 | + | ||
| 64 | + | func (shortcutsInhibitor) Destroy(*keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1) {} | |
| 65 | + | ||
| 66 | + | type idleManager struct{ server *Server } | |
| 67 | + | ||
| 68 | + | func (idleManager) Destroy(*idleinhibit.ZwpIdleInhibitManagerV1) {} | |
| 69 | + | ||
| 70 | + | // CreateInhibitor adds an idle inhibitor; a surface may have several. | |
| 71 | + | func (m idleManager) CreateInhibitor(r *idleinhibit.ZwpIdleInhibitManagerV1, id uint32, surf *wayland.Surface) { | |
| 72 | + | state := m.server.surfaceOf(surf) | |
| 73 | + | if state == nil { | |
| 74 | + | return | |
| 75 | + | } | |
| 76 | + | res, err := idleinhibit.NewZwpIdleInhibitorV1(r.Client(), r.Version(), id, idleInhibitor{}) | |
| 77 | + | if err != nil { | |
| 78 | + | return | |
| 79 | + | } | |
| 80 | + | in := &inhibitor{surf: state} | |
| 81 | + | m.server.addInhibitor(in) | |
| 82 | + | res.OnDestroy = func() { m.server.removeInhibitor(in) } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | type idleInhibitor struct{} | |
| 86 | + | ||
| 87 | + | func (idleInhibitor) Destroy(*idleinhibit.ZwpIdleInhibitorV1) {} | |
| 88 | + | ||
| 89 | + | func (s *Server) addInhibitor(in *inhibitor) { | |
| 90 | + | s.inhibitors = append(s.inhibitors, in) | |
| 91 | + | s.syncInhibitors() | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | func (s *Server) removeInhibitor(in *inhibitor) { | |
| 95 | + | for i, x := range s.inhibitors { | |
| 96 | + | if x == in { | |
| 97 | + | s.inhibitors = append(s.inhibitors[:i], s.inhibitors[i+1:]...) | |
| 98 | + | break | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | in.surf = nil | |
| 102 | + | s.syncInhibitors() | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // syncInhibitors tells core, per window, whether it has inhibitors of | |
| 106 | + | // each kind, after any change: an inhibitor added or removed, a window | |
| 107 | + | // mapped or unmapped. | |
| 108 | + | func (s *Server) syncInhibitors() { | |
| 109 | + | shortcuts, idle := map[ports.WindowID]bool{}, map[ports.WindowID]bool{} | |
| 110 | + | for _, in := range s.inhibitors { | |
| 111 | + | in.window = 0 | |
| 112 | + | if in.surf != nil && !in.surf.destroyed { | |
| 113 | + | in.window = in.surf.root().windowID() | |
| 114 | + | } | |
| 115 | + | if in.window == 0 { | |
| 116 | + | continue | |
| 117 | + | } | |
| 118 | + | if in.shortcut != nil { | |
| 119 | + | shortcuts[in.window] = true | |
| 120 | + | } else { | |
| 121 | + | idle[in.window] = true | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | |
| 125 | + | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | |
| 126 | + | } | |
| 127 | + | for id, on := range diffSets(s.idleWindows, idle) { | |
| 128 | + | s.emit(ports.IdleInhibit{Window: id, Active: on}) | |
| 129 | + | } | |
| 130 | + | s.shortcutWindows, s.idleWindows = shortcuts, idle | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | // diffSets is the membership changes from old to cur. | |
| 134 | + | func diffSets(old, cur map[ports.WindowID]bool) map[ports.WindowID]bool { | |
| 135 | + | out := map[ports.WindowID]bool{} | |
| 136 | + | for id := range cur { | |
| 137 | + | if !old[id] { | |
| 138 | + | out[id] = true | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | for id := range old { | |
| 142 | + | if !cur[id] { | |
| 143 | + | out[id] = false | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | return out | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // setShortcutsInhibit applies core's decision to a window's shortcuts | |
| 150 | + | // inhibitor. | |
| 151 | + | func (s *Server) setShortcutsInhibit(c ports.ShortcutsInhibitState) { | |
| 152 | + | for _, in := range s.inhibitors { | |
| 153 | + | if in.shortcut == nil || in.window != c.Window || in.active == c.Active || !in.shortcut.Resource.Alive() { | |
| 154 | + | continue | |
| 155 | + | } | |
| 156 | + | in.active = c.Active | |
| 157 | + | if c.Active { | |
| 158 | + | in.shortcut.SendActive() | |
| 159 | + | } else { | |
| 160 | + | in.shortcut.SendInactive() | |
| 161 | + | } | |
| 162 | + | } | |
| 163 | + | } | |
| @@ -0,0 +1,163 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 5 | + | "github.com/bnema/purego-libwayland/protocol/idleinhibit" | |||
| 6 | + | "github.com/bnema/purego-libwayland/protocol/keyboardshortcutsinhibit" | |||
| 7 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Inhibitors. keyboard-shortcuts-inhibit: a window asks for every key, | |||
| 12 | + | // compositor binds included, while it has keyboard focus; core decides | |||
| 13 | + | // when it is in effect (ports.ShortcutsInhibitState) and the client hears | |||
| 14 | + | // active/inactive. idle-inhibit: a window keeps the session awake while | |||
| 15 | + | // it is mapped. Both are reported to core per window. | |||
| 16 | + | ||||
| 17 | + | // inhibitor is one inhibitor object on a surface. | |||
| 18 | + | type inhibitor struct { | |||
| 19 | + | surf *surface | |||
| 20 | + | shortcut *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1 // nil: idle | |||
| 21 | + | window ports.WindowID // reported to core (0: not yet) | |||
| 22 | + | active bool // shortcuts: last state sent | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | func registerInhibit(d *server.Display, s *Server) error { | |||
| 26 | + | if err := keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 27 | + | _, _ = keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitManagerV1(c, int32(v), id, shortcutsManager{s}) | |||
| 28 | + | }); err != nil { | |||
| 29 | + | return err | |||
| 30 | + | } | |||
| 31 | + | return idleinhibit.NewZwpIdleInhibitManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 32 | + | _, _ = idleinhibit.NewZwpIdleInhibitManagerV1(c, int32(v), id, idleManager{s}) | |||
| 33 | + | }) | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | type shortcutsManager struct{ server *Server } | |||
| 37 | + | ||||
| 38 | + | func (shortcutsManager) Destroy(*keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1) {} | |||
| 39 | + | ||||
| 40 | + | // InhibitShortcuts adds a surface's inhibitor; one per surface and seat | |||
| 41 | + | // (there is one seat). | |||
| 42 | + | func (m shortcutsManager) InhibitShortcuts(r *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1, id uint32, surf *wayland.Surface, _ *wayland.Seat) { | |||
| 43 | + | state := m.server.surfaceOf(surf) | |||
| 44 | + | if state == nil { | |||
| 45 | + | return | |||
| 46 | + | } | |||
| 47 | + | for _, in := range m.server.inhibitors { | |||
| 48 | + | if in.surf == state && in.shortcut != nil { | |||
| 49 | + | r.PostError(uint32(keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited), "surface already inhibits shortcuts") | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | } | |||
| 53 | + | res, err := keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitorV1(r.Client(), r.Version(), id, shortcutsInhibitor{}) | |||
| 54 | + | if err != nil { | |||
| 55 | + | return | |||
| 56 | + | } | |||
| 57 | + | in := &inhibitor{surf: state, shortcut: res} | |||
| 58 | + | m.server.addInhibitor(in) | |||
| 59 | + | res.OnDestroy = func() { m.server.removeInhibitor(in) } | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | type shortcutsInhibitor struct{} | |||
| 63 | + | ||||
| 64 | + | func (shortcutsInhibitor) Destroy(*keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1) {} | |||
| 65 | + | ||||
| 66 | + | type idleManager struct{ server *Server } | |||
| 67 | + | ||||
| 68 | + | func (idleManager) Destroy(*idleinhibit.ZwpIdleInhibitManagerV1) {} | |||
| 69 | + | ||||
| 70 | + | // CreateInhibitor adds an idle inhibitor; a surface may have several. | |||
| 71 | + | func (m idleManager) CreateInhibitor(r *idleinhibit.ZwpIdleInhibitManagerV1, id uint32, surf *wayland.Surface) { | |||
| 72 | + | state := m.server.surfaceOf(surf) | |||
| 73 | + | if state == nil { | |||
| 74 | + | return | |||
| 75 | + | } | |||
| 76 | + | res, err := idleinhibit.NewZwpIdleInhibitorV1(r.Client(), r.Version(), id, idleInhibitor{}) | |||
| 77 | + | if err != nil { | |||
| 78 | + | return | |||
| 79 | + | } | |||
| 80 | + | in := &inhibitor{surf: state} | |||
| 81 | + | m.server.addInhibitor(in) | |||
| 82 | + | res.OnDestroy = func() { m.server.removeInhibitor(in) } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | type idleInhibitor struct{} | |||
| 86 | + | ||||
| 87 | + | func (idleInhibitor) Destroy(*idleinhibit.ZwpIdleInhibitorV1) {} | |||
| 88 | + | ||||
| 89 | + | func (s *Server) addInhibitor(in *inhibitor) { | |||
| 90 | + | s.inhibitors = append(s.inhibitors, in) | |||
| 91 | + | s.syncInhibitors() | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | func (s *Server) removeInhibitor(in *inhibitor) { | |||
| 95 | + | for i, x := range s.inhibitors { | |||
| 96 | + | if x == in { | |||
| 97 | + | s.inhibitors = append(s.inhibitors[:i], s.inhibitors[i+1:]...) | |||
| 98 | + | break | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | in.surf = nil | |||
| 102 | + | s.syncInhibitors() | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // syncInhibitors tells core, per window, whether it has inhibitors of | |||
| 106 | + | // each kind, after any change: an inhibitor added or removed, a window | |||
| 107 | + | // mapped or unmapped. | |||
| 108 | + | func (s *Server) syncInhibitors() { | |||
| 109 | + | shortcuts, idle := map[ports.WindowID]bool{}, map[ports.WindowID]bool{} | |||
| 110 | + | for _, in := range s.inhibitors { | |||
| 111 | + | in.window = 0 | |||
| 112 | + | if in.surf != nil && !in.surf.destroyed { | |||
| 113 | + | in.window = in.surf.root().windowID() | |||
| 114 | + | } | |||
| 115 | + | if in.window == 0 { | |||
| 116 | + | continue | |||
| 117 | + | } | |||
| 118 | + | if in.shortcut != nil { | |||
| 119 | + | shortcuts[in.window] = true | |||
| 120 | + | } else { | |||
| 121 | + | idle[in.window] = true | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | |||
| 125 | + | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | |||
| 126 | + | } | |||
| 127 | + | for id, on := range diffSets(s.idleWindows, idle) { | |||
| 128 | + | s.emit(ports.IdleInhibit{Window: id, Active: on}) | |||
| 129 | + | } | |||
| 130 | + | s.shortcutWindows, s.idleWindows = shortcuts, idle | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | // diffSets is the membership changes from old to cur. | |||
| 134 | + | func diffSets(old, cur map[ports.WindowID]bool) map[ports.WindowID]bool { | |||
| 135 | + | out := map[ports.WindowID]bool{} | |||
| 136 | + | for id := range cur { | |||
| 137 | + | if !old[id] { | |||
| 138 | + | out[id] = true | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | for id := range old { | |||
| 142 | + | if !cur[id] { | |||
| 143 | + | out[id] = false | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | return out | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | // setShortcutsInhibit applies core's decision to a window's shortcuts | |||
| 150 | + | // inhibitor. | |||
| 151 | + | func (s *Server) setShortcutsInhibit(c ports.ShortcutsInhibitState) { | |||
| 152 | + | for _, in := range s.inhibitors { | |||
| 153 | + | if in.shortcut == nil || in.window != c.Window || in.active == c.Active || !in.shortcut.Resource.Alive() { | |||
| 154 | + | continue | |||
| 155 | + | } | |||
| 156 | + | in.active = c.Active | |||
| 157 | + | if c.Active { | |||
| 158 | + | in.shortcut.SendActive() | |||
| 159 | + | } else { | |||
| 160 | + | in.shortcut.SendInactive() | |||
| 161 | + | } | |||
| 162 | + | } | |||
| 163 | + | } | |||
A internal/adapters/wayland/inhibit_test.go +118 -0
| @@ -0,0 +1,118 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |
| 8 | + | "github.com/bnema/purego-libwayland/protocol/idleinhibit" | |
| 9 | + | "github.com/bnema/purego-libwayland/protocol/keyboardshortcutsinhibit" | |
| 10 | + | "github.com/bnema/wlturbo" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | type inhibitorProxy struct { | |
| 14 | + | wlturbo.BaseProxy | |
| 15 | + | states chan bool | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | func (p *inhibitorProxy) Dispatch(e *wlturbo.Event) { | |
| 19 | + | p.states <- uint32(e.Opcode) == keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1EventActive | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | // clientEvent waits for the next client event of type T. | |
| 23 | + | func clientEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |
| 24 | + | t.Helper() | |
| 25 | + | deadline := time.After(2 * time.Second) | |
| 26 | + | for { | |
| 27 | + | select { | |
| 28 | + | case ev := <-events: | |
| 29 | + | if v, ok := ev.(T); ok { | |
| 30 | + | return v | |
| 31 | + | } | |
| 32 | + | case <-deadline: | |
| 33 | + | var zero T | |
| 34 | + | t.Fatalf("no %T", zero) | |
| 35 | + | return zero | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func TestShortcutsInhibitor(t *testing.T) { | |
| 41 | + | s, events, commands, _, dir := contentServer(t) | |
| 42 | + | c := protocolClient(t, s, dir) | |
| 43 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 44 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 45 | + | registerProtocol(t, c, seat) | |
| 46 | + | mgr := bindProtocol(t, c, "zwp_keyboard_shortcuts_inhibit_manager_v1") | |
| 47 | + | in := c.AllocateID() | |
| 48 | + | p := &inhibitorProxy{states: make(chan bool, 4)} | |
| 49 | + | p.SetID(in) | |
| 50 | + | c.Context().Register(p) | |
| 51 | + | requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, in, surf, seat) | |
| 52 | + | if v := clientEvent[ports.ShortcutsInhibit](t, events); v != (ports.ShortcutsInhibit{Window: w.ID, Active: true}) { | |
| 53 | + | t.Fatal(v) | |
| 54 | + | } | |
| 55 | + | // Core decides: active, then inactive. | |
| 56 | + | state := func(active bool) { | |
| 57 | + | t.Helper() | |
| 58 | + | commands <- ports.ShortcutsInhibitState{Window: w.ID, Active: active} | |
| 59 | + | deadline := time.Now().Add(2 * time.Second) | |
| 60 | + | for time.Now().Before(deadline) { | |
| 61 | + | if err := c.Roundtrip(); err != nil { | |
| 62 | + | t.Fatal(err) | |
| 63 | + | } | |
| 64 | + | select { | |
| 65 | + | case got := <-p.states: | |
| 66 | + | if got != active { | |
| 67 | + | t.Fatalf("state %v", got) | |
| 68 | + | } | |
| 69 | + | return | |
| 70 | + | case <-time.After(5 * time.Millisecond): | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | t.Fatal("no active/inactive event") | |
| 74 | + | } | |
| 75 | + | state(true) | |
| 76 | + | state(false) | |
| 77 | + | // Destroying it tells core. | |
| 78 | + | requestProtocol(t, c, in, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1RequestDestroy) | |
| 79 | + | if v := clientEvent[ports.ShortcutsInhibit](t, events); v.Active { | |
| 80 | + | t.Fatal(v) | |
| 81 | + | } | |
| 82 | + | // A second inhibitor on the same surface is a protocol error. | |
| 83 | + | requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, c.AllocateID(), surf, seat) | |
| 84 | + | requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, c.AllocateID(), surf, seat) | |
| 85 | + | expectProtocolError(t, c, mgr, uint32(keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited)) | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // Idle inhibitors: several per surface, the window is reported once and | |
| 89 | + | // released when the last one goes. | |
| 90 | + | func TestIdleInhibitors(t *testing.T) { | |
| 91 | + | s, events, _, _, dir := contentServer(t) | |
| 92 | + | c := protocolClient(t, s, dir) | |
| 93 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 94 | + | mgr := bindProtocol(t, c, "zwp_idle_inhibit_manager_v1") | |
| 95 | + | a, b := c.AllocateID(), c.AllocateID() | |
| 96 | + | registerProtocol(t, c, a) | |
| 97 | + | registerProtocol(t, c, b) | |
| 98 | + | requestProtocol(t, c, mgr, idleinhibit.ZwpIdleInhibitManagerV1RequestCreateInhibitor, a, surf) | |
| 99 | + | requestProtocol(t, c, mgr, idleinhibit.ZwpIdleInhibitManagerV1RequestCreateInhibitor, b, surf) | |
| 100 | + | if v := clientEvent[ports.IdleInhibit](t, events); v != (ports.IdleInhibit{Window: w.ID, Active: true}) { | |
| 101 | + | t.Fatal(v) | |
| 102 | + | } | |
| 103 | + | requestProtocol(t, c, a, idleinhibit.ZwpIdleInhibitorV1RequestDestroy) | |
| 104 | + | requestProtocol(t, c, b, idleinhibit.ZwpIdleInhibitorV1RequestDestroy) | |
| 105 | + | if v := clientEvent[ports.IdleInhibit](t, events); v.Active { | |
| 106 | + | t.Fatal(v) | |
| 107 | + | } | |
| 108 | + | if err := c.Roundtrip(); err != nil { | |
| 109 | + | t.Fatal(err) | |
| 110 | + | } | |
| 111 | + | select { | |
| 112 | + | case ev := <-events: | |
| 113 | + | if _, ok := ev.(ports.IdleInhibit); ok { | |
| 114 | + | t.Fatalf("extra %+v", ev) | |
| 115 | + | } | |
| 116 | + | default: | |
| 117 | + | } | |
| 118 | + | } | |
| @@ -0,0 +1,118 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 8 | + | "github.com/bnema/purego-libwayland/protocol/idleinhibit" | |||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/keyboardshortcutsinhibit" | |||
| 10 | + | "github.com/bnema/wlturbo" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | type inhibitorProxy struct { | |||
| 14 | + | wlturbo.BaseProxy | |||
| 15 | + | states chan bool | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | func (p *inhibitorProxy) Dispatch(e *wlturbo.Event) { | |||
| 19 | + | p.states <- uint32(e.Opcode) == keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1EventActive | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | // clientEvent waits for the next client event of type T. | |||
| 23 | + | func clientEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |||
| 24 | + | t.Helper() | |||
| 25 | + | deadline := time.After(2 * time.Second) | |||
| 26 | + | for { | |||
| 27 | + | select { | |||
| 28 | + | case ev := <-events: | |||
| 29 | + | if v, ok := ev.(T); ok { | |||
| 30 | + | return v | |||
| 31 | + | } | |||
| 32 | + | case <-deadline: | |||
| 33 | + | var zero T | |||
| 34 | + | t.Fatalf("no %T", zero) | |||
| 35 | + | return zero | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func TestShortcutsInhibitor(t *testing.T) { | |||
| 41 | + | s, events, commands, _, dir := contentServer(t) | |||
| 42 | + | c := protocolClient(t, s, dir) | |||
| 43 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 44 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 45 | + | registerProtocol(t, c, seat) | |||
| 46 | + | mgr := bindProtocol(t, c, "zwp_keyboard_shortcuts_inhibit_manager_v1") | |||
| 47 | + | in := c.AllocateID() | |||
| 48 | + | p := &inhibitorProxy{states: make(chan bool, 4)} | |||
| 49 | + | p.SetID(in) | |||
| 50 | + | c.Context().Register(p) | |||
| 51 | + | requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, in, surf, seat) | |||
| 52 | + | if v := clientEvent[ports.ShortcutsInhibit](t, events); v != (ports.ShortcutsInhibit{Window: w.ID, Active: true}) { | |||
| 53 | + | t.Fatal(v) | |||
| 54 | + | } | |||
| 55 | + | // Core decides: active, then inactive. | |||
| 56 | + | state := func(active bool) { | |||
| 57 | + | t.Helper() | |||
| 58 | + | commands <- ports.ShortcutsInhibitState{Window: w.ID, Active: active} | |||
| 59 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 60 | + | for time.Now().Before(deadline) { | |||
| 61 | + | if err := c.Roundtrip(); err != nil { | |||
| 62 | + | t.Fatal(err) | |||
| 63 | + | } | |||
| 64 | + | select { | |||
| 65 | + | case got := <-p.states: | |||
| 66 | + | if got != active { | |||
| 67 | + | t.Fatalf("state %v", got) | |||
| 68 | + | } | |||
| 69 | + | return | |||
| 70 | + | case <-time.After(5 * time.Millisecond): | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | t.Fatal("no active/inactive event") | |||
| 74 | + | } | |||
| 75 | + | state(true) | |||
| 76 | + | state(false) | |||
| 77 | + | // Destroying it tells core. | |||
| 78 | + | requestProtocol(t, c, in, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1RequestDestroy) | |||
| 79 | + | if v := clientEvent[ports.ShortcutsInhibit](t, events); v.Active { | |||
| 80 | + | t.Fatal(v) | |||
| 81 | + | } | |||
| 82 | + | // A second inhibitor on the same surface is a protocol error. | |||
| 83 | + | requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, c.AllocateID(), surf, seat) | |||
| 84 | + | requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, c.AllocateID(), surf, seat) | |||
| 85 | + | expectProtocolError(t, c, mgr, uint32(keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited)) | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // Idle inhibitors: several per surface, the window is reported once and | |||
| 89 | + | // released when the last one goes. | |||
| 90 | + | func TestIdleInhibitors(t *testing.T) { | |||
| 91 | + | s, events, _, _, dir := contentServer(t) | |||
| 92 | + | c := protocolClient(t, s, dir) | |||
| 93 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 94 | + | mgr := bindProtocol(t, c, "zwp_idle_inhibit_manager_v1") | |||
| 95 | + | a, b := c.AllocateID(), c.AllocateID() | |||
| 96 | + | registerProtocol(t, c, a) | |||
| 97 | + | registerProtocol(t, c, b) | |||
| 98 | + | requestProtocol(t, c, mgr, idleinhibit.ZwpIdleInhibitManagerV1RequestCreateInhibitor, a, surf) | |||
| 99 | + | requestProtocol(t, c, mgr, idleinhibit.ZwpIdleInhibitManagerV1RequestCreateInhibitor, b, surf) | |||
| 100 | + | if v := clientEvent[ports.IdleInhibit](t, events); v != (ports.IdleInhibit{Window: w.ID, Active: true}) { | |||
| 101 | + | t.Fatal(v) | |||
| 102 | + | } | |||
| 103 | + | requestProtocol(t, c, a, idleinhibit.ZwpIdleInhibitorV1RequestDestroy) | |||
| 104 | + | requestProtocol(t, c, b, idleinhibit.ZwpIdleInhibitorV1RequestDestroy) | |||
| 105 | + | if v := clientEvent[ports.IdleInhibit](t, events); v.Active { | |||
| 106 | + | t.Fatal(v) | |||
| 107 | + | } | |||
| 108 | + | if err := c.Roundtrip(); err != nil { | |||
| 109 | + | t.Fatal(err) | |||
| 110 | + | } | |||
| 111 | + | select { | |||
| 112 | + | case ev := <-events: | |||
| 113 | + | if _, ok := ev.(ports.IdleInhibit); ok { | |||
| 114 | + | t.Fatalf("extra %+v", ev) | |||
| 115 | + | } | |||
| 116 | + | default: | |||
| 117 | + | } | |||
| 118 | + | } | |||
M internal/adapters/wayland/server.go +12 -6
| @@ -124,12 +124,16 @@ type Server struct { | |||
| 124 | 124 | timelines map[*server.Resource]*timeline | |
| 125 | 125 | syncWait *syncWaiter | |
| 126 | 126 | // flips is each output's latest flip (pacer, under Do). | |
| 127 | - | flips map[string]ports.FlipInfo | |
| 128 | - | contentNotify chan struct{} | |
| 129 | - | contentReady chan struct{} | |
| 130 | - | outputs []*output | |
| 131 | - | focusedOutput string | |
| 132 | - | fractions map[*surface]*fractionalscale.WpFractionalScaleV1 | |
| 127 | + | flips map[string]ports.FlipInfo | |
| 128 | + | // inhibitors are the live inhibitor objects (inhibit.go); | |
| 129 | + | // shortcutWindows and idleWindows what core was told. | |
| 130 | + | inhibitors []*inhibitor | |
| 131 | + | shortcutWindows, idleWindows map[ports.WindowID]bool | |
| 132 | + | contentNotify chan struct{} | |
| 133 | + | contentReady chan struct{} | |
| 134 | + | outputs []*output | |
| 135 | + | focusedOutput string | |
| 136 | + | fractions map[*surface]*fractionalscale.WpFractionalScaleV1 | |
| 133 | 137 | // cursorSurface is the wl_pointer.set_cursor surface in use. | |
| 134 | 138 | cursorSurface *surface | |
| 135 | 139 | // cursorMu guards only the latest cursor change for forwardCursors. | |
| @@ -578,6 +582,8 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 578 | 582 | s.setOutputs(c) | |
| 579 | 583 | case ports.SlotsPending: | |
| 580 | 584 | s.slotsPending = c.Pending | |
| 585 | + | case ports.ShortcutsInhibitState: | |
| 586 | + | s.setShortcutsInhibit(c) | |
| 581 | 587 | case ports.CloseWindow: | |
| 582 | 588 | w := s.windows[c.ID] | |
| 583 | 589 | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() { | |
| @@ -124,12 +124,16 @@ type Server struct { | |||||
| 124 | timelines map[*server.Resource]*timeline | 124 | timelines map[*server.Resource]*timeline | ||
| 125 | syncWait *syncWaiter | 125 | syncWait *syncWaiter | ||
| 126 | // flips is each output's latest flip (pacer, under Do). | 126 | // flips is each output's latest flip (pacer, under Do). | ||
| 127 | - | flips map[string]ports.FlipInfo | 127 | + | flips map[string]ports.FlipInfo |
| 128 | - | contentNotify chan struct{} | 128 | + | // inhibitors are the live inhibitor objects (inhibit.go); |
| 129 | - | contentReady chan struct{} | 129 | + | // shortcutWindows and idleWindows what core was told. |
| 130 | - | outputs []*output | 130 | + | inhibitors []*inhibitor |
| 131 | - | focusedOutput string | 131 | + | shortcutWindows, idleWindows map[ports.WindowID]bool |
| 132 | - | fractions map[*surface]*fractionalscale.WpFractionalScaleV1 | 132 | + | contentNotify chan struct{} |
| 133 | + | contentReady chan struct{} | |||
| 134 | + | outputs []*output | |||
| 135 | + | focusedOutput string | |||
| 136 | + | fractions map[*surface]*fractionalscale.WpFractionalScaleV1 | |||
| 133 | // cursorSurface is the wl_pointer.set_cursor surface in use. | 137 | // cursorSurface is the wl_pointer.set_cursor surface in use. | ||
| 134 | cursorSurface *surface | 138 | cursorSurface *surface | ||
| 135 | // cursorMu guards only the latest cursor change for forwardCursors. | 139 | // cursorMu guards only the latest cursor change for forwardCursors. | ||
| @@ -578,6 +582,8 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 578 | s.setOutputs(c) | 582 | s.setOutputs(c) | ||
| 579 | case ports.SlotsPending: | 583 | case ports.SlotsPending: | ||
| 580 | s.slotsPending = c.Pending | 584 | s.slotsPending = c.Pending | ||
| 585 | + | case ports.ShortcutsInhibitState: | |||
| 586 | + | s.setShortcutsInhibit(c) | |||
| 581 | case ports.CloseWindow: | 587 | case ports.CloseWindow: | ||
| 582 | w := s.windows[c.ID] | 588 | w := s.windows[c.ID] | ||
| 583 | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() { | 589 | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() { | ||
M internal/adapters/wayland/xdg.go +2 -0
| @@ -182,6 +182,7 @@ func (w *window) unmap() { | |||
| 182 | 182 | w.xdg.acked = false | |
| 183 | 183 | w.xdg.serials = nil | |
| 184 | 184 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | |
| 185 | + | w.xdg.server.syncInhibitors() | |
| 185 | 186 | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |
| 186 | 187 | } | |
| 187 | 188 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |
| @@ -215,6 +216,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||
| 215 | 216 | // Its size comes with the commit, in WindowResized (afterCommit). | |
| 216 | 217 | w.floating = w.floats() | |
| 217 | 218 | x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, Slot: slot, PID: r.Client().PID(), Floating: w.floating, Width: w.floatW, Height: w.floatH}) | |
| 219 | + | x.server.syncInhibitors() | |
| 218 | 220 | x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Str("slot", slot).Bool("floating", w.floating).Msg("window mapped") | |
| 219 | 221 | } else if !buffer && w.mapped { | |
| 220 | 222 | w.unmap() | |
| @@ -182,6 +182,7 @@ func (w *window) unmap() { | |||||
| 182 | w.xdg.acked = false | 182 | w.xdg.acked = false | ||
| 183 | w.xdg.serials = nil | 183 | w.xdg.serials = nil | ||
| 184 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | 184 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | ||
| 185 | + | w.xdg.server.syncInhibitors() | |||
| 185 | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | 186 | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | ||
| 186 | } | 187 | } | ||
| 187 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | 188 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | ||
| @@ -215,6 +216,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||||
| 215 | // Its size comes with the commit, in WindowResized (afterCommit). | 216 | // Its size comes with the commit, in WindowResized (afterCommit). | ||
| 216 | w.floating = w.floats() | 217 | w.floating = w.floats() | ||
| 217 | x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, Slot: slot, PID: r.Client().PID(), Floating: w.floating, Width: w.floatW, Height: w.floatH}) | 218 | x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, Slot: slot, PID: r.Client().PID(), Floating: w.floating, Width: w.floatW, Height: w.floatH}) | ||
| 219 | + | x.server.syncInhibitors() | |||
| 218 | x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Str("slot", slot).Bool("floating", w.floating).Msg("window mapped") | 220 | x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Str("slot", slot).Bool("floating", w.floating).Msg("window mapped") | ||
| 219 | } else if !buffer && w.mapped { | 221 | } else if !buffer && w.mapped { | ||
| 220 | w.unmap() | 222 | w.unmap() | ||
M internal/core/core.go +61 -1
| @@ -89,6 +89,12 @@ type Core struct { | |||
| 89 | 89 | // pointer moving to another output does not. | |
| 90 | 90 | layerFocus WindowID | |
| 91 | 91 | layerOver map[*screen]WindowID | |
| 92 | + | // inhibitors are the windows asking to keep compositor binds while | |
| 93 | + | // focused, inhibiting the one active now (0: none); idle are those | |
| 94 | + | // keeping the session awake. | |
| 95 | + | inhibitors map[WindowID]bool | |
| 96 | + | inhibiting WindowID | |
| 97 | + | idle map[WindowID]bool | |
| 92 | 98 | } | |
| 93 | 99 | ||
| 94 | 100 | func keyName(s string) string { | |
| @@ -271,7 +277,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) { | |||
| 271 | 277 | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | |
| 272 | 278 | } | |
| 273 | 279 | // A placeholder screen holds windows until the first output arrives. | |
| 274 | - | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}} | |
| 280 | + | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}, inhibitors: map[WindowID]bool{}, idle: map[WindowID]bool{}} | |
| 275 | 281 | if err := c.apply(cfg); err != nil { | |
| 276 | 282 | return nil, err | |
| 277 | 283 | } | |
| @@ -448,12 +454,37 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 448 | 454 | } | |
| 449 | 455 | c.focus = focus | |
| 450 | 456 | } | |
| 457 | + | if err := c.updateInhibit(ctx); err != nil { | |
| 458 | + | return err | |
| 459 | + | } | |
| 451 | 460 | c.resolveConstraint() | |
| 452 | 461 | latest(c.ch.Scenes, scenes) | |
| 453 | 462 | c.publishState() | |
| 454 | 463 | return nil | |
| 455 | 464 | } | |
| 456 | 465 | ||
| 466 | + | // updateInhibit makes the shortcuts inhibitor of the keyboard focus the | |
| 467 | + | // active one and tells wayland when it changes. | |
| 468 | + | func (c *Core) updateInhibit(ctx context.Context) error { | |
| 469 | + | want := WindowID(0) | |
| 470 | + | if f := c.keyboardFocus(); f != 0 && c.inhibitors[f] { | |
| 471 | + | want = f | |
| 472 | + | } | |
| 473 | + | if want == c.inhibiting { | |
| 474 | + | return nil | |
| 475 | + | } | |
| 476 | + | if c.inhibiting != 0 { | |
| 477 | + | if err := c.command(ctx, ports.ShortcutsInhibitState{Window: c.inhibiting}); err != nil { | |
| 478 | + | return err | |
| 479 | + | } | |
| 480 | + | } | |
| 481 | + | c.inhibiting = want | |
| 482 | + | if want != 0 { | |
| 483 | + | return c.command(ctx, ports.ShortcutsInhibitState{Window: want, Active: true}) | |
| 484 | + | } | |
| 485 | + | return nil | |
| 486 | + | } | |
| 487 | + | ||
| 457 | 488 | // resolveConstraint turns the active constraint into global coordinates | |
| 458 | 489 | // and sends it to input when it changed. A hidden window holds nothing. | |
| 459 | 490 | func (c *Core) resolveConstraint() { | |
| @@ -616,6 +647,24 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 616 | 647 | if p := c.popups[v.ID]; p != nil { | |
| 617 | 648 | p.mapped = true | |
| 618 | 649 | } | |
| 650 | + | case ports.ShortcutsInhibit: | |
| 651 | + | if v.Active { | |
| 652 | + | c.inhibitors[v.Window] = true | |
| 653 | + | } else { | |
| 654 | + | delete(c.inhibitors, v.Window) | |
| 655 | + | } | |
| 656 | + | if err := c.updateInhibit(ctx); err != nil { | |
| 657 | + | return nil | |
| 658 | + | } | |
| 659 | + | continue | |
| 660 | + | case ports.IdleInhibit: | |
| 661 | + | if v.Active { | |
| 662 | + | c.idle[v.Window] = true | |
| 663 | + | } else { | |
| 664 | + | delete(c.idle, v.Window) | |
| 665 | + | } | |
| 666 | + | c.publishState() | |
| 667 | + | continue | |
| 619 | 668 | case ports.WindowAppID: | |
| 620 | 669 | if info, ok := c.clients[v.ID]; ok { | |
| 621 | 670 | info.AppID = v.AppID | |
| @@ -631,6 +680,8 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 631 | 680 | return nil | |
| 632 | 681 | } | |
| 633 | 682 | delete(c.clients, v.ID) | |
| 683 | + | delete(c.inhibitors, v.ID) | |
| 684 | + | delete(c.idle, v.ID) | |
| 634 | 685 | if s, _ := c.screenOf(v.ID); s != nil { | |
| 635 | 686 | s.mon.RemoveWindow(v.ID) | |
| 636 | 687 | } | |
| @@ -797,6 +848,15 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 797 | 848 | if !ok { | |
| 798 | 849 | continue | |
| 799 | 850 | } | |
| 851 | + | // A focused window inhibiting shortcuts gets every key: | |
| 852 | + | // no bind runs (emergency quit and VT switch are handled by | |
| 853 | + | // input before core). | |
| 854 | + | if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() { | |
| 855 | + | if err := c.command(ctx, ports.ForwardKey{ID: c.inhibiting, Key: key}); err != nil { | |
| 856 | + | return nil | |
| 857 | + | } | |
| 858 | + | continue | |
| 859 | + | } | |
| 800 | 860 | name := keyName(key.Keysym) | |
| 801 | 861 | // A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1 | |
| 802 | 862 | // prints exclam on US); then the physical key (code:N). | |
| @@ -89,6 +89,12 @@ type Core struct { | |||||
| 89 | // pointer moving to another output does not. | 89 | // pointer moving to another output does not. | ||
| 90 | layerFocus WindowID | 90 | layerFocus WindowID | ||
| 91 | layerOver map[*screen]WindowID | 91 | layerOver map[*screen]WindowID | ||
| 92 | + | // inhibitors are the windows asking to keep compositor binds while | |||
| 93 | + | // focused, inhibiting the one active now (0: none); idle are those | |||
| 94 | + | // keeping the session awake. | |||
| 95 | + | inhibitors map[WindowID]bool | |||
| 96 | + | inhibiting WindowID | |||
| 97 | + | idle map[WindowID]bool | |||
| 92 | } | 98 | } | ||
| 93 | 99 | ||||
| 94 | func keyName(s string) string { | 100 | func keyName(s string) string { | ||
| @@ -271,7 +277,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) { | |||||
| 271 | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | 277 | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | ||
| 272 | } | 278 | } | ||
| 273 | // A placeholder screen holds windows until the first output arrives. | 279 | // A placeholder screen holds windows until the first output arrives. | ||
| 274 | - | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}} | 280 | + | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}, inhibitors: map[WindowID]bool{}, idle: map[WindowID]bool{}} |
| 275 | if err := c.apply(cfg); err != nil { | 281 | if err := c.apply(cfg); err != nil { | ||
| 276 | return nil, err | 282 | return nil, err | ||
| 277 | } | 283 | } | ||
| @@ -448,12 +454,37 @@ func (c *Core) publish(ctx context.Context) error { | |||||
| 448 | } | 454 | } | ||
| 449 | c.focus = focus | 455 | c.focus = focus | ||
| 450 | } | 456 | } | ||
| 457 | + | if err := c.updateInhibit(ctx); err != nil { | |||
| 458 | + | return err | |||
| 459 | + | } | |||
| 451 | c.resolveConstraint() | 460 | c.resolveConstraint() | ||
| 452 | latest(c.ch.Scenes, scenes) | 461 | latest(c.ch.Scenes, scenes) | ||
| 453 | c.publishState() | 462 | c.publishState() | ||
| 454 | return nil | 463 | return nil | ||
| 455 | } | 464 | } | ||
| 456 | 465 | ||||
| 466 | + | // updateInhibit makes the shortcuts inhibitor of the keyboard focus the | |||
| 467 | + | // active one and tells wayland when it changes. | |||
| 468 | + | func (c *Core) updateInhibit(ctx context.Context) error { | |||
| 469 | + | want := WindowID(0) | |||
| 470 | + | if f := c.keyboardFocus(); f != 0 && c.inhibitors[f] { | |||
| 471 | + | want = f | |||
| 472 | + | } | |||
| 473 | + | if want == c.inhibiting { | |||
| 474 | + | return nil | |||
| 475 | + | } | |||
| 476 | + | if c.inhibiting != 0 { | |||
| 477 | + | if err := c.command(ctx, ports.ShortcutsInhibitState{Window: c.inhibiting}); err != nil { | |||
| 478 | + | return err | |||
| 479 | + | } | |||
| 480 | + | } | |||
| 481 | + | c.inhibiting = want | |||
| 482 | + | if want != 0 { | |||
| 483 | + | return c.command(ctx, ports.ShortcutsInhibitState{Window: want, Active: true}) | |||
| 484 | + | } | |||
| 485 | + | return nil | |||
| 486 | + | } | |||
| 487 | + | ||||
| 457 | // resolveConstraint turns the active constraint into global coordinates | 488 | // resolveConstraint turns the active constraint into global coordinates | ||
| 458 | // and sends it to input when it changed. A hidden window holds nothing. | 489 | // and sends it to input when it changed. A hidden window holds nothing. | ||
| 459 | func (c *Core) resolveConstraint() { | 490 | func (c *Core) resolveConstraint() { | ||
| @@ -616,6 +647,24 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 616 | if p := c.popups[v.ID]; p != nil { | 647 | if p := c.popups[v.ID]; p != nil { | ||
| 617 | p.mapped = true | 648 | p.mapped = true | ||
| 618 | } | 649 | } | ||
| 650 | + | case ports.ShortcutsInhibit: | |||
| 651 | + | if v.Active { | |||
| 652 | + | c.inhibitors[v.Window] = true | |||
| 653 | + | } else { | |||
| 654 | + | delete(c.inhibitors, v.Window) | |||
| 655 | + | } | |||
| 656 | + | if err := c.updateInhibit(ctx); err != nil { | |||
| 657 | + | return nil | |||
| 658 | + | } | |||
| 659 | + | continue | |||
| 660 | + | case ports.IdleInhibit: | |||
| 661 | + | if v.Active { | |||
| 662 | + | c.idle[v.Window] = true | |||
| 663 | + | } else { | |||
| 664 | + | delete(c.idle, v.Window) | |||
| 665 | + | } | |||
| 666 | + | c.publishState() | |||
| 667 | + | continue | |||
| 619 | case ports.WindowAppID: | 668 | case ports.WindowAppID: | ||
| 620 | if info, ok := c.clients[v.ID]; ok { | 669 | if info, ok := c.clients[v.ID]; ok { | ||
| 621 | info.AppID = v.AppID | 670 | info.AppID = v.AppID | ||
| @@ -631,6 +680,8 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 631 | return nil | 680 | return nil | ||
| 632 | } | 681 | } | ||
| 633 | delete(c.clients, v.ID) | 682 | delete(c.clients, v.ID) | ||
| 683 | + | delete(c.inhibitors, v.ID) | |||
| 684 | + | delete(c.idle, v.ID) | |||
| 634 | if s, _ := c.screenOf(v.ID); s != nil { | 685 | if s, _ := c.screenOf(v.ID); s != nil { | ||
| 635 | s.mon.RemoveWindow(v.ID) | 686 | s.mon.RemoveWindow(v.ID) | ||
| 636 | } | 687 | } | ||
| @@ -797,6 +848,15 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 797 | if !ok { | 848 | if !ok { | ||
| 798 | continue | 849 | continue | ||
| 799 | } | 850 | } | ||
| 851 | + | // A focused window inhibiting shortcuts gets every key: | |||
| 852 | + | // no bind runs (emergency quit and VT switch are handled by | |||
| 853 | + | // input before core). | |||
| 854 | + | if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() { | |||
| 855 | + | if err := c.command(ctx, ports.ForwardKey{ID: c.inhibiting, Key: key}); err != nil { | |||
| 856 | + | return nil | |||
| 857 | + | } | |||
| 858 | + | continue | |||
| 859 | + | } | |||
| 800 | name := keyName(key.Keysym) | 860 | name := keyName(key.Keysym) | ||
| 801 | // A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1 | 861 | // A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1 | ||
| 802 | // prints exclam on US); then the physical key (code:N). | 862 | // prints exclam on US); then the physical key (code:N). | ||
A internal/core/inhibit_test.go +90 -0
| @@ -0,0 +1,90 @@ | |||
| 1 | + | package core_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/nefertty/internal/adapters/config" | |
| 9 | + | "github.com/bnema/nefertty/internal/core" | |
| 10 | + | "github.com/bnema/nefertty/internal/ports" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // commandOf waits for the next command of type T, skipping others. | |
| 14 | + | func commandOf[T ports.ClientCommand](t *testing.T, commands <-chan ports.ClientCommand) T { | |
| 15 | + | t.Helper() | |
| 16 | + | deadline := time.After(2 * time.Second) | |
| 17 | + | for { | |
| 18 | + | select { | |
| 19 | + | case c := <-commands: | |
| 20 | + | if v, ok := c.(T); ok { | |
| 21 | + | return v | |
| 22 | + | } | |
| 23 | + | case <-deadline: | |
| 24 | + | var zero T | |
| 25 | + | t.Fatalf("no %T", zero) | |
| 26 | + | return zero | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // A focused window inhibiting shortcuts gets bound keys; it stops when | |
| 32 | + | // it loses focus, and binds run again. | |
| 33 | + | func TestShortcutsInhibit(t *testing.T) { | |
| 34 | + | cfg := config.Defaults() | |
| 35 | + | input := make(chan ports.InputEvent, 4) | |
| 36 | + | client := make(chan ports.ClientEvent, 4) | |
| 37 | + | commands := make(chan ports.ClientCommand, 64) | |
| 38 | + | scenes := make(chan []ports.Scene, 1) | |
| 39 | + | spawn := make(chan ports.SpawnRequest, 4) | |
| 40 | + | state := make(chan ports.State, 1) | |
| 41 | + | output := make(chan ports.OutputEvent, 1) | |
| 42 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn, State: state}) | |
| 43 | + | if err != nil { | |
| 44 | + | t.Fatal(err) | |
| 45 | + | } | |
| 46 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 47 | + | done := make(chan error, 1) | |
| 48 | + | go func() { done <- c.Run(ctx) }() | |
| 49 | + | defer func() { cancel(); receive(t, done) }() | |
| 50 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |
| 51 | + | scene(t, scenes) | |
| 52 | + | client <- ports.WindowMapped{ID: 1} | |
| 53 | + | scene(t, scenes) | |
| 54 | + | client <- ports.ShortcutsInhibit{Window: 1, Active: true} | |
| 55 | + | if v := commandOf[ports.ShortcutsInhibitState](t, commands); v != (ports.ShortcutsInhibitState{Window: 1, Active: true}) { | |
| 56 | + | t.Fatal(v) | |
| 57 | + | } | |
| 58 | + | bound := ports.KeyEvent{Keysym: "Return", Mods: ports.ModSuper, Pressed: true} | |
| 59 | + | input <- bound | |
| 60 | + | if v := commandOf[ports.ForwardKey](t, commands); v != (ports.ForwardKey{ID: 1, Key: bound}) { | |
| 61 | + | t.Fatal(v) | |
| 62 | + | } | |
| 63 | + | select { | |
| 64 | + | case r := <-spawn: | |
| 65 | + | t.Fatalf("bind ran: %v", r) | |
| 66 | + | case <-time.After(30 * time.Millisecond): | |
| 67 | + | } | |
| 68 | + | // A new window takes focus: the inhibitor goes inactive, binds run. | |
| 69 | + | client <- ports.WindowMapped{ID: 2} | |
| 70 | + | if v := commandOf[ports.ShortcutsInhibitState](t, commands); v != (ports.ShortcutsInhibitState{Window: 1}) { | |
| 71 | + | t.Fatal(v) | |
| 72 | + | } | |
| 73 | + | input <- bound | |
| 74 | + | receive(t, spawn) | |
| 75 | + | // Idle inhibitors show in the state. | |
| 76 | + | client <- ports.IdleInhibit{Window: 2, Active: true} | |
| 77 | + | deadline := time.After(2 * time.Second) | |
| 78 | + | for { | |
| 79 | + | select { | |
| 80 | + | case st := <-state: | |
| 81 | + | for _, w := range st.Windows { | |
| 82 | + | if w.ID == 2 && w.IdleInhibit { | |
| 83 | + | return | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | case <-deadline: | |
| 87 | + | t.Fatal("idle inhibit not in state") | |
| 88 | + | } | |
| 89 | + | } | |
| 90 | + | } | |
| @@ -0,0 +1,90 @@ | |||||
| 1 | + | package core_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/nefertty/internal/adapters/config" | |||
| 9 | + | "github.com/bnema/nefertty/internal/core" | |||
| 10 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // commandOf waits for the next command of type T, skipping others. | |||
| 14 | + | func commandOf[T ports.ClientCommand](t *testing.T, commands <-chan ports.ClientCommand) T { | |||
| 15 | + | t.Helper() | |||
| 16 | + | deadline := time.After(2 * time.Second) | |||
| 17 | + | for { | |||
| 18 | + | select { | |||
| 19 | + | case c := <-commands: | |||
| 20 | + | if v, ok := c.(T); ok { | |||
| 21 | + | return v | |||
| 22 | + | } | |||
| 23 | + | case <-deadline: | |||
| 24 | + | var zero T | |||
| 25 | + | t.Fatalf("no %T", zero) | |||
| 26 | + | return zero | |||
| 27 | + | } | |||
| 28 | + | } | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // A focused window inhibiting shortcuts gets bound keys; it stops when | |||
| 32 | + | // it loses focus, and binds run again. | |||
| 33 | + | func TestShortcutsInhibit(t *testing.T) { | |||
| 34 | + | cfg := config.Defaults() | |||
| 35 | + | input := make(chan ports.InputEvent, 4) | |||
| 36 | + | client := make(chan ports.ClientEvent, 4) | |||
| 37 | + | commands := make(chan ports.ClientCommand, 64) | |||
| 38 | + | scenes := make(chan []ports.Scene, 1) | |||
| 39 | + | spawn := make(chan ports.SpawnRequest, 4) | |||
| 40 | + | state := make(chan ports.State, 1) | |||
| 41 | + | output := make(chan ports.OutputEvent, 1) | |||
| 42 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn, State: state}) | |||
| 43 | + | if err != nil { | |||
| 44 | + | t.Fatal(err) | |||
| 45 | + | } | |||
| 46 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 47 | + | done := make(chan error, 1) | |||
| 48 | + | go func() { done <- c.Run(ctx) }() | |||
| 49 | + | defer func() { cancel(); receive(t, done) }() | |||
| 50 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 51 | + | scene(t, scenes) | |||
| 52 | + | client <- ports.WindowMapped{ID: 1} | |||
| 53 | + | scene(t, scenes) | |||
| 54 | + | client <- ports.ShortcutsInhibit{Window: 1, Active: true} | |||
| 55 | + | if v := commandOf[ports.ShortcutsInhibitState](t, commands); v != (ports.ShortcutsInhibitState{Window: 1, Active: true}) { | |||
| 56 | + | t.Fatal(v) | |||
| 57 | + | } | |||
| 58 | + | bound := ports.KeyEvent{Keysym: "Return", Mods: ports.ModSuper, Pressed: true} | |||
| 59 | + | input <- bound | |||
| 60 | + | if v := commandOf[ports.ForwardKey](t, commands); v != (ports.ForwardKey{ID: 1, Key: bound}) { | |||
| 61 | + | t.Fatal(v) | |||
| 62 | + | } | |||
| 63 | + | select { | |||
| 64 | + | case r := <-spawn: | |||
| 65 | + | t.Fatalf("bind ran: %v", r) | |||
| 66 | + | case <-time.After(30 * time.Millisecond): | |||
| 67 | + | } | |||
| 68 | + | // A new window takes focus: the inhibitor goes inactive, binds run. | |||
| 69 | + | client <- ports.WindowMapped{ID: 2} | |||
| 70 | + | if v := commandOf[ports.ShortcutsInhibitState](t, commands); v != (ports.ShortcutsInhibitState{Window: 1}) { | |||
| 71 | + | t.Fatal(v) | |||
| 72 | + | } | |||
| 73 | + | input <- bound | |||
| 74 | + | receive(t, spawn) | |||
| 75 | + | // Idle inhibitors show in the state. | |||
| 76 | + | client <- ports.IdleInhibit{Window: 2, Active: true} | |||
| 77 | + | deadline := time.After(2 * time.Second) | |||
| 78 | + | for { | |||
| 79 | + | select { | |||
| 80 | + | case st := <-state: | |||
| 81 | + | for _, w := range st.Windows { | |||
| 82 | + | if w.ID == 2 && w.IdleInhibit { | |||
| 83 | + | return | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | case <-deadline: | |||
| 87 | + | t.Fatal("idle inhibit not in state") | |||
| 88 | + | } | |||
| 89 | + | } | |||
| 90 | + | } | |||
M internal/core/state.go +1 -1
| @@ -40,7 +40,7 @@ func (c *Core) state() ports.State { | |||
| 40 | 40 | n := indexOf(m.Workspaces, w) + 1 | |
| 41 | 41 | for _, id := range w.windows() { | |
| 42 | 42 | info := c.clients[id] | |
| 43 | - | st.Windows = append(st.Windows, ports.WindowState{ID: id, AppID: info.AppID, PID: info.PID, Output: s.name(), Workspace: n, Visible: w == cur}) | |
| 43 | + | st.Windows = append(st.Windows, ports.WindowState{ID: id, AppID: info.AppID, PID: info.PID, Output: s.name(), Workspace: n, Visible: w == cur, IdleInhibit: c.idle[id]}) | |
| 44 | 44 | } | |
| 45 | 45 | } | |
| 46 | 46 | } | |
| @@ -40,7 +40,7 @@ func (c *Core) state() ports.State { | |||||
| 40 | n := indexOf(m.Workspaces, w) + 1 | 40 | n := indexOf(m.Workspaces, w) + 1 | ||
| 41 | for _, id := range w.windows() { | 41 | for _, id := range w.windows() { | ||
| 42 | info := c.clients[id] | 42 | info := c.clients[id] | ||
| 43 | - | st.Windows = append(st.Windows, ports.WindowState{ID: id, AppID: info.AppID, PID: info.PID, Output: s.name(), Workspace: n, Visible: w == cur}) | 43 | + | st.Windows = append(st.Windows, ports.WindowState{ID: id, AppID: info.AppID, PID: info.PID, Output: s.name(), Workspace: n, Visible: w == cur, IdleInhibit: c.idle[id]}) |
| 44 | } | 44 | } | ||
| 45 | } | 45 | } | ||
| 46 | } | 46 | } | ||
M internal/ports/messages.go +30 -0
| @@ -34,6 +34,25 @@ type WindowUnmapped struct{ ID WindowID } | |||
| 34 | 34 | ||
| 35 | 35 | func (WindowUnmapped) clientEvent() {} | |
| 36 | 36 | ||
| 37 | + | // ShortcutsInhibit carries wayland → core that a window asks for (Active) | |
| 38 | + | // or stops asking for all keys, compositor binds included, while it has | |
| 39 | + | // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1). | |
| 40 | + | type ShortcutsInhibit struct { | |
| 41 | + | Window WindowID | |
| 42 | + | Active bool | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func (ShortcutsInhibit) clientEvent() {} | |
| 46 | + | ||
| 47 | + | // IdleInhibit carries wayland → core that a window keeps (Active) or | |
| 48 | + | // stops keeping the session from going idle (zwp_idle_inhibit_v1). | |
| 49 | + | type IdleInhibit struct { | |
| 50 | + | Window WindowID | |
| 51 | + | Active bool | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | func (IdleInhibit) clientEvent() {} | |
| 55 | + | ||
| 37 | 56 | // WindowFullscreenRequest carries wayland → core fullscreen requests. | |
| 38 | 57 | type WindowFullscreenRequest struct { | |
| 39 | 58 | ID WindowID | |
| @@ -371,6 +390,15 @@ type ClientCommand interface{ clientCommand() } | |||
| 371 | 390 | ||
| 372 | 391 | // ConfigureWindow carries core → wayland geometry and state. Output is the | |
| 373 | 392 | // connector showing the window, empty while it is hidden. | |
| 393 | + | // ShortcutsInhibitState carries core → wayland whether a window's | |
| 394 | + | // shortcuts inhibitor is in effect (it has keyboard focus). | |
| 395 | + | type ShortcutsInhibitState struct { | |
| 396 | + | Window WindowID | |
| 397 | + | Active bool | |
| 398 | + | } | |
| 399 | + | ||
| 400 | + | func (ShortcutsInhibitState) clientCommand() {} | |
| 401 | + | ||
| 374 | 402 | type ConfigureWindow struct { | |
| 375 | 403 | ID WindowID | |
| 376 | 404 | Width, Height int | |
| @@ -769,4 +797,6 @@ type WindowState struct { | |||
| 769 | 797 | // Visible means on the workspace on screen (it may be behind a | |
| 770 | 798 | // fullscreen window). | |
| 771 | 799 | Visible bool | |
| 800 | + | // IdleInhibit is set while the window keeps the session from idling. | |
| 801 | + | IdleInhibit bool `json:",omitempty"` | |
| 772 | 802 | } | |
| @@ -34,6 +34,25 @@ type WindowUnmapped struct{ ID WindowID } | |||||
| 34 | 34 | ||||
| 35 | func (WindowUnmapped) clientEvent() {} | 35 | func (WindowUnmapped) clientEvent() {} | ||
| 36 | 36 | ||||
| 37 | + | // ShortcutsInhibit carries wayland → core that a window asks for (Active) | |||
| 38 | + | // or stops asking for all keys, compositor binds included, while it has | |||
| 39 | + | // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1). | |||
| 40 | + | type ShortcutsInhibit struct { | |||
| 41 | + | Window WindowID | |||
| 42 | + | Active bool | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func (ShortcutsInhibit) clientEvent() {} | |||
| 46 | + | ||||
| 47 | + | // IdleInhibit carries wayland → core that a window keeps (Active) or | |||
| 48 | + | // stops keeping the session from going idle (zwp_idle_inhibit_v1). | |||
| 49 | + | type IdleInhibit struct { | |||
| 50 | + | Window WindowID | |||
| 51 | + | Active bool | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | func (IdleInhibit) clientEvent() {} | |||
| 55 | + | ||||
| 37 | // WindowFullscreenRequest carries wayland → core fullscreen requests. | 56 | // WindowFullscreenRequest carries wayland → core fullscreen requests. | ||
| 38 | type WindowFullscreenRequest struct { | 57 | type WindowFullscreenRequest struct { | ||
| 39 | ID WindowID | 58 | ID WindowID | ||
| @@ -371,6 +390,15 @@ type ClientCommand interface{ clientCommand() } | |||||
| 371 | 390 | ||||
| 372 | // ConfigureWindow carries core → wayland geometry and state. Output is the | 391 | // ConfigureWindow carries core → wayland geometry and state. Output is the | ||
| 373 | // connector showing the window, empty while it is hidden. | 392 | // connector showing the window, empty while it is hidden. | ||
| 393 | + | // ShortcutsInhibitState carries core → wayland whether a window's | |||
| 394 | + | // shortcuts inhibitor is in effect (it has keyboard focus). | |||
| 395 | + | type ShortcutsInhibitState struct { | |||
| 396 | + | Window WindowID | |||
| 397 | + | Active bool | |||
| 398 | + | } | |||
| 399 | + | ||||
| 400 | + | func (ShortcutsInhibitState) clientCommand() {} | |||
| 401 | + | ||||
| 374 | type ConfigureWindow struct { | 402 | type ConfigureWindow struct { | ||
| 375 | ID WindowID | 403 | ID WindowID | ||
| 376 | Width, Height int | 404 | Width, Height int | ||
| @@ -769,4 +797,6 @@ type WindowState struct { | |||||
| 769 | // Visible means on the workspace on screen (it may be behind a | 797 | // Visible means on the workspace on screen (it may be behind a | ||
| 770 | // fullscreen window). | 798 | // fullscreen window). | ||
| 771 | Visible bool | 799 | Visible bool | ||
| 800 | + | // IdleInhibit is set while the window keeps the session from idling. | |||
| 801 | + | IdleInhibit bool `json:",omitempty"` | |||
| 772 | } | 802 | } | ||