diff --git a/.gitignore b/.gitignore
index aa1cd9b..46f649e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,9 @@
# files & folders for development use
debug
+# Archive file
+*.tar.gz
+
################################################################################
# Byte-compiled / optimized / DLL files
__pycache__/
diff --git a/.travis.yml b/.travis.yml
index bed3cfc..4e692cd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -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
diff --git a/Makefile b/Makefile
index d82dcdf..9ccaa05 100644
--- a/Makefile
+++ b/Makefile
@@ -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)
diff --git a/README.md b/README.md
index f588f79..cb22819 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,102 @@
-# VirtScreen
+
+
+
+ VirtScreen
+
-> Make your iPad/tablet/computer as a secondary monitor on Linux.
+
+ Make your iPad/tablet/computer as a secondary monitor on Linux.
+
-
+
+
+## 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.
-
+### 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
diff --git a/data/icon.xcf b/data/icon.xcf
deleted file mode 100644
index ba7b0fe..0000000
Binary files a/data/icon.xcf and /dev/null differ
diff --git a/data/icon_full.svg b/data/icon_full.svg
new file mode 100644
index 0000000..7e5732e
--- /dev/null
+++ b/data/icon_full.svg
@@ -0,0 +1,182 @@
+
diff --git a/data/systray_icon.svg b/data/systray_icon.svg
new file mode 100644
index 0000000..3187b20
--- /dev/null
+++ b/data/systray_icon.svg
@@ -0,0 +1,180 @@
+
+
diff --git a/data/virtscreen.png b/data/virtscreen.png
index ff65365..03d75e9 100644
Binary files a/data/virtscreen.png and b/data/virtscreen.png differ
diff --git a/launch_env.sh b/launch_env.sh
deleted file mode 100755
index 12c9cc3..0000000
--- a/launch_env.sh
+++ /dev/null
@@ -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
diff --git a/package/appimage/.gitignore b/package/appimage/.gitignore
index a874d89..1e70004 100644
--- a/package/appimage/.gitignore
+++ b/package/appimage/.gitignore
@@ -1,2 +1,2 @@
-VirtScreen-x86_64.AppImage
-virtscreen.AppDir
+*.AppImage
+*.AppDir
diff --git a/package/appimage/AppRun b/package/appimage/AppRun
index 2512c2b..f8e633e 100755
--- a/package/appimage/AppRun
+++ b/package/appimage/AppRun
@@ -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 $@
diff --git a/package/archlinux/.SRCINFO b/package/archlinux/.SRCINFO
deleted file mode 100644
index 5a5310f..0000000
--- a/package/archlinux/.SRCINFO
+++ /dev/null
@@ -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
-
diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD
index bbfc14f..b5ae795 100644
--- a/package/archlinux/PKGBUILD
+++ b/package/archlinux/PKGBUILD
@@ -1,15 +1,15 @@
# Maintainer: Bumsik Kim
_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"
diff --git a/package/build_all.sh b/package/build_all.sh
deleted file mode 100755
index c9ce382..0000000
--- a/package/build_all.sh
+++ /dev/null
@@ -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
diff --git a/package/debian/.gitignore b/package/debian/.gitignore
new file mode 100644
index 0000000..b006ca0
--- /dev/null
+++ b/package/debian/.gitignore
@@ -0,0 +1,3 @@
+*.deb
+*.buildinfo
+*.changes
diff --git a/package/debian/Makefile b/package/debian/Makefile
new file mode 100644
index 0000000..b9629da
--- /dev/null
+++ b/package/debian/Makefile
@@ -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
diff --git a/package/debian/Makefile.virtualenv b/package/debian/Makefile.virtualenv
deleted file mode 100644
index 02e1778..0000000
--- a/package/debian/Makefile.virtualenv
+++ /dev/null
@@ -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
diff --git a/package/debian/_common.sh b/package/debian/_common.sh
deleted file mode 100644
index 566bc68..0000000
--- a/package/debian/_common.sh
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/package/debian/build.sh b/package/debian/build.sh
new file mode 100755
index 0000000..dea5e40
--- /dev/null
+++ b/package/debian/build.sh
@@ -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
diff --git a/package/debian/contents.sh b/package/debian/contents.sh
deleted file mode 100755
index 4928a86..0000000
--- a/package/debian/contents.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/bash
-
-source _common.sh
-
-cd build
-dpkg -c virtscreen_$PKGVER-1_all.deb
diff --git a/package/debian/control b/package/debian/control
index 6fbe87c..f4874a0 100644
--- a/package/debian/control
+++ b/package/debian/control
@@ -2,15 +2,15 @@ Source: virtscreen
Section: utils
Priority: optional
Maintainer: Bumsik Kim
-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.
diff --git a/package/debian/control.virtualenv b/package/debian/control.virtualenv
deleted file mode 100644
index f4874a0..0000000
--- a/package/debian/control.virtualenv
+++ /dev/null
@@ -1,16 +0,0 @@
-Source: virtscreen
-Section: utils
-Priority: optional
-Maintainer: Bumsik Kim
-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.
diff --git a/package/debian/copy_debian.sh b/package/debian/copy_debian.sh
deleted file mode 100755
index 91d1d33..0000000
--- a/package/debian/copy_debian.sh
+++ /dev/null
@@ -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
diff --git a/package/debian/debmake.sh b/package/debian/debmake.sh
deleted file mode 100755
index 55d46a8..0000000
--- a/package/debian/debmake.sh
+++ /dev/null
@@ -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
diff --git a/package/debian/debuild.sh b/package/debian/debuild.sh
deleted file mode 100755
index 27e359e..0000000
--- a/package/debian/debuild.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-
-source _common.sh
-
-cd build
-cd virtscreen-$PKGVER
-if [ $1 = "virtualenv" ]; then
- dpkg-buildpackage -b
-else
- debuild
-fi
diff --git a/package/debian/rules b/package/debian/rules
deleted file mode 100755
index 3201ffa..0000000
--- a/package/debian/rules
+++ /dev/null
@@ -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
diff --git a/package/pypi/.gitignore b/package/pypi/.gitignore
new file mode 100644
index 0000000..5b08461
--- /dev/null
+++ b/package/pypi/.gitignore
@@ -0,0 +1,2 @@
+virtscreen*.whl
+*.tar.gz
diff --git a/setup.py b/setup.py
index f1355a7..36eeb37 100644
--- a/setup.py
+++ b/setup.py
@@ -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',
],
},
diff --git a/virtscreen/__init__.py b/virtscreen/__init__.py
index 8be907d..e69de29 100644
--- a/virtscreen/__init__.py
+++ b/virtscreen/__init__.py
@@ -1 +0,0 @@
-__all__ = ['virtscreen']
\ No newline at end of file
diff --git a/virtscreen/__main__.py b/virtscreen/__main__.py
new file mode 100755
index 0000000..5237741
--- /dev/null
+++ b/virtscreen/__main__.py
@@ -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()
diff --git a/virtscreen/assets/config.default.json b/virtscreen/assets/config.default.json
index bdf67b2..ae1978c 100644
--- a/virtscreen/assets/config.default.json
+++ b/virtscreen/assets/config.default.json
@@ -1,5 +1,5 @@
{
- "version": "0.2.4",
+ "version": "0.3.1",
"x11vncVersion": "0.9.15",
"theme_color": 8,
"virt": {
diff --git a/virtscreen/assets/data.json b/virtscreen/assets/data.json
index 4380a48..ec975a5 100644
--- a/virtscreen/assets/data.json
+++ b/virtscreen/assets/data.json
@@ -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": [""]
}
}
}
\ No newline at end of file
diff --git a/virtscreen/assets/main.qml b/virtscreen/assets/main.qml
index c2c1f74..43a4fdf 100644
--- a/virtscreen/assets/main.qml
+++ b/virtscreen/assets/main.qml
@@ -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
diff --git a/virtscreen/display.py b/virtscreen/display.py
new file mode 100644
index 0000000..6a162b2
--- /dev/null
+++ b/virtscreen/display.py
@@ -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
diff --git a/virtscreen/icon/full_256x256.png b/virtscreen/icon/full_256x256.png
new file mode 100644
index 0000000..03d75e9
Binary files /dev/null and b/virtscreen/icon/full_256x256.png differ
diff --git a/virtscreen/icon/icon.png b/virtscreen/icon/icon.png
deleted file mode 100644
index ff65365..0000000
Binary files a/virtscreen/icon/icon.png and /dev/null differ
diff --git a/virtscreen/icon/icon_tablet_off.png b/virtscreen/icon/icon_tablet_off.png
deleted file mode 100644
index 373fd8f..0000000
Binary files a/virtscreen/icon/icon_tablet_off.png and /dev/null differ
diff --git a/virtscreen/icon/icon_tablet_on.png b/virtscreen/icon/icon_tablet_on.png
deleted file mode 100644
index be9b72b..0000000
Binary files a/virtscreen/icon/icon_tablet_on.png and /dev/null differ
diff --git a/virtscreen/icon/systray_no_tablet.png b/virtscreen/icon/systray_no_tablet.png
new file mode 100644
index 0000000..92be6da
Binary files /dev/null and b/virtscreen/icon/systray_no_tablet.png differ
diff --git a/virtscreen/icon/systray_tablet_off.png b/virtscreen/icon/systray_tablet_off.png
new file mode 100644
index 0000000..a6d850e
Binary files /dev/null and b/virtscreen/icon/systray_tablet_off.png differ
diff --git a/virtscreen/icon/systray_tablet_on.png b/virtscreen/icon/systray_tablet_on.png
new file mode 100644
index 0000000..686202d
Binary files /dev/null and b/virtscreen/icon/systray_tablet_on.png differ
diff --git a/virtscreen/path.py b/virtscreen/path.py
new file mode 100644
index 0000000..3d7cabf
--- /dev/null
+++ b/virtscreen/path.py
@@ -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"
diff --git a/virtscreen/process.py b/virtscreen/process.py
new file mode 100644
index 0000000..6d00040
--- /dev/null
+++ b/virtscreen/process.py
@@ -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)
diff --git a/virtscreen/qt_backend.py b/virtscreen/qt_backend.py
new file mode 100644
index 0000000..97988d3
--- /dev/null
+++ b/virtscreen/qt_backend.py
@@ -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']
diff --git a/virtscreen/virtscreen.py b/virtscreen/virtscreen.py
deleted file mode 100755
index 8d49b77..0000000
--- a/virtscreen/virtscreen.py
+++ /dev/null
@@ -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()
diff --git a/virtscreen/xrandr.py b/virtscreen/xrandr.py
new file mode 100644
index 0000000..e0e5da7
--- /dev/null
+++ b/virtscreen/xrandr.py
@@ -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()