detached 343f5a5

feat(core): lay out rotated outputs in their transformed size

commit
343f5a5b2f8a417040e98cb1309cbf618b65d171
author
brice · · 2026-10-03 18:40 UTC
parent
c1a5a02
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +135 -17
M internal/core/core.go +2 -2
@@ -282,7 +282,7 @@ func (c *Core) apply(cfg ports.Config) error {
282282 for _, s := range c.screens {
283283 c.settings(s.mon)
284284 }
285- c.applyConfigScales()
285+ c.applyOutputConfig()
286286 return nil
287287 }
288288
@@ -549,7 +549,7 @@ func (c *Core) publish(ctx context.Context) error {
549549 if frame != (Rect{W: o.W, H: o.H}) {
550550 clip = frame
551551 }
552- scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Off: sc.off, 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: shownLayers(sc)}
552+ scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, 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: shownLayers(sc)}
553553 layout := sc.mon.Layout()
554554 var real map[WindowID]Placement
555555 if sc.mon.ov.open {
@@ -282,7 +282,7 @@ func (c *Core) apply(cfg ports.Config) error {
282 for _, s := range c.screens {282 for _, s := range c.screens {
283 c.settings(s.mon)283 c.settings(s.mon)
284 }284 }
285- c.applyConfigScales()285+ c.applyOutputConfig()
286 return nil286 return nil
287 }287 }
288 288
@@ -549,7 +549,7 @@ func (c *Core) publish(ctx context.Context) error {
549 if frame != (Rect{W: o.W, H: o.H}) {549 if frame != (Rect{W: o.W, H: o.H}) {
550 clip = frame550 clip = frame
551 }551 }
552- scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Off: sc.off, 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: shownLayers(sc)}552+ scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, 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: shownLayers(sc)}
553 layout := sc.mon.Layout()553 layout := sc.mon.Layout()
554 var real map[WindowID]Placement554 var real map[WindowID]Placement
555 if sc.mon.ov.open {555 if sc.mon.ov.open {
M internal/core/screens.go +38 -14
@@ -13,11 +13,14 @@ type screen struct {
1313 info ports.OutputInfo
1414 mon *Monitor
1515 scale, cfgScale float64
16- primary bool
17- x, y int // logical origin in the global layout
18- off bool // turned off by a client (output power management)
19- layers []ports.LayerSurface
20- placed []ports.SceneLayer
16+ // transform is how the physical target holds the screen's logical
17+ // output (output.<name>.transform); 90/270 swap width and height.
18+ transform ports.BufferTransform
19+ primary bool
20+ x, y int // logical origin in the global layout
21+ off bool // turned off by a client (output power management)
22+ layers []ports.LayerSurface
23+ placed []ports.SceneLayer
2124 // capture holds the attached layers of the capture exclusion: shown over
2225 // a fullscreen window (capture.go).
2326 capture map[WindowID]bool
@@ -29,8 +32,14 @@ type screen struct {
2932
3033 func (s *screen) name() string { return s.info.Name }
3134
32-// modeSize is the physical mode size in the screen's logical orientation.
33-func (s *screen) modeSize() (w, h int) { return s.info.Width, s.info.Height }
35+// modeSize is the physical mode size in the screen's logical orientation:
36+// width and height swap for a 90/270 transform.
37+func (s *screen) modeSize() (w, h int) {
38+ if s.transform.Rotated() {
39+ return s.info.Height, s.info.Width
40+ }
41+ return s.info.Width, s.info.Height
42+}
3443
3544 // rect is the screen's place in the global logical layout.
3645 func (s *screen) rect() Rect {
@@ -40,7 +49,7 @@ func (s *screen) rect() Rect {
4049
4150 func (s *screen) placement() ports.OutputPlacement {
4251 r := s.rect()
43- return ports.OutputPlacement{Info: s.info, X: r.X, Y: r.Y, Width: r.W, Height: r.H, Scale: s.scale, Primary: s.primary}
52+ return ports.OutputPlacement{Info: s.info, X: r.X, Y: r.Y, Width: r.W, Height: r.H, Scale: s.scale, Transform: s.transform, Primary: s.primary}
4453 }
4554
4655 // setScale resizes the logical output; layer placement follows.
@@ -197,14 +206,27 @@ func (c *Core) configScale(name string) float64 {
197206 return SnapScale(v)
198207 }
199208
200-// applyConfigScales picks up output.<name>.scale when it changed in the
201-// config, replacing any live scale-up/scale-down adjustment.
202-func (c *Core) applyConfigScales() {
209+// configTransform is output.<name>.transform, else 0.
210+func (c *Core) configTransform(name string) ports.BufferTransform {
211+ o, _ := c.outputConfig(name)
212+ return o.Transform
213+}
214+
215+// applyOutputConfig picks up output.<name>.scale and .transform when they
216+// changed in the config. A new scale replaces any live scale-up/scale-down
217+// adjustment; a transform alone keeps it.
218+func (c *Core) applyOutputConfig() {
203219 for _, s := range c.screens {
204- if v := c.configScale(s.name()); v != s.cfgScale {
205- s.cfgScale = v
206- s.setScale(v)
220+ v, t := c.configScale(s.name()), c.configTransform(s.name())
221+ if v == s.cfgScale && t == s.transform {
222+ continue
223+ }
224+ scale := s.scale
225+ if v != s.cfgScale {
226+ scale = v
207227 }
228+ s.cfgScale, s.transform = v, t
229+ s.setScale(scale)
208230 }
209231 c.order()
210232 }
@@ -221,6 +243,7 @@ func (c *Core) addScreen(info ports.OutputInfo) {
221243 s.mon.Key = info.Key()
222244 c.settings(s.mon)
223245 }
246+ s.transform = c.configTransform(info.Name)
224247 s.setScale(s.scale)
225248 c.order()
226249 return
@@ -238,6 +261,7 @@ func (c *Core) addScreen(info ports.OutputInfo) {
238261 s.mon.Name, s.mon.Key = info.Name, info.Key()
239262 c.settings(s.mon)
240263 s.cfgScale = c.configScale(info.Name)
264+ s.transform = c.configTransform(info.Name)
241265 s.setScale(s.cfgScale)
242266 s.primary = c.isPrimary(info.Name)
243267 c.order()
@@ -13,11 +13,14 @@ type screen struct {
13 info ports.OutputInfo13 info ports.OutputInfo
14 mon *Monitor14 mon *Monitor
15 scale, cfgScale float6415 scale, cfgScale float64
16- primary bool16+ // transform is how the physical target holds the screen's logical
17- x, y int // logical origin in the global layout17+ // output (output.<name>.transform); 90/270 swap width and height.
18- off bool // turned off by a client (output power management)18+ transform ports.BufferTransform
19- layers []ports.LayerSurface19+ primary bool
20- placed []ports.SceneLayer20+ x, y int // logical origin in the global layout
21+ off bool // turned off by a client (output power management)
22+ layers []ports.LayerSurface
23+ placed []ports.SceneLayer
21 // capture holds the attached layers of the capture exclusion: shown over24 // capture holds the attached layers of the capture exclusion: shown over
22 // a fullscreen window (capture.go).25 // a fullscreen window (capture.go).
23 capture map[WindowID]bool26 capture map[WindowID]bool
@@ -29,8 +32,14 @@ type screen struct {
29 32
30 func (s *screen) name() string { return s.info.Name }33 func (s *screen) name() string { return s.info.Name }
31 34
32-// modeSize is the physical mode size in the screen's logical orientation.35+// modeSize is the physical mode size in the screen's logical orientation:
33-func (s *screen) modeSize() (w, h int) { return s.info.Width, s.info.Height }36+// width and height swap for a 90/270 transform.
37+func (s *screen) modeSize() (w, h int) {
38+ if s.transform.Rotated() {
39+ return s.info.Height, s.info.Width
40+ }
41+ return s.info.Width, s.info.Height
42+}
34 43
35 // rect is the screen's place in the global logical layout.44 // rect is the screen's place in the global logical layout.
36 func (s *screen) rect() Rect {45 func (s *screen) rect() Rect {
@@ -40,7 +49,7 @@ func (s *screen) rect() Rect {
40 49
41 func (s *screen) placement() ports.OutputPlacement {50 func (s *screen) placement() ports.OutputPlacement {
42 r := s.rect()51 r := s.rect()
43- return ports.OutputPlacement{Info: s.info, X: r.X, Y: r.Y, Width: r.W, Height: r.H, Scale: s.scale, Primary: s.primary}52+ return ports.OutputPlacement{Info: s.info, X: r.X, Y: r.Y, Width: r.W, Height: r.H, Scale: s.scale, Transform: s.transform, Primary: s.primary}
44 }53 }
45 54
46 // setScale resizes the logical output; layer placement follows.55 // setScale resizes the logical output; layer placement follows.
@@ -197,14 +206,27 @@ func (c *Core) configScale(name string) float64 {
197 return SnapScale(v)206 return SnapScale(v)
198 }207 }
199 208
200-// applyConfigScales picks up output.<name>.scale when it changed in the209+// configTransform is output.<name>.transform, else 0.
201-// config, replacing any live scale-up/scale-down adjustment.210+func (c *Core) configTransform(name string) ports.BufferTransform {
202-func (c *Core) applyConfigScales() {211+ o, _ := c.outputConfig(name)
212+ return o.Transform
213+}
214+
215+// applyOutputConfig picks up output.<name>.scale and .transform when they
216+// changed in the config. A new scale replaces any live scale-up/scale-down
217+// adjustment; a transform alone keeps it.
218+func (c *Core) applyOutputConfig() {
203 for _, s := range c.screens {219 for _, s := range c.screens {
204- if v := c.configScale(s.name()); v != s.cfgScale {220+ v, t := c.configScale(s.name()), c.configTransform(s.name())
205- s.cfgScale = v221+ if v == s.cfgScale && t == s.transform {
206- s.setScale(v)222+ continue
223+ }
224+ scale := s.scale
225+ if v != s.cfgScale {
226+ scale = v
207 }227 }
228+ s.cfgScale, s.transform = v, t
229+ s.setScale(scale)
208 }230 }
209 c.order()231 c.order()
210 }232 }
@@ -221,6 +243,7 @@ func (c *Core) addScreen(info ports.OutputInfo) {
221 s.mon.Key = info.Key()243 s.mon.Key = info.Key()
222 c.settings(s.mon)244 c.settings(s.mon)
223 }245 }
246+ s.transform = c.configTransform(info.Name)
224 s.setScale(s.scale)247 s.setScale(s.scale)
225 c.order()248 c.order()
226 return249 return
@@ -238,6 +261,7 @@ func (c *Core) addScreen(info ports.OutputInfo) {
238 s.mon.Name, s.mon.Key = info.Name, info.Key()261 s.mon.Name, s.mon.Key = info.Name, info.Key()
239 c.settings(s.mon)262 c.settings(s.mon)
240 s.cfgScale = c.configScale(info.Name)263 s.cfgScale = c.configScale(info.Name)
264+ s.transform = c.configTransform(info.Name)
241 s.setScale(s.cfgScale)265 s.setScale(s.cfgScale)
242 s.primary = c.isPrimary(info.Name)266 s.primary = c.isPrimary(info.Name)
243 c.order()267 c.order()
M internal/core/screens_test.go +94 -0
@@ -1409,3 +1409,97 @@ func TestPoweredOffScreenStaysNeighbor(t *testing.T) {
14091409 t.Fatalf("focus-monitor-right: %s, want the powered-off DP-2", got)
14101410 }
14111411 }
1412+
1413+func TestRotatedOutputLayout(t *testing.T) {
1414+ r := startMulti(t, func(c *ports.Config) {
1415+ c.Outputs = []ports.OutputConfig{{Name: left.Name, Transform: 1}}
1416+ }, left, right)
1417+ out := lastOutputs(t, r.commands)
1418+ if len(out.Outputs) != 2 {
1419+ t.Fatalf("%+v", out.Outputs)
1420+ }
1421+ if o := out.Outputs[0]; o.Info.Name != "DP-1" || o.Width != 100 || o.Height != 200 || o.Transform != 1 {
1422+ t.Fatalf("DP-1: %+v", o)
1423+ }
1424+ if o := out.Outputs[1]; o.Info.Name != "DP-2" || o.X != 100 || o.Width != 400 || o.Height != 200 || o.Transform != 0 {
1425+ t.Fatalf("DP-2: %+v", o)
1426+ }
1427+ r.mapWindow(t, 1)
1428+ set := r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
1429+ for _, s := range set {
1430+ switch s.Output {
1431+ case "DP-1":
1432+ if s.OutputWidth != 100 || s.OutputHeight != 200 || s.Transform != 1 {
1433+ t.Fatalf("DP-1 scene: %dx%d t=%d", s.OutputWidth, s.OutputHeight, s.Transform)
1434+ }
1435+ case "DP-2":
1436+ if s.OutputWidth != 400 || s.OutputHeight != 200 || s.Transform != 0 {
1437+ t.Fatalf("DP-2 scene: %dx%d t=%d", s.OutputWidth, s.OutputHeight, s.Transform)
1438+ }
1439+ }
1440+ }
1441+}
1442+
1443+func TestRotatedOutputNeighbor(t *testing.T) {
1444+ r := startMulti(t, func(c *ports.Config) {
1445+ c.Outputs = []ports.OutputConfig{
1446+ {Name: "DP-1", Transform: 1},
1447+ {Name: "DP-2", Anchor: ports.OutputAnchor{Relation: ports.RelationBelow, To: "DP-1"}},
1448+ }
1449+ }, left, right)
1450+ out := lastOutputs(t, r.commands)
1451+ if o := out.Outputs[1]; o.Info.Name != "DP-2" || o.Y != 200 {
1452+ t.Fatalf("DP-2: %+v", o)
1453+ }
1454+ r.mapWindow(t, 1)
1455+ r.key(t, "Down", ports.ModAlt|ports.ModCtrl)
1456+ if got := lastOutputs(t, r.commands).Focused; got != "DP-2" {
1457+ t.Fatalf("focus-monitor-down: %s", got)
1458+ }
1459+}
1460+
1461+func TestTransformReload(t *testing.T) {
1462+ r := startMulti(t, func(c *ports.Config) {
1463+ c.Binds["Alt+equal"] = "scale-up"
1464+ c.Outputs = []ports.OutputConfig{{Name: "DP-1"}, {Name: "DP-2"}}
1465+ }, left, right)
1466+ // Scale DP-2 (focus follows the key to it) with the bind first.
1467+ r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
1468+ set := r.key(t, "equal", ports.ModAlt)
1469+ scale := set[1].Scale
1470+ if scale == 1 {
1471+ t.Fatal("scale-up did not change the scale")
1472+ }
1473+ if out := lastOutputs(t, r.commands); out.Outputs[0].Width != 200 || out.Outputs[1].X != 200 {
1474+ t.Fatalf("%+v", out.Outputs)
1475+ }
1476+
1477+ r.cfg.Outputs[0].Transform = 1
1478+ r.reload <- ports.ConfigChanged{Config: r.cfg}
1479+ set = receive(t, r.scenes)
1480+ out := lastOutputs(t, r.commands)
1481+ if o := out.Outputs[0]; o.Width != 100 || o.Height != 200 || o.Transform != 1 {
1482+ t.Fatalf("DP-1: %+v", o)
1483+ }
1484+ if o := out.Outputs[1]; o.X != 100 {
1485+ t.Fatalf("DP-2 not re-placed: %+v", o)
1486+ }
1487+ if set[1].Scale != scale || out.Outputs[1].Scale != scale {
1488+ t.Fatalf("scale %v changed to %v by a transform reload", scale, set[1].Scale)
1489+ }
1490+
1491+ // A transform change on the scaled output itself keeps its live scale.
1492+ r.cfg.Outputs[1].Transform = 1
1493+ r.reload <- ports.ConfigChanged{Config: r.cfg}
1494+ set = receive(t, r.scenes)
1495+ if set[1].Scale != scale || set[1].Transform != 1 {
1496+ t.Fatalf("scale %v transform %d", set[1].Scale, set[1].Transform)
1497+ }
1498+ // Back to normal.
1499+ r.cfg.Outputs[0].Transform = 0
1500+ r.reload <- ports.ConfigChanged{Config: r.cfg}
1501+ _ = receive(t, r.scenes)
1502+ if o := lastOutputs(t, r.commands).Outputs[0]; o.Width != 200 || o.Transform != 0 {
1503+ t.Fatalf("DP-1: %+v", o)
1504+ }
1505+}
@@ -1409,3 +1409,97 @@ func TestPoweredOffScreenStaysNeighbor(t *testing.T) {
1409 t.Fatalf("focus-monitor-right: %s, want the powered-off DP-2", got)1409 t.Fatalf("focus-monitor-right: %s, want the powered-off DP-2", got)
1410 }1410 }
1411 }1411 }
1412+
1413+func TestRotatedOutputLayout(t *testing.T) {
1414+ r := startMulti(t, func(c *ports.Config) {
1415+ c.Outputs = []ports.OutputConfig{{Name: left.Name, Transform: 1}}
1416+ }, left, right)
1417+ out := lastOutputs(t, r.commands)
1418+ if len(out.Outputs) != 2 {
1419+ t.Fatalf("%+v", out.Outputs)
1420+ }
1421+ if o := out.Outputs[0]; o.Info.Name != "DP-1" || o.Width != 100 || o.Height != 200 || o.Transform != 1 {
1422+ t.Fatalf("DP-1: %+v", o)
1423+ }
1424+ if o := out.Outputs[1]; o.Info.Name != "DP-2" || o.X != 100 || o.Width != 400 || o.Height != 200 || o.Transform != 0 {
1425+ t.Fatalf("DP-2: %+v", o)
1426+ }
1427+ r.mapWindow(t, 1)
1428+ set := r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
1429+ for _, s := range set {
1430+ switch s.Output {
1431+ case "DP-1":
1432+ if s.OutputWidth != 100 || s.OutputHeight != 200 || s.Transform != 1 {
1433+ t.Fatalf("DP-1 scene: %dx%d t=%d", s.OutputWidth, s.OutputHeight, s.Transform)
1434+ }
1435+ case "DP-2":
1436+ if s.OutputWidth != 400 || s.OutputHeight != 200 || s.Transform != 0 {
1437+ t.Fatalf("DP-2 scene: %dx%d t=%d", s.OutputWidth, s.OutputHeight, s.Transform)
1438+ }
1439+ }
1440+ }
1441+}
1442+
1443+func TestRotatedOutputNeighbor(t *testing.T) {
1444+ r := startMulti(t, func(c *ports.Config) {
1445+ c.Outputs = []ports.OutputConfig{
1446+ {Name: "DP-1", Transform: 1},
1447+ {Name: "DP-2", Anchor: ports.OutputAnchor{Relation: ports.RelationBelow, To: "DP-1"}},
1448+ }
1449+ }, left, right)
1450+ out := lastOutputs(t, r.commands)
1451+ if o := out.Outputs[1]; o.Info.Name != "DP-2" || o.Y != 200 {
1452+ t.Fatalf("DP-2: %+v", o)
1453+ }
1454+ r.mapWindow(t, 1)
1455+ r.key(t, "Down", ports.ModAlt|ports.ModCtrl)
1456+ if got := lastOutputs(t, r.commands).Focused; got != "DP-2" {
1457+ t.Fatalf("focus-monitor-down: %s", got)
1458+ }
1459+}
1460+
1461+func TestTransformReload(t *testing.T) {
1462+ r := startMulti(t, func(c *ports.Config) {
1463+ c.Binds["Alt+equal"] = "scale-up"
1464+ c.Outputs = []ports.OutputConfig{{Name: "DP-1"}, {Name: "DP-2"}}
1465+ }, left, right)
1466+ // Scale DP-2 (focus follows the key to it) with the bind first.
1467+ r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
1468+ set := r.key(t, "equal", ports.ModAlt)
1469+ scale := set[1].Scale
1470+ if scale == 1 {
1471+ t.Fatal("scale-up did not change the scale")
1472+ }
1473+ if out := lastOutputs(t, r.commands); out.Outputs[0].Width != 200 || out.Outputs[1].X != 200 {
1474+ t.Fatalf("%+v", out.Outputs)
1475+ }
1476+
1477+ r.cfg.Outputs[0].Transform = 1
1478+ r.reload <- ports.ConfigChanged{Config: r.cfg}
1479+ set = receive(t, r.scenes)
1480+ out := lastOutputs(t, r.commands)
1481+ if o := out.Outputs[0]; o.Width != 100 || o.Height != 200 || o.Transform != 1 {
1482+ t.Fatalf("DP-1: %+v", o)
1483+ }
1484+ if o := out.Outputs[1]; o.X != 100 {
1485+ t.Fatalf("DP-2 not re-placed: %+v", o)
1486+ }
1487+ if set[1].Scale != scale || out.Outputs[1].Scale != scale {
1488+ t.Fatalf("scale %v changed to %v by a transform reload", scale, set[1].Scale)
1489+ }
1490+
1491+ // A transform change on the scaled output itself keeps its live scale.
1492+ r.cfg.Outputs[1].Transform = 1
1493+ r.reload <- ports.ConfigChanged{Config: r.cfg}
1494+ set = receive(t, r.scenes)
1495+ if set[1].Scale != scale || set[1].Transform != 1 {
1496+ t.Fatalf("scale %v transform %d", set[1].Scale, set[1].Transform)
1497+ }
1498+ // Back to normal.
1499+ r.cfg.Outputs[0].Transform = 0
1500+ r.reload <- ports.ConfigChanged{Config: r.cfg}
1501+ _ = receive(t, r.scenes)
1502+ if o := lastOutputs(t, r.commands).Outputs[0]; o.Width != 200 || o.Transform != 0 {
1503+ t.Fatalf("DP-1: %+v", o)
1504+ }
1505+}
M internal/core/security.go +1 -1
@@ -186,7 +186,7 @@ func (c *Core) publishProtected(ctx context.Context) error {
186186 for _, sc := range c.screens {
187187 c.seq++
188188 o := sc.mon.Output()
189- scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Off: sc.off, Background: "#000000"}
189+ scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: "#000000"}
190190 if s, ok := c.lockSurface(sc); ok {
191191 scene.Windows = []ports.SceneWindow{{ID: s.ID, Rect: Rect{W: o.W, H: o.H}, Fullscreen: true, Focused: s.ID == focus}}
192192 }
@@ -186,7 +186,7 @@ func (c *Core) publishProtected(ctx context.Context) error {
186 for _, sc := range c.screens {186 for _, sc := range c.screens {
187 c.seq++187 c.seq++
188 o := sc.mon.Output()188 o := sc.mon.Output()
189- scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Off: sc.off, Background: "#000000"}189+ scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: "#000000"}
190 if s, ok := c.lockSurface(sc); ok {190 if s, ok := c.lockSurface(sc); ok {
191 scene.Windows = []ports.SceneWindow{{ID: s.ID, Rect: Rect{W: o.W, H: o.H}, Fullscreen: true, Focused: s.ID == focus}}191 scene.Windows = []ports.SceneWindow{{ID: s.ID, Rect: Rect{W: o.W, H: o.H}, Fullscreen: true, Focused: s.ID == focus}}
192 }192 }

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