Author SHA1 Message Date
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
Manatsawin Hanmongkolchai 2f395012a8 X11 port to xcb and composite screen grab
Now we get window capture, but I broke everything and the code quality too
2021-01-29 00:21:22 +07:00
Manatsawin Hanmongkolchai 6a30000e24 Update compatibility 2021-01-28 23:01:41 +07:00
107 changed files with 2492 additions and 1155 deletions

No files matched your search

+5 -2
View File
@@ -1,6 +1,9 @@
build/
dist/
runekit/image/*.c
runekit/image/*.*.*
runekit/image/format.c
runekit/image/format.*.*
setup.py
*.egg-info
__pycache__
runekit/_resources.py
RuneKitApp.spec
+17
View File
@@ -0,0 +1,17 @@
dev: runekit/_resources.py
dist/runekit.tar.gz: main.py poetry.lock $(wildcard runekit/**/*)
poetry build -f sdist
cd dist; cp runekit-*.tar.gz runekit.tar.gz
runekit/_resources.py: resources.qrc $(wildcard runekit/**/*.js) $(wildcard runekit/**/*.png)
pyside2-rcc $< -o $@
dist/RuneKitApp.app: main.py poetry.lock $(wildcard runekit/**/*)
pyinstaller -w -n RuneKitApp \
--exclude-module tkinter \
-s -d noarchive \
--osx-bundle-identifier de.cupco.runekit \
$<
.PHONY: dev
+87 -42
View File
@@ -4,74 +4,119 @@
Alt1-compatible toolbox for RuneScape 3, for Linux and macOS.
**Alpha quality software**
## What works
**Overlay are not supported yet**
| App | Linux | macOS |
| ------------------------------------------------------------------------------- | ------ | ------ |
| **Clue Solver** | &nbsp; | &nbsp; |
| - Text/Emote | ✅ | ✅ |
| - Map scroll | ✅ | ? |
| - Compass | ✅ | ? |
| - Scan | ✅ | ? |
| - Celtic knot | ✅ | ✅ |
| - Slide | ✅ | ✅ |
| - Lockbox | ✅ | ? |
| - Towers | ? | ? |
| Example app | ✅ | ✅ |
| **AfkWarden** | &nbsp; | &nbsp; |
| - Inactive | 1 | ✅ |
| - XP Counter | ❌ | ❌ |
| - Chatbox | ✅ | ✅ |
| - Crafting menu | ✅ | ✅ |
| - Buffs | ✅ | ✅ |
| - Action bar stats | ✅ | ✅ |
| - Dialog box | ✅ | ✅ |
| - Target death | ? | ? |
| - Sheathe stance | ? | ? |
| - Castle Wars | ? | ? |
| - Fight kiln waves | ? | ? |
| - Target death | ? | ? |
| - Item drops | ? | ? |
| **DgKey** | &nbsp; | &nbsp; |
| - Show map | ✅ | ? |
| - Track key | ✅ | ? |
| - Select player location | ❌ | ? |
| **[ArchMatCounter](https://zerogwafa.github.io/ArchMatCounter/appconfig.json)** | ? | ? |
| **Bank price checker** | ✅ | ? |
App | Linux | macOS
-------------------------------|-------|----------
Clue solver (Text/Emote) | ✅ | ✅
Clue solver (Map scroll) | ? | ?
Clue solver (Compass) | ? | 2
Clue solver (Scan) | ? | ?
Clue solver (Lockbox) | ? | ?
Clue solver (Celtic Knot) | ? | ✅
Clue solver (Slide) | ? | ✅
Clue solver (Towers) | ? | ?
Example app | ✅ | ✅
AfkWarden (Inactive) | ❌ | ✅
AfkWarden (XP Counter) | ❌ | ❌
AfkWarden (Chatbox) | 1 | ✅
AfkWarden (Crafting menu) | ✅ | ✅
AfkWarden (Buffs) | ✅ | ✅
AfkWarden (Action bar stats) | ✅ | ✅
AfkWarden (Dialog box) | ✅ | ✅
AfkWarden (Target death) | ? | ?
AfkWarden (Sheathe stance) | ? | ?
AfkWarden (Castle Wars) | ? | ?
AfkWarden (Fight kiln waves) | ? | ?
AfkWarden (Target death) | ? | ?
AfkWarden (Item drops) | ? | ?
- 1: Only keyboard activity count as active
- 1: Can be setup but only fire once (needs inactive to work)
- 2: Detected but cannot place mark
## Installation setup
These instructions are for those who have little/no programming experience and prepare your environment to run the instructions in the following section `Running`.
- [macOS](./docs/macos-setup.md)
- [video guide](https://youtu.be/X03SyyV9Bro) from PupRS
## Running
This project use [Poetry](https://python-poetry.org) as package manager.
Requires Poetry 1.1
Requires Poetry 1.1.
```sh
# Try this first
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
```
WIP Instruction:
### Linux additional instruction
- Run RuneScape before starting!!
- Linux: Game window MUST be ENTIRELY visible. (No minimize, no other window on top, including RuneKit)
- macOS: HiDPI (Retina) support may or may not work. Use external monitor for best result
- macOS: You will need to add Python in System Preferences > Security > Privacy in these sections:
- Accessibility
- Screen Recording:
- Note that it wmight appear as the closest macOS application (eg. your terminal emulator) instead of Python
Requires libxcb to be installed
### macOS additional instruction
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
## Developer
Start with `--remote-debugging-port=9222` to enable remote debugger protocol.
To debug, go to `chrome://inspect` on Chrome/Chromium.
## Technical Features
- [Alt1 Compatibility](compatibility.md)
| Platform Feature | Linux | macOS |
| ----------------------- | ------ | ------ |
| **Game Manager** | &nbsp; | &nbsp; |
| Instance change signals | ❌ | ❌ |
| **Instance** | &nbsp; | &nbsp; |
| alt1_pressed | 1 | ✅ |
| game_activity | 2 | ✅ |
| positionChanged | ✅ | ✅ |
| scalingChanged | ❌ | ❌ |
| focusChanged | ✅ | ✅ |
| set_taskbar_progress | ❌ | ❌ |
| Window capture | ✅ | ✅ |
- 1: Works, but the game also receive the key
- 2: Only detect keyboard activity
## Known issues
- On macOS some values include the size of window decorator, including the screenshot
- This can be tested with AfkScape color picker
## 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.
+2 -8
View File
@@ -2,9 +2,7 @@ from Cython.Build import cythonize
# use cythonize to build the extensions
modules = [
"runekit/image/_utils.pyx",
"runekit/image/_chat.pyx",
"runekit/image/_font.pyx",
# "runekit/image/format.pyx",
]
extensions = cythonize(modules)
@@ -13,8 +11,4 @@ extensions = cythonize(modules)
def build(setup_kwargs):
"""Needed for the poetry building interface."""
setup_kwargs.update(
{
"ext_modules": extensions,
}
)
setup_kwargs.update({"ext_modules": extensions, "scripts": ["main.py"]})
+62
View File
@@ -0,0 +1,62 @@
# Alt1 API
- [ ] alt1.overlay
- [ ] alt1.captureMethod
- [ ] alt1.permission*
- [x] alt1.lastWorldHop
- [x] alt1.currentWorld
- [ ] alt1.rsPing
- [ ] alt1.rsFps
- [ ] alt1.openInfo
- [x] alt1.screen*
- [x] alt1.captureInterval
- [x] alt1.mousePosition
- [x] alt1.rsLinked
- [x] alt1.rsWidth, rsHeight, rsScaling
- [x] alt1.rsActive
- [x] alt1.userResize
- [ ] alt1.identiyAppUrl
- [ ] alt1.openBrowser
- [ ] alt1.clearBinds
- [ ] alt1.registerStatusDaemon
- [ ] alt1.getStatusDaemonState
- [ ] alt1.showNotification
- [ ] alt1.closeApp
- [x] alt1.setTooltip
- [x] alt1.clearTooltip
- [x] alt1.setTaskbarProgress
- [ ] alt1.setTitleBarText - Implemented but not validated
- [x] alt1.overLayRect
- [x] alt1.overLayText
- [x] alt1.overLayTextEx
- [ ] alt1.overLayLine
- [ ] 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
- [ ] alt1.bindScreenRegion
- [x] alt1.bindGetRegion
- [ ] alt1.bindReadString
- [ ] alt1.bindReadColorString
- [ ] alt1.bindReadStringEx
- [ ] alt1.bindReadRightClickString
- [ ] alt1.bindGetPixel
- [ ] alt1.bindFindSubImg
# Alt1 Events
- [x] alt1pressed
- [ ] menudetected
- [ ] permissionchanged
- [ ] daemonrun
- [ ] userevent
- [x] rsfocus
- [x] blur
rslinked, rsunlinked probably not going to be implemented
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

+67
View File
@@ -0,0 +1,67 @@
# MacOS Setup (for programming novices)
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).
+67 -17
View File
@@ -1,11 +1,17 @@
import logging
import sys
import traceback
import click
from PySide2.QtWebEngine import QtWebEngine
from PySide2.QtWidgets import QApplication
from PySide2.QtWidgets import (
QApplication,
QMessageBox,
QInputDialog,
QDialog,
)
from runekit.browser import init
import runekit._resources
from runekit import browser
from runekit.game import get_platform_manager
from runekit.host import Host
@@ -17,27 +23,71 @@ from runekit.host import Host
)
@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")
@click.argument("app_url", required=False)
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()
browser.init()
app = QApplication(["runekit", *qt_args])
app.setQuitOnLastWindowClosed(False)
game_manager = None
try:
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]
if app_url == "settings":
settings_wnd = host.open_settings()
settings_wnd.destroyed.connect(app.quit)
else:
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))
if len(game_instances) == 0:
QMessageBox(
QMessageBox.Critical,
"Game not found",
"Cannot find RuneScape. Launch the game first",
).exec_()
return
logging.info("Loading app")
host.start_app(app_url, game)
game = game_instances[game_index]
app.exec_()
sys.exit(0)
if app_url is None:
dialog = QInputDialog()
dialog.setComboBoxEditable(True)
dialog.setComboBoxItems(
[
"https://runeapps.org/apps/alt1/example/appconfig.json",
"https://runeapps.org/apps/clue/appconfig.json",
"https://runeapps.org/apps/alt1/afkscape/appconfig.json",
]
)
dialog.setLabelText("Enter appconfig URL")
dialog.setWindowTitle("RuneKit")
if dialog.exec_() == QDialog.DialogCode.Rejected:
return
app_url = dialog.textValue()
logging.info("Loading app")
game_app = host.start_app(app_url, game)
game_app.window.destroyed.connect(app.quit)
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:
game_manager.stop()
if __name__ == "__main__":
Generated
+535 -13
View File
@@ -1,3 +1,11 @@
[[package]]
name = "altgraph"
version = "0.17"
description = "Python graph (network) package"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "appdirs"
version = "1.4.4"
@@ -6,6 +14,28 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "arrow"
version = "0.17.0"
description = "Better dates & times for Python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
python-dateutil = ">=2.7.0"
[[package]]
name = "binaryornot"
version = "0.4.4"
description = "Ultra-lightweight pure Python package to check if a file is binary or text."
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
chardet = ">=3.0.2"
[[package]]
name = "black"
version = "20.8b1"
@@ -28,6 +58,22 @@ typing-extensions = ">=3.7.4"
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]]
name = "briefcase"
version = "0.3.4"
description = "Tools to support converting a Python project into a standalone native application."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
cookiecutter = ">=1.0"
dmgbuild = {version = ">=1.3.3", markers = "sys_platform == \"darwin\""}
GitPython = ">=3.0.8"
Jinja2 = "<3.0"
requests = ">=2.22.0"
toml = ">=0.10.0"
[[package]]
name = "certifi"
version = "2020.12.5"
@@ -36,6 +82,17 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "cffi"
version = "1.14.4"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pycparser = "*"
[[package]]
name = "chardet"
version = "4.0.0"
@@ -52,6 +109,25 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "cookiecutter"
version = "1.7.2"
description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
binaryornot = ">=0.4.4"
click = ">=7.0"
Jinja2 = "<3.0.0"
jinja2-time = ">=0.2.0"
MarkupSafe = "<2.0.0"
poyo = ">=0.5.0"
python-slugify = ">=4.0.0"
requests = ">=2.23.0"
six = ">=1.10"
[[package]]
name = "cython"
version = "0.29.21"
@@ -60,6 +136,59 @@ category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "dmgbuild"
version = "1.4.2"
description = "macOS command line utility to build disk images"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
ds_store = ">=1.1.0"
mac_alias = ">=2.0.1"
[[package]]
name = "ds-store"
version = "1.3.0"
description = "Manipulate Finder .DS_Store files from Python"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
mac_alias = ">=2.0.1"
[[package]]
name = "future"
version = "0.18.2"
description = "Clean single-source support for Python 3 and 2"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "gitdb"
version = "4.0.5"
description = "Git Object Database"
category = "dev"
optional = false
python-versions = ">=3.4"
[package.dependencies]
smmap = ">=3.0.1,<4"
[[package]]
name = "gitpython"
version = "3.1.12"
description = "Python Git Library"
category = "dev"
optional = false
python-versions = ">=3.4"
[package.dependencies]
gitdb = ">=4.0.1,<5"
[[package]]
name = "idna"
version = "2.10"
@@ -68,6 +197,59 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "jinja2"
version = "2.11.3"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
MarkupSafe = ">=0.23"
[package.extras]
i18n = ["Babel (>=0.8)"]
[[package]]
name = "jinja2-time"
version = "0.2.0"
description = "Jinja2 Extension for Dates and Times"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
arrow = "*"
jinja2 = "*"
[[package]]
name = "mac-alias"
version = "2.2.0"
description = "Generate/parse Mac OS Alias records from Python"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "macholib"
version = "1.14"
description = "Mach-O header analysis and editing"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
altgraph = ">=0.15"
[[package]]
name = "markupsafe"
version = "1.1.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
@@ -84,6 +266,17 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pefile"
version = "2019.4.18"
description = "Python PE parsing module"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
future = "*"
[[package]]
name = "pillow"
version = "8.1.0"
@@ -92,6 +285,60 @@ category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "poyo"
version = "0.5.0"
description = "A lightweight YAML Parser for Python. 🐓"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[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"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyinstaller"
version = "4.2"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2020.6"
pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
[package.extras]
encryption = ["tinyaes (>=1.0.0)"]
hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2020.11"
description = "Community maintained hooks for PyInstaller"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "pyobjc-core"
version = "7.1"
@@ -161,15 +408,37 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.10"
shiboken2 = "5.15.2"
[[package]]
name = "python-xlib"
version = "0.29"
description = "Python X Library"
category = "main"
name = "python-dateutil"
version = "2.8.1"
description = "Extensions to the standard Python datetime module"
category = "dev"
optional = false
python-versions = "*"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
[package.dependencies]
six = ">=1.10.0"
six = ">=1.5"
[[package]]
name = "python-slugify"
version = "4.0.1"
description = "A Python Slugify application that handles Unicode"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
text-unidecode = ">=1.3"
[package.extras]
unidecode = ["Unidecode (>=1.1.1)"]
[[package]]
name = "pywin32-ctypes"
version = "0.2.0"
description = ""
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "regex"
@@ -213,6 +482,30 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "smmap"
version = "3.0.5"
description = "A pure Python implementation of a sliding window memory map manager"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "sysv-ipc"
version = "1.1.0"
description = "System V IPC primitives (semaphores, shared memory and message queues) for Python"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "text-unidecode"
version = "1.3"
description = "The most basic Text::Unidecode port"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "toml"
version = "0.10.2"
@@ -239,7 +532,7 @@ python-versions = "*"
[[package]]
name = "urllib3"
version = "1.26.2"
version = "1.26.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
@@ -250,23 +543,89 @@ brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "xcffib"
version = "0.11.1"
description = "A drop in replacement for xpyb, an XCB python binding"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
cffi = ">=1.1.0"
six = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.9,<3.10"
content-hash = "7499ffeaf18341e92d2e7b1af5f9b0a21be4b702f988fad58b589708499aed06"
content-hash = "a672d8acb153cf90111eb266cf7a9e3dbc747e09dea5f19e4ee0367faa18f9c8"
[metadata.files]
altgraph = [
{file = "altgraph-0.17-py2.py3-none-any.whl", hash = "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe"},
{file = "altgraph-0.17.tar.gz", hash = "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa"},
]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
arrow = [
{file = "arrow-0.17.0-py2.py3-none-any.whl", hash = "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5"},
{file = "arrow-0.17.0.tar.gz", hash = "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"},
]
binaryornot = [
{file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"},
{file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"},
]
black = [
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
]
briefcase = [
{file = "briefcase-0.3.4-py2.py3-none-any.whl", hash = "sha256:241e01d4d3c73df610a8728cb709caef61033884e62cb017494a296b21f4a120"},
{file = "briefcase-0.3.4.tar.gz", hash = "sha256:c1bfebf97ea0f11a6bb6ca37266b0e22ef78bf53750b82f1b6ac8a86faf46731"},
]
certifi = [
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
]
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"},
]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
@@ -275,6 +634,10 @@ click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
]
cookiecutter = [
{file = "cookiecutter-1.7.2-py2.py3-none-any.whl", hash = "sha256:430eb882d028afb6102c084bab6cf41f6559a77ce9b18dc6802e3bc0cc5f4a30"},
{file = "cookiecutter-1.7.2.tar.gz", hash = "sha256:efb6b2d4780feda8908a873e38f0e61778c23f6a2ea58215723bcceb5b515dac"},
]
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"},
@@ -312,10 +675,96 @@ cython = [
{file = "Cython-0.29.21-py2.py3-none-any.whl", hash = "sha256:5c4276fdcbccdf1e3c1756c7aeb8395e9a36874fa4d30860e7694f43d325ae13"},
{file = "Cython-0.29.21.tar.gz", hash = "sha256:e57acb89bd55943c8d8bf813763d20b9099cc7165c0f16b707631a7654be9cad"},
]
dmgbuild = [
{file = "dmgbuild-1.4.2.tar.gz", hash = "sha256:1b03eaa229128c03a4da71ff97a4efa3ac008f04ddbe70b51ef797a21b73857c"},
]
ds-store = [
{file = "ds_store-1.3.0.tar.gz", hash = "sha256:e52478f258626600c1f53fc18c1ddcd8542fa0bca41d4bd81d57c04c87aabf24"},
]
future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
gitdb = [
{file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"},
{file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"},
]
gitpython = [
{file = "GitPython-3.1.12-py3-none-any.whl", hash = "sha256:867ec3dfb126aac0f8296b19fb63b8c4a399f32b4b6fafe84c4b10af5fa9f7b5"},
{file = "GitPython-3.1.12.tar.gz", hash = "sha256:42dbefd8d9e2576c496ed0059f3103dcef7125b9ce16f9d5f9c834aed44a1dac"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
jinja2 = [
{file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
{file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
]
jinja2-time = [
{file = "jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40"},
{file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"},
]
mac-alias = [
{file = "mac_alias-2.2.0.tar.gz", hash = "sha256:0eb84a63f98bf54c2f9fbdc4de956a63e64eb8a4a124143a1c1f5a78326442f0"},
]
macholib = [
{file = "macholib-1.14-py2.py3-none-any.whl", hash = "sha256:c500f02867515e6c60a27875b408920d18332ddf96b4035ef03beddd782d4281"},
{file = "macholib-1.14.tar.gz", hash = "sha256:0c436bc847e7b1d9bda0560351bf76d7caf930fb585a828d13608839ef42c432"},
]
markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
{file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"},
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"},
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"},
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"},
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"},
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"},
{file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"},
{file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"},
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
]
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"},
@@ -324,6 +773,9 @@ pathspec = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
]
pefile = [
{file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"},
]
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"},
@@ -358,6 +810,51 @@ pillow = [
{file = "Pillow-8.1.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5"},
{file = "Pillow-8.1.0.tar.gz", hash = "sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba"},
]
poyo = [
{file = "poyo-0.5.0-py2.py3-none-any.whl", hash = "sha256:3e2ca8e33fdc3c411cd101ca395668395dd5dc7ac775b8e809e3def9f9fe041a"},
{file = "poyo-0.5.0.tar.gz", hash = "sha256:e26956aa780c45f011ca9886f044590e2d8fd8b61db7b1c1cf4e0869f48ed4dd"},
]
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.2.tar.gz", hash = "sha256:f5c0eeb2aa663cce9a5404292c0195011fa500a6501c873a466b2e8cad3c950c"},
]
pyinstaller-hooks-contrib = [
{file = "pyinstaller-hooks-contrib-2020.11.tar.gz", hash = "sha256:fc3290a2ca337d1d58c579c223201360bfe74caed6454eaf5a2550b77dbda45c"},
{file = "pyinstaller_hooks_contrib-2020.11-py2.py3-none-any.whl", hash = "sha256:fa8280b79d8a2b267a2e43ff44f73b3e4a68fc8d205b8d34e8e06c960f7c2fcf"},
]
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"},
@@ -402,9 +899,16 @@ pyside2 = [
{file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win32.whl", hash = "sha256:087a0b719bb967405ea85fd202757c761f1fc73d0e2397bc3a6a15376782ee75"},
{file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:1316aa22dd330df096daf7b0defe9c00297a66e0b4907f057aaa3e88c53d1aff"},
]
python-xlib = [
{file = "python-xlib-0.29.tar.gz", hash = "sha256:e4bcb756f4aa05be7b82ee21de0ba04d73414018727b42ebd9fbcf409ea75d13"},
{file = "python_xlib-0.29-py2.py3-none-any.whl", hash = "sha256:044095d1b1a5eab5a79f8d0b66f811a9ac6acd038dd3bae00cb3dbe90b32a7e3"},
python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
{file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
]
python-slugify = [
{file = "python-slugify-4.0.1.tar.gz", hash = "sha256:69a517766e00c1268e5bbfc0d010a0a8508de0b18d30ad5a1ff357f8ae724270"},
]
pywin32-ctypes = [
{file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
{file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
]
regex = [
{file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
@@ -465,6 +969,21 @@ six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
]
smmap = [
{file = "smmap-3.0.5-py2.py3-none-any.whl", hash = "sha256:7bfcf367828031dc893530a29cb35eb8c8f2d7c8f2d0989354d75d24c8573714"},
{file = "smmap-3.0.5.tar.gz", hash = "sha256:84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50"},
]
sysv-ipc = [
{file = "sysv_ipc-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ff6e3c98d8aa3e1ae5378d86869c4699c76f94318705d08085c97fb010770a31"},
{file = "sysv_ipc-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:30611dccecd8fa0dc9f83d4d465e5047692df0ec9a095abef8b90ccc64afbdaf"},
{file = "sysv_ipc-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4bbbe4e5ad5ab859e6d21fc8ff3e3b01082b959fa5c2cd76289dbcd56350c5f9"},
{file = "sysv_ipc-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:61190b934b0277f7a189092dcabceee8d9736fd73de90fd9b50ea69f0bac4723"},
{file = "sysv_ipc-1.1.0.tar.gz", hash = "sha256:0f063cbd36ec232032e425769ebc871f195a7d183b9af32f9901589ea7129ac3"},
]
text-unidecode = [
{file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@@ -507,6 +1026,9 @@ typing-extensions = [
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
]
urllib3 = [
{file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"},
{file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"},
{file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"},
{file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"},
]
xcffib = [
{file = "xcffib-0.11.1.tar.gz", hash = "sha256:12949cfe2e68c806efd57596bb9bf3c151f399d4b53e15d1101b2e9baaa66f5a"},
]
+5 -1
View File
@@ -10,15 +10,19 @@ python = "^3.9,<3.10"
requests = "^2.25.1"
PySide2 = "^5.15.2"
Pillow = "^8.1.0"
python-xlib = [{ version="^0.29", platform="linux" }]
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"
psutil = "^5.8.0"
[tool.poetry.dev-dependencies]
Cython = "^0.29.21"
black = "^20.8b1"
briefcase = "^0.3.4"
pyinstaller = "^4.2"
[tool.poetry.build]
script = "build.py"
+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>
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# Used for Platypus boot
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
./RuneKitApp/RuneKitApp
-16
View File
@@ -25,19 +25,3 @@ class RequestHandler(typing.TypedDict):
handlerName: str
handlerUrl: str
handlerScript: str
UnblendModes = typing.Literal["raw", "removebg", "blackbg"]
class FontMeta(typing.TypedDict):
basey: int
spacewidth: int
treshold: float
color: typing.Tuple[int, int, int]
unblendmode: UnblendModes
shadow: bool
chars: str
seconds: str
img: typing.Optional[str]
bonus: typing.Optional[typing.Dict[str, int]]
+2
View File
@@ -27,6 +27,8 @@ class App:
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:
+25 -5
View File
@@ -1,11 +1,12 @@
import typing
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, Signal, QEvent
from PySide2.QtGui import QIcon, QCloseEvent
from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PySide2.QtWidgets import QMainWindow
from runekit.browser import Alt1WebChannel
from runekit.ui.windowframe import WindowFrame
if TYPE_CHECKING:
from runekit.app.app import App
@@ -14,17 +15,28 @@ if TYPE_CHECKING:
class BrowserWindow(QMainWindow):
app: "App"
browser: QWebEngineView
framed = False
def __init__(self, app: "App", **kwargs):
super().__init__(**kwargs)
self.app = app
if self.framed:
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)
if hasattr(self, "frame"):
self.frame.set_content(self.browser)
else:
self.setCentralWidget(self.browser)
page = QWebEnginePage(self.app.get_web_profile(), self.browser)
page.setWebChannel(Alt1WebChannel(app=self.app, parent=self.browser))
@@ -50,8 +62,16 @@ 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()
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()
+10 -8
View File
@@ -1,5 +1,6 @@
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
@@ -13,15 +14,16 @@ class AppWindow(BrowserWindow):
logger: logging.Logger
app_icon: Optional[QIcon]
framed = sys.platform != "darwin"
def __init__(self, **kwargs):
# 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
if self.framed:
flags |= Qt.CustomizeWindowHint
super().__init__(flags=flags, **kwargs)
self.pool = QThreadPool(parent=self)
self.setWindowTitle(self.app.manifest["appName"])
+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()
+77 -27
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,7 +34,7 @@
});
};
if (typeof window.qt !== 'undefined') {
setup()
setup();
} else {
document.addEventListener('DOMContentLoaded', setup);
}
@@ -50,11 +64,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 +78,6 @@
permissionOverlay: true,
permissionPixel: true,
lastWorldHop: 0,
currentWorld: -1,
rsPing: 0,
rsFps: 60,
openInfo: '{}',
@@ -100,7 +114,7 @@
},
get captureInterval() {
if(!syncChan) return 300;
if(!syncChan) return 1000;
return api.captureInterval;
},
@@ -147,6 +161,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);
},
@@ -201,38 +220,65 @@
},
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;
},
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 +314,9 @@
// },
// bindFindSubImg(id, imgstr, imgwidth, x, y, w, h) {
// return '';
// }
// },
addOCRFont(){
},
};
channel.then(function(chan){
@@ -287,24 +335,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);
+110 -44
View File
@@ -1,12 +1,9 @@
import base64
import json
import logging
import time
import secrets
from typing import TYPE_CHECKING, Dict, Callable, List
from urllib.parse import urljoin
from PIL import Image
from PySide2.QtCore import (
QObject,
Slot,
@@ -24,45 +21,16 @@ 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,
)
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 Alt1Api(QObject):
app: "App"
@@ -75,11 +43,13 @@ class Alt1Api(QObject):
_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.rpc_funcs = {
"getRegion": self.get_region,
@@ -92,6 +62,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 +84,7 @@ class Alt1Api(QObject):
self.screen_update_signal.emit()
screen_update_signal = Signal()
# endregion
# region Qt Properties
@@ -148,9 +120,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 +175,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,7 +188,7 @@ 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 image_to_stream(self.app.game_instance.grab_region(x, y, w, h))
def bind_region(self, x, y, w, h):
if not self.app.has_permission("pixel"):
@@ -233,7 +211,7 @@ class Alt1Api(QObject):
print("no img index %d" % id)
return ""
return _image_to_stream(image, x, y, w, h)
return image_to_stream(image, x, y, w, h)
# endregion
@@ -268,6 +246,87 @@ 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, 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
@@ -392,7 +451,14 @@ class RuneKitSchemeHandler(QWebEngineUrlSchemeHandler):
processor = RuneKitRequestProcess(self, req, parent=req)
processor.signals.successSignal.connect(self.on_success)
self.thread_pool.start(processor)
if "overLay" in req.requestUrl().path():
# FIXME: RuneKitRequestProcess should be able to notify main thread that
# this should run in main thread
# Overlay functions require main thread
processor.run()
else:
self.thread_pool.start(processor)
@Slot(QWebEngineUrlRequestJob, bytes, bytes)
def on_success(self, request, content_type, reply):
+249
View File
@@ -0,0 +1,249 @@
import collections
import functools
import logging
from typing import List, Dict, Tuple, Optional, TYPE_CHECKING
from PySide2.QtCore import QObject, QTimer, Qt
from PySide2.QtGui import QFont, QPen
from PySide2.QtWidgets import (
QGraphicsItem,
QGraphicsDropShadowEffect,
QGraphicsTextItem,
QGraphicsRectItem,
QGraphicsItemGroup,
QGraphicsLineItem,
)
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))
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_set_group_z(self, name: str, z_index: int):
for item in self.groups.get(name, []):
item.setZValue(z_index)
+13 -19
View File
@@ -1,5 +1,4 @@
import secrets
from pathlib import Path
from typing import TYPE_CHECKING
from PySide2.QtCore import QFile, QIODevice
@@ -11,13 +10,6 @@ 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
@@ -25,7 +17,7 @@ class WebProfile(QWebEngineProfile):
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()
@@ -42,23 +34,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)
+48
View File
@@ -0,0 +1,48 @@
import base64
from PIL import Image
from PySide2.QtGui import QColor
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
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)
+23 -8
View File
@@ -1,18 +1,20 @@
import abc
import dataclasses
import time
from typing import TYPE_CHECKING
from PySide2.QtCore import QObject, Signal, Property, QRect, Slot
from typing import TYPE_CHECKING, Literal, Optional
from PIL import Image
from PySide2.QtCore import QObject, Signal, Property, QRect, Slot
from PySide2.QtGui import QWindow
from PySide2.QtWidgets import QGraphicsItem
if TYPE_CHECKING:
from .manager import GameManager
PROGRESS_TYPE = Literal["RESET", "IN_PROGRESS", "ERROR", "LOADING", "PAUSED"]
class GameInstance(QObject):
refresh_rate = 300
refresh_rate = 1000
manager: "GameManager"
_last_game_activity: float = 0
@@ -34,6 +36,7 @@ class GameInstance(QObject):
@abc.abstractmethod
def get_position(self) -> QRect:
"""Return the game window position, without space used by window decoration"""
...
positionChanged = Signal(QRect)
@@ -53,6 +56,13 @@ class GameInstance(QObject):
focusChanged = Signal(bool)
focused = Property(bool, is_focused, notify=focusChanged)
@abc.abstractmethod
def get_world(self) -> Optional[int]:
...
worldChanged = Signal(int)
world = Property(int, get_world, notify=worldChanged)
@abc.abstractmethod
def grab_game(self) -> Image:
...
@@ -65,6 +75,11 @@ class GameInstance(QObject):
image = self.grab_game()
return image.crop((x, y, x + w, y + h))
@abc.abstractmethod
def set_taskbar_progress(self, type, progress):
...
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
+4
View File
@@ -16,3 +16,7 @@ class GameManager(QObject):
instance_removed = Signal()
instance_changed = Signal()
instances = Property(list, get_instances, notify=instance_changed)
def stop(self):
"""Stop the GameManager and any GameInstances"""
pass
+90
View File
@@ -0,0 +1,90 @@
from typing import TYPE_CHECKING, Callable, Tuple, Dict
from PySide2.QtCore import Qt, QRect
from PySide2.QtGui import QGuiApplication, QPen
from PySide2.QtWidgets import (
QMainWindow,
QGraphicsView,
QGraphicsScene,
QGraphicsItem,
QGraphicsRectItem,
)
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.setAttribute(Qt.WA_NoSystemBackground, True)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setAttribute(Qt.WA_DeleteOnClose, True)
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())
+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]
+5
View File
@@ -44,3 +44,8 @@ class QtGrabMixin(QtBaseMixin):
pixmap.save("/tmp/qtshot.bmp", None, 100)
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 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")
+37 -9
View File
@@ -1,12 +1,15 @@
import time
from functools import reduce
from typing import List, Dict, Optional
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,19 @@ class QuartzGameManager(GameManager):
Quartz.CFRunLoopGetCurrent(), source, Quartz.kCFRunLoopCommonModes
)
def _setup_overlay(self):
self.overlay = DesktopWideOverlay()
# Seems like QGraphicsView has a delay before applying stylesheet
# Put some delay to allow it to initialize and not flash
QTimer.singleShot(1000, lambda: self.overlay.show())
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
@@ -78,8 +108,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 +125,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)
+144 -14
View File
@@ -1,32 +1,99 @@
from typing import TYPE_CHECKING
import logging
import time
from typing import TYPE_CHECKING, List, Union
from PySide2.QtCore import QRect
import xcffib.composite
import xcffib.xproto
from PySide2.QtCore import QRect, Signal, Slot
from PySide2.QtGui import QWindow, QGuiApplication
from Xlib.xobject.drawable import Window
from PySide2.QtWidgets import QGraphicsItem
from runekit.game.instance import GameInstance
from runekit.game.qt import QtGrabMixin
from runekit.game.psutil import PsUtilNetStat
from runekit.game.qt import QtGrabMixin, QtEmbedMixin
from .ximage import zpixmap_shm_to_image
if TYPE_CHECKING:
from .manager import X11GameManager
class X11GameInstance(QtGrabMixin, GameInstance):
xwindow: Window
class X11GameInstance(QtGrabMixin, QtEmbedMixin, PsUtilNetStat, GameInstance):
wid: int
refresh_rate = 100
manager: "X11GameManager"
overlay: QGraphicsItem
def __init__(self, manager: "X11GameManager", window: Window, **kwargs):
game_last_grab = 0.0
game_last_image = None
embedded_windows: List[QWindow]
cached_position = None
input_signal = Signal(xcffib.Event)
config_signal = Signal(xcffib.Event)
def __init__(self, manager: "X11GameManager", wid: int, **kwargs):
super().__init__(**kwargs)
self.manager = manager
self.xwindow = window
self.qwindow = QWindow.fromWinId(self.xwindow.id)
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.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 get_position(self) -> QRect:
geom = self.xwindow.get_geometry()
def _setup_x(self):
self.manager.xcomposite.RedirectWindow(
self.wid, xcffib.composite.Redirect.Automatic
)
self.name_pixmap()
self.manager.connection.core.ChangeWindowAttributes(
self.wid,
xcffib.xproto.CW.EventMask,
[
xcffib.xproto.EventMask.KeyPress
| xcffib.xproto.EventMask.StructureNotify
],
)
return QRect(geom.x, geom.y, geom.width, geom.height)
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._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:
if self.cached_position:
return self.cached_position
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())
@@ -36,8 +103,71 @@ class X11GameInstance(QtGrabMixin, GameInstance):
return self.is_focused
def _update_is_focused(self):
self.is_focused = self.manager.get_active_window() == self.xwindow.id
self.is_focused = self.manager.get_active_window() == self.wid
def set_taskbar_progress(self, type_, progress):
# Not supported
# TODO: Implement this on Unity? I don't use Unity
# TODO: If progress = 1 set urgency flag
pass
def grab_game(self):
if (time.monotonic() - self.game_last_grab) * 1000 < self.refresh_rate:
return self.game_last_image
xid = shm = None
try:
geom = self.manager.connection.core.GetGeometry(self.pixmap_id).reply()
xid, shm = self.manager.get_shm(geom.width * geom.height * 4)
size = (
self.manager.xshm.GetImage(
self.pixmap_id,
0,
0,
geom.width,
geom.height,
0xFFFFFF,
xcffib.xproto.ImageFormat.ZPixmap,
xid,
0,
)
.reply()
.size
)
out = zpixmap_shm_to_image(shm, size, geom.width, geom.height)
self.game_last_image = out
self.game_last_grab = time.monotonic()
return out
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()
+189 -125
View File
@@ -1,157 +1,221 @@
import logging
from typing import List, Dict
import struct
from typing import List, Dict, Tuple, Union
from PySide2.QtCore import QThread, Slot, Signal, QObject
from Xlib import X
from Xlib.display import Display
from Xlib.ext import xinput, ge
from Xlib.protocol import event
from Xlib.xobject.drawable import Window
import Xlib.threaded
import sysv_ipc
import xcffib
import xcffib.composite
import xcffib.shm
import xcffib.xinput
import xcffib.xproto
from PySide2.QtCore import QThread, Slot, QObject
from runekit.game import GameManager
from .instance import GameInstance, X11GameInstance
from ..overlay import DesktopWideOverlay
MAX_SHM = 10
NET_ACTIVE_WINDOW = "_NET_ACTIVE_WINDOW"
class X11GameManager(GameManager):
connection: xcffib.Connection
_instances: Dict[int, X11GameInstance]
_atom: Dict[bytes, int]
_shm: List[Tuple[int, sysv_ipc.SharedMemory]]
def __init__(self, **kwargs):
super().__init__(*kwargs)
self._instances = {}
self._atom = {}
self._shm = []
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)
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_thread.start()
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 wm_class:
instance_name, app_name = wm_class.split("\00")
if app_name == "RuneScape":
if wid not in self._instances:
instance = X11GameInstance(self, wid, parent=self)
self._instances[wid] = instance
query = self.connection.core.QueryTree(wid).reply()
for child in query.children:
visit(child)
visit(self.screen.root)
return list(self._instances.values())
def get_active_window(self) -> int:
return self.get_property(self.screen.root, "_NET_ACTIVE_WINDOW")
def _setup_overlay(self):
self.overlay = DesktopWideOverlay()
self.overlay.show()
def _setup_composite(self):
self.xcomposite.QueryVersion(0, 4, is_checked=True)
def get_property(
self,
wid: int,
name: str,
type_=xcffib.xproto.GetPropertyType.Any,
index=0,
max_values=1000,
):
reply = self.connection.core.GetProperty(
False,
wid,
self.get_atom(name),
type_,
index,
max_values,
).reply()
if reply.type == xcffib.xproto.Atom.STRING:
return reply.value.to_string()[:-1]
elif reply.type in (xcffib.xproto.Atom.WINDOW, xcffib.xproto.Atom.CARDINAL):
return struct.unpack("=I", reply.value.buf()[:4])[0]
return reply.value
def get_atom(self, atom: str) -> int:
if isinstance(atom, int):
return atom
if hasattr(xcffib.xproto.Atom, atom):
return getattr(xcffib.xproto.Atom, atom)
atom = atom.encode("ascii")
if atom in self._atom:
return self._atom[atom]
out = self.connection.core.InternAtom(False, len(atom), atom).reply().atom
self._atom[atom] = out
return out
def get_shm(self, size: int) -> Tuple[int, sysv_ipc.SharedMemory]:
for item in self._shm:
if item[1].size >= size:
self._shm.remove(item)
return item
shm = sysv_ipc.SharedMemory(None, flags=sysv_ipc.IPC_CREX, size=size)
xid = self.connection.generate_id()
self.xshm.Attach(xid, shm.id, False, is_checked=True)
return xid, shm
def free_shm(self, shm: Tuple[int, sysv_ipc.SharedMemory]):
self._shm.append(shm)
self.gc_shm()
def gc_shm(self):
while len(self._shm) > MAX_SHM:
xid, shm = self._shm.pop(0)
self.xshm.Detach(xid)
shm.detach()
shm.remove()
class X11EventWorker(QObject):
on_active_window_changed = Signal(int)
on_alt1 = Signal(int)
def __init__(self, manager: "X11GameManager", **kwargs):
super().__init__(**kwargs)
self.manager = manager
self.display = manager.display
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
self.handlers = {
ge.GenericEventCode: self.dispatch_ge,
X.PropertyNotify: self.on_property_change,
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,
}
self.ge_handlers = {xinput.KeyPress: self.on_key_press}
self.active_win_id = self.manager.get_active_window()
self._NET_ACTIVE_WINDOW = self.display.get_atom("_NET_ACTIVE_WINDOW")
@Slot()
def run(self):
root = self.display.screen().root
root.change_attributes(event_mask=X.PropertyChangeMask)
self.manager.connection.core.ChangeWindowAttributes(
self.manager.screen.root,
xcffib.xproto.CW.EventMask,
[xcffib.xproto.EventMask.PropertyChange],
is_checked=True,
)
current_thread = QThread.currentThread()
while True:
evt = self.display.next_event()
if evt.send_event != 0:
if current_thread.isInterruptionRequested():
return
evt = self.manager.connection.poll_for_event()
if evt is None:
QThread.msleep(10)
continue
try:
handler = self.handlers.get(evt.type)
if handler:
handler(evt)
except:
self.logger.error("Error handling event %s", repr(evt), exc_info=True)
continue
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 dispatch_ge(self, evt: ge.GenericEvent):
handler = self.ge_handlers.get(evt.evtype)
if handler:
handler(evt)
def on_key_press(self, evt):
window = evt.data.event
# alt1
if evt.data.mods.effective_mods & X.Mod1Mask and evt.data.detail == 10:
self.on_alt1.emit(window.id)
self.logger.debug("alt1 pressed %s", repr(evt))
window.xinput_ungrab_keycode(evt.data.deviceid, evt.data.detail, (X.Mod1Mask,))
self.manager._setup_grab(window)
def on_property_change(self, evt: event.PropertyNotify):
if evt.atom == self._NET_ACTIVE_WINDOW:
def on_property_change(self, evt: xcffib.xproto.PropertyNotifyEvent):
if evt.atom == self.manager.get_atom(NET_ACTIVE_WINDOW):
active_win_id = self.manager.get_active_window()
if self.active_win_id == active_win_id:
return
self.active_win_id = 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:
self.logger.debug(
"Active window changed to %d - %s", active_win_id, repr(evt)
"Got configure event for %d but is not registered", evt.window
)
self.on_active_window_changed.emit(active_win_id)
class X11GameManager(GameManager):
display: Display
_instance: Dict[int, X11GameInstance]
def __init__(self, **kwargs):
super().__init__(*kwargs)
self.display = Display()
self._NET_ACTIVE_WINDOW = self.display.get_atom("_NET_ACTIVE_WINDOW")
self._instance = {}
self.event_thread = QThread()
self.event_worker = X11EventWorker(self)
self.event_worker.moveToThread(self.event_thread)
self.event_thread.started.connect(self.event_worker.run)
self.event_worker.on_active_window_changed.connect(
self.on_active_window_changed
)
self.event_worker.on_alt1.connect(self.on_alt1)
self.event_thread.start()
def get_instances(self) -> List[GameInstance]:
def visit(window):
try:
wm_class = window.get_wm_class()
except:
return
if wm_class and wm_class[0] == "RuneScape":
if window.id not in self._instance:
instance = X11GameInstance(self, window, parent=self)
self._setup_grab(window)
self._instance[window.id] = instance
for child in window.query_tree().children:
visit(child)
visit(self.display.screen().root)
return list(self._instance.values())
def _setup_grab(self, window: Window):
# alt1
window.xinput_grab_keycode(
xinput.AllDevices,
X.CurrentTime,
10,
xinput.GrabModeAsync,
xinput.GrabModeAsync,
True,
xinput.KeyPressMask,
(X.Mod1Mask,),
)
def get_active_window(self) -> int:
resp = self.display.screen().root.get_full_property(
self._NET_ACTIVE_WINDOW, X.AnyPropertyType
)
return resp.value[0]
@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.focusChanged.emit(active)
instance.is_focused = active
@Slot(int)
def on_alt1(self, winid):
if winid in self._instance:
self._instance[winid].alt1_pressed.emit()
+16
View File
@@ -0,0 +1,16 @@
import sysv_ipc
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))
+43
View File
@@ -0,0 +1,43 @@
from PySide2.QtCore import QAbstractItemModel, Qt, QModelIndex
class ApplicationModel(QAbstractItemModel):
h_columns = ("Name", "URL", "Description")
def columnCount(self, parent: QModelIndex) -> int:
return len(self.h_columns)
def rowCount(self, parent: QModelIndex) -> int:
if parent.isValid():
return 0
return 1
def data(self, index: QModelIndex, role: int):
if role != Qt.DisplayRole:
return None
col = index.column()
if col == 0:
return "TestApp"
elif col == 1:
return "https://runeapps.org/apps/alt1/example/appconfig.json"
elif col == 2:
return "Lorem ipsum dolor sit amet"
def index(self, row: int, column: int, parent: QModelIndex) -> QModelIndex:
return self.createIndex(row, column)
def parent(self, index: QModelIndex) -> QModelIndex:
return QModelIndex()
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
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]
+9 -4
View File
@@ -1,16 +1,15 @@
from typing import TYPE_CHECKING
from PySide2.QtWidgets import QWidget
from runekit.alt1.utils import fetch_manifest
from runekit.app import App
from runekit.host.settings import SettingsDialog
from runekit.ui import AutoNotifier
if TYPE_CHECKING:
from runekit.game import GameInstance
class Host(QWidget):
class Host:
"""Host is the root of our application"""
def __init__(self):
@@ -26,4 +25,10 @@ class Host(QWidget):
game_instance=instance,
source_url=manifest_url,
)
app.get_window(parent=self).show()
app.get_window().show()
return app
def open_settings(self):
wnd = SettingsDialog(self)
wnd.show()
return wnd
+104
View File
@@ -0,0 +1,104 @@
import sys
from typing import TYPE_CHECKING
from PySide2.QtCore import QSize, Qt, Slot, QTimer
from PySide2.QtWidgets import (
QTabWidget,
QVBoxLayout,
QMainWindow,
QWidget,
QFormLayout,
QLabel,
QComboBox,
QCheckBox,
QTableWidget,
QHBoxLayout,
QTreeView,
)
from .applications import ApplicationModel
from ..ui import TooltipNotifier, TrayIconNotifier
if TYPE_CHECKING:
from .host import Host
class SettingsDialog(QMainWindow):
def __init__(self, host: "Host", **kwargs):
super().__init__(flags=Qt.Dialog, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
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(500, 300)
class ApplicationPage(QWidget):
host: "Host"
def __init__(self, host: "Host", **kwargs):
super().__init__(**kwargs)
self.host = host
self._layout()
def _layout(self):
layout = QHBoxLayout(self)
self.setLayout(layout)
tree = QTreeView(self)
model = ApplicationModel(self)
tree.setModel(model)
tree.setUniformRowHeights(True)
layout.addWidget(tree, 1)
class InterfacePage(QWidget):
host: "Host"
def __init__(self, host: "Host", **kwargs):
super().__init__(**kwargs)
self.host = host
self._layout()
def _layout(self):
layout = QFormLayout(self)
self.setLayout(layout)
tooltip_label = QLabel("Display tooltips as", self)
tooltip_field = QComboBox(self)
tooltip_field.insertItem(0, "Disabled")
tooltip_field.insertItem(1, "Notification") # XXX: Check support
tooltip_field.insertItem(2, "Cursor tooltip")
tooltip_field.currentIndexChanged.connect(self.preview_tooltip)
layout.addRow(tooltip_label, tooltip_field)
border_field = QCheckBox("Styled window border", self)
border_field.setDisabled(sys.platform == "darwin")
border_field.setChecked(sys.platform != "darwin")
layout.addRow(None, border_field)
@Slot(int)
def preview_tooltip(self, index: int):
if index == 1:
notifier = TrayIconNotifier()
elif index == 2:
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(""))
+27
View File
@@ -0,0 +1,27 @@
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
-55
View File
@@ -1,55 +0,0 @@
from ._utils cimport image_to_ptr, unblend, ImageRGBA, RGB
cdef struct Rectangle:
int x
int y
int width
int height
def get_chat_color(image, tuple box_, list colors):
cdef Rectangle box = Rectangle(box_[0], box_[1], box_[2], box_[3])
cdef float best_score = -1.0
cdef RGB best = RGB(0, 0, 0)
cdef ImageRGBA **data = image_to_ptr(image)
cdef double score
cdef ImageRGBA i1, i2
cdef (unsigned char, unsigned char) pixel1, pixel2
cdef int x, y
cdef RGB color
for color in colors:
score = 0.0
for y in range(box.y, box.y + box.height):
for x in range(box.x, box.x + box.width):
if x < 0 or x + 1 >= box.height:
continue
if y < 0 or y + 1 >= box.width:
continue
i1 = data[y][x]
i2 = data[y + 1][x + 1]
pixel1 = unblend(i1.r, i1.g, i1.b, color.r, color.g, color.b)
pixel2 = unblend(i2.r, i2.g, i2.b, color.r, color.g, color.b)
score += (pixel1[0] / 255 * pixel1[1] / 255) * (pixel2[0] / 255 * (255.0 - pixel2[1]) / 255)
if score > best_score:
best_score = score
best = color
return (best.r, best.g, best.b)
-9
View File
@@ -1,9 +0,0 @@
cdef struct CharInfo:
int width
int height
Py_UNICODE chr
int bonus
bint secondary
unsigned char *pixels
-77
View File
@@ -1,77 +0,0 @@
from cpython.mem cimport PyMem_Malloc, PyMem_Free
from ._font cimport CharInfo
from ._utils cimport image_to_ptr, ImageRGBA
cdef class FontDefinition:
cdef CharInfo *chars
cdef size_t chars_len
cdef readonly int width
cdef readonly int height
cdef readonly int space_width
cdef readonly bint shadow
cdef readonly int base_y
cdef readonly int min_rating
cdef readonly int max_spaces
def __dealloc__(self):
for i in range(self.chars_len):
PyMem_Free(self.chars[i].pixels)
PyMem_Free(self.chars)
cpdef FontDefinition generate_font(image, str chars, str seconds, bonuses, int base_y, int space_width, int threshold, bint shadow):
cdef int chars_len = len(chars)
cdef FontDefinition out = FontDefinition()
cdef CharInfo *cinfo = <CharInfo*> PyMem_Malloc(sizeof(CharInfo) * chars_len)
cdef ImageRGBA **raw = image_to_ptr(image)
cdef int width = image.width
cdef int height = image.height
cdef int min_y = image.height - 1
cdef int max_y = 0
cdef int ds = -1
cdef int i, dx, cur_char, letter_width, x, y
cdef Py_UNICODE letter
cdef CharInfo *chr
threshold *= 255
for dx in range(width):
if ds == -1:
i = height - 1
if raw[i][dx].r == 255 and raw[i][dx].a == 255:
ds = dx
continue
letter = chars[cur_char]
letter_width = ds - dx
cinfo[cur_char] = CharInfo(
width=letter_width,
chr=letter,
bonus=0,
secondary=letter in seconds,
)
if bonuses and letter in bonuses:
cinfo[cur_char].bonus = bonuses[letter]
cur_char += 1
out.width = max(out.width, letter_width)
for y in range(height - 1):
for x in range(letter_width):
if raw[y][x + ds].r >= threshold:
min_y = min(min_y, y)
max_y = max(max_y, y)
ds = -1
out.height = max_y + 1 - min_y
out.base_y = base_y - min_y
for a in range(chars_len):
chr = &cinfo[a]
# TODO
return out
-16
View File
@@ -1,16 +0,0 @@
cdef struct ImageRGBA:
unsigned char r
unsigned char g
unsigned char b
unsigned char a
cdef struct RGB:
unsigned char r
unsigned char g
unsigned char b
cpdef (unsigned char, unsigned char) unblend(int r, int g, int b, int R, int G, int B) nogil
cdef ImageRGBA** image_to_ptr(image)
cpdef int can_blend(int rm, int gm, int bm, int r1, int g1, int b1, int p) nogil
# cdef (double, double, double) decompose2col(unsigned char rp, unsigned char gp, unsigned char bp, unsigned char r1, unsigned char g1, unsigned char b1, unsigned char r2, unsigned char g2, unsigned char b2) nogil
# cdef (double, double, double) decompose3col(unsigned char rp, unsigned char gp, unsigned char bp, unsigned char r1, unsigned char g1, unsigned char b1, unsigned char r2, unsigned char g2, unsigned char b2, double r3, double g3, double b3) nogil
-116
View File
@@ -1,116 +0,0 @@
from PIL.Image import new as image_new
from libc.math cimport sqrt, fabs
from ._utils cimport ImageRGBA
cpdef (unsigned char, unsigned char) unblend(int r, int g, int b, int R, int G, int B) nogil:
cdef double m = sqrt(r * r + g * g + b * b)
cdef double n = sqrt(R * R + G * G + B * B)
cdef double x = (r * R + g * G + b * B) / n
cdef double y = sqrt(max(0.0, m * m - x * x))
cdef int r1 = int(max(0.0, (63.75 - y) * 4))
cdef int r2 = int(x / n * 255)
if r2 > 255:
r1 = max(0, r1 - r2 + 255)
r2 = 255
return r1, r2
cdef ImageRGBA** image_to_ptr(image):
ptr = dict(image.im.unsafe_ptrs)['image32']
cdef size_t ptr_int = <size_t> ptr
return <ImageRGBA**> ptr_int
def rgb(unsigned char r, unsigned char g, unsigned char b):
return RGB(r, g,b)
cpdef int can_blend(int rm, int gm, int bm, int r1, int g1, int b1, int p) nogil:
"""
Determine whether color [rgb]m can be a result of a blend with color [rgb]1 that is p (0-1) of the mix
:param rm: resulting color
:param r1: first color of the mix (the other color is unknown)
:param p: the portion of the [rgb]1 in the mix (0-1)
:return: the number that the second color has to lie outside of the possible color ranges
"""
cdef int m = min(50, p/(1-p))
cdef int r = rm + (rm - r1) * m
cdef int g = gm + (gm - g1) * m
cdef int b = bm + (bm - b1) * m
return max(-r, -g, -b, r - 255, g - 255, b - 255)
def unblend_known_background(img, bg_img, bint shadow, unsigned char r, unsigned char g, unsigned char b):
if bg_img:
assert img.width == bg_img.width and img.height == bg_img.height
out_img = image_new('RGBA', (img.width, img.height), None)
cdef ImageRGBA **outptr = image_to_ptr(out_img)
cdef ImageRGBA **imgptr = image_to_ptr(img)
cdef ImageRGBA **bgptr = image_to_ptr(bg_img)
cdef int width = img.width
cdef int height = img.height
cdef int x, y
cdef (double, double, double) col
cdef double m
cdef unsigned char red
with nogil:
for y in range(height):
for x in range(width):
pixel = imgptr[y][x]
bg_pixel = bgptr[y][x]
col = decompose2col(pixel.r, pixel.g, pixel.b, r, g, b, bg_pixel.r, bg_pixel.g, bg_pixel.b)
if shadow:
m = 1 - col[1] - fabs(col[2])
red = <unsigned char> (m * 255)
outptr[y][x].r = red
outptr[y][x].g = <unsigned char> (col[0] / m * 255.0)
outptr[y][x].b = red
else:
red = <unsigned char> (col[0] * 255)
outptr[y][x].r = red
outptr[y][x].g = red
outptr[y][x].b = red
outptr[y][x].a = 255
return out_img
cdef inline (double, double, double) decompose2col(double rp, double gp, double bp, double r1, double g1, double b1, double r2, double g2, double b2) nogil:
cdef double r3 = g1 * b2 - g2 * b1
cdef double g3 = b1 * r2 - b2 * r1
cdef double b3 = r1 * g2 - r2 * g1
cdef double norm = 255.0 / sqrt(r3 * r3 + g3 * g3 + b3 * b3)
r3 *= norm
g3 *= norm
b3 *= norm
return decompose3col(rp, gp, bp, r1, g1, b1, r2, g2, b2, r3, g3, b3)
cdef inline (double, double, double) decompose3col(double rp, double gp, double bp, double r1, double g1, double b1, double r2, double g2, double b2, double r3, double g3, double b3) nogil:
cdef double A = g2 * b3 - b2 * g3
cdef double B = g3 * b1 - b3 * g1
cdef double C = g1 * b2 - b1 * g2
cdef double D = b2 * r3 - r2 * b3
cdef double E = b3 * r1 - r3 * b1
cdef double F = b1 * r2 - r1 * b2
cdef double G = r2 * g3 - g2 * r3
cdef double H = r3 * g1 - g3 * r1
cdef double I = r1 * g2 - g1 * r2
cdef double det = r1 * A + g1 * D + b1 * G
cdef double x = (A * rp + D * gp + G * bp) / det
cdef double y = (B * rp + E * gp + H * bp) / det
cdef double z = (C * rp + F * gp + I * bp) / det
return x, y, z
-291
View File
@@ -1,291 +0,0 @@
from typing import List, Tuple, Optional, TypedDict, NamedTuple
from PIL import Image
from runekit.alt1.schema import FontMeta
from ._chat import get_chat_color
from ._utils import rgb, can_blend
Color = Tuple[int, int, int]
class Rectangle(NamedTuple):
x: int
y: int
width: int
height: int
class OCRLine(NamedTuple):
text: str
area: Rectangle
class ChatboxLineFragment(TypedDict):
color: int
text: str
index: int
class ReadCharInfo(NamedTuple):
chr: str
base_char: CharInfo
x: int
y: int
score: float
size_score: float
def read_chatbox_line(
image: Image,
x: int,
y: int,
colors: List[Color],
font: FontDefinition,
backwards=False,
allow_gap=False,
budget=15,
allow_multi_col=True,
):
colors = [rgb(*x) for x in colors]
fragment_count = 0
max_fragment_len = 0
scan_x = x
text = ""
fragments: List[ChatboxLineFragment] = []
total_space_size = 0
while True:
chat_color = get_chat_color(image, (scan_x, y - 8, 6, 8), colors)
ocr_line = read_chat_line(
image,
chat_color,
font,
scan_x,
y,
backward=backwards and fragment_count == 0,
forward=True,
scan_mode=True,
)
if not ocr_line or not ocr_line.text or not ocr_line.area[4]:
if not allow_multi_col and fragment_count != 0:
break
if total_space_size >= font["width"] and (
not allow_gap
or total_space_size >= 20
or scan_x - x >= 100
or budget < 0
):
break
space_size = font["space_width"]
if total_space_size != 0 or fragment_count == 0:
space_size = 1
scan_x += space_size
total_space_size += space_size
budget -= 1
continue
if total_space_size >= 2 and ocr_line.text:
text += " "
if len(fragments) > 0:
fragments[-1]["text"] += " "
coded_color = chat_color[0] * 256 * 256 + chat_color[1] * 256 + chat_color[2]
fragments.append(
{
"color": coded_color,
"text": ocr_line.text,
"index": len(text),
}
)
fragment_count += 1
text += ocr_line.text
max_fragment_len = max(max_fragment_len, len(ocr_line.text))
scan_x = max(
scan_x + font["space_width"], ocr_line.area.x + ocr_line.area.width
)
total_space_size = 0
if len(fragments) == 0 or max_fragment_len <= 2:
return None
return {
"fragments": fragments,
"text": text,
}
def read_chat_line(
image: Image,
ref_color: Color,
font: FontDefinition,
x: int,
y: int,
backward=False,
forward=True,
scan_mode=False,
) -> Optional[OCRLine]:
text = ""
w_fw = x
w_bw = x
if forward:
shift_x = 0
last_not_found = False
cur_text = ""
while True:
flag2 = scan_mode and not text # FIXME: What's my name?
char = read_chat_char(
image,
ref_color,
font,
x + shift_x,
y,
backward=False,
allow_secondary=not flag2,
)
if not char:
if flag2:
backward = False
break
if last_not_found:
break
cur_text += " "
shift_x += font["space_width"]
last_not_found = True
else:
last_not_found = False
shift_x += char.base_char["width"]
if char.base_char.secondary:
cur_text += char.chr
continue
text = text + cur_text + char.chr
cur_text = ""
w_fw = x + shift_x
if backward:
shift_x = 0
last_not_found = False
cur_text = ""
while True:
char = read_chat_char(image, ref_color, font, x + shift_x, y, backward=True)
if not char:
if last_not_found:
break
cur_text = " " + cur_text
shift_x -= font["space_width"]
last_not_found = True
continue
last_not_found = False
shift_x -= char.base_char.width
if char.base_char.secondary:
cur_text = char.chr + cur_text
continue
text = char.chr + cur_text + text
cur_text = ""
w_bw = x + shift_x
return OCRLine(text=text, area=Rectangle(w_bw, y - 9, w_fw - w_bw, 10))
class _chatScore(NamedTuple):
score: int
size_score: int
chr: CharInfo
def read_chat_char(
image: Image,
ref_color: Color,
font: FontDefinition,
x: int,
y: int,
backward=False,
allow_secondary=True,
) -> Optional[ReadCharInfo]:
y -= font["base_y"]
shadow = font["shadow"]
# Boundary check
if y < 0 or y + font["height"] >= image.height:
return None
if not backward:
if x < 0 or x + font["width"] > image.width:
return None
else:
if x - font["width"] < 0 or x > image.width:
return None
scores: List[_chatScore] = []
for chr in font["chars"]:
if chr.secondary and not allow_secondary:
continue
if backward:
chrx = x - chr.width
else:
chrx = x
a = 0
score = 0
while a < len(chr.pixels):
ix = chrx + chr.pixels[a]
iy = y + chr.pixels[a + 1]
pixel = image.getpixel((ix, iy))
if not shadow:
penalty = can_blend(
pixel[0],
pixel[1],
pixel[2],
ref_color[0],
ref_color[1],
ref_color[2],
chr.pixels[a + 2] / 255,
)
a += 3
else:
lum = chr.pixels[a + 3] / 255
penalty = can_blend(
pixel[0],
pixel[1],
pixel[2],
ref_color[0] * lum,
ref_color[1] * lum,
ref_color[2] * lum,
chr.pixels[a + 2] / 255,
)
a += 4
score += max(0, penalty)
size_score = score - chr.bonus
scores.append(_chatScore(score=score, size_score=size_score, chr=chr))
scores.sort(key=lambda item: item.size_score)
if not scores:
return None
winner = scores[0]
if winner.score > 400:
return None
shift_x = 0
shift_y = font["base_y"]
return ReadCharInfo(
chr=winner.chr.chr,
base_char=winner.chr,
x=x + shift_x,
y=y + shift_y,
score=winner.score,
size_score=winner.size_score,
)
-65
View File
@@ -1,65 +0,0 @@
import json
from pathlib import Path
from typing import TypedDict, NamedTuple, List, Optional
from PIL import Image
from runekit.alt1.schema import FontMeta
from ._utils import unblend_known_background
FONT_BASE = Path(__file__).parent / "fonts"
class CharInfo(NamedTuple):
width: int
chr: str
bonus: int
secondary: bool
pixels: List[int]
class FontDefinition(TypedDict):
chars: List[CharInfo]
width: int
height: int
space_width: int
shadow: bool
base_y: int
min_rating: Optional[int]
max_spaces: Optional[int]
def load_font(name: str) -> FontDefinition:
meta_file = FONT_BASE / (name + ".fontmeta.json")
with meta_file.open() as fp:
meta: FontMeta = json.load(fp)
return load_font_file(FONT_BASE / (name + ".data.png"), meta)
def load_font_file(image: Path, meta: FontMeta) -> FontDefinition:
img = Image.open(image)
return load_font_image(img, meta)
def load_font_image(image: Image, meta: FontMeta) -> FontDefinition:
if meta["unblendmode"] == "removebg":
bg_height = int((image.height - 1) / 2)
background = image.crop((0, bg_height + 1, image.width, image.height))
font_img = unblend_known_background(
image.crop((0, 0, image.width, bg_height)),
background,
meta["shadow"],
meta["color"][0],
meta["color"][1],
meta["color"][2],
)
font_img.show()
elif meta["unblendmode"] == "raw":
raise NotImplementedError
elif meta["unblendmode"] == "blackbg":
raise NotImplementedError
if __name__ == "__main__":
load_font("chat_8px")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

