detached 0e2dc8e

test(core): cover the focus pulse above a panel

Also make the unfocused-inset test fail when its tile is missing, and
describe the usable area in the separators and visibleCount docs.
commit
0e2dc8ef94437d82359769ccdb37556a7279536c
author
brice · · 2026-10-07 14:29 UTC
parent
29cd2a5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +28 -11
M internal/core/overview_cascade_test.go +4 -4
@@ -194,15 +194,15 @@ func TestOverviewCascadeMaximizedCardPickRecordsWindow(t *testing.T) {
194194 // elsewhere fills its band.
195195 func TestOverviewCascadeUnfocusedMaximizedNotInset(t *testing.T) {
196196 w := cascadeWorkspace(6)
197+ w.SetGaps(8)
197198 w.Focus = 4
198199 w.maximize(4)
199200 w.Columns[0].FullWidth = true
200201 tiles, _, _ := w.previewTiles()
201202 g := w.gap()
202- for _, p := range tiles {
203- if p.ID == 1 && (p.Rect.X != g || p.Rect.W != w.Usable.W-2*g) {
204- t.Fatalf("unfocused maximized column %+v, want X %d W %d", p.Rect, g, w.Usable.W-2*g)
205- }
203+ p := previewOf(t, tiles, 1)
204+ if p.Hidden || p.Rect.X != g || p.Rect.W != w.Usable.W-2*g {
205+ t.Fatalf("unfocused maximized column %+v, want X %d W %d", p, g, w.Usable.W-2*g)
206206 }
207207 }
208208
@@ -194,15 +194,15 @@ func TestOverviewCascadeMaximizedCardPickRecordsWindow(t *testing.T) {
194 // elsewhere fills its band.194 // elsewhere fills its band.
195 func TestOverviewCascadeUnfocusedMaximizedNotInset(t *testing.T) {195 func TestOverviewCascadeUnfocusedMaximizedNotInset(t *testing.T) {
196 w := cascadeWorkspace(6)196 w := cascadeWorkspace(6)
197+ w.SetGaps(8)
197 w.Focus = 4198 w.Focus = 4
198 w.maximize(4)199 w.maximize(4)
199 w.Columns[0].FullWidth = true200 w.Columns[0].FullWidth = true
200 tiles, _, _ := w.previewTiles()201 tiles, _, _ := w.previewTiles()
201 g := w.gap()202 g := w.gap()
202- for _, p := range tiles {203+ p := previewOf(t, tiles, 1)
203- if p.ID == 1 && (p.Rect.X != g || p.Rect.W != w.Usable.W-2*g) {204+ if p.Hidden || p.Rect.X != g || p.Rect.W != w.Usable.W-2*g {
204- t.Fatalf("unfocused maximized column %+v, want X %d W %d", p.Rect, g, w.Usable.W-2*g)205+ t.Fatalf("unfocused maximized column %+v, want X %d W %d", p, g, w.Usable.W-2*g)
205- }
206 }206 }
207 }207 }
208 208
M internal/core/pulse.go +3 -3
@@ -151,11 +151,11 @@ func (m *Monitor) settled() bool {
151151 return true
152152 }
153153
154-// visibleCount is the number of windows of layout shown in frame.
155-func visibleCount(layout []Placement, frame Rect) int {
154+// visibleCount is the number of windows of layout shown in area.
155+func visibleCount(layout []Placement, area Rect) int {
156156 n := 0
157157 for _, p := range layout {
158- if p.Preview == 0 && onScreen(p, frame) {
158+ if p.Preview == 0 && onScreen(p, area) {
159159 n++
160160 }
161161 }
@@ -151,11 +151,11 @@ func (m *Monitor) settled() bool {
151 return true151 return true
152 }152 }
153 153
154-// visibleCount is the number of windows of layout shown in frame.154+// visibleCount is the number of windows of layout shown in area.
155-func visibleCount(layout []Placement, frame Rect) int {155+func visibleCount(layout []Placement, area Rect) int {
156 n := 0156 n := 0
157 for _, p := range layout {157 for _, p := range layout {
158- if p.Preview == 0 && onScreen(p, frame) {158+ if p.Preview == 0 && onScreen(p, area) {
159 n++159 n++
160 }160 }
161 }161 }
M internal/core/pulse_test.go +16 -0
@@ -185,6 +185,22 @@ func TestFocusPulseSkipsAloneOnScreen(t *testing.T) {
185185 }
186186 }
187187
188+// A cascade window alone in its band above a bottom panel is alone on
189+// screen: the next band, under the panel, does not count.
190+func TestFocusPulseSkipsAloneAbovePanel(t *testing.T) {
191+ c, ic := pulseCore(t)
192+ m := c.cur().mon
193+ m.SetOverflow(OverflowCascade)
194+ m.SetMaxColumns(1)
195+ m.SetUsable(Rect{W: 300, H: 180})
196+ m.Current().FocusID(1)
197+ indicatorScene(t, c)
198+ ic.now = ic.now.Add(pulseSettle)
199+ if c.pulseTick() || c.animating() {
200+ t.Fatal("a window alone in its band above a panel pulses")
201+ }
202+}
203+
188204 // A swipe, its landing slide or a transition still shows the neighbours: the
189205 // pulse waits for the layout to settle, then decides on it.
190206 func TestFocusPulseWaitsForSlide(t *testing.T) {
@@ -185,6 +185,22 @@ func TestFocusPulseSkipsAloneOnScreen(t *testing.T) {
185 }185 }
186 }186 }
187 187
188+// A cascade window alone in its band above a bottom panel is alone on
189+// screen: the next band, under the panel, does not count.
190+func TestFocusPulseSkipsAloneAbovePanel(t *testing.T) {
191+ c, ic := pulseCore(t)
192+ m := c.cur().mon
193+ m.SetOverflow(OverflowCascade)
194+ m.SetMaxColumns(1)
195+ m.SetUsable(Rect{W: 300, H: 180})
196+ m.Current().FocusID(1)
197+ indicatorScene(t, c)
198+ ic.now = ic.now.Add(pulseSettle)
199+ if c.pulseTick() || c.animating() {
200+ t.Fatal("a window alone in its band above a panel pulses")
201+ }
202+}
203+
188 // A swipe, its landing slide or a transition still shows the neighbours: the204 // A swipe, its landing slide or a transition still shows the neighbours: the
189 // pulse waits for the layout to settle, then decides on it.205 // pulse waits for the layout to settle, then decides on it.
190 func TestFocusPulseWaitsForSlide(t *testing.T) {206 func TestFocusPulseWaitsForSlide(t *testing.T) {
M internal/core/separators.go +5 -4
@@ -8,10 +8,11 @@ import "github.com/bnema/neferwl/internal/ports"
88 // sides of the focused tile, including corners. With exactly two visible
99 // tiles and no gaps, each lights its own half of the shared line (left/top
1010 // first). With gaps, each tile owns its neighbor-facing lines and lights
11-// them whole. Hidden, scrolled-off, other-workspace and other-output tiles
12-// do not count. Lines at output edges are omitted. Floating windows differ
13-// from tmux tiles: each gets its own full border, drawn with that window
14-// (Separator.Window), even when it is the only visible window. Only the
11+// them whole. Hidden, scrolled-off, under-panel, other-workspace and
12+// other-output tiles do not count, nor do tiles that share no line. Lines
13+// at output edges are omitted. Floating windows differ from tmux tiles:
14+// each gets its own full border, drawn with that window (Separator.Window),
15+// even when it is the only visible window. Only the
1516 // focused output's focused window can light lines; keyboard grabs by popups
1617 // and layers do not change the underlying window's visual focus.
1718
@@ -8,10 +8,11 @@ import "github.com/bnema/neferwl/internal/ports"
8 // sides of the focused tile, including corners. With exactly two visible8 // sides of the focused tile, including corners. With exactly two visible
9 // tiles and no gaps, each lights its own half of the shared line (left/top9 // tiles and no gaps, each lights its own half of the shared line (left/top
10 // first). With gaps, each tile owns its neighbor-facing lines and lights10 // first). With gaps, each tile owns its neighbor-facing lines and lights
11-// them whole. Hidden, scrolled-off, other-workspace and other-output tiles11+// them whole. Hidden, scrolled-off, under-panel, other-workspace and
12-// do not count. Lines at output edges are omitted. Floating windows differ12+// other-output tiles do not count, nor do tiles that share no line. Lines
13-// from tmux tiles: each gets its own full border, drawn with that window13+// at output edges are omitted. Floating windows differ from tmux tiles:
14-// (Separator.Window), even when it is the only visible window. Only the14+// each gets its own full border, drawn with that window (Separator.Window),
15+// even when it is the only visible window. Only the
15 // focused output's focused window can light lines; keyboard grabs by popups16 // focused output's focused window can light lines; keyboard grabs by popups
16 // and layers do not change the underlying window's visual focus.17 // and layers do not change the underlying window's visual focus.
17 18

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