Compare commits

..

2 Commits

Author SHA1 Message Date
274852b895 Add macOS vibrancy support and related build scripts
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
- Introduced native macOS window effects with a new dynamic library for enhanced UI.
- Updated build scripts to compile the macOS effects library and integrate it into the application.
- Modified Electron context to apply macOS-specific styles and effects based on the platform.
- Enhanced CSS for macOS vibrancy support in the application layout.
- Updated configuration files to include new build steps and dependencies for macOS.
2026-08-02 02:44:24 +01:00
f90ef10b78 Refactor Windows installer scripts and enhance Electron navigation
- Added macros for uninstalling previous Farm Control installations in the NSIS installer.
- Updated the MSI wrapped configuration to include custom actions for handling previous installations.
- Improved the Electron navigation by modifying CSS classes for better drag-and-drop functionality.
- Enhanced build scripts to include the WixUtilExtension for improved MSI packaging.
2026-08-02 01:49:37 +01:00
19 changed files with 946 additions and 212 deletions

3
.gitignore vendored
View File

@ -32,3 +32,6 @@ stats.html
test-results.xml test-results.xml
dist/* dist/*
# Native macOS vibrancy (built by scripts/build-macos-effects.mjs)
src/bun/libMacWindowEffects.dylib

View File

@ -104,14 +104,45 @@
line-height: 0.7; line-height: 0.7;
} }
.electron-navigation-wrapper { .electron-navigation-wrapper.electrobun-webkit-app-region-drag {
-webkit-app-region: drag; user-select: none;
} }
.electron-navigation-wrapper li, /* Native macOS vibrancy — transparent web content over NSVisualEffectView */
.electron-navigation-wrapper button, html.macos-vibrancy {
.electron-navigation-wrapper .ant-tag { --macos-window-corner-radius: 15px; /* keep in sync with MAC_WINDOW_CORNER_RADIUS */
-webkit-app-region: no-drag; }
html.macos-vibrancy,
html.macos-vibrancy body,
html.macos-vibrancy #root {
background: transparent !important;
}
html.macos-vibrancy .ant-layout.ant-layout-has-sider,
html.macos-vibrancy .main-layout {
background: transparent !important;
}
html.macos-vibrancy .light-mode .ant-layout-sider {
background: rgba(255, 255, 255, 0.75) !important;
}
html.macos-vibrancy .dark-mode .ant-layout-sider {
background: rgba(10, 10, 10, 0.65) !important;
}
html.macos-vibrancy .light-mode .ant-layout-content {
background: transparent !important;
}
html.macos-vibrancy .dark-mode .ant-layout-content {
background: transparent !important;
}
html.macos-vibrancy .ant-layout-sider-light,
html.macos-vibrancy .electron-sider .ant-menu-light {
background: transparent !important;
} }
.electron-navigation { .electron-navigation {

View File

@ -42,6 +42,7 @@ export default {
}, },
copy: { copy: {
"dist/mainview": "views/mainview", "dist/mainview": "views/mainview",
"src/bun/libMacWindowEffects.dylib": "bun/libMacWindowEffects.dylib",
}, },
watch: ["scripts", "src"], watch: ["scripts", "src"],
watchIgnore: ["dist/**", "build/**", "app_dist/**"], watchIgnore: ["dist/**", "build/**", "app_dist/**"],

View File

@ -0,0 +1,402 @@
#import <Cocoa/Cocoa.h>
static NSString *const kElectrobunVibrancyViewIdentifier =
@"ElectrobunVibrancyView";
static NSString *const kElectrobunNativeDragViewIdentifier =
@"ElectrobunNativeDragView";
static NSString *const kElectrobunWindowBorderViewIdentifier =
@"ElectrobunWindowBorderView";
static CGFloat gWindowCornerRadius = 15.0;
@interface ElectrobunWindowBorderView : NSView
@property(nonatomic) CGFloat cornerRadius;
@end
@implementation ElectrobunWindowBorderView
@synthesize cornerRadius = _cornerRadius;
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
_cornerRadius = gWindowCornerRadius;
}
return self;
}
- (BOOL)isOpaque {
return NO;
}
- (BOOL)acceptsFirstMouse:(NSEvent *)event {
(void)event;
return NO;
}
- (NSView *)hitTest:(NSPoint)point {
(void)point;
return nil;
}
- (void)setFrame:(NSRect)frame {
[super setFrame:frame];
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)dirtyRect {
(void)dirtyRect;
NSRect bounds = [self bounds];
CGFloat radius = MAX(0.0, self.cornerRadius);
NSBezierPath *borderPath = [NSBezierPath
bezierPathWithRoundedRect:NSInsetRect(bounds, 0.5, 0.5)
xRadius:radius
yRadius:radius];
if (@available(macOS 10.14, *)) {
NSAppearance *appearance = [self effectiveAppearance];
BOOL isDark =
[[appearance bestMatchFromAppearancesWithNames:@[
NSAppearanceNameAqua, NSAppearanceNameDarkAqua
]] isEqualToString:NSAppearanceNameDarkAqua];
if (isDark) {
[[NSColor colorWithWhite:1.0 alpha:0.18] setStroke];
} else {
[[NSColor colorWithWhite:1.0 alpha:0.72] setStroke];
}
} else {
[[NSColor colorWithWhite:1.0 alpha:0.72] setStroke];
}
borderPath.lineWidth = 1.0;
[borderPath stroke];
}
@end
@interface ElectrobunNativeDragView : NSView
@end
@implementation ElectrobunNativeDragView
- (BOOL)isOpaque {
return NO;
}
- (void)drawRect:(NSRect)dirtyRect {
(void)dirtyRect;
}
- (void)mouseDown:(NSEvent *)event {
NSWindow *window = [self window];
if (window != nil && event != nil) {
[window performWindowDragWithEvent:event];
}
}
@end
static NSVisualEffectView *findVibrancyView(NSView *contentView) {
for (NSView *subview in [contentView subviews]) {
if ([subview isKindOfClass:[NSVisualEffectView class]] &&
[[subview identifier]
isEqualToString:kElectrobunVibrancyViewIdentifier]) {
return (NSVisualEffectView *)subview;
}
}
return nil;
}
static ElectrobunNativeDragView *findNativeDragView(NSView *contentView) {
for (NSView *subview in [contentView subviews]) {
if ([subview isKindOfClass:[ElectrobunNativeDragView class]] &&
[[subview identifier]
isEqualToString:kElectrobunNativeDragViewIdentifier]) {
return (ElectrobunNativeDragView *)subview;
}
}
return nil;
}
static ElectrobunWindowBorderView *findWindowBorderView(NSView *contentView) {
for (NSView *subview in [contentView subviews]) {
if ([subview isKindOfClass:[ElectrobunWindowBorderView class]] &&
[[subview identifier]
isEqualToString:kElectrobunWindowBorderViewIdentifier]) {
return (ElectrobunWindowBorderView *)subview;
}
}
return nil;
}
static void applyWindowCornerRadius(NSWindow *window, CGFloat radius) {
gWindowCornerRadius = MAX(0.0, radius);
NSView *contentView = [window contentView];
if (contentView == nil) {
return;
}
contentView.wantsLayer = YES;
contentView.layer.cornerRadius = gWindowCornerRadius;
contentView.layer.masksToBounds = YES;
NSVisualEffectView *effectView = findVibrancyView(contentView);
if (effectView != nil) {
effectView.wantsLayer = YES;
effectView.layer.cornerRadius = gWindowCornerRadius;
effectView.layer.masksToBounds = YES;
}
ElectrobunWindowBorderView *borderView = findWindowBorderView(contentView);
if (borderView != nil) {
borderView.cornerRadius = gWindowCornerRadius;
[borderView setNeedsDisplay:YES];
}
}
static void ensureWindowBorder(NSWindow *window) {
NSView *contentView = [window contentView];
if (contentView == nil) {
return;
}
ElectrobunWindowBorderView *borderView = findWindowBorderView(contentView);
if (borderView == nil) {
borderView = [[ElectrobunWindowBorderView alloc]
initWithFrame:[contentView bounds]];
[borderView setIdentifier:kElectrobunWindowBorderViewIdentifier];
[borderView
setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
}
borderView.cornerRadius = gWindowCornerRadius;
[borderView setFrame:[contentView bounds]];
if ([borderView superview] == nil) {
[contentView addSubview:borderView
positioned:NSWindowAbove
relativeTo:nil];
} else {
[borderView removeFromSuperview];
[contentView addSubview:borderView
positioned:NSWindowAbove
relativeTo:nil];
}
[borderView setNeedsDisplay:YES];
}
extern "C" bool enableWindowVibrancy(void *windowPtr, double cornerRadius) {
if (windowPtr == nullptr) {
return false;
}
__block BOOL success = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
NSWindow *window = (__bridge NSWindow *)windowPtr;
if (![window isKindOfClass:[NSWindow class]]) {
return;
}
applyWindowCornerRadius(window, (CGFloat)cornerRadius);
[window setOpaque:NO];
[window setBackgroundColor:[NSColor clearColor]];
[window setTitlebarAppearsTransparent:YES];
[window setHasShadow:YES];
NSView *contentView = [window contentView];
if (contentView == nil) {
return;
}
NSVisualEffectView *effectView = findVibrancyView(contentView);
if (effectView == nil) {
effectView = [[NSVisualEffectView alloc]
initWithFrame:[contentView bounds]];
[effectView setIdentifier:kElectrobunVibrancyViewIdentifier];
[effectView
setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
}
if (@available(macOS 10.14, *)) {
[effectView setMaterial:NSVisualEffectMaterialUnderWindowBackground];
} else {
[effectView setMaterial:NSVisualEffectMaterialSidebar];
}
[effectView setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
[effectView setState:NSVisualEffectStateActive];
if ([effectView superview] == nil) {
NSView *relativeView = [[contentView subviews] firstObject];
if (relativeView != nil) {
[contentView addSubview:effectView
positioned:NSWindowBelow
relativeTo:relativeView];
} else {
[contentView addSubview:effectView];
}
}
ensureWindowBorder(window);
applyWindowCornerRadius(window, gWindowCornerRadius);
[window invalidateShadow];
success = YES;
});
return success;
}
extern "C" bool setWindowCornerRadius(void *windowPtr, double cornerRadius) {
if (windowPtr == nullptr) {
return false;
}
__block BOOL success = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
NSWindow *window = (__bridge NSWindow *)windowPtr;
if (![window isKindOfClass:[NSWindow class]]) {
return;
}
applyWindowCornerRadius(window, (CGFloat)cornerRadius);
ensureWindowBorder(window);
[window invalidateShadow];
success = YES;
});
return success;
}
extern "C" bool ensureWindowShadow(void *windowPtr) {
if (windowPtr == nullptr) {
return false;
}
__block BOOL success = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
NSWindow *window = (__bridge NSWindow *)windowPtr;
if (![window isKindOfClass:[NSWindow class]]) {
return;
}
[window setHasShadow:YES];
[window invalidateShadow];
ensureWindowBorder(window);
applyWindowCornerRadius(window, gWindowCornerRadius);
success = YES;
});
return success;
}
extern "C" bool setWindowTrafficLightsPosition(void *windowPtr, double x,
double yFromTop) {
if (windowPtr == nullptr) {
return false;
}
__block BOOL success = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
NSWindow *window = (__bridge NSWindow *)windowPtr;
if (![window isKindOfClass:[NSWindow class]]) {
return;
}
NSButton *closeButton =
[window standardWindowButton:NSWindowCloseButton];
NSButton *minimizeButton =
[window standardWindowButton:NSWindowMiniaturizeButton];
NSButton *zoomButton = [window standardWindowButton:NSWindowZoomButton];
if (closeButton == nil || minimizeButton == nil || zoomButton == nil) {
return;
}
NSView *buttonContainer = [closeButton superview];
if (buttonContainer == nil) {
return;
}
CGFloat spacing = NSMinX(minimizeButton.frame) - NSMinX(closeButton.frame);
if (spacing <= 0) {
spacing = closeButton.frame.size.width + 6.0;
}
BOOL flipped = [buttonContainer isFlipped];
CGFloat targetY = yFromTop;
if (!flipped) {
targetY = buttonContainer.frame.size.height - yFromTop -
closeButton.frame.size.height;
}
targetY = MAX(0.0, targetY);
CGFloat currentX = x;
NSArray *buttons = @[ closeButton, minimizeButton, zoomButton ];
for (NSButton *button in buttons) {
[button setFrameOrigin:NSMakePoint(currentX, targetY)];
currentX += spacing;
}
[buttonContainer setNeedsLayout:YES];
[buttonContainer layoutSubtreeIfNeeded];
[window invalidateShadow];
success = YES;
});
return success;
}
extern "C" bool setNativeWindowDragRegion(void *windowPtr, double x,
double height) {
if (windowPtr == nullptr) {
return false;
}
__block BOOL success = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
NSWindow *window = (__bridge NSWindow *)windowPtr;
if (![window isKindOfClass:[NSWindow class]]) {
return;
}
NSView *contentView = [window contentView];
if (contentView == nil) {
return;
}
CGFloat dragX = MAX(0.0, x);
CGFloat dragHeight = MAX(0.0, height);
CGFloat dragWidth = MAX(0.0, contentView.bounds.size.width - dragX);
if (dragHeight <= 0.0 || dragWidth <= 0.0) {
return;
}
BOOL flipped = [contentView isFlipped];
CGFloat dragY = flipped ? 0.0 : contentView.bounds.size.height - dragHeight;
dragY = MAX(0.0, dragY);
ElectrobunNativeDragView *dragView = findNativeDragView(contentView);
if (dragView == nil) {
dragView = [[ElectrobunNativeDragView alloc] initWithFrame:NSZeroRect];
[dragView setIdentifier:kElectrobunNativeDragViewIdentifier];
}
[dragView setFrame:NSMakeRect(dragX, dragY, dragWidth, dragHeight)];
[dragView setAutoresizingMask:NSViewWidthSizable];
if ([dragView superview] == nil) {
[contentView addSubview:dragView
positioned:NSWindowAbove
relativeTo:nil];
}
success = YES;
});
return success;
}

