detached 60e7285

feat(state): report fullscreen windows

commit
60e7285b70507af218302663044daffa7449c384
author
brice · · 2026-10-23 00:55 UTC
parent
f2d166b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +33 -6
M docs/desktop.md +2 -1
@@ -131,7 +131,7 @@ Workspace IDs are `name:<name>` for named workspaces, stable across launches. Nu
131131
132132 ## State for scripts
133133
134-NeferWL writes its state as JSON to `$XDG_RUNTIME_DIR/neferwl/<wayland socket>.json`, also given to started programs as `$NEFERWL_STATE`. It lists outputs, workspaces, the focused window, and every window with its app ID, PID, workspace, column and row.
134+NeferWL writes its state as JSON to `$XDG_RUNTIME_DIR/neferwl/<wayland socket>.json`, also given to started programs as `$NEFERWL_STATE`. It lists outputs, workspaces, the focused window, and every window with its app ID, PID, workspace, column and row, and whether it is floating or the fullscreen window of its workspace (`floating`, `fullscreen`).
135135
136136 The `workspaces` list holds every workspace with its `visible` flag, its columns and the windows in them, each with `x`, `y`, `width` and `height` in logical pixels of its output. The file is written at most once per second.
137137
@@ -139,5 +139,6 @@ The `workspaces` list holds every workspace with its `visible` flag, its columns
139139 neferwl state # the whole state
140140 neferwl state output-of "$PID" # the output showing that process's window
141141 neferwl state | jq '[.windows[] | select(.hidden)] | length' # windows in hidden stashes
142+neferwl state | jq '[.windows[] | select(.fullscreen)] | length' # fullscreen windows
142143 neferwl state | jq '.workspaces[] | select(.visible) | .columns[] | .width' # column widths on screen
143144 ```
@@ -131,7 +131,7 @@ Workspace IDs are `name:<name>` for named workspaces, stable across launches. Nu
131 131
132 ## State for scripts132 ## State for scripts
133 133
134-NeferWL writes its state as JSON to `$XDG_RUNTIME_DIR/neferwl/<wayland socket>.json`, also given to started programs as `$NEFERWL_STATE`. It lists outputs, workspaces, the focused window, and every window with its app ID, PID, workspace, column and row.134+NeferWL writes its state as JSON to `$XDG_RUNTIME_DIR/neferwl/<wayland socket>.json`, also given to started programs as `$NEFERWL_STATE`. It lists outputs, workspaces, the focused window, and every window with its app ID, PID, workspace, column and row, and whether it is floating or the fullscreen window of its workspace (`floating`, `fullscreen`).
135 135
136 The `workspaces` list holds every workspace with its `visible` flag, its columns and the windows in them, each with `x`, `y`, `width` and `height` in logical pixels of its output. The file is written at most once per second.136 The `workspaces` list holds every workspace with its `visible` flag, its columns and the windows in them, each with `x`, `y`, `width` and `height` in logical pixels of its output. The file is written at most once per second.
137 137
@@ -139,5 +139,6 @@ The `workspaces` list holds every workspace with its `visible` flag, its columns
139 neferwl state # the whole state139 neferwl state # the whole state
140 neferwl state output-of "$PID" # the output showing that process's window140 neferwl state output-of "$PID" # the output showing that process's window
141 neferwl state | jq '[.windows[] | select(.hidden)] | length' # windows in hidden stashes141 neferwl state | jq '[.windows[] | select(.hidden)] | length' # windows in hidden stashes
142+neferwl state | jq '[.windows[] | select(.fullscreen)] | length' # fullscreen windows
142 neferwl state | jq '.workspaces[] | select(.visible) | .columns[] | .width' # column widths on screen143 neferwl state | jq '.workspaces[] | select(.visible) | .columns[] | .width' # column widths on screen
143 ```144 ```
M internal/adapters/statefile/statefile.go +3 -1
@@ -100,6 +100,8 @@ type Window struct {
100100 Visible bool `json:"visible"`
101101 // Floating is set for native floats (dialogs) and stashed windows.
102102 Floating bool `json:"floating"`
103+ // Fullscreen is set for the fullscreen window of its workspace.
104+ Fullscreen bool `json:"fullscreen"`
103105 // StashIndex is the 1-based place of a stashed window in its
104106 // workspace's stash of StashCount windows; both are 0 outside it.
105107 StashIndex int `json:"stash_index"`
@@ -139,7 +141,7 @@ func fromPorts(st ports.State, ids *workspaceid.IDs) State {
139141 }
140142
141143 func window(w ports.WindowState, ids *workspaceid.IDs) Window {
142- return Window{ID: uint64(w.ID), AppID: w.AppID, PID: w.PID, Output: w.Output, Workspace: w.Workspace, WorkspaceID: ids.ID(w.WorkspaceID, w.WorkspaceName), Visible: w.Visible, Floating: w.Floating, StashIndex: w.StashIndex, StashCount: w.StashCount, Column: w.Column, Row: w.Row, Hidden: w.Hidden}
144+ return Window{ID: uint64(w.ID), AppID: w.AppID, PID: w.PID, Output: w.Output, Workspace: w.Workspace, WorkspaceID: ids.ID(w.WorkspaceID, w.WorkspaceName), Visible: w.Visible, Floating: w.Floating, Fullscreen: w.Fullscreen, StashIndex: w.StashIndex, StashCount: w.StashCount, Column: w.Column, Row: w.Row, Hidden: w.Hidden}
143145 }
144146
145147 // writeInterval is the least time between two writes of the file.
@@ -100,6 +100,8 @@ type Window struct {
100 Visible bool `json:"visible"`100 Visible bool `json:"visible"`
101 // Floating is set for native floats (dialogs) and stashed windows.101 // Floating is set for native floats (dialogs) and stashed windows.
102 Floating bool `json:"floating"`102 Floating bool `json:"floating"`
103+ // Fullscreen is set for the fullscreen window of its workspace.
104+ Fullscreen bool `json:"fullscreen"`
103 // StashIndex is the 1-based place of a stashed window in its105 // StashIndex is the 1-based place of a stashed window in its
104 // workspace's stash of StashCount windows; both are 0 outside it.106 // workspace's stash of StashCount windows; both are 0 outside it.
105 StashIndex int `json:"stash_index"`107 StashIndex int `json:"stash_index"`
@@ -139,7 +141,7 @@ func fromPorts(st ports.State, ids *workspaceid.IDs) State {
139 }141 }
140 142
141 func window(w ports.WindowState, ids *workspaceid.IDs) Window {143 func window(w ports.WindowState, ids *workspaceid.IDs) Window {
142- return Window{ID: uint64(w.ID), AppID: w.AppID, PID: w.PID, Output: w.Output, Workspace: w.Workspace, WorkspaceID: ids.ID(w.WorkspaceID, w.WorkspaceName), Visible: w.Visible, Floating: w.Floating, StashIndex: w.StashIndex, StashCount: w.StashCount, Column: w.Column, Row: w.Row, Hidden: w.Hidden}144+ return Window{ID: uint64(w.ID), AppID: w.AppID, PID: w.PID, Output: w.Output, Workspace: w.Workspace, WorkspaceID: ids.ID(w.WorkspaceID, w.WorkspaceName), Visible: w.Visible, Floating: w.Floating, Fullscreen: w.Fullscreen, StashIndex: w.StashIndex, StashCount: w.StashCount, Column: w.Column, Row: w.Row, Hidden: w.Hidden}
143 }145 }
144 146
145 // writeInterval is the least time between two writes of the file.147 // writeInterval is the least time between two writes of the file.
M internal/adapters/statefile/statefile_test.go +3 -3
@@ -67,15 +67,15 @@ func TestRunWritesAndRemoves(t *testing.T) {
6767
6868 // Stash and cell fields reach the file with their script-facing names.
6969 func TestWindowScriptFields(t *testing.T) {
70- got := window(ports.WindowState{ID: 3, Floating: true, StashIndex: 2, StashCount: 4, Column: 5, Row: 6, Hidden: true}, workspaceid.WithPrefix("0a1b2c3d"))
71- if !got.Floating || got.StashIndex != 2 || got.StashCount != 4 || got.Column != 5 || got.Row != 6 || !got.Hidden {
70+ got := window(ports.WindowState{ID: 3, Floating: true, Fullscreen: true, StashIndex: 2, StashCount: 4, Column: 5, Row: 6, Hidden: true}, workspaceid.WithPrefix("0a1b2c3d"))
71+ if !got.Floating || !got.Fullscreen || got.StashIndex != 2 || got.StashCount != 4 || got.Column != 5 || got.Row != 6 || !got.Hidden {
7272 t.Fatalf("%+v", got)
7373 }
7474 data, err := json.Marshal(got)
7575 if err != nil {
7676 t.Fatal(err)
7777 }
78- for _, key := range []string{`"floating":true`, `"stash_index":2`, `"stash_count":4`, `"column":5`, `"row":6`, `"hidden":true`} {
78+ for _, key := range []string{`"floating":true`, `"fullscreen":true`, `"stash_index":2`, `"stash_count":4`, `"column":5`, `"row":6`, `"hidden":true`} {
7979 if !strings.Contains(string(data), key) {
8080 t.Fatalf("%s missing %s", data, key)
8181 }
@@ -67,15 +67,15 @@ func TestRunWritesAndRemoves(t *testing.T) {
67 67
68 // Stash and cell fields reach the file with their script-facing names.68 // Stash and cell fields reach the file with their script-facing names.
69 func TestWindowScriptFields(t *testing.T) {69 func TestWindowScriptFields(t *testing.T) {
70- got := window(ports.WindowState{ID: 3, Floating: true, StashIndex: 2, StashCount: 4, Column: 5, Row: 6, Hidden: true}, workspaceid.WithPrefix("0a1b2c3d"))70+ got := window(ports.WindowState{ID: 3, Floating: true, Fullscreen: true, StashIndex: 2, StashCount: 4, Column: 5, Row: 6, Hidden: true}, workspaceid.WithPrefix("0a1b2c3d"))
71- if !got.Floating || got.StashIndex != 2 || got.StashCount != 4 || got.Column != 5 || got.Row != 6 || !got.Hidden {71+ if !got.Floating || !got.Fullscreen || got.StashIndex != 2 || got.StashCount != 4 || got.Column != 5 || got.Row != 6 || !got.Hidden {
72 t.Fatalf("%+v", got)72 t.Fatalf("%+v", got)
73 }73 }
74 data, err := json.Marshal(got)74 data, err := json.Marshal(got)
75 if err != nil {75 if err != nil {
76 t.Fatal(err)76 t.Fatal(err)
77 }77 }
78- for _, key := range []string{`"floating":true`, `"stash_index":2`, `"stash_count":4`, `"column":5`, `"row":6`, `"hidden":true`} {78+ for _, key := range []string{`"floating":true`, `"fullscreen":true`, `"stash_index":2`, `"stash_count":4`, `"column":5`, `"row":6`, `"hidden":true`} {
79 if !strings.Contains(string(data), key) {79 if !strings.Contains(string(data), key) {
80 t.Fatalf("%s missing %s", data, key)80 t.Fatalf("%s missing %s", data, key)
81 }81 }
M internal/core/screens_test.go +22 -0
@@ -1621,3 +1621,25 @@ func transformReloadClampsPointer(t *testing.T, r *landRig) {
16211621 }
16221622 }
16231623 }
1624+
1625+// The state marks the fullscreen window of a workspace, and only it.
1626+func TestStateReportsFullscreen(t *testing.T) {
1627+ r := startMulti(t, nil, left, right)
1628+ r.key(t, "Left", ports.ModAlt|ports.ModCtrl) // DP-1
1629+ r.mapWindow(t, 1)
1630+ r.mapWindow(t, 2)
1631+ r.key(t, "f", ports.ModAlt|ports.ModShift) // 2 fullscreen
1632+ st := stateAfter(t, r.state, func(st ports.State) bool {
1633+ for _, w := range st.Windows {
1634+ if w.Fullscreen {
1635+ return true
1636+ }
1637+ }
1638+ return false
1639+ })
1640+ for _, w := range st.Windows {
1641+ if w.Fullscreen != (w.ID == 2) {
1642+ t.Fatalf("window %d fullscreen = %v", w.ID, w.Fullscreen)
1643+ }
1644+ }
1645+}
@@ -1621,3 +1621,25 @@ func transformReloadClampsPointer(t *testing.T, r *landRig) {
1621 }1621 }
1622 }1622 }
1623 }1623 }
1624+
1625+// The state marks the fullscreen window of a workspace, and only it.
1626+func TestStateReportsFullscreen(t *testing.T) {
1627+ r := startMulti(t, nil, left, right)
1628+ r.key(t, "Left", ports.ModAlt|ports.ModCtrl) // DP-1
1629+ r.mapWindow(t, 1)
1630+ r.mapWindow(t, 2)
1631+ r.key(t, "f", ports.ModAlt|ports.ModShift) // 2 fullscreen
1632+ st := stateAfter(t, r.state, func(st ports.State) bool {
1633+ for _, w := range st.Windows {
1634+ if w.Fullscreen {
1635+ return true
1636+ }
1637+ }
1638+ return false
1639+ })
1640+ for _, w := range st.Windows {
1641+ if w.Fullscreen != (w.ID == 2) {
1642+ t.Fatalf("window %d fullscreen = %v", w.ID, w.Fullscreen)
1643+ }
1644+ }
1645+}
M internal/core/state.go +1 -1
@@ -67,7 +67,7 @@ func (c *Core) stateInto(st ports.State) ports.State {
6767 c.scratch.ids = w.appendWindows(c.scratch.ids[:0])
6868 for _, id := range c.scratch.ids {
6969 rec := c.windows.lookup(id)
70- v := ports.WindowState{ID: id, AppID: rec.client.AppID, PID: rec.client.PID, Output: s.name(), Workspace: n, WorkspaceID: w.ID, WorkspaceName: w.Name, Visible: shown[id], IdleInhibit: rec.idleInhibit, Floating: w.isFloat(id)}
70+ v := ports.WindowState{ID: id, AppID: rec.client.AppID, PID: rec.client.PID, Output: s.name(), Workspace: n, WorkspaceID: w.ID, WorkspaceName: w.Name, Visible: shown[id], IdleInhibit: rec.idleInhibit, Floating: w.isFloat(id), Fullscreen: w.fullscreen == id}
7171 if i := w.stashIndex(id); i >= 0 {
7272 v.StashIndex, v.StashCount, v.Hidden = i+1, len(w.Stash), w.stashHidden
7373 }
@@ -67,7 +67,7 @@ func (c *Core) stateInto(st ports.State) ports.State {
67 c.scratch.ids = w.appendWindows(c.scratch.ids[:0])67 c.scratch.ids = w.appendWindows(c.scratch.ids[:0])
68 for _, id := range c.scratch.ids {68 for _, id := range c.scratch.ids {
69 rec := c.windows.lookup(id)69 rec := c.windows.lookup(id)
70- v := ports.WindowState{ID: id, AppID: rec.client.AppID, PID: rec.client.PID, Output: s.name(), Workspace: n, WorkspaceID: w.ID, WorkspaceName: w.Name, Visible: shown[id], IdleInhibit: rec.idleInhibit, Floating: w.isFloat(id)}70+ v := ports.WindowState{ID: id, AppID: rec.client.AppID, PID: rec.client.PID, Output: s.name(), Workspace: n, WorkspaceID: w.ID, WorkspaceName: w.Name, Visible: shown[id], IdleInhibit: rec.idleInhibit, Floating: w.isFloat(id), Fullscreen: w.fullscreen == id}
71 if i := w.stashIndex(id); i >= 0 {71 if i := w.stashIndex(id); i >= 0 {
72 v.StashIndex, v.StashCount, v.Hidden = i+1, len(w.Stash), w.stashHidden72 v.StashIndex, v.StashCount, v.Hidden = i+1, len(w.Stash), w.stashHidden
73 }73 }
M internal/ports/state.go +2 -0
@@ -68,6 +68,8 @@ type WindowState struct {
6868 IdleInhibit bool `json:",omitempty"`
6969 // Floating is set for native floats and stashed windows.
7070 Floating bool
71+ // Fullscreen is set for its workspace's fullscreen window.
72+ Fullscreen bool
7173 // StashIndex is the 1-based place of a stashed window in its stash of
7274 // StashCount windows; both are 0 outside the stash.
7375 StashIndex, StashCount int
@@ -68,6 +68,8 @@ type WindowState struct {
68 IdleInhibit bool `json:",omitempty"`68 IdleInhibit bool `json:",omitempty"`
69 // Floating is set for native floats and stashed windows.69 // Floating is set for native floats and stashed windows.
70 Floating bool70 Floating bool
71+ // Fullscreen is set for its workspace's fullscreen window.
72+ Fullscreen bool
71 // StashIndex is the 1-based place of a stashed window in its stash of73 // StashIndex is the 1-based place of a stashed window in its stash of
72 // StashCount windows; both are 0 outside the stash.74 // StashCount windows; both are 0 outside the stash.
73 StashIndex, StashCount int75 StashIndex, StashCount int

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