1
0
Fork 0
mirror of https://github.com/kbumsik/VirtScreen.git synced 2025-03-09 15:40:18 +00:00

Compare commits

...

33 commits

Author SHA1 Message Date
Bumsik Kim
9637d62816
Version bump: v0.3.1 2018-11-09 18:36:07 +09:00
Bumsik Kim
bd115a29f2
FIXED: abort program when ~/.config/virtscreen does not exist 2018-11-09 18:15:51 +09:00
Bumsik Kim
6759ac6ae2
Version bump: 0.3.0 2018-11-07 17:50:06 +09:00
Bumsik Kim
88079ad98a
Makefile: Added tar.gz archive 2018-11-07 17:50:01 +09:00
Bumsik Kim
4a2e7d0c54
Remove versions in packages name 2018-11-07 06:17:26 +09:00
Bumsik Kim
a3e65b8270
Add make debug 2018-11-07 05:31:22 +09:00
Bumsik Kim
00388dcb0a
Enable status printing in CLI mode #8 2018-11-07 05:19:37 +09:00
Bumsik Kim
ebbbf97cdf
Use python logging module #8 2018-11-07 04:44:37 +09:00
Bumsik Kim
f27db06b17
Disable overriding PATH environment variable #19 2018-11-06 21:06:50 +09:00
Luke Anderson
9047091dd0 Fixed parsing of configuration file so that desktop environment is correctly correlated with the display setting app, according to the data.json configuration. 2018-11-04 10:57:41 +09:00
Luke Anderson
3e997c596a Added os.environ.get calls to prevent the assumption that the environment variables are set, which causes a KeyError. 2018-11-04 10:57:41 +09:00
Bumsik Kim
f62fa66f51
Version bump: v0.2.5. PyPI: update setup.py metadata. Debian: added --yes option for debmake 2018-08-22 14:54:45 -04:00
Bumsik Kim
facb96ca19
Icon: updated virtscreen.png 2018-06-29 21:08:54 -04:00
Bumsik Kim
2fd2119a7a
Icon: fix icon wasn't a perfect square 2018-06-29 20:37:23 -04:00
Bumsik Kim
9b8c1a71a4
main.py: delted Twisted code, added Qt application name 2018-06-29 20:35:38 -04:00
Bumsik Kim
451ada820b
README: updated How to Use 2018-06-29 20:33:35 -04:00
Bumsik Kim
c31f7054c4
Icon: Applied the new icon to the app 2018-06-29 19:22:09 -04:00
Bumsik Kim
d2ebf4bb0d
Icon: deleted unnecessary header 2018-06-29 18:18:49 -04:00
Bumsik Kim
3fe258a96b
README: added the new SVG file 2018-06-29 18:17:57 -04:00
Bumsik Kim
706a8d9ddf
Icon: added a new SVG icon 2018-06-29 18:03:14 -04:00
Bumsik Kim
28dabf2271
Changed main.py to __main__.py 2018-06-28 10:26:41 -04:00
Bumsik Kim
96c6066a91
split single virtscreen.py into submodules 2018-06-28 10:12:08 -04:00
Bumsik Kim
2ea15b8943
README: added asyncio 2018-06-28 08:14:53 -04:00
Bumsik Kim
c09fffe6e8
Backend: switched to asyncio from Twisted 2018-06-28 07:21:31 -04:00
Bumsik Kim
8393dab1a5
README: added FUreatures and CLI usage 2018-06-26 16:27:37 -04:00
Bumsik Kim
f5884ae9a1
AppImage: added command line arguments 2018-06-26 14:35:53 -04:00
Bumsik Kim
2bf8dedf9d
Debian: upgraded to AppImage based package 2018-06-26 13:12:34 -04:00
Bumsik Kim
7dcf8a8bde
Debian: cleanup build system 2018-06-26 05:02:30 -04:00
Bumsik Kim
a97e532b93
package/build_all.sh: Deleted and updated Makefile and Travis accordingly 2018-06-26 04:35:32 -04:00
Bumsik Kim
19d8e1a180
AUR: deleted md5sum 2018-06-26 04:32:03 -04:00
Bumsik Kim
d357296306
Makefile: cleanup build system 2018-06-26 03:49:45 -04:00
Bumsik Kim
af7b9348a5
AUR: version bump 2018-06-25 14:38:58 -04:00
Bumsik Kim
41af47f65e
README: updated installation section 2018-06-25 14:32:18 -04:00
46 changed files with 1623 additions and 1233 deletions

