mirror of
https://github.com/aaddrick/claude-desktop-debian.git
synced 2026-05-17 08:36:35 +03:00
276 lines
11 KiB
JavaScript
276 lines
11 KiB
JavaScript
// Inject frame fix before main app loads
|
|
const Module = require('module');
|
|
const path = require('path');
|
|
const originalRequire = Module.prototype.require;
|
|
|
|
console.log('[Frame Fix] Wrapper loaded');
|
|
|
|
// Fix process.resourcesPath to match the actual location of app.asar.
|
|
// In Nix builds, electron is a separate store path so process.resourcesPath
|
|
// points to the Electron package's resources dir, not where our tray icons
|
|
// and app.asar.unpacked live. Deriving from __dirname (the asar root) gives
|
|
// the correct path; for deb/AppImage builds the values already match.
|
|
const derivedResourcesPath = path.dirname(__dirname);
|
|
if (derivedResourcesPath !== process.resourcesPath) {
|
|
console.log('[Frame Fix] Correcting process.resourcesPath');
|
|
console.log('[Frame Fix] Was:', process.resourcesPath);
|
|
console.log('[Frame Fix] Now:', derivedResourcesPath);
|
|
process.resourcesPath = derivedResourcesPath;
|
|
}
|
|
|
|
// Menu bar visibility mode, controlled by CLAUDE_MENU_BAR env var:
|
|
// 'auto' - hidden by default, Alt toggles visibility (current default)
|
|
// 'visible' - always visible, Alt does not toggle (stable layout)
|
|
// 'hidden' - always hidden, Alt does not toggle
|
|
// Also accepts boolean-style aliases: 1/true/yes/on -> visible, 0/false/no/off -> hidden
|
|
const VALID_MENU_BAR_MODES = ['auto', 'visible', 'hidden'];
|
|
const MENU_BAR_ALIASES = {
|
|
'1': 'visible', 'true': 'visible', 'yes': 'visible', 'on': 'visible',
|
|
'0': 'hidden', 'false': 'hidden', 'no': 'hidden', 'off': 'hidden',
|
|
};
|
|
const rawMenuBarMode = (process.env.CLAUDE_MENU_BAR || 'auto').toLowerCase();
|
|
const resolvedMode = MENU_BAR_ALIASES[rawMenuBarMode] || rawMenuBarMode;
|
|
const MENU_BAR_MODE = VALID_MENU_BAR_MODES.includes(resolvedMode) ? resolvedMode : 'auto';
|
|
if (resolvedMode !== rawMenuBarMode) {
|
|
console.log(`[Frame Fix] CLAUDE_MENU_BAR '${process.env.CLAUDE_MENU_BAR}' resolved to '${resolvedMode}'`);
|
|
} else if (resolvedMode !== MENU_BAR_MODE) {
|
|
console.warn(`[Frame Fix] Unknown CLAUDE_MENU_BAR value '${process.env.CLAUDE_MENU_BAR}', falling back to 'auto'. Valid: ${VALID_MENU_BAR_MODES.join(', ')}, or 0/1/true/false/yes/no/on/off`);
|
|
}
|
|
console.log(`[Frame Fix] Menu bar mode: ${MENU_BAR_MODE}`);
|
|
|
|
// Detect if a window intends to be frameless (popup/Quick Entry/About)
|
|
// Quick Entry: titleBarStyle:"", skipTaskbar:true, transparent:true, resizable:false
|
|
// About: titleBarStyle:"", skipTaskbar:true, resizable:false
|
|
// Main: titleBarStyle:"", titleBarOverlay:false(linux), resizable (has minWidth)
|
|
// The main window has minWidth set; popups do not.
|
|
function isPopupWindow(options) {
|
|
if (!options) return false;
|
|
if (options.frame === false) return true;
|
|
if (options.titleBarStyle === '' && !options.minWidth) return true;
|
|
return false;
|
|
}
|
|
|
|
// CSS injection for Linux scrollbar styling
|
|
// Respects both light and dark themes via prefers-color-scheme
|
|
const LINUX_CSS = `
|
|
/* Scrollbar styling - thin, unobtrusive, adapts to theme */
|
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb {
|
|
background: rgba(128, 128, 128, 0.3);
|
|
border-radius: 4px;
|
|
transition: background 0.15s ease;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(128, 128, 128, 0.55);
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
::-webkit-scrollbar-thumb {
|
|
background: rgba(200, 200, 200, 0.2);
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(200, 200, 200, 0.4);
|
|
}
|
|
}
|
|
`;
|
|
|
|
// Build the patched BrowserWindow class and Menu interceptor once,
|
|
// on first require('electron'), then reuse via Proxy on every access.
|
|
let PatchedBrowserWindow = null;
|
|
let patchedSetApplicationMenu = null;
|
|
let electronModule = null;
|
|
|
|
Module.prototype.require = function(id) {
|
|
const result = originalRequire.apply(this, arguments);
|
|
|
|
if (id === 'electron') {
|
|
// Build patches once from the real electron module
|
|
if (!PatchedBrowserWindow) {
|
|
electronModule = result;
|
|
const OriginalBrowserWindow = result.BrowserWindow;
|
|
const OriginalMenu = result.Menu;
|
|
|
|
PatchedBrowserWindow = class BrowserWindowWithFrame extends OriginalBrowserWindow {
|
|
constructor(options) {
|
|
console.log('[Frame Fix] BrowserWindow constructor called');
|
|
let popup = false;
|
|
if (process.platform === 'linux') {
|
|
options = options || {};
|
|
const originalFrame = options.frame;
|
|
popup = isPopupWindow(options);
|
|
|
|
if (popup) {
|
|
// Popup/Quick Entry windows: keep frameless for proper UX
|
|
options.frame = false;
|
|
// Remove macOS-specific titlebar options that don't apply on Linux
|
|
delete options.titleBarStyle;
|
|
delete options.titleBarOverlay;
|
|
console.log('[Frame Fix] Popup detected, keeping frameless');
|
|
} else {
|
|
// Main window: force native frame
|
|
options.frame = true;
|
|
// Menu bar behavior depends on CLAUDE_MENU_BAR mode:
|
|
// 'auto' (default): hidden, Alt toggles
|
|
// 'visible'/'hidden': no Alt toggle
|
|
options.autoHideMenuBar = (MENU_BAR_MODE === 'auto');
|
|
// Remove custom titlebar options
|
|
delete options.titleBarStyle;
|
|
delete options.titleBarOverlay;
|
|
console.log(`[Frame Fix] Modified frame from ${originalFrame} to true`);
|
|
}
|
|
}
|
|
super(options);
|
|
|
|
if (process.platform === 'linux') {
|
|
// Hide menu bar after window creation (unless user wants it visible)
|
|
if (MENU_BAR_MODE !== 'visible') {
|
|
this.setMenuBarVisibility(false);
|
|
}
|
|
|
|
// Inject CSS for Linux scrollbar styling
|
|
this.webContents.on('did-finish-load', () => {
|
|
this.webContents.insertCSS(LINUX_CSS).catch(() => {});
|
|
});
|
|
|
|
// Ensure menu bar stays hidden on show events
|
|
this.on('show', () => {
|
|
if (MENU_BAR_MODE !== 'visible') {
|
|
this.setMenuBarVisibility(false);
|
|
}
|
|
});
|
|
|
|
if (!popup) {
|
|
// Directly set child view bounds to match content size.
|
|
// This bypasses Chromium's stale LayoutManagerBase cache
|
|
// (only invalidated via _NET_WM_STATE atom changes, which
|
|
// KWin corner-snap/quick-tile never sets). Instead of
|
|
// monkey-patching getContentBounds() (which causes drag
|
|
// resize jitter at ~60Hz), we only act on discrete state
|
|
// changes. Fixes: #239
|
|
const fixChildBounds = () => {
|
|
if (this.isDestroyed()) return;
|
|
const children = this.contentView?.children;
|
|
if (!children?.length) return;
|
|
const [cw, ch] = this.getContentSize();
|
|
if (cw <= 0 || ch <= 0) return;
|
|
const cur = children[0].getBounds();
|
|
if (cur.width !== cw || cur.height !== ch) {
|
|
children[0].setBounds({ x: 0, y: 0, width: cw, height: ch });
|
|
}
|
|
};
|
|
|
|
// Geometry settles in stages after state changes.
|
|
// Three passes at 0/16/150ms cover immediate, next-frame,
|
|
// and compositor-animation-complete timing.
|
|
const fixAfterStateChange = () => {
|
|
fixChildBounds();
|
|
setTimeout(fixChildBounds, 16);
|
|
setTimeout(fixChildBounds, 150);
|
|
};
|
|
|
|
for (const evt of ['maximize', 'unmaximize',
|
|
'enter-full-screen', 'leave-full-screen']) {
|
|
this.on(evt, fixAfterStateChange);
|
|
}
|
|
|
|
// KWin corner-snap/quick-tile emits 'moved' but not
|
|
// 'maximize'/'unmaximize'. Guard with a size-change check
|
|
// so normal window drags (position-only) are ignored.
|
|
let lastSize = [0, 0];
|
|
this.on('moved', () => {
|
|
if (this.isDestroyed()) return;
|
|
const [w, h] = this.getSize();
|
|
if (w !== lastSize[0] || h !== lastSize[1]) {
|
|
lastSize = [w, h];
|
|
fixAfterStateChange();
|
|
}
|
|
});
|
|
|
|
// Tiling WMs (Hyprland, i3, sway) emit 'resize' on
|
|
// workspace switches with stale getContentBounds()
|
|
// cache. The size-change guard in fixChildBounds()
|
|
// prevents unnecessary work during drag resize.
|
|
// Fixes: #323
|
|
this.on('resize', fixAfterStateChange);
|
|
|
|
// ready-to-show fires once per window lifecycle
|
|
this.once('ready-to-show', () => {
|
|
if (MENU_BAR_MODE !== 'visible') {
|
|
this.setMenuBarVisibility(false);
|
|
}
|
|
// One-time jiggle for initial layout. Fixes: #84
|
|
const [w, h] = this.getSize();
|
|
this.setSize(w + 1, h + 1);
|
|
setTimeout(() => {
|
|
if (this.isDestroyed()) return;
|
|
this.setSize(w, h);
|
|
fixAfterStateChange();
|
|
}, 50);
|
|
});
|
|
|
|
// Fixes: #149 - KDE Plasma: Window demands attention on Alt+Tab
|
|
this.on('focus', () => {
|
|
this.flashFrame(false);
|
|
});
|
|
}
|
|
|
|
console.log('[Frame Fix] Linux patches applied');
|
|
}
|
|
}
|
|
};
|
|
|
|
// Copy static methods and properties from original
|
|
for (const key of Object.getOwnPropertyNames(OriginalBrowserWindow)) {
|
|
if (key !== 'prototype' && key !== 'length' && key !== 'name') {
|
|
try {
|
|
const descriptor = Object.getOwnPropertyDescriptor(OriginalBrowserWindow, key);
|
|
if (descriptor) {
|
|
Object.defineProperty(PatchedBrowserWindow, key, descriptor);
|
|
}
|
|
} catch (e) {
|
|
// Ignore errors for non-configurable properties
|
|
}
|
|
}
|
|
}
|
|
|
|
// Intercept Menu.setApplicationMenu to hide menu bar on Linux
|
|
const originalSetAppMenu = OriginalMenu.setApplicationMenu.bind(OriginalMenu);
|
|
patchedSetApplicationMenu = function(menu) {
|
|
console.log('[Frame Fix] Intercepting setApplicationMenu');
|
|
originalSetAppMenu(menu);
|
|
if (process.platform === 'linux' && MENU_BAR_MODE !== 'visible') {
|
|
for (const win of PatchedBrowserWindow.getAllWindows()) {
|
|
if (win.isDestroyed()) continue;
|
|
win.setMenuBarVisibility(false);
|
|
}
|
|
console.log('[Frame Fix] Menu bar hidden on all windows');
|
|
}
|
|
};
|
|
|
|
console.log('[Frame Fix] Patches built successfully');
|
|
}
|
|
|
|
// Return a Proxy that intercepts property access on the electron module.
|
|
// This is needed because electron's exports use non-configurable getters,
|
|
// so we cannot directly reassign module.BrowserWindow.
|
|
return new Proxy(result, {
|
|
get(target, prop, receiver) {
|
|
if (prop === 'BrowserWindow') return PatchedBrowserWindow;
|
|
if (prop === 'Menu') {
|
|
// Return a proxy for Menu that intercepts setApplicationMenu
|
|
const originalMenu = target.Menu;
|
|
return new Proxy(originalMenu, {
|
|
get(menuTarget, menuProp) {
|
|
if (menuProp === 'setApplicationMenu') return patchedSetApplicationMenu;
|
|
return Reflect.get(menuTarget, menuProp);
|
|
}
|
|
});
|
|
}
|
|
return Reflect.get(target, prop, receiver);
|
|
}
|
|
});
|
|
}
|
|
|
|
return result;
|
|
};
|