Compare commits
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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** | | |
|
||||
| - Text/Emote | ✅ | ✅ |
|
||||
| - Map scroll | ✅ | ? |
|
||||
| - Compass | ✅ | ? |
|
||||
| - Scan | ✅ | ? |
|
||||
| - Celtic knot | ✅ | ✅ |
|
||||
| - Slide | ✅ | ✅ |
|
||||
| - Lockbox | ✅ | ? |
|
||||
| - Towers | ? | ? |
|
||||
| Example app | ✅ | ✅ |
|
||||
| **AfkWarden** | | |
|
||||
| - 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** | | |
|
||||
| - 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** | | |
|
||||
| Instance change signals | ❌ | ❌ |
|
||||
| **Instance** | | |
|
||||
| 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,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"]})
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@@ -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:
|
||||
|
||||

|
||||
|
||||
Then, go ahead and open it with `Archive Utitlity`:
|
||||
|
||||

|
||||
|
||||
You should now find the folder in your `Downloads` folder (use `Finder` to find it):
|
||||
|
||||

|
||||
|
||||
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`:
|
||||
|
||||

|
||||
|
||||
You can check that this worked by closing and reopening your terminal, then running `which brew`. You should get the following output:
|
||||
|
||||

|
||||
|
||||
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).
|
||||
|
||||

|
||||
|
||||
If you're having trouble finding the downloads at the link, remember to scroll to the bottom of the page.
|
||||
|
||||
## (4/4) Installing poetry
|
||||
|
||||
Run the following command in your terminal:
|
||||
|
||||
```bash
|
||||
brew install poetry
|
||||
```
|
||||
|
||||
## One last thing...
|
||||
|
||||
Before you go and run the commands from the other page, you'll want to change what file directory your terminal is in to be the folder where you downloaded `runekit-master`. Assuming you downloaded it as instructed in step 1, you can do this by running the following:
|
||||
|
||||
```bash
|
||||
cd ~/Downloads/runekit-master
|
||||
```
|
||||
|
||||
**Note**: if you close and reopen the terminal, you'll need to renavigate to the correct folder (just rerun the above command).
|
||||
|
||||
That simple. Now you can go back to the other file and pick up where you left off, [here](../README.md#running).
|
||||
@@ -1,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__":
|
||||
|
||||
@@ -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"},
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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]]
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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"])
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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(""))
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
cdef struct CharInfo:
|
||||
int width
|
||||
int height
|
||||
Py_UNICODE chr
|
||||
int bonus
|
||||
bint secondary
|
||||
|
||||
unsigned char *pixels
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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")
|
||||
|
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
|
||||
}
|
||||
|
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": ",.-:;\"'"
|
||||
}
|
||||
|
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": ",.-:;\"'"
|
||||
}
|
||||
|
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": ",.-:;\"'|"
|
||||
}
|
||||
|
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": ",.-:;\"'"
|
||||
}
|
||||
|
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": ",.-:;\"'"
|
||||
}
|
||||
|
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": ",.-:;\"'"
|
||||
}
|
||||
|
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": ",.-:;\"'"
|
||||
}
|
||||
|
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": ",.-:;\"'|*"
|
||||
}
|
||||
|
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": ",.-:;\"'"
|
||||
}
|
||||
|
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": ",.-:;\"'"
|
||||
}
|
||||
|
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"
|
||||
}
|
||||
|
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": ",.-:;\"'"
|
||||
}
|
||||
|
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": ",.-:;\"'"
|
||||
}
|
||||
|
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": ""
|
||||
}
|
||||
@@ -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,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):
|
||||
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 941 B |
|
After Width: | Height: | Size: 241 B |
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 182 B |
|
After Width: | Height: | Size: 323 B |
|
After Width: | Height: | Size: 266 B |
|
After Width: | Height: | Size: 264 B |
|
After Width: | Height: | Size: 248 B |
|
After Width: | Height: | Size: 189 B |
|
After Width: | Height: | Size: 189 B |
@@ -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"
|
||||
}
|
||||
|
After Width: | Height: | Size: 424 B |
|
After Width: | Height: | Size: 409 B |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 385 B |
|
After Width: | Height: | Size: 373 B |
|
After Width: | Height: | Size: 372 B |