Compare commits
No files matched your search
@@ -4,3 +4,6 @@ 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,7 +2,7 @@ from Cython.Build import cythonize
|
||||
|
||||
# use cythonize to build the extensions
|
||||
modules = [
|
||||
"runekit/image/format.pyx",
|
||||
# "runekit/image/format.pyx",
|
||||
]
|
||||
|
||||
extensions = cythonize(modules)
|
||||
@@ -11,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
|
||||
|
||||
@@ -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: '{}',
|
||||
@@ -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,15 +1,17 @@
|
||||
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 = 1000
|
||||
@@ -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(""))
|
||||
@@ -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 |
|
After Width: | Height: | Size: 451 B |
|
After Width: | Height: | Size: 448 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,6 +1,6 @@
|
||||
from typing import Union, Optional
|
||||
from typing import Union
|
||||
|
||||
from PySide2.QtCore import Qt, QTimer, Slot, QSize
|
||||
from PySide2.QtCore import Qt, QTimer, Slot
|
||||
from PySide2.QtGui import QCursor
|
||||
from PySide2.QtWidgets import QLabel, QMainWindow
|
||||
|
||||
@@ -15,8 +15,8 @@ class TooltipManager(QMainWindow):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
flags=Qt.Dialog
|
||||
| Qt.X11BypassWindowManagerHint
|
||||
flags=Qt.Popup
|
||||
| Qt.FramelessWindowHint
|
||||
| Qt.WindowStaysOnTopHint
|
||||
| Qt.WindowTransparentForInput
|
||||
)
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtGui import QPixmap, Qt, QResizeEvent
|
||||
from PySide2.QtWidgets import (
|
||||
QWidget,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QLabel,
|
||||
QSizePolicy,
|
||||
QPushButton,
|
||||
)
|
||||
|
||||
SKIN = ":/runekit/ui/skins/default/"
|
||||
|
||||
|
||||
class WindowFrame(QWidget):
|
||||
on_minimize = Signal()
|
||||
on_settings = Signal()
|
||||
on_exit = Signal()
|
||||
|
||||
content: QWidget
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._layout()
|
||||
|
||||
def _layout(self):
|
||||
self._inner = _FrameInner(parent=self)
|
||||
self._buttons = _WindowButtons(parent=self)
|
||||
self._buttons.on_minimize.connect(self.on_minimize)
|
||||
self._buttons.on_settings.connect(self.on_settings)
|
||||
self._buttons.on_exit.connect(self.on_exit)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setMargin(0)
|
||||
self.setLayout(layout)
|
||||
layout.addWidget(self._inner)
|
||||
self._buttons.show()
|
||||
|
||||
def resizeEvent(self, evt: QResizeEvent):
|
||||
buttons_size = self._buttons.sizeHint()
|
||||
self._buttons.setGeometry(
|
||||
evt.size().width() - buttons_size.width(),
|
||||
0,
|
||||
buttons_size.width(),
|
||||
buttons_size.height(),
|
||||
)
|
||||
|
||||
def set_content(self, content: QWidget):
|
||||
self._inner.set_content(content)
|
||||
self.content = content
|
||||
|
||||
|
||||
class _FrameInner(QWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._layout()
|
||||
# TODO: Set background
|
||||
|
||||
def _layout(self):
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
top_row = QHBoxLayout()
|
||||
layout.addLayout(top_row)
|
||||
top_row.setContentsMargins(0, 0, 0, 0)
|
||||
top_row.setSpacing(0)
|
||||
top_row.addWidget(self.build_image("borderTL", Qt.TopEdge | Qt.LeftEdge))
|
||||
top_row.addWidget(self.build_image("borderT", Qt.TopEdge, h_fix=False))
|
||||
top_row.addWidget(self.build_image("borderTR", Qt.TopEdge | Qt.RightEdge))
|
||||
|
||||
mid_row = QHBoxLayout()
|
||||
layout.addLayout(mid_row)
|
||||
mid_row.setContentsMargins(0, 0, 0, 0)
|
||||
mid_row.setSpacing(0)
|
||||
mid_row.addWidget(self.build_image("borderL", Qt.LeftEdge, v_fix=False))
|
||||
self.content = QWidget(self)
|
||||
mid_row.addWidget(self.content, 1)
|
||||
mid_row.addWidget(self.build_image("borderR", Qt.RightEdge, v_fix=False))
|
||||
|
||||
bot_row = QHBoxLayout()
|
||||
layout.addLayout(bot_row)
|
||||
bot_row.setContentsMargins(0, 0, 0, 0)
|
||||
bot_row.setSpacing(0)
|
||||
bot_row.addWidget(
|
||||
self.build_image(
|
||||
"borderBL",
|
||||
Qt.BottomEdge | Qt.LeftEdge,
|
||||
)
|
||||
)
|
||||
bot_row.addWidget(self.build_image("borderB", Qt.BottomEdge, h_fix=False))
|
||||
bot_row.addWidget(self.build_image("borderBR", Qt.BottomEdge | Qt.RightEdge))
|
||||
|
||||
def build_image(self, name: str, edge=None, h_fix=True, v_fix=True):
|
||||
pixmap = QPixmap(SKIN + name)
|
||||
label = _ResizeHandle(edge, parent=self)
|
||||
label.setSizePolicy(
|
||||
QSizePolicy.Fixed if h_fix else QSizePolicy.Preferred,
|
||||
QSizePolicy.Fixed if v_fix else QSizePolicy.Preferred,
|
||||
)
|
||||
label.setScaledContents(True)
|
||||
label.setPixmap(pixmap)
|
||||
return label
|
||||
|
||||
def set_content(self, content: QWidget):
|
||||
self.layout().replaceWidget(self.content, content)
|
||||
self.content = content
|
||||
|
||||
|
||||
class _WindowButtons(QWidget):
|
||||
on_minimize = Signal()
|
||||
on_settings = Signal()
|
||||
on_exit = Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._layout()
|
||||
|
||||
def _layout(self):
|
||||
layout = QHBoxLayout()
|
||||
self.setLayout(layout)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.addStretch(1)
|
||||
|
||||
drag_area = _DragHandle(self)
|
||||
layout.addWidget(drag_area, alignment=Qt.AlignTop)
|
||||
|
||||
minimize_button = QPushButton(self)
|
||||
minimize_button.clicked.connect(self.on_minimize)
|
||||
minimize_button.setStyleSheet(
|
||||
self.button_style("minimize.png", "minimizeHover.png")
|
||||
)
|
||||
layout.addWidget(minimize_button, alignment=Qt.AlignTop)
|
||||
|
||||
settings_button = QPushButton(self)
|
||||
settings_button.clicked.connect(self.on_settings)
|
||||
settings_button.setStyleSheet(
|
||||
self.button_style("settings.png", "settingsHover.png")
|
||||
)
|
||||
layout.addWidget(settings_button, alignment=Qt.AlignTop)
|
||||
|
||||
exit_button = QPushButton(self)
|
||||
exit_button.clicked.connect(self.on_exit)
|
||||
exit_button.setStyleSheet(self.button_style("exit.png", "exitHover.png"))
|
||||
layout.addWidget(exit_button, alignment=Qt.AlignTop)
|
||||
|
||||
def button_style(self, normal, hover):
|
||||
return f"""
|
||||
QPushButton {{
|
||||
image: url({SKIN}{normal});
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}}
|
||||
QPushButton::hover {{
|
||||
image: url({SKIN}{hover});
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
class _DragHandle(QLabel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.setPixmap(QPixmap(SKIN + "borderDrag.png"))
|
||||
self.setCursor(Qt.OpenHandCursor)
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
self.window().windowHandle().startSystemMove()
|
||||
|
||||
|
||||
class _ResizeHandle(QLabel):
|
||||
def __init__(self, edge, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.edge = edge
|
||||
|
||||
if edge == Qt.TopEdge & Qt.LeftEdge:
|
||||
self.setCursor(Qt.SizeFDiagCursor)
|
||||
elif edge == Qt.TopEdge:
|
||||
self.setCursor(Qt.SizeVerCursor)
|
||||
elif edge == Qt.TopEdge & Qt.RightEdge:
|
||||
self.setCursor(Qt.SizeBDiagCursor)
|
||||
elif edge == Qt.LeftEdge:
|
||||
self.setCursor(Qt.SizeHorCursor)
|
||||
elif edge == Qt.RightEdge:
|
||||
self.setCursor(Qt.SizeHorCursor)
|
||||
elif edge == Qt.BottomEdge & Qt.LeftEdge:
|
||||
self.setCursor(Qt.SizeBDiagCursor)
|
||||
elif edge == Qt.BottomEdge:
|
||||
self.setCursor(Qt.SizeVerCursor)
|
||||
elif edge == Qt.BottomEdge & Qt.RightEdge:
|
||||
self.setCursor(Qt.SizeFDiagCursor)
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
if self.edge:
|
||||
self.window().windowHandle().startSystemResize(self.edge)
|
||||