Skip to content

Working on Gnome 51 - #590

Open
abdullah-rafiq wants to merge 61 commits into
forge-ext:mainfrom
abdullah-rafiq:main
Open

abdullah-rafiq wants to merge 61 commits into
forge-ext:mainfrom
abdullah-rafiq:main

Conversation

@abdullah-rafiq

Copy link
Copy Markdown

I am working on it things still need to be fixed. But it's the first step, and its in working condition

GilJSantana and others added 30 commits October 11, 2026 12:54
A WINDOW node is allowed to hold a null rect, since its branch in the
setter breaks immediately and never touches an actor. split() then copies
that value onto a newly created CON node via `newConNode.rect = node.rect`,
and the CON branch does dereference it, so set_size() throws.

This surfaces as an unhandled `TypeError: rect is null` at tree.js:129,
reached from trackWindow() -> command("Split") -> split() when
auto-split-enabled is true and the window has not been through a
renderTree pass yet, so its rect is still unset.

The throw escapes a display "workareas-changed" handler, which runs while
mutter is reconfiguring monitors and work areas.

Guard the actor geometry calls instead of the assignment, since any caller
can reach the setter with a null rect. The node still records _rect = null
and the following renderTree assigns a real rect and sizes the actor, so
there is no visual change.

Refs forge-ext#540
With "make -j<n>" the prerequisites of "build: clean ..." run
concurrently, so "clean" can delete schemas/gschemas.compiled right
after it was compiled. The installed extension then fails to load with
"gschemas.compiled ... not found". Serialize the Makefile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r broken

Reading ~/.config/forge/config/windows.json did JSON.parse() without a
fallback, so an empty file or a typo in it made enabling Forge fail ("an error
occurred while loading this extension", forge-ext#415). Forge itself could leave the
file empty: loadFile() wrote the default rules to a new file without closing
the stream.

When the file is empty, isn't valid JSON or has no overrides list, use the
default rules and say so with console.warn (Forge's Logger is silent in
release builds). The next change of the rules replaces the file, so first keep
a copy of the broken one as windows.json.bak. Close the stream after writing.

Fixes forge-ext#415

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…issions

patchCss() named the backup after this.configMgr.stylesheetFileName,
which doesn't exist, so it went to "undefined.bak" in GNOME Shell's
working directory (the home folder). Both copies took their source's
permissions: with a read-only install (e.g. NixOS) the user's stylesheet
and the backup became read-only, and at the next update the copy failed
with "Permission denied" and Forge didn't start.

The backup is stylesheet.css.bak next to the stylesheet, the copies get
default permissions, a read-only copy left by an earlier version is
replaced, and a failed update is logged instead of stopping Forge.

Fixes forge-ext#266

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reads

getCssRule() returned {} for a rule it didn't find, and getCssProperty()
read that as a rule ("cssRule.declarations is undefined"), so Forge
didn't start and the preferences didn't open when the user's
stylesheet was empty, lacked a rule (e.g. from an older Forge) or had
a comment in it (no selectors).

Lookups now return null / {} for what is missing. The rules and
properties the user's stylesheet lacks are taken from the default
stylesheet; a stylesheet that can't be parsed is replaced by the
default one. In both cases the completed stylesheet is saved, so the
shell styles everything, and the user's file is kept as
stylesheet.css.bak.

Fixes forge-ext#448

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXwsqMVgdEQYGec49zjaiP
Gio.File.replace_contents() writes to a temporary file and renames it
over the destination when the stream is closed. When a write fails part
way, for example with ENOSPC on a full disk, GLib still closes the stream
normally, so the partially written temporary file replaces the original.
On a full disk that file is empty.

Forge saves windows.json every time a window is closed, so a full disk
empties it and the next login fails with "JSON.parse: unexpected end of
data" (forge-ext#415). The stylesheet is saved the same way.

GLib.file_set_contents() removes its temporary file when a write fails
and leaves the original in place. Like the REPLACE_DESTINATION flag used
before, it replaces symlinks and does not keep the old permissions.
Use maximize flags so single-axis tiled windows are recognized, and avoid a second unmaximize call on Mutter 49+. Mutter 51 can restore an unmaximized window's old configured rectangle during that redundant call, causing tiled Wayland windows to bounce across monitors.

Adapted from jcrussell/forge commit 6653216 and forge-ext/forge PR forge-ext#589.
Tree.split() pushed a window down into a new container by creating a new
node for it, leaving the old node detached but with its parentNode still
set. The new node lost the window's mode (a window being dragged turned
back from GRAB_TILE to TILE) and state, and the old one stayed
referenced, e.g. as the drag-and-drop target.

