From 342a72a8b69d41f344ccdabdb11ffe1b86ee6cd7 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Jul 2026 19:54:52 -0600 Subject: [PATCH 1/6] fix(avatar-overlay): add support for full-window input mode on Linux and handle input shape based on overlay configuration --- scripts/patch-linux-window-ui.test.js | 133 +++++++++++++++++++++++++ scripts/patches/impl/avatar-overlay.js | 4 +- 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/scripts/patch-linux-window-ui.test.js b/scripts/patch-linux-window-ui.test.js index 82fcd4f93..1e6f282ea 100644 --- a/scripts/patch-linux-window-ui.test.js +++ b/scripts/patch-linux-window-ui.test.js @@ -3391,6 +3391,7 @@ test("adds Linux avatar overlay mouse passthrough recovery", () => { assert.match(patched, /codexLinuxSyncAvatarPointerInteractivity\(e\)/); assert.match(patched, /codexLinuxBuildAvatarInputShape\(e\)/); assert.match(patched, /codexLinuxApplyAvatarInputShape\(e\)/); + assert.match(patched, /codexLinuxShouldUseWholeWindowInput\(\)\{return this\.codexLinuxWholeWindowInput===!0\}/); assert.match(patched, /codexLinuxIsI3Session\(\)/); assert.match(patched, /process\.env\.I3SOCK/); assert.match(patched, /codexLinuxApplyAvatarCompositorHints\(e\)/); @@ -3479,6 +3480,126 @@ test("keeps the avatar overlay core patch idempotent after pet overlay compositi assert.deepEqual(warnings, []); }); +test("pet overlay opts into full-window input on X11 and Wayland", () => { + const patched = applyPetOverlayPatch( + applyLinuxOpaqueBackgroundPatch( + applyLinuxAvatarOverlayMousePassthroughPatch( + `${latestAvatarOverlayBundleFixture()}${currentOpaqueWindowSurfaceBackgroundBundle}`, + ), + ), + ); + const cursor = { x: 100, y: 100 }; + let ozonePlatform = "x11"; + const context = { + globalThis: {}, + clearInterval() {}, + process: { env: {}, platform: "linux" }, + require(moduleName) { + if (moduleName === "node:child_process") return { execFile() {} }; + assert.equal(moduleName, "electron"); + return { + app: { commandLine: { getSwitchValue: () => ozonePlatform }, getName: () => "Codex" }, + screen: { getCursorScreenPoint: () => cursor }, + }; + }, + setInterval() { + return { unref() {} }; + }, + }; + vm.runInNewContext(`${patched};globalThis.AvatarOverlayController=fV;`, context); + const controller = new context.globalThis.AvatarOverlayController( + { sendMessageToAllRegisteredWindows() {} }, + { set() {} }, + ); + controller.layout = { + mascot: { left: 220, top: 190, width: 113, height: 122 }, + tray: { left: 57, top: 55, width: 276, height: 131 }, + }; + const window = { + getContentBounds: () => ({ x: 0, y: 0, width: 356, height: 320 }), + isDestroyed: () => false, + isVisible: () => true, + setIgnoreMouseEvents() {}, + setShape() {}, + }; + + controller.codexPetOverlaySyncWindow(window); + assert.equal(controller.codexLinuxWholeWindowInput, true); + + assert.deepEqual(JSON.parse(JSON.stringify(controller.codexLinuxBuildAvatarInputShape(window))), [ + { x: 0, y: 0, width: 356, height: 320 }, + ]); + cursor.x = 10; + cursor.y = 300; + assert.equal(controller.codexLinuxIsCursorInAvatarInteractiveRegion(window), true); + ozonePlatform = "wayland"; + controller.pointerInteractive = false; + assert.equal(controller.codexLinuxIsAvatarShapeBackend(), false); + assert.equal(controller.codexLinuxSyncAvatarPointerInteractivity(window), true); + assert.equal(controller.pointerInteractive, true); +}); + +test("locked pet overlay keeps only mascot and tray interactive on X11 and Wayland", () => { + const patched = applyPetOverlayPatch( + applyLinuxOpaqueBackgroundPatch( + applyLinuxAvatarOverlayMousePassthroughPatch( + `${latestAvatarOverlayBundleFixture()}${currentOpaqueWindowSurfaceBackgroundBundle}`, + ), + ), + { feature: { manifest: { petOverlay: { lockPosition: true } }, settings: {} } }, + ); + const cursor = { x: 10, y: 300 }; + let ozonePlatform = "x11"; + const context = { + globalThis: {}, + clearInterval() {}, + process: { env: {}, platform: "linux" }, + require(moduleName) { + if (moduleName === "node:child_process") return { execFile() {} }; + assert.equal(moduleName, "electron"); + return { + app: { commandLine: { getSwitchValue: () => ozonePlatform }, getName: () => "Codex" }, + screen: { getCursorScreenPoint: () => cursor }, + }; + }, + setInterval() { + return { unref() {} }; + }, + }; + vm.runInNewContext(`${patched};globalThis.AvatarOverlayController=fV;`, context); + const controller = new context.globalThis.AvatarOverlayController( + { sendMessageToAllRegisteredWindows() {} }, + { set() {} }, + ); + controller.layout = { + mascot: { left: 220, top: 190, width: 113, height: 122 }, + tray: { left: 57, top: 55, width: 276, height: 131 }, + }; + const ignored = []; + const window = { + getContentBounds: () => ({ x: 0, y: 0, width: 356, height: 320 }), + isDestroyed: () => false, + isVisible: () => true, + setIgnoreMouseEvents: (...args) => ignored.push(args), + setShape() {}, + }; + + controller.codexPetOverlaySyncWindow(window); + assert.equal(controller.codexLinuxWholeWindowInput, false); + assert.deepEqual(JSON.parse(JSON.stringify(controller.codexLinuxBuildAvatarInputShape(window))), [ + { x: 220, y: 190, width: 113, height: 122 }, + { x: 57, y: 55, width: 276, height: 131 }, + ]); + + ozonePlatform = "wayland"; + controller.window = window; + controller.pointerInteractive = true; + assert.equal(controller.codexLinuxIsAvatarShapeBackend(), false); + assert.equal(controller.codexLinuxIsCursorInAvatarInteractiveRegion(window), false); + controller.applyPointerInteractivityPolicy(); + assert.deepEqual(JSON.parse(JSON.stringify(ignored)), [[true, { forward: true }]]); +}); + test("keeps Linux avatar overlay above the app while reply inputs are focusable", () => { const patched = applyPatchTwice( applyLinuxAvatarOverlayMousePassthroughPatch, @@ -3581,6 +3702,18 @@ test("Linux avatar overlay interactivity is bounded to avatar regions", () => { { x: 0, y: 0, width: 356, height: 320 }, ]); controller.dragState = null; + assert.equal(controller.codexLinuxShouldUseWholeWindowInput(), false); + controller.codexLinuxWholeWindowInput = true; + assert.deepEqual(serializeShape(controller.codexLinuxBuildAvatarInputShape(overlayWindow)), [ + { x: 0, y: 0, width: 356, height: 320 }, + ]); + assert.equal( + controller.codexLinuxIsCursorInAvatarInteractiveRegion({ + getContentBounds: () => ({ x: 5743, y: 936, width: 356, height: 320 }), + }), + true, + ); + controller.codexLinuxWholeWindowInput = false; context.process.env.WAYLAND_DISPLAY = "wayland-0"; assert.equal(controller.codexLinuxIsAvatarShapeBackend(), false); assert.equal(controller.codexLinuxApplyAvatarInputShape(overlayWindow), false); diff --git a/scripts/patches/impl/avatar-overlay.js b/scripts/patches/impl/avatar-overlay.js index 3f9cc06d9..405436047 100644 --- a/scripts/patches/impl/avatar-overlay.js +++ b/scripts/patches/impl/avatar-overlay.js @@ -77,11 +77,11 @@ function replaceAvatarMethodText(source, method, replacement) { } function avatarCursorRegionPatch(electronVar) { - return `codexLinuxIsCursorInAvatarInteractiveRegion(e){let t=this.layout;if(t==null)return!1;let __codexCursor=${electronVar}.screen.getCursorScreenPoint(),__codexBounds=e.getContentBounds(),__codexX=__codexCursor.x-__codexBounds.x,__codexY=__codexCursor.y-__codexBounds.y,__codexWindowHit=__codexX>=0&&__codexY>=0&&__codexX<=__codexBounds.width&&__codexY<=__codexBounds.height;if(!__codexWindowHit)return!1;let __codexHit=e=>e!=null&&__codexX>=e.left&&__codexX<=e.left+e.width&&__codexY>=e.top&&__codexY<=e.top+e.height;return __codexHit(t.mascot)||__codexHit(t.tray)}`; + return `codexLinuxIsCursorInAvatarInteractiveRegion(e){let t=this.layout;if(t==null)return!1;let __codexCursor=${electronVar}.screen.getCursorScreenPoint(),__codexBounds=e.getContentBounds(),__codexX=__codexCursor.x-__codexBounds.x,__codexY=__codexCursor.y-__codexBounds.y,__codexWindowHit=__codexX>=0&&__codexY>=0&&__codexX<=__codexBounds.width&&__codexY<=__codexBounds.height;if(!__codexWindowHit)return!1;if(this.codexLinuxShouldUseWholeWindowInput())return!0;let __codexHit=e=>e!=null&&__codexX>=e.left&&__codexX<=e.left+e.width&&__codexY>=e.top&&__codexY<=e.top+e.height;return __codexHit(t.mascot)||__codexHit(t.tray)}`; } function avatarInputShapePatch() { - return "codexLinuxBuildAvatarInputShape(e){let t=this.layout;if(t==null)return null;let r;try{r=e.getContentBounds()}catch{return null}if(r==null||!Number.isFinite(r.width)||!Number.isFinite(r.height))return null;if(this.dragState!=null)return[{x:0,y:0,width:r.width,height:r.height}];let i=e=>{if(e==null)return null;let t=Math.max(0,e.left),n=Math.max(0,e.top),i=Math.min(r.width,e.left+e.width)-t,a=Math.min(r.height,e.top+e.height)-n;return i<=0||a<=0?null:{x:t,y:n,width:i,height:a}};return[i(t.mascot),i(t.tray)].filter(Boolean)}"; + return "codexLinuxShouldUseWholeWindowInput(){return this.codexLinuxWholeWindowInput===!0}codexLinuxBuildAvatarInputShape(e){let t=this.layout;if(t==null)return null;let r;try{r=e.getContentBounds()}catch{return null}if(r==null||!Number.isFinite(r.width)||!Number.isFinite(r.height))return null;if(this.dragState!=null||this.codexLinuxShouldUseWholeWindowInput())return[{x:0,y:0,width:r.width,height:r.height}];let i=e=>{if(e==null)return null;let t=Math.max(0,e.left),n=Math.max(0,e.top),i=Math.min(r.width,e.left+e.width)-t,a=Math.min(r.height,e.top+e.height)-n;return i<=0||a<=0?null:{x:t,y:n,width:i,height:a}};return[i(t.mascot),i(t.tray)].filter(Boolean)}"; } function avatarApplyInputShapePatch() { From 9da7416ae8286c849f0ae407c9714dee454b3dea Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Jul 2026 19:55:02 -0600 Subject: [PATCH 2/6] feat(pet-overlay): add draggable mascot support with tray positioning improvements and native drag handling for Linux --- linux-features/pet-overlay/README.md | 13 +- linux-features/pet-overlay/patch.js | 244 ++++---- linux-features/pet-overlay/test.js | 840 ++++++++++----------------- 3 files changed, 413 insertions(+), 684 deletions(-) diff --git a/linux-features/pet-overlay/README.md b/linux-features/pet-overlay/README.md index 399532e23..2501991f7 100644 --- a/linux-features/pet-overlay/README.md +++ b/linux-features/pet-overlay/README.md @@ -46,8 +46,13 @@ Feature settings can be overridden in the gitignored `features.json` file: ``` `lockPosition: false` preserves manual pet moves when the existing window -position is visible. Set it to `true` only when you want the mascot pinned to -the configured screen corner on every layout pass. +position is visible. In this default unlocked mode the overlay is frameless: +transparent space moves the native `356×320` window, while the mascot is a +`no-drag` region so its renderer gesture can reposition the mascot within the +view. Existing tray controls keep their own interactive regions. Set +`lockPosition` to `true` only when you want the mascot pinned to the configured +screen corner on every layout pass; that mode removes the full-surface +drag/input opt-in. ## Options @@ -164,6 +169,10 @@ node --test linux-features/pet-overlay/test.js For a manual check, enable the feature, rebuild, and launch the app: - The pet overlay should remain transparent. +- With `lockPosition: false`, it should be frameless; drag transparent space + to move the window, and drag the mascot to reposition it inside the view. + Tray controls should remain clickable, and the window position lasts only + for the running session. - Selecting a different pet should update the open overlay without restarting Codex. - On Hyprland, the pet should have no visible compositor border or shadow. - On Niri, the pet should open floating, avoid initial focus, and move by diff --git a/linux-features/pet-overlay/patch.js b/linux-features/pet-overlay/patch.js index adb0d5606..f7fb37bcf 100644 --- a/linux-features/pet-overlay/patch.js +++ b/linux-features/pet-overlay/patch.js @@ -2,7 +2,6 @@ const VALID_GRAVITIES = new Set(["bottom-right", "bottom-left", "top-right", "top-left"]); const DESCRIPTOR_ID = "pet-overlay-main"; -const POINTER_REGION_DESCRIPTOR_ID = "pet-overlay-pointer-region"; const AVATAR_SELECTION_REFRESH_MARKER = "codexPetOverlayRefreshAvatarWindows"; function findMatchingBrace(source, openIndex) { @@ -65,7 +64,6 @@ function mergedPetOverlaySettings(context = {}) { alwaysOnTop: booleanSetting(overrides.alwaysOnTop, booleanSetting(defaults.alwaysOnTop, true)), gravity, hyprland: booleanSetting(overrides.hyprland, booleanSetting(defaults.hyprland, true)), - kwin: booleanSetting(overrides.kwin, booleanSetting(defaults.kwin, true)), lockPosition: booleanSetting(overrides.lockPosition, booleanSetting(defaults.lockPosition, false)), margin: integerSetting(overrides.margin ?? defaults.margin, 24, 0, 512), mode, @@ -150,7 +148,7 @@ function firstMethodArgument(methodText, methodName, index) { function buildPetOverlayMethods(settings) { return [ - `codexPetOverlaySettings(){let e={margin:${settings.margin},gravity:\`${settings.gravity}\`,allWorkspaces:${boolLiteral(settings.allWorkspaces)},alwaysOnTop:${boolLiteral(settings.alwaysOnTop)},skipTaskbar:${boolLiteral(settings.skipTaskbar)},lockPosition:${boolLiteral(settings.lockPosition)},mode:\`${settings.mode}\`,hyprland:${boolLiteral(settings.hyprland)},kwin:${boolLiteral(settings.kwin)},niri:${boolLiteral(settings.niri)}};try{let t=process.env.CODEX_PET_OVERLAY_MARGIN??process.env.CODEX_PET_LINUX_MARGIN,n=Number(t);Number.isFinite(n)&&(e.margin=Math.max(0,Math.min(512,Math.round(n))));let r=process.env.CODEX_PET_OVERLAY_GRAVITY??process.env.CODEX_PET_LINUX_GRAVITY;[\`bottom-right\`,\`bottom-left\`,\`top-right\`,\`top-left\`].includes(r)&&(e.gravity=r);let i=process.env.CODEX_PET_OVERLAY_MODE??process.env.CODEX_PET_LINUX_MODE;(i===\`interactive\`||i===\`passive\`)&&(e.mode=i);let a=process.env.CODEX_PET_OVERLAY_LOCK_POSITION??process.env.CODEX_PET_LINUX_LOCK_POSITION;a===\`1\`&&(e.lockPosition=!0),a===\`0\`&&(e.lockPosition=!1);let o=process.env.CODEX_PET_OVERLAY_HYPRLAND??process.env.CODEX_PET_LINUX_HYPRLAND;o===\`1\`&&(e.hyprland=!0),o===\`0\`&&(e.hyprland=!1);let s=process.env.CODEX_PET_OVERLAY_KWIN;s===\`1\`&&(e.kwin=!0),s===\`0\`&&(e.kwin=!1);let c=process.env.CODEX_PET_OVERLAY_NIRI;c===\`1\`&&(e.niri=!0),c===\`0\`&&(e.niri=!1)}catch{}return e}`, + `codexPetOverlaySettings(){let e={margin:${settings.margin},gravity:\`${settings.gravity}\`,allWorkspaces:${boolLiteral(settings.allWorkspaces)},alwaysOnTop:${boolLiteral(settings.alwaysOnTop)},skipTaskbar:${boolLiteral(settings.skipTaskbar)},lockPosition:${boolLiteral(settings.lockPosition)},mode:\`${settings.mode}\`,hyprland:${boolLiteral(settings.hyprland)},niri:${boolLiteral(settings.niri)}};try{let t=process.env.CODEX_PET_OVERLAY_MARGIN??process.env.CODEX_PET_LINUX_MARGIN,n=Number(t);Number.isFinite(n)&&(e.margin=Math.max(0,Math.min(512,Math.round(n))));let r=process.env.CODEX_PET_OVERLAY_GRAVITY??process.env.CODEX_PET_LINUX_GRAVITY;[\`bottom-right\`,\`bottom-left\`,\`top-right\`,\`top-left\`].includes(r)&&(e.gravity=r);let i=process.env.CODEX_PET_OVERLAY_MODE??process.env.CODEX_PET_LINUX_MODE;(i===\`interactive\`||i===\`passive\`)&&(e.mode=i);let a=process.env.CODEX_PET_OVERLAY_LOCK_POSITION??process.env.CODEX_PET_LINUX_LOCK_POSITION;a===\`1\`&&(e.lockPosition=!0),a===\`0\`&&(e.lockPosition=!1);let o=process.env.CODEX_PET_OVERLAY_HYPRLAND??process.env.CODEX_PET_LINUX_HYPRLAND;o===\`1\`&&(e.hyprland=!0),o===\`0\`&&(e.hyprland=!1);let s=process.env.CODEX_PET_OVERLAY_NIRI;s===\`1\`&&(e.niri=!0),s===\`0\`&&(e.niri=!1)}catch{}return e}`, "codexPetOverlayRect(e){if(e==null)return null;let t=Number(e.x),n=Number(e.y),r=Number(e.width),i=Number(e.height);return[t,n,r,i].every(Number.isFinite)&&r>0&&i>0?{x:t,y:n,width:r,height:i}:null}", "codexPetOverlayDisplayRect(e){return this.codexPetOverlayRect(e?.workArea??e?.bounds??e)}", "codexPetOverlayWindowBounds(e){try{return this.codexPetOverlayRect(e?.getBounds?.()??e?.getContentBounds?.())}catch{return null}}", @@ -159,12 +157,17 @@ function buildPetOverlayMethods(settings) { "codexPetOverlayMascotRect(e){let t=e?.mascot;if(t==null)return null;let n=Number(t.left),r=Number(t.top),i=Number(t.width),a=Number(t.height);return[n,r,i,a].every(Number.isFinite)&&i>0&&a>0?{left:n,top:r,width:i,height:a}:null}", "codexPetOverlayLayoutAtWindowPosition(e,t){if(e==null||t==null||t.windowBounds==null)return t;let n={...t.windowBounds,x:Math.round(e.x),y:Math.round(e.y)},r=this.codexPetOverlayMascotRect(t),i=r==null?{x:n.x,y:n.y,width:t.anchor?.width??n.width,height:t.anchor?.height??n.height}:{x:n.x+r.left,y:n.y+r.top,width:r.width,height:r.height},a=t.anchor==null?t.anchor:{...t.anchor,x:Math.round(i.x),y:Math.round(i.y),width:t.anchor.width??i.width,height:t.anchor.height??i.height};return{...t,anchor:a,windowBounds:n}}", "codexPetOverlayGravityBounds(e,t,n){if(e==null||t==null||t.windowBounds==null)return null;let r={...t.windowBounds},i=this.codexPetOverlayMascotRect(t)??{left:0,top:0,width:Number(r.width),height:Number(r.height)},a=Number(i.left),o=Number(i.top),s=Number(i.width),c=Number(i.height);if(![a,o,s,c].every(Number.isFinite)||s<=0||c<=0)return null;let l=Math.max(0,Math.min(512,Number(n?.margin)||0)),u=String(n?.gravity??`bottom-right`);return r.x=u.endsWith(`left`)?Math.round(e.x+l-a):Math.round(e.x+e.width-l-a-s),r.y=u.startsWith(`top`)?Math.round(e.y+l-o):Math.round(e.y+e.height-l-o-c),r}", - "codexPetOverlayTrayAboveLeft(e){if(process.platform!==`linux`||e==null||e.windowBounds==null||e.mascot==null||e.tray==null)return e;let t=Number(e.windowBounds.width),n=Number(e.windowBounds.height),r=Number(e.mascot.width),i=Number(e.mascot.height),a=Number(e.tray.width),o=Number(e.tray.height);if(![t,n,r,i,a,o].every(Number.isFinite)||t<=0||n<=0||r<=0||i<=0||a<=0||o<=0)return e;let s=Math.max(0,Math.round(t-r)),c=Math.max(0,Math.round(n-i)),l=Math.max(0,Math.min(Math.round(t-a),Math.round(s+r-a))),u=Math.max(0,Math.min(Math.round(n-o),Math.round(c-o-4))),d=e.anchor??{x:Number(e.windowBounds.x)+(Number(e.mascot.left)||0),y:Number(e.windowBounds.y)+(Number(e.mascot.top)||0),width:r,height:i},p={...e.windowBounds,x:Math.round(Number(d.x)-s),y:Math.round(Number(d.y)-c)},h={...d,x:Math.round(Number(d.x)),y:Math.round(Number(d.y)),width:d.width??r,height:d.height??i};return{...e,anchor:h,mascot:{...e.mascot,left:s,top:c,width:r,height:i},tray:{...e.tray,left:l,top:u,width:a,height:o},placement:`top-end`,windowBounds:p}}", + "codexPetOverlayTrayAboveLeft(e){if(process.platform!==`linux`||e==null||e.windowBounds==null||e.mascot==null||e.tray==null)return e;let t=Number(e.windowBounds.width),n=Number(e.windowBounds.height),r=Number(e.mascot.width),i=Number(e.mascot.height),a=Number(e.tray.width),o=Number(e.tray.height);if(![t,n,r,i,a,o].every(Number.isFinite)||t<=0||n<=0||r<=0||i<=0||a<=0||o<=0)return e;let s=4,c=Math.max(0,Math.min(Math.round(t),Math.round(a))),l=Math.max(0,Math.min(Math.max(0,Math.round(n-i-s)),Math.round(o))),u=this.codexPetOverlayMascotLocalPosition,d=Number(u?.left),p=Number(u?.top),h=[d,p].every(Number.isFinite),m=Math.max(0,Math.min(Math.round(t-r),Math.round(h?d:t-r))),f=Math.max(0,Math.min(Math.round(n-i),Math.round(h?p:n-i))),g=Math.max(0,Math.round(n-i-s-l)),v=Math.max(0,Math.min(Math.round(n-i),Math.round(l+s)));if(gg&&f=l+s?f-l-s:f+i+s;x=Math.max(0,Math.min(Math.round(n-l),Math.round(x)));let y=e.anchor??{x:Number(e.windowBounds.x)+(Number(e.mascot.left)||0),y:Number(e.windowBounds.y)+(Number(e.mascot.top)||0),width:r,height:i},b=h?{...e.windowBounds}:{...e.windowBounds,x:Math.round(Number(y.x)-m),y:Math.round(Number(y.y)-f)},k={...y,x:Math.round(Number(b.x)+m),y:Math.round(Number(b.y)+f),width:y.width??r,height:y.height??i};return{...e,anchor:k,mascot:{...e.mascot,left:m,top:f,width:r,height:i},tray:{...e.tray,left:w,top:x,width:c,height:l},placement:`top-end`,windowBounds:b}}", + "codexPetOverlayDragFromCurrentRenderer(e){let t=this.window;return!(t==null||t.isDestroyed?.()||t.webContents?.id!==e)}", + "codexPetOverlayStartLocalMascotDrag(e,t){if(!this.codexPetOverlayDragFromCurrentRenderer(e)||process.platform!==`linux`||this.codexPetOverlayShouldLockPosition())return!1;let n=this.layout,r=this.codexPetOverlayMascotRect(n),i=Number(t?.pointerWindowX),a=Number(t?.pointerWindowY);if(r==null||![i,a].every(Number.isFinite)||ir.left+r.width||a>r.top+r.height)return!1;return this.codexPetOverlayMascotDragState={offsetX:i-r.left,offsetY:a-r.top},!0}", + "codexPetOverlayMoveLocalMascotDrag(e,t){if(!this.codexPetOverlayDragFromCurrentRenderer(e))return!1;let n=this.codexPetOverlayMascotDragState,r=this.layout,i=this.window,a=this.codexPetOverlayMascotRect(r),o=this.codexPetOverlayWindowBounds(i);if(n==null||r==null||a==null||o==null)return!1;let s=Number(t?.pointerWindowX),c=Number(t?.pointerWindowY);if(![s,c].every(Number.isFinite)){let e=Number(t?.pointerScreenX),r=Number(t?.pointerScreenY);if(![e,r].every(Number.isFinite))return!0;s=e-o.x,c=r-o.y}let l=Math.max(0,Math.min(Math.round(o.width-a.width),Math.round(s-n.offsetX))),u=Math.max(0,Math.min(Math.round(o.height-a.height),Math.round(c-n.offsetY)));this.codexPetOverlayMascotLocalPosition={left:l,top:u};let d=this.codexPetOverlayTrayAboveLeft({...r,windowBounds:{...r.windowBounds,x:o.x,y:o.y,width:o.width,height:o.height}});this.layout=d;try{this.compositionHost.updateMascotRect?.(d.mascot)}catch{}try{this.sendLayoutToRenderer(i,null)}catch{}return!0}", + "codexPetOverlayEndLocalMascotDrag(e){return!this.codexPetOverlayDragFromCurrentRenderer(e)||this.codexPetOverlayMascotDragState==null?!1:(this.codexPetOverlayMascotDragState=null,!0)}", "codexPetOverlayRememberLayout(e,t){let n=this.codexPetOverlayRect(t);this.codexPetOverlayDesiredDisplayBounds=n==null?null:{x:Math.round(n.x),y:Math.round(n.y),width:Math.round(n.width),height:Math.round(n.height)};let r=this.codexPetOverlayRect(e?.windowBounds);if(r!=null){let i={x:Math.round(r.x),y:Math.round(r.y),width:Math.round(r.width),height:Math.round(r.height)},a=this.codexPetOverlayDesiredWindowBounds,o=a==null||a.x!==i.x||a.y!==i.y||a.width!==i.width||a.height!==i.height;this.codexPetOverlayDesiredWindowBounds=i;if(o&&this.window!=null){let e=this.codexPetOverlaySettings();try{e.lockPosition===!0&&this.codexPetOverlayScheduleHyprlandHints(this.window)}catch{}try{this.dragState!=null?this.codexPetOverlayQueueKWinDrag(this.window):this.codexPetOverlayScheduleKWinHints(this.window)}catch{}try{this.dragState!=null?this.codexPetOverlayQueueNiriDrag(this.window):this.codexPetOverlayScheduleNiriHints(this.window)}catch{}}}return e}", "codexPetOverlayLayoutForDisplay(e,t,n){if(process.platform!==`linux`||t==null||t.windowBounds==null)return this.codexPetOverlayRememberLayout(this.codexPetOverlayTrayAboveLeft(t));let r=this.codexPetOverlayDisplayRect(e),i=this.codexPetOverlaySettings(),a=t;if(i.lockPosition===!0&&r!=null){let e=this.codexPetOverlayGravityBounds(r,a,i);e!=null&&(a=this.codexPetOverlayLayoutAtWindowPosition(e,a))}else if(this.dragState==null){let e=this.codexPetOverlayWindowBounds(n),o=!1;try{o=n?.isVisible?.()===!0}catch{}e!=null&&r!=null&&this.codexPetOverlayBoundsNearDisplay(e,r)&&(o||this.codexPetOverlayInitialPositionDone===!0||this.codexPetOverlayManualPosition===!0)?(this.codexPetOverlayInitialPositionDone=!0,this.codexPetOverlayMoved(e,a.windowBounds)&&(this.codexPetOverlayManualPosition=!0),a=this.codexPetOverlayLayoutAtWindowPosition(e,a)):this.codexPetOverlayInitialPositionDone=!0}return this.codexPetOverlayRememberLayout(this.codexPetOverlayTrayAboveLeft(a),r)}", - "codexPetOverlayInstallTransparentRenderer(e){try{if(e.__codexPetOverlayTransparentRendererInstalled)return;e.__codexPetOverlayTransparentRendererInstalled=!0;let t=e.webContents,n=()=>{try{t==null||t.isDestroyed?.()||t.insertCSS?.(`html,body,#root,main,[data-avatar-overlay-content-frame=\"true\"]{background:transparent!important;background-color:transparent!important;}[data-codex-window-type=\"electron\"].electron-opaque,[data-codex-window-type=\"electron\"].electron-opaque body{background:transparent!important;background-color:transparent!important;background-image:none!important;}`,{cssOrigin:`author`}),t==null||t.isDestroyed?.()||t.executeJavaScript?.(`try{document.documentElement.style.background=\"transparent\";document.body&&(document.body.style.background=\"transparent\")}catch{}`,!0)}catch{}};t?.on?.(`did-finish-load`,n),n()}catch{}}", + "codexPetOverlayShouldUseWholeWindowInput(){return process.platform===`linux`&&this.codexPetOverlaySettings().lockPosition!==!0}", + "codexPetOverlayInstallTransparentRenderer(e){try{this.codexLinuxWholeWindowInput=this.codexPetOverlayShouldUseWholeWindowInput();if(e.__codexPetOverlayTransparentRendererInstalled)return;e.__codexPetOverlayTransparentRendererInstalled=!0;let t=e.webContents,n=()=>{try{let n=`html,body,#root,main,[data-avatar-overlay-content-frame=\"true\"]{background:transparent!important;background-color:transparent!important;}[data-codex-window-type=\"electron\"].electron-opaque,[data-codex-window-type=\"electron\"].electron-opaque body{background:transparent!important;background-color:transparent!important;background-image:none!important;}`;this.codexLinuxWholeWindowInput=this.codexPetOverlayShouldUseWholeWindowInput(),this.codexLinuxWholeWindowInput&&(n+=`html,body,#root,main,[data-avatar-overlay-content-frame=\"true\"]{-webkit-app-region:drag!important;app-region:drag!important;user-select:none!important;-webkit-user-select:none!important;}[data-avatar-overlay-hit-region=\"mascot\"],[data-avatar-mascot=\"true\"],.no-drag,[data-avatar-overlay-hit-region=\"notification-tray\"],[data-avatar-overlay-hit-region=\"notification-scroll-control\"]{-webkit-app-region:no-drag!important;app-region:no-drag!important;}`),t==null||t.isDestroyed?.()||t.insertCSS?.(n,{cssOrigin:`author`}),t==null||t.isDestroyed?.()||t.executeJavaScript?.(`try{document.documentElement.style.background=\"transparent\";document.body&&(document.body.style.background=\"transparent\")}catch{}`,!0)}catch{}};t?.on?.(`did-finish-load`,n),n()}catch{}}", "codexPetOverlayRestoreFocusableAfterInactiveShow(e){try{let t=setTimeout(()=>{try{e==null||e.isDestroyed?.()||this.window!==e||this.codexPetOverlaySettings().mode===`passive`||e.setFocusable?.(!0)}catch{}},0);try{t.unref?.()}catch{}}catch{}}", - "codexPetOverlaySyncWindow(e,t=!1){if(process.platform!==`linux`||e==null||e.isDestroyed?.())return;let n=this.codexPetOverlaySettings(),r=n.mode!==`passive`;try{e.setTitle?.(`Codex Pet Overlay`)}catch{}try{e.setFocusable?.(r&&!t)}catch{}try{t&&r&&this.codexPetOverlayRestoreFocusableAfterInactiveShow(e)}catch{}try{e.setSkipTaskbar?.(!!n.skipTaskbar)}catch{}try{e.setAlwaysOnTop?.(!!n.alwaysOnTop)}catch{}try{e.setBackgroundColor?.(`#00000000`)}catch{}try{this.codexPetOverlayInstallTransparentRenderer(e)}catch{}try{e.setOpacity?.(1)}catch{}try{e.setVisibleOnAllWorkspaces?.(!!n.allWorkspaces,{visibleOnFullScreen:!!n.allWorkspaces})}catch{try{e.setVisibleOnAllWorkspaces?.(!!n.allWorkspaces)}catch{}}try{n.alwaysOnTop&&e.moveTop?.()}catch{}try{this.codexPetOverlayScheduleHyprlandHints(e)}catch{}try{this.codexPetOverlayScheduleKWinHints(e)}catch{}try{this.codexPetOverlayScheduleNiriHints(e)}catch{}}", + "codexPetOverlaySyncWindow(e,t=!1){if(process.platform!==`linux`||e==null||e.isDestroyed?.())return;this.codexLinuxWholeWindowInput=this.codexPetOverlayShouldUseWholeWindowInput();let n=this.codexPetOverlaySettings(),r=n.mode!==`passive`;try{e.setTitle?.(`Codex Pet Overlay`)}catch{}try{e.setFocusable?.(r&&!t)}catch{}try{t&&r&&this.codexPetOverlayRestoreFocusableAfterInactiveShow(e)}catch{}try{e.setSkipTaskbar?.(!!n.skipTaskbar)}catch{}try{e.setAlwaysOnTop?.(!!n.alwaysOnTop)}catch{}try{e.setBackgroundColor?.(`#00000000`)}catch{}try{this.codexPetOverlayInstallTransparentRenderer(e)}catch{}try{e.setOpacity?.(1)}catch{}try{e.setVisibleOnAllWorkspaces?.(!!n.allWorkspaces,{visibleOnFullScreen:!!n.allWorkspaces})}catch{try{e.setVisibleOnAllWorkspaces?.(!!n.allWorkspaces)}catch{}}try{n.alwaysOnTop&&e.moveTop?.()}catch{}try{this.codexPetOverlayScheduleHyprlandHints(e)}catch{}try{this.codexPetOverlayScheduleKWinHints(e)}catch{}try{this.codexPetOverlayScheduleNiriHints(e)}catch{}}", "codexPetOverlayHyprlandSession(){if(process.platform!==`linux`)return!1;let e=[process.env.HYPRLAND_INSTANCE_SIGNATURE,process.env.XDG_CURRENT_DESKTOP,process.env.DESKTOP_SESSION].filter(Boolean).join(`:`).toLowerCase();return e.includes(`hyprland`)}", "codexPetOverlayShouldUseHyprland(){return process.platform===`linux`&&this.codexPetOverlaySettings().hyprland===!0&&this.codexPetOverlayHyprlandSession()}", "codexPetOverlayHyprctl(e,t){if(this.codexPetOverlayHyprctlUnavailable)return;try{let n=typeof require==`function`?require(`node:child_process`):null;if(typeof n?.execFile!=`function`){this.codexPetOverlayHyprctlUnavailable=!0;return}n.execFile(`hyprctl`,e,{timeout:1200},(e,...n)=>{e?.code===`ENOENT`&&(this.codexPetOverlayHyprctlUnavailable=!0),typeof t==`function`&&t(e,...n)})}catch(e){e?.code===`ENOENT`&&(this.codexPetOverlayHyprctlUnavailable=!0);try{typeof t==`function`&&t(e)}catch{}}}", @@ -176,42 +179,16 @@ function buildPetOverlayMethods(settings) { "codexPetOverlayFindHyprlandClient(e,t){if(!this.codexPetOverlayShouldUseHyprland())return;let n=this.codexPetOverlayWindowBounds(e);this.codexPetOverlayHyprctl([`clients`,`-j`],(e,r)=>{if(e)return;let i;try{i=JSON.parse(String(r??``))}catch{return}let a=this.codexPetOverlaySelectHyprlandClient(i,n);a!=null&&typeof t==`function`&&t(a)})}", "codexPetOverlayApplyHyprlandHints(e){let t=this.codexPetOverlaySettings();if(process.platform!==`linux`||e==null||e.isDestroyed?.()||!this.codexPetOverlayShouldUseHyprland())return;this.codexPetOverlayFindHyprlandClient(e,n=>{if(e.isDestroyed?.()||this.window!==e)return;let r=`address:${n.address}`,i=this.codexPetOverlayDesiredWindowBounds,a=Number(i?.x),o=Number(i?.y),s=Math.round(a),c=Math.round(o);t.lockPosition&&[a,o].every(Number.isFinite)&&this.codexPetOverlayHyprlandDispatch(`hl.dsp.window.move({ window = \"${r}\", x = ${s}, y = ${c} })`,[`movewindowpixel`,`exact ${s} ${c},${r}`]);t.allWorkspaces&&n.pinned!==!0&&this.codexPetOverlayHyprlandDispatch(`hl.dsp.window.pin({ action = \"on\", window = \"${r}\" })`,[`pin`,r]);this.codexPetOverlayHyprlandSetProp(r,`decorate`,`0`);this.codexPetOverlayHyprlandSetProp(r,`no_shadow`,`1`);this.codexPetOverlayHyprlandSetProp(r,`no_blur`,`1`);this.codexPetOverlayHyprlandSetProp(r,`no_anim`,`1`);this.codexPetOverlayHyprlandSetProp(r,`border_size`,`0`);this.codexPetOverlayHyprlandSetProp(r,`rounding`,`0`);this.codexPetOverlayHyprlandSetProp(r,`opacity`,`1.0 override 1.0 override 1.0 override`);this.codexPetOverlayHyprlandSetProp(r,`opaque`,`0`);this.codexPetOverlayHyprlandSetProp(r,`force_rgbx`,`0`);t.alwaysOnTop&&this.codexPetOverlayHyprlandDispatch(`hl.dsp.window.alter_zorder({ mode = \"top\", window = \"${r}\" })`,[`alterzorder`,`top,${r}`])})}", "codexPetOverlayScheduleHyprlandHints(e){if(!this.codexPetOverlayShouldUseHyprland())return;try{this.codexPetOverlayHyprlandTimers?.forEach(clearTimeout)}catch{}this.codexPetOverlayHyprlandTimers=[0,80,300,1000,2500,5000,10000].map(t=>{let n=setTimeout(()=>{try{e==null||e.isDestroyed?.()||this.codexPetOverlayApplyHyprlandHints(e)}catch{}},t);try{n.unref?.()}catch{}return n})}", - "codexPetOverlayKWinSession(){if(process.platform!==`linux`)return!1;let e=String(process.env.KDE_FULL_SESSION??``).toLowerCase();if(process.env.KDE_SESSION_VERSION||e===`1`||e===`true`)return!0;let t=[process.env.XDG_CURRENT_DESKTOP,process.env.DESKTOP_SESSION].filter(Boolean).join(`:`).toLowerCase();return t.includes(`kde`)||t.includes(`plasma`)}", - "codexPetOverlayShouldUseKWin(){return process.platform===`linux`&&this.codexPetOverlaySettings().kwin===!0&&this.codexPetOverlayKWinSession()}", - "codexPetOverlayKWinQdbus(e,t,n=!1){if(this.codexPetOverlayKWinUnavailable){try{typeof t==`function`&&t({code:`ENOENT`})}catch{}return}let r=e;try{let i=typeof require==`function`?require(`node:child_process`):null;if(typeof i?.execFile!=`function`){this.codexPetOverlayKWinUnavailable=!0;try{typeof t==`function`&&t({code:`ENOENT`})}catch{}return}i.execFile(n?`qdbus`:`qdbus6`,r,{timeout:1500},(e,...i)=>{if(e?.code===`ENOENT`&&!n){this.codexPetOverlayKWinQdbus(r,t,!0);return}e?.code===`ENOENT`&&(this.codexPetOverlayKWinUnavailable=!0);try{typeof t==`function`&&t(e,...i)}catch{}})}catch(e){if(e?.code===`ENOENT`&&!n){this.codexPetOverlayKWinQdbus(r,t,!0);return}e?.code===`ENOENT`&&(this.codexPetOverlayKWinUnavailable=!0);try{typeof t==`function`&&t(e)}catch{}}}", - "codexPetOverlayKWinScript(e,t){let n=this.codexPetOverlayRect(e),r=this.codexPetOverlaySettings(),i={pid:Number(process.pid),title:`Codex Pet Overlay`,alwaysOnTop:!!r.alwaysOnTop,allWorkspaces:!!r.allWorkspaces,skipTaskbar:!!r.skipTaskbar,move:!!t,x:Math.round(Number(n?.x)),y:Math.round(Number(n?.y)),width:Math.round(Number(n?.width)),height:Math.round(Number(n?.height))};return `(function(){var d=${JSON.stringify(i)};function windows(){try{if(typeof workspace.windowList==='function')return workspace.windowList()}catch(e){}try{if(typeof workspace.clientList==='function')return workspace.clientList()}catch(e){}try{if(workspace.stackingOrder&&typeof workspace.stackingOrder.length==='number')return workspace.stackingOrder}catch(e){}return[]}var a=windows().filter(function(w){try{return String(w.caption||'')===d.title&&Number(w.pid)===d.pid}catch(e){return false}});if(a.length!==1)return;var w=a[0];try{w.keepAbove=d.alwaysOnTop}catch(e){}try{w.skipTaskbar=d.skipTaskbar}catch(e){}try{w.skipPager=d.skipTaskbar}catch(e){}try{w.onAllDesktops=d.allWorkspaces}catch(e){}try{w.noBorder=true}catch(e){}if(d.move&&isFinite(d.x)&&isFinite(d.y)){try{var g=w.frameGeometry;w.frameGeometry={x:d.x,y:d.y,width:isFinite(d.width)&&d.width>0?d.width:g.width,height:isFinite(d.height)&&d.height>0?d.height:g.height}}catch(e){}}if(d.alwaysOnTop){try{if(typeof workspace.raiseWindow==='function')workspace.raiseWindow(w)}catch(e){}}})()`}", - "codexPetOverlayKWinRun(e,t,n){if(!this.codexPetOverlayShouldUseKWin()){try{typeof n==`function`&&n({code:`DISABLED`})}catch{}return}let r;try{let i=require(`node:fs`),a=require(`node:os`),o=require(`node:path`),s=(this.codexPetOverlayKWinScriptGeneration??0)+1;this.codexPetOverlayKWinScriptGeneration=s;let c=`codex_pet_overlay_${process.pid}_${Date.now()}_${s}`,l=o.join(a.tmpdir(),`${c}.js`);i.writeFileSync(l,this.codexPetOverlayKWinScript(e,t),{encoding:`utf8`,flag:`wx`,mode:384}),r=()=>{try{i.unlinkSync(l)}catch{}};let u=[`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.loadScript`,l,c];this.codexPetOverlayKWinQdbus(u,e=>{if(e){r();try{typeof n==`function`&&n(e)}catch{}return}this.codexPetOverlayKWinQdbus([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.start`],e=>{this.codexPetOverlayKWinQdbus([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.unloadScript`,c],()=>{r();try{typeof n==`function`&&n(e)}catch{}})})})}catch(e){try{r?.()}catch{}try{typeof n==`function`&&n(e)}catch{}}}", - "codexPetOverlayApplyKWinHints(e){if(e==null||e.isDestroyed?.()||this.window!==e||!this.codexPetOverlayShouldUseKWin()||this.dragState!=null||this.codexPetOverlayKWinDragState!=null||this.codexPetOverlayKWinHintInFlight)return;this.codexPetOverlayKWinHintInFlight=!0;let t=this.codexPetOverlaySettings();this.codexPetOverlayKWinRun(this.codexPetOverlayDesiredWindowBounds,t.lockPosition===!0,()=>{this.codexPetOverlayKWinHintInFlight=!1,this.codexPetOverlayKWinDragState==null&&this.window===e&&!e.isDestroyed?.()&&this.codexPetOverlayKWinPendingHints&&(this.codexPetOverlayKWinPendingHints=!1,this.codexPetOverlayScheduleKWinHints(e))})}", - "codexPetOverlayScheduleKWinHints(e){if(!this.codexPetOverlayShouldUseKWin()||this.dragState!=null||this.codexPetOverlayKWinDragState!=null)return;if(this.codexPetOverlayKWinHintInFlight){this.codexPetOverlayKWinPendingHints=!0;return}this.codexPetOverlayKWinPendingHints=!1;try{this.codexPetOverlayKWinTimers?.forEach(clearTimeout)}catch{}this.codexPetOverlayKWinTimers=[0,80,300,1000,2500].map(t=>{let n=setTimeout(()=>{try{this.codexPetOverlayApplyKWinHints(e)}catch{}},t);try{n.unref?.()}catch{}return n})}", - "codexPetOverlayKWinExecSync(e){if(this.codexPetOverlayKWinUnavailable||!this.codexPetOverlayShouldUseKWin())return!1;try{let t=typeof require==`function`?require(`node:child_process`):null;if(typeof t?.execFileSync!=`function`)return!1;for(let n of [`qdbus6`,`qdbus`])try{t.execFileSync(n,e,{timeout:750,stdio:`ignore`});return!0}catch(e){if(e?.code!==`ENOENT`)return!1}this.codexPetOverlayKWinUnavailable=!0}catch{}return!1}", - "codexPetOverlayKWinDragScript(){let e={pid:Number(process.pid),title:`Codex Pet Overlay`};return `(function(){var d=${JSON.stringify(e)};function windows(){try{if(typeof workspace.windowList==='function')return workspace.windowList()}catch(e){}try{if(typeof workspace.clientList==='function')return workspace.clientList()}catch(e){}return[]}var a=windows().filter(function(w){try{return String(w.caption||'')===d.title&&Number(w.pid)===d.pid}catch(e){return false}});if(a.length!==1)return;var w=a[0],p=workspace.cursorPos,g=w.frameGeometry,dx=Number(p.x)-Number(g.x),dy=Number(p.y)-Number(g.y),active=true;function move(){if(!active)return;try{var p=workspace.cursorPos,g=w.frameGeometry;w.frameGeometry={x:Math.round(Number(p.x)-dx),y:Math.round(Number(p.y)-dy),width:g.width,height:g.height}}catch(e){stop()}}function stop(){if(!active)return;active=false;try{workspace.cursorPosChanged.disconnect(move)}catch(e){}}try{workspace.cursorPosChanged.connect(move)}catch(e){return}try{workspace.windowRemoved.connect(function(v){if(v===w)stop()})}catch(e){}try{if(typeof workspace.raiseWindow==='function')workspace.raiseWindow(w)}catch(e){}})()`}", - "codexPetOverlayReleaseKWinDrag(e){if(e==null)return;try{this.codexPetOverlayKWinExecSync([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.unloadScript`,e.pluginName])}catch{}try{require(`node:fs`).unlinkSync(e.scriptPath)}catch{}}", - "codexPetOverlayStartKWinDrag(e){let t;try{let n=require(`node:fs`),r=require(`node:os`),i=require(`node:path`),a=(this.codexPetOverlayKWinDragGeneration??0)+1,o=`codex_pet_overlay_drag_${process.pid}_${Date.now()}_${a}`,s=i.join(r.tmpdir(),`${o}.js`);n.writeFileSync(s,this.codexPetOverlayKWinDragScript(),{encoding:`utf8`,flag:`wx`,mode:384}),t={generation:a,window:e,pluginName:o,scriptPath:s};let c=[`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.loadScript`,s,o];if(!this.codexPetOverlayKWinExecSync(c)||!this.codexPetOverlayKWinExecSync([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.start`])){this.codexPetOverlayReleaseKWinDrag(t);return null}return t}catch(e){this.codexPetOverlayReleaseKWinDrag(t);return null}}", - "codexPetOverlayBeginKWinDrag(e){if(e==null||e.isDestroyed?.()||this.window!==e||!this.codexPetOverlayShouldUseKWin())return;try{this.codexPetOverlayKWinTimers?.forEach(clearTimeout)}catch{}let t=this.codexPetOverlayKWinDragState;t!=null&&(this.codexPetOverlayKWinDragState=null,this.codexPetOverlayReleaseKWinDrag(t));let n=this.codexPetOverlayStartKWinDrag(e);if(n==null)return;let r=null;try{r=Number(e.getContentBounds?.().x)}catch{}this.codexPetOverlayKWinDragGeneration=n.generation,this.codexPetOverlayKWinDragState=n,this.windowServerDragActive=!0,Number.isFinite(r)&&(this.windowServerDragWindowX=r)}", - "codexPetOverlayKWinDragCurrent(e){if(e==null||this.codexPetOverlayKWinDragState!==e||this.codexPetOverlayKWinDragGeneration!==e.generation)return!1;if(this.window===e.window&&!e.window?.isDestroyed?.())return!0;this.codexPetOverlayKWinDragState=null,this.codexPetOverlayReleaseKWinDrag(e);return!1}", - "codexPetOverlayQueueKWinDrag(e){let t=this.codexPetOverlayKWinDragState;this.codexPetOverlayKWinDragCurrent(t)&&t.window===e&&(this.windowServerDragActive=!0)}", - "codexPetOverlayEndKWinDrag(e,t){let n=this.codexPetOverlayKWinDragState;if(!this.codexPetOverlayKWinDragCurrent(n)||n.window!==e)return!1;this.codexPetOverlayKWinDragState=null,this.codexPetOverlayReleaseKWinDrag(n);try{typeof t==`function`&&t()}catch{}try{this.window!=null&&!this.window.isDestroyed?.()&&this.codexPetOverlayScheduleKWinHints(this.window)}catch{}return!0}", "codexPetOverlayNiriSession(){if(process.platform!==`linux`)return!1;let e=[process.env.NIRI_SOCKET,process.env.XDG_CURRENT_DESKTOP,process.env.DESKTOP_SESSION].filter(Boolean).join(`:`).toLowerCase();return e.includes(`niri`)}", "codexPetOverlayShouldUseNiri(){return process.platform===`linux`&&this.codexPetOverlaySettings().niri===!0&&this.codexPetOverlayNiriSession()}", - "codexPetOverlayFinishNiriProcess(){this.codexPetOverlayNiriProcessCount=Math.max(0,(this.codexPetOverlayNiriProcessCount??1)-1);if(this.codexPetOverlayNiriProcessCount===0){let e=this.codexPetOverlayNiriDragState;if(e!=null)this.codexPetOverlayPumpNiriDrag(e);else{let e=this.codexPetOverlayNiriPendingHintsWindow;this.codexPetOverlayNiriPendingHintsWindow=null;try{e!=null&&!e.isDestroyed?.()&&this.window===e&&this.codexPetOverlayScheduleNiriHints(e)}catch{}}}}", - "codexPetOverlayNiri(e,t){if(this.codexPetOverlayNiriUnavailable){try{typeof t==`function`&&t({code:`ENOENT`})}catch{}return}let n=!1;try{let r=typeof require==`function`?require(`node:child_process`):null;if(typeof r?.execFile!=`function`){this.codexPetOverlayNiriUnavailable=!0;try{typeof t==`function`&&t({code:`ENOENT`})}catch{}return}this.codexPetOverlayNiriProcessCount=(this.codexPetOverlayNiriProcessCount??0)+1,n=!0,r.execFile(`niri`,[`msg`,...e],{timeout:1200},(e,...n)=>{e?.code===`ENOENT`&&(this.codexPetOverlayNiriUnavailable=!0);try{typeof t==`function`&&t(e,...n)}finally{this.codexPetOverlayFinishNiriProcess()}})}catch(e){e?.code===`ENOENT`&&(this.codexPetOverlayNiriUnavailable=!0),n&&this.codexPetOverlayFinishNiriProcess();try{typeof t==`function`&&t(e)}catch{}}}", + "codexPetOverlayNiri(e,t){if(this.codexPetOverlayNiriUnavailable)return;try{let n=typeof require==`function`?require(`node:child_process`):null;if(typeof n?.execFile!=`function`){this.codexPetOverlayNiriUnavailable=!0;return}n.execFile(`niri`,[`msg`,...e],{timeout:1200},(e,...n)=>{e?.code===`ENOENT`&&(this.codexPetOverlayNiriUnavailable=!0),typeof t==`function`&&t(e,...n)})}catch(e){e?.code===`ENOENT`&&(this.codexPetOverlayNiriUnavailable=!0);try{typeof t==`function`&&t(e)}catch{}}}", "codexPetOverlayNiriWindowSize(e){let t=e?.layout?.window_size??e?.layout?.windowSize??e?.window_size??e?.size;if(!Array.isArray(t))return null;let n=Number(t[0]),r=Number(t[1]);return[n,r].every(Number.isFinite)&&n>0&&r>0?{width:n,height:r}:null}", "codexPetOverlayNiriPositiveInteger(e){return typeof e==`number`&&Number.isSafeInteger(e)&&e>0?e:null}", "codexPetOverlayNiriLocalMove(){let e=this.codexPetOverlayRect(this.codexPetOverlayDesiredWindowBounds),t=this.codexPetOverlayRect(this.codexPetOverlayDesiredDisplayBounds);if(e==null||t==null)return null;let n=e.x-t.x,r=e.y-t.y;return[n,r].every(Number.isFinite)?{x:Math.round(n),y:Math.round(r)}:null}", "codexPetOverlaySelectNiriWindow(e,t){if(!Array.isArray(e))return null;let n=this.codexPetOverlayRect(t),r=[];for(let i of e){let e=this.codexPetOverlayNiriPositiveInteger(i?.id),t=this.codexPetOverlayNiriPositiveInteger(i?.pid);if(e==null||t!==process.pid||String(i?.title??``)!==`Codex Pet Overlay`)continue;if(i.is_floating!=null&&typeof i.is_floating!=`boolean`)continue;let a=this.codexPetOverlayNiriWindowSize(i),o=a==null?0:Math.abs(a.width-Number(n?.width??a.width))+Math.abs(a.height-Number(n?.height??a.height)),s=a==null?0:a.width*a.height;r.push({window:i,id:e,sizeScore:o,area:s})}if(n!=null){let e=r.filter(e=>e.sizeScore<=16);return e.length===1?e[0].window:null}let i=r.filter(e=>e.area>0&&e.area<=300000);return i.length===1?i[0].window:r.length===1?r[0].window:null}", - "codexPetOverlayFindNiriWindow(e,t){if(!this.codexPetOverlayShouldUseNiri()){try{typeof t==`function`&&t({code:`DISABLED`})}catch{}return}let n=this.codexPetOverlayWindowBounds(e);this.codexPetOverlayNiri([`--json`,`windows`],(e,r)=>{if(e){try{typeof t==`function`&&t(e)}catch{}return}let i;try{i=JSON.parse(String(r??``))}catch{try{typeof t==`function`&&t({code:`INVALID_JSON`})}catch{}return}let a=this.codexPetOverlaySelectNiriWindow(i,n);try{typeof t==`function`&&t(null,a)}catch{}})}", - "codexPetOverlayApplyNiriHints(e,t=this.codexPetOverlayNiriEpoch){if(process.platform!==`linux`||e==null||e.isDestroyed?.()||!this.codexPetOverlayShouldUseNiri()||this.codexPetOverlayNiriDragState!=null||this.codexPetOverlayNiriDragCallOwner!=null)return;this.codexPetOverlayFindNiriWindow(e,(n,r)=>{if(n||e.isDestroyed?.()||this.window!==e||t!==this.codexPetOverlayNiriEpoch||this.codexPetOverlayNiriDragState!=null||this.codexPetOverlayNiriDragCallOwner!=null)return;let i=this.codexPetOverlayNiriPositiveInteger(r?.id);if(i==null)return;let a=()=>{if(e.isDestroyed?.()||this.window!==e||t!==this.codexPetOverlayNiriEpoch||this.codexPetOverlayNiriDragState!=null||this.codexPetOverlayNiriDragCallOwner!=null)return;let n=this.codexPetOverlayNiriLocalMove();n!=null&&this.codexPetOverlayNiri([`action`,`move-floating-window`,`--id`,String(i),`-x`,String(n.x),`-y`,String(n.y)])};r.is_floating===!0?a():this.codexPetOverlayNiri([`action`,`move-window-to-floating`,`--id`,String(i)],n=>{n||a()})})}", - "codexPetOverlayScheduleNiriHints(e){let t=this.codexPetOverlayNiriDragState;if(t!=null&&(this.window!==t.window||t.window?.isDestroyed?.())){try{t.retryTimer!=null&&clearTimeout(t.retryTimer)}catch{}this.codexPetOverlayNiriDragState=null,this.codexPetOverlayNiriEpoch=(this.codexPetOverlayNiriEpoch??0)+1}if(this.codexPetOverlayNiriUnavailable||!this.codexPetOverlayShouldUseNiri()||this.dragState!=null||this.codexPetOverlayNiriDragState!=null)return;if(this.codexPetOverlayNiriDragCallOwner!=null||(this.codexPetOverlayNiriProcessCount??0)>0){this.codexPetOverlayNiriPendingHintsWindow=e;return}this.codexPetOverlayNiriPendingHintsWindow=null;try{this.codexPetOverlayNiriTimers?.forEach(clearTimeout)}catch{}let n=(this.codexPetOverlayNiriEpoch??0)+1;this.codexPetOverlayNiriEpoch=n,this.codexPetOverlayNiriTimers=[0,80,300,1000].map(t=>{let r=setTimeout(()=>{try{e==null||e.isDestroyed?.()||this.codexPetOverlayApplyNiriHints(e,n)}catch{}},t);try{r.unref?.()}catch{}return r})}", - "codexPetOverlayBeginNiriDrag(e){if(e==null||e.isDestroyed?.()||this.window!==e||!this.codexPetOverlayShouldUseNiri())return;try{this.codexPetOverlayNiriTimers?.forEach(clearTimeout),this.codexPetOverlayNiriDragState?.retryTimer!=null&&clearTimeout(this.codexPetOverlayNiriDragState.retryTimer)}catch{}this.codexPetOverlayNiriEpoch=(this.codexPetOverlayNiriEpoch??0)+1;let t=(this.codexPetOverlayNiriDragGeneration??0)+1;this.codexPetOverlayNiriDragGeneration=t;let n=this.codexPetOverlayNiriLocalMove();this.codexPetOverlayNiriDragState={generation:t,window:e,id:null,floating:!1,latestTarget:n==null?null:{x:n.x,y:n.y},inFlight:!1,released:!1,persisted:!1,complete:null,retryIndex:0,retryTimer:null},this.codexPetOverlayPumpNiriDrag(this.codexPetOverlayNiriDragState)}", - "codexPetOverlayNiriDragCurrent(e){if(e==null||this.codexPetOverlayNiriDragState!==e||this.codexPetOverlayNiriDragGeneration!==e.generation)return!1;if(this.window===e.window&&!e.window?.isDestroyed?.())return!0;try{e.retryTimer!=null&&clearTimeout(e.retryTimer)}catch{}this.codexPetOverlayNiriDragState=null,this.codexPetOverlayNiriEpoch=(this.codexPetOverlayNiriEpoch??0)+1;let t=this.window;try{t!=null&&!t.isDestroyed?.()&&this.codexPetOverlayScheduleNiriHints(t)}catch{}return!1}", - "codexPetOverlayStartNiriDragCall(e){if(this.codexPetOverlayNiriDragCallOwner!=null)return!1;e.inFlight=!0,this.codexPetOverlayNiriDragCallOwner=e;return!0}", - "codexPetOverlayFinishNiriDragCall(e){e.inFlight=!1,this.codexPetOverlayNiriDragCallOwner===e&&(this.codexPetOverlayNiriDragCallOwner=null);let t=this.codexPetOverlayNiriDragState;if(t!=null&&t!==e)this.codexPetOverlayPumpNiriDrag(t);else if(t==null){let e=this.window;try{e!=null&&!e.isDestroyed?.()&&this.codexPetOverlayScheduleNiriHints(e)}catch{}}}", - "codexPetOverlayQueueNiriDrag(e){let t=this.codexPetOverlayNiriDragState;if(!this.codexPetOverlayNiriDragCurrent(t)||t.window!==e)return;let n=this.codexPetOverlayNiriLocalMove();n!=null&&(t.latestTarget={x:n.x,y:n.y}),this.codexPetOverlayPumpNiriDrag(t)}", - "codexPetOverlayRetryNiriDrag(e,t){if(!this.codexPetOverlayNiriDragCurrent(e))return;if(t?.code===`ENOENT`){this.codexPetOverlayAbortNiriDrag(e);return}if(e.retryIndex>=3){this.codexPetOverlayAbortNiriDrag(e);return}let n=[0,80,300][e.retryIndex]??300;e.retryTimer=setTimeout(()=>{e.retryTimer=null,this.codexPetOverlayNiriDragCurrent(e)&&this.codexPetOverlayPumpNiriDrag(e)},n);try{e.retryTimer.unref?.()}catch{}}", - "codexPetOverlayAbortNiriDrag(e){if(!this.codexPetOverlayNiriDragCurrent(e))return;try{e.retryTimer!=null&&clearTimeout(e.retryTimer)}catch{}e.inFlight=!1,e.retryTimer=null,e.released?this.codexPetOverlayFinalizeNiriDrag(e):this.codexPetOverlayNiriDragState=null}", - "codexPetOverlayFinalizeNiriDrag(e){if(!this.codexPetOverlayNiriDragCurrent(e)||!e.released||e.persisted)return;e.persisted=!0;let t=e.complete;this.codexPetOverlayNiriDragState=null;try{typeof t==`function`&&t()}catch{}}", - "codexPetOverlayPumpNiriDrag(e){if(!this.codexPetOverlayNiriDragCurrent(e)||e.inFlight||e.retryTimer!=null||this.codexPetOverlayNiriDragCallOwner!=null||(this.codexPetOverlayNiriProcessCount??0)>0)return;if(e.id==null){if(e.retryIndex>=3){this.codexPetOverlayAbortNiriDrag(e);return}e.retryIndex+=1;if(!this.codexPetOverlayStartNiriDragCall(e))return;this.codexPetOverlayFindNiriWindow(e.window,(t,n)=>{this.codexPetOverlayFinishNiriDragCall(e);if(!this.codexPetOverlayNiriDragCurrent(e))return;let r=this.codexPetOverlayNiriPositiveInteger(n?.id);if(t||r==null){this.codexPetOverlayRetryNiriDrag(e,t);return}e.id=r,e.floating=n.is_floating===!0,this.codexPetOverlayPumpNiriDrag(e)});return}if(!e.floating){if(!this.codexPetOverlayStartNiriDragCall(e))return;let t=e.id;this.codexPetOverlayNiri([`action`,`move-window-to-floating`,`--id`,String(t)],n=>{this.codexPetOverlayFinishNiriDragCall(e);if(!this.codexPetOverlayNiriDragCurrent(e))return;if(n){e.id=null,e.floating=!1,this.codexPetOverlayRetryNiriDrag(e,n);return}e.floating=!0,this.codexPetOverlayPumpNiriDrag(e)});return}let t=e.latestTarget;if(t!=null){e.latestTarget=null;if(!this.codexPetOverlayStartNiriDragCall(e)){e.latestTarget=t;return}let n=e.id;this.codexPetOverlayNiri([`action`,`move-floating-window`,`--id`,String(n),`-x`,String(t.x),`-y`,String(t.y)],n=>{this.codexPetOverlayFinishNiriDragCall(e);if(!this.codexPetOverlayNiriDragCurrent(e))return;if(n){e.latestTarget??=t,e.id=null,e.floating=!1,this.codexPetOverlayRetryNiriDrag(e,n);return}this.codexPetOverlayPumpNiriDrag(e)});return}e.released&&this.codexPetOverlayFinalizeNiriDrag(e)}", - "codexPetOverlayEndNiriDrag(e,t){let n=this.codexPetOverlayNiriDragState;if(!this.codexPetOverlayNiriDragCurrent(n)||n.window!==e)return!1;n.released=!0,n.complete=t;let r=this.codexPetOverlayNiriLocalMove();r!=null&&(n.latestTarget={x:r.x,y:r.y}),this.codexPetOverlayPumpNiriDrag(n);return!0}", + "codexPetOverlayFindNiriWindow(e,t){if(!this.codexPetOverlayShouldUseNiri())return;let n=this.codexPetOverlayWindowBounds(e);this.codexPetOverlayNiri([`--json`,`windows`],(e,r)=>{if(e)return;let i;try{i=JSON.parse(String(r??``))}catch{return}let a=this.codexPetOverlaySelectNiriWindow(i,n);a!=null&&typeof t==`function`&&t(a)})}", + "codexPetOverlayApplyNiriHints(e){if(process.platform!==`linux`||e==null||e.isDestroyed?.()||!this.codexPetOverlayShouldUseNiri())return;this.codexPetOverlayFindNiriWindow(e,t=>{if(e.isDestroyed?.()||this.window!==e)return;let n=this.codexPetOverlayNiriPositiveInteger(t?.id);if(n==null)return;t.is_floating!==!0&&this.codexPetOverlayNiri([`action`,`move-window-to-floating`,`--id`,String(n)]);let r=this.codexPetOverlayNiriLocalMove();r!=null&&this.codexPetOverlayNiri([`action`,`move-floating-window`,`--id`,String(n),`-x`,String(r.x),`-y`,String(r.y)])})}", + "codexPetOverlayScheduleNiriHints(e){if(!this.codexPetOverlayShouldUseNiri())return;try{this.codexPetOverlayNiriTimers?.forEach(clearTimeout)}catch{}this.codexPetOverlayNiriTimers=[0,80,300,1000].map(t=>{let n=setTimeout(()=>{try{e==null||e.isDestroyed?.()||this.codexPetOverlayApplyNiriHints(e)}catch{}},t);try{n.unref?.()}catch{}return n})}", "codexPetOverlayShouldLockPosition(){return process.platform===`linux`&&this.codexPetOverlaySettings().lockPosition===!0}", ].join(""); } @@ -236,6 +213,26 @@ function patchCreateWindowTitle(source) { return replaceMethodText(source, method, replacement); } +function patchCreateWindowFrame(source) { + const method = findAvatarOverlayMethod(source, /async createWindow\([^)]*\)\{/); + if (method == null) { + console.warn("WARN: Could not find avatar overlay createWindow - skipping pet overlay frame patch"); + return source; + } + if (method.text.includes("frame:process.platform===`linux`?!1:!0")) { + return source; + } + const replacement = method.text.replace( + "appearance:`avatarOverlay`,", + "appearance:`avatarOverlay`,frame:process.platform===`linux`?!1:!0,", + ); + if (replacement === method.text) { + console.warn("WARN: Could not identify avatar overlay appearance option - skipping pet overlay frame patch"); + return source; + } + return replaceMethodText(source, method, replacement); +} + function ensurePetOverlayMethods(source, settings) { if (source.includes("codexPetOverlaySettings(){")) { return source; @@ -253,60 +250,6 @@ function ensurePetOverlayMethods(source, settings) { source.slice(insertionPoint.start); } -function patchCompositorDragLifecycle(source) { - let patched = source; - const startMethod = findAvatarOverlayMethod(patched, /startDrag\([^)]*\)\{/); - if (startMethod == null) { - console.warn("WARN: Could not find avatar overlay startDrag for compositor transport - skipping pet overlay patch"); - return patched; - } - const needsKWinStart = !startMethod.text.includes("codexPetOverlayBeginKWinDrag("); - const needsNiriStart = !startMethod.text.includes("codexPetOverlayBeginNiriDrag("); - if (needsKWinStart || needsNiriStart) { - const windowMatch = startMethod.text.match(/let ([A-Za-z_$][\w$]*)=this\.window;/); - if (windowMatch == null || !startMethod.text.includes("this.dragState=")) { - console.warn("WARN: Could not identify current avatar overlay drag start shape - skipping compositor transport hook"); - return patched; - } - const hooks = [ - needsKWinStart ? `this.codexPetOverlayBeginKWinDrag(${windowMatch[1]})` : null, - needsNiriStart ? `this.codexPetOverlayBeginNiriDrag(${windowMatch[1]})` : null, - ].filter(Boolean).join(","); - patched = replaceMethodText( - patched, - startMethod, - `${startMethod.text.slice(0, -1)},${hooks}}`, - ); - } - - const endMethod = findAvatarOverlayMethod(patched, /endDrag\([^)]*\)\{/); - if (endMethod == null) { - console.warn("WARN: Could not find avatar overlay endDrag for compositor transport - skipping pet overlay patch"); - return patched; - } - if ( - endMethod.text.includes("codexPetOverlayEndKWinDrag(") && - endMethod.text.includes("codexPetOverlayEndNiriDrag(") - ) { - return patched; - } - const completionPattern = /[A-Za-z_$][\w$]*\?this\.persistWindowBounds\(([A-Za-z_$][\w$]*),[A-Za-z_$][\w$]*\?\?this\.getCurrentDisplay\(\)\):this\.reclampWindowToVisibleDisplay\(\{shouldPersist:!0\}\)/; - const completionMatch = endMethod.text.match(completionPattern); - if (completionMatch == null) { - console.warn("WARN: Could not identify current avatar overlay drag completion shape - skipping compositor transport hook"); - return patched; - } - const windowVar = completionMatch[1]; - const completionNeedle = endMethod.text.slice(completionMatch.index, -1); - return replaceMethodText( - patched, - endMethod, - endMethod.text.slice(0, completionMatch.index) + - `this.codexPetOverlayEndKWinDrag(${windowVar},()=>{${completionNeedle}})||this.codexPetOverlayEndNiriDrag(${windowVar},()=>{${completionNeedle}})||(()=>{${completionNeedle}})()` + - "}", - ); -} - function patchApplyLayout(source) { if ( source.includes("=this.codexPetOverlayLayoutForDisplay(") || @@ -376,13 +319,65 @@ function patchLockedDrag(source) { ); } +function patchLocalMascotDrag(source) { + if (source.includes("if(this.codexPetOverlayStartLocalMascotDrag(")) { + return source; + } + const patches = [ + ["startDrag", "codexPetOverlayStartLocalMascotDrag"], + ["moveDrag", "codexPetOverlayMoveLocalMascotDrag"], + ["endDrag", "codexPetOverlayEndLocalMascotDrag"], + ]; + let patched = source; + for (const [methodName, helperName] of patches) { + const method = findAvatarOverlayMethod(patched, new RegExp(`${methodName}\\([^)]*\\)\\{`)); + const callerArg = method == null ? null : firstMethodArgument(method.match[0], methodName, 0); + const eventArg = method == null ? null : firstMethodArgument(method.match[0], methodName, 1); + if (method == null || callerArg == null || (methodName !== "endDrag" && eventArg == null)) { + if (methodName === "moveDrag") { + return source; + } + console.warn(`WARN: Could not find avatar overlay ${methodName} - skipping local pet drag patch`); + return source; + } + const call = methodName === "endDrag" + ? `this.${helperName}(${callerArg})` + : `this.${helperName}(${callerArg},${eventArg})`; + patched = replaceMethodText( + patched, + method, + method.text.slice(0, method.match[0].length) + `if(${call})return;` + method.text.slice(method.match[0].length), + ); + } + return patched; +} + +function patchLocalMascotDragLifecycle(source) { + if (source.includes("this.dragState=null,this.codexPetOverlayMascotDragState=null,")) { + return source; + } + const method = findAvatarOverlayMethod(source, /async createWindow\([^)]*\)\{/); + if (method == null || !method.text.includes("this.dragState=null,")) { + console.warn("WARN: Could not find avatar overlay lifecycle state - skipping local pet drag cleanup"); + return source; + } + return replaceMethodText( + source, + method, + method.text.replaceAll( + "this.dragState=null,", + "this.dragState=null,this.codexPetOverlayMascotDragState=null,", + ), + ); +} + function patchPassiveCreateWindow(source, settings) { if (settings.mode !== "passive") { return source; } return source - .split("appearance:`avatarOverlay`,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:process.platform===`linux`?!0:!1") - .join("appearance:`avatarOverlay`,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:!1"); + .split("appearance:`avatarOverlay`,frame:process.platform===`linux`?!1:!0,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:process.platform===`linux`?!0:!1") + .join("appearance:`avatarOverlay`,frame:process.platform===`linux`?!1:!0,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:!1"); } function patchAvatarSelectionRefresh(source) { @@ -403,19 +398,23 @@ function patchAvatarSelectionRefresh(source) { return helper + source.replace(handler, replacement); } -function hasCompletePetOverlayPatch(source, settings, avatarSelectionRefreshExpected) { +function hasCompletePetOverlayPatch(source, settings, avatarSelectionRefreshExpected, localMascotDragExpected) { const requiredMarkers = [ source.includes("codexPetOverlaySettings(){"), /let [A-Za-z_$][\w$]*=this\.codexPetOverlayLayoutForDisplay\([A-Za-z_$][\w$]*,this\.getLayoutForDisplay\([A-Za-z_$][\w$]*\),[A-Za-z_$][\w$]*\);/.test(source), /process\.platform===`linux`\?this\.codexPetOverlaySyncWindow\([A-Za-z_$][\w$]*,!0\):[A-Za-z_$][\w$]*\.moveTop\(\),[A-Za-z_$][\w$]*\.showInactive\(\),/.test(source), source.includes("if(this.codexPetOverlayShouldLockPosition())return;"), - source.includes("this.codexPetOverlayBeginKWinDrag("), - source.includes("this.codexPetOverlayEndKWinDrag("), - source.includes("this.codexPetOverlayBeginNiriDrag("), - source.includes("this.codexPetOverlayEndNiriDrag("), source.includes("===`avatarOverlay`?{backgroundColor:`#00000000`,backgroundMaterial:null}:"), source.includes("title:`Codex Pet Overlay`,width:"), ]; + if (localMascotDragExpected) { + requiredMarkers.push( + source.includes("if(this.codexPetOverlayStartLocalMascotDrag("), + source.includes("if(this.codexPetOverlayMoveLocalMascotDrag("), + source.includes("if(this.codexPetOverlayEndLocalMascotDrag("), + source.includes("this.dragState=null,this.codexPetOverlayMascotDragState=null,"), + ); + } if (avatarSelectionRefreshExpected) { requiredMarkers.push( source.includes(`function ${AVATAR_SELECTION_REFRESH_MARKER}(`), @@ -424,7 +423,7 @@ function hasCompletePetOverlayPatch(source, settings, avatarSelectionRefreshExpe } if (settings.mode === "passive") { requiredMarkers.push( - source.includes("appearance:`avatarOverlay`,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:!1"), + source.includes("appearance:`avatarOverlay`,frame:process.platform===`linux`?!1:!0,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:!1"), ); } return requiredMarkers.every(Boolean); @@ -456,52 +455,29 @@ function applyPetOverlayPatch(source, context) { } const settings = mergedPetOverlaySettings(context); const avatarSelectionRefreshExpected = source.includes('"set-setting":async'); + const localMascotDragExpected = /moveDrag\([^)]*\)\{/.test(source); let patched = patchAvatarTransparentBackground(source); patched = patchCreateWindowTitle(patched); + patched = patchCreateWindowFrame(patched); patched = patchApplyLayout(patched); patched = patchShowWindow(patched); patched = patchLockedDrag(patched); - patched = patchCompositorDragLifecycle(patched); patched = ensurePetOverlayMethods(patched, settings); + patched = patchLocalMascotDrag(patched); + if (localMascotDragExpected) { + patched = patchLocalMascotDragLifecycle(patched); + } patched = patchPassiveCreateWindow(patched, settings); if (avatarSelectionRefreshExpected) { patched = patchAvatarSelectionRefresh(patched); } - if (!hasCompletePetOverlayPatch(patched, settings, avatarSelectionRefreshExpected)) { + if (!hasCompletePetOverlayPatch(patched, settings, avatarSelectionRefreshExpected, localMascotDragExpected)) { console.warn("WARN: Pet overlay patch is incomplete - discarding all pet overlay changes"); return source; } return patched; } -function applyPetOverlayPointerRegionPatch(source) { - const marker = "closest(`[data-avatar-overlay-hit-region]`)==null"; - if (source.includes(marker)) { - return source; - } - const dragMarkers = [...source.matchAll(/avatar-overlay-drag-start/g)]; - if (dragMarkers.length !== 1) { - console.warn("WARN: Expected one avatar overlay drag marker - skipping pet pointer-region patch"); - return source; - } - const dragMarkerIndex = dragMarkers[0].index; - // Scope the generic pointer predicate to the minified handler that owns the - // avatar drag dispatch. Other handlers in this asset use the same predicate. - const handlerStart = source.lastIndexOf("=>{", dragMarkerIndex); - const handlerPrefix = handlerStart === -1 ? "" : source.slice(handlerStart + 3, dragMarkerIndex); - const pointerGuard = /([A-Za-z_$][\w$]*)\.button!==0\|\|!\(\1\.target instanceof Element\)\|\|\1\.target\.closest\(`\.no-drag`\)!=null\|\|\(/g; - const matches = [...handlerPrefix.matchAll(pointerGuard)]; - if (matches.length !== 1) { - console.warn("WARN: Could not find avatar overlay pointer-down guard - skipping pet pointer-region patch"); - return source; - } - const [match] = matches; - const matchIndex = handlerStart + 3 + match.index; - const eventVar = match[1]; - const replacement = `${eventVar}.button!==0||!(${eventVar}.target instanceof Element)||${eventVar}.target.closest(\`[data-avatar-overlay-hit-region]\`)==null||${eventVar}.target.closest(\`.no-drag\`)!=null||(`; - return source.slice(0, matchIndex) + replacement + source.slice(matchIndex + match[0].length); -} - const descriptors = [ { id: DESCRIPTOR_ID, @@ -510,23 +486,11 @@ const descriptors = [ ciPolicy: "optional", apply: applyPetOverlayPatch, }, - { - id: POINTER_REGION_DESCRIPTOR_ID, - phase: "webview-asset", - order: 20_510, - ciPolicy: "optional", - pattern: /^avatar-overlay-page-[^.]+\.js$/, - missingDescription: "avatar overlay page bundle", - skipDescription: "pet pointer-region patch", - apply: applyPetOverlayPointerRegionPatch, - }, ]; module.exports = { DESCRIPTOR_ID, - POINTER_REGION_DESCRIPTOR_ID, descriptors, applyPetOverlayPatch, - applyPetOverlayPointerRegionPatch, mergedPetOverlaySettings, }; diff --git a/linux-features/pet-overlay/test.js b/linux-features/pet-overlay/test.js index 697c3854f..4613c0d08 100644 --- a/linux-features/pet-overlay/test.js +++ b/linux-features/pet-overlay/test.js @@ -16,9 +16,7 @@ const { } = require("../../scripts/lib/linux-features.js"); const { DESCRIPTOR_ID, - POINTER_REGION_DESCRIPTOR_ID, applyPetOverlayPatch, - applyPetOverlayPointerRegionPatch, mergedPetOverlaySettings, } = require("./patch.js"); @@ -53,17 +51,17 @@ function currentAvatarOverlayBundleFixture() { "var settingsHandlers={\"set-setting\":async({key:e,value:t})=>(this.setSettingValue(e,t),{success:!0})};", "var rV=`/avatar-overlay`,zB={width:356,height:320},oV={width:112,height:121},k2={width:0,height:0},O2={width:276,height:131};", "var h2=class{constructor(e,t,n,r){this.cursorSource=e;this.pointerAnchorX=t;this.pointerAnchorY=n;this.displayBounds=r}};", - "var fV=class{window=null;rendererReady=!1;layout=null;mascotSize=oV;traySize=null;pointerInteractive=!1;mousePassthroughEnabled=!1;windowStagedForNativePresentation=!1;layoutMode=`native`;compositionHost={setOverlayWindow(){},isNativeMaterialAttached(){return!1},getCursorPosition(){return null},performWindowDrag(){return!1},updateMascotRect(){}};nativePositionController={clear(){}};", + "var fV=class{window=null;rendererReady=!1;layout=null;mascotSize=oV;traySize=null;pointerInteractive=!1;mousePassthroughEnabled=!1;windowStagedForNativePresentation=!1;layoutMode=`native`;compositionHost={setOverlayWindow(){},isNativeMaterialAttached(){return!1},getCursorPosition(){return null},updateMascotRect(){}};nativePositionController={clear(){}};", "constructor(e,t){this.windowManager=e,this.globalState=t}", "isOpen(){let e=this.window;return e!=null&&!e.isDestroyed()&&e.isVisible()&&!this.windowStagedForNativePresentation}", "startDrag(e,t,n=!1){let r=this.window;if(r==null||r.isDestroyed()||r.webContents.id!==e)return;this.cancelMomentum();let i=this.getLayout(r),o=this.compositionHost.getCursorPosition(),s=t.pointerScreenX!=null?{x:t.pointerScreenX,y:t.pointerScreenY}:a.screen.getCursorScreenPoint();this.dragState=new h2(o==null?`renderer`:`native`,t.pointerWindowX-i.mascot.left,t.pointerWindowY-i.mascot.top,a.screen.getDisplayNearestPoint(s).bounds,n),this.windowServerDragActive=this.layoutMode===`native`&&!n&&this.compositionHost.performWindowDrag(),this.windowServerDragActive||(this.windowServerDragWindowX=null)}", - "endDrag(e,t){let n=this.window;if(n==null||n.isDestroyed()||n.webContents.id!==e)return;let r=this.dragState,i=this.windowServerDragActive,a=null;this.dragState=null,this.windowServerDragActive=!1,this.windowServerDragWindowX=null,i?this.persistWindowBounds(n,a??this.getCurrentDisplay()):this.reclampWindowToVisibleDisplay({shouldPersist:!0});let o=this.dockTarget;o!=null&&this.dockPresentation(o.anchor,o.onDock)}", + "moveDrag(e,t){let n=this.window;if(n==null||n.isDestroyed()||n.webContents.id!==e)return;this.lastMove=t}endDrag(e,t){let n=this.window;if(n==null||n.isDestroyed()||n.webContents.id!==e)return;let r=this.dragState,i=this.windowServerDragActive,a=null;this.dragState=null,this.windowServerDragActive=!1,this.windowServerDragWindowX=null,i?this.persistWindowBounds(n,a??this.getCurrentDisplay()):this.reclampWindowToVisibleDisplay({shouldPersist:!0});let o=this.dockTarget;o!=null&&this.dockPresentation(o.anchor,o.onDock)}", "setElementSize(e,{elementSizeRevision:t,isTrayVisible:n,mascot:r,nativeCompositionEnabled:a,tray:o}){let i=this.window;i==null||i.isDestroyed()||i.webContents.id!==e||(this.cancelMomentum(),this.layoutMode=n==null?`native`:`legacy`,this.mascotSize=r,this.traySize=o,this.applyLatestElementSizes(i),this.stageWindowForNativePresentation(i),this.showWindowIfReady(i))}", "applyLatestElementSizes(e){this.anchor={...this.anchor,width:this.mascotSize.width,height:this.mascotSize.height},this.applyLayout(e)}", "async createWindow(e){let t=await this.windowManager.createWindow({title:a.app.getName(),width:zB.width,height:zB.height,appearance:`avatarOverlay`,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:process.platform===`linux`?!0:!1,show:!1,initialRoute:rV});return this.window=t,this.compositionHost.setOverlayWindow(t),this.rendererReady=this.windowManager.isWebContentsReady(t.webContents.id),this.displayBounds=null,this.displayId=null,this.dragState=null,this.layout=null,this.mascotSize=oV,this.mousePassthroughEnabled=!1,this.traySize=null,t.on(`closed`,()=>{this.window===t&&(this.cancelMomentum(),this.window=null,this.dragState=null,this.layout=null,this.rendererReady=!1,this.pointerInteractive=!1,this.mousePassthroughEnabled=!1,this.compositionHost.setOverlayWindow(null),this.broadcastOpenState())}),t}", "applyLayout(e,t=this.getCurrentDisplay(),n=!1,r=!0,i=null){if(e.isDestroyed())return;let o=this.getLayoutForDisplay(t);this.displayId=t.id,this.layout=o,this.setWindowBounds(e,o.windowBounds,n,r),this.compositionHost.updateMascotRect(o.mascot),this.sendLayoutToRenderer(e,i)}getLayoutForDisplay(e){return UB({anchor:this.anchor,displayBounds:this.layoutMode===`native`?e.workArea:e.bounds,mode:this.layoutMode,mascotSize:this.mascotSize,nativeMaterialAttached:this.compositionHost.isNativeMaterialAttached(),previousPlacement:this.placement,traySize:this.traySize??(this.layoutMode===`native`?k2:O2)})}getLayout(e){if(this.layout??this.applyLayout(e),this.layout==null)throw Error(`Expected avatar overlay layout`);return this.layout}", "showWindow(e){if(e.isDestroyed())return;let t=this.isOpen();this.windowStagedForNativePresentation&&=(e.setOpacity(1),!1),e.moveTop(),e.showInactive(),!t&&this.isOpen()&&(this.finishPendingPresentation(),this.broadcastOpenState())}showWindowIfReady(e){!this.rendererReady||this.initialPresentationState!==`ready`||(this.showWindow(e),this.applyPointerInteractivityPolicy())}stageWindowForNativePresentation(e){e.isDestroyed()||this.applyPointerInteractivityPolicy()}broadcastOpenState(){this.windowManager.sendMessageToAllRegisteredWindows({type:`avatar-overlay-open-state-changed`,isOpen:this.isOpen()})}", - "applyPointerInteractivityPolicy(){return null}cancelMomentum(){}finishPendingPresentation(){}sendLayoutToRenderer(){}setWindowBounds(){}persistWindowBounds(){}reclampWindowToVisibleDisplay({shouldPersist:e}){e&&this.persistWindowBounds(this.window,this.getCurrentDisplay())}dockPresentation(){}getCurrentDisplay(){return{id:1,bounds:{x:0,y:0,width:1920,height:1080},workArea:{x:0,y:0,width:1920,height:1080}}}};", + "applyPointerInteractivityPolicy(){return null}cancelMomentum(){}finishPendingPresentation(){}sendLayoutToRenderer(){}setWindowBounds(){}getCurrentDisplay(){return{id:1,bounds:{x:0,y:0,width:1920,height:1080},workArea:{x:0,y:0,width:1920,height:1080}}}};", "function L9({platform:e,appearance:t,opaqueWindowSurfaceEnabled:n,prefersDarkColors:r}){return n?{backgroundColor:r?_ne:vne,backgroundMaterial:e===`win32`?`none`:null}:e===`win32`?{backgroundColor:k9,backgroundMaterial:`mica`}:{backgroundColor:k9,backgroundMaterial:null}}", ].join(""); } @@ -94,15 +92,6 @@ function controllerFromPatchedSource(patched, overrides = {}) { if (moduleName === "node:child_process") { return overrides.childProcess ?? { execFile() {} }; } - if (moduleName === "node:fs") { - return fs; - } - if (moduleName === "node:os") { - return os; - } - if (moduleName === "node:path") { - return path; - } if (moduleName === "electron") { return { app: { getName: () => "Codex" }, @@ -163,10 +152,7 @@ test("pet-overlay is discoverable and disabled until listed in features.json", ( const descriptors = loadLinuxFeaturePatchDescriptors({ featuresRoot }); assert.deepEqual( descriptors.map((descriptor) => [descriptor.id, descriptor.phase, descriptor.ciPolicy]), - [ - [`feature:pet-overlay:${DESCRIPTOR_ID}`, "main-bundle", "optional"], - [`feature:pet-overlay:${POINTER_REGION_DESCRIPTOR_ID}`, "webview-asset", "optional"], - ], + [[`feature:pet-overlay:${DESCRIPTOR_ID}`, "main-bundle", "optional"]], ); const plan = enabledLinuxFeatureInstallPlan({ featuresRoot }); assert.deepEqual( @@ -227,28 +213,6 @@ test("patches current avatar overlay layout, transparency, and window sync", () assert.equal((patched.match(/codexPetOverlayLayoutForDisplay/g) ?? []).length, 2); }); -test("limits renderer drag starts to visible pet and tray hit regions", () => { - const unrelated = "a=e=>{e.button!==0||!(e.target instanceof Element)||e.target.closest(`.no-drag`)!=null||(e.preventDefault(),k.dispatchMessage(`unrelated-drag-start`,{}))};"; - const avatar = "Ge=e=>{e.button!==0||!(e.target instanceof Element)||e.target.closest(`.no-drag`)!=null||(e.preventDefault(),k.dispatchMessage(`avatar-overlay-drag-start`,{}))}"; - const source = unrelated + avatar; - const patched = applyPetOverlayPointerRegionPatch(source); - - assert.match(patched, new RegExp(unrelated.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); - assert.match( - patched, - /e\.target\.closest\(`\[data-avatar-overlay-hit-region\]`\)==null\|\|e\.target\.closest\(`\.no-drag`\)!=null/, - ); - assert.equal(applyPetOverlayPointerRegionPatch(patched), patched); -}); - -test("pointer-region patch fails closed when the current pointer guard drifts", () => { - const source = "Ge=e=>{if(e.button===0)k.dispatchMessage(`avatar-overlay-drag-start`,{})}"; - const { result, warnings } = captureWarnings(() => applyPetOverlayPointerRegionPatch(source)); - - assert.equal(result, source); - assert.match(warnings.join("\n"), /Could not find avatar overlay pointer-down guard/); -}); - test("refreshes only the avatar overlay after the selected pet changes", async () => { const patched = applyPatchTwice(currentAvatarOverlayBundleFixture()); const reloads = []; @@ -342,20 +306,6 @@ test("discards every change when a required current hook drifts", () => { assert.match(warnings.join("\n"), /Pet overlay patch is incomplete/); }); -test("patches the current dock-threshold drag completion shape", () => { - const source = currentAvatarOverlayBundleFixture().replace( - "i?this.persistWindowBounds(n,a??this.getCurrentDisplay()):this.reclampWindowToVisibleDisplay({shouldPersist:!0});let o=this.dockTarget;o!=null&&this.dockPresentation(o.anchor,o.onDock)", - "i?this.persistWindowBounds(n,a??this.getCurrentDisplay()):this.reclampWindowToVisibleDisplay({shouldPersist:!0});let o=this.dockTarget,s=this.anchor;o!=null&&shouldDock({current:s,target:o.anchor}).shouldDock&&this.dockPresentation(o.anchor,o.onDock)", - ); - const { result, warnings } = captureWarnings(() => applyPetOverlayPatch(source)); - - assert.notEqual(result, source); - assert.doesNotMatch(warnings.join("\n"), /drag completion shape/); - assert.match(result, /this\.codexPetOverlayEndKWinDrag\(n,\(\)=>\{i\?this\.persistWindowBounds/); - assert.match(result, /this\.codexPetOverlayEndNiriDrag\(n,\(\)=>\{i\?this\.persistWindowBounds/); - assert.match(result, /let o=this\.dockTarget,s=this\.anchor;o!=null&&shouldDock/); -}); - test("passive mode fails closed when the current create-window shape drifts", () => { const source = currentAvatarOverlayBundleFixture().replace( "appearance:`avatarOverlay`,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:process.platform===`linux`?!0:!1", @@ -499,6 +449,199 @@ test("unlocked layout does not re-anchor while a drag is active", () => { assert.deepEqual(JSON.parse(JSON.stringify(result.windowBounds)), layout.windowBounds); }); +test("unlocked mascot drags stay inside the overlay and do not invoke native window dragging", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const { controller } = controllerFromPatchedSource(patched); + const nativeDragCalls = []; + const rendererLayouts = []; + controller.window = { + getBounds: () => ({ x: 100, y: 200, width: 356, height: 320 }), + isDestroyed: () => false, + webContents: { id: 1 }, + }; + controller.layout = { + anchor: { x: 110, y: 210, width: 40, height: 40 }, + mascot: { left: 10, top: 10, width: 40, height: 40 }, + placement: "top-end", + tray: { left: 10, top: 54, width: 276, height: 131 }, + windowBounds: { x: 100, y: 200, width: 356, height: 320 }, + }; + controller.compositionHost = { + performWindowDrag: () => nativeDragCalls.push("window"), + updateMascotRect: () => {}, + }; + controller.sendLayoutToRenderer = (_window, layout) => rendererLayouts.push(layout); + + controller.startDrag(1, { pointerWindowX: 20, pointerWindowY: 20 }); + controller.moveDrag(1, { pointerScreenX: 350, pointerScreenY: 500 }); + + assert.deepEqual(nativeDragCalls, []); + assert.deepEqual(JSON.parse(JSON.stringify(controller.codexPetOverlayMascotLocalPosition)), { left: 240, top: 280 }); + assert.deepEqual(JSON.parse(JSON.stringify(controller.layout.mascot)), { left: 240, top: 280, width: 40, height: 40 }); + assert.deepEqual(JSON.parse(JSON.stringify(controller.layout.tray)), { left: 4, top: 145, width: 276, height: 131 }); + assert.equal(rendererLayouts.length, 1); +}); + +test("notification tray chooses a non-overlapping vertical side and stays within overlay bounds", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const { controller } = controllerFromPatchedSource(patched); + const windowBounds = { x: 400, y: 300, width: 356, height: 320 }; + const baseLayout = { + anchor: { x: 410, y: 310, width: 112, height: 121 }, + mascot: { left: 10, top: 10, width: 112, height: 121 }, + placement: "top-end", + tray: { left: 0, top: 0, width: 276, height: 131 }, + windowBounds, + }; + + controller.codexPetOverlayMascotLocalPosition = { left: 0, top: 0 }; + const atTop = controller.codexPetOverlayTrayAboveLeft(baseLayout); + assert.deepEqual(JSON.parse(JSON.stringify(atTop.mascot)), { left: 0, top: 0, width: 112, height: 121 }); + assert.equal(atTop.tray.top, 125); + assert.ok(atTop.tray.top >= atTop.mascot.top + atTop.mascot.height + 4); + assert.deepEqual(JSON.parse(JSON.stringify(atTop.windowBounds)), windowBounds); + + controller.codexPetOverlayMascotLocalPosition = { left: 100, top: 100 }; + const snappedFromMiddle = controller.codexPetOverlayTrayAboveLeft(baseLayout); + assert.deepEqual(JSON.parse(JSON.stringify(snappedFromMiddle.mascot)), { left: 100, top: 135, width: 112, height: 121 }); + assert.deepEqual(JSON.parse(JSON.stringify(controller.codexPetOverlayMascotLocalPosition)), { left: 100, top: 135 }); + assert.equal(snappedFromMiddle.tray.top, 0); + assert.ok(snappedFromMiddle.tray.top + snappedFromMiddle.tray.height + 4 <= snappedFromMiddle.mascot.top); + assert.deepEqual(JSON.parse(JSON.stringify(snappedFromMiddle.windowBounds)), windowBounds); + + controller.codexPetOverlayMascotLocalPosition = { left: 244, top: 180 }; + const withUpperSpace = controller.codexPetOverlayTrayAboveLeft(baseLayout); + assert.deepEqual(JSON.parse(JSON.stringify(withUpperSpace.mascot)), { left: 244, top: 180, width: 112, height: 121 }); + assert.equal(withUpperSpace.tray.top, 45); + assert.ok(withUpperSpace.tray.top + withUpperSpace.tray.height + 4 <= withUpperSpace.mascot.top); + assert.ok(withUpperSpace.tray.left >= 0); + assert.ok(withUpperSpace.tray.left + withUpperSpace.tray.width <= windowBounds.width); + assert.ok(withUpperSpace.tray.top >= 0); + assert.ok(withUpperSpace.tray.top + withUpperSpace.tray.height <= windowBounds.height); + assert.deepEqual(JSON.parse(JSON.stringify(withUpperSpace.windowBounds)), windowBounds); + + controller.codexPetOverlayMascotLocalPosition = { left: 0, top: 0 }; + const oversizedTray = controller.codexPetOverlayTrayAboveLeft({ + ...baseLayout, + tray: { left: 0, top: 0, width: 700, height: 700 }, + }); + assert.equal(oversizedTray.tray.width, 356); + assert.equal(oversizedTray.tray.height, 195); + assert.ok(oversizedTray.tray.left >= 0); + assert.ok(oversizedTray.tray.top >= 0); + assert.ok(oversizedTray.tray.left + oversizedTray.tray.width <= windowBounds.width); + assert.ok(oversizedTray.tray.top + oversizedTray.tray.height <= windowBounds.height); + assert.ok(oversizedTray.tray.top >= oversizedTray.mascot.top + oversizedTray.mascot.height + 4); +}); + +test("another renderer cannot consume or mutate a local mascot drag", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const { controller } = controllerFromPatchedSource(patched); + const nativeDragCalls = []; + let boundsWrites = 0; + controller.window = { + getBounds: () => ({ x: 100, y: 200, width: 356, height: 320 }), + isDestroyed: () => false, + webContents: { id: 1 }, + }; + controller.layout = { + anchor: { x: 110, y: 210, width: 40, height: 40 }, + mascot: { left: 10, top: 10, width: 40, height: 40 }, + placement: "top-end", + tray: { left: 10, top: 54, width: 276, height: 131 }, + windowBounds: { x: 100, y: 200, width: 356, height: 320 }, + }; + controller.compositionHost = { + getCursorPosition: () => null, + performWindowDrag: () => nativeDragCalls.push("window"), + updateMascotRect() {}, + }; + controller.setWindowBounds = () => { + boundsWrites += 1; + }; + const before = JSON.parse(JSON.stringify(controller.layout)); + + controller.startDrag(2, { pointerWindowX: 20, pointerWindowY: 20 }); + controller.moveDrag(2, { pointerScreenX: 350, pointerScreenY: 500 }); + + assert.equal(controller.codexPetOverlayMascotDragState, undefined); + assert.equal(controller.codexPetOverlayMascotLocalPosition, undefined); + assert.deepEqual(JSON.parse(JSON.stringify(controller.layout)), before); + assert.deepEqual(nativeDragCalls, []); + assert.equal(boundsWrites, 0); + + controller.endDrag(2, {}); + assert.equal(controller.codexPetOverlayMascotDragState, undefined); +}); + +test("overlay close and recreate clear only ephemeral mascot drag state", async () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const { controller } = controllerFromPatchedSource(patched); + let closed; + const localPosition = { left: 120, top: 80 }; + controller.codexPetOverlayMascotDragState = { offsetX: 3, offsetY: 4 }; + controller.codexPetOverlayMascotLocalPosition = localPosition; + controller.windowManager.createWindow = async () => ({ + isDestroyed: () => false, + isVisible: () => false, + on(event, handler) { + if (event === "closed") { + closed = handler; + } + }, + webContents: { id: 1 }, + }); + + await controller.createWindow(); + assert.equal(controller.codexPetOverlayMascotDragState, null); + assert.equal(controller.codexPetOverlayMascotLocalPosition, localPosition); + + controller.codexPetOverlayMascotDragState = { offsetX: 7, offsetY: 8 }; + closed(); + assert.equal(controller.codexPetOverlayMascotDragState, null); + assert.equal(controller.codexPetOverlayMascotLocalPosition, localPosition); +}); + +test("background drags retain native window movement and local mascot offsets survive later layout", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const { controller } = controllerFromPatchedSource(patched); + const nativeDragCalls = []; + controller.window = { + getBounds: () => ({ x: 900, y: 410, width: 356, height: 320 }), + isDestroyed: () => false, + isVisible: () => true, + webContents: { id: 1 }, + }; + controller.layout = { + anchor: { x: 110, y: 210, width: 40, height: 40 }, + mascot: { left: 10, top: 10, width: 40, height: 40 }, + placement: "top-end", + tray: { left: 10, top: 54, width: 276, height: 131 }, + windowBounds: { x: 100, y: 200, width: 356, height: 320 }, + }; + controller.compositionHost = { + getCursorPosition: () => null, + performWindowDrag: () => nativeDragCalls.push("window"), + updateMascotRect() {}, + }; + controller.getCurrentDisplay = () => null; + controller.persistWindowBounds = () => {}; + controller.codexPetOverlayMascotLocalPosition = { left: 100, top: 50 }; + + controller.startDrag(1, { pointerWindowX: 300, pointerWindowY: 20 }); + assert.deepEqual(nativeDragCalls, ["window"]); + controller.endDrag(1, {}); + + const result = controller.codexPetOverlayLayoutForDisplay( + { workArea: { x: 0, y: 0, width: 1920, height: 1080 } }, + controller.layout, + controller.window, + ); + assert.deepEqual(JSON.parse(JSON.stringify(result.windowBounds)), { x: 900, y: 410, width: 356, height: 320 }); + assert.deepEqual(JSON.parse(JSON.stringify(result.mascot)), { left: 100, top: 50, width: 40, height: 40 }); + assert.deepEqual(JSON.parse(JSON.stringify(result.tray)), { left: 0, top: 94, width: 276, height: 131 }); +}); + test("syncs overlay window hints without requiring Hyprland", () => { const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); const calls = []; @@ -550,6 +693,13 @@ test("syncs overlay window hints without requiring Hyprland", () => { assert.equal(calls[6][2].cssOrigin, "author"); assert.equal(calls[7][0], "js"); assert.match(calls[6][1], /background:transparent!important/); + assert.match(calls[6][1], /-webkit-app-region:drag!important/); + assert.match(calls[6][1], /app-region:drag!important/); + assert.match(calls[6][1], /user-select:none!important/); + assert.match( + calls[6][1], + /\[data-avatar-overlay-hit-region="mascot"\],\[data-avatar-mascot="true"\],\.no-drag,\[data-avatar-overlay-hit-region="notification-tray"\],\[data-avatar-overlay-hit-region="notification-scroll-control"\]\{-webkit-app-region:no-drag!important;app-region:no-drag!important;\}/, + ); assert.match(calls[7][1], /document\.documentElement\.style\.background/); assert.deepEqual(calls.slice(8), [["opacity", 1], ["workspaces", true, true], "moveTop", "showInactive"]); assert.deepEqual(timers.map((timer) => timer.delay), [0]); @@ -560,8 +710,97 @@ test("syncs overlay window hints without requiring Hyprland", () => { assert.deepEqual(calls.at(-1), ["focusable", true]); handlers["did-finish-load"](); - assert.equal(calls.filter(([kind]) => kind === "css").length, 2); - assert.equal(calls.filter(([kind]) => kind === "js").length, 2); + const cssCalls = calls.filter(([kind]) => kind === "css"); + const jsCalls = calls.filter(([kind]) => kind === "js"); + assert.equal(cssCalls.length, 2); + assert.equal(jsCalls.length, 2); + assert.match(cssCalls[1][1], /background:transparent!important/); + assert.match(cssCalls[1][1], /-webkit-app-region:drag!important/); + assert.match(cssCalls[1][1], /app-region:drag!important/); + assert.match( + cssCalls[1][1], + /\[data-avatar-overlay-hit-region="mascot"\],\[data-avatar-mascot="true"\],\.no-drag,\[data-avatar-overlay-hit-region="notification-tray"\],\[data-avatar-overlay-hit-region="notification-scroll-control"\]\{-webkit-app-region:no-drag!important;app-region:no-drag!important;\}/, + ); + assert.match(jsCalls[1][1], /document\.documentElement\.style\.background/); +}); + +test("unlocked pet overlays are frameless only on Linux and opt into whole-window input", async () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + assert.match(patched, /frame:process\.platform===`linux`\?!1:!0/); + assert.match(patched, /codexPetOverlayShouldUseWholeWindowInput\(\)\{return process\.platform===`linux`&&this\.codexPetOverlaySettings\(\)\.lockPosition!==!0\}/); + assert.match(patched, /codexLinuxWholeWindowInput=this\.codexPetOverlayShouldUseWholeWindowInput\(\)/); + + const { controller } = controllerFromPatchedSource(patched); + const created = []; + const makeWindow = () => ({ + isDestroyed: () => false, + isVisible: () => false, + on() {}, + webContents: { id: 1 }, + }); + controller.windowManager.createWindow = async (options) => { + created.push(options); + return makeWindow(); + }; + await controller.createWindow(); + assert.equal(created[0].frame, false); + controller.codexPetOverlaySyncWindow(controller.window); + assert.equal(controller.codexLinuxWholeWindowInput, true); + + const { controller: nonLinuxController } = controllerFromPatchedSource(patched, { + process: { platform: "darwin" }, + }); + const nonLinuxCreated = []; + nonLinuxController.windowManager.createWindow = async (options) => { + nonLinuxCreated.push(options); + return makeWindow(); + }; + await nonLinuxController.createWindow(); + assert.equal(nonLinuxCreated[0].frame, true); + assert.equal(nonLinuxController.codexLinuxWholeWindowInput, undefined); +}); + +test("unlocked drag CSS protects native mascot markup without an overlay hit region", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const insertedCss = []; + const { controller } = controllerFromPatchedSource(patched); + const nativeMascotMarkup = '
'; + assert.doesNotMatch(nativeMascotMarkup, /data-avatar-overlay-hit-region/); + + controller.codexPetOverlayInstallTransparentRenderer({ + isDestroyed: () => false, + webContents: { + insertCSS: (css) => insertedCss.push(css), + isDestroyed: () => false, + on() {}, + }, + }); + + assert.match( + insertedCss[0], + /\[data-avatar-mascot="true"\],\.no-drag,\[data-avatar-overlay-hit-region="notification-tray"\],\[data-avatar-overlay-hit-region="notification-scroll-control"\]\{-webkit-app-region:no-drag!important;app-region:no-drag!important;\}/, + ); +}); + +test("locked pet overlays omit drag CSS and whole-window input", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture(), { + feature: { manifest: { petOverlay: { lockPosition: true } }, settings: {} }, + }); + const calls = []; + const { controller } = controllerFromPatchedSource(patched); + const window = { + isDestroyed: () => false, + webContents: { + insertCSS: (css) => calls.push(css), + isDestroyed: () => false, + on() {}, + }, + }; + controller.codexPetOverlayInstallTransparentRenderer(window); + assert.equal(calls.length, 1); + assert.doesNotMatch(calls[0], /app-region:(drag|no-drag)/); + assert.doesNotMatch(calls[0], /-webkit-app-region:(drag|no-drag)/); + assert.equal(controller.codexLinuxWholeWindowInput, false); }); test("passive mode makes the overlay non-focusable", () => { @@ -571,7 +810,7 @@ test("passive mode makes the overlay non-focusable", () => { assert.match( patched, - /appearance:`avatarOverlay`,alwaysOnTop:process\.platform===`linux`,skipTaskbar:process\.platform===`linux`,focusable:!1/, + /appearance:`avatarOverlay`,frame:process\.platform===`linux`\?!1:!0,alwaysOnTop:process\.platform===`linux`,skipTaskbar:process\.platform===`linux`,focusable:!1/, ); }); @@ -671,6 +910,7 @@ test("runtime lock override blocks drag start", () => { process: { env: { CODEX_PET_OVERLAY_LOCK_POSITION: "1" } }, }); controller.window = { isDestroyed: () => false, webContents: { id: 1 } }; + controller.layoutMode = "legacy"; controller.getLayout = () => ({ mascot: { left: 0, top: 0 } }); controller.dragState = { preserved: true }; @@ -692,6 +932,7 @@ test("runtime unlock override permits drag on a locked build", () => { process: { env: { CODEX_PET_OVERLAY_LOCK_POSITION: "0" } }, }); controller.window = { isDestroyed: () => false, webContents: { id: 1 } }; + controller.layoutMode = "legacy"; controller.getLayout = () => ({ mascot: { left: 0, top: 0 } }); controller.startDrag(1, { @@ -802,58 +1043,6 @@ function runNiriHintScenario({ return calls; } -function createAsyncNiriDragScenario() { - const calls = []; - const pending = []; - const timers = []; - const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); - const { controller } = controllerFromPatchedSource(patched, { - process: { env: { XDG_CURRENT_DESKTOP: "niri" } }, - childProcess: { - execFile(command, args, options, callback) { - assert.equal(command, "niri"); - assert.equal(options.timeout, 1200); - calls.push(args); - pending.push({ args, callback }); - }, - }, - setTimeout(callback, delay) { - const timer = { callback, delay, unref() {} }; - timers.push(timer); - return timer; - }, - clearTimeout(timer) { - timer.cleared = true; - }, - }); - const window = { - getBounds: () => ({ x: 100, y: 100, width: 356, height: 320 }), - isDestroyed: () => false, - webContents: { id: 1 }, - }; - controller.window = window; - controller.codexPetOverlayDesiredDisplayBounds = { x: 0, y: 0, width: 1920, height: 1080 }; - controller.codexPetOverlayDesiredWindowBounds = { x: 100, y: 100, width: 356, height: 320 }; - return { calls, controller, pending, timers, window }; -} - -function completePendingNiriCall(scenario, { error = null, stdout = "ok" } = {}) { - const call = scenario.pending.shift(); - assert.ok(call, "expected a pending niri call"); - call.callback(error, stdout); - return call.args; -} - -function niriPetWindow(id, isFloating = true) { - return JSON.stringify([{ - id, - is_floating: isFloating, - layout: { window_size: [356, 320] }, - pid: 4242, - title: "Codex Pet Overlay", - }]); -} - test("targets only the unambiguous Hyprland pet window address", () => { const calls = runHyprlandHintScenario({ clientsJson: JSON.stringify([ @@ -1278,202 +1467,6 @@ test("environment overrides can turn Hyprland handling off", () => { assert.deepEqual(calls, []); }); -test("KWin bridge applies keep-above hints and exact Wayland geometry to only the matching pet", () => { - const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture(), { - feature: { manifest: { petOverlay: { kwin: true, lockPosition: true } }, settings: {} }, - }); - const calls = []; - let script = null; - const { controller } = controllerFromPatchedSource(patched, { - process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, - childProcess: { - execFile(command, args, options, callback) { - calls.push([command, args]); - assert.equal(command, "qdbus6"); - assert.equal(options.timeout, 1500); - if (args.includes("org.kde.kwin.Scripting.loadScript")) { - script = fs.readFileSync(args[3], "utf8"); - } - callback(null, "ok"); - }, - }, - }); - const window = { isDestroyed: () => false }; - controller.window = window; - controller.codexPetOverlayDesiredWindowBounds = { x: 610, y: 330, width: 356, height: 320 }; - - controller.codexPetOverlayApplyKWinHints(window); - - assert.deepEqual( - calls.map(([, args]) => args[2]), - [ - "org.kde.kwin.Scripting.loadScript", - "org.kde.kwin.Scripting.start", - "org.kde.kwin.Scripting.unloadScript", - ], - ); - assert.ok(script); - const pet = { - caption: "Codex Pet Overlay", - frameGeometry: { x: 10, y: 20, width: 356, height: 320 }, - pid: 4242, - }; - const main = { - caption: "ChatGPT", - frameGeometry: { x: 0, y: 0, width: 1280, height: 820 }, - pid: 4242, - }; - const raised = []; - vm.runInNewContext(script, { - workspace: { - raiseWindow: (target) => raised.push(target), - windowList: () => [main, pet], - }, - }); - - assert.equal(pet.keepAbove, true); - assert.equal(pet.onAllDesktops, true); - assert.equal(pet.skipTaskbar, true); - assert.equal(pet.skipPager, true); - assert.equal(pet.noBorder, true); - assert.deepEqual(JSON.parse(JSON.stringify(pet.frameGeometry)), { x: 610, y: 330, width: 356, height: 320 }); - assert.deepEqual(raised, [pet]); - assert.equal(main.keepAbove, undefined); - - const duplicateA = { caption: "Codex Pet Overlay", frameGeometry: {}, pid: 4242 }; - const duplicateB = { caption: "Codex Pet Overlay", frameGeometry: {}, pid: 4242 }; - vm.runInNewContext(script, { - workspace: { - raiseWindow() {}, - windowList: () => [duplicateA, duplicateB], - }, - }); - assert.equal(duplicateA.keepAbove, undefined); - assert.equal(duplicateB.keepAbove, undefined); -}); - -test("KWin drag follows compositor cursor changes without losing the grab offset", () => { - const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); - const calls = []; - let script = null; - const { controller } = controllerFromPatchedSource(patched, { - process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, - childProcess: { - execFileSync(command, args, options) { - calls.push([command, args, options]); - if (args.includes("org.kde.kwin.Scripting.loadScript")) { - script = fs.readFileSync(args[3], "utf8"); - } - }, - }, - }); - const window = { - getContentBounds: () => ({ x: 145, y: 210, width: 356, height: 320 }), - isDestroyed: () => false, - }; - let persisted = false; - controller.window = window; - - controller.codexPetOverlayBeginKWinDrag(window); - assert.equal(calls.length, 2); - assert.equal(calls[0][0], "qdbus6"); - assert.equal(calls[0][1][2], "org.kde.kwin.Scripting.loadScript"); - assert.equal(calls[1][1][2], "org.kde.kwin.Scripting.start"); - assert.equal(calls[0][2].timeout, 750); - assert.equal(controller.windowServerDragActive, true); - assert.equal(controller.windowServerDragWindowX, 145); - assert.ok(script); - - const cursorSignal = { callback: null, connect(callback) { this.callback = callback; }, disconnect() {} }; - const removedSignal = { connect() {} }; - const pet = { - caption: "Codex Pet Overlay", - frameGeometry: { x: 100, y: 200, width: 356, height: 320 }, - pid: 4242, - }; - const workspace = { - cursorPos: { x: 130, y: 250 }, - cursorPosChanged: cursorSignal, - raiseWindow() {}, - windowList: () => [pet], - windowRemoved: removedSignal, - }; - vm.runInNewContext(script, { workspace }); - assert.equal(typeof cursorSignal.callback, "function"); - workspace.cursorPos = { x: 300, y: 410 }; - cursorSignal.callback(); - assert.deepEqual( - JSON.parse(JSON.stringify(pet.frameGeometry)), - { x: 270, y: 360, width: 356, height: 320 }, - ); - - controller.codexPetOverlayQueueKWinDrag(window); - assert.equal(calls.length, 2, "pointer updates must not spawn compositor processes"); - const scriptPath = controller.codexPetOverlayKWinDragState.scriptPath; - assert.equal(fs.existsSync(scriptPath), true); - assert.equal(controller.codexPetOverlayEndKWinDrag(window, () => { persisted = true; }), true); - assert.equal(calls.length, 3); - assert.equal(calls[2][1][2], "org.kde.kwin.Scripting.unloadScript"); - assert.equal(fs.existsSync(scriptPath), false); - assert.equal(persisted, true); - assert.equal(controller.codexPetOverlayKWinDragState, null); -}); - -test("KWin drag falls back without repeatedly probing missing qdbus commands", () => { - const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); - let calls = 0; - const { controller } = controllerFromPatchedSource(patched, { - process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, - childProcess: { - execFileSync() { - calls += 1; - const error = new Error("qdbus missing"); - error.code = "ENOENT"; - throw error; - }, - }, - }); - const window = { isDestroyed: () => false }; - controller.window = window; - - controller.codexPetOverlayBeginKWinDrag(window); - - assert.equal(calls, 2); - assert.equal(controller.codexPetOverlayKWinDragState, undefined); - assert.equal(controller.windowServerDragActive, undefined); - - controller.codexPetOverlayBeginKWinDrag(window); - assert.equal(calls, 2); -}); - -test("settings and environment can disable KWin handling", () => { - const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture(), { - feature: { manifest: { petOverlay: { kwin: true } }, settings: { petOverlay: { kwin: false } } }, - }); - const disabled = controllerFromPatchedSource(patched, { - process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, - }).controller; - assert.equal(disabled.codexPetOverlayShouldUseKWin(), false); - - const overridden = controllerFromPatchedSource( - applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), - { process: { env: { XDG_CURRENT_DESKTOP: "KDE", CODEX_PET_OVERLAY_KWIN: "0" } } }, - ).controller; - assert.equal(overridden.codexPetOverlayShouldUseKWin(), false); - - const kdeSession = controllerFromPatchedSource( - applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), - { process: { env: { KDE_SESSION_VERSION: "6" } } }, - ).controller; - assert.equal(kdeSession.codexPetOverlayShouldUseKWin(), true); - - const falseKdeSession = controllerFromPatchedSource( - applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), - { process: { env: { KDE_FULL_SESSION: "false" } } }, - ).controller; - assert.equal(falseKdeSession.codexPetOverlayShouldUseKWin(), false); -}); - test("targets a tiled Niri pet window by id and moves it without focus actions", () => { const calls = runNiriHintScenario({ windowsJson: JSON.stringify([ @@ -1782,242 +1775,6 @@ test("Niri scheduling is coalesced when desired bounds change repeatedly", () => assert.deepEqual(cleared, timers.slice(0, 4)); }); -test("Niri drag keeps one move in flight and emits only the latest queued target", () => { - const scenario = createAsyncNiriDragScenario(); - const { controller, pending, window } = scenario; - - controller.codexPetOverlayBeginNiriDrag(window); - assert.equal(pending.length, 1); - completePendingNiriCall(scenario, { stdout: niriPetWindow(9) }); - assert.equal(pending.length, 1); - assert.equal(JSON.stringify(pending[0].args.slice(-4)), JSON.stringify(["-x", "100", "-y", "100"])); - - controller.codexPetOverlayDesiredWindowBounds = { x: 600, y: 100, width: 356, height: 320 }; - controller.codexPetOverlayQueueNiriDrag(window); - controller.codexPetOverlayDesiredWindowBounds = { x: 120, y: 100, width: 356, height: 320 }; - controller.codexPetOverlayQueueNiriDrag(window); - - assert.equal(pending.length, 1, "a second compositor move must not overlap the first"); - completePendingNiriCall(scenario); - assert.equal(pending.length, 1); - assert.equal(JSON.stringify(pending[0].args.slice(-4)), JSON.stringify(["-x", "120", "-y", "100"])); - assert.equal(scenario.calls.some((args) => args.includes("600")), false); -}); - -test("Niri drag waits for an already-running bootstrap compositor action", () => { - const scenario = createAsyncNiriDragScenario(); - const { controller, pending, window } = scenario; - - controller.codexPetOverlayNiri(["action", "move-floating-window", "--id", "9", "-x", "40", "-y", "40"]); - assert.equal(pending.length, 1); - controller.codexPetOverlayBeginNiriDrag(window); - assert.equal(pending.length, 1, "drag discovery must wait for the bootstrap action"); - - completePendingNiriCall(scenario); - assert.equal(pending.length, 1); - assert.equal(pending[0].args.includes("windows"), true); -}); - -test("completed Niri processes do not schedule another hint batch without a pending request", () => { - const scenario = createAsyncNiriDragScenario(); - const { controller, pending, timers } = scenario; - - controller.codexPetOverlayNiri(["--json", "windows"]); - assert.equal(pending.length, 1); - completePendingNiriCall(scenario, { stdout: "[]" }); - - assert.equal(pending.length, 0); - assert.deepEqual(timers, []); -}); - -test("Niri drag floats a tiled pet before its first move", () => { - const scenario = createAsyncNiriDragScenario(); - const { controller, pending, window } = scenario; - - controller.codexPetOverlayBeginNiriDrag(window); - completePendingNiriCall(scenario, { stdout: niriPetWindow(9, false) }); - - assert.equal(pending.length, 1); - assert.equal( - JSON.stringify(pending[0].args), - JSON.stringify(["msg", "action", "move-window-to-floating", "--id", "9"]), - ); - assert.equal(scenario.calls.some((args) => args.includes("move-floating-window")), false); - - completePendingNiriCall(scenario); - assert.equal(pending.length, 1); - assert.equal(pending[0].args.includes("move-floating-window"), true); -}); - -test("Niri endDrag drains the final move before persisting and docking", () => { - const scenario = createAsyncNiriDragScenario(); - const { controller, pending, window } = scenario; - const completed = []; - controller.getLayout = () => ({ mascot: { left: 0, top: 0 } }); - controller.persistWindowBounds = (target, display) => completed.push(["persist", target, display]); - controller.dockTarget = { anchor: "dock-anchor", onDock: "dock-handler" }; - controller.dockPresentation = (anchor, onDock) => completed.push(["dock", anchor, onDock]); - - controller.startDrag(1, { - pointerScreenX: 100, - pointerScreenY: 100, - pointerWindowX: 20, - pointerWindowY: 20, - }); - controller.codexPetOverlayDesiredWindowBounds = { x: 120, y: 100, width: 356, height: 320 }; - controller.codexPetOverlayQueueNiriDrag(window); - controller.endDrag(1, {}); - - assert.deepEqual(completed, []); - completePendingNiriCall(scenario, { stdout: niriPetWindow(9) }); - assert.equal(pending.length, 1); - assert.equal(JSON.stringify(pending[0].args.slice(-4)), JSON.stringify(["-x", "120", "-y", "100"])); - assert.deepEqual(completed, []); - completePendingNiriCall(scenario); - - assert.equal(completed.length, 2); - assert.equal(completed[0][0], "persist"); - assert.equal(completed[0][1], window); - assert.equal(completed[0][2]?.id, 1); - assert.deepEqual(completed[1], ["dock", "dock-anchor", "dock-handler"]); - assert.equal(controller.codexPetOverlayNiriDragState, null); -}); - -test("stale Niri callbacks clear drag state and reschedule hints for a replacement window", () => { - const scenario = createAsyncNiriDragScenario(); - const { controller, pending, timers, window: oldWindow } = scenario; - controller.codexPetOverlayBeginNiriDrag(oldWindow); - assert.equal(pending.length, 1); - - const newWindow = { - getBounds: () => ({ x: 300, y: 200, width: 356, height: 320 }), - isDestroyed: () => false, - webContents: { id: 2 }, - }; - controller.window = newWindow; - completePendingNiriCall(scenario, { stdout: niriPetWindow(41) }); - - assert.equal(controller.codexPetOverlayNiriDragState, null); - assert.deepEqual(timers.map((timer) => timer.delay), [0, 80, 300, 1000]); -}); - -test("Niri hint scheduling clears an idle drag state left by a replaced window", () => { - const scenario = createAsyncNiriDragScenario(); - const { controller, pending, timers, window: oldWindow } = scenario; - controller.codexPetOverlayBeginNiriDrag(oldWindow); - completePendingNiriCall(scenario, { stdout: niriPetWindow(9) }); - completePendingNiriCall(scenario); - assert.equal(pending.length, 0); - assert.notEqual(controller.codexPetOverlayNiriDragState, null); - - const newWindow = { - getBounds: () => ({ x: 300, y: 200, width: 356, height: 320 }), - isDestroyed: () => false, - webContents: { id: 2 }, - }; - controller.window = newWindow; - controller.dragState = null; - controller.codexPetOverlayScheduleNiriHints(newWindow); - - assert.equal(controller.codexPetOverlayNiriDragState, null); - assert.deepEqual(timers.map((timer) => timer.delay), [0, 80, 300, 1000]); -}); - -test("stale Niri discovery callbacks cannot continue a replacement window drag", () => { - const scenario = createAsyncNiriDragScenario(); - const { controller, pending, window: oldWindow } = scenario; - controller.codexPetOverlayBeginNiriDrag(oldWindow); - - const newWindow = { - getBounds: () => ({ x: 300, y: 200, width: 356, height: 320 }), - isDestroyed: () => false, - webContents: { id: 2 }, - }; - controller.window = newWindow; - controller.codexPetOverlayDesiredWindowBounds = { x: 300, y: 200, width: 356, height: 320 }; - controller.codexPetOverlayBeginNiriDrag(newWindow); - assert.equal(pending.length, 1, "replacement discovery must wait for the previous call"); - - completePendingNiriCall(scenario, { stdout: niriPetWindow(41) }); - assert.equal(pending.length, 1, "the replacement discovery starts only after the stale call completes"); - completePendingNiriCall(scenario, { stdout: niriPetWindow(42) }); - assert.equal(pending.length, 1); - assert.equal(pending[0].args.includes("42"), true); - assert.equal(scenario.calls.some((args) => args.includes("41") && args.includes("action")), false); -}); - -test("Niri drag discovery recovery is bounded and ENOENT aborts immediately", () => { - const scenario = createAsyncNiriDragScenario(); - const { controller, pending, timers, window } = scenario; - controller.codexPetOverlayBeginNiriDrag(window); - - completePendingNiriCall(scenario, { error: new Error("discovery failed"), stdout: "" }); - assert.deepEqual(timers.map((timer) => timer.delay), [80]); - timers.shift().callback(); - completePendingNiriCall(scenario, { error: new Error("discovery failed"), stdout: "" }); - assert.deepEqual(timers.map((timer) => timer.delay), [300]); - timers.shift().callback(); - completePendingNiriCall(scenario, { stdout: "[]" }); - - assert.equal(scenario.calls.filter((args) => args.includes("windows")).length, 3); - assert.equal(controller.codexPetOverlayNiriDragState, null); - assert.equal(pending.length, 0); - - const missingScenario = createAsyncNiriDragScenario(); - missingScenario.controller.codexPetOverlayBeginNiriDrag(missingScenario.window); - const missingError = new Error("missing niri"); - missingError.code = "ENOENT"; - completePendingNiriCall(missingScenario, { error: missingError, stdout: "" }); - assert.equal(missingScenario.controller.codexPetOverlayNiriDragState, null); - assert.deepEqual(missingScenario.timers, []); -}); - -test("Niri drag action failure invalidates the cached id and rediscovers once serialized", () => { - const scenario = createAsyncNiriDragScenario(); - const { controller, pending, timers, window } = scenario; - controller.codexPetOverlayBeginNiriDrag(window); - completePendingNiriCall(scenario, { stdout: niriPetWindow(9) }); - assert.equal(pending.length, 1); - assert.equal(pending[0].args.includes("move-floating-window"), true); - - completePendingNiriCall(scenario, { error: new Error("move failed"), stdout: "" }); - assert.equal(pending.length, 0); - assert.deepEqual(timers.map((timer) => timer.delay), [80]); - timers.shift().callback(); - assert.equal(pending.length, 1); - assert.equal(pending[0].args.includes("windows"), true); - - completePendingNiriCall(scenario, { stdout: niriPetWindow(10) }); - assert.equal(pending.length, 1); - assert.equal(pending[0].args.includes("10"), true); - assert.equal(scenario.calls.filter((args) => args.includes("move-floating-window")).length, 2); -}); - -test("stale Niri action completion cannot continue a replacement drag", () => { - const scenario = createAsyncNiriDragScenario(); - const { controller, pending, window: oldWindow } = scenario; - controller.codexPetOverlayBeginNiriDrag(oldWindow); - completePendingNiriCall(scenario, { stdout: niriPetWindow(41) }); - assert.equal(pending.length, 1); - assert.equal(pending[0].args.includes("41"), true); - - const newWindow = { - getBounds: () => ({ x: 300, y: 200, width: 356, height: 320 }), - isDestroyed: () => false, - webContents: { id: 2 }, - }; - controller.window = newWindow; - controller.codexPetOverlayDesiredWindowBounds = { x: 300, y: 200, width: 356, height: 320 }; - controller.codexPetOverlayBeginNiriDrag(newWindow); - assert.equal(pending.length, 1, "replacement drag must not overlap the previous compositor action"); - - completePendingNiriCall(scenario); - assert.equal(pending.length, 1, "replacement discovery starts after the old action completes"); - completePendingNiriCall(scenario, { stdout: niriPetWindow(42) }); - assert.equal(pending.length, 1); - assert.equal(pending[0].args.includes("42"), true); -}); - test("settings validation falls back to safe defaults", () => { assert.deepEqual( mergedPetOverlaySettings({ @@ -2031,7 +1788,6 @@ test("settings validation falls back to safe defaults", () => { alwaysOnTop: true, gravity: "bottom-right", hyprland: true, - kwin: true, lockPosition: true, margin: 512, mode: "interactive", From 04564a4ed0398fb5d35cdb451b1de52853290a2d Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Fri, 17 Jul 2026 13:16:46 -0600 Subject: [PATCH 3/6] fix(pet-overlay): restore KWin Plasma bridge --- linux-features/pet-overlay/patch.js | 68 +++++++++++++++++++++- linux-features/pet-overlay/test.js | 88 +++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/linux-features/pet-overlay/patch.js b/linux-features/pet-overlay/patch.js index f7fb37bcf..e0e810220 100644 --- a/linux-features/pet-overlay/patch.js +++ b/linux-features/pet-overlay/patch.js @@ -64,6 +64,7 @@ function mergedPetOverlaySettings(context = {}) { alwaysOnTop: booleanSetting(overrides.alwaysOnTop, booleanSetting(defaults.alwaysOnTop, true)), gravity, hyprland: booleanSetting(overrides.hyprland, booleanSetting(defaults.hyprland, true)), + kwin: booleanSetting(overrides.kwin, booleanSetting(defaults.kwin, true)), lockPosition: booleanSetting(overrides.lockPosition, booleanSetting(defaults.lockPosition, false)), margin: integerSetting(overrides.margin ?? defaults.margin, 24, 0, 512), mode, @@ -148,7 +149,7 @@ function firstMethodArgument(methodText, methodName, index) { function buildPetOverlayMethods(settings) { return [ - `codexPetOverlaySettings(){let e={margin:${settings.margin},gravity:\`${settings.gravity}\`,allWorkspaces:${boolLiteral(settings.allWorkspaces)},alwaysOnTop:${boolLiteral(settings.alwaysOnTop)},skipTaskbar:${boolLiteral(settings.skipTaskbar)},lockPosition:${boolLiteral(settings.lockPosition)},mode:\`${settings.mode}\`,hyprland:${boolLiteral(settings.hyprland)},niri:${boolLiteral(settings.niri)}};try{let t=process.env.CODEX_PET_OVERLAY_MARGIN??process.env.CODEX_PET_LINUX_MARGIN,n=Number(t);Number.isFinite(n)&&(e.margin=Math.max(0,Math.min(512,Math.round(n))));let r=process.env.CODEX_PET_OVERLAY_GRAVITY??process.env.CODEX_PET_LINUX_GRAVITY;[\`bottom-right\`,\`bottom-left\`,\`top-right\`,\`top-left\`].includes(r)&&(e.gravity=r);let i=process.env.CODEX_PET_OVERLAY_MODE??process.env.CODEX_PET_LINUX_MODE;(i===\`interactive\`||i===\`passive\`)&&(e.mode=i);let a=process.env.CODEX_PET_OVERLAY_LOCK_POSITION??process.env.CODEX_PET_LINUX_LOCK_POSITION;a===\`1\`&&(e.lockPosition=!0),a===\`0\`&&(e.lockPosition=!1);let o=process.env.CODEX_PET_OVERLAY_HYPRLAND??process.env.CODEX_PET_LINUX_HYPRLAND;o===\`1\`&&(e.hyprland=!0),o===\`0\`&&(e.hyprland=!1);let s=process.env.CODEX_PET_OVERLAY_NIRI;s===\`1\`&&(e.niri=!0),s===\`0\`&&(e.niri=!1)}catch{}return e}`, + `codexPetOverlaySettings(){let e={margin:${settings.margin},gravity:\`${settings.gravity}\`,allWorkspaces:${boolLiteral(settings.allWorkspaces)},alwaysOnTop:${boolLiteral(settings.alwaysOnTop)},skipTaskbar:${boolLiteral(settings.skipTaskbar)},lockPosition:${boolLiteral(settings.lockPosition)},mode:\`${settings.mode}\`,hyprland:${boolLiteral(settings.hyprland)},kwin:${boolLiteral(settings.kwin)},niri:${boolLiteral(settings.niri)}};try{let t=process.env.CODEX_PET_OVERLAY_MARGIN??process.env.CODEX_PET_LINUX_MARGIN,n=Number(t);Number.isFinite(n)&&(e.margin=Math.max(0,Math.min(512,Math.round(n))));let r=process.env.CODEX_PET_OVERLAY_GRAVITY??process.env.CODEX_PET_LINUX_GRAVITY;[\`bottom-right\`,\`bottom-left\`,\`top-right\`,\`top-left\`].includes(r)&&(e.gravity=r);let i=process.env.CODEX_PET_OVERLAY_MODE??process.env.CODEX_PET_LINUX_MODE;(i===\`interactive\`||i===\`passive\`)&&(e.mode=i);let a=process.env.CODEX_PET_OVERLAY_LOCK_POSITION??process.env.CODEX_PET_LINUX_LOCK_POSITION;a===\`1\`&&(e.lockPosition=!0),a===\`0\`&&(e.lockPosition=!1);let o=process.env.CODEX_PET_OVERLAY_HYPRLAND??process.env.CODEX_PET_LINUX_HYPRLAND;o===\`1\`&&(e.hyprland=!0),o===\`0\`&&(e.hyprland=!1);let s=process.env.CODEX_PET_OVERLAY_KWIN;s===\`1\`&&(e.kwin=!0),s===\`0\`&&(e.kwin=!1);let c=process.env.CODEX_PET_OVERLAY_NIRI;c===\`1\`&&(e.niri=!0),c===\`0\`&&(e.niri=!1)}catch{}return e}`, "codexPetOverlayRect(e){if(e==null)return null;let t=Number(e.x),n=Number(e.y),r=Number(e.width),i=Number(e.height);return[t,n,r,i].every(Number.isFinite)&&r>0&&i>0?{x:t,y:n,width:r,height:i}:null}", "codexPetOverlayDisplayRect(e){return this.codexPetOverlayRect(e?.workArea??e?.bounds??e)}", "codexPetOverlayWindowBounds(e){try{return this.codexPetOverlayRect(e?.getBounds?.()??e?.getContentBounds?.())}catch{return null}}", @@ -179,6 +180,21 @@ function buildPetOverlayMethods(settings) { "codexPetOverlayFindHyprlandClient(e,t){if(!this.codexPetOverlayShouldUseHyprland())return;let n=this.codexPetOverlayWindowBounds(e);this.codexPetOverlayHyprctl([`clients`,`-j`],(e,r)=>{if(e)return;let i;try{i=JSON.parse(String(r??``))}catch{return}let a=this.codexPetOverlaySelectHyprlandClient(i,n);a!=null&&typeof t==`function`&&t(a)})}", "codexPetOverlayApplyHyprlandHints(e){let t=this.codexPetOverlaySettings();if(process.platform!==`linux`||e==null||e.isDestroyed?.()||!this.codexPetOverlayShouldUseHyprland())return;this.codexPetOverlayFindHyprlandClient(e,n=>{if(e.isDestroyed?.()||this.window!==e)return;let r=`address:${n.address}`,i=this.codexPetOverlayDesiredWindowBounds,a=Number(i?.x),o=Number(i?.y),s=Math.round(a),c=Math.round(o);t.lockPosition&&[a,o].every(Number.isFinite)&&this.codexPetOverlayHyprlandDispatch(`hl.dsp.window.move({ window = \"${r}\", x = ${s}, y = ${c} })`,[`movewindowpixel`,`exact ${s} ${c},${r}`]);t.allWorkspaces&&n.pinned!==!0&&this.codexPetOverlayHyprlandDispatch(`hl.dsp.window.pin({ action = \"on\", window = \"${r}\" })`,[`pin`,r]);this.codexPetOverlayHyprlandSetProp(r,`decorate`,`0`);this.codexPetOverlayHyprlandSetProp(r,`no_shadow`,`1`);this.codexPetOverlayHyprlandSetProp(r,`no_blur`,`1`);this.codexPetOverlayHyprlandSetProp(r,`no_anim`,`1`);this.codexPetOverlayHyprlandSetProp(r,`border_size`,`0`);this.codexPetOverlayHyprlandSetProp(r,`rounding`,`0`);this.codexPetOverlayHyprlandSetProp(r,`opacity`,`1.0 override 1.0 override 1.0 override`);this.codexPetOverlayHyprlandSetProp(r,`opaque`,`0`);this.codexPetOverlayHyprlandSetProp(r,`force_rgbx`,`0`);t.alwaysOnTop&&this.codexPetOverlayHyprlandDispatch(`hl.dsp.window.alter_zorder({ mode = \"top\", window = \"${r}\" })`,[`alterzorder`,`top,${r}`])})}", "codexPetOverlayScheduleHyprlandHints(e){if(!this.codexPetOverlayShouldUseHyprland())return;try{this.codexPetOverlayHyprlandTimers?.forEach(clearTimeout)}catch{}this.codexPetOverlayHyprlandTimers=[0,80,300,1000,2500,5000,10000].map(t=>{let n=setTimeout(()=>{try{e==null||e.isDestroyed?.()||this.codexPetOverlayApplyHyprlandHints(e)}catch{}},t);try{n.unref?.()}catch{}return n})}", + "codexPetOverlayKWinSession(){if(process.platform!==`linux`)return!1;let e=String(process.env.KDE_FULL_SESSION??``).toLowerCase();if(process.env.KDE_SESSION_VERSION||e===`1`||e===`true`)return!0;let t=[process.env.XDG_CURRENT_DESKTOP,process.env.DESKTOP_SESSION].filter(Boolean).join(`:`).toLowerCase();return t.includes(`kde`)||t.includes(`plasma`)}", + "codexPetOverlayShouldUseKWin(){return process.platform===`linux`&&this.codexPetOverlaySettings().kwin===!0&&this.codexPetOverlayKWinSession()}", + "codexPetOverlayKWinQdbus(e,t,n=!1){if(this.codexPetOverlayKWinUnavailable){try{typeof t===`function`&&t({code:`ENOENT`})}catch{}return}let r=e;try{let i=typeof require===`function`?require(`node:child_process`):null;if(typeof i?.execFile!==`function`){this.codexPetOverlayKWinUnavailable=!0;try{typeof t===`function`&&t({code:`ENOENT`})}catch{}return}i.execFile(n?`qdbus`:`qdbus6`,r,{timeout:1500},(e,...i)=>{if(e?.code===`ENOENT`&&!n){this.codexPetOverlayKWinQdbus(r,t,!0);return}e?.code===`ENOENT`&&(this.codexPetOverlayKWinUnavailable=!0);try{typeof t===`function`&&t(e,...i)}catch{}})}catch(e){if(e?.code===`ENOENT`&&!n){this.codexPetOverlayKWinQdbus(r,t,!0);return}e?.code===`ENOENT`&&(this.codexPetOverlayKWinUnavailable=!0);try{typeof t===`function`&&t(e)}catch{}}}", + "codexPetOverlayKWinScript(e,t){let n=this.codexPetOverlayRect(e),r=this.codexPetOverlaySettings(),i={pid:Number(process.pid),title:`Codex Pet Overlay`,alwaysOnTop:!!r.alwaysOnTop,allWorkspaces:!!r.allWorkspaces,skipTaskbar:!!r.skipTaskbar,move:!!t,x:Math.round(Number(n?.x)),y:Math.round(Number(n?.y)),width:Math.round(Number(n?.width)),height:Math.round(Number(n?.height))};return `(function(){var d=${JSON.stringify(i)};function windows(){try{if(typeof workspace.windowList==='function')return workspace.windowList()}catch(e){}try{if(typeof workspace.clientList==='function')return workspace.clientList()}catch(e){}try{if(workspace.stackingOrder&&typeof workspace.stackingOrder.length==='number')return workspace.stackingOrder}catch(e){}return[]}var a=windows().filter(function(w){try{return String(w.caption||'')===d.title&&Number(w.pid)===d.pid}catch(e){return false}});if(a.length!==1)return;var w=a[0];try{w.keepAbove=d.alwaysOnTop}catch(e){}try{w.skipTaskbar=d.skipTaskbar}catch(e){}try{w.skipPager=d.skipTaskbar}catch(e){}try{w.onAllDesktops=d.allWorkspaces}catch(e){}try{w.noBorder=true}catch(e){}if(d.move&&isFinite(d.x)&&isFinite(d.y)){try{var g=w.frameGeometry;w.frameGeometry={x:d.x,y:d.y,width:isFinite(d.width)&&d.width>0?d.width:g.width,height:isFinite(d.height)&&d.height>0?d.height:g.height}}catch(e){}}if(d.alwaysOnTop){try{if(typeof workspace.raiseWindow==='function')workspace.raiseWindow(w)}catch(e){}}})()`}", + "codexPetOverlayKWinRun(e,t,n){if(!this.codexPetOverlayShouldUseKWin()){try{typeof n===`function`&&n({code:`DISABLED`})}catch{}return}let r;try{let i=require(`node:fs`),a=require(`node:os`),o=require(`node:path`),s=(this.codexPetOverlayKWinScriptGeneration??0)+1;this.codexPetOverlayKWinScriptGeneration=s;let c=`codex_pet_overlay_${process.pid}_${Date.now()}_${s}`,l=o.join(a.tmpdir(),`${c}.js`);i.writeFileSync(l,this.codexPetOverlayKWinScript(e,t),{encoding:`utf8`,flag:`wx`,mode:384}),r=()=>{try{i.unlinkSync(l)}catch{}};let u=[`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.loadScript`,l,c];this.codexPetOverlayKWinQdbus(u,e=>{if(e){r();try{typeof n===`function`&&n(e)}catch{}return}this.codexPetOverlayKWinQdbus([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.start`],e=>{this.codexPetOverlayKWinQdbus([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.unloadScript`,c],()=>{r();try{typeof n===`function`&&n(e)}catch{}})})})}catch(e){try{r?.()}catch{}try{typeof n===`function`&&n(e)}catch{}}}", + "codexPetOverlayApplyKWinHints(e){if(e==null||e.isDestroyed?.()||this.window!==e||!this.codexPetOverlayShouldUseKWin()||this.dragState!=null||this.codexPetOverlayKWinDragState!=null||this.codexPetOverlayKWinHintInFlight)return;this.codexPetOverlayKWinHintInFlight=!0;let t=this.codexPetOverlaySettings();this.codexPetOverlayKWinRun(this.codexPetOverlayDesiredWindowBounds,t.lockPosition===!0,()=>{this.codexPetOverlayKWinHintInFlight=!1,this.codexPetOverlayKWinDragState==null&&this.window===e&&!e.isDestroyed?.()&&this.codexPetOverlayKWinPendingHints&&(this.codexPetOverlayKWinPendingHints=!1,this.codexPetOverlayScheduleKWinHints(e))})}", + "codexPetOverlayScheduleKWinHints(e){if(!this.codexPetOverlayShouldUseKWin()||this.dragState!=null||this.codexPetOverlayKWinDragState!=null)return;if(this.codexPetOverlayKWinHintInFlight){this.codexPetOverlayKWinPendingHints=!0;return}this.codexPetOverlayKWinPendingHints=!1;try{this.codexPetOverlayKWinTimers?.forEach(clearTimeout)}catch{}this.codexPetOverlayKWinTimers=[0,80,300,1000,2500].map(t=>{let n=setTimeout(()=>{try{this.codexPetOverlayApplyKWinHints(e)}catch{}},t);try{n.unref?.()}catch{}return n})}", + "codexPetOverlayKWinExecSync(e){if(this.codexPetOverlayKWinUnavailable||!this.codexPetOverlayShouldUseKWin())return!1;try{let t=typeof require===`function`?require(`node:child_process`):null;if(typeof t?.execFileSync!==`function`)return!1;for(let n of [`qdbus6`,`qdbus`])try{t.execFileSync(n,e,{timeout:750,stdio:`ignore`});return!0}catch(e){if(e?.code!==`ENOENT`)return!1}this.codexPetOverlayKWinUnavailable=!0}catch{}return!1}", + "codexPetOverlayKWinDragScript(){let e={pid:Number(process.pid),title:`Codex Pet Overlay`};return `(function(){var d=${JSON.stringify(e)};function windows(){try{if(typeof workspace.windowList==='function')return workspace.windowList()}catch(e){}try{if(typeof workspace.clientList==='function')return workspace.clientList()}catch(e){}return[]}var a=windows().filter(function(w){try{return String(w.caption||'')===d.title&&Number(w.pid)===d.pid}catch(e){return false}});if(a.length!==1)return;var w=a[0],p=workspace.cursorPos,g=w.frameGeometry,dx=Number(p.x)-Number(g.x),dy=Number(p.y)-Number(g.y),active=true;function move(){if(!active)return;try{var p=workspace.cursorPos,g=w.frameGeometry;w.frameGeometry={x:Math.round(Number(p.x)-dx),y:Math.round(Number(p.y)-dy),width:g.width,height:g.height}}catch(e){stop()}}function stop(){if(!active)return;active=false;try{workspace.cursorPosChanged.disconnect(move)}catch(e){}}try{workspace.cursorPosChanged.connect(move)}catch(e){return}try{workspace.windowRemoved.connect(function(v){if(v===w)stop()})}catch(e){}try{if(typeof workspace.raiseWindow==='function')workspace.raiseWindow(w)}catch(e){}})()`}", + "codexPetOverlayReleaseKWinDrag(e){if(e==null)return;try{this.codexPetOverlayKWinExecSync([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.unloadScript`,e.pluginName])}catch{}try{require(`node:fs`).unlinkSync(e.scriptPath)}catch{}}", + "codexPetOverlayStartKWinDrag(e){let t;try{let n=require(`node:fs`),r=require(`node:os`),i=require(`node:path`),a=(this.codexPetOverlayKWinDragGeneration??0)+1,o=`codex_pet_overlay_drag_${process.pid}_${Date.now()}_${a}`,s=i.join(r.tmpdir(),`${o}.js`);n.writeFileSync(s,this.codexPetOverlayKWinDragScript(),{encoding:`utf8`,flag:`wx`,mode:384}),t={generation:a,window:e,pluginName:o,scriptPath:s};let c=[`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.loadScript`,s,o];if(!this.codexPetOverlayKWinExecSync(c)||!this.codexPetOverlayKWinExecSync([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.start`])){this.codexPetOverlayReleaseKWinDrag(t);return null}return t}catch(e){this.codexPetOverlayReleaseKWinDrag(t);return null}}", + "codexPetOverlayBeginKWinDrag(e){if(e==null||e.isDestroyed?.()||this.window!==e||!this.codexPetOverlayShouldUseKWin())return;try{this.codexPetOverlayKWinTimers?.forEach(clearTimeout)}catch{}let t=this.codexPetOverlayKWinDragState;t!=null&&(this.codexPetOverlayKWinDragState=null,this.codexPetOverlayReleaseKWinDrag(t));let n=this.codexPetOverlayStartKWinDrag(e);if(n==null)return;let r=null;try{r=Number(e.getContentBounds?.().x)}catch{}this.codexPetOverlayKWinDragGeneration=n.generation,this.codexPetOverlayKWinDragState=n,this.windowServerDragActive=!0,Number.isFinite(r)&&(this.windowServerDragWindowX=r)}", + "codexPetOverlayKWinDragCurrent(e){if(e==null||this.codexPetOverlayKWinDragState!==e||this.codexPetOverlayKWinDragGeneration!==e.generation)return!1;if(this.window===e.window&&!e.window?.isDestroyed?.())return!0;this.codexPetOverlayKWinDragState=null,this.codexPetOverlayReleaseKWinDrag(e);return!1}", + "codexPetOverlayQueueKWinDrag(e){let t=this.codexPetOverlayKWinDragState;this.codexPetOverlayKWinDragCurrent(t)&&t.window===e&&(this.windowServerDragActive=!0)}", + "codexPetOverlayEndKWinDrag(e,t){let n=this.codexPetOverlayKWinDragState;if(!this.codexPetOverlayKWinDragCurrent(n)||n.window!==e)return!1;this.codexPetOverlayKWinDragState=null,this.codexPetOverlayReleaseKWinDrag(n);try{typeof t===`function`&&t()}catch{}try{this.window!=null&&!this.window.isDestroyed?.()&&this.codexPetOverlayScheduleKWinHints(this.window)}catch{}return!0}", "codexPetOverlayNiriSession(){if(process.platform!==`linux`)return!1;let e=[process.env.NIRI_SOCKET,process.env.XDG_CURRENT_DESKTOP,process.env.DESKTOP_SESSION].filter(Boolean).join(`:`).toLowerCase();return e.includes(`niri`)}", "codexPetOverlayShouldUseNiri(){return process.platform===`linux`&&this.codexPetOverlaySettings().niri===!0&&this.codexPetOverlayNiriSession()}", "codexPetOverlayNiri(e,t){if(this.codexPetOverlayNiriUnavailable)return;try{let n=typeof require==`function`?require(`node:child_process`):null;if(typeof n?.execFile!=`function`){this.codexPetOverlayNiriUnavailable=!0;return}n.execFile(`niri`,[`msg`,...e],{timeout:1200},(e,...n)=>{e?.code===`ENOENT`&&(this.codexPetOverlayNiriUnavailable=!0),typeof t==`function`&&t(e,...n)})}catch(e){e?.code===`ENOENT`&&(this.codexPetOverlayNiriUnavailable=!0);try{typeof t==`function`&&t(e)}catch{}}}", @@ -250,6 +266,52 @@ function ensurePetOverlayMethods(source, settings) { source.slice(insertionPoint.start); } +function patchCompositorDragLifecycle(source) { + let patched = source; + const startMethod = findAvatarOverlayMethod(patched, /startDrag\([^)]*\)\{/); + if (startMethod == null) { + console.warn("WARN: Could not find avatar overlay startDrag for compositor transport - skipping pet overlay patch"); + return patched; + } + const needsKWinStart = !startMethod.text.includes("codexPetOverlayBeginKWinDrag("); + if (needsKWinStart) { + const windowMatch = startMethod.text.match(/let ([A-Za-z_$][\w$]*)=this\.window;/); + if (windowMatch == null || !startMethod.text.includes("this.dragState=")) { + console.warn("WARN: Could not identify current avatar overlay drag start shape - skipping compositor transport hook"); + return patched; + } + patched = replaceMethodText( + patched, + startMethod, + `${startMethod.text.slice(0, -1)},this.codexPetOverlayBeginKWinDrag(${windowMatch[1]})}`, + ); + } + + const endMethod = findAvatarOverlayMethod(patched, /endDrag\([^)]*\)\{/); + if (endMethod == null) { + console.warn("WARN: Could not find avatar overlay endDrag for compositor transport - skipping pet overlay patch"); + return patched; + } + if (endMethod.text.includes("codexPetOverlayEndKWinDrag(")) { + return patched; + } + const completionPattern = /[A-Za-z_$][\w$]*\?this\.persistWindowBounds\(([A-Za-z_$][\w$]*),[A-Za-z_$][\w$]*\?\?this\.getCurrentDisplay\(\)\):this\.reclampWindowToVisibleDisplay\(\{shouldPersist:!0\}\)/; + const completionMatch = endMethod.text.match(completionPattern); + if (completionMatch == null) { + console.warn("WARN: Could not identify current avatar overlay drag completion shape - skipping compositor transport hook"); + return patched; + } + const windowVar = completionMatch[1]; + const completionNeedle = endMethod.text.slice(completionMatch.index, -1); + return replaceMethodText( + patched, + endMethod, + endMethod.text.slice(0, completionMatch.index) + + `this.codexPetOverlayEndKWinDrag(${windowVar},()=>{${completionNeedle}})||(()=>{${completionNeedle}})()` + + "}", + ); +} + function patchApplyLayout(source) { if ( source.includes("=this.codexPetOverlayLayoutForDisplay(") || @@ -404,6 +466,9 @@ function hasCompletePetOverlayPatch(source, settings, avatarSelectionRefreshExpe /let [A-Za-z_$][\w$]*=this\.codexPetOverlayLayoutForDisplay\([A-Za-z_$][\w$]*,this\.getLayoutForDisplay\([A-Za-z_$][\w$]*\),[A-Za-z_$][\w$]*\);/.test(source), /process\.platform===`linux`\?this\.codexPetOverlaySyncWindow\([A-Za-z_$][\w$]*,!0\):[A-Za-z_$][\w$]*\.moveTop\(\),[A-Za-z_$][\w$]*\.showInactive\(\),/.test(source), source.includes("if(this.codexPetOverlayShouldLockPosition())return;"), + source.includes("codexPetOverlayKWinQdbus("), + source.includes("this.codexPetOverlayBeginKWinDrag("), + source.includes("this.codexPetOverlayEndKWinDrag("), source.includes("===`avatarOverlay`?{backgroundColor:`#00000000`,backgroundMaterial:null}:"), source.includes("title:`Codex Pet Overlay`,width:"), ]; @@ -462,6 +527,7 @@ function applyPetOverlayPatch(source, context) { patched = patchApplyLayout(patched); patched = patchShowWindow(patched); patched = patchLockedDrag(patched); + patched = patchCompositorDragLifecycle(patched); patched = ensurePetOverlayMethods(patched, settings); patched = patchLocalMascotDrag(patched); if (localMascotDragExpected) { diff --git a/linux-features/pet-overlay/test.js b/linux-features/pet-overlay/test.js index 4613c0d08..14cb09ddb 100644 --- a/linux-features/pet-overlay/test.js +++ b/linux-features/pet-overlay/test.js @@ -92,6 +92,9 @@ function controllerFromPatchedSource(patched, overrides = {}) { if (moduleName === "node:child_process") { return overrides.childProcess ?? { execFile() {} }; } + if (moduleName === "node:fs") return fs; + if (moduleName === "node:os") return os; + if (moduleName === "node:path") return path; if (moduleName === "electron") { return { app: { getName: () => "Codex" }, @@ -1788,6 +1791,7 @@ test("settings validation falls back to safe defaults", () => { alwaysOnTop: true, gravity: "bottom-right", hyprland: true, + kwin: true, lockPosition: true, margin: 512, mode: "interactive", @@ -1796,3 +1800,87 @@ test("settings validation falls back to safe defaults", () => { }, ); }); + +test("KWin Plasma bridge remains available with its runtime override and drag lifecycle", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + + assert.equal(mergedPetOverlaySettings({}).kwin, true); + assert.match(patched, /CODEX_PET_OVERLAY_KWIN/); + assert.match(patched, /codexPetOverlayKWinQdbus\(/); + assert.match(patched, /codexPetOverlayKWinDragScript\(/); + assert.match(patched, /codexPetOverlayBeginKWinDrag\(/); + assert.match(patched, /codexPetOverlayEndKWinDrag\(/); +}); + +test("KWin hints target only the matching Plasma pet and apply its Wayland bounds", () => { + const calls = []; + let script = null; + const { controller } = controllerFromPatchedSource( + applyPetOverlayPatch(currentAvatarOverlayBundleFixture(), { + feature: { manifest: { petOverlay: { kwin: true, lockPosition: true } }, settings: {} }, + }), + { + process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, + childProcess: { + execFile(command, args, options, callback) { + calls.push([command, args]); + assert.equal(command, "qdbus6"); + assert.equal(options.timeout, 1500); + if (args.includes("org.kde.kwin.Scripting.loadScript")) script = fs.readFileSync(args[3], "utf8"); + callback(null, "ok"); + }, + }, + }, + ); + const window = { isDestroyed: () => false }; + controller.window = window; + controller.codexPetOverlayDesiredWindowBounds = { x: 610, y: 330, width: 356, height: 320 }; + + controller.codexPetOverlayApplyKWinHints(window); + + assert.deepEqual(calls.map(([, args]) => args[2]), [ + "org.kde.kwin.Scripting.loadScript", + "org.kde.kwin.Scripting.start", + "org.kde.kwin.Scripting.unloadScript", + ]); + const pet = { caption: "Codex Pet Overlay", frameGeometry: { x: 0, y: 0, width: 356, height: 320 }, pid: 4242 }; + const foreign = { caption: "ChatGPT", frameGeometry: {}, pid: 4242 }; + vm.runInNewContext(script, { workspace: { raiseWindow() {}, windowList: () => [foreign, pet] } }); + assert.equal(pet.keepAbove, true); + assert.equal(pet.noBorder, true); + assert.deepEqual(JSON.parse(JSON.stringify(pet.frameGeometry)), { x: 610, y: 330, width: 356, height: 320 }); + assert.equal(foreign.keepAbove, undefined); +}); + +test("KWin drag uses qdbus, cleans up its temporary script, and honors its runtime override", () => { + const calls = []; + const { controller } = controllerFromPatchedSource(applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), { + process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, + childProcess: { + execFileSync(command, args, options) { + calls.push([command, args, options]); + }, + }, + }); + const window = { + getContentBounds: () => ({ x: 145, y: 210, width: 356, height: 320 }), + isDestroyed: () => false, + }; + controller.window = window; + + controller.codexPetOverlayBeginKWinDrag(window); + const scriptPath = controller.codexPetOverlayKWinDragState.scriptPath; + assert.deepEqual(calls.slice(0, 2).map(([, args]) => args[2]), [ + "org.kde.kwin.Scripting.loadScript", + "org.kde.kwin.Scripting.start", + ]); + assert.equal(fs.existsSync(scriptPath), true); + assert.equal(controller.codexPetOverlayEndKWinDrag(window, () => {}), true); + assert.equal(calls[2][1][2], "org.kde.kwin.Scripting.unloadScript"); + assert.equal(fs.existsSync(scriptPath), false); + + const overridden = controllerFromPatchedSource(applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), { + process: { env: { XDG_CURRENT_DESKTOP: "KDE", CODEX_PET_OVERLAY_KWIN: "0" } }, + }).controller; + assert.equal(overridden.codexPetOverlayShouldUseKWin(), false); +}); From 5e200577cc3ecec8a9a78f4e944b520d4a98bf39 Mon Sep 17 00:00:00 2001 From: Gary Lysenko Date: Sat, 18 Jul 2026 08:10:25 +0300 Subject: [PATCH 4/6] fix(pet-overlay): restore Niri drag transport --- linux-features/pet-overlay/patch.js | 37 +++- linux-features/pet-overlay/test.js | 295 +++++++++++++++++++++++++++- 2 files changed, 321 insertions(+), 11 deletions(-) diff --git a/linux-features/pet-overlay/patch.js b/linux-features/pet-overlay/patch.js index e0e810220..301e7fb15 100644 --- a/linux-features/pet-overlay/patch.js +++ b/linux-features/pet-overlay/patch.js @@ -197,14 +197,25 @@ function buildPetOverlayMethods(settings) { "codexPetOverlayEndKWinDrag(e,t){let n=this.codexPetOverlayKWinDragState;if(!this.codexPetOverlayKWinDragCurrent(n)||n.window!==e)return!1;this.codexPetOverlayKWinDragState=null,this.codexPetOverlayReleaseKWinDrag(n);try{typeof t===`function`&&t()}catch{}try{this.window!=null&&!this.window.isDestroyed?.()&&this.codexPetOverlayScheduleKWinHints(this.window)}catch{}return!0}", "codexPetOverlayNiriSession(){if(process.platform!==`linux`)return!1;let e=[process.env.NIRI_SOCKET,process.env.XDG_CURRENT_DESKTOP,process.env.DESKTOP_SESSION].filter(Boolean).join(`:`).toLowerCase();return e.includes(`niri`)}", "codexPetOverlayShouldUseNiri(){return process.platform===`linux`&&this.codexPetOverlaySettings().niri===!0&&this.codexPetOverlayNiriSession()}", - "codexPetOverlayNiri(e,t){if(this.codexPetOverlayNiriUnavailable)return;try{let n=typeof require==`function`?require(`node:child_process`):null;if(typeof n?.execFile!=`function`){this.codexPetOverlayNiriUnavailable=!0;return}n.execFile(`niri`,[`msg`,...e],{timeout:1200},(e,...n)=>{e?.code===`ENOENT`&&(this.codexPetOverlayNiriUnavailable=!0),typeof t==`function`&&t(e,...n)})}catch(e){e?.code===`ENOENT`&&(this.codexPetOverlayNiriUnavailable=!0);try{typeof t==`function`&&t(e)}catch{}}}", + "codexPetOverlayFinishNiriProcess(){this.codexPetOverlayNiriProcessCount=Math.max(0,(this.codexPetOverlayNiriProcessCount??1)-1);if(this.codexPetOverlayNiriProcessCount===0){let e=this.codexPetOverlayNiriDragState;if(e!=null)this.codexPetOverlayPumpNiriDrag(e);else{let e=this.codexPetOverlayNiriPendingHintsWindow;this.codexPetOverlayNiriPendingHintsWindow=null;try{e!=null&&!e.isDestroyed?.()&&this.window===e&&this.codexPetOverlayScheduleNiriHints(e)}catch{}}}}", + "codexPetOverlayNiri(e,t){if(this.codexPetOverlayNiriUnavailable){try{typeof t==`function`&&t({code:`ENOENT`})}catch{}return}let n=!1;try{let r=typeof require==`function`?require(`node:child_process`):null;if(typeof r?.execFile!=`function`){this.codexPetOverlayNiriUnavailable=!0;try{typeof t==`function`&&t({code:`ENOENT`})}catch{}return}this.codexPetOverlayNiriProcessCount=(this.codexPetOverlayNiriProcessCount??0)+1,n=!0,r.execFile(`niri`,[`msg`,...e],{timeout:1200},(e,...n)=>{e?.code===`ENOENT`&&(this.codexPetOverlayNiriUnavailable=!0);try{typeof t==`function`&&t(e,...n)}finally{this.codexPetOverlayFinishNiriProcess()}})}catch(e){e?.code===`ENOENT`&&(this.codexPetOverlayNiriUnavailable=!0),n&&this.codexPetOverlayFinishNiriProcess();try{typeof t==`function`&&t(e)}catch{}}}", "codexPetOverlayNiriWindowSize(e){let t=e?.layout?.window_size??e?.layout?.windowSize??e?.window_size??e?.size;if(!Array.isArray(t))return null;let n=Number(t[0]),r=Number(t[1]);return[n,r].every(Number.isFinite)&&n>0&&r>0?{width:n,height:r}:null}", "codexPetOverlayNiriPositiveInteger(e){return typeof e==`number`&&Number.isSafeInteger(e)&&e>0?e:null}", "codexPetOverlayNiriLocalMove(){let e=this.codexPetOverlayRect(this.codexPetOverlayDesiredWindowBounds),t=this.codexPetOverlayRect(this.codexPetOverlayDesiredDisplayBounds);if(e==null||t==null)return null;let n=e.x-t.x,r=e.y-t.y;return[n,r].every(Number.isFinite)?{x:Math.round(n),y:Math.round(r)}:null}", "codexPetOverlaySelectNiriWindow(e,t){if(!Array.isArray(e))return null;let n=this.codexPetOverlayRect(t),r=[];for(let i of e){let e=this.codexPetOverlayNiriPositiveInteger(i?.id),t=this.codexPetOverlayNiriPositiveInteger(i?.pid);if(e==null||t!==process.pid||String(i?.title??``)!==`Codex Pet Overlay`)continue;if(i.is_floating!=null&&typeof i.is_floating!=`boolean`)continue;let a=this.codexPetOverlayNiriWindowSize(i),o=a==null?0:Math.abs(a.width-Number(n?.width??a.width))+Math.abs(a.height-Number(n?.height??a.height)),s=a==null?0:a.width*a.height;r.push({window:i,id:e,sizeScore:o,area:s})}if(n!=null){let e=r.filter(e=>e.sizeScore<=16);return e.length===1?e[0].window:null}let i=r.filter(e=>e.area>0&&e.area<=300000);return i.length===1?i[0].window:r.length===1?r[0].window:null}", - "codexPetOverlayFindNiriWindow(e,t){if(!this.codexPetOverlayShouldUseNiri())return;let n=this.codexPetOverlayWindowBounds(e);this.codexPetOverlayNiri([`--json`,`windows`],(e,r)=>{if(e)return;let i;try{i=JSON.parse(String(r??``))}catch{return}let a=this.codexPetOverlaySelectNiriWindow(i,n);a!=null&&typeof t==`function`&&t(a)})}", - "codexPetOverlayApplyNiriHints(e){if(process.platform!==`linux`||e==null||e.isDestroyed?.()||!this.codexPetOverlayShouldUseNiri())return;this.codexPetOverlayFindNiriWindow(e,t=>{if(e.isDestroyed?.()||this.window!==e)return;let n=this.codexPetOverlayNiriPositiveInteger(t?.id);if(n==null)return;t.is_floating!==!0&&this.codexPetOverlayNiri([`action`,`move-window-to-floating`,`--id`,String(n)]);let r=this.codexPetOverlayNiriLocalMove();r!=null&&this.codexPetOverlayNiri([`action`,`move-floating-window`,`--id`,String(n),`-x`,String(r.x),`-y`,String(r.y)])})}", - "codexPetOverlayScheduleNiriHints(e){if(!this.codexPetOverlayShouldUseNiri())return;try{this.codexPetOverlayNiriTimers?.forEach(clearTimeout)}catch{}this.codexPetOverlayNiriTimers=[0,80,300,1000].map(t=>{let n=setTimeout(()=>{try{e==null||e.isDestroyed?.()||this.codexPetOverlayApplyNiriHints(e)}catch{}},t);try{n.unref?.()}catch{}return n})}", + "codexPetOverlayFindNiriWindow(e,t){if(!this.codexPetOverlayShouldUseNiri()){try{typeof t==`function`&&t({code:`DISABLED`})}catch{}return}let n=this.codexPetOverlayWindowBounds(e);this.codexPetOverlayNiri([`--json`,`windows`],(e,r)=>{if(e){try{typeof t==`function`&&t(e)}catch{}return}let i;try{i=JSON.parse(String(r??``))}catch{try{typeof t==`function`&&t({code:`INVALID_JSON`})}catch{}return}let a=this.codexPetOverlaySelectNiriWindow(i,n);try{typeof t==`function`&&t(null,a)}catch{}})}", + "codexPetOverlayApplyNiriHints(e,t=this.codexPetOverlayNiriEpoch){if(process.platform!==`linux`||e==null||e.isDestroyed?.()||!this.codexPetOverlayShouldUseNiri()||this.codexPetOverlayNiriDragState!=null||this.codexPetOverlayNiriDragCallOwner!=null)return;this.codexPetOverlayFindNiriWindow(e,(n,r)=>{if(n||e.isDestroyed?.()||this.window!==e||t!==this.codexPetOverlayNiriEpoch||this.codexPetOverlayNiriDragState!=null||this.codexPetOverlayNiriDragCallOwner!=null)return;let i=this.codexPetOverlayNiriPositiveInteger(r?.id);if(i==null)return;let a=()=>{if(e.isDestroyed?.()||this.window!==e||t!==this.codexPetOverlayNiriEpoch||this.codexPetOverlayNiriDragState!=null||this.codexPetOverlayNiriDragCallOwner!=null)return;let n=this.codexPetOverlayNiriLocalMove();n!=null&&this.codexPetOverlayNiri([`action`,`move-floating-window`,`--id`,String(i),`-x`,String(n.x),`-y`,String(n.y)])};r.is_floating===!0?a():this.codexPetOverlayNiri([`action`,`move-window-to-floating`,`--id`,String(i)],n=>{n||a()})})}", + "codexPetOverlayScheduleNiriHints(e){let t=this.codexPetOverlayNiriDragState;if(t!=null&&(this.window!==t.window||t.window?.isDestroyed?.())){try{t.retryTimer!=null&&clearTimeout(t.retryTimer)}catch{}this.codexPetOverlayNiriDragState=null,this.codexPetOverlayNiriEpoch=(this.codexPetOverlayNiriEpoch??0)+1}if(this.codexPetOverlayNiriUnavailable||!this.codexPetOverlayShouldUseNiri()||this.dragState!=null||this.codexPetOverlayNiriDragState!=null)return;if(this.codexPetOverlayNiriDragCallOwner!=null||(this.codexPetOverlayNiriProcessCount??0)>0){this.codexPetOverlayNiriPendingHintsWindow=e;return}this.codexPetOverlayNiriPendingHintsWindow=null;try{this.codexPetOverlayNiriTimers?.forEach(clearTimeout)}catch{}let n=(this.codexPetOverlayNiriEpoch??0)+1;this.codexPetOverlayNiriEpoch=n,this.codexPetOverlayNiriTimers=[0,80,300,1000].map(t=>{let r=setTimeout(()=>{try{e==null||e.isDestroyed?.()||this.codexPetOverlayApplyNiriHints(e,n)}catch{}},t);try{r.unref?.()}catch{}return r})}", + "codexPetOverlayBeginNiriDrag(e){if(e==null||e.isDestroyed?.()||this.window!==e||!this.codexPetOverlayShouldUseNiri())return;try{this.codexPetOverlayNiriTimers?.forEach(clearTimeout),this.codexPetOverlayNiriDragState?.retryTimer!=null&&clearTimeout(this.codexPetOverlayNiriDragState.retryTimer)}catch{}this.codexPetOverlayNiriEpoch=(this.codexPetOverlayNiriEpoch??0)+1;let t=(this.codexPetOverlayNiriDragGeneration??0)+1;this.codexPetOverlayNiriDragGeneration=t;let n=this.codexPetOverlayNiriLocalMove();this.codexPetOverlayNiriDragState={generation:t,window:e,id:null,floating:!1,latestTarget:n==null?null:{x:n.x,y:n.y},inFlight:!1,released:!1,persisted:!1,complete:null,retryIndex:0,retryTimer:null},this.codexPetOverlayPumpNiriDrag(this.codexPetOverlayNiriDragState)}", + "codexPetOverlayNiriDragCurrent(e){if(e==null||this.codexPetOverlayNiriDragState!==e||this.codexPetOverlayNiriDragGeneration!==e.generation)return!1;if(this.window===e.window&&!e.window?.isDestroyed?.())return!0;try{e.retryTimer!=null&&clearTimeout(e.retryTimer)}catch{}this.codexPetOverlayNiriDragState=null,this.codexPetOverlayNiriEpoch=(this.codexPetOverlayNiriEpoch??0)+1;let t=this.window;try{t!=null&&!t.isDestroyed?.()&&this.codexPetOverlayScheduleNiriHints(t)}catch{}return!1}", + "codexPetOverlayStartNiriDragCall(e){if(this.codexPetOverlayNiriDragCallOwner!=null)return!1;e.inFlight=!0,this.codexPetOverlayNiriDragCallOwner=e;return!0}", + "codexPetOverlayFinishNiriDragCall(e){e.inFlight=!1,this.codexPetOverlayNiriDragCallOwner===e&&(this.codexPetOverlayNiriDragCallOwner=null);let t=this.codexPetOverlayNiriDragState;if(t!=null&&t!==e)this.codexPetOverlayPumpNiriDrag(t);else if(t==null){let e=this.window;try{e!=null&&!e.isDestroyed?.()&&this.codexPetOverlayScheduleNiriHints(e)}catch{}}}", + "codexPetOverlayQueueNiriDrag(e){let t=this.codexPetOverlayNiriDragState;if(!this.codexPetOverlayNiriDragCurrent(t)||t.window!==e)return;let n=this.codexPetOverlayNiriLocalMove();n!=null&&(t.latestTarget={x:n.x,y:n.y}),this.codexPetOverlayPumpNiriDrag(t)}", + "codexPetOverlayRetryNiriDrag(e,t){if(!this.codexPetOverlayNiriDragCurrent(e))return;if(t?.code===`ENOENT`){this.codexPetOverlayAbortNiriDrag(e);return}if(e.retryIndex>=3){this.codexPetOverlayAbortNiriDrag(e);return}let n=[0,80,300][e.retryIndex]??300;e.retryTimer=setTimeout(()=>{e.retryTimer=null,this.codexPetOverlayNiriDragCurrent(e)&&this.codexPetOverlayPumpNiriDrag(e)},n);try{e.retryTimer.unref?.()}catch{}}", + "codexPetOverlayAbortNiriDrag(e){if(!this.codexPetOverlayNiriDragCurrent(e))return;try{e.retryTimer!=null&&clearTimeout(e.retryTimer)}catch{}e.inFlight=!1,e.retryTimer=null,e.released?this.codexPetOverlayFinalizeNiriDrag(e):this.codexPetOverlayNiriDragState=null}", + "codexPetOverlayFinalizeNiriDrag(e){if(!this.codexPetOverlayNiriDragCurrent(e)||!e.released||e.persisted)return;e.persisted=!0;let t=e.complete;this.codexPetOverlayNiriDragState=null;try{typeof t==`function`&&t()}catch{}}", + "codexPetOverlayPumpNiriDrag(e){if(!this.codexPetOverlayNiriDragCurrent(e)||e.inFlight||e.retryTimer!=null||this.codexPetOverlayNiriDragCallOwner!=null||(this.codexPetOverlayNiriProcessCount??0)>0)return;if(e.id==null){if(e.retryIndex>=3){this.codexPetOverlayAbortNiriDrag(e);return}e.retryIndex+=1;if(!this.codexPetOverlayStartNiriDragCall(e))return;this.codexPetOverlayFindNiriWindow(e.window,(t,n)=>{this.codexPetOverlayFinishNiriDragCall(e);if(!this.codexPetOverlayNiriDragCurrent(e))return;let r=this.codexPetOverlayNiriPositiveInteger(n?.id);if(t||r==null){this.codexPetOverlayRetryNiriDrag(e,t);return}e.id=r,e.floating=n.is_floating===!0,this.codexPetOverlayPumpNiriDrag(e)});return}if(!e.floating){if(!this.codexPetOverlayStartNiriDragCall(e))return;let t=e.id;this.codexPetOverlayNiri([`action`,`move-window-to-floating`,`--id`,String(t)],n=>{this.codexPetOverlayFinishNiriDragCall(e);if(!this.codexPetOverlayNiriDragCurrent(e))return;if(n){e.id=null,e.floating=!1,this.codexPetOverlayRetryNiriDrag(e,n);return}e.floating=!0,this.codexPetOverlayPumpNiriDrag(e)});return}let t=e.latestTarget;if(t!=null){e.latestTarget=null;if(!this.codexPetOverlayStartNiriDragCall(e)){e.latestTarget=t;return}let n=e.id;this.codexPetOverlayNiri([`action`,`move-floating-window`,`--id`,String(n),`-x`,String(t.x),`-y`,String(t.y)],n=>{this.codexPetOverlayFinishNiriDragCall(e);if(!this.codexPetOverlayNiriDragCurrent(e))return;if(n){e.latestTarget??=t,e.id=null,e.floating=!1,this.codexPetOverlayRetryNiriDrag(e,n);return}this.codexPetOverlayPumpNiriDrag(e)});return}e.released&&this.codexPetOverlayFinalizeNiriDrag(e)}", + "codexPetOverlayEndNiriDrag(e,t){let n=this.codexPetOverlayNiriDragState;if(!this.codexPetOverlayNiriDragCurrent(n)||n.window!==e)return!1;n.released=!0,n.complete=t;let r=this.codexPetOverlayNiriLocalMove();r!=null&&(n.latestTarget={x:r.x,y:r.y}),this.codexPetOverlayPumpNiriDrag(n);return!0}", "codexPetOverlayShouldLockPosition(){return process.platform===`linux`&&this.codexPetOverlaySettings().lockPosition===!0}", ].join(""); } @@ -274,16 +285,21 @@ function patchCompositorDragLifecycle(source) { return patched; } const needsKWinStart = !startMethod.text.includes("codexPetOverlayBeginKWinDrag("); - if (needsKWinStart) { + const needsNiriStart = !startMethod.text.includes("codexPetOverlayBeginNiriDrag("); + if (needsKWinStart || needsNiriStart) { const windowMatch = startMethod.text.match(/let ([A-Za-z_$][\w$]*)=this\.window;/); if (windowMatch == null || !startMethod.text.includes("this.dragState=")) { console.warn("WARN: Could not identify current avatar overlay drag start shape - skipping compositor transport hook"); return patched; } + const hooks = [ + needsKWinStart ? `this.codexPetOverlayBeginKWinDrag(${windowMatch[1]})` : null, + needsNiriStart ? `this.codexPetOverlayBeginNiriDrag(${windowMatch[1]})` : null, + ].filter(Boolean).join(","); patched = replaceMethodText( patched, startMethod, - `${startMethod.text.slice(0, -1)},this.codexPetOverlayBeginKWinDrag(${windowMatch[1]})}`, + `${startMethod.text.slice(0, -1)},${hooks}}`, ); } @@ -292,7 +308,10 @@ function patchCompositorDragLifecycle(source) { console.warn("WARN: Could not find avatar overlay endDrag for compositor transport - skipping pet overlay patch"); return patched; } - if (endMethod.text.includes("codexPetOverlayEndKWinDrag(")) { + if ( + endMethod.text.includes("codexPetOverlayEndKWinDrag(") && + endMethod.text.includes("codexPetOverlayEndNiriDrag(") + ) { return patched; } const completionPattern = /[A-Za-z_$][\w$]*\?this\.persistWindowBounds\(([A-Za-z_$][\w$]*),[A-Za-z_$][\w$]*\?\?this\.getCurrentDisplay\(\)\):this\.reclampWindowToVisibleDisplay\(\{shouldPersist:!0\}\)/; @@ -307,7 +326,7 @@ function patchCompositorDragLifecycle(source) { patched, endMethod, endMethod.text.slice(0, completionMatch.index) + - `this.codexPetOverlayEndKWinDrag(${windowVar},()=>{${completionNeedle}})||(()=>{${completionNeedle}})()` + + `this.codexPetOverlayEndKWinDrag(${windowVar},()=>{${completionNeedle}})||this.codexPetOverlayEndNiriDrag(${windowVar},()=>{${completionNeedle}})||(()=>{${completionNeedle}})()` + "}", ); } @@ -469,6 +488,8 @@ function hasCompletePetOverlayPatch(source, settings, avatarSelectionRefreshExpe source.includes("codexPetOverlayKWinQdbus("), source.includes("this.codexPetOverlayBeginKWinDrag("), source.includes("this.codexPetOverlayEndKWinDrag("), + source.includes("this.codexPetOverlayBeginNiriDrag("), + source.includes("this.codexPetOverlayEndNiriDrag("), source.includes("===`avatarOverlay`?{backgroundColor:`#00000000`,backgroundMaterial:null}:"), source.includes("title:`Codex Pet Overlay`,width:"), ]; diff --git a/linux-features/pet-overlay/test.js b/linux-features/pet-overlay/test.js index 14cb09ddb..5b44bb2d6 100644 --- a/linux-features/pet-overlay/test.js +++ b/linux-features/pet-overlay/test.js @@ -51,7 +51,7 @@ function currentAvatarOverlayBundleFixture() { "var settingsHandlers={\"set-setting\":async({key:e,value:t})=>(this.setSettingValue(e,t),{success:!0})};", "var rV=`/avatar-overlay`,zB={width:356,height:320},oV={width:112,height:121},k2={width:0,height:0},O2={width:276,height:131};", "var h2=class{constructor(e,t,n,r){this.cursorSource=e;this.pointerAnchorX=t;this.pointerAnchorY=n;this.displayBounds=r}};", - "var fV=class{window=null;rendererReady=!1;layout=null;mascotSize=oV;traySize=null;pointerInteractive=!1;mousePassthroughEnabled=!1;windowStagedForNativePresentation=!1;layoutMode=`native`;compositionHost={setOverlayWindow(){},isNativeMaterialAttached(){return!1},getCursorPosition(){return null},updateMascotRect(){}};nativePositionController={clear(){}};", + "var fV=class{window=null;rendererReady=!1;layout=null;mascotSize=oV;traySize=null;pointerInteractive=!1;mousePassthroughEnabled=!1;windowStagedForNativePresentation=!1;layoutMode=`native`;compositionHost={setOverlayWindow(){},isNativeMaterialAttached(){return!1},getCursorPosition(){return null},performWindowDrag(){return!1},updateMascotRect(){}};nativePositionController={clear(){}};", "constructor(e,t){this.windowManager=e,this.globalState=t}", "isOpen(){let e=this.window;return e!=null&&!e.isDestroyed()&&e.isVisible()&&!this.windowStagedForNativePresentation}", "startDrag(e,t,n=!1){let r=this.window;if(r==null||r.isDestroyed()||r.webContents.id!==e)return;this.cancelMomentum();let i=this.getLayout(r),o=this.compositionHost.getCursorPosition(),s=t.pointerScreenX!=null?{x:t.pointerScreenX,y:t.pointerScreenY}:a.screen.getCursorScreenPoint();this.dragState=new h2(o==null?`renderer`:`native`,t.pointerWindowX-i.mascot.left,t.pointerWindowY-i.mascot.top,a.screen.getDisplayNearestPoint(s).bounds,n),this.windowServerDragActive=this.layoutMode===`native`&&!n&&this.compositionHost.performWindowDrag(),this.windowServerDragActive||(this.windowServerDragWindowX=null)}", @@ -920,8 +920,8 @@ test("runtime lock override blocks drag start", () => { controller.startDrag(1, { pointerScreenX: 100, pointerScreenY: 100, - pointerWindowX: 20, - pointerWindowY: 20, + pointerWindowX: 80, + pointerWindowY: 80, }); assert.deepEqual(JSON.parse(JSON.stringify(controller.dragState)), { preserved: true }); @@ -1046,6 +1046,58 @@ function runNiriHintScenario({ return calls; } +function createAsyncNiriDragScenario() { + const calls = []; + const pending = []; + const timers = []; + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const { controller } = controllerFromPatchedSource(patched, { + process: { env: { XDG_CURRENT_DESKTOP: "niri" } }, + childProcess: { + execFile(command, args, options, callback) { + assert.equal(command, "niri"); + assert.equal(options.timeout, 1200); + calls.push(args); + pending.push({ args, callback }); + }, + }, + setTimeout(callback, delay) { + const timer = { callback, delay, unref() {} }; + timers.push(timer); + return timer; + }, + clearTimeout(timer) { + timer.cleared = true; + }, + }); + const window = { + getBounds: () => ({ x: 100, y: 100, width: 356, height: 320 }), + isDestroyed: () => false, + webContents: { id: 1 }, + }; + controller.window = window; + controller.codexPetOverlayDesiredDisplayBounds = { x: 0, y: 0, width: 1920, height: 1080 }; + controller.codexPetOverlayDesiredWindowBounds = { x: 100, y: 100, width: 356, height: 320 }; + return { calls, controller, pending, timers, window }; +} + +function completePendingNiriCall(scenario, { error = null, stdout = "ok" } = {}) { + const call = scenario.pending.shift(); + assert.ok(call, "expected a pending niri call"); + call.callback(error, stdout); + return call.args; +} + +function niriPetWindow(id, isFloating = true) { + return JSON.stringify([{ + id, + is_floating: isFloating, + layout: { window_size: [356, 320] }, + pid: 4242, + title: "Codex Pet Overlay", + }]); +} + test("targets only the unambiguous Hyprland pet window address", () => { const calls = runHyprlandHintScenario({ clientsJson: JSON.stringify([ @@ -1778,6 +1830,243 @@ test("Niri scheduling is coalesced when desired bounds change repeatedly", () => assert.deepEqual(cleared, timers.slice(0, 4)); }); +test("Niri drag keeps one move in flight and emits only the latest queued target", () => { + const scenario = createAsyncNiriDragScenario(); + const { controller, pending, window } = scenario; + + controller.codexPetOverlayBeginNiriDrag(window); + assert.equal(pending.length, 1); + completePendingNiriCall(scenario, { stdout: niriPetWindow(9) }); + assert.equal(pending.length, 1); + assert.equal(JSON.stringify(pending[0].args.slice(-4)), JSON.stringify(["-x", "100", "-y", "100"])); + + controller.codexPetOverlayDesiredWindowBounds = { x: 600, y: 100, width: 356, height: 320 }; + controller.codexPetOverlayQueueNiriDrag(window); + controller.codexPetOverlayDesiredWindowBounds = { x: 120, y: 100, width: 356, height: 320 }; + controller.codexPetOverlayQueueNiriDrag(window); + + assert.equal(pending.length, 1, "a second compositor move must not overlap the first"); + completePendingNiriCall(scenario); + assert.equal(pending.length, 1); + assert.equal(JSON.stringify(pending[0].args.slice(-4)), JSON.stringify(["-x", "120", "-y", "100"])); + assert.equal(scenario.calls.some((args) => args.includes("600")), false); +}); + +test("Niri drag waits for an already-running bootstrap compositor action", () => { + const scenario = createAsyncNiriDragScenario(); + const { controller, pending, window } = scenario; + + controller.codexPetOverlayNiri(["action", "move-floating-window", "--id", "9", "-x", "40", "-y", "40"]); + assert.equal(pending.length, 1); + controller.codexPetOverlayBeginNiriDrag(window); + assert.equal(pending.length, 1, "drag discovery must wait for the bootstrap action"); + + completePendingNiriCall(scenario); + assert.equal(pending.length, 1); + assert.equal(pending[0].args.includes("windows"), true); +}); + +test("completed Niri processes do not schedule another hint batch without a pending request", () => { + const scenario = createAsyncNiriDragScenario(); + const { controller, pending, timers } = scenario; + + controller.codexPetOverlayNiri(["--json", "windows"]); + assert.equal(pending.length, 1); + completePendingNiriCall(scenario, { stdout: "[]" }); + + assert.equal(pending.length, 0); + assert.deepEqual(timers, []); +}); + +test("Niri drag floats a tiled pet before its first move", () => { + const scenario = createAsyncNiriDragScenario(); + const { controller, pending, window } = scenario; + + controller.codexPetOverlayBeginNiriDrag(window); + completePendingNiriCall(scenario, { stdout: niriPetWindow(9, false) }); + + assert.equal(pending.length, 1); + assert.equal( + JSON.stringify(pending[0].args), + JSON.stringify(["msg", "action", "move-window-to-floating", "--id", "9"]), + ); + assert.equal(scenario.calls.some((args) => args.includes("move-floating-window")), false); + + completePendingNiriCall(scenario); + assert.equal(pending.length, 1); + assert.equal(pending[0].args.includes("move-floating-window"), true); +}); + +test("Niri endDrag drains the final move before persisting and docking", () => { + const scenario = createAsyncNiriDragScenario(); + const { controller, pending, window } = scenario; + const completed = []; + controller.getLayout = () => ({ mascot: { left: 0, top: 0 } }); + controller.compositionHost.performWindowDrag = () => true; + controller.persistWindowBounds = (target, display) => completed.push(["persist", target, display]); + controller.dockTarget = { anchor: "dock-anchor", onDock: "dock-handler" }; + controller.dockPresentation = (anchor, onDock) => completed.push(["dock", anchor, onDock]); + + controller.startDrag(1, { + pointerScreenX: 100, + pointerScreenY: 100, + pointerWindowX: 20, + pointerWindowY: 20, + }); + controller.codexPetOverlayDesiredWindowBounds = { x: 120, y: 100, width: 356, height: 320 }; + controller.codexPetOverlayQueueNiriDrag(window); + controller.endDrag(1, {}); + + assert.deepEqual(completed, []); + completePendingNiriCall(scenario, { stdout: niriPetWindow(9) }); + assert.equal(pending.length, 1); + assert.equal(JSON.stringify(pending[0].args.slice(-4)), JSON.stringify(["-x", "120", "-y", "100"])); + assert.deepEqual(completed, []); + completePendingNiriCall(scenario); + + assert.equal(completed.length, 2); + assert.equal(completed[0][0], "persist"); + assert.equal(completed[0][1], window); + assert.equal(completed[0][2]?.id, 1); + assert.deepEqual(completed[1], ["dock", "dock-anchor", "dock-handler"]); + assert.equal(controller.codexPetOverlayNiriDragState, null); +}); + +test("stale Niri callbacks clear drag state and reschedule hints for a replacement window", () => { + const scenario = createAsyncNiriDragScenario(); + const { controller, pending, timers, window: oldWindow } = scenario; + controller.codexPetOverlayBeginNiriDrag(oldWindow); + assert.equal(pending.length, 1); + + const newWindow = { + getBounds: () => ({ x: 300, y: 200, width: 356, height: 320 }), + isDestroyed: () => false, + webContents: { id: 2 }, + }; + controller.window = newWindow; + completePendingNiriCall(scenario, { stdout: niriPetWindow(41) }); + + assert.equal(controller.codexPetOverlayNiriDragState, null); + assert.deepEqual(timers.map((timer) => timer.delay), [0, 80, 300, 1000]); +}); + +test("Niri hint scheduling clears an idle drag state left by a replaced window", () => { + const scenario = createAsyncNiriDragScenario(); + const { controller, pending, timers, window: oldWindow } = scenario; + controller.codexPetOverlayBeginNiriDrag(oldWindow); + completePendingNiriCall(scenario, { stdout: niriPetWindow(9) }); + completePendingNiriCall(scenario); + assert.equal(pending.length, 0); + assert.notEqual(controller.codexPetOverlayNiriDragState, null); + + const newWindow = { + getBounds: () => ({ x: 300, y: 200, width: 356, height: 320 }), + isDestroyed: () => false, + webContents: { id: 2 }, + }; + controller.window = newWindow; + controller.dragState = null; + controller.codexPetOverlayScheduleNiriHints(newWindow); + + assert.equal(controller.codexPetOverlayNiriDragState, null); + assert.deepEqual(timers.map((timer) => timer.delay), [0, 80, 300, 1000]); +}); + +test("stale Niri discovery callbacks cannot continue a replacement window drag", () => { + const scenario = createAsyncNiriDragScenario(); + const { controller, pending, window: oldWindow } = scenario; + controller.codexPetOverlayBeginNiriDrag(oldWindow); + + const newWindow = { + getBounds: () => ({ x: 300, y: 200, width: 356, height: 320 }), + isDestroyed: () => false, + webContents: { id: 2 }, + }; + controller.window = newWindow; + controller.codexPetOverlayDesiredWindowBounds = { x: 300, y: 200, width: 356, height: 320 }; + controller.codexPetOverlayBeginNiriDrag(newWindow); + assert.equal(pending.length, 1, "replacement discovery must wait for the previous call"); + + completePendingNiriCall(scenario, { stdout: niriPetWindow(41) }); + assert.equal(pending.length, 1, "the replacement discovery starts only after the stale call completes"); + completePendingNiriCall(scenario, { stdout: niriPetWindow(42) }); + assert.equal(pending.length, 1); + assert.equal(pending[0].args.includes("42"), true); + assert.equal(scenario.calls.some((args) => args.includes("41") && args.includes("action")), false); +}); + +test("Niri drag discovery recovery is bounded and ENOENT aborts immediately", () => { + const scenario = createAsyncNiriDragScenario(); + const { controller, pending, timers, window } = scenario; + controller.codexPetOverlayBeginNiriDrag(window); + + completePendingNiriCall(scenario, { error: new Error("discovery failed"), stdout: "" }); + assert.deepEqual(timers.map((timer) => timer.delay), [80]); + timers.shift().callback(); + completePendingNiriCall(scenario, { error: new Error("discovery failed"), stdout: "" }); + assert.deepEqual(timers.map((timer) => timer.delay), [300]); + timers.shift().callback(); + completePendingNiriCall(scenario, { stdout: "[]" }); + + assert.equal(scenario.calls.filter((args) => args.includes("windows")).length, 3); + assert.equal(controller.codexPetOverlayNiriDragState, null); + assert.equal(pending.length, 0); + + const missingScenario = createAsyncNiriDragScenario(); + missingScenario.controller.codexPetOverlayBeginNiriDrag(missingScenario.window); + const missingError = new Error("missing niri"); + missingError.code = "ENOENT"; + completePendingNiriCall(missingScenario, { error: missingError, stdout: "" }); + assert.equal(missingScenario.controller.codexPetOverlayNiriDragState, null); + assert.deepEqual(missingScenario.timers, []); +}); + +test("Niri drag action failure invalidates the cached id and rediscovers once serialized", () => { + const scenario = createAsyncNiriDragScenario(); + const { controller, pending, timers, window } = scenario; + controller.codexPetOverlayBeginNiriDrag(window); + completePendingNiriCall(scenario, { stdout: niriPetWindow(9) }); + assert.equal(pending.length, 1); + assert.equal(pending[0].args.includes("move-floating-window"), true); + + completePendingNiriCall(scenario, { error: new Error("move failed"), stdout: "" }); + assert.equal(pending.length, 0); + assert.deepEqual(timers.map((timer) => timer.delay), [80]); + timers.shift().callback(); + assert.equal(pending.length, 1); + assert.equal(pending[0].args.includes("windows"), true); + + completePendingNiriCall(scenario, { stdout: niriPetWindow(10) }); + assert.equal(pending.length, 1); + assert.equal(pending[0].args.includes("10"), true); + assert.equal(scenario.calls.filter((args) => args.includes("move-floating-window")).length, 2); +}); + +test("stale Niri action completion cannot continue a replacement drag", () => { + const scenario = createAsyncNiriDragScenario(); + const { controller, pending, window: oldWindow } = scenario; + controller.codexPetOverlayBeginNiriDrag(oldWindow); + completePendingNiriCall(scenario, { stdout: niriPetWindow(41) }); + assert.equal(pending.length, 1); + assert.equal(pending[0].args.includes("41"), true); + + const newWindow = { + getBounds: () => ({ x: 300, y: 200, width: 356, height: 320 }), + isDestroyed: () => false, + webContents: { id: 2 }, + }; + controller.window = newWindow; + controller.codexPetOverlayDesiredWindowBounds = { x: 300, y: 200, width: 356, height: 320 }; + controller.codexPetOverlayBeginNiriDrag(newWindow); + assert.equal(pending.length, 1, "replacement drag must not overlap the previous compositor action"); + + completePendingNiriCall(scenario); + assert.equal(pending.length, 1, "replacement discovery starts after the old action completes"); + completePendingNiriCall(scenario, { stdout: niriPetWindow(42) }); + assert.equal(pending.length, 1); + assert.equal(pending[0].args.includes("42"), true); +}); + test("settings validation falls back to safe defaults", () => { assert.deepEqual( mergedPetOverlaySettings({ From 799342ad14599334c418800b037c3681a74575e9 Mon Sep 17 00:00:00 2001 From: Gary Lysenko Date: Sat, 18 Jul 2026 08:11:09 +0300 Subject: [PATCH 5/6] test(pet-overlay): keep lock override fixture scoped --- linux-features/pet-overlay/test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linux-features/pet-overlay/test.js b/linux-features/pet-overlay/test.js index 5b44bb2d6..c3b82ff14 100644 --- a/linux-features/pet-overlay/test.js +++ b/linux-features/pet-overlay/test.js @@ -920,8 +920,8 @@ test("runtime lock override blocks drag start", () => { controller.startDrag(1, { pointerScreenX: 100, pointerScreenY: 100, - pointerWindowX: 80, - pointerWindowY: 80, + pointerWindowX: 20, + pointerWindowY: 20, }); assert.deepEqual(JSON.parse(JSON.stringify(controller.dragState)), { preserved: true }); From c5cd8a8ba2a74db1f3f7ade576b6a685035b7a39 Mon Sep 17 00:00:00 2001 From: Gary Lysenko Date: Sat, 18 Jul 2026 08:13:26 +0300 Subject: [PATCH 6/6] test(pet-overlay): restore KWin drag regressions --- linux-features/pet-overlay/test.js | 67 +++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/linux-features/pet-overlay/test.js b/linux-features/pet-overlay/test.js index c3b82ff14..eda3b17f5 100644 --- a/linux-features/pet-overlay/test.js +++ b/linux-features/pet-overlay/test.js @@ -2143,11 +2143,15 @@ test("KWin hints target only the matching Plasma pet and apply its Wayland bound test("KWin drag uses qdbus, cleans up its temporary script, and honors its runtime override", () => { const calls = []; + let script = null; const { controller } = controllerFromPatchedSource(applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), { process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, childProcess: { execFileSync(command, args, options) { calls.push([command, args, options]); + if (args.includes("org.kde.kwin.Scripting.loadScript")) { + script = fs.readFileSync(args[3], "utf8"); + } }, }, }); @@ -2155,6 +2159,7 @@ test("KWin drag uses qdbus, cleans up its temporary script, and honors its runti getContentBounds: () => ({ x: 145, y: 210, width: 356, height: 320 }), isDestroyed: () => false, }; + let persisted = false; controller.window = window; controller.codexPetOverlayBeginKWinDrag(window); @@ -2163,13 +2168,73 @@ test("KWin drag uses qdbus, cleans up its temporary script, and honors its runti "org.kde.kwin.Scripting.loadScript", "org.kde.kwin.Scripting.start", ]); + assert.equal(calls[0][0], "qdbus6"); + assert.equal(calls[0][2].timeout, 750); + assert.equal(controller.windowServerDragActive, true); + assert.equal(controller.windowServerDragWindowX, 145); + assert.ok(script); + + const cursorSignal = { callback: null, connect(callback) { this.callback = callback; }, disconnect() {} }; + const removedSignal = { connect() {} }; + const pet = { + caption: "Codex Pet Overlay", + frameGeometry: { x: 100, y: 200, width: 356, height: 320 }, + pid: 4242, + }; + const workspace = { + cursorPos: { x: 130, y: 250 }, + cursorPosChanged: cursorSignal, + raiseWindow() {}, + windowList: () => [pet], + windowRemoved: removedSignal, + }; + vm.runInNewContext(script, { workspace }); + assert.equal(typeof cursorSignal.callback, "function"); + workspace.cursorPos = { x: 300, y: 410 }; + cursorSignal.callback(); + assert.deepEqual( + JSON.parse(JSON.stringify(pet.frameGeometry)), + { x: 270, y: 360, width: 356, height: 320 }, + ); + + controller.codexPetOverlayQueueKWinDrag(window); + assert.equal(calls.length, 2, "pointer updates must not spawn compositor processes"); assert.equal(fs.existsSync(scriptPath), true); - assert.equal(controller.codexPetOverlayEndKWinDrag(window, () => {}), true); + assert.equal(controller.codexPetOverlayEndKWinDrag(window, () => { persisted = true; }), true); assert.equal(calls[2][1][2], "org.kde.kwin.Scripting.unloadScript"); assert.equal(fs.existsSync(scriptPath), false); + assert.equal(persisted, true); + assert.equal(controller.codexPetOverlayKWinDragState, null); const overridden = controllerFromPatchedSource(applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), { process: { env: { XDG_CURRENT_DESKTOP: "KDE", CODEX_PET_OVERLAY_KWIN: "0" } }, }).controller; assert.equal(overridden.codexPetOverlayShouldUseKWin(), false); }); + +test("KWin drag falls back without repeatedly probing missing qdbus commands", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + let calls = 0; + const { controller } = controllerFromPatchedSource(patched, { + process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, + childProcess: { + execFileSync() { + calls += 1; + const error = new Error("qdbus missing"); + error.code = "ENOENT"; + throw error; + }, + }, + }); + const window = { isDestroyed: () => false }; + controller.window = window; + + controller.codexPetOverlayBeginKWinDrag(window); + + assert.equal(calls, 2); + assert.equal(controller.codexPetOverlayKWinDragState, undefined); + assert.equal(controller.windowServerDragActive, undefined); + + controller.codexPetOverlayBeginKWinDrag(window); + assert.equal(calls, 2); +});