3
.gitignore vendored
View file

@ -11,6 +11,9 @@
# files & folders for development use
debug
# Archive file
*.tar.gz
################################################################################
# Byte-compiled / optimized / DLL files
__pycache__/

View file

@ -4,23 +4,28 @@ python: '3.6'
services:
- docker
install:
- make docker-pull
- pip3 install .
install: |
docker pull kbumsik/virtscreen
pip3 install .
script:
- echo No test scripts implemented yet. Travis is used only for deploy yet.
script: |
echo No test scripts implemented yet. Travis is used only for deploy yet.
before_deploy:
- package/build_all.sh $TRAVIS_TAG
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/build/virtscreen_$TRAVIS_TAG-1_all.deb
- package/appimage/VirtScreen-x86_64.AppImage
- package/debian/virtscreen.deb
- package/appimage/VirtScreen.AppImage
skip_cleanup: true
on:
tags: true

150
Makefile
View file

@ -1,94 +1,93 @@
# See https://packaging.python.org/tutorials/distributing-packages/#packaging-your-project
# for python packaging reference.
VERSION ?= 0.3.1
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)
DOCKER_RUN_DEB=docker run -v $(shell pwd)/package/debian:/app $(DOCKER_NAME)
.PHONY:
python-wheel:
python3 setup.py bdist_wheel --universal
python-install:
pip3 install . --user
python-uninstall:
pip3 uninstall virtscreen
python-clean:
rm -rf build dist virtscreen.egg-info virtscreen/qml/*.qmlc
pip-upload: python-wheel
twine upload dist/*
PKG_APPIMAGE=package/appimage/VirtScreen.AppImage
PKG_DEBIAN=package/debian/virtscreen.deb
ARCHIVE=virtscreen-$(VERSION).tar.gz
.ONESHELL:
# Docker
docker-build:
docker build -f Dockerfile -t $(DOCKER_NAME) .
.PHONY: run debug run-appimage debug-appimage
all: package/pypi/*.whl $(ARCHIVE) $(PKG_APPIMAGE) $(PKG_DEBIAN)
# Run script
run:
python3 -m virtscreen
debug:
QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 python3 -m virtscreen --log=DEBUG
run-appimage: $(PKG_APPIMAGE)
$<
debug-appimage: $(PKG_APPIMAGE)
QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 $< --log=DEBUG
# tar.gz
.PHONY: archive
archive $(ARCHIVE):
git archive --format=tar.gz --prefix=virtscreen-$(VERSION)/ -o $@ HEAD
# Docker tools
.PHONY: docker docker-build
docker:
$(DOCKER_RUN_TTY) /bin/bash
docker-rm:
docker image rm -f $(DOCKER_NAME)
docker-pull:
docker pull $(DOCKER_NAME)
docker-build:
docker build -f Dockerfile -t $(DOCKER_NAME) .
docker-push:
docker login
docker push $(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
appimage-build:
.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:
$(DOCKER_RUN) rm -rf package/appimage/virtscreen.AppDir package/appimage/VirtScreen-x86_64.AppImage
-rm -rf package/appimage/virtscreen.AppDir $(PKG_APPIMAGE)
# For Debian packaging, https://www.debian.org/doc/manuals/debmake-doc/ch08.en.html#setup-py
deb-make:
$(DOCKER_RUN_DEB) /app/debmake.sh
# 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
deb-build: deb-make
$(DOCKER_RUN_DEB) /app/copy_debian.sh
$(DOCKER_RUN_DEB) /app/debuild.sh
$(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:
$(DOCKER_RUN_DEB) /app/contents.sh
deb-env-make:
$(DOCKER_RUN_DEB) /app/debmake.sh virtualenv
deb-env-build: deb-env-make
$(DOCKER_RUN_DEB) /app/copy_debian.sh virtualenv
$(DOCKER_RUN_DEB) /app/debuild.sh virtualenv
deb-chown:
$(DOCKER_RUN_DEB) chown -R $(shell id -u):$(shell id -u) /app/build
deb-contents: $(PKG_DEBIAN)
$(DOCKER_RUN) dpkg -c $<
deb-clean:
$(DOCKER_RUN_DEB) rm -rf /app/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
# and: https://wiki.archlinux.org/index.php/Creating_packages
arch-update:
cd package/archlinux
makepkg --printsrcinfo > .SRCINFO
.PHONY: arch-upload arch-clean
arch-install: arch-update
cd package/archlinux
makepkg -si
arch-build: arch-update
cd package/archlinux
makepkg
arch-upload: arch-update
arch-upload: package/archlinux/.SRCINFO
cd package/archlinux
git clone ssh://aur@aur.archlinux.org/virtscreen.git
cp PKGBUILD virtscreen
@ -100,8 +99,33 @@ arch-upload: arch-update
cd ..
rm -rf virtscreen
package/archlinux/.SRCINFO:
cd package/archlinux
makepkg --printsrcinfo > .SRCINFO
arch-clean:
cd package/archlinux
rm -rf pkg src *.tar*
-rm -rf pkg src *.tar* .SRCINFO
clean: appimage-clean arch-clean deb-clean python-clean
# Override version
.PHONY: override-version
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)

102
README.md
View file

@ -1,37 +1,102 @@
# 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>
![gif example](https://raw.githubusercontent.com/kbumsik/VirtScreen/master/data/gif_example.gif)
<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 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
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.
![desktop entry](https://raw.githubusercontent.com/kbumsik/VirtScreen/master/data/desktop_entry.png)
### 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
$ 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
### 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)
#### `.deb` package
Download a `.deb` package from [releases page](https://github.com/kbumsik/VirtScreen/releases). Then install it:
```bash
```shell
sudo apt-get update
sudo apt-get install x11vnc qtbase5-dev
wget https://github.com/kbumsik/VirtScreen/releases/download/0.2.1/virtscreen_0.2.1-1_all.deb
sudo dpkg -i virtscreen_0.2.1-1_all.deb
rm virtscreen_0.2.1-1_all.deb
sudo apt-get install x11vnc
sudo dpkg -i virtscreen.deb
rm virtscreen.deb
```
### Arch Linux (AUR)
@ -44,14 +109,7 @@ yaourt virtscreen
### 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.
You need [`x11vnc`](https://github.com/LibVNC/x11vnc), `xrandr`. To install (e.g. on Ubuntu):
```bash
sudo apt-get install x11vnc # On Debian/Ubuntu, xrandr is included.
```
After you install the dependancies then run:
Although not recommended, you may install it using `pip`. In this case, you need to install the dependancy (`xrandr` and `x11vnc`) manually.
```bash
sudo pip install virtscreen

Binary file not shown.

182
data/icon_full.svg Normal file
View 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
View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

View file

@ -1,13 +0,0 @@
#!/bin/sh
# This script is only for isolated miniconda environment
# Used in Debian & AppImage package
ENV=/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

View file

@ -1,2 +1,2 @@
VirtScreen-x86_64.AppImage
virtscreen.AppDir
*.AppImage
*.AppDir

View file

@ -4,7 +4,6 @@
SCRIPTDIR=$(dirname $0)
ENV=$SCRIPTDIR/usr/share/virtscreen/env
echo $SCRIPTDIR
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
@ -12,4 +11,4 @@ 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
$ENV/bin/python3 $ENV/bin/virtscreen $@

View file

@ -1,22 +0,0 @@
pkgbase = virtscreen
pkgdesc = Make your iPad/tablet/computer as a secondary monitor on Linux
pkgver = 0.2.1
pkgrel = 1
url = https://github.com/kbumsik/VirtScreen
arch = i686
arch = x86_64
license = GPL
makedepends = python-pip
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.2.1.tar.gz
sha256sums = 9af568a73ff3523144bfbeacb7131d4fff9fc4fb8ee3fddb90d78f54b774acb7
pkgname = virtscreen

View file

@ -1,15 +1,15 @@
# Maintainer: Bumsik Kim <k.bumsik@gmail.com>
_pkgname_camelcase=VirtScreen
pkgname=virtscreen
pkgver=0.2.1
pkgver=0.3.1
pkgrel=1
pkgdesc="Make your iPad/tablet/computer as a secondary monitor on Linux"
arch=("i686" "x86_64")
url="https://github.com/kbumsik/VirtScreen"
license=('GPL')
groups=()
depends=('xorg-xrandr' 'x11vnc' 'python-pyqt5' 'python-twisted' 'python-netifaces' 'python-qt5reactor')
makedepends=('python-pip')
depends=('xorg-xrandr' 'x11vnc' 'python-pyqt5' 'qt5-quickcontrols2' 'python-quamash-git' 'python-netifaces')
makedepends=('python-pip' 'perl')
optdepends=(
'arandr: for display settings option'
)
@ -20,13 +20,21 @@ backup=()
options=()
install=
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=()
sha256sums=('9af568a73ff3523144bfbeacb7131d4fff9fc4fb8ee3fddb90d78f54b774acb7')
md5sums=('SKIP')
prepare() {
cd $srcdir/src
# Delete PyQt5 from install_requires because python-pyqt5 does not have PyPI metadata.
# See https://bugs.archlinux.org/task/58887
perl -pi -e "s/\'PyQt5>=\d+\.\d+\.\d+\',//" \
setup.py
}
package() {
cd $_pkgname_camelcase-$pkgver
PIP_CONFIG_FILE=/dev/null /usr/bin/pip install --isolated --root="$pkgdir" --ignore-installed --no-deps .
cd $srcdir/src
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
# install -Dm644 "data/$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop"
# install -Dm644 "data/icon.png" "$pkgdir/usr/share/pixmaps/$pkgname.png"

View file

@ -1,58 +0,0 @@
#!/bin/bash
# Get parameters. Just return 0 if no parameter passed
if [ -n "$1" ]; then
VERSION=$1
else
exit 0
fi
# Directory
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ROOT=$DIR/..
override_version () {
# Update python setup.py
perl -pi -e "s/version=\'\d+\.\d+\.\d+\'/version=\'$VERSION\'/" \
$ROOT/setup.py
# Update .json files in the module
perl -pi -e "s/\"version\"\s*\:\s*\"\d+\.\d+\.\d+\"/\"version\"\: \"$VERSION\"/" \
$ROOT/virtscreen/assets/data.json
perl -pi -e "s/\"version\"\s*\:\s*\"\d+\.\d+\.\d+\"/\"version\"\: \"$VERSION\"/" \
$ROOT/virtscreen/assets/config.default.json
# Arch AUR
perl -pi -e "s/pkgver=\d+\.\d+\.\d+/pkgver=$VERSION/" \
$ROOT/package/archlinux/PKGBUILD
# Debian
perl -pi -e "s/PKGVER=\d+\.\d+\.\d+/PKGVER=$VERSION/" \
$ROOT/package/debian/_common.sh
}
build_pypi () {
make -C $ROOT python-wheel
}
build_appimage () {
make -C $ROOT appimage-build
}
build_arch () {
wget -q https://github.com/kbumsik/VirtScreen/archive/$VERSION.tar.gz
SHA256=$(sha256sum $VERSION.tar.gz | cut -d' ' -f1)
# Arch AUR
perl -pi -e "s/sha256sums=\('.*'\)/sha256sums=('$SHA256')/" \
$ROOT/package/archlinux/PKGBUILD
rm $VERSION.tar.gz
make -C $ROOT arch-upload
}
build_debian () {
make -C $ROOT deb-env-build
make -C $ROOT deb-chown
}
override_version
# build_pypi
build_appimage
# build_arch
build_debian

3
package/debian/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*.deb
*.buildinfo
*.changes

28
package/debian/Makefile Normal file
View 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

View file

@ -1,40 +0,0 @@
prefix = /usr
all:
: # do nothing
SHELL = /bin/bash
install:
# Create virtualenv
install -d $(DESTDIR)$(prefix)/share/virtscreen
source $(HOME)/miniconda/bin/activate && \
conda create -y --copy --prefix $(DESTDIR)$(prefix)/share/virtscreen/env python=3.6
# Install VirtScreen using pip
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 launch_env.sh \
$(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 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

View file

@ -1,7 +0,0 @@
#!/bin/bash
PKGVER=0.2.4
# Required for debmake
DEBEMAIL="k.bumsik@gmail.com"
DEBFULLNAME="Bumsik Kim"
export PKGVER DEBEMAIL DEBFULLNAME

44
package/debian/build.sh Executable file
View 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

View file

@ -1,6 +0,0 @@
#!/bin/bash
source _common.sh
cd build
dpkg -c virtscreen_$PKGVER-1_all.deb

View file

@ -2,15 +2,15 @@ Source: virtscreen
Section: utils
Priority: optional
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
Homepage: https://github.com/kbumsik/VirtScreen
X-Python3-Version: >= 3.6
X-Python3-Version: >= 3.5
Package: virtscreen
Architecture: all
Multi-Arch: foreign
Depends: ${misc:Depends}, ${python3:Depends}, x11vnc, python3-pyqt5, python3-twisted, python3-netifaces
Depends: ${misc:Depends}, x11vnc
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.

View file

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

View file

@ -1,11 +0,0 @@
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source $DIR/_common.sh
if [ $1 = "virtualenv" ]; then
cp -f $DIR/control.virtualenv $DIR/build/virtscreen-$PKGVER/debian/control
cp -f $DIR/README.Debian $DIR/build/virtscreen-$PKGVER/debian/
else
cp -f $DIR/{control,rules,README.Debian} $DIR/build/virtscreen-$PKGVER/debian
fi

View file

@ -1,20 +0,0 @@
#!/bin/bash
source _common.sh
mkdir build
cd build
# Download
wget -q 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 -f ../../Makefile.virtualenv Makefile
debmake -b':sh'
else
debmake -b':py3'
fi

View file

@ -1,11 +0,0 @@
#!/bin/bash
source _common.sh
cd build
cd virtscreen-$PKGVER
if [ $1 = "virtualenv" ]; then
dpkg-buildpackage -b
else
debuild
fi

View file

@ -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
View file

@ -0,0 +1,2 @@
virtscreen*.whl
*.tar.gz

View file

@ -40,7 +40,7 @@ setup(
# For a discussion on single-sourcing the version across setup.py and the
# project code, see
# https://packaging.python.org/en/latest/single_source_version.html
version='0.2.4', # Required
version='0.3.1', # Required
# This is a one-line description or tagline of what your project does. This
# corresponds to the "Summary" metadata field:
@ -107,8 +107,8 @@ setup(
# Specify the Python versions you support here. In particular, ensure
# that you indicate whether you support Python 2, Python 3 or both.
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
# Environment
'Environment :: X11 Applications',
@ -116,8 +116,7 @@ setup(
'Operating System :: POSIX :: Linux',
# Framework used
'Framework :: Twisted',
# 'Framework :: AsyncIO',
'Framework :: AsyncIO',
],
# This field adds keywords for your project which will appear on the
@ -136,7 +135,7 @@ setup(
#
# py_modules=["my_module"],
#
packages=['virtscreen'], # Required
packages=find_packages(), # Required
# 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
@ -145,8 +144,7 @@ setup(
# For an analysis of "install_requires" vs pip's requirements files see:
# https://packaging.python.org/en/latest/requirements.html
install_requires=['PyQt5>=5.10.1',
'Twisted>=17.9.0',
'qt5reactor>=0.5',
'Quamash>=0.6.0',
'netifaces>=0.10.6'], # Optional
# List additional groups of dependencies here (e.g. development
@ -196,7 +194,7 @@ setup(
# executes the function `main` from this package when invoked:
entry_points={ # Optional
'console_scripts': [
'virtscreen = virtscreen.virtscreen:main',
'virtscreen = virtscreen.__main__:main',
],
},

View file

@ -1 +0,0 @@
__all__ = ['virtscreen']

218
virtscreen/__main__.py Executable file
View 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()

View file

@ -1,5 +1,5 @@
{
"version": "0.2.4",
"version": "0.3.1",
"x11vncVersion": "0.9.15",
"theme_color": 8,
"virt": {

View file

@ -1,5 +1,5 @@
{
"version": "0.2.4",
"version": "0.3.1",
"x11vncOptions": {
"-ncache": {
"value": "-ncache",
@ -34,7 +34,7 @@
"value": "arandr",
"name": "ARandR",
"args": "arandr",
"XDG_CURRENT_DESKTOP": []
"XDG_CURRENT_DESKTOP": [""]
}
}
}

View file

@ -129,9 +129,9 @@ Item {
// Sytray Icon
SystemTrayIcon {
id: sysTrayIcon
iconSource: backend.vncState == Backend.CONNECTED ? "../icon/icon_tablet_on.png" :
backend.virtScreenCreated ? "../icon/icon_tablet_off.png" :
"../icon/icon.png"
iconSource: backend.vncState == Backend.CONNECTED ? "../icon/systray_tablet_on.png" :
backend.virtScreenCreated ? "../icon/systray_tablet_off.png" :
"../icon/systray_no_tablet.png"
visible: true
property bool clicked: false

108
virtscreen/display.py Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

38
virtscreen/path.py Normal file
View 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
View 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
View 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']

View file

@ -1,894 +0,0 @@
#!/usr/bin/python3
# Python standard packages
import sys
import os
import subprocess
import signal
import re
import atexit
import time
import json
import shutil
import argparse
from pathlib import Path
from enum import Enum
from typing import List, Dict, Callable
# 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)
# 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)
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()
print("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()
# -------------------------------------------------------------------------------
# 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)
onVncUsePasswordChanged = pyqtSignal(bool)
onVncStateChanged = pyqtSignal(VNCState)
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.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
# 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)
# Qt Slots
@pyqtSlot(str, int, int, bool, bool)
def createVirtScreen(self, device, width, height, portrait, hidpi, pos=''):
self.xrandr.virt_name = device
print("Creating a Virtual Screen...")
try:
self.xrandr.create_virtual_screen(width, height, portrait, hidpi, pos)
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)
def startVNC(self, port):
# 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)
# 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 = 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(3) # 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()
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']
# -------------------------------------------------------------------------------
# Main Code
# -------------------------------------------------------------------------------
def main() -> None:
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')
# 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)
# Start main
args = parser.parse_args()
if any(vars(args).values()):
main_cli(args)
else:
main_gui()
print('Program should not reach here.')
sys.exit(1)
def check_env(msg: Callable[[str], None]) -> None:
if os.environ['XDG_SESSION_TYPE'].lower() == 'wayland':
msg("Currently Wayland is not supported")
sys.exit(1)
if not HOME_PATH:
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)
if not shutil.which('x11vnc'):
msg("x11vnc is not installed.")
sys.exit(1)
try:
test = XRandR()
except RuntimeError as e:
msg(str(e))
sys.exit(1)
def main_gui():
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QApplication(sys.argv)
# 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(dialog)
# 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')
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_())
reactor.run()
def main_cli(args: argparse.Namespace):
for key, value in vars(args).items():
print(key, ": ", value)
# Check the environment
check_env(print)
if not os.path.exists(CONFIG_PATH):
print("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()
# 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 vars(args)[prop]:
config['virt'][prop] = True
args_position = ['left', 'right', 'above', 'below']
tmp_args = {k: vars(args)[k] for k in args_position}
if not any(tmp_args.values()):
print("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):
print('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)
from twisted.internet import reactor # pylint: disable=E0401
backend.startVNC(config['vnc']['port'])
reactor.run()
if __name__ == '__main__':
main()

139
virtscreen/xrandr.py Normal file
View 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()