When such a detached node was appended elsewhere, removeChild() looked it
up by value, found the window's other node, and spliced at node.index,
which is null for a detached node: splice(null, 1) removed the parent's
first child, another window. The detached node then joined the tree as
a second node of the same window, and every render moved the window to
both tiles.

Move the window's own node into the new container in split(), make
removeChild() detach exactly the given node and clear its parentNode,
and drop any duplicate window node before rendering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The float toggle (window-toggle-float) saves a per-window rule, with the
window id, in windows.json. Three problems made it fail intermittently:

- addFloatOverride() returned early when any float rule existed for the
  window class, including a rule for another window of that class, so a
  second terminal, say, could not be floated.
- Window ids only hold for a session, but the per-window rules stayed in
  windows.json across sessions, where they blocked the case above and
  could match an unrelated window.
- reloadWindowOverrides() dropped the per-window rules from the cache,
  so a reload triggered from the preferences tiled back the windows
  floated with the toggle.

Only treat a class-wide rule, or this window's own rule, as "already
floating"; purge per-window rules left by previous sessions at startup;
and keep the current session's per-window rules when reloading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Let focus-update and raise-float events opt into coalescing so a rapid focus burst cannot build a long queue of redundant renders. Their callbacks inspect the current focus when dispatched.

Adapted from forge-ext/forge PR forge-ext#589 commit 0c67b1b.
Do not steal focus while the shell is modal or locked. Treat popup, notification, and override-redirect windows as occluders, and only focus when the pointer enters another eligible window.

Adapted from jcrussell/forge commit 9354f87.
Window wrappers can outlive their compositor actor. Route focus, raise, and activate calls through a managed-window check so close and swap races do not invoke Mutter on an unmanaged window.

Adapted from jcrussell/forge commit 7bb2af9.
Wait for unmapped Wayland toplevels to become mapped before adding them to the tiling tree, clean up their temporary signal watches on unmanage or disable, and skip redundant geometry requests within one pixel. Preserve the initial map animation on first placement.

Adapted from forge-ext/forge PR forge-ext#589 and jcrussell/forge GNOME 51 fixes.
Wayland may report a temporary monitor crossing while a tiled window is being resized. Keep the tree node on its current monitor until the next render restores its tile, while allowing floats and user-driven drags to move normally.

Adapted from forge-ext/forge PR forge-ext#589 commit b2202e5.
Auto-split only when the focused node has a computed rectangle and parent. Splitting an unplaced focused window could leave a new window untracked and break the next float toggle.

Adapted from forge-ext/forge PR forge-ext#589 commit dd924c3.
Only clear the firstRender marker when a geometry request succeeds or the window is already in its tile. A new Wayland window may be rendered before its first frame, and clearing the marker on that skipped move made the next placement strip the opening animation.

Adapted from jcrussell/forge first-placement animation handling.
Create missing user config files even when their parent directories already exist, and avoid falling back to writing the bundled windows.json or stylesheet.css when the user config cannot be created. Report failed custom-file creation without modifying extension defaults.

Adapted from forge-ext/forge PR forge-ext#536 and PR forge-ext#587.
abdullah-rafiq and others added 29 commits October 29, 2026 22:26
Synchronously re-home surviving windows before removing a doomed workspace subtree so windows are not stranded without tiling. Re-number workspace and monitor tree nodes on workspace removal and addition so index-keyed lookups remain valid. Rekey workspace signal entries in two phases and anchor them to their MetaWorkspace object so reorders disconnect the correct workspace without handler leaks. Tear down monitor bins and container decorations during workspace removal and scaffold cleanup on disable.
…clamping

