feat(state): report fullscreen windows
- commit
- 60e7285b70507af218302663044daffa7449c384
- author
- brice · · 2026-10-23 00:55 UTC
- parent
- f2d166b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 131 | 131 | ||
| 132 | 132 | ## 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 | 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 | 139 | neferwl state # the whole state | |
| 140 | 140 | neferwl state output-of "$PID" # the output showing that process's window | |
| 141 | 141 | neferwl state | jq '[.windows[] | select(.hidden)] | length' # windows in hidden stashes | |
| 142 | + | neferwl state | jq '[.windows[] | select(.fullscreen)] | length' # fullscreen windows | |
| 142 | 143 | neferwl state | jq '.workspaces[] | select(.visible) | .columns[] | .width' # column widths on screen | |
| 143 | 144 | ``` | |
| @@ -131,7 +131,7 @@ Workspace IDs are `name:<name>` for named workspaces, stable across launches. Nu | |||||
| 131 | 131 | ||||
| 132 | ## State for scripts | 132 | ## 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 state | 139 | neferwl state # the whole state | ||
| 140 | neferwl state output-of "$PID" # the output showing that process's window | 140 | neferwl state output-of "$PID" # the output showing that process's window | ||
| 141 | neferwl state | jq '[.windows[] | select(.hidden)] | length' # windows in hidden stashes | 141 | 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 screen | 143 | 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 { | |||
| 100 | 100 | Visible bool `json:"visible"` | |
| 101 | 101 | // Floating is set for native floats (dialogs) and stashed windows. | |
| 102 | 102 | Floating bool `json:"floating"` | |
| 103 | + | // Fullscreen is set for the fullscreen window of its workspace. | |
| 104 | + | Fullscreen bool `json:"fullscreen"` | |
| 103 | 105 | // StashIndex is the 1-based place of a stashed window in its | |
| 104 | 106 | // workspace's stash of StashCount windows; both are 0 outside it. | |
| 105 | 107 | StashIndex int `json:"stash_index"` | |
| @@ -139,7 +141,7 @@ func fromPorts(st ports.State, ids *workspaceid.IDs) State { | |||
| 139 | 141 | } | |
| 140 | 142 | ||
| 141 | 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 | 147 | // 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 its | 105 | // 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) { | |||
| 67 | 67 | ||
| 68 | 68 | // Stash and cell fields reach the file with their script-facing names. | |
| 69 | 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")) | |
| 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 { | |
| 72 | 72 | t.Fatalf("%+v", got) | |
| 73 | 73 | } | |
| 74 | 74 | data, err := json.Marshal(got) | |
| 75 | 75 | if err != nil { | |
| 76 | 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 | 79 | if !strings.Contains(string(data), key) { | |
| 80 | 80 | t.Fatalf("%s missing %s", data, key) | |
| 81 | 81 | } | |
| @@ -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) { | |||
| 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 | + | } | |
| @@ -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 { | |||
| 67 | 67 | c.scratch.ids = w.appendWindows(c.scratch.ids[:0]) | |
| 68 | 68 | for _, id := range c.scratch.ids { | |
| 69 | 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 | 71 | if i := w.stashIndex(id); i >= 0 { | |
| 72 | 72 | v.StashIndex, v.StashCount, v.Hidden = i+1, len(w.Stash), w.stashHidden | |
| 73 | 73 | } | |
| @@ -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.stashHidden | 72 | 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 { | |||
| 68 | 68 | IdleInhibit bool `json:",omitempty"` | |
| 69 | 69 | // Floating is set for native floats and stashed windows. | |
| 70 | 70 | Floating bool | |
| 71 | + | // Fullscreen is set for its workspace's fullscreen window. | |
| 72 | + | Fullscreen bool | |
| 71 | 73 | // StashIndex is the 1-based place of a stashed window in its stash of | |
| 72 | 74 | // StashCount windows; both are 0 outside the stash. | |
| 73 | 75 | 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 bool | 70 | 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 of | 73 | // 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 int | 75 | StashIndex, StashCount int | ||