View File

@ -94,7 +94,8 @@
"build:electron": "vite build && electron-builder", "build:electron": "vite build && electron-builder",
"build:cloudflare": "cross-env VITE_DEPLOY_TARGET=cloudflare vite build", "build:cloudflare": "cross-env VITE_DEPLOY_TARGET=cloudflare vite build",
"deploy": "npm run build:cloudflare && wrangler pages deploy --branch main", "deploy": "npm run build:cloudflare && wrangler pages deploy --branch main",
"postinstall": "bun scripts/patch-electrobun-src.mjs", "postinstall": "bun scripts/patch-electrobun-src.mjs && bun scripts/build-macos-effects.mjs",
"build:macos-effects": "bun scripts/build-macos-effects.mjs",
"clean": "bun scripts/clean-build.mjs" "clean": "bun scripts/clean-build.mjs"
}, },
"eslintConfig": { "eslintConfig": {

View File

@ -43,6 +43,7 @@ Function .onInit
${If} ${Silent} ${If} ${Silent}
SetAutoClose true SetAutoClose true
${EndIf} ${EndIf}
!insertmacro uninstallPreviousFarmControl
FunctionEnd FunctionEnd
Section "Farm Control" SecMain Section "Farm Control" SecMain

View File

@ -1,3 +1,33 @@
!macro quitFarmControl
DetailPrint "Stopping running Farm Control processes..."
ExecWait 'taskkill /F /IM launcher.exe /T' $R0
!macroend
!macro uninstallPreviousFarmControl
ClearErrors
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" "UninstallString"
StrCmp $R0 "" check_install_dir run_uninstall
check_install_dir:
ReadRegStr $R0 HKLM "Software\Tom Butcher\Farm Control" "InstallDir"
StrCmp $R0 "" check_default_dir
StrCpy $R0 "$R0\Uninstall.exe"
Goto run_uninstall
check_default_dir:
StrCpy $R0 "$PROGRAMFILES64\Farm Control\Uninstall.exe"
IfFileExists $R0 run_uninstall done_uninstall
run_uninstall:
IfFileExists $R0 0 done_uninstall
DetailPrint "Removing previous Farm Control installation..."
!insertmacro quitFarmControl
ExecWait '$R0 /S' $R1
DetailPrint "Previous installation removed (exit code: $R1)"
done_uninstall:
!macroend
!macro createDesktopShortcut !macro createDesktopShortcut
CreateShortCut "$DESKTOP\Farm Control.lnk" "$INSTDIR\bin\launcher.exe" CreateShortCut "$DESKTOP\Farm Control.lnk" "$INSTDIR\bin\launcher.exe"
!macroend !macroend

View File

@ -23,8 +23,42 @@
<Property Id="DISABLEADVTSHORTCUTS" Value="1" /> <Property Id="DISABLEADVTSHORTCUTS" Value="1" />
<Property Id="MSIINSTALLPERUSER" Value="1" /> <Property Id="MSIINSTALLPERUSER" Value="1" />
<Property Id="PREVIOUS_UNINSTALL_CMD">
<RegistrySearch
Id="PreviousUninstallCmdSearch"
Root="HKLM"
Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control"
Name="UninstallString"
Type="raw" />
</Property>
<Property Id="PREVIOUS_INSTALL_DIR">
<RegistrySearch
Id="PreviousInstallDirSearch"
Root="HKLM"
Key="Software\Tom Butcher\Farm Control"
Name="InstallDir"
Type="directory" />
</Property>
<Binary Id="WrappedExe" SourceFile="__SETUP_EXE__" /> <Binary Id="WrappedExe" SourceFile="__SETUP_EXE__" />
<CustomAction
Id="KillFarmControl"
BinaryKey="WixCA"
DllEntry="WixQuietExec"
Execute="deferred"
Impersonate="no"
Return="ignore" />
<CustomAction
Id="UninstallPrevious"
BinaryKey="WixCA"
DllEntry="WixQuietExec"
Execute="deferred"
Impersonate="no"
Return="ignore" />
<CustomAction <CustomAction
Id="RunInstaller" Id="RunInstaller"
BinaryKey="WrappedExe" BinaryKey="WrappedExe"
@ -45,8 +79,33 @@
<ComponentRef Id="EmptyComponent" /> <ComponentRef Id="EmptyComponent" />
</Feature> </Feature>
<CustomAction
Id="SetKillFarmControl"
Property="QtKillFarmControl"
Value="taskkill.exe /F /IM launcher.exe /T"
Execute="immediate" />
<CustomAction
Id="SetUninstallPreviousFromRegistry"
Property="QtUninstallPrevious"
Value="&quot;[PREVIOUS_UNINSTALL_CMD]&quot; /S"
Execute="immediate" />
<CustomAction
Id="SetUninstallPreviousFromInstallDir"
Property="QtUninstallPrevious"
Value="&quot;[PREVIOUS_INSTALL_DIR]Uninstall.exe&quot; /S"
Execute="immediate" />
<InstallExecuteSequence> <InstallExecuteSequence>
<Custom Action="RunInstaller" After="ProcessComponents">NOT Installed</Custom> <Custom Action="SetKillFarmControl" Before="KillFarmControl">NOT Installed</Custom>
<Custom Action="KillFarmControl" After="SetKillFarmControl">NOT Installed</Custom>
<Custom Action="SetUninstallPreviousFromRegistry" After="KillFarmControl">NOT Installed AND PREVIOUS_UNINSTALL_CMD</Custom>
<Custom Action="SetUninstallPreviousFromInstallDir" After="KillFarmControl">NOT Installed AND NOT PREVIOUS_UNINSTALL_CMD AND PREVIOUS_INSTALL_DIR</Custom>
<Custom Action="UninstallPrevious" After="SetUninstallPreviousFromRegistry">NOT Installed AND (PREVIOUS_UNINSTALL_CMD OR PREVIOUS_INSTALL_DIR)</Custom>
<Custom Action="UninstallPrevious" After="SetUninstallPreviousFromInstallDir">NOT Installed AND (PREVIOUS_UNINSTALL_CMD OR PREVIOUS_INSTALL_DIR)</Custom>
<Custom Action="RunInstaller" After="UninstallPrevious">NOT Installed AND (PREVIOUS_UNINSTALL_CMD OR PREVIOUS_INSTALL_DIR)</Custom>
<Custom Action="RunInstaller" After="KillFarmControl">NOT Installed AND NOT PREVIOUS_UNINSTALL_CMD AND NOT PREVIOUS_INSTALL_DIR</Custom>
</InstallExecuteSequence> </InstallExecuteSequence>
</Product> </Product>
</Wix> </Wix>

View File

@ -0,0 +1,48 @@
import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const srcFile = path.join(rootDir, "native/macos/window-effects.mm");
const outFile = path.join(rootDir, "src/bun/libMacWindowEffects.dylib");
function createPlaceholder() {
mkdirSync(path.dirname(outFile), { recursive: true });
writeFileSync(outFile, "");
console.log(`build-macos-effects: created placeholder dylib at ${outFile}`);
}
if (process.platform !== "darwin") {
createPlaceholder();
process.exit(0);
}
if (!existsSync(srcFile)) {
console.error(`build-macos-effects: missing source file ${srcFile}`);
process.exit(1);
}
mkdirSync(path.dirname(outFile), { recursive: true });
const result = spawnSync(
"xcrun",
[
"clang++",
"-dynamiclib",
"-fobjc-arc",
"-framework",
"Cocoa",
srcFile,
"-o",
outFile,
],
{ cwd: rootDir, stdio: "inherit" },
);
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
const { size } = statSync(outFile);
console.log(`build-macos-effects: built ${outFile} (${size} bytes)`);

View File

@ -103,12 +103,12 @@ $wixObj = Join-Path $workDir "project.wixobj"
Push-Location $workDir Push-Location $workDir
try { try {
& $wix.Candle "-arch" "x64" $projectWxs & $wix.Candle "-arch" "x64" "-ext" "WixUtilExtension" $projectWxs
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "candle.exe failed with exit code $LASTEXITCODE" throw "candle.exe failed with exit code $LASTEXITCODE"
} }
& $wix.Light "-out" $outputMsiPath "-spdb" "-sw1076" $wixObj & $wix.Light "-out" $outputMsiPath "-spdb" "-sw1076" "-ext" "WixUtilExtension" $wixObj
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "light.exe failed with exit code $LASTEXITCODE" throw "light.exe failed with exit code $LASTEXITCODE"
} }

