detached 4b5275b

merge: arrange layer surfaces

commit
4b5275b99af3003ff23fe3750226065a95f14f14
author
brice · · 2026-09-23 16:08 UTC
merge of
e35468a + 567a285 · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +185 -2
M internal/core/core.go +19 -2
@@ -40,6 +40,9 @@ type Core struct {
4040 buttons map[uint32]bool
4141 cursorX, cursorY float64
4242 seq uint64
43+ layers []ports.LayerSurface
44+ placed []ports.SceneLayer
45+ layerChanged bool
4346 }
4447
4548 func keyName(s string) string {
@@ -144,7 +147,7 @@ func (c *Core) command(ctx context.Context, v ports.ClientCommand) error {
144147 }
145148 func (c *Core) publish(ctx context.Context) error {
146149 c.seq++
147- scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Windows: make([]ports.SceneWindow, 0)}
150+ scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), c.placed...)}
148151 alive := map[WindowID]bool{}
149152 focus, _ := c.ws.Focused()
150153 for _, p := range c.ws.Layout() {
@@ -205,6 +208,12 @@ func (c *Core) Run(ctx context.Context) error {
205208 continue
206209 }
207210 switch v := ev.(type) {
211+ case ports.LayerChanged:
212+ c.layers = append([]ports.LayerSurface(nil), v.Layers...)
213+ c.layerChanged = true
214+ var usable ports.Rect
215+ c.placed, usable = arrangeLayers(c.ws.Output.W, c.ws.Output.H, c.layers)
216+ c.ws.SetUsable(usable)
208217 case ports.WindowMapped:
209218 c.ws.AddWindow(v.ID)
210219 case ports.WindowUnmapped:
@@ -229,8 +238,16 @@ func (c *Core) Run(ctx context.Context) error {
229238 switch v := ev.(type) {
230239 case ports.OutputMode:
231240 c.ws.SetOutput(v.Width, v.Height)
241+ if c.layerChanged {
242+ var usable ports.Rect
243+ c.placed, usable = arrangeLayers(c.ws.Output.W, c.ws.Output.H, c.layers)
244+ c.ws.SetUsable(usable)
245+ }
232246 case ports.OutputUsable:
233- c.ws.SetUsable(v.Rect)
247+ // Once layer state arrives, its exclusive zones take precedence.
248+ if !c.layerChanged {
249+ c.ws.SetUsable(v.Rect)
250+ }
234251 }
235252 if err := c.publish(ctx); err != nil {
236253 return nil
@@ -40,6 +40,9 @@ type Core struct {
40 buttons map[uint32]bool40 buttons map[uint32]bool
41 cursorX, cursorY float6441 cursorX, cursorY float64
42 seq uint6442 seq uint64
43+ layers []ports.LayerSurface
44+ placed []ports.SceneLayer
45+ layerChanged bool
43 }46 }
44 47
45 func keyName(s string) string {48 func keyName(s string) string {
@@ -144,7 +147,7 @@ func (c *Core) command(ctx context.Context, v ports.ClientCommand) error {
144 }147 }
145 func (c *Core) publish(ctx context.Context) error {148 func (c *Core) publish(ctx context.Context) error {
146 c.seq++149 c.seq++
147- scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Windows: make([]ports.SceneWindow, 0)}150+ scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), c.placed...)}
148 alive := map[WindowID]bool{}151 alive := map[WindowID]bool{}
149 focus, _ := c.ws.Focused()152 focus, _ := c.ws.Focused()
150 for _, p := range c.ws.Layout() {153 for _, p := range c.ws.Layout() {
@@ -205,6 +208,12 @@ func (c *Core) Run(ctx context.Context) error {
205 continue208 continue
206 }209 }
207 switch v := ev.(type) {210 switch v := ev.(type) {
211+ case ports.LayerChanged:
212+ c.layers = append([]ports.LayerSurface(nil), v.Layers...)
213+ c.layerChanged = true
214+ var usable ports.Rect
215+ c.placed, usable = arrangeLayers(c.ws.Output.W, c.ws.Output.H, c.layers)
216+ c.ws.SetUsable(usable)
208 case ports.WindowMapped:217 case ports.WindowMapped:
209 c.ws.AddWindow(v.ID)218 c.ws.AddWindow(v.ID)
210 case ports.WindowUnmapped:219 case ports.WindowUnmapped:
@@ -229,8 +238,16 @@ func (c *Core) Run(ctx context.Context) error {
229 switch v := ev.(type) {238 switch v := ev.(type) {
230 case ports.OutputMode:239 case ports.OutputMode:
231 c.ws.SetOutput(v.Width, v.Height)240 c.ws.SetOutput(v.Width, v.Height)
241+ if c.layerChanged {
242+ var usable ports.Rect
243+ c.placed, usable = arrangeLayers(c.ws.Output.W, c.ws.Output.H, c.layers)
244+ c.ws.SetUsable(usable)
245+ }
232 case ports.OutputUsable:246 case ports.OutputUsable:
233- c.ws.SetUsable(v.Rect)247+ // Once layer state arrives, its exclusive zones take precedence.
248+ if !c.layerChanged {
249+ c.ws.SetUsable(v.Rect)
250+ }
234 }251 }
235 if err := c.publish(ctx); err != nil {252 if err := c.publish(ctx); err != nil {
236 return nil253 return nil
A internal/core/layers.go +60 -0
@@ -0,0 +1,60 @@
1+package core
2+
3+import (
4+ "sort"
5+
6+ "github.com/bnema/nefertty/internal/ports"
7+)
8+
9+// arrangeLayers places exclusive surfaces first, then sorts the resulting scene by layer.
10+func arrangeLayers(outW, outH int, layers []ports.LayerSurface) (placed []ports.SceneLayer, usable ports.Rect) {
11+ full := ports.Rect{W: max(outW, 0), H: max(outH, 0)}
12+ usable = full
13+ for pass := 0; pass < 2; pass++ {
14+ for _, s := range layers {
15+ if (s.ExclusiveZone > 0) != (pass == 0) {
16+ continue
17+ }
18+ bounds := usable
19+ if s.ExclusiveZone == -1 {
20+ bounds = full
21+ }
22+ top, right, bottom, left := int(s.Margin[0]), int(s.Margin[1]), int(s.Margin[2]), int(s.Margin[3])
23+ x, y := bounds.X+(bounds.W-s.Width)/2, bounds.Y+(bounds.H-s.Height)/2
24+ if s.Anchor&ports.AnchorLeft != 0 {
25+ x = bounds.X + left
26+ } else if s.Anchor&ports.AnchorRight != 0 {
27+ x = bounds.X + bounds.W - s.Width - right
28+ }
29+ if s.Anchor&ports.AnchorTop != 0 {
30+ y = bounds.Y + top
31+ } else if s.Anchor&ports.AnchorBottom != 0 {
32+ y = bounds.Y + bounds.H - s.Height - bottom
33+ }
34+ placed = append(placed, ports.SceneLayer{ID: s.ID, Layer: s.Layer, Rect: ports.Rect{X: x, Y: y, W: s.Width, H: s.Height}})
35+ if s.ExclusiveZone <= 0 {
36+ continue
37+ }
38+ a := s.Anchor
39+ z := int(s.ExclusiveZone)
40+ switch {
41+ case a&ports.AnchorTop != 0 && a&ports.AnchorBottom == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight):
42+ z = min(max(z+top, 0), usable.H)
43+ usable.Y += z
44+ usable.H -= z
45+ case a&ports.AnchorBottom != 0 && a&ports.AnchorTop == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight):
46+ z = min(max(z+bottom, 0), usable.H)
47+ usable.H -= z
48+ case a&ports.AnchorLeft != 0 && a&ports.AnchorRight == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom):
49+ z = min(max(z+left, 0), usable.W)
50+ usable.X += z
51+ usable.W -= z
52+ case a&ports.AnchorRight != 0 && a&ports.AnchorLeft == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom):
53+ z = min(max(z+right, 0), usable.W)
54+ usable.W -= z
55+ }
56+ }
57+ }
58+ sort.SliceStable(placed, func(i, j int) bool { return placed[i].Layer < placed[j].Layer })
59+ return
60+}
@@ -0,0 +1,60 @@
1+package core
2+
3+import (
4+ "sort"
5+
6+ "github.com/bnema/nefertty/internal/ports"
7+)
8+
9+// arrangeLayers places exclusive surfaces first, then sorts the resulting scene by layer.
10+func arrangeLayers(outW, outH int, layers []ports.LayerSurface) (placed []ports.SceneLayer, usable ports.Rect) {
11+ full := ports.Rect{W: max(outW, 0), H: max(outH, 0)}
12+ usable = full
13+ for pass := 0; pass < 2; pass++ {
14+ for _, s := range layers {
15+ if (s.ExclusiveZone > 0) != (pass == 0) {
16+ continue
17+ }
18+ bounds := usable
19+ if s.ExclusiveZone == -1 {
20+ bounds = full
21+ }
22+ top, right, bottom, left := int(s.Margin[0]), int(s.Margin[1]), int(s.Margin[2]), int(s.Margin[3])
23+ x, y := bounds.X+(bounds.W-s.Width)/2, bounds.Y+(bounds.H-s.Height)/2
24+ if s.Anchor&ports.AnchorLeft != 0 {
25+ x = bounds.X + left
26+ } else if s.Anchor&ports.AnchorRight != 0 {
27+ x = bounds.X + bounds.W - s.Width - right
28+ }
29+ if s.Anchor&ports.AnchorTop != 0 {
30+ y = bounds.Y + top
31+ } else if s.Anchor&ports.AnchorBottom != 0 {
32+ y = bounds.Y + bounds.H - s.Height - bottom
33+ }
34+ placed = append(placed, ports.SceneLayer{ID: s.ID, Layer: s.Layer, Rect: ports.Rect{X: x, Y: y, W: s.Width, H: s.Height}})
35+ if s.ExclusiveZone <= 0 {
36+ continue
37+ }
38+ a := s.Anchor
39+ z := int(s.ExclusiveZone)
40+ switch {
41+ case a&ports.AnchorTop != 0 && a&ports.AnchorBottom == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight):
42+ z = min(max(z+top, 0), usable.H)
43+ usable.Y += z
44+ usable.H -= z
45+ case a&ports.AnchorBottom != 0 && a&ports.AnchorTop == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight):
46+ z = min(max(z+bottom, 0), usable.H)
47+ usable.H -= z
48+ case a&ports.AnchorLeft != 0 && a&ports.AnchorRight == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom):
49+ z = min(max(z+left, 0), usable.W)
50+ usable.X += z
51+ usable.W -= z
52+ case a&ports.AnchorRight != 0 && a&ports.AnchorLeft == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom):
53+ z = min(max(z+right, 0), usable.W)
54+ usable.W -= z
55+ }
56+ }
57+ }
58+ sort.SliceStable(placed, func(i, j int) bool { return placed[i].Layer < placed[j].Layer })
59+ return
60+}
A internal/core/layers_test.go +106 -0
@@ -0,0 +1,106 @@
1+package core
2+
3+import (
4+ "context"
5+ "testing"
6+ "time"
7+
8+ "github.com/bnema/nefertty/internal/ports"
9+)
10+
11+func TestArrangeLayers(t *testing.T) {
12+ bar := ports.LayerSurface{ID: 1, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30}
13+ tests := []struct {
14+ name string
15+ layers []ports.LayerSurface
16+ rects []ports.Rect
17+ usable ports.Rect
18+ }{
19+ {"top", []ports.LayerSurface{bar}, []ports.Rect{{W: 1920, H: 30}}, ports.Rect{Y: 30, W: 1920, H: 1050}},
20+ {"bottom margin", []ports.LayerSurface{{Anchor: ports.AnchorBottom, Width: 100, Height: 20, ExclusiveZone: 20, Margin: [4]int32{0, 0, 5, 0}}}, []ports.Rect{{X: 910, Y: 1055, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1055}},
21+ {"left dock", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorTop | ports.AnchorBottom, Width: 40, Height: 1080, ExclusiveZone: 40}}, []ports.Rect{{W: 40, H: 1080}}, ports.Rect{X: 40, W: 1880, H: 1080}},
22+ {"center", []ports.LayerSurface{{Width: 100, Height: 40, Layer: ports.LayerOverlay}}, []ports.Rect{{X: 910, Y: 520, W: 100, H: 40}}, ports.Rect{W: 1920, H: 1080}},
23+ {"minus one", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: ports.AnchorTop, Width: 100, Height: 20, ExclusiveZone: -1}}, []ports.Rect{{W: 1920, H: 30}, {X: 910, W: 100, H: 20}}, ports.Rect{Y: 30, W: 1920, H: 1050}},
24+ {"stack", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: bar.Anchor, Width: 1920, Height: 20, ExclusiveZone: 20}}, []ports.Rect{{W: 1920, H: 30}, {Y: 30, W: 1920, H: 20}}, ports.Rect{Y: 50, W: 1920, H: 1030}},
25+ }
26+ for _, tt := range tests {
27+ t.Run(tt.name, func(t *testing.T) {
28+ got, u := arrangeLayers(1920, 1080, tt.layers)
29+ if u != tt.usable || len(got) != len(tt.rects) {
30+ t.Fatalf("got %v %v", got, u)
31+ }
32+ for i, r := range tt.rects {
33+ if got[i].Rect != r {
34+ t.Fatalf("rect %d: %v want %v", i, got[i].Rect, r)
35+ }
36+ }
37+ })
38+ }
39+ layers := []ports.LayerSurface{{ID: 1, Layer: ports.LayerOverlay}, {ID: 2, Layer: ports.LayerBottom}, {ID: 3, Layer: ports.LayerBackground}, {ID: 4, Layer: ports.LayerTop}, {ID: 5, Layer: ports.LayerBottom}}
40+ got, _ := arrangeLayers(100, 100, layers)
41+ for i, id := range []ports.WindowID{3, 2, 5, 4, 1} {
42+ if got[i].ID != id {
43+ t.Fatalf("order: %v", got)
44+ }
45+ }
46+}
47+
48+func TestLayerChangedChannels(t *testing.T) {
49+ client := make(chan ports.ClientEvent)
50+ output := make(chan ports.OutputEvent)
51+ commands := make(chan ports.ClientCommand, 16)
52+ scenes := make(chan ports.Scene, 1)
53+ cfg := ports.Config{}
54+ cfg.Keyboard.CmdKey = "super"
55+ cfg.Layout.DefaultColumnWidth = "1/2"
56+ c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes})
57+ if err != nil {
58+ t.Fatal(err)
59+ }
60+ ctx, cancel := context.WithCancel(context.Background())
61+ defer cancel()
62+ done := make(chan struct{})
63+ go func() { defer close(done); _ = c.Run(ctx) }()
64+ recv := func() ports.Scene {
65+ t.Helper()
66+ select {
67+ case s := <-scenes:
68+ return s
69+ case <-time.After(time.Second):
70+ t.Fatal("scene timeout")
71+ return ports.Scene{}
72+ }
73+ }
74+ output <- ports.OutputMode{Width: 1920, Height: 1080}
75+ recv()
76+ client <- ports.WindowMapped{ID: 1}
77+ before := recv()
78+ for len(commands) > 0 {
79+ <-commands
80+ }
81+ bar := ports.LayerSurface{ID: 2, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30}
82+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
83+ scene := recv()
84+ if len(scene.Layers) != 1 || scene.Layers[0].Rect != (ports.Rect{W: 1920, H: 30}) || len(scene.Windows) != 1 || scene.Windows[0].Rect.Y < 30 {
85+ t.Fatalf("scene: %v", scene)
86+ }
87+ if scene.Windows[0].Rect.H != before.Windows[0].Rect.H-30 {
88+ t.Fatalf("height: %v vs %v", scene.Windows, before.Windows)
89+ }
90+ select {
91+ case v := <-commands:
92+ cfg, ok := v.(ports.ConfigureWindow)
93+ if !ok || cfg.ID != 1 || cfg.Height != scene.Windows[0].Rect.H {
94+ t.Fatalf("configure: %v", v)
95+ }
96+ case <-time.After(time.Second):
97+ t.Fatal("configure timeout")
98+ }
99+ scene.Layers[0].Rect.Y = 99
100+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
101+ if next := recv(); next.Layers[0].Rect.Y != 0 {
102+ t.Fatalf("scene alias: %v", next)
103+ }
104+ cancel()
105+ <-done
106+}
@@ -0,0 +1,106 @@
1+package core
2+
3+import (
4+ "context"
5+ "testing"
6+ "time"
7+
8+ "github.com/bnema/nefertty/internal/ports"
9+)
10+
11+func TestArrangeLayers(t *testing.T) {
12+ bar := ports.LayerSurface{ID: 1, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30}
13+ tests := []struct {
14+ name string
15+ layers []ports.LayerSurface
16+ rects []ports.Rect
17+ usable ports.Rect
18+ }{
19+ {"top", []ports.LayerSurface{bar}, []ports.Rect{{W: 1920, H: 30}}, ports.Rect{Y: 30, W: 1920, H: 1050}},
20+ {"bottom margin", []ports.LayerSurface{{Anchor: ports.AnchorBottom, Width: 100, Height: 20, ExclusiveZone: 20, Margin: [4]int32{0, 0, 5, 0}}}, []ports.Rect{{X: 910, Y: 1055, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1055}},
21+ {"left dock", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorTop | ports.AnchorBottom, Width: 40, Height: 1080, ExclusiveZone: 40}}, []ports.Rect{{W: 40, H: 1080}}, ports.Rect{X: 40, W: 1880, H: 1080}},
22+ {"center", []ports.LayerSurface{{Width: 100, Height: 40, Layer: ports.LayerOverlay}}, []ports.Rect{{X: 910, Y: 520, W: 100, H: 40}}, ports.Rect{W: 1920, H: 1080}},
23+ {"minus one", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: ports.AnchorTop, Width: 100, Height: 20, ExclusiveZone: -1}}, []ports.Rect{{W: 1920, H: 30}, {X: 910, W: 100, H: 20}}, ports.Rect{Y: 30, W: 1920, H: 1050}},
24+ {"stack", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: bar.Anchor, Width: 1920, Height: 20, ExclusiveZone: 20}}, []ports.Rect{{W: 1920, H: 30}, {Y: 30, W: 1920, H: 20}}, ports.Rect{Y: 50, W: 1920, H: 1030}},
25+ }
26+ for _, tt := range tests {
27+ t.Run(tt.name, func(t *testing.T) {
28+ got, u := arrangeLayers(1920, 1080, tt.layers)
29+ if u != tt.usable || len(got) != len(tt.rects) {
30+ t.Fatalf("got %v %v", got, u)
31+ }
32+ for i, r := range tt.rects {
33+ if got[i].Rect != r {
34+ t.Fatalf("rect %d: %v want %v", i, got[i].Rect, r)
35+ }
36+ }
37+ })
38+ }
39+ layers := []ports.LayerSurface{{ID: 1, Layer: ports.LayerOverlay}, {ID: 2, Layer: ports.LayerBottom}, {ID: 3, Layer: ports.LayerBackground}, {ID: 4, Layer: ports.LayerTop}, {ID: 5, Layer: ports.LayerBottom}}
40+ got, _ := arrangeLayers(100, 100, layers)
41+ for i, id := range []ports.WindowID{3, 2, 5, 4, 1} {
42+ if got[i].ID != id {
43+ t.Fatalf("order: %v", got)
44+ }
45+ }
46+}
47+
48+func TestLayerChangedChannels(t *testing.T) {
49+ client := make(chan ports.ClientEvent)
50+ output := make(chan ports.OutputEvent)
51+ commands := make(chan ports.ClientCommand, 16)
52+ scenes := make(chan ports.Scene, 1)
53+ cfg := ports.Config{}
54+ cfg.Keyboard.CmdKey = "super"
55+ cfg.Layout.DefaultColumnWidth = "1/2"
56+ c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes})
57+ if err != nil {
58+ t.Fatal(err)
59+ }
60+ ctx, cancel := context.WithCancel(context.Background())
61+ defer cancel()
62+ done := make(chan struct{})
63+ go func() { defer close(done); _ = c.Run(ctx) }()
64+ recv := func() ports.Scene {
65+ t.Helper()
66+ select {
67+ case s := <-scenes:
68+ return s
69+ case <-time.After(time.Second):
70+ t.Fatal("scene timeout")
71+ return ports.Scene{}
72+ }
73+ }
74+ output <- ports.OutputMode{Width: 1920, Height: 1080}
75+ recv()
76+ client <- ports.WindowMapped{ID: 1}
77+ before := recv()
78+ for len(commands) > 0 {
79+ <-commands
80+ }
81+ bar := ports.LayerSurface{ID: 2, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30}
82+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
83+ scene := recv()
84+ if len(scene.Layers) != 1 || scene.Layers[0].Rect != (ports.Rect{W: 1920, H: 30}) || len(scene.Windows) != 1 || scene.Windows[0].Rect.Y < 30 {
85+ t.Fatalf("scene: %v", scene)
86+ }
87+ if scene.Windows[0].Rect.H != before.Windows[0].Rect.H-30 {
88+ t.Fatalf("height: %v vs %v", scene.Windows, before.Windows)
89+ }
90+ select {
91+ case v := <-commands:
92+ cfg, ok := v.(ports.ConfigureWindow)
93+ if !ok || cfg.ID != 1 || cfg.Height != scene.Windows[0].Rect.H {
94+ t.Fatalf("configure: %v", v)
95+ }
96+ case <-time.After(time.Second):
97+ t.Fatal("configure timeout")
98+ }
99+ scene.Layers[0].Rect.Y = 99
100+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
101+ if next := recv(); next.Layers[0].Rect.Y != 0 {
102+ t.Fatalf("scene alias: %v", next)
103+ }
104+ cancel()
105+ <-done
106+}

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 23

Tags 10

↑↓ moveenter openesc close