- utils.js: add getMonitorGeometrySafe() null-safe wrapper
- window.js determineSplitLayout: use getMonitorGeometrySafe instead of raw call
- window.js move(): clamp committed frame to work area; accept optional workArea param
- tree.js move() edge-wrap branch: capture priorParent, call _finishMove before return
- tree.js move() cross-monitor branch: do geometry move before reparent (finalized safety); pass workArea to extWm.move()
- tree.js move(): refactor end to call shared _finishMove(parentNode, parentTarget)
- tree.js: add _finishMove() shared epilogue for structural moves (resetSiblingPercent + resetLayoutSingleChild)
- tree.js processStacked(): cap totalBars to nodeHeight-1 so content stays in container
- tree.js processTabbed(): guard nodeHeight to Math.max(..., 1) preventing negative height
The workspace-removal port called Utils.isWindowAlive and
Utils.createMonitorWorkspaceId, but neither helper existed, so the
synchronous rehome threw and its catch skipped every window. Mutter also
shifts workspace indices before emitting workspace-removed while the tree
is renumbered after the rehome, so windows relocated into a later index
resolved to the doomed workspace node and were removed with it. Map live
indices back to the tree's old keys during removal, rebuild the scaffold
when a workspace count change emits no add/remove signal, and stop MODE
searches from falling through into LAYOUT matching.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Applying a tile rectangle to a fullscreen window fought Mutter's
fullscreen geometry on every render, and a window tracked while
get_current_monitor() transiently reported -1 built an unresolvable
monitor key and scheduled a reload that retried it. Skip fullscreen
windows in placement, reject moves while fullscreen, and fall back to the
window's monitor when the current monitor is invalid.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Closing the last window of a single-child sub-container collapsed that
container, but the auto-exit-tabbed check then inspected the detached
sub-container instead of the tabbed grandparent, leaving a one-tab layout
stuck in tabbed mode. Also stop the live-resize polling source in
_removeSignals so disabling during a grab cannot recreate a tree.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Closing the last window in one container reset the percentages of every
sibling at the monitor and workspace levels, discarding split proportions
the user had resized. Reset only within the container that actually lost
its tiled children.
Validate get_monitor() through one helper before every remaining
workspace/monitor lookup, fall back to the node's monitor while rehoming,
and catch invalid per-monitor work areas. Stacked layout now uses the same
DPI-scaled bar height as tabbed, gives every window the shared content
area below a vertical tab column, and shares decoration placement with
tabbed so both layouts stay aligned.
Skip finalized windows while cleaning the tree instead of reading their
title and class, give drag-created containers the settings object the rest
of the tree uses, and clear child tab references when a decoration is
destroyed so stacked/tabbed containers can rebuild tabs after they are
detached and reattached.
The Windows checkout wrote CRLF into the Makefile, so make inside WSL
failed on carriage returns in every recipe; force LF for it in
.gitattributes. Also drop the literal quotes around the UUID, replace the
install directory instead of layering over stale files, skip gettext
cleanly when it is not installed, and report a missing
glib-compile-schemas instead of failing with a bare command error.
Disable Forge, rebuild and replace its install directory, then re-enable
it, so testing a new build no longer needs three manual commands while the
running Shell may still have old modules loaded.
A download without a .git directory made bare `git shortlog` read stdin
and wait forever during the metadata step. Pass HEAD so git fails fast,
skip shortlog outside a work tree, and emit a fallback contributor entry
so the preferences About page still builds.
gnome-extensions resolves user extensions from XDG_DATA_HOME, so a custom
data home installed Forge into a directory the Shell never scanned and
reported that the extension did not exist.
The Tree constructor binds a signal to every workspace, but
WindowManager created the Tree before allocating _workspaceSignals, so
the first bindWorkspaceSignals() call threw on an undefined Map and left
the extension in the ERROR state. Allocate the signal maps and pending
window set before the Tree is constructed.
…n disable

