123 Commits
Author SHA1 Message Date
Bd b9fdd3126b Merge branch 'whs:master' into master 2023-08-10 14:21:57 +02:00
Manatsawin Hanmongkolchai 8b86d856b2 Add note for alt1-electron
[ci skip]
2022-09-25 14:53:30 +07:00
Manatsawin Hanmongkolchai 49f88cb724 AppImage: Use Python 3.9.7 (#17) 2021-10-10 10:21:49 +07:00
Scott Chow 754dec4aba sc - dont open cors in browser 2021-10-10 09:52:09 +07:00
Manatsawin Hanmongkolchai c7de74d4d9 [ci skip] You don't need to sudo for xattr 2021-08-27 23:36:35 +07:00
Manatsawin Hanmongkolchai 70b7c4c47c [ci skip] Update instruction for macOS 2021-08-27 23:01:52 +07:00
Manatsawin Hanmongkolchai 41268929c5 Run make dist in poetry 2021-08-27 21:50:06 +07:00
Manatsawin Hanmongkolchai b2a7bca38a bump: Run on any ubuntu idc 2021-08-27 21:49:44 +07:00
Manatsawin Hanmongkolchai d84ca1f2fd Auto build mac app 2021-08-27 21:48:57 +07:00
Manatsawin Hanmongkolchai 6085ae275b Merge branch 'master' of github.com:whs/runekit 2021-08-27 21:43:08 +07:00
Manatsawin Hanmongkolchai 191557ea13 Fix readme indent 2021-08-21 14:48:01 +07:00
Manatsawin Hanmongkolchai 60d26edb65 Update readme for Mac, add zip task for mac 2021-08-21 14:39:16 +07:00
Manatsawin Hanmongkolchai b5723e6d79 Fix pyinstaller build 2021-08-21 13:59:18 +07:00
Manatsawin Hanmongkolchai 0c5386d744 Fix mac overlay 2021-08-21 13:13:17 +07:00
Manatsawin Hanmongkolchai d8a0cf9985 mac: Update deps, fixed window border and no-op app launches 2021-08-21 12:38:58 +07:00
Manatsawin Hanmongkolchai 2ec79babf9 Bump dependencies 2021-08-11 00:14:41 +07:00
Manatsawin Hanmongkolchai 4881a7680b Python 3.9.6 2021-08-10 23:22:53 +07:00
Manatsawin Hanmongkolchai 4a23bcfedd Bump python to 3.9.5 2021-06-11 20:22:55 +07:00
Manatsawin Hanmongkolchai 8d73198e97 Merge branch 'master' of github.com:whs/runekit 2021-06-11 20:21:44 +07:00
Bd 2df6fee5fe Fix TypeError Reduce of empty array with no initial value (#5) 2021-06-11 20:20:20 +07:00
bdnugget 58bcd0a41b Fix TypeError Reduce of empty array with no initial value 2021-06-10 21:09:30 +02:00
Manatsawin Hanmongkolchai 5957bd2051 Lockfile: update 2021-04-15 22:20:09 +07:00
Manatsawin Hanmongkolchai efda981bfa Split is_color_percent_gte into algo 2021-04-15 17:46:30 +07:00
Manatsawin Hanmongkolchai f9f02b11c8 overlay: Detect and get out of black screen condition automatically 2021-04-14 13:51:14 +07:00
Manatsawin Hanmongkolchai 53f6074195 Move the game detection off main thread 2021-04-14 11:13:51 +07:00
Manatsawin Hanmongkolchai bb84f1762b Dialog window follow game window movement 2021-04-13 22:39:23 +07:00
Manatsawin Hanmongkolchai 5a3f906afa Shading window goes to pin top right spot 2021-04-13 22:25:12 +07:00
Manatsawin Hanmongkolchai 87bb059084 Instance create/remove event 2021-04-13 22:12:57 +07:00
Manatsawin Hanmongkolchai 7e1b0252ef Show app at game top left 2021-04-13 10:45:35 +07:00
Manatsawin Hanmongkolchai 155ad1419f Implemented shade button 2021-04-13 10:39:48 +07:00
Manatsawin Hanmongkolchai b4721b9fcf Fix first boot assertionerror, and notification selector 2021-03-21 22:22:38 +07:00
Manatsawin Hanmongkolchai 1ec0c782fd Select notification type 2021-03-21 22:15:00 +07:00
Manatsawin Hanmongkolchai 18b2105ecf settings: window border 2021-03-21 22:05:04 +07:00
Manatsawin Hanmongkolchai 4fac791999 Install app 2021-03-18 14:33:59 +07:00
Manatsawin Hanmongkolchai 710407046b psutil: rename to avoid collision 2021-03-12 10:15:46 +07:00
Manatsawin Hanmongkolchai 2579f3a468 store: mkdir for parents too 2021-03-11 21:25:54 +07:00
Manatsawin Hanmongkolchai ea538dd007 Clear release and redeploy 2021-03-09 00:40:16 +07:00
Manatsawin Hanmongkolchai bb39708ae3 Update the way we bump tag 2021-03-09 00:33:22 +07:00
Manatsawin Hanmongkolchai d522dbd7a6 Remove tag prefix 2021-03-09 00:30:15 +07:00
Manatsawin Hanmongkolchai ef9fd20b0b Bump continuous tag 2021-03-09 00:29:23 +07:00
Manatsawin Hanmongkolchai 37823fe431 Update installation guide 2021-03-09 00:16:32 +07:00
Manatsawin Hanmongkolchai 7b38da8bf7 Merge branch 'whs/launcher' 2021-03-09 00:14:38 +07:00
Manatsawin Hanmongkolchai 315d050756 Separate master and tag build 2021-03-09 00:01:56 +07:00
Manatsawin Hanmongkolchai a072a0a1f9 Put appimage in root 2021-03-08 23:55:20 +07:00
Manatsawin Hanmongkolchai adb0280299 Get linuxdeploy normally 2021-03-08 23:54:08 +07:00
Manatsawin Hanmongkolchai 6a2f5a39d9 Fix permissions in ci 2021-03-08 23:49:37 +07:00
Manatsawin Hanmongkolchai db2846f760 Fix version to tag 2021-03-08 23:41:49 +07:00
Manatsawin Hanmongkolchai 1211038e63 Download liunxdeploy 2021-03-08 23:39:52 +07:00
Manatsawin Hanmongkolchai 064856f17a Fix steps 2021-03-08 23:35:12 +07:00
Manatsawin Hanmongkolchai cd21678ed2 Add CI 2021-03-08 23:33:53 +07:00
Manatsawin Hanmongkolchai 1d4c89dc40 Merge branch 'whs/appimage' 2021-03-08 22:37:39 +07:00
Manatsawin Hanmongkolchai 0a81cfbd78 Build AppImage 2021-03-08 22:37:02 +07:00
Manatsawin Hanmongkolchai 4a024bb603 App manager 2021-03-08 00:47:38 +07:00
Manatsawin Hanmongkolchai 0beb47389f Load app icon from appstore 2021-03-02 00:03:52 +07:00
Manatsawin Hanmongkolchai 2126edf10e Stabilize app launcher initialization, download and show app icon 2021-03-01 01:58:24 +07:00
Manatsawin Hanmongkolchai 3706fa911f Merge branch 'master' into whs/launcher 2021-02-28 13:15:27 +07:00
Manatsawin Hanmongkolchai 8eab6d9d21 Implement bindGetRegionBuffer, captureMultiAsync 2021-02-28 12:46:44 +07:00
Manatsawin Hanmongkolchai 61f33fbd2e Reenabled captureAsync, as cluesolver is at blame here 2021-02-28 00:35:35 +07:00
Manatsawin Hanmongkolchai 75c2d14077 Fixed launcher boot, disable captureAsync for now as it is bugged in upstream 2021-02-28 00:32:12 +07:00
Manatsawin Hanmongkolchai da7e01284e WIP: launcher 2021-02-22 11:51:41 +07:00
Manatsawin Hanmongkolchai 5bb38fea64 Launcher 2021-02-21 23:50:18 +07:00
Manatsawin Hanmongkolchai f9a9e0bdc0 Remove base64 param from image_to_stream 2021-02-21 00:16:11 +07:00
Manatsawin Hanmongkolchai ee68f4cea6 Fix getRegionRaw color space 2021-02-21 00:14:10 +07:00
Manatsawin Hanmongkolchai 67ce199dd2 Add support for the raw & async capture api 2021-02-20 23:10:48 +07:00
Manatsawin Hanmongkolchai 4d9a63c497 Numpy intermediate format (#4) 2021-02-20 22:24:16 +07:00
Manatsawin Hanmongkolchai 0750ad3d7e Move docs to wiki 2021-02-11 18:17:52 +07:00
Manatsawin Hanmongkolchai 9f9ce8c8f7 Remove sync overlay api code 2021-02-11 10:05:49 +07:00
Manatsawin Hanmongkolchai 9a5ca07d1e Fix qimage buffer not persisting 2021-02-10 23:06:56 +07:00
Manatsawin Hanmongkolchai a13b8dd2d8 Add support for overlayImage 2021-02-10 22:28:31 +07:00
Manatsawin Hanmongkolchai 2d36573285 Update meg answers 2021-02-10 18:13:34 +07:00
Manatsawin Hanmongkolchai a7cbeb8288 Update compatibility list 2021-02-10 00:09:44 +07:00
Manatsawin Hanmongkolchai 76865e6d13 Implement showNotification 2021-02-10 00:07:16 +07:00
Manatsawin Hanmongkolchai bce1e92369 Open link in desktop browser 2021-02-09 23:16:59 +07:00
Manatsawin Hanmongkolchai 352b1066c9 Allow media playback without user interaction 2021-02-09 22:05:50 +07:00
Manatsawin Hanmongkolchai e88f708f1b Cleanup dependencies from mac installer 2021-02-09 21:56:38 +07:00
Manatsawin Hanmongkolchai 20711b67ab mac setup: add link to the packaged release 2021-02-08 08:52:44 +07:00
Manatsawin Hanmongkolchai dbe6d20255 Add runekit file for platypus 2021-02-08 08:24:09 +07:00
Manatsawin Hanmongkolchai 2a2efeee22 Add pyinstaller
The .app don't actually work but the binary dist work somehow
2021-02-08 07:47:00 +07:00
Manatsawin Hanmongkolchai 93dfe1f304 QRC resource file 2021-02-08 07:13:10 +07:00
Manatsawin Hanmongkolchai b18e927b58 Fix tooltip design on mac 2021-02-08 06:46:30 +07:00
Manatsawin Hanmongkolchai 52c795dbab WIP: Settings UI 2021-02-08 00:52:55 +07:00
Manatsawin Hanmongkolchai 16bddeeaa8 DgKey is confirmed to work 2021-02-07 23:30:51 +07:00
Manatsawin Hanmongkolchai 8b01c00946 Fix overlay not working after switching back from map clue to other type 2021-02-07 16:14:03 +07:00
Manatsawin Hanmongkolchai 2f4d893713 Working scan and compass clue, reduce refresh rate on x11 to 100 2021-02-07 14:45:50 +07:00
Manatsawin Hanmongkolchai dd920e53d9 rect: fix pen size 2021-02-07 14:08:27 +07:00
Manatsawin Hanmongkolchai a764cc07f0 Add freeze support 2021-02-07 13:52:25 +07:00
Manatsawin Hanmongkolchai 084d0158fb Make overlay fast again 2021-02-07 12:50:19 +07:00
Manatsawin Hanmongkolchai 5f58e656ba Merge pull request #3 from schoentoon/master
Trim the xcffib reply to 4 bytes to avoid an unpacking error
2021-02-07 01:20:45 +07:00
Manatsawin Hanmongkolchai 6279592c22 Overlay text and group calls 2021-02-07 01:16:10 +07:00
Toon Schoenmakers 99146dfff5 Trim the xcffib reply to 4 bytes to avoid an unpacking error
I kept getting a "struct.error: unpack requires a buffer of 4 bytes"
upon launch. After some quick investigation I found out that this buffer
for me would contain something like b'\x03\x00\xc0\x07\x00\x00\x00\x00'.
Where the last 4 bytes would always be 0. This patch simply trims this
buffer to the first 4 bytes, after which everything started working on
my setup.
2021-02-06 02:43:19 +01:00
Manatsawin Hanmongkolchai d39b9018f1 Overlay on Linux and netstat 2021-02-05 01:06:40 +07:00
Manatsawin Hanmongkolchai e26997b9ab Overlay for mac 2021-02-05 00:08:23 +07:00
Manatsawin Hanmongkolchai b7f937a30a Add world hop logging 2021-02-04 11:57:09 +07:00
Manatsawin Hanmongkolchai 6d29ee79d1 window: Make it resizable on mac 2021-02-04 06:54:49 +07:00
Manatsawin Hanmongkolchai eab30cb7e2 Merge pull request #2 from scottpchow23/master
add documentation for macOS setup
2021-02-04 06:33:41 +07:00
Scott Chow 98a3afebe8 Update README.md 2021-02-03 14:05:14 -08:00
Scott Chow f99378684d Update download path based on Puprs's feedback 2021-02-03 13:22:25 -08:00
Scott Chow ac8f6ba1fb add last bit of clarification 2021-02-02 20:39:27 -08:00
Scott Chow 5eac90cdac update gitignore and make top level README point to sub-README 2021-02-02 20:27:49 -08:00
Scott Chow 16431d1678 Merge pull request #1 from scottpchow23/updateMacOSDocumentation 2021-02-02 20:20:29 -08:00
Scott Chow 47c6776614 Merge branch 'master' into updateMacOSDocumentation 2021-02-02 20:20:17 -08:00
Scott Chow 38055b0bf4 add documentation for macOS setup 2021-02-02 20:17:06 -08:00
Manatsawin Hanmongkolchai 77189f6bf2 Remove debug print 2021-02-03 08:44:02 +07:00
Manatsawin Hanmongkolchai 167ef17ece World hop check 2021-02-03 08:20:37 +07:00
Manatsawin Hanmongkolchai cb2485000a Merge branch 'master' of github.com:whs/runekit 2021-02-03 06:48:21 +07:00
Manatsawin Hanmongkolchai e5b964594b README: Update compatibility matrix, change to beta 2021-02-02 23:57:54 +07:00
Manatsawin Hanmongkolchai 502f2d4297 Better permission prompts in mac 2021-02-02 18:21:48 +07:00
Manatsawin Hanmongkolchai 86fdddbcbf Read path as relative to base dir (for packaged app) 2021-02-02 18:11:55 +07:00
Manatsawin Hanmongkolchai 9f25a2d8fd Add makefile 2021-02-02 15:53:04 +07:00
Manatsawin Hanmongkolchai 9da89d960c Rudimentary app launcher 2021-02-02 15:39:10 +07:00
Manatsawin Hanmongkolchai 53969e15e7 fixup! fixup! fixup! fixup! Fix readme 2021-01-31 21:01:28 +07:00
Manatsawin Hanmongkolchai b3cc3c08d0 fixup! fixup! fixup! Fix readme 2021-01-31 18:37:10 +07:00
Manatsawin Hanmongkolchai f72ad560fb fixup! fixup! Fix readme 2021-01-31 18:36:12 +07:00
Manatsawin Hanmongkolchai 342fe86e03 fixup! Fix readme 2021-01-31 17:21:19 +07:00
Manatsawin Hanmongkolchai 1ea5a73b11 Fix readme 2021-01-31 17:19:48 +07:00
Manatsawin Hanmongkolchai f7e356c6aa Remove active window log 2021-01-31 15:54:19 +07:00
Manatsawin Hanmongkolchai a764d06146 Alt1, positioning event on Linux 2021-01-31 14:48:13 +07:00
Manatsawin Hanmongkolchai bee71af748 Merge branch 'master' of github.com:whs/runekit 2021-01-31 11:57:31 +07:00
Manatsawin Hanmongkolchai b002c76e47 Better message on crash, and on macOS check accessibility permission on startup 2021-01-31 11:55:12 +07:00
Manatsawin Hanmongkolchai 4a53d75532 Alt1 style frame 2021-01-31 00:25:51 +07:00
Manatsawin Hanmongkolchai f183b160de Fix crashing on exit 2021-01-30 16:04:47 +07:00
Manatsawin Hanmongkolchai d8330fd2e1 Browser cache 2021-01-30 14:43:39 +07:00
Manatsawin Hanmongkolchai 24b5c95518 Backport from alt1 hotkey branch 2021-01-30 14:36:35 +07:00
86 changed files with 3812 additions and 1002 deletions

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
name: AppImage
on: [push]
jobs:
build:
runs-on: ubuntu-18.04 # Leave this on the earliest supported OS!
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('poetry.lock') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Get linuxdeploy
run: |
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -O linuxdeploy.AppImage
chmod +x linuxdeploy.AppImage
- name: Install dependencies
run: |
python -m pip install poetry
poetry install
- name: Build resources
run: poetry run make runekit/_resources.py
- name: Build AppImage
run: LINUXDEPLOY=./linuxdeploy.AppImage make dist/RuneKit.AppImage
# Deploy
- name: Upload to CI
uses: softprops/action-gh-release@v1
if: github.ref == 'refs/heads/master'
with:
tag_name: continuous
prerelease: true
name: Continuous Integration
body: These files are automatically built with GitHub Actions from the master branch.
files: |
dist/RuneKit.AppImage
dist/runekit.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload to CI
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
dist/RuneKit.AppImage
dist/runekit.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+47
View File
@@ -0,0 +1,47 @@
name: Mac
on: [push]
jobs:
build:
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('poetry.lock') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install poetry
poetry install
- name: Build resources
run: poetry run make runekit/_resources.py
- name: Build app
run: poetry run make dist/RuneKit.app.zip
# Deploy
- name: Upload to CI
uses: softprops/action-gh-release@v1
if: github.ref == 'refs/heads/master'
with:
tag_name: continuous
prerelease: true
name: Continuous Integration
body: These files are automatically built with GitHub Actions from the master branch.
files: |
dist/RuneKit.app.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload to CI
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
dist/RuneKit.app.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+21
View File
@@ -0,0 +1,21 @@
name: Bump tags
on: [push]
jobs:
bump:
runs-on: ubuntu-latest
steps:
- name: Bump tag
if: github.ref == 'refs/heads/master'
uses: richardsimko/update-tag@v1
with:
tag_name: continuous
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Clear old releases
uses: dev-drprasad/delete-tag-and-release@v0.1.3
if: github.ref == 'refs/heads/master'
with:
delete_release: true
tag_name: continuous
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+4
View File
@@ -4,3 +4,7 @@ runekit/image/format.c
runekit/image/format.*.*
setup.py
*.egg-info
__pycache__
runekit/_resources.py
*.AppImage
*.pyo
+25 -1
View File
@@ -14,6 +14,22 @@ The `game` package is the only one allowed to use platform-dependent code. All o
- game: Interact with the game window. Since accessing other application's window is not abstracted by Qt, this package is the only one allowed to be platform dependent
- image: Image processing tools
### Image format
The library has two internal image formats:
- numpy.ndarray of shape (height, width, 4)
- Current standard
- Store pixels in BGRA32 format (ARGB 8 bit per channel in little endian)
- Should be faster as no channel swapping is needed
- Requires copying when accessing raw buffer
- Support OpenCV operation
- PIL Image
- Deprecated
- Store pixels in RGBA32 format
- Slower as when communicating with Alt1 we need to swap to BGRA
- Can access raw buffer without copying
- Easier to work with for basic operations
## App
Packages related to Alt1 app.
@@ -25,9 +41,17 @@ Packages related to Alt1 app.
## UI
- host: Main window of RuneKit
- host: RuneKit Qt root
- ui: Collection of UI tools
## Porting
With this design it should be simple to port RuneKit to any platform supported by Qt by just reimplementing `game` for that platform
# Settings design
- apps/
- <b2b size 16 of manifest url> (json): App manifest
- _folder
- <folder name>/0 (str): App ID
- _meta
- isDefaultLoaded (bool): Has default apps list downloaded
+47
View File
@@ -0,0 +1,47 @@
LINUXDEPLOY ?= linuxdeploy-$(shell uname -m).AppImage
dev: runekit/_resources.py
runekit/_resources.py: resources.qrc $(wildcard runekit/**/*.js) $(wildcard runekit/**/*.png)
pyside2-rcc $< -o $@
# Sdist
dist/runekit.tar.gz: main.py poetry.lock runekit/_resources.py $(wildcard runekit/**/*)
poetry build -f sdist
cd dist; cp runekit-*.tar.gz runekit.tar.gz
# Mac
dist/RuneKit.app: RuneKit.spec main.py poetry.lock runekit/_resources.py $(wildcard runekit/**/*)
pyinstaller -w -n RuneKitApp --noconfirm \
--exclude-module tkinter \
-s -d noarchive \
--osx-bundle-identifier de.cupco.runekit \
$<
dist/RuneKit.app.zip: dist/RuneKit.app
cd dist; zip -r -9 RuneKit.app.zip RuneKit.app
# AppImage
build/python3.9.7.AppImage:
mkdir build || true
wget https://github.com/niess/python-appimage/releases/download/python3.9/python3.9.7-cp39-cp39-manylinux1_x86_64.AppImage -O "$@"
chmod +x "$@"
build/appdir: build/python3.9.7.AppImage
$< --appimage-extract
mv squashfs-root build/appdir
dist/RuneKit.AppImage: dist/runekit.tar.gz build/appdir deploy/runekit-appimage.sh
build/appdir/usr/bin/python3 -m pip install dist/runekit.tar.gz
rm $(wildcard build/appdir/*.desktop) $(wildcard build/appdir/usr/share/applications/*.desktop) $(wildcard build/appdir/usr/share/metainfo/*)
cp deploy/RuneKit.desktop build/appdir/
cp deploy/RuneKit.desktop build/appdir/usr/share/applications/
cp deploy/RuneKit.appdata.xml build/appdir/usr/share/metainfo/
cp deploy/runekit-appimage.sh build/appdir/AppRun
$(LINUXDEPLOY) --appdir build/appdir --output appimage
cp RuneKit-*.AppImage "$@"
.PHONY: dev
+44 -79
View File
@@ -4,48 +4,39 @@
Alt1-compatible toolbox for RuneScape 3, for Linux and macOS.
**Alpha quality software**
**See also [Alt1 Electron](https://github.com/skillbert/alt1-electron) the experimental official Alt1 replacement from the Alt1 authors.**
## What works
* [Compatibility](https://github.com/whs/runekit/wiki/App-Compatibility)
* [Troubleshooting](https://github.com/whs/runekit/wiki/Troubleshooting)
**Overlay are not supported yet**
## Installing
App | Linux | macOS
-------------------------------|-------|----------
**Clue Solver** ||
- Text/Emote | ✅ | ✅
- Map scroll | ? | ?
- Compass | ? | 2
- Scan | ? | ?
- Lockbox | ? | ?
- Celtic knot | ? | ✅
- Slide | ? | ✅
- Towers | ? | ?
Example app | ✅ | ✅
**AfkWarden** ||
- Inactive | ❌ | ✅
- XP Counter | ❌ | ❌
- Chatbox | 1 | ✅
- Crafting menu | ✅ | ✅
- Buffs | ✅ | ✅
- Action bar stats | ✅ | ✅
- Dialog box | ✅ | ✅
- Target death | ? | ?
- Sheathe stance | ? | ?
- Castle Wars | ? | ?
- Fight kiln waves | ? | ?
- Target death | ? | ?
- Item drops | ? | ?
**DgKey** ||
- Show map | ✅ | ?
- Track key | ❌ | ?
- Select player location | ❌ | ?
**[ArchMatCounter](https://zerogwafa.github.io/ArchMatCounter/appconfig.json)** | ? | ?
### Linux
- 1: Can be setup but only fire once (needs inactive to work)
- 2: Detected but cannot place mark
1. [Download RuneKit.AppImage](https://github.com/whs/runekit/releases/tag/continuous)
2. Mark file as executable (`chmod +x`)
3. Start the game
4. Run `RuneKit.AppImage`.
- On first start it will download app list
5. Right click the tray icon and start any application
## Running
### macOS
1. [Download RuneKit.app](https://github.com/whs/runekit/releases/tag/continuous) and unzip (if you use Safari it should automatically unzip)
2. Open Terminal (search in spotlight/launchpad if you can't find it)
3. Type `xattr -dr com.apple.quarantine ` (including trailing space) and drop the app onto Terminal so it would be like `yourname@yourmacname ~ % xattr -dr com.apple.quarantine /Users/yourname/Downloads/RuneKit.app`. Press enter.
4. Launch the app. The first launch might spring in the dock for a good minute.
5. If permission prompt appears, grant it in System Preferences > Security. **Then quit RuneKit (right click dock icon > quit or force quit) and start it again.** Don't quit RuneKit while it is downloading app list! The list of permissions are:
- Accessibility - for access to game window
- Input Monitoring - for hooking alt+1 and idle detection
- Screen Recording - for capturing game
6. Once all permission has been granted the application appears as system tray icon (top right)
## Troubleshooting
[See wiki](https://github.com/whs/runekit/wiki/Troubleshooting)
## Developer
This project use [Poetry](https://python-poetry.org) as package manager.
@@ -53,60 +44,34 @@ Requires Poetry 1.1.
```sh
poetry install
poetry build
cp build/lib.*/runekit/image/*.cpython*.* runekit/image/
# If previous fails and you're on Big Sur, try this instead
SYSTEM_VERSION_COMPAT=1 poetry install
poetry run make dev
# If you just want to load AFKWarden
poetry run python main.py https://runeapps.org/apps/alt1/afkscape/appconfig.json
# If you'd like to pick what app you load
poetry run python main.py
```
### Linux additional instruction
Requires libxcb to be installed
### macOS additional instruction
You'll probably need Command Line Tools to build the C code, run: `xcode-select --install` to get it
You will need to add Python in System Preferences > Security > Privacy in these sections:
- Accessibility
- Screen Recording
Note that Python might appear as the closest macOS application (eg. your terminal emulator) instead of Python
For best result, the game window should be on non-Retina display
## Developer
Start with `--remote-debugging-port=9222` to enable remote debugger protocol.
To debug, go to `chrome://inspect` on Chrome/Chromium.
## Technical Features
### Building .app on Mac
* [Alt1 Compatibility](compatibility.md)
Platform Feature | Linux | macOS
------------------------------|-------|--------
**Game Manager** ||
Instance change signals | ❌ | ❌
**Instance** ||
alt1_pressed | ❌ | ✅
game_activity | ❌ | ✅
positionChanged | ❌ | ✅
scalingChanged | ❌ | ❌
focusChanged | ✅ | ✅
set_taskbar_progress | ❌ | ❌
Window capture | ✅ | ✅
## Known issues
- On macOS some values include the size of window decorator, including the screenshot
- This can be tested with AfkScape color picker
1. Run the normal build steps
2. XCode > Settings > Account and download your dev key
- I don't have paid Apple Developer CA to test
3. Set codesign_identity in RuneKit.spec or leave it None for ad-hoc sign
5. `poetry run make dist/RuneKit.app.zip`
## License
This protect is [No Maintenance Intended](https://unmaintained.tech/).
This project is [No Maintenance Intended](https://unmaintained.tech/).
It is provided as-is and may not be actively maintained. There's no support, and no promise that pull requests will be
reviewed and merged. In other word: I wrote this for my own use and there's no point in keeping it to myself so I'm sharing it.
However, making it a proper open source project is an ongoing work that I'm not willing to do.
However, maintaining it as a proper open source project is an ongoing work that I don't have the bandwidth to do.
This project is [licensed](LICENSE) under GPLv3, and contains code from [third parties](THIRD_PARTY_LICENSE.md).
Contains code from the Alt1 application.
+54
View File
@@ -0,0 +1,54 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['main.py'],
pathex=['runekit'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=['tkinter'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=True)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='RuneKitApp',
debug=False,
bootloader_ignore_signals=False,
strip=True,
upx=False,
console=False,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=True,
upx=False,
upx_exclude=[],
name='RuneKit')
app = BUNDLE(coll,
name='RuneKit.app',
icon=None,
bundle_identifier='de.cupco.runekit',
info_plist={
'LSEnvironment': {
'LANG': 'en_US.UTF-8',
'LC_CTYPE': 'en_US.UTF-8'
}
})
-18
View File
@@ -1,18 +0,0 @@
from Cython.Build import cythonize
# use cythonize to build the extensions
modules = [
"runekit/image/format.pyx",
]
extensions = cythonize(modules)
def build(setup_kwargs):
"""Needed for the poetry building interface."""
setup_kwargs.update(
{
"ext_modules": extensions,
}
)
+18 -17
View File
@@ -3,8 +3,8 @@
- [ ] alt1.overlay
- [ ] alt1.captureMethod
- [ ] alt1.permission*
- [ ] alt1.lastWorldHop
- [ ] alt1.currentWorld
- [x] alt1.lastWorldHop
- [x] alt1.currentWorld
- [ ] alt1.rsPing
- [ ] alt1.rsFps
- [ ] alt1.openInfo
@@ -16,26 +16,27 @@
- [x] alt1.rsActive
- [x] alt1.userResize
- [ ] alt1.identiyAppUrl
- [ ] alt1.openBrowser
- [ ] alt1.clearBinds
- [x] alt1.openBrowser
- [x] alt1.clearBinds
- [ ] alt1.registerStatusDaemon
- [ ] alt1.getStatusDaemonState
- [ ] alt1.showNotification
- [ ] alt1.closeApp
- [x] alt1.showNotification
- [x] alt1.closeApp
- [x] alt1.setTooltip
- [x] alt1.clearTooltip
- [x] alt1.setTaskbarProgress
- [ ] alt1.setTitleBarText - Implemented but not validated
- [ ] alt1.overLayRect
- [ ] alt1.overLayTextEx
- [ ] alt1.overLayLine
- [ ] alt1.overLayImage
- [ ] alt1.overLayClearGroup
- [ ] alt1.overLaySetGroup
- [ ] alt1.overLayFreezeGroup
- [ ] alt1.overLayContinueGroup
- [ ] alt1.overLayRefreshGroup
- [ ] alt1.overLaySetGroupZIndex
- [ ] alt1.setTitleBarText
- [x] alt1.overLayRect
- [x] alt1.overLayText
- [x] alt1.overLayTextEx
- [x] alt1.overLayLine
- [x] alt1.overLayImage
- [x] alt1.overLayClearGroup
- [x] alt1.overLaySetGroup
- [x] alt1.overLayFreezeGroup
- [x] alt1.overLayContinueGroup
- [x] alt1.overLayRefreshGroup
- [x] alt1.overLaySetGroupZIndex
- [x] alt1.getRegion
- [ ] alt1.getRegionMulti
- [x] alt1.bindRegion
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>runekit</id>
<metadata_license>GPLv3</metadata_license>
<project_license>GPLv3</project_license>
<name>RuneKit</name>
<description>
<p>Alt1-compatible toolbox for RuneScape 3</p>
</description>
<launchable type="desktop-id">RuneKit.desktop</launchable>
<url type="homepage">https://github.com/whs/runekit</url>
<provides>
<binary>runekit</binary>
<python3>runekit</python3>
</provides>
</component>
+7
View File
@@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name=RuneKit
Exec=runekit
Comment=Alt1-compatible toolbox for RuneScape 3
Icon=python
Categories=Game;
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# Export APPRUN if running from an extracted image
self="$(readlink -f -- $0)"
here="${self%/*}"
APPDIR="${APPDIR:-${here}}"
# Export SSL certificate
export SSL_CERT_FILE="${APPDIR}/opt/_internal/certs.pem"
export PYTHONHOME="${APPDIR}/opt/python3.9"
exec $APPDIR/usr/bin/runekit $@
Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+75
View File
@@ -0,0 +1,75 @@
# MacOS Setup (for programming novices)
## Packaged release
[Grab the packaged .app](https://github.com/whs/runekit/releases/tag/platypus-initial)
Note that it might be outdated from the source release
## Running from source
This guide is assuming that you're willing install a few things and run a few commands in your `terminal` application.
Not sure how to open the terminal? You can try this [link](https://lmgtfy.app/?q=how+to+open+terminal+on+mac).
Got your terminal open? Great, time to run some commands.
### (1/4) Downloading this code
But first, we need to download this code. If you know how to use `git` to do that, great. If that last sentence made no sense, keep reading.
We're going to begin by downloading the code as a zip file. You can do this by clicking the green button on the GitHub repo page then clicking `Download Zip`, as shown below:
![download the code](./images/download-code.png)
Then, go ahead and open it with `Archive Utitlity`:
![unzip](./images/unzip.png)
You should now find the folder in your `Downloads` folder (use `Finder` to find it):
![downloads folder](./images/downloads-folder.png)
Great, we have the code! Next step please.
### (2/4) Installing Homebrew (a package manager)
Basically, follow the instructions on their (Homebrew's) website [here](https://brew.sh/).
Copy the command into your terminal prompt, then hit `enter`:
![install brew](./images/install-brew.png)
You can check that this worked by closing and reopening your terminal, then running `which brew`. You should get the following output:
![which brew](./images/which-brew.png)
Awesome, we'll need this tool later. For now, on to Python.
### (3/4) Installing Python (3.9.1)
Alright, we're going to install Python the straightforward way. We're going to do that by visiting Python's website [here](https://www.python.org/downloads/release/python-391/) and downloading + running the `macOS 64-bit` installer (which of the two depends on what type of mac you have).
![install python](./images/install-python.png)
If you're having trouble finding the downloads at the link, remember to scroll to the bottom of the page.
### (4/4) Installing poetry
Run the following command in your terminal:
```bash
brew install poetry
```
### One last thing...
Before you go and run the commands from the other page, you'll want to change what file directory your terminal is in to be the folder where you downloaded `runekit-master`. Assuming you downloaded it as instructed in step 1, you can do this by running the following:
```bash
cd ~/Downloads/runekit-master
```
**Note**: if you close and reopen the terminal, you'll need to renavigate to the correct folder (just rerun the above command).
That simple. Now you can go back to the other file and pick up where you left off, [here](../README.md#running).
+1 -41
View File
@@ -1,44 +1,4 @@
import logging
import sys
import click
from PySide2.QtWebEngine import QtWebEngine
from PySide2.QtWidgets import QApplication
from runekit.browser import init
from runekit.game import get_platform_manager
from runekit.host import Host
@click.command(
context_settings=dict(
ignore_unknown_options=True,
)
)
@click.option("--game-index", default=0, help="Game instance index, starting from 0")
@click.argument("qt_args", nargs=-1, type=click.UNPROCESSED)
@click.argument("app_url")
def main(app_url, game_index, qt_args):
logging.basicConfig(level=logging.DEBUG)
logging.info("Starting QtWebEngine")
QtWebEngine.initialize()
init()
app = QApplication([sys.argv[0], *qt_args])
host = Host()
game_manager = get_platform_manager()
logging.info("Scanning for game instances")
game_instances = game_manager.get_instances()
logging.info("Found %d instances", len(game_instances))
game = game_instances[game_index]
logging.info("Loading app")
host.start_app(app_url, game)
app.exec_()
sys.exit(0)
from runekit.main import main
if __name__ == "__main__":
main()
Generated
+456 -266
View File
@@ -1,3 +1,11 @@
[[package]]
name = "altgraph"
version = "0.17.2"
description = "Python graph (network) package"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "appdirs"
version = "1.4.4"
@@ -30,7 +38,7 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]]
name = "certifi"
version = "2020.12.5"
version = "2021.10.8"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
@@ -38,7 +46,7 @@ python-versions = "*"
[[package]]
name = "cffi"
version = "1.14.4"
version = "1.14.6"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
@@ -48,36 +56,53 @@ python-versions = "*"
pycparser = "*"
[[package]]
name = "chardet"
version = "4.0.0"
description = "Universal encoding detector for Python 2 and 3"
name = "charset-normalizer"
version = "2.0.6"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
python-versions = ">=3.5.0"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]]
name = "click"
version = "7.1.2"
version = "8.0.2"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "cython"
version = "0.29.21"
description = "The Cython compiler for writing C extensions for the Python language."
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "idna"
version = "2.10"
version = "3.2"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
python-versions = ">=3.5"
[[package]]
name = "macholib"
version = "1.15.2"
description = "Mach-O header analysis and editing"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
altgraph = ">=0.15"
[[package]]
name = "mypy-extensions"
@@ -87,22 +112,52 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "numpy"
version = "1.21.2"
description = "NumPy is the fundamental package for array computing with Python."
category = "main"
optional = false
python-versions = ">=3.7,<3.11"
[[package]]
name = "opencv-python-headless"
version = "4.5.3.56"
description = "Wrapper package for OpenCV python bindings."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
numpy = ">=1.21.0"
[[package]]
name = "pathspec"
version = "0.8.1"
version = "0.9.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "pillow"
version = "8.1.0"
version = "8.3.2"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "psutil"
version = "5.8.0"
description = "Cross-platform lib for process and system monitoring in Python."
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
[[package]]
name = "pycparser"
version = "2.20"
@@ -111,9 +166,34 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyinstaller"
version = "4.5.1"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
pyinstaller-hooks-contrib = ">=2020.6"
[package.extras]
encryption = ["tinyaes (>=1.0.0)"]
hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2021.3"
description = "Community maintained hooks for PyInstaller"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "pyobjc-core"
version = "7.1"
version = "7.3"
description = "Python<->ObjC Interoperability Module"
category = "main"
optional = false
@@ -121,52 +201,52 @@ python-versions = ">=3.6"
[[package]]
name = "pyobjc-framework-applicationservices"
version = "7.1"
version = "7.3"
description = "Wrappers for the framework ApplicationServices on macOS"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyobjc-core = ">=7.1"
pyobjc-framework-Cocoa = ">=7.1"
pyobjc-framework-Quartz = ">=7.1"
pyobjc-core = ">=7.3"
pyobjc-framework-Cocoa = ">=7.3"
pyobjc-framework-Quartz = ">=7.3"
[[package]]
name = "pyobjc-framework-cocoa"
version = "7.1"
version = "7.3"
description = "Wrappers for the Cocoa frameworks on macOS"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyobjc-core = ">=7.1"
pyobjc-core = ">=7.3"
[[package]]
name = "pyobjc-framework-coretext"
version = "7.1"
version = "7.3"
description = "Wrappers for the framework CoreText on macOS"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyobjc-core = ">=7.1"
pyobjc-framework-Cocoa = ">=7.1"
pyobjc-framework-Quartz = ">=7.1"
pyobjc-core = ">=7.3"
pyobjc-framework-Cocoa = ">=7.3"
pyobjc-framework-Quartz = ">=7.3"
[[package]]
name = "pyobjc-framework-quartz"
version = "7.1"
version = "7.3"
description = "Wrappers for the Quartz frameworks on macOS"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyobjc-core = ">=7.1"
pyobjc-framework-Cocoa = ">=7.1"
pyobjc-core = ">=7.3"
pyobjc-framework-Cocoa = ">=7.3"
[[package]]
name = "pyside2"
@@ -181,7 +261,7 @@ shiboken2 = "5.15.2"
[[package]]
name = "regex"
version = "2020.11.13"
version = "2021.10.8"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
@@ -189,21 +269,21 @@ python-versions = "*"
[[package]]
name = "requests"
version = "2.25.1"
version = "2.26.0"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<5"
idna = ">=2.5,<3"
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
urllib3 = ">=1.21.1,<1.27"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
name = "shiboken2"
@@ -215,7 +295,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.10"
[[package]]
name = "six"
version = "1.15.0"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
@@ -239,7 +319,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "typed-ast"
version = "1.4.2"
version = "1.4.3"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
@@ -247,7 +327,7 @@ python-versions = "*"
[[package]]
name = "typing-extensions"
version = "3.7.4.3"
version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
optional = false
@@ -255,7 +335,7 @@ python-versions = "*"
[[package]]
name = "urllib3"
version = "1.26.3"
version = "1.26.7"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
@@ -281,9 +361,13 @@ six = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.9,<3.10"
content-hash = "7798946c9dfae93142a329e7abe1bf4398c9c631bf0a19a0775b56db7809f949"
content-hash = "6d7ef5bf604ccfd42e5b3251cf4e6bc5e1ee5d89294bb39f72f984abeb1dd43f"
[metadata.files]
altgraph = [
{file = "altgraph-0.17.2-py2.py3-none-any.whl", hash = "sha256:743628f2ac6a7c26f5d9223c91ed8ecbba535f506f4b6f558885a8a56a105857"},
{file = "altgraph-0.17.2.tar.gz", hash = "sha256:ebf2269361b47d97b3b88e696439f6e4cbc607c17c51feb1754f90fb79839158"},
]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
@@ -292,177 +376,283 @@ black = [
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
]
certifi = [
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
]
cffi = [
{file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"},
{file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"},
{file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"},
{file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"},
{file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"},
{file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"},
{file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"},
{file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"},
{file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"},
{file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"},
{file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"},
{file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"},
{file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"},
{file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"},
{file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"},
{file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"},
{file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"},
{file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"},
{file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"},
{file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"},
{file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"},
{file = "cffi-1.14.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01"},
{file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"},
{file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"},
{file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"},
{file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"},
{file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"},
{file = "cffi-1.14.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e"},
{file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"},
{file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"},
{file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"},
{file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"},
{file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"},
{file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"},
{file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"},
{file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"},
{file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"},
{file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"},
{file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"},
{file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"},
{file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"},
{file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"},
{file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"},
{file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"},
{file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"},
{file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"},
{file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"},
{file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"},
{file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"},
{file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"},
{file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"},
{file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"},
{file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"},
{file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"},
{file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"},
{file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"},
{file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"},
{file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"},
{file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"},
{file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"},
{file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"},
{file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"},
{file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"},
{file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"},
{file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"},
{file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"},
{file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"},
{file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"},
{file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"},
{file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"},
{file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"},
{file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"},
{file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"},
{file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"},
{file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"},
{file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"},
{file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"},
{file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"},
{file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"},
{file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"},
{file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"},
]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
charset-normalizer = [
{file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
{file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
{file = "click-8.0.2-py3-none-any.whl", hash = "sha256:3fab8aeb8f15f5452ae7511ad448977b3417325bceddd53df87e0bb81f3a8cf8"},
{file = "click-8.0.2.tar.gz", hash = "sha256:7027bc7bbafaab8b2c2816861d8eb372429ee3c02e193fc2f93d6c4ab9de49c5"},
]
cython = [
{file = "Cython-0.29.21-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c541b2b49c6638f2b5beb9316726db84a8d1c132bf31b942dae1f9c7f6ad3b92"},
{file = "Cython-0.29.21-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b8d8497091c1dc8705d1575c71e908a93b1f127a174b2d472020f3d84263ac28"},
{file = "Cython-0.29.21-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:695a6bcaf9e12b1e471dfce96bbecf22a1487adc2ac6106b15960a2b51b97f5d"},
{file = "Cython-0.29.21-cp27-cp27m-win32.whl", hash = "sha256:171b9f70ceafcec5852089d0f9c1e75b0d554f46c882cd4e2e4acaba9bd7d148"},
{file = "Cython-0.29.21-cp27-cp27m-win_amd64.whl", hash = "sha256:539e59949aab4955c143a468810123bf22d3e8556421e1ce2531ed4893914ca0"},
{file = "Cython-0.29.21-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e93acd1f603a0c1786e0841f066ae7cef014cf4750e3cd06fd03cfdf46361419"},
{file = "Cython-0.29.21-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:2922e3031ba9ebbe7cb9200b585cc33b71d66023d78450dcb883f824f4969371"},
{file = "Cython-0.29.21-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:497841897942f734b0abc2dead2d4009795ee992267a70a23485fd0e937edc0b"},
{file = "Cython-0.29.21-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0ac10bf476476a9f7ef61ec6e44c280ef434473124ad31d3132b720f7b0e8d2a"},
{file = "Cython-0.29.21-cp34-cp34m-win32.whl", hash = "sha256:31c71a615f38401b0dc1f2a5a9a6c421ffd8908c4cd5bbedc4014c1b876488e8"},
{file = "Cython-0.29.21-cp34-cp34m-win_amd64.whl", hash = "sha256:c4b78356074fcaac04ecb4de289f11d506e438859877670992ece11f9c90f37b"},
{file = "Cython-0.29.21-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:b2f9172e4d6358f33ecce6a4339b5960f9f83eab67ea244baa812737793826b7"},
{file = "Cython-0.29.21-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:856c7fb31d247ce713d60116375e1f8153d0291ab5e92cca7d8833a524ba9991"},
{file = "Cython-0.29.21-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:715294cd2246b39a8edca464a8366eb635f17213e4a6b9e74e52d8b877a8cb63"},
{file = "Cython-0.29.21-cp35-cp35m-win32.whl", hash = "sha256:23f3a00b843a19de8bb4468b087db5b413a903213f67188729782488d67040e0"},
{file = "Cython-0.29.21-cp35-cp35m-win_amd64.whl", hash = "sha256:ccb77faeaad99e99c6c444d04862c6cf604204fe0a07d4c8f9cbf2c9012d7d5a"},
{file = "Cython-0.29.21-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e272ed97d20b026f4f25a012b25d7d7672a60e4f72b9ca385239d693cd91b2d5"},
{file = "Cython-0.29.21-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:8c6e25e9cc4961bb2abb1777c6fa9d0fa2d9b014beb3276cebe69996ff162b78"},
{file = "Cython-0.29.21-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:57ead89128dee9609119c93d3926c7a2add451453063147900408a50144598c6"},
{file = "Cython-0.29.21-cp36-cp36m-win32.whl", hash = "sha256:0e25c209c75df8785480dcef85db3d36c165dbc0f4c503168e8763eb735704f2"},
{file = "Cython-0.29.21-cp36-cp36m-win_amd64.whl", hash = "sha256:a0674f246ad5e1571ef29d4c5ec1d6ecabe9e6c424ad0d6fee46b914d5d24d69"},
{file = "Cython-0.29.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5da187bebe38030325e1c0b5b8a804d489410be2d384c0ef3ba39493c67eb51e"},
{file = "Cython-0.29.21-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9ce5e5209f8406ffc2b058b1293cce7a954911bb7991e623564d489197c9ba30"},
{file = "Cython-0.29.21-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5e545a48f919e40079b0efe7b0e081c74b96f9ef25b9c1ff4cdbd95764426b58"},
{file = "Cython-0.29.21-cp37-cp37m-win32.whl", hash = "sha256:c8435959321cf8aec867bbad54b83b7fb8343204b530d85d9ea7a1f5329d5ac2"},
{file = "Cython-0.29.21-cp37-cp37m-win_amd64.whl", hash = "sha256:540b3bee0711aac2e99bda4fa0a46dbcd8c74941666bfc1ef9236b1a64eeffd9"},
{file = "Cython-0.29.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:93f5fed1c9445fb7afe20450cdaf94b0e0356d47cc75008105be89c6a2e417b1"},
{file = "Cython-0.29.21-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9207fdedc7e789a3dcaca628176b80c82fbed9ae0997210738cbb12536a56699"},
{file = "Cython-0.29.21-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:603b9f1b8e93e8b494d3e89320c410679e21018e48b6cbc77280f5db71f17dc0"},
{file = "Cython-0.29.21-cp38-cp38-win32.whl", hash = "sha256:473df5d5e400444a36ed81c6596f56a5b52a3481312d0a48d68b777790f730ae"},
{file = "Cython-0.29.21-cp38-cp38-win_amd64.whl", hash = "sha256:b8a8a31b9e8860634adbca30fea1d0c7f08e208b3d7611f3e580e5f20992e5d7"},
{file = "Cython-0.29.21-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7ebaa8800c376bcdae596fb1372cb4232a5ef957619d35839520d2786f2debb9"},
{file = "Cython-0.29.21-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c111ac9abdf715762e4fb87395e59d61c0fbb6ce79eb2e24167700b6cfa8ba79"},
{file = "Cython-0.29.21-py2.py3-none-any.whl", hash = "sha256:5c4276fdcbccdf1e3c1756c7aeb8395e9a36874fa4d30860e7694f43d325ae13"},
{file = "Cython-0.29.21.tar.gz", hash = "sha256:e57acb89bd55943c8d8bf813763d20b9099cc7165c0f16b707631a7654be9cad"},
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
{file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
{file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
]
macholib = [
{file = "macholib-1.15.2-py2.py3-none-any.whl", hash = "sha256:885613dd02d3e26dbd2b541eb4cc4ce611b841f827c0958ab98656e478b9e6f6"},
{file = "macholib-1.15.2.tar.gz", hash = "sha256:1542c41da3600509f91c165cb897e7e54c0e74008bd8da5da7ebbee519d593d2"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
numpy = [
{file = "numpy-1.21.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52a664323273c08f3b473548bf87c8145b7513afd63e4ebba8496ecd3853df13"},
{file = "numpy-1.21.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a7b9db0a2941434cd930dacaafe0fc9da8f3d6157f9d12f761bbde93f46218"},
{file = "numpy-1.21.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f2dc79c093f6c5113718d3d90c283f11463d77daa4e83aeeac088ec6a0bda52"},
{file = "numpy-1.21.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a55e4d81c4260386f71d22294795c87609164e22b28ba0d435850fbdf82fc0c5"},
{file = "numpy-1.21.2-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:426a00b68b0d21f2deb2ace3c6d677e611ad5a612d2c76494e24a562a930c254"},
{file = "numpy-1.21.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:298156f4d3d46815eaf0fcf0a03f9625fc7631692bd1ad851517ab93c3168fc6"},
{file = "numpy-1.21.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09858463db6dd9f78b2a1a05c93f3b33d4f65975771e90d2cf7aadb7c2f66edf"},
{file = "numpy-1.21.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:805459ad8baaf815883d0d6f86e45b3b0b67d823a8f3fa39b1ed9c45eaf5edf1"},
{file = "numpy-1.21.2-cp37-cp37m-win32.whl", hash = "sha256:f545c082eeb09ae678dd451a1b1dbf17babd8a0d7adea02897a76e639afca310"},
{file = "numpy-1.21.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b160b9a99ecc6559d9e6d461b95c8eec21461b332f80267ad2c10394b9503496"},
{file = "numpy-1.21.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a5109345f5ce7ddb3840f5970de71c34a0ff7fceb133c9441283bb8250f532a3"},
{file = "numpy-1.21.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:209666ce9d4a817e8a4597cd475b71b4878a85fa4b8db41d79fdb4fdee01dde2"},
{file = "numpy-1.21.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c01b59b33c7c3ba90744f2c695be571a3bd40ab2ba7f3d169ffa6db3cfba614f"},
{file = "numpy-1.21.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e42029e184008a5fd3d819323345e25e2337b0ac7f5c135b7623308530209d57"},
{file = "numpy-1.21.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7fdc7689daf3b845934d67cb221ba8d250fdca20ac0334fea32f7091b93f00d3"},
{file = "numpy-1.21.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550564024dc5ceee9421a86fc0fb378aa9d222d4d0f858f6669eff7410c89bef"},
{file = "numpy-1.21.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf75d5825ef47aa51d669b03ce635ecb84d69311e05eccea083f31c7570c9931"},
{file = "numpy-1.21.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a9da45b748caad72ea4a4ed57e9cd382089f33c5ec330a804eb420a496fa760f"},
{file = "numpy-1.21.2-cp38-cp38-win32.whl", hash = "sha256:e167b9805de54367dcb2043519382be541117503ce99e3291cc9b41ca0a83557"},
{file = "numpy-1.21.2-cp38-cp38-win_amd64.whl", hash = "sha256:466e682264b14982012887e90346d33435c984b7fead7b85e634903795c8fdb0"},
{file = "numpy-1.21.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:dd0e3651d210068d13e18503d75aaa45656eef51ef0b261f891788589db2cc38"},
{file = "numpy-1.21.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92a0ab128b07799dd5b9077a9af075a63467d03ebac6f8a93e6440abfea4120d"},
{file = "numpy-1.21.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fde50062d67d805bc96f1a9ecc0d37bfc2a8f02b937d2c50824d186aa91f2419"},
{file = "numpy-1.21.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:640c1ccfd56724f2955c237b6ccce2e5b8607c3bc1cc51d3933b8c48d1da3723"},
{file = "numpy-1.21.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5de64950137f3a50b76ce93556db392e8f1f954c2d8207f78a92d1f79aa9f737"},
{file = "numpy-1.21.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b342064e647d099ca765f19672696ad50c953cac95b566af1492fd142283580f"},
{file = "numpy-1.21.2-cp39-cp39-win32.whl", hash = "sha256:30fc68307c0155d2a75ad19844224be0f2c6f06572d958db4e2053f816b859ad"},
{file = "numpy-1.21.2-cp39-cp39-win_amd64.whl", hash = "sha256:b5e8590b9245803c849e09bae070a8e1ff444f45e3f0bed558dd722119eea724"},
{file = "numpy-1.21.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d96a6a7d74af56feb11e9a443150216578ea07b7450f7c05df40eec90af7f4a7"},
{file = "numpy-1.21.2.zip", hash = "sha256:423216d8afc5923b15df86037c6053bf030d15cc9e3224206ef868c2d63dd6dc"},
]
opencv-python-headless = [
{file = "opencv-python-headless-4.5.3.56.tar.gz", hash = "sha256:3308d22efec8a69932709fc76456ef7e0d1cd9eb2272d94b7e5df0674d3e324f"},
{file = "opencv_python_headless-4.5.3.56-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b563106e93525bd30072a27448cc2b5c11e45bf6025e74e1d40efd5349a31736"},
{file = "opencv_python_headless-4.5.3.56-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:ecd84834f58daf94d3a4874a11997180380ef9af11b1fd5f81dbb8599b3c3446"},
{file = "opencv_python_headless-4.5.3.56-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0ef768d119a00eb6be0ba7953ab875218084d078db564af89af2b3891b6d7a3b"},
{file = "opencv_python_headless-4.5.3.56-cp36-cp36m-win32.whl", hash = "sha256:d70f3a44fb25f555da36e6c242cd1dffac6c4637eb900584f2a70a46e8e3cf05"},
{file = "opencv_python_headless-4.5.3.56-cp36-cp36m-win_amd64.whl", hash = "sha256:3599939dc61ab32c91cb2dbfd80a8199b53bf8a08cbb636ee9c2f27d74fe4f8f"},
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:3b1aa94894872e17a5fa07794562d8c44add78dd4c90a12fa1d946789e088784"},
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:fc9fa0fa88423c0e74db52fe13939aa506789f924a2a879e31f1d415e39a0db2"},
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90686bd1292972ad47298154c8ad37543080b2fc0417b68a350d30232b77f670"},
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:c54ad51bfc4623de287bd68f14f481f72300bd16a2d9e14850c1ef1a2dfc7dca"},
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-win32.whl", hash = "sha256:fd0f0629e27d97eec2f51500afbe0845876fdcd89d412035f4d7beb618c51066"},
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-win_amd64.whl", hash = "sha256:b224afffbc4ea21fe100824ff08a79ad1fae3088c1280b8ff815067b827e5240"},
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:bfe8989ed4af5e14ef1480c89fe259f5d4bc66c5ae0e7d56d7c778ef6ebbcdf4"},
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1a00d01d8d7250d6b87c63eb7b2df1e541fdb10919c22940cc53884bb12057c9"},
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:39eaae19d5fa906c7ec2db76447c0ff9df6305633493f1b86e4c0080ed1cb036"},
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:e19ba664cae0bc6ad39e2fbf3b3f16c93009132f792dcb7ca0105f0cc15997ed"},
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-win32.whl", hash = "sha256:ebfeaff7e0bb1c293008a49b2b2c033a7cec3805006a8284fa1a44ad8c30446b"},
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-win_amd64.whl", hash = "sha256:0efe30ce934ec2ab7272143d17f68885910a715c6aeffd02973c04b3d8b99ca6"},
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:0e8080dc5baf6855b663634d9c7a45732e2999bbe44aa5e4a14c15f3631998e4"},
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:465b157480902ecd6b2c4c46d547e4cfdec7595177bcc612c6f4c351966a5a1e"},
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d4c784339c366d27d0b4503a073c97a7cd78657937b3755fdbc9168a002f6409"},
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c7a1d365f2f8ce413847479a2d39bee2be6d5027119b08c4c5f6594f1f57673d"},
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-win32.whl", hash = "sha256:0935bf569e2edc1af8bc38f00813e37b1ba63e4b2eb0dc48651f405068b8b36d"},
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-win_amd64.whl", hash = "sha256:f1082f4f15a681b435686ed3fc0232128a222fed6792a47fae5cf081eb751ccf"},
]
pathspec = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
pillow = [
{file = "Pillow-8.1.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a"},
{file = "Pillow-8.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2"},
{file = "Pillow-8.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174"},
{file = "Pillow-8.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded"},
{file = "Pillow-8.1.0-cp36-cp36m-win32.whl", hash = "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d"},
{file = "Pillow-8.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d"},
{file = "Pillow-8.1.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234"},
{file = "Pillow-8.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8"},
{file = "Pillow-8.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17"},
{file = "Pillow-8.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7"},
{file = "Pillow-8.1.0-cp37-cp37m-win32.whl", hash = "sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e"},
{file = "Pillow-8.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b"},
{file = "Pillow-8.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0"},
{file = "Pillow-8.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a"},
{file = "Pillow-8.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d"},
{file = "Pillow-8.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae"},
{file = "Pillow-8.1.0-cp38-cp38-win32.whl", hash = "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59"},
{file = "Pillow-8.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c"},
{file = "Pillow-8.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6"},
{file = "Pillow-8.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378"},
{file = "Pillow-8.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7"},
{file = "Pillow-8.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0"},
{file = "Pillow-8.1.0-cp39-cp39-win32.whl", hash = "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b"},
{file = "Pillow-8.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865"},
{file = "Pillow-8.1.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9"},
{file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913"},
{file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206"},
{file = "Pillow-8.1.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9"},
{file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032"},
{file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820"},
{file = "Pillow-8.1.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5"},
{file = "Pillow-8.1.0.tar.gz", hash = "sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba"},
{file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"},
{file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"},
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"},
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"},
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"},
{file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"},
{file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"},
{file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"},
{file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"},
{file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"},
{file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"},
{file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"},
{file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"},
{file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"},
{file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"},
{file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"},
{file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"},
{file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"},
{file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"},
{file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"},
{file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"},
{file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"},
]
psutil = [
{file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"},
{file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"},
{file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"},
{file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"},
{file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"},
{file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"},
{file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"},
{file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"},
{file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"},
{file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"},
{file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"},
{file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"},
{file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"},
{file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"},
{file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"},
{file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"},
{file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"},
{file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"},
{file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"},
{file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"},
{file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"},
{file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"},
{file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"},
{file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"},
{file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"},
{file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"},
{file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"},
{file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"},
]
pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
]
pyinstaller = [
{file = "pyinstaller-4.5.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:ecc2baadeeefd2b6fbf39d13c65d4aa603afdda1c6aaaebc4577ba72893fee9e"},
{file = "pyinstaller-4.5.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4d848cd782ee0893d7ad9fe2bfe535206a79f0b6760cecc5f2add831258b9322"},
{file = "pyinstaller-4.5.1-py3-none-manylinux2014_i686.whl", hash = "sha256:8f747b190e6ad30e2d2fd5da9a64636f61aac8c038c0b7f685efa92c782ea14f"},
{file = "pyinstaller-4.5.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c587da8f521a7ce1b9efb4e3d0117cd63c92dc6cedff24590aeef89372f53012"},
{file = "pyinstaller-4.5.1-py3-none-win32.whl", hash = "sha256:fed9f5e4802769a416a8f2ca171c6be961d1861cc05a0b71d20dfe05423137e9"},
{file = "pyinstaller-4.5.1-py3-none-win_amd64.whl", hash = "sha256:aae456205c68355f9597411090576bb31b614e53976b4c102d072bbe5db8392a"},
{file = "pyinstaller-4.5.1.tar.gz", hash = "sha256:30733baaf8971902286a0ddf77e5499ac5f7bf8e7c39163e83d4f8c696ef265e"},
]
pyinstaller-hooks-contrib = [
{file = "pyinstaller-hooks-contrib-2021.3.tar.gz", hash = "sha256:169b09802a19f83593114821d6ba0416a05c7071ef0ca394f7bfb7e2c0c916c8"},
{file = "pyinstaller_hooks_contrib-2021.3-py2.py3-none-any.whl", hash = "sha256:a52bc3834281266bbf77239cfc9521923336ca622f44f90924546ed6c6d3ad5e"},
]
pyobjc-core = [
{file = "pyobjc-core-7.1.tar.gz", hash = "sha256:a0616d5d816b4471f8f782c3a9a8923d2cc85014d88ad4f7fec694be9e6ea349"},
{file = "pyobjc_core-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9fb45c9916f2a03ecd6b9ecde4c35d1d0f1a590ae2ea2372f9d9a360226ac1d"},
{file = "pyobjc_core-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff8e87358c6195a2937004f279050cce3d4c02cd77acd73c5ad367307def855"},
{file = "pyobjc_core-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:afb38efd3f2960eb49eb78552d465cfd025a9d6efa06cd4cd8694dafbe7c6e06"},
{file = "pyobjc_core-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cb329c4119044fe83bcb3c5d4794d636c706ff0cb7c1c77d36ef5c373100082"},
{file = "pyobjc_core-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7913d7b20217c294900537faf58e5cc15942ed7af277bf05db25667d18255114"},
{file = "pyobjc-core-7.3.tar.gz", hash = "sha256:5081aedf8bb40aac1a8ad95adac9e44e148a882686ded614adf46bb67fd67574"},
{file = "pyobjc_core-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e93ad769a20b908778fe950f62a843a6d8f0fa71996e5f3cc9fab5ae7d17771"},
{file = "pyobjc_core-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f63fd37bbf3785af4ddb2f86cad5ca81c62cfc7d1c0099637ca18343c3656c1"},
{file = "pyobjc_core-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9b1311f72f2e170742a7ee3a8149f52c35158dc024a21e88d6f1e52ba5d718b"},
{file = "pyobjc_core-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8d5e12a0729dfd1d998a861998b422d0a3e41923d75ea229bacf31372c831d7b"},
{file = "pyobjc_core-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:efdee8c4884405e0c0186c57f87d7bfaa0abc1f50b18e865db3caea3a1f329b9"},
]
pyobjc-framework-applicationservices = [
{file = "pyobjc-framework-ApplicationServices-7.1.tar.gz", hash = "sha256:84d790bafa256d05121950554645f9edd2ba4c59fc2684dfdfa701e8ad28baf6"},
{file = "pyobjc_framework_ApplicationServices-7.1-py2.py3-none-any.whl", hash = "sha256:ef8c7729e0c5b676f3bcafff3fde3714f938b821263cc3528db0cd37c3845991"},
{file = "pyobjc-framework-ApplicationServices-7.3.tar.gz", hash = "sha256:1925ac30a817e557d1c08450005103bbf76ebd3ff473631fe9875070377b0b4d"},
{file = "pyobjc_framework_ApplicationServices-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:daa4a9c51a927630fdd3d3f627e03ebc370aee3c397305db85a0a8ba4c28ae93"},
{file = "pyobjc_framework_ApplicationServices-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:167aa21ee47b0ee6e4e399915371d183ae84880dc3813c27519e759acb9d20c9"},
{file = "pyobjc_framework_ApplicationServices-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a98f0f1e21465868f9dd32588ae71e5e6a4cb5c434d4158c9e12273fd7b8f27"},
{file = "pyobjc_framework_ApplicationServices-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2d55796610e6293e83cc40183347e7f75a7c0682775cc19e5986945efa9cac1b"},
{file = "pyobjc_framework_ApplicationServices-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:afd1ef147447fe7b06a271458eabb37ece6436705abf86265d7fb57310eca45f"},
]
pyobjc-framework-cocoa = [
{file = "pyobjc-framework-Cocoa-7.1.tar.gz", hash = "sha256:67966152b3d38a0225176fceca2e9f56d849c8e7445548da09a00cb13155ec3e"},
{file = "pyobjc_framework_Cocoa-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bef77eafaac5eaf1d91d479d5483fd02216caa3edc27e8f5adc9af0b3fecdac3"},
{file = "pyobjc_framework_Cocoa-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2ea3582c456827dc20e648c905fdbcf8d3dfae89434f981e9b761cd07262049"},
{file = "pyobjc_framework_Cocoa-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a4050f2d776f40c2409a151c6f7896420e936934b3bdbfabedf91509637ed9b"},
{file = "pyobjc_framework_Cocoa-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f68f022f1f6d5985c418e10c6608c562fcf4bfe3714ec64fd10ce3dc6221bd4"},
{file = "pyobjc_framework_Cocoa-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ecfefd4c48dae42275c18679c69f6f2fff970e711097515a0a8732fc10194018"},
{file = "pyobjc-framework-Cocoa-7.3.tar.gz", hash = "sha256:b18d05e7a795a3455ad191c3e43d6bfa673c2a4fd480bb1ccf57191051b80b7e"},
{file = "pyobjc_framework_Cocoa-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9edffdfa6dd1f71f21b531c3e61fdd3e4d5d3bf6c5a528c98e88828cd60bac11"},
{file = "pyobjc_framework_Cocoa-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:35a6340437a4e0109a302150b7d1f6baf57004ccf74834f9e6062fcafe2fd8d7"},
{file = "pyobjc_framework_Cocoa-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7c3886f2608ab3ed02482f8b2ebf9f782b324c559e84b52cfd92dba8a1109872"},
{file = "pyobjc_framework_Cocoa-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e8e7a1a82cca21d9bfac9115baf065305f3da577bf240085964dfb9c9fff337"},
{file = "pyobjc_framework_Cocoa-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c15f43077c9a2ba1853eb402ff7a9515df9e584315bc2fcb779d4c95ef46dc5"},
]
pyobjc-framework-coretext = [
{file = "pyobjc-framework-CoreText-7.1.tar.gz", hash = "sha256:f3682299f02e75b4d5c7b0ede216e1cea3556ede0fc98a5319e7c2d359b140a7"},
{file = "pyobjc_framework_CoreText-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:83b8bd84fa7666983ba2ed1d8cf7d0cc67e1c2190f5b59c1f026806da896146e"},
{file = "pyobjc_framework_CoreText-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:171d3d60738b51b971ad4ea91016e9a2cc8164311b0aa2de34cf84da1faa7d30"},
{file = "pyobjc_framework_CoreText-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d33565a158a511a2cd3ac4c15ececb5b6a033f8ead6d18ee688a6de147c52d1b"},
{file = "pyobjc_framework_CoreText-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9c8eed9c876e723a93bdff78ffd767f9da6132562702b95f3b289a9618c196a3"},
{file = "pyobjc_framework_CoreText-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd42eeaa4351c24fb3d12bf954ffb5c588d3a33ae363388c092e5bd4c5e10d27"},
{file = "pyobjc-framework-CoreText-7.3.tar.gz", hash = "sha256:5b5fc91bcbd2fe5199f6b65971d62bea02f942c76d6acb59168c041c7af435d9"},
{file = "pyobjc_framework_CoreText-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a3ae27d5756b9d62d113e7c4f12022f8812bc95bc277f920f0fe2ca45b5272be"},
{file = "pyobjc_framework_CoreText-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fd3f11b3358cbf5a56d4a01e736322daa5b6ce6e3701d41cc9eafcede0267faa"},
{file = "pyobjc_framework_CoreText-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d290880894256497425a1abd116076e7a74c8b92a2b1b27d899ede16f52e5f20"},
{file = "pyobjc_framework_CoreText-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b172b22f51625a0f92c9730b9d9a193f12bd5e56552d77b6841dfaac0d16bab0"},
{file = "pyobjc_framework_CoreText-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:862e5c2c1bd6f5d276321c9a2d557dd8fb62ab1da55722a582b9e6621b05c1bd"},
]
pyobjc-framework-quartz = [
{file = "pyobjc-framework-Quartz-7.1.tar.gz", hash = "sha256:73102c9f4dbfa13275621014785ab3b684cf03ce93a4b0b270500c795349bea9"},
{file = "pyobjc_framework_Quartz-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7207a26244f02d4534ebb007fa55a9dc7c1b7fbb490d1e89e0d62cfd175e20f3"},
{file = "pyobjc_framework_Quartz-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5bc7a4fb3ea80b5af6910cc27729a0774a96327a69583fcf28057cb2ffce33ac"},
{file = "pyobjc_framework_Quartz-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c0469d60d4a79fc252f74adaa8177d2c680621d858c1b8ef19c411e903e2c892"},
{file = "pyobjc_framework_Quartz-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:04953c031fc35020682bd4613b9b5a9688bdb9eab7ed76fd8dcf028783568b4f"},
{file = "pyobjc_framework_Quartz-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d8e0c086faf649f86386d0ed99194c6d0704b602576e2b258532b635b510b790"},
{file = "pyobjc-framework-Quartz-7.3.tar.gz", hash = "sha256:98812844c34262def980bdf60923a875cd43428a8375b6fd53bd2cd800eccf0b"},
{file = "pyobjc_framework_Quartz-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ef18f5a16511ded65980bf4f5983ea5d35c88224dbad1b3112abd29c60413ea"},
{file = "pyobjc_framework_Quartz-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b41eec8d4b10c7c7e011e2f9051367f5499ef315ba52dfbae573c3a2e05469c"},
{file = "pyobjc_framework_Quartz-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c65456ed045dfe1711d0298734e5a3ad670f8c770f7eb3b19979256c388bdd2"},
{file = "pyobjc_framework_Quartz-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ddbca6b466584c3dc0e5b701b1c2a9b5d97ddc1d79a949927499ebb1be1f210"},
{file = "pyobjc_framework_Quartz-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7aba3cd966a0768dd58a35680742820f0c5ac596a9cd11014e2057818e65b0af"},
]
pyside2 = [
{file = "PySide2-5.15.2-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:4f17a0161995678110447711d685fcd7b15b762810e8f00f6dc239bffb70a32e"},
@@ -473,51 +663,51 @@ pyside2 = [
{file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:1316aa22dd330df096daf7b0defe9c00297a66e0b4907f057aaa3e88c53d1aff"},
]
regex = [
{file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"},
{file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"},
{file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"},
{file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"},
{file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"},
{file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"},
{file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"},
{file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"},
{file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"},
{file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"},
{file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"},
{file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"},
{file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"},
{file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"},
{file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"},
{file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"},
{file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"},
{file = "regex-2021.10.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:981c786293a3115bc14c103086ae54e5ee50ca57f4c02ce7cf1b60318d1e8072"},
{file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51feefd58ac38eb91a21921b047da8644155e5678e9066af7bcb30ee0dca7361"},
{file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8de658d7db5987b11097445f2b1f134400e2232cb40e614e5f7b6f5428710e"},
{file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1ce02f420a7ec3b2480fe6746d756530f69769292eca363218c2291d0b116a01"},
{file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39079ebf54156be6e6902f5c70c078f453350616cfe7bfd2dd15bdb3eac20ccc"},
{file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ff24897f6b2001c38a805d53b6ae72267025878d35ea225aa24675fbff2dba7f"},
{file = "regex-2021.10.8-cp310-cp310-win32.whl", hash = "sha256:c6569ba7b948c3d61d27f04e2b08ebee24fec9ff8e9ea154d8d1e975b175bfa7"},
{file = "regex-2021.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:45cb0f7ff782ef51bc79e227a87e4e8f24bc68192f8de4f18aae60b1d60bc152"},
{file = "regex-2021.10.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fab3ab8aedfb443abb36729410403f0fe7f60ad860c19a979d47fb3eb98ef820"},
{file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e55f8d66f1b41d44bc44c891bcf2c7fad252f8f323ee86fba99d71fd1ad5e3"},
{file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d52c5e089edbdb6083391faffbe70329b804652a53c2fdca3533e99ab0580d9"},
{file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1abbd95cbe9e2467cac65c77b6abd9223df717c7ae91a628502de67c73bf6838"},
{file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b5c215f3870aa9b011c00daeb7be7e1ae4ecd628e9beb6d7e6107e07d81287"},
{file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f540f153c4f5617bc4ba6433534f8916d96366a08797cbbe4132c37b70403e92"},
{file = "regex-2021.10.8-cp36-cp36m-win32.whl", hash = "sha256:1f51926db492440e66c89cd2be042f2396cf91e5b05383acd7372b8cb7da373f"},
{file = "regex-2021.10.8-cp36-cp36m-win_amd64.whl", hash = "sha256:5f55c4804797ef7381518e683249310f7f9646da271b71cb6b3552416c7894ee"},
{file = "regex-2021.10.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb2baff66b7d2267e07ef71e17d01283b55b3cc51a81b54cc385e721ae172ba4"},
{file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e527ab1c4c7cf2643d93406c04e1d289a9d12966529381ce8163c4d2abe4faf"},
{file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c98b013273e9da5790ff6002ab326e3f81072b4616fd95f06c8fa733d2745f"},
{file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:55ef044899706c10bc0aa052f2fc2e58551e2510694d6aae13f37c50f3f6ff61"},
{file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0ab3530a279a3b7f50f852f1bab41bc304f098350b03e30a3876b7dd89840e"},
{file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a37305eb3199d8f0d8125ec2fb143ba94ff6d6d92554c4b8d4a8435795a6eccd"},
{file = "regex-2021.10.8-cp37-cp37m-win32.whl", hash = "sha256:2efd47704bbb016136fe34dfb74c805b1ef5c7313aef3ce6dcb5ff844299f432"},
{file = "regex-2021.10.8-cp37-cp37m-win_amd64.whl", hash = "sha256:924079d5590979c0e961681507eb1773a142553564ccae18d36f1de7324e71ca"},
{file = "regex-2021.10.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b09d3904bf312d11308d9a2867427479d277365b1617e48ad09696fa7dfcdf59"},
{file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f125fce0a0ae4fd5c3388d369d7a7d78f185f904c90dd235f7ecf8fe13fa741"},
{file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f199419a81c1016e0560c39773c12f0bd924c37715bffc64b97140d2c314354"},
{file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:09e1031e2059abd91177c302da392a7b6859ceda038be9e015b522a182c89e4f"},
{file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c070d5895ac6aeb665bd3cd79f673775caf8d33a0b569e98ac434617ecea57d"},
{file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:176796cb7f82a7098b0c436d6daac82f57b9101bb17b8e8119c36eecf06a60a3"},
{file = "regex-2021.10.8-cp38-cp38-win32.whl", hash = "sha256:5e5796d2f36d3c48875514c5cd9e4325a1ca172fc6c78b469faa8ddd3d770593"},
{file = "regex-2021.10.8-cp38-cp38-win_amd64.whl", hash = "sha256:e4204708fa116dd03436a337e8e84261bc8051d058221ec63535c9403a1582a1"},
{file = "regex-2021.10.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b6ee6555b6fbae578f1468b3f685cdfe7940a65675611365a7ea1f8d724991"},
{file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973499dac63625a5ef9dfa4c791aa33a502ddb7615d992bdc89cf2cc2285daa3"},
{file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88dc3c1acd3f0ecfde5f95c32fcb9beda709dbdf5012acdcf66acbc4794468eb"},
{file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4786dae85c1f0624ac77cb3813ed99267c9adb72e59fdc7297e1cf4d6036d493"},
{file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe6ce4f3d3c48f9f402da1ceb571548133d3322003ce01b20d960a82251695d2"},
{file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9e3e2cea8f1993f476a6833ef157f5d9e8c75a59a8d8b0395a9a6887a097243b"},
{file = "regex-2021.10.8-cp39-cp39-win32.whl", hash = "sha256:82cfb97a36b1a53de32b642482c6c46b6ce80803854445e19bc49993655ebf3b"},
{file = "regex-2021.10.8-cp39-cp39-win_amd64.whl", hash = "sha256:b04e512eb628ea82ed86eb31c0f7fc6842b46bf2601b66b1356a7008327f7700"},
{file = "regex-2021.10.8.tar.gz", hash = "sha256:26895d7c9bbda5c52b3635ce5991caa90fbb1ddfac9c9ff1c7ce505e2282fb2a"},
]
requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
]
shiboken2 = [
{file = "shiboken2-5.15.2-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:03f41b0693b91c7f89627f1085a4ecbe8591c03f904118a034854d935e0e766c"},
@@ -528,8 +718,8 @@ shiboken2 = [
{file = "shiboken2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:14a33169cf1bd919e4c4c4408fffbcd424c919a3f702df412b8d72b694e4c1d5"},
]
six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
sysv-ipc = [
{file = "sysv_ipc-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ff6e3c98d8aa3e1ae5378d86869c4699c76f94318705d08085c97fb010770a31"},
@@ -543,45 +733,45 @@ toml = [
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typed-ast = [
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
{file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
{file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
{file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
{file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
{file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
{file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
{file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
{file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
{file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
{file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
{file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
{file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
{file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
{file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
{file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
{file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
{file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
{file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
{file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
{file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
{file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
{file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
{file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
{file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
{file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
{file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
{file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
{file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
{file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
]
typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
urllib3 = [
{file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"},
{file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"},
{file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
{file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
]
xcffib = [
{file = "xcffib-0.11.1.tar.gz", hash = "sha256:12949cfe2e68c806efd57596bb9bf3c151f399d4b53e15d1101b2e9baaa66f5a"},
+12 -13
View File
@@ -4,26 +4,25 @@ version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
license = "GPL"
include = ["runekit/_resources.py"]
[tool.poetry.dependencies]
python = "^3.9,<3.10"
requests = "^2.25.1"
requests = "^2.26.0"
PySide2 = "^5.15.2"
Pillow = "^8.1.0"
Pillow = "^8.3.1"
sysv-ipc = [{ version="^1.1.0", platform="linux" }]
xcffib = [{ version="^0.11.1", platform="linux" }]
pyobjc-framework-Quartz = [{ version="7.1", platform="darwin" }]
pyobjc-framework-ApplicationServices = [{ version="7.1", platform="darwin" }]
pyobjc-framework-CoreText = [{ version="7.1", platform="darwin" }]
click = "^7.1.2"
pyobjc-framework-Quartz = [{ version="7.3", platform="darwin" }]
pyobjc-framework-ApplicationServices = [{ version="7.3", platform="darwin" }]
pyobjc-framework-CoreText = [{ version="7.3", platform="darwin" }]
click = "^8.0.1"
psutil = "^5.8.0"
opencv-python-headless = "^4.5.3.56"
[tool.poetry.dev-dependencies]
Cython = "^0.29.21"
black = "^20.8b1"
pyinstaller = [{ version="^4.5.1", platform="darwin" }]
[tool.poetry.build]
script = "build.py"
[build-system]
requires = ["poetry-core>=1.1.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
runekit = 'runekit.main:main'
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource>
<file>runekit/browser/alt1.js</file>
<file>runekit/ui/trayicon.png</file>
<file>runekit/ui/skins/default/borderDrag.png</file>
<file>runekit/ui/skins/default/exit.png</file>
<file>runekit/ui/skins/default/exitHover.png</file>
<file>runekit/ui/skins/default/settings.png</file>
<file>runekit/ui/skins/default/settingsHover.png</file>
<file>runekit/ui/skins/default/minimize.png</file>
<file>runekit/ui/skins/default/minimizeHover.png</file>
<file>runekit/ui/skins/default/borderTL.png</file>
<file>runekit/ui/skins/default/borderT.png</file>
<file>runekit/ui/skins/default/borderTR.png</file>
<file>runekit/ui/skins/default/borderL.png</file>
<file>runekit/ui/skins/default/borderR.png</file>
<file>runekit/ui/skins/default/borderBL.png</file>
<file>runekit/ui/skins/default/borderB.png</file>
<file>runekit/ui/skins/default/borderBR.png</file>
</qresource>
</RCC>
+10
View File
@@ -25,3 +25,13 @@ class RequestHandler(typing.TypedDict):
handlerName: str
handlerUrl: str
handlerScript: str
class RectLike(typing.TypedDict):
x: int
y: int
width: int
height: int
CaptureMulti = typing.Dict[str, typing.Optional[RectLike]]
+1 -1
View File
@@ -5,7 +5,7 @@ import requests
from runekit.alt1.schema import AppManifest
def fetch_manifest(url: str) -> AppManifest:
def fetch_bom_json(url: str):
req = requests.get(url)
req.raise_for_status()
+1
View File
@@ -1 +1,2 @@
from .app import App
from .store import AppStore
+22 -1
View File
@@ -13,20 +13,38 @@ if TYPE_CHECKING:
class App:
host: "Host"
manifest: AppManifest
app_id: str
game_instance: "GameInstance"
source_url: Optional[str]
window: Optional[AppWindow] = None
alt1api: Optional[Alt1Api] = None
web_profile: Optional[WebProfile] = None
def __init__(self, host, manifest, game_instance, source_url=None):
def __init__(self, host, app_id, manifest, game_instance, source_url=None):
self.host = host
self.app_id = app_id
self.manifest = manifest
self.game_instance = game_instance
self.source_url = source_url
def close(self):
if self.window:
self.window.deleteLater()
self.window = None
if self.alt1api:
self.alt1api.deleteLater()
self.alt1api = None
if self.web_profile:
self.web_profile.deleteLater()
self.web_profile = None
def __del__(self):
self.close()
def get_window(self, **kwargs) -> AppWindow:
self.window = AppWindow(app=self, **kwargs)
self.window.winId() # force native window
# self.game_instance.embed_window(self.window.windowHandle()) # FIXME
return self.window
def get_api(self) -> Alt1Api:
@@ -49,6 +67,7 @@ class App:
@property
def absolute_app_url(self) -> str:
# TODO: Remove
if self.source_url:
return urljoin(self.source_url, self.manifest["appUrl"])
@@ -56,6 +75,7 @@ class App:
@property
def absolute_config_url(self) -> str:
# TODO: Remove
if self.source_url:
return urljoin(self.source_url, self.manifest["configUrl"])
@@ -63,6 +83,7 @@ class App:
@property
def absolute_icon_url(self) -> Optional[str]:
# TODO: Remove
if self.source_url and "iconUrl" in self.manifest:
return urljoin(self.source_url, self.manifest["iconUrl"])
+340
View File
@@ -0,0 +1,340 @@
import sys
import traceback
from pathlib import Path
import json
import logging
import hashlib
from typing import Iterator, Tuple, Optional, Union, List
from urllib.parse import urljoin
import requests
from PySide2.QtCore import (
QObject,
QSettings,
QThread,
Signal,
Slot,
QStandardPaths,
)
from PySide2.QtGui import QIcon, QPixmap
from PySide2.QtWidgets import QProgressDialog, QMessageBox
from runekit.alt1.schema import AppManifest
from runekit.alt1.utils import fetch_bom_json
REGISTRY_URL = "https://runeapps.org/data/alt1/defaultapps.json"
logger = logging.getLogger(__name__)
def app_id(manifest_url):
assert "://" in manifest_url
return hashlib.blake2b(manifest_url.encode("utf8"), digest_size=16).hexdigest()
class _FetchRegistryThread(QThread):
progress = Signal(int)
items = Signal(int)
label = Signal(str)
failed = Signal(tuple)
canceled = False
def __init__(
self,
parent: "AppStore",
apps_manifest: Optional[str] = None,
apps: Optional[List[str]] = None,
):
super().__init__(parent=parent)
assert (not apps_manifest and apps) or (
not apps and apps_manifest
), "One of apps_manifest or apps must be set but not both. apps_manifest={}, apps={}".format(
apps_manifest, apps
)
self.appstore = parent
self.apps_manifest = apps_manifest
self.apps = apps
def run(self):
try:
progress_extra = 0
if self.apps_manifest:
apps = fetch_bom_json(REGISTRY_URL)
self.items.emit(len(apps) + 1)
self.progress.emit(1)
progress_extra = 1
else:
apps = self.apps
self.items.emit(len(apps))
for index, app in enumerate(apps):
if type(app) == str:
self.label.emit(f"Installing app {app}")
self.add_app(app)
else:
self.label.emit(f"Installing {app['name']}")
url = urljoin(REGISTRY_URL, app["url"])
self.add_app(url, app["folder"])
self.progress.emit(index + progress_extra + 1)
if self.canceled:
return
finally:
self.items.emit(100)
self.progress.emit(100)
def add_app(self, url, folder=""):
try:
manifest = fetch_bom_json(url)
self.appstore.add_app(url, manifest)
self.appstore.add_app_to_folder(app_id(url), folder)
except:
logger.warning("Unable to add application %s", url, exc_info=True)
self.failed.emit(sys.exc_info())
@Slot()
def cancel(self):
self.canceled = True
class AppStore(QObject):
app_change = Signal()
def __init__(self):
super().__init__()
self.settings = QSettings(self)
qt_write_base = Path(
QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation)
)
qt_write_base.mkdir(parents=True, exist_ok=True)
self.icon_write_dir = qt_write_base / "app_icons"
self.icon_write_dir.mkdir(exist_ok=True)
def has_default_apps(self) -> bool:
return self.settings.value("apps/_meta/isDefaultLoaded")
def load_default_apps(self):
def on_progress(value):
if value != self.app_progress.maximum():
return
self.settings.setValue("apps/_meta/isDefaultLoaded", True)
logger.info("Default apps loaded")
self.add_app_thread.progress.disconnect(on_progress)
del self.app_progress
del self.add_app_thread
self.app_progress = QProgressDialog("Loading default apps", "Cancel", 0, 100)
self.add_app_thread = _FetchRegistryThread(self, apps_manifest=REGISTRY_URL)
self.add_app_thread.progress.connect(self.app_progress.setValue)
self.add_app_thread.progress.connect(on_progress)
self.add_app_thread.items.connect(self.app_progress.setMaximum)
self.add_app_thread.label.connect(self.app_progress.setLabelText)
self.app_progress.canceled.connect(self.add_app_thread.cancel)
self.add_app_thread.start()
self.app_progress.show()
def add_app_ui(self, manifests: List[str]):
def on_progress(value):
if value != self.app_progress.maximum():
return
self.add_app_thread.progress.disconnect(on_progress)
del self.app_progress
del self.add_app_thread
def on_failed(exc_info):
msg = QMessageBox(
QMessageBox.Critical,
"Fail to add application",
f"Failed to add application: \n\n{exc_info[0].__name__}: {exc_info[1]}",
)
msg.setDetailedText("".join(traceback.format_exception(*exc_info)))
msg.exec_()
self.app_progress = QProgressDialog("Installing app", "Cancel", 0, 100)
self.add_app_thread = _FetchRegistryThread(self, apps=manifests)
self.add_app_thread.progress.connect(self.app_progress.setValue)
self.add_app_thread.progress.connect(on_progress)
self.add_app_thread.items.connect(self.app_progress.setMaximum)
self.add_app_thread.label.connect(self.app_progress.setLabelText)
self.add_app_thread.failed.connect(on_failed)
self.app_progress.canceled.connect(self.add_app_thread.cancel)
self.add_app_thread.start()
self.app_progress.exec_()
def add_app(self, manifest_url: str, manifest: AppManifest):
appid = app_id(manifest_url)
try:
manifest["appUrl"] = urljoin(manifest_url, manifest["appUrl"])
manifest["iconUrl"] = (
urljoin(manifest_url, manifest["iconUrl"])
if "iconUrl" in manifest and manifest["iconUrl"]
else ""
)
manifest["configUrl"] = urljoin(manifest_url, manifest["configUrl"])
except KeyError:
raise AddAppError(manifest_url)
if manifest["iconUrl"]:
self.download_app_icon(appid, manifest["iconUrl"])
self.settings.setValue(f"apps/{appid}", json.dumps(manifest))
self.app_change.emit()
logger.info(
"Application %s (%s:%s) installed", manifest["appName"], appid, manifest_url
)
def mkdir(self, folder: str):
assert folder
assert "/" not in folder
settings = QSettings()
settings.beginGroup(f"apps/_folder/{folder}")
settings.setValue("_dir", "true")
self.app_change.emit()
def add_app_to_folder(self, appid: str, folder: str = "", _emit=True):
assert "/" not in folder
settings = QSettings()
settings.beginGroup(f"apps/_folder/{folder}")
last_id = 0
for key in settings.childKeys():
if "_" in key:
continue
last_id = max(int(key), last_id)
if appid == settings.value(key):
settings.endGroup()
return
keys = [int(x) for x in settings.childKeys() if "_" not in x]
last_id = max(keys) if keys else 0
settings.endGroup()
settings.setValue(f"apps/_folder/{folder}/{last_id + 1}", appid)
if _emit:
self.app_change.emit()
def delete_app_from_folder(self, appid: str, folder: str):
assert "/" not in folder
settings = QSettings()
settings.beginGroup(f"apps/_folder/{folder}")
for key in settings.childKeys():
if appid == settings.value(key):
settings.remove(key)
self.app_change.emit()
return
def remove_app(self, appid: str):
settings = QSettings()
settings.beginGroup("apps/_folder/")
def recurse():
for key in settings.childKeys():
if settings.value(key) == appid:
settings.remove(key)
for childGroup in settings.childGroups():
settings.beginGroup(childGroup)
recurse()
settings.endGroup()
recurse()
self.settings.remove(f"apps/{appid}")
icon_file = self.icon_write_dir / (appid + ".png")
icon_file.unlink(True)
self.app_change.emit()
def rmdir(self, folder: str):
assert folder
assert "/" not in folder
settings = QSettings()
settings.beginGroup(f"apps/_folder/{folder}")
for key in settings.childKeys():
if "_" in key:
continue
try:
int(key)
except ValueError:
continue
self.add_app_to_folder(settings.value(key), "", _emit=False)
settings.remove("")
self.app_change.emit()
def download_app_icon(self, appid: str, url: str):
dest = self.icon_write_dir / (appid + ".png")
req = requests.get(url)
with dest.open("wb") as fp:
fp.write(req.content)
logger.info("App icon %s wrote to %s", appid, str(dest))
def icon(self, appid: str) -> Optional[QIcon]:
fn = QStandardPaths.locate(
QStandardPaths.AppConfigLocation, "app_icons/" + appid + ".png"
)
if fn == "":
return None
return QIcon(QPixmap(fn))
def all_apps(self) -> Iterator[Tuple[str, AppManifest]]:
settings = QSettings()
settings.beginGroup("apps")
for appid in settings.childKeys():
if appid.startswith("_"):
continue
manifest = json.loads(settings.value(appid))
yield appid, manifest
settings.endGroup()
def list_app(self, root: str) -> Iterator[Tuple[str, Union[AppManifest, None]]]:
settings = QSettings()
settings.beginGroup(f"apps/_folder/" + root)
for key in settings.childGroups():
yield key, None
for key in settings.childKeys():
try:
int(key)
except ValueError:
continue
appid = settings.value(key)
yield appid, self[appid]
def __iter__(self):
yield from self.all_apps()
def __getitem__(self, item: str) -> AppManifest:
assert not item.startswith("_")
return json.loads(self.settings.value(f"apps/{item}"))
class AddAppError(ValueError):
def __init__(self, url):
super().__init__(f"Unable to parse manifest of application at {url}")
+110 -9
View File
@@ -1,37 +1,101 @@
import typing
import logging
import sys
from typing import TYPE_CHECKING
from PySide2.QtCore import Qt, Slot, QRect
from PySide2.QtGui import QIcon
from PySide2.QtCore import Qt, Slot, QRect, QObject, QUrl
from PySide2.QtGui import QIcon, QDesktopServices
from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PySide2.QtWidgets import QMainWindow
from runekit.browser import Alt1WebChannel
from runekit.ui.game_snap import GameSnapMixin
from runekit.ui.windowframe import WindowFrame
if TYPE_CHECKING:
from runekit.app.app import App
class BrowserWindow(QMainWindow):
class PageClass(QWebEnginePage):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
def javaScriptConsoleMessage(
self,
level: QWebEnginePage.JavaScriptConsoleMessageLevel,
message: str,
line_no: int,
src_id: str,
):
logger = self.logger.getChild(self.url().toString())
log = logger.debug
if level == QWebEnginePage.InfoMessageLevel:
log = logger.debug
elif level == QWebEnginePage.WarningMessageLevel:
log = logger.warning
elif level == QWebEnginePage.ErrorMessageLevel:
log = logger.error
log("%s", message)
def acceptNavigationRequest(
self, url: QUrl, type_: QWebEnginePage.NavigationType, is_main_frame: bool
) -> bool:
if not is_main_frame:
return super().acceptNavigationRequest(url, type_, is_main_frame)
# This breaks AFK Warden popups
# if (
# type_ != QWebEnginePage.NavigationTypeTyped
# and url.authority() != self.url().authority()
# ):
# # The web is only allowed pages on the same origin
# # Cross origin pages would open in browser
# if url.scheme() in ("http", "https"):
# QDesktopServices.openUrl(url)
# return False
return super().acceptNavigationRequest(url, type_, is_main_frame)
class BrowserWindow(GameSnapMixin, QMainWindow):
app: "App"
browser: QWebEngineView
page_class = PageClass
framed = False
def __init__(self, app: "App", **kwargs):
super().__init__(**kwargs)
self.app = app
if self.framed:
self.setAttribute(Qt.WA_TranslucentBackground)
self.frame = WindowFrame(parent=self)
self.frame.on_exit.connect(self.close)
self.setCentralWidget(self.frame)
self._setup_browser()
self.setWindowTitle(self.app.manifest["appName"])
self.setAttribute(Qt.WA_DeleteOnClose)
def _setup_browser(self):
self.browser = BrowserView(self)
self.setCentralWidget(self.browser)
page = QWebEnginePage(self.app.get_web_profile(), self.browser)
if hasattr(self, "frame"):
self.frame.set_content(self.browser)
else:
self.setCentralWidget(self.browser)
self.browser.setPage(self.get_page())
def get_page(self) -> QWebEnginePage:
page = self.page_class(self.app.get_web_profile(), self.browser)
page.setWebChannel(Alt1WebChannel(app=self.app, parent=self.browser))
page.geometryChangeRequested.connect(self.on_geometry_change)
page.iconChanged.connect(self.on_icon_changed)
page.windowCloseRequested.connect(self.on_window_close)
self.browser.setPage(page)
page.featurePermissionRequested.connect(self.on_permission_request)
return page
@Slot(QRect)
def on_geometry_change(self, size: QRect):
@@ -45,13 +109,50 @@ class BrowserWindow(QMainWindow):
def on_window_close(self):
self.close()
@Slot(QUrl, QWebEnginePage.Feature)
def on_permission_request(self, origin: QUrl, feature: QWebEnginePage.Feature):
if feature == QWebEnginePage.Notifications and self.app.has_permission(
"overlay"
):
# FIXME: This doesn't really work - PySide2 doesn't have QWebEngineNotification
self.browser.page().setFeaturePermission(
origin, feature, QWebEnginePage.PermissionGrantedByUser
)
elif (
feature in (QWebEnginePage.DesktopVideoCapture, QWebEnginePage.DesktopAudioVideoCapture)
and self.app.has_permission('pixel')
):
self.browser.page().setFeaturePermission(
origin, feature, QWebEnginePage.PermissionGrantedByUser
)
else:
self.browser.page().setFeaturePermission(
origin, feature, QWebEnginePage.PermissionDeniedByUser
)
class BrowserView(QWebEngineView):
def createWindow(self, type_: QWebEnginePage.WebWindowType) -> QWebEngineView:
from runekit.app.view.popup_window import PopupWindow
app_window = typing.cast(BrowserWindow, self.parent())
app_window = self.browser_window()
popup = PopupWindow(app=app_window.app, parent=app_window)
popup.setWindowIcon(app_window.windowIcon())
popup.show()
def show(ok: bool):
popup.browser.loadFinished.disconnect(show)
if ok:
popup.show()
else:
popup.close()
popup.browser.loadFinished.connect(show)
return popup.browser
def browser_window(self) -> BrowserWindow:
parent: QObject = self.parent()
while parent is not None:
if isinstance(parent, BrowserWindow):
return parent
parent = parent.parent()
+20 -42
View File
@@ -1,9 +1,9 @@
import logging
from typing import Optional, TYPE_CHECKING
import sys
from typing import Optional
import requests
from PySide2.QtCore import QSize, Qt, QRunnable, Slot, QObject, Signal, QThreadPool
from PySide2.QtGui import QIcon, QPixmap
from PySide2.QtCore import QSize, Qt, QThreadPool, QSettings
from PySide2.QtGui import QIcon
from runekit.app.view.browser_window import BrowserWindow
@@ -14,16 +14,19 @@ class AppWindow(BrowserWindow):
app_icon: Optional[QIcon]
def __init__(self, **kwargs):
self.settings = QSettings()
super().__init__(**kwargs)
self.settings.setParent(self)
# TODO: Hide from taskbar/group this as part of one big window?
super().__init__(
flags=Qt.WindowTitleHint
| Qt.WindowCloseButtonHint
| Qt.NoDropShadowWindowHint
| Qt.WindowStaysOnTopHint,
**kwargs
)
flags = Qt.NoDropShadowWindowHint | Qt.WindowStaysOnTopHint | Qt.Window
if self.framed:
flags |= Qt.CustomizeWindowHint
self.setWindowFlags(flags)
self.pool = QThreadPool(parent=self)
self.setWindowTitle(self.app.manifest["appName"])
self.snap_to_game()
self.logger = logging.getLogger(
__name__
@@ -34,20 +37,14 @@ class AppWindow(BrowserWindow):
)
self.browser.load(self.app.absolute_app_url)
self._update_app_icon()
def _update_app_icon(self):
if self.app.absolute_icon_url:
self.logger.debug("Fetching app icon")
icon_job = IconFetcher(self.app.absolute_icon_url)
self.app_icon = self.app.host.app_store.icon(self.app.app_id)
if self.app_icon:
self.setWindowIcon(self.app_icon)
def update_app_icon(icon: QPixmap):
self.logger.debug("App icon loaded")
self.app_icon = icon
self.setWindowIcon(QIcon(icon))
icon_job.signals.finished.connect(update_app_icon)
self.pool.start(icon_job)
@property
def framed(self) -> bool:
return self.settings.value("settings/styledBorder", "true") == "true" and sys.platform != "darwin"
def minimumSize(self) -> QSize:
return QSize(self.app.manifest["minWidth"], self.app.manifest["minHeight"])
@@ -59,22 +56,3 @@ class AppWindow(BrowserWindow):
def maximumSize(self) -> QSize:
return QSize(self.app.manifest["maxWidth"], self.app.manifest["maxHeight"])
class IconFetcherSignal(QObject):
finished = Signal(QPixmap)
class IconFetcher(QRunnable):
def __init__(self, icon_url: str):
super().__init__()
self.icon_url = icon_url
self.signals = IconFetcherSignal()
@Slot()
def run(self):
icon_req = requests.get(self.icon_url)
icon_req.raise_for_status()
pixmap = QPixmap()
pixmap.loadFromData(icon_req.content)
self.signals.finished.emit(pixmap)
+3
View File
@@ -1,7 +1,10 @@
from PySide2.QtWebEngine import QtWebEngine
from .api import Alt1WebChannel, Alt1Api
from .profile import WebProfile
from .scheme import register as register_scheme
def init():
QtWebEngine.initialize()
register_scheme()
+145 -36
View File
@@ -8,6 +8,20 @@
// (even though if discovered, still cannot bypass profiles restriction)
const RPC_TOKEN = '%%RPC_TOKEN%%'; // replaced in profile.py
if (window.parent !== window) {
// If running in subframe just use parent's alt1.
// Can't run separate instances as we have request ids
// that the native part don't know about
let wnd = window;
while(wnd.parent && wnd.parent !== wnd) {
// this is not going to work cross origin
wnd = wnd.parent;
}
window.alt1 = wnd.alt1;
return;
}
// FIXME: Even though RPC is secure, this is not...
let syncChan;
let api;
@@ -20,15 +34,25 @@
});
};
if (typeof window.qt !== 'undefined') {
setup()
setup();
} else {
document.addEventListener('DOMContentLoaded', setup);
}
});
function str2ab(str) {
let buf = new ArrayBuffer(str.length);
let bufView = new Uint8ClampedArray(buf);
for (let i=0, strLen=str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i) & 0xFF;
}
return bufView;
}
function syncRpc(msg, json) {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'rk:' + JSON.stringify(msg), false); // sync xhr
xhr.overrideMimeType('text/plain; charset=x-user-defined');
xhr.setRequestHeader('token', RPC_TOKEN);
xhr.send(null);
@@ -38,6 +62,25 @@
return xhr.responseText;
}
function asyncRpc(msg, responseType='') {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'rk:' + JSON.stringify(msg));
xhr.setRequestHeader('token', RPC_TOKEN);
xhr.responseType = responseType;
xhr.addEventListener('load', (e) => {
resolve(xhr.response);
});
xhr.addEventListener('error' ,(e) => {
e.xhr = xhr;
reject(e);
});
xhr.send();
return xhr.response;
})
}
function emit(param){
let listeners = alt1.events[param.eventName];
for(let i = 0; i < listeners.length; i++){
@@ -50,11 +93,12 @@
}
let lastGameActivity = window.performance.now();
let drawCallId = 0;
window.alt1 = {
overlay: {},
version: '1.1.1',
versionint: 1001001,
version: '1.2.0',
versionint: 1002000,
maxtransfer: 4000000, // also hardcoded in lib...
skinName: '',
captureMethod: '',
@@ -63,7 +107,6 @@
permissionOverlay: true,
permissionPixel: true,
lastWorldHop: 0,
currentWorld: -1,
rsPing: 0,
rsFps: 60,
openInfo: '{}',
@@ -147,6 +190,11 @@
return window.performance.now() - lastGameActivity;
},
get currentWorld() {
if(!syncChan) return -1;
return api.world || -1;
},
userResize(left, top, right, bottom) {
window.resizeTo(right-left, bottom-top);
},
@@ -160,11 +208,13 @@
});
},
// openBrowser(url) {
// },
openBrowser(url) {
window.open(url, '_blank');
},
// clearBinds() {
// },
clearBinds() {
window.alt1.clearTooltip();
},
// registerStatusDaemon(serverUrl, state) {
// },
@@ -172,11 +222,15 @@
// return '';
// },
// showNotification() {
// },
showNotification(title, message, icon) {
// FIXME: Just use HTML5 Notification once QWebEngineNotification is implemented
channel.then(function(chan){
chan.objects.alt1.showNotification(title, message, icon);
});
},
closeApp() {
window.close()
window.close();
},
setTooltip(tooltip) {
@@ -197,42 +251,71 @@
},
setTitleBarText(text) {
document.title = text;
},
overLayRect(color, x, y, w, h, time, lineWidth) {
console.trace('overLayRect');
return false;
channel.then(function(chan){
chan.objects.alt1.overlayRect(drawCallId++, color, x, y, w, h, time, lineWidth);
});
return true;
},
overLayText(str, color, size, x, y, time) {
return alt1.overLayTextEx(str, color, size, x, y, time, '', false, true);
},
overLayTextEx(str, color, size, x, y, time, fontname, centered, shadow) {
console.trace('overLayTextEx');
return false;
channel.then(function(chan){
chan.objects.alt1.overlayTextEx(drawCallId++, str, color, size, x, y, time, fontname, centered, shadow);
});
return true;
},
overLayLine(color, width, x1, y1, x2, y2, time) {
console.trace('overLayLine');
return false;
channel.then(function(chan){
chan.objects.alt1.overlayLine(drawCallId++, color, width, x1, y1, x2, y2, time);
});
return true;
},
overLayImage(x, y, imgstr, imgwidth, time) {
console.trace('overLayImage');
return false;
channel.then(function(chan){
chan.objects.alt1.overlayImage(drawCallId++, x, y, imgstr, imgwidth, time);
});
return true;
},
overLayClearGroup(group) {
console.trace('overLayClearGroup');
channel.then(function(chan){
chan.objects.alt1.overlayClearGroup(drawCallId++, group);
});
return true;
},
overLaySetGroup(group) {
console.trace('overLaySetGroup');
channel.then(function(chan){
chan.objects.alt1.overlaySetGroup(drawCallId++, group);
});
return true;
},
overLayFreezeGroup(group) {
console.trace('overLayFreezeGroup');
channel.then(function(chan){
chan.objects.alt1.overlayFreezeGroup(drawCallId++, group);
});
return true;
},
overLayContinueGroup(group) {
console.trace('overLayContinueGroup');
channel.then(function(chan){
chan.objects.alt1.overlayContinueGroup(drawCallId++, group);
});
return true;
},
overLayRefreshGroup(group) {
console.trace('overLayRefreshGroup');
channel.then(function(chan){
chan.objects.alt1.overlayRefreshGroup(drawCallId++, group);
});
return true;
},
overLaySetGroupZIndex(groupname, zIndex) {
console.trace('overLaySetGroupZIndex');
overLaySetGroupZIndex(group, zIndex) {
channel.then(function(chan){
chan.objects.alt1.overlaySetGroupZIndex(drawCallId++, group, zIndex);
});
return true;
},
getRegion(x, y, w, h) {
@@ -268,7 +351,31 @@
// },
// bindFindSubImg(id, imgstr, imgwidth, x, y, w, h) {
// return '';
// }
// },
capture(x, y, w, h) {
let data = syncRpc({func: 'getRegionRaw', x: x, y: y, w: w, h: h});
return str2ab(data);
},
captureAsync(x, y, w, h) {
return asyncRpc({func: 'getRegionRaw', x: x, y: y, w: w, h: h}, 'arraybuffer')
.then((data) => new Uint8ClampedArray(data));
},
async captureMultiAsync(areas) {
// XXX: This override the current bind
let boundId = await asyncRpc({func: 'bindRegion', x: 0, y: 0, w: alt1.rsWidth, h: alt1.rsHeight}, 'json');
let bounds = await Promise.all(Object.keys(areas).map((area) => {
let value = areas[area];
return asyncRpc({func: 'bindGetRegionRaw', id: boundId, x: value.x, y: value.y, w: value.width, h: value.height}, 'arraybuffer')
.then((data) => ({[area]: new Uint8ClampedArray(data)}));
}));
return bounds.reduce((a, b) => ({...a, ...b}), {});
},
bindGetRegionBuffer(id, x, y, w, h) {
let data = syncRpc({func: 'bindGetRegionRaw', id: id, x: x, y: y, w: w, h: h});
return str2ab(data);
},
addOCRFont(){
},
};
channel.then(function(chan){
@@ -287,24 +394,26 @@
lastGameActivity = window.performance.now();
});
instance.world_change_signal.connect(function(){
alt1.lastWorldHop = new Date().getTime();
});
instance.alt1Signal.connect(function (pos){
let mouseX = pos >> 16;
let mouseY = pos & 0xFFFF;
let mouseRsX = mouseX - instance.gamePositionX;
let mouseRsY = mouseY - instance.gamePositionY;
let event = {
eventName: 'alt1pressed',
text: '',
mouseAbs: {
x: mouseX + instance.gamePositionX,
y: mouseY + instance.gamePositionY,
},
mouseRs: {
x: mouseX,
y: mouseY,
},
mouseRs: {
x: mouseRsX,
y: mouseRsY,
},
x: mouseRsX,
y: mouseRsY,
x: mouseX,
y: mouseY,
rsLinked: true,
};
emit(event);
+163 -44
View File
@@ -1,12 +1,10 @@
import base64
import json
import logging
import time
import secrets
from typing import TYPE_CHECKING, Dict, Callable, List
from typing import TYPE_CHECKING, Dict, Callable, List, NamedTuple
from urllib.parse import urljoin
from PIL import Image
from PySide2.QtCore import (
QObject,
Slot,
@@ -24,44 +22,22 @@ from PySide2.QtGui import QGuiApplication, QCursor, QScreen
from PySide2.QtWebChannel import QWebChannel
from PySide2.QtWebEngineCore import QWebEngineUrlSchemeHandler, QWebEngineUrlRequestJob
from runekit.browser.overlay import OverlayApi
from runekit.browser.utils import (
ApiPermissionDeniedException,
image_to_stream,
encode_mouse,
decode_image,
)
from runekit.game.instance import ImageType
from runekit.ui.tray import tray_icon
if TYPE_CHECKING:
from runekit.app.app import App
TRANSFER_LIMIT = 4_000_000
class ApiPermissionDeniedException(Exception):
required_permission: str
def __init__(self, required_permission: str):
super().__init__(
"Permission '%s' is needed for this action".format(required_permission)
)
self.required_permission = required_permission
def _image_to_stream(image: Image, x=0, y=0, width=None, height=None) -> bytes:
assert image.mode == "RGB" or image.mode == "RGBA"
if width is None:
width = image.width
if height is None:
height = image.height
if width * height * 4 > TRANSFER_LIMIT:
return ""
if image.mode == "RGB":
image = image.convert("RGBA")
r, g, b, a = image.crop((x, y, x + width, y + height)).split()
image = Image.merge("RGBA", (b, g, r, a))
return base64.b64encode(image.tobytes())
def encode_mouse(x: int, y: int) -> int:
return (x << 16) | y
class BoundedRegion(NamedTuple):
image: ImageType
class Alt1Api(QObject):
@@ -71,20 +47,31 @@ class Alt1Api(QObject):
alt1Signal = Signal(int)
_screen_info: QRect
_bound_regions: List
_bound_regions: List[BoundedRegion]
_game_active = False
_game_position: QRect
_game_scaling: float
_overlay: OverlayApi
def __init__(self, app, **kwargs):
super().__init__(**kwargs)
self.app = app
self._bound_regions = []
self._overlay = OverlayApi(self, parent=self)
self.logger = logging.getLogger(
__name__
+ "."
+ self.__class__.__name__
+ "."
+ self.app.manifest["appName"]
)
self.rpc_funcs = {
"getRegion": self.get_region,
"getRegionRaw": self.get_region_raw,
"bindRegion": self.bind_region,
"bindGetRegion": self.bind_get_region,
"bindGetRegionRaw": self.bind_get_region_raw,
}
self._update_screen_info()
@@ -92,6 +79,7 @@ class Alt1Api(QObject):
self._game_scaling = self.app.game_instance.get_scaling()
self._private = Alt1ApiPrivate(self, parent=self)
self.app.game_instance.game_activity.connect(self.game_activity_signal)
self.app.game_instance.worldChanged.connect(self.world_change_signal)
poll_timer = QTimer(self)
poll_timer.setInterval(250)
@@ -113,6 +101,7 @@ class Alt1Api(QObject):
self.screen_update_signal.emit()
screen_update_signal = Signal()
# endregion
# region Qt Properties
@@ -148,9 +137,9 @@ class Alt1Api(QObject):
return 0
value = QCursor.pos()
pos = self.app.game_instance.get_position()
pos = self._game_position
if not pos.contains(value, True):
if not pos.contains(value, True) or not self._game_active:
# Cursor is out of game
return 0
@@ -203,6 +192,12 @@ class Alt1Api(QObject):
gameActive = Property(bool, get_game_active, notify=game_active_change_signal)
game_activity_signal = Signal()
def get_world(self):
return self.app.game_instance.get_world()
world_change_signal = Signal()
world = Property(bool, get_world, notify=world_change_signal)
# endregion
# region Sync RPC handlers
@@ -210,14 +205,25 @@ class Alt1Api(QObject):
if not self.app.has_permission("pixel"):
raise ApiPermissionDeniedException("pixel")
return _image_to_stream(self.app.game_instance.grab_region(x, y, w, h))
return base64.b64encode(
image_to_stream(self.app.game_instance.grab_region(x, y, w, h))
)
def get_region_raw(self, x, y, w, h):
if not self.app.has_permission("pixel"):
raise ApiPermissionDeniedException("pixel")
return image_to_stream(
self.app.game_instance.grab_region(x, y, w, h), mode="rgba"
)
def bind_region(self, x, y, w, h):
if not self.app.has_permission("pixel"):
return 0
# Alt1 only support one region
self._bound_regions = [self.app.game_instance.grab_region(x, y, w, h)]
bound_data = self.app.game_instance.grab_region(x, y, w, h)
self._bound_regions = [BoundedRegion(image=bound_data)]
return 1
def bind_get_region(self, id, x, y, w, h):
@@ -230,10 +236,25 @@ class Alt1Api(QObject):
try:
image = self._bound_regions[id - 1]
except IndexError:
print("no img index %d" % id)
self.logger.warning("bindGetRegion(%d) but image not bound", id)
return ""
return _image_to_stream(image, x, y, w, h)
return base64.b64encode(image_to_stream(image.image, x, y, w, h))
def bind_get_region_raw(self, id, x, y, w, h):
if not self.app.has_permission("pixel"):
raise ApiPermissionDeniedException("pixel")
if id == 0:
return ""
try:
image = self._bound_regions[id - 1]
except IndexError:
self.logger.warning("bindGetRegionRaw(%d) but image not bound", id)
return ""
return image_to_stream(image.image, x, y, w, h, mode="rgba", ignore_limit=True)
# endregion
@@ -245,6 +266,13 @@ class Alt1Api(QObject):
self.app.host.notifier.notify(str(text))
@Slot(str, str, str)
def showNotification(self, title, message, icon):
if not self.app.has_permission("overlay"):
raise ApiPermissionDeniedException("overlay")
tray_icon().showMessage(title, message)
@Slot(str)
def identifyAppUrl(self, url):
"""Update app manifest from url"""
@@ -268,6 +296,97 @@ class Alt1Api(QObject):
type_map[type_.toDouble(0)], progress.toDouble(0)
)
@Slot(int, int, int, int, int, int, int, int)
def overlayRect(self, call_id, color, x, y, w, h, timeout, line_width):
if not self.app.has_permission("overlay"):
raise ApiPermissionDeniedException("overlay")
self._overlay.enqueue(
call_id, "overlay_rect", color, x, y, w, h, timeout, line_width
)
@Slot(int, int, int, int, int, int, int, int)
def overlayLine(self, call_id, color, width, x1, y1, x2, y2, timeout):
if not self.app.has_permission("overlay"):
raise ApiPermissionDeniedException("overlay")
self._overlay.enqueue(
call_id, "overlay_line", color, width, x1, y1, x2, y2, timeout
)
@Slot(int, int, int, str, int, int)
def overlayImage(self, call_id, x, y, img_str, img_width, timeout):
if not self.app.has_permission("overlay"):
raise ApiPermissionDeniedException("overlay")
img_bytes = base64.b64decode(img_str)
self._overlay.enqueue(
call_id, "overlay_image", img_bytes, x, y, img_width, timeout
)
@Slot(int, str, int, int, int, int, int, str, bool, bool)
def overlayTextEx(
self, call_id, message, color, size, x, y, timeout, font_name, centered, shadow
):
if not self.app.has_permission("overlay"):
raise ApiPermissionDeniedException("overlay")
self._overlay.enqueue(
call_id,
"overlay_text",
message,
color,
size,
x,
y,
timeout,
font_name,
centered,
shadow,
)
@Slot(int, str)
def overlaySetGroup(self, call_id, name):
if not self.app.has_permission("overlay"):
raise ApiPermissionDeniedException("overlay")
self._overlay.enqueue(call_id, "overlay_set_group", name)
@Slot(int, str)
def overlayClearGroup(self, call_id, name):
if not self.app.has_permission("overlay"):
raise ApiPermissionDeniedException("overlay")
self._overlay.enqueue(call_id, "overlay_clear_group", name)
@Slot(int, str)
def overlayFreezeGroup(self, call_id, name):
if not self.app.has_permission("overlay"):
raise ApiPermissionDeniedException("overlay")
self._overlay.enqueue(call_id, "overlay_freeze_group", name)
@Slot(int, str)
def overlayContinueGroup(self, call_id, name):
if not self.app.has_permission("overlay"):
raise ApiPermissionDeniedException("overlay")
self._overlay.enqueue(call_id, "overlay_continue_group", name)
@Slot(int, str)
def overlayRefreshGroup(self, call_id, name):
if not self.app.has_permission("overlay"):
raise ApiPermissionDeniedException("overlay")
self._overlay.enqueue(call_id, "overlay_refresh_group", name)
@Slot(int, str, int)
def overlaySetGroupZIndex(self, call_id, group, zIndex):
if not self.app.has_permission("overlay"):
raise ApiPermissionDeniedException("overlay")
self._overlay.enqueue(call_id, "overlay_set_group_z", group, zIndex)
# endregion
+270
View File
@@ -0,0 +1,270 @@
import sys
import collections
import functools
import hashlib
import logging
from typing import List, Dict, Tuple, Optional, TYPE_CHECKING
from PySide2.QtCore import QObject, QTimer, Qt
from PySide2.QtGui import QFont, QPen, QImage, QPixmap
from PySide2.QtWidgets import (
QGraphicsItem,
QGraphicsDropShadowEffect,
QGraphicsTextItem,
QGraphicsRectItem,
QGraphicsItemGroup,
QGraphicsLineItem,
QGraphicsPixmapItem,
)
from .utils import decode_color
if TYPE_CHECKING:
from .api import Alt1Api
def barrier(f):
"""Mark f as sync barrier - all preceeding calls must be finished.
Only affect calls from scheduler"""
f.barrier = True
return f
def ensure_overlay(f):
def out(self: "OverlayApi", *args, **kwargs):
if not self.overlay_area:
return
return f(self, *args, **kwargs)
return functools.update_wrapper(out, f)
class OverlayApi(QObject):
# TODO: I think this could be implemented by QGraphicsItemGroup, maybe even more performant
current_group = ""
groups: Dict[str, List[QGraphicsItem]]
frozen_group: Dict[str, QGraphicsItemGroup]
queue: List[Tuple[int, str, List]]
last_call_id: Optional[int] = None
overlay_area: Optional[QGraphicsItem] = None
def __init__(self, base_api: "Alt1Api", **kwargs):
super().__init__(**kwargs)
self.logger = logging.getLogger(__name__)
self.api = base_api
self.reset()
try:
self.overlay_area = self.api.app.game_instance.get_overlay_area()
except NotImplementedError:
pass
@ensure_overlay
def enqueue(self, call_id, command, *args):
# XXX: command is NOT validated. Must be from secure source
assert command[0] != "_"
if call_id == 0:
self.logger.info("Call ID reset")
self.reset()
self.queue.append((call_id, command, args))
self.queue.sort(key=lambda x: x[0])
# self.logger.debug("%d %s %s", call_id, command, repr(args))
QTimer.singleShot(0, self.process_queue)
def process_queue(self):
while len(self.queue) > 0:
head = self.queue[0]
f = getattr(self, head[1])
if (
hasattr(f, "barrier")
and self.last_call_id is not None
and head[0] != self.last_call_id + 1
):
return
head = self.queue.pop(0)
self.last_call_id = head[0]
try:
f(*head[2])
except:
self.logger.error(
"API call exception #%d %s(%s)",
head[0],
head[1],
repr(head[2]),
exc_info=True,
)
def _finalize_gfx(
self, gfx: QGraphicsItem, timeout: int, group: Optional[str] = None
):
timeout = min(20000, max(timeout, 1))
if group is None:
group = self.current_group
if group in self.frozen_group:
self.frozen_group[group].addToGroup(gfx)
else:
self.groups[group].append(gfx)
gfx.setParentItem(self.overlay_area)
def hide():
try:
gfx.scene().removeItem(gfx)
except AttributeError:
pass
try:
self.groups[group].remove(gfx)
except (KeyError, ValueError):
pass
QTimer.singleShot(timeout, hide)
def reset(self):
if hasattr(self, "groups"):
for item in self.groups.values():
item.scene().removeItem(item)
self.groups = collections.defaultdict(list)
self.frozen_group = {}
self.queue = []
self.last_call_id = None
@barrier
@ensure_overlay
def overlay_set_group(self, name: str):
self.current_group = name
@barrier
@ensure_overlay
def overlay_clear_group(self, name: str):
for item in self.groups.get(name, []):
item.scene().removeItem(item)
try:
del self.groups[name]
except KeyError:
pass
@barrier
@ensure_overlay
def overlay_freeze_group(self, name: str):
if name in self.frozen_group:
return
self.frozen_group[name] = QGraphicsItemGroup()
@barrier
@ensure_overlay
def overlay_continue_group(self, name: str):
if name not in self.frozen_group:
return
gfx = self.frozen_group[name]
del self.frozen_group[name]
self._finalize_gfx(gfx, 20000, group=name)
@barrier
@ensure_overlay
def overlay_refresh_group(self, name: str):
if name not in self.frozen_group:
return
self.overlay_continue_group(name)
self.overlay_freeze_group(name)
@ensure_overlay
def overlay_rect(
self, color: int, x: int, y: int, w: int, h: int, timeout: int, line_width: int
):
pen = QPen(decode_color(color))
pen.setWidthF(max(1.0, line_width / 10))
gfx = QGraphicsRectItem(x, y, w, h)
gfx.setPen(pen)
self._finalize_gfx(gfx, timeout)
@ensure_overlay
def overlay_line(
self,
color: int,
line_width: int,
x1: int,
y1: int,
x2: int,
y2: int,
timeout: int,
):
pen = QPen(decode_color(color))
pen.setWidthF(max(1.0, line_width / 10))
gfx = QGraphicsLineItem(x1, y1, x2, y2)
gfx.setPen(pen)
self._finalize_gfx(gfx, timeout)
@ensure_overlay
def overlay_text(
self,
message: str,
color: int,
size: int,
x: int,
y: int,
timeout: int,
font_name: str,
centered: bool,
shadow: bool,
):
gfx = QGraphicsTextItem(message)
gfx.setDefaultTextColor(decode_color(color))
if font_name == "" and sys.platform == "darwin":
# Don't use Helvetica on Mac
font_name = "Verdana"
font = QFont(font_name, min(50, size))
font.setStyleHint(QFont.SansSerif)
gfx.setFont(font)
if shadow:
effect = QGraphicsDropShadowEffect(gfx)
effect.setBlurRadius(0)
effect.setColor(Qt.GlobalColor.black)
effect.setOffset(1, 1)
gfx.setGraphicsEffect(effect)
if centered:
# The provided x, y is at the center of the text
bound = gfx.boundingRect()
gfx.setPos(x - (bound.width() / 2), y - (bound.height() / 2))
else:
gfx.setPos(x, y)
self._finalize_gfx(gfx, timeout)
@ensure_overlay
def overlay_image(self, img: bytes, x: int, y: int, width: int, timeout: int):
img = self.get_qimage(img, width)
gfx = QGraphicsPixmapItem(QPixmap.fromImage(img))
gfx.setPos(x, y)
self._finalize_gfx(gfx, timeout)
@ensure_overlay
def overlay_set_group_z(self, name: str, z_index: int):
for item in self.groups.get(name, []):
item.setZValue(z_index)
@functools.lru_cache(100)
def get_qimage(self, img: bytes, width: int):
height = len(img) / width / 4
return QImage(img, width, height, QImage.Format_ARGB32)
+24 -21
View File
@@ -1,9 +1,12 @@
import secrets
from pathlib import Path
from typing import TYPE_CHECKING
from PySide2.QtCore import QFile, QIODevice
from PySide2.QtWebEngineWidgets import QWebEngineProfile, QWebEngineScript
from PySide2.QtWebEngineWidgets import (
QWebEngineProfile,
QWebEngineScript,
QWebEngineSettings,
)
from runekit.browser.api import RuneKitSchemeHandler
from runekit.browser.scheme import RuneKitScheme
@@ -11,25 +14,23 @@ from runekit.browser.scheme import RuneKitScheme
if TYPE_CHECKING:
from runekit.app import App
script_file = Path(__file__).parent / "alt1.js"
with script_file.open() as fp:
src = fp.read()
_added_qwebchannel = False
class WebProfile(QWebEngineProfile):
rpc_secret: str
app: "App"
def __init__(self, app=None, parent=None):
# TODO: Persist
super().__init__(parent)
super().__init__("app", parent=parent)
self.app = app
self.rpc_secret = secrets.token_urlsafe(64)
self._insert_scheme_handlers()
self._insert_alt1_api()
self.settings().setAttribute(
QWebEngineSettings.PlaybackRequiresUserGesture, False
)
self.settings().setAttribute(QWebEngineSettings.PdfViewerEnabled, False)
self.settings().setAttribute(QWebEngineSettings.ScreenCaptureEnabled, False)
self.settings().setAttribute(QWebEngineSettings.AutoLoadIconsForPage, False)
def _insert_scheme_handlers(self):
self.installUrlSchemeHandler(
@@ -42,23 +43,25 @@ class WebProfile(QWebEngineProfile):
)
def _insert_alt1_api(self):
global _added_qwebchannel, src
qwc_file = QFile(":/qtwebchannel/qwebchannel.js", parent=self)
if not qwc_file.open(QIODevice.ReadOnly):
raise IOError
src = bytes(qwc_file.readAll()).decode("utf8")
qwc_file.close()
if not _added_qwebchannel:
qwc_file = QFile(":/qtwebchannel/qwebchannel.js", parent=self)
if not qwc_file.open(QIODevice.ReadOnly):
raise IOError
qwc_src = bytes(qwc_file.readAll()).decode("utf8")
qwc_file.close()
src += "\n;;\n"
src = qwc_src + "\n;;\n" + src
_added_qwebchannel = True
alt1_file = QFile(":/runekit/browser/alt1.js", parent=self)
if not alt1_file.open(QIODevice.ReadOnly):
raise IOError
src += bytes(alt1_file.readAll()).decode("utf8")
alt1_file.close()
script = QWebEngineScript()
script.setName("alt1.js")
script.setWorldId(QWebEngineScript.MainWorld)
script.setInjectionPoint(QWebEngineScript.DocumentCreation)
script.setSourceCode(src.replace("%%RPC_TOKEN%%", self.rpc_secret))
script.setRunsOnSubFrames(True)
self.scripts().insert(script)
+107
View File
@@ -0,0 +1,107 @@
import base64
from typing import TypeVar
import numpy as np
from PIL import Image
from PySide2.QtGui import QColor
from runekit.game.instance import ImageType
from runekit.image.np_utils import np_crop
TRANSFER_LIMIT = 4_000_000
class ApiPermissionDeniedException(Exception):
required_permission: str
def __init__(self, required_permission: str):
super().__init__(
"Permission '%s' is needed for this action".format(required_permission)
)
self.required_permission = required_permission
ImgTypeG = TypeVar("T", np.ndarray, Image.Image)
def ensure_image_rgba(image: ImgTypeG) -> ImgTypeG:
# XXX: This function is not idempotent!
if isinstance(image, np.ndarray):
return image[:, :, [2, 1, 0, 3]]
else:
if image.mode == "RGB":
image = image.convert("RGBA")
return image
def ensure_image_bgra(image: ImgTypeG) -> ImgTypeG:
# XXX: This function is not idempotent!
if isinstance(image, np.ndarray):
return image
else:
if image.mode == "RGB":
image = image.convert("RGBA")
r, g, b, a = image.split()
image = Image.merge("RGBA", (b, g, r, a))
return image
def ensure_image(image: ImgTypeG, mode: str) -> ImgTypeG:
if mode == "rgba":
return ensure_image_rgba(image)
elif mode == "bgra":
return ensure_image_bgra(image)
else:
raise ValueError("invalid mode")
def image_to_stream(
image: ImageType,
x=0,
y=0,
width=None,
height=None,
mode="bgra",
ignore_limit=False,
) -> bytes:
if isinstance(image, np.ndarray):
out = np_crop(image, x, y, width, height)
out = ensure_image(out, mode).tobytes()
else:
assert image.mode == "RGBA"
if width is None:
width = image.width
if height is None:
height = image.height
if not ignore_limit and width * height * 4 > TRANSFER_LIMIT:
return ""
image = image.crop((x, y, x + width, y + height))
out = ensure_image(image, mode).tobytes()
return out
def encode_mouse(x: int, y: int) -> int:
return (x << 16) | y
def decode_color(color: int) -> QColor:
r = (color >> 16) & 0xFF
g = (color >> 8) & 0xFF
b = (color >> 0) & 0xFF
a = (color >> 24) & 0xFF
return QColor.fromRgb(r, g, b, a)
def decode_image(img: str, width: int) -> np.ndarray:
img = base64.b64decode(img)
img = np.frombuffer(img, "<B")
img.shape = (-1, width, 4)
return img
+30 -9
View File
@@ -1,15 +1,20 @@
import abc
import time
from typing import TYPE_CHECKING, Literal
from PySide2.QtCore import QObject, Signal, Property, QRect, Slot
from typing import TYPE_CHECKING, Literal, Optional, Union
import numpy as np
from PIL import Image
from PySide2.QtCore import QObject, Signal, Property, QRect, Slot
from PySide2.QtGui import QWindow
from PySide2.QtWidgets import QGraphicsItem
from runekit.image.np_utils import np_crop
if TYPE_CHECKING:
from .manager import GameManager
PROGRESS_TYPE = Literal["RESET", "IN_PROGRESS", "ERROR", "LOADING", "PAUSED"]
ImageType = Union[np.ndarray, Image.Image]
class GameInstance(QObject):
@@ -56,17 +61,33 @@ class GameInstance(QObject):
focused = Property(bool, is_focused, notify=focusChanged)
@abc.abstractmethod
def grab_game(self) -> Image:
def get_world(self) -> Optional[int]:
...
worldChanged = Signal(int)
world = Property(int, get_world, notify=worldChanged)
@abc.abstractmethod
def grab_game(self) -> ImageType:
...
@abc.abstractmethod
def grab_desktop(self, x: int, y: int, w: int, h: int) -> Image:
def grab_desktop(self, x: int, y: int, w: int, h: int) -> ImageType:
...
def grab_region(self, x: int, y: int, w: int, h: int) -> Image:
def grab_region(self, x: int, y: int, w: int, h: int) -> ImageType:
image = self.grab_game()
return image.crop((x, y, x + w, y + h))
@abc.abstractmethod
if isinstance(image, np.ndarray):
return np_crop(image, x, y, w, h)
else:
return image.crop((x, y, x + w, y + h))
def set_taskbar_progress(self, type_: PROGRESS_TYPE, progress: float):
...
pass
def embed_window(self, window: QWindow):
pass
def get_overlay_area(self) -> QGraphicsItem:
raise NotImplementedError
+11 -3
View File
@@ -1,5 +1,5 @@
import abc
from typing import List
from typing import List, Union
from PySide2.QtCore import QObject, Property, Signal
@@ -12,7 +12,15 @@ class GameManager(QObject):
"""Return a list of active game instance. The instances returned should be stable (same instance for all invocation)"""
...
instance_added = Signal()
instance_removed = Signal()
@abc.abstractmethod
def get_active_instance(self) -> Union[GameInstance, None]:
...
instance_added = Signal(GameInstance)
instance_removed = Signal(GameInstance)
instance_changed = Signal()
instances = Property(list, get_instances, notify=instance_changed)
def stop(self):
"""Stop the GameManager and any GameInstances"""
pass
+108
View File
@@ -0,0 +1,108 @@
import logging
from typing import TYPE_CHECKING, Callable, Tuple, Dict
import numpy as np
from PySide2.QtCore import Qt, QRect, QTimer
from PySide2.QtGui import QGuiApplication, QPen
from PySide2.QtWidgets import (
QMainWindow,
QGraphicsView,
QGraphicsScene,
QGraphicsItem,
QGraphicsRectItem,
)
from .qt import qpixmap_to_np
from ..image import is_color_percent_gte
if TYPE_CHECKING:
from .instance import GameInstance
class DesktopWideOverlay(QMainWindow):
_instances: Dict[int, QGraphicsItem]
view: QGraphicsView
scene: QGraphicsScene
def __init__(self):
super().__init__(
flags=Qt.Widget
| Qt.FramelessWindowHint
| Qt.BypassWindowManagerHint
| Qt.WindowTransparentForInput
| Qt.WindowStaysOnTopHint
)
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
self.setAttribute(Qt.WA_NoSystemBackground, True)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setAttribute(Qt.WA_DeleteOnClose, True)
self.setStyleSheet("background: transparent")
self._instances = {}
virtual_screen = QRect(0, 0, 0, 0)
for screen in QGuiApplication.screens():
# TODO: Handle screen change
geom = screen.virtualGeometry()
virtual_screen = virtual_screen.united(geom)
self.scene = QGraphicsScene(
0, 0, virtual_screen.width(), virtual_screen.height(), parent=self
)
self.view = QGraphicsView(self.scene, self)
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.view.setStyleSheet("background: transparent")
self.view.setGeometry(0, 0, virtual_screen.width(), virtual_screen.height())
self.view.setInteractive(False)
self.transparent_pen = QPen()
self.transparent_pen.setBrush(Qt.NoBrush)
self.setGeometry(virtual_screen)
def add_instance(
self, instance: "GameInstance"
) -> Tuple[QGraphicsItem, Callable[[], None]]:
"""Add instance to manage, return a disconnect function and the canvas"""
positionChanged = lambda rect: self.on_instance_moved(instance, rect)
instance.positionChanged.connect(positionChanged)
focusChanged = lambda focus: self.on_instance_focus_change(instance, focus)
instance.focusChanged.connect(focusChanged)
instance_pos = instance.get_position()
gfx = QGraphicsRectItem(rect=instance_pos)
gfx.setPen(self.transparent_pen)
gfx.setPos(instance_pos.x(), instance_pos.y())
self.scene.addItem(gfx)
self._instances[instance.wid] = gfx
def disconnect():
gfx.hide()
self.scene.removeItem(gfx)
instance.positionChanged.disconnect(positionChanged)
instance.focusChanged.disconnect(focusChanged)
return gfx, disconnect
def on_instance_focus_change(self, instance, focus):
# self._instances[instance.wid].setVisible(focus)
pass
def on_instance_moved(self, instance, pos: QRect):
rect = self._instances[instance.wid]
rect.setRect(0, 0, pos.width(), pos.height())
rect.setPos(pos.x(), pos.y())
def check_compatibility(self):
QTimer.singleShot(300, self._check_compatibility)
def _check_compatibility(self):
# If we cause black screen then hide ourself out of shame...
screenshot = QGuiApplication.primaryScreen().grabWindow(0)
image = qpixmap_to_np(screenshot)
if is_color_percent_gte(image, color=[0, 0, 0], percent=0.95):
self.logger.warning("Detected black screen condition. Disabling overlay")
self.hide()
+94
View File
@@ -0,0 +1,94 @@
import re
import socket
from typing import Optional
import logging
import psutil
from PySide2.QtCore import QTimer, Slot
logger = logging.getLogger(__name__)
mode = None
MODE_PROCESS = 1
MODE_GLOBAL = 2
class PsUtilBaseMixin:
pid: int
class PsUtilNetStat(PsUtilBaseMixin):
__cached_rdns: dict[str, str]
__last_world = None
world_regex = re.compile(r"^world([0-9]+)\.runescape\.com$")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__cached_rdns = {}
self.__world_timer = QTimer(self)
self.__world_timer.start(60_000)
self.__world_timer.timeout.connect(self.__update_world)
def get_world(self):
if self.__last_world is None:
self.__last_world = self.fetch_world()
return self.__last_world
def fetch_world(self) -> Optional[int]:
try:
addrs = self.__get_connections()
except psutil.AccessDenied:
logger.warning("Cannot get connections", exc_info=True)
return None
for conn in addrs:
if conn.status != psutil.CONN_ESTABLISHED:
continue
dns_name = self._rdns(conn.raddr.ip)
matched_name = self.world_regex.match(dns_name)
if not matched_name:
continue
world = int(matched_name.group(1))
return world
def __get_connections(self):
global mode
if mode is not None:
return self.__get_connections_mode(mode)
last_exc = None
for method in [MODE_PROCESS, MODE_GLOBAL]:
try:
out = self.__get_connections_mode(method)
mode = method
return out
except psutil.AccessDenied as e:
last_exc = e
raise last_exc
def __get_connections_mode(self, mode_: int):
if mode_ is MODE_PROCESS:
return psutil.Process(self.pid).connections()
elif mode_ is MODE_GLOBAL:
return [item for item in psutil.net_connections() if item.pid == self.pid]
else:
raise ValueError(f"Unknown mode {mode_}")
@Slot()
def __update_world(self):
new_world = self.fetch_world()
if new_world != self.__last_world:
logger.info("World hopped from %d to %d", self.__last_world, new_world)
self.__last_world = new_world
self.worldChanged.emit(new_world)
def _rdns(self, ip: str) -> Optional[str]:
if ip in self.__cached_rdns:
return self.__cached_rdns[ip]
self.__cached_rdns[ip] = socket.getnameinfo((ip, 0), 0)[0]
return self.__cached_rdns[ip]
+37 -8
View File
@@ -1,13 +1,37 @@
import logging
import time
from PIL import Image
from PySide2.QtGui import QGuiApplication, QWindow
import numpy as np
from PySide2.QtCore import QBuffer, QIODevice
from PySide2.QtGui import QGuiApplication, QWindow, QPixmap
import cv2
from runekit.image.np_utils import np_save_image
_debug_dump_file = False
logger = logging.getLogger(__name__)
def qpixmap_to_np(im: QPixmap) -> np.ndarray:
# from PIL.ImageQt.fromqimage
buffer = QBuffer()
buffer.open(QIODevice.ReadWrite)
if im.hasAlphaChannel():
im.save(buffer, "png")
else:
im.save(buffer, "ppm")
npbuf = np.frombuffer(buffer.data(), "<B")
buffer.close()
out = cv2.imdecode(npbuf, cv2.IMREAD_UNCHANGED)
if out.shape[2] == 3:
# Add alpha channel
out = np.pad(out, ((0, 0), (0, 0), (0, 1)), constant_values=0xFF)
return out
class QtBaseMixin:
qwindow: QWindow
@@ -18,29 +42,34 @@ class QtGrabMixin(QtBaseMixin):
__game_last_grab = 0.0
__game_last_image = None
def grab_game(self) -> Image:
def grab_game(self) -> np.ndarray:
if (time.monotonic() - self.__game_last_grab) * 1000 < self.refresh_rate:
return self.__game_last_image
screen = QGuiApplication.primaryScreen()
pixmap = screen.grabWindow(self.qwindow.winId())
image = Image.fromqpixmap(pixmap)
image = qpixmap_to_np(pixmap)
if _debug_dump_file:
logger.debug("dumping file")
pixmap.save("/tmp/qtshot.bmp", None, 100)
np_save_image(image, "/tmp/qtshot.bmp")
self.__game_last_image = image
self.__game_last_grab = time.monotonic()
return self.__game_last_image
def grab_desktop(self, x, y, w, h) -> Image:
def grab_desktop(self, x, y, w, h) -> np.ndarray:
screen = QGuiApplication.primaryScreen()
pixmap = screen.grabWindow(0, x, y, w, h)
image = Image.fromqpixmap(pixmap)
image = qpixmap_to_np(pixmap)
if _debug_dump_file:
logger.debug("dumping file")
pixmap.save("/tmp/qtshot.bmp", None, 100)
np_save_image(image, "/tmp/qtshot.bmp")
return image
class QtEmbedMixin(QtBaseMixin):
def embed_window(self, window: QWindow):
window.setParent(self.qwindow)
+23 -4
View File
@@ -1,6 +1,7 @@
import io
import logging
import time
from typing import TYPE_CHECKING
import Quartz
import objc
@@ -8,8 +9,13 @@ from PIL import Image
from PySide2.QtCore import QRect
from PySide2.QtGui import QGuiApplication
import ApplicationServices
from PySide2.QtWidgets import QGraphicsItem
from ..instance import GameInstance
from ..psutil_mixins import PsUtilNetStat
if TYPE_CHECKING:
from .manager import QuartzGameManager
_debug_dump_file = False
logger = logging.getLogger(__name__)
@@ -71,13 +77,14 @@ def on_ax_event(observer, element, notification, ptr):
self.logger.warning("Got unknown AX event %s", notification)
class QuartzGameInstance(GameInstance):
class QuartzGameInstance(PsUtilNetStat, GameInstance):
_is_active = False
overlay: QGraphicsItem
__game_last_grab = 0.0
__game_last_image = None
def __init__(self, manager, wid, pid, **kwargs):
def __init__(self, manager: "QuartzGameManager", wid, pid, **kwargs):
super().__init__(**kwargs)
self.manager = manager
self.wid = wid
@@ -87,6 +94,7 @@ class QuartzGameInstance(GameInstance):
self._setup_observer()
self.update_is_active()
self.overlay, self._overlay_disconnect = self.manager.overlay.add_instance(self)
def _setup_observer(self):
self._ax_element = ApplicationServices.AXUIElementCreateApplication(self.pid)
@@ -123,6 +131,7 @@ class QuartzGameInstance(GameInstance):
)
objc.context.unregister(self)
self._overlay_disconnect()
def get_position(self) -> QRect:
# The docs say this API is expensive...
@@ -155,6 +164,10 @@ class QuartzGameInstance(GameInstance):
[self.wid],
Quartz.kCGWindowImageBoundsIgnoreFraming,
)
if not imgref:
self.manager.request_accessibility_popup.emit()
raise NoCapturePermission
out = cgimageref_to_image(imgref)
scale = self.get_scaling()
if scale > 1:
@@ -176,8 +189,8 @@ class QuartzGameInstance(GameInstance):
out = cgimageref_to_image(imgref)
return out.resize((w, h), Image.NEAREST)
def set_taskbar_progress(self, type, progress):
pass
def get_overlay_area(self) -> QGraphicsItem:
return self.overlay
class AXAPIError(Exception):
@@ -188,6 +201,7 @@ class AXAPIError(Exception):
ApplicationServices.kAXErrorNotificationAlreadyRegistered: "The notification has already been registered",
ApplicationServices.kAXErrorCannotComplete: "The function cannot complete because messaging has failed in some way.",
ApplicationServices.kAXErrorFailure: "There is some sort of system memory failure.",
ApplicationServices.kAXErrorAPIDisabled: "Assistive applications are not enabled in System Preferences.",
}
def __init__(self, code):
@@ -195,3 +209,8 @@ class AXAPIError(Exception):
raise ValueError("Success")
super().__init__(self.mapping.get(code, f"API Error: {code}"))
class NoCapturePermission(Exception):
def __init__(self):
super().__init__("Screen Recording permission is not allowed")
+49 -10
View File
@@ -1,12 +1,15 @@
import time
from functools import reduce
from typing import List, Dict, Optional
from typing import List, Dict, Optional, Union
import Quartz
from PySide2.QtCore import QTimer
import ApplicationServices
from PySide2.QtCore import QTimer, Signal, Slot
from PySide2.QtGui import QDesktopServices
from PySide2.QtWidgets import QMessageBox
from .instance import QuartzGameInstance
from runekit.game.overlay import DesktopWideOverlay
from ..instance import GameInstance
from ..manager import GameManager
@@ -15,12 +18,26 @@ has_prompted_accessibility = False
class QuartzGameManager(GameManager):
_instances: Dict[int, GameInstance]
overlay: DesktopWideOverlay
request_accessibility_popup = Signal()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._instances = {}
self.request_accessibility_popup.connect(self.accessibility_popup)
self._setup_tap()
ApplicationServices.AXIsProcessTrustedWithOptions(
{
ApplicationServices.kAXTrustedCheckOptionPrompt: True,
}
)
while not ApplicationServices.AXIsProcessTrusted():
time.sleep(0.1)
self._setup_overlay()
def _setup_tap(self):
events = [
Quartz.kCGEventLeftMouseDown,
@@ -42,6 +59,24 @@ class QuartzGameManager(GameManager):
Quartz.CFRunLoopGetCurrent(), source, Quartz.kCFRunLoopCommonModes
)
def _setup_overlay(self):
self.overlay = DesktopWideOverlay()
def start():
self.overlay.show()
self.overlay.check_compatibility()
# Seems like QGraphicsView has a delay before applying stylesheet
# Put some delay to allow it to initialize and not flash
QTimer.singleShot(1000, start)
def stop(self):
try:
self.overlay.hide()
self.overlay.deleteLater()
except RuntimeError:
pass
def get_instances(self) -> List[GameInstance]:
windows = Quartz.CGWindowListCopyWindowInfo(
Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID
@@ -58,6 +93,12 @@ class QuartzGameManager(GameManager):
return list(self._instances.values())
def get_active_instance(self) -> Union[GameInstance, None]:
if not self._instances:
return None
return list(self._instances.values())[0]
def get_instance_by_pid(self, pid: int) -> Optional[QuartzGameInstance]:
for instance in self._instances.values():
if instance.pid == pid:
@@ -78,8 +119,9 @@ class QuartzGameManager(GameManager):
instance = self.get_instance_by_pid(front_app.processIdentifier())
else:
instance = self._instances.get(nsevent.windowNumber())
if not instance:
return event
if not instance:
return event
# Check for cmd1
if nsevent.type() == Quartz.NSEventTypeKeyDown:
@@ -94,25 +136,22 @@ class QuartzGameManager(GameManager):
return event
@Slot()
def accessibility_popup(self):
global has_prompted_accessibility
if has_prompted_accessibility:
return
# TODO: Check AXIsProcessTrusted?
has_prompted_accessibility = True
msgbox = QMessageBox(
QMessageBox.Warning,
"Permission required",
"RuneKit needs Accessibility Access for global hotkey and game activity monitoring\n\nOpen System Preferences > Security > Privacy > Accessibility to allow this",
"RuneKit needs Screen Recording permission\n\nOpen System Preferences > Security > Privacy > Screen Recording to allow this",
QMessageBox.Open | QMessageBox.Ignore,
)
button = msgbox.exec()
if button == QMessageBox.Open:
QDesktopServices.openUrl(
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
"x-apple.systempreferences:com.apple.preference.security?Privacy_Screen Recording"
)
# FIXME: Closing this dialog will close the app, idk why
# I think Qt recognize the dialog as the last app window (which is untrue)
-66
View File
@@ -1,66 +0,0 @@
import xcffib
opcode_cache = {}
def get_extension_by_opcode(connection, opcode):
if opcode in opcode_cache:
return opcode_cache[opcode]
for name in connection.core.ListExtensions().reply().names:
ext_opcode = (
connection.core.QueryExtension(name.name_len, name.name)
.reply()
.major_opcode
)
opcode_cache[ext_opcode] = xcffib.ExtensionKey(name.name.to_string())
try:
return opcode_cache[opcode]
except KeyError:
raise ExtensionNotFound(opcode)
class ExtensionNotFound(Exception):
def __init__(self, opcode):
self.opcode = opcode
super().__init__(f"Extension with opcode {opcode} was not found")
class GeGenericEvent(xcffib.Event):
def __init__(self, unpacker):
if isinstance(unpacker, xcffib.Protobj):
unpacker = xcffib.MemoryUnpacker(unpacker.pack())
super().__init__(unpacker)
try:
base = unpacker.offset
(
self.extension,
self.sequence_number,
self.length,
self.evtype,
) = unpacker.unpack("xBHIH")
self.payload = xcffib.List(unpacker, "I", 8 + self.length)
self.bufsize = unpacker.offset - base
except:
import traceback
traceback.print_exc()
raise
def to_event(self, connection):
extension = get_extension_by_opcode(connection, self.extension)
constructor = xcffib.extensions[extension][1][self.evtype]
unpacker = xcffib.MemoryUnpacker(self.payload.buf())
try:
return constructor(unpacker)
except:
import traceback
traceback.print_exc()
raise
xcffib.core_events[35] = GeGenericEvent
+91 -83
View File
@@ -1,142 +1,122 @@
import logging
import time
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, List, Union
from PySide2.QtCore import QRect, Slot, Signal
from PySide2.QtGui import QWindow, QGuiApplication
import xcffib.xproto
import xcffib.composite
import xcffib.xinput
from PIL import Image, ImageOps
import xcffib.xproto
from PySide2.QtCore import QRect, Signal, Slot
from PySide2.QtGui import QWindow, QGuiApplication
from PySide2.QtWidgets import QGraphicsItem
from runekit.game.instance import GameInstance
from runekit.game.qt import QtGrabMixin
from runekit.game.psutil_mixins import PsUtilNetStat
from runekit.game.qt import QtGrabMixin, QtEmbedMixin
from .ximage import zpixmap_shm_to_image
if TYPE_CHECKING:
from .manager import X11GameManager
KEY_1 = 10
class X11GameInstance(QtGrabMixin, GameInstance):
class X11GameInstance(QtGrabMixin, QtEmbedMixin, PsUtilNetStat, GameInstance):
wid: int
refresh_rate = 100
manager: "X11GameManager"
overlay: QGraphicsItem
game_last_grab = 0.0
game_last_image = None
embedded_windows: List[QWindow]
cached_position = None
key_press = Signal(xcffib.xinput.KeyPressEvent)
input_signal = Signal(xcffib.Event)
config_signal = Signal(xcffib.Event)
destroy_signal = Signal(xcffib.Event)
def __init__(self, manager: "X11GameManager", wid: int, **kwargs):
super().__init__(**kwargs)
self.manager = manager
self.wid = wid
self.pid = manager.get_property(wid, "_NET_WM_PID")
self.qwindow = QWindow.fromWinId(wid)
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
self._setup()
self.key_press.connect(self.on_key_press)
self.embedded_windows = []
self._setup_x()
self._setup_overlay()
self.input_signal.connect(self.on_input)
self.config_signal.connect(self.on_config)
self._update_is_focused()
def _setup(self):
def _setup_x(self):
self.manager.xcomposite.RedirectWindow(
self.wid, xcffib.composite.Redirect.Automatic
)
self.pixmap_id = self.manager.connection.generate_id()
self.manager.xcomposite.NameWindowPixmap(
self.wid, self.pixmap_id, is_checked=True
)
self._grab_alt1()
# self.manager.xinput.XIGrabDevice(
# self.wid,
# xcffib.xproto.Time.CurrentTime,
# xcffib.xproto.Cursor._None,
# xcffib.xinput.Device.All,
# xcffib.xinput.GrabMode22.Async,
# xcffib.xinput.GrabMode22.Async,
# True,
# 1,
# (
# xcffib.xinput.XIEventMask.KeyPress
# | xcffib.xinput.XIEventMask.ButtonPress,
# ),
# )
def _grab_alt1(self):
self.manager.xinput.XIPassiveGrabDevice(
xcffib.xproto.Time.CurrentTime,
self.name_pixmap()
self.manager.connection.core.ChangeWindowAttributesChecked(
self.wid,
xcffib.xproto.Cursor._None,
KEY_1,
xcffib.xinput.Device.All,
1,
1,
xcffib.xinput.GrabType.Keycode,
xcffib.xinput.GrabMode22.Async,
xcffib.xproto.GrabMode.Async,
True,
(
xcffib.xinput.XIEventMask.KeyPress
| xcffib.xinput.XIEventMask.KeyRelease,
),
(xcffib.xproto.KeyButMask.Mod1,),
xcffib.xproto.CW.EventMask,
[
xcffib.xproto.EventMask.KeyPress
| xcffib.xproto.EventMask.StructureNotify
],
)
def _ungrab_alt1(self):
self.manager.xinput.XIPassiveUngrabDevice(
self.wid,
KEY_1,
xcffib.xinput.Device.All,
1,
xcffib.xinput.GrabType.Keycode,
(xcffib.xproto.KeyButMask.Mod1,),
)
def _setup_overlay(self):
self.overlay, self._overlay_disconnect = self.manager.overlay.add_instance(self)
def __del__(self):
self.manager.connection.core.FreePixmap(self.pixmap_id)
self.manager.xcomposite.UnredirectWindow(
self.wid, xcffib.composite.Redirect.Automatic
)
self._ungrab_alt1()
self._overlay_disconnect()
def name_pixmap(self):
if hasattr(self, "pixmap_id"):
self.manager.connection.core.FreePixmap(self.pixmap_id)
self.pixmap_id = self.manager.connection.generate_id()
self.manager.xcomposite.NameWindowPixmap(
self.wid, self.pixmap_id, is_checked=True
)
def get_position(self) -> QRect:
geom = self.manager.connection.core.GetGeometry(self.wid).reply()
if self.cached_position:
return self.cached_position
return QRect(geom.x, geom.y, geom.width, geom.height)
geom = self.manager.connection.core.GetGeometry(self.wid).reply()
translated = self.manager.connection.core.TranslateCoordinates(
self.wid, self.manager.screen.root, 0, 0
).reply()
self.cached_position = QRect(
translated.dst_x, translated.dst_y, geom.width, geom.height
)
return self.cached_position
def get_scaling(self) -> float:
screen = QGuiApplication.screenAt(self.get_position().topLeft())
pos = self.get_position().topLeft()
if pos.x() < 0:
pos.setX(0)
if pos.y() < 0:
pos.setY(0)
screen = QGuiApplication.screenAt(pos)
return screen.devicePixelRatio()
def is_focused(self) -> bool:
return self.is_focused
return self._is_focused
def _update_is_focused(self):
self.is_focused = self.manager.get_active_window() == self.wid
self._is_focused = self.manager.get_active_window() == self.wid
def set_taskbar_progress(self, type_, progress):
# TODO: Implement this on Unity? I don't use Unity
# TODO: If progress = 1 set urgency flag
pass
@Slot(xcffib.xinput.KeyPressEvent)
def on_key_press(self, evt: xcffib.xinput.KeyPressEvent):
# Check for alt1
if evt.flags & xcffib.xinput.KeyEventFlags.KeyRepeat:
return
if evt.mods.effective & xcffib.xproto.KeyButMask.Mod1 and evt.detail == KEY_1:
if isinstance(evt, xcffib.xinput.KeyPressEvent):
self.alt1_pressed.emit()
self.logger.debug("alt1 pressed %s", repr(evt))
else:
self._ungrab_alt1()
self._grab_alt1()
return
def grab_game(self):
if (time.monotonic() - self.game_last_grab) * 1000 < self.refresh_rate:
return self.game_last_image
@@ -170,3 +150,31 @@ class X11GameInstance(QtGrabMixin, GameInstance):
finally:
if shm:
self.manager.free_shm((xid, shm))
def embed_window(self, window: QWindow):
super().embed_window(window)
self.embedded_windows.append(window)
def get_overlay_area(self) -> QGraphicsItem:
return self.overlay
@Slot(xcffib.Event)
def on_config(self, evt: xcffib.xproto.ConfigureNotifyEvent):
self.name_pixmap()
translated = self.manager.connection.core.TranslateCoordinates(
self.wid, self.manager.screen.root, 0, 0
).reply()
size = QRect(translated.dst_x, translated.dst_y, evt.width, evt.height)
self.cached_position = size
self.positionChanged.emit(size)
@Slot(xcffib.Event)
def on_input(
self, evt: Union[xcffib.xproto.KeyPressEvent, xcffib.xproto.ButtonPressEvent]
):
if evt.detail == 10 and evt.state & xcffib.xproto.KeyButMask.Mod1:
self.alt1_pressed.emit()
else:
self.game_activity.emit()
+151 -107
View File
@@ -1,88 +1,117 @@
import logging
import sys
from typing import List, Dict, Tuple
import os
import struct
from typing import List, Dict, Tuple, Union
from PySide2.QtCore import QThread, Slot, Signal, QObject
import sysv_ipc
import xcffib
import xcffib.xproto
import xcffib.composite
import xcffib.shm
import xcffib.xinput
import sysv_ipc
import xcffib.xproto
from PySide2.QtCore import QThread, Slot, QObject, Signal
from runekit.game import GameManager
from .instance import GameInstance, X11GameInstance
from . import ge
from .utils import ClassDict
from ..overlay import DesktopWideOverlay
MAX_SHM = 10
NET_ACTIVE_WINDOW = "_NET_ACTIVE_WINDOW"
WM_APP_NAME = os.getenv("RK_WM_APP_NAME", "RuneScape")
class X11GameManager(GameManager):
connection: xcffib.Connection
_instance: Dict[int, X11GameInstance]
_instances: Dict[int, X11GameInstance]
_atom: Dict[bytes, int]
_shm: List[Tuple[int, sysv_ipc.SharedMemory]]
def __init__(self, **kwargs):
super().__init__(*kwargs)
self._instance = {}
self._instances = {}
self._atom = {}
self._shm = []
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
self.connection = xcffib.Connection()
self.screen = self.connection.get_screen_pointers()[self.connection.pref_screen]
self.xcomposite = self.connection(xcffib.composite.key)
self.xshm = self.connection(xcffib.shm.key)
self.xinput = self.connection(xcffib.xinput.key)
self._setup_composite()
self._setup_overlay()
self.event_thread = QThread()
self.event_thread = QThread(self)
self.event_worker = X11EventWorker(self)
self.event_worker.moveToThread(self.event_thread)
self.event_thread.started.connect(self.event_worker.run)
self.event_thread.finished.connect(self.event_worker.deleteLater)
self.event_worker.on_active_window_changed.connect(
self.on_active_window_changed
)
self.event_worker.create_signal.connect(self.on_game_opened)
self.event_worker.destroy_signal.connect(self.on_game_closed)
self.event_thread.start()
self.get_instances()
def stop(self):
self.event_thread.requestInterruption()
self.event_thread.quit()
self.event_thread.wait()
def get_instances(self) -> List[GameInstance]:
def visit(wid: int):
wm_class = self.get_property(wid, xcffib.xproto.Atom.WM_CLASS)
if self.is_game(wid):
if wid not in self._instances:
self.logger.info("Found game instance %d", wid)
instance = X11GameInstance(self, wid, parent=self)
self._instances[wid] = instance
if wm_class:
instance_name, app_name = wm_class.split("\00")
if app_name == "RuneScape":
if wid not in self._instance:
instance = X11GameInstance(self, wid, parent=self)
self._instance[wid] = instance
return
query = self.connection.core.QueryTree(wid).reply()
for child in query.children:
visit(child)
try:
query = self.connection.core.QueryTree(wid).reply()
for child in query.children:
visit(child)
except xcffib.xproto.WindowError:
return
visit(self.screen.root)
return list(self._instance.values())
return list(self._instances.values())
def get_active_instance(self) -> Union[GameInstance, None]:
for instance in self._instances.values():
if instance.is_focused():
return instance
return None
def is_game(self, wid: int) -> bool:
geom_req = self.connection.core.GetGeometry(wid)
try:
wm_class = self.get_property(wid, xcffib.xproto.Atom.WM_CLASS)
except xcffib.xproto.WindowError:
geom_req.discard_reply()
return False
geom = geom_req.reply()
if geom.width == 32 and geom.height == 32:
# OpenGL test window
return False
if not wm_class:
return False
instance_name, app_name = wm_class.split("\00")
return app_name == WM_APP_NAME
def get_active_window(self) -> int:
return self.get_property(self.screen.root, "_NET_ACTIVE_WINDOW")
@Slot(int)
def on_active_window_changed(self, winid):
for id_, instance in self._instance.items():
active = winid == id_
if active != instance.is_focused:
instance.is_focused = active
instance.focusChanged.emit(active)
def _setup_overlay(self):
self.overlay = DesktopWideOverlay()
self.overlay.show()
self.overlay.check_compatibility()
def _setup_composite(self):
self.xcomposite.QueryVersion(0, 4, is_checked=True)
@@ -106,8 +135,8 @@ class X11GameManager(GameManager):
if reply.type == xcffib.xproto.Atom.STRING:
return reply.value.to_string()[:-1]
elif reply.type == xcffib.xproto.Atom.WINDOW:
return int.from_bytes(reply.value.buf(), byteorder=sys.byteorder)
elif reply.type in (xcffib.xproto.Atom.WINDOW, xcffib.xproto.Atom.CARDINAL):
return struct.unpack("=I", reply.value.buf()[:4])[0]
return reply.value
@@ -150,101 +179,116 @@ class X11GameManager(GameManager):
shm.detach()
shm.remove()
@Slot(xcffib.Event)
def on_game_opened(self, evt: xcffib.xproto.CreateNotifyEvent):
self.logger.info("New game window opened %d", evt.window)
self._instances[evt.window] = X11GameInstance(self, evt.window, parent=self)
@Slot(int)
def on_game_closed(self, wid: int):
if wid not in self._instances:
return
self.logger.info("Game window %d closed", wid)
instance = self._instances[wid]
del self._instances[wid]
self.instance_removed.emit(instance)
self.instance_changed.emit()
class X11EventWorker(QObject):
on_active_window_changed = Signal(int)
stop = False
create_signal = Signal(xcffib.Event)
destroy_signal = Signal(int)
def __init__(self, manager: "X11GameManager", **kwargs):
super().__init__(**kwargs)
self.manager = manager
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
self.handlers = ClassDict(
{
xcffib.xproto.PropertyNotifyEvent: self.on_property_change,
xcffib.xinput.KeyPressEvent: self.on_key_press,
xcffib.xinput.KeyReleaseEvent: self.on_key_press,
}
)
self.handlers = {
xcffib.xproto.PropertyNotifyEvent: self.on_property_change,
xcffib.xproto.ConfigureNotifyEvent: self.on_configure_event,
xcffib.xproto.KeyPressEvent: self.on_input_event,
xcffib.xproto.ButtonPressEvent: self.on_input_event,
xcffib.xproto.CreateNotifyEvent: self.on_create,
xcffib.xproto.DestroyNotifyEvent: self.on_destroy,
}
self.active_win_id = self.manager.get_active_window()
@Slot()
def run(self):
self.manager.connection.core.ChangeWindowAttributes(
self.manager.connection.core.ChangeWindowAttributesChecked(
self.manager.screen.root,
xcffib.xproto.CW.EventMask,
[xcffib.xproto.EventMask.PropertyChange],
is_checked=True,
)
[
xcffib.xproto.EventMask.PropertyChange
| xcffib.xproto.EventMask.SubstructureNotify,
],
).check()
current_thread = QThread.currentThread()
while True:
if self.stop:
self.logger.debug("Exiting")
if current_thread.isInterruptionRequested():
return
evt = self.manager.connection.wait_for_event()
if isinstance(evt, ge.GeGenericEvent):
evt = evt.to_event(self.manager.connection)
try:
handler = self.handlers[evt]
except KeyError:
self.logger.debug("Unhandled event %s", repr(evt))
evt = self.manager.connection.poll_for_event()
if evt is None:
QThread.msleep(10)
continue
try:
handler(evt)
except:
self.logger.error("Error handling event %s", repr(evt), exc_info=True)
def on_key_press(self, evt: xcffib.xinput.KeyPressEvent):
print(
{
"deviceid": evt.deviceid,
"time": evt.time,
"detail": evt.detail,
"root": evt.root,
"event": evt.event,
"child": evt.child,
"root_x": evt.root_x,
"root_y": evt.root_y,
"event_x": evt.event_x,
"event_y": evt.event_y,
"buttons_len": evt.buttons_len,
"valuators_len": evt.valuators_len,
"sourceid": evt.sourceid,
"flags": evt.flags,
}
)
try:
instance = self.manager._instance[evt.root]
except KeyError:
self.logger.debug(
"Unknown KeyPressEvent to instance %s: %s", evt.root, repr(evt)
)
self.manager.xinput.XIPassiveUngrabDevice(
evt.root,
evt.detail,
evt.deviceid,
1,
xcffib.xinput.GrabType.Keycode,
evt.mods.effective,
)
return
instance.key_press.emit(evt)
for wanted_type, handler in self.handlers.items():
if isinstance(evt, wanted_type):
try:
handler(evt)
except:
self.logger.error(
"Error handling event %s", repr(evt), exc_info=True
)
def on_property_change(self, evt: xcffib.xproto.PropertyNotifyEvent):
if evt.atom == self.manager.get_atom(NET_ACTIVE_WINDOW):
if (
evt.state == xcffib.xproto.Property.NewValue
and evt.atom == self.manager.get_atom(NET_ACTIVE_WINDOW)
and evt.window == self.manager.screen.root
):
active_win_id = self.manager.get_active_window()
if self.active_win_id == active_win_id:
return
self.active_win_id = active_win_id
self.logger.debug(
"Active window changed to %d - %s", active_win_id, repr(evt)
)
self.on_active_window_changed.emit(active_win_id)
for id_, instance in self.manager._instances.items():
active = active_win_id == id_
if active != instance._is_focused:
instance._is_focused = active
instance.focusChanged.emit(active)
def on_input_event(
self, evt: Union[xcffib.xproto.KeyPressEvent, xcffib.xproto.ButtonPressEvent]
):
try:
self.manager._instances[evt.event].input_signal.emit(evt)
except KeyError:
self.logger.debug("Got input event for %d but is not registered", evt.event)
def on_configure_event(self, evt: xcffib.xproto.ConfigureNotifyEvent):
try:
self.manager._instances[evt.window].config_signal.emit(evt)
except KeyError:
pass
def on_create(self, evt: xcffib.xproto.CreateNotifyEvent):
if evt.window in self.manager._instances:
return
# At this point we aren't sure if it's actually the game...
if not self.manager.is_game(evt.window):
return
self.create_signal.emit(evt)
def on_destroy(self, evt: xcffib.xproto.DestroyNotifyEvent):
if evt.window in self.manager._instances:
self.destroy_signal.emit(evt.window)
-7
View File
@@ -1,7 +0,0 @@
class ClassDict(dict[type]):
def __getitem__(self, item):
for key, value in self.items():
if isinstance(item, key):
return value
raise KeyError
+7 -10
View File
@@ -1,16 +1,13 @@
import sysv_ipc
import numpy as np
from PIL import ImageOps, Image
def zpixmap_shm_to_image(
shm: sysv_ipc.SharedMemory, size: int, width: int, height: int
) -> Image:
out = Image.frombytes(
"RGBA",
(width, height),
shm.read(size),
"raw",
)
b, g, r, a = out.split()
a = ImageOps.invert(a)
return Image.merge("RGBA", (r, g, b, a))
) -> np.ndarray:
out = np.frombuffer(shm.read(size), "<B").copy()
out.shape = (height, width, 4)
out[:, :, 3] = 0xFF # Convert BGRX to BGRA
return out
+194
View File
@@ -0,0 +1,194 @@
import typing
import json
from typing import Optional, TypedDict, Union, List
from PySide2.QtCore import QAbstractItemModel, Qt, QModelIndex, QMimeData
from PySide2.QtGui import QIcon
from runekit.alt1.schema import AppManifest
from runekit.app import AppStore
MIMETYPE = "application/x-qabstractitemmodeldatalist"
class _InternalData(TypedDict):
id: str
data: Optional[AppManifest]
index: QModelIndex
parent: Union[QModelIndex, None]
children: List["_InternalData"]
class AppStoreModel(QAbstractItemModel):
h_columns = ("Name", "URL", "Description")
model: List[_InternalData]
def __init__(self, parent, store: AppStore):
super().__init__(parent)
self.store = store
self.store.app_change.connect(self.on_app_change)
self.model = self.build_model()
def on_app_change(self):
self.beginResetModel()
self.model = self.build_model()
self.endResetModel()
def build_model(self, root="", parent=None) -> List[_InternalData]:
apps = self.store.list_app(root)
out = []
for app_id, meta in apps:
if not meta:
data = {
"id": app_id,
"parent": parent,
}
index = data["index"] = self.createIndex(len(out), 0, data)
data["children"] = self.build_model(app_id, index)
out.append(data)
continue
data = {
"id": app_id,
"data": meta,
"parent": parent,
}
data["index"] = self.createIndex(len(out), 0, data)
out.append(data)
return out
def columnCount(self, parent: QModelIndex) -> int:
return len(self.h_columns)
def rowCount(self, parent: QModelIndex) -> int:
if parent.isValid():
data = typing.cast(_InternalData, parent.internalPointer())
return len(data["children"])
return len(self.model)
def data(self, index: QModelIndex, role: int):
left_col = index.siblingAtColumn(0)
col = index.column()
app = typing.cast(_InternalData, left_col.internalPointer())
if "data" not in app:
# Folder
if role == Qt.DisplayRole:
if col == 0:
return app["id"]
elif role == Qt.DecorationRole:
if col == 0:
return QIcon.fromTheme("folder")
return None
if role in (Qt.DisplayRole, Qt.ToolTipRole):
if col == 0:
return app["data"]["appName"]
elif col == 1:
return app["data"]["appUrl"]
elif col == 2:
return app["data"]["description"]
elif role == Qt.DecorationRole and col == 0:
return self.store.icon(app["id"])
else:
return None
def hasChildren(self, index: QModelIndex) -> bool:
if not index.isValid():
return True
if index.column() != 0 or not index.internalPointer():
return False
data = typing.cast(_InternalData, index.internalPointer())
return "data" not in data
def index(self, row: int, column: int, parent: QModelIndex) -> QModelIndex:
if column != 0:
leftmost = self.index(row, 0, parent)
return self.createIndex(row, column, leftmost.internalPointer())
if parent.isValid():
data = typing.cast(_InternalData, parent.internalPointer())
return data["children"][row]["index"]
if row >= len(self.model):
return QModelIndex()
return self.model[row]["index"]
def parent(self, index: QModelIndex) -> QModelIndex:
data = typing.cast(_InternalData, index.internalPointer())
return data["parent"] or QModelIndex()
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
out = Qt.ItemIsSelectable | Qt.ItemIsEnabled
if not index.isValid():
return out | Qt.ItemIsDropEnabled
data = typing.cast(_InternalData, index.internalPointer())
if "data" in data:
out |= Qt.ItemIsDragEnabled
else:
# Can't drag folder but can drop
out |= Qt.ItemIsDropEnabled
if "children" not in data:
out |= Qt.ItemNeverHasChildren
return out
def headerData(self, section: int, orientation: Qt.Orientation, role: int):
if role != Qt.DisplayRole:
return None
if orientation != Qt.Orientation.Horizontal:
return None
return self.h_columns[section]
def supportedDropActions(self) -> Qt.DropActions:
return Qt.MoveAction
def mimeData(self, indexes: List[QModelIndex]) -> QMimeData:
indexes = [item for item in indexes if item.column() == 0]
if not indexes:
return None
data = typing.cast(_InternalData, indexes[0].internalPointer())
out = QMimeData()
out_data = {
"id": data["id"],
"parent": "",
"type": "dir" if "data" not in data else "app",
}
if data["parent"]:
out_data["parent"] = data["parent"].internalPointer()["id"]
out.setData(MIMETYPE, json.dumps(out_data).encode("ascii"))
return out
def dropMimeData(
self,
data: QMimeData,
action: Qt.DropAction,
row: int,
column: int,
parent: QModelIndex,
) -> bool:
data = json.loads(data.data(MIMETYPE).data())
if parent.isValid():
new_folder = parent.internalPointer()["id"]
else:
new_folder = ""
if data["parent"] == new_folder:
return False
self.store.add_app_to_folder(data["id"], new_folder)
self.store.delete_app_from_folder(data["id"], data["parent"])
return True
+100 -13
View File
@@ -1,29 +1,116 @@
from typing import TYPE_CHECKING
import logging
from typing import TYPE_CHECKING, List
from PySide2.QtWidgets import QWidget
from PySide2.QtCore import Slot, QCoreApplication
from PySide2.QtWidgets import QMessageBox
from runekit.alt1.utils import fetch_manifest
from runekit.app import App
from runekit.alt1.schema import AppManifest
from runekit.alt1.utils import fetch_bom_json
from runekit.app import App, AppStore
from runekit.app.store import app_id
from runekit.host.settings import SettingsDialog
from runekit.ui import AutoNotifier
from runekit.ui.tray import TrayIcon
if TYPE_CHECKING:
from runekit.game import GameInstance
from runekit.game import GameInstance, GameManager
class Host(QWidget):
class Host:
"""Host is the root of our application"""
def __init__(self):
super().__init__()
self.notifier = AutoNotifier()
open_app: List[App]
def start_app(self, manifest_url, instance: "GameInstance") -> App:
manifest = fetch_manifest(manifest_url)
def __init__(self, manager: "GameManager"):
super().__init__()
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
self.manager = manager
self.open_app = []
self.notifier = AutoNotifier()
self.app_store = AppStore()
self.tray_icon = TrayIcon(self)
self.setting_dialog = SettingsDialog(self)
self.tray_icon.show()
QCoreApplication.instance().aboutToQuit.connect(self.on_before_quit)
self.app_store.app_change.connect(self.tray_icon.update_menu)
self.tray_icon.on_settings.connect(self.open_settings)
self.manager.instance_removed.connect(self.on_game_quit)
def on_before_quit(self):
for app in self.open_app:
app.close()
def __del__(self):
self.open_app = []
def launch_app_from_url(self, manifest_url) -> App:
manifest = fetch_bom_json(manifest_url)
manifest["configUrl"] = manifest_url
return self.launch_app(app_id(manifest_url), manifest)
@Slot(str)
def launch_app_id(self, appid):
try:
manifest = self.app_store[appid]
except KeyError:
QMessageBox.critical(
None,
"App missing",
"Cannot find application by that ID",
)
return
self.launch_app(appid, manifest)
def launch_app(self, appid: str, manifest: AppManifest):
instance = self.manager.get_active_instance()
if not instance:
instances = self.manager.get_instances()
if not instances:
QMessageBox.critical(
None,
"No game instances found",
"Cannot find open RuneScape window. Please launch the game before any apps",
)
return
instance = instances[0]
app = App(
host=self,
app_id=appid,
manifest=manifest,
game_instance=instance,
source_url=manifest_url,
source_url=manifest["configUrl"],
)
app.get_window(parent=self).show()
def on_destroy():
try:
self.open_app.remove(app)
except ValueError:
pass
window = app.get_window()
window.show()
window.destroyed.connect(on_destroy)
self.open_app.append(app)
return app
@Slot()
def open_settings(self):
self.setting_dialog.show()
self.setting_dialog.raise_()
@Slot()
def on_game_quit(self, instance: "GameInstance"):
self.logger.info("Game instance is closing")
for app in self.open_app[:]:
if app.game_instance == instance:
self.logger.debug(
"Closing app window %s because instance is closing",
app.manifest["appName"],
)
app.close()
self.open_app.remove(app)
+221
View File
@@ -0,0 +1,221 @@
import json
import sys
from typing import TYPE_CHECKING
from PySide2.QtCore import QSize, Qt, Slot, QTimer, QSettings
from PySide2.QtGui import QIcon
from PySide2.QtWidgets import (
QTabWidget,
QVBoxLayout,
QMainWindow,
QWidget,
QFormLayout,
QLabel,
QComboBox,
QCheckBox,
QTableWidget,
QHBoxLayout,
QTreeView,
QSpinBox,
QPushButton,
QAbstractItemView,
QInputDialog,
QMessageBox,
)
from .appstore_model import AppStoreModel
from ..ui import TooltipNotifier, TrayIconNotifier, AutoNotifier
if TYPE_CHECKING:
from .host import Host
class SettingsDialog(QMainWindow):
def __init__(self, host: "Host", **kwargs):
super().__init__(flags=Qt.Dialog, **kwargs)
self.host = host
self._layout()
self.setWindowTitle("RuneKit Settings")
def _layout(self):
tab = QTabWidget(self)
tab.addTab(ApplicationPage(self.host, parent=self), "Applications")
tab.addTab(InterfacePage(self.host, parent=self), "Interface")
self.setCentralWidget(tab)
self.setContentsMargins(11, 11, 11, 11)
def sizeHint(self) -> QSize:
return QSize(800, 500)
class ApplicationPage(QWidget):
host: "Host"
def __init__(self, host: "Host", **kwargs):
super().__init__(**kwargs)
self.host = host
self.model = AppStoreModel(self, self.host.app_store)
self._layout()
def _layout(self):
layout = QHBoxLayout(self)
self.setLayout(layout)
self.tree = QTreeView(self)
self.tree.setModel(self.model)
self.tree.setUniformRowHeights(True)
self.tree.setColumnWidth(0, 200)
self.tree.setDragEnabled(True)
self.tree.setDragDropMode(QAbstractItemView.InternalMove)
self.tree.viewport().setAcceptDrops(True)
layout.addWidget(self.tree, 1)
buttons = QVBoxLayout()
buttons.setAlignment(Qt.AlignTop)
add_button = QPushButton(QIcon.fromTheme("list-add"), "", self)
add_button.setToolTip("Add application")
add_button.clicked.connect(self.on_add)
buttons.addWidget(add_button)
mkdir_button = QPushButton(QIcon.fromTheme("folder-new"), "", self)
mkdir_button.setToolTip("Make directory")
mkdir_button.clicked.connect(self.on_mkdir)
buttons.addWidget(mkdir_button)
delete_button = QPushButton(QIcon.fromTheme("list-remove"), "", self)
delete_button.setToolTip("Remove selected item")
delete_button.clicked.connect(self.on_delete)
buttons.addWidget(delete_button)
layout.addLayout(buttons)
def on_add(self):
dialog = QInputDialog(self)
dialog.setLabelText("Enter appconfig.json URL")
if dialog.exec_() == QInputDialog.Rejected:
return
app_url = dialog.textValue().strip()
self.host.app_store.add_app_ui([app_url])
def on_mkdir(self):
dialog = QInputDialog(self)
dialog.setLabelText("Directory name")
if dialog.exec_() == QInputDialog.Rejected:
return
dirname = dialog.textValue().strip()
if not dirname or "/" in dirname:
QMessageBox.critical(
self, "Invalid input", "This directory name cannot be used"
)
return
self.host.app_store.mkdir(dirname)
def on_delete(self):
data = self.model.mimeData(self.tree.selectedIndexes())
if not data:
return
data = json.loads(data.data("application/x-qabstractitemmodeldatalist").data())
if data["type"] == "dir":
confirm = QMessageBox.question(
self,
"Remove folder",
f"Remove {data['id']}?\n\nAll applications will be moved to the top level",
)
if confirm == QMessageBox.StandardButton.No:
return
self.host.app_store.rmdir(data["id"])
else:
app = self.host.app_store[data["id"]]
confirm = QMessageBox.question(
self,
"Remove application",
f"Remove {app['appName']}?",
)
if confirm == QMessageBox.StandardButton.No:
return
self.host.app_store.remove_app(data["id"])
# FIXME: Clear settings???
class InterfacePage(QWidget):
host: "Host"
def __init__(self, host: "Host", **kwargs):
super().__init__(**kwargs)
self.host = host
self.settings = QSettings(self)
self._layout()
def _layout(self):
layout = QFormLayout(self)
self.setLayout(layout)
layout.addRow(
None,
QLabel("RuneKit needs to be restarted for changes to take effect", self),
)
tooltip_label = QLabel("Display tooltips as", self)
tooltip_field = QComboBox(self)
selected = int(
self.settings.value("settings/tooltip", AutoNotifier.METHOD_NOTIFICATION)
)
selected_idx = 0
for idx, method in enumerate(AutoNotifier.availableMethods().items()):
tooltip_field.insertItem(idx, method[1])
if method[0] == selected:
selected_idx = idx
tooltip_field.setCurrentIndex(selected_idx)
tooltip_field.currentIndexChanged.connect(self.preview_tooltip)
tooltip_field.currentIndexChanged.connect(self.on_tooltip_change)
layout.addRow(tooltip_label, tooltip_field)
border_field = QCheckBox("Styled window border", self)
border_field.setDisabled(sys.platform == "darwin")
border_field.setChecked(
(self.settings.value("settings/styledBorder", "true") == "true")
and sys.platform != "darwin"
)
border_field.stateChanged.connect(self.on_change_styled_border)
layout.addRow(None, border_field)
@Slot(int)
def preview_tooltip(self, index: int):
type_ = list(AutoNotifier.availableMethods().items())[index][0]
if type_ == AutoNotifier.METHOD_NOTIFICATION:
notifier = TrayIconNotifier()
elif type_ == AutoNotifier.METHOD_TOOLTIP:
notifier = TooltipNotifier()
else:
return
if hasattr(self, "_last_notifier"):
self._last_notifier.notify("")
self._last_notifier = notifier
notifier.notify("This is an example notification")
QTimer.singleShot(3000, lambda: notifier.notify(""))
@Slot(int)
def on_tooltip_change(self, index: int):
idx = list(AutoNotifier.availableMethods().items())[index]
self.settings.setValue("settings/tooltip", idx[0])
@Slot(int)
def on_change_styled_border(self, state: int):
checked = state == Qt.Checked
self.settings.setValue("settings/styledBorder", checked)
+1 -27
View File
@@ -1,27 +1 @@
import logging
import struct
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from PIL import Image
try:
from .format import image_to_bgra
except ImportError:
logging.warning("Cannot import native image code, using pure python")
image_8bpp = struct.Struct("BBBB")
def image_to_bgra(image: "Image", x, y, w, h) -> bytes:
buf = bytearray(w * h * image_8bpp.size)
index = 0
for p in image.crop((x, y, x + w, y + h)).getdata():
r = p[0]
g = p[1]
b = p[2]
a = p[3] if len(p) == 4 else 255
image_8bpp.pack_into(buf, index, b, g, r, a)
index += image_8bpp.size
return buf
from .algo import is_color_percent_gte
+11
View File
@@ -0,0 +1,11 @@
from typing import List
import numpy as np
def is_color_percent_gte(image: np.ndarray, color: List, percent: float) -> bool:
"""Check that the image has at least percent% of the given color"""
black_pixels = np.count_nonzero(np.all(image[:, :, : len(color)] == color, axis=-1))
total_pixels = image.shape[0] * image.shape[1]
color_percent = black_pixels / total_pixels
return color_percent >= percent
-46
View File
@@ -1,46 +0,0 @@
from cpython.mem cimport PyMem_Malloc, PyMem_Free
cdef struct ImageRGBA:
unsigned char r
unsigned char g
unsigned char b
unsigned char a
def image_to_bgra(image, long ix, long iy, unsigned long w, unsigned long h):
ptr = dict(image.im.unsafe_ptrs)['image32']
cdef size_t ptr_int = <size_t> ptr
cdef ImageRGBA **image32 = <ImageRGBA**> ptr_int
if ix < 0:
ix = 0
if iy < 0:
iy = 0
if ix + w > image.width:
w = image.width - ix
if iy + h > image.height:
h = image.height - iy
cdef size_t size = w * h * 4
cdef unsigned char *buf = <unsigned char*> PyMem_Malloc(size)
cdef size_t index, x, y
cdef unsigned char r, g, b, a
with nogil:
for y in range(iy, iy+h):
for x in range(ix, ix+w):
index = (((y-iy) * w) + (x-ix)) * 4
r = image32[y][x].r
g = image32[y][x].g
b = image32[y][x].b
a = image32[y][x].a
buf[index] = b
buf[index+1] = g
buf[index+2] = r
buf[index+3] = a
try:
return buf[:size]
finally:
PyMem_Free(buf)
+63
View File
@@ -0,0 +1,63 @@
from typing import TYPE_CHECKING
import numpy as np
if TYPE_CHECKING:
from runekit.game.instance import ImageType
def np_crop(image: np.ndarray, x: int, y: int, w: int, h: int) -> np.ndarray:
shape = image.shape
img_width = shape[1]
img_height = shape[0]
if w is None:
w = img_width
if h is None:
h = img_height
x_pad_left = max(0, -x)
y_pad_left = max(0, -y)
x1 = max(0, x)
y1 = max(0, y)
x2 = min(x1 + w - x_pad_left, img_width - x_pad_left)
y2 = min(y1 + h - y_pad_left, img_height - y_pad_left)
image = image[y1:y2, x1:x2]
x_pad = (x_pad_left, w - (x2 - x1) - x_pad_left)
y_pad = (y_pad_left, h - (y2 - y1) - y_pad_left)
if x_pad != (0, 0) or y_pad != (0, 0):
image = np.pad(image, (y_pad, x_pad, (0, 0)))
return image
def np_save_image(image: np.ndarray, out: str):
from PIL import Image
size = (image.shape[1], image.shape[0])
if len(image.shape) == 2:
# Grayscale
Image.frombuffer("L", size, image.tobytes(), "raw", "L", 0, 1).save(out)
elif image.shape[2] == 3:
# RGB
Image.frombuffer(
"RGB", size, image[:, :, ::-1].tobytes(), "raw", "RGB", 0, 1
).save(out)
else:
# RGBA
Image.frombuffer(
"RGBA", size, image[:, :, [2, 1, 0, 3]].tobytes(), "raw", "RGBA", 0, 1
).save(out)
def ensure_np_image(image: "ImageType") -> np.ndarray:
if isinstance(image, np.ndarray):
return image
if image.mode == "RGB":
image = image.convert("RGBA")
out = np.array(image)
out = out[:, :, [2, 1, 0, 3]] # B G R A
return out
+82
View File
@@ -0,0 +1,82 @@
import logging
import signal
import sys
import traceback
import click
from PySide2.QtCore import QSettings, Qt, QTimer
from PySide2.QtWidgets import (
QApplication,
QMessageBox,
)
import runekit._resources
from runekit import browser
from runekit.game import get_platform_manager
from runekit.host import Host
@click.command(
context_settings=dict(
ignore_unknown_options=True,
)
)
@click.option("--game-index", default=0, help="Game instance index, starting from 0")
@click.argument("qt_args", nargs=-1, type=click.UNPROCESSED)
@click.argument("app_url", required=False)
def main(app_url, game_index, qt_args):
logging.basicConfig(level=logging.DEBUG)
logging.info("Starting QtWebEngine")
browser.init()
app = QApplication(["runekit", *qt_args])
app.setQuitOnLastWindowClosed(False)
app.setOrganizationName("cupco.de")
app.setOrganizationDomain("cupco.de")
app.setApplicationName("RuneKit")
signal.signal(signal.SIGINT, lambda no, frame: app.quit())
timer = QTimer()
timer.start(300)
timer.timeout.connect(lambda: None)
QSettings.setDefaultFormat(QSettings.IniFormat)
try:
game_manager = get_platform_manager()
host = Host(game_manager)
if app_url == "settings":
host.open_settings()
host.setting_dialog.setAttribute(Qt.WA_DeleteOnClose)
host.setting_dialog.destroyed.connect(app.quit)
elif app_url:
logging.info("Loading app")
game_app = host.launch_app_from_url(app_url)
game_app.window.destroyed.connect(app.quit)
else:
if not host.app_store.has_default_apps():
host.app_store.load_default_apps()
app.exec_()
sys.exit(0)
except Exception as e:
msg = QMessageBox(
QMessageBox.Critical,
"Oh No!",
f"Fatal error: \n\n{e.__class__.__name__}: {e}",
)
msg.setDetailedText(traceback.format_exc())
msg.exec_()
raise
finally:
if game_manager is not None:
logging.debug("Stopping game manager")
game_manager.stop()
if __name__ == "__main__":
main()
+41
View File
@@ -0,0 +1,41 @@
from typing import TYPE_CHECKING
from PySide2.QtCore import QRect
from PySide2.QtWidgets import QMainWindow
if TYPE_CHECKING:
from runekit.app import App
class GameSnapMixin(QMainWindow):
app: "App"
__last_game_pos: QRect
def snap_to_game(self):
rect = self.app.game_instance.get_position()
pos = rect.topLeft()
if pos.x() < 0:
pos.setX(0)
if pos.y() < 0:
pos.setY(0)
self.move(pos)
self.__last_game_pos = self.app.game_instance.get_position()
self.app.game_instance.positionChanged.connect(self._update_game_snap)
def _update_game_snap(self, game_pos: QRect):
# If the top left spot of us fall into game window, then we move with the game
tl = self.geometry().topLeft()
if not game_pos.contains(tl):
self.__last_game_pos = game_pos
return
dx = game_pos.x() - self.__last_game_pos.x()
dy = game_pos.y() - self.__last_game_pos.y()
pos = self.pos()
pos.setX(pos.x() + dx)
pos.setY(pos.y() + dy)
self.move(pos)
self.__last_game_pos = game_pos
+39 -16
View File
@@ -1,13 +1,11 @@
import abc
from pathlib import Path
from typing import Union
from typing import Union, Dict
from PySide2.QtGui import QIcon
from PySide2.QtCore import QObject, QSettings
from PySide2.QtWidgets import QSystemTrayIcon
from .tooltip import TooltipManager
TRAY_ICON = Path(__file__).parent / "trayicon.png"
from .tray import tray_icon
class Notifier(abc.ABC):
@@ -16,6 +14,11 @@ class Notifier(abc.ABC):
...
class StubNotifier(Notifier):
def notify(self, text):
pass
class TooltipNotifier(Notifier):
def __init__(self):
self.manager = TooltipManager()
@@ -25,26 +28,46 @@ class TooltipNotifier(Notifier):
class TrayIconNotifier(Notifier):
def __init__(self):
self.icon = QSystemTrayIcon(QIcon(str(TRAY_ICON)))
self.icon.show()
def notify(self, text):
if text:
self.icon.showMessage("RuneKit", text)
tray_icon().showMessage("RuneKit", text)
class AutoNotifier(Notifier):
class AutoNotifier(QObject):
notifier: Notifier
METHOD_STUB = 0
METHOD_NOTIFICATION = 1
METHOD_TOOLTIP = 2
def __init__(self):
super().__init__()
self.settings = QSettings(self)
type_ = int(self.settings.value("settings/tooltip", self.METHOD_NOTIFICATION))
if (
QSystemTrayIcon.isSystemTrayAvailable()
and QSystemTrayIcon.supportsMessages()
and type_ == self.METHOD_NOTIFICATION
):
self.notifier = TrayIconNotifier()
elif type_ == self.METHOD_TOOLTIP:
self.notifier = TooltipNotifier()
else:
self.notifier = StubNotifier()
def notify(self, text):
self.notifier.notify(text)
@classmethod
def availableMethods(cls) -> Dict[int, str]:
out = {
cls.METHOD_STUB: "Disabled",
cls.METHOD_TOOLTIP: "Cursor tooltip",
}
if (
QSystemTrayIcon.isSystemTrayAvailable()
and QSystemTrayIcon.supportsMessages()
):
self.notifier = TrayIconNotifier()
else:
self.notifier = TooltipNotifier()
out[cls.METHOD_NOTIFICATION] = "Notification"
def notify(self, text):
self.notifier.notify(text)
return out
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

+17
View File
@@ -0,0 +1,17 @@
{
"id": "default",
"name": "Default",
"url": "https://runeapps.org/nis/default/",
"borderTop": 4,
"borderLeft": 4,
"borderRight": 4,
"borderBottom": 4,
"addressHeight": 28,
"titleHeight": 36,
"fontColor": "192,192,192",
"titleColor": "192,192,192",
"addressBackColor": "76,78,84"
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+4 -4
View File
@@ -1,6 +1,6 @@
from typing import Union, Optional
from typing import Union
from PySide2.QtCore import Qt, QTimer, Slot, QSize
from PySide2.QtCore import Qt, QTimer, Slot
from PySide2.QtGui import QCursor
from PySide2.QtWidgets import QLabel, QMainWindow
@@ -15,8 +15,8 @@ class TooltipManager(QMainWindow):
def __init__(self):
super().__init__(
flags=Qt.Dialog
| Qt.X11BypassWindowManagerHint
flags=Qt.Popup
| Qt.FramelessWindowHint
| Qt.WindowStaysOnTopHint
| Qt.WindowTransparentForInput
)
+67
View File
@@ -0,0 +1,67 @@
from typing import TYPE_CHECKING
from PySide2.QtCore import Slot, Signal, QCoreApplication
from PySide2.QtGui import QIcon
from PySide2.QtWidgets import QSystemTrayIcon, QMenu
if TYPE_CHECKING:
from runekit.host import Host
class TrayIcon(QSystemTrayIcon):
on_settings = Signal()
host: "Host"
def __init__(self, host: "Host"):
global _tray_icon
super().__init__(QIcon(":/runekit/ui/trayicon.png"))
_tray_icon = self
self.host = host
self._setup_menu()
self.setContextMenu(self.menu)
def _setup_menu(self):
if not hasattr(self, "menu"):
self.menu = QMenu("RuneKit")
self.menu.clear()
self._setup_app_menu("", self.menu)
self.menu.addSeparator()
self.menu_settings = self.menu.addAction("Settings")
self.menu_settings.triggered.connect(self.on_settings)
self.menu.addAction(
QIcon.fromTheme("application-exit"),
"Exit",
lambda: QCoreApplication.instance().quit(),
)
def _setup_app_menu(self, path: str, menu: QMenu):
for app_id, manifest in self.host.app_store.list_app(path):
if manifest is None:
# Folder
submenu = menu.addMenu(QIcon.fromTheme("folder"), app_id)
self._setup_app_menu(app_id, submenu)
continue
app_menu = menu.addAction(manifest["appName"])
app_menu.triggered.connect(
lambda _=None, app_id=app_id: self.host.launch_app_id(app_id)
)
app_menu.setToolTip(manifest["description"])
icon = self.host.app_store.icon(app_id)
if icon:
app_menu.setIcon(icon)
@Slot()
def update_menu(self):
self._setup_menu()
def tray_icon() -> TrayIcon:
return _tray_icon
+241
View File
@@ -0,0 +1,241 @@
from PySide2.QtCore import Signal, QPoint
from PySide2.QtGui import QPixmap, Qt, QResizeEvent
from PySide2.QtWidgets import (
QWidget,
QHBoxLayout,
QVBoxLayout,
QLabel,
QSizePolicy,
QPushButton,
)
SKIN = ":/runekit/ui/skins/default/"
class WindowFrame(QWidget):
on_minimize = Signal()
on_settings = Signal()
on_exit = Signal()
content: QWidget
shade = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._layout()
def _layout(self):
self._inner = _FrameInner(parent=self)
self._buttons = _WindowButtons(parent=self)
self._buttons.on_minimize.connect(self.on_minimize)
self._buttons.on_minimize.connect(self.handle_shade)
self._buttons.on_settings.connect(self.on_settings)
self._buttons.on_exit.connect(self.on_exit)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setMargin(0)
self.setLayout(layout)
layout.addWidget(self._inner)
self._buttons.show()
def resizeEvent(self, evt: QResizeEvent):
buttons_size = self._buttons.sizeHint()
self._buttons.setGeometry(
evt.size().width() - buttons_size.width(),
0,
buttons_size.width(),
buttons_size.height(),
)
def set_content(self, content: QWidget):
self._inner.set_content(content)
self.content = content
def handle_shade(self):
self.set_shade(not self.shade)
def set_shade(self, shade: bool):
if shade == self.shade:
return
self.shade = shade
if self.shade:
self.previous_window_geom = self.window().size()
self.previous_window_min_size = self.window().minimumSize()
top_right = self.window().geometry().topRight()
self._inner.hide()
self.window().setMinimumSize(self._buttons.sizeHint())
size = self._buttons.sizeHint()
self.window().resize(size)
new_top_left = QPoint(top_right)
new_top_left.setX(top_right.x() - size.width())
self.window().move(new_top_left)
else:
pos = self.window().geometry().topRight()
self._inner.show()
if hasattr(self, "previous_window_geom"):
self.window().setMinimumSize(self.previous_window_min_size)
geom = self.previous_window_geom
else:
geom = self.window().sizeHint()
pos.setX(pos.x() - geom.width())
self.window().resize(geom)
self.window().move(pos)
class _FrameInner(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._layout()
# TODO: Set background
def _layout(self):
layout = QVBoxLayout()
self.setLayout(layout)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
top_row = QHBoxLayout()
layout.addLayout(top_row)
top_row.setContentsMargins(0, 0, 0, 0)
top_row.setSpacing(0)
top_row.addWidget(self.build_image("borderTL", Qt.TopEdge | Qt.LeftEdge))
top_row.addWidget(self.build_image("borderT", Qt.TopEdge, h_fix=False))
top_row.addWidget(self.build_image("borderTR", Qt.TopEdge | Qt.RightEdge))
mid_row = QHBoxLayout()
layout.addLayout(mid_row)
mid_row.setContentsMargins(0, 0, 0, 0)
mid_row.setSpacing(0)
mid_row.addWidget(self.build_image("borderL", Qt.LeftEdge, v_fix=False))
self.content = QWidget(self)
mid_row.addWidget(self.content, 1)
mid_row.addWidget(self.build_image("borderR", Qt.RightEdge, v_fix=False))
bot_row = QHBoxLayout()
layout.addLayout(bot_row)
bot_row.setContentsMargins(0, 0, 0, 0)
bot_row.setSpacing(0)
bot_row.addWidget(
self.build_image(
"borderBL",
Qt.BottomEdge | Qt.LeftEdge,
)
)
bot_row.addWidget(self.build_image("borderB", Qt.BottomEdge, h_fix=False))
bot_row.addWidget(self.build_image("borderBR", Qt.BottomEdge | Qt.RightEdge))
def build_image(self, name: str, edge=None, h_fix=True, v_fix=True):
pixmap = QPixmap(SKIN + name)
label = _ResizeHandle(edge, parent=self)
label.setSizePolicy(
QSizePolicy.Fixed if h_fix else QSizePolicy.Preferred,
QSizePolicy.Fixed if v_fix else QSizePolicy.Preferred,
)
label.setScaledContents(True)
label.setPixmap(pixmap)
return label
def set_content(self, content: QWidget):
self.layout().replaceWidget(self.content, content)
self.content = content
class _WindowButtons(QWidget):
on_minimize = Signal()
on_settings = Signal()
on_exit = Signal()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._layout()
def _layout(self):
layout = QHBoxLayout()
self.setLayout(layout)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addStretch(1)
drag_area = _DragHandle(self)
layout.addWidget(drag_area, alignment=Qt.AlignTop)
minimize_button = QPushButton(self)
minimize_button.clicked.connect(self.on_minimize)
minimize_button.setStyleSheet(
self.button_style("minimize.png", "minimizeHover.png")
)
layout.addWidget(minimize_button, alignment=Qt.AlignTop)
# settings_button = QPushButton(self)
# settings_button.clicked.connect(self.on_settings)
# settings_button.setStyleSheet(
# self.button_style("settings.png", "settingsHover.png")
# )
# layout.addWidget(settings_button, alignment=Qt.AlignTop)
exit_button = QPushButton(self)
exit_button.clicked.connect(self.on_exit)
exit_button.setStyleSheet(self.button_style("exit.png", "exitHover.png"))
layout.addWidget(exit_button, alignment=Qt.AlignTop)
def button_style(self, normal, hover):
return f"""
QPushButton {{
image: url({SKIN}{normal});
border: none;
padding: 0;
margin: 0;
width: 12px;
height: 12px;
}}
QPushButton::hover {{
image: url({SKIN}{hover});
}}
"""
class _DragHandle(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.setPixmap(QPixmap(SKIN + "borderDrag.png"))
self.setCursor(Qt.OpenHandCursor)
def mousePressEvent(self, ev):
self.window().windowHandle().startSystemMove()
class _ResizeHandle(QLabel):
def __init__(self, edge, *args, **kwargs):
super().__init__(*args, **kwargs)
self.edge = edge
if edge == Qt.TopEdge & Qt.LeftEdge:
self.setCursor(Qt.SizeFDiagCursor)
elif edge == Qt.TopEdge:
self.setCursor(Qt.SizeVerCursor)
elif edge == Qt.TopEdge & Qt.RightEdge:
self.setCursor(Qt.SizeBDiagCursor)
elif edge == Qt.LeftEdge:
self.setCursor(Qt.SizeHorCursor)
elif edge == Qt.RightEdge:
self.setCursor(Qt.SizeHorCursor)
elif edge == Qt.BottomEdge & Qt.LeftEdge:
self.setCursor(Qt.SizeBDiagCursor)
elif edge == Qt.BottomEdge:
self.setCursor(Qt.SizeVerCursor)
elif edge == Qt.BottomEdge & Qt.RightEdge:
self.setCursor(Qt.SizeFDiagCursor)
def mousePressEvent(self, ev):
if self.edge:
self.window().windowHandle().startSystemResize(self.edge)