View File

@ -3,34 +3,34 @@ import {
existsSync, existsSync,
mkdirSync, mkdirSync,
readFileSync, readFileSync,
writeFileSync, writeFileSync
} from "node:fs"; } from 'node:fs'
import path from "node:path"; import path from 'node:path'
import { fileURLToPath } from "node:url"; import { fileURLToPath } from 'node:url'
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const electrobunDir = path.join(rootDir, "node_modules/electrobun"); const electrobunDir = path.join(rootDir, 'node_modules/electrobun')
const sharedSrcDir = path.join(electrobunDir, "src/shared"); const sharedSrcDir = path.join(electrobunDir, 'src/shared')
const sharedDistDir = path.join(electrobunDir, "dist/api/shared"); const sharedDistDir = path.join(electrobunDir, 'dist/api/shared')
if (!existsSync(sharedDistDir)) { if (!existsSync(sharedDistDir)) {
console.error("patch-electrobun-src: electrobun dist/api/shared not found"); console.error('patch-electrobun-src: electrobun dist/api/shared not found')
process.exit(1); process.exit(1)
} }
mkdirSync(sharedSrcDir, { recursive: true }); mkdirSync(sharedSrcDir, { recursive: true })
for (const fileName of [ for (const fileName of [
"cef-version.ts", 'cef-version.ts',
"bun-version.ts", 'bun-version.ts',
"electrobun-version.ts", 'electrobun-version.ts',
"naming.ts", 'naming.ts',
"rpc.ts", 'rpc.ts'
]) { ]) {
const source = path.join(sharedDistDir, fileName); const source = path.join(sharedDistDir, fileName)
const destination = path.join(sharedSrcDir, fileName); const destination = path.join(sharedSrcDir, fileName)
if (existsSync(source)) { if (existsSync(source)) {
cpSync(source, destination); cpSync(source, destination)
} }
} }
@ -109,14 +109,14 @@ export function getPlatformOS(): SupportedOS {
export function getPlatformArch(): SupportedArch { export function getPlatformArch(): SupportedArch {
return ARCH; return ARCH;
} }
`; `
writeFileSync(path.join(sharedSrcDir, "platform.ts"), platformPatch); writeFileSync(path.join(sharedSrcDir, 'platform.ts'), platformPatch)
const templatesDir = path.join(electrobunDir, "src/cli/templates"); const templatesDir = path.join(electrobunDir, 'src/cli/templates')
mkdirSync(templatesDir, { recursive: true }); mkdirSync(templatesDir, { recursive: true })
writeFileSync( writeFileSync(
path.join(templatesDir, "embedded.ts"), path.join(templatesDir, 'embedded.ts'),
`export function getTemplateNames() { `export function getTemplateNames() {
return []; return [];
} }
@ -124,8 +124,8 @@ writeFileSync(
export function getTemplate(name: string) { export function getTemplate(name: string) {
throw new Error(\`Template not available in patched electrobun CLI: \${name}\`); throw new Error(\`Template not available in patched electrobun CLI: \${name}\`);
} }
`, `
); )
const RCEDIT_RESOLVER_FN = `function resolveProjectRceditPkgPath(projectRoot) { const RCEDIT_RESOLVER_FN = `function resolveProjectRceditPkgPath(projectRoot) {
const candidates = [ const candidates = [
@ -140,35 +140,41 @@ const RCEDIT_RESOLVER_FN = `function resolveProjectRceditPkgPath(projectRoot) {
throw new Error( throw new Error(
"rcedit not found under " + projectRoot + ". Install rcedit in the project (bun add -d rcedit).", "rcedit not found under " + projectRoot + ". Install rcedit in the project (bun add -d rcedit).",
); );
}`; }`
const cliPath = path.join(electrobunDir, "src/cli/index.ts"); const cliPath = path.join(electrobunDir, 'src/cli/index.ts')
let cliSource = readFileSync(cliPath, "utf8"); let cliSource = readFileSync(cliPath, 'utf8')
if (!cliSource.includes("HOST_ARCH")) { if (!cliSource.includes('HOST_ARCH')) {
cliSource = cliSource.replace( cliSource = cliSource.replace(
'import { OS, ARCH } from "../shared/platform";', 'import { OS, ARCH } from "../shared/platform";',
'import { OS, ARCH, HOST_ARCH } from "../shared/platform";', 'import { OS, ARCH, HOST_ARCH } from "../shared/platform";'
); )
cliSource = cliSource.replace( cliSource = cliSource.replace(
"const hostPaths = getPlatformPaths(OS, ARCH);", 'const hostPaths = getPlatformPaths(OS, ARCH);',
"const hostPaths = getPlatformPaths(OS, HOST_ARCH);", 'const hostPaths = getPlatformPaths(OS, HOST_ARCH);'
); )
} }
if (!cliSource.includes("toolPaths")) { if (!cliSource.includes('toolPaths')) {
cliSource = cliSource.replace( cliSource = cliSource.replace(
"const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);", 'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);',
"const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);\n\t\tconst toolPaths = getPlatformPaths(currentTarget.os, ARCH !== HOST_ARCH ? HOST_ARCH : ARCH);", 'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);\n\t\tconst toolPaths = getPlatformPaths(currentTarget.os, ARCH !== HOST_ARCH ? HOST_ARCH : ARCH);'
); )
cliSource = cliSource.replaceAll("const zstdPath = targetPaths.ZSTD;", "const zstdPath = toolPaths.ZSTD;"); cliSource = cliSource.replaceAll(
cliSource = cliSource.replaceAll("const bsdiffpath = targetPaths.BSDIFF;", "const bsdiffpath = toolPaths.BSDIFF;"); 'const zstdPath = targetPaths.ZSTD;',
'const zstdPath = toolPaths.ZSTD;'
)
cliSource = cliSource.replaceAll(
'const bsdiffpath = targetPaths.BSDIFF;',
'const bsdiffpath = toolPaths.BSDIFF;'
)
cliSource = cliSource.replace( cliSource = cliSource.replace(
"zigAsarCli = join(targetPaths.BSPATCH).replace(\"bspatch\", \"zig-asar\");", 'zigAsarCli = join(targetPaths.BSPATCH).replace("bspatch", "zig-asar");',
"zigAsarCli = join(toolPaths.BSPATCH).replace(\"bspatch\", \"zig-asar\");", 'zigAsarCli = join(toolPaths.BSPATCH).replace("bspatch", "zig-asar");'
); )
writeFileSync(cliPath, cliSource); writeFileSync(cliPath, cliSource)
cliSource = readFileSync(cliPath, "utf8"); cliSource = readFileSync(cliPath, 'utf8')
} }
if (!cliSource.includes("hookBunBinary")) { if (!cliSource.includes('hookBunBinary')) {
cliSource = cliSource.replace( cliSource = cliSource.replace(
`const hostPaths = getPlatformPaths(OS, HOST_ARCH); `const hostPaths = getPlatformPaths(OS, HOST_ARCH);
@ -178,41 +184,41 @@ if (!cliSource.includes("hookBunBinary")) {
? hostPaths.BUN_BINARY ? hostPaths.BUN_BINARY
: process.execPath; : process.execPath;
const result = Bun.spawnSync([hookBunBinary, hookScript],`, const result = Bun.spawnSync([hookBunBinary, hookScript],`
); )
cliSource = cliSource.replace( cliSource = cliSource.replace(
"console.error(\"Tried to run with bun at:\", hostPaths.BUN_BINARY);", 'console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);',
"console.error(\"Tried to run with bun at:\", hookBunBinary);", 'console.error("Tried to run with bun at:", hookBunBinary);'
); )
writeFileSync(cliPath, cliSource); writeFileSync(cliPath, cliSource)
cliSource = readFileSync(cliPath, "utf8"); cliSource = readFileSync(cliPath, 'utf8')
} }
if (!cliSource.includes("resolveProjectRceditPkgPath")) { if (!cliSource.includes('resolveProjectRceditPkgPath')) {
cliSource = cliSource.replaceAll( cliSource = cliSource.replaceAll(
"const rceditPkgPath = require.resolve(\"rcedit/package.json\");", 'const rceditPkgPath = require.resolve("rcedit/package.json");',
"const rceditPkgPath = resolveProjectRceditPkgPath(projectRoot);", 'const rceditPkgPath = resolveProjectRceditPkgPath(projectRoot);'
); )
cliSource = cliSource.replace( cliSource = cliSource.replace(
"function getPlatformPaths(", 'function getPlatformPaths(',
`${RCEDIT_RESOLVER_FN} `${RCEDIT_RESOLVER_FN}
function getPlatformPaths(`, function getPlatformPaths(`
); )
writeFileSync(cliPath, cliSource); writeFileSync(cliPath, cliSource)
cliSource = readFileSync(cliPath, "utf8"); cliSource = readFileSync(cliPath, 'utf8')
} else { } else {
cliSource = cliSource.replace( cliSource = cliSource.replace(
/function resolveProjectRceditPkgPath\(projectRoot\) \{[\s\S]*?\n\}/m, /function resolveProjectRceditPkgPath\(projectRoot\) \{[\s\S]*?\n\}/m,
RCEDIT_RESOLVER_FN, RCEDIT_RESOLVER_FN
); )
writeFileSync(cliPath, cliSource); writeFileSync(cliPath, cliSource)
} }
const electrobunCjsPath = path.join(electrobunDir, "bin/electrobun.cjs"); const electrobunCjsPath = path.join(electrobunDir, 'bin/electrobun.cjs')
let electrobunCjs = readFileSync(electrobunCjsPath, "utf8"); let electrobunCjs = readFileSync(electrobunCjsPath, 'utf8')
if (!electrobunCjs.includes("farmcontrol-use-patched-cli")) { if (!electrobunCjs.includes('farmcontrol-use-patched-cli')) {
electrobunCjs = electrobunCjs.replace( electrobunCjs = electrobunCjs.replace(
`async function main() { `async function main() {
try { try {
@ -259,19 +265,21 @@ if (!electrobunCjs.includes("farmcontrol-use-patched-cli")) {
const child = spawn(cliPath, args, { const child = spawn(cliPath, args, {
stdio: 'inherit', stdio: 'inherit',
cwd: process.cwd() cwd: process.cwd()
});`, });`
); )
writeFileSync(electrobunCjsPath, electrobunCjs); writeFileSync(electrobunCjsPath, electrobunCjs)
} }
const rceditSrc = path.join(rootDir, "node_modules/rcedit"); const rceditSrc = path.join(rootDir, 'node_modules/rcedit')
const rceditDest = path.join(electrobunDir, "node_modules/rcedit"); const rceditDest = path.join(electrobunDir, 'node_modules/rcedit')
if (existsSync(rceditSrc)) { if (existsSync(rceditSrc)) {
mkdirSync(path.join(electrobunDir, "node_modules"), { recursive: true }); mkdirSync(path.join(electrobunDir, 'node_modules'), { recursive: true })
cpSync(rceditSrc, rceditDest, { recursive: true, force: true }); cpSync(rceditSrc, rceditDest, { recursive: true, force: true })
} }
const markerPath = path.join(electrobunDir, ".farmcontrol-electrobun-patched"); const markerPath = path.join(electrobunDir, '.farmcontrol-electrobun-patched')
writeFileSync(markerPath, `patched-at=${new Date().toISOString()}\n`); writeFileSync(markerPath, `patched-at=${new Date().toISOString()}\n`)
console.log("patch-electrobun-src: electrobun src/shared ready for cross-arch builds"); console.log(
'patch-electrobun-src: electrobun src/shared ready for cross-arch builds'
)

View File

@ -36,6 +36,16 @@ if (buildEnv === "stable" && process.env.ELECTROBUN_OS === "macos") {
} }
} }
const buildMacosEffects = spawnSync(
"bun",
[path.join(rootDir, "scripts/build-macos-effects.mjs")],
{ cwd: rootDir, stdio: "inherit", env: process.env },
);
if (buildMacosEffects.status !== 0) {
process.exit(buildMacosEffects.status ?? 1);
}
const prepareIcons = spawnSync( const prepareIcons = spawnSync(
"bun", "bun",
[ [

View File

@ -26,6 +26,16 @@ if (patchResult.status !== 0) {
process.exit(patchResult.status ?? 1); process.exit(patchResult.status ?? 1);
} }
const macosEffectsResult = spawnSync(
"bun",
[path.join(rootDir, "scripts/build-macos-effects.mjs")],
{ cwd: rootDir, stdio: "inherit", env: process.env },
);
if (macosEffectsResult.status !== 0) {
process.exit(macosEffectsResult.status ?? 1);
}
const ensureCoreEnv = { const ensureCoreEnv = {
...process.env, ...process.env,
ELECTROBUN_TARGET_ARCH: targetArch, ELECTROBUN_TARGET_ARCH: targetArch,

View File

@ -30,7 +30,7 @@ const DashboardLayout = ({ children }) => {
<MessageProvider> <MessageProvider>
<Layout <Layout
style={{ height: 'var(--unit-100vh)' }} style={{ height: 'var(--unit-100vh)' }}
className={isDarkMode ? 'dark-mode' : 'light-mode'} className={`${isDarkMode ? 'dark-mode' : 'light-mode'} main-layout`}
> >
<DashboardNavigation /> <DashboardNavigation />
<Layout> <Layout>

View File

@ -199,134 +199,138 @@ const DashboardNavigation = () => {
</Text> </Text>
</Flex> </Flex>
)} )}
<Menu <div className='electrobun-webkit-app-region-no-drag'>
mode='horizontal' <Menu
className={isElectron ? 'electron-navigation' : null} mode='horizontal'
items={mainMenuItems} className={isElectron ? 'electron-navigation' : null}
style={{ items={mainMenuItems}
flexWrap: 'wrap', style={{
flexGrow: isMobile ? 0 : 1, flexWrap: 'wrap',
border: 0, flexGrow: isMobile ? 0 : 1,
width: isMobile ? '64px' : 'unset' border: 0,
}} width: isMobile ? '64px' : 'unset'
onClick={handleMainMenuClick} }}
selectedKeys={[selectedKey]} onClick={handleMainMenuClick}
overflowedIndicator={ selectedKeys={[selectedKey]}
<Button overflowedIndicator={
type='text'
icon={<MenuIcon />}
style={{ marginBottom: '4px' }}
/>
}
/>
{isMobile && <div style={{ flexGrow: 1 }} />}
<Flex
gap={'small'}
align='center'
style={{ marginTop: '-2px', marginRight: '6px' }}
>
<Space style={{ paddingTop: '2px', marginRight: '8px' }}>
<WebAppSwitcher />
<KeyboardShortcut
shortcut='alt+q'
hint='ALT Q'
onTrigger={() => showSpotlight()}
>
<Button <Button
icon={<SearchIcon />}
type='text' type='text'
style={{ marginTop: '4px' }} icon={<MenuIcon />}
onClick={() => showSpotlight()} style={{ marginBottom: '4px' }}
/> />
</KeyboardShortcut> }
<Badge />
count={unreadCount} </div>
size='small' {isMobile && <div style={{ flexGrow: 1 }} />}
offset={[-5, 8]} <div className='electrobun-webkit-app-region-no-drag'>
style={{ padding: 0, fontWeight: 600 }} <Flex
> gap={'small'}
align='center'
style={{ marginTop: '-2px', marginRight: '6px' }}
>
<Space style={{ paddingTop: '2px', marginRight: '8px' }}>
<WebAppSwitcher />
<KeyboardShortcut <KeyboardShortcut
shortcut='alt+n' shortcut='alt+q'
hint='ALT N' hint='ALT Q'
onTrigger={() => toggleNotificationCenter()} onTrigger={() => showSpotlight()}
> >
<Button <Button
icon={<BellIcon />} icon={<SearchIcon />}
type='text' type='text'
style={{ marginTop: '2px' }} style={{ marginTop: '4px' }}
onClick={() => toggleNotificationCenter()} onClick={() => showSpotlight()}
/> />
</KeyboardShortcut> </KeyboardShortcut>
</Badge> <Badge
</Space> count={unreadCount}
{import.meta.env.MODE === 'development' && ( size='small'
<Space> offset={[-5, 8]}
{apiServerState === 'connected' ? ( style={{ padding: 0, fontWeight: 600 }}
<Tooltip title='Connected to api server' arrow={false}>
<Tag
color='success'
style={{ marginRight: 0 }}
icon={<CloudIcon />}
/>
</Tooltip>
) : null}
{apiServerState === 'connecting' ? (
<Tooltip title='Connecting to api erver...' arrow={false}>
<Tag
color='warning'
style={{ marginRight: 0 }}
icon={<LoadingOutlined />}
/>
</Tooltip>
) : null}
{apiServerState === 'disconnected' ? (
<Tooltip title='Disconnected from api server' arrow={false}>
<Tag
color='error'
style={{ marginRight: 0 }}
icon={<CloudIcon />}
/>
</Tooltip>
) : null}
<Tooltip title='Developer' arrow={false}>
<Tag
color='yellow'
style={{ marginRight: 0 }}
icon={<DeveloperIcon />}
onClick={() => {
navigate('/dashboard/developer/sessionstorage')
}}
/>
</Tooltip>
</Space>
)}
{userProfile ? (
<Space>
<Popover
content={userPopoverContent}
placement='bottomRight'
trigger='hover'
open={userPopoverOpen}
onOpenChange={setUserPopoverOpen}
arrow={false}
> >
<Tag style={{ marginRight: 0 }} icon={<PersonIcon />}> <KeyboardShortcut
{!isMobile && (userProfile?.name || userProfile.username)} shortcut='alt+n'
</Tag> hint='ALT N'
</Popover> onTrigger={() => toggleNotificationCenter()}
>
<Button
icon={<BellIcon />}
type='text'
style={{ marginTop: '2px' }}
onClick={() => toggleNotificationCenter()}
/>
</KeyboardShortcut>
</Badge>
</Space> </Space>
) : null} {import.meta.env.MODE === 'development' && (
{isElectron && availableUpdate ? ( <Space>
<Tag {apiServerState === 'connected' ? (
icon={<CloudIcon />} <Tooltip title='Connected to api server' arrow={false}>
style={{ cursor: 'pointer', margin: '2px 0 0 0' }} <Tag
color='cyan' color='success'
onClick={() => checkForUpdates()} style={{ marginRight: 0 }}
> icon={<CloudIcon />}
Update Available />
</Tag> </Tooltip>
) : null} ) : null}
</Flex> {apiServerState === 'connecting' ? (
<Tooltip title='Connecting to api erver...' arrow={false}>
<Tag
color='warning'
style={{ marginRight: 0 }}
icon={<LoadingOutlined />}
/>
</Tooltip>
) : null}
{apiServerState === 'disconnected' ? (
<Tooltip title='Disconnected from api server' arrow={false}>
<Tag
color='error'
style={{ marginRight: 0 }}
icon={<CloudIcon />}
/>
</Tooltip>
) : null}
<Tooltip title='Developer' arrow={false}>
<Tag
color='yellow'
style={{ marginRight: 0 }}
icon={<DeveloperIcon />}
onClick={() => {
navigate('/dashboard/developer/sessionstorage')
}}
/>
</Tooltip>
</Space>
)}
{userProfile ? (
<Space>
<Popover
content={userPopoverContent}
placement='bottomRight'
trigger='hover'
open={userPopoverOpen}
onOpenChange={setUserPopoverOpen}
arrow={false}
>
<Tag style={{ marginRight: 0 }} icon={<PersonIcon />}>
{!isMobile && (userProfile?.name || userProfile.username)}
</Tag>
</Popover>
</Space>
) : null}
{isElectron && availableUpdate ? (
<Tag
icon={<CloudIcon />}
style={{ cursor: 'pointer', margin: '2px 0 0 0' }}
color='cyan'
onClick={() => checkForUpdates()}
>
Update Available
</Tag>
) : null}
</Flex>
</div>
{isOtherApp ? <DashboardWindowButtons /> : null} {isOtherApp ? <DashboardWindowButtons /> : null}
</Flex> </Flex>
) )
@ -335,7 +339,7 @@ const DashboardNavigation = () => {
<> <>
{isElectron ? ( {isElectron ? (
<Flex <Flex
className={`ant-menu-horizontal ant-menu-light${!notificationCenterVisible ? ' electron-navigation-wrapper' : ''}`} className={`ant-menu-horizontal ant-menu-light${!notificationCenterVisible ? ' electron-navigation-wrapper electrobun-webkit-app-region-drag' : ''}`}
style={{ lineHeight: '40px', padding: '0 2px 0 2px' }} style={{ lineHeight: '40px', padding: '0 2px 0 2px' }}
> >
{navigationContents} {navigationContents}

View File

@ -39,7 +39,10 @@ const DashboardWindowButtons = () => {
<div style={{ width: '80px' }} /> <div style={{ width: '80px' }} />
) : null ) : null
) : ( ) : (
<div style={{ width: '95px', marginRight: '2.5px' }}> <div
className='electrobun-webkit-app-region-no-drag'
style={{ width: '95px', marginRight: '2.5px' }}
>
<Flex style={{ position: 'relative', zIndex: 9999 }}> <Flex style={{ position: 'relative', zIndex: 9999 }}>
{maximizeButton} {maximizeButton}
{minimizeButton} {minimizeButton}

View File

@ -118,7 +118,12 @@ const ElectronProvider = ({ children }) => {
if (useElectrobun) { if (useElectrobun) {
desktopAPI.getOsInfo().then((info) => { desktopAPI.getOsInfo().then((info) => {
if (info?.platform) setPlatform(info.platform) if (info?.platform) {
setPlatform(info.platform)
if (info.platform === 'darwin') {
document.documentElement.classList.add('macos-vibrancy')
}
}
}) })
desktopAPI.getWindowState().then(applyWindowState) desktopAPI.getWindowState().then(applyWindowState)
@ -142,7 +147,12 @@ const ElectronProvider = ({ children }) => {
if (!ipcRenderer) return if (!ipcRenderer) return
ipcRenderer.invoke('os-info').then((info) => { ipcRenderer.invoke('os-info').then((info) => {
if (info?.platform) setPlatform(info.platform) if (info?.platform) {
setPlatform(info.platform)
if (info.platform === 'darwin') {
document.documentElement.classList.add('macos-vibrancy')
}
}
}) })
ipcRenderer.invoke('window-state').then(applyWindowState) ipcRenderer.invoke('window-state').then(applyWindowState)

View File

@ -0,0 +1,105 @@
import { dlopen, FFIType } from 'bun:ffi'
import { existsSync, statSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const MAC_TRAFFIC_LIGHTS_X = 14
const MAC_TRAFFIC_LIGHTS_Y = 12
export const MAC_WINDOW_CORNER_RADIUS = 15
function resolveDylibPath() {
const moduleDir = path.dirname(fileURLToPath(import.meta.url))
const candidates = [
path.join(moduleDir, '../bun/libMacWindowEffects.dylib'),
path.join(moduleDir, 'libMacWindowEffects.dylib')
]
for (const candidate of candidates) {
if (!existsSync(candidate)) {
continue
}
try {
if (statSync(candidate).size > 0) {
return candidate
}
} catch {
// Ignore unreadable paths and keep searching.
}
}
return null
}
export function applyMacOSWindowEffects(mainWindow) {
if (process.platform !== 'darwin' || !mainWindow?.ptr) {
return
}
const dylibPath = resolveDylibPath()
if (!dylibPath) {
console.warn(
'macOS vibrancy: native effects library not found; using transparent window only.'
)
return
}
try {
const lib = dlopen(dylibPath, {
enableWindowVibrancy: {
args: [FFIType.ptr, FFIType.f64],
returns: FFIType.bool
},
ensureWindowShadow: {
args: [FFIType.ptr],
returns: FFIType.bool
},
setWindowCornerRadius: {
args: [FFIType.ptr, FFIType.f64],
returns: FFIType.bool
},
setWindowTrafficLightsPosition: {
args: [FFIType.ptr, FFIType.f64, FFIType.f64],
returns: FFIType.bool
}
})
const vibrancyEnabled = lib.symbols.enableWindowVibrancy(
mainWindow.ptr,
MAC_WINDOW_CORNER_RADIUS
)
const shadowEnabled = lib.symbols.ensureWindowShadow(mainWindow.ptr)
const alignButtons = () =>
lib.symbols.setWindowTrafficLightsPosition(
mainWindow.ptr,
MAC_TRAFFIC_LIGHTS_X,
MAC_TRAFFIC_LIGHTS_Y
)
const refreshWindowChrome = () => {
lib.symbols.setWindowCornerRadius(
mainWindow.ptr,
MAC_WINDOW_CORNER_RADIUS
)
alignButtons()
}
const buttonsAlignedNow = alignButtons()
setTimeout(() => {
refreshWindowChrome()
}, 120)
mainWindow.on?.('resize', () => {
refreshWindowChrome()
})
console.log(
`macOS vibrancy applied (vibrancy=${vibrancyEnabled}, shadow=${shadowEnabled}, trafficLights=${buttonsAlignedNow}, cornerRadius=${MAC_WINDOW_CORNER_RADIUS})`
)
} catch (error) {
console.warn(
'macOS vibrancy: failed to apply native window effects:',
error
)
}
}

View File

@ -1,7 +1,10 @@
import Electrobun, { BrowserWindow, Updater, Utils } from "electrobun/bun"; import Electrobun, { BrowserWindow, Updater, Utils } from "electrobun/bun";
import { applyApplicationMenu, setupApplicationMenuEvents } from "./menu.js"; import { applyApplicationMenu, setupApplicationMenuEvents } from "./menu.js";
import { applyMacOSWindowEffects } from "./macos-window-effects.js";
import { sendToRenderer, setMessageSender } from "./notify.js"; import { sendToRenderer, setMessageSender } from "./notify.js";
const isMacOS = process.platform === "darwin";
const DEV_SERVER_PORT = 5780; const DEV_SERVER_PORT = 5780;
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`; const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
const PROTOCOL_PREFIX = "farmcontrol://"; const PROTOCOL_PREFIX = "farmcontrol://";
@ -127,6 +130,7 @@ export async function createMainWindow(rpc) {
rpc, rpc,
titleBarStyle: "hiddenInset", titleBarStyle: "hiddenInset",
trafficLightOffset: { x: 14, y: 12 }, trafficLightOffset: { x: 14, y: 12 },
...(isMacOS ? { transparent: true } : {}),
frame: { frame: {
width: 1200, width: 1200,
height: 800, height: 800,
@ -135,6 +139,10 @@ export async function createMainWindow(rpc) {
}, },
}); });
if (isMacOS) {
applyMacOSWindowEffects(mainWindow);
}
setupMainWindowMessaging(mainWindow); setupMainWindowMessaging(mainWindow);
applyApplicationMenu(); applyApplicationMenu();
setupApplicationMenuEvents({ setupApplicationMenuEvents({