diff --git a/package.json b/package.json index b2384f68..10c03ffc 100644 --- a/package.json +++ b/package.json @@ -86,15 +86,7 @@ "mac": { "category": "public.app-category.utilities", "icon": "supercmd.icns", - "target": [ - { - "target": "default", - "arch": [ - "x64", - "arm64" - ] - } - ], + "target": "dmg", "hardenedRuntime": true, "notarize": { "teamId": "T7HT4U4666" diff --git a/src/main/main.ts b/src/main/main.ts index babc68b7..343efc97 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1814,13 +1814,16 @@ async function captureWindowManagementTargetWindow(): Promise { if (info?.id) { capturedWindowId = String(info.id); } - capturedWorkArea = normalizeWindowManagementArea(info?.workArea); - if (!capturedWorkArea) { - const { screen: electronScreen } = require('electron'); - const bounds = info?.bounds; - const normalizedBounds = bounds && Number.isFinite(bounds.x) && Number.isFinite(bounds.y) && - Number.isFinite(bounds.width) && Number.isFinite(bounds.height) && - bounds.width > 0 && bounds.height > 0 + const { screen: electronScreen } = require('electron'); + const bounds = info?.bounds; + const normalizedBounds = + bounds && + Number.isFinite(bounds.x) && + Number.isFinite(bounds.y) && + Number.isFinite(bounds.width) && + Number.isFinite(bounds.height) && + bounds.width > 0 && + bounds.height > 0 ? { x: Math.round(bounds.x), y: Math.round(bounds.y), @@ -1828,11 +1831,11 @@ async function captureWindowManagementTargetWindow(): Promise { height: Math.max(1, Math.round(bounds.height)), } : null; - const display = normalizedBounds - ? electronScreen.getDisplayMatching(normalizedBounds) - : electronScreen.getDisplayNearestPoint(electronScreen.getCursorScreenPoint()); - capturedWorkArea = normalizeWindowManagementArea(display?.workArea); - } + const display = normalizedBounds + ? electronScreen.getDisplayMatching(normalizedBounds) + : electronScreen.getDisplayNearestPoint(electronScreen.getCursorScreenPoint()); + capturedWorkArea = + normalizeWindowManagementDisplayWorkArea(display) || normalizeWindowManagementArea(info?.workArea); } catch (error) { console.warn('[WindowManager] Failed to capture target window:', error); return; @@ -2998,6 +3001,129 @@ function normalizeWindowManagementArea( }; } +type MacDockSettings = { + orientation: 'bottom' | 'left' | 'right'; + autohide: boolean; + reserve: number; +}; + +let cachedMacDockSettings: MacDockSettings | null | undefined; + +function readMacDockSettings(): MacDockSettings | null { + if (process.platform !== 'darwin') return null; + if (cachedMacDockSettings !== undefined) return cachedMacDockSettings; + + const readDefault = (key: string): string | null => { + try { + return String( + require('child_process').execFileSync('/usr/bin/defaults', ['read', 'com.apple.dock', key], { + encoding: 'utf8', + timeout: 400, + }) || '' + ).trim(); + } catch { + return null; + } + }; + + const rawOrientation = String(readDefault('orientation') || 'bottom').trim(); + const orientation: MacDockSettings['orientation'] = + rawOrientation === 'left' || rawOrientation === 'right' ? rawOrientation : 'bottom'; + const rawAutohide = String(readDefault('autohide') || '').trim().toLowerCase(); + const autohide = rawAutohide === '1' || rawAutohide === 'true' || rawAutohide === 'yes'; + const rawTileSize = readDefault('tilesize'); + const tileSize = rawTileSize == null ? NaN : Number(rawTileSize); + cachedMacDockSettings = { + orientation, + autohide, + reserve: Math.max(48, Math.round((Number.isFinite(tileSize) ? tileSize : 45) + 17)), + }; + return cachedMacDockSettings; +} + +function readMacDockFrame(): { x: number; y: number; width: number; height: number } | null { + if (process.platform !== 'darwin') return null; + try { + const raw = String( + require('child_process').execFileSync( + '/usr/bin/osascript', + ['-e', 'tell application "System Events" to tell process "Dock" to get {position, size} of list 1'], + { encoding: 'utf8', timeout: 400 } + ) || '' + ).trim(); + const [x, y, width, height] = raw + .split(',') + .map((value) => Number(String(value || '').trim())); + if (![x, y, width, height].every((value) => Number.isFinite(value))) return null; + if (width <= 0 || height <= 0) return null; + return { + x: Math.round(x), + y: Math.round(y), + width: Math.max(1, Math.round(width)), + height: Math.max(1, Math.round(height)), + }; + } catch { + return null; + } +} + +function isMacDockVisibleOnDisplay(bounds: { x: number; y: number; width: number; height: number }): boolean { + const dockFrame = readMacDockFrame(); + if (!dockFrame) return false; + const overlapWidth = Math.max( + 0, + Math.min(dockFrame.x + dockFrame.width, bounds.x + bounds.width) - Math.max(dockFrame.x, bounds.x) + ); + const overlapHeight = Math.max( + 0, + Math.min(dockFrame.y + dockFrame.height, bounds.y + bounds.height) - Math.max(dockFrame.y, bounds.y) + ); + return overlapWidth > 4 && overlapHeight > 4; +} + +function reserveAutoHiddenDockSpace( + area: { x: number; y: number; width: number; height: number }, + bounds: { x: number; y: number; width: number; height: number } | null +): { x: number; y: number; width: number; height: number } { + const dock = readMacDockSettings(); + if (!dock?.autohide || !bounds || !isMacDockVisibleOnDisplay(bounds)) return area; + + if (dock.orientation === 'left') { + const leftInset = area.x - bounds.x; + if (leftInset >= Math.floor(dock.reserve / 2)) return area; + return { + ...area, + x: area.x + dock.reserve, + width: Math.max(1, area.width - dock.reserve), + }; + } + + if (dock.orientation === 'right') { + const rightInset = bounds.x + bounds.width - (area.x + area.width); + if (rightInset >= Math.floor(dock.reserve / 2)) return area; + return { + ...area, + width: Math.max(1, area.width - dock.reserve), + }; + } + + const bottomInset = bounds.y + bounds.height - (area.y + area.height); + if (bottomInset >= Math.floor(dock.reserve / 2)) return area; + return { + ...area, + height: Math.max(1, area.height - dock.reserve), + }; +} + +function normalizeWindowManagementDisplayWorkArea( + display: { bounds?: { x?: number; y?: number; width?: number; height?: number }; workArea?: { x?: number; y?: number; width?: number; height?: number } } | null | undefined +): { x: number; y: number; width: number; height: number } | null { + const area = normalizeWindowManagementArea(display?.workArea); + const bounds = normalizeWindowManagementArea(display?.bounds); + if (!area) return null; + return reserveAutoHiddenDockSpace(area, bounds); +} + function getNativeWindowFineTuneAction(commandId: string): string | null { const normalized = String(commandId || '').trim(); if (!normalized.startsWith(WINDOW_MANAGEMENT_FINE_TUNE_COMMAND_PREFIX)) return null; @@ -3669,20 +3795,22 @@ async function executeWindowManagementFineTuneCommand( } windowManagementTargetWindowId = String(target.id); + const center = { + x: target.bounds.x + target.bounds.width / 2, + y: target.bounds.y + target.bounds.height / 2, + }; + const targetDisplay = require('electron').screen.getDisplayMatching(target.bounds); let area = + normalizeWindowManagementDisplayWorkArea(targetDisplay) || normalizeWindowManagementArea(target.workArea) || normalizeWindowManagementArea(windowManagementTargetWorkArea); if (!area) { - const center = { - x: target.bounds.x + target.bounds.width / 2, - y: target.bounds.y + target.bounds.height / 2, - }; - area = normalizeWindowManagementArea(screen.getDisplayNearestPoint(center)?.workArea); + area = normalizeWindowManagementDisplayWorkArea(screen.getDisplayNearestPoint(center)); } if (!area) { - area = normalizeWindowManagementArea( - screen.getDisplayNearestPoint(screen.getCursorScreenPoint())?.workArea || screen.getPrimaryDisplay()?.workArea - ); + area = + normalizeWindowManagementDisplayWorkArea(screen.getDisplayNearestPoint(screen.getCursorScreenPoint())) || + normalizeWindowManagementDisplayWorkArea(screen.getPrimaryDisplay()); } if (!area) return false; windowManagementTargetWorkArea = area; @@ -3747,20 +3875,22 @@ async function executeWindowManagementLayoutCommand( if (!target?.id || !target.bounds) return false; windowManagementTargetWindowId = String(target.id); + const center = { + x: target.bounds.x + target.bounds.width / 2, + y: target.bounds.y + target.bounds.height / 2, + }; + const targetDisplay = require('electron').screen.getDisplayMatching(target.bounds); let area = + normalizeWindowManagementDisplayWorkArea(targetDisplay) || normalizeWindowManagementArea(target.workArea) || normalizeWindowManagementArea(windowManagementTargetWorkArea); if (!area) { - const center = { - x: target.bounds.x + target.bounds.width / 2, - y: target.bounds.y + target.bounds.height / 2, - }; - area = normalizeWindowManagementArea(screen.getDisplayNearestPoint(center)?.workArea); + area = normalizeWindowManagementDisplayWorkArea(screen.getDisplayNearestPoint(center)); } if (!area) { - area = normalizeWindowManagementArea( - screen.getDisplayNearestPoint(screen.getCursorScreenPoint())?.workArea || screen.getPrimaryDisplay()?.workArea - ); + area = + normalizeWindowManagementDisplayWorkArea(screen.getDisplayNearestPoint(screen.getCursorScreenPoint())) || + normalizeWindowManagementDisplayWorkArea(screen.getPrimaryDisplay()); } if (!area) return false; windowManagementTargetWorkArea = area; @@ -16822,9 +16952,10 @@ if let tiff = image?.tiffRepresentation { const targetNode = snapshot.target; const target = targetNode ? toWindowManagementWindowFromNode(targetNode, true) : null; const { screen: electronScreen } = require('electron'); - let workArea = normalizeWindowManagementArea(targetNode?.workArea) || cloneWorkArea(windowManagementTargetWorkArea); const targetBounds = targetNode?.bounds; - if (!workArea && targetBounds) { + let workArea: { x: number; y: number; width: number; height: number } | null = null; + + if (targetBounds) { const normalizedBounds = Number.isFinite(targetBounds.x) && Number.isFinite(targetBounds.y) && @@ -16840,14 +16971,19 @@ if let tiff = image?.tiffRepresentation { } : null; if (normalizedBounds) { - workArea = normalizeWindowManagementArea( - electronScreen.getDisplayMatching(normalizedBounds)?.workArea + workArea = normalizeWindowManagementDisplayWorkArea( + electronScreen.getDisplayMatching(normalizedBounds) ); } } + + if (!workArea) { + workArea = normalizeWindowManagementArea(targetNode?.workArea) || cloneWorkArea(windowManagementTargetWorkArea); + } + if (!workArea) { const fallbackDisplay = electronScreen.getDisplayNearestPoint(electronScreen.getCursorScreenPoint()); - workArea = normalizeWindowManagementArea(fallbackDisplay?.workArea); + workArea = normalizeWindowManagementDisplayWorkArea(fallbackDisplay); } if (targetNode?.id) { windowManagementTargetWindowId = String(targetNode.id); @@ -16897,28 +17033,27 @@ if let tiff = image?.tiffRepresentation { const cursorPoint = screen.getCursorScreenPoint(); const activeDisplay = screen.getDisplayNearestPoint(cursorPoint); - return displays.map((display: any, index: number) => ({ - id: String(index + 1), - active: display.id === activeDisplay?.id, - screenId: String(display.id), - bounds: { - x: Number(display.bounds?.x || 0), - y: Number(display.bounds?.y || 0), - width: Number(display.bounds?.width || 0), - height: Number(display.bounds?.height || 0), - }, - workArea: { - x: Number(display.workArea?.x || display.bounds?.x || 0), - y: Number(display.workArea?.y || display.bounds?.y || 0), - width: Number(display.workArea?.width || display.bounds?.width || 0), - height: Number(display.workArea?.height || display.bounds?.height || 0), - }, - size: { - width: display.bounds.width, - height: display.bounds.height - }, - type: 'user' - })); + return displays.map((display: any, index: number) => { + const bounds = normalizeWindowManagementArea(display?.bounds) || { + x: 0, + y: 0, + width: 0, + height: 0, + }; + const workArea = normalizeWindowManagementDisplayWorkArea(display) || bounds; + return { + id: String(index + 1), + active: display.id === activeDisplay?.id, + screenId: String(display.id), + bounds, + workArea, + size: { + width: workArea.width, + height: workArea.height + }, + type: 'user' + }; + }); } catch (error) { console.error('Failed to get desktops:', error); return []; @@ -16936,7 +17071,10 @@ if let tiff = image?.tiffRepresentation { if (bounds === 'fullscreen') { const { screen: electronScreen } = require('electron'); const display = electronScreen.getDisplayNearestPoint(electronScreen.getCursorScreenPoint()); - const area = display?.workArea || electronScreen.getPrimaryDisplay().workArea; + const area = + normalizeWindowManagementDisplayWorkArea(display) || + normalizeWindowManagementDisplayWorkArea(electronScreen.getPrimaryDisplay()) || + normalizeWindowManagementArea(display?.workArea || electronScreen.getPrimaryDisplay().workArea); nextEntry = { id: normalizedId, x: Math.round(Number(area?.x || 0)), diff --git a/src/native/window-adjust.swift b/src/native/window-adjust.swift index 7cc756a8..31f08015 100644 --- a/src/native/window-adjust.swift +++ b/src/native/window-adjust.swift @@ -90,6 +90,82 @@ private func clamp(_ value: CGFloat, _ minValue: CGFloat, _ maxValue: CGFloat) - return max(minValue, min(maxValue, value)) } +private func dockSettings() -> (orientation: String, reserve: CGFloat, autohide: Bool) { + let defaults = UserDefaults(suiteName: "com.apple.dock") + let rawOrientation = defaults?.string(forKey: "orientation") ?? "bottom" + let orientation: String + switch rawOrientation { + case "left", "right": + orientation = rawOrientation + default: + orientation = "bottom" + } + + let tileSize = CGFloat((defaults?.object(forKey: "tilesize") as? NSNumber)?.doubleValue ?? 45) + return (orientation, max(48, tileSize + 17), defaults?.bool(forKey: "autohide") ?? false) +} + +private func dockListFrame() -> CGRect? { + guard let dockApp = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first else { + return nil + } + let dockElement = AXUIElementCreateApplication(dockApp.processIdentifier) + guard let childrenRaw = copyAttribute(dockElement, kAXChildrenAttribute as CFString), + let children = childrenRaw as? [AXUIElement] else { + return nil + } + + for child in children { + guard attributeString(child, kAXRoleAttribute as CFString) == kAXListRole as String, + let position = decodePoint(copyAttribute(child, kAXPositionAttribute as CFString)), + let size = decodeSize(copyAttribute(child, kAXSizeAttribute as CFString)), + size.width > 0, + size.height > 0 else { + continue + } + return CGRect(x: position.x, y: position.y, width: size.width, height: size.height) + } + return nil +} + +private func dockIsVisible(on displayBounds: CGRect) -> Bool { + guard let dockFrame = dockListFrame() else { return false } + let intersection = dockFrame.intersection(displayBounds) + guard !intersection.isNull, !intersection.isInfinite else { return false } + return intersection.width > 4 && intersection.height > 4 +} + +private func reserveAutoHiddenDockSpace( + in visibleArea: CGRect, + displayBounds: CGRect, + screenFrame: CGRect, + visibleFrame: CGRect +) -> CGRect { + let dock = dockSettings() + guard dock.autohide, dockIsVisible(on: displayBounds) else { return visibleArea } + + var area = visibleArea + switch dock.orientation { + case "left": + let leftInset = visibleFrame.minX - screenFrame.minX + if leftInset < 1 { + area.origin.x += dock.reserve + area.size.width = max(1, area.width - dock.reserve) + } + case "right": + let rightInset = screenFrame.maxX - visibleFrame.maxX + if rightInset < 1 { + area.size.width = max(1, area.width - dock.reserve) + } + default: + let bottomInset = visibleFrame.minY - screenFrame.minY + if bottomInset < 1 { + area.size.height = max(1, area.height - dock.reserve) + } + } + return area +} + private func copyAttribute(_ element: AXUIElement, _ attribute: CFString) -> CFTypeRef? { var value: CFTypeRef? let status = AXUIElementCopyAttributeValue(element, attribute, &value) @@ -375,21 +451,46 @@ private func bestDisplayId(for rect: CGRect) -> CGDirectDisplayID? { return bestDisplay } -private func displayBounds(for rect: CGRect) -> CGRect? { - if let displayId = bestDisplayId(for: rect) { - return CGDisplayBounds(displayId) +private func visibleBounds(forDisplayId displayId: CGDirectDisplayID) -> CGRect? { + let displayBounds = CGDisplayBounds(displayId) + guard displayBounds.width > 0, displayBounds.height > 0 else { return nil } + + let screen = NSScreen.screens.first { screen in + guard let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else { + return false + } + return CGDirectDisplayID(screenNumber.uint32Value) == displayId } - let mainDisplay = CGMainDisplayID() - let bounds = CGDisplayBounds(mainDisplay) - if bounds.width > 0, bounds.height > 0 { - return bounds + guard let screen = screen else { return displayBounds } + let screenFrame = screen.frame + let visibleFrame = screen.visibleFrame + + let visibleLeft = displayBounds.origin.x + (visibleFrame.origin.x - screenFrame.origin.x) + let visibleTop = displayBounds.origin.y + (screenFrame.origin.y + screenFrame.height - (visibleFrame.origin.y + visibleFrame.height)) + let visibleArea = CGRect( + x: visibleLeft, + y: visibleTop, + width: visibleFrame.width, + height: visibleFrame.height + ) + return reserveAutoHiddenDockSpace( + in: visibleArea, + displayBounds: displayBounds, + screenFrame: screenFrame, + visibleFrame: visibleFrame + ) +} + +private func visibleBounds(for rect: CGRect) -> CGRect? { + if let displayId = bestDisplayId(for: rect) { + return visibleBounds(forDisplayId: displayId) } - return nil + return visibleBounds(forDisplayId: CGMainDisplayID()) } private func screenVisibleArea(for frame: WindowFrame) -> CGRect? { let rect = CGRect(x: frame.x, y: frame.y, width: frame.width, height: frame.height) - return displayBounds(for: rect) + return visibleBounds(for: rect) } private func visibleArea(forWindowId windowId: Int) -> CGRect? { @@ -407,7 +508,7 @@ private func visibleArea(forWindowId windowId: Int) -> CGRect? { let windowBounds = CGRect(dictionaryRepresentation: boundsDict) else { return nil } - return displayBounds(for: windowBounds) + return visibleBounds(for: windowBounds) } private func adjustedFrame(_ base: WindowFrame, action: AdjustAction, forcedArea: CGRect?, preferredWindowId: Int?) -> WindowFrame { diff --git a/src/renderer/src/WindowManagerPanel.tsx b/src/renderer/src/WindowManagerPanel.tsx index 64053419..8f06ce62 100644 --- a/src/renderer/src/WindowManagerPanel.tsx +++ b/src/renderer/src/WindowManagerPanel.tsx @@ -537,6 +537,29 @@ function getHostMetrics(hostWindow: Window | null | undefined): ScreenArea { }; } +async function getActiveDesktopWorkAreaFallback(): Promise { + try { + const desktops = (await window.electron.getDesktops?.()) || []; + const activeDesktop = desktops.find((desktop: any) => desktop?.active) || desktops[0]; + const workArea = activeDesktop?.workArea; + const x = Number(workArea?.x); + const y = Number(workArea?.y); + const width = Number(workArea?.width); + const height = Number(workArea?.height); + if (![x, y, width, height].every((value) => Number.isFinite(value))) { + return null; + } + return { + left: Math.round(x), + top: Math.round(y), + width: Math.max(1, Math.round(width)), + height: Math.max(1, Math.round(height)), + }; + } catch { + return null; + } +} + function normalizeScreenArea(raw: any, fallback: ScreenArea): ScreenArea { const x = Number(raw?.x); const y = Number(raw?.y); @@ -1201,6 +1224,7 @@ function findBestTargetWindowForArea(windows: ManagedWindow[], area: ScreenArea) async function resolveWindowManagementExecutionContext(): Promise<{ target: ManagedWindow | null; area: ScreenArea }> { const hostArea = getHostMetrics(window); + const desktopFallbackArea = await getActiveDesktopWorkAreaFallback(); let target: ManagedWindow | null = null; let workAreaRaw: any = null; try { @@ -1220,7 +1244,7 @@ async function resolveWindowManagementExecutionContext(): Promise<{ target: Mana target = (await window.electron.getActiveWindow?.()) as ManagedWindow | null; } catch {} } - const area = normalizeScreenArea(workAreaRaw, hostArea); + const area = normalizeScreenArea(workAreaRaw, desktopFallbackArea || hostArea); if (target && !isManageableWindow(target)) { target = null; @@ -1448,6 +1472,7 @@ const WindowManagerPanel: React.FC = ({ show, portalTar if (contextInFlightRef.current) return contextInFlightRef.current; const promise = (async () => { + const desktopFallbackArea = await getActiveDesktopWorkAreaFallback(); let target: ManagedWindow | null = null; let workAreaRaw: any = null; try { @@ -1467,7 +1492,7 @@ const WindowManagerPanel: React.FC = ({ show, portalTar target = (await window.electron.getActiveWindow?.()) as ManagedWindow | null; } catch {} } - const area = normalizeScreenArea(workAreaRaw, hostArea); + const area = normalizeScreenArea(workAreaRaw, desktopFallbackArea || hostArea); if (target && !isManageableWindow(target)) { target = null; diff --git a/src/renderer/styles/index.css b/src/renderer/styles/index.css index 912c7dd6..67689bda 100644 --- a/src/renderer/styles/index.css +++ b/src/renderer/styles/index.css @@ -740,9 +740,11 @@ html { } .dark .cursor-prompt-surface { - background: rgba(7, 9, 13, 0.95); - border-color: rgba(var(--on-surface-rgb), 0.14); - box-shadow: none; + background: var(--settings-panel-bg); + border-color: var(--ui-panel-border); + box-shadow: + 0 12px 30px rgba(var(--backdrop-rgb), 0.18), + inset 0 1px 0 rgba(var(--on-surface-rgb), 0.08); } .cursor-prompt-drag-handle {