detached 80d95c9

test(core): drive the hidden grab popup test through Run and cover fullscreen focus and moves

commit
80d95c9c58ae63f1d5699503f546befeb39bbd1b
author
brice · · 2026-09-28 14:31 UTC
committer
brice · · 2026-09-28 14:44 UTC
parent
10fab51
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +69 -28
M internal/core/floating_test.go +18 -0
@@ -122,6 +122,24 @@ func TestFullscreenPinsFocus(t *testing.T) {
122122 if e := w.Apply(ActionCloseWindow); e.Close != 3 {
123123 t.Fatalf("close %d, want the fullscreen window", e.Close)
124124 }
125+ // A fixed-overflow fullscreen tile pins the focus too.
126+ f := workspace()
127+ f.Overflow = OverflowFixed
128+ f.AddWindow(1)
129+ f.AddWindow(2)
130+ f.SetFullscreen(2, true)
131+ if f.FocusColumn(-1); f.Focus != 1 {
132+ t.Fatalf("fixed: focus moved to column %d", f.Focus)
133+ }
134+ // Scroll overflow: moving off a fullscreen column still works; it no
135+ // longer covers.
136+ s := workspace()
137+ s.AddWindow(1)
138+ s.AddWindow(2)
139+ s.SetFullscreen(2, true)
140+ if s.FocusColumn(-1); s.Focus != 0 || s.cover() != 0 {
141+ t.Fatalf("scroll: focus %d cover %d", s.Focus, s.cover())
142+ }
125143 }
126144
127145 // A fullscreen window does not float until it leaves fullscreen.
@@ -122,6 +122,24 @@ func TestFullscreenPinsFocus(t *testing.T) {
122 if e := w.Apply(ActionCloseWindow); e.Close != 3 {122 if e := w.Apply(ActionCloseWindow); e.Close != 3 {
123 t.Fatalf("close %d, want the fullscreen window", e.Close)123 t.Fatalf("close %d, want the fullscreen window", e.Close)
124 }124 }
125+ // A fixed-overflow fullscreen tile pins the focus too.
126+ f := workspace()
127+ f.Overflow = OverflowFixed
128+ f.AddWindow(1)
129+ f.AddWindow(2)
130+ f.SetFullscreen(2, true)
131+ if f.FocusColumn(-1); f.Focus != 1 {
132+ t.Fatalf("fixed: focus moved to column %d", f.Focus)
133+ }
134+ // Scroll overflow: moving off a fullscreen column still works; it no
135+ // longer covers.
136+ s := workspace()
137+ s.AddWindow(1)
138+ s.AddWindow(2)
139+ s.SetFullscreen(2, true)
140+ if s.FocusColumn(-1); s.Focus != 0 || s.cover() != 0 {
141+ t.Fatalf("scroll: focus %d cover %d", s.Focus, s.cover())
142+ }
125 }143 }
126 144
127 // A fullscreen window does not float until it leaves fullscreen.145 // A fullscreen window does not float until it leaves fullscreen.
M internal/core/layer_input_test.go +36 -0
@@ -394,3 +394,39 @@ func TestPopupInputRegionPassesThroughToWindow(t *testing.T) {
394394 t.Fatalf("popup interior motion: %+v", v)
395395 }
396396 }
397+
398+// A grabbing popup hidden with its parent under a fullscreen window is
399+// closed, and the keyboard goes to the fullscreen window.
400+func TestHiddenGrabPopupLosesKeyboard(t *testing.T) {
401+ cfg := config.Defaults()
402+ cfg.Border.Width = 0
403+ client := make(chan ports.ClientEvent, 8)
404+ output := make(chan ports.OutputEvent, 8)
405+ commands := make(chan ports.ClientCommand, 64)
406+ scenes := make(chan []ports.Scene, 1)
407+ c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes})
408+ if err != nil {
409+ t.Fatal(err)
410+ }
411+ ctx, cancel := context.WithCancel(context.Background())
412+ defer cancel()
413+ go c.Run(ctx)
414+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
415+ <-scenes
416+ client <- ports.WindowMapped{ID: 1}
417+ <-scenes
418+ pos := ports.Positioner{Width: 10, Height: 10, AnchorRect: ports.Rect{W: 1, H: 1}, Anchor: ports.EdgeBottomRight, Gravity: ports.EdgeBottomRight}
419+ client <- ports.PopupRequest{ID: 3, Parent: 1, Positioner: pos, Grab: true}
420+ next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 3 })
421+ client <- ports.PopupMapped{ID: 3}
422+ if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 3 {
423+ t.Fatalf("focus %d, want the grabbing popup", f.ID)
424+ }
425+ // A new fullscreen window covers its parent: the popup is hidden.
426+ client <- ports.WindowMapped{ID: 2}
427+ client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true}
428+ next(t, commands, func(v ports.ClosePopup) bool { return v.ID == 3 })
429+ if f := next(t, commands, func(v ports.FocusWindow) bool { return v.ID != 3 }); f.ID != 2 {
430+ t.Fatalf("focus %d, want the fullscreen window", f.ID)
431+ }
432+}
@@ -394,3 +394,39 @@ func TestPopupInputRegionPassesThroughToWindow(t *testing.T) {
394 t.Fatalf("popup interior motion: %+v", v)394 t.Fatalf("popup interior motion: %+v", v)
395 }395 }
396 }396 }
397+
398+// A grabbing popup hidden with its parent under a fullscreen window is
399+// closed, and the keyboard goes to the fullscreen window.
400+func TestHiddenGrabPopupLosesKeyboard(t *testing.T) {
401+ cfg := config.Defaults()
402+ cfg.Border.Width = 0
403+ client := make(chan ports.ClientEvent, 8)
404+ output := make(chan ports.OutputEvent, 8)
405+ commands := make(chan ports.ClientCommand, 64)
406+ scenes := make(chan []ports.Scene, 1)
407+ c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes})
408+ if err != nil {
409+ t.Fatal(err)
410+ }
411+ ctx, cancel := context.WithCancel(context.Background())
412+ defer cancel()
413+ go c.Run(ctx)
414+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
415+ <-scenes
416+ client <- ports.WindowMapped{ID: 1}
417+ <-scenes
418+ pos := ports.Positioner{Width: 10, Height: 10, AnchorRect: ports.Rect{W: 1, H: 1}, Anchor: ports.EdgeBottomRight, Gravity: ports.EdgeBottomRight}
419+ client <- ports.PopupRequest{ID: 3, Parent: 1, Positioner: pos, Grab: true}
420+ next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 3 })
421+ client <- ports.PopupMapped{ID: 3}
422+ if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 3 {
423+ t.Fatalf("focus %d, want the grabbing popup", f.ID)
424+ }
425+ // A new fullscreen window covers its parent: the popup is hidden.
426+ client <- ports.WindowMapped{ID: 2}
427+ client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true}
428+ next(t, commands, func(v ports.ClosePopup) bool { return v.ID == 3 })
429+ if f := next(t, commands, func(v ports.FocusWindow) bool { return v.ID != 3 }); f.ID != 2 {
430+ t.Fatalf("focus %d, want the fullscreen window", f.ID)
431+ }
432+}
M internal/core/monitor_test.go +15 -0
@@ -770,4 +770,19 @@ func TestFollowMoveIntoFullscreenLands(t *testing.T) {
770770 }
771771 }
772772 }
773+ // Without follow, the fullscreen workspace keeps its window: the moved
774+ // one waits there, hidden, and the view stays.
775+ m := newMonitor("", "")
776+ m.SetOutput(100, 80)
777+ m.SetOverflow(OverflowFixed)
778+ m.AddWindow(1)
779+ m.AddWindow(2)
780+ m.ToggleFullscreen()
781+ m.Focus(len(m.Workspaces) - 1)
782+ m.AddWindow(5)
783+ from := m.Current()
784+ m.MoveToWorkspace(1, true)
785+ if fs := m.Workspaces[1]; m.Current() != from || fs.cover() != 2 || fs.floatIndex(5) < 0 {
786+ t.Fatalf("no follow: workspaces %v floats %v", windows(m), fs.Floats)
787+ }
773788 }
@@ -770,4 +770,19 @@ func TestFollowMoveIntoFullscreenLands(t *testing.T) {
770 }770 }
771 }771 }
772 }772 }
773+ // Without follow, the fullscreen workspace keeps its window: the moved
774+ // one waits there, hidden, and the view stays.
775+ m := newMonitor("", "")
776+ m.SetOutput(100, 80)
777+ m.SetOverflow(OverflowFixed)
778+ m.AddWindow(1)
779+ m.AddWindow(2)
780+ m.ToggleFullscreen()
781+ m.Focus(len(m.Workspaces) - 1)
782+ m.AddWindow(5)
783+ from := m.Current()
784+ m.MoveToWorkspace(1, true)
785+ if fs := m.Workspaces[1]; m.Current() != from || fs.cover() != 2 || fs.floatIndex(5) < 0 {
786+ t.Fatalf("no follow: workspaces %v floats %v", windows(m), fs.Floats)
787+ }
773 }788 }
M internal/core/popup_test.go +0 -28
@@ -73,31 +73,3 @@ func TestFloatingWindows(t *testing.T) {
7373 }
7474 }
7575
76-// A grabbing popup hidden with its parent under a fullscreen window has no
77-// keyboard; the fullscreen window has it.
78-func TestHiddenGrabPopupLosesKeyboard(t *testing.T) {
79- cfg := ports.Config{}
80- cfg.Keyboard.CmdKey = "super"
81- cfg.Layout.MaxColumns = 2
82- c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)})
83- if err != nil {
84- t.Fatal(err)
85- }
86- c.addScreen(ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80})
87- m := c.cur().mon
88- m.AddWindow(1)
89- m.AddWindow(2)
90- c.popups[3] = &popupState{id: 3, parent: 1, grab: true, mapped: true, rect: Rect{W: 5, H: 5}}
91- c.popupOrder = append(c.popupOrder, 3)
92- m.Current().FocusID(1)
93- if f := c.keyboardFocus(); f != 3 {
94- t.Fatalf("focus %d, want the grabbing popup", f)
95- }
96- m.SetFullscreen(2, true)
97- // Scroll overflow: the fullscreen window covers once its column is on
98- // screen.
99- m.Current().FocusID(2)
100- if f := c.keyboardFocus(); f != 2 {
101- t.Fatalf("focus %d, want the fullscreen window", f)
102- }
103-}
@@ -73,31 +73,3 @@ func TestFloatingWindows(t *testing.T) {
73 }73 }
74 }74 }
75 75
76-// A grabbing popup hidden with its parent under a fullscreen window has no
77-// keyboard; the fullscreen window has it.
78-func TestHiddenGrabPopupLosesKeyboard(t *testing.T) {
79- cfg := ports.Config{}
80- cfg.Keyboard.CmdKey = "super"
81- cfg.Layout.MaxColumns = 2
82- c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)})
83- if err != nil {
84- t.Fatal(err)
85- }
86- c.addScreen(ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80})
87- m := c.cur().mon
88- m.AddWindow(1)
89- m.AddWindow(2)
90- c.popups[3] = &popupState{id: 3, parent: 1, grab: true, mapped: true, rect: Rect{W: 5, H: 5}}
91- c.popupOrder = append(c.popupOrder, 3)
92- m.Current().FocusID(1)
93- if f := c.keyboardFocus(); f != 3 {
94- t.Fatalf("focus %d, want the grabbing popup", f)
95- }
96- m.SetFullscreen(2, true)
97- // Scroll overflow: the fullscreen window covers once its column is on
98- // screen.
99- m.Current().FocusID(2)
100- if f := c.keyboardFocus(); f != 2 {
101- t.Fatalf("focus %d, want the fullscreen window", f)
102- }
103-}

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