@@ -1,11 +0,0 @@
{
"basey": 11,
"spacewidth": 3,
"treshold": 0.6,
"color": [ 255, 255, 255 ],
"unblendmode": "raw",
"shadow": false,
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
"seconds": ",.-:;\"'",
"spriteid": 17814
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

@@ -1,10 +0,0 @@
{
"basey": 17,
"spacewidth": 4,
"treshold": 0.6,
"color": [ 255, 255, 255 ],
"unblendmode": "raw",
"shadow": false,
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
"seconds": ",.-:;\"'"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

@@ -1,10 +0,0 @@
{
"basey": 8,
"spacewidth": 3,
"treshold": 0.6,
"color": [ 255, 255, 255 ],
"unblendmode": "raw",
"shadow": false,
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
"seconds": ",.-:;\"'"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

@@ -1,10 +0,0 @@
{
"basey": 10,
"spacewidth": 3,
"treshold": 0.6,
"color": [ 255, 255, 255 ],
"unblendmode": "raw",
"shadow": false,
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
"seconds": ",.-:;\"'|"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

@@ -1,10 +0,0 @@
{
"basey": 10,
"spacewidth": 3,
"treshold": 0.6,
"color": [ 127, 169, 255 ],
"unblendmode": "blackbg",
"shadow": false,
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
"seconds": ",.-:;\"'"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

@@ -1,10 +0,0 @@
{
"basey": 8,
"spacewidth": 3,
"treshold": 0.6,
"color": [ 255, 255, 255 ],
"unblendmode": "raw",
"shadow": true,
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
"seconds": ",.-:;\"'"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

@@ -1,10 +0,0 @@
{
"basey": 8,
"spacewidth": 3,
"treshold": 0.6,
"color": [ 255, 255, 255 ],
"unblendmode": "raw",
"shadow": false,
"chars": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz:()0123456789%,.'?",
"seconds": ",.-:;\"'"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

@@ -1,10 +0,0 @@
{
"basey": 10,
"spacewidth": 3,
"treshold": 0.6,
"color": [ 127, 169, 255 ],
"unblendmode": "removebg",
"shadow": true,
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
"seconds": ",.-:;\"'"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

@@ -1,10 +0,0 @@
{
"basey": 13,
"spacewidth": 5,
"treshold": 0.6,
"color": [ 255, 255, 255 ],
"unblendmode": "raw",
"shadow": false,
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
"seconds": ",.-:;\"'|*"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

@@ -1,10 +0,0 @@
{
"basey": 12,
"spacewidth": 3,
"treshold": 0.6,
"color": [ 127, 169, 255 ],
"unblendmode": "removebg",
"shadow": true,
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
"seconds": ",.-:;\"'"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

@@ -1,10 +0,0 @@
{
"basey": 13,
"spacewidth": 4,
"treshold": 0.6,
"color": [ 127, 169, 255 ],
"unblendmode": "removebg",
"shadow": true,
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
"seconds": ",.-:;\"'"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

@@ -1,11 +0,0 @@
{
"basey": 15,
"spacewidth": 6,
"treshold": 0.6,
"color": [ 127, 169, 255 ],
"unblendmode": "removebg",
"shadow": true,
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
"seconds": ",.-:;\"'",
"//":"Seems to have a 2px letter spacing and 4px spacewidth, the letter spacing is included in the font definition after the characters"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

@@ -1,10 +0,0 @@
{
"basey": 10,
"spacewidth": 3,
"treshold": 0.6,
"color": [ 127, 169, 255 ],
"unblendmode": "removebg",
"shadow": true,
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
"seconds": ",.-:;\"'"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

@@ -1,10 +0,0 @@
{
"basey": 8,
"spacewidth": 4,
"treshold": 0.9,
"color": [ 255, 255, 255 ],
"unblendmode": "raw",
"shadow": false,
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
"seconds": ",.-:;\"'"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 B

@@ -1,10 +0,0 @@
{
"basey": 7,
"spacewidth": 4,
"treshold": 0.9,
"color": [ 255, 255, 255 ],
"unblendmode": "raw",
"shadow": true,
"chars": "0123456789m()",
"seconds": ""
}
+46
View File
@@ -0,0 +1,46 @@
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)
+1 -4
View File
@@ -1,5 +1,4 @@
import abc
from pathlib import Path
from typing import Union
from PySide2.QtGui import QIcon
@@ -7,8 +6,6 @@ from PySide2.QtWidgets import QSystemTrayIcon
from .tooltip import TooltipManager
TRAY_ICON = Path(__file__).parent / "trayicon.png"
class Notifier(abc.ABC):
@abc.abstractmethod
@@ -26,7 +23,7 @@ class TooltipNotifier(Notifier):
class TrayIconNotifier(Notifier):
def __init__(self):
self.icon = QSystemTrayIcon(QIcon(str(TRAY_ICON)))
self.icon = QSystemTrayIcon(QIcon(":/runekit/ui/trayicon.png"))
self.icon.show()
def notify(self, text):
View File
Whitespace-only changes.
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

Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more