diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index aedbe995..29e73812 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,12 +1,31 @@ name: Bug Report -description: Report an Issue or Bug with the Package -title: "[Bug]: " +description: | + Found a bug in NativePHP? You're in the right place! labels: ["bug"] body: - type: markdown attributes: value: | - We're sorry to hear you have a problem. Can you help us solve it by providing the following details. + We're sorry to hear you have a problem. + + Before submitting your report, please make sure you've been through the section "[Debugging](https://nativephp.com/docs/getting-started/debugging)" in the docs. + + Please also ensure that you have the latest version of NativePHP packages installed, and are using [supported versions](https://nativephp.com/docs/desktop/1/getting-started/support-policy) of PHP and Laravel. + + If nothing here has helped you, please provide as much useful context as you can here to help us solve help you. + + Note that reams and reams of logs isn't helpful - please share only relevant errors. + + If possible, please prepare a reproduction repo and link to it in the Notes field. + - type: textarea + id: what-doing + attributes: + label: What were you trying to do? + description: Some context about what you were trying to achieve + placeholder: Trying to build my app for production + validations: + required: true + - type: textarea id: what-happened attributes: @@ -15,6 +34,7 @@ body: placeholder: I cannot currently do X thing because when I do, it breaks X thing. validations: required: true + - type: textarea id: how-to-reproduce attributes: @@ -23,52 +43,30 @@ body: placeholder: When I do X I see Y. validations: required: true - - type: input - id: package-version - attributes: - label: Package Version - description: What version of our Package are you running? Please be as specific as possible - placeholder: 2.0.0 - validations: - required: true - - type: input - id: php-version + + - type: textarea + id: debug attributes: - label: PHP Version - description: What version of PHP are you running? Please be as specific as possible - placeholder: 8.2.0 + label: Debug Output + description: Please provide output from the NativePHP Debug command. This will help us understand your environment and the issue you're facing. (`php artisan native:debug`) validations: required: true - - type: input - id: laravel-version - attributes: - label: Laravel Version - description: What version of Laravel are you running? Please be as specific as possible - placeholder: 9.0.0 - validations: - required: true - - type: input - id: node-version - attributes: - label: Node Version - description: What version of Node are you running? Please be as specific as possible - placeholder: '18.17' - validations: - required: true + - type: dropdown id: operating-systems attributes: - label: Which operating systems does with happen with? + label: Which operating systems have you seen this occur on? description: You may select more than one. multiple: true options: - macOS - Windows - Linux + - type: textarea id: notes attributes: label: Notes - description: Use this field to provide any other notes that you feel might be relevant to the issue. + description: Use this field to provide any other notes that you feel might be relevant to the issue. Include links to any reproduction repos you've created here. validations: required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 90b5184e..534c2acd 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,11 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - name: Ask a question - url: https://github.com/nativephp/nativephp-laravel/discussions/new?category=q-a + url: https://github.com/orgs/nativephp/discussions/new?category=q-a about: Ask the community for help - name: Request a feature - url: https://github.com/nativephp/nativephp-laravel/discussions/new?category=ideas + url: https://github.com/orgs/nativephp/discussions/new?category=ideas about: Share ideas for new features - name: Report a security issue - url: https://github.com/nativephp/nativephp-laravel/security/policy + url: https://github.com/nativephp/laravel/security/policy about: Learn how to notify us for sensitive bugs diff --git a/.github/funding.yml b/.github/funding.yml new file mode 100644 index 00000000..23b9601c --- /dev/null +++ b/.github/funding.yml @@ -0,0 +1,2 @@ +github: simonhamp +open_collective: nativephp diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index ca2197dc..1a13177d 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.6.0 + uses: dependabot/fetch-metadata@v2.4.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 7520a186..4ae67bf2 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -1,9 +1,15 @@ -name: Fix PHP code style issues +# Check and fix PHP code style issues +# Pull request: automatically fix PHP code style issues +# Main branch: only check PHP code style issues since we don't have write permission +name: Check and fix PHP code style issues on: push: paths: - '**.php' + pull_request: + paths: + - '**.php' permissions: contents: write @@ -14,14 +20,24 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: - ref: ${{ github.head_ref }} + ref: ${{ github.head_ref || github.sha }} + + - name: Check PHP code style issues + if: github.event_name == 'push' + uses: aglipanci/laravel-pint-action@2.5 + with: + verboseMode: true + testMode: true - name: Fix PHP code style issues - uses: aglipanci/laravel-pint-action@2.3.0 + if: github.event_name == 'pull_request' + uses: aglipanci/laravel-pint-action@2.5 - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v4 + if: github.event_name == 'pull_request' + uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: Fix styling + diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 9d41c0cf..871c602f 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -1,26 +1,43 @@ name: PHPStan on: + workflow_dispatch: push: - paths: - - '**.php' - - 'phpstan.neon.dist' + branches: [main] + pull_request: + branches: [main] jobs: phpstan: - name: phpstan - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + php: [8.3] + steps: - - uses: actions/checkout@v3 + + - name: Checkout code + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' - coverage: none + php-version: ${{ matrix.php }} + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- - - name: Install composer dependencies - uses: ramsey/composer-install@v2 + - name: Install Dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - - name: Run PHPStan - run: ./vendor/bin/phpstan --error-format=github + - name: Run analysis + run: ./vendor/bin/phpstan analyse --error-format=github diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 57152e11..68e6ca9b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -10,22 +10,18 @@ jobs: test: runs-on: ${{ matrix.os }} strategy: - fail-fast: true + fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - php: [8.2, 8.1] - laravel: [10.*] + php: [8.4, 8.3] + laravel: [12.*, 11.*] stability: [prefer-lowest, prefer-stable] - include: - - laravel: 10.* - testbench: 8.* - carbon: ^2.63 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -41,11 +37,14 @@ jobs: - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:${{ matrix.carbon }}" --no-interaction --no-update + composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update composer update --${{ matrix.stability }} --prefer-dist --no-interaction - name: List Installed Dependencies - run: composer show -D + run: composer show + +# - name: Debug PhpUnit version +# run: composer why phpunit/phpunit -t - name: Execute tests run: vendor/bin/pest diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 8c12ba9e..ec40921c 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: main @@ -24,7 +24,7 @@ jobs: release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: branch: main commit_message: Update CHANGELOG diff --git a/.gitignore b/.gitignore index e26945a7..3dd7896c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ composer.lock coverage docs phpunit.xml -phpstan.neon testbench.yaml vendor node_modules diff --git a/.well-known/funding-manifest-urls b/.well-known/funding-manifest-urls new file mode 100644 index 00000000..90fe782e --- /dev/null +++ b/.well-known/funding-manifest-urls @@ -0,0 +1 @@ +https://nativephp.com/funding.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b1c437c7..1e898901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,253 @@ All notable changes to `nativephp-laravel` will be documented in this file. +## 1.0.0-beta.1 - 2025-01-21 + +### What's Changed + +* Child process queue workers by @XbNz in https://github.com/NativePHP/laravel/pull/450 +* fix: static analysis by @SRWieZ in https://github.com/NativePHP/laravel/pull/452 +* feat: default notification title by @SRWieZ in https://github.com/NativePHP/laravel/pull/451 +* Fix menubar not ready by @SRWieZ in https://github.com/NativePHP/laravel/pull/453 +* Add support for Window::show() by @curtisblackwell in https://github.com/NativePHP/laravel/pull/454 +* Fix: Return type mismatch between screen facade and screen class methods. by @kondi3 in https://github.com/NativePHP/laravel/pull/463 + +### New Contributors + +* @kondi3 made their first contribution in https://github.com/NativePHP/laravel/pull/463 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.7.0...0.8.0 + +## 0.7.0 - 2024-12-19 + +### What's Changed + +* Fix Settings facade DocBloc by @SRWieZ in https://github.com/NativePHP/laravel/pull/419 +* Fake test double for WindowManager::Class by @XbNz in https://github.com/NativePHP/laravel/pull/422 +* Child process test double by @XbNz in https://github.com/NativePHP/laravel/pull/430 +* fix: Notification facade docbloc by @SRWieZ in https://github.com/NativePHP/laravel/pull/428 +* Improvements to window test doubles by @XbNz in https://github.com/NativePHP/laravel/pull/426 +* fix: child process cmd: option except iterable array by @SRWieZ in https://github.com/NativePHP/laravel/pull/429 +* feat: improve Settings by @SRWieZ in https://github.com/NativePHP/laravel/pull/432 +* Dock goodies by @simonhamp in https://github.com/NativePHP/laravel/pull/421 +* MenuBars continued by @simonhamp in https://github.com/NativePHP/laravel/pull/420 +* Global shortcut test double by @XbNz in https://github.com/NativePHP/laravel/pull/436 +* Menu improvements by @simonhamp in https://github.com/NativePHP/laravel/pull/423 +* fix: database migration on first launch by @SRWieZ in https://github.com/NativePHP/laravel/pull/439 +* Fixes and improvements to powerMonitor by @SRWieZ in https://github.com/NativePHP/laravel/pull/445 +* feat: phpstan level 5 by @SRWieZ in https://github.com/NativePHP/laravel/pull/446 + +### New Contributors + +* @SRWieZ made their first contribution in https://github.com/NativePHP/laravel/pull/419 +* @XbNz made their first contribution in https://github.com/NativePHP/laravel/pull/422 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.6.4...0.7.0 + +## 0.6.4 - 2024-11-17 + +### What's Changed + +* Fix some DB stuff by @simonhamp in https://github.com/NativePHP/laravel/pull/413 +* Add dedicated PHP ChildProcess endpoint by @gwleuverink in https://github.com/NativePHP/laravel/pull/414 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.6.3...0.6.4 + +## 0.6.3 - 2024-11-14 + +### What's Changed + +* Fix native:migrate:fresh by @miagg in https://github.com/NativePHP/laravel/pull/408 +* Fix accessing window properties by @simonhamp in https://github.com/NativePHP/laravel/pull/410 +* MenuBar improvements by @simonhamp in https://github.com/NativePHP/laravel/pull/411 + +### New Contributors + +* @miagg made their first contribution in https://github.com/NativePHP/laravel/pull/408 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.6.2...0.6.3 + +## 0.6.2 - 2024-11-13 + +### What's Changed + +* Enable WAL mode in SQLite by @simonhamp in https://github.com/NativePHP/laravel/pull/405 +* Migrate the dev DB when created by @simonhamp in https://github.com/NativePHP/laravel/pull/406 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.6.1...0.6.2 + +## 0.6.1 - 2024-11-04 + +### What's Changed + +* Add a function to the window manager in order to list all open windows by @JCombee in https://github.com/NativePHP/laravel/pull/396 +* Add exception handler by @simonhamp in https://github.com/NativePHP/laravel/pull/398 + +### New Contributors + +* @JCombee made their first contribution in https://github.com/NativePHP/laravel/pull/396 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.6.0...0.6.1 + +## 0.6.0 - 2024-11-01 + +### What's Changed + +* Skip links as supporting library doesn't support that by @danielpetrica in https://github.com/NativePHP/laravel/pull/378 +* Add `getCenterOfActiveScreen` in Screen class by @danielpetrica in https://github.com/NativePHP/laravel/pull/375 +* Improved window management by @simonhamp in https://github.com/NativePHP/laravel/pull/391 +* Child processes by @simonhamp and @gwleuverink in https://github.com/NativePHP/laravel/pull/389 + +### New Contributors + +* @danielpetrica made their first contribution in https://github.com/NativePHP/laravel/pull/378 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.5.7...0.6.0 + +## 0.5.7 - 2024-09-16 + +### What's Changed + +* Feature: Broadcast custom Events on IPC by @gwleuverink in https://github.com/NativePHP/laravel/pull/367 + +### New Contributors + +* @gwleuverink made their first contribution in https://github.com/NativePHP/laravel/pull/367 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.5.6...0.5.7 + +## 0.5.6 - 2024-09-09 + +### What's Changed + +* Fix chainable by @simonhamp in https://github.com/NativePHP/laravel/pull/360 +* Fixed Tests by @RobertWesner in https://github.com/NativePHP/laravel/pull/362 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.5.5...0.5.6 + +## 0.5.5 - 2024-09-02 + +### What's Changed + +* Battery/AC power, plus more by @danjohnson95 in https://github.com/NativePHP/laravel/pull/355 +* Safe storage by @simonhamp in https://github.com/NativePHP/laravel/pull/357 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.5.4...0.5.5 + +## 0.5.4 - 2024-08-24 + +### What's Changed + +* Add fullscreenable support by @simonhamp in https://github.com/NativePHP/laravel/pull/340 +* Implemented minimizing by @RobertWesner in https://github.com/NativePHP/laravel/pull/347 +* Fluent API for opening windows maximized/minimized by @simonhamp in https://github.com/NativePHP/laravel/pull/349 + +### New Contributors + +* @RobertWesner made their first contribution in https://github.com/NativePHP/laravel/pull/347 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.5.3...0.5.4 + +## 0.5.3 - 2024-07-18 + +### What's Changed + +* Add the reload() method to the WindowManager by @shomisha in https://github.com/NativePHP/laravel/pull/294 +* Add `trafficLightPosition()` method on Window by @sarukomine in https://github.com/NativePHP/laravel/pull/310 +* Add a new `System::timezone` helper to detect and normalise system time zone data +* Fix `native:migrate:fresh` so that it behaves more like Laravel's `migrate:fresh` (e.g. you can use seeders etc) +* Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/NativePHP/laravel/pull/333 + +### New Contributors + +* @shomisha made their first contribution in https://github.com/NativePHP/laravel/pull/294 +* @sarukomine made their first contribution in https://github.com/NativePHP/laravel/pull/310 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.5.2...0.5.3 + +## 0.5.2 - 2024-05-02 + +### What's Changed + +* Fixes a regression introduced in 0.5.1 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.5.1...0.5.2 + +## 0.5.1 - 2024-05-02 + +### What's Changed + +* Add a default `NATIVEPHP_APP_ID` to fix #284 +* Fix for #175 `native:migrate:fresh` command @me-shaon in https://github.com/NativePHP/laravel/pull/198 +* Add hide window functionality by @braceyourself in https://github.com/NativePHP/laravel/pull/144 +* Update Tests workflow by @milwad-dev in https://github.com/NativePHP/laravel/pull/265 +* Bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/NativePHP/laravel/pull/255 +* Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 by @dependabot in https://github.com/NativePHP/laravel/pull/260 +* Bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 by @dependabot in https://github.com/NativePHP/laravel/pull/268 +* Bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 by @dependabot in https://github.com/NativePHP/laravel/pull/275 + +### New Contributors + +* @braceyourself made their first contribution in https://github.com/NativePHP/laravel/pull/144 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.5.0...0.6.0 + +## 0.5.0 - 2024-04-01 + +### What's Changed + +* Add Laravel 11 support by @meliani in https://github.com/NativePHP/laravel/pull/262 +* Add printToPDF function by @basst85 in https://github.com/NativePHP/laravel/pull/104 +* Add method to update an existing context-menu of the menu bar by @bbredewold in https://github.com/NativePHP/laravel/pull/108 +* Add required config for using GitHub as an updater provider by @danjohnson95 in https://github.com/NativePHP/laravel/pull/189 +* Add ability to exclude files and entire folders from built application by @nexxai in https://github.com/NativePHP/laravel/pull/165 +* Add ability to use string events by @LukeTowers in https://github.com/NativePHP/laravel/pull/67 +* Fix 'native:db:seed' command not working by @me-shaon in https://github.com/NativePHP/laravel/pull/199 +* Fix an issue that prevented setting the position on open windows by @curtisblackwell in https://github.com/NativePHP/laravel/pull/215 +* Fix PHP Fatal Error deleteDirectoryRecursive function in MinifyApplicationCommand by @LunashaGit in https://github.com/NativePHP/laravel/pull/249 +* Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/NativePHP/laravel/pull/208 +* Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/NativePHP/laravel/pull/217 +* Bump aglipanci/laravel-pint-action from 2.3.0 to 2.3.1 by @dependabot in https://github.com/NativePHP/laravel/pull/240 + +### New Contributors + +* @me-shaon made their first contribution in https://github.com/NativePHP/laravel/pull/199 +* @bbredewold made their first contribution in https://github.com/NativePHP/laravel/pull/108 +* @danjohnson95 made their first contribution in https://github.com/NativePHP/laravel/pull/189 +* @curtisblackwell made their first contribution in https://github.com/NativePHP/laravel/pull/215 +* @nexxai made their first contribution in https://github.com/NativePHP/laravel/pull/165 +* @LunashaGit made their first contribution in https://github.com/NativePHP/laravel/pull/249 +* @LukeTowers made their first contribution in https://github.com/NativePHP/laravel/pull/67 +* @meliani made their first contribution in https://github.com/NativePHP/laravel/pull/262 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.4.0...0.5.0 + +## 0.4.0 - 2023-08-09 + +### What's Changed + +- Added the ability to remove custom .env keys when bundling the application +- Allow custom php.ini settings by @mpociot in https://github.com/NativePHP/laravel/pull/98 +- Add method to configure disks by @mpociot in https://github.com/NativePHP/laravel/pull/99 +- Printer support by @mpociot in https://github.com/NativePHP/laravel/pull/103 +- Adds 'php artisan native:migrate fresh' command by @shanerbaner82 in https://github.com/NativePHP/laravel/pull/81 +- Add Tests For `Windows` by @milwad-dev in https://github.com/NativePHP/laravel/pull/100 +- import VerifyCsrfToken and refactor by @JaberWiki in https://github.com/NativePHP/laravel/pull/110 +- Modified return type of clipboard image method by @blankRSD in https://github.com/NativePHP/laravel/pull/111 +- add windowPosition by @DanielHudson in https://github.com/NativePHP/laravel/pull/112 +- Implement MenuBarDroppedFiles event by @ArondeParon in https://github.com/NativePHP/laravel/pull/113 + +### New Contributors + +- @mpociot made their first contribution in https://github.com/NativePHP/laravel/pull/98 +- @shanerbaner82 made their first contribution in https://github.com/NativePHP/laravel/pull/81 +- @JaberWiki made their first contribution in https://github.com/NativePHP/laravel/pull/110 +- @blankRSD made their first contribution in https://github.com/NativePHP/laravel/pull/111 +- @DanielHudson made their first contribution in https://github.com/NativePHP/laravel/pull/112 +- @ArondeParon made their first contribution in https://github.com/NativePHP/laravel/pull/113 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.3.0...0.4.0 + ## 0.3.0 - 2023-07-31 ### What's Changed diff --git a/README.md b/README.md index 04e464d7..c1284be9 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,16 @@ Check out the [Getting Started](https://nativephp.com/docs/1/getting-started/int - [Application Lifecycle](https://nativephp.com/docs/1/the-basics/app-lifecycle) - [Contributing Guide](https://github.com/NativePHP/laravel/blob/main/CONTRIBUTING.md) +## Sponsors + +Thanks to the following sponsors for funding NativePHP development. Please consider [sponsoring](https://nativephp.com/docs/getting-started/sponsoring). + +- [BeyondCode](https://beyondco.de/?utm_source=nativephp-docs&utm_medium=logo&utm_campaign=nativephp) - Essential tools for web developers. +- [Laradevs](https://laradevs.com/?ref=nativephp-docs) - Connecting the best Laravel Developers with the best Laravel Teams. +- [RedGalaxy](https://www.redgalaxy.co.uk) - A web application development studio based in Cambridgeshire, building solutions to help businesses improve efficiency and profitability. +- [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. +- [KaasHosting](https://www.kaashosting.nl/?lang=en) - Minecraft Server and VPS hosting from The Netherlands. + ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. @@ -33,6 +43,7 @@ Please review [our security policy](../../security/policy) on how to report secu ## Credits - [Marcel Pociot](https://github.com/mpociot) +- [Simon Hamp](https://github.com/simonhamp) - [All Contributors](../../contributors) ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..da9c516d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/composer.json b/composer.json index be7f4a1c..7d55ade2 100644 --- a/composer.json +++ b/composer.json @@ -8,36 +8,51 @@ ], "homepage": "https://github.com/nativephp/laravel", "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/simonhamp" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/nativephp" + } + ], "authors": [ { "name": "Marcel Pociot", "email": "marcel@beyondco.de", "role": "Developer" + }, + { + "name": "Simon Hamp", + "email": "simon.hamp@me.com", + "role": "Developer" } ], "require": { - "php": "^8.1", - "illuminate/contracts": "^10.0", - "spatie/laravel-package-tools": "^1.14.0", - "symfony/finder": "^6.2" + "php": "^8.3", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "spatie/laravel-package-tools": "^1.16.4", + "symfony/finder": "^6.2|^7.0" }, "require-dev": { + "guzzlehttp/guzzle": "^7.0", "laravel/pint": "^1.0", - "nunomaduro/collision": "^7.9", - "nunomaduro/larastan": "^2.0.1", - "orchestra/testbench": "^8.0", - "pestphp/pest": "^2.0", - "pestphp/pest-plugin-arch": "^2.0", - "pestphp/pest-plugin-laravel": "^2.0", + "larastan/larastan": "^2.0|^3.1", + "nunomaduro/collision": "^7.11|^8.1.1", + "orchestra/testbench": "^8.0|^9.0|^10.0", + "pestphp/pest": "^v2.30|^3.0", + "pestphp/pest-plugin-arch": "^2.0|^3.0", + "pestphp/pest-plugin-laravel": "^2.0|^3.1", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", + "phpstan/phpstan-phpunit": "^1.0|^2.0", "spatie/laravel-ray": "^1.26" }, "autoload": { "psr-4": { - "Native\\Laravel\\": "src/", - "Native\\Laravel\\Database\\Factories\\": "database/factories/" + "Native\\Laravel\\": "src/" } }, "autoload-dev": { @@ -46,6 +61,11 @@ } }, "scripts": { + "qa" : [ + "@composer format", + "@composer analyse", + "@composer test" + ], "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", "analyse": "vendor/bin/phpstan analyse", "test": "vendor/bin/pest", @@ -65,11 +85,22 @@ "Native\\Laravel\\NativeServiceProvider" ], "aliases": { + "ChildProcess": "Native\\Laravel\\Facades\\ChildProcess", + "Clipboard": "Native\\Laravel\\Facades\\Clipboard", "ContextMenu": "Native\\Laravel\\Facades\\ContextMenu", "Dock": "Native\\Laravel\\Facades\\Dock", + "GlobalShortcut": "Native\\Laravel\\Facades\\GlobalShortcut", + "Menu": "Native\\Laravel\\Facades\\Menu", + "MenuBar": "Native\\Laravel\\Facades\\MenuBar", + "Notification": "Native\\Laravel\\Facades\\Notification", + "PowerMonitor": "Native\\Laravel\\Facades\\PowerMonitor", "Process": "Native\\Laravel\\Facades\\Process", - "Window": "Native\\Laravel\\Facades\\Window", - "Clipboard": "Native\\Laravel\\Facades\\Clipboard" + "QueueWorker": "Native\\Laravel\\Facades\\QueueWorker", + "Screen": "Native\\Laravel\\Facades\\Screen", + "Settings": "Native\\Laravel\\Facades\\Settings", + "Shell": "Native\\Laravel\\Facades\\Shell", + "System": "Native\\Laravel\\Facades\\System", + "Window": "Native\\Laravel\\Facades\\Window" } } }, diff --git a/config/nativephp-internal.php b/config/nativephp-internal.php index 4210df92..bb7dbf23 100644 --- a/config/nativephp-internal.php +++ b/config/nativephp-internal.php @@ -29,4 +29,27 @@ * The URL to the NativePHP API. */ 'api_url' => env('NATIVEPHP_API_URL', 'http://localhost:4000/api/'), + + /** + * Configuration for the Zephpyr API. + */ + 'zephpyr' => [ + 'host' => env('ZEPHPYR_HOST', 'https://zephpyr.com'), + 'token' => env('ZEPHPYR_TOKEN'), + 'key' => env('ZEPHPYR_KEY'), + ], + + /** + * The credentials to use Apples Notarization service. + */ + 'notarization' => [ + 'apple_id' => env('NATIVEPHP_APPLE_ID'), + 'apple_id_pass' => env('NATIVEPHP_APPLE_ID_PASS'), + 'apple_team_id' => env('NATIVEPHP_APPLE_TEAM_ID'), + ], + + /** + * The binary path of PHP for NativePHP to use at build. + */ + 'php_binary_path' => env('NATIVEPHP_PHP_BINARY_PATH'), ]; diff --git a/config/nativephp.php b/config/nativephp.php index 8d951f07..d68245fc 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -13,7 +13,7 @@ * usually in the form of a reverse domain name. * For example: com.nativephp.app */ - 'app_id' => env('NATIVEPHP_APP_ID'), + 'app_id' => env('NATIVEPHP_APP_ID', 'com.nativephp.app'), /** * If your application allows deep linking, you can specify the scheme @@ -31,6 +31,21 @@ */ 'author' => env('NATIVEPHP_APP_AUTHOR'), + /** + * The copyright notice for your application. + */ + 'copyright' => env('NATIVEPHP_APP_COPYRIGHT'), + + /** + * The description of your application. + */ + 'description' => env('NATIVEPHP_APP_DESCRIPTION', 'An awesome app built with NativePHP'), + + /** + * The Website of your application. + */ + 'website' => env('NATIVEPHP_APP_WEBSITE', 'https://nativephp.com'), + /** * The default service provider for your application. This provider * takes care of bootstrapping your application and configuring @@ -38,6 +53,36 @@ */ 'provider' => \App\Providers\NativeAppServiceProvider::class, + /** + * A list of environment keys that should be removed from the + * .env file when the application is bundled for production. + * You may use wildcards to match multiple keys. + */ + 'cleanup_env_keys' => [ + 'AWS_*', + 'GITHUB_*', + 'DO_SPACES_*', + '*_SECRET', + 'ZEPHPYR_*', + 'NATIVEPHP_UPDATER_PATH', + 'NATIVEPHP_APPLE_ID', + 'NATIVEPHP_APPLE_ID_PASS', + 'NATIVEPHP_APPLE_TEAM_ID', + ], + + /** + * A list of files and folders that should be removed from the + * final app before it is bundled for production. + * You may use glob / wildcard patterns here. + */ + 'cleanup_exclude_files' => [ + 'build', + 'temp', + 'content', + 'node_modules', + '*/tests', + ], + /** * The NativePHP updater configuration. */ @@ -51,11 +96,22 @@ /** * The updater provider to use. - * Supported: "s3", "spaces" + * Supported: "github", "s3", "spaces" */ 'default' => env('NATIVEPHP_UPDATER_PROVIDER', 'spaces'), 'providers' => [ + 'github' => [ + 'driver' => 'github', + 'repo' => env('GITHUB_REPO'), + 'owner' => env('GITHUB_OWNER'), + 'token' => env('GITHUB_TOKEN'), + 'vPrefixedTagName' => env('GITHUB_V_PREFIXED_TAG_NAME', true), + 'private' => env('GITHUB_PRIVATE', false), + 'channel' => env('GITHUB_CHANNEL', 'latest'), + 'releaseType' => env('GITHUB_RELEASE_TYPE', 'draft'), + ], + 's3' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), @@ -76,4 +132,32 @@ ], ], ], + + /** + * The queue workers that get auto-started on your application start. + */ + 'queue_workers' => [ + 'default' => [ + 'queues' => ['default'], + 'memory_limit' => 128, + 'timeout' => 60, + 'sleep' => 3, + ], + ], + + /** + * Define your own scripts to run before and after the build process. + */ + 'prebuild' => [ + // 'npm run build', + ], + + 'postbuild' => [ + // 'rm -rf public/build', + ], + + /** + * Custom PHP binary path. + */ + 'binary_path' => env('NATIVEPHP_PHP_BINARY_PATH', null), ]; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index e69de29b..00000000 diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..b6543b0b --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,21 @@ +parameters: + + paths: + - src/ + - config/ +# - tests/ + + + # Level 9 is the highest level + level: 5 + + + noEnvCallsOutsideOfConfig: false # Don't know why he doesn't consider our config/ directory as config + + ignoreErrors: + - '#Class App\\Providers\\NativeAppServiceProvider not found#' + - '#Class Native\\Laravel\\ChildProcess has an uninitialized readonly property#' + + + excludePaths: + - ./src/NativeServiceProvider.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index a91953bd..00000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,14 +0,0 @@ -includes: - - phpstan-baseline.neon - -parameters: - level: 4 - paths: - - src - - config - - database - tmpDir: build/phpstan - checkOctaneCompatibility: true - checkModelProperties: true - checkMissingIterableValueType: false - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8b18acdc..266bd0e1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,36 +1,35 @@ - - - - tests - - - - - ./src - - - - - - - - - - + + + + tests + + + + + + + + + + + + + + + ./src + + diff --git a/routes/api.php b/routes/api.php index c7a5d5af..18df0ab4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@ PreventRegularBrowserAccess::class], function () { Route::post('_native/api/booted', NativeAppBootedController::class); Route::post('_native/api/events', DispatchEventFromAppController::class); -})->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class); +})->withoutMiddleware(VerifyCsrfToken::class); Route::get('_native/api/cookie', CreateSecurityCookieController::class); diff --git a/src/Alert.php b/src/Alert.php new file mode 100644 index 00000000..1303721c --- /dev/null +++ b/src/Alert.php @@ -0,0 +1,94 @@ +type = $type; + + return $this; + } + + public function title(string $title): self + { + $this->title = $title; + + return $this; + } + + public function detail(string $detail): self + { + $this->detail = $detail; + + return $this; + } + + public function buttons(array $buttons): self + { + $this->buttons = $buttons; + + return $this; + } + + public function defaultId(int $defaultId): self + { + $this->defaultId = $defaultId; + + return $this; + } + + public function cancelId(int $cancelId): self + { + $this->cancelId = $cancelId; + + return $this; + } + + public function show(string $message): int + { + $response = $this->client->post('alert/message', [ + 'message' => $message, + 'type' => $this->type, + 'title' => $this->title, + 'detail' => $this->detail, + 'buttons' => $this->buttons, + 'defaultId' => $this->defaultId, + 'cancelId' => $this->cancelId, + ]); + + return (int) $response->json('result'); + } + + public function error(string $title, string $message): bool + { + $response = $this->client->post('alert/error', [ + 'title' => $title, + 'message' => $message, + ]); + + return (bool) $response->json('result'); + } +} diff --git a/src/App.php b/src/App.php index 15fba4e6..f7fa1c3a 100644 --- a/src/App.php +++ b/src/App.php @@ -3,11 +3,20 @@ namespace Native\Laravel; use Native\Laravel\Client\Client; +use Phar; class App { - public function __construct(protected Client $client) + public function __construct(protected Client $client) {} + + public function quit(): void + { + $this->client->post('app/quit'); + } + + public function relaunch(): void { + $this->client->post('app/relaunch'); } public function focus(): void @@ -59,4 +68,23 @@ public function clearRecentDocuments(): void { $this->client->delete('app/recent-documents'); } + + public function isRunningBundled(): bool + { + return Phar::running() !== ''; + + } + + public function openAtLogin(?bool $open = null): bool + { + if ($open === null) { + return (bool) $this->client->get('app/open-at-login')->json('open'); + } + + $this->client->post('app/open-at-login', [ + 'open' => $open, + ]); + + return $open; + } } diff --git a/src/AutoUpdater.php b/src/AutoUpdater.php new file mode 100644 index 00000000..79500455 --- /dev/null +++ b/src/AutoUpdater.php @@ -0,0 +1,25 @@ +client->post('auto-updater/check-for-updates'); + } + + public function quitAndInstall(): void + { + $this->client->post('auto-updater/quit-and-install'); + } + + public function downloadUpdate(): void + { + $this->client->post('auto-updater/download-update'); + } +} diff --git a/src/ChildProcess.php b/src/ChildProcess.php new file mode 100644 index 00000000..6ba99721 --- /dev/null +++ b/src/ChildProcess.php @@ -0,0 +1,161 @@ +alias; + + $process = $this->client->get("child-process/get/{$alias}")->json(); + + if (! $process) { + return null; + } + + return $this->fromRuntimeProcess($process); + } + + public function all(): array + { + $processes = $this->client->get('child-process/')->json(); + + if (empty($processes)) { + return []; + } + + $hydrated = []; + + foreach ($processes as $alias => $process) { + $hydrated[$alias] = (new static($this->client)) + ->fromRuntimeProcess($process); + } + + return $hydrated; + } + + /** + * @param string|string[] $cmd + * @return $this + */ + public function start( + string|array $cmd, + string $alias, + ?string $cwd = null, + ?array $env = null, + bool $persistent = false + ): self { + $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; + + $process = $this->client->post('child-process/start', [ + 'alias' => $alias, + 'cmd' => $cmd, + 'cwd' => $cwd ?? base_path(), + 'env' => $env, + 'persistent' => $persistent, + ])->json(); + + return $this->fromRuntimeProcess($process); + } + + /** + * @param string|string[] $cmd + * @return $this + */ + public function php(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false, ?array $iniSettings = null): self + { + $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; + + $process = $this->client->post('child-process/start-php', [ + 'alias' => $alias, + 'cmd' => $cmd, + 'cwd' => base_path(), + 'env' => $env, + 'persistent' => $persistent, + 'iniSettings' => $iniSettings, + ])->json(); + + return $this->fromRuntimeProcess($process); + } + + /** + * @param string|string[] $cmd + * @return $this + */ + public function artisan(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false, ?array $iniSettings = null): self + { + $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; + + $cmd = ['artisan', ...$cmd]; + + return $this->php($cmd, $alias, env: $env, persistent: $persistent, iniSettings: $iniSettings); + } + + public function stop(?string $alias = null): void + { + $this->client->post('child-process/stop', [ + 'alias' => $alias ?? $this->alias, + ])->json(); + } + + public function restart(?string $alias = null): ?self + { + $process = $this->client->post('child-process/restart', [ + 'alias' => $alias ?? $this->alias, + ])->json(); + + if (! $process) { + return null; + } + + return $this->fromRuntimeProcess($process); + } + + public function message(string $message, ?string $alias = null): self + { + $this->client->post('child-process/message', [ + 'alias' => $alias ?? $this->alias, + 'message' => $message, + ])->json(); + + return $this; + } + + protected function fromRuntimeProcess($process) + { + if (isset($process['pid'])) { + // @phpstan-ignore-next-line + $this->pid = $process['pid']; + } + + foreach ($process['settings'] as $key => $value) { + if (! property_exists($this, $key)) { + throw new \RuntimeException("Property {$key} does not exist on ".__CLASS__); + } + + $this->{$key} = $value; + } + + return $this; + } +} diff --git a/src/Client/Client.php b/src/Client/Client.php index e444ea4a..9a8ec815 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -21,9 +21,9 @@ public function __construct() ->asJson(); } - public function get(string $endpoint): Response + public function get(string $endpoint, array|string|null $query = null): Response { - return $this->client->get($endpoint); + return $this->client->get($endpoint, $query); } public function post(string $endpoint, array $data = []): Response diff --git a/src/Clipboard.php b/src/Clipboard.php index a91400ac..22884bce 100644 --- a/src/Clipboard.php +++ b/src/Clipboard.php @@ -6,9 +6,7 @@ class Clipboard { - public function __construct(protected Client $client) - { - } + public function __construct(protected Client $client) {} public function clear() { @@ -41,7 +39,7 @@ public function html($html = null): string return $html; } - public function image($image = null): string + public function image($image = null): ?string { if (is_null($image)) { return $this->client->get('clipboard/image')->json('image'); diff --git a/src/Commands/DebugCommand.php b/src/Commands/DebugCommand.php new file mode 100644 index 00000000..2bb7633d --- /dev/null +++ b/src/Commands/DebugCommand.php @@ -0,0 +1,167 @@ +debugInfo = collect(); + intro('Generating Debug Information...'); + + $this->processEnvironment() + ->processNativePHP(); + + switch ($this->argument('output')) { + case 'File': + $this->outputToFile(); + break; + case 'Clipboard': + $this->outputToClipboard(); + break; + case 'Console': + $this->outputToConsole(); + break; + default: + error('Invalid output option specified.'); + } + + outro('Debug Information Generated.'); + } + + private function processEnvironment(): static + { + $locationCommand = 'which'; + + if (Environment::isWindows()) { + $locationCommand = 'where'; + } + + info('Generating Environment Data...'); + $environment = [ + 'PHP' => [ + 'Version' => phpversion(), + 'Path' => PHP_BINARY, + ], + 'Laravel' => [ + 'Version' => app()->version(), + 'ConfigCached' => $this->laravel->configurationIsCached(), + 'RoutesCached' => $this->laravel->routesAreCached(), + 'DebugEnabled' => $this->laravel->hasDebugModeEnabled(), + ], + 'Node' => [ + 'Version' => trim(Process::run('node -v')->output()), + 'Path' => trim(Process::run("$locationCommand node")->output()), + ], + 'NPM' => [ + 'Version' => trim(Process::run('npm -v')->output()), + 'Path' => trim(Process::run("$locationCommand npm")->output()), + ], + 'OperatingSystem' => PHP_OS, + ]; + + $this->debugInfo->put('Environment', $environment); + + return $this; + } + + private function processNativePHP(): static + { + info('Processing NativePHP Data...'); + // Get composer versions + $versions = collect([ + 'nativephp/electron' => null, + 'nativephp/laravel' => null, + 'nativephp/php-bin' => null, + ])->mapWithKeys(function ($version, $key) { + try { + $version = InstalledVersions::getVersion($key); + } catch (\OutOfBoundsException) { + $version = 'Not Installed'; + } + + return [$key => $version]; + }); + + $isNotarizationConfigured = config('nativephp-internal.notarization.apple_id') + && config('nativephp-internal.notarization.apple_id_pass') + && config('nativephp-internal.notarization.apple_team_id'); + + $this->debugInfo->put( + 'NativePHP', + [ + 'Versions' => $versions, + 'Configuration' => [ + 'Provider' => config('nativephp.provider'), + 'BuildHooks' => [ + 'Pre' => config('nativephp.prebuild'), + 'Post' => config('nativephp.postbuild'), + ], + 'NotarizationEnabled' => $isNotarizationConfigured, + 'CustomPHPBinary' => config('nativephp-internal.php_binary_path') ?? false, + ], + ] + ); + + return $this; + } + + protected function promptForMissingArgumentsUsing(): array + { + return [ + 'output' => fn () => select( + 'Where would you like to output the debug information?', + ['File', 'Clipboard', 'Console'], + 'File' + ), + ]; + } + + private function outputToFile(): void + { + File::put(base_path('nativephp_debug.json'), json_encode($this->debugInfo->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + note('Debug information saved to '.base_path('nativephp_debug.json')); + } + + private function outputToConsole(): void + { + $this->output->writeln( + print_r($this->debugInfo->toArray(), true) + ); + } + + private function outputToClipboard(): void + { + $json = json_encode($this->debugInfo->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + // Copy json to clipboard + if (Environment::isWindows()) { + Process::run('echo '.escapeshellarg($json).' | clip'); + } elseif (Environment::isLinux()) { + Process::run('echo '.escapeshellarg($json).' | xclip -selection clipboard'); + } else { + Process::run('echo '.escapeshellarg($json).' | pbcopy'); + } + } +} diff --git a/src/Commands/FreshCommand.php b/src/Commands/FreshCommand.php index 843822c8..96746c61 100644 --- a/src/Commands/FreshCommand.php +++ b/src/Commands/FreshCommand.php @@ -2,21 +2,23 @@ namespace Native\Laravel\Commands; -use Illuminate\Console\Command; +use Illuminate\Database\Console\Migrations\FreshCommand as BaseFreshCommand; use Native\Laravel\NativeServiceProvider; -class FreshCommand extends Command +class FreshCommand extends BaseFreshCommand { - protected $description = 'Run the database migrations in the NativePHP development environment'; + protected $name = 'native:migrate:fresh'; - protected $signature = 'native:migrate fresh'; + protected $description = 'Drop all tables and re-run all migrations in the NativePHP development environment'; public function handle() { - unlink(config('nativephp-internal.database_path')); + $nativeServiceProvider = new NativeServiceProvider($this->laravel); - (new NativeServiceProvider($this->laravel))->rewriteDatabase(); + $nativeServiceProvider->removeDatabase(); - $this->call('native:migrate'); + $nativeServiceProvider->rewriteDatabase(); + + return parent::handle(); } } diff --git a/src/Commands/LoadPHPConfigurationCommand.php b/src/Commands/LoadPHPConfigurationCommand.php index b3b7a714..1a2ba579 100644 --- a/src/Commands/LoadPHPConfigurationCommand.php +++ b/src/Commands/LoadPHPConfigurationCommand.php @@ -14,6 +14,8 @@ public function handle() /** @var ProvidesPhpIni $provider */ $provider = app(config('nativephp.provider')); $phpIni = []; + + /* * @phpstan-ignore-next-line */ if (method_exists($provider, 'phpIni')) { $phpIni = $provider->phpIni(); } diff --git a/src/Commands/MigrateCommand.php b/src/Commands/MigrateCommand.php index ebf8522d..9093d671 100644 --- a/src/Commands/MigrateCommand.php +++ b/src/Commands/MigrateCommand.php @@ -22,6 +22,6 @@ public function handle() { (new NativeServiceProvider($this->laravel))->rewriteDatabase(); - parent::handle(); + return parent::handle(); } } diff --git a/src/Commands/MinifyApplicationCommand.php b/src/Commands/MinifyApplicationCommand.php deleted file mode 100644 index 85260893..00000000 --- a/src/Commands/MinifyApplicationCommand.php +++ /dev/null @@ -1,36 +0,0 @@ -argument('app')); - - if (! is_dir($appPath)) { - $this->error('The app path is not a directory'); - - return; - } - - $this->info('Minifying application…'); - - $compactor = new \Native\Laravel\Compactor\Php(); - - $phpFiles = Finder::create() - ->files() - ->name('*.php') - ->in($appPath); - - foreach ($phpFiles as $phpFile) { - $minifiedContent = $compactor->compact($phpFile->getRealPath(), $phpFile->getContents()); - file_put_contents($phpFile->getRealPath(), $minifiedContent); - } - } -} diff --git a/src/Commands/SeedDatabaseCommand.php b/src/Commands/SeedDatabaseCommand.php index c83879e7..cdc032e0 100644 --- a/src/Commands/SeedDatabaseCommand.php +++ b/src/Commands/SeedDatabaseCommand.php @@ -15,6 +15,6 @@ public function handle() { (new NativeServiceProvider($this->laravel))->rewriteDatabase(); - parent::handle(); + return parent::handle(); } } diff --git a/src/Compactor/Php.php b/src/Compactor/Php.php deleted file mode 100644 index 8f93b75d..00000000 --- a/src/Compactor/Php.php +++ /dev/null @@ -1,188 +0,0 @@ -canProcessFile($file)) { - return $this->compactContent($contents); - } - - $this->compactContent($contents); - } - - protected function compactContent(string $contents): string - { - $output = ''; - $tokens = PhpToken::tokenize($contents); - $tokenCount = count($tokens); - - for ($index = 0; $index < $tokenCount; $index++) { - $token = $tokens[$index]; - $tokenText = $token->text; - - if ($token->is([T_COMMENT, T_DOC_COMMENT])) { - if (str_starts_with($tokenText, '#[')) { - // This is, in all likelihood, the start of a PHP >= 8.0 attribute. - // Note: $tokens may be updated by reference as well! - $retokenized = $this->retokenizeAttribute($tokens, $index); - - if (null !== $retokenized) { - array_splice($tokens, $index, 1, $retokenized); - $tokenCount = count($tokens); - } - - $attributeCloser = self::findAttributeCloser($tokens, $index); - - if (is_int($attributeCloser)) { - $output .= '#['; - } else { - // Turns out this was not an attribute. Treat it as a plain comment. - $output .= str_repeat("\n", mb_substr_count($tokenText, "\n")); - } - } elseif (str_contains($tokenText, '@')) { - try { - $output .= $this->compactAnnotations($tokenText); - } catch (RuntimeException) { - $output .= $tokenText; - } - } else { - $output .= str_repeat("\n", mb_substr_count($tokenText, "\n")); - } - } elseif ($token->is(T_WHITESPACE)) { - $whitespace = $tokenText; - $previousIndex = ($index - 1); - - // Handle whitespace potentially being split into two tokens after attribute retokenization. - $nextToken = $tokens[$index + 1] ?? null; - - if (null !== $nextToken - && $nextToken->is(T_WHITESPACE) - ) { - $whitespace .= $nextToken->text; - $index++; - } - - // reduce wide spaces - $whitespace = preg_replace('{[ \t]+}', ' ', $whitespace); - - // normalize newlines to \n - $whitespace = preg_replace('{(?:\r\n|\r|\n)}', "\n", $whitespace); - - // If the new line was split off from the whitespace token due to it being included in - // the previous (comment) token (PHP < 8), remove leading spaces. - - $previousToken = $tokens[$previousIndex]; - - if ($previousToken->is(T_COMMENT) - && str_contains($previousToken->text, "\n") - ) { - $whitespace = ltrim($whitespace, ' '); - } - - // trim leading spaces - $whitespace = preg_replace('{\n +}', "\n", $whitespace); - - $output .= $whitespace; - } else { - $output .= $tokenText; - } - } - - return $output; - } - - private function compactAnnotations(string $docblock): string - { - return $docblock; - } - - /** - * @param list $tokens - */ - private static function findAttributeCloser(array $tokens, int $opener): ?int - { - $tokenCount = count($tokens); - $brackets = [$opener]; - $closer = null; - - for ($i = ($opener + 1); $i < $tokenCount; $i++) { - $tokenText = $tokens[$i]->text; - - // Allow for short arrays within attributes. - if ('[' === $tokenText) { - $brackets[] = $i; - - continue; - } - - if (']' === $tokenText) { - array_pop($brackets); - - if (0 === count($brackets)) { - $closer = $i; - break; - } - } - } - - return $closer; - } - - /** - * @param non-empty-list $tokens - */ - private function retokenizeAttribute(array &$tokens, int $opener): ?array - { - Assert::keyExists($tokens, $opener); - - /** @var PhpToken $token */ - $token = $tokens[$opener]; - $attributeBody = mb_substr($token->text, 2); - $subTokens = PhpToken::tokenize('text; - } - - $subTokens = PhpToken::tokenize('windowPosition = $position; + + return $this; + } + + public function trayLeft(): self + { + return $this->windowPosition('trayLeft'); + } + + public function trayBottomLeft(): self + { + return $this->windowPosition('trayBottomLeft'); + } + + public function trayRight(): self + { + return $this->windowPosition('trayRight'); + } + + public function trayBottomRight(): self + { + return $this->windowPosition('trayBottomRight'); + } + + public function trayCenter(): self + { + return $this->windowPosition('trayCenter'); + } + + public function trayBottomCenter(): self + { + return $this->windowPosition('trayBottomCenter'); + } + + public function topLeft(): self + { + return $this->windowPosition('topLeft'); + } + + public function topRight(): self + { + return $this->windowPosition('topRight'); + } + + public function bottomLeft(): self + { + return $this->windowPosition('bottomLeft'); + } + + public function bottomRight(): self + { + return $this->windowPosition('bottomRight'); + } + + public function topCenter(): self + { + return $this->windowPosition('topCenter'); + } + + public function bottomCenter(): self + { + return $this->windowPosition('bottomCenter'); + } + + public function leftCenter(): self + { + return $this->windowPosition('leftCenter'); + } + + public function rightCenter(): self + { + return $this->windowPosition('rightCenter'); + } + + public function center(): self + { + return $this->windowPosition('center'); + } +} diff --git a/src/Concerns/HasUrl.php b/src/Concerns/HasUrl.php index bb98b015..0a642c22 100644 --- a/src/Concerns/HasUrl.php +++ b/src/Concerns/HasUrl.php @@ -15,7 +15,7 @@ public function url(string $url): self public function route(string $route, array $parameters = []): self { - $this->url = route($route, $parameters); + $this->url(route($route, $parameters)); return $this; } diff --git a/src/Concerns/InteractsWithNativeApp.php b/src/Concerns/InteractsWithNativeApp.php deleted file mode 100644 index 7dd6ee78..00000000 --- a/src/Concerns/InteractsWithNativeApp.php +++ /dev/null @@ -1,34 +0,0 @@ -start(); - - if (is_iterable($totalSteps)) { - foreach ($totalSteps as $value) { - $callback($value, $bar); - - $bar->advance(); - } - } else { - $callback($bar); - } - - $bar->finish(); - - if (is_iterable($totalSteps)) { - return $totalSteps; - } - } -} diff --git a/src/ContextMenu.php b/src/ContextMenu.php index e0e8fa72..d56690e0 100644 --- a/src/ContextMenu.php +++ b/src/ContextMenu.php @@ -7,9 +7,7 @@ class ContextMenu { - public function __construct(protected Client $client) - { - } + public function __construct(protected Client $client) {} public function register(Menu $menu) { diff --git a/src/Contracts/ChildProcess.php b/src/Contracts/ChildProcess.php new file mode 100644 index 00000000..0a4f8777 --- /dev/null +++ b/src/Contracts/ChildProcess.php @@ -0,0 +1,28 @@ + + */ + public function all(): array; + + public function get(string $id): Window; +} diff --git a/src/DTOs/QueueConfig.php b/src/DTOs/QueueConfig.php new file mode 100644 index 00000000..29308a03 --- /dev/null +++ b/src/DTOs/QueueConfig.php @@ -0,0 +1,37 @@ + $queuesToConsume + */ + public function __construct( + public readonly string $alias, + public readonly array $queuesToConsume, + public readonly int $memoryLimit, + public readonly int $timeout, + public readonly int|float $sleep, + ) {} + + /** + * @return array + */ + public static function fromConfigArray(array $config): array + { + return array_map( + function (array|string $worker, string $alias) { + return new self( + $alias, + $worker['queues'] ?? ['default'], + $worker['memory_limit'] ?? 128, + $worker['timeout'] ?? 60, + $worker['sleep'] ?? 3, + ); + }, + $config, + array_keys($config), + ); + } +} diff --git a/src/DataObjects/Printer.php b/src/DataObjects/Printer.php index 30795a87..ba7d054b 100644 --- a/src/DataObjects/Printer.php +++ b/src/DataObjects/Printer.php @@ -11,7 +11,5 @@ public function __construct( public int $status, public bool $isDefault, public array $options - ) { - - } + ) {} } diff --git a/src/Dialog.php b/src/Dialog.php index 0913a690..5b8d8913 100644 --- a/src/Dialog.php +++ b/src/Dialog.php @@ -26,13 +26,11 @@ class Dialog protected $windowReference; - public function __construct(protected Client $client) - { - } + final public function __construct(protected Client $client) {} public static function new() { - return new static(new Client()); + return new static(new Client); } public function title(string $title): self @@ -110,27 +108,16 @@ public function properties(array $properties): self return $this; } - public function asSheet(string $windowId = null): self + public function asSheet(?string $windowId = null): self { - if (is_null($windowId)) { - $this->windowReference = Window::current()->id; - } else { - $this->windowReference = $windowId; - } + $this->windowReference = $windowId ?? Window::current()->getId(); return $this; } public function open() { - $result = $this->client->post('dialog/open', [ - 'title' => $this->title, - 'windowReference' => $this->windowReference, - 'defaultPath' => $this->defaultPath, - 'filters' => $this->filters, - 'buttonLabel' => $this->buttonLabel, - 'properties' => array_unique($this->properties), - ])->json('result'); + $result = $this->client->post('dialog/open', $this->dialogData())->json('result'); if (! in_array('multiSelections', $this->properties)) { return $result[0] ?? null; @@ -141,13 +128,18 @@ public function open() public function save() { - return $this->client->post('dialog/save', [ + return $this->client->post('dialog/save', $this->dialogData())->json('result'); + } + + public function dialogData(): array + { + return [ 'title' => $this->title, 'windowReference' => $this->windowReference, 'defaultPath' => $this->defaultPath, 'filters' => $this->filters, 'buttonLabel' => $this->buttonLabel, 'properties' => array_unique($this->properties), - ])->json('result'); + ]; } } diff --git a/src/Dock.php b/src/Dock.php index 63a7a49f..9691ff89 100644 --- a/src/Dock.php +++ b/src/Dock.php @@ -7,9 +7,7 @@ class Dock { - public function __construct(protected Client $client) - { - } + public function __construct(protected Client $client) {} public function menu(Menu $menu) { @@ -19,4 +17,40 @@ public function menu(Menu $menu) 'items' => $items, ]); } + + public function show() + { + $this->client->post('dock/show'); + } + + public function hide() + { + $this->client->post('dock/hide'); + } + + public function icon(string $path) + { + $this->client->post('dock/icon', ['path' => $path]); + } + + public function bounce(string $type = 'informational') + { + $this->client->post('dock/bounce', ['type' => $type]); + } + + public function cancelBounce() + { + $this->client->post('dock/cancel-bounce'); + } + + public function badge(?string $label = null): ?string + { + if (is_null($label)) { + return $this->client->get('dock/badge'); + } + + $this->client->post('dock/badge', ['label' => $label]); + + return null; + } } diff --git a/src/Enums/PowerStatesEnum.php b/src/Enums/PowerStatesEnum.php new file mode 100644 index 00000000..b1996fb4 --- /dev/null +++ b/src/Enums/PowerStatesEnum.php @@ -0,0 +1,9 @@ +broadcastOn(); + + // Only events dispatched on the nativephp channel + if (! in_array('nativephp', $channels)) { + return; + } + + // Only post custom events to broadcasting endpoint + if (str_starts_with($eventName, 'Native\\Laravel\\Events')) { + return; + } + + $this->client->post('broadcast', [ + 'event' => "\\{$eventName}", + 'payload' => $event, + ]); + }); + } +} diff --git a/src/Events/Menu/MenuItemClicked.php b/src/Events/Menu/MenuItemClicked.php index 2a2a456f..961ed1c7 100644 --- a/src/Events/Menu/MenuItemClicked.php +++ b/src/Events/Menu/MenuItemClicked.php @@ -12,9 +12,7 @@ class MenuItemClicked implements ShouldBroadcastNow { use Dispatchable, InteractsWithSockets, SerializesModels; - public function __construct(public array $item) - { - } + public function __construct(public array $item, public array $combo = []) {} public function broadcastOn() { diff --git a/src/Events/MenuBar/MenuBarClicked.php b/src/Events/MenuBar/MenuBarClicked.php new file mode 100644 index 00000000..4ac8f9e9 --- /dev/null +++ b/src/Events/MenuBar/MenuBarClicked.php @@ -0,0 +1,23 @@ +state = PowerStatesEnum::from($state); + } + + public function broadcastOn() + { + return [ + new Channel('nativephp'), + ]; + } +} diff --git a/src/Events/PowerMonitor/ScreenLocked.php b/src/Events/PowerMonitor/ScreenLocked.php new file mode 100644 index 00000000..dbaca58a --- /dev/null +++ b/src/Events/PowerMonitor/ScreenLocked.php @@ -0,0 +1,23 @@ +limit = (int) $limit; + } + + public function broadcastOn() + { + return [ + new Channel('nativephp'), + ]; + } +} diff --git a/src/Events/PowerMonitor/ThermalStateChanged.php b/src/Events/PowerMonitor/ThermalStateChanged.php new file mode 100644 index 00000000..bed05832 --- /dev/null +++ b/src/Events/PowerMonitor/ThermalStateChanged.php @@ -0,0 +1,29 @@ +state = ThermalStatesEnum::from($state); + } + + public function broadcastOn() + { + return [ + new Channel('nativephp'), + ]; + } +} diff --git a/src/Events/PowerMonitor/UserDidBecomeActive.php b/src/Events/PowerMonitor/UserDidBecomeActive.php new file mode 100644 index 00000000..e077a526 --- /dev/null +++ b/src/Events/PowerMonitor/UserDidBecomeActive.php @@ -0,0 +1,23 @@ +reportable(function (\Throwable $e) { + error_log("[NATIVE_EXCEPTION]: {$e->getMessage()} ({$e->getCode()}) in {$e->getFile()}:{$e->getLine()}"); + }); + } +} diff --git a/src/Facades/Alert.php b/src/Facades/Alert.php new file mode 100644 index 00000000..2d5e1f2b --- /dev/null +++ b/src/Facades/Alert.php @@ -0,0 +1,24 @@ +make(ChildProcessFake::class), function ($fake) { + static::swap($fake); + }); + } + + protected static function getFacadeAccessor() + { + self::clearResolvedInstance(ChildProcessContract::class); + + return ChildProcessContract::class; + } +} diff --git a/src/Facades/Clipboard.php b/src/Facades/Clipboard.php index 0de117b4..c10fd5d7 100644 --- a/src/Facades/Clipboard.php +++ b/src/Facades/Clipboard.php @@ -8,7 +8,7 @@ * @method static void clear() * @method static string text($text = null) * @method static string html($html = null) - * @method static string image($image = null) + * @method static string|null image($image = null) */ class Clipboard extends Facade { diff --git a/src/Facades/Dock.php b/src/Facades/Dock.php index f9386433..ddeaffb4 100644 --- a/src/Facades/Dock.php +++ b/src/Facades/Dock.php @@ -6,7 +6,13 @@ use Native\Laravel\Menu\Menu; /** + * @method static void bounce(string $type = 'informational') + * @method static void|string badge(?string $type = null) + * @method static void cancelBounce() + * @method static void hide() + * @method static void icon(string $Path) * @method static void menu(Menu $menu) + * @method static void show() */ class Dock extends Facade { diff --git a/src/Facades/GlobalShortcut.php b/src/Facades/GlobalShortcut.php index f0131682..d5a506f0 100644 --- a/src/Facades/GlobalShortcut.php +++ b/src/Facades/GlobalShortcut.php @@ -3,6 +3,8 @@ namespace Native\Laravel\Facades; use Illuminate\Support\Facades\Facade; +use Native\Laravel\Contracts\GlobalShortcut as GlobalShortcutContract; +use Native\Laravel\Fakes\GlobalShortcutFake; /** * @method static \Native\Laravel\GlobalShortcut key(string $key) @@ -12,8 +14,15 @@ */ class GlobalShortcut extends Facade { + public static function fake() + { + return tap(static::getFacadeApplication()->make(GlobalShortcutFake::class), function ($fake) { + static::swap($fake); + }); + } + protected static function getFacadeAccessor() { - return \Native\Laravel\GlobalShortcut::class; + return GlobalShortcutContract::class; } } diff --git a/src/Facades/Menu.php b/src/Facades/Menu.php new file mode 100644 index 00000000..d197305e --- /dev/null +++ b/src/Facades/Menu.php @@ -0,0 +1,51 @@ +make(PowerMonitorFake::class), function ($fake) { + static::swap($fake); + }); + } + + protected static function getFacadeAccessor(): string + { + return PowerMonitorContract::class; + } +} diff --git a/src/Facades/QueueWorker.php b/src/Facades/QueueWorker.php new file mode 100644 index 00000000..3b504bec --- /dev/null +++ b/src/Facades/QueueWorker.php @@ -0,0 +1,29 @@ +make(QueueWorkerFake::class), function ($fake) { + static::swap($fake); + }); + } + + protected static function getFacadeAccessor(): string + { + self::clearResolvedInstance(QueueWorkerContract::class); + + return QueueWorkerContract::class; + } +} diff --git a/src/Facades/Screen.php b/src/Facades/Screen.php index fe917f1c..82dfcfa5 100644 --- a/src/Facades/Screen.php +++ b/src/Facades/Screen.php @@ -7,6 +7,9 @@ /** * @method static object cursorPosition() * @method static array displays() + * @method static array getCenterOfActiveScreen() + * @method static array active() + * @method static array primary() */ class Screen extends Facade { diff --git a/src/Facades/Settings.php b/src/Facades/Settings.php index d1126c5c..527c0f91 100644 --- a/src/Facades/Settings.php +++ b/src/Facades/Settings.php @@ -5,8 +5,10 @@ use Illuminate\Support\Facades\Facade; /** - * @method static void set($key, $value) - * @method static void mixed($key, $default = null) + * @method static void set(string $key, $value) + * @method static mixed get(string $key, $default = null) + * @method static void forget(string $key) + * @method static void clear() */ class Settings extends Facade { diff --git a/src/Facades/Shell.php b/src/Facades/Shell.php index aa5f7c1c..27c8b038 100644 --- a/src/Facades/Shell.php +++ b/src/Facades/Shell.php @@ -4,6 +4,12 @@ use Illuminate\Support\Facades\Facade; +/** + * @method static void showInFolder(string $path) + * @method static string openFile(string $path) + * @method static void trashFile(string $path) + * @method static void openExternal(string $url) + */ class Shell extends Facade { protected static function getFacadeAccessor() diff --git a/src/Facades/System.php b/src/Facades/System.php index 31ff6846..396d1151 100644 --- a/src/Facades/System.php +++ b/src/Facades/System.php @@ -3,10 +3,19 @@ namespace Native\Laravel\Facades; use Illuminate\Support\Facades\Facade; +use Native\Laravel\Enums\SystemThemesEnum; /** * @method static bool canPromptTouchID() * @method static bool promptTouchID(string $reason) + * @method static bool canEncrypt() + * @method static string encrypt(string $string) + * @method static string decrypt(string $string) + * @method static array printers() + * @method static void print(string $html, ?\Native\Laravel\DataObjects\Printer $printer = null) + * @method static string printToPDF(string $reason) + * @method static string timezone() + * @method static SystemThemesEnum theme(?SystemThemesEnum $theme = null) */ class System extends Facade { diff --git a/src/Facades/Window.php b/src/Facades/Window.php index 35cf900f..f73e2164 100644 --- a/src/Facades/Window.php +++ b/src/Facades/Window.php @@ -3,19 +3,32 @@ namespace Native\Laravel\Facades; use Illuminate\Support\Facades\Facade; +use Native\Laravel\Contracts\WindowManager as WindowManagerContract; +use Native\Laravel\Fakes\WindowManagerFake; /** * @method static \Native\Laravel\Windows\PendingOpenWindow open(string $id = 'main') * @method static void close($id = null) * @method static object current() + * @method static array all() * @method static void resize($width, $height, $id = null) * @method static void position($x, $y, $animated = false, $id = null) * @method static void alwaysOnTop($alwaysOnTop = true, $id = null) + * @method static void reload($id = null) + * @method static void maximize($id = null) + * @method static void minimize($id = null) */ class Window extends Facade { + public static function fake() + { + return tap(static::getFacadeApplication()->make(WindowManagerFake::class), function ($fake) { + static::swap($fake); + }); + } + protected static function getFacadeAccessor() { - return \Native\Laravel\Windows\WindowManager::class; + return WindowManagerContract::class; } } diff --git a/src/Fakes/ChildProcessFake.php b/src/Fakes/ChildProcessFake.php new file mode 100644 index 00000000..52436c20 --- /dev/null +++ b/src/Fakes/ChildProcessFake.php @@ -0,0 +1,256 @@ + + */ + public array $gets = []; + + /** + * @var array + */ + public array $starts = []; + + /** + * @var array + */ + public array $phps = []; + + /** + * @var array + */ + public array $artisans = []; + + /** + * @var array + */ + public array $stops = []; + + /** + * @var array + */ + public array $restarts = []; + + /** + * @var array + */ + public array $messages = []; + + public function get(?string $alias = null): self + { + $this->gets[] = $alias; + + return $this; + } + + public function all(): array + { + return [$this]; + } + + public function start( + array|string $cmd, + string $alias, + ?string $cwd = null, + ?array $env = null, + bool $persistent = false + ): self { + $this->starts[] = [ + 'cmd' => $cmd, + 'alias' => $alias, + 'cwd' => $cwd, + 'env' => $env, + 'persistent' => $persistent, + ]; + + return $this; + } + + public function php( + array|string $cmd, + string $alias, + ?array $env = null, + ?bool $persistent = false, + ?array $iniSettings = null + ): self { + $this->phps[] = [ + 'cmd' => $cmd, + 'alias' => $alias, + 'env' => $env, + 'persistent' => $persistent, + 'iniSettings' => $iniSettings, + ]; + + return $this; + } + + public function artisan( + array|string $cmd, + string $alias, + ?array $env = null, + ?bool $persistent = false, + ?array $iniSettings = null + ): self { + $this->artisans[] = [ + 'cmd' => $cmd, + 'alias' => $alias, + 'env' => $env, + 'persistent' => $persistent, + 'iniSettings' => $iniSettings, + ]; + + return $this; + } + + public function stop(?string $alias = null): void + { + $this->stops[] = $alias; + } + + public function restart(?string $alias = null): self + { + $this->restarts[] = $alias; + + return $this; + } + + public function message(string $message, ?string $alias = null): self + { + $this->messages[] = [ + 'message' => $message, + 'alias' => $alias, + ]; + + return $this; + } + + /** + * @param string|Closure(string): bool $alias + */ + public function assertGet(string|Closure $alias): void + { + if (is_callable($alias) === false) { + PHPUnit::assertContains($alias, $this->gets); + + return; + } + + $hit = empty( + array_filter( + $this->gets, + fn (mixed $get) => $alias($get) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param Closure(array|string $cmd, string $alias, ?string $cwd, ?array $env, bool $persistent): bool $callback + */ + public function assertStarted(Closure $callback): void + { + $hit = empty( + array_filter( + $this->starts, + fn (array $started) => $callback(...$started) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param Closure(array|string $cmd, string $alias, ?array $env, ?bool $persistent): bool $callback + */ + public function assertPhp(Closure $callback): void + { + $hit = empty( + array_filter( + $this->phps, + fn (array $php) => $callback(...$php) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param Closure(array|string $cmd, string $alias, ?array $env, ?bool $persistent): bool $callback + */ + public function assertArtisan(Closure $callback): void + { + $hit = empty( + array_filter( + $this->artisans, + fn (array $artisan) => $callback(...$artisan) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param string|Closure(string): bool $alias + */ + public function assertStop(string|Closure $alias): void + { + if (is_callable($alias) === false) { + PHPUnit::assertContains($alias, $this->stops); + + return; + } + + $hit = empty( + array_filter( + $this->stops, + fn (mixed $stop) => $alias($stop) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param string|Closure(string): bool $alias + */ + public function assertRestart(string|Closure $alias): void + { + if (is_callable($alias) === false) { + PHPUnit::assertContains($alias, $this->restarts); + + return; + } + + $hit = empty( + array_filter( + $this->restarts, + fn (mixed $restart) => $alias($restart) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param Closure(string $message, string|null $alias): bool $callback + */ + public function assertMessage(Closure $callback): void + { + $hit = empty( + array_filter( + $this->messages, + fn (array $message) => $callback(...$message) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } +} diff --git a/src/Fakes/GlobalShortcutFake.php b/src/Fakes/GlobalShortcutFake.php new file mode 100644 index 00000000..13efa1ee --- /dev/null +++ b/src/Fakes/GlobalShortcutFake.php @@ -0,0 +1,100 @@ + + */ + public array $keys = []; + + /** + * @var array + */ + public array $events = []; + + public int $registeredCount = 0; + + public int $unregisteredCount = 0; + + public function key(string $key): self + { + $this->keys[] = $key; + + return $this; + } + + public function event(string $event): self + { + $this->events[] = $event; + + return $this; + } + + public function register(): void + { + $this->registeredCount++; + } + + public function unregister(): void + { + $this->unregisteredCount++; + } + + /** + * @param string|Closure(string): bool $key + */ + public function assertKey(string|Closure $key): void + { + if (is_callable($key) === false) { + PHPUnit::assertContains($key, $this->keys); + + return; + } + + $hit = empty( + array_filter( + $this->keys, + fn (string $keyIteration) => $key($keyIteration) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param string|Closure(string): bool $event + */ + public function assertEvent(string|Closure $event): void + { + if (is_callable($event) === false) { + PHPUnit::assertContains($event, $this->events); + + return; + } + + $hit = empty( + array_filter( + $this->events, + fn (string $eventIteration) => $event($eventIteration) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + public function assertRegisteredCount(int $count): void + { + PHPUnit::assertSame($count, $this->registeredCount); + } + + public function assertUnregisteredCount(int $count): void + { + PHPUnit::assertSame($count, $this->unregisteredCount); + } +} diff --git a/src/Fakes/PowerMonitorFake.php b/src/Fakes/PowerMonitorFake.php new file mode 100644 index 00000000..caaf2823 --- /dev/null +++ b/src/Fakes/PowerMonitorFake.php @@ -0,0 +1,93 @@ +getSystemIdleStateCount++; + + $this->getSystemIdleStateCalls[] = $threshold; + + return SystemIdleStatesEnum::UNKNOWN; + } + + public function getSystemIdleTime(): int + { + $this->getSystemIdleTimeCount++; + + return 0; + } + + public function getCurrentThermalState(): ThermalStatesEnum + { + $this->getCurrentThermalStateCount++; + + return ThermalStatesEnum::UNKNOWN; + } + + public function isOnBatteryPower(): bool + { + $this->isOnBatteryPowerCount++; + + return false; + } + + /** + * @param int|Closure(int): bool $key + */ + public function assertGetSystemIdleState(int|Closure $key): void + { + if (is_callable($key) === false) { + PHPUnit::assertContains($key, $this->getSystemIdleStateCalls); + + return; + } + + $hit = empty( + array_filter( + $this->getSystemIdleStateCalls, + fn (int $keyIteration) => $key($keyIteration) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + public function assertGetSystemIdleStateCount(int $count): void + { + PHPUnit::assertSame($count, $this->getSystemIdleStateCount); + } + + public function assertGetSystemIdleTimeCount(int $count): void + { + PHPUnit::assertSame($count, $this->getSystemIdleTimeCount); + } + + public function assertGetCurrentThermalStateCount(int $count): void + { + PHPUnit::assertSame($count, $this->getCurrentThermalStateCount); + } + + public function assertIsOnBatteryPowerCount(int $count): void + { + PHPUnit::assertSame($count, $this->isOnBatteryPowerCount); + } +} diff --git a/src/Fakes/QueueWorkerFake.php b/src/Fakes/QueueWorkerFake.php new file mode 100644 index 00000000..6482dd9f --- /dev/null +++ b/src/Fakes/QueueWorkerFake.php @@ -0,0 +1,61 @@ + + */ + public array $ups = []; + + /** + * @var array + */ + public array $downs = []; + + public function up(QueueConfig $config): void + { + $this->ups[] = $config; + } + + public function down(string $alias): void + { + $this->downs[] = $alias; + } + + public function assertUp(Closure $callback): void + { + $hit = empty( + array_filter( + $this->ups, + fn (QueueConfig $up) => $callback($up) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + public function assertDown(string|Closure $alias): void + { + if (is_callable($alias) === false) { + PHPUnit::assertContains($alias, $this->downs); + + return; + } + + $hit = empty( + array_filter( + $this->downs, + fn (string $down) => $alias($down) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } +} diff --git a/src/Fakes/WindowManagerFake.php b/src/Fakes/WindowManagerFake.php new file mode 100644 index 00000000..64112b34 --- /dev/null +++ b/src/Fakes/WindowManagerFake.php @@ -0,0 +1,211 @@ + $windows + */ + public function alwaysReturnWindows(array $windows): self + { + $this->forcedWindowReturnValues = $windows; + + return $this; + } + + public function open(string $id = 'main') + { + $this->opened[] = $id; + + $this->ensureForceReturnWindowsProvided(); + + $matchingWindows = array_filter( + $this->forcedWindowReturnValues, + fn (Window $window) => $window->getId() === $id + ); + + if (empty($matchingWindows)) { + return $this->forcedWindowReturnValues[array_rand($this->forcedWindowReturnValues)]->setClient($this->client); + } + + Assert::count($matchingWindows, 1); + + return Arr::first($matchingWindows)->setClient($this->client); + } + + public function close($id = null) + { + $this->closed[] = $id; + } + + public function hide($id = null) + { + $this->hidden[] = $id; + } + + public function show($id = null) + { + $this->shown[] = $id; + } + + public function current(): Window + { + $this->ensureForceReturnWindowsProvided(); + + return $this->forcedWindowReturnValues[array_rand($this->forcedWindowReturnValues)]; + } + + /** + * @return array + */ + public function all(): array + { + $this->ensureForceReturnWindowsProvided(); + + return $this->forcedWindowReturnValues; + } + + public function get(string $id): Window + { + $this->ensureForceReturnWindowsProvided(); + + $matchingWindows = array_filter($this->forcedWindowReturnValues, fn (Window $window) => $window->getId() === $id); + + Assert::notEmpty($matchingWindows); + Assert::count($matchingWindows, 1); + + return Arr::first($matchingWindows); + } + + /** + * @param string|Closure(string): bool $id + */ + public function assertOpened(string|Closure $id): void + { + if (is_callable($id) === false) { + PHPUnit::assertContains($id, $this->opened); + + return; + } + + $hit = empty( + array_filter( + $this->opened, + fn (string $openedId) => $id($openedId) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param string|Closure(string): bool $id + */ + public function assertClosed(string|Closure $id): void + { + if (is_callable($id) === false) { + PHPUnit::assertContains($id, $this->closed); + + return; + } + + $hit = empty( + array_filter( + $this->closed, + fn (mixed $closedId) => $id($closedId) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param string|Closure(string): bool $id + */ + public function assertHidden(string|Closure $id): void + { + if (is_callable($id) === false) { + PHPUnit::assertContains($id, $this->hidden); + + return; + } + + $hit = empty( + array_filter( + $this->hidden, + fn (mixed $hiddenId) => $id($hiddenId) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param string|Closure(string): bool $id + */ + public function assertShown(string|Closure $id): void + { + if (is_callable($id) === false) { + PHPUnit::assertContains($id, $this->shown); + + return; + } + + $hit = empty( + array_filter( + $this->shown, + fn (mixed $shownId) => $id($shownId) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + public function assertOpenedCount(int $expected): void + { + PHPUnit::assertCount($expected, $this->opened); + } + + public function assertClosedCount(int $expected): void + { + PHPUnit::assertCount($expected, $this->closed); + } + + public function assertHiddenCount(int $expected): void + { + PHPUnit::assertCount($expected, $this->hidden); + } + + public function assertShownCount(int $expected): void + { + PHPUnit::assertCount($expected, $this->shown); + } + + private function ensureForceReturnWindowsProvided(): void + { + Assert::notEmpty($this->forcedWindowReturnValues, 'No windows were provided to return'); + } +} diff --git a/src/GlobalShortcut.php b/src/GlobalShortcut.php index 24020cf9..3859e6af 100644 --- a/src/GlobalShortcut.php +++ b/src/GlobalShortcut.php @@ -3,16 +3,15 @@ namespace Native\Laravel; use Native\Laravel\Client\Client; +use Native\Laravel\Contracts\GlobalShortcut as GlobalShortcutContract; -class GlobalShortcut +class GlobalShortcut implements GlobalShortcutContract { protected string $key; protected string $event; - public function __construct(protected Client $client) - { - } + public function __construct(protected Client $client) {} public function key(string $key): self { diff --git a/src/Http/Controllers/DispatchEventFromAppController.php b/src/Http/Controllers/DispatchEventFromAppController.php index 2ae7bb98..0f467f10 100644 --- a/src/Http/Controllers/DispatchEventFromAppController.php +++ b/src/Http/Controllers/DispatchEventFromAppController.php @@ -9,10 +9,13 @@ class DispatchEventFromAppController public function __invoke(Request $request) { $event = $request->get('event'); - if (class_exists($event)) { - $event = new $event(...$request->get('payload', [])); + $payload = $request->get('payload', []); + if (class_exists($event)) { + $event = new $event(...$payload); event($event); + } else { + event($event, $payload); } return response()->json([ diff --git a/src/Http/Controllers/NativeAppBootedController.php b/src/Http/Controllers/NativeAppBootedController.php index 8d56bc5f..36ed7351 100644 --- a/src/Http/Controllers/NativeAppBootedController.php +++ b/src/Http/Controllers/NativeAppBootedController.php @@ -12,7 +12,7 @@ public function __invoke(Request $request) $provider = app(config('nativephp.provider')); $provider->boot(); - event(new ApplicationBooted()); + event(new ApplicationBooted); return response()->json([ 'success' => true, diff --git a/src/Http/Middleware/PreventRegularBrowserAccess.php b/src/Http/Middleware/PreventRegularBrowserAccess.php index de1c72bf..6722a3da 100644 --- a/src/Http/Middleware/PreventRegularBrowserAccess.php +++ b/src/Http/Middleware/PreventRegularBrowserAccess.php @@ -9,6 +9,10 @@ class PreventRegularBrowserAccess { public function handle(Request $request, Closure $next) { + if (! config('nativephp-internal.running')) { + return $next($request); + } + // Explicitly skip for the cookie-setting route if ($request->path() === '_native/api/cookie') { return $next($request); diff --git a/src/Logging/LogWatcher.php b/src/Logging/LogWatcher.php index 650882b0..e48eaf43 100644 --- a/src/Logging/LogWatcher.php +++ b/src/Logging/LogWatcher.php @@ -8,10 +8,7 @@ class LogWatcher { - public function __construct(protected Client $client) - { - - } + public function __construct(protected Client $client) {} public function register(): void { diff --git a/src/Menu/Items/Checkbox.php b/src/Menu/Items/Checkbox.php index 7482170b..baa44fc9 100644 --- a/src/Menu/Items/Checkbox.php +++ b/src/Menu/Items/Checkbox.php @@ -6,8 +6,9 @@ class Checkbox extends MenuItem { protected string $type = 'checkbox'; - public function __construct(string $label, protected bool $isChecked = false, protected ?string $accelerator = null) - { - $this->label = $label; - } + public function __construct( + protected ?string $label, + protected bool $isChecked = false, + protected ?string $accelerator = null + ) {} } diff --git a/src/Menu/Items/Event.php b/src/Menu/Items/Event.php deleted file mode 100644 index 178349c0..00000000 --- a/src/Menu/Items/Event.php +++ /dev/null @@ -1,20 +0,0 @@ - 'event', - 'event' => $this->event, - 'label' => $this->label, - ]); - } -} diff --git a/src/Menu/Items/Label.php b/src/Menu/Items/Label.php index 571c5ae5..cd03d091 100644 --- a/src/Menu/Items/Label.php +++ b/src/Menu/Items/Label.php @@ -4,8 +4,8 @@ class Label extends MenuItem { - public function __construct(string $label) - { - $this->label = $label; - } + public function __construct( + protected ?string $label, + protected ?string $accelerator = null + ) {} } diff --git a/src/Menu/Items/Link.php b/src/Menu/Items/Link.php index 684cb7eb..25bc3c17 100644 --- a/src/Menu/Items/Link.php +++ b/src/Menu/Items/Link.php @@ -6,15 +6,26 @@ class Link extends MenuItem { protected string $type = 'link'; - public function __construct(protected string $url, protected ?string $label, protected ?string $accelerator = null) + protected bool $openInBrowser = false; + + public function __construct( + protected string $url, + protected ?string $label, + protected ?string $accelerator = null + ) {} + + public function openInBrowser(bool $openInBrowser = true): self { + $this->openInBrowser = $openInBrowser; + return $this; } public function toArray(): array { return array_merge(parent::toArray(), [ 'url' => $this->url, + 'openInBrowser' => $this->openInBrowser, ]); } } diff --git a/src/Menu/Items/MenuItem.php b/src/Menu/Items/MenuItem.php index 34d41ba0..7fe90517 100644 --- a/src/Menu/Items/MenuItem.php +++ b/src/Menu/Items/MenuItem.php @@ -3,11 +3,15 @@ namespace Native\Laravel\Menu\Items; use Native\Laravel\Contracts\MenuItem as MenuItemContract; +use Native\Laravel\Facades\Menu as MenuFacade; +use Native\Laravel\Menu\Menu; abstract class MenuItem implements MenuItemContract { protected string $type = 'normal'; + protected ?string $id = null; + protected ?string $label = null; protected ?string $sublabel = null; @@ -18,15 +22,33 @@ abstract class MenuItem implements MenuItemContract protected ?string $toolTip = null; + protected ?Menu $submenu = null; + protected bool $isEnabled = true; protected bool $isVisible = true; protected bool $isChecked = false; - public function enabled($enabled = true): self + protected ?string $event = null; + + public function enabled(): self + { + $this->isEnabled = true; + + return $this; + } + + public function disabled(): self + { + $this->isEnabled = false; + + return $this; + } + + public function id(string $id): self { - $this->isEnabled = $enabled; + $this->id = $id; return $this; } @@ -66,6 +88,11 @@ public function accelerator(string $accelerator): self return $this; } + public function hotkey(string $hotkey): self + { + return $this->accelerator($hotkey); + } + public function checked($checked = true): self { $this->isChecked = $checked; @@ -73,18 +100,34 @@ public function checked($checked = true): self return $this; } - public function toolTip(string $toolTip): self + public function tooltip(string $toolTip): self { $this->toolTip = $toolTip; return $this; } + public function submenu(MenuItemContract ...$items): self + { + $this->submenu = MenuFacade::make(...$items); + + return $this; + } + + public function event(string $event): self + { + $this->event = $event; + + return $this; + } + public function toArray(): array { return array_filter([ 'type' => $this->type, + 'id' => $this->id, 'label' => $this->label, + 'event' => $this->event, 'sublabel' => $this->sublabel, 'toolTip' => $this->toolTip, 'enabled' => $this->isEnabled, @@ -92,6 +135,7 @@ public function toArray(): array 'checked' => $this->isChecked, 'accelerator' => $this->accelerator, 'icon' => $this->icon, + 'submenu' => $this->submenu?->toArray(), ], fn ($value) => $value !== null); } } diff --git a/src/Menu/Items/Radio.php b/src/Menu/Items/Radio.php index 63587a2e..759af459 100644 --- a/src/Menu/Items/Radio.php +++ b/src/Menu/Items/Radio.php @@ -6,8 +6,9 @@ class Radio extends MenuItem { protected string $type = 'radio'; - public function __construct(string $label) - { - $this->label = $label; - } + public function __construct( + protected ?string $label, + protected bool $isChecked = false, + protected ?string $accelerator = null + ) {} } diff --git a/src/Menu/Items/Role.php b/src/Menu/Items/Role.php index c8a04bd6..bde40a4a 100644 --- a/src/Menu/Items/Role.php +++ b/src/Menu/Items/Role.php @@ -8,10 +8,10 @@ class Role extends MenuItem { protected string $type = 'role'; - public function __construct(protected RolesEnum $role, protected ?string $label = '') - { - - } + public function __construct( + protected RolesEnum $role, + protected ?string $label = '' + ) {} public function toArray(): array { diff --git a/src/Menu/Menu.php b/src/Menu/Menu.php index d27dee06..c97a30d7 100644 --- a/src/Menu/Menu.php +++ b/src/Menu/Menu.php @@ -3,32 +3,19 @@ namespace Native\Laravel\Menu; use Illuminate\Support\Traits\Conditionable; +use JsonSerializable; use Native\Laravel\Client\Client; use Native\Laravel\Contracts\MenuItem; -use Native\Laravel\Enums\RolesEnum; -use Native\Laravel\Menu\Items\Checkbox; -use Native\Laravel\Menu\Items\Event; -use Native\Laravel\Menu\Items\Label; -use Native\Laravel\Menu\Items\Link; -use Native\Laravel\Menu\Items\Role; -use Native\Laravel\Menu\Items\Separator; -class Menu implements MenuItem +class Menu implements JsonSerializable, MenuItem { use Conditionable; protected array $items = []; - protected string $prepend = ''; + protected string $label = ''; - public function __construct(protected Client $client) - { - } - - public static function new(): static - { - return new static(new Client()); - } + public function __construct(protected Client $client) {} public function register(): void { @@ -39,81 +26,11 @@ public function register(): void ]); } - public function prepend(string $prepend): self - { - $this->prepend = $prepend; - - return $this; - } - - public function submenu(string $header, Menu $submenu): static - { - return $this->add($submenu->prepend($header)); - } - - public function separator(): static - { - return $this->add(new Separator()); - } - - public function quit(): static - { - return $this->add(new Role(RolesEnum::QUIT)); - } - public function label(string $label): self { - return $this->add(new Label($label)); - } - - public function checkbox(string $label, bool $checked = false, string $hotkey = null): self - { - return $this->add(new Checkbox($label, $checked, $hotkey)); - } - - public function event(string $event, string $text, string $hotkey = null): self - { - return $this->add(new Event($event, $text, $hotkey)); - } - - public function link(string $url, string $text, string $hotkey = null): self - { - return $this->add(new Link($url, $text, $hotkey)); - } - - public function appMenu(): static - { - return $this->add(new Role(RolesEnum::APP_MENU)); - } - - public function fileMenu($label = 'File'): static - { - return $this->add(new Role(RolesEnum::FILE_MENU, $label)); - } - - public function editMenu($label = 'Edit'): static - { - return $this->add(new Role(RolesEnum::EDIT_MENU, $label)); - } - - public function viewMenu($label = 'View'): static - { - return $this->add(new Role(RolesEnum::VIEW_MENU, $label)); - } + $this->label = $label; - public function windowMenu($label = 'Window'): static - { - return $this->add(new Role(RolesEnum::WINDOW_MENU, $label)); - } - - public function toggleFullscreen(): static - { - return $this->add(new Role(RolesEnum::TOGGLE_FULL_SCREEN)); - } - - public function toggleDevTools(): static - { - return $this->add(new Role(RolesEnum::TOGGLE_DEV_TOOLS)); + return $this; } public function add(MenuItem $item): self @@ -125,12 +42,18 @@ public function add(MenuItem $item): self public function toArray(): array { - $items = collect($this->items)->map(fn (MenuItem $item) => $item->toArray())->toArray(); - $label = $this->prepend; + $items = collect($this->items) + ->map(fn (MenuItem $item) => $item->toArray()) + ->toArray(); return [ - 'label' => $label, + 'label' => $this->label, 'submenu' => $items, ]; } + + public function jsonSerialize(): array + { + return $this->toArray(); + } } diff --git a/src/Menu/MenuBuilder.php b/src/Menu/MenuBuilder.php new file mode 100644 index 00000000..f2ef764a --- /dev/null +++ b/src/Menu/MenuBuilder.php @@ -0,0 +1,174 @@ +client); + + foreach ($items as $item) { + $menu->add($item); + } + + return $menu; + } + + public function create(MenuItem ...$items): void + { + $this->make(...$items) + ->register(); + } + + public function default(): void + { + $this->create( + $this->app(), + $this->file(), + $this->edit(), + $this->view(), + $this->window(), + ); + } + + public function label(string $label, ?string $hotkey = null): Items\Label + { + return new Items\Label($label, $hotkey); + } + + public function checkbox(string $label, bool $checked = false, ?string $hotkey = null): Items\Checkbox + { + return new Items\Checkbox($label, $checked, $hotkey); + } + + public function radio(string $label, bool $checked = false, ?string $hotkey = null): Items\Radio + { + return new Items\Radio($label, $checked, $hotkey); + } + + public function link(string $url, ?string $label = null, ?string $hotkey = null): Items\Link + { + return new Items\Link($url, $label, $hotkey); + } + + public function route(string $route, ?string $label = null, ?string $hotkey = null): Items\Link + { + return new Items\Link(route($route), $label, $hotkey); + } + + public function app(): Items\Role + { + return new Items\Role(RolesEnum::APP_MENU); + } + + public function file(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::FILE_MENU, $label); + } + + public function edit(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::EDIT_MENU, $label); + } + + public function view(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::VIEW_MENU, $label); + } + + public function window(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::WINDOW_MENU, $label); + } + + public function separator(): Items\Separator + { + return new Items\Separator; + } + + public function fullscreen(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::TOGGLE_FULL_SCREEN, $label); + } + + public function devTools(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::TOGGLE_DEV_TOOLS, $label); + } + + public function undo(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::UNDO, $label); + } + + public function redo(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::REDO, $label); + } + + public function cut(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::CUT, $label); + } + + public function copy(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::COPY, $label); + } + + public function paste(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::PASTE, $label); + } + + public function pasteAndMatchStyle(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::PASTE_STYLE, $label); + } + + public function reload(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::RELOAD, $label); + } + + public function minimize(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::MINIMIZE, $label); + } + + public function close(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::CLOSE, $label); + } + + public function quit(?string $label = null): Items\Role + { + if (is_null($label)) { + $label = __('Quit').' '.config('app.name'); + } + + return new Items\Role(RolesEnum::QUIT, $label); + } + + public function help(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::HELP, $label); + } + + public function hide(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::HIDE, $label); + } + + public function about(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::ABOUT, $label); + } +} diff --git a/src/MenuBar/MenuBar.php b/src/MenuBar/MenuBar.php index 30b9876a..c3b6ce8a 100644 --- a/src/MenuBar/MenuBar.php +++ b/src/MenuBar/MenuBar.php @@ -4,21 +4,27 @@ use Native\Laravel\Client\Client; use Native\Laravel\Concerns\HasDimensions; +use Native\Laravel\Concerns\HasPositioner; use Native\Laravel\Concerns\HasUrl; use Native\Laravel\Concerns\HasVibrancy; use Native\Laravel\Menu\Menu; class MenuBar { - use HasVibrancy; use HasDimensions; + use HasPositioner; use HasUrl; + use HasVibrancy; protected string $icon = ''; protected string $label = ''; - protected bool $onlyShowContextWindow = false; + protected string $tooltip = ''; + + protected bool $resizable = true; + + protected bool $onlyShowContextMenu = false; protected ?Menu $contextMenu = null; @@ -26,6 +32,8 @@ class MenuBar protected bool $showDockIcon = false; + protected bool $showOnAllWorkspaces = false; + protected Client $client; public function __construct() @@ -49,28 +57,35 @@ public function icon(string $icon): self public function onlyShowContextMenu(bool $onlyContextMenu = true): self { - $this->onlyShowContextWindow = $onlyContextMenu; + $this->onlyShowContextMenu = $onlyContextMenu; return $this; } - public function url(string $url): self + public function showDockIcon($value = true): self { - $this->url = $url; + $this->showDockIcon = $value; return $this; } - public function showDockIcon($value = true): self + public function label(string $label = ''): self { - $this->showDockIcon = $value; + $this->label = $label; return $this; } - public function label(string $label = ''): self + public function tooltip(string $tooltip = ''): self { - $this->label = $label; + $this->tooltip = $tooltip; + + return $this; + } + + public function resizable(bool $resizable = true): static + { + $this->resizable = $resizable; return $this; } @@ -82,6 +97,13 @@ public function alwaysOnTop($alwaysOnTop = true): self return $this; } + public function showOnAllWorkspaces($showOnAllWorkspaces = true): self + { + $this->showOnAllWorkspaces = $showOnAllWorkspaces; + + return $this; + } + public function withContextMenu(Menu $menu): self { $this->contextMenu = $menu; @@ -94,18 +116,22 @@ public function toArray(): array return [ 'url' => $this->url, 'icon' => $this->icon, + 'windowPosition' => $this->windowPosition, 'x' => $this->x, 'y' => $this->y, 'label' => $this->label, + 'tooltip' => $this->tooltip, + 'resizable' => $this->resizable, 'width' => $this->width, 'height' => $this->height, 'vibrancy' => $this->vibrancy, 'showDockIcon' => $this->showDockIcon, 'transparency' => $this->transparent, 'backgroundColor' => $this->backgroundColor, - 'onlyShowContextWindow' => $this->onlyShowContextWindow, + 'onlyShowContextMenu' => $this->onlyShowContextMenu, 'contextMenu' => ! is_null($this->contextMenu) ? $this->contextMenu->toArray()['submenu'] : null, 'alwaysOnTop' => $this->alwaysOnTop, + 'showOnAllWorkspaces' => $this->showOnAllWorkspaces, ]; } } diff --git a/src/MenuBar/MenuBarManager.php b/src/MenuBar/MenuBarManager.php index b4b09412..f9c2bdea 100644 --- a/src/MenuBar/MenuBarManager.php +++ b/src/MenuBar/MenuBarManager.php @@ -3,17 +3,15 @@ namespace Native\Laravel\MenuBar; use Native\Laravel\Client\Client; +use Native\Laravel\Menu\Menu; class MenuBarManager { - public function __construct(protected Client $client) - { - - } + public function __construct(protected Client $client) {} public function create() { - return (new PendingCreateMenuBar())->setClient($this->client); + return (new PendingCreateMenuBar)->setClient($this->client); } public function show() @@ -32,4 +30,33 @@ public function label(string $label) 'label' => $label, ]); } + + public function tooltip(string $tooltip) + { + $this->client->post('menu-bar/tooltip', [ + 'tooltip' => $tooltip, + ]); + } + + public function icon(string $icon) + { + $this->client->post('menu-bar/icon', [ + 'icon' => $icon, + ]); + } + + public function resize(int $width, int $height) + { + $this->client->post('menu-bar/resize', [ + 'width' => $width, + 'height' => $height, + ]); + } + + public function contextMenu(Menu $contextMenu) + { + $this->client->post('menu-bar/context-menu', [ + 'contextMenu' => $contextMenu->toArray()['submenu'], + ]); + } } diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index f332bcca..74dfba3c 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -2,12 +2,32 @@ namespace Native\Laravel; +use Illuminate\Console\Application; +use Illuminate\Foundation\Application as Foundation; +use Illuminate\Foundation\Http\Kernel; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\DB; +use Native\Laravel\ChildProcess as ChildProcessImplementation; +use Native\Laravel\Commands\DebugCommand; +use Native\Laravel\Commands\FreshCommand; use Native\Laravel\Commands\LoadPHPConfigurationCommand; use Native\Laravel\Commands\LoadStartupConfigurationCommand; use Native\Laravel\Commands\MigrateCommand; -use Native\Laravel\Commands\MinifyApplicationCommand; +use Native\Laravel\Commands\SeedDatabaseCommand; +use Native\Laravel\Contracts\ChildProcess as ChildProcessContract; +use Native\Laravel\Contracts\GlobalShortcut as GlobalShortcutContract; +use Native\Laravel\Contracts\PowerMonitor as PowerMonitorContract; +use Native\Laravel\Contracts\QueueWorker as QueueWorkerContract; +use Native\Laravel\Contracts\WindowManager as WindowManagerContract; +use Native\Laravel\DTOs\QueueConfig; +use Native\Laravel\Events\EventWatcher; +use Native\Laravel\Exceptions\Handler; +use Native\Laravel\GlobalShortcut as GlobalShortcutImplementation; +use Native\Laravel\Http\Middleware\PreventRegularBrowserAccess; use Native\Laravel\Logging\LogWatcher; +use Native\Laravel\PowerMonitor as PowerMonitorImplementation; +use Native\Laravel\Windows\WindowManager as WindowManagerImplementation; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -18,10 +38,10 @@ public function configurePackage(Package $package): void $package ->name('nativephp') ->hasCommands([ + DebugCommand::class, + FreshCommand::class, MigrateCommand::class, - MinifyApplicationCommand::class, - LoadStartupConfigurationCommand::class, - LoadPHPConfigurationCommand::class, + SeedDatabaseCommand::class, ]) ->hasConfigFile() ->hasRoute('api') @@ -32,29 +52,83 @@ public function packageRegistered() { $this->mergeConfigFrom($this->package->basePath('/../config/nativephp-internal.php'), 'nativephp-internal'); + $this->app->singleton(FreshCommand::class, function ($app) { + return new FreshCommand($app['migrator']); + }); + $this->app->singleton(MigrateCommand::class, function ($app) { return new MigrateCommand($app['migrator'], $app['events']); }); + $this->app->bind(WindowManagerContract::class, function (Foundation $app) { + return $app->make(WindowManagerImplementation::class); + }); + + $this->app->bind(ChildProcessContract::class, function (Foundation $app) { + return $app->make(ChildProcessImplementation::class); + }); + + $this->app->bind(GlobalShortcutContract::class, function (Foundation $app) { + return $app->make(GlobalShortcutImplementation::class); + }); + + $this->app->bind(PowerMonitorContract::class, function (Foundation $app) { + return $app->make(PowerMonitorImplementation::class); + }); + + $this->app->bind(QueueWorkerContract::class, function (Foundation $app) { + return $app->make(QueueWorker::class); + }); + if (config('nativephp-internal.running')) { + $this->app->singleton( + \Illuminate\Contracts\Debug\ExceptionHandler::class, + Handler::class + ); + + // Automatically prevent browser access + $this->app->make(Kernel::class)->pushMiddleware( + PreventRegularBrowserAccess::class, + ); + + Application::starting(function ($app) { + $app->resolveCommands([ + LoadStartupConfigurationCommand::class, + LoadPHPConfigurationCommand::class, + MigrateCommand::class, + ]); + }); + $this->configureApp(); } } + public function bootingPackage() + { + if (config('nativephp-internal.running')) { + $this->rewriteDatabase(); + } + } + protected function configureApp() { if (config('app.debug')) { app(LogWatcher::class)->register(); } - $this->rewriteStoragePath(); + app(EventWatcher::class)->register(); - $this->rewriteDatabase(); + $this->rewriteStoragePath(); $this->configureDisks(); config(['session.driver' => 'file']); config(['queue.default' => 'database']); + + // XXX: This logic may need to change when we ditch the internal web server + if (! $this->app->runningInConsole()) { + $this->fireUpQueueWorkers(); + } } protected function rewriteStoragePath() @@ -82,23 +156,49 @@ public function rewriteDatabase() { $databasePath = config('nativephp-internal.database_path'); + // Automatically create the database in development mode if (config('app.debug')) { $databasePath = database_path('nativephp.sqlite'); if (! file_exists($databasePath)) { touch($databasePath); + + Artisan::call('native:migrate'); } } - config(['database.connections.nativephp' => [ - 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), - 'database' => $databasePath, - 'prefix' => '', - 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), - ]]); + config([ + 'database.connections.nativephp' => [ + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => $databasePath, + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + ]); config(['database.default' => 'nativephp']); + config(['queue.failed.database' => 'nativephp']); + config(['queue.batching.database' => 'nativephp']); + config(['queue.connections.database.connection' => 'nativephp']); + + if (file_exists($databasePath)) { + DB::statement('PRAGMA journal_mode=WAL;'); + DB::statement('PRAGMA busy_timeout=5000;'); + } + } + + public function removeDatabase() + { + $databasePath = config('nativephp-internal.database_path'); + + if (config('app.debug')) { + $databasePath = database_path('nativephp.sqlite'); + } + + @unlink($databasePath); + @unlink($databasePath.'-shm'); + @unlink($databasePath.'-wal'); } protected function configureDisks(): void @@ -121,11 +221,23 @@ protected function configureDisks(): void continue; } - config(['filesystems.disks.'.$disk => [ - 'driver' => 'local', - 'root' => env($env, ''), - 'throw' => false, - ]]); + config([ + 'filesystems.disks.'.$disk => [ + 'driver' => 'local', + 'root' => env($env, ''), + 'throw' => false, + 'links' => 'skip', + ], + ]); + } + } + + protected function fireUpQueueWorkers(): void + { + $queueConfigs = QueueConfig::fromConfigArray(config('nativephp.queue_workers')); + + foreach ($queueConfigs as $queueConfig) { + $this->app->make(QueueWorkerContract::class)->up($queueConfig); } } } diff --git a/src/Notification.php b/src/Notification.php index fd4d023a..578d860e 100644 --- a/src/Notification.php +++ b/src/Notification.php @@ -6,19 +6,35 @@ class Notification { + public ?string $reference = null; + protected string $title; protected string $body; protected string $event = ''; - public function __construct(protected Client $client) + private bool $hasReply = false; + + private string $replyPlaceholder = ''; + + private array $actions = []; + + final public function __construct(protected Client $client) { + $this->title = config('app.name'); } public static function new() { - return new static(new Client()); + return new static(new Client); + } + + public function reference(string $reference): self + { + $this->reference = $reference; + + return $this; } public function title(string $title): self @@ -35,6 +51,21 @@ public function event(string $event): self return $this; } + public function hasReply(string $placeholder = ''): self + { + $this->hasReply = true; + $this->replyPlaceholder = $placeholder; + + return $this; + } + + public function addAction(string $label): self + { + $this->actions[] = $label; + + return $this; + } + public function message(string $body): self { $this->body = $body; @@ -42,12 +73,23 @@ public function message(string $body): self return $this; } - public function show(): void + public function show(): self { - $this->client->post('notification', [ + $response = $this->client->post('notification', [ + 'reference' => $this->reference, 'title' => $this->title, 'body' => $this->body, 'event' => $this->event, + 'hasReply' => $this->hasReply, + 'replyPlaceholder' => $this->replyPlaceholder, + 'actions' => array_map(fn (string $label) => [ + 'type' => 'button', + 'text' => $label, + ], $this->actions), ]); + + $this->reference = $response->json('reference'); + + return $this; } } diff --git a/src/PowerMonitor.php b/src/PowerMonitor.php new file mode 100644 index 00000000..c0307b11 --- /dev/null +++ b/src/PowerMonitor.php @@ -0,0 +1,39 @@ +client->get('power-monitor/get-system-idle-state', [ + 'threshold' => $threshold, + ])->json('result'); + + return SystemIdleStatesEnum::tryFrom($result) ?? SystemIdleStatesEnum::UNKNOWN; + } + + public function getSystemIdleTime(): int + { + return $this->client->get('power-monitor/get-system-idle-time')->json('result'); + } + + public function getCurrentThermalState(): ThermalStatesEnum + { + $result = $this->client->get('power-monitor/get-current-thermal-state')->json('result'); + + return ThermalStatesEnum::tryFrom($result) ?? ThermalStatesEnum::UNKNOWN; + } + + public function isOnBatteryPower(): bool + { + return $this->client->get('power-monitor/is-on-battery-power')->json('result'); + } +} diff --git a/src/Process.php b/src/Process.php index 7a189f9b..a78d983f 100644 --- a/src/Process.php +++ b/src/Process.php @@ -6,9 +6,7 @@ class Process { - public function __construct(protected Client $client) - { - } + public function __construct(protected Client $client) {} public function arch(): string { diff --git a/src/ProgressBar.php b/src/ProgressBar.php index b4a67607..c9e318f8 100644 --- a/src/ProgressBar.php +++ b/src/ProgressBar.php @@ -16,13 +16,11 @@ class ProgressBar protected float $maxSecondsBetweenRedraws = 1; - public function __construct(protected int $maxSteps, protected Client $client) - { - } + final public function __construct(protected int $maxSteps, protected Client $client) {} public static function create(int $maxSteps): static { - return new static($maxSteps, new Client()); + return new static($maxSteps, new Client); } public function start() diff --git a/src/QueueWorker.php b/src/QueueWorker.php new file mode 100644 index 00000000..2c998080 --- /dev/null +++ b/src/QueueWorker.php @@ -0,0 +1,52 @@ +has("nativephp.queue_workers.{$config}")) { + $config = QueueConfig::fromConfigArray([ + $config => config("nativephp.queue_workers.{$config}"), + ])[0]; + } + + if (! $config instanceof QueueConfig) { + throw new \InvalidArgumentException("Invalid queue configuration alias [$config]"); + } + + $command = app()->isLocal() + ? 'queue:listen' + : 'queue:work'; + + $this->childProcess->artisan( + [ + $command, + "--name={$config->alias}", + '--queue='.implode(',', $config->queuesToConsume), + "--memory={$config->memoryLimit}", + "--timeout={$config->timeout}", + "--sleep={$config->sleep}", + ], + 'queue_'.$config->alias, + persistent: true, + iniSettings: [ + 'memory_limit' => "{$config->memoryLimit}M", + ] + ); + } + + public function down(string $alias): void + { + $this->childProcess->stop('queue_'.$alias); + } +} diff --git a/src/Screen.php b/src/Screen.php index b0f4e7f2..9f34c101 100644 --- a/src/Screen.php +++ b/src/Screen.php @@ -6,9 +6,7 @@ class Screen { - public function __construct(protected Client $client) - { - } + public function __construct(protected Client $client) {} public function cursorPosition(): object { @@ -19,4 +17,32 @@ public function displays(): array { return $this->client->get('screen/displays')->json('displays'); } + + public function primary(): array + { + return $this->client->get('screen/primary-display')->json('primaryDisplay'); + } + + public function active(): array + { + return $this->client->get('screen/active')->json(); + } + + /** + * Returns the center of the screen where the mouse pointer is placed. + * + * @return array + */ + public function getCenterOfActiveScreen(): array + { + /* Navigate every screen and check for cursor position against the bounds of the screen. */ + $activeScreen = $this->active(); + + $bounds = $activeScreen['bounds']; + + return [ + 'x' => $bounds['x'] + $bounds['width'] / 2, + 'y' => $bounds['y'] + $bounds['height'] / 2, + ]; + } } diff --git a/src/Settings.php b/src/Settings.php index 0c7b6eec..ddcc0302 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -6,19 +6,33 @@ class Settings { - public function __construct(protected Client $client) - { - } + public function __construct(protected Client $client) {} - public function set($key, $value): void + public function set(string $key, $value): void { $this->client->post('settings/'.$key, [ 'value' => $value, ]); } - public function get($key, $default = null): mixed + public function get(string $key, $default = null): mixed + { + $response = $this->client->get('settings/'.$key)->json('value'); + + if ($response === null) { + return $default instanceof \Closure ? $default() : $default; + } + + return $response; + } + + public function forget(string $key): void + { + $this->client->delete('settings/'.$key); + } + + public function clear(): void { - return $this->client->get('settings/'.$key)->json('value') ?? $default; + $this->client->delete('settings/'); } } diff --git a/src/Shell.php b/src/Shell.php index 38dedca0..93893219 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -6,9 +6,7 @@ class Shell { - public function __construct(protected Client $client) - { - } + public function __construct(protected Client $client) {} public function showInFolder(string $path): void { diff --git a/src/Support/Environment.php b/src/Support/Environment.php new file mode 100644 index 00000000..70f20367 --- /dev/null +++ b/src/Support/Environment.php @@ -0,0 +1,31 @@ + 'Etc/GMT+12', + 'UTC-11' => 'Etc/GMT+11', + 'Aleutian Standard Time' => 'America/Adak', + 'Hawaiian Standard Time' => 'Pacific/Honolulu', + 'Marquesas Standard Time' => 'Pacific/Marquesas', + 'Alaskan Standard Time' => 'America/Anchorage', + 'UTC-09' => 'Etc/GMT+9', + 'Pacific Standard Time (Mexico)' => 'America/Tijuana', + 'UTC-08' => 'Etc/GMT+8', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'US Mountain Standard Time' => 'America/Phoenix', + 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', + 'Mountain Standard Time' => 'America/Denver', + 'Yukon Standard Time' => 'America/Whitehorse', + 'Central America Standard Time' => 'America/Guatemala', + 'Central Standard Time' => 'America/Chicago', + 'Easter Island Standard Time' => 'Pacific/Easter', + 'Central Standard Time (Mexico)' => 'America/Mexico_City', + 'Canada Central Standard Time' => 'America/Regina', + 'SA Pacific Standard Time' => 'America/Bogota', + 'Eastern Standard Time (Mexico)' => 'America/Cancun', + 'Eastern Standard Time' => 'America/New_York', + 'Haiti Standard Time' => 'America/Port-au-Prince', + 'Cuba Standard Time' => 'America/Havana', + 'US Eastern Standard Time' => 'America/Indianapolis', + 'Turks And Caicos Standard Time' => 'America/Grand_Turk', + 'Paraguay Standard Time' => 'America/Asuncion', + 'Atlantic Standard Time' => 'America/Halifax', + 'Venezuela Standard Time' => 'America/Caracas', + 'Central Brazilian Standard Time' => 'America/Cuiaba', + 'SA Western Standard Time' => 'America/La_Paz', + 'Pacific SA Standard Time' => 'America/Santiago', + 'Newfoundland Standard Time' => 'America/St_Johns', + 'Tocantins Standard Time' => 'America/Araguaina', + 'E. South America Standard Time' => 'America/Sao_Paulo', + 'Argentina Standard Time' => 'America/Argentina/Buenos_Aires', + 'Greenland Standard Time' => 'America/Godthab', + 'Montevideo Standard Time' => 'America/Montevideo', + 'Magallanes Standard Time' => 'America/Punta_Arenas', + 'Saint Pierre Standard Time' => 'America/Miquelon', + 'Bahia Standard Time' => 'America/Bahia', + 'UTC-02' => 'Etc/GMT+2', + 'Azores Standard Time' => 'Atlantic/Azores', + 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', + 'UTC' => 'Etc/UTC', + 'GMT Standard Time' => 'Europe/London', + 'Greenwich Standard Time' => 'Atlantic/Reykjavik', + 'Sao Tome Standard Time' => 'Africa/Sao_Tome', + 'Morocco Standard Time' => 'Africa/Casablanca', + 'W. Europe Standard Time' => 'Europe/Berlin', + 'Central Europe Standard Time' => 'Europe/Budapest', + 'Romance Standard Time' => 'Europe/Paris', + 'Central European Standard Time' => 'Europe/Warsaw', + 'W. Central Africa Standard Time' => 'Africa/Lagos', + 'Jordan Standard Time' => 'Asia/Amman', + 'GTB Standard Time' => 'Europe/Bucharest', + 'Middle East Standard Time' => 'Asia/Beirut', + 'Egypt Standard Time' => 'Africa/Cairo', + 'E. Europe Standard Time' => 'Europe/Chisinau', + 'Syria Standard Time' => 'Asia/Damascus', + 'West Bank Standard Time' => 'Asia/Hebron', + 'South Africa Standard Time' => 'Africa/Johannesburg', + 'FLE Standard Time' => 'Europe/Kiev', + 'Israel Standard Time' => 'Asia/Jerusalem', + 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', + 'Sudan Standard Time' => 'Africa/Khartoum', + 'Libya Standard Time' => 'Africa/Tripoli', + 'Namibia Standard Time' => 'Africa/Windhoek', + 'Arabic Standard Time' => 'Asia/Baghdad', + 'Turkey Standard Time' => 'Europe/Istanbul', + 'Arab Standard Time' => 'Asia/Riyadh', + 'Belarus Standard Time' => 'Europe/Minsk', + 'Russian Standard Time' => 'Europe/Moscow', + 'E. Africa Standard Time' => 'Africa/Nairobi', + 'Iran Standard Time' => 'Asia/Tehran', + 'Arabian Standard Time' => 'Asia/Dubai', + 'Astrakhan Standard Time' => 'Europe/Astrakhan', + 'Azerbaijan Standard Time' => 'Asia/Baku', + 'Russia Time Zone 3' => 'Europe/Samara', + 'Mauritius Standard Time' => 'Indian/Mauritius', + 'Saratov Standard Time' => 'Europe/Saratov', + 'Georgian Standard Time' => 'Asia/Tbilisi', + 'Caucasus Standard Time' => 'Asia/Yerevan', + 'Afghanistan Standard Time' => 'Asia/Kabul', + 'West Asia Standard Time' => 'Asia/Tashkent', + 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', + 'Pakistan Standard Time' => 'Asia/Karachi', + 'India Standard Time' => 'Asia/Kolkata', + 'Sri Lanka Standard Time' => 'Asia/Colombo', + 'Nepal Standard Time' => 'Asia/Kathmandu', + 'Central Asia Standard Time' => 'Asia/Almaty', + 'Bangladesh Standard Time' => 'Asia/Dhaka', + 'Omsk Standard Time' => 'Asia/Omsk', + 'Myanmar Standard Time' => 'Asia/Yangon', + 'SE Asia Standard Time' => 'Asia/Bangkok', + 'Altai Standard Time' => 'Asia/Barnaul', + 'W. Mongolia Standard Time' => 'Asia/Hovd', + 'North Asia Standard Time' => 'Asia/Krasnoyarsk', + 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', + 'Tomsk Standard Time' => 'Asia/Tomsk', + 'China Standard Time' => 'Asia/Shanghai', + 'North Asia East Standard Time' => 'Asia/Irkutsk', + 'Singapore Standard Time' => 'Asia/Singapore', + 'W. Australia Standard Time' => 'Australia/Perth', + 'Taipei Standard Time' => 'Asia/Taipei', + 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', + 'North Korea Standard Time' => 'Asia/Pyongyang', + 'Aus Central W. Standard Time' => 'Australia/Eucla', + 'Transbaikal Standard Time' => 'Asia/Chita', + 'Tokyo Standard Time' => 'Asia/Tokyo', + 'Korea Standard Time' => 'Asia/Seoul', + 'Yakutsk Standard Time' => 'Asia/Yakutsk', + 'Cen. Australia Standard Time' => 'Australia/Adelaide', + 'AUS Central Standard Time' => 'Australia/Darwin', + 'E. Australia Standard Time' => 'Australia/Brisbane', + 'AUS Eastern Standard Time' => 'Australia/Sydney', + 'West Pacific Standard Time' => 'Pacific/Port_Moresby', + 'Tasmania Standard Time' => 'Australia/Hobart', + 'Vladivostok Standard Time' => 'Asia/Vladivostok', + 'Lord Howe Standard Time' => 'Australia/Lord_Howe', + 'Bougainville Standard Time' => 'Pacific/Bougainville', + 'Russia Time Zone 10' => 'Asia/Srednekolymsk', + 'Magadan Standard Time' => 'Asia/Magadan', + 'Norfolk Standard Time' => 'Pacific/Norfolk', + 'Sakhalin Standard Time' => 'Asia/Sakhalin', + 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', + 'Russia Time Zone 11' => 'Asia/Kamchatka', + 'New Zealand Standard Time' => 'Pacific/Auckland', + 'UTC+12' => 'Etc/GMT-12', + 'Fiji Standard Time' => 'Pacific/Fiji', + 'Kamchatka Standard Time' => 'Asia/Kamchatka', + 'Chatham Islands Standard Time' => 'Pacific/Chatham', + 'UTC+13' => 'Etc/GMT-13', + 'Tonga Standard Time' => 'Pacific/Tongatapu', + 'Samoa Standard Time' => 'Pacific/Apia', + 'Line Islands Standard Time' => 'Pacific/Kiritimati', + ]; + + private array $nix = [ + 'ACDT' => 'Australia/Adelaide', + 'ACST' => 'Australia/Darwin', + 'ACT' => 'Australia/Darwin', + 'ACWST' => 'Australia/Eucla', + 'ADT' => 'America/Halifax', + 'AEDT' => 'Australia/Sydney', + 'AEST' => 'Australia/Brisbane', + 'AET' => 'Australia/Sydney', + 'AFT' => 'Asia/Kabul', + 'AKDT' => 'America/Anchorage', + 'AKST' => 'America/Anchorage', + 'ALMT' => 'Asia/Almaty', + 'AMST' => 'America/Campo_Grande', + 'AMT' => 'America/Campo_Grande', + 'ANAST' => 'Asia/Anadyr', + 'ANAT' => 'Asia/Anadyr', + 'AQTT' => 'Asia/Aqtau', + 'ART' => 'America/Argentina/Buenos_Aires', + 'AST' => 'America/Halifax', + 'AWDT' => 'Australia/Perth', + 'AWST' => 'Australia/Perth', + 'AZOST' => 'Atlantic/Azores', + 'AZOT' => 'Atlantic/Azores', + 'AZST' => 'Asia/Baku', + 'AZT' => 'Asia/Baku', + 'BNT' => 'Asia/Brunei', + 'BIOT' => 'Indian/Chagos', + 'BIT' => 'Pacific/Pago_Pago', + 'BOT' => 'America/La_Paz', + 'BRST' => 'America/Sao_Paulo', + 'BRT' => 'America/Sao_Paulo', + 'BST' => 'Europe/London', + 'BTT' => 'Asia/Thimphu', + 'CAT' => 'Africa/Maputo', + 'CCT' => 'Indian/Cocos', + 'CDT' => 'America/Chicago', + 'CEST' => 'Europe/Berlin', + 'CET' => 'Europe/Berlin', + 'CHADT' => 'Pacific/Chatham', + 'CHAST' => 'Pacific/Chatham', + 'CHOT' => 'Asia/Choibalsan', + 'CHOST' => 'Asia/Choibalsan', + 'CHST' => 'Pacific/Guam', + 'CHUT' => 'Pacific/Chuuk', + 'CIST' => 'Pacific/Rarotonga', + 'CIT' => 'Asia/Makassar', + 'CKT' => 'Pacific/Rarotonga', + 'CLST' => 'America/Santiago', + 'CLT' => 'America/Santiago', + 'COST' => 'America/Bogota', + 'COT' => 'America/Bogota', + 'CST' => 'America/Chicago', + 'CT' => 'America/Chicago', + 'CVT' => 'Atlantic/Cape_Verde', + 'CWST' => 'Australia/Eucla', + 'CXT' => 'Indian/Christmas', + 'DAVT' => 'Antarctica/Davis', + 'DDUT' => 'Antarctica/DumontDUrville', + 'DFT' => 'Europe/Paris', + 'EASST' => 'Pacific/Easter', + 'EAST' => 'Pacific/Easter', + 'EAT' => 'Africa/Nairobi', + 'ECT' => 'Europe/Paris', + 'EDT' => 'America/New_York', + 'EEST' => 'Europe/Istanbul', + 'EET' => 'Europe/Istanbul', + 'EGST' => 'Atlantic/Reykjavik', + 'EGT' => 'Atlantic/Reykjavik', + 'EST' => 'America/New_York', + 'ET' => 'America/New_York', + 'FET' => 'Europe/Kaliningrad', + 'FJT' => 'Pacific/Fiji', + 'FKST' => 'Atlantic/Stanley', + 'FKT' => 'Atlantic/Stanley', + 'FNT' => 'America/Noronha', + 'GALT' => 'Pacific/Galapagos', + 'GAMT' => 'Pacific/Gambier', + 'GET' => 'Asia/Tbilisi', + 'GFT' => 'America/Cayenne', + 'GILT' => 'Pacific/Tarawa', + 'GMT' => 'Atlantic/Reykjavik', + 'GST' => 'Asia/Dubai', + 'GYT' => 'America/Guyana', + 'HDT' => 'Pacific/Honolulu', + 'HAEC' => 'Europe/Paris', + 'HST' => 'Pacific/Honolulu', + 'HKT' => 'Asia/Hong_Kong', + 'HMT' => 'Asia/Kolkata', + 'HOVT' => 'Asia/Hovd', + 'ICT' => 'Asia/Bangkok', + 'IDLW' => 'Pacific/Wake', + 'IDT' => 'Asia/Jerusalem', + 'IOT' => 'Indian/Chagos', + 'IRDT' => 'Asia/Tehran', + 'IRKT' => 'Asia/Irkutsk', + 'IRST' => 'Asia/Tehran', + 'IST' => 'Asia/Kolkata', + 'JST' => 'Asia/Tokyo', + 'KALT' => 'Europe/Kaliningrad', + 'KGT' => 'Asia/Bishkek', + 'KOST' => 'Pacific/Kosrae', + 'KRAT' => 'Asia/Krasnoyarsk', + 'KST' => 'Asia/Seoul', + 'LHDT' => 'Australia/Lord_Howe', + 'LINT' => 'Pacific/Kiritimati', + 'MAGT' => 'Asia/Magadan', + 'MART' => 'Pacific/Marquesas', + 'MAWT' => 'Antarctica/Mawson', + 'MDT' => 'America/Denver', + 'MET' => 'Europe/Berlin', + 'MEST' => 'Europe/Berlin', + 'MHT' => 'Pacific/Kwajalein', + 'MIST' => 'Antarctica/Macquarie', + 'MIT' => 'Pacific/Apia', + 'MMT' => 'Asia/Yangon', + 'MSK' => 'Europe/Moscow', + 'MST' => 'America/Denver', + 'MUT' => 'Indian/Mauritius', + 'MVT' => 'Indian/Maldives', + 'MYT' => 'Asia/Kuala_Lumpur', + 'NCT' => 'Pacific/Noumea', + 'NDT' => 'America/St_Johns', + 'NFT' => 'Pacific/Norfolk', + 'NPT' => 'Asia/Kathmandu', + 'NST' => 'America/St_Johns', + 'NT' => 'America/St_Johns', + 'NUT' => 'Pacific/Niue', + 'NZDT' => 'Pacific/Auckland', + 'NZST' => 'Pacific/Auckland', + 'OMST' => 'Asia/Omsk', + 'ORAT' => 'Asia/Oral', + 'PDT' => 'America/Los_Angeles', + 'PETT' => 'Asia/Kamchatka', + 'PHOT' => 'Pacific/Enderbury', + 'PHST' => 'Asia/Manila', + 'PKT' => 'Asia/Karachi', + 'PMDT' => 'America/Miquelon', + 'PMST' => 'America/Miquelon', + 'PONT' => 'Pacific/Pohnpei', + 'PST' => 'America/Los_Angeles', + 'PT' => 'America/Los_Angeles', + 'PYST' => 'America/Asuncion', + 'PYT' => 'America/Asuncion', + 'RET' => 'Indian/Reunion', + 'ROTT' => 'Antarctica/Rothera', + 'SAKT' => 'Asia/Sakhalin', + 'SAMT' => 'Europe/Samara', + 'SAST' => 'Africa/Johannesburg', + 'SBT' => 'Pacific/Guadalcanal', + 'SCT' => 'Indian/Mahe', + 'SGT' => 'Asia/Singapore', + 'SLST' => 'Asia/Colombo', + 'SRET' => 'Asia/Srednekolymsk', + 'SRT' => 'America/Paramaribo', + 'SST' => 'Pacific/Apia', + 'SYOT' => 'Antarctica/Syowa', + 'TAHT' => 'Pacific/Tahiti', + 'THA' => 'Asia/Bangkok', + 'TFT' => 'Indian/Kerguelen', + 'TJT' => 'Asia/Dushanbe', + 'TKT' => 'Pacific/Fakaofo', + 'TLT' => 'Asia/Dili', + 'TMT' => 'Asia/Ashgabat', + 'TOT' => 'Pacific/Tongatapu', + 'TVT' => 'Pacific/Funafuti', + 'ULAST' => 'Asia/Ulaanbaatar', + 'ULAT' => 'Asia/Ulaanbaatar', + 'UYST' => 'America/Montevideo', + 'UYT' => 'America/Montevideo', + 'UZT' => 'Asia/Tashkent', + 'VET' => 'America/Caracas', + 'VLAT' => 'Asia/Vladivostok', + 'VOLT' => 'Europe/Volgograd', + 'VOST' => 'Antarctica/Vostok', + 'VUT' => 'Pacific/Efate', + 'WAKT' => 'Pacific/Wake', + 'WAST' => 'Africa/Windhoek', + 'WAT' => 'Africa/Lagos', + 'WEST' => 'Europe/Lisbon', + 'WET' => 'Europe/Lisbon', + 'WIT' => 'Asia/Jayapura', + 'WGST' => 'America/Godthab', + 'WGT' => 'America/Godthab', + 'WST' => 'Australia/Perth', + 'YAKT' => 'Asia/Yakutsk', + 'YEKT' => 'Asia/Yekaterinburg', + ]; + + public function translateFromWindowsString(string $source): string + { + return $this->windows[$source] ?? 'UTC'; + } + + public function translateFromAbbreviatedString(string $source): string + { + return $this->nix[$source] ?? 'UTC'; + } +} diff --git a/src/System.php b/src/System.php index 03b879b5..da23025a 100644 --- a/src/System.php +++ b/src/System.php @@ -4,12 +4,13 @@ use Native\Laravel\Client\Client; use Native\Laravel\DataObjects\Printer; +use Native\Laravel\Enums\SystemThemesEnum; +use Native\Laravel\Support\Environment; +use Native\Laravel\Support\Timezones; class System { - public function __construct(protected Client $client) - { - } + public function __construct(protected Client $client) {} public function canPromptTouchID(): bool { @@ -23,6 +24,25 @@ public function promptTouchID(string $reason): bool ])->successful(); } + public function canEncrypt(): bool + { + return $this->client->get('system/can-encrypt')->json('result'); + } + + public function encrypt(string $string): ?string + { + return $this->client->post('system/encrypt', [ + 'string' => $string, + ])->json('result'); + } + + public function decrypt(string $string): ?string + { + return $this->client->post('system/decrypt', [ + 'string' => $string, + ])->json('result'); + } + /** * @return array<\Native\Laravel\DataObjects\Printer> */ @@ -42,11 +62,44 @@ public function printers(): array })->toArray(); } - public function print(string $html, Printer $printer = null): void + public function print(string $html, ?Printer $printer = null): void { $this->client->post('system/print', [ 'html' => $html, 'printer' => $printer->name ?? '', ]); } + + public function printToPDF(string $html): string + { + return $this->client->post('system/print-to-pdf', [ + 'html' => $html, + ])->json('result'); + } + + public function timezone(): string + { + $timezones = new Timezones; + + if (Environment::isWindows()) { + $timezone = $timezones->translateFromWindowsString(exec('tzutil /g')); + } else { + $timezone = $timezones->translateFromAbbreviatedString(exec('date +%Z')); + } + + return $timezone; + } + + public function theme(?SystemThemesEnum $theme = null): SystemThemesEnum + { + if ($theme) { + $result = $this->client->post('system/theme', [ + 'theme' => $theme, + ])->json('result'); + } else { + $result = $this->client->get('system/theme')->json('result'); + } + + return SystemThemesEnum::from($result); + } } diff --git a/src/Windows/PendingOpenWindow.php b/src/Windows/PendingOpenWindow.php index 46175d45..50279534 100644 --- a/src/Windows/PendingOpenWindow.php +++ b/src/Windows/PendingOpenWindow.php @@ -12,5 +12,9 @@ public function __destruct() protected function open(): void { $this->client->post('window/open', $this->toArray()); + + foreach ($this->afterOpenCallbacks as $cb) { + $cb($this); + } } } diff --git a/src/Windows/Window.php b/src/Windows/Window.php index fc570587..7eadd248 100644 --- a/src/Windows/Window.php +++ b/src/Windows/Window.php @@ -6,25 +6,32 @@ use Native\Laravel\Concerns\HasDimensions; use Native\Laravel\Concerns\HasUrl; use Native\Laravel\Concerns\HasVibrancy; +use Native\Laravel\Facades\Window as WindowFacade; class Window { - use HasVibrancy; use HasDimensions; - use HasUrl; + use HasUrl { + HasUrl::url as defaultUrl; + } + use HasVibrancy; protected bool $autoHideMenuBar = false; protected bool $fullscreen = false; + protected bool $fullscreenable = true; + protected bool $kiosk = false; - protected $rememberState = false; + protected bool $rememberState = false; protected bool $alwaysOnTop = false; protected bool $showDevTools = false; + protected bool $devToolsOpen = false; + protected bool $resizable = true; protected bool $movable = true; @@ -37,18 +44,26 @@ class Window protected bool $focusable = true; + protected bool $focused = false; + protected bool $hasShadow = true; protected bool $frame = true; protected string $titleBarStyle = 'default'; + protected array $trafficLightPosition = []; + protected string $title = ''; protected string $id = 'main'; protected Client $client; + protected array $afterOpenCallbacks = []; + + protected array $webPreferences = []; + public function __construct(string $id) { $this->id = $id; @@ -64,10 +79,36 @@ public function id(string $id = 'main'): self return $this; } + public function getId(): string + { + return $this->id; + } + public function title(string $title): self { $this->title = $title; + if (! $this instanceof PendingOpenWindow) { + $this->client->post('window/title', [ + 'id' => $this->id, + 'title' => $title, + ]); + } + + return $this; + } + + public function url(string $url) + { + $this->defaultUrl($url); + + if (! $this instanceof PendingOpenWindow) { + $this->client->post('window/url', [ + 'id' => $this->id, + 'url' => $url, + ]); + } + return $this; } @@ -88,6 +129,13 @@ public function titleBarHiddenInset(): self return $this->titleBarStyle('hiddenInset'); } + public function trafficLightPosition(int $x, int $y): self + { + $this->trafficLightPosition = ['x' => $x, 'y' => $y]; + + return $this; + } + public function rememberState(): self { $this->rememberState = true; @@ -128,21 +176,43 @@ public function setClient(Client $client): self return $this; } - public function alwaysOnTop($alwaysOnTop = true): self + public function alwaysOnTop(bool $alwaysOnTop = true): self { $this->alwaysOnTop = $alwaysOnTop; return $this; } - public function showDevTools($showDevTools = true): self + public function showDevTools(bool $showDevTools = true): self { $this->showDevTools = $showDevTools; + if (! $this instanceof PendingOpenWindow) { + $this->client->post('window/show-dev-tools', [ + 'id' => $this->id, + ]); + } + + return $this; + } + + public function hideDevTools(): self + { + if (! $this instanceof PendingOpenWindow) { + $this->client->post('window/hide-dev-tools', [ + 'id' => $this->id, + ]); + } + return $this; } - public function resizable($resizable = true): static + public function devToolsOpen(): bool + { + return $this->devToolsOpen; + } + + public function resizable(bool $resizable = true): static { $this->resizable = $resizable; @@ -170,10 +240,27 @@ public function maximizable($maximizable = true): static return $this; } - public function closable($closable = true): static + public function minimized(): static + { + return $this->afterOpen(fn () => WindowFacade::minimize($this->id)); + } + + public function maximized(): static + { + return $this->afterOpen(fn () => WindowFacade::maximize($this->id)); + } + + public function closable(bool $closable = true): static { $this->closable = $closable; + if (! $this instanceof PendingOpenWindow) { + $this->client->post('window/closable', [ + 'id' => $this->id, + 'closable' => $closable, + ]); + } + return $this; } @@ -193,20 +280,34 @@ public function hideMenu($autoHideMenuBar = true): static return $this; } - public function fullscreen($fullscreen = false): static + public function fullscreen($fullscreen = true): static { $this->fullscreen = $fullscreen; return $this; } - public function kiosk($kiosk = false): static + public function fullscreenable($fullscreenable = true): static + { + $this->fullscreenable = $fullscreenable; + + return $this; + } + + public function kiosk($kiosk = true): static { $this->kiosk = $kiosk; return $this; } + public function webPreferences(array $preferences): static + { + $this->webPreferences = $preferences; + + return $this; + } + public function toArray() { return [ @@ -225,6 +326,7 @@ public function toArray() 'hasShadow' => $this->hasShadow, 'frame' => $this->frame, 'titleBarStyle' => $this->titleBarStyle, + 'trafficLightPosition' => $this->trafficLightPosition, 'showDevTools' => $this->showDevTools, 'vibrancy' => $this->vibrancy, 'transparency' => $this->transparent, @@ -237,8 +339,32 @@ public function toArray() 'closable' => $this->closable, 'title' => $this->title, 'fullscreen' => $this->fullscreen, + 'fullscreenable' => $this->fullscreenable, 'kiosk' => $this->kiosk, 'autoHideMenuBar' => $this->autoHideMenuBar, + 'transparent' => $this->transparent, + 'webPreferences' => $this->webPreferences, ]; } + + public function afterOpen(callable $cb): static + { + $this->afterOpenCallbacks[] = $cb; + + return $this; + } + + public function fromRuntimeWindow(object $window): static + { + foreach ($window as $key => $value) { + $this->{$key} = $value; + } + + return $this; + } + + public function __get($var) + { + return $this->$var ?? null; + } } diff --git a/src/Windows/WindowManager.php b/src/Windows/WindowManager.php index c7209c2d..bdd7f90d 100644 --- a/src/Windows/WindowManager.php +++ b/src/Windows/WindowManager.php @@ -4,15 +4,13 @@ use Native\Laravel\Client\Client; use Native\Laravel\Concerns\DetectsWindowId; +use Native\Laravel\Contracts\WindowManager as WindowManagerContract; -class WindowManager +class WindowManager implements WindowManagerContract { use DetectsWindowId; - public function __construct(protected Client $client) - { - - } + public function __construct(protected Client $client) {} public function open(string $id = 'main') { @@ -26,9 +24,48 @@ public function close($id = null) ]); } - public function current() + public function hide($id = null) { - return (object) $this->client->get('window/current')->json(); + $this->client->post('window/hide', [ + 'id' => $id ?? $this->detectId(), + ]); + } + + public function show($id = null) + { + $this->client->post('window/show', [ + 'id' => $id ?? $this->detectId(), + ]); + } + + public function current(): Window + { + $window = (object) $this->client->get('window/current')->json(); + + return (new Window($window->id)) + ->setClient($this->client) + ->fromRuntimeWindow($window); + } + + public function all(): array + { + $windows = (array) $this->client->get('window/all')->json(); + + return array_map( + fn ($window) => (new Window($window['id'])) + ->setClient($this->client) + ->fromRuntimeWindow((object) $window), + $windows + ); + } + + public function get(string $id): Window + { + $window = (object) $this->client->get("window/get/{$id}")->json(); + + return (new Window($id)) + ->setClient($this->client) + ->fromRuntimeWindow($window); } public function resize($width, $height, $id = null) @@ -42,7 +79,7 @@ public function resize($width, $height, $id = null) public function position($x, $y, $animated = false, $id = null) { - $this->client->post('window/resize', [ + $this->client->post('window/position', [ 'id' => $id ?? $this->detectId(), 'x' => $x, 'y' => $y, @@ -64,4 +101,18 @@ public function maximize($id = null): void 'id' => $id ?? $this->detectId(), ]); } + + public function minimize($id = null): void + { + $this->client->post('window/minimize', [ + 'id' => $id ?? $this->detectId(), + ]); + } + + public function reload($id = null): void + { + $this->client->post('window/reload', [ + 'id' => $id ?? $this->detectId(), + ]); + } } diff --git a/tests/ChildProcess/ChildProcessTest.php b/tests/ChildProcess/ChildProcessTest.php new file mode 100644 index 00000000..c78db8ce --- /dev/null +++ b/tests/ChildProcess/ChildProcessTest.php @@ -0,0 +1,126 @@ +makePartial() + ->shouldAllowMockingProtectedMethods(); + + $this->instance(ChildProcessImplement::class, $mock->allows([ + 'fromRuntimeProcess' => $mock, + ])); +}); + +it('can start a child process', function () { + ChildProcess::start('foo bar', 'some-alias', 'path/to/dir', ['baz' => 'zah']); + + Http::assertSent(function (Request $request) { + return $request->url() === 'http://localhost:4000/api/child-process/start' && + $request['alias'] === 'some-alias' && + $request['cmd'] === ['foo bar'] && + $request['cwd'] === 'path/to/dir' && + $request['env'] === ['baz' => 'zah']; + }); +}); + +it('can start a php command', function () { + ChildProcess::php("-r 'sleep(5);'", 'some-alias', ['baz' => 'zah']); + + Http::assertSent(function (Request $request) { + return $request->url() === 'http://localhost:4000/api/child-process/start-php' && + $request['alias'] === 'some-alias' && + $request['cmd'] === ["-r 'sleep(5);'"] && + $request['cwd'] === base_path() && + $request['env'] === ['baz' => 'zah']; + }); +}); + +it('can start a artisan command', function () { + ChildProcess::artisan('foo:bar --verbose', 'some-alias', ['baz' => 'zah']); + + Http::assertSent(function (Request $request) { + return $request->url() === 'http://localhost:4000/api/child-process/start-php' && + $request['alias'] === 'some-alias' && + $request['cmd'] === ['artisan', 'foo:bar --verbose'] && + $request['cwd'] === base_path() && + $request['env'] === ['baz' => 'zah']; + }); +}); + +it('accepts either a string or a array as start command argument', function () { + ChildProcess::start('foo bar', 'some-alias'); + Http::assertSent(fn (Request $request) => $request['cmd'] === ['foo bar']); + + ChildProcess::start(['foo', 'baz'], 'some-alias'); + Http::assertSent(fn (Request $request) => $request['cmd'] === ['foo', 'baz']); +}); + +it('accepts either a string or a array as php command argument', function () { + ChildProcess::php("-r 'sleep(5);'", 'some-alias'); + Http::assertSent(fn (Request $request) => $request['cmd'] === ["-r 'sleep(5);'"]); + + ChildProcess::php(['-r', "'sleep(5);'"], 'some-alias'); + Http::assertSent(fn (Request $request) => $request['cmd'] === ['-r', "'sleep(5);'"]); +}); + +it('accepts either a string or a array as artisan command argument', function () { + ChildProcess::artisan('foo:bar', 'some-alias'); + Http::assertSent(fn (Request $request) => $request['cmd'] === ['artisan', 'foo:bar']); + + ChildProcess::artisan(['foo:baz'], 'some-alias'); + Http::assertSent(fn (Request $request) => $request['cmd'] === ['artisan', 'foo:baz']); +}); + +it('sets the cwd to the base path if none was given', function () { + ChildProcess::start(['foo', 'bar'], 'some-alias', cwd: 'path/to/dir'); + Http::assertSent(fn (Request $request) => $request['cwd'] === 'path/to/dir'); + + ChildProcess::start(['foo', 'bar'], 'some-alias'); + Http::assertSent(fn (Request $request) => $request['cwd'] === base_path()); +}); + +it('can stop a child process', function () { + ChildProcess::stop('some-alias'); + + Http::assertSent(function (Request $request) { + return $request->url() === 'http://localhost:4000/api/child-process/stop' && + $request['alias'] === 'some-alias'; + }); +}); + +it('can send messages to a child process', function () { + ChildProcess::message('some-message', 'some-alias'); + + Http::assertSent(function (Request $request) { + return $request->url() === 'http://localhost:4000/api/child-process/message' && + $request['alias'] === 'some-alias' && + $request['message'] === 'some-message'; + }); +}); + +it('can mark a process as persistent', function () { + ChildProcess::start('foo bar', 'some-alias', persistent: true); + Http::assertSent(fn (Request $request) => $request['persistent'] === true); +}); + +it('can mark a php command as persistent', function () { + ChildProcess::php("-r 'sleep(5);'", 'some-alias', persistent: true); + Http::assertSent(fn (Request $request) => $request['persistent'] === true); +}); + +it('can mark a artisan command as persistent', function () { + ChildProcess::artisan('foo:bar', 'some-alias', persistent: true); + Http::assertSent(fn (Request $request) => $request['persistent'] === true); +}); + +it('marks the process as non-persistent by default', function () { + ChildProcess::start('foo bar', 'some-alias'); + Http::assertSent(fn (Request $request) => $request['persistent'] === false); +}); diff --git a/tests/DTOs/QueueWorkerTest.php b/tests/DTOs/QueueWorkerTest.php new file mode 100644 index 00000000..01197aac --- /dev/null +++ b/tests/DTOs/QueueWorkerTest.php @@ -0,0 +1,81 @@ +toBeArray(); + expect($configObject)->toHaveCount(count($config)); + + foreach ($config as $alias => $worker) { + if (is_string($worker)) { + expect( + Arr::first( + array_filter($configObject, fn (QueueConfig $config) => $config->alias === $worker)) + )->queuesToConsume->toBe(['default'] + ); + + expect(Arr::first(array_filter($configObject, + fn (QueueConfig $config) => $config->alias === $worker)))->memoryLimit->toBe(128); + expect(Arr::first(array_filter($configObject, + fn (QueueConfig $config) => $config->alias === $worker)))->timeout->toBe(60); + expect(Arr::first(array_filter($configObject, + fn (QueueConfig $config) => $config->alias === $worker)))->sleep->toBe(3); + + continue; + } + + expect( + Arr::first( + array_filter($configObject, fn (QueueConfig $config) => $config->alias === $alias)) + )->queuesToConsume->toBe($worker['queues'] ?? ['default'] + ); + + expect(Arr::first(array_filter($configObject, + fn (QueueConfig $config) => $config->alias === $alias)))->memoryLimit->toBe($worker['memory_limit'] ?? 128); + expect(Arr::first(array_filter($configObject, + fn (QueueConfig $config) => $config->alias === $alias)))->timeout->toBe($worker['timeout'] ?? 60); + expect(Arr::first(array_filter($configObject, + fn (QueueConfig $config) => $config->alias === $alias)))->sleep->toBe($worker['sleep'] ?? 3); + } +})->with([ + [ + [ + 'queue_workers' => [ + 'some_worker' => [ + 'queues' => ['default'], + 'memory_limit' => 64, + 'timeout' => 60, + 'sleep' => 3, + ], + ], + ], + ], + [ + [ + 'queue_workers' => [ + 'some_worker' => [], + 'another_worker' => [], + ], + ], + ], + [ + [ + 'queue_workers' => [ + 'some_worker' => [ + ], + 'another_worker' => [ + 'queues' => ['default', 'another'], + ], + 'yet_another_worker' => [ + 'memory_limit' => 256, + ], + 'one_more_worker' => [ + 'timeout' => 120, + ], + ], + ], + ], +]); diff --git a/tests/Fakes/FakeChildProcessTest.php b/tests/Fakes/FakeChildProcessTest.php new file mode 100644 index 00000000..aae9d264 --- /dev/null +++ b/tests/Fakes/FakeChildProcessTest.php @@ -0,0 +1,217 @@ +toBeInstanceOf(ChildProcessFake::class); +}); + +it('asserts get using string', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->get('testA'); + $fake->get('testB'); + + $fake->assertGet('testA'); + $fake->assertGet('testB'); + + try { + $fake->assertGet('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts get using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->get('testA'); + $fake->get('testB'); + + $fake->assertGet(fn (string $alias) => $alias === 'testA'); + $fake->assertGet(fn (string $alias) => $alias === 'testB'); + + try { + $fake->assertGet(fn (string $alias) => $alias === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts started using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->start('cmdA', 'aliasA', 'cwdA', ['envA'], true); + $fake->start('cmdB', 'aliasB', 'cwdB', ['envB'], false); + + $fake->assertStarted(fn ($cmd, $alias, $cwd, $env, $persistent) => $alias === 'aliasA' && + $cmd === 'cmdA' && + $cwd === 'cwdA' && + $env === ['envA'] && + $persistent === true); + + $fake->assertStarted(fn ($cmd, $alias, $cwd, $env, $persistent) => $alias === 'aliasB' && + $cmd === 'cmdB' && + $cwd === 'cwdB' && + $env === ['envB'] && + $persistent === false); + + try { + $fake->assertStarted(fn ($cmd, $alias, $cwd, $env, $persistent) => $alias === 'aliasC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts php using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->php('cmdA', 'aliasA', ['envA'], true); + $fake->php('cmdB', 'aliasB', ['envB'], false); + + $fake->assertPhp(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasA' && + $cmd === 'cmdA' && + $env === ['envA'] && + $persistent === true); + + $fake->assertPhp(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasB' && + $cmd === 'cmdB' && + $env === ['envB'] && + $persistent === false); + + try { + $fake->assertPhp(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts artisan using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->artisan('cmdA', 'aliasA', ['envA'], true); + $fake->artisan('cmdB', 'aliasB', ['envB'], false); + + $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasA' && + $cmd === 'cmdA' && + $env === ['envA'] && + $persistent === true); + + $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasB' && + $cmd === 'cmdB' && + $env === ['envB'] && + $persistent === false); + + try { + $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts stop using string', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->stop('testA'); + $fake->stop('testB'); + + $fake->assertStop('testA'); + $fake->assertStop('testB'); + + try { + $fake->assertStop('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts stop using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->stop('testA'); + $fake->stop('testB'); + + $fake->assertStop(fn (string $alias) => $alias === 'testA'); + $fake->assertStop(fn (string $alias) => $alias === 'testB'); + + try { + $fake->assertStop(fn (string $alias) => $alias === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts restart using string', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->restart('testA'); + $fake->restart('testB'); + + $fake->assertRestart('testA'); + $fake->assertRestart('testB'); + + try { + $fake->assertRestart('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts restart using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->restart('testA'); + $fake->restart('testB'); + + $fake->assertRestart(fn (string $alias) => $alias === 'testA'); + $fake->assertRestart(fn (string $alias) => $alias === 'testB'); + + try { + $fake->assertRestart(fn (string $alias) => $alias === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts message using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->message('messageA', 'aliasA'); + $fake->message('messageB', 'aliasB'); + + $fake->assertMessage(fn (string $message, string $alias) => $message === 'messageA' && $alias === 'aliasA'); + $fake->assertMessage(fn (string $message, string $alias) => $message === 'messageB' && $alias === 'aliasB'); + + try { + $fake->assertMessage(fn (string $message, string $alias) => $message === 'messageC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); diff --git a/tests/Fakes/FakeGlobalShortcutTest.php b/tests/Fakes/FakeGlobalShortcutTest.php new file mode 100644 index 00000000..0847cb97 --- /dev/null +++ b/tests/Fakes/FakeGlobalShortcutTest.php @@ -0,0 +1,122 @@ +toBeInstanceOf(GlobalShortcutFake::class); +}); + +it('asserts key using string', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->key('testA'); + $fake->key('testB'); + + $fake->assertKey('testA'); + $fake->assertKey('testB'); + + try { + $fake->assertKey('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts key using callable', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->key('testA'); + $fake->key('testB'); + + $fake->assertKey(fn (string $key) => $key === 'testA'); + $fake->assertKey(fn (string $key) => $key === 'testB'); + + try { + $fake->assertKey(fn (string $key) => $key === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts event using string', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->event('testA'); + $fake->event('testB'); + + $fake->assertEvent('testA'); + $fake->assertEvent('testB'); + + try { + $fake->assertEvent('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts event using callable', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->event('testA'); + $fake->event('testB'); + + $fake->assertEvent(fn (string $event) => $event === 'testA'); + $fake->assertEvent(fn (string $event) => $event === 'testB'); + + try { + $fake->assertEvent(fn (string $event) => $event === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts registered count', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->register(); + $fake->register(); + $fake->register(); + + $fake->assertRegisteredCount(3); + + try { + $fake->assertRegisteredCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts unregistered count', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->unregister(); + $fake->unregister(); + $fake->unregister(); + + $fake->assertUnregisteredCount(3); + + try { + $fake->assertUnregisteredCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); diff --git a/tests/Fakes/FakePowerMonitorTest.php b/tests/Fakes/FakePowerMonitorTest.php new file mode 100644 index 00000000..4761877c --- /dev/null +++ b/tests/Fakes/FakePowerMonitorTest.php @@ -0,0 +1,123 @@ +toBeInstanceOf(PowerMonitorFake::class); +}); + +it('asserts getSystemIdleState using int', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->getSystemIdleState(10); + $fake->getSystemIdleState(60); + + $fake->assertGetSystemIdleState(10); + $fake->assertGetSystemIdleState(60); + + try { + $fake->assertGetSystemIdleState(20); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts getSystemIdleState using callable', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->getSystemIdleState(10); + $fake->getSystemIdleState(60); + + $fake->assertGetSystemIdleState(fn (int $key) => $key === 10); + $fake->assertGetSystemIdleState(fn (int $key) => $key === 60); + + try { + $fake->assertGetSystemIdleState(fn (int $key) => $key === 20); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts getSystemIdleState count', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->getSystemIdleState(10); + $fake->getSystemIdleState(20); + $fake->getSystemIdleState(60); + + $fake->assertGetSystemIdleStateCount(3); + + try { + $fake->assertGetSystemIdleStateCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts getSystemIdleTime count', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->getSystemIdleTime(); + $fake->getSystemIdleTime(); + $fake->getSystemIdleTime(); + + $fake->assertGetSystemIdleTimeCount(3); + + try { + $fake->assertGetSystemIdleTimeCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts getCurrentThermalState count', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->getCurrentThermalState(); + $fake->getCurrentThermalState(); + $fake->getCurrentThermalState(); + + $fake->assertGetCurrentThermalStateCount(3); + + try { + $fake->assertGetCurrentThermalStateCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts isOnBatteryPower count', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->isOnBatteryPower(); + $fake->isOnBatteryPower(); + $fake->isOnBatteryPower(); + + $fake->assertIsOnBatteryPowerCount(3); + + try { + $fake->assertIsOnBatteryPowerCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); diff --git a/tests/Fakes/FakeQueueWorkerTest.php b/tests/Fakes/FakeQueueWorkerTest.php new file mode 100644 index 00000000..150f2a28 --- /dev/null +++ b/tests/Fakes/FakeQueueWorkerTest.php @@ -0,0 +1,69 @@ +toBeInstanceOf(QueueWorkerFake::class); +}); + +it('asserts up using callable', function () { + swap(QueueWorkerContract::class, $fake = app(QueueWorkerFake::class)); + + $fake->up(new QueueConfig('testA', ['default'], 123, 123, 0)); + $fake->up(new QueueConfig('testB', ['default'], 123, 123, 0)); + + $fake->assertUp(fn (QueueConfig $up) => $up->alias === 'testA'); + $fake->assertUp(fn (QueueConfig $up) => $up->alias === 'testB'); + + try { + $fake->assertUp(fn (QueueConfig $up) => $up->alias === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts down using string', function () { + swap(QueueWorkerContract::class, $fake = app(QueueWorkerFake::class)); + + $fake->down('testA'); + $fake->down('testB'); + + $fake->assertDown('testA'); + $fake->assertDown('testB'); + + try { + $fake->assertDown('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts down using callable', function () { + swap(QueueWorkerContract::class, $fake = app(QueueWorkerFake::class)); + + $fake->down('testA'); + $fake->down('testB'); + + $fake->assertDown(fn (string $alias) => $alias === 'testA'); + $fake->assertDown(fn (string $alias) => $alias === 'testB'); + + try { + $fake->assertDown(fn (string $alias) => $alias === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); diff --git a/tests/Fakes/FakeWindowManagerTest.php b/tests/Fakes/FakeWindowManagerTest.php new file mode 100644 index 00000000..f6c4804c --- /dev/null +++ b/tests/Fakes/FakeWindowManagerTest.php @@ -0,0 +1,353 @@ +toBeInstanceOf(WindowManagerFake::class); +}); + +it('asserts that a window was opened', function () { + Http::fake(['*' => Http::response(status: 200)]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows([ + new PendingOpenWindow('doesnt-matter'), + ]); + app(WindowManagerContract::class)->open('main'); + app(WindowManagerContract::class)->open('secondary'); + + $fake->assertOpened('main'); + $fake->assertOpened('secondary'); + + try { + $fake->assertOpened('tertiary'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts that a window was opened using callable', function () { + Http::fake(['*' => Http::response(status: 200)]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows([ + new PendingOpenWindow('doesnt-matter'), + ]); + + app(WindowManagerContract::class)->open('main'); + app(WindowManagerContract::class)->open('secondary'); + + $fake->assertOpened(fn (string $id) => $id === 'main'); + $fake->assertOpened(fn (string $id) => $id === 'secondary'); + + try { + $fake->assertOpened(fn (string $id) => $id === 'tertiary'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts that a window was closed', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->close('main'); + app(WindowManagerContract::class)->close('secondary'); + + $fake->assertClosed('main'); + $fake->assertClosed('secondary'); + + try { + $fake->assertClosed('tertiary'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts that a window was closed using callable', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->close('main'); + app(WindowManagerContract::class)->close('secondary'); + + $fake->assertClosed(fn (string $id) => $id === 'main'); + $fake->assertClosed(fn (string $id) => $id === 'secondary'); + + try { + $fake->assertClosed(fn (string $id) => $id === 'tertiary'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts that a window was hidden', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->hide('main'); + app(WindowManagerContract::class)->hide('secondary'); + + $fake->assertHidden('main'); + $fake->assertHidden('secondary'); + + try { + $fake->assertHidden('tertiary'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts that a window was hidden using callable', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->hide('main'); + app(WindowManagerContract::class)->hide('secondary'); + + $fake->assertHidden(fn (string $id) => $id === 'main'); + $fake->assertHidden(fn (string $id) => $id === 'secondary'); + + try { + $fake->assertHidden(fn (string $id) => $id === 'tertiary'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts that a window was shown', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->show('main'); + app(WindowManagerContract::class)->show('secondary'); + + $fake->assertShown('main'); + $fake->assertShown('secondary'); + + try { + $fake->assertShown('tertiary'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts opened count', function () { + Http::fake(['*' => Http::response(status: 200)]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows([ + new PendingOpenWindow('doesnt-matter'), + ]); + + app(WindowManagerContract::class)->open('main'); + app(WindowManagerContract::class)->open(); + app(WindowManagerContract::class)->open(); + + $fake->assertOpenedCount(3); + + try { + $fake->assertOpenedCount(4); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts closed count', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->close('main'); + app(WindowManagerContract::class)->close(); + app(WindowManagerContract::class)->close(); + + $fake->assertClosedCount(3); + + try { + $fake->assertClosedCount(4); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts hidden count', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->hide('main'); + app(WindowManagerContract::class)->hide(); + app(WindowManagerContract::class)->hide(); + + $fake->assertHiddenCount(3); + + try { + $fake->assertHiddenCount(4); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts shown count', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->show('main'); + app(WindowManagerContract::class)->show(); + app(WindowManagerContract::class)->show(); + + $fake->assertShownCount(3); + + try { + $fake->assertShownCount(4); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('forces the return value of current window', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + new WindowClass('testB'), + ]); + + expect($windows)->toContain(app(WindowManagerContract::class)->current()); +}); + +it('forces the return value of all windows', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + new WindowClass('testB'), + ]); + + expect(app(WindowManagerContract::class)->all())->toBe($windows); +}); + +it('forces the return value of a specific window', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + new WindowClass('testB'), + ]); + + expect(app(WindowManagerContract::class)->get('testA'))->toBe($windows[0]); + expect(app(WindowManagerContract::class)->get('testB'))->toBe($windows[1]); +}); + +test('that the get method throws an exception if multiple matching window ids exist', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + new WindowClass('testA'), + ]); + + app(WindowManagerContract::class)->get('testA'); +})->throws(InvalidArgumentException::class); + +test('that the get method throws an exception if no matching window id exists', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + ]); + + app(WindowManagerContract::class)->get('testB'); +})->throws(InvalidArgumentException::class); + +test('that the current method throws an exception if no forced window return values are provided', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->current(); +})->throws(InvalidArgumentException::class); + +test('that the all method throws an exception if no forced window return values are provided', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->all(); +})->throws(InvalidArgumentException::class); + +test('that the open method throws an exception if no forced window return values are provided', function () { + Http::fake([ + '*' => Http::response(status: 200), + ]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->open('test'); +})->throws(InvalidArgumentException::class); + +test('that the open method throws an exception if multiple matching window ids exist', function () { + Http::fake([ + '*' => Http::response(status: 200), + ]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + new WindowClass('testA'), + ]); + + app(WindowManagerContract::class)->open('testA'); +})->throws(InvalidArgumentException::class); + +test('that the open method returns a random window if none match the id provided', function () { + Http::fake([ + '*' => Http::response(status: 200), + ]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows($windows = [ + new PendingOpenWindow('testA'), + ]); + + expect($windows)->toContain(app(WindowManagerContract::class)->open('testC')); +}); + +test('that the open method returns a window if a matching window id exists', function () { + Http::fake([ + '*' => Http::response(status: 200), + ]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows($windows = [ + new PendingOpenWindow('testA'), + ]); + + expect(app(WindowManagerContract::class)->open('testA'))->toBe($windows[0]); +}); diff --git a/tests/Http/Controller/DispatchEventFromAppControllerTest.php b/tests/Http/Controller/DispatchEventFromAppControllerTest.php index 278c4145..39b2dcf7 100644 --- a/tests/Http/Controller/DispatchEventFromAppControllerTest.php +++ b/tests/Http/Controller/DispatchEventFromAppControllerTest.php @@ -4,10 +4,7 @@ class TestEvent { - public function __construct(public string $test = '') - { - - } + public function __construct(public string $test = '') {} } it('dispatches an event', function () { @@ -22,15 +19,16 @@ public function __construct(public string $test = '') Event::assertDispatched(TestEvent::class); }); -it('dispatches no event in case it does not exist', function () { +// Since 45b7ccfcb86ebf35be35c1eb7fbb9f05a224448f nonexistent classes are handled as string events +it('dispatches a string event', function () { Event::fake(); $this->withoutMiddleware() ->post('_native/api/events', [ - 'event' => InvalidEvent::class, + 'event' => 'some-event-that-is-no-class', ]); - Event::assertNotDispatched(InvalidEvent::class); + Event::assertDispatched('some-event-that-is-no-class'); }); it('passes the payload to the event', function () { diff --git a/tests/Http/Controller/NativeAppBootedControllerTest.php b/tests/Http/Controller/NativeAppBootedControllerTest.php index f8e96487..0a9bc7fe 100644 --- a/tests/Http/Controller/NativeAppBootedControllerTest.php +++ b/tests/Http/Controller/NativeAppBootedControllerTest.php @@ -5,10 +5,7 @@ class TestProvider { - public function boot() - { - - } + public function boot() {} } it('boots the NativePHP provider', function () { diff --git a/tests/MenuBar/MenuBarTest.php b/tests/MenuBar/MenuBarTest.php index ec19c7b6..79dbb801 100644 --- a/tests/MenuBar/MenuBarTest.php +++ b/tests/MenuBar/MenuBarTest.php @@ -1,7 +1,7 @@ set('nativephp-internal.api_url', 'https://jsonplaceholder.typicode.com/todos/1'); @@ -9,18 +9,24 @@ $menuBar = MenuBar::create() ->showDockIcon() ->alwaysOnTop() + ->showOnAllWorkspaces() ->label('milwad') ->icon('nativephp.png') ->url('https://github.com/milwad-dev') ->withContextMenu( - Menu::new()->label('My Application')->quit(), + Menu::make( + Menu::label('My Application'), + Menu::quit(), + ), ); $menuBarArray = $menuBar->toArray(); $this->assertTrue($menuBarArray['showDockIcon']); $this->assertTrue($menuBarArray['alwaysOnTop']); + $this->assertTrue($menuBarArray['showOnAllWorkspaces']); $this->assertEquals('milwad', $menuBarArray['label']); $this->assertEquals('https://github.com/milwad-dev', $menuBarArray['url']); $this->assertEquals('nativephp.png', $menuBarArray['icon']); + $this->assertEquals('trayCenter', $menuBarArray['windowPosition']); $this->assertIsArray($menuBarArray['contextMenu']); }); diff --git a/tests/QueueWorker/QueueWorkerTest.php b/tests/QueueWorker/QueueWorkerTest.php new file mode 100644 index 00000000..7605a565 --- /dev/null +++ b/tests/QueueWorker/QueueWorkerTest.php @@ -0,0 +1,44 @@ +toBe([ + 'queue:work', + "--name={$workerName}", + '--queue=default', + '--memory=128', + '--timeout=61', + '--sleep=5', + ]); + + expect($iniSettings)->toBe([ + 'memory_limit' => '128M', + ]); + + expect($alias)->toBe('queue_some_worker'); + expect($env)->toBeNull(); + expect($persistent)->toBeTrue(); + + return true; + }); +}); + +it('hits the child process with relevant alias spin down a queue worker', function () { + ChildProcess::fake(); + + QueueWorker::down('some_worker'); + + ChildProcess::assertStop('queue_some_worker'); +}); diff --git a/tests/Windows/WindowTest.php b/tests/Windows/WindowTest.php new file mode 100644 index 00000000..8cf21d6a --- /dev/null +++ b/tests/Windows/WindowTest.php @@ -0,0 +1,104 @@ +andReturn(new WindowClass('main')); + + $window = Window::open() + ->setClient(new Client) + ->id('main') + ->title('milwad') + ->titleBarStyle('milwad') + ->rememberState() + ->frameless() + ->focusable() + ->hasShadow() + ->alwaysOnTop() + ->showDevTools() + ->resizable() + ->movable() + ->minimizable() + ->maximizable() + ->closable() + ->fullscreen() + ->kiosk() + ->hideMenu(); + + $windowArray = $window->toArray(); + + expect($windowArray['id'])->toBe('main'); + expect($windowArray['title'])->toBe('milwad'); + expect($windowArray['titleBarStyle'])->toBe('milwad'); + expect($windowArray['rememberState'])->toBeTrue(); + expect($windowArray['frame'])->toBeFalse(); + expect($windowArray['focusable'])->toBeTrue(); + expect($windowArray['hasShadow'])->toBeTrue(); + expect($windowArray['alwaysOnTop'])->toBeTrue(); + expect($windowArray['showDevTools'])->toBeTrue(); + expect($windowArray['resizable'])->toBeTrue(); + expect($windowArray['movable'])->toBeTrue(); + expect($windowArray['minimizable'])->toBeTrue(); + expect($windowArray['maximizable'])->toBeTrue(); + expect($windowArray['closable'])->toBeTrue(); + expect($windowArray['fullscreen'])->toBeTrue(); + expect($windowArray['kiosk'])->toBeTrue(); + expect($windowArray['autoHideMenuBar'])->toBeTrue(); +}); + +it('test title bar for window', function () { + Window::shouldReceive('open') + ->andReturn(new WindowClass('main')); + + $window = Window::open() + ->titleBarHidden(); + + expect($window->toArray()['titleBarStyle'])->toBe('hidden'); + + $window->titleBarHiddenInset(); + + expect($window->toArray()['titleBarStyle'])->toBe('hiddenInset'); + + $window->titleBarButtonsOnHover(); + + expect($window->toArray()['titleBarStyle'])->toBe('customButtonsOnHover'); +}); + +it('test for trafficLightPosition in window', function () { + Window::shouldReceive('open') + ->andReturn(new WindowClass('main')); + + $window = Window::open() + ->trafficLightPosition(10, 10); + + expect($window->toArray()['trafficLightPosition']) + ->toBeArray() + ->toHaveKeys(['x', 'y']) + ->toHaveLength(2) + ->toMatchArray(['x' => 10, 'y' => 10]); + + $window->trafficLightPosition(5, 15); + + expect($window->toArray()['trafficLightPosition']) + ->toBeArray() + ->toHaveKeys(['x', 'y']) + ->toHaveLength(2) + ->toMatchArray(['x' => 5, 'y' => 15]); +}); + +it('test for invisibleFrameless in window', function () { + Window::shouldReceive('open') + ->andReturn(new WindowClass('main')); + + $window = Window::open()->invisibleFrameless(); + $windowArray = $window->toArray(); + + expect($windowArray['frame'])->toBeFalse(); + expect($windowArray['transparent'])->toBeTrue(); + expect($windowArray['focusable'])->toBeFalse(); + expect($windowArray['hasShadow'])->toBeFalse(); +});