Compare commits
54 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9637d62816 | ||
|
|
bd115a29f2 | ||
|
|
6759ac6ae2 | ||
|
|
88079ad98a | ||
|
|
4a2e7d0c54 | ||
|
|
a3e65b8270 | ||
|
|
00388dcb0a | ||
|
|
ebbbf97cdf | ||
|
|
f27db06b17 | ||
|
|
9047091dd0 | ||
|
|
3e997c596a | ||
|
|
f62fa66f51 | ||
|
|
facb96ca19 | ||
|
|
2fd2119a7a | ||
|
|
9b8c1a71a4 | ||
|
|
451ada820b | ||
|
|
c31f7054c4 | ||
|
|
d2ebf4bb0d | ||
|
|
3fe258a96b | ||
|
|
706a8d9ddf | ||
|
|
28dabf2271 | ||
|
|
96c6066a91 | ||
|
|
2ea15b8943 | ||
|
|
c09fffe6e8 | ||
|
|
8393dab1a5 | ||
|
|
f5884ae9a1 | ||
|
|
2bf8dedf9d | ||
|
|
7dcf8a8bde | ||
|
|
a97e532b93 | ||
|
|
19d8e1a180 | ||
|
|
d357296306 | ||
|
|
af7b9348a5 | ||
|
|
41af47f65e | ||
|
|
418072b80c | ||
|
|
5fabf6a773 | ||
|
|
a40ea8ec29 | ||
|
|
aba7c949b5 | ||
|
|
9b2925b428 | ||
|
|
458108119d | ||
|
|
ec392f50f5 | ||
|
|
2665d829a2 | ||
|
|
ce84f0daa8 | ||
|
|
d1563d14bc | ||
|
|
7ebe935805 | ||
|
|
ec88e497fa | ||
|
|
8c65910f27 | ||
|
|
7941f1909a | ||
|
|
ee0efafa02 | ||
|
|
9ec6256fc1 | ||
|
|
30ffe32f82 | ||
|
|
439769ca21 | ||
|
|
3dfa0f92e3 | ||
|
|
0d01fa0816 | ||
|
|
1d439478bb |
3
.gitignore
vendored
|
|
@ -11,6 +11,9 @@
|
||||||
# files & folders for development use
|
# files & folders for development use
|
||||||
debug
|
debug
|
||||||
|
|
||||||
|
# Archive file
|
||||||
|
*.tar.gz
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
|
||||||
40
.travis.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
sudo: required
|
||||||
|
language: python
|
||||||
|
python: '3.6'
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
|
||||||
|
install: |
|
||||||
|
docker pull kbumsik/virtscreen
|
||||||
|
pip3 install .
|
||||||
|
|
||||||
|
script: |
|
||||||
|
echo No test scripts implemented yet. Travis is used only for deploy yet.
|
||||||
|
|
||||||
|
before_deploy: |
|
||||||
|
if [ -n "$TRAVIS_TAG" ]; then
|
||||||
|
VERSION=$TRAVIS_TAG make override_version
|
||||||
|
fi
|
||||||
|
make package/pypi/*.whl
|
||||||
|
make package/appimage/VirtScreen.AppImage
|
||||||
|
make package/debian/virtscreen.deb
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
- provider: releases
|
||||||
|
api_key:
|
||||||
|
secure: zFbsCIKcsvWU/Yc+9k294Qj8QY48VlkV8DSScP5gz6dQegeUSaSHI/YafherkFQ0B03bIY8yc7roMtDo7HAkEnPptjFhdUiOFI11+xDVb3s7Y8Ek2nV3znQzdtR4CR/94l3in6R3DH+eNA6+6Je/NIWLdVcvRX07RBSfBVdPmnsAyAD9KNTsl8Q4c20HgtLNxfWv2s5eCyD+heCTLYrErEZKZ5vYeeANmWomHvT2ED/4QerpBP8wkh59QXD1S79CF7oyq6X173ZJUQVxdBP+OSXt/mDBAoqf+TV6okawRZn48JluvCWAJ7BceX7t9emd1rVI/s8t3wCP+eMcmNn5g/6UJaCPnTJ5YplTuUWRc63UFSkE0AY8WYcRlrz+/OiXYgQ8LMXfN23aWgarHCbS2vHR3Afu9gpLCoKucr36hKhs3zfjJzVLFFW16mnbaTFcBzfDDRpkvOANB1aZwGVRFpTIWIMjkn0+lxWTC/moIJvQlfRPsC4dN5cDAilRQlguHzayebtGE8X0PuIe9A8bkET3V/y+KPnQiSJ7J+5PNoDSdqRAE4IKvVOLEyHtlqBVkvIHKnugUnWPIZ21gm5RemMEj9/YGa8Efwz7PIKtJJ3kFMGDYKVlIKyB+rg/TFWNdo6jjevnWM6y4SfVI3kFyjA+mp31o6nshrQy0zVQpd8=
|
||||||
|
file:
|
||||||
|
- package/debian/virtscreen.deb
|
||||||
|
- package/appimage/VirtScreen.AppImage
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
repo: kbumsik/VirtScreen
|
||||||
|
- provider: pypi
|
||||||
|
user: kbumsik
|
||||||
|
password:
|
||||||
|
secure: d7ozcWf9/j2mpyYX60o7yo/0dPnTkA/1FxPm6GV3bst264z1NVh4G4+J0o/jIpLKA9lEd5QbBUgnLnNIBGGBeEghYCeof/yZnekCntYd75tIAiaIkwBzaYu3n5wfxpEVUIDngTh+biH4EU4iq+Kxrg/KxMi+MetFWL6EVJgtIUarjr2wkBYmKAOEkNvyXWkIEJqUn0xuQSGmqGyNxRjoAPv+6i9QR7KnTCaEPOrEzwKyxhzOL33acBrmaymRFC7EznmaTIHMzGqBcaj3rljC6Kk5bnepSzncNTT8C4v8MuJZPF+oYPN5n16Xy4odAJlt1+pWsuAbhB6Gk/l5Z0zoKjIIuH2LkMWkm2MDO3qbmuu9qfEWg1Y+MmbhnVQf+1qRO7i0vMt9WP5X6IDPkBeXYibUiFZVwYY2AmBchRCD7XvIL1+0JEGQadtAR8EJWNPKCpRgl3p9WTyMVtGgob/UEzknRJWDAYk4u3R4yiMw+shqdc/osRyjoadVQZFZs/80QqLTBUFkR3XlBfNmyywtu3ux9PNnCEgoPO28K6EWj70UaujN87ByjFQ1b4n+wuWwFkp5PTJYLSHgXI8oR29VB9xk4mmKNU4MnAApokgbs4Gqb3jY6KHm5t/MIMqYcrOrqT8OYqwpvfie1FMLXvvtowcgVnUup7vOAaq9mafZpJI=
|
||||||
|
distributions: "bdist_wheel"
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
repo: kbumsik/VirtScreen
|
||||||
|
|
@ -1,23 +1,29 @@
|
||||||
# Or bionic
|
# Or bionic
|
||||||
FROM ubuntu:latest
|
FROM ubuntu:bionic
|
||||||
LABEL author="Bumsik Kim <k.bumsik@gmail.com>"
|
LABEL author="Bumsik Kim <k.bumsik@gmail.com>"
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
CMD ["/bin/bash"]
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y python3-all python3-pip fakeroot debmake debhelper fakeroot wget tar && \
|
apt-get install -y python3-all python3-pip python3-wheel fakeroot debmake debhelper fakeroot wget tar curl && \
|
||||||
apt-get autoremove -y && \
|
apt-get autoremove -y && \
|
||||||
ln /usr/bin/python3 /usr/bin/python && \
|
ln /usr/bin/python3 /usr/bin/python && \
|
||||||
ln /usr/bin/pip3 /usr/bin/pip && \
|
ln /usr/bin/pip3 /usr/bin/pip && \
|
||||||
rm -rf /var/cache/apt/archives/*.deb && \
|
rm -rf /var/cache/apt/archives/*.deb && \
|
||||||
pip install virtualenv && \
|
pip install virtualenv && \
|
||||||
pip install --upgrade pip setuptools && \
|
pip install --upgrade pip setuptools
|
||||||
useradd -c Builder -m -U builder
|
|
||||||
|
|
||||||
USER builder
|
|
||||||
|
|
||||||
# Get Miniconda and make it the main Python interpreter
|
# Get Miniconda and make it the main Python interpreter
|
||||||
RUN wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh && \
|
RUN wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh && \
|
||||||
bash ~/miniconda.sh -b -p ~/miniconda && \
|
bash ~/miniconda.sh -b -p ~/miniconda && \
|
||||||
rm ~/miniconda.sh
|
rm ~/miniconda.sh
|
||||||
|
|
||||||
|
# AppImageKit
|
||||||
|
WORKDIR /opt
|
||||||
|
RUN wget https://github.com/AppImage/AppImageKit/releases/download/10/appimagetool-x86_64.AppImage && \
|
||||||
|
chmod a+x appimagetool-x86_64.AppImage && \
|
||||||
|
./appimagetool-x86_64.AppImage --appimage-extract && \
|
||||||
|
mv squashfs-root appimagetool && \
|
||||||
|
rm appimagetool-x86_64.AppImage
|
||||||
|
ENV PATH=/opt/appimagetool/usr/bin:$PATH
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["/bin/bash"]
|
||||||
|
|
@ -6,4 +6,4 @@ include LICENSE.txt
|
||||||
|
|
||||||
# Include data directories
|
# Include data directories
|
||||||
include data/virtscreen.png
|
include data/virtscreen.png
|
||||||
include data/virtscreen.desktop
|
include virtscreen.desktop
|
||||||
|
|
|
||||||
143
Makefile
|
|
@ -1,66 +1,93 @@
|
||||||
# See https://packaging.python.org/tutorials/distributing-packages/#packaging-your-project
|
# See https://packaging.python.org/tutorials/distributing-packages/#packaging-your-project
|
||||||
# for python packaging reference.
|
# for python packaging reference.
|
||||||
|
VERSION ?= 0.3.1
|
||||||
|
|
||||||
.PHONY:
|
DOCKER_NAME=kbumsik/virtscreen
|
||||||
|
DOCKER_RUN=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME)
|
||||||
|
DOCKER_RUN_TTY=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME)
|
||||||
|
|
||||||
python-wheel:
|
PKG_APPIMAGE=package/appimage/VirtScreen.AppImage
|
||||||
/usr/bin/python3 setup.py bdist_wheel --universal
|
PKG_DEBIAN=package/debian/virtscreen.deb
|
||||||
|
ARCHIVE=virtscreen-$(VERSION).tar.gz
|
||||||
python-install:
|
|
||||||
/usr/bin/pip3 install . --user
|
|
||||||
|
|
||||||
python-uninstall:
|
|
||||||
/usr/bin/pip3 uninstall virtscreen
|
|
||||||
|
|
||||||
python-clean:
|
|
||||||
rm -rf build dist virtscreen.egg-info virtscreen/qml/*.qmlc
|
|
||||||
|
|
||||||
pip-upload: python-wheel
|
|
||||||
twine upload dist/*
|
|
||||||
|
|
||||||
.ONESHELL:
|
.ONESHELL:
|
||||||
|
|
||||||
# For Debian packaging, https://www.debian.org/doc/manuals/debmake-doc/ch08.en.html#setup-py
|
.PHONY: run debug run-appimage debug-appimage
|
||||||
deb-docker-build:
|
|
||||||
docker build -f package/debian/Dockerfile -t debmake .
|
|
||||||
|
|
||||||
deb-docker:
|
all: package/pypi/*.whl $(ARCHIVE) $(PKG_APPIMAGE) $(PKG_DEBIAN)
|
||||||
docker run --privileged --interactive --tty -v $(shell pwd)/package/debian:/app debmake /bin/bash
|
|
||||||
|
|
||||||
deb-docker-rm:
|
# Run script
|
||||||
docker image rm -f debmake
|
run:
|
||||||
|
python3 -m virtscreen
|
||||||
|
|
||||||
deb-make:
|
debug:
|
||||||
docker run --privileged --interactive --tty --rm -v $(shell pwd)/package/debian:/app debmake /app/debmake.sh
|
QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 python3 -m virtscreen --log=DEBUG
|
||||||
|
|
||||||
deb-build: deb-clean deb-make
|
run-appimage: $(PKG_APPIMAGE)
|
||||||
package/debian/copy_debian.sh
|
$<
|
||||||
docker run --privileged --interactive --tty --rm -v $(shell pwd)/package/debian:/app debmake /app/debuild.sh
|
|
||||||
|
|
||||||
deb-contents:
|
debug-appimage: $(PKG_APPIMAGE)
|
||||||
docker run --privileged --interactive --tty --rm -v $(shell pwd)/package/debian:/app debmake /app/contents.sh
|
QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 $< --log=DEBUG
|
||||||
|
|
||||||
deb-env-make:
|
# tar.gz
|
||||||
docker run --privileged --interactive --tty --rm -v $(shell pwd)/package/debian:/app debmake /app/debmake.sh virtualenv
|
.PHONY: archive
|
||||||
|
|
||||||
deb-env-build: deb-clean deb-env-make
|
archive $(ARCHIVE):
|
||||||
package/debian/copy_debian.sh virtualenv
|
git archive --format=tar.gz --prefix=virtscreen-$(VERSION)/ -o $@ HEAD
|
||||||
docker run --privileged --interactive --tty --rm -v $(shell pwd)/package/debian:/app debmake /app/debuild.sh virtualenv
|
|
||||||
|
# Docker tools
|
||||||
|
.PHONY: docker docker-build
|
||||||
|
|
||||||
|
docker:
|
||||||
|
$(DOCKER_RUN_TTY) /bin/bash
|
||||||
|
|
||||||
|
docker-build:
|
||||||
|
docker build -f Dockerfile -t $(DOCKER_NAME) .
|
||||||
|
|
||||||
|
# Python wheel package for PyPI
|
||||||
|
.PHONY: wheel-clean
|
||||||
|
|
||||||
|
package/pypi/%.whl:
|
||||||
|
python3 setup.py bdist_wheel --universal
|
||||||
|
cp dist/* package/pypi
|
||||||
|
-rm -rf build dist *.egg-info
|
||||||
|
|
||||||
|
wheel-clean:
|
||||||
|
-rm package/pypi/virtscreen*.whl
|
||||||
|
|
||||||
|
# For AppImage packaging, https://github.com/AppImage/AppImageKit/wiki/Creating-AppImages
|
||||||
|
.PHONY: appimage-clean
|
||||||
|
.SECONDARY: $(PKG_APPIMAGE)
|
||||||
|
|
||||||
|
$(PKG_APPIMAGE):
|
||||||
|
$(DOCKER_RUN) package/appimage/build.sh
|
||||||
|
$(DOCKER_RUN) mv package/appimage/VirtScreen-x86_64.AppImage $@
|
||||||
|
$(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/appimage
|
||||||
|
|
||||||
|
appimage-clean:
|
||||||
|
-rm -rf package/appimage/virtscreen.AppDir $(PKG_APPIMAGE)
|
||||||
|
|
||||||
|
# For Debian packaging, https://www.debian.org/doc/manuals/maint-guide/index.en.html
|
||||||
|
# https://www.debian.org/doc/manuals/debmake-doc/ch08.en.html#setup-py
|
||||||
|
.PHONY: deb-contents deb-clean
|
||||||
|
|
||||||
|
$(PKG_DEBIAN): $(PKG_APPIMAGE) $(ARCHIVE)
|
||||||
|
$(DOCKER_RUN) package/debian/build.sh
|
||||||
|
$(DOCKER_RUN) mv package/debian/*.deb $@
|
||||||
|
$(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/debian
|
||||||
|
|
||||||
|
deb-contents: $(PKG_DEBIAN)
|
||||||
|
$(DOCKER_RUN) dpkg -c $<
|
||||||
|
|
||||||
deb-clean:
|
deb-clean:
|
||||||
rm -rf package/debian/build
|
rm -rf package/debian/build package/debian/*.deb package/debian/*.buildinfo \
|
||||||
|
package/debian/*.changes
|
||||||
|
|
||||||
# For AUR: https://wiki.archlinux.org/index.php/Python_package_guidelines
|
# For AUR: https://wiki.archlinux.org/index.php/Python_package_guidelines
|
||||||
# and: https://wiki.archlinux.org/index.php/Creating_packages
|
# and: https://wiki.archlinux.org/index.php/Creating_packages
|
||||||
arch-update:
|
.PHONY: arch-upload arch-clean
|
||||||
cd package/archlinux
|
|
||||||
makepkg --printsrcinfo > .SRCINFO
|
|
||||||
|
|
||||||
arch-install: arch-update
|
arch-upload: package/archlinux/.SRCINFO
|
||||||
cd package/archlinux
|
|
||||||
makepkg -si
|
|
||||||
|
|
||||||
arch-upload: arch-update
|
|
||||||
cd package/archlinux
|
cd package/archlinux
|
||||||
git clone ssh://aur@aur.archlinux.org/virtscreen.git
|
git clone ssh://aur@aur.archlinux.org/virtscreen.git
|
||||||
cp PKGBUILD virtscreen
|
cp PKGBUILD virtscreen
|
||||||
|
|
@ -72,11 +99,33 @@ arch-upload: arch-update
|
||||||
cd ..
|
cd ..
|
||||||
rm -rf virtscreen
|
rm -rf virtscreen
|
||||||
|
|
||||||
|
package/archlinux/.SRCINFO:
|
||||||
|
cd package/archlinux
|
||||||
|
makepkg --printsrcinfo > .SRCINFO
|
||||||
|
|
||||||
arch-clean:
|
arch-clean:
|
||||||
cd package/archlinux
|
cd package/archlinux
|
||||||
rm -rf pkg src *.tar*
|
-rm -rf pkg src *.tar* .SRCINFO
|
||||||
|
|
||||||
launch:
|
# Override version
|
||||||
./launch.sh
|
.PHONY: override-version
|
||||||
|
|
||||||
clean: arch-clean deb-clean python-clean
|
override-version:
|
||||||
|
# Update python setup.py
|
||||||
|
perl -pi -e "s/version=\'\d+\.\d+\.\d+\'/version=\'$(VERSION)\'/" \
|
||||||
|
setup.py
|
||||||
|
# Update .json files in the module
|
||||||
|
perl -pi -e "s/\"version\"\s*\:\s*\"\d+\.\d+\.\d+\"/\"version\"\: \"$(VERSION)\"/" \
|
||||||
|
virtscreen/assets/data.json
|
||||||
|
perl -pi -e "s/\"version\"\s*\:\s*\"\d+\.\d+\.\d+\"/\"version\"\: \"$(VERSION)\"/" \
|
||||||
|
virtscreen/assets/config.default.json
|
||||||
|
# Arch AUR
|
||||||
|
perl -pi -e "s/pkgver=\d+\.\d+\.\d+/pkgver=$(VERSION)/" \
|
||||||
|
package/archlinux/PKGBUILD
|
||||||
|
# Debian
|
||||||
|
perl -pi -e "s/PKGVER=\d+\.\d+\.\d+/PKGVER=$(VERSION)/" \
|
||||||
|
package/debian/build.sh
|
||||||
|
|
||||||
|
# Clean packages
|
||||||
|
clean: appimage-clean arch-clean deb-clean wheel-clean
|
||||||
|
-rm -f $(ARCHIVE)
|
||||||
|
|
|
||||||
110
README.md
|
|
@ -1,54 +1,116 @@
|
||||||
# VirtScreen
|
<h1 align="center">
|
||||||
|
<img src="data/icon_full.svg" width="21%">
|
||||||
|
<br/>
|
||||||
|
VirtScreen
|
||||||
|
</h1>
|
||||||
|
|
||||||
> Make your iPad/tablet/computer as a secondary monitor on Linux.
|
<h4 align="center">
|
||||||
|
Make your iPad/tablet/computer as a secondary monitor on Linux.
|
||||||
|
</h4>
|
||||||
|
|
||||||

|
<div align="center">
|
||||||
|
<a href="https://github.com/kbumsik/VirtScreen">
|
||||||
|
<img src="https://raw.githubusercontent.com/kbumsik/VirtScreen/master/data/gif_example.gif" alt="VirtScreen" width="80%">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
VirtScreen is an easy-to-use Linux GUI app that creates a virtual secondary screen and shares it through VNC.
|
VirtScreen is an easy-to-use Linux GUI app that creates a virtual secondary screen and shares it through VNC.
|
||||||
|
|
||||||
VirtScreen is based on [PyQt5](https://www.riverbankcomputing.com/software/pyqt/intro) and [Twisted](https://twistedmatrix.com) in Python side and uses [x11vnc](https://github.com/LibVNC/x11vnc) and XRandR.
|
VirtScreen is based on [PyQt5](https://www.riverbankcomputing.com/software/pyqt/intro) and [asyncio](https://docs.python.org/3/library/asyncio.html) in Python side and uses [x11vnc](https://github.com/LibVNC/x11vnc) and XRandR.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* No more typing commands - create a second VNC screen with a few clicks from the GUI.
|
||||||
|
* ...But there is also command-line only options for CLI lovers.
|
||||||
|
* Highly configurable - resolutions, portrait mode, and HiDPI mode.
|
||||||
|
* Works on any Linux Distro with Xorg
|
||||||
|
* Lightweight
|
||||||
|
* System Tray Icon
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
Upon installation (see Installing section to install), there will be a desktop entry called `VirtScreen`
|
1. Run the app.
|
||||||
|
2. Set options (resolution etc.) and enable the virtual screen.
|
||||||
|
3. Go to VNC tab and then start the VNC server.
|
||||||
|
4. Run your favorite VNC client app on your second device and connect it to the IP address appeared on the app.
|
||||||
|
|
||||||

|
### CLI-only option
|
||||||
|
|
||||||
Or you can run it using a command line:
|
You can run VirtScreen with `virtscreen` (or `./VirtScreen.AppImage` if you use the AppImage package) with additional arguments.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ virtscreen
|
usage: virtscreen [-h] [--auto] [--left] [--right] [--above] [--below]
|
||||||
|
[--portrait] [--hidpi]
|
||||||
|
|
||||||
|
Make your iPad/tablet/computer as a secondary monitor on Linux.
|
||||||
|
|
||||||
|
You can start VirtScreen in the following two modes:
|
||||||
|
|
||||||
|
- GUI mode: A system tray icon will appear when no argument passed.
|
||||||
|
You need to use this first to configure a virtual screen.
|
||||||
|
- CLI mode: After configured the virtual screen, you can start VirtScreen
|
||||||
|
in CLI mode if you do not want a GUI, by passing any arguments
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--auto create a virtual screen automatically using previous
|
||||||
|
settings (from both GUI mode and CLI mode)
|
||||||
|
--left a virtual screen will be created left to the primary
|
||||||
|
monitor
|
||||||
|
--right right to the primary monitor
|
||||||
|
--above, --up above the primary monitor
|
||||||
|
--below, --down below the primary monitor
|
||||||
|
--portrait Portrait mode. Width and height of the screen are swapped
|
||||||
|
--hidpi HiDPI mode. Width and height are doubled
|
||||||
|
|
||||||
|
example:
|
||||||
|
virtscreen # GUI mode. You need to use this first
|
||||||
|
# to configure the screen
|
||||||
|
virtscreen --auto # CLI mode. Scrren will be created using previous
|
||||||
|
# settings (from both GUI mode and CLI mode)
|
||||||
|
virtscreen --left # CLI mode. On the left to the primary monitor
|
||||||
|
virtscreen --below # CLI mode. Below the primary monitor.
|
||||||
|
virtscreen --below --portrait # Below, and portrait mode.
|
||||||
|
virtscreen --below --portrait --hipdi # Below, portrait, HiDPI mode.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
### Universal package (AppImage)
|
||||||
|
|
||||||
|
Download a `.AppImage` package from [releases page](https://github.com/kbumsik/VirtScreen/releases). Then make it executable:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
chmod a+x VirtScreen.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can run it by double click the file or `./VirtScreen.AppImage` in terminal.
|
||||||
|
|
||||||
### Debian (Ubuntu)
|
### Debian (Ubuntu)
|
||||||
|
|
||||||
A PPA package will be available soon.
|
Download a `.deb` package from [releases page](https://github.com/kbumsik/VirtScreen/releases). Then install it:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install x11vnc
|
||||||
|
sudo dpkg -i virtscreen.deb
|
||||||
|
rm virtscreen.deb
|
||||||
|
```
|
||||||
|
|
||||||
### Arch Linux (AUR)
|
### Arch Linux (AUR)
|
||||||
|
|
||||||
There is [`virtscreen` AUR package](https://aur.archlinux.org/packages/virtscreen/) available. Though there are many ways to install the AUR package, one of the easiest way is to use [`aurman`](https://github.com/polygamma/aurman) AUR helper:
|
There is [`virtscreen` AUR package](https://aur.archlinux.org/packages/virtscreen/) available. Though there are many ways to install the AUR package, one of the easiest way is to use [`yaourt`](https://github.com/polygamma/aurman) AUR helper:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ aurman -S virtscreen
|
yaourt virtscreen
|
||||||
```
|
```
|
||||||
|
|
||||||
### Python `pip`
|
### Python `pip`
|
||||||
|
|
||||||
If your distro is none of above, you may install it using `pip`. In this case, you need to install the dependancies manually.
|
Although not recommended, you may install it using `pip`. In this case, you need to install the dependancy (`xrandr` and `x11vnc`) manually.
|
||||||
|
|
||||||
#### Dependancies
|
|
||||||
|
|
||||||
You need [`x11vnc`](https://github.com/LibVNC/x11vnc), `xrandr`, and PyQt5 libraries. To install (e.g. on Ubuntu):
|
|
||||||
```bash
|
|
||||||
$ sudo apt-get install x11vnc qtbase5-dev # On Debian/Ubuntu, xrandr is included.
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Installing
|
|
||||||
|
|
||||||
After you install the dependancies then run:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sudo pip install virtscreen
|
sudo pip install virtscreen
|
||||||
```
|
```
|
||||||
|
|
|
||||||
BIN
data/icon.xcf
182
data/icon_full.svg
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="122.79081mm"
|
||||||
|
height="122.79081mm"
|
||||||
|
viewBox="0 0 122.79081 122.79081"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="0.92.2 2405546, 2018-03-11"
|
||||||
|
sodipodi:docname="icon_full.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient1066">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#6f8a91;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop1062" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#6f8a91;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop1064" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient4555">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#a4b5b9;stop-opacity:1"
|
||||||
|
offset="0"
|
||||||
|
id="stop4551" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#8ba1a6;stop-opacity:1"
|
||||||
|
offset="1"
|
||||||
|
id="stop4553" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient1066"
|
||||||
|
id="linearGradient1068"
|
||||||
|
x1="145.00168"
|
||||||
|
y1="321.47601"
|
||||||
|
x2="683.82404"
|
||||||
|
y2="482.69577"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="scale(0.26458334)" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient4555"
|
||||||
|
id="linearGradient837"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="98.37323"
|
||||||
|
y1="80.037163"
|
||||||
|
x2="132.22571"
|
||||||
|
y2="124.80769"
|
||||||
|
gradientTransform="translate(0.21652862,-1.4950989)" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient4555"
|
||||||
|
id="linearGradient842"
|
||||||
|
x1="26.029924"
|
||||||
|
y1="31.875429"
|
||||||
|
x2="148.82074"
|
||||||
|
y2="154.66602"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(3.0621348)" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="1.2219349"
|
||||||
|
inkscape:cx="68.62512"
|
||||||
|
inkscape:cy="261.78697"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
showborder="false"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1034"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:object-nodes="false"
|
||||||
|
inkscape:snap-smooth-nodes="false"
|
||||||
|
inkscape:object-paths="false"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:guide-bbox="true"
|
||||||
|
inkscape:snap-intersection-paths="true"
|
||||||
|
inkscape:snap-nodes="true"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:pagecheckerboard="true" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(53.98708,-31.875429)">
|
||||||
|
<path
|
||||||
|
id="path1014"
|
||||||
|
d="m 130.43648,59.620133 -74.572192,3.962548 -7.597984,42.012419 10.291879,10.29188 -16.295647,2.88458 35.894448,35.89445 H 154.94506 V 84.128715 Z"
|
||||||
|
style="fill:none;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<g
|
||||||
|
id="g888"
|
||||||
|
transform="translate(-83.079173)"
|
||||||
|
inkscape:export-filename="/home/kbumsik/Dropbox/Projects/VirtScreen/virtscreen/icon/full.png"
|
||||||
|
inkscape:export-xdpi="52.955105"
|
||||||
|
inkscape:export-ydpi="52.955105">
|
||||||
|
<rect
|
||||||
|
y="31.875429"
|
||||||
|
x="29.092093"
|
||||||
|
height="122.79081"
|
||||||
|
width="122.79081"
|
||||||
|
id="rect834"
|
||||||
|
style="opacity:1;fill:url(#linearGradient842);fill-opacity:1;stroke:none;stroke-width:6.35238171;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" />
|
||||||
|
<path
|
||||||
|
id="path1009"
|
||||||
|
d="m 130.43647,59.61999 -74.572189,3.962548 -7.597988,42.012422 10.29188,10.29188 -16.295648,2.88458 35.89445,35.89445 H 151.88271 V 81.066226 Z"
|
||||||
|
style="fill:url(#linearGradient1068);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<g
|
||||||
|
id="g877">
|
||||||
|
<g
|
||||||
|
id="g865">
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8.24230289;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||||
|
d="m 55.14522,57.599343 c -3.811016,0 -6.878886,3.518771 -6.878886,7.890732 V 105.59529 H 58.610348 V 67.196961 h 63.754312 v 38.398329 h 10.34401 V 65.490075 c 0,-4.371961 -3.06787,-7.890732 -6.87889,-7.890732 z"
|
||||||
|
id="path1016"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8.42327976;stroke-linejoin:bevel;stroke-miterlimit:3.20000005;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 39.826996,110.15428 v 2.68424 c 0,4.48975 3.421591,8.10409 7.671187,8.10409 h 85.979207 c 4.2496,0 7.67061,-3.61434 7.67061,-8.10409 v -2.68424 z"
|
||||||
|
id="path1018"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g869">
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:7.00793839;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1020"
|
||||||
|
width="41.864029"
|
||||||
|
height="53.648178"
|
||||||
|
x="96.424461"
|
||||||
|
y="75.294144"
|
||||||
|
ry="4.7652273" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:url(#linearGradient837);fill-opacity:1;stroke:none;stroke-width:5.28829765;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1022"
|
||||||
|
width="30.171503"
|
||||||
|
height="42.388714"
|
||||||
|
x="102.27074"
|
||||||
|
y="80.923874"
|
||||||
|
ry="2.2456675e-14" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.7 KiB |
180
data/systray_icon.svg
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="104.16939mm"
|
||||||
|
height="104.16939mm"
|
||||||
|
viewBox="0 0 104.16939 104.16939"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="0.92.2 2405546, 2018-03-11"
|
||||||
|
sodipodi:docname="systray_icon.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient1066">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#6f8a91;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop1062" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#6f8a91;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop1064" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient4555">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#a4b5b9;stop-opacity:1"
|
||||||
|
offset="0"
|
||||||
|
id="stop4551" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#8ba1a6;stop-opacity:1"
|
||||||
|
offset="1"
|
||||||
|
id="stop4553" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient1066"
|
||||||
|
id="linearGradient1068"
|
||||||
|
x1="145.00168"
|
||||||
|
y1="321.47601"
|
||||||
|
x2="683.82404"
|
||||||
|
y2="482.69577"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(-109.95437,-120.47406)" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient4555"
|
||||||
|
id="linearGradient837"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="98.37323"
|
||||||
|
y1="80.037163"
|
||||||
|
x2="132.22571"
|
||||||
|
y2="124.80769"
|
||||||
|
gradientTransform="translate(0.21652862,-1.4950989)" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient4555"
|
||||||
|
id="linearGradient842"
|
||||||
|
x1="26.029924"
|
||||||
|
y1="31.875429"
|
||||||
|
x2="148.82074"
|
||||||
|
y2="154.66602"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.8483484,0,0,0.8483484,16.320331,14.144671)" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="1.2219349"
|
||||||
|
inkscape:cx="-145.53325"
|
||||||
|
inkscape:cy="226.5969"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
showborder="false"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1034"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:object-nodes="false"
|
||||||
|
inkscape:snap-smooth-nodes="false"
|
||||||
|
inkscape:object-paths="false"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:guide-bbox="true"
|
||||||
|
inkscape:snap-intersection-paths="true"
|
||||||
|
inkscape:snap-nodes="true"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:pagecheckerboard="true" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-38.402805,-41.186142)">
|
||||||
|
<path
|
||||||
|
id="path1014"
|
||||||
|
d="m 130.43648,59.620133 -74.572192,3.962548 -7.597984,42.012419 10.291879,10.29188 -16.295647,2.88458 35.894448,35.89445 H 154.94506 V 84.128715 Z"
|
||||||
|
style="fill:none;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<g
|
||||||
|
id="g953">
|
||||||
|
<rect
|
||||||
|
y="41.186142"
|
||||||
|
x="38.402805"
|
||||||
|
height="104.16939"
|
||||||
|
width="104.16939"
|
||||||
|
id="rect834"
|
||||||
|
style="opacity:1;fill:url(#linearGradient842);fill-opacity:1;stroke:none;stroke-width:5.38903284;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" />
|
||||||
|
<path
|
||||||
|
id="path1009"
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458333,29.092093,31.875429)"
|
||||||
|
d="M 383.0332,104.86133 101.18555,119.83789 72.46875,278.625 111.36719,317.52344 49.777344,328.42578 150.25195,428.90039 H 428.90039 V 150.72852 Z"
|
||||||
|
style="fill:url(#linearGradient1068);fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<g
|
||||||
|
id="g877">
|
||||||
|
<g
|
||||||
|
id="g865">
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8.24230289;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||||
|
d="m 55.14522,57.599343 c -3.811016,0 -6.878886,3.518771 -6.878886,7.890732 V 105.59529 H 58.610348 V 67.196961 h 63.754312 v 38.398329 h 10.34401 V 65.490075 c 0,-4.371961 -3.06787,-7.890732 -6.87889,-7.890732 z"
|
||||||
|
id="path1016"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8.42327976;stroke-linejoin:bevel;stroke-miterlimit:3.20000005;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 39.826996,110.15428 v 2.68424 c 0,4.48975 3.421591,8.10409 7.671187,8.10409 h 85.979207 c 4.2496,0 7.67061,-3.61434 7.67061,-8.10409 v -2.68424 z"
|
||||||
|
id="path1018"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g869">
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:7.00793839;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1020"
|
||||||
|
width="41.864029"
|
||||||
|
height="53.648178"
|
||||||
|
x="96.424461"
|
||||||
|
y="75.294144"
|
||||||
|
ry="4.7652273" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:url(#linearGradient837);fill-opacity:1;stroke:none;stroke-width:5.28829765;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1022"
|
||||||
|
width="30.171503"
|
||||||
|
height="42.388714"
|
||||||
|
x="102.27074"
|
||||||
|
y="80.923874"
|
||||||
|
ry="2.2456675e-14" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 21 KiB |
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# Script to run virtscreen locally
|
|
||||||
# This is not intended to be included in the distributed package
|
|
||||||
virtscreen/virtscreen.py
|
|
||||||
2
package/appimage/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.AppImage
|
||||||
|
*.AppDir
|
||||||
14
package/appimage/AppRun
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# This script is only for isolated miniconda environment
|
||||||
|
# Used in AppImage package
|
||||||
|
SCRIPTDIR=$(dirname $0)
|
||||||
|
ENV=$SCRIPTDIR/usr/share/virtscreen/env
|
||||||
|
|
||||||
|
export PYTHONPATH=$ENV/lib/python3.6
|
||||||
|
export LD_LIBRARY_PATH=$ENV/lib
|
||||||
|
export QT_PLUGIN_PATH=$ENV/lib/python3.6/site-packages/PyQt5/Qt/plugins
|
||||||
|
export QML2_IMPORT_PATH=$ENV/lib/python3.6/site-packages/PyQt5/Qt/qml
|
||||||
|
# export QT_QPA_FONTDIR=/usr/share/fonts
|
||||||
|
# export QT_XKB_CONFIG_ROOT=/usr/share/X11/xkb
|
||||||
|
|
||||||
|
$ENV/bin/python3 $ENV/bin/virtscreen $@
|
||||||
32
package/appimage/build.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Directory
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
ROOT=$DIR/../..
|
||||||
|
|
||||||
|
cd $ROOT/package/appimage
|
||||||
|
mkdir virtscreen.AppDir
|
||||||
|
cd virtscreen.AppDir
|
||||||
|
# Create virtualenv
|
||||||
|
install -d usr/share/virtscreen
|
||||||
|
source $HOME/miniconda/bin/activate && \
|
||||||
|
conda create -y --copy --prefix usr/share/virtscreen/env python=3.6
|
||||||
|
# Install VirtScreen using pip
|
||||||
|
source $HOME/miniconda/bin/activate && \
|
||||||
|
source activate usr/share/virtscreen/env && \
|
||||||
|
pip install $ROOT
|
||||||
|
# Delete unnecessary installed files done by setup.py
|
||||||
|
rm -rf usr/share/virtscreen/env/lib/python3.6/site-packages/usr
|
||||||
|
# Copy desktop entry, icon, and AppRun
|
||||||
|
install -m 644 -D $ROOT/virtscreen.desktop \
|
||||||
|
usr/share/applications/virtscreen.desktop
|
||||||
|
install -m 644 -D $ROOT/virtscreen.desktop \
|
||||||
|
.
|
||||||
|
install -m 644 -D $ROOT/data/virtscreen.png \
|
||||||
|
usr/share/pixmaps/virtscreen.png
|
||||||
|
install -m 644 -D $ROOT/data/virtscreen.png \
|
||||||
|
.
|
||||||
|
install -m 755 -D $ROOT/package/appimage/AppRun \
|
||||||
|
.
|
||||||
|
cd ..
|
||||||
|
appimagetool virtscreen.AppDir
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
pkgbase = virtscreen
|
|
||||||
pkgdesc = Make your iPad/tablet/computer as a secondary monitor on Linux
|
|
||||||
pkgver = 0.1.3
|
|
||||||
pkgrel = 1
|
|
||||||
url = https://github.com/kbumsik/VirtScreen
|
|
||||||
arch = i686
|
|
||||||
arch = x86_64
|
|
||||||
license = GPL
|
|
||||||
makedepends = python-setuptools
|
|
||||||
depends = xorg-xrandr
|
|
||||||
depends = x11vnc
|
|
||||||
depends = python-pyqt5
|
|
||||||
depends = python-twisted
|
|
||||||
depends = python-netifaces
|
|
||||||
depends = python-qt5reactor
|
|
||||||
optdepends = arandr: for display settings option
|
|
||||||
provides = virtscreen
|
|
||||||
source = https://github.com/kbumsik/VirtScreen/archive/0.1.3.tar.gz
|
|
||||||
sha256sums = 79cd7a07fc5eb9d6034812cca39612cb1cbef109bd2c8e939a45e2186a82cac2
|
|
||||||
|
|
||||||
pkgname = virtscreen
|
|
||||||
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
# Maintainer: Bumsik Kim <k.bumsik@gmail.com>
|
# Maintainer: Bumsik Kim <k.bumsik@gmail.com>
|
||||||
_pkgname_camelcase=VirtScreen
|
_pkgname_camelcase=VirtScreen
|
||||||
pkgname=virtscreen
|
pkgname=virtscreen
|
||||||
pkgver=0.1.3
|
pkgver=0.3.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Make your iPad/tablet/computer as a secondary monitor on Linux"
|
pkgdesc="Make your iPad/tablet/computer as a secondary monitor on Linux"
|
||||||
arch=("i686" "x86_64")
|
arch=("i686" "x86_64")
|
||||||
url="https://github.com/kbumsik/VirtScreen"
|
url="https://github.com/kbumsik/VirtScreen"
|
||||||
license=('GPL')
|
license=('GPL')
|
||||||
groups=()
|
groups=()
|
||||||
depends=('xorg-xrandr' 'x11vnc' 'python-pyqt5' 'python-twisted' 'python-netifaces' 'python-qt5reactor')
|
depends=('xorg-xrandr' 'x11vnc' 'python-pyqt5' 'qt5-quickcontrols2' 'python-quamash-git' 'python-netifaces')
|
||||||
makedepends=('python-setuptools')
|
makedepends=('python-pip' 'perl')
|
||||||
optdepends=(
|
optdepends=(
|
||||||
'arandr: for display settings option'
|
'arandr: for display settings option'
|
||||||
)
|
)
|
||||||
|
|
@ -20,19 +20,21 @@ backup=()
|
||||||
options=()
|
options=()
|
||||||
install=
|
install=
|
||||||
changelog=
|
changelog=
|
||||||
source=(https://github.com/kbumsik/$_pkgname_camelcase/archive/$pkgver.tar.gz)
|
source=(src::git+https://github.com/kbumsik/$_pkgname_camelcase.git#tag=$pkgver)
|
||||||
noextract=()
|
noextract=()
|
||||||
sha256sums=('79cd7a07fc5eb9d6034812cca39612cb1cbef109bd2c8e939a45e2186a82cac2')
|
md5sums=('SKIP')
|
||||||
|
|
||||||
build() {
|
prepare() {
|
||||||
echo "$pkgdir"
|
cd $srcdir/src
|
||||||
cd $_pkgname_camelcase-$pkgver
|
# Delete PyQt5 from install_requires because python-pyqt5 does not have PyPI metadata.
|
||||||
/usr/bin/python3 setup.py build
|
# See https://bugs.archlinux.org/task/58887
|
||||||
|
perl -pi -e "s/\'PyQt5>=\d+\.\d+\.\d+\',//" \
|
||||||
|
setup.py
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cd $_pkgname_camelcase-$pkgver
|
cd $srcdir/src
|
||||||
/usr/bin/python3 setup.py install --root="$pkgdir/" --optimize=1 --skip-build
|
PIP_CONFIG_FILE=/dev/null /usr/bin/pip install --isolated --root="$pkgdir" --ignore-installed --ignore-requires-python --no-deps .
|
||||||
# These are already installed by setup.py
|
# These are already installed by setup.py
|
||||||
# install -Dm644 "data/$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop"
|
# install -Dm644 "data/$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop"
|
||||||
# install -Dm644 "data/icon.png" "$pkgdir/usr/share/pixmaps/$pkgname.png"
|
# install -Dm644 "data/icon.png" "$pkgdir/usr/share/pixmaps/$pkgname.png"
|
||||||
|
|
|
||||||
3
package/debian/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
*.deb
|
||||||
|
*.buildinfo
|
||||||
|
*.changes
|
||||||
28
package/debian/Makefile
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
prefix = /usr
|
||||||
|
|
||||||
|
all:
|
||||||
|
: # do nothing
|
||||||
|
|
||||||
|
SHELL = /bin/bash
|
||||||
|
install:
|
||||||
|
mkdir -p $(DESTDIR)$(prefix)/bin
|
||||||
|
install -m 755 VirtScreen.AppImage \
|
||||||
|
$(DESTDIR)$(prefix)/bin/virtscreen
|
||||||
|
# Copy desktop entry and icon
|
||||||
|
install -m 644 -D virtscreen.desktop \
|
||||||
|
$(DESTDIR)$(prefix)/share/applications/virtscreen.desktop
|
||||||
|
install -m 644 -D data/virtscreen.png \
|
||||||
|
$(DESTDIR)$(prefix)/share/pixmaps/virtscreen.png
|
||||||
|
|
||||||
|
clean:
|
||||||
|
: # do nothing
|
||||||
|
|
||||||
|
distclean: clean
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
: # do nothing
|
||||||
|
|
||||||
|
# override_dh_usrlocal:
|
||||||
|
# : # do nothing
|
||||||
|
|
||||||
|
.PHONY: all install clean distclean uninstall
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
prefix = /usr
|
|
||||||
|
|
||||||
all:
|
|
||||||
: # do nothing
|
|
||||||
|
|
||||||
SHELL = /bin/bash
|
|
||||||
install:
|
|
||||||
# Create virtualenv
|
|
||||||
install -d $(DESTDIR)$(prefix)/share/virtscreen
|
|
||||||
virtualenv $(DESTDIR)$(prefix)/share/virtscreen/env --always-copy
|
|
||||||
source $(HOME)/miniconda/bin/activate && \
|
|
||||||
conda create -y --copy --prefix $(DESTDIR)$(prefix)/share/virtscreen/env python=3.6
|
|
||||||
source $(HOME)/miniconda/bin/activate && \
|
|
||||||
source activate $(DESTDIR)$(prefix)/share/virtscreen/env && \
|
|
||||||
pip install .
|
|
||||||
# Fix hashbang and move executable
|
|
||||||
sed -i "1s:.*:#!$(prefix)/share/virtscreen/env/bin/python3:" \
|
|
||||||
$(DESTDIR)$(prefix)/share/virtscreen/env/bin/virtscreen
|
|
||||||
install -D $(DESTDIR)$(prefix)/share/virtscreen/env/bin/virtscreen \
|
|
||||||
$(DESTDIR)$(prefix)/bin/virtscreen
|
|
||||||
# Delete unnecessary installed files done by setup.py
|
|
||||||
rm -rf $(DESTDIR)$(prefix)/share/virtscreen/env/lib/python3.6/site-packages/usr
|
|
||||||
# Copy desktop entry and icon
|
|
||||||
install -m 644 -D data/virtscreen.desktop \
|
|
||||||
$(DESTDIR)$(prefix)/share/applications/virtscreen.desktop
|
|
||||||
install -m 644 -D data/virtscreen.png \
|
|
||||||
$(DESTDIR)$(prefix)/share/pixmaps/virtscreen.png
|
|
||||||
|
|
||||||
clean:
|
|
||||||
: # do nothing
|
|
||||||
|
|
||||||
distclean: clean
|
|
||||||
|
|
||||||
uninstall:
|
|
||||||
: # do nothing
|
|
||||||
|
|
||||||
# override_dh_usrlocal:
|
|
||||||
# : # do nothing
|
|
||||||
|
|
||||||
.PHONY: all install clean distclean uninstall
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
PKGVER=0.1.3
|
|
||||||
# Required for debmake
|
|
||||||
DEBEMAIL="k.bumsik@gmail.com"
|
|
||||||
DEBFULLNAME="Bumsik Kim"
|
|
||||||
export PKGVER DEBEMAIL DEBFULLNAME
|
|
||||||
44
package/debian/build.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PKGVER=0.3.1
|
||||||
|
# Required for debmake
|
||||||
|
DEBEMAIL="k.bumsik@gmail.com"
|
||||||
|
DEBFULLNAME="Bumsik Kim"
|
||||||
|
export PKGVER DEBEMAIL DEBFULLNAME
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
ROOT=$SCRIPT_DIR/../..
|
||||||
|
|
||||||
|
# Generate necessary files for package building (generated by debmake)
|
||||||
|
cd $ROOT/package/debian
|
||||||
|
cp $ROOT/virtscreen-$PKGVER.tar.gz .
|
||||||
|
tar -xzmf virtscreen-$PKGVER.tar.gz
|
||||||
|
cp $ROOT/package/debian/Makefile \
|
||||||
|
$ROOT/package/debian/virtscreen-$PKGVER/Makefile
|
||||||
|
cd $ROOT/package/debian/virtscreen-$PKGVER
|
||||||
|
debmake --yes -b':sh'
|
||||||
|
|
||||||
|
# copy files to build
|
||||||
|
# debmake files
|
||||||
|
mkdir -p $ROOT/package/debian/build
|
||||||
|
cp -R $ROOT/package/debian/virtscreen-$PKGVER/debian \
|
||||||
|
$ROOT/package/debian/build/debian
|
||||||
|
cp $ROOT/package/debian/Makefile \
|
||||||
|
$ROOT/package/debian/build/
|
||||||
|
cp $ROOT/package/debian/{control,README.Debian} \
|
||||||
|
$ROOT/package/debian/build/debian/
|
||||||
|
# binary and data files
|
||||||
|
cp $ROOT/package/appimage/VirtScreen.AppImage \
|
||||||
|
$ROOT/package/debian/build/
|
||||||
|
cp $ROOT/virtscreen.desktop \
|
||||||
|
$ROOT/package/debian/build/
|
||||||
|
cp -R $ROOT/data \
|
||||||
|
$ROOT/package/debian/build/
|
||||||
|
|
||||||
|
# Build .deb package
|
||||||
|
cd $ROOT/package/debian/build
|
||||||
|
dpkg-buildpackage -b
|
||||||
|
|
||||||
|
# cleanup
|
||||||
|
rm -rf $ROOT/package/debian/virtscreen-$PKGVER \
|
||||||
|
$ROOT/package/debian/*.tar.gz
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
source _common.sh
|
|
||||||
|
|
||||||
cd build
|
|
||||||
dpkg -c virtscreen_$PKGVER-1_all.deb
|
|
||||||
|
|
@ -2,15 +2,15 @@ Source: virtscreen
|
||||||
Section: utils
|
Section: utils
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Maintainer: Bumsik Kim <k.bumsik@gmail.com>
|
Maintainer: Bumsik Kim <k.bumsik@gmail.com>
|
||||||
Build-Depends: debhelper (>=9), dh-python, python3-all
|
Build-Depends: debhelper (>=9), python3-all
|
||||||
Standards-Version: 3.9.8
|
Standards-Version: 3.9.8
|
||||||
Homepage: https://github.com/kbumsik/VirtScreen
|
Homepage: https://github.com/kbumsik/VirtScreen
|
||||||
X-Python3-Version: >= 3.6
|
X-Python3-Version: >= 3.5
|
||||||
|
|
||||||
Package: virtscreen
|
Package: virtscreen
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Multi-Arch: foreign
|
Multi-Arch: foreign
|
||||||
Depends: ${misc:Depends}, ${python3:Depends}, x11vnc, python3-pyqt5, qtbase5-dev, python3-twisted, python3-netifaces
|
Depends: ${misc:Depends}, x11vnc
|
||||||
Description: Make your iPad/tablet/computer as a secondary monitor on Linux
|
Description: Make your iPad/tablet/computer as a secondary monitor on Linux
|
||||||
VirtScreen is an easy-to-use Linux GUI app that creates a virtual
|
VirtScreen is an easy-to-use Linux GUI app that creates a virtual
|
||||||
secondary screen and shares it through VNC.
|
secondary screen and shares it through VNC.
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
Source: virtscreen
|
|
||||||
Section: utils
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Bumsik Kim <k.bumsik@gmail.com>
|
|
||||||
Build-Depends: debhelper (>=9), python3-all
|
|
||||||
Standards-Version: 3.9.8
|
|
||||||
Homepage: https://github.com/kbumsik/VirtScreen
|
|
||||||
X-Python3-Version: >= 3.5
|
|
||||||
|
|
||||||
Package: virtscreen
|
|
||||||
Architecture: all
|
|
||||||
Multi-Arch: foreign
|
|
||||||
Depends: ${misc:Depends}, x11vnc, qtbase5-dev
|
|
||||||
Description: Make your iPad/tablet/computer as a secondary monitor on Linux
|
|
||||||
VirtScreen is an easy-to-use Linux GUI app that creates a virtual
|
|
||||||
secondary screen and shares it through VNC.
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
source $DIR/_common.sh
|
|
||||||
|
|
||||||
if [ $1 = "virtualenv" ]; then
|
|
||||||
cp $DIR/control.virtualenv $DIR/build/virtscreen-$PKGVER/debian/control
|
|
||||||
cp $DIR/README.Debian $DIR/build/virtscreen-$PKGVER/debian/
|
|
||||||
else
|
|
||||||
cp $DIR/{control,rules,README.Debian} $DIR/build/virtscreen-$PKGVER/debian
|
|
||||||
fi
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
source _common.sh
|
|
||||||
|
|
||||||
mkdir build
|
|
||||||
cd build
|
|
||||||
# Download
|
|
||||||
wget https://github.com/kbumsik/VirtScreen/archive/$PKGVER.tar.gz
|
|
||||||
tar -xzmf $PKGVER.tar.gz
|
|
||||||
# rename packages
|
|
||||||
mv VirtScreen-$PKGVER virtscreen-$PKGVER
|
|
||||||
mv $PKGVER.tar.gz virtscreen-$PKGVER.tar.gz
|
|
||||||
|
|
||||||
cd virtscreen-$PKGVER
|
|
||||||
if [ $1 = "virtualenv" ]; then
|
|
||||||
cp ../../Makefile.virtualenv Makefile
|
|
||||||
debmake -b':sh'
|
|
||||||
else
|
|
||||||
debmake -b':py3'
|
|
||||||
fi
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
source _common.sh
|
|
||||||
|
|
||||||
cd build
|
|
||||||
cd virtscreen-$PKGVER
|
|
||||||
if [ $1 = "virtualenv" ]; then
|
|
||||||
dpkg-buildpackage -b
|
|
||||||
else
|
|
||||||
debuild
|
|
||||||
fi
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
#!/usr/bin/make -f
|
|
||||||
# You must remove unused comment lines for the released package.
|
|
||||||
export DH_VERBOSE = 1
|
|
||||||
|
|
||||||
%:
|
|
||||||
dh $@ --with python3 --buildsystem=pybuild
|
|
||||||
|
|
||||||
#override_dh_auto_install:
|
|
||||||
# dh_auto_install -- prefix=/usr
|
|
||||||
|
|
||||||
#override_dh_install:
|
|
||||||
# dh_install --list-missing -X.pyc -X.pyo
|
|
||||||
2
package/pypi/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
virtscreen*.whl
|
||||||
|
*.tar.gz
|
||||||
16
setup.py
|
|
@ -40,7 +40,7 @@ setup(
|
||||||
# For a discussion on single-sourcing the version across setup.py and the
|
# For a discussion on single-sourcing the version across setup.py and the
|
||||||
# project code, see
|
# project code, see
|
||||||
# https://packaging.python.org/en/latest/single_source_version.html
|
# https://packaging.python.org/en/latest/single_source_version.html
|
||||||
version='0.2.0', # Required
|
version='0.3.1', # Required
|
||||||
|
|
||||||
# This is a one-line description or tagline of what your project does. This
|
# This is a one-line description or tagline of what your project does. This
|
||||||
# corresponds to the "Summary" metadata field:
|
# corresponds to the "Summary" metadata field:
|
||||||
|
|
@ -107,8 +107,8 @@ setup(
|
||||||
# Specify the Python versions you support here. In particular, ensure
|
# Specify the Python versions you support here. In particular, ensure
|
||||||
# that you indicate whether you support Python 2, Python 3 or both.
|
# that you indicate whether you support Python 2, Python 3 or both.
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.5',
|
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
'Environment :: X11 Applications',
|
'Environment :: X11 Applications',
|
||||||
|
|
@ -116,8 +116,7 @@ setup(
|
||||||
'Operating System :: POSIX :: Linux',
|
'Operating System :: POSIX :: Linux',
|
||||||
|
|
||||||
# Framework used
|
# Framework used
|
||||||
'Framework :: Twisted',
|
'Framework :: AsyncIO',
|
||||||
# 'Framework :: AsyncIO',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
# This field adds keywords for your project which will appear on the
|
# This field adds keywords for your project which will appear on the
|
||||||
|
|
@ -136,7 +135,7 @@ setup(
|
||||||
#
|
#
|
||||||
# py_modules=["my_module"],
|
# py_modules=["my_module"],
|
||||||
#
|
#
|
||||||
packages=['virtscreen'], # Required
|
packages=find_packages(), # Required
|
||||||
|
|
||||||
# This field lists other packages that your project depends on to run.
|
# This field lists other packages that your project depends on to run.
|
||||||
# Any package you put here will be installed by pip when your project is
|
# Any package you put here will be installed by pip when your project is
|
||||||
|
|
@ -145,8 +144,7 @@ setup(
|
||||||
# For an analysis of "install_requires" vs pip's requirements files see:
|
# For an analysis of "install_requires" vs pip's requirements files see:
|
||||||
# https://packaging.python.org/en/latest/requirements.html
|
# https://packaging.python.org/en/latest/requirements.html
|
||||||
install_requires=['PyQt5>=5.10.1',
|
install_requires=['PyQt5>=5.10.1',
|
||||||
'Twisted>=17.9.0',
|
'Quamash>=0.6.0',
|
||||||
'qt5reactor>=0.5',
|
|
||||||
'netifaces>=0.10.6'], # Optional
|
'netifaces>=0.10.6'], # Optional
|
||||||
|
|
||||||
# List additional groups of dependencies here (e.g. development
|
# List additional groups of dependencies here (e.g. development
|
||||||
|
|
@ -180,7 +178,7 @@ setup(
|
||||||
data_files=[
|
data_files=[
|
||||||
# Desktop entries spec:
|
# Desktop entries spec:
|
||||||
# https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/
|
# https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/
|
||||||
('share/applications', ['data/virtscreen.desktop']),
|
('share/applications', ['virtscreen.desktop']),
|
||||||
# $XDG_DATA_DIRS/icons
|
# $XDG_DATA_DIRS/icons
|
||||||
# https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout
|
# https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout
|
||||||
('share/icons', ['data/virtscreen.png']),
|
('share/icons', ['data/virtscreen.png']),
|
||||||
|
|
@ -196,7 +194,7 @@ setup(
|
||||||
# executes the function `main` from this package when invoked:
|
# executes the function `main` from this package when invoked:
|
||||||
entry_points={ # Optional
|
entry_points={ # Optional
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'virtscreen = virtscreen.virtscreen:main',
|
'virtscreen = virtscreen.__main__:main',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Encoding=UTF-8
|
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=VirtScreen
|
Name=VirtScreen
|
||||||
Comment=Make your iPad/tablet/computer as a secondary monitor on Linux
|
Comment=Make your iPad/tablet/computer as a secondary monitor on Linux
|
||||||
Exec=bash -c "export PATH=$PATH:$HOME/.local/bin; virtscreen"
|
Exec=bash -c "export PATH=\\$PATH:\\$HOME/.local/bin; virtscreen"
|
||||||
Icon=virtscreen
|
Icon=virtscreen
|
||||||
Terminal=false
|
Terminal=false
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
__all__ = ['virtscreen']
|
|
||||||
218
virtscreen/__main__.py
Executable file
|
|
@ -0,0 +1,218 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
# Python standard packages
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from typing import Callable
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# Import OpenGL library for Nvidia driver
|
||||||
|
# https://github.com/Ultimaker/Cura/pull/131#issuecomment-176088664
|
||||||
|
import ctypes
|
||||||
|
from ctypes.util import find_library
|
||||||
|
ctypes.CDLL(find_library('GL'), ctypes.RTLD_GLOBAL)
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine
|
||||||
|
from PyQt5.QtGui import QIcon
|
||||||
|
from PyQt5.QtCore import Qt, QUrl
|
||||||
|
from quamash import QEventLoop
|
||||||
|
|
||||||
|
from .display import DisplayProperty
|
||||||
|
from .xrandr import XRandR
|
||||||
|
from .qt_backend import Backend, Cursor, Network
|
||||||
|
from .path import HOME_PATH, ICON_PATH, MAIN_QML_PATH, CONFIG_PATH, LOGGING_PATH
|
||||||
|
|
||||||
|
def error(*args, **kwargs) -> None:
|
||||||
|
"""Error printing"""
|
||||||
|
args = ('Error: ', *args)
|
||||||
|
print(*args, file=sys.stderr, **kwargs)
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Start main program"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
formatter_class=argparse.RawTextHelpFormatter,
|
||||||
|
description='Make your iPad/tablet/computer as a secondary monitor on Linux.\n\n'
|
||||||
|
'You can start VirtScreen in the following two modes:\n\n'
|
||||||
|
' - GUI mode: A system tray icon will appear when no argument passed.\n'
|
||||||
|
' You need to use this first to configure a virtual screen.\n'
|
||||||
|
' - CLI mode: After configured the virtual screen, you can start VirtScreen\n'
|
||||||
|
' in CLI mode if you do not want a GUI, by passing any arguments\n',
|
||||||
|
epilog='example:\n'
|
||||||
|
'virtscreen # GUI mode. You need to use this first\n'
|
||||||
|
' to configure the screen\n'
|
||||||
|
'virtscreen --auto # CLI mode. Scrren will be created using previous\n'
|
||||||
|
' settings (from both GUI mode and CLI mode)\n'
|
||||||
|
'virtscreen --left # CLI mode. On the left to the primary monitor\n'
|
||||||
|
'virtscreen --below # CLI mode. Below the primary monitor.\n'
|
||||||
|
'virtscreen --below --portrait # Below, and portrait mode.\n'
|
||||||
|
'virtscreen --below --portrait --hipdi # Below, portrait, HiDPI mode.\n')
|
||||||
|
parser.add_argument('--auto', action='store_true',
|
||||||
|
help='create a virtual screen automatically using previous\n'
|
||||||
|
'settings (from both GUI mode and CLI mode)')
|
||||||
|
parser.add_argument('--left', action='store_true',
|
||||||
|
help='a virtual screen will be created left to the primary\n'
|
||||||
|
'monitor')
|
||||||
|
parser.add_argument('--right', action='store_true',
|
||||||
|
help='right to the primary monitor')
|
||||||
|
parser.add_argument('--above', '--up', action='store_true',
|
||||||
|
help='above the primary monitor')
|
||||||
|
parser.add_argument('--below', '--down', action='store_true',
|
||||||
|
help='below the primary monitor')
|
||||||
|
parser.add_argument('--portrait', action='store_true',
|
||||||
|
help='Portrait mode. Width and height of the screen are swapped')
|
||||||
|
parser.add_argument('--hidpi', action='store_true',
|
||||||
|
help='HiDPI mode. Width and height are doubled')
|
||||||
|
parser.add_argument('--log', type=str,
|
||||||
|
help='Python logging level, For example, --log=INFO.\n'
|
||||||
|
'Only used for reporting bugs and debugging')
|
||||||
|
# Add signal handler
|
||||||
|
def on_exit(self, signum=None, frame=None):
|
||||||
|
sys.exit(0)
|
||||||
|
for sig in [signal.SIGINT, signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]:
|
||||||
|
signal.signal(sig, on_exit)
|
||||||
|
|
||||||
|
args = vars(parser.parse_args())
|
||||||
|
cli_args = ['auto', 'left', 'right', 'above', 'below', 'portrait', 'hidpi']
|
||||||
|
# Start main
|
||||||
|
if any((value and arg in cli_args) for arg, value in args.items()):
|
||||||
|
main_cli(args)
|
||||||
|
else:
|
||||||
|
main_gui(args)
|
||||||
|
error('Program should not reach here.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def check_env(args: argparse.Namespace, msg: Callable[[str], None]) -> None:
|
||||||
|
"""Check environments and arguments before start. This also enable logging"""
|
||||||
|
if os.environ.get('XDG_SESSION_TYPE', '').lower() == 'wayland':
|
||||||
|
msg("Currently Wayland is not supported")
|
||||||
|
sys.exit(1)
|
||||||
|
# Check ~/.config/virtscreen
|
||||||
|
if not HOME_PATH: # This is set in path.py
|
||||||
|
msg("Cannot detect home directory.")
|
||||||
|
sys.exit(1)
|
||||||
|
if not os.path.exists(HOME_PATH):
|
||||||
|
try:
|
||||||
|
os.makedirs(HOME_PATH)
|
||||||
|
except:
|
||||||
|
msg("Cannot create ~/.config/virtscreen")
|
||||||
|
sys.exit(1)
|
||||||
|
# Check x11vnc
|
||||||
|
if not shutil.which('x11vnc'):
|
||||||
|
msg("x11vnc is not installed.")
|
||||||
|
sys.exit(1)
|
||||||
|
# Enable logging
|
||||||
|
if args['log'] is None:
|
||||||
|
args['log'] = 'WARNING'
|
||||||
|
log_level = getattr(logging, args['log'].upper(), None)
|
||||||
|
if not isinstance(log_level, int):
|
||||||
|
error('Please choose a correct python logging level')
|
||||||
|
sys.exit(1)
|
||||||
|
# When logging level is INFO or lower, print logs in terminal
|
||||||
|
# Otherwise log to a file
|
||||||
|
log_to_file = True if log_level > logging.INFO else False
|
||||||
|
FORMAT = "[%(levelname)s:%(filename)s:%(lineno)s:%(funcName)s()] %(message)s"
|
||||||
|
logging.basicConfig(level=log_level, format=FORMAT,
|
||||||
|
**({'filename': LOGGING_PATH} if log_to_file else {}))
|
||||||
|
if log_to_file:
|
||||||
|
logger = logging.getLogger()
|
||||||
|
handler = RotatingFileHandler(LOGGING_PATH, mode='a', maxBytes=1024*4, backupCount=1)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
logging.info('logging enabled')
|
||||||
|
del args['log']
|
||||||
|
logging.info(f'{args}')
|
||||||
|
# Check if xrandr is correctly parsed.
|
||||||
|
try:
|
||||||
|
test = XRandR()
|
||||||
|
except RuntimeError as e:
|
||||||
|
msg(str(e))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def main_gui(args: argparse.Namespace):
|
||||||
|
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
loop = QEventLoop(app)
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
# Check environment first
|
||||||
|
from PyQt5.QtWidgets import QMessageBox, QSystemTrayIcon
|
||||||
|
def dialog(message: str) -> None:
|
||||||
|
QMessageBox.critical(None, "VirtScreen", message)
|
||||||
|
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
|
dialog("Cannot detect system tray on this system.")
|
||||||
|
sys.exit(1)
|
||||||
|
check_env(args, dialog)
|
||||||
|
|
||||||
|
app.setApplicationName("VirtScreen")
|
||||||
|
app.setWindowIcon(QIcon(ICON_PATH))
|
||||||
|
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
|
||||||
|
|
||||||
|
# Register the Python type. Its URI is 'People', it's v1.0 and the type
|
||||||
|
# will be called 'Person' in QML.
|
||||||
|
qmlRegisterType(DisplayProperty, 'VirtScreen.DisplayProperty', 1, 0, 'DisplayProperty')
|
||||||
|
qmlRegisterType(Backend, 'VirtScreen.Backend', 1, 0, 'Backend')
|
||||||
|
qmlRegisterType(Cursor, 'VirtScreen.Cursor', 1, 0, 'Cursor')
|
||||||
|
qmlRegisterType(Network, 'VirtScreen.Network', 1, 0, 'Network')
|
||||||
|
|
||||||
|
# Create a component factory and load the QML script.
|
||||||
|
engine = QQmlApplicationEngine()
|
||||||
|
engine.load(QUrl(MAIN_QML_PATH))
|
||||||
|
if not engine.rootObjects():
|
||||||
|
dialog("Failed to load QML")
|
||||||
|
sys.exit(1)
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
with loop:
|
||||||
|
loop.run_forever()
|
||||||
|
|
||||||
|
def main_cli(args: argparse.Namespace):
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
# Check the environment
|
||||||
|
check_env(args, print)
|
||||||
|
if not os.path.exists(CONFIG_PATH):
|
||||||
|
error("Configuration file does not exist.\n"
|
||||||
|
"Configure a virtual screen using GUI first.")
|
||||||
|
sys.exit(1)
|
||||||
|
# By instantiating the backend, additional verifications of config
|
||||||
|
# file will be done.
|
||||||
|
backend = Backend(logger=print)
|
||||||
|
# Get settings
|
||||||
|
with open(CONFIG_PATH, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
# Override settings from arguments
|
||||||
|
position = ''
|
||||||
|
if not args['auto']:
|
||||||
|
args_virt = ['portrait', 'hidpi']
|
||||||
|
for prop in args_virt:
|
||||||
|
if args[prop]:
|
||||||
|
config['virt'][prop] = True
|
||||||
|
args_position = ['left', 'right', 'above', 'below']
|
||||||
|
tmp_args = {k: args[k] for k in args_position}
|
||||||
|
if not any(tmp_args.values()):
|
||||||
|
error("Choose a position relative to the primary monitor. (e.g. --left)")
|
||||||
|
sys.exit(1)
|
||||||
|
for key, value in tmp_args.items():
|
||||||
|
if value:
|
||||||
|
position = key
|
||||||
|
# Create virtscreen and Start VNC
|
||||||
|
def handle_error(msg):
|
||||||
|
error(msg)
|
||||||
|
sys.exit(1)
|
||||||
|
backend.onError.connect(handle_error)
|
||||||
|
backend.createVirtScreen(config['virt']['device'], config['virt']['width'],
|
||||||
|
config['virt']['height'], config['virt']['portrait'],
|
||||||
|
config['virt']['hidpi'], position)
|
||||||
|
def handle_vnc_changed(state):
|
||||||
|
if state is backend.VNCState.OFF:
|
||||||
|
sys.exit(0)
|
||||||
|
backend.onVncStateChanged.connect(handle_vnc_changed)
|
||||||
|
backend.startVNC(config['vnc']['port'])
|
||||||
|
loop.run_forever()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
@ -23,6 +23,10 @@ ApplicationWindow {
|
||||||
property int margin: 10
|
property int margin: 10
|
||||||
property int popupWidth: width - 26
|
property int popupWidth: width - 26
|
||||||
|
|
||||||
|
screen: Qt.application.screens[0]
|
||||||
|
x: screen.virtualX
|
||||||
|
y: screen.virtualY
|
||||||
|
|
||||||
// hide screen when loosing focus
|
// hide screen when loosing focus
|
||||||
property bool autoClose: true
|
property bool autoClose: true
|
||||||
property bool ignoreCloseOnce: false
|
property bool ignoreCloseOnce: false
|
||||||
|
|
@ -46,7 +50,7 @@ ApplicationWindow {
|
||||||
menuBar: ToolBar {
|
menuBar: ToolBar {
|
||||||
id: toolbar
|
id: toolbar
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
font.pointSize: 11 //parent.font.pointSize + 1
|
font.pixelSize: height * 0.34
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -60,7 +64,7 @@ ApplicationWindow {
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
id: menuButton
|
id: menuButton
|
||||||
anchors.right: parent.right
|
Layout.alignment: Qt.AlignRight
|
||||||
text: qsTr("⋮")
|
text: qsTr("⋮")
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
text: parent.text
|
text: parent.text
|
||||||
|
|
@ -150,35 +154,38 @@ ApplicationWindow {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
Text {
|
Text {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
font { weight: Font.Bold; pointSize: 15 }
|
font { weight: Font.Bold; pixelSize: 20 }
|
||||||
text: "VirtScreen" + " v" + settings.version
|
text: "VirtScreen" + " v" + settings.version
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
font { pixelSize: 13 }
|
||||||
text: "Make your iPad/tablet/computer<br/>as a secondary monitor.<br/>"
|
text: "Make your iPad/tablet/computer<br/>as a secondary monitor.<br/>"
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
font { pixelSize: 14 }
|
||||||
text: "- <a href='https://github.com/kbumsik/VirtScreen'>Project Website</a>"
|
text: "- <a href='https://github.com/kbumsik/VirtScreen'>Project Website</a>"
|
||||||
onLinkActivated: Qt.openUrlExternally(link)
|
onLinkActivated: Qt.openUrlExternally(link)
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
font { pixelSize: 14 }
|
||||||
text: "- <a href='https://github.com/kbumsik/VirtScreen/issues'>Issues & Bug Report</a>"
|
text: "- <a href='https://github.com/kbumsik/VirtScreen/issues'>Issues & Bug Report</a>"
|
||||||
onLinkActivated: Qt.openUrlExternally(link)
|
onLinkActivated: Qt.openUrlExternally(link)
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
font { pointSize: 10 }
|
font { pixelSize: 14 }
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
lineHeight: 0.7
|
lineHeight: 0.7
|
||||||
text: "<br/>Copyright © 2018 Bumsik Kim <a href='https://kbumsik.io/'>Homepage</a><br/>"
|
text: "<br/>Copyright © 2018 Bumsik Kim <a href='https://kbumsik.io/'>Homepage</a><br/>"
|
||||||
onLinkActivated: Qt.openUrlExternally(link)
|
onLinkActivated: Qt.openUrlExternally(link)
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
font { pointSize: 9 }
|
font { pixelSize: 11 }
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
text: "This program comes with absolutely no warranty.<br/>" +
|
text: "This program comes with absolutely no warranty.<br/>" +
|
||||||
"See the <a href='https://github.com/kbumsik/VirtScreen/blob/master/LICENSE'>" +
|
"See the <a href='https://github.com/kbumsik/VirtScreen/blob/master/LICENSE'>" +
|
||||||
|
|
@ -202,8 +209,7 @@ ApplicationWindow {
|
||||||
TextField {
|
TextField {
|
||||||
id: passwordFIeld
|
id: passwordFIeld
|
||||||
focus: true
|
focus: true
|
||||||
anchors.left: parent.left
|
Layout.fillWidth: true
|
||||||
anchors.right: parent.right
|
|
||||||
placeholderText: "New Password";
|
placeholderText: "New Password";
|
||||||
echoMode: TextInput.Password;
|
echoMode: TextInput.Password;
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +240,8 @@ ApplicationWindow {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
ScrollView {
|
ScrollView {
|
||||||
anchors.fill: parent
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
TextArea {
|
TextArea {
|
||||||
// readOnly: true
|
// readOnly: true
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,12 @@ Dialog {
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
font { pointSize: 10 }
|
Layout.fillWidth: true
|
||||||
|
font { pixelSize: 14 }
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
text: "<b>Warning</b>: Edit only if 'VIRTUAL1' is not available<br/>" +
|
text: "<b>Warning</b>: Edit only if 'VIRTUAL1' is not available. " +
|
||||||
"If so, please note that the virtual screen may be<br/>" +
|
"If so, please note that the virtual screen may be " +
|
||||||
"unstable/unavailable depending on a graphic<br/>" +
|
"unstable/unavailable depending on a graphic " +
|
||||||
"card and its driver."
|
"card and its driver."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import VirtScreen.Backend 1.0
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
GroupBox {
|
GroupBox {
|
||||||
title: "Virtual Display"
|
title: "Virtual Screen"
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
enabled: backend.virtScreenCreated ? false : true
|
enabled: backend.virtScreenCreated ? false : true
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Dialog {
|
||||||
x: (window.width - width) / 2
|
x: (window.width - width) / 2
|
||||||
y: (window.width - height) / 2
|
y: (window.width - height) / 2
|
||||||
width: popupWidth
|
width: popupWidth
|
||||||
height: 300
|
height: 350
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var request = new XMLHttpRequest();
|
var request = new XMLHttpRequest();
|
||||||
|
|
@ -33,7 +33,27 @@ Dialog {
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
RowLayout {
|
||||||
|
TextField {
|
||||||
|
id: vncCustomArgsTextField
|
||||||
|
enabled: vncCustomArgsCheckbox.checked
|
||||||
|
Layout.fillWidth: true
|
||||||
|
placeholderText: "Custom x11vnc arguments"
|
||||||
|
onTextEdited: {
|
||||||
|
settings.customX11vncArgs.value = text;
|
||||||
|
}
|
||||||
|
text: vncCustomArgsCheckbox.checked ? settings.customX11vncArgs.value : ""
|
||||||
|
}
|
||||||
|
CheckBox {
|
||||||
|
id: vncCustomArgsCheckbox
|
||||||
|
checked: settings.customX11vncArgs.enabled
|
||||||
|
onToggled: {
|
||||||
|
settings.customX11vncArgs.enabled = checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
enabled: !vncCustomArgsCheckbox.checked
|
||||||
Repeater {
|
Repeater {
|
||||||
id: vncOptionsRepeater
|
id: vncOptionsRepeater
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
|
@ -50,7 +70,7 @@ Dialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
RowLayout {
|
RowLayout {
|
||||||
// Empty layout
|
// Empty layout
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,14 @@ import QtQuick.Controls 2.3
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
import VirtScreen.Backend 1.0
|
import VirtScreen.Backend 1.0
|
||||||
|
import VirtScreen.Network 1.0
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
// virtscreen.py Network interfaces backend.
|
||||||
|
Network {
|
||||||
|
id: network
|
||||||
|
}
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
title: "VNC Server"
|
title: "VNC Server"
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
@ -27,8 +33,6 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
Label { text: "Password"; Layout.fillWidth: true }
|
Label { text: "Password"; Layout.fillWidth: true }
|
||||||
Button {
|
Button {
|
||||||
text: "Delete"
|
text: "Delete"
|
||||||
|
|
@ -84,9 +88,8 @@ ColumnLayout {
|
||||||
GroupBox {
|
GroupBox {
|
||||||
title: "Available IP addresses"
|
title: "Available IP addresses"
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
implicitHeight: 145
|
implicitHeight: 145
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
ListView {
|
ListView {
|
||||||
id: ipListView
|
id: ipListView
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -98,14 +101,13 @@ ColumnLayout {
|
||||||
anchors.bottom: ipListView.bottom
|
anchors.bottom: ipListView.bottom
|
||||||
policy: ScrollBar.AlwaysOn
|
policy: ScrollBar.AlwaysOn
|
||||||
}
|
}
|
||||||
model: backend.ipAddresses
|
model: network.ipAddresses
|
||||||
delegate: TextEdit {
|
delegate: TextEdit {
|
||||||
text: modelData
|
text: modelData
|
||||||
readOnly: true
|
readOnly: true
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
font.pointSize: 12
|
font.pixelSize: 14
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.3.1",
|
||||||
"x11vncVersion": "0.9.15",
|
"x11vncVersion": "0.9.15",
|
||||||
"theme_color": 8,
|
"theme_color": 8,
|
||||||
"virt": {
|
"virt": {
|
||||||
|
|
@ -31,5 +31,9 @@
|
||||||
"arg": null
|
"arg": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"customX11vncArgs": {
|
||||||
|
"enabled": false,
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
"presets": []
|
"presets": []
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.3.1",
|
||||||
"x11vncOptions": {
|
"x11vncOptions": {
|
||||||
"-ncache": {
|
"-ncache": {
|
||||||
"value": "-ncache",
|
"value": "-ncache",
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
"value": "arandr",
|
"value": "arandr",
|
||||||
"name": "ARandR",
|
"name": "ARandR",
|
||||||
"args": "arandr",
|
"args": "arandr",
|
||||||
"XDG_CURRENT_DESKTOP": []
|
"XDG_CURRENT_DESKTOP": [""]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,12 +4,18 @@ import Qt.labs.platform 1.0
|
||||||
|
|
||||||
import VirtScreen.DisplayProperty 1.0
|
import VirtScreen.DisplayProperty 1.0
|
||||||
import VirtScreen.Backend 1.0
|
import VirtScreen.Backend 1.0
|
||||||
|
import VirtScreen.Cursor 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
property alias window: mainLoader.item
|
property alias window: mainLoader.item
|
||||||
property var settings: JSON.parse(backend.settings)
|
property var settings: JSON.parse(backend.settings)
|
||||||
property bool autostart: settings.vnc.autostart
|
property bool autostart: settings.vnc.autostart
|
||||||
|
|
||||||
|
function saveSettings () {
|
||||||
|
settings.vnc.autostart = autostart;
|
||||||
|
backend.settings = JSON.stringify(settings, null, 4);
|
||||||
|
}
|
||||||
|
|
||||||
function createVirtScreen () {
|
function createVirtScreen () {
|
||||||
backend.createVirtScreen(settings.virt.device, settings.virt.width,
|
backend.createVirtScreen(settings.virt.device, settings.virt.width,
|
||||||
settings.virt.height, settings.virt.portrait,
|
settings.virt.height, settings.virt.portrait,
|
||||||
|
|
@ -17,18 +23,8 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
function startVNC () {
|
function startVNC () {
|
||||||
var options = '';
|
saveSettings();
|
||||||
var data = settings.x11vncOptions;
|
backend.startVNC(settings.vnc.port);
|
||||||
for (var key in data) {
|
|
||||||
if(data[key].available && data[key].enabled) {
|
|
||||||
options += key + ' ';
|
|
||||||
if(data[key].arg !== null) {
|
|
||||||
options += data[key].arg.toString() + ' ';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('options: ', options);
|
|
||||||
backend.startVNC(settings.vnc.port, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopVNC () {
|
function stopVNC () {
|
||||||
|
|
@ -61,6 +57,11 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// virtscreen.py Cursor class.
|
||||||
|
Cursor {
|
||||||
|
id: cursor
|
||||||
|
}
|
||||||
|
|
||||||
// Timer object and function
|
// Timer object and function
|
||||||
Timer {
|
Timer {
|
||||||
id: timer
|
id: timer
|
||||||
|
|
@ -109,17 +110,14 @@ Item {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Move window to the corner of the primary display
|
// Move window to the corner of the primary display
|
||||||
var primary = backend.primary;
|
var cursor_x = (cursor.x / window.screen.devicePixelRatio) - window.screen.virtualX;
|
||||||
var width = primary.width;
|
var cursor_y = (cursor.y / window.screen.devicePixelRatio) - window.screen.virtualY;
|
||||||
var height = primary.height;
|
var x_mid = window.screen.width / 2;
|
||||||
var cursor_x = backend.cursor_x - primary.x_offset;
|
var y_mid = window.screen.height / 2;
|
||||||
var cursor_y = backend.cursor_y - primary.y_offset;
|
var x = window.screen.width - window.width; //(cursor_x > x_mid)? width - window.width : 0;
|
||||||
var x_mid = width / 2;
|
var y = (cursor_y > y_mid)? window.screen.height - window.height : 0;
|
||||||
var y_mid = height / 2;
|
x += window.screen.virtualX;
|
||||||
var x = width - window.width; //(cursor_x > x_mid)? width - window.width : 0;
|
y += window.screen.virtualY;
|
||||||
var y = (cursor_y > y_mid)? height - window.height : 0;
|
|
||||||
x += primary.x_offset;
|
|
||||||
y += primary.y_offset;
|
|
||||||
window.x = x;
|
window.x = x;
|
||||||
window.y = y;
|
window.y = y;
|
||||||
window.show();
|
window.show();
|
||||||
|
|
@ -131,9 +129,9 @@ Item {
|
||||||
// Sytray Icon
|
// Sytray Icon
|
||||||
SystemTrayIcon {
|
SystemTrayIcon {
|
||||||
id: sysTrayIcon
|
id: sysTrayIcon
|
||||||
iconSource: backend.vncState == Backend.CONNECTED ? "../icon/icon_tablet_on.png" :
|
iconSource: backend.vncState == Backend.CONNECTED ? "../icon/systray_tablet_on.png" :
|
||||||
backend.virtScreenCreated ? "../icon/icon_tablet_off.png" :
|
backend.virtScreenCreated ? "../icon/systray_tablet_off.png" :
|
||||||
"../icon/icon.png"
|
"../icon/systray_no_tablet.png"
|
||||||
visible: true
|
visible: true
|
||||||
property bool clicked: false
|
property bool clicked: false
|
||||||
|
|
||||||
|
|
@ -231,8 +229,7 @@ Item {
|
||||||
id: quitAction
|
id: quitAction
|
||||||
text: qsTr("&Quit")
|
text: qsTr("&Quit")
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
settings.vnc.autostart = autostart;
|
saveSettings();
|
||||||
backend.settings = JSON.stringify(settings, null, 4);
|
|
||||||
backend.quitProgram();
|
backend.quitProgram();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
108
virtscreen/display.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
"""Display information data classes"""
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtProperty
|
||||||
|
|
||||||
|
|
||||||
|
class Display(object):
|
||||||
|
"""Display information"""
|
||||||
|
__slots__ = ['name', 'primary', 'connected', 'active', 'width', 'height',
|
||||||
|
'x_offset', 'y_offset']
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.name: str = None
|
||||||
|
self.primary: bool = False
|
||||||
|
self.connected: bool = False
|
||||||
|
self.active: bool = False
|
||||||
|
self.width: int = 0
|
||||||
|
self.height: int = 0
|
||||||
|
self.x_offset: int = 0
|
||||||
|
self.y_offset: int = 0
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
ret = f"{self.name}"
|
||||||
|
if self.connected:
|
||||||
|
ret += " connected"
|
||||||
|
else:
|
||||||
|
ret += " disconnected"
|
||||||
|
if self.primary:
|
||||||
|
ret += " primary"
|
||||||
|
if self.active:
|
||||||
|
ret += f" {self.width}x{self.height}+{self.x_offset}+{self.y_offset}"
|
||||||
|
else:
|
||||||
|
ret += f" not active {self.width}x{self.height}"
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayProperty(QObject):
|
||||||
|
"""Wrapper around Display class for Qt"""
|
||||||
|
def __init__(self, display: Display, parent=None):
|
||||||
|
super(DisplayProperty, self).__init__(parent)
|
||||||
|
self._display = display
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display(self):
|
||||||
|
return self._display
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def name(self):
|
||||||
|
return self._display.name
|
||||||
|
|
||||||
|
@name.setter
|
||||||
|
def name(self, name):
|
||||||
|
self._display.name = name
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def primary(self):
|
||||||
|
return self._display.primary
|
||||||
|
|
||||||
|
@primary.setter
|
||||||
|
def primary(self, primary):
|
||||||
|
self._display.primary = primary
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def connected(self):
|
||||||
|
return self._display.connected
|
||||||
|
|
||||||
|
@connected.setter
|
||||||
|
def connected(self, connected):
|
||||||
|
self._display.connected = connected
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def active(self):
|
||||||
|
return self._display.active
|
||||||
|
|
||||||
|
@active.setter
|
||||||
|
def active(self, active):
|
||||||
|
self._display.active = active
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant=True)
|
||||||
|
def width(self):
|
||||||
|
return self._display.width
|
||||||
|
|
||||||
|
@width.setter
|
||||||
|
def width(self, width):
|
||||||
|
self._display.width = width
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant=True)
|
||||||
|
def height(self):
|
||||||
|
return self._display.height
|
||||||
|
|
||||||
|
@height.setter
|
||||||
|
def height(self, height):
|
||||||
|
self._display.height = height
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant=True)
|
||||||
|
def x_offset(self):
|
||||||
|
return self._display.x_offset
|
||||||
|
|
||||||
|
@x_offset.setter
|
||||||
|
def x_offset(self, x_offset):
|
||||||
|
self._display.x_offset = x_offset
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant=True)
|
||||||
|
def y_offset(self):
|
||||||
|
return self._display.y_offset
|
||||||
|
|
||||||
|
@y_offset.setter
|
||||||
|
def y_offset(self, y_offset):
|
||||||
|
self._display.y_offset = y_offset
|
||||||
BIN
virtscreen/icon/full_256x256.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 109 KiB |
BIN
virtscreen/icon/systray_no_tablet.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
virtscreen/icon/systray_tablet_off.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
virtscreen/icon/systray_tablet_on.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
38
virtscreen/path.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
"""File path definitions"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
# Sanitize environment variables
|
||||||
|
# https://wiki.sei.cmu.edu/confluence/display/c/ENV03-C.+Sanitize+the+environment+when+invoking+external+programs
|
||||||
|
|
||||||
|
# Setting home path
|
||||||
|
# Rewrite $HOME env for consistency. This will make
|
||||||
|
# Path.home() to look up in the password directory (pwd module)
|
||||||
|
try:
|
||||||
|
os.environ['HOME'] = str(Path.home())
|
||||||
|
# os.environ['PATH'] = os.confstr("CS_PATH") # Sanitize $PATH, Deleted by Issue #19.
|
||||||
|
|
||||||
|
# https://www.freedesktop.org/software/systemd/man/file-hierarchy.html
|
||||||
|
# HOME_PATH will point to ~/.config/virtscreen by default
|
||||||
|
if ('XDG_CONFIG_HOME' in os.environ) and len(os.environ['XDG_CONFIG_HOME']):
|
||||||
|
HOME_PATH = os.environ['XDG_CONFIG_HOME']
|
||||||
|
else:
|
||||||
|
HOME_PATH = os.environ['HOME'] + '/.config'
|
||||||
|
HOME_PATH = HOME_PATH + "/virtscreen"
|
||||||
|
except OSError:
|
||||||
|
HOME_PATH = '' # This will be checked in _main_.py.
|
||||||
|
# Setting base path
|
||||||
|
BASE_PATH = os.path.dirname(__file__) # Location of this script
|
||||||
|
# Path in ~/.virtscreen
|
||||||
|
X11VNC_LOG_PATH = HOME_PATH + "/x11vnc_log.txt"
|
||||||
|
X11VNC_PASSWORD_PATH = HOME_PATH + "/x11vnc_passwd"
|
||||||
|
CONFIG_PATH = HOME_PATH + "/config.json"
|
||||||
|
LOGGING_PATH = HOME_PATH + "/log.txt"
|
||||||
|
# Path in the program path
|
||||||
|
ICON_PATH = BASE_PATH + "/icon/full_256x256.png"
|
||||||
|
ASSETS_PATH = BASE_PATH + "/assets"
|
||||||
|
DATA_PATH = ASSETS_PATH + "/data.json"
|
||||||
|
DEFAULT_CONFIG_PATH = ASSETS_PATH + "/config.default.json"
|
||||||
|
MAIN_QML_PATH = ASSETS_PATH + "/main.qml"
|
||||||
100
virtscreen/process.py
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
"""Subprocess wrapper"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import asyncio
|
||||||
|
import signal
|
||||||
|
import shlex
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class SubprocessWrapper:
|
||||||
|
"""Subprocess wrapper class"""
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_output(self, arg) -> None:
|
||||||
|
return subprocess.check_output(shlex.split(arg), stderr=subprocess.STDOUT).decode('utf-8')
|
||||||
|
|
||||||
|
def run(self, arg: str, input: str = None, check=False) -> str:
|
||||||
|
if input:
|
||||||
|
input = input.encode('utf-8')
|
||||||
|
return subprocess.run(shlex.split(arg), input=input, stdout=subprocess.PIPE,
|
||||||
|
check=check, stderr=subprocess.STDOUT).stdout.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
class _Protocol(asyncio.SubprocessProtocol):
|
||||||
|
"""SubprocessProtocol implementation"""
|
||||||
|
|
||||||
|
def __init__(self, outer):
|
||||||
|
self.outer = outer
|
||||||
|
self.transport: asyncio.SubprocessTransport
|
||||||
|
|
||||||
|
def connection_made(self, transport):
|
||||||
|
logging.info("connectionMade!")
|
||||||
|
self.outer.connected()
|
||||||
|
self.transport = transport
|
||||||
|
transport.get_pipe_transport(0).close() # No more input
|
||||||
|
|
||||||
|
def pipe_data_received(self, fd, data):
|
||||||
|
if fd == 1: # stdout
|
||||||
|
self.outer.out_recevied(data)
|
||||||
|
if self.outer.logfile is not None:
|
||||||
|
self.outer.logfile.write(data)
|
||||||
|
elif fd == 2: # stderr
|
||||||
|
self.outer.err_recevied(data)
|
||||||
|
if self.outer.logfile is not None:
|
||||||
|
self.outer.logfile.write(data)
|
||||||
|
|
||||||
|
def pipe_connection_lost(self, fd, exc):
|
||||||
|
if fd == 0: # stdin
|
||||||
|
logging.info("stdin is closed. (we probably did it)")
|
||||||
|
elif fd == 1: # stdout
|
||||||
|
logging.info("The child closed their stdout.")
|
||||||
|
elif fd == 2: # stderr
|
||||||
|
logging.info("The child closed their stderr.")
|
||||||
|
|
||||||
|
def connection_lost(self, exc):
|
||||||
|
logging.info("Subprocess connection lost.")
|
||||||
|
|
||||||
|
def process_exited(self):
|
||||||
|
if self.outer.logfile is not None:
|
||||||
|
self.outer.logfile.close()
|
||||||
|
self.transport.close()
|
||||||
|
return_code = self.transport.get_returncode()
|
||||||
|
if return_code is None:
|
||||||
|
logging.error("Unknown exit")
|
||||||
|
self.outer.ended(1)
|
||||||
|
return
|
||||||
|
logging.info(f"processEnded, status {return_code}")
|
||||||
|
self.outer.ended(return_code)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncSubprocess():
|
||||||
|
"""Asynchronous subprocess wrapper class"""
|
||||||
|
|
||||||
|
def __init__(self, connected, out_recevied, err_recevied, ended, logfile=None):
|
||||||
|
self.connected = connected
|
||||||
|
self.out_recevied = out_recevied
|
||||||
|
self.err_recevied = err_recevied
|
||||||
|
self.ended = ended
|
||||||
|
self.logfile = logfile
|
||||||
|
self.transport: asyncio.SubprocessTransport
|
||||||
|
self.protocol: _Protocol
|
||||||
|
|
||||||
|
async def _run(self, arg: str, loop: asyncio.AbstractEventLoop):
|
||||||
|
self.transport, self.protocol = await loop.subprocess_exec(
|
||||||
|
lambda: _Protocol(self), *shlex.split(arg), env=os.environ)
|
||||||
|
|
||||||
|
def run(self, arg: str):
|
||||||
|
"""Spawn a process.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
arg {str} -- arguments in string
|
||||||
|
"""
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.create_task(self._run(arg, loop))
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Kill a spawned process."""
|
||||||
|
self.transport.send_signal(signal.SIGINT)
|
||||||
364
virtscreen/qt_backend.py
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
"""GUI backend"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import atexit
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
|
||||||
|
from PyQt5.QtGui import QCursor
|
||||||
|
from PyQt5.QtQml import QQmlListProperty
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
from netifaces import interfaces, ifaddresses, AF_INET
|
||||||
|
|
||||||
|
from .display import DisplayProperty
|
||||||
|
from .xrandr import XRandR
|
||||||
|
from .process import AsyncSubprocess, SubprocessWrapper
|
||||||
|
from .path import (DATA_PATH, CONFIG_PATH, DEFAULT_CONFIG_PATH,
|
||||||
|
X11VNC_PASSWORD_PATH, X11VNC_LOG_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
class Backend(QObject):
|
||||||
|
""" Backend class for QML frontend """
|
||||||
|
|
||||||
|
class VNCState:
|
||||||
|
""" Enum to indicate a state of the VNC server """
|
||||||
|
OFF = 0
|
||||||
|
ERROR = 1
|
||||||
|
WAITING = 2
|
||||||
|
CONNECTED = 3
|
||||||
|
|
||||||
|
Q_ENUMS(VNCState)
|
||||||
|
|
||||||
|
# Signals
|
||||||
|
onVirtScreenCreatedChanged = pyqtSignal(bool)
|
||||||
|
onVncUsePasswordChanged = pyqtSignal(bool)
|
||||||
|
onVncStateChanged = pyqtSignal(VNCState)
|
||||||
|
onDisplaySettingClosed = pyqtSignal()
|
||||||
|
onError = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None, logger=logging.info, error_logger=logging.error):
|
||||||
|
super(Backend, self).__init__(parent)
|
||||||
|
# Virtual screen properties
|
||||||
|
self.xrandr: XRandR = XRandR()
|
||||||
|
self._virtScreenCreated: bool = False
|
||||||
|
# VNC server properties
|
||||||
|
self._vncUsePassword: bool = False
|
||||||
|
self._vncState: self.VNCState = self.VNCState.OFF
|
||||||
|
# Primary screen and mouse posistion
|
||||||
|
self.vncServer: AsyncSubprocess
|
||||||
|
# Info/error logger
|
||||||
|
self.log: Callable[[str], None] = logger
|
||||||
|
self.log_error: Callable[[str], None] = error_logger
|
||||||
|
# Check config file
|
||||||
|
# and initialize if needed
|
||||||
|
need_init = False
|
||||||
|
if not os.path.exists(CONFIG_PATH):
|
||||||
|
shutil.copy(DEFAULT_CONFIG_PATH, CONFIG_PATH)
|
||||||
|
need_init = True
|
||||||
|
# Version check
|
||||||
|
file_match = True
|
||||||
|
with open(CONFIG_PATH, 'r') as f_config, open(DATA_PATH, 'r') as f_data:
|
||||||
|
config = json.load(f_config)
|
||||||
|
data = json.load(f_data)
|
||||||
|
if config['version'] != data['version']:
|
||||||
|
file_match = False
|
||||||
|
# Override config with default when version doesn't match
|
||||||
|
if not file_match:
|
||||||
|
shutil.copy(DEFAULT_CONFIG_PATH, CONFIG_PATH)
|
||||||
|
need_init = True
|
||||||
|
# initialize config file
|
||||||
|
if need_init:
|
||||||
|
# 1. Available x11vnc options
|
||||||
|
# Get available x11vnc options from x11vnc first
|
||||||
|
p = SubprocessWrapper()
|
||||||
|
arg = 'x11vnc -opts'
|
||||||
|
ret = p.run(arg)
|
||||||
|
options = tuple(m.group(1) for m in re.finditer(r"\s*(-\w+)\s+", ret))
|
||||||
|
# Set/unset available x11vnc options flags in config
|
||||||
|
with open(CONFIG_PATH, 'r') as f, open(DATA_PATH, 'r') as f_data:
|
||||||
|
config = json.load(f)
|
||||||
|
data = json.load(f_data)
|
||||||
|
for key, value in config["x11vncOptions"].items():
|
||||||
|
if key in options:
|
||||||
|
value["available"] = True
|
||||||
|
else:
|
||||||
|
value["available"] = False
|
||||||
|
# Default Display settings app for a Desktop Environment
|
||||||
|
desktop_environ = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
|
||||||
|
for key, value in data['displaySettingApps'].items():
|
||||||
|
if desktop_environ in value['XDG_CURRENT_DESKTOP']:
|
||||||
|
config["displaySettingApp"] = key
|
||||||
|
# Save the new config
|
||||||
|
with open(CONFIG_PATH, 'w') as f:
|
||||||
|
f.write(json.dumps(config, indent=4, sort_keys=True))
|
||||||
|
|
||||||
|
def promptError(self, msg):
|
||||||
|
self.log_error(msg)
|
||||||
|
self.onError.emit(msg)
|
||||||
|
|
||||||
|
# Qt properties
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def settings(self):
|
||||||
|
with open(CONFIG_PATH, "r") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
@settings.setter
|
||||||
|
def settings(self, json_str):
|
||||||
|
with open(CONFIG_PATH, "w") as f:
|
||||||
|
f.write(json_str)
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=onVirtScreenCreatedChanged)
|
||||||
|
def virtScreenCreated(self):
|
||||||
|
return self._virtScreenCreated
|
||||||
|
|
||||||
|
@virtScreenCreated.setter
|
||||||
|
def virtScreenCreated(self, value):
|
||||||
|
self._virtScreenCreated = value
|
||||||
|
self.onVirtScreenCreatedChanged.emit(value)
|
||||||
|
|
||||||
|
@pyqtProperty(QQmlListProperty, constant=True)
|
||||||
|
def screens(self):
|
||||||
|
try:
|
||||||
|
return QQmlListProperty(DisplayProperty, self, [DisplayProperty(x) for x in self.xrandr.screens])
|
||||||
|
except RuntimeError as e:
|
||||||
|
self.promptError(str(e))
|
||||||
|
return QQmlListProperty(DisplayProperty, self, [])
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=onVncUsePasswordChanged)
|
||||||
|
def vncUsePassword(self):
|
||||||
|
if os.path.isfile(X11VNC_PASSWORD_PATH):
|
||||||
|
self._vncUsePassword = True
|
||||||
|
else:
|
||||||
|
if self._vncUsePassword:
|
||||||
|
self.vncUsePassword = False
|
||||||
|
return self._vncUsePassword
|
||||||
|
|
||||||
|
@vncUsePassword.setter
|
||||||
|
def vncUsePassword(self, use):
|
||||||
|
self._vncUsePassword = use
|
||||||
|
self.onVncUsePasswordChanged.emit(use)
|
||||||
|
|
||||||
|
@pyqtProperty(VNCState, notify=onVncStateChanged)
|
||||||
|
def vncState(self):
|
||||||
|
return self._vncState
|
||||||
|
|
||||||
|
@vncState.setter
|
||||||
|
def vncState(self, state):
|
||||||
|
self._vncState = state
|
||||||
|
self.onVncStateChanged.emit(self._vncState)
|
||||||
|
|
||||||
|
# Qt Slots
|
||||||
|
@pyqtSlot(str, int, int, bool, bool)
|
||||||
|
def createVirtScreen(self, device, width, height, portrait, hidpi, pos=''):
|
||||||
|
self.xrandr.virt_name = device
|
||||||
|
self.log("Creating a Virtual Screen...")
|
||||||
|
try:
|
||||||
|
self.xrandr.create_virtual_screen(width, height, portrait, hidpi, pos)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
self.promptError(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
||||||
|
return
|
||||||
|
except RuntimeError as e:
|
||||||
|
self.promptError(str(e))
|
||||||
|
return
|
||||||
|
self.virtScreenCreated = True
|
||||||
|
self.log("The Virtual Screen successfully created.")
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def deleteVirtScreen(self):
|
||||||
|
self.log("Deleting the Virtual Screen...")
|
||||||
|
if self.vncState is not self.VNCState.OFF:
|
||||||
|
self.promptError("Turn off the VNC server first")
|
||||||
|
self.virtScreenCreated = True
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.xrandr.delete_virtual_screen()
|
||||||
|
except RuntimeError as e:
|
||||||
|
self.promptError(str(e))
|
||||||
|
return
|
||||||
|
self.virtScreenCreated = False
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def createVNCPassword(self, password):
|
||||||
|
if password:
|
||||||
|
password += '\n' + password + '\n\n' # verify + confirm
|
||||||
|
p = SubprocessWrapper()
|
||||||
|
try:
|
||||||
|
p.run(f"x11vnc -storepasswd {X11VNC_PASSWORD_PATH}", input=password, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
self.promptError(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
||||||
|
return
|
||||||
|
self.vncUsePassword = True
|
||||||
|
else:
|
||||||
|
self.promptError("Empty password")
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def deleteVNCPassword(self):
|
||||||
|
if os.path.isfile(X11VNC_PASSWORD_PATH):
|
||||||
|
os.remove(X11VNC_PASSWORD_PATH)
|
||||||
|
self.vncUsePassword = False
|
||||||
|
else:
|
||||||
|
self.promptError("Failed deleting the password file")
|
||||||
|
|
||||||
|
@pyqtSlot(int)
|
||||||
|
def startVNC(self, port):
|
||||||
|
# Check if a virtual screen created
|
||||||
|
if not self.virtScreenCreated:
|
||||||
|
self.promptError("Virtual Screen not crated.")
|
||||||
|
return
|
||||||
|
if self.vncState is not self.VNCState.OFF:
|
||||||
|
self.promptError("VNC Server is already running.")
|
||||||
|
return
|
||||||
|
# regex used in callbacks
|
||||||
|
patter_connected = re.compile(r"^.*Got connection from client.*$", re.M)
|
||||||
|
patter_disconnected = re.compile(r"^.*client_count: 0*$", re.M)
|
||||||
|
|
||||||
|
# define callbacks
|
||||||
|
def _connected():
|
||||||
|
self.log(f"VNC started. Now connect a VNC client to port {port}.")
|
||||||
|
self.vncState = self.VNCState.WAITING
|
||||||
|
|
||||||
|
def _received(data):
|
||||||
|
data = data.decode("utf-8")
|
||||||
|
if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data):
|
||||||
|
self.log("VNC connected.")
|
||||||
|
self.vncState = self.VNCState.CONNECTED
|
||||||
|
if (self._vncState is self.VNCState.CONNECTED) and patter_disconnected.search(data):
|
||||||
|
self.log("VNC disconnected.")
|
||||||
|
self.vncState = self.VNCState.WAITING
|
||||||
|
|
||||||
|
def _ended(exitCode):
|
||||||
|
if exitCode is not 0:
|
||||||
|
self.vncState = self.VNCState.ERROR
|
||||||
|
self.promptError('X11VNC: Error occurred.\n'
|
||||||
|
'Double check if the port is already used.')
|
||||||
|
self.vncState = self.VNCState.OFF # TODO: better handling error state
|
||||||
|
else:
|
||||||
|
self.vncState = self.VNCState.OFF
|
||||||
|
self.log("VNC Exited.")
|
||||||
|
atexit.unregister(self.stopVNC)
|
||||||
|
# load settings
|
||||||
|
with open(CONFIG_PATH, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
options = ''
|
||||||
|
if config['customX11vncArgs']['enabled']:
|
||||||
|
options = config['customX11vncArgs']['value']
|
||||||
|
else:
|
||||||
|
for key, value in config['x11vncOptions'].items():
|
||||||
|
if value['available'] and value['enabled']:
|
||||||
|
options += key + ' '
|
||||||
|
if value['arg'] is not None:
|
||||||
|
options += str(value['arg']) + ' '
|
||||||
|
# Sart x11vnc, turn settings object into VNC arguments format
|
||||||
|
logfile = open(X11VNC_LOG_PATH, "wb")
|
||||||
|
self.vncServer = AsyncSubprocess(_connected, _received, _received, _ended, logfile)
|
||||||
|
try:
|
||||||
|
virt = self.xrandr.get_virtual_screen()
|
||||||
|
except RuntimeError as e:
|
||||||
|
self.promptError(str(e))
|
||||||
|
return
|
||||||
|
clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}"
|
||||||
|
arg = f"x11vnc -rfbport {port} -clip {clip} {options}"
|
||||||
|
if self.vncUsePassword:
|
||||||
|
arg += f" -rfbauth {X11VNC_PASSWORD_PATH}"
|
||||||
|
self.vncServer.run(arg)
|
||||||
|
# auto stop on exit
|
||||||
|
atexit.register(self.stopVNC, force=True)
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def openDisplaySetting(self, app: str = "arandr"):
|
||||||
|
# define callbacks
|
||||||
|
def _connected():
|
||||||
|
self.log("External Display Setting opened.")
|
||||||
|
|
||||||
|
def _received(data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _ended(exitCode):
|
||||||
|
self.log("External Display Setting closed.")
|
||||||
|
self.onDisplaySettingClosed.emit()
|
||||||
|
if exitCode is not 0:
|
||||||
|
self.promptError(f'Error opening "{running_program}".')
|
||||||
|
with open(DATA_PATH, 'r') as f:
|
||||||
|
data = json.load(f)['displaySettingApps']
|
||||||
|
if app not in data:
|
||||||
|
self.promptError('Wrong display settings program')
|
||||||
|
return
|
||||||
|
program_list = [data[app]['args'], "arandr"]
|
||||||
|
program = AsyncSubprocess(_connected, _received, _received, _ended, None)
|
||||||
|
running_program = ''
|
||||||
|
for arg in program_list:
|
||||||
|
if not shutil.which(arg.split()[0]):
|
||||||
|
continue
|
||||||
|
running_program = arg
|
||||||
|
program.run(arg)
|
||||||
|
return
|
||||||
|
self.promptError('Failed to find a display settings program.\n'
|
||||||
|
'Please install ARandR package.\n'
|
||||||
|
'(e.g. sudo apt-get install arandr)\n'
|
||||||
|
'Please issue a feature request\n'
|
||||||
|
'if you wish to add a display settings\n'
|
||||||
|
'program for your Desktop Environment.')
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def stopVNC(self, force=False):
|
||||||
|
if force:
|
||||||
|
# Usually called from atexit().
|
||||||
|
self.vncServer.close()
|
||||||
|
time.sleep(3) # Make sure X11VNC shutdown before execute next atexit().
|
||||||
|
if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED):
|
||||||
|
self.vncServer.close()
|
||||||
|
else:
|
||||||
|
self.promptError("stopVNC called while it is not running")
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def clearCache(self):
|
||||||
|
# engine.clearComponentCache()
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def quitProgram(self):
|
||||||
|
self.blockSignals(True) # This will prevent invoking auto-restart or etc.
|
||||||
|
QApplication.instance().quit()
|
||||||
|
|
||||||
|
|
||||||
|
class Cursor(QObject):
|
||||||
|
""" Global mouse cursor position """
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(Cursor, self).__init__(parent)
|
||||||
|
|
||||||
|
@pyqtProperty(int)
|
||||||
|
def x(self):
|
||||||
|
cursor = QCursor().pos()
|
||||||
|
return cursor.x()
|
||||||
|
|
||||||
|
@pyqtProperty(int)
|
||||||
|
def y(self):
|
||||||
|
cursor = QCursor().pos()
|
||||||
|
return cursor.y()
|
||||||
|
|
||||||
|
|
||||||
|
class Network(QObject):
|
||||||
|
""" Backend class for network interfaces """
|
||||||
|
onIPAddressesChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(Network, self).__init__(parent)
|
||||||
|
|
||||||
|
@pyqtProperty('QStringList', notify=onIPAddressesChanged)
|
||||||
|
def ipAddresses(self):
|
||||||
|
for interface in interfaces():
|
||||||
|
if interface == 'lo':
|
||||||
|
continue
|
||||||
|
addresses = ifaddresses(interface).get(AF_INET, None)
|
||||||
|
if addresses is None:
|
||||||
|
continue
|
||||||
|
for link in addresses:
|
||||||
|
if link is not None:
|
||||||
|
yield link['addr']
|
||||||
|
|
@ -1,764 +0,0 @@
|
||||||
#!/usr/bin/python3
|
|
||||||
|
|
||||||
# Python standard packages
|
|
||||||
import sys, os, subprocess, signal, re, atexit, time, json, shutil
|
|
||||||
from pathlib import Path
|
|
||||||
from enum import Enum
|
|
||||||
from typing import List, Dict
|
|
||||||
# PyQt5 packages
|
|
||||||
from PyQt5.QtWidgets import QApplication
|
|
||||||
from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
|
|
||||||
from PyQt5.QtGui import QIcon, QCursor
|
|
||||||
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine, QQmlListProperty
|
|
||||||
# Twisted and netifaces
|
|
||||||
from twisted.internet import protocol, error
|
|
||||||
from netifaces import interfaces, ifaddresses, AF_INET
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
# file path definitions
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
# Sanitize environment variables
|
|
||||||
# https://wiki.sei.cmu.edu/confluence/display/c/ENV03-C.+Sanitize+the+environment+when+invoking+external+programs
|
|
||||||
|
|
||||||
# Delete $HOME env for security reason. This will make
|
|
||||||
# Path.home() to look up in the password directory (pwd module)
|
|
||||||
if 'HOME' in os.environ:
|
|
||||||
del os.environ['HOME']
|
|
||||||
os.environ['HOME'] = str(Path.home())
|
|
||||||
os.environ['PATH'] = os.confstr("CS_PATH") # Sanitize $PATH
|
|
||||||
|
|
||||||
# Setting home path and base path
|
|
||||||
# https://www.freedesktop.org/software/systemd/man/file-hierarchy.html
|
|
||||||
# HOME_PATH will point to ~/.config/virtscreen by default
|
|
||||||
if 'XDG_CONFIG_HOME' in os.environ and os.environ['XDG_CONFIG_HOME']:
|
|
||||||
HOME_PATH = os.environ['XDG_CONFIG_HOME']
|
|
||||||
else:
|
|
||||||
HOME_PATH = os.environ['HOME']
|
|
||||||
if HOME_PATH is not None:
|
|
||||||
HOME_PATH = HOME_PATH + "/.config"
|
|
||||||
if HOME_PATH is not None:
|
|
||||||
HOME_PATH = HOME_PATH + "/virtscreen"
|
|
||||||
BASE_PATH = os.path.dirname(__file__)
|
|
||||||
# Path in ~/.virtscreen
|
|
||||||
X11VNC_LOG_PATH = HOME_PATH + "/x11vnc_log.txt"
|
|
||||||
X11VNC_PASSWORD_PATH = HOME_PATH + "/x11vnc_passwd"
|
|
||||||
CONFIG_PATH = HOME_PATH + "/config.json"
|
|
||||||
# Path in the program path
|
|
||||||
ICON_PATH = BASE_PATH + "/icon/icon.png"
|
|
||||||
ASSETS_PATH = BASE_PATH + "/assets"
|
|
||||||
DATA_PATH = ASSETS_PATH + "/data.json"
|
|
||||||
DEFAULT_CONFIG_PATH = ASSETS_PATH + "/config.default.json"
|
|
||||||
MAIN_QML_PATH = ASSETS_PATH + "/main.qml"
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
# Subprocess wrapper
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
class SubprocessWrapper:
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def check_output(self, arg) -> None:
|
|
||||||
return subprocess.check_output(arg.split(), stderr=subprocess.STDOUT).decode('utf-8')
|
|
||||||
|
|
||||||
def run(self, arg: str, input: str = None, check=False) -> str:
|
|
||||||
if input:
|
|
||||||
input = input.encode('utf-8')
|
|
||||||
return subprocess.run(arg.split(), input=input, stdout=subprocess.PIPE,
|
|
||||||
check=check, stderr=subprocess.STDOUT).stdout.decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
# Twisted class
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
class ProcessProtocol(protocol.ProcessProtocol):
|
|
||||||
def __init__(self, onConnected, onOutReceived, onErrRecevied, onProcessEnded, logfile=None):
|
|
||||||
self.onConnected = onConnected
|
|
||||||
self.onOutReceived = onOutReceived
|
|
||||||
self.onErrRecevied = onErrRecevied
|
|
||||||
self.onProcessEnded = onProcessEnded
|
|
||||||
self.logfile = logfile
|
|
||||||
# We cannot import this at the top of the file because qt5reactor should
|
|
||||||
# be installed in the main function first.
|
|
||||||
from twisted.internet import reactor # pylint: disable=E0401
|
|
||||||
self.reactor = reactor
|
|
||||||
|
|
||||||
def run(self, arg: str):
|
|
||||||
"""Spawn a process
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
arg {str} -- arguments in string
|
|
||||||
"""
|
|
||||||
|
|
||||||
args = arg.split()
|
|
||||||
self.reactor.spawnProcess(self, args[0], args=args, env=os.environ)
|
|
||||||
|
|
||||||
def kill(self):
|
|
||||||
"""Kill a spawned process
|
|
||||||
"""
|
|
||||||
self.transport.signalProcess('INT')
|
|
||||||
|
|
||||||
def connectionMade(self):
|
|
||||||
print("connectionMade!")
|
|
||||||
self.onConnected()
|
|
||||||
self.transport.closeStdin() # No more input
|
|
||||||
|
|
||||||
def outReceived(self, data):
|
|
||||||
# print("outReceived! with %d bytes!" % len(data))
|
|
||||||
self.onOutReceived(data)
|
|
||||||
if self.logfile is not None:
|
|
||||||
self.logfile.write(data)
|
|
||||||
|
|
||||||
def errReceived(self, data):
|
|
||||||
# print("errReceived! with %d bytes!" % len(data))
|
|
||||||
self.onErrRecevied(data)
|
|
||||||
if self.logfile is not None:
|
|
||||||
self.logfile.write(data)
|
|
||||||
|
|
||||||
def inConnectionLost(self):
|
|
||||||
print("inConnectionLost! stdin is closed! (we probably did it)")
|
|
||||||
pass
|
|
||||||
|
|
||||||
def outConnectionLost(self):
|
|
||||||
print("outConnectionLost! The child closed their stdout!")
|
|
||||||
pass
|
|
||||||
|
|
||||||
def errConnectionLost(self):
|
|
||||||
print("errConnectionLost! The child closed their stderr.")
|
|
||||||
pass
|
|
||||||
|
|
||||||
def processExited(self, reason):
|
|
||||||
exitCode = reason.value.exitCode
|
|
||||||
if exitCode is None:
|
|
||||||
print("Unknown exit")
|
|
||||||
return
|
|
||||||
print("processEnded, status", exitCode)
|
|
||||||
|
|
||||||
def processEnded(self, reason):
|
|
||||||
if self.logfile is not None:
|
|
||||||
self.logfile.close()
|
|
||||||
exitCode = reason.value.exitCode
|
|
||||||
if exitCode is None:
|
|
||||||
print("Unknown exit")
|
|
||||||
self.onProcessEnded(1)
|
|
||||||
return
|
|
||||||
print("processEnded, status", exitCode)
|
|
||||||
print("quitting")
|
|
||||||
self.onProcessEnded(exitCode)
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
# Display properties
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
class Display(object):
|
|
||||||
__slots__ = ['name', 'primary', 'connected', 'active', 'width', 'height', 'x_offset', 'y_offset']
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.name: str = None
|
|
||||||
self.primary: bool = False
|
|
||||||
self.connected: bool = False
|
|
||||||
self.active: bool = False
|
|
||||||
self.width: int = 0
|
|
||||||
self.height: int = 0
|
|
||||||
self.x_offset: int = 0
|
|
||||||
self.y_offset: int = 0
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
ret = f"{self.name}"
|
|
||||||
if self.connected:
|
|
||||||
ret += " connected"
|
|
||||||
else:
|
|
||||||
ret += " disconnected"
|
|
||||||
if self.primary:
|
|
||||||
ret += " primary"
|
|
||||||
if self.active:
|
|
||||||
ret += f" {self.width}x{self.height}+{self.x_offset}+{self.y_offset}"
|
|
||||||
else:
|
|
||||||
ret += f" not active {self.width}x{self.height}"
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class DisplayProperty(QObject):
|
|
||||||
def __init__(self, display: Display, parent=None):
|
|
||||||
super(DisplayProperty, self).__init__(parent)
|
|
||||||
self._display = display
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display(self):
|
|
||||||
return self._display
|
|
||||||
|
|
||||||
@pyqtProperty(str, constant=True)
|
|
||||||
def name(self):
|
|
||||||
return self._display.name
|
|
||||||
|
|
||||||
@name.setter
|
|
||||||
def name(self, name):
|
|
||||||
self._display.name = name
|
|
||||||
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def primary(self):
|
|
||||||
return self._display.primary
|
|
||||||
|
|
||||||
@primary.setter
|
|
||||||
def primary(self, primary):
|
|
||||||
self._display.primary = primary
|
|
||||||
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def connected(self):
|
|
||||||
return self._display.connected
|
|
||||||
|
|
||||||
@connected.setter
|
|
||||||
def connected(self, connected):
|
|
||||||
self._display.connected = connected
|
|
||||||
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def active(self):
|
|
||||||
return self._display.active
|
|
||||||
|
|
||||||
@active.setter
|
|
||||||
def active(self, active):
|
|
||||||
self._display.active = active
|
|
||||||
|
|
||||||
@pyqtProperty(int, constant=True)
|
|
||||||
def width(self):
|
|
||||||
return self._display.width
|
|
||||||
|
|
||||||
@width.setter
|
|
||||||
def width(self, width):
|
|
||||||
self._display.width = width
|
|
||||||
|
|
||||||
@pyqtProperty(int, constant=True)
|
|
||||||
def height(self):
|
|
||||||
return self._display.height
|
|
||||||
|
|
||||||
@height.setter
|
|
||||||
def height(self, height):
|
|
||||||
self._display.height = height
|
|
||||||
|
|
||||||
@pyqtProperty(int, constant=True)
|
|
||||||
def x_offset(self):
|
|
||||||
return self._display.x_offset
|
|
||||||
|
|
||||||
@x_offset.setter
|
|
||||||
def x_offset(self, x_offset):
|
|
||||||
self._display.x_offset = x_offset
|
|
||||||
|
|
||||||
@pyqtProperty(int, constant=True)
|
|
||||||
def y_offset(self):
|
|
||||||
return self._display.y_offset
|
|
||||||
|
|
||||||
@y_offset.setter
|
|
||||||
def y_offset(self, y_offset):
|
|
||||||
self._display.y_offset = y_offset
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
# Screen adjustment class
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
class XRandR(SubprocessWrapper):
|
|
||||||
VIRT_SCREEN_SUFFIX = "_virt"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(XRandR, self).__init__()
|
|
||||||
self.mode_name: str
|
|
||||||
self.screens: List[Display] = []
|
|
||||||
self.virt: Display() = None
|
|
||||||
self.primary: Display() = None
|
|
||||||
self.virt_name: str = ''
|
|
||||||
self.virt_idx: int = None
|
|
||||||
self.primary_idx: int = None
|
|
||||||
# Primary display
|
|
||||||
self._update_screens()
|
|
||||||
|
|
||||||
def _update_screens(self) -> None:
|
|
||||||
output = self.run("xrandr")
|
|
||||||
self.primary = None
|
|
||||||
self.virt = None
|
|
||||||
self.screens = []
|
|
||||||
self.virt_idx = None
|
|
||||||
self.primary_idx = None
|
|
||||||
pattern = re.compile(r"^(\S*)\s+(connected|disconnected)\s+((primary)\s+)?"
|
|
||||||
r"((\d+)x(\d+)\+(\d+)\+(\d+)\s+)?.*$", re.M)
|
|
||||||
for idx, match in enumerate(pattern.finditer(output)):
|
|
||||||
screen = Display()
|
|
||||||
screen.name = match.group(1)
|
|
||||||
if self.virt_name and screen.name == self.virt_name:
|
|
||||||
self.virt_idx = idx
|
|
||||||
screen.primary = True if match.group(4) else False
|
|
||||||
if screen.primary:
|
|
||||||
self.primary_idx = idx
|
|
||||||
screen.connected = True if match.group(2) == "connected" else False
|
|
||||||
screen.active = True if match.group(5) else False
|
|
||||||
self.screens.append(screen)
|
|
||||||
if not screen.active:
|
|
||||||
continue
|
|
||||||
screen.width = int(match.group(6))
|
|
||||||
screen.height = int(match.group(7))
|
|
||||||
screen.x_offset = int(match.group(8))
|
|
||||||
screen.y_offset = int(match.group(9))
|
|
||||||
print("Display information:")
|
|
||||||
for s in self.screens:
|
|
||||||
print("\t", s)
|
|
||||||
if self.primary_idx is None:
|
|
||||||
raise RuntimeError("There is no primary screen detected.\n"
|
|
||||||
"Go to display settings and set\n"
|
|
||||||
"a primary screen\n")
|
|
||||||
if self.virt_idx == self.primary_idx:
|
|
||||||
raise RuntimeError("Virtual screen must be selected other than the primary screen")
|
|
||||||
if self.virt_idx is not None:
|
|
||||||
self.virt = self.screens[self.virt_idx]
|
|
||||||
elif self.virt_name and self.virt_idx is None:
|
|
||||||
raise RuntimeError("No virtual screen name found")
|
|
||||||
self.primary = self.screens[self.primary_idx]
|
|
||||||
|
|
||||||
def _add_screen_mode(self, width, height, portrait, hidpi) -> None:
|
|
||||||
if not self.virt or not self.virt_name:
|
|
||||||
raise RuntimeError("No virtual screen selected.\n"
|
|
||||||
"Go to Display->Virtual Display->Advaced\n"
|
|
||||||
"To select a device.")
|
|
||||||
# Set virtual screen property first
|
|
||||||
self.virt.width = width
|
|
||||||
self.virt.height = height
|
|
||||||
if portrait:
|
|
||||||
self.virt.width = height
|
|
||||||
self.virt.height = width
|
|
||||||
if hidpi:
|
|
||||||
self.virt.width *= 2
|
|
||||||
self.virt.height *= 2
|
|
||||||
self.mode_name = str(self.virt.width) + "x" + str(self.virt.height) + self.VIRT_SCREEN_SUFFIX
|
|
||||||
# Then create using xrandr command
|
|
||||||
args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}"
|
|
||||||
try:
|
|
||||||
self.check_output(args_addmode)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
# When failed create mode and then add again
|
|
||||||
output = self.run(f"cvt {self.virt.width} {self.virt.height}")
|
|
||||||
mode = re.search(r"^.*Modeline\s*\".*\"\s*(.*)$", output, re.M).group(1)
|
|
||||||
# Create new screen mode
|
|
||||||
self.check_output(f"xrandr --newmode {self.mode_name} {mode}")
|
|
||||||
# Add mode again
|
|
||||||
self.check_output(args_addmode)
|
|
||||||
# After adding mode the program should delete the mode automatically on exit
|
|
||||||
atexit.register(self.delete_virtual_screen)
|
|
||||||
for sig in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]:
|
|
||||||
signal.signal(sig, self._signal_handler)
|
|
||||||
|
|
||||||
def _signal_handler(self, signum=None, frame=None) -> None:
|
|
||||||
self.delete_virtual_screen()
|
|
||||||
os._exit(0)
|
|
||||||
|
|
||||||
def get_primary_screen(self) -> Display:
|
|
||||||
self._update_screens()
|
|
||||||
return self.primary
|
|
||||||
|
|
||||||
def get_virtual_screen(self) -> Display:
|
|
||||||
self._update_screens()
|
|
||||||
return self.virt
|
|
||||||
|
|
||||||
def create_virtual_screen(self, width, height, portrait=False, hidpi=False) -> None:
|
|
||||||
print("creating: ", self.virt)
|
|
||||||
self._update_screens()
|
|
||||||
self._add_screen_mode(width, height, portrait, hidpi)
|
|
||||||
self.check_output(f"xrandr --output {self.virt.name} --mode {self.mode_name}")
|
|
||||||
self.check_output("sleep 5")
|
|
||||||
self.check_output(f"xrandr --output {self.virt.name} --preferred")
|
|
||||||
self._update_screens()
|
|
||||||
|
|
||||||
def delete_virtual_screen(self) -> None:
|
|
||||||
self._update_screens()
|
|
||||||
try:
|
|
||||||
self.virt.name
|
|
||||||
self.mode_name
|
|
||||||
except AttributeError:
|
|
||||||
return
|
|
||||||
self.run(f"xrandr --output {self.virt.name} --off")
|
|
||||||
self.run(f"xrandr --delmode {self.virt.name} {self.mode_name}")
|
|
||||||
atexit.unregister(self.delete_virtual_screen)
|
|
||||||
self._update_screens()
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
# QML Backend class
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
class Backend(QObject):
|
|
||||||
""" Backend class for QML frontend """
|
|
||||||
|
|
||||||
class VNCState:
|
|
||||||
""" Enum to indicate a state of the VNC server """
|
|
||||||
OFF = 0
|
|
||||||
ERROR = 1
|
|
||||||
WAITING = 2
|
|
||||||
CONNECTED = 3
|
|
||||||
|
|
||||||
Q_ENUMS(VNCState)
|
|
||||||
|
|
||||||
# Signals
|
|
||||||
onVirtScreenCreatedChanged = pyqtSignal(bool)
|
|
||||||
onVirtScreenIndexChanged = pyqtSignal(int)
|
|
||||||
onVncUsePasswordChanged = pyqtSignal(bool)
|
|
||||||
onVncStateChanged = pyqtSignal(VNCState)
|
|
||||||
onIPAddressesChanged = pyqtSignal()
|
|
||||||
onDisplaySettingClosed = pyqtSignal()
|
|
||||||
onError = pyqtSignal(str)
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super(Backend, self).__init__(parent)
|
|
||||||
# Virtual screen properties
|
|
||||||
self.xrandr: XRandR = XRandR()
|
|
||||||
self._virtScreenCreated: bool = False
|
|
||||||
# VNC server properties
|
|
||||||
self._vncUsePassword: bool = False
|
|
||||||
self._vncState: self.VNCState = self.VNCState.OFF
|
|
||||||
# Primary screen and mouse posistion
|
|
||||||
self._primaryProp: DisplayProperty
|
|
||||||
self.vncServer: ProcessProtocol
|
|
||||||
# Check config file
|
|
||||||
# and initialize if needed
|
|
||||||
need_init = False
|
|
||||||
if not os.path.exists(CONFIG_PATH):
|
|
||||||
shutil.copy(DEFAULT_CONFIG_PATH, CONFIG_PATH)
|
|
||||||
need_init = True
|
|
||||||
# Version check
|
|
||||||
file_match = True
|
|
||||||
with open(CONFIG_PATH, 'r') as f_config, open(DATA_PATH, 'r') as f_data:
|
|
||||||
config = json.load(f_config)
|
|
||||||
data = json.load(f_data)
|
|
||||||
if config['version'] != data['version']:
|
|
||||||
file_match = False
|
|
||||||
# Override config with default when version doesn't match
|
|
||||||
if not file_match:
|
|
||||||
shutil.copy(DEFAULT_CONFIG_PATH, CONFIG_PATH)
|
|
||||||
need_init = True
|
|
||||||
# initialize config file
|
|
||||||
if need_init:
|
|
||||||
# 1. Available x11vnc options
|
|
||||||
# Get available x11vnc options from x11vnc first
|
|
||||||
p = SubprocessWrapper()
|
|
||||||
arg = 'x11vnc -opts'
|
|
||||||
ret = p.run(arg)
|
|
||||||
options = tuple(m.group(1) for m in re.finditer("\s*(-\w+)\s+", ret))
|
|
||||||
# Set/unset available x11vnc options flags in config
|
|
||||||
with open(CONFIG_PATH, 'r') as f, open(DATA_PATH, 'r') as f_data:
|
|
||||||
config = json.load(f)
|
|
||||||
data = json.load(f_data)
|
|
||||||
for key, value in config["x11vncOptions"].items():
|
|
||||||
if key in options:
|
|
||||||
value["available"] = True
|
|
||||||
else:
|
|
||||||
value["available"] = False
|
|
||||||
# 2. Default Display settings app for a Desktop Environment
|
|
||||||
desktop_environ = os.environ['XDG_CURRENT_DESKTOP'].lower()
|
|
||||||
for key, value in data['displaySettingApps'].items():
|
|
||||||
for de in value['XDG_CURRENT_DESKTOP']:
|
|
||||||
if de in desktop_environ:
|
|
||||||
config["displaySettingApp"] = key
|
|
||||||
# Save the new config
|
|
||||||
with open(CONFIG_PATH, 'w') as f:
|
|
||||||
f.write(json.dumps(config, indent=4, sort_keys=True))
|
|
||||||
|
|
||||||
# Qt properties
|
|
||||||
@pyqtProperty(str, constant=True)
|
|
||||||
def settings(self):
|
|
||||||
with open(CONFIG_PATH, "r") as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
@settings.setter
|
|
||||||
def settings(self, json_str):
|
|
||||||
with open(CONFIG_PATH, "w") as f:
|
|
||||||
f.write(json_str)
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=onVirtScreenCreatedChanged)
|
|
||||||
def virtScreenCreated(self):
|
|
||||||
return self._virtScreenCreated
|
|
||||||
|
|
||||||
@virtScreenCreated.setter
|
|
||||||
def virtScreenCreated(self, value):
|
|
||||||
self._virtScreenCreated = value
|
|
||||||
self.onVirtScreenCreatedChanged.emit(value)
|
|
||||||
|
|
||||||
@pyqtProperty(QQmlListProperty, constant=True)
|
|
||||||
def screens(self):
|
|
||||||
try:
|
|
||||||
return QQmlListProperty(DisplayProperty, self, [DisplayProperty(x) for x in self.xrandr.screens])
|
|
||||||
except RuntimeError as e:
|
|
||||||
self.onError.emit(str(e))
|
|
||||||
return QQmlListProperty(DisplayProperty, self, [])
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=onVncUsePasswordChanged)
|
|
||||||
def vncUsePassword(self):
|
|
||||||
if os.path.isfile(X11VNC_PASSWORD_PATH):
|
|
||||||
self._vncUsePassword = True
|
|
||||||
else:
|
|
||||||
if self._vncUsePassword:
|
|
||||||
self.vncUsePassword = False
|
|
||||||
return self._vncUsePassword
|
|
||||||
|
|
||||||
@vncUsePassword.setter
|
|
||||||
def vncUsePassword(self, use):
|
|
||||||
self._vncUsePassword = use
|
|
||||||
self.onVncUsePasswordChanged.emit(use)
|
|
||||||
|
|
||||||
@pyqtProperty(VNCState, notify=onVncStateChanged)
|
|
||||||
def vncState(self):
|
|
||||||
return self._vncState
|
|
||||||
|
|
||||||
@vncState.setter
|
|
||||||
def vncState(self, state):
|
|
||||||
self._vncState = state
|
|
||||||
self.onVncStateChanged.emit(self._vncState)
|
|
||||||
|
|
||||||
@pyqtProperty('QStringList', notify=onIPAddressesChanged)
|
|
||||||
def ipAddresses(self):
|
|
||||||
for interface in interfaces():
|
|
||||||
if interface == 'lo':
|
|
||||||
continue
|
|
||||||
addresses = ifaddresses(interface).get(AF_INET, None)
|
|
||||||
if addresses is None:
|
|
||||||
continue
|
|
||||||
for link in addresses:
|
|
||||||
if link is not None:
|
|
||||||
yield link['addr']
|
|
||||||
|
|
||||||
@pyqtProperty(DisplayProperty)
|
|
||||||
def primary(self):
|
|
||||||
try:
|
|
||||||
self._primaryProp = DisplayProperty(self.xrandr.get_primary_screen())
|
|
||||||
except RuntimeError as e:
|
|
||||||
self.onError.emit(str(e))
|
|
||||||
return
|
|
||||||
return self._primaryProp
|
|
||||||
|
|
||||||
@pyqtProperty(int)
|
|
||||||
def cursor_x(self):
|
|
||||||
cursor = QCursor().pos()
|
|
||||||
return cursor.x()
|
|
||||||
|
|
||||||
@pyqtProperty(int)
|
|
||||||
def cursor_y(self):
|
|
||||||
cursor = QCursor().pos()
|
|
||||||
return cursor.y()
|
|
||||||
|
|
||||||
# Qt Slots
|
|
||||||
@pyqtSlot(str, int, int, bool, bool)
|
|
||||||
def createVirtScreen(self, device, width, height, portrait, hidpi):
|
|
||||||
self.xrandr.virt_name = device
|
|
||||||
print("Creating a Virtual Screen...")
|
|
||||||
try:
|
|
||||||
self.xrandr.create_virtual_screen(width, height, portrait, hidpi)
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
|
||||||
return
|
|
||||||
except RuntimeError as e:
|
|
||||||
self.onError.emit(str(e))
|
|
||||||
return
|
|
||||||
self.virtScreenCreated = True
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def deleteVirtScreen(self):
|
|
||||||
print("Deleting the Virtual Screen...")
|
|
||||||
if self.vncState is not self.VNCState.OFF:
|
|
||||||
self.onError.emit("Turn off the VNC server first")
|
|
||||||
self.virtScreenCreated = True
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self.xrandr.delete_virtual_screen()
|
|
||||||
except RuntimeError as e:
|
|
||||||
self.onError.emit(str(e))
|
|
||||||
return
|
|
||||||
self.virtScreenCreated = False
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def createVNCPassword(self, password):
|
|
||||||
if password:
|
|
||||||
password += '\n' + password + '\n\n' # verify + confirm
|
|
||||||
p = SubprocessWrapper()
|
|
||||||
try:
|
|
||||||
p.run(f"x11vnc -storepasswd {X11VNC_PASSWORD_PATH}", input=password, check=True)
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
|
||||||
return
|
|
||||||
self.vncUsePassword = True
|
|
||||||
else:
|
|
||||||
self.onError.emit("Empty password")
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def deleteVNCPassword(self):
|
|
||||||
if os.path.isfile(X11VNC_PASSWORD_PATH):
|
|
||||||
os.remove(X11VNC_PASSWORD_PATH)
|
|
||||||
self.vncUsePassword = False
|
|
||||||
else:
|
|
||||||
self.onError.emit("Failed deleting the password file")
|
|
||||||
|
|
||||||
@pyqtSlot(int, str)
|
|
||||||
def startVNC(self, port, options=''):
|
|
||||||
# Check if a virtual screen created
|
|
||||||
if not self.virtScreenCreated:
|
|
||||||
self.onError.emit("Virtual Screen not crated.")
|
|
||||||
return
|
|
||||||
if self.vncState is not self.VNCState.OFF:
|
|
||||||
self.onError.emit("VNC Server is already running.")
|
|
||||||
return
|
|
||||||
# regex used in callbacks
|
|
||||||
patter_connected = re.compile(r"^.*Got connection from client.*$", re.M)
|
|
||||||
patter_disconnected = re.compile(r"^.*client_count: 0*$", re.M)
|
|
||||||
|
|
||||||
# define callbacks
|
|
||||||
def _onConnected():
|
|
||||||
print("VNC started.")
|
|
||||||
self.vncState = self.VNCState.WAITING
|
|
||||||
|
|
||||||
def _onReceived(data):
|
|
||||||
data = data.decode("utf-8")
|
|
||||||
if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data):
|
|
||||||
print("VNC connected.")
|
|
||||||
self.vncState = self.VNCState.CONNECTED
|
|
||||||
if (self._vncState is self.VNCState.CONNECTED) and patter_disconnected.search(data):
|
|
||||||
print("VNC disconnected.")
|
|
||||||
self.vncState = self.VNCState.WAITING
|
|
||||||
|
|
||||||
def _onEnded(exitCode):
|
|
||||||
if exitCode is not 0:
|
|
||||||
self.vncState = self.VNCState.ERROR
|
|
||||||
self.onError.emit('X11VNC: Error occurred.\n'
|
|
||||||
'Double check if the port is already used.')
|
|
||||||
self.vncState = self.VNCState.OFF # TODO: better handling error state
|
|
||||||
else:
|
|
||||||
self.vncState = self.VNCState.OFF
|
|
||||||
print("VNC Exited.")
|
|
||||||
atexit.unregister(self.stopVNC)
|
|
||||||
|
|
||||||
logfile = open(X11VNC_LOG_PATH, "wb")
|
|
||||||
self.vncServer = ProcessProtocol(_onConnected, _onReceived, _onReceived, _onEnded, logfile)
|
|
||||||
try:
|
|
||||||
virt = self.xrandr.get_virtual_screen()
|
|
||||||
except RuntimeError as e:
|
|
||||||
self.onError.emit(str(e))
|
|
||||||
return
|
|
||||||
clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}"
|
|
||||||
arg = f"x11vnc -rfbport {port} -clip {clip} {options}"
|
|
||||||
if self.vncUsePassword:
|
|
||||||
arg += f" -rfbauth {X11VNC_PASSWORD_PATH}"
|
|
||||||
self.vncServer.run(arg)
|
|
||||||
# auto stop on exit
|
|
||||||
atexit.register(self.stopVNC, force=True)
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def openDisplaySetting(self, app: str = "arandr"):
|
|
||||||
# define callbacks
|
|
||||||
def _onConnected():
|
|
||||||
print("External Display Setting opened.")
|
|
||||||
|
|
||||||
def _onReceived(data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _onEnded(exitCode):
|
|
||||||
print("External Display Setting closed.")
|
|
||||||
self.onDisplaySettingClosed.emit()
|
|
||||||
if exitCode is not 0:
|
|
||||||
self.onError.emit(f'Error opening "{running_program}".')
|
|
||||||
with open(DATA_PATH, 'r') as f:
|
|
||||||
data = json.load(f)['displaySettingApps']
|
|
||||||
if app not in data:
|
|
||||||
self.onError.emit('Wrong display settings program')
|
|
||||||
return
|
|
||||||
program_list = [data[app]['args'], "arandr"]
|
|
||||||
program = ProcessProtocol(_onConnected, _onReceived, _onReceived, _onEnded, None)
|
|
||||||
running_program = ''
|
|
||||||
for arg in program_list:
|
|
||||||
if not shutil.which(arg.split()[0]):
|
|
||||||
continue
|
|
||||||
running_program = arg
|
|
||||||
program.run(arg)
|
|
||||||
return
|
|
||||||
self.onError.emit('Failed to find a display settings program.\n'
|
|
||||||
'Please install ARandR package.\n'
|
|
||||||
'(e.g. sudo apt-get install arandr)\n'
|
|
||||||
'Please issue a feature request\n'
|
|
||||||
'if you wish to add a display settings\n'
|
|
||||||
'program for your Desktop Environment.')
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def stopVNC(self, force=False):
|
|
||||||
if force:
|
|
||||||
# Usually called from atexit().
|
|
||||||
self.vncServer.kill()
|
|
||||||
time.sleep(2) # Make sure X11VNC shutdown before execute next atexit().
|
|
||||||
if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED):
|
|
||||||
self.vncServer.kill()
|
|
||||||
else:
|
|
||||||
self.onError.emit("stopVNC called while it is not running")
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def clearCache(self):
|
|
||||||
engine.clearComponentCache()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def quitProgram(self):
|
|
||||||
self.blockSignals(True) # This will prevent invoking auto-restart or etc.
|
|
||||||
QApplication.instance().quit()
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
# Main Code
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
def main():
|
|
||||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QSystemTrayIcon, QMessageBox
|
|
||||||
|
|
||||||
if not QSystemTrayIcon.isSystemTrayAvailable():
|
|
||||||
QMessageBox.critical(None, "VirtScreen",
|
|
||||||
"Cannot detect system tray on this system.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if os.environ['XDG_SESSION_TYPE'] == 'wayland':
|
|
||||||
QMessageBox.critical(None, "VirtScreen",
|
|
||||||
"Currently Wayland is not supported")
|
|
||||||
sys.exit(1)
|
|
||||||
if not HOME_PATH:
|
|
||||||
QMessageBox.critical(None, "VirtScreen",
|
|
||||||
"Cannot detect home directory.")
|
|
||||||
sys.exit(1)
|
|
||||||
if not os.path.exists(HOME_PATH):
|
|
||||||
try:
|
|
||||||
os.makedirs(HOME_PATH)
|
|
||||||
except:
|
|
||||||
QMessageBox.critical(None, "VirtScreen",
|
|
||||||
"Cannot create ~/.config/virtscreen")
|
|
||||||
sys.exit(1)
|
|
||||||
if not shutil.which('x11vnc'):
|
|
||||||
QMessageBox.critical(None, "VirtScreen",
|
|
||||||
"x11vnc is not installed.")
|
|
||||||
sys.exit(1)
|
|
||||||
try:
|
|
||||||
test = XRandR()
|
|
||||||
except RuntimeError as e:
|
|
||||||
QMessageBox.critical(None, "VirtScreen", str(e))
|
|
||||||
sys.exit(1)
|
|
||||||
# Replace Twisted reactor with qt5reactor
|
|
||||||
import qt5reactor # pylint: disable=E0401
|
|
||||||
qt5reactor.install()
|
|
||||||
from twisted.internet import reactor # pylint: disable=E0401
|
|
||||||
|
|
||||||
app.setWindowIcon(QIcon(ICON_PATH))
|
|
||||||
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
|
|
||||||
# os.environ["QT_QUICK_CONTROLS_STYLE"] = "Fusion"
|
|
||||||
|
|
||||||
# Register the Python type. Its URI is 'People', it's v1.0 and the type
|
|
||||||
# will be called 'Person' in QML.
|
|
||||||
qmlRegisterType(DisplayProperty, 'VirtScreen.DisplayProperty', 1, 0, 'DisplayProperty')
|
|
||||||
qmlRegisterType(Backend, 'VirtScreen.Backend', 1, 0, 'Backend')
|
|
||||||
|
|
||||||
# Create a component factory and load the QML script.
|
|
||||||
engine = QQmlApplicationEngine()
|
|
||||||
engine.load(QUrl(MAIN_QML_PATH))
|
|
||||||
if not engine.rootObjects():
|
|
||||||
QMessageBox.critical(None, "VirtScreen", "Failed to load QML")
|
|
||||||
sys.exit(1)
|
|
||||||
sys.exit(app.exec_())
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
139
virtscreen/xrandr.py
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
"""XRandr parser"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import atexit
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .display import Display
|
||||||
|
from .process import SubprocessWrapper
|
||||||
|
|
||||||
|
|
||||||
|
VIRT_SCREEN_SUFFIX = "_virt"
|
||||||
|
|
||||||
|
|
||||||
|
class XRandR(SubprocessWrapper):
|
||||||
|
"""XRandr parser class"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(XRandR, self).__init__()
|
||||||
|
self.mode_name: str
|
||||||
|
self.screens: List[Display] = []
|
||||||
|
self.virt: Display() = None
|
||||||
|
self.primary: Display() = None
|
||||||
|
self.virt_name: str = ''
|
||||||
|
self.virt_idx: int = None
|
||||||
|
self.primary_idx: int = None
|
||||||
|
# Primary display
|
||||||
|
self._update_screens()
|
||||||
|
|
||||||
|
def _update_screens(self) -> None:
|
||||||
|
output = self.run("xrandr")
|
||||||
|
self.primary = None
|
||||||
|
self.virt = None
|
||||||
|
self.screens = []
|
||||||
|
self.virt_idx = None
|
||||||
|
self.primary_idx = None
|
||||||
|
pattern = re.compile(r"^(\S*)\s+(connected|disconnected)\s+((primary)\s+)?"
|
||||||
|
r"((\d+)x(\d+)\+(\d+)\+(\d+)\s+)?.*$", re.M)
|
||||||
|
for idx, match in enumerate(pattern.finditer(output)):
|
||||||
|
screen = Display()
|
||||||
|
screen.name = match.group(1)
|
||||||
|
if self.virt_name and screen.name == self.virt_name:
|
||||||
|
self.virt_idx = idx
|
||||||
|
screen.primary = True if match.group(4) else False
|
||||||
|
if screen.primary:
|
||||||
|
self.primary_idx = idx
|
||||||
|
screen.connected = True if match.group(2) == "connected" else False
|
||||||
|
screen.active = True if match.group(5) else False
|
||||||
|
self.screens.append(screen)
|
||||||
|
if not screen.active:
|
||||||
|
continue
|
||||||
|
screen.width = int(match.group(6))
|
||||||
|
screen.height = int(match.group(7))
|
||||||
|
screen.x_offset = int(match.group(8))
|
||||||
|
screen.y_offset = int(match.group(9))
|
||||||
|
logging.info("Display information:")
|
||||||
|
for s in self.screens:
|
||||||
|
logging.info(f"\t{s}")
|
||||||
|
if self.primary_idx is None:
|
||||||
|
raise RuntimeError("There is no primary screen detected.\n"
|
||||||
|
"Go to display settings and set\n"
|
||||||
|
"a primary screen\n")
|
||||||
|
if self.virt_idx == self.primary_idx:
|
||||||
|
raise RuntimeError("Virtual screen must be selected other than the primary screen")
|
||||||
|
if self.virt_idx is not None:
|
||||||
|
self.virt = self.screens[self.virt_idx]
|
||||||
|
elif self.virt_name and self.virt_idx is None:
|
||||||
|
raise RuntimeError("No virtual screen name found")
|
||||||
|
self.primary = self.screens[self.primary_idx]
|
||||||
|
|
||||||
|
def _add_screen_mode(self, width, height, portrait, hidpi) -> None:
|
||||||
|
if not self.virt or not self.virt_name:
|
||||||
|
raise RuntimeError("No virtual screen selected.\n"
|
||||||
|
"Go to Display->Virtual Display->Advaced\n"
|
||||||
|
"To select a device.")
|
||||||
|
# Set virtual screen property first
|
||||||
|
self.virt.width = width
|
||||||
|
self.virt.height = height
|
||||||
|
if portrait:
|
||||||
|
self.virt.width = height
|
||||||
|
self.virt.height = width
|
||||||
|
if hidpi:
|
||||||
|
self.virt.width *= 2
|
||||||
|
self.virt.height *= 2
|
||||||
|
self.mode_name = str(self.virt.width) + "x" + str(self.virt.height) + VIRT_SCREEN_SUFFIX
|
||||||
|
# Then create using xrandr command
|
||||||
|
args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}"
|
||||||
|
try:
|
||||||
|
self.check_output(args_addmode)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
# When failed create mode and then add again
|
||||||
|
output = self.run(f"cvt {self.virt.width} {self.virt.height}")
|
||||||
|
mode = re.search(r"^.*Modeline\s*\".*\"\s*(.*)$", output, re.M).group(1)
|
||||||
|
# Create new screen mode
|
||||||
|
self.check_output(f"xrandr --newmode {self.mode_name} {mode}")
|
||||||
|
# Add mode again
|
||||||
|
self.check_output(args_addmode)
|
||||||
|
# After adding mode the program should delete the mode automatically on exit
|
||||||
|
atexit.register(self.delete_virtual_screen)
|
||||||
|
|
||||||
|
def get_primary_screen(self) -> Display:
|
||||||
|
self._update_screens()
|
||||||
|
return self.primary
|
||||||
|
|
||||||
|
def get_virtual_screen(self) -> Display:
|
||||||
|
self._update_screens()
|
||||||
|
return self.virt
|
||||||
|
|
||||||
|
def create_virtual_screen(self, width, height, portrait=False, hidpi=False, pos='') -> None:
|
||||||
|
self._update_screens()
|
||||||
|
logging.info(f"creating: {self.virt}")
|
||||||
|
self._add_screen_mode(width, height, portrait, hidpi)
|
||||||
|
arg_pos = ['left', 'right', 'above', 'below']
|
||||||
|
xrandr_pos = ['--left-of', '--right-of', '--above', '--below']
|
||||||
|
if pos and pos in arg_pos:
|
||||||
|
# convert pos for xrandr
|
||||||
|
pos = xrandr_pos[arg_pos.index(pos)]
|
||||||
|
pos += ' ' + self.primary.name
|
||||||
|
elif not pos:
|
||||||
|
pos = '--preferred'
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Incorrect position option selected.")
|
||||||
|
self.check_output(f"xrandr --output {self.virt.name} --mode {self.mode_name}")
|
||||||
|
self.check_output("sleep 5")
|
||||||
|
self.check_output(f"xrandr --output {self.virt.name} {pos}")
|
||||||
|
self._update_screens()
|
||||||
|
|
||||||
|
def delete_virtual_screen(self) -> None:
|
||||||
|
self._update_screens()
|
||||||
|
try:
|
||||||
|
self.virt.name
|
||||||
|
self.mode_name
|
||||||
|
except AttributeError:
|
||||||
|
return
|
||||||
|
self.run(f"xrandr --output {self.virt.name} --off")
|
||||||
|
self.run(f"xrandr --delmode {self.virt.name} {self.mode_name}")
|
||||||
|
atexit.unregister(self.delete_virtual_screen)
|
||||||
|
self._update_screens()
|
||||||