The tree binds workspace signals from its own constructor, so any code path
that reaches bindWorkspaceSignals before the WindowManager constructor finishes
threw on an undefined Map and left enable() in an error state. Create the
bookkeeping lazily instead of relying on field order, and release the
hover-focus timeout during disable instead of leaving it to expire.
On Wayland Desktop Icons NG draws the desktop as an ordinary NORMAL window
titled @!<x>,<y>;BDHF. Forge tracked it as a tile, unmaximized it, and DING
re-maximized and raised itself, putting the icons above every window and
stealing clicks.
A workspace window-added timer or a queued event could fire after disable()
tore down the tree and the extension nulled its settings, giving
"TypeError: can't access property get_boolean, this.ext.settings is null"
(repeatedly seen on GNOME 51 Wayland). Guard the render path and the
scheduled callbacks so late work is dropped instead of resurrecting a tree.
Current DING titles the desktop window "Desktop Icons <monitor+1>" (hardcoded,
not translated) and only calls set_type(DESKTOP) after the window exists, so
Forge still tracked it before the type changed. Match that title too; the
GNOME 51 journal showed "Meta Window Desktop Icons 1" being tiled.
Move the title rules into a dependency-free module so the DING formats can be
tested without GNOME Shell, and run them with node:test via `npm run test:unit`.
Covers both the current "Desktop Icons <n>" title and the older @!x,y;BDHF form,
plus ordinary titles that must not match.
`tsconfig.json` already had allowJs/checkJs but nothing ran it and the
compiler was not installed. Add TypeScript, stubs for the shell modules
and gi://Shell (no typings package exists), and a script that fails CI
only on new errors; the 52 known ones are recorded in
types/typecheck-baseline.json so the check can ratchet down.

The annotation pass also found two GLib callbacks that still reached into
the tree after disable(): the reload idle and the live-resize tick. Both
re-check `disabled`, because a source that is already dispatching cannot
be removed. Typing createEnum() as Record<T, T> keeps the enum member
names, which removed 239 spurious type errors.
Type-checking is now clean (0 errors, down from 448). Beyond annotations,
the checker surfaced real defects: tabbed/stacked center drops and swaps
could reach set_style_class_name(undefined), two grab-op branches compared
against constants GNOME 51 does not define, and the deprecated
enableListenForBindings() path referenced an undefined windowConfig.
The extension clears its services at teardown, so every consumer saw a
nullable settings/theme/tree. Declaring those non-nullable and breaking
the type once, in the teardown cast, lets the checker catch the real
hazards instead: nullable GSettings.get_string()/File.get_path() results,
unguarded createNode() results, and a border width that handed the "3"
string from removePx() to a double GObject property.
…ng tab icons

Ported from jcrussell/forge (forge-qy65, forge-hcbz, forge-2s37). At integer
HiDPI the tab icon was scaled twice (create_icon_texture already takes a
logical size), while the border inset and window gaps stayed unscaled against
physical-pixel rects, so borders painted over content and gaps visually halved.
Three defects the reference fork had already closed:

- Logger kept the Gio.Settings handed to init() past disable(), so a disabled
  extension kept reading logging-enabled off a stranded object (forge-l2f7).
- _updateCss() would overwrite a user stylesheet with a truncated round-trip
  when the parser had recorded errors, and left the AST diverged from disk when
  the write failed (forge-y3jy, forge-rohk).
- Finalized Meta.Window wrappers are truthy, so the swap/resize pair-walk could
  throw on .minimized before the prune ran (forge-3cfd, forge-ib39).
get_workspace() throws on a finalized wrapper, so re-reading it after the
monitor and node lookups races the null guard above. Captured once, matching
the existing isWindowManaged liveness probes (upstream forge-ib39).
Both were hardcoded: the bar height was a 35px literal and the bar always sat
above the content. Height is now read live from a clamped schema key, and the
bar can be pinned to the bottom of its container, moving the content anchor
rather than the reserved height. Ported from jcrussell/forge (forge-1a5,
forge-drf) with the metadata-driven re-render wired in.
@abdullah-rafiq abdullah-rafiq changed the title Some what Working on Gnome 41 Working on Gnome 51 Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants