├── resources ├── views │ └── .gitkeep └── stubs │ └── NativeAppServiceProvider.php.stub ├── .well-known └── funding-manifest-urls ├── src ├── Contracts │ ├── MenuItem.php │ ├── ProvidesPhpIni.php │ ├── QueueWorker.php │ ├── GlobalShortcut.php │ ├── PowerMonitor.php │ ├── WindowManager.php │ └── ChildProcess.php ├── Menu │ ├── Items │ │ ├── Separator.php │ │ ├── Label.php │ │ ├── Radio.php │ │ ├── Checkbox.php │ │ ├── Role.php │ │ ├── Link.php │ │ └── MenuItem.php │ ├── Menu.php │ └── MenuBuilder.php ├── Enums │ ├── PowerStatesEnum.php │ ├── SystemThemesEnum.php │ ├── SystemIdleStatesEnum.php │ ├── ThermalStatesEnum.php │ └── RolesEnum.php ├── MenuBar │ ├── PendingCreateMenuBar.php │ ├── MenuBarManager.php │ └── MenuBar.php ├── DataObjects │ └── Printer.php ├── Events │ ├── App │ │ ├── ApplicationBooted.php │ │ ├── OpenFile.php │ │ └── OpenedFromURL.php │ ├── MenuBar │ │ ├── MenuBarHidden.php │ │ ├── MenuBarShown.php │ │ ├── MenuBarCreated.php │ │ ├── MenuBarDroppedFiles.php │ │ ├── MenuBarRightClicked.php │ │ ├── MenuBarDoubleClicked.php │ │ └── MenuBarClicked.php │ ├── ChildProcess │ │ ├── ProcessSpawned.php │ │ ├── ErrorReceived.php │ │ ├── ProcessExited.php │ │ ├── StartupError.php │ │ └── MessageReceived.php │ ├── PowerMonitor │ │ ├── Shutdown.php │ │ ├── ScreenLocked.php │ │ ├── ScreenUnlocked.php │ │ ├── UserDidBecomeActive.php │ │ ├── UserDidResignActive.php │ │ ├── SpeedLimitChanged.php │ │ ├── PowerStateChanged.php │ │ └── ThermalStateChanged.php │ ├── AutoUpdater │ │ ├── CheckingForUpdate.php │ │ ├── Error.php │ │ ├── DownloadProgress.php │ │ ├── UpdateAvailable.php │ │ ├── UpdateCancelled.php │ │ ├── UpdateNotAvailable.php │ │ └── UpdateDownloaded.php │ ├── Windows │ │ ├── WindowClosed.php │ │ ├── WindowShown.php │ │ ├── WindowBlurred.php │ │ ├── WindowFocused.php │ │ ├── WindowHidden.php │ │ ├── WindowMaximized.php │ │ ├── WindowMinimized.php │ │ └── WindowResized.php │ ├── Menu │ │ └── MenuItemClicked.php │ ├── Settings │ │ └── SettingChanged.php │ ├── Notifications │ │ ├── NotificationClosed.php │ │ ├── NotificationClicked.php │ │ ├── NotificationReply.php │ │ └── NotificationActionClicked.php │ └── EventWatcher.php ├── Facades │ ├── ContextMenu.php │ ├── AutoUpdater.php │ ├── Process.php │ ├── Clipboard.php │ ├── Settings.php │ ├── Shell.php │ ├── Screen.php │ ├── Dock.php │ ├── Notification.php │ ├── Alert.php │ ├── MenuBar.php │ ├── QueueWorker.php │ ├── GlobalShortcut.php │ ├── System.php │ ├── PowerMonitor.php │ ├── App.php │ ├── Window.php │ ├── ChildProcess.php │ └── Menu.php ├── Concerns │ ├── HasUrl.php │ ├── DetectsWindowId.php │ ├── HasVibrancy.php │ ├── HasDimensions.php │ └── HasPositioner.php ├── Exceptions │ └── Handler.php ├── Windows │ ├── PendingOpenWindow.php │ ├── WindowManager.php │ └── Window.php ├── Http │ ├── Controllers │ │ ├── NativeAppBootedController.php │ │ ├── CreateSecurityCookieController.php │ │ └── DispatchEventFromAppController.php │ └── Middleware │ │ └── PreventRegularBrowserAccess.php ├── ContextMenu.php ├── Commands │ ├── LoadStartupConfigurationCommand.php │ ├── WipeDatabaseCommand.php │ ├── LoadPHPConfigurationCommand.php │ ├── FreshCommand.php │ ├── MigrateCommand.php │ ├── SeedDatabaseCommand.php │ └── DebugCommand.php ├── AutoUpdater.php ├── Process.php ├── Support │ └── Environment.php ├── Logging │ └── LogWatcher.php ├── Settings.php ├── Shell.php ├── GlobalShortcut.php ├── DTOs │ └── QueueConfig.php ├── Client │ └── Client.php ├── Dock.php ├── PowerMonitor.php ├── Screen.php ├── Fakes │ ├── QueueWorkerFake.php │ ├── GlobalShortcutFake.php │ ├── PowerMonitorFake.php │ ├── WindowManagerFake.php │ └── ChildProcessFake.php ├── Clipboard.php ├── QueueWorker.php ├── Alert.php ├── Notification.php ├── ProgressBar.php ├── App.php ├── Dialog.php ├── System.php ├── ChildProcess.php └── NativeServiceProvider.php ├── SECURITY.md ├── phpstan.neon ├── routes └── api.php ├── LICENSE.md ├── config ├── nativephp-internal.php └── nativephp.php ├── README.md ├── CONTRIBUTING.md └── composer.json /resources/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.well-known/funding-manifest-urls: -------------------------------------------------------------------------------- 1 | https://nativephp.com/funding.json 2 | -------------------------------------------------------------------------------- /src/Contracts/MenuItem.php: -------------------------------------------------------------------------------- 1 | create(); 10 | } 11 | 12 | protected function create(): void 13 | { 14 | $this->client->post('menu-bar/create', $this->toArray()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/DataObjects/Printer.php: -------------------------------------------------------------------------------- 1 | url = $url; 12 | 13 | return $this; 14 | } 15 | 16 | public function route(string $route, array $parameters = []): self 17 | { 18 | $this->url(route($route, $parameters)); 19 | 20 | return $this; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | reportable(function (\Throwable $e) { 12 | error_log("[NATIVE_EXCEPTION]: {$e->getMessage()} ({$e->getCode()}) in {$e->getFile()}:{$e->getLine()}"); 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Facades/Process.php: -------------------------------------------------------------------------------- 1 | open(); 10 | } 11 | 12 | protected function open(): void 13 | { 14 | $this->client->post('window/open', $this->toArray()); 15 | 16 | foreach ($this->afterOpenCallbacks as $cb) { 17 | $cb($this); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Contracts/PowerMonitor.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function all(): array; 21 | 22 | public function get(string $id): Window; 23 | } 24 | -------------------------------------------------------------------------------- /src/Menu/Items/Role.php: -------------------------------------------------------------------------------- 1 | $this->role->value, 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Http/Controllers/NativeAppBootedController.php: -------------------------------------------------------------------------------- 1 | boot(); 14 | 15 | event(new ApplicationBooted); 16 | 17 | return response()->json([ 18 | 'success' => true, 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | 3 | paths: 4 | - src/ 5 | - config/ 6 | # - tests/ 7 | 8 | 9 | # Level 9 is the highest level 10 | level: 5 11 | 12 | 13 | noEnvCallsOutsideOfConfig: false # Don't know why he doesn't consider our config/ directory as config 14 | 15 | ignoreErrors: 16 | - '#Class App\\Providers\\NativeAppServiceProvider not found#' 17 | - '#Class Native\\Laravel\\ChildProcess has an uninitialized readonly property#' 18 | 19 | 20 | excludePaths: 21 | - ./src/NativeServiceProvider.php 22 | -------------------------------------------------------------------------------- /src/ContextMenu.php: -------------------------------------------------------------------------------- 1 | toArray()['submenu']; 15 | 16 | $this->client->post('context', [ 17 | 'entries' => $items, 18 | ]); 19 | } 20 | 21 | public function remove() 22 | { 23 | $this->client->delete('context'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Commands/LoadStartupConfigurationCommand.php: -------------------------------------------------------------------------------- 1 | get('secret') !== config('native-php.secret'), 403); 12 | 13 | return redirect('/')->cookie(cookie( 14 | name: '_php_native', 15 | value: config('native-php.secret'), 16 | domain: 'localhost', 17 | httpOnly: true, 18 | )); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Commands/WipeDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | laravel))->rewriteDatabase(); 17 | 18 | return parent::handle(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Concerns/DetectsWindowId.php: -------------------------------------------------------------------------------- 1 | headers->get('Referer'); 12 | $currentUrl = URL::current(); 13 | 14 | // Return the _windowId query parameter from either the previous or current URL. 15 | $parsedUrl = parse_url($previousUrl ?? $currentUrl); 16 | parse_str($parsedUrl['query'] ?? '', $query); 17 | 18 | return $query['_windowId'] ?? null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/AutoUpdater.php: -------------------------------------------------------------------------------- 1 | client->post('auto-updater/check-for-updates'); 14 | } 15 | 16 | public function quitAndInstall(): void 17 | { 18 | $this->client->post('auto-updater/quit-and-install'); 19 | } 20 | 21 | public function downloadUpdate(): void 22 | { 23 | $this->client->post('auto-updater/download-update'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Events/MenuBar/MenuBarHidden.php: -------------------------------------------------------------------------------- 1 | fresh()->arch; 14 | } 15 | 16 | public function platform(): string 17 | { 18 | return $this->fresh()->platform; 19 | } 20 | 21 | public function uptime(): float 22 | { 23 | return $this->fresh()->uptime; 24 | } 25 | 26 | public function fresh(): object 27 | { 28 | return (object) $this->client->get('process')->json(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Events/App/OpenFile.php: -------------------------------------------------------------------------------- 1 | get('event'); 12 | $payload = $request->get('payload', []); 13 | 14 | if (class_exists($event)) { 15 | $event = new $event(...$payload); 16 | event($event); 17 | } else { 18 | event($event, $payload); 19 | } 20 | 21 | return response()->json([ 22 | 'success' => true, 23 | ]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Events/PowerMonitor/ScreenUnlocked.php: -------------------------------------------------------------------------------- 1 | PreventRegularBrowserAccess::class], function () { 11 | Route::post('_native/api/booted', NativeAppBootedController::class); 12 | Route::post('_native/api/events', DispatchEventFromAppController::class); 13 | })->withoutMiddleware(VerifyCsrfToken::class); 14 | 15 | Route::get('_native/api/cookie', CreateSecurityCookieController::class); 16 | -------------------------------------------------------------------------------- /src/Events/Windows/WindowResized.php: -------------------------------------------------------------------------------- 1 | limit = (int) $limit; 20 | } 21 | 22 | public function broadcastOn() 23 | { 24 | return [ 25 | new Channel('nativephp'), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Menu/Items/Link.php: -------------------------------------------------------------------------------- 1 | openInBrowser = $openInBrowser; 20 | 21 | return $this; 22 | } 23 | 24 | public function toArray(): array 25 | { 26 | return array_merge(parent::toArray(), [ 27 | 'url' => $this->url, 28 | 'openInBrowser' => $this->openInBrowser, 29 | ]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Enums/RolesEnum.php: -------------------------------------------------------------------------------- 1 | state = PowerStatesEnum::from($state); 21 | } 22 | 23 | public function broadcastOn() 24 | { 25 | return [ 26 | new Channel('nativephp'), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Facades/QueueWorker.php: -------------------------------------------------------------------------------- 1 | make(QueueWorkerFake::class), function ($fake) { 19 | static::swap($fake); 20 | }); 21 | } 22 | 23 | protected static function getFacadeAccessor(): string 24 | { 25 | self::clearResolvedInstance(QueueWorkerContract::class); 26 | 27 | return QueueWorkerContract::class; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Events/PowerMonitor/ThermalStateChanged.php: -------------------------------------------------------------------------------- 1 | state = ThermalStatesEnum::from($state); 21 | } 22 | 23 | public function broadcastOn() 24 | { 25 | return [ 26 | new Channel('nativephp'), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Facades/GlobalShortcut.php: -------------------------------------------------------------------------------- 1 | make(GlobalShortcutFake::class), function ($fake) { 20 | static::swap($fake); 21 | }); 22 | } 23 | 24 | protected static function getFacadeAccessor() 25 | { 26 | return GlobalShortcutContract::class; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Commands/LoadPHPConfigurationCommand.php: -------------------------------------------------------------------------------- 1 | phpIni(); 26 | } 27 | echo json_encode($phpIni); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Facades/System.php: -------------------------------------------------------------------------------- 1 | $message->level, 18 | 'message' => $message->message, 19 | 'context' => $message->context, 20 | ]; 21 | 22 | try { 23 | $this->client->post('debug/log', $payload); 24 | } catch (\Throwable $e) { 25 | // The server might not be running, or the connection could fail, hiding the error. 26 | } 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Facades/PowerMonitor.php: -------------------------------------------------------------------------------- 1 | make(PowerMonitorFake::class), function ($fake) { 20 | static::swap($fake); 21 | }); 22 | } 23 | 24 | protected static function getFacadeAccessor(): string 25 | { 26 | return PowerMonitorContract::class; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Contracts/ChildProcess.php: -------------------------------------------------------------------------------- 1 | client->post('settings/'.$key, [ 14 | 'value' => $value, 15 | ]); 16 | } 17 | 18 | public function get(string $key, $default = null): mixed 19 | { 20 | $response = $this->client->get('settings/'.$key)->json('value'); 21 | 22 | if ($response === null) { 23 | return $default instanceof \Closure ? $default() : $default; 24 | } 25 | 26 | return $response; 27 | } 28 | 29 | public function forget(string $key): void 30 | { 31 | $this->client->delete('settings/'.$key); 32 | } 33 | 34 | public function clear(): void 35 | { 36 | $this->client->delete('settings/'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Commands/FreshCommand.php: -------------------------------------------------------------------------------- 1 | laravel); 22 | 23 | $nativeServiceProvider->removeDatabase(); 24 | 25 | $nativeServiceProvider->rewriteDatabase(); 26 | 27 | return parent::handle(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Shell.php: -------------------------------------------------------------------------------- 1 | client->post('shell/show-item-in-folder', [ 14 | 'path' => $path, 15 | ]); 16 | } 17 | 18 | public function openFile(string $path): string 19 | { 20 | return $this->client->post('shell/open-item', [ 21 | 'path' => $path, 22 | ])->json('result'); 23 | } 24 | 25 | public function trashFile(string $path): void 26 | { 27 | $this->client->delete('shell/trash-item', [ 28 | 'path' => $path, 29 | ]); 30 | } 31 | 32 | public function openExternal(string $url): void 33 | { 34 | $this->client->post('shell/open-external', [ 35 | 'url' => $url, 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Commands/MigrateCommand.php: -------------------------------------------------------------------------------- 1 | signature = 'native:'.$this->signature; 20 | 21 | parent::__construct($migrator, $dispatcher); 22 | } 23 | 24 | public function handle() 25 | { 26 | (new NativeServiceProvider($this->laravel))->rewriteDatabase(); 27 | 28 | return parent::handle(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Events/AutoUpdater/UpdateAvailable.php: -------------------------------------------------------------------------------- 1 | path() === '_native/api/cookie') { 18 | return $next($request); 19 | } 20 | 21 | $cookie = $request->cookie('_php_native'); 22 | $header = $request->header('X-NativePHP-Secret'); 23 | 24 | if ($cookie && $cookie === config('nativephp-internal.secret')) { 25 | return $next($request); 26 | } 27 | 28 | if ($header && $header === config('nativephp-internal.secret')) { 29 | return $next($request); 30 | } 31 | 32 | return abort(403); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Events/AutoUpdater/UpdateDownloaded.php: -------------------------------------------------------------------------------- 1 | key = $key; 19 | 20 | return $this; 21 | } 22 | 23 | public function event(string $event): self 24 | { 25 | $this->event = $event; 26 | 27 | return $this; 28 | } 29 | 30 | public function register(): void 31 | { 32 | $this->client->post('global-shortcuts', [ 33 | 'key' => $this->key, 34 | 'event' => $this->event, 35 | ]); 36 | } 37 | 38 | public function unregister(): void 39 | { 40 | $this->client->delete('global-shortcuts', [ 41 | 'key' => $this->key, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DTOs/QueueConfig.php: -------------------------------------------------------------------------------- 1 | $queuesToConsume 9 | */ 10 | public function __construct( 11 | public readonly string $alias, 12 | public readonly array $queuesToConsume, 13 | public readonly int $memoryLimit, 14 | public readonly int $timeout, 15 | public readonly int|float $sleep, 16 | ) {} 17 | 18 | /** 19 | * @return array 20 | */ 21 | public static function fromConfigArray(array $config): array 22 | { 23 | return array_map( 24 | function (array|string $worker, string $alias) { 25 | return new self( 26 | $alias, 27 | $worker['queues'] ?? ['default'], 28 | $worker['memory_limit'] ?? 128, 29 | $worker['timeout'] ?? 60, 30 | $worker['sleep'] ?? 3, 31 | ); 32 | }, 33 | $config, 34 | array_keys($config), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Client/Client.php: -------------------------------------------------------------------------------- 1 | client = Http::asJson() 16 | ->baseUrl(config('nativephp-internal.api_url', '')) 17 | ->timeout(60 * 60) 18 | ->withHeaders([ 19 | 'X-NativePHP-Secret' => config('nativephp-internal.secret'), 20 | ]) 21 | ->asJson(); 22 | } 23 | 24 | public function get(string $endpoint, array|string|null $query = null): Response 25 | { 26 | return $this->client->get($endpoint, $query); 27 | } 28 | 29 | public function post(string $endpoint, array $data = []): Response 30 | { 31 | return $this->client->post($endpoint, $data); 32 | } 33 | 34 | public function delete(string $endpoint, array $data = []): Response 35 | { 36 | return $this->client->delete($endpoint, $data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) NativePHP 4 | 5 | Copyright (c) NativePHP 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/Concerns/HasVibrancy.php: -------------------------------------------------------------------------------- 1 | backgroundColor = $color; 16 | 17 | return $this; 18 | } 19 | 20 | public function transparent($value = true): self 21 | { 22 | $this->transparent = $value; 23 | if ($value === true) { 24 | $this->backgroundColor = '#00000000'; 25 | } 26 | 27 | return $this; 28 | } 29 | 30 | public function vibrancy(string $vibrancy): self 31 | { 32 | $this->vibrancy = $vibrancy; 33 | 34 | return $this; 35 | } 36 | 37 | public function lightVibrancy(): self 38 | { 39 | return $this->vibrancy('light'); 40 | } 41 | 42 | public function blendBackgroundBehindWindow(): self 43 | { 44 | $this->transparent(); 45 | 46 | return $this->vibrancy('under-window'); 47 | } 48 | 49 | public function darkVibrancy(): self 50 | { 51 | return $this->vibrancy('dark'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Events/EventWatcher.php: -------------------------------------------------------------------------------- 1 | broadcastOn(); 26 | 27 | // Only events dispatched on the nativephp channel 28 | if (! in_array('nativephp', $channels)) { 29 | return; 30 | } 31 | 32 | // Only post custom events to broadcasting endpoint 33 | if (str_starts_with($eventName, 'Native\\Laravel\\Events')) { 34 | return; 35 | } 36 | 37 | $this->client->post('broadcast', [ 38 | 'event' => "\\{$eventName}", 39 | 'payload' => $event, 40 | ]); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Dock.php: -------------------------------------------------------------------------------- 1 | toArray()['submenu']; 15 | 16 | $this->client->post('dock', [ 17 | 'items' => $items, 18 | ]); 19 | } 20 | 21 | public function show() 22 | { 23 | $this->client->post('dock/show'); 24 | } 25 | 26 | public function hide() 27 | { 28 | $this->client->post('dock/hide'); 29 | } 30 | 31 | public function icon(string $path) 32 | { 33 | $this->client->post('dock/icon', ['path' => $path]); 34 | } 35 | 36 | public function bounce(string $type = 'informational') 37 | { 38 | $this->client->post('dock/bounce', ['type' => $type]); 39 | } 40 | 41 | public function cancelBounce() 42 | { 43 | $this->client->post('dock/cancel-bounce'); 44 | } 45 | 46 | public function badge(?string $label = null): ?string 47 | { 48 | if (is_null($label)) { 49 | return $this->client->get('dock/badge'); 50 | } 51 | 52 | $this->client->post('dock/badge', ['label' => $label]); 53 | 54 | return null; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Commands/SeedDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | addArgument('class', mode: InputArgument::OPTIONAL, description: 'The class name of the root seeder'); 24 | $this->addOption('class', mode: InputOption::VALUE_OPTIONAL, description: 'The class name of the root seeder', default: 'Database\\Seeders\\DatabaseSeeder'); 25 | } 26 | 27 | public function handle() 28 | { 29 | // Add the database option here so it won't show up in `--help` 30 | $this->addOption('database', mode: InputOption::VALUE_REQUIRED, default: 'nativephp'); 31 | 32 | (new NativeServiceProvider($this->laravel))->rewriteDatabase(); 33 | 34 | return parent::handle(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/PowerMonitor.php: -------------------------------------------------------------------------------- 1 | client->get('power-monitor/get-system-idle-state', [ 17 | 'threshold' => $threshold, 18 | ])->json('result'); 19 | 20 | return SystemIdleStatesEnum::tryFrom($result) ?? SystemIdleStatesEnum::UNKNOWN; 21 | } 22 | 23 | public function getSystemIdleTime(): int 24 | { 25 | return $this->client->get('power-monitor/get-system-idle-time')->json('result'); 26 | } 27 | 28 | public function getCurrentThermalState(): ThermalStatesEnum 29 | { 30 | $result = $this->client->get('power-monitor/get-current-thermal-state')->json('result'); 31 | 32 | return ThermalStatesEnum::tryFrom($result) ?? ThermalStatesEnum::UNKNOWN; 33 | } 34 | 35 | public function isOnBatteryPower(): bool 36 | { 37 | return $this->client->get('power-monitor/is-on-battery-power')->json('result'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Screen.php: -------------------------------------------------------------------------------- 1 | client->get('screen/cursor-position')->json(); 14 | } 15 | 16 | public function displays(): array 17 | { 18 | return $this->client->get('screen/displays')->json('displays'); 19 | } 20 | 21 | public function primary(): array 22 | { 23 | return $this->client->get('screen/primary-display')->json('primaryDisplay'); 24 | } 25 | 26 | public function active(): array 27 | { 28 | return $this->client->get('screen/active')->json(); 29 | } 30 | 31 | /** 32 | * Returns the center of the screen where the mouse pointer is placed. 33 | * 34 | * @return array 35 | */ 36 | public function getCenterOfActiveScreen(): array 37 | { 38 | /* Navigate every screen and check for cursor position against the bounds of the screen. */ 39 | $activeScreen = $this->active(); 40 | 41 | $bounds = $activeScreen['bounds']; 42 | 43 | return [ 44 | 'x' => $bounds['x'] + $bounds['width'] / 2, 45 | 'y' => $bounds['y'] + $bounds['height'] / 2, 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Menu/Menu.php: -------------------------------------------------------------------------------- 1 | toArray()['submenu']; 23 | 24 | $this->client->post('menu', [ 25 | 'items' => $items, 26 | ]); 27 | } 28 | 29 | public function label(string $label): self 30 | { 31 | $this->label = $label; 32 | 33 | return $this; 34 | } 35 | 36 | public function add(MenuItem $item): self 37 | { 38 | $this->items[] = $item; 39 | 40 | return $this; 41 | } 42 | 43 | public function toArray(): array 44 | { 45 | $items = collect($this->items) 46 | ->map(fn (MenuItem $item) => $item->toArray()) 47 | ->toArray(); 48 | 49 | return [ 50 | 'label' => $this->label, 51 | 'submenu' => $items, 52 | ]; 53 | } 54 | 55 | public function jsonSerialize(): array 56 | { 57 | return $this->toArray(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Concerns/HasDimensions.php: -------------------------------------------------------------------------------- 1 | width = $width; 26 | 27 | return $this; 28 | } 29 | 30 | public function height($height): self 31 | { 32 | $this->height = $height; 33 | 34 | return $this; 35 | } 36 | 37 | public function minWidth($width): self 38 | { 39 | $this->minWidth = $width; 40 | 41 | return $this; 42 | } 43 | 44 | public function minHeight($height): self 45 | { 46 | $this->minHeight = $height; 47 | 48 | return $this; 49 | } 50 | 51 | public function maxWidth($width): self 52 | { 53 | $this->maxWidth = $width; 54 | 55 | return $this; 56 | } 57 | 58 | public function maxHeight($height): self 59 | { 60 | $this->maxHeight = $height; 61 | 62 | return $this; 63 | } 64 | 65 | public function position($x, $y): self 66 | { 67 | $this->x = $x; 68 | $this->y = $y; 69 | 70 | return $this; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Facades/Window.php: -------------------------------------------------------------------------------- 1 | make(WindowManagerFake::class), function ($fake) { 30 | static::swap($fake); 31 | }); 32 | } 33 | 34 | protected static function getFacadeAccessor() 35 | { 36 | return WindowManagerContract::class; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Facades/ChildProcess.php: -------------------------------------------------------------------------------- 1 | make(ChildProcessFake::class), function ($fake) { 24 | static::swap($fake); 25 | }); 26 | } 27 | 28 | protected static function getFacadeAccessor() 29 | { 30 | self::clearResolvedInstance(ChildProcessContract::class); 31 | 32 | return ChildProcessContract::class; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Fakes/QueueWorkerFake.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public array $ups = []; 16 | 17 | /** 18 | * @var array 19 | */ 20 | public array $downs = []; 21 | 22 | public function up(QueueConfig $config): void 23 | { 24 | $this->ups[] = $config; 25 | } 26 | 27 | public function down(string $alias): void 28 | { 29 | $this->downs[] = $alias; 30 | } 31 | 32 | public function assertUp(Closure $callback): void 33 | { 34 | $hit = empty( 35 | array_filter( 36 | $this->ups, 37 | fn (QueueConfig $up) => $callback($up) === true 38 | ) 39 | ) === false; 40 | 41 | PHPUnit::assertTrue($hit); 42 | } 43 | 44 | public function assertDown(string|Closure $alias): void 45 | { 46 | if (is_callable($alias) === false) { 47 | PHPUnit::assertContains($alias, $this->downs); 48 | 49 | return; 50 | } 51 | 52 | $hit = empty( 53 | array_filter( 54 | $this->downs, 55 | fn (string $down) => $alias($down) === true 56 | ) 57 | ) === false; 58 | 59 | PHPUnit::assertTrue($hit); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/MenuBar/MenuBarManager.php: -------------------------------------------------------------------------------- 1 | setClient($this->client); 15 | } 16 | 17 | public function show() 18 | { 19 | $this->client->post('menu-bar/show'); 20 | } 21 | 22 | public function hide() 23 | { 24 | $this->client->post('menu-bar/close'); 25 | } 26 | 27 | public function label(string $label) 28 | { 29 | $this->client->post('menu-bar/label', [ 30 | 'label' => $label, 31 | ]); 32 | } 33 | 34 | public function tooltip(string $tooltip) 35 | { 36 | $this->client->post('menu-bar/tooltip', [ 37 | 'tooltip' => $tooltip, 38 | ]); 39 | } 40 | 41 | public function icon(string $icon) 42 | { 43 | $this->client->post('menu-bar/icon', [ 44 | 'icon' => $icon, 45 | ]); 46 | } 47 | 48 | public function resize(int $width, int $height) 49 | { 50 | $this->client->post('menu-bar/resize', [ 51 | 'width' => $width, 52 | 'height' => $height, 53 | ]); 54 | } 55 | 56 | public function contextMenu(Menu $contextMenu) 57 | { 58 | $this->client->post('menu-bar/context-menu', [ 59 | 'contextMenu' => $contextMenu->toArray()['submenu'], 60 | ]); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Clipboard.php: -------------------------------------------------------------------------------- 1 | client->delete('clipboard'); 14 | } 15 | 16 | public function text($text = null): string 17 | { 18 | if (is_null($text)) { 19 | return $this->client->get('clipboard/text')->json('text'); 20 | } 21 | 22 | $this->client->post('clipboard/text', [ 23 | 'text' => $text, 24 | ]); 25 | 26 | return $text; 27 | } 28 | 29 | public function html($html = null): string 30 | { 31 | if (is_null($html)) { 32 | return $this->client->get('clipboard/html')->json('html'); 33 | } 34 | 35 | $this->client->post('clipboard/html', [ 36 | 'html' => $html, 37 | ]); 38 | 39 | return $html; 40 | } 41 | 42 | public function image($image = null): ?string 43 | { 44 | if (is_null($image)) { 45 | return $this->client->get('clipboard/image')->json('image'); 46 | } 47 | 48 | $dataUri = $image; 49 | 50 | if (is_string($image) && file_exists($image)) { 51 | $type = pathinfo($image, PATHINFO_EXTENSION); 52 | $data = file_get_contents($image); 53 | $dataUri = "data:image/{$type};base64,".base64_encode($data); 54 | } 55 | 56 | $this->client->post('clipboard/image', [ 57 | 'image' => $dataUri, 58 | ]); 59 | 60 | return $image; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/QueueWorker.php: -------------------------------------------------------------------------------- 1 | has("nativephp.queue_workers.{$config}")) { 18 | $config = QueueConfig::fromConfigArray([ 19 | $config => config("nativephp.queue_workers.{$config}"), 20 | ])[0]; 21 | } 22 | 23 | if (! $config instanceof QueueConfig) { 24 | throw new \InvalidArgumentException("Invalid queue configuration alias [$config]"); 25 | } 26 | 27 | $command = app()->isLocal() 28 | ? 'queue:listen' 29 | : 'queue:work'; 30 | 31 | $this->childProcess->artisan( 32 | [ 33 | $command, 34 | "--name={$config->alias}", 35 | '--queue='.implode(',', $config->queuesToConsume), 36 | "--memory={$config->memoryLimit}", 37 | "--timeout={$config->timeout}", 38 | "--sleep={$config->sleep}", 39 | ], 40 | 'queue_'.$config->alias, 41 | persistent: true, 42 | iniSettings: [ 43 | 'memory_limit' => "{$config->memoryLimit}M", 44 | ] 45 | ); 46 | } 47 | 48 | public function down(string $alias): void 49 | { 50 | $this->childProcess->stop('queue_'.$alias); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Concerns/HasPositioner.php: -------------------------------------------------------------------------------- 1 | windowPosition = $position; 12 | 13 | return $this; 14 | } 15 | 16 | public function trayLeft(): self 17 | { 18 | return $this->windowPosition('trayLeft'); 19 | } 20 | 21 | public function trayBottomLeft(): self 22 | { 23 | return $this->windowPosition('trayBottomLeft'); 24 | } 25 | 26 | public function trayRight(): self 27 | { 28 | return $this->windowPosition('trayRight'); 29 | } 30 | 31 | public function trayBottomRight(): self 32 | { 33 | return $this->windowPosition('trayBottomRight'); 34 | } 35 | 36 | public function trayCenter(): self 37 | { 38 | return $this->windowPosition('trayCenter'); 39 | } 40 | 41 | public function trayBottomCenter(): self 42 | { 43 | return $this->windowPosition('trayBottomCenter'); 44 | } 45 | 46 | public function topLeft(): self 47 | { 48 | return $this->windowPosition('topLeft'); 49 | } 50 | 51 | public function topRight(): self 52 | { 53 | return $this->windowPosition('topRight'); 54 | } 55 | 56 | public function bottomLeft(): self 57 | { 58 | return $this->windowPosition('bottomLeft'); 59 | } 60 | 61 | public function bottomRight(): self 62 | { 63 | return $this->windowPosition('bottomRight'); 64 | } 65 | 66 | public function topCenter(): self 67 | { 68 | return $this->windowPosition('topCenter'); 69 | } 70 | 71 | public function bottomCenter(): self 72 | { 73 | return $this->windowPosition('bottomCenter'); 74 | } 75 | 76 | public function leftCenter(): self 77 | { 78 | return $this->windowPosition('leftCenter'); 79 | } 80 | 81 | public function rightCenter(): self 82 | { 83 | return $this->windowPosition('rightCenter'); 84 | } 85 | 86 | public function center(): self 87 | { 88 | return $this->windowPosition('center'); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Alert.php: -------------------------------------------------------------------------------- 1 | type = $type; 31 | 32 | return $this; 33 | } 34 | 35 | public function title(string $title): self 36 | { 37 | $this->title = $title; 38 | 39 | return $this; 40 | } 41 | 42 | public function detail(string $detail): self 43 | { 44 | $this->detail = $detail; 45 | 46 | return $this; 47 | } 48 | 49 | public function buttons(array $buttons): self 50 | { 51 | $this->buttons = $buttons; 52 | 53 | return $this; 54 | } 55 | 56 | public function defaultId(int $defaultId): self 57 | { 58 | $this->defaultId = $defaultId; 59 | 60 | return $this; 61 | } 62 | 63 | public function cancelId(int $cancelId): self 64 | { 65 | $this->cancelId = $cancelId; 66 | 67 | return $this; 68 | } 69 | 70 | public function show(string $message): int 71 | { 72 | $response = $this->client->post('alert/message', [ 73 | 'message' => $message, 74 | 'type' => $this->type, 75 | 'title' => $this->title, 76 | 'detail' => $this->detail, 77 | 'buttons' => $this->buttons, 78 | 'defaultId' => $this->defaultId, 79 | 'cancelId' => $this->cancelId, 80 | ]); 81 | 82 | return (int) $response->json('result'); 83 | } 84 | 85 | public function error(string $title, string $message): bool 86 | { 87 | $response = $this->client->post('alert/error', [ 88 | 'title' => $title, 89 | 'message' => $message, 90 | ]); 91 | 92 | return (bool) $response->json('result'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Facades/Menu.php: -------------------------------------------------------------------------------- 1 | title = config('app.name'); 26 | } 27 | 28 | public static function new() 29 | { 30 | return new static(new Client); 31 | } 32 | 33 | public function reference(string $reference): self 34 | { 35 | $this->reference = $reference; 36 | 37 | return $this; 38 | } 39 | 40 | public function title(string $title): self 41 | { 42 | $this->title = $title; 43 | 44 | return $this; 45 | } 46 | 47 | public function event(string $event): self 48 | { 49 | $this->event = $event; 50 | 51 | return $this; 52 | } 53 | 54 | public function hasReply(string $placeholder = ''): self 55 | { 56 | $this->hasReply = true; 57 | $this->replyPlaceholder = $placeholder; 58 | 59 | return $this; 60 | } 61 | 62 | public function addAction(string $label): self 63 | { 64 | $this->actions[] = $label; 65 | 66 | return $this; 67 | } 68 | 69 | public function message(string $body): self 70 | { 71 | $this->body = $body; 72 | 73 | return $this; 74 | } 75 | 76 | public function show(): self 77 | { 78 | $response = $this->client->post('notification', [ 79 | 'reference' => $this->reference, 80 | 'title' => $this->title, 81 | 'body' => $this->body, 82 | 'event' => $this->event, 83 | 'hasReply' => $this->hasReply, 84 | 'replyPlaceholder' => $this->replyPlaceholder, 85 | 'actions' => array_map(fn (string $label) => [ 86 | 'type' => 'button', 87 | 'text' => $label, 88 | ], $this->actions), 89 | ]); 90 | 91 | $this->reference = $response->json('reference'); 92 | 93 | return $this; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /config/nativephp-internal.php: -------------------------------------------------------------------------------- 1 | env('NATIVEPHP_RUNNING', false), 10 | 11 | /** 12 | * The path to the NativePHP storage directory. This is used to store 13 | * uploaded files and other data. 14 | */ 15 | 'storage_path' => env('NATIVEPHP_STORAGE_PATH'), 16 | 17 | /** 18 | * The path to the NativePHP database directory. This is used to store 19 | * the SQLite database file. 20 | */ 21 | 'database_path' => env('NATIVEPHP_DATABASE_PATH'), 22 | 23 | /** 24 | * The secret key used to communicate with the NativePHP API. 25 | */ 26 | 'secret' => env('NATIVEPHP_SECRET'), 27 | 28 | /** 29 | * The URL to the NativePHP API. 30 | */ 31 | 'api_url' => env('NATIVEPHP_API_URL', 'http://localhost:4000/api/'), 32 | 33 | /** 34 | * Configuration for the Zephpyr API. 35 | */ 36 | 'zephpyr' => [ 37 | 'host' => env('ZEPHPYR_HOST', 'https://zephpyr.com'), 38 | 'token' => env('ZEPHPYR_TOKEN'), 39 | 'key' => env('ZEPHPYR_KEY'), 40 | ], 41 | 42 | /** 43 | * The credentials to use Apples Notarization service. 44 | */ 45 | 'notarization' => [ 46 | 'apple_id' => env('NATIVEPHP_APPLE_ID'), 47 | 'apple_id_pass' => env('NATIVEPHP_APPLE_ID_PASS'), 48 | 'apple_team_id' => env('NATIVEPHP_APPLE_TEAM_ID'), 49 | ], 50 | 51 | /** 52 | * The credentials to use Azure Trusted Signing service. 53 | */ 54 | 'azure_trusted_signing' => [ 55 | 'tenant_id' => env('AZURE_TENANT_ID'), 56 | 'client_id' => env('AZURE_CLIENT_ID'), 57 | 'client_secret' => env('AZURE_CLIENT_SECRET'), 58 | 'publisher_name' => env('NATIVEPHP_AZURE_PUBLISHER_NAME'), 59 | 'endpoint' => env('NATIVEPHP_AZURE_ENDPOINT'), 60 | 'certificate_profile_name' => env('NATIVEPHP_AZURE_CERTIFICATE_PROFILE_NAME'), 61 | 'code_signing_account_name' => env('NATIVEPHP_AZURE_CODE_SIGNING_ACCOUNT_NAME'), 62 | ], 63 | 64 | /** 65 | * The binary path of PHP for NativePHP to use at build. 66 | */ 67 | 'php_binary_path' => env('NATIVEPHP_PHP_BINARY_PATH'), 68 | ]; 69 | -------------------------------------------------------------------------------- /src/ProgressBar.php: -------------------------------------------------------------------------------- 1 | lastWriteTime = microtime(true); 29 | $this->setProgress(0); 30 | } 31 | 32 | public function advance($step = 1) 33 | { 34 | $this->setProgress($this->step + $step); 35 | } 36 | 37 | public function setProgress(int $step) 38 | { 39 | if ($this->maxSteps && $step > $this->maxSteps) { 40 | $this->maxSteps = $step; 41 | } elseif ($step < 0) { 42 | $step = 0; 43 | } 44 | 45 | $redrawFreq = 1; 46 | $prevPeriod = (int) ($this->step / $redrawFreq); 47 | $currPeriod = (int) ($step / $redrawFreq); 48 | 49 | $this->step = $step; 50 | $this->percent = $this->maxSteps ? (float) $this->step / $this->maxSteps : 0; 51 | $timeInterval = microtime(true) - $this->lastWriteTime; 52 | 53 | // Draw regardless of other limits 54 | if ($this->maxSteps === $step) { 55 | $this->display(); 56 | 57 | return; 58 | } 59 | 60 | // Throttling 61 | if ($timeInterval < $this->minSecondsBetweenRedraws) { 62 | return; 63 | } 64 | 65 | // Draw each step period, but not too late 66 | if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) { 67 | $this->display(); 68 | } 69 | } 70 | 71 | public function finish() 72 | { 73 | $this->client->post('progress-bar/update', [ 74 | 'percent' => -1, 75 | ]); 76 | } 77 | 78 | public function display() 79 | { 80 | $this->lastWriteTime = microtime(true); 81 | 82 | $this->client->post('progress-bar/update', [ 83 | 'percent' => $this->percent, 84 | ]); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Fakes/GlobalShortcutFake.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public array $keys = []; 15 | 16 | /** 17 | * @var array 18 | */ 19 | public array $events = []; 20 | 21 | public int $registeredCount = 0; 22 | 23 | public int $unregisteredCount = 0; 24 | 25 | public function key(string $key): self 26 | { 27 | $this->keys[] = $key; 28 | 29 | return $this; 30 | } 31 | 32 | public function event(string $event): self 33 | { 34 | $this->events[] = $event; 35 | 36 | return $this; 37 | } 38 | 39 | public function register(): void 40 | { 41 | $this->registeredCount++; 42 | } 43 | 44 | public function unregister(): void 45 | { 46 | $this->unregisteredCount++; 47 | } 48 | 49 | /** 50 | * @param string|Closure(string): bool $key 51 | */ 52 | public function assertKey(string|Closure $key): void 53 | { 54 | if (is_callable($key) === false) { 55 | PHPUnit::assertContains($key, $this->keys); 56 | 57 | return; 58 | } 59 | 60 | $hit = empty( 61 | array_filter( 62 | $this->keys, 63 | fn (string $keyIteration) => $key($keyIteration) === true 64 | ) 65 | ) === false; 66 | 67 | PHPUnit::assertTrue($hit); 68 | } 69 | 70 | /** 71 | * @param string|Closure(string): bool $event 72 | */ 73 | public function assertEvent(string|Closure $event): void 74 | { 75 | if (is_callable($event) === false) { 76 | PHPUnit::assertContains($event, $this->events); 77 | 78 | return; 79 | } 80 | 81 | $hit = empty( 82 | array_filter( 83 | $this->events, 84 | fn (string $eventIteration) => $event($eventIteration) === true 85 | ) 86 | ) === false; 87 | 88 | PHPUnit::assertTrue($hit); 89 | } 90 | 91 | public function assertRegisteredCount(int $count): void 92 | { 93 | PHPUnit::assertSame($count, $this->registeredCount); 94 | } 95 | 96 | public function assertUnregisteredCount(int $count): void 97 | { 98 | PHPUnit::assertSame($count, $this->unregisteredCount); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Fakes/PowerMonitorFake.php: -------------------------------------------------------------------------------- 1 | getSystemIdleStateCount++; 26 | 27 | $this->getSystemIdleStateCalls[] = $threshold; 28 | 29 | return SystemIdleStatesEnum::UNKNOWN; 30 | } 31 | 32 | public function getSystemIdleTime(): int 33 | { 34 | $this->getSystemIdleTimeCount++; 35 | 36 | return 0; 37 | } 38 | 39 | public function getCurrentThermalState(): ThermalStatesEnum 40 | { 41 | $this->getCurrentThermalStateCount++; 42 | 43 | return ThermalStatesEnum::UNKNOWN; 44 | } 45 | 46 | public function isOnBatteryPower(): bool 47 | { 48 | $this->isOnBatteryPowerCount++; 49 | 50 | return false; 51 | } 52 | 53 | /** 54 | * @param int|Closure(int): bool $key 55 | */ 56 | public function assertGetSystemIdleState(int|Closure $key): void 57 | { 58 | if (is_callable($key) === false) { 59 | PHPUnit::assertContains($key, $this->getSystemIdleStateCalls); 60 | 61 | return; 62 | } 63 | 64 | $hit = empty( 65 | array_filter( 66 | $this->getSystemIdleStateCalls, 67 | fn (int $keyIteration) => $key($keyIteration) === true 68 | ) 69 | ) === false; 70 | 71 | PHPUnit::assertTrue($hit); 72 | } 73 | 74 | public function assertGetSystemIdleStateCount(int $count): void 75 | { 76 | PHPUnit::assertSame($count, $this->getSystemIdleStateCount); 77 | } 78 | 79 | public function assertGetSystemIdleTimeCount(int $count): void 80 | { 81 | PHPUnit::assertSame($count, $this->getSystemIdleTimeCount); 82 | } 83 | 84 | public function assertGetCurrentThermalStateCount(int $count): void 85 | { 86 | PHPUnit::assertSame($count, $this->getCurrentThermalStateCount); 87 | } 88 | 89 | public function assertIsOnBatteryPowerCount(int $count): void 90 | { 91 | PHPUnit::assertSame($count, $this->isOnBatteryPowerCount); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/App.php: -------------------------------------------------------------------------------- 1 | client->post('app/quit'); 15 | } 16 | 17 | public function relaunch(): void 18 | { 19 | $this->client->post('app/relaunch'); 20 | } 21 | 22 | public function focus(): void 23 | { 24 | $this->client->post('app/focus'); 25 | } 26 | 27 | public function hide(): void 28 | { 29 | $this->client->post('app/hide'); 30 | } 31 | 32 | public function isHidden(): bool 33 | { 34 | return $this->client->get('app/is-hidden')->json('is_hidden'); 35 | } 36 | 37 | public function getLocale(): string 38 | { 39 | return $this->client->get('app/locale')->json('locale'); 40 | } 41 | 42 | public function getLocaleCountryCode(): string 43 | { 44 | return $this->client->get('app/locale-country-code')->json('locale_country_code'); 45 | } 46 | 47 | public function getSystemLocale(): string 48 | { 49 | return $this->client->get('app/system-locale')->json('system_locale'); 50 | } 51 | 52 | public function version(): string 53 | { 54 | return $this->client->get('app/version')->json('version'); 55 | } 56 | 57 | public function badgeCount($count = null): int 58 | { 59 | if ($count === null) { 60 | return (int) $this->client->get('app/badge-count')->json('count'); 61 | } 62 | 63 | $this->client->post('app/badge-count', [ 64 | 'count' => (int) $count, 65 | ]); 66 | 67 | return (int) $count; 68 | } 69 | 70 | public function addRecentDocument(string $path): void 71 | { 72 | $this->client->post('app/recent-documents', [ 73 | 'path' => $path, 74 | ]); 75 | } 76 | 77 | public function recentDocuments(): array 78 | { 79 | return $this->client->get('app/recent-documents')->json('documents'); 80 | } 81 | 82 | public function clearRecentDocuments(): void 83 | { 84 | $this->client->delete('app/recent-documents'); 85 | } 86 | 87 | public function isRunningBundled(): bool 88 | { 89 | return Phar::running() !== ''; 90 | 91 | } 92 | 93 | public function openAtLogin(?bool $open = null): bool 94 | { 95 | if ($open === null) { 96 | return (bool) $this->client->get('app/open-at-login')->json('open'); 97 | } 98 | 99 | $this->client->post('app/open-at-login', [ 100 | 'open' => $open, 101 | ]); 102 | 103 | return $open; 104 | } 105 | 106 | public function isEmojiPanelSupported(): bool 107 | { 108 | return (bool) $this->client->get('app/is-emoji-panel-supported')->json('supported'); 109 | } 110 | 111 | public function showEmojiPanel(): void 112 | { 113 | $this->client->post('app/show-emoji-panel'); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel wrapper for the NativePHP framework. 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/nativephp/laravel.svg?style=flat-square)](https://packagist.org/packages/nativephp/laravel) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/nativephp/laravel/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/nativephp/laravel/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/nativephp/laravel/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/nativephp/laravel/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/nativephp/laravel?style=flat-square)](https://packagist.org/packages/nativephp/laravel) 7 | 8 | Write native desktop applications using PHP. 9 | To learn more, visit the [official website](https://nativephp.com). 10 | 11 | ## Documentation 12 | 13 | You can find the NativePHP [documentation on the website](https://nativephp.com). 14 | Check out the [Getting Started](https://nativephp.com/docs/1/getting-started/introduction) page for a quick overview. 15 | - [Getting Started](https://nativephp.com/docs/1/getting-started/introduction) 16 | - [Installation](https://nativephp.com/docs/1/getting-started/installation) 17 | - [Configuration](https://nativephp.com/docs/1/getting-started/configuration) 18 | - [Application Lifecycle](https://nativephp.com/docs/1/the-basics/app-lifecycle) 19 | - [Contributing Guide](https://github.com/NativePHP/laravel/blob/main/CONTRIBUTING.md) 20 | 21 | ## Sponsors 22 | 23 | Thanks to the following sponsors for funding NativePHP development. Please consider [sponsoring](https://nativephp.com/docs/getting-started/sponsoring). 24 | 25 | - [BeyondCode](https://beyondco.de/?utm_source=nativephp-docs&utm_medium=logo&utm_campaign=nativephp) - Essential tools for web developers. 26 | - [Laradevs](https://laradevs.com/?ref=nativephp-docs) - Connecting the best Laravel Developers with the best Laravel Teams. 27 | - [RedGalaxy](https://www.redgalaxy.co.uk) - A web application development studio based in Cambridgeshire, building solutions to help businesses improve efficiency and profitability. 28 | - [Sevalla](https://sevalla.com/?utm_source=nativephp&utm_medium=Referral&utm_campaign=homepage) - Host and manage your applications, databases, and static sites in a single, intuitive platform. 29 | - [KaasHosting](https://www.kaashosting.nl/?lang=en) - Minecraft Server and VPS hosting from The Netherlands. 30 | 31 | ## Changelog 32 | 33 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 34 | 35 | ## Contributing 36 | 37 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 38 | 39 | ## Security Vulnerabilities 40 | 41 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 42 | 43 | ## Credits 44 | 45 | - [Marcel Pociot](https://github.com/mpociot) 46 | - [Simon Hamp](https://github.com/simonhamp) 47 | - [All Contributors](../../contributors) 48 | 49 | ## License 50 | 51 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to NativePHP 2 | 3 | Thank you for considering contributing to NativePHP! We appreciate your interest in making this project better and welcome your contributions. 4 | 5 | ## How to Contribute 6 | 7 | We believe that open collaboration leads to the best results. To contribute to the project, follow these steps: 8 | 9 | 1. **Fork the repository**: Fork the NativePHP repository to your own GitHub account. This will create a copy of the project under your account. 10 | 11 | 2. **Clone the repository**: Clone the forked repository to your local machine using `git clone`. This allows you to work on the project locally. 12 | 13 | 3. **Create a branch**: Before making any changes, create a new branch in the repository. The branch name should be descriptive and reflect the changes you plan to make. 14 | 15 | 4. **Make changes**: Make your desired changes and improvements to the project. Please ensure that your changes adhere to the coding guidelines and best practices of the project. 16 | 17 | 5. **Test your changes**: Test your changes thoroughly to ensure they work as intended and do not introduce any new issues. 18 | 19 | 6. **Commit your changes**: Once you are satisfied with your changes, commit them with clear and concise commit messages that explain the purpose of the changes. 20 | 21 | 7. **Pull Request**: Push your changes to your forked repository and create a Pull Request (PR) to the `main` branch of the original NativePHP repository. Provide a detailed description of the changes you made, the problem you solved, and any other relevant information. 22 | 23 | 8. **Code Review**: The project maintainers will review your Pull Request. Be prepared to make changes or address any feedback you receive during the review process. 24 | 25 | 9. **Merge**: Once your Pull Request has been approved and any necessary changes have been made, it will be merged into the main project. Congratulations on your successful contribution! 26 | 27 | ## Code Style and Guidelines 28 | 29 | Please follow the coding style and guidelines of the project. If the project uses a specific coding style (e.g., Python's PEP 8), make sure your contributions comply with it. 30 | 31 | ## Reporting Issues 32 | 33 | If you encounter any bugs, have questions, or want to suggest improvements, please create an issue in the GitHub repository. Clearly, describe the problem or suggestion, providing steps to reproduce the issue if applicable. 34 | 35 | ## Code of Conduct 36 | 37 | We expect all contributors to adhere to our Code of Conduct. Be respectful, inclusive, and considerate of others when participating in this project. Any unacceptable behavior will not be tolerated. 38 | 39 | ## License 40 | 41 | By contributing to NativePHP, you agree that your contributions will be licensed under the same license as the project (if applicable). Make sure you understand and comply with the licensing terms before submitting your contributions. 42 | 43 | ## Acknowledgments 44 | 45 | Contributors play a vital role in the success of this project. Your efforts will be acknowledged in the project's documentation and release notes, and your GitHub profile will be listed as a contributor. 46 | 47 | Thank you for your interest in contributing to NativePHP. Together, we can build an amazing open-source community and make the project even better! 48 | -------------------------------------------------------------------------------- /src/Windows/WindowManager.php: -------------------------------------------------------------------------------- 1 | setClient($this->client); 18 | } 19 | 20 | public function close($id = null) 21 | { 22 | $this->client->post('window/close', [ 23 | 'id' => $id ?? $this->detectId(), 24 | ]); 25 | } 26 | 27 | public function hide($id = null) 28 | { 29 | $this->client->post('window/hide', [ 30 | 'id' => $id ?? $this->detectId(), 31 | ]); 32 | } 33 | 34 | public function show($id = null) 35 | { 36 | $this->client->post('window/show', [ 37 | 'id' => $id ?? $this->detectId(), 38 | ]); 39 | } 40 | 41 | public function current(): Window 42 | { 43 | $window = (object) $this->client->get('window/current')->json(); 44 | 45 | return (new Window($window->id)) 46 | ->setClient($this->client) 47 | ->fromRuntimeWindow($window); 48 | } 49 | 50 | public function all(): array 51 | { 52 | $windows = (array) $this->client->get('window/all')->json(); 53 | 54 | return array_map( 55 | fn ($window) => (new Window($window['id'])) 56 | ->setClient($this->client) 57 | ->fromRuntimeWindow((object) $window), 58 | $windows 59 | ); 60 | } 61 | 62 | public function get(string $id): Window 63 | { 64 | $window = (object) $this->client->get("window/get/{$id}")->json(); 65 | 66 | return (new Window($id)) 67 | ->setClient($this->client) 68 | ->fromRuntimeWindow($window); 69 | } 70 | 71 | public function resize($width, $height, $id = null) 72 | { 73 | $this->client->post('window/resize', [ 74 | 'id' => $id ?? $this->detectId(), 75 | 'width' => $width, 76 | 'height' => $height, 77 | ]); 78 | } 79 | 80 | public function position($x, $y, $animated = false, $id = null) 81 | { 82 | $this->client->post('window/position', [ 83 | 'id' => $id ?? $this->detectId(), 84 | 'x' => $x, 85 | 'y' => $y, 86 | 'animate' => $animated, 87 | ]); 88 | } 89 | 90 | public function alwaysOnTop($alwaysOnTop = true, $id = null): void 91 | { 92 | $this->client->post('window/always-on-top', [ 93 | 'id' => $id ?? $this->detectId(), 94 | 'alwaysOnTop' => $alwaysOnTop, 95 | ]); 96 | } 97 | 98 | public function maximize($id = null): void 99 | { 100 | $this->client->post('window/maximize', [ 101 | 'id' => $id ?? $this->detectId(), 102 | ]); 103 | } 104 | 105 | public function minimize($id = null): void 106 | { 107 | $this->client->post('window/minimize', [ 108 | 'id' => $id ?? $this->detectId(), 109 | ]); 110 | } 111 | 112 | public function reload($id = null): void 113 | { 114 | $this->client->post('window/reload', [ 115 | 'id' => $id ?? $this->detectId(), 116 | ]); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Menu/Items/MenuItem.php: -------------------------------------------------------------------------------- 1 | isEnabled = true; 38 | 39 | return $this; 40 | } 41 | 42 | public function disabled(): self 43 | { 44 | $this->isEnabled = false; 45 | 46 | return $this; 47 | } 48 | 49 | public function id(string $id): self 50 | { 51 | $this->id = $id; 52 | 53 | return $this; 54 | } 55 | 56 | public function label(string $label): self 57 | { 58 | $this->label = $label; 59 | 60 | return $this; 61 | } 62 | 63 | public function sublabel(string $sublabel): self 64 | { 65 | $this->sublabel = $sublabel; 66 | 67 | return $this; 68 | } 69 | 70 | public function icon(string $icon): self 71 | { 72 | $this->icon = $icon; 73 | 74 | return $this; 75 | } 76 | 77 | public function visible($visible = true): self 78 | { 79 | $this->isVisible = $visible; 80 | 81 | return $this; 82 | } 83 | 84 | public function accelerator(string $accelerator): self 85 | { 86 | $this->accelerator = $accelerator; 87 | 88 | return $this; 89 | } 90 | 91 | public function hotkey(string $hotkey): self 92 | { 93 | return $this->accelerator($hotkey); 94 | } 95 | 96 | public function checked($checked = true): self 97 | { 98 | $this->isChecked = $checked; 99 | 100 | return $this; 101 | } 102 | 103 | public function tooltip(string $toolTip): self 104 | { 105 | $this->toolTip = $toolTip; 106 | 107 | return $this; 108 | } 109 | 110 | public function submenu(MenuItemContract ...$items): self 111 | { 112 | $this->submenu = MenuFacade::make(...$items); 113 | 114 | return $this; 115 | } 116 | 117 | public function event(string $event): self 118 | { 119 | $this->event = $event; 120 | 121 | return $this; 122 | } 123 | 124 | public function toArray(): array 125 | { 126 | return array_filter([ 127 | 'type' => $this->type, 128 | 'id' => $this->id, 129 | 'label' => $this->label, 130 | 'event' => $this->event, 131 | 'sublabel' => $this->sublabel, 132 | 'toolTip' => $this->toolTip, 133 | 'enabled' => $this->isEnabled, 134 | 'visible' => $this->isVisible, 135 | 'checked' => $this->isChecked, 136 | 'accelerator' => $this->accelerator, 137 | 'icon' => $this->icon, 138 | 'submenu' => $this->submenu?->toArray(), 139 | ], fn ($value) => $value !== null); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Dialog.php: -------------------------------------------------------------------------------- 1 | title = $title; 39 | 40 | return $this; 41 | } 42 | 43 | public function defaultPath(string $defaultPath): self 44 | { 45 | $this->defaultPath = $defaultPath; 46 | 47 | return $this; 48 | } 49 | 50 | public function button(string $buttonLabel): self 51 | { 52 | $this->buttonLabel = $buttonLabel; 53 | 54 | return $this; 55 | } 56 | 57 | public function multiple() 58 | { 59 | $this->properties[] = 'multiSelections'; 60 | 61 | return $this; 62 | } 63 | 64 | public function withHiddenFiles() 65 | { 66 | $this->properties[] = 'showHiddenFiles'; 67 | 68 | return $this; 69 | } 70 | 71 | public function files() 72 | { 73 | $this->properties = array_diff($this->properties, ['openDirectory']); 74 | $this->properties = ['openFile']; 75 | 76 | return $this; 77 | } 78 | 79 | public function folders() 80 | { 81 | $this->properties = array_diff($this->properties, ['openFile']); 82 | $this->properties[] = 'openDirectory'; 83 | 84 | return $this; 85 | } 86 | 87 | public function dontResolveSymlinks(): self 88 | { 89 | $this->properties[] = 'noResolveAliases'; 90 | 91 | return $this; 92 | } 93 | 94 | public function filter(string $name, array $extensions): self 95 | { 96 | $this->filters[] = [ 97 | 'name' => $name, 98 | 'extensions' => $extensions, 99 | ]; 100 | 101 | return $this; 102 | } 103 | 104 | public function properties(array $properties): self 105 | { 106 | $this->properties = $properties; 107 | 108 | return $this; 109 | } 110 | 111 | public function asSheet(?string $windowId = null): self 112 | { 113 | $this->windowReference = $windowId ?? Window::current()->getId(); 114 | 115 | return $this; 116 | } 117 | 118 | public function open() 119 | { 120 | $result = $this->client->post('dialog/open', $this->dialogData())->json('result'); 121 | 122 | if (! in_array('multiSelections', $this->properties)) { 123 | return $result[0] ?? null; 124 | } 125 | 126 | return $result; 127 | } 128 | 129 | public function save() 130 | { 131 | return $this->client->post('dialog/save', $this->dialogData())->json('result'); 132 | } 133 | 134 | public function dialogData(): array 135 | { 136 | return [ 137 | 'title' => $this->title, 138 | 'windowReference' => $this->windowReference, 139 | 'defaultPath' => $this->defaultPath, 140 | 'filters' => $this->filters, 141 | 'buttonLabel' => $this->buttonLabel, 142 | 'properties' => array_unique($this->properties), 143 | ]; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/System.php: -------------------------------------------------------------------------------- 1 | client->get('system/can-prompt-touch-id')->json('result'); 18 | } 19 | 20 | public function promptTouchID(string $reason): bool 21 | { 22 | return $this->client->post('system/prompt-touch-id', [ 23 | 'reason' => $reason, 24 | ])->successful(); 25 | } 26 | 27 | public function canEncrypt(): bool 28 | { 29 | return $this->client->get('system/can-encrypt')->json('result'); 30 | } 31 | 32 | public function encrypt(string $string): ?string 33 | { 34 | return $this->client->post('system/encrypt', [ 35 | 'string' => $string, 36 | ])->json('result'); 37 | } 38 | 39 | public function decrypt(string $string): ?string 40 | { 41 | return $this->client->post('system/decrypt', [ 42 | 'string' => $string, 43 | ])->json('result'); 44 | } 45 | 46 | /** 47 | * @return array<\Native\Laravel\DataObjects\Printer> 48 | */ 49 | public function printers(): array 50 | { 51 | $printers = $this->client->get('system/printers')->json('printers'); 52 | 53 | return collect($printers)->map(function ($printer) { 54 | return new Printer( 55 | data_get($printer, 'name'), 56 | data_get($printer, 'displayName'), 57 | data_get($printer, 'description'), 58 | data_get($printer, 'status'), 59 | data_get($printer, 'isDefault'), 60 | data_get($printer, 'options'), 61 | ); 62 | })->toArray(); 63 | } 64 | 65 | /** 66 | * For $settings options, see https://www.electronjs.org/docs/latest/api/web-contents#contentsprintoptions-callback 67 | */ 68 | public function print(string $html, ?Printer $printer = null, ?array $settings = []): void 69 | { 70 | $this->client->post('system/print', [ 71 | 'html' => $html, 72 | 'printer' => $printer->name ?? '', 73 | 'settings' => $settings, 74 | ]); 75 | } 76 | 77 | /** 78 | * For $settings options, see https://www.electronjs.org/docs/latest/api/web-contents#contentsprinttopdfoptions 79 | */ 80 | public function printToPDF(string $html, ?array $settings = []): string 81 | { 82 | return $this->client->post('system/print-to-pdf', [ 83 | 'html' => $html, 84 | 'settings' => $settings, 85 | ])->json('result'); 86 | } 87 | 88 | public function timezone(): string 89 | { 90 | $timezones = new Timezones; 91 | 92 | if (Environment::isWindows()) { 93 | $timezone = $timezones->translateFromWindowsString(exec('tzutil /g')); 94 | } else { 95 | $timezone = $timezones->translateFromAbbreviatedString(exec('date +%Z')); 96 | } 97 | 98 | return $timezone; 99 | } 100 | 101 | public function theme(?SystemThemesEnum $theme = null): SystemThemesEnum 102 | { 103 | if ($theme) { 104 | $result = $this->client->post('system/theme', [ 105 | 'theme' => $theme, 106 | ])->json('result'); 107 | } else { 108 | $result = $this->client->get('system/theme')->json('result'); 109 | } 110 | 111 | return SystemThemesEnum::from($result); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/MenuBar/MenuBar.php: -------------------------------------------------------------------------------- 1 | url = url('/'); 42 | } 43 | 44 | public function setClient(Client $client): self 45 | { 46 | $this->client = $client; 47 | 48 | return $this; 49 | } 50 | 51 | public function icon(string $icon): self 52 | { 53 | $this->icon = $icon; 54 | 55 | return $this; 56 | } 57 | 58 | public function onlyShowContextMenu(bool $onlyContextMenu = true): self 59 | { 60 | $this->onlyShowContextMenu = $onlyContextMenu; 61 | 62 | return $this; 63 | } 64 | 65 | public function showDockIcon($value = true): self 66 | { 67 | $this->showDockIcon = $value; 68 | 69 | return $this; 70 | } 71 | 72 | public function label(string $label = ''): self 73 | { 74 | $this->label = $label; 75 | 76 | return $this; 77 | } 78 | 79 | public function tooltip(string $tooltip = ''): self 80 | { 81 | $this->tooltip = $tooltip; 82 | 83 | return $this; 84 | } 85 | 86 | public function resizable(bool $resizable = true): static 87 | { 88 | $this->resizable = $resizable; 89 | 90 | return $this; 91 | } 92 | 93 | public function alwaysOnTop($alwaysOnTop = true): self 94 | { 95 | $this->alwaysOnTop = $alwaysOnTop; 96 | 97 | return $this; 98 | } 99 | 100 | public function showOnAllWorkspaces($showOnAllWorkspaces = true): self 101 | { 102 | $this->showOnAllWorkspaces = $showOnAllWorkspaces; 103 | 104 | return $this; 105 | } 106 | 107 | public function withContextMenu(Menu $menu): self 108 | { 109 | $this->contextMenu = $menu; 110 | 111 | return $this; 112 | } 113 | 114 | public function toArray(): array 115 | { 116 | return [ 117 | 'url' => $this->url, 118 | 'icon' => $this->icon, 119 | 'windowPosition' => $this->windowPosition, 120 | 'x' => $this->x, 121 | 'y' => $this->y, 122 | 'label' => $this->label, 123 | 'tooltip' => $this->tooltip, 124 | 'resizable' => $this->resizable, 125 | 'width' => $this->width, 126 | 'height' => $this->height, 127 | 'vibrancy' => $this->vibrancy, 128 | 'showDockIcon' => $this->showDockIcon, 129 | 'transparency' => $this->transparent, 130 | 'backgroundColor' => $this->backgroundColor, 131 | 'onlyShowContextMenu' => $this->onlyShowContextMenu, 132 | 'contextMenu' => ! is_null($this->contextMenu) ? $this->contextMenu->toArray()['submenu'] : null, 133 | 'alwaysOnTop' => $this->alwaysOnTop, 134 | 'showOnAllWorkspaces' => $this->showOnAllWorkspaces, 135 | ]; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nativephp/laravel", 3 | "description": "Laravel wrapper for the NativePHP framework.", 4 | "keywords": [ 5 | "nativephp", 6 | "laravel", 7 | "nativephp-laravel" 8 | ], 9 | "homepage": "https://github.com/nativephp/laravel", 10 | "license": "MIT", 11 | "funding": [ 12 | { 13 | "type": "github", 14 | "url": "https://github.com/sponsors/simonhamp" 15 | }, 16 | { 17 | "type": "opencollective", 18 | "url": "https://opencollective.com/nativephp" 19 | } 20 | ], 21 | "authors": [ 22 | { 23 | "name": "Marcel Pociot", 24 | "email": "marcel@beyondco.de", 25 | "role": "Developer" 26 | }, 27 | { 28 | "name": "Simon Hamp", 29 | "email": "simon.hamp@me.com", 30 | "role": "Developer" 31 | } 32 | ], 33 | "require": { 34 | "php": "^8.3", 35 | "illuminate/contracts": "^10.0|^11.0|^12.0", 36 | "spatie/laravel-package-tools": "^1.16.4", 37 | "symfony/finder": "^6.2|^7.0" 38 | }, 39 | "require-dev": { 40 | "guzzlehttp/guzzle": "^7.0", 41 | "laravel/pint": "^1.0", 42 | "larastan/larastan": "^2.0|^3.1", 43 | "nunomaduro/collision": "^7.11|^8.1.1", 44 | "orchestra/testbench": "^8.0|^9.0|^10.0", 45 | "pestphp/pest": "^v2.30|^3.0", 46 | "pestphp/pest-plugin-arch": "^2.0|^3.0", 47 | "pestphp/pest-plugin-laravel": "^2.0|^3.1", 48 | "phpstan/extension-installer": "^1.1", 49 | "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", 50 | "phpstan/phpstan-phpunit": "^1.0|^2.0", 51 | "spatie/laravel-ray": "^1.26" 52 | }, 53 | "autoload": { 54 | "psr-4": { 55 | "Native\\Laravel\\": "src/" 56 | } 57 | }, 58 | "autoload-dev": { 59 | "psr-4": { 60 | "Native\\Laravel\\Tests\\": "tests/" 61 | } 62 | }, 63 | "scripts": { 64 | "qa" : [ 65 | "@composer format", 66 | "@composer analyse", 67 | "@composer test" 68 | ], 69 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 70 | "analyse": "vendor/bin/phpstan analyse", 71 | "test": "vendor/bin/pest", 72 | "test-coverage": "vendor/bin/pest --coverage", 73 | "format": "vendor/bin/pint" 74 | }, 75 | "config": { 76 | "sort-packages": true, 77 | "allow-plugins": { 78 | "pestphp/pest-plugin": true, 79 | "phpstan/extension-installer": true 80 | } 81 | }, 82 | "extra": { 83 | "laravel": { 84 | "providers": [ 85 | "Native\\Laravel\\NativeServiceProvider" 86 | ], 87 | "aliases": { 88 | "ChildProcess": "Native\\Laravel\\Facades\\ChildProcess", 89 | "Clipboard": "Native\\Laravel\\Facades\\Clipboard", 90 | "ContextMenu": "Native\\Laravel\\Facades\\ContextMenu", 91 | "Dock": "Native\\Laravel\\Facades\\Dock", 92 | "GlobalShortcut": "Native\\Laravel\\Facades\\GlobalShortcut", 93 | "Menu": "Native\\Laravel\\Facades\\Menu", 94 | "MenuBar": "Native\\Laravel\\Facades\\MenuBar", 95 | "Notification": "Native\\Laravel\\Facades\\Notification", 96 | "PowerMonitor": "Native\\Laravel\\Facades\\PowerMonitor", 97 | "Process": "Native\\Laravel\\Facades\\Process", 98 | "QueueWorker": "Native\\Laravel\\Facades\\QueueWorker", 99 | "Screen": "Native\\Laravel\\Facades\\Screen", 100 | "Settings": "Native\\Laravel\\Facades\\Settings", 101 | "Shell": "Native\\Laravel\\Facades\\Shell", 102 | "System": "Native\\Laravel\\Facades\\System", 103 | "Window": "Native\\Laravel\\Facades\\Window" 104 | } 105 | } 106 | }, 107 | "minimum-stability": "dev", 108 | "prefer-stable": true 109 | } 110 | -------------------------------------------------------------------------------- /src/ChildProcess.php: -------------------------------------------------------------------------------- 1 | alias; 29 | 30 | $process = $this->client->get("child-process/get/{$alias}")->json(); 31 | 32 | if (! $process) { 33 | return null; 34 | } 35 | 36 | return $this->fromRuntimeProcess($process); 37 | } 38 | 39 | public function all(): array 40 | { 41 | $processes = $this->client->get('child-process/')->json(); 42 | 43 | if (empty($processes)) { 44 | return []; 45 | } 46 | 47 | $hydrated = []; 48 | 49 | foreach ($processes as $alias => $process) { 50 | $hydrated[$alias] = (new static($this->client)) 51 | ->fromRuntimeProcess($process); 52 | } 53 | 54 | return $hydrated; 55 | } 56 | 57 | /** 58 | * @param string|string[] $cmd 59 | * @return $this 60 | */ 61 | public function start( 62 | string|array $cmd, 63 | string $alias, 64 | ?string $cwd = null, 65 | ?array $env = null, 66 | bool $persistent = false 67 | ): self { 68 | $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; 69 | 70 | $process = $this->client->post('child-process/start', [ 71 | 'alias' => $alias, 72 | 'cmd' => $cmd, 73 | 'cwd' => $cwd ?? base_path(), 74 | 'env' => $env, 75 | 'persistent' => $persistent, 76 | ])->json(); 77 | 78 | return $this->fromRuntimeProcess($process); 79 | } 80 | 81 | /** 82 | * @param string|string[] $cmd 83 | * @return $this 84 | */ 85 | public function php(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false, ?array $iniSettings = null): self 86 | { 87 | $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; 88 | 89 | $process = $this->client->post('child-process/start-php', [ 90 | 'alias' => $alias, 91 | 'cmd' => $cmd, 92 | 'cwd' => base_path(), 93 | 'env' => $env, 94 | 'persistent' => $persistent, 95 | 'iniSettings' => $iniSettings, 96 | ])->json(); 97 | 98 | return $this->fromRuntimeProcess($process); 99 | } 100 | 101 | /** 102 | * @param string|string[] $cmd 103 | * @return $this 104 | */ 105 | public function artisan(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false, ?array $iniSettings = null): self 106 | { 107 | $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; 108 | 109 | $cmd = ['artisan', ...$cmd]; 110 | 111 | return $this->php($cmd, $alias, env: $env, persistent: $persistent, iniSettings: $iniSettings); 112 | } 113 | 114 | public function stop(?string $alias = null): void 115 | { 116 | $this->client->post('child-process/stop', [ 117 | 'alias' => $alias ?? $this->alias, 118 | ])->json(); 119 | } 120 | 121 | public function restart(?string $alias = null): ?self 122 | { 123 | $process = $this->client->post('child-process/restart', [ 124 | 'alias' => $alias ?? $this->alias, 125 | ])->json(); 126 | 127 | if (! $process) { 128 | return null; 129 | } 130 | 131 | return $this->fromRuntimeProcess($process); 132 | } 133 | 134 | public function message(string $message, ?string $alias = null): self 135 | { 136 | $this->client->post('child-process/message', [ 137 | 'alias' => $alias ?? $this->alias, 138 | 'message' => $message, 139 | ])->json(); 140 | 141 | return $this; 142 | } 143 | 144 | protected function fromRuntimeProcess($process) 145 | { 146 | if (isset($process['pid'])) { 147 | // @phpstan-ignore-next-line 148 | $this->pid = $process['pid']; 149 | } 150 | 151 | foreach ($process['settings'] as $key => $value) { 152 | if (! property_exists($this, $key)) { 153 | throw new \RuntimeException("Property {$key} does not exist on ".__CLASS__); 154 | } 155 | 156 | $this->{$key} = $value; 157 | } 158 | 159 | return $this; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Menu/MenuBuilder.php: -------------------------------------------------------------------------------- 1 | client); 16 | 17 | foreach ($items as $item) { 18 | $menu->add($item); 19 | } 20 | 21 | return $menu; 22 | } 23 | 24 | public function create(MenuItem ...$items): void 25 | { 26 | $this->make(...$items) 27 | ->register(); 28 | } 29 | 30 | public function default(): void 31 | { 32 | $this->create( 33 | $this->app(), 34 | $this->file(), 35 | $this->edit(), 36 | $this->view(), 37 | $this->window(), 38 | ); 39 | } 40 | 41 | public function label(string $label, ?string $hotkey = null): Items\Label 42 | { 43 | return new Items\Label($label, $hotkey); 44 | } 45 | 46 | public function checkbox(string $label, bool $checked = false, ?string $hotkey = null): Items\Checkbox 47 | { 48 | return new Items\Checkbox($label, $checked, $hotkey); 49 | } 50 | 51 | public function radio(string $label, bool $checked = false, ?string $hotkey = null): Items\Radio 52 | { 53 | return new Items\Radio($label, $checked, $hotkey); 54 | } 55 | 56 | public function link(string $url, ?string $label = null, ?string $hotkey = null): Items\Link 57 | { 58 | return new Items\Link($url, $label, $hotkey); 59 | } 60 | 61 | public function route(string $route, ?string $label = null, ?string $hotkey = null): Items\Link 62 | { 63 | return new Items\Link(route($route), $label, $hotkey); 64 | } 65 | 66 | public function app(): Items\Role 67 | { 68 | return new Items\Role(RolesEnum::APP_MENU); 69 | } 70 | 71 | public function file(?string $label = null): Items\Role 72 | { 73 | return new Items\Role(RolesEnum::FILE_MENU, $label); 74 | } 75 | 76 | public function edit(?string $label = null): Items\Role 77 | { 78 | return new Items\Role(RolesEnum::EDIT_MENU, $label); 79 | } 80 | 81 | public function view(?string $label = null): Items\Role 82 | { 83 | return new Items\Role(RolesEnum::VIEW_MENU, $label); 84 | } 85 | 86 | public function window(?string $label = null): Items\Role 87 | { 88 | return new Items\Role(RolesEnum::WINDOW_MENU, $label); 89 | } 90 | 91 | public function separator(): Items\Separator 92 | { 93 | return new Items\Separator; 94 | } 95 | 96 | public function fullscreen(?string $label = null): Items\Role 97 | { 98 | return new Items\Role(RolesEnum::TOGGLE_FULL_SCREEN, $label); 99 | } 100 | 101 | public function devTools(?string $label = null): Items\Role 102 | { 103 | return new Items\Role(RolesEnum::TOGGLE_DEV_TOOLS, $label); 104 | } 105 | 106 | public function undo(?string $label = null): Items\Role 107 | { 108 | return new Items\Role(RolesEnum::UNDO, $label); 109 | } 110 | 111 | public function redo(?string $label = null): Items\Role 112 | { 113 | return new Items\Role(RolesEnum::REDO, $label); 114 | } 115 | 116 | public function cut(?string $label = null): Items\Role 117 | { 118 | return new Items\Role(RolesEnum::CUT, $label); 119 | } 120 | 121 | public function copy(?string $label = null): Items\Role 122 | { 123 | return new Items\Role(RolesEnum::COPY, $label); 124 | } 125 | 126 | public function paste(?string $label = null): Items\Role 127 | { 128 | return new Items\Role(RolesEnum::PASTE, $label); 129 | } 130 | 131 | public function pasteAndMatchStyle(?string $label = null): Items\Role 132 | { 133 | return new Items\Role(RolesEnum::PASTE_STYLE, $label); 134 | } 135 | 136 | public function reload(?string $label = null): Items\Role 137 | { 138 | return new Items\Role(RolesEnum::RELOAD, $label); 139 | } 140 | 141 | public function minimize(?string $label = null): Items\Role 142 | { 143 | return new Items\Role(RolesEnum::MINIMIZE, $label); 144 | } 145 | 146 | public function close(?string $label = null): Items\Role 147 | { 148 | return new Items\Role(RolesEnum::CLOSE, $label); 149 | } 150 | 151 | public function quit(?string $label = null): Items\Role 152 | { 153 | if (is_null($label)) { 154 | $label = __('Quit').' '.config('app.name'); 155 | } 156 | 157 | return new Items\Role(RolesEnum::QUIT, $label); 158 | } 159 | 160 | public function help(?string $label = null): Items\Role 161 | { 162 | return new Items\Role(RolesEnum::HELP, $label); 163 | } 164 | 165 | public function hide(?string $label = null): Items\Role 166 | { 167 | return new Items\Role(RolesEnum::HIDE, $label); 168 | } 169 | 170 | public function about(?string $label = null): Items\Role 171 | { 172 | return new Items\Role(RolesEnum::ABOUT, $label); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /config/nativephp.php: -------------------------------------------------------------------------------- 1 | env('NATIVEPHP_APP_VERSION', '1.0.0'), 10 | 11 | /** 12 | * The ID of your application. This should be a unique identifier 13 | * usually in the form of a reverse domain name. 14 | * For example: com.nativephp.app 15 | */ 16 | 'app_id' => env('NATIVEPHP_APP_ID', 'com.nativephp.app'), 17 | 18 | /** 19 | * If your application allows deep linking, you can specify the scheme 20 | * to use here. This is the scheme that will be used to open your 21 | * application from within other applications. 22 | * For example: "nativephp" 23 | * 24 | * This would allow you to open your application using a URL like: 25 | * nativephp://some/path 26 | */ 27 | 'deeplink_scheme' => env('NATIVEPHP_DEEPLINK_SCHEME'), 28 | 29 | /** 30 | * The author of your application. 31 | */ 32 | 'author' => env('NATIVEPHP_APP_AUTHOR'), 33 | 34 | /** 35 | * The copyright notice for your application. 36 | */ 37 | 'copyright' => env('NATIVEPHP_APP_COPYRIGHT'), 38 | 39 | /** 40 | * The description of your application. 41 | */ 42 | 'description' => env('NATIVEPHP_APP_DESCRIPTION', 'An awesome app built with NativePHP'), 43 | 44 | /** 45 | * The Website of your application. 46 | */ 47 | 'website' => env('NATIVEPHP_APP_WEBSITE', 'https://nativephp.com'), 48 | 49 | /** 50 | * The default service provider for your application. This provider 51 | * takes care of bootstrapping your application and configuring 52 | * any global hotkeys, menus, windows, etc. 53 | */ 54 | 'provider' => \App\Providers\NativeAppServiceProvider::class, 55 | 56 | /** 57 | * A list of environment keys that should be removed from the 58 | * .env file when the application is bundled for production. 59 | * You may use wildcards to match multiple keys. 60 | */ 61 | 'cleanup_env_keys' => [ 62 | 'AWS_*', 63 | 'AZURE_*', 64 | 'GITHUB_*', 65 | 'DO_SPACES_*', 66 | '*_SECRET', 67 | 'ZEPHPYR_*', 68 | 'NATIVEPHP_UPDATER_PATH', 69 | 'NATIVEPHP_APPLE_ID', 70 | 'NATIVEPHP_APPLE_ID_PASS', 71 | 'NATIVEPHP_APPLE_TEAM_ID', 72 | 'NATIVEPHP_AZURE_PUBLISHER_NAME', 73 | 'NATIVEPHP_AZURE_ENDPOINT', 74 | 'NATIVEPHP_AZURE_CERTIFICATE_PROFILE_NAME', 75 | 'NATIVEPHP_AZURE_CODE_SIGNING_ACCOUNT_NAME', 76 | ], 77 | 78 | /** 79 | * A list of files and folders that should be removed from the 80 | * final app before it is bundled for production. 81 | * You may use glob / wildcard patterns here. 82 | */ 83 | 'cleanup_exclude_files' => [ 84 | 'build', 85 | 'temp', 86 | 'content', 87 | 'node_modules', 88 | '*/tests', 89 | ], 90 | 91 | /** 92 | * The NativePHP updater configuration. 93 | */ 94 | 'updater' => [ 95 | /** 96 | * Whether or not the updater is enabled. Please note that the 97 | * updater will only work when your application is bundled 98 | * for production. 99 | */ 100 | 'enabled' => env('NATIVEPHP_UPDATER_ENABLED', true), 101 | 102 | /** 103 | * The updater provider to use. 104 | * Supported: "github", "s3", "spaces" 105 | */ 106 | 'default' => env('NATIVEPHP_UPDATER_PROVIDER', 'spaces'), 107 | 108 | 'providers' => [ 109 | 'github' => [ 110 | 'driver' => 'github', 111 | 'repo' => env('GITHUB_REPO'), 112 | 'owner' => env('GITHUB_OWNER'), 113 | 'token' => env('GITHUB_TOKEN'), 114 | 'vPrefixedTagName' => env('GITHUB_V_PREFIXED_TAG_NAME', true), 115 | 'private' => env('GITHUB_PRIVATE', false), 116 | 'channel' => env('GITHUB_CHANNEL', 'latest'), 117 | 'releaseType' => env('GITHUB_RELEASE_TYPE', 'draft'), 118 | ], 119 | 120 | 's3' => [ 121 | 'driver' => 's3', 122 | 'key' => env('AWS_ACCESS_KEY_ID'), 123 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 124 | 'region' => env('AWS_DEFAULT_REGION'), 125 | 'bucket' => env('AWS_BUCKET'), 126 | 'endpoint' => env('AWS_ENDPOINT'), 127 | 'path' => env('NATIVEPHP_UPDATER_PATH', null), 128 | ], 129 | 130 | 'spaces' => [ 131 | 'driver' => 'spaces', 132 | 'key' => env('DO_SPACES_KEY_ID'), 133 | 'secret' => env('DO_SPACES_SECRET_ACCESS_KEY'), 134 | 'name' => env('DO_SPACES_NAME'), 135 | 'region' => env('DO_SPACES_REGION'), 136 | 'path' => env('NATIVEPHP_UPDATER_PATH', null), 137 | ], 138 | ], 139 | ], 140 | 141 | /** 142 | * The queue workers that get auto-started on your application start. 143 | */ 144 | 'queue_workers' => [ 145 | 'default' => [ 146 | 'queues' => ['default'], 147 | 'memory_limit' => 128, 148 | 'timeout' => 60, 149 | 'sleep' => 3, 150 | ], 151 | ], 152 | 153 | /** 154 | * Define your own scripts to run before and after the build process. 155 | */ 156 | 'prebuild' => [ 157 | // 'npm run build', 158 | ], 159 | 160 | 'postbuild' => [ 161 | // 'rm -rf public/build', 162 | ], 163 | 164 | /** 165 | * Custom PHP binary path. 166 | */ 167 | 'binary_path' => env('NATIVEPHP_PHP_BINARY_PATH', null), 168 | ]; 169 | -------------------------------------------------------------------------------- /src/Fakes/WindowManagerFake.php: -------------------------------------------------------------------------------- 1 | $windows 31 | */ 32 | public function alwaysReturnWindows(array $windows): self 33 | { 34 | $this->forcedWindowReturnValues = $windows; 35 | 36 | return $this; 37 | } 38 | 39 | public function open(string $id = 'main') 40 | { 41 | $this->opened[] = $id; 42 | 43 | $this->ensureForceReturnWindowsProvided(); 44 | 45 | $matchingWindows = array_filter( 46 | $this->forcedWindowReturnValues, 47 | fn (Window $window) => $window->getId() === $id 48 | ); 49 | 50 | if (empty($matchingWindows)) { 51 | return $this->forcedWindowReturnValues[array_rand($this->forcedWindowReturnValues)]->setClient($this->client); 52 | } 53 | 54 | Assert::count($matchingWindows, 1); 55 | 56 | return Arr::first($matchingWindows)->setClient($this->client); 57 | } 58 | 59 | public function close($id = null) 60 | { 61 | $this->closed[] = $id; 62 | } 63 | 64 | public function hide($id = null) 65 | { 66 | $this->hidden[] = $id; 67 | } 68 | 69 | public function show($id = null) 70 | { 71 | $this->shown[] = $id; 72 | } 73 | 74 | public function current(): Window 75 | { 76 | $this->ensureForceReturnWindowsProvided(); 77 | 78 | return $this->forcedWindowReturnValues[array_rand($this->forcedWindowReturnValues)]; 79 | } 80 | 81 | /** 82 | * @return array 83 | */ 84 | public function all(): array 85 | { 86 | $this->ensureForceReturnWindowsProvided(); 87 | 88 | return $this->forcedWindowReturnValues; 89 | } 90 | 91 | public function get(string $id): Window 92 | { 93 | $this->ensureForceReturnWindowsProvided(); 94 | 95 | $matchingWindows = array_filter($this->forcedWindowReturnValues, fn (Window $window) => $window->getId() === $id); 96 | 97 | Assert::notEmpty($matchingWindows); 98 | Assert::count($matchingWindows, 1); 99 | 100 | return Arr::first($matchingWindows); 101 | } 102 | 103 | /** 104 | * @param string|Closure(string): bool $id 105 | */ 106 | public function assertOpened(string|Closure $id): void 107 | { 108 | if (is_callable($id) === false) { 109 | PHPUnit::assertContains($id, $this->opened); 110 | 111 | return; 112 | } 113 | 114 | $hit = empty( 115 | array_filter( 116 | $this->opened, 117 | fn (string $openedId) => $id($openedId) === true 118 | ) 119 | ) === false; 120 | 121 | PHPUnit::assertTrue($hit); 122 | } 123 | 124 | /** 125 | * @param string|Closure(string): bool $id 126 | */ 127 | public function assertClosed(string|Closure $id): void 128 | { 129 | if (is_callable($id) === false) { 130 | PHPUnit::assertContains($id, $this->closed); 131 | 132 | return; 133 | } 134 | 135 | $hit = empty( 136 | array_filter( 137 | $this->closed, 138 | fn (mixed $closedId) => $id($closedId) === true 139 | ) 140 | ) === false; 141 | 142 | PHPUnit::assertTrue($hit); 143 | } 144 | 145 | /** 146 | * @param string|Closure(string): bool $id 147 | */ 148 | public function assertHidden(string|Closure $id): void 149 | { 150 | if (is_callable($id) === false) { 151 | PHPUnit::assertContains($id, $this->hidden); 152 | 153 | return; 154 | } 155 | 156 | $hit = empty( 157 | array_filter( 158 | $this->hidden, 159 | fn (mixed $hiddenId) => $id($hiddenId) === true 160 | ) 161 | ) === false; 162 | 163 | PHPUnit::assertTrue($hit); 164 | } 165 | 166 | /** 167 | * @param string|Closure(string): bool $id 168 | */ 169 | public function assertShown(string|Closure $id): void 170 | { 171 | if (is_callable($id) === false) { 172 | PHPUnit::assertContains($id, $this->shown); 173 | 174 | return; 175 | } 176 | 177 | $hit = empty( 178 | array_filter( 179 | $this->shown, 180 | fn (mixed $shownId) => $id($shownId) === true 181 | ) 182 | ) === false; 183 | 184 | PHPUnit::assertTrue($hit); 185 | } 186 | 187 | public function assertOpenedCount(int $expected): void 188 | { 189 | PHPUnit::assertCount($expected, $this->opened); 190 | } 191 | 192 | public function assertClosedCount(int $expected): void 193 | { 194 | PHPUnit::assertCount($expected, $this->closed); 195 | } 196 | 197 | public function assertHiddenCount(int $expected): void 198 | { 199 | PHPUnit::assertCount($expected, $this->hidden); 200 | } 201 | 202 | public function assertShownCount(int $expected): void 203 | { 204 | PHPUnit::assertCount($expected, $this->shown); 205 | } 206 | 207 | private function ensureForceReturnWindowsProvided(): void 208 | { 209 | Assert::notEmpty($this->forcedWindowReturnValues, 'No windows were provided to return'); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Commands/DebugCommand.php: -------------------------------------------------------------------------------- 1 | debugInfo = collect(); 34 | intro('Generating Debug Information...'); 35 | 36 | $this->processEnvironment() 37 | ->processNativePHP(); 38 | 39 | switch ($this->argument('output')) { 40 | case 'File': 41 | $this->outputToFile(); 42 | break; 43 | case 'Clipboard': 44 | $this->outputToClipboard(); 45 | break; 46 | case 'Console': 47 | $this->outputToConsole(); 48 | break; 49 | default: 50 | error('Invalid output option specified.'); 51 | } 52 | 53 | outro('Debug Information Generated.'); 54 | } 55 | 56 | private function processEnvironment(): static 57 | { 58 | $locationCommand = 'which'; 59 | 60 | if (Environment::isWindows()) { 61 | $locationCommand = 'where'; 62 | } 63 | 64 | info('Generating Environment Data...'); 65 | $environment = [ 66 | 'PHP' => [ 67 | 'Version' => phpversion(), 68 | 'Path' => PHP_BINARY, 69 | ], 70 | 'Laravel' => [ 71 | 'Version' => app()->version(), 72 | 'ConfigCached' => $this->laravel->configurationIsCached(), 73 | 'RoutesCached' => $this->laravel->routesAreCached(), 74 | 'DebugEnabled' => $this->laravel->hasDebugModeEnabled(), 75 | ], 76 | 'Node' => [ 77 | 'Version' => trim(Process::run('node -v')->output()), 78 | 'Path' => trim(Process::run("$locationCommand node")->output()), 79 | ], 80 | 'NPM' => [ 81 | 'Version' => trim(Process::run('npm -v')->output()), 82 | 'Path' => trim(Process::run("$locationCommand npm")->output()), 83 | ], 84 | 'OperatingSystem' => PHP_OS, 85 | ]; 86 | 87 | $this->debugInfo->put('Environment', $environment); 88 | 89 | return $this; 90 | } 91 | 92 | private function processNativePHP(): static 93 | { 94 | info('Processing NativePHP Data...'); 95 | // Get composer versions 96 | $versions = collect([ 97 | 'nativephp/electron' => null, 98 | 'nativephp/laravel' => null, 99 | 'nativephp/php-bin' => null, 100 | ])->mapWithKeys(function ($version, $key) { 101 | try { 102 | $version = InstalledVersions::getVersion($key); 103 | } catch (\OutOfBoundsException) { 104 | $version = 'Not Installed'; 105 | } 106 | 107 | return [$key => $version]; 108 | }); 109 | 110 | $isNotarizationConfigured = config('nativephp-internal.notarization.apple_id') 111 | && config('nativephp-internal.notarization.apple_id_pass') 112 | && config('nativephp-internal.notarization.apple_team_id'); 113 | 114 | $isAzureTrustedSigningConfigured = config('nativephp-internal.azure_trusted_signing.tenant_id') 115 | && config('nativephp-internal.azure_trusted_signing.client_id') 116 | && config('nativephp-internal.azure_trusted_signing.client_secret') 117 | && config('nativephp-internal.azure_trusted_signing.publisher_name') 118 | && config('nativephp-internal.azure_trusted_signing.endpoint') 119 | && config('nativephp-internal.azure_trusted_signing.certificate_profile_name') 120 | && config('nativephp-internal.azure_trusted_signing.code_signing_account_name'); 121 | 122 | $this->debugInfo->put( 123 | 'NativePHP', 124 | [ 125 | 'Versions' => $versions, 126 | 'Configuration' => [ 127 | 'Provider' => config('nativephp.provider'), 128 | 'BuildHooks' => [ 129 | 'Pre' => config('nativephp.prebuild'), 130 | 'Post' => config('nativephp.postbuild'), 131 | ], 132 | 'NotarizationEnabled' => $isNotarizationConfigured, 133 | 'AzureTrustedSigningEnabled' => $isAzureTrustedSigningConfigured, 134 | 'CustomPHPBinary' => config('nativephp-internal.php_binary_path') ?? false, 135 | ], 136 | ] 137 | ); 138 | 139 | return $this; 140 | } 141 | 142 | protected function promptForMissingArgumentsUsing(): array 143 | { 144 | return [ 145 | 'output' => fn () => select( 146 | 'Where would you like to output the debug information?', 147 | ['File', 'Clipboard', 'Console'], 148 | 'File' 149 | ), 150 | ]; 151 | } 152 | 153 | private function outputToFile(): void 154 | { 155 | File::put(base_path('nativephp_debug.json'), json_encode($this->debugInfo->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 156 | note('Debug information saved to '.base_path('nativephp_debug.json')); 157 | } 158 | 159 | private function outputToConsole(): void 160 | { 161 | $this->output->writeln( 162 | print_r($this->debugInfo->toArray(), true) 163 | ); 164 | } 165 | 166 | private function outputToClipboard(): void 167 | { 168 | $json = json_encode($this->debugInfo->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 169 | 170 | // Copy json to clipboard 171 | if (Environment::isWindows()) { 172 | Process::run('echo '.escapeshellarg($json).' | clip'); 173 | } elseif (Environment::isLinux()) { 174 | Process::run('echo '.escapeshellarg($json).' | xclip -selection clipboard'); 175 | } else { 176 | Process::run('echo '.escapeshellarg($json).' | pbcopy'); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Fakes/ChildProcessFake.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public array $gets = []; 15 | 16 | /** 17 | * @var array 18 | */ 19 | public array $starts = []; 20 | 21 | /** 22 | * @var array 23 | */ 24 | public array $phps = []; 25 | 26 | /** 27 | * @var array 28 | */ 29 | public array $artisans = []; 30 | 31 | /** 32 | * @var array 33 | */ 34 | public array $stops = []; 35 | 36 | /** 37 | * @var array 38 | */ 39 | public array $restarts = []; 40 | 41 | /** 42 | * @var array 43 | */ 44 | public array $messages = []; 45 | 46 | public function get(?string $alias = null): self 47 | { 48 | $this->gets[] = $alias; 49 | 50 | return $this; 51 | } 52 | 53 | public function all(): array 54 | { 55 | return [$this]; 56 | } 57 | 58 | public function start( 59 | array|string $cmd, 60 | string $alias, 61 | ?string $cwd = null, 62 | ?array $env = null, 63 | bool $persistent = false 64 | ): self { 65 | $this->starts[] = [ 66 | 'cmd' => $cmd, 67 | 'alias' => $alias, 68 | 'cwd' => $cwd, 69 | 'env' => $env, 70 | 'persistent' => $persistent, 71 | ]; 72 | 73 | return $this; 74 | } 75 | 76 | public function php( 77 | array|string $cmd, 78 | string $alias, 79 | ?array $env = null, 80 | ?bool $persistent = false, 81 | ?array $iniSettings = null 82 | ): self { 83 | $this->phps[] = [ 84 | 'cmd' => $cmd, 85 | 'alias' => $alias, 86 | 'env' => $env, 87 | 'persistent' => $persistent, 88 | 'iniSettings' => $iniSettings, 89 | ]; 90 | 91 | return $this; 92 | } 93 | 94 | public function artisan( 95 | array|string $cmd, 96 | string $alias, 97 | ?array $env = null, 98 | ?bool $persistent = false, 99 | ?array $iniSettings = null 100 | ): self { 101 | $this->artisans[] = [ 102 | 'cmd' => $cmd, 103 | 'alias' => $alias, 104 | 'env' => $env, 105 | 'persistent' => $persistent, 106 | 'iniSettings' => $iniSettings, 107 | ]; 108 | 109 | return $this; 110 | } 111 | 112 | public function stop(?string $alias = null): void 113 | { 114 | $this->stops[] = $alias; 115 | } 116 | 117 | public function restart(?string $alias = null): self 118 | { 119 | $this->restarts[] = $alias; 120 | 121 | return $this; 122 | } 123 | 124 | public function message(string $message, ?string $alias = null): self 125 | { 126 | $this->messages[] = [ 127 | 'message' => $message, 128 | 'alias' => $alias, 129 | ]; 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * @param string|Closure(string): bool $alias 136 | */ 137 | public function assertGet(string|Closure $alias): void 138 | { 139 | if (is_callable($alias) === false) { 140 | PHPUnit::assertContains($alias, $this->gets); 141 | 142 | return; 143 | } 144 | 145 | $hit = empty( 146 | array_filter( 147 | $this->gets, 148 | fn (mixed $get) => $alias($get) === true 149 | ) 150 | ) === false; 151 | 152 | PHPUnit::assertTrue($hit); 153 | } 154 | 155 | /** 156 | * @param Closure(array|string $cmd, string $alias, ?string $cwd, ?array $env, bool $persistent): bool $callback 157 | */ 158 | public function assertStarted(Closure $callback): void 159 | { 160 | $hit = empty( 161 | array_filter( 162 | $this->starts, 163 | fn (array $started) => $callback(...$started) === true 164 | ) 165 | ) === false; 166 | 167 | PHPUnit::assertTrue($hit); 168 | } 169 | 170 | /** 171 | * @param Closure(array|string $cmd, string $alias, ?array $env, ?bool $persistent): bool $callback 172 | */ 173 | public function assertPhp(Closure $callback): void 174 | { 175 | $hit = empty( 176 | array_filter( 177 | $this->phps, 178 | fn (array $php) => $callback(...$php) === true 179 | ) 180 | ) === false; 181 | 182 | PHPUnit::assertTrue($hit); 183 | } 184 | 185 | /** 186 | * @param Closure(array|string $cmd, string $alias, ?array $env, ?bool $persistent): bool $callback 187 | */ 188 | public function assertArtisan(Closure $callback): void 189 | { 190 | $hit = empty( 191 | array_filter( 192 | $this->artisans, 193 | fn (array $artisan) => $callback(...$artisan) === true 194 | ) 195 | ) === false; 196 | 197 | PHPUnit::assertTrue($hit); 198 | } 199 | 200 | /** 201 | * @param string|Closure(string): bool $alias 202 | */ 203 | public function assertStop(string|Closure $alias): void 204 | { 205 | if (is_callable($alias) === false) { 206 | PHPUnit::assertContains($alias, $this->stops); 207 | 208 | return; 209 | } 210 | 211 | $hit = empty( 212 | array_filter( 213 | $this->stops, 214 | fn (mixed $stop) => $alias($stop) === true 215 | ) 216 | ) === false; 217 | 218 | PHPUnit::assertTrue($hit); 219 | } 220 | 221 | /** 222 | * @param string|Closure(string): bool $alias 223 | */ 224 | public function assertRestart(string|Closure $alias): void 225 | { 226 | if (is_callable($alias) === false) { 227 | PHPUnit::assertContains($alias, $this->restarts); 228 | 229 | return; 230 | } 231 | 232 | $hit = empty( 233 | array_filter( 234 | $this->restarts, 235 | fn (mixed $restart) => $alias($restart) === true 236 | ) 237 | ) === false; 238 | 239 | PHPUnit::assertTrue($hit); 240 | } 241 | 242 | /** 243 | * @param Closure(string $message, string|null $alias): bool $callback 244 | */ 245 | public function assertMessage(Closure $callback): void 246 | { 247 | $hit = empty( 248 | array_filter( 249 | $this->messages, 250 | fn (array $message) => $callback(...$message) === true 251 | ) 252 | ) === false; 253 | 254 | PHPUnit::assertTrue($hit); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/NativeServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('nativephp') 41 | ->hasCommands([ 42 | DebugCommand::class, 43 | FreshCommand::class, 44 | MigrateCommand::class, 45 | SeedDatabaseCommand::class, 46 | WipeDatabaseCommand::class, 47 | ]) 48 | ->hasConfigFile() 49 | ->hasRoute('api') 50 | ->publishesServiceProvider('NativeAppServiceProvider'); 51 | } 52 | 53 | public function packageRegistered() 54 | { 55 | $this->mergeConfigFrom($this->package->basePath('/../config/nativephp-internal.php'), 'nativephp-internal'); 56 | 57 | $this->app->singleton(FreshCommand::class, function ($app) { 58 | return new FreshCommand($app['migrator']); 59 | }); 60 | 61 | $this->app->singleton(MigrateCommand::class, function ($app) { 62 | return new MigrateCommand($app['migrator'], $app['events']); 63 | }); 64 | 65 | $this->app->bind(WindowManagerContract::class, function (Foundation $app) { 66 | return $app->make(WindowManagerImplementation::class); 67 | }); 68 | 69 | $this->app->bind(ChildProcessContract::class, function (Foundation $app) { 70 | return $app->make(ChildProcessImplementation::class); 71 | }); 72 | 73 | $this->app->bind(GlobalShortcutContract::class, function (Foundation $app) { 74 | return $app->make(GlobalShortcutImplementation::class); 75 | }); 76 | 77 | $this->app->bind(PowerMonitorContract::class, function (Foundation $app) { 78 | return $app->make(PowerMonitorImplementation::class); 79 | }); 80 | 81 | $this->app->bind(QueueWorkerContract::class, function (Foundation $app) { 82 | return $app->make(QueueWorker::class); 83 | }); 84 | 85 | if (config('nativephp-internal.running')) { 86 | $this->app->singleton( 87 | \Illuminate\Contracts\Debug\ExceptionHandler::class, 88 | Handler::class 89 | ); 90 | 91 | // Automatically prevent browser access 92 | $this->app->make(Kernel::class)->pushMiddleware( 93 | PreventRegularBrowserAccess::class, 94 | ); 95 | 96 | Application::starting(function ($app) { 97 | $app->resolveCommands([ 98 | LoadStartupConfigurationCommand::class, 99 | LoadPHPConfigurationCommand::class, 100 | MigrateCommand::class, 101 | ]); 102 | }); 103 | 104 | $this->configureApp(); 105 | } 106 | } 107 | 108 | public function bootingPackage() 109 | { 110 | if (config('nativephp-internal.running')) { 111 | $this->rewriteDatabase(); 112 | } 113 | } 114 | 115 | protected function configureApp() 116 | { 117 | if (config('app.debug')) { 118 | app(LogWatcher::class)->register(); 119 | } 120 | 121 | app(EventWatcher::class)->register(); 122 | 123 | $this->rewriteStoragePath(); 124 | 125 | $this->configureDisks(); 126 | 127 | config(['session.driver' => 'file']); 128 | config(['queue.default' => 'database']); 129 | 130 | // XXX: This logic may need to change when we ditch the internal web server 131 | if (! $this->app->runningInConsole()) { 132 | $this->fireUpQueueWorkers(); 133 | } 134 | } 135 | 136 | protected function rewriteStoragePath() 137 | { 138 | if (config('app.debug')) { 139 | return; 140 | } 141 | 142 | $oldStoragePath = $this->app->storagePath(); 143 | 144 | $this->app->useStoragePath(config('nativephp-internal.storage_path')); 145 | 146 | // Patch all config values that contain the old storage path 147 | $config = Arr::dot(config()->all()); 148 | 149 | foreach ($config as $key => $value) { 150 | if (is_string($value) && str_contains($value, $oldStoragePath)) { 151 | $newValue = str_replace($oldStoragePath, config('nativephp-internal.storage_path'), $value); 152 | config([$key => $newValue]); 153 | } 154 | } 155 | } 156 | 157 | public function rewriteDatabase() 158 | { 159 | $databasePath = config('nativephp-internal.database_path'); 160 | 161 | // Automatically create the database in development mode 162 | if (config('app.debug')) { 163 | $databasePath = database_path('nativephp.sqlite'); 164 | 165 | if (! file_exists($databasePath)) { 166 | touch($databasePath); 167 | 168 | Artisan::call('native:migrate'); 169 | } 170 | } 171 | 172 | config([ 173 | 'database.connections.nativephp' => [ 174 | 'driver' => 'sqlite', 175 | 'url' => env('DATABASE_URL'), 176 | 'database' => $databasePath, 177 | 'prefix' => '', 178 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 179 | ], 180 | ]); 181 | 182 | config(['database.default' => 'nativephp']); 183 | config(['queue.failed.database' => 'nativephp']); 184 | config(['queue.batching.database' => 'nativephp']); 185 | config(['queue.connections.database.connection' => 'nativephp']); 186 | 187 | if (file_exists($databasePath)) { 188 | DB::statement('PRAGMA journal_mode=WAL;'); 189 | DB::statement('PRAGMA busy_timeout=5000;'); 190 | } 191 | } 192 | 193 | public function removeDatabase() 194 | { 195 | $databasePath = config('nativephp-internal.database_path'); 196 | 197 | if (config('app.debug')) { 198 | $databasePath = database_path('nativephp.sqlite'); 199 | } 200 | 201 | @unlink($databasePath); 202 | @unlink($databasePath.'-shm'); 203 | @unlink($databasePath.'-wal'); 204 | } 205 | 206 | protected function configureDisks(): void 207 | { 208 | $disks = [ 209 | 'NATIVEPHP_USER_HOME_PATH' => 'user_home', 210 | 'NATIVEPHP_APP_DATA_PATH' => 'app_data', 211 | 'NATIVEPHP_USER_DATA_PATH' => 'user_data', 212 | 'NATIVEPHP_DESKTOP_PATH' => 'desktop', 213 | 'NATIVEPHP_DOCUMENTS_PATH' => 'documents', 214 | 'NATIVEPHP_DOWNLOADS_PATH' => 'downloads', 215 | 'NATIVEPHP_MUSIC_PATH' => 'music', 216 | 'NATIVEPHP_PICTURES_PATH' => 'pictures', 217 | 'NATIVEPHP_VIDEOS_PATH' => 'videos', 218 | 'NATIVEPHP_RECENT_PATH' => 'recent', 219 | 'NATIVEPHP_EXTRAS_PATH' => 'extras', 220 | ]; 221 | 222 | foreach ($disks as $env => $disk) { 223 | if (! env($env)) { 224 | continue; 225 | } 226 | 227 | config([ 228 | 'filesystems.disks.'.$disk => [ 229 | 'driver' => 'local', 230 | 'root' => env($env, ''), 231 | 'throw' => false, 232 | 'links' => 'skip', 233 | ], 234 | ]); 235 | } 236 | } 237 | 238 | protected function fireUpQueueWorkers(): void 239 | { 240 | $queueConfigs = QueueConfig::fromConfigArray(config('nativephp.queue_workers')); 241 | 242 | foreach ($queueConfigs as $queueConfig) { 243 | $this->app->make(QueueWorkerContract::class)->up($queueConfig); 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/Windows/Window.php: -------------------------------------------------------------------------------- 1 | id = $id; 82 | $this->title = config('app.name'); 83 | $this->url = url('/'); 84 | $this->showDevTools = config('app.debug'); 85 | } 86 | 87 | public function id(string $id = 'main'): self 88 | { 89 | $this->id = $id; 90 | 91 | return $this; 92 | } 93 | 94 | public function getId(): string 95 | { 96 | return $this->id; 97 | } 98 | 99 | public function title(string $title): self 100 | { 101 | $this->title = $title; 102 | 103 | if (! $this instanceof PendingOpenWindow) { 104 | $this->client->post('window/title', [ 105 | 'id' => $this->id, 106 | 'title' => $title, 107 | ]); 108 | } 109 | 110 | return $this; 111 | } 112 | 113 | public function url(string $url) 114 | { 115 | $this->defaultUrl($url); 116 | 117 | if (! $this instanceof PendingOpenWindow) { 118 | $this->client->post('window/url', [ 119 | 'id' => $this->id, 120 | 'url' => $url, 121 | ]); 122 | } 123 | 124 | return $this; 125 | } 126 | 127 | public function titleBarStyle($style): self 128 | { 129 | $this->titleBarStyle = $style; 130 | 131 | return $this; 132 | } 133 | 134 | public function titleBarHidden(): self 135 | { 136 | return $this->titleBarStyle('hidden'); 137 | } 138 | 139 | public function titleBarHiddenInset(): self 140 | { 141 | return $this->titleBarStyle('hiddenInset'); 142 | } 143 | 144 | public function trafficLightPosition(int $x, int $y): self 145 | { 146 | $this->trafficLightPosition = ['x' => $x, 'y' => $y]; 147 | 148 | return $this; 149 | } 150 | 151 | public function rememberState(): self 152 | { 153 | $this->rememberState = true; 154 | 155 | return $this; 156 | } 157 | 158 | public function frameless(): self 159 | { 160 | $this->frame = false; 161 | 162 | return $this; 163 | } 164 | 165 | public function focusable($value = true): self 166 | { 167 | $this->focusable = $value; 168 | 169 | return $this; 170 | } 171 | 172 | public function skipTaskbar($value = true): self 173 | { 174 | $this->skipTaskbar = $value; 175 | 176 | return $this; 177 | } 178 | 179 | public function hiddenInMissionControl($value = true): self 180 | { 181 | $this->hiddenInMissionControl = $value; 182 | 183 | return $this; 184 | } 185 | 186 | public function hasShadow($value = true): self 187 | { 188 | $this->hasShadow = $value; 189 | 190 | return $this; 191 | } 192 | 193 | public function titleBarButtonsOnHover(): self 194 | { 195 | return $this->titleBarStyle('customButtonsOnHover'); 196 | } 197 | 198 | public function setClient(Client $client): self 199 | { 200 | $this->client = $client; 201 | 202 | return $this; 203 | } 204 | 205 | public function alwaysOnTop(bool $alwaysOnTop = true): self 206 | { 207 | $this->alwaysOnTop = $alwaysOnTop; 208 | 209 | return $this; 210 | } 211 | 212 | public function showDevTools(bool $showDevTools = true): self 213 | { 214 | $this->showDevTools = $showDevTools; 215 | 216 | if (! $this instanceof PendingOpenWindow) { 217 | $this->client->post('window/show-dev-tools', [ 218 | 'id' => $this->id, 219 | ]); 220 | } 221 | 222 | return $this; 223 | } 224 | 225 | public function hideDevTools(): self 226 | { 227 | if (! $this instanceof PendingOpenWindow) { 228 | $this->client->post('window/hide-dev-tools', [ 229 | 'id' => $this->id, 230 | ]); 231 | } 232 | 233 | return $this; 234 | } 235 | 236 | public function devToolsOpen(): bool 237 | { 238 | return $this->devToolsOpen; 239 | } 240 | 241 | public function resizable(bool $resizable = true): static 242 | { 243 | $this->resizable = $resizable; 244 | 245 | return $this; 246 | } 247 | 248 | public function movable($movable = true): static 249 | { 250 | $this->movable = $movable; 251 | 252 | return $this; 253 | } 254 | 255 | public function minimizable($minimizable = true): static 256 | { 257 | $this->minimizable = $minimizable; 258 | 259 | return $this; 260 | } 261 | 262 | public function maximizable($maximizable = true): static 263 | { 264 | $this->maximizable = $maximizable; 265 | 266 | return $this; 267 | } 268 | 269 | public function minimized(): static 270 | { 271 | return $this->afterOpen(fn () => WindowFacade::minimize($this->id)); 272 | } 273 | 274 | public function maximized(): static 275 | { 276 | return $this->afterOpen(fn () => WindowFacade::maximize($this->id)); 277 | } 278 | 279 | public function closable(bool $closable = true): static 280 | { 281 | $this->closable = $closable; 282 | 283 | if (! $this instanceof PendingOpenWindow) { 284 | $this->client->post('window/closable', [ 285 | 'id' => $this->id, 286 | 'closable' => $closable, 287 | ]); 288 | } 289 | 290 | return $this; 291 | } 292 | 293 | public function invisibleFrameless(): self 294 | { 295 | return $this 296 | ->frameless() 297 | ->transparent() 298 | ->focusable(false) 299 | ->hasShadow(false); 300 | } 301 | 302 | public function hideMenu($autoHideMenuBar = true): static 303 | { 304 | $this->autoHideMenuBar = $autoHideMenuBar; 305 | 306 | return $this; 307 | } 308 | 309 | public function fullscreen($fullscreen = true): static 310 | { 311 | $this->fullscreen = $fullscreen; 312 | 313 | return $this; 314 | } 315 | 316 | public function fullscreenable($fullscreenable = true): static 317 | { 318 | $this->fullscreenable = $fullscreenable; 319 | 320 | return $this; 321 | } 322 | 323 | public function kiosk($kiosk = true): static 324 | { 325 | $this->kiosk = $kiosk; 326 | 327 | return $this; 328 | } 329 | 330 | public function webPreferences(array $preferences): static 331 | { 332 | $this->webPreferences = $preferences; 333 | 334 | return $this; 335 | } 336 | 337 | public function zoomFactor(float $zoomFactor = 1.0): self 338 | { 339 | $this->zoomFactor = $zoomFactor; 340 | 341 | if (! $this instanceof PendingOpenWindow) { 342 | $this->client->post('window/set-zoom-factor', [ 343 | 'id' => $this->id, 344 | 'zoomFactor' => $zoomFactor, 345 | ]); 346 | } 347 | 348 | return $this; 349 | } 350 | 351 | public function preventLeaveDomain(bool $preventLeaveDomain = true): self 352 | { 353 | $this->preventLeaveDomain = $preventLeaveDomain; 354 | 355 | return $this; 356 | } 357 | 358 | public function preventLeavePage(bool $preventLeavePage = true): self 359 | { 360 | $this->preventLeavePage = $preventLeavePage; 361 | 362 | return $this; 363 | } 364 | 365 | public function suppressNewWindows(): self 366 | { 367 | $this->suppressNewWindows = true; 368 | 369 | return $this; 370 | } 371 | 372 | public function toArray() 373 | { 374 | return [ 375 | 'id' => $this->id, 376 | 'url' => $this->url, 377 | 'x' => $this->x, 378 | 'y' => $this->y, 379 | 'rememberState' => $this->rememberState, 380 | 'width' => $this->width, 381 | 'height' => $this->height, 382 | 'minWidth' => $this->minWidth, 383 | 'minHeight' => $this->minHeight, 384 | 'maxWidth' => $this->maxWidth, 385 | 'maxHeight' => $this->maxHeight, 386 | 'focusable' => $this->focusable, 387 | 'skipTaskbar' => $this->skipTaskbar, 388 | 'hiddenInMissionControl' => $this->hiddenInMissionControl, 389 | 'hasShadow' => $this->hasShadow, 390 | 'frame' => $this->frame, 391 | 'titleBarStyle' => $this->titleBarStyle, 392 | 'trafficLightPosition' => $this->trafficLightPosition, 393 | 'showDevTools' => $this->showDevTools, 394 | 'vibrancy' => $this->vibrancy, 395 | 'transparency' => $this->transparent, 396 | 'backgroundColor' => $this->backgroundColor, 397 | 'alwaysOnTop' => $this->alwaysOnTop, 398 | 'resizable' => $this->resizable, 399 | 'movable' => $this->movable, 400 | 'minimizable' => $this->minimizable, 401 | 'maximizable' => $this->maximizable, 402 | 'closable' => $this->closable, 403 | 'title' => $this->title, 404 | 'fullscreen' => $this->fullscreen, 405 | 'fullscreenable' => $this->fullscreenable, 406 | 'kiosk' => $this->kiosk, 407 | 'autoHideMenuBar' => $this->autoHideMenuBar, 408 | 'transparent' => $this->transparent, 409 | 'webPreferences' => $this->webPreferences, 410 | 'zoomFactor' => $this->zoomFactor, 411 | 'preventLeaveDomain' => $this->preventLeaveDomain, 412 | 'preventLeavePage' => $this->preventLeavePage, 413 | 'suppressNewWindows' => $this->suppressNewWindows, 414 | ]; 415 | } 416 | 417 | public function afterOpen(callable $cb): static 418 | { 419 | $this->afterOpenCallbacks[] = $cb; 420 | 421 | return $this; 422 | } 423 | 424 | public function fromRuntimeWindow(object $window): static 425 | { 426 | foreach ($window as $key => $value) { 427 | $this->{$key} = $value; 428 | } 429 | 430 | return $this; 431 | } 432 | 433 | public function __get($var) 434 | { 435 | return $this->$var ?? null; 436 | } 437 | } 438 | --------------------------------------------------------------------------------