v0.1.0 d9218d6

core.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"github.com/bnema/neferwl/internal/ports"
8	"maps"
9	"slices"
10	"strconv"
11	"strings"
12	"unicode"
13)
14
15var ErrQuit = errors.New("quit requested")
16
17// Channels connects the owner to adapters. Scenes must have capacity 1: core is
18// its only sender and may drain a stale set; consumers only receive. Each
19// send holds one scene per output.
20type Channels struct {
21	Client   <-chan ports.ClientEvent
22	Input    <-chan ports.InputEvent
23	Output   <-chan ports.OutputEvent
24	Config   <-chan ports.ConfigChanged
25	Commands chan<- ports.ClientCommand
26	Spawn    chan<- ports.SpawnRequest
27	Scenes   chan []ports.Scene
28	// Layouts, when set, receives the output layout whenever it changes,
29	// latest first (capacity 1, drained like Scenes). Input and cursors use it.
30	Layouts chan ports.Layout
31	// Constraints, when set, receives the active pointer constraint in
32	// global logical coordinates whenever it changes (capacity 1, drained
33	// like Scenes). Input applies it to the pointer.
34	Constraints  chan ports.PointerConstraint
35	ConfigErrors chan<- error
36	// State, when set, receives a snapshot for scripts whenever it changes,
37	// latest first (capacity 1, drained like Scenes).
38	State chan ports.State
39	// Terminal, when set, keeps a window on every workspace on screen: an
40	// empty one gets the configured terminal (the terminal is the desktop).
41	Terminal bool
42}
43type binding struct {
44	mods ports.Mods
45	key  string
46}
47type Core struct {
48	ch               Channels
49	cfg              ports.Config
50	screens          []*screen
51	focusScreen      int
52	binds            map[binding]Action
53	pressed          map[string]bool
54	sent             map[WindowID]ports.ConfigureWindow
55	focus            WindowID
56	pointer          WindowID
57	grab             WindowID
58	buttons          map[uint32]bool
59	cursorX, cursorY float64 // global, logical
60	pointerOutput    string  // output under the pointer at the last motion
61	// constrained is the constraint wayland activated; constraint is it
62	// resolved to global coordinates, as last sent to input.
63	constrained ports.PointerConstrained
64	constraint  ports.PointerConstraint
65	seq         uint64
66	// layerChanged is set once layer state arrives from wayland.
67	layerChanged bool
68	sentOutputs  ports.SetOutputs
69	specs        []NamedWorkspace
70	presets      []Width
71	overflow     Overflow
72	slots        map[slotKey]*slotState
73	// toSpawn holds slots to start; Run sends them (apply has no context).
74	toSpawn     []slotKey
75	sentPending bool
76	// terms are the terminals spawned for empty workspaces, by SlotEnv
77	// token, until their window maps.
78	terms map[string]*termSpawn
79	// clients holds the app ID and PID of mapped windows, for State.
80	clients   map[WindowID]ports.WindowMapped
81	sentState ports.State
82	// popups are the placed xdg_popups; popupOrder stacks them, oldest first.
83	popups     map[WindowID]*popupState
84	popupOrder []WindowID
85	// layerFocus is the on-demand layer surface the user clicked; it keeps
86	// the keyboard while it stays mapped on-demand and no window focus
87	// changes on any output (layerOver, taken at the click): any click
88	// elsewhere, bind, activation or new window takes the keyboard back. The
89	// pointer moving to another output does not.
90	layerFocus 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
98}
99
100func keyName(s string) string {
101	if len(s) == 1 && unicode.IsLetter(rune(s[0])) {
102		return strings.ToLower(s)
103	}
104	return s
105}
106func parseBind(s, cmd string) (binding, error) {
107	parts := strings.Split(s, "+")
108	b := binding{key: keyName(parts[len(parts)-1])}
109	if b.key == "" {
110		return b, fmt.Errorf("invalid bind %q", s)
111	}
112	for _, p := range parts[:len(parts)-1] {
113		var m ports.Mods
114		switch p {
115		case "Cmd":
116			switch cmd {
117			case "super":
118				m = ports.ModSuper
119			case "alt":
120				m = ports.ModAlt
121			case "ctrl":
122				m = ports.ModCtrl
123			}
124		case "Shift":
125			m = ports.ModShift
126		case "Ctrl":
127			m = ports.ModCtrl
128		case "Alt":
129			m = ports.ModAlt
130		case "Super":
131			m = ports.ModSuper
132		}
133		if m == 0 || b.mods&m != 0 {
134			return b, fmt.Errorf("invalid bind %q", s)
135		}
136		b.mods |= m
137	}
138	return b, nil
139}
140func (c *Core) apply(cfg ports.Config) error {
141	if cfg.Layout.MaxColumns < 1 {
142		return fmt.Errorf("invalid max columns")
143	}
144	presets := make([]Width, 0, len(cfg.Layout.Presets))
145	for _, s := range cfg.Layout.Presets {
146		v, e := ParseWidth(s)
147		if e != nil {
148			return e
149		}
150		presets = append(presets, v)
151	}
152	if cfg.Layout.Gaps < 0 {
153		return fmt.Errorf("invalid gaps")
154	}
155	if cfg.Keyboard.CmdKey != "super" && cfg.Keyboard.CmdKey != "alt" && cfg.Keyboard.CmdKey != "ctrl" {
156		return fmt.Errorf("invalid cmd key")
157	}
158	binds := map[binding]Action{}
159	for combo, a := range cfg.Binds {
160		b, e := parseBind(combo, cfg.Keyboard.CmdKey)
161		if e != nil {
162			return e
163		}
164		if _, ok := binds[b]; ok {
165			return fmt.Errorf("duplicate bind %q", combo)
166		}
167		if _, ok := SpawnArgv(Action(a)); ok {
168			binds[b] = Action(a)
169			continue
170		}
171		if Action(a) == ActionScaleUp || Action(a) == ActionScaleDown {
172			binds[b] = Action(a)
173			continue
174		}
175		if _, _, ok := WorkspaceArg(Action(a)); ok {
176			binds[b] = Action(a)
177			continue
178		}
179		if _, ok := NamedArg(Action(a)); ok {
180			binds[b] = Action(a)
181			continue
182		}
183		switch Action(a) {
184		case "none", ActionSpawnTerminal, ActionFocusColumnLeft, ActionFocusColumnRight, ActionFocusWindowUp, ActionFocusWindowDown, ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth, ActionToggleFullscreen, ActionCloseWindow, ActionQuit, ActionFocusWorkspaceUp, ActionFocusWorkspaceDown, ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight:
185		default:
186			return fmt.Errorf("invalid action %q", a)
187		}
188		binds[b] = Action(a)
189	}
190	overflow := func(v string) (Overflow, error) {
191		switch o := Overflow(v); o {
192		case "", OverflowScroll, OverflowFixed:
193			return o, nil
194		}
195		return "", fmt.Errorf("invalid overflow %q", v)
196	}
197	defOverflow, err := overflow(cfg.Layout.Overflow)
198	if err != nil {
199		return err
200	}
201	if defOverflow == "" {
202		defOverflow = OverflowScroll
203	}
204	named := make([]NamedWorkspace, 0, len(cfg.Workspaces))
205	seen := map[string]bool{}
206	for _, ws := range cfg.Workspaces {
207		o, err := overflow(ws.Overflow)
208		if err != nil {
209			return err
210		}
211		if ws.Name == "" || seen[ws.Name] || ws.MaxColumns < 0 {
212			return fmt.Errorf("invalid workspace %q", ws.Name)
213		}
214		seen[ws.Name] = true
215		named = append(named, NamedWorkspace{Name: ws.Name, Monitor: ws.Monitor, Hidden: ws.Hidden, MaxColumns: ws.MaxColumns, Overflow: o})
216	}
217	specs, err := parseSlots(cfg.Workspaces)
218	if err != nil {
219		return err
220	}
221	c.cfg = cfg
222	c.binds = binds
223	c.specs, c.presets, c.overflow = named, presets, defOverflow
224	c.toSpawn = append(c.toSpawn, c.updateSlots(specs)...)
225	c.named()
226	for _, s := range c.screens {
227		c.settings(s.mon)
228	}
229	c.applyConfigScales()
230	return nil
231}
232
233// publishPending tells wayland at once when slots start waiting, before
234// their windows can map.
235func (c *Core) publishPending(ctx context.Context) error {
236	if p := c.anyPending(); p != c.sentPending {
237		if err := c.command(ctx, ports.SlotsPending{Pending: p}); err != nil {
238			return err
239		}
240		c.sentPending = p
241	}
242	return nil
243}
244
245// spawnSlots starts the slots queued by config. With shown set (the user
246// just switched to another workspace), it also refills the empty slots of
247// the workspace now on screen.
248func (c *Core) spawnSlots(ctx context.Context, shown bool) error {
249	keys := c.toSpawn
250	c.toSpawn = nil
251	if shown {
252		keys = append(keys, c.refill()...)
253	}
254	started := map[slotKey]bool{}
255	for _, key := range keys {
256		st, ok := c.slots[key]
257		// A key can be queued twice (new slot on the workspace on screen).
258		if !ok || started[key] || st.window != 0 {
259			continue
260		}
261		started[key] = true
262		req := c.spawnSlot(key)
263		// Wayland must read slot tokens before this window can map.
264		if err := c.publishPending(ctx); err != nil {
265			return err
266		}
267		select {
268		case <-ctx.Done():
269			return ctx.Err()
270		case c.ch.Spawn <- req:
271		}
272	}
273	return nil
274}
275func New(cfg ports.Config, ch Channels) (*Core, error) {
276	if cap(ch.Scenes) != 1 || (ch.Layouts != nil && cap(ch.Layouts) != 1) || (ch.Constraints != nil && cap(ch.Constraints) != 1) || (ch.State != nil && cap(ch.State) != 1) {
277		return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1")
278	}
279	// A placeholder screen holds windows until the first output arrives.
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{}}
281	if err := c.apply(cfg); err != nil {
282		return nil, err
283	}
284	return c, nil
285}
286func (c *Core) command(ctx context.Context, v ports.ClientCommand) error {
287	select {
288	case <-ctx.Done():
289		return ctx.Err()
290	case c.ch.Commands <- v:
291		return nil
292	}
293}
294
295// clientRect is the placement minus the border on its inset sides.
296// The renderer applies the same inset (ports.Border).
297func (c *Core) clientRect(p Placement) Rect {
298	if p.Fullscreen {
299		return p.Rect
300	}
301	return p.Rect.Inset(p.Inset, c.cfg.Border.Width)
302}
303
304// visible reports whether the window, layer surface or popup is on screen
305// on any output.
306func (c *Core) visible(id WindowID) bool {
307	_, _, ok := c.windowRect(id)
308	return ok
309}
310
311// windowFocus is the focused window of each output.
312func (c *Core) windowFocus() map[*screen]WindowID {
313	m := make(map[*screen]WindowID, len(c.screens))
314	for _, sc := range c.screens {
315		m[sc], _ = sc.mon.Focused()
316	}
317	return m
318}
319
320// keyboardFocus is the mapped top/overlay layer with exclusive keyboard
321// interactivity and the highest ID on any output, else a grabbing popup,
322// else a clicked on-demand layer (see layerFocus), else the focused window
323// of the focused output.
324func (c *Core) keyboardFocus() WindowID {
325	var layer WindowID
326	for _, sc := range c.screens {
327		for _, l := range sc.layers {
328			if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer {
329				layer = l.ID
330			}
331		}
332	}
333	if layer != 0 {
334		return layer
335	}
336	// A menu with a grab takes the keyboard until it closes.
337	if g := c.grabFocus(); g != 0 {
338		return g
339	}
340	if c.layerFocus != 0 && (!c.onDemand(c.layerFocus) || !maps.Equal(c.layerOver, c.windowFocus())) {
341		c.layerFocus, c.layerOver = 0, nil
342	}
343	if c.layerFocus != 0 {
344		return c.layerFocus
345	}
346	id, _ := c.cur().mon.Focused()
347	return id
348}
349
350// setLayers splits the mapped layer surfaces by output. A surface without an
351// output, or on an unknown one, goes to the focused output.
352func (c *Core) setLayers(all []ports.LayerSurface) {
353	for _, sc := range c.screens {
354		sc.layers = nil
355	}
356	for _, l := range all {
357		sc := c.cur()
358		if i := c.screenIndex(l.Output); i >= 0 {
359			sc = c.screens[i]
360		}
361		sc.layers = append(sc.layers, l)
362	}
363	for _, sc := range c.screens {
364		sc.arrange()
365	}
366}
367
368func (c *Core) publish(ctx context.Context) error {
369	if err := c.closeHiddenPopups(ctx); err != nil {
370		return err
371	}
372	// Clients learn outputs and scales before the configures sized for them.
373	if v := (ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name()}); !sameOutputs(v, c.sentOutputs) {
374		if err := c.command(ctx, v); err != nil {
375			return err
376		}
377		if !slices.Equal(v.Outputs, c.sentOutputs.Outputs) && c.ch.Layouts != nil {
378			latest(c.ch.Layouts, v.Outputs)
379		}
380		c.sentOutputs = v
381	}
382	terms := c.fillEmpty()
383	if err := c.publishPending(ctx); err != nil {
384		return err
385	}
386	for _, req := range terms {
387		select {
388		case <-ctx.Done():
389			return ctx.Err()
390		case c.ch.Spawn <- req:
391		}
392	}
393	alive := map[WindowID]bool{}
394	focus := c.keyboardFocus()
395	scenes := make([]ports.Scene, 0, len(c.screens))
396	// Before the first output (and after the last is unplugged) the
397	// placeholder's scene has no output name: no renderer draws it.
398	for i, sc := range c.screens {
399		c.seq++
400		o := sc.mon.Output()
401		scene := ports.Scene{Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), sc.placed...)}
402		layout := sc.mon.Layout()
403		// Only the focused output lights the focused window's lines.
404		scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), Rect{W: o.W, H: o.H}, i == c.focusScreen)
405		for _, p := range layout {
406			alive[p.ID] = true
407			// Only the focused output has an activated window.
408			focused := p.Focused && i == c.focusScreen
409			scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Inset: p.Inset})
410			floating := p.Floating
411			if p.Hidden {
412				if old, ok := c.sent[p.ID]; ok && (old.Activated || old.Output != "") {
413					old.Activated, old.Output = false, ""
414					if err := c.command(ctx, old); err != nil {
415						return err
416					}
417					c.sent[p.ID] = old
418				}
419				continue
420			}
421			r := c.clientRect(p)
422			v := ports.ConfigureWindow{ID: p.ID, Width: r.W, Height: r.H, Fullscreen: p.Fullscreen, Activated: focused, Floating: floating && !p.Fullscreen, Output: sc.name()}
423			if v.Floating {
424				// A floating window picks its own size.
425				v.Width, v.Height = 0, 0
426			}
427			if old, ok := c.sent[p.ID]; !ok || old != v {
428				if err := c.command(ctx, v); err != nil {
429					return err
430				}
431				c.sent[p.ID] = v
432			}
433		}
434		scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
435		scenes = append(scenes, scene)
436	}
437	for id := range c.sent {
438		if !alive[id] {
439			delete(c.sent, id)
440		}
441	}
442	// A workspace switch can hide the window under the pointer; it must not get
443	// clicks. The next motion re-runs hit-testing.
444	if c.pointer != 0 && !c.visible(c.pointer) {
445		c.pointer = 0
446		if c.grab == 0 {
447			if err := c.command(ctx, ports.PointerFocus{}); err != nil {
448				return err
449			}
450		}
451	}
452	if focus != c.focus {
453		if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil {
454			return err
455		}
456		c.focus = focus
457	}
458	if err := c.updateInhibit(ctx); err != nil {
459		return err
460	}
461	c.resolveConstraint()
462	latest(c.ch.Scenes, scenes)
463	c.publishState()
464	return nil
465}
466
467// updateInhibit makes the shortcuts inhibitor of the keyboard focus the
468// active one and tells wayland when it changes.
469func (c *Core) updateInhibit(ctx context.Context) error {
470	want := WindowID(0)
471	if f := c.keyboardFocus(); f != 0 && c.inhibitors[f] {
472		want = f
473	}
474	if want == c.inhibiting {
475		return nil
476	}
477	if c.inhibiting != 0 {
478		if err := c.command(ctx, ports.ShortcutsInhibitState{Window: c.inhibiting}); err != nil {
479			return err
480		}
481	}
482	c.inhibiting = want
483	if want != 0 {
484		return c.command(ctx, ports.ShortcutsInhibitState{Window: want, Active: true})
485	}
486	return nil
487}
488
489// resolveConstraint turns the active constraint into global coordinates
490// and sends it to input when it changed. A hidden window holds nothing.
491func (c *Core) resolveConstraint() {
492	var g ports.PointerConstraint
493	if id := c.constrained.ID; id != 0 {
494		if s, _ := c.screenOf(id); s != nil {
495			for _, p := range s.mon.Layout() {
496				if p.ID != id || p.Hidden {
497					continue
498				}
499				r := c.clientRect(p)
500				r.X += s.x
501				if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
502					// The region is clipped to the window.
503					x0, y0 := max(r.X, r.X+w.X), max(r.Y, r.Y+w.Y)
504					x1, y1 := min(r.X+r.W, r.X+w.X+w.W), min(r.Y+r.H, r.Y+w.Y+w.H)
505					if x1 > x0 && y1 > y0 {
506						r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
507					}
508				}
509				g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
510			}
511		}
512	}
513	// The cursor follows core; input resyncs to it only on a change.
514	if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
515		return
516	}
517	// A layout change must not leave a locked cursor off its window.
518	c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY)
519	g.X, g.Y = c.cursorX, c.cursorY
520	c.constraint = g
521	if c.ch.Constraints != nil {
522		latest(c.ch.Constraints, g)
523	}
524}
525
526// latest replaces any unread value: only the owner sends and drains;
527// consumers only receive.
528func latest[T any](ch chan T, v T) {
529	select {
530	case ch <- v:
531	default:
532		select {
533		case <-ch:
534		default:
535		}
536		select {
537		case ch <- v:
538		default:
539		}
540	}
541}
542
543func sameOutputs(a, b ports.SetOutputs) bool {
544	return a.Focused == b.Focused && slices.Equal(a.Outputs, b.Outputs)
545}
546
547// allLayers lists the layer surfaces of every output.
548func (c *Core) allLayers() []ports.LayerSurface {
549	var all []ports.LayerSurface
550	for _, sc := range c.screens {
551		all = append(all, sc.layers...)
552	}
553	return all
554}
555
556// hit returns the surface under the global logical point and the point in
557// its surface coordinates: popups, then overlay and top layers, then
558// windows (fullscreen wins; otherwise the last visible placement is
559// topmost), then bottom and background layers.
560func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
561	o, ok := c.layout().At(x, y)
562	if !ok {
563		return 0, 0, 0
564	}
565	lx, ly := x-float64(o.X), y-float64(o.Y)
566	sc := c.screens[c.screenIndex(o.Info.Name)]
567	// Layer popups are over everything; window popups are over the windows
568	// only, under the top and overlay layers (as drawn).
569	if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
570		return id, px, py
571	}
572	if id, px, py := layerAt(sc, lx, ly, true); id != 0 {
573		return id, px, py
574	}
575	if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 {
576		return id, px, py
577	}
578	var id WindowID
579	var sx, sy float64
580	full := false
581	for _, p := range sc.mon.Layout() {
582		r := c.clientRect(p)
583		if !p.Hidden && r.W > 0 && r.H > 0 && lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) {
584			// Floating windows come last in the layout and are on top,
585			// even of a fullscreen window; nothing else is.
586			if full && !p.Floating {
587				continue
588			}
589			if id == 0 || p.Fullscreen || p.Floating {
590				id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y)
591			}
592			full = full || p.Fullscreen
593		}
594	}
595	if id == 0 {
596		return layerAt(sc, lx, ly, false)
597	}
598	return id, sx, sy
599}
600
601func (c *Core) Run(ctx context.Context) error {
602	// Startup commands run once per session; a config reload does not
603	// run them again.
604	for _, argv := range c.cfg.Startup {
605		select {
606		case <-ctx.Done():
607			return nil
608		case c.ch.Spawn <- ports.SpawnRequest{Argv: slices.Clone(argv)}:
609		}
610	}
611	if c.spawnSlots(ctx, false) != nil || c.publishPending(ctx) != nil {
612		return nil
613	}
614	c.publishState()
615	for {
616		select {
617		case <-ctx.Done():
618			return nil
619		case ev, ok := <-c.ch.Client:
620			if !ok {
621				c.ch.Client = nil
622				continue
623			}
624			switch v := ev.(type) {
625			case ports.LayerChanged:
626				c.layerChanged = true
627				c.setLayers(v.Layers)
628			case ports.WindowMapped:
629				c.clients[v.ID] = v
630				if v.Floating {
631					if s, _ := c.screenOf(v.ID); s == nil {
632						c.cur().mon.AddFloating(v.ID, v.Width, v.Height)
633					}
634				} else if v.Slot == "" || (!c.placeSlotWindow(v.ID, v.Slot) && !c.placeTerminal(v.ID, v.Slot)) {
635					if s, _ := c.screenOf(v.ID); s == nil {
636						c.cur().mon.AddWindow(v.ID)
637					}
638				}
639			case ports.WindowResized:
640				if _, w := c.screenOf(v.ID); w != nil {
641					w.ResizeFloating(v.ID, v.Width, v.Height)
642				}
643			case ports.PopupRequest:
644				if err := c.placePopup(ctx, v); err != nil {
645					return nil
646				}
647			case ports.PopupMapped:
648				if p := c.popups[v.ID]; p != nil {
649					p.mapped = true
650				}
651			case ports.ShortcutsInhibit:
652				if v.Active {
653					c.inhibitors[v.Window] = true
654				} else {
655					delete(c.inhibitors, v.Window)
656				}
657				if err := c.updateInhibit(ctx); err != nil {
658					return nil
659				}
660				continue
661			case ports.IdleInhibit:
662				if v.Active {
663					c.idle[v.Window] = true
664				} else {
665					delete(c.idle, v.Window)
666				}
667				c.publishState()
668				continue
669			case ports.WindowAppID:
670				if info, ok := c.clients[v.ID]; ok {
671					info.AppID = v.AppID
672					c.clients[v.ID] = info
673				}
674			case ports.WindowUnmapped:
675				if c.popups[v.ID] != nil {
676					if err := c.dropPopup(ctx, v.ID); err != nil {
677						return nil
678					}
679				}
680				if err := c.closePopupsOf(ctx, v.ID); err != nil {
681					return nil
682				}
683				delete(c.clients, v.ID)
684				delete(c.inhibitors, v.ID)
685				delete(c.idle, v.ID)
686				if s, _ := c.screenOf(v.ID); s != nil {
687					s.mon.RemoveWindow(v.ID)
688				}
689				c.releaseSlots()
690				if c.pointer == v.ID {
691					c.pointer = 0
692					if err := c.command(ctx, ports.PointerFocus{}); err != nil {
693						return nil
694					}
695				}
696			case ports.PointerConstrained:
697				c.constrained = v
698			case ports.WindowFullscreenRequest:
699				if s, _ := c.screenOf(v.ID); s != nil {
700					s.mon.SetFullscreen(v.ID, v.Fullscreen)
701				}
702			case ports.WindowActivate:
703				if c.activate(ctx, v.ID) != nil {
704					return nil
705				}
706			}
707			if err := c.publish(ctx); err != nil {
708				return nil
709			}
710		case ev, ok := <-c.ch.Output:
711			if !ok {
712				c.ch.Output = nil
713				continue
714			}
715			switch v := ev.(type) {
716			case ports.OutputAdded:
717				c.addScreen(v.Info)
718				if c.layerChanged {
719					c.setLayers(c.allLayers())
720				}
721			case ports.OutputRemoved:
722				c.removeScreen(v.Name)
723				if c.layerChanged {
724					c.setLayers(c.allLayers())
725				}
726				c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, c.cursorX, c.cursorY)
727			}
728			if err := c.publish(ctx); err != nil {
729				return nil
730			}
731		case ev, ok := <-c.ch.Config:
732			if !ok {
733				c.ch.Config = nil
734				continue
735			}
736			if err := c.apply(ev.Config); err != nil {
737				select {
738				case c.ch.ConfigErrors <- err:
739				default:
740				}
741				continue
742			}
743			if c.spawnSlots(ctx, false) != nil {
744				return nil
745			}
746			if err := c.publish(ctx); err != nil {
747				return nil
748			}
749		case ev, ok := <-c.ch.Input:
750			if !ok {
751				c.ch.Input = nil
752				continue
753			}
754			switch v := ev.(type) {
755			case ports.PointerMotion:
756				// A locked pointer stays still; relative motion still flows.
757				if c.constraint.Mode != ports.ConstraintLock {
758					c.cursorX, c.cursorY = c.constraint.Clamp(c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y))
759				}
760				// The focused screen follows the pointer, so new windows
761				// and launchers open where the user is.
762				// Only a pointer entering another output switches: keyboard
763				// moves to another screen stick until then. Not mid-drag.
764				if o, ok := c.layout().At(c.cursorX, c.cursorY); ok && o.Info.Name != c.pointerOutput && c.grab == 0 {
765					c.pointerOutput = o.Info.Name
766					if o.Info.Name != c.cur().name() {
767						c.focusScreen = c.screenIndex(o.Info.Name)
768						if err := c.publish(ctx); err != nil {
769							return nil
770						}
771					}
772				}
773				id, x, y := c.hit(c.cursorX, c.cursorY)
774				// Layout changes are intentionally re-hit-tested only on motion.
775				if id != c.pointer {
776					c.pointer = id
777					if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
778						return nil
779					}
780				}
781				if id != 0 {
782					if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, TimeMsec: v.TimeMsec, TimeUsec: v.TimeUsec}); err != nil {
783						return nil
784					}
785				}
786				continue
787			case ports.PointerButton:
788				id := c.pointer
789				if c.grab != 0 {
790					id = c.grab
791				}
792				if v.Pressed && len(c.buttons) == 0 {
793					// A click outside an open menu closes it.
794					if err := c.dismissGrabs(ctx, id); err != nil {
795						return nil
796					}
797				}
798				if v.Pressed {
799					if len(c.buttons) == 0 {
800						c.grab = id
801					}
802					c.buttons[v.Button] = true
803				} else {
804					delete(c.buttons, v.Button)
805				}
806				if id != 0 {
807					if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil {
808						return nil
809					}
810					// A click on an on-demand layer gives it the keyboard.
811					if v.Pressed && c.onDemand(id) && c.layerFocus != id {
812						c.layerFocus = id
813						c.layerOver = c.windowFocus()
814						if err := c.publish(ctx); err != nil {
815							return nil
816						}
817					} else if v.Pressed && c.layerFocus != 0 && id != c.layerFocus && c.popupRoot(id) != c.layerFocus {
818						// A click anywhere else takes the keyboard back.
819						c.layerFocus = 0
820						if err := c.publish(ctx); err != nil {
821							return nil
822						}
823					}
824					// A click focuses the window and its output.
825					s, w := c.screenOf(id)
826					if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) {
827						w.FocusID(id)
828						c.focusScreen = c.screenIndex(s.name())
829						if err := c.publish(ctx); err != nil {
830							return nil
831						}
832					}
833				}
834				if len(c.buttons) == 0 {
835					c.grab = 0
836				}
837				continue
838			case ports.PointerAxis:
839				// Scroll goes to the window under the pointer, which has the
840				// pointer focus even mid-drag.
841				if c.pointer != 0 {
842					if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
843						return nil
844					}
845				}
846				continue
847			}
848			key, ok := ev.(ports.KeyEvent)
849			if !ok {
850				continue
851			}
852			// A focused window inhibiting shortcuts gets every key:
853			// no bind runs (emergency quit and VT switch are handled by
854			// input before core).
855			if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() {
856				if held := heldKey(key); !key.Pressed && c.pressed[held] {
857					// Its press ran a bind before inhibiting began: the
858					// window never saw it.
859					delete(c.pressed, held)
860					continue
861				}
862				if err := c.command(ctx, ports.ForwardKey{ID: c.inhibiting, Key: key}); err != nil {
863					return nil
864				}
865				continue
866			}
867			name := keyName(key.Keysym)
868			// A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1
869			// prints exclam on US); then the physical key (code:N).
870			action, bound := c.binds[binding{key: name, mods: key.Mods}]
871			if !bound && key.Base != "" {
872				action, bound = c.binds[binding{key: keyName(key.Base), mods: key.Mods}]
873			}
874			if !bound && key.Keycode != 0 {
875				action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
876			}
877			// Track presses by physical key: Shift may be released before the key.
878			held := heldKey(key)
879			if !key.Pressed {
880				consumed := c.pressed[held]
881				delete(c.pressed, held)
882				if consumed {
883					continue
884				}
885			}
886			if key.Pressed {
887				if bound {
888					c.pressed[held] = true
889					if action == ActionScaleUp || action == ActionScaleDown {
890						dir := 1
891						if action == ActionScaleDown {
892							dir = -1
893						}
894						sc := c.cur()
895						sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
896						c.order()
897						if err := c.publish(ctx); err != nil {
898							return nil
899						}
900						continue
901					}
902					before := c.cur().mon.Current()
903					c.layerFocus = 0 // a bind acts on the windows
904					effect := c.applyAction(action)
905					if effect.Quit {
906						return ErrQuit
907					}
908					if effect.Spawn {
909						argv := effect.Argv
910						if argv == nil {
911							argv = append([]string(nil), c.cfg.Terminal.Command...)
912						}
913						select {
914						case <-ctx.Done():
915							return nil
916						case c.ch.Spawn <- ports.SpawnRequest{Argv: argv}:
917						}
918					}
919					if c.afterShow(ctx, before) != nil {
920						return nil
921					}
922					if effect.Close != 0 {
923						if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
924							return nil
925						}
926					}
927					if err := c.publish(ctx); err != nil {
928						return nil
929					}
930					continue
931				}
932				c.pressed[held] = false
933			}
934			if id := c.keyboardFocus(); id != 0 {
935				if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {
936					return nil
937				}
938			}
939		}
940	}
941}
942
943// afterShow keeps slots and guests right once a workspace came on screen;
944// before is the focused screen's workspace before the change.
945func (c *Core) afterShow(ctx context.Context, before *Workspace) error {
946	// Showing a declared workspace refills its empty slots.
947	c.releaseSlots()
948	c.settleGuests()
949	return c.spawnSlots(ctx, c.cur().mon.Current() != before)
950}
951
952// activate brings a window forward for a valid xdg-activation token. The
953// token proves a user action in the requesting client (a click on a link,
954// a notification), so unlike other client requests it may move the user
955// (the exception to the ADR 011 golden rule).
956func (c *Core) activate(ctx context.Context, id WindowID) error {
957	s, w := c.screenOf(id)
958	if s == nil || c.popups[id] != nil {
959		return nil
960	}
961	before := c.cur().mon.Current()
962	if w != s.mon.Current() {
963		s.mon.show(w)
964	}
965	w.Activate(id)
966	c.focusScreen = c.screenIndex(s.name())
967	return c.afterShow(ctx, before)
968}
969
970// heldKey names a key for press tracking: by physical key, since Shift
971// may be released before the key.
972func heldKey(key ports.KeyEvent) string {
973	if key.Keycode != 0 {
974		return "#" + strconv.FormatUint(uint64(key.Keycode), 10)
975	}
976	return keyName(key.Keysym)
977}

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 26

Tags 10

↑↓ moveenter openesc close