detached 7646086

Merge branch 'feat/x6-place' into feat/xwayland-builtin

commit
76460863eaaddbfba6b4a15cf3cdb3f3d2d29258
author
brice · · 2026-10-22 10:59 UTC
committer
brice · · 2026-10-22 17:54 UTC
merge of
aa24d35 + cac0ab6 · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +116 -4
M internal/adapters/wayland/server.go +2 -0
@@ -780,6 +780,8 @@ func (s *Server) apply(cmd ports.ClientCommand) {
780780 if w := s.windows[c.ID]; w != nil && w.popup != nil {
781781 w.popup.dismiss()
782782 }
783+ case ports.PlaceWindow:
784+ // Consumed by the X11 host.
783785 case ports.ConfigureWindow:
784786 w := s.windows[c.ID]
785787 if w == nil || !w.toplevelAlive() || !w.alive() {
@@ -780,6 +780,8 @@ func (s *Server) apply(cmd ports.ClientCommand) {
780 if w := s.windows[c.ID]; w != nil && w.popup != nil {780 if w := s.windows[c.ID]; w != nil && w.popup != nil {
781 w.popup.dismiss()781 w.popup.dismiss()
782 }782 }
783+ case ports.PlaceWindow:
784+ // Consumed by the X11 host.
783 case ports.ConfigureWindow:785 case ports.ConfigureWindow:
784 w := s.windows[c.ID]786 w := s.windows[c.ID]
785 if w == nil || !w.toplevelAlive() || !w.alive() {787 if w == nil || !w.toplevelAlive() || !w.alive() {
M internal/core/core.go +6 -4
@@ -103,9 +103,11 @@ type Core struct {
103103 binds map[binding]Action
104104 pressed map[string]bool
105105 configures configures
106- pointer WindowID
107- grab WindowID
108- buttons map[uint32]bool
106+ // placed is the last position (X, Y of the Rect) sent in a PlaceWindow, per window.
107+ placed map[WindowID]Rect
108+ pointer WindowID
109+ grab WindowID
110+ buttons map[uint32]bool
109111 // drag is the pointer drag in progress (drag.go); swallow holds the
110112 // buttons whose release no client must see. mods are the modifiers
111113 // held, cmdMod the one keyboard.cmd names. lastButton is the latest
@@ -417,7 +419,7 @@ func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) {
417419 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")
418420 }
419421 // A placeholder screen holds windows until the first output arrives.
420- c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures(), ping: make(chan chan struct{}), out: newOutbox()}
422+ c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures(), placed: map[WindowID]Rect{}, ping: make(chan chan struct{}), out: newOutbox()}
421423 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}
422424 if err := c.apply(cfg); err != nil {
423425 return nil, err
@@ -103,9 +103,11 @@ type Core struct {
103 binds map[binding]Action103 binds map[binding]Action
104 pressed map[string]bool104 pressed map[string]bool
105 configures configures105 configures configures
106- pointer WindowID106+ // placed is the last position (X, Y of the Rect) sent in a PlaceWindow, per window.
107- grab WindowID107+ placed map[WindowID]Rect
108- buttons map[uint32]bool108+ pointer WindowID
109+ grab WindowID
110+ buttons map[uint32]bool
109 // drag is the pointer drag in progress (drag.go); swallow holds the111 // drag is the pointer drag in progress (drag.go); swallow holds the
110 // buttons whose release no client must see. mods are the modifiers112 // buttons whose release no client must see. mods are the modifiers
111 // held, cmdMod the one keyboard.cmd names. lastButton is the latest113 // held, cmdMod the one keyboard.cmd names. lastButton is the latest
@@ -417,7 +419,7 @@ func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) {
417 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")419 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")
418 }420 }
419 // A placeholder screen holds windows until the first output arrives.421 // A placeholder screen holds windows until the first output arrives.
420- c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures(), ping: make(chan chan struct{}), out: newOutbox()}422+ c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures(), placed: map[WindowID]Rect{}, ping: make(chan chan struct{}), out: newOutbox()}
421 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}423 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}
422 if err := c.apply(cfg); err != nil {424 if err := c.apply(cfg); err != nil {
423 return nil, err425 return nil, err
M internal/core/core_test.go +4 -0
@@ -38,6 +38,10 @@ func command(t *testing.T, ch <-chan ports.ClientCommand) ports.ClientCommand {
3838 for {
3939 switch v := receive(t, ch); v.(type) {
4040 case nil, ports.SetOutputs, ports.UserActivity:
41+ case ports.PlaceWindow:
42+ // Only Positioned (X11) windows are placed; every scenario
43+ // using this helper maps xdg windows.
44+ t.Fatalf("unexpected %+v", v)
4145 default:
4246 return v
4347 }
@@ -38,6 +38,10 @@ func command(t *testing.T, ch <-chan ports.ClientCommand) ports.ClientCommand {
38 for {38 for {
39 switch v := receive(t, ch); v.(type) {39 switch v := receive(t, ch); v.(type) {
40 case nil, ports.SetOutputs, ports.UserActivity:40 case nil, ports.SetOutputs, ports.UserActivity:
41+ case ports.PlaceWindow:
42+ // Only Positioned (X11) windows are placed; every scenario
43+ // using this helper maps xdg windows.
44+ t.Fatalf("unexpected %+v", v)
41 default:45 default:
42 return v46 return v
43 }47 }
A internal/core/place_test.go +80 -0
@@ -0,0 +1,80 @@
1+package core_test
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/core"
8+ "github.com/bnema/neferwl/internal/ports"
9+)
10+
11+// places collects the PlaceWindow commands queued so far.
12+func places(commands chan ports.ClientCommand) (out []ports.PlaceWindow) {
13+ for len(commands) > 0 {
14+ if v, ok := (<-commands).(ports.PlaceWindow); ok {
15+ out = append(out, v)
16+ }
17+ }
18+ return out
19+}
20+
21+func TestPlaceWindow(t *testing.T) {
22+ cfg := altCmdDefaults()
23+ cfg.Border.Width = 0
24+ cfg.Layout.Gaps = 8
25+ client := make(chan ports.ClientEvent, 16)
26+ input := make(chan ports.InputEvent, 16)
27+ output := make(chan ports.OutputEvent, 8)
28+ commands := make(chan ports.ClientCommand, 256)
29+ scenes := make(chan []ports.Scene, 1)
30+ c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
31+ if err != nil {
32+ t.Fatal(err)
33+ }
34+ ctx, cancel := context.WithCancel(context.Background())
35+ defer cancel()
36+ go func() { _ = c.Run(ctx) }()
37+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
38+ scene(t, scenes)
39+
40+ // An xdg window is never placed.
41+ client <- ports.WindowMapped{ID: 1}
42+ scene(t, scenes)
43+ if got := places(commands); len(got) != 0 {
44+ t.Fatal(got)
45+ }
46+
47+ // A positioned window is placed once on map.
48+ client <- ports.WindowMapped{ID: 2, Positioned: true}
49+ scene(t, scenes)
50+ first := places(commands)
51+ if len(first) != 1 || first[0].ID != 2 {
52+ t.Fatal(first)
53+ }
54+
55+ // The same layout sends nothing more.
56+ client <- ports.WindowMapped{ID: 1}
57+ scene(t, scenes)
58+ if got := places(commands); len(got) != 0 {
59+ t.Fatal(got)
60+ }
61+
62+ // A position change sends it again, for the positioned window only.
63+ client <- ports.WindowMapped{ID: 3}
64+ scene(t, scenes)
65+ moved := places(commands)
66+ if len(moved) != 1 || moved[0].ID != 2 || (moved[0].X == first[0].X && moved[0].Y == first[0].Y) {
67+ t.Fatal(first, moved)
68+ }
69+
70+ // Unmap prunes it: mapping again places from nothing.
71+ client <- ports.WindowUnmapped{ID: 2}
72+ scene(t, scenes)
73+ places(commands)
74+ client <- ports.WindowMapped{ID: 2, Positioned: true}
75+ scene(t, scenes)
76+ again := places(commands)
77+ if len(again) != 1 || again[0].ID != 2 {
78+ t.Fatal(again)
79+ }
80+}
@@ -0,0 +1,80 @@
1+package core_test
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/core"
8+ "github.com/bnema/neferwl/internal/ports"
9+)
10+
11+// places collects the PlaceWindow commands queued so far.
12+func places(commands chan ports.ClientCommand) (out []ports.PlaceWindow) {
13+ for len(commands) > 0 {
14+ if v, ok := (<-commands).(ports.PlaceWindow); ok {
15+ out = append(out, v)
16+ }
17+ }
18+ return out
19+}
20+
21+func TestPlaceWindow(t *testing.T) {
22+ cfg := altCmdDefaults()
23+ cfg.Border.Width = 0
24+ cfg.Layout.Gaps = 8
25+ client := make(chan ports.ClientEvent, 16)
26+ input := make(chan ports.InputEvent, 16)
27+ output := make(chan ports.OutputEvent, 8)
28+ commands := make(chan ports.ClientCommand, 256)
29+ scenes := make(chan []ports.Scene, 1)
30+ c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
31+ if err != nil {
32+ t.Fatal(err)
33+ }
34+ ctx, cancel := context.WithCancel(context.Background())
35+ defer cancel()
36+ go func() { _ = c.Run(ctx) }()
37+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
38+ scene(t, scenes)
39+
40+ // An xdg window is never placed.
41+ client <- ports.WindowMapped{ID: 1}
42+ scene(t, scenes)
43+ if got := places(commands); len(got) != 0 {
44+ t.Fatal(got)
45+ }
46+
47+ // A positioned window is placed once on map.
48+ client <- ports.WindowMapped{ID: 2, Positioned: true}
49+ scene(t, scenes)
50+ first := places(commands)
51+ if len(first) != 1 || first[0].ID != 2 {
52+ t.Fatal(first)
53+ }
54+
55+ // The same layout sends nothing more.
56+ client <- ports.WindowMapped{ID: 1}
57+ scene(t, scenes)
58+ if got := places(commands); len(got) != 0 {
59+ t.Fatal(got)
60+ }
61+
62+ // A position change sends it again, for the positioned window only.
63+ client <- ports.WindowMapped{ID: 3}
64+ scene(t, scenes)
65+ moved := places(commands)
66+ if len(moved) != 1 || moved[0].ID != 2 || (moved[0].X == first[0].X && moved[0].Y == first[0].Y) {
67+ t.Fatal(first, moved)
68+ }
69+
70+ // Unmap prunes it: mapping again places from nothing.
71+ client <- ports.WindowUnmapped{ID: 2}
72+ scene(t, scenes)
73+ places(commands)
74+ client <- ports.WindowMapped{ID: 2, Positioned: true}
75+ scene(t, scenes)
76+ again := places(commands)
77+ if len(again) != 1 || again[0].ID != 2 {
78+ t.Fatal(again)
79+ }
80+}
M internal/core/scene.go +10 -0
@@ -116,6 +116,16 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
116116 }
117117 c.configures.mark(v)
118118 }
119+ if !p.Hidden && p.Preview == 0 && c.windows.lookup(p.ID).client.Positioned {
120+ cr := c.clientRect(ps)
121+ at := Rect{X: sc.x + cr.X, Y: sc.y + cr.Y}
122+ if old, ok := c.placed[p.ID]; !ok || old != at {
123+ if err := c.command(ctx, ports.PlaceWindow{ID: p.ID, X: at.X, Y: at.Y}); err != nil {
124+ return ports.Scene{}, err
125+ }
126+ c.placed[p.ID] = at
127+ }
128+ }
119129 }
120130 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
121131 if icon, ok := c.dataDragIconOn(i, sc); ok {
@@ -116,6 +116,16 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
116 }116 }
117 c.configures.mark(v)117 c.configures.mark(v)
118 }118 }
119+ if !p.Hidden && p.Preview == 0 && c.windows.lookup(p.ID).client.Positioned {
120+ cr := c.clientRect(ps)
121+ at := Rect{X: sc.x + cr.X, Y: sc.y + cr.Y}
122+ if old, ok := c.placed[p.ID]; !ok || old != at {
123+ if err := c.command(ctx, ports.PlaceWindow{ID: p.ID, X: at.X, Y: at.Y}); err != nil {
124+ return ports.Scene{}, err
125+ }
126+ c.placed[p.ID] = at
127+ }
128+ }
119 }129 }
120 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)130 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
121 if icon, ok := c.dataDragIconOn(i, sc); ok {131 if icon, ok := c.dataDragIconOn(i, sc); ok {
M internal/core/transition.go +1 -0
@@ -453,6 +453,7 @@ func (c *Core) unmapWindow(v ports.WindowUnmapped) {
453453 // The window is unmapped: a toplevel that maps again keeps its ID and
454454 // must be configured from nothing (a leaving entry keeps no configure).
455455 c.configures.forget(v.ID)
456+ delete(c.placed, v.ID)
456457 sc, w := c.screenOf(v.ID)
457458 if sc == nil {
458459 return
@@ -453,6 +453,7 @@ func (c *Core) unmapWindow(v ports.WindowUnmapped) {
453 // The window is unmapped: a toplevel that maps again keeps its ID and453 // The window is unmapped: a toplevel that maps again keeps its ID and
454 // must be configured from nothing (a leaving entry keeps no configure).454 // must be configured from nothing (a leaving entry keeps no configure).
455 c.configures.forget(v.ID)455 c.configures.forget(v.ID)
456+ delete(c.placed, v.ID)
456 sc, w := c.screenOf(v.ID)457 sc, w := c.screenOf(v.ID)
457 if sc == nil {458 if sc == nil {
458 return459 return
M internal/ports/client_commands.go +10 -0
@@ -47,6 +47,16 @@ type ConfigureWindow struct {
4747
4848 func (ConfigureWindow) clientCommand() {}
4949
50+// PlaceWindow carries core → wayland the position of a Positioned window's
51+// client rect, in layout logical coordinates (output origin included). It
52+// follows the ConfigureWindow of a frame and is sent once per change.
53+type PlaceWindow struct {
54+ ID WindowID
55+ X, Y int
56+}
57+
58+func (PlaceWindow) clientCommand() {}
59+
5060 // OutputPlacement is one output in the global layout. X, Y, Width and
5161 // Height are logical (after Transform: they swap for 90/270);
5262 // physical = logical × Scale. Info.Width and Info.Height are the physical
@@ -47,6 +47,16 @@ type ConfigureWindow struct {
47 47
48 func (ConfigureWindow) clientCommand() {}48 func (ConfigureWindow) clientCommand() {}
49 49
50+// PlaceWindow carries core → wayland the position of a Positioned window's
51+// client rect, in layout logical coordinates (output origin included). It
52+// follows the ConfigureWindow of a frame and is sent once per change.
53+type PlaceWindow struct {
54+ ID WindowID
55+ X, Y int
56+}
57+
58+func (PlaceWindow) clientCommand() {}
59+
50 // OutputPlacement is one output in the global layout. X, Y, Width and60 // OutputPlacement is one output in the global layout. X, Y, Width and
51 // Height are logical (after Transform: they swap for 90/270);61 // Height are logical (after Transform: they swap for 90/270);
52 // physical = logical × Scale. Info.Width and Info.Height are the physical62 // physical = logical × Scale. Info.Width and Info.Height are the physical
M internal/ports/client_events.go +3 -0
@@ -20,6 +20,9 @@ type WindowMapped struct {
2020 // xdg-foreign), 0 when none. A dialog of a fullscreen window shows over
2121 // it.
2222 Parent WindowID
23+ // Positioned windows are placed by core: it sends PlaceWindow with the
24+ // client rect origin. Set only by the X11 host, false for xdg.
25+ Positioned bool
2326 // Remap is set when the toplevel mapped before and was unmapped (a tray
2427 // app restored): window rules only place its first map.
2528 Remap bool
@@ -20,6 +20,9 @@ type WindowMapped struct {
20 // xdg-foreign), 0 when none. A dialog of a fullscreen window shows over20 // xdg-foreign), 0 when none. A dialog of a fullscreen window shows over
21 // it.21 // it.
22 Parent WindowID22 Parent WindowID
23+ // Positioned windows are placed by core: it sends PlaceWindow with the
24+ // client rect origin. Set only by the X11 host, false for xdg.
25+ Positioned bool
23 // Remap is set when the toplevel mapped before and was unmapped (a tray26 // Remap is set when the toplevel mapped before and was unmapped (a tray
24 // app restored): window rules only place its first map.27 // app restored): window rules only place its first map.
25 Remap bool28 Remap bool

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