detached 41da69c

fix(core): honour the fullscreen state a window maps with

An X11 window mapping with _NET_WM_STATE_FULLSCREEN (a Proton game) was
mapped floating when fixed-size, and its fullscreen request, sent after
the map, was dropped by the fullscreen grace. WindowMapped now carries
Fullscreen: core maps it as a tile of its own, fullscreen and focused.
commit
41da69c5abbaae254105b5c5a4860b277dc81ca9
author
brice · · 2026-10-22 18:13 UTC
parent
5eb9885
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
checks
1/1
7 files · +115 -7
M docs/desktop.md +1 -1
@@ -62,7 +62,7 @@ With `xwayland = builtin` (experimental), neferwl starts `Xwayland` itself and m
6262
6363 Games with `builtin`:
6464
65-- **Fullscreen:** an X11 window can ask for fullscreen (`_NET_WM_STATE_FULLSCREEN`) and leave it again; `toggle-fullscreen` works too. A request within 1 second of the window mapping is ignored, so a launcher that remembers a fullscreen size does not cover the windows it opens next. The app can ask again later.
65+- **Fullscreen:** an X11 window can ask for fullscreen (`_NET_WM_STATE_FULLSCREEN`) and leave it again; `toggle-fullscreen` works too. A window that maps with the fullscreen state already set (games do) opens fullscreen in a column of its own, focused, even if its size is fixed. A request made within 1 second *after* the map is ignored, so a launcher that remembers a fullscreen size does not cover the windows it opens next. The app can ask again later.
6666 - **Output:** a window that opens exactly at one output's area opens on that output, and its fullscreen fills it.
6767 - **Keyboard grab:** an X11 app that grabs the keyboard inhibits the compositor binds only while it is focused and fullscreen; a windowed app cannot lock the binds. There is no escape bind, as with Wayland shortcut inhibitors; leave by unfullscreening from the app or switching away with the mouse.
6868 - **Pointer:** mouse-look works through relative pointer and pointer constraints.
@@ -62,7 +62,7 @@ With `xwayland = builtin` (experimental), neferwl starts `Xwayland` itself and m
62 62
63 Games with `builtin`:63 Games with `builtin`:
64 64
65-- **Fullscreen:** an X11 window can ask for fullscreen (`_NET_WM_STATE_FULLSCREEN`) and leave it again; `toggle-fullscreen` works too. A request within 1 second of the window mapping is ignored, so a launcher that remembers a fullscreen size does not cover the windows it opens next. The app can ask again later.65+- **Fullscreen:** an X11 window can ask for fullscreen (`_NET_WM_STATE_FULLSCREEN`) and leave it again; `toggle-fullscreen` works too. A window that maps with the fullscreen state already set (games do) opens fullscreen in a column of its own, focused, even if its size is fixed. A request made within 1 second *after* the map is ignored, so a launcher that remembers a fullscreen size does not cover the windows it opens next. The app can ask again later.
66 - **Output:** a window that opens exactly at one output's area opens on that output, and its fullscreen fills it.66 - **Output:** a window that opens exactly at one output's area opens on that output, and its fullscreen fills it.
67 - **Keyboard grab:** an X11 app that grabs the keyboard inhibits the compositor binds only while it is focused and fullscreen; a windowed app cannot lock the binds. There is no escape bind, as with Wayland shortcut inhibitors; leave by unfullscreening from the app or switching away with the mouse.67 - **Keyboard grab:** an X11 app that grabs the keyboard inhibits the compositor binds only while it is focused and fullscreen; a windowed app cannot lock the binds. There is no escape bind, as with Wayland shortcut inhibitors; leave by unfullscreening from the app or switching away with the mouse.
68 - **Pointer:** mouse-look works through relative pointer and pointer constraints.68 - **Pointer:** mouse-look works through relative pointer and pointer constraints.
M internal/adapters/wayland/x11window.go +1 -4
@@ -311,10 +311,7 @@ func (x *x11Window) tryMap() {
311311 if w.parent != nil && w.parent.mapped {
312312 parent = w.parent.id
313313 }
314- s.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, PID: x.info.PID, Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent, Positioned: true, Modal: w.modal, Remap: w.gen > 1, HasAt: x.info.HasAt, AtX: x.info.AtX, AtY: x.info.AtY})
315- if x.info.Fullscreen {
316- s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true})
317- }
314+ s.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, PID: x.info.PID, Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent, Positioned: true, Modal: w.modal, Remap: w.gen > 1, HasAt: x.info.HasAt, AtX: x.info.AtX, AtY: x.info.AtY, Fullscreen: x.info.Fullscreen})
318315 s.syncInhibitors()
319316 s.toplevelChanged(w)
320317 s.castInventoryChanged()
@@ -311,10 +311,7 @@ func (x *x11Window) tryMap() {
311 if w.parent != nil && w.parent.mapped {311 if w.parent != nil && w.parent.mapped {
312 parent = w.parent.id312 parent = w.parent.id
313 }313 }
314- s.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, PID: x.info.PID, Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent, Positioned: true, Modal: w.modal, Remap: w.gen > 1, HasAt: x.info.HasAt, AtX: x.info.AtX, AtY: x.info.AtY})314+ s.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, PID: x.info.PID, Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent, Positioned: true, Modal: w.modal, Remap: w.gen > 1, HasAt: x.info.HasAt, AtX: x.info.AtX, AtY: x.info.AtY, Fullscreen: x.info.Fullscreen})
315- if x.info.Fullscreen {
316- s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true})
317- }
318 s.syncInhibitors()315 s.syncInhibitors()
319 s.toplevelChanged(w)316 s.toplevelChanged(w)
320 s.castInventoryChanged()317 s.castInventoryChanged()
M internal/app/testdata/x11probe/main.go +16 -0
@@ -9,6 +9,7 @@
99 // -override-for (prints "override unmapped"). With -override-dismiss-on-focusout
1010 // it unmaps it instead when the main window loses focus (prints "dismissed").
1111 //
12+// With -initial-fullscreen the window maps already fullscreen and fixed-size.
1213 // With -fullscreen it asks the window manager for _NET_WM_STATE_FULLSCREEN
1314 // -fullscreen-delay after the window is mapped (0: right at the map) and
1415 // prints "fullscreen WxH" when the window is resized; -fullscreen-off asks to
@@ -71,6 +72,7 @@ func run() error {
7172 fullscreen := flag.Bool("fullscreen", false, "request _NET_WM_STATE_FULLSCREEN after the window is mapped")
7273 fullscreenDelay := flag.Duration("fullscreen-delay", 0, "delay between the map and the fullscreen request")
7374 fullscreenOff := flag.Duration("fullscreen-off", 0, "leave fullscreen this long after the request (0: never)")
75+ initialFullscreen := flag.Bool("initial-fullscreen", false, "set _NET_WM_STATE_FULLSCREEN and fixed-size WM_NORMAL_HINTS (min=max=size) before mapping")
7476 keyboardGrab := flag.Bool("keyboard-grab", false, "XGrabKeyboard on the window once it is mapped")
7577 at := flag.String("at", "0,0", "window position X,Y in root coordinates")
7678 copyText := flag.String("copy", "", "own a selection with TEXT (or @file) and serve it, no window")
@@ -144,6 +146,20 @@ func run() error {
144146 cv.SetX(int32(atoi(atX))).SetY(int32(atoi(atY))).SetWidth(uint32(w)).SetHeight(uint32(h))
145147 xproto.ConfigureWindow(b, win, &cv)
146148 }
149+ if *initialFullscreen {
150+ // Like a Proton game: fullscreen state and a fixed size, both set
151+ // before the map.
152+ var st [4]byte
153+ binary.LittleEndian.PutUint32(st[:], netWMFullscreen)
154+ xproto.ChangeProperty(b, xproto.PropModeReplace, win, xproto.Atom(netWMState), xproto.AtomAtom, 32, 1, st[:])
155+ var nh [72]byte // WM_SIZE_HINTS: flags, x, y, w, h, min, max, ...
156+ binary.LittleEndian.PutUint32(nh[0:], 16|32) // PMinSize|PMaxSize
157+ for _, off := range []int{20, 28} {
158+ binary.LittleEndian.PutUint32(nh[off:], uint32(w))
159+ binary.LittleEndian.PutUint32(nh[off+4:], uint32(h))
160+ }
161+ xproto.ChangeProperty(b, xproto.PropModeReplace, win, xproto.AtomWmNormalHints, xproto.AtomWmSizeHints, 32, 18, nh[:])
162+ }
147163 xproto.MapWindow(b, win)
148164
149165 // The override-redirect window is created once the main window is mapped.
@@ -9,6 +9,7 @@
9 // -override-for (prints "override unmapped"). With -override-dismiss-on-focusout9 // -override-for (prints "override unmapped"). With -override-dismiss-on-focusout
10 // it unmaps it instead when the main window loses focus (prints "dismissed").10 // it unmaps it instead when the main window loses focus (prints "dismissed").
11 //11 //
12+// With -initial-fullscreen the window maps already fullscreen and fixed-size.
12 // With -fullscreen it asks the window manager for _NET_WM_STATE_FULLSCREEN13 // With -fullscreen it asks the window manager for _NET_WM_STATE_FULLSCREEN
13 // -fullscreen-delay after the window is mapped (0: right at the map) and14 // -fullscreen-delay after the window is mapped (0: right at the map) and
14 // prints "fullscreen WxH" when the window is resized; -fullscreen-off asks to15 // prints "fullscreen WxH" when the window is resized; -fullscreen-off asks to
@@ -71,6 +72,7 @@ func run() error {
71 fullscreen := flag.Bool("fullscreen", false, "request _NET_WM_STATE_FULLSCREEN after the window is mapped")72 fullscreen := flag.Bool("fullscreen", false, "request _NET_WM_STATE_FULLSCREEN after the window is mapped")
72 fullscreenDelay := flag.Duration("fullscreen-delay", 0, "delay between the map and the fullscreen request")73 fullscreenDelay := flag.Duration("fullscreen-delay", 0, "delay between the map and the fullscreen request")
73 fullscreenOff := flag.Duration("fullscreen-off", 0, "leave fullscreen this long after the request (0: never)")74 fullscreenOff := flag.Duration("fullscreen-off", 0, "leave fullscreen this long after the request (0: never)")
75+ initialFullscreen := flag.Bool("initial-fullscreen", false, "set _NET_WM_STATE_FULLSCREEN and fixed-size WM_NORMAL_HINTS (min=max=size) before mapping")
74 keyboardGrab := flag.Bool("keyboard-grab", false, "XGrabKeyboard on the window once it is mapped")76 keyboardGrab := flag.Bool("keyboard-grab", false, "XGrabKeyboard on the window once it is mapped")
75 at := flag.String("at", "0,0", "window position X,Y in root coordinates")77 at := flag.String("at", "0,0", "window position X,Y in root coordinates")
76 copyText := flag.String("copy", "", "own a selection with TEXT (or @file) and serve it, no window")78 copyText := flag.String("copy", "", "own a selection with TEXT (or @file) and serve it, no window")
@@ -144,6 +146,20 @@ func run() error {
144 cv.SetX(int32(atoi(atX))).SetY(int32(atoi(atY))).SetWidth(uint32(w)).SetHeight(uint32(h))146 cv.SetX(int32(atoi(atX))).SetY(int32(atoi(atY))).SetWidth(uint32(w)).SetHeight(uint32(h))
145 xproto.ConfigureWindow(b, win, &cv)147 xproto.ConfigureWindow(b, win, &cv)
146 }148 }
149+ if *initialFullscreen {
150+ // Like a Proton game: fullscreen state and a fixed size, both set
151+ // before the map.
152+ var st [4]byte
153+ binary.LittleEndian.PutUint32(st[:], netWMFullscreen)
154+ xproto.ChangeProperty(b, xproto.PropModeReplace, win, xproto.Atom(netWMState), xproto.AtomAtom, 32, 1, st[:])
155+ var nh [72]byte // WM_SIZE_HINTS: flags, x, y, w, h, min, max, ...
156+ binary.LittleEndian.PutUint32(nh[0:], 16|32) // PMinSize|PMaxSize
157+ for _, off := range []int{20, 28} {
158+ binary.LittleEndian.PutUint32(nh[off:], uint32(w))
159+ binary.LittleEndian.PutUint32(nh[off+4:], uint32(h))
160+ }
161+ xproto.ChangeProperty(b, xproto.PropModeReplace, win, xproto.AtomWmNormalHints, xproto.AtomWmSizeHints, 32, 18, nh[:])
162+ }
147 xproto.MapWindow(b, win)163 xproto.MapWindow(b, win)
148 164
149 // The override-redirect window is created once the main window is mapped.165 // The override-redirect window is created once the main window is mapped.
M internal/app/xwayland_test.go +28 -2
@@ -458,8 +458,34 @@ func TestXwaylandFullscreenOff(t *testing.T) {
458458 })
459459 }
460460
461-// TestXwaylandFullscreenGrace: a request made at once after the map stays
462-// tiled (fullscreenGrace in core).
461+// TestXwaylandInitialFullscreen: a fixed-size window that maps with
462+// _NET_WM_STATE_FULLSCREEN already set (a Proton game) opens fullscreen in a
463+// column of its own, focused, beside the tiled window already open. This is
464+// the state it maps with, not a request: the grace does not apply.
465+func TestXwaylandInitialFullscreen(t *testing.T) {
466+ e := startXwaylandWith(t, [][2]int{{xwaylandW, xwaylandH}}, func(c *ports.Config) { c.Layout.Gaps = 20 })
467+ shot := filepath.Join(e.shots, "latest.png")
468+ first := startProbe(t, e.bin, e.display, "-class", "First", "-size", "200x100")
469+ until(t, "first tiled", func() bool {
470+ return strings.Contains(first.out.String(), "mapped ") && len(readState(e.statePath).Windows) == 1
471+ })
472+ p := startProbe(t, e.bin, e.display, "-class", "Game", "-initial-fullscreen", "-size", "640x360")
473+ until(t, "game mapped", func() bool { return strings.Contains(p.out.String(), "mapped ") })
474+ until(t, "game in its own column, fullscreen", func() bool {
475+ st := readState(e.statePath)
476+ for _, w := range st.Windows {
477+ if w.AppID == "Game" {
478+ return !w.Floating && w.Column == 2 && w.Row == 1
479+ }
480+ }
481+ return false
482+ })
483+ until(t, "output all green", func() bool { return fullOutput(shot) >= 0.99 })
484+ until(t, "game focused", func() bool { return strings.Contains(p.out.String(), "focus") })
485+}
486+
487+// TestXwaylandFullscreenGrace: a request made at once after the map (not the
488+// state it maps with) stays tiled (fullscreenGrace in core).
463489 func TestXwaylandFullscreenGrace(t *testing.T) {
464490 e := startXwayland(t)
465491 p := startProbe(t, e.bin, e.display, "-fullscreen")
@@ -458,8 +458,34 @@ func TestXwaylandFullscreenOff(t *testing.T) {
458 })458 })
459 }459 }
460 460
461-// TestXwaylandFullscreenGrace: a request made at once after the map stays461+// TestXwaylandInitialFullscreen: a fixed-size window that maps with
462-// tiled (fullscreenGrace in core).462+// _NET_WM_STATE_FULLSCREEN already set (a Proton game) opens fullscreen in a
463+// column of its own, focused, beside the tiled window already open. This is
464+// the state it maps with, not a request: the grace does not apply.
465+func TestXwaylandInitialFullscreen(t *testing.T) {
466+ e := startXwaylandWith(t, [][2]int{{xwaylandW, xwaylandH}}, func(c *ports.Config) { c.Layout.Gaps = 20 })
467+ shot := filepath.Join(e.shots, "latest.png")
468+ first := startProbe(t, e.bin, e.display, "-class", "First", "-size", "200x100")
469+ until(t, "first tiled", func() bool {
470+ return strings.Contains(first.out.String(), "mapped ") && len(readState(e.statePath).Windows) == 1
471+ })
472+ p := startProbe(t, e.bin, e.display, "-class", "Game", "-initial-fullscreen", "-size", "640x360")
473+ until(t, "game mapped", func() bool { return strings.Contains(p.out.String(), "mapped ") })
474+ until(t, "game in its own column, fullscreen", func() bool {
475+ st := readState(e.statePath)
476+ for _, w := range st.Windows {
477+ if w.AppID == "Game" {
478+ return !w.Floating && w.Column == 2 && w.Row == 1
479+ }
480+ }
481+ return false
482+ })
483+ until(t, "output all green", func() bool { return fullOutput(shot) >= 0.99 })
484+ until(t, "game focused", func() bool { return strings.Contains(p.out.String(), "focus") })
485+}
486+
487+// TestXwaylandFullscreenGrace: a request made at once after the map (not the
488+// state it maps with) stays tiled (fullscreenGrace in core).
463 func TestXwaylandFullscreenGrace(t *testing.T) {489 func TestXwaylandFullscreenGrace(t *testing.T) {
464 e := startXwayland(t)490 e := startXwayland(t)
465 p := startProbe(t, e.bin, e.display, "-fullscreen")491 p := startProbe(t, e.bin, e.display, "-fullscreen")
M internal/core/core_test.go +46 -0
@@ -4,6 +4,7 @@ import (
44 "context"
55 "errors"
66 "reflect"
7+ "regexp"
78 "slices"
89 "testing"
910 "testing/synctest"
@@ -1254,3 +1255,48 @@ func TestWindowActivateShowsAndFocuses(t *testing.T) {
12541255 }
12551256
12561257 }
1258+
1259+// A window that maps with the fullscreen state (an X11 game with
1260+// _NET_WM_STATE_FULLSCREEN set before the map) covers its screen, focused,
1261+// in a tile of its own even when it was classified floating; a rule that
1262+// floats it does not change that. The grace still drops a later request.
1263+func TestMappedFullscreenHonoured(t *testing.T) {
1264+ for _, tc := range []struct {
1265+ name string
1266+ rule bool
1267+ }{{"fixed size", false}, {"floating rule", true}} {
1268+ t.Run(tc.name, func(t *testing.T) {
1269+ cfg := scrollDefaults()
1270+ cfg.Border.Width = 0
1271+ if tc.rule {
1272+ on := true
1273+ cfg.Rules = []ports.WindowRule{{AppID: regexp.MustCompile("^game$"), Floating: &on}}
1274+ }
1275+ client := make(chan ports.ClientEvent, 8)
1276+ output := make(chan ports.OutputEvent, 8)
1277+ commands := make(chan ports.ClientCommand, 64)
1278+ scenes := make(chan []ports.Scene, 1)
1279+ c, err := core.New(cfg, core.Channels{Client: client, Input: make(chan ports.InputEvent, 8), Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)})
1280+ if err != nil {
1281+ t.Fatal(err)
1282+ }
1283+ ctx, cancel := context.WithCancel(context.Background())
1284+ defer cancel()
1285+ go c.Run(ctx)
1286+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
1287+ scene(t, scenes)
1288+ client <- ports.WindowMapped{ID: 1}
1289+ scene(t, scenes)
1290+ client <- ports.WindowMapped{ID: 2, AppID: "game", Floating: true, Width: 100, Height: 80, Fullscreen: true}
1291+ s := scene(t, scenes)
1292+ for !slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == 2 && w.Fullscreen && w.Focused && !w.Hidden }) {
1293+ s = scene(t, scenes)
1294+ }
1295+ for _, w := range s.Windows {
1296+ if w.ID == 2 && (w.Rect.W != 100 || w.Rect.H != 80) {
1297+ t.Fatalf("fullscreen rect %+v, want the output", w.Rect)
1298+ }
1299+ }
1300+ })
1301+ }
1302+}
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 "errors"5 "errors"
6 "reflect"6 "reflect"
7+ "regexp"
7 "slices"8 "slices"
8 "testing"9 "testing"
9 "testing/synctest"10 "testing/synctest"
@@ -1254,3 +1255,48 @@ func TestWindowActivateShowsAndFocuses(t *testing.T) {
1254 }1255 }
1255 1256
1256 }1257 }
1258+
1259+// A window that maps with the fullscreen state (an X11 game with
1260+// _NET_WM_STATE_FULLSCREEN set before the map) covers its screen, focused,
1261+// in a tile of its own even when it was classified floating; a rule that
1262+// floats it does not change that. The grace still drops a later request.
1263+func TestMappedFullscreenHonoured(t *testing.T) {
1264+ for _, tc := range []struct {
1265+ name string
1266+ rule bool
1267+ }{{"fixed size", false}, {"floating rule", true}} {
1268+ t.Run(tc.name, func(t *testing.T) {
1269+ cfg := scrollDefaults()
1270+ cfg.Border.Width = 0
1271+ if tc.rule {
1272+ on := true
1273+ cfg.Rules = []ports.WindowRule{{AppID: regexp.MustCompile("^game$"), Floating: &on}}
1274+ }
1275+ client := make(chan ports.ClientEvent, 8)
1276+ output := make(chan ports.OutputEvent, 8)
1277+ commands := make(chan ports.ClientCommand, 64)
1278+ scenes := make(chan []ports.Scene, 1)
1279+ c, err := core.New(cfg, core.Channels{Client: client, Input: make(chan ports.InputEvent, 8), Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)})
1280+ if err != nil {
1281+ t.Fatal(err)
1282+ }
1283+ ctx, cancel := context.WithCancel(context.Background())
1284+ defer cancel()
1285+ go c.Run(ctx)
1286+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
1287+ scene(t, scenes)
1288+ client <- ports.WindowMapped{ID: 1}
1289+ scene(t, scenes)
1290+ client <- ports.WindowMapped{ID: 2, AppID: "game", Floating: true, Width: 100, Height: 80, Fullscreen: true}
1291+ s := scene(t, scenes)
1292+ for !slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == 2 && w.Fullscreen && w.Focused && !w.Hidden }) {
1293+ s = scene(t, scenes)
1294+ }
1295+ for _, w := range s.Windows {
1296+ if w.ID == 2 && (w.Rect.W != 100 || w.Rect.H != 80) {
1297+ t.Fatalf("fullscreen rect %+v, want the output", w.Rect)
1298+ }
1299+ }
1300+ })
1301+ }
1302+}
M internal/core/transition.go +19 -0
@@ -400,11 +400,17 @@ func (c *Core) appearFrom(rm *rectMotion, p Placement, old *rectMotion, now time
400400 // client is configured once, to its final size.
401401 func (c *Core) mapWindow(v ports.WindowMapped) {
402402 now := c.now()
403+ if v.Fullscreen && v.Parent == 0 {
404+ // Mapped fullscreen: a tile of its own, whatever size class the
405+ // client reported (a fixed-size game is not a float).
406+ v.Floating, v.Width, v.Height = false, 0, 0
407+ }
403408 c.windows.mapped(v, now)
404409 if s, _ := c.screenOf(v.ID); s != nil || !c.animOn() || c.security.Protected {
405410 // Mapped before (a re-map keeps its place), animations off or a
406411 // session lock: nothing to animate.
407412 c.placement.place(c, v)
413+ c.initialFullscreen(v)
408414 return
409415 }
410416 shots := c.snapshot(now)
@@ -416,10 +422,23 @@ func (c *Core) mapWindow(v ports.WindowMapped) {
416422 }
417423 }
418424 c.placement.place(c, v)
425+ c.initialFullscreen(v)
419426 c.transition(shots, now)
420427 c.appearMapped(v.ID, now)
421428 }
422429
430+// initialFullscreen makes a window that mapped fullscreen cover its screen.
431+// The fullscreen grace does not apply: it is the state the client opened
432+// with, not a later request. A re-map (screen already known) is left alone.
433+func (c *Core) initialFullscreen(v ports.WindowMapped) {
434+ if !v.Fullscreen || v.Remap {
435+ return
436+ }
437+ if s, _ := c.screenOf(v.ID); s != nil {
438+ s.mon.SetFullscreen(v.ID, true)
439+ }
440+}
441+
423442 // appearMapped starts the entrance of a window that just mapped when it
424443 // shows on its screen's current workspace as a tile or an ordinary float:
425444 // not with a preview open or a session lock, not fullscreen (it keeps
@@ -400,11 +400,17 @@ func (c *Core) appearFrom(rm *rectMotion, p Placement, old *rectMotion, now time
400 // client is configured once, to its final size.400 // client is configured once, to its final size.
401 func (c *Core) mapWindow(v ports.WindowMapped) {401 func (c *Core) mapWindow(v ports.WindowMapped) {
402 now := c.now()402 now := c.now()
403+ if v.Fullscreen && v.Parent == 0 {
404+ // Mapped fullscreen: a tile of its own, whatever size class the
405+ // client reported (a fixed-size game is not a float).
406+ v.Floating, v.Width, v.Height = false, 0, 0
407+ }
403 c.windows.mapped(v, now)408 c.windows.mapped(v, now)
404 if s, _ := c.screenOf(v.ID); s != nil || !c.animOn() || c.security.Protected {409 if s, _ := c.screenOf(v.ID); s != nil || !c.animOn() || c.security.Protected {
405 // Mapped before (a re-map keeps its place), animations off or a410 // Mapped before (a re-map keeps its place), animations off or a
406 // session lock: nothing to animate.411 // session lock: nothing to animate.
407 c.placement.place(c, v)412 c.placement.place(c, v)
413+ c.initialFullscreen(v)
408 return414 return
409 }415 }
410 shots := c.snapshot(now)416 shots := c.snapshot(now)
@@ -416,10 +422,23 @@ func (c *Core) mapWindow(v ports.WindowMapped) {
416 }422 }
417 }423 }
418 c.placement.place(c, v)424 c.placement.place(c, v)
425+ c.initialFullscreen(v)
419 c.transition(shots, now)426 c.transition(shots, now)
420 c.appearMapped(v.ID, now)427 c.appearMapped(v.ID, now)
421 }428 }
422 429
430+// initialFullscreen makes a window that mapped fullscreen cover its screen.
431+// The fullscreen grace does not apply: it is the state the client opened
432+// with, not a later request. A re-map (screen already known) is left alone.
433+func (c *Core) initialFullscreen(v ports.WindowMapped) {
434+ if !v.Fullscreen || v.Remap {
435+ return
436+ }
437+ if s, _ := c.screenOf(v.ID); s != nil {
438+ s.mon.SetFullscreen(v.ID, true)
439+ }
440+}
441+
423 // appearMapped starts the entrance of a window that just mapped when it442 // appearMapped starts the entrance of a window that just mapped when it
424 // shows on its screen's current workspace as a tile or an ordinary float:443 // shows on its screen's current workspace as a tile or an ordinary float:
425 // not with a preview open or a session lock, not fullscreen (it keeps444 // not with a preview open or a session lock, not fullscreen (it keeps
M internal/ports/client_events.go +4 -0
@@ -29,6 +29,10 @@ type WindowMapped struct {
2929 // Modal is set for a dialog that blocks its parent (xdg_dialog_v1):
3030 // focusing the parent focuses the dialog.
3131 Modal bool
32+ // Fullscreen is set when the client asked for fullscreen before mapping
33+ // (X11 _NET_WM_STATE_FULLSCREEN): core maps it as a tile covering its
34+ // screen, without the fullscreen grace of later requests.
35+ Fullscreen bool
3236 // HasAt asks for the screen containing the layout point (AtX, AtY) when
3337 // no rule sets a monitor: the X11 host sets it for a window that maps at
3438 // exactly one output's rect.
@@ -29,6 +29,10 @@ type WindowMapped struct {
29 // Modal is set for a dialog that blocks its parent (xdg_dialog_v1):29 // Modal is set for a dialog that blocks its parent (xdg_dialog_v1):
30 // focusing the parent focuses the dialog.30 // focusing the parent focuses the dialog.
31 Modal bool31 Modal bool
32+ // Fullscreen is set when the client asked for fullscreen before mapping
33+ // (X11 _NET_WM_STATE_FULLSCREEN): core maps it as a tile covering its
34+ // screen, without the fullscreen grace of later requests.
35+ Fullscreen bool
32 // HasAt asks for the screen containing the layout point (AtX, AtY) when36 // HasAt asks for the screen containing the layout point (AtX, AtY) when
33 // no rule sets a monitor: the X11 host sets it for a window that maps at37 // no rule sets a monitor: the X11 host sets it for a window that maps at
34 // exactly one output's rect.38 // exactly one output's rect.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close