From 41af47f65ed1c1a05bc0d7271b4ffbefb447933b Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Mon, 25 Jun 2018 14:32:18 -0400 Subject: [PATCH 01/33] README: updated installation section --- README.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f588f79..0f5c89a 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,25 @@ $ virtscreen ## 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-x86_64.AppImage +``` + +Then you can run it by double click the file or `./VirtScreen-x86_64.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_0.2.4-1_all.deb +rm virtscreen_0.2.4-1_all.deb ``` ### Arch Linux (AUR) @@ -44,7 +53,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. +Although not recommended, 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 From af7b9348a5419e952121ccd24918d42f663a6c4f Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Mon, 25 Jun 2018 14:38:58 -0400 Subject: [PATCH 02/33] AUR: version bump --- package/archlinux/.SRCINFO | 6 +++--- package/archlinux/PKGBUILD | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package/archlinux/.SRCINFO b/package/archlinux/.SRCINFO index 5a5310f..9388d69 100644 --- a/package/archlinux/.SRCINFO +++ b/package/archlinux/.SRCINFO @@ -1,6 +1,6 @@ pkgbase = virtscreen pkgdesc = Make your iPad/tablet/computer as a secondary monitor on Linux - pkgver = 0.2.1 + pkgver = 0.2.4 pkgrel = 1 url = https://github.com/kbumsik/VirtScreen arch = i686 @@ -15,8 +15,8 @@ pkgbase = virtscreen 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 + source = https://github.com/kbumsik/VirtScreen/archive/0.2.4.tar.gz + sha256sums = 0a62fd5e2b89ff7d83f9769d33b6a795c452a8bf09cf2e61ccd8282b40cefd6f pkgname = virtscreen diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index bbfc14f..3d35137 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Bumsik Kim _pkgname_camelcase=VirtScreen pkgname=virtscreen -pkgver=0.2.1 +pkgver=0.2.4 pkgrel=1 pkgdesc="Make your iPad/tablet/computer as a secondary monitor on Linux" arch=("i686" "x86_64") @@ -22,7 +22,7 @@ install= changelog= source=(https://github.com/kbumsik/$_pkgname_camelcase/archive/$pkgver.tar.gz) noextract=() -sha256sums=('9af568a73ff3523144bfbeacb7131d4fff9fc4fb8ee3fddb90d78f54b774acb7') +sha256sums=('0a62fd5e2b89ff7d83f9769d33b6a795c452a8bf09cf2e61ccd8282b40cefd6f') package() { cd $_pkgname_camelcase-$pkgver From d357296306f2761eb5a3561d63e2a763e8b16ced Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Tue, 26 Jun 2018 03:33:46 -0400 Subject: [PATCH 03/33] Makefile: cleanup build system --- .travis.yml | 16 +++---- Makefile | 95 ++++++++++++++------------------------ package/archlinux/.SRCINFO | 22 --------- package/build_all.sh | 13 +++--- package/pypi/.gitignore | 1 + 5 files changed, 50 insertions(+), 97 deletions(-) delete mode 100644 package/archlinux/.SRCINFO create mode 100644 package/pypi/.gitignore diff --git a/.travis.yml b/.travis.yml index bed3cfc..04944e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,22 +4,22 @@ 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: | + package/build_all.sh $TRAVIS_TAG 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/debian/virtscreen_$TRAVIS_TAG-1_all.deb - package/appimage/VirtScreen-x86_64.AppImage skip_cleanup: true on: diff --git a/Makefile b/Makefile index d82dcdf..64cf5e9 100644 --- a/Makefile +++ b/Makefile @@ -6,89 +6,59 @@ 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/* - .ONESHELL: -# Docker -docker-build: - docker build -f Dockerfile -t $(DOCKER_NAME) . +# Docker tools +.PHONY: docker docker-build docker: $(DOCKER_RUN_TTY) /bin/bash - -docker-rm: - docker image rm -f $(DOCKER_NAME) + +docker-build: + docker build -f Dockerfile -t $(DOCKER_NAME) . -docker-pull: - docker pull $(DOCKER_NAME) +# Python wheel package for PyPI +.PHONY: wheel-clean -docker-push: - docker login - docker push $(DOCKER_NAME) +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 + +package/appimage/%.AppImage: $(DOCKER_RUN) package/appimage/build.sh $(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 package/appimage/VirtScreen-x86_64.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 +.PHONY: deb-contents deb-clean -deb-build: deb-make - $(DOCKER_RUN_DEB) /app/copy_debian.sh - $(DOCKER_RUN_DEB) /app/debuild.sh +package/debian/%.deb: + $(DOCKER_RUN_DEB) /app/debmake.sh virtualenv + $(DOCKER_RUN_DEB) /app/copy_debian.sh virtualenv + $(DOCKER_RUN_DEB) /app/debuild.sh virtualenv + $(DOCKER_RUN_DEB) chown -R $(shell id -u):$(shell id -u) /app/build + cp package/debian/build/virtscreen*.deb 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-clean: - $(DOCKER_RUN_DEB) rm -rf /app/build + rm -rf package/debian/build package/debian/*.deb # 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 +70,13 @@ 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 +# Clean packages +clean: appimage-clean arch-clean deb-clean wheel-clean diff --git a/package/archlinux/.SRCINFO b/package/archlinux/.SRCINFO deleted file mode 100644 index 9388d69..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.4 - 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.4.tar.gz - sha256sums = 0a62fd5e2b89ff7d83f9769d33b6a795c452a8bf09cf2e61ccd8282b40cefd6f - -pkgname = virtscreen - diff --git a/package/build_all.sh b/package/build_all.sh index c9ce382..897a517 100755 --- a/package/build_all.sh +++ b/package/build_all.sh @@ -29,11 +29,11 @@ override_version () { } build_pypi () { - make -C $ROOT python-wheel + make -C $ROOT package/pypi/virtscreen-$VERSION-py2.py3-none-any.whl } build_appimage () { - make -C $ROOT appimage-build + make -C $ROOT package/appimage/VirtScreen-x86_64.AppImage } build_arch () { @@ -43,16 +43,15 @@ build_arch () { perl -pi -e "s/sha256sums=\('.*'\)/sha256sums=('$SHA256')/" \ $ROOT/package/archlinux/PKGBUILD rm $VERSION.tar.gz - make -C $ROOT arch-upload + # make -C $ROOT arch-upload } build_debian () { - make -C $ROOT deb-env-build - make -C $ROOT deb-chown + make -C $ROOT package/debian/virtscreen_$VERSION-1_all.deb } override_version -# build_pypi +build_pypi build_appimage -# build_arch +build_arch build_debian diff --git a/package/pypi/.gitignore b/package/pypi/.gitignore new file mode 100644 index 0000000..0f66ccb --- /dev/null +++ b/package/pypi/.gitignore @@ -0,0 +1 @@ +virtscreen*.whl From 19d8e1a18040930a824acfa92665d7025462ab68 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Tue, 26 Jun 2018 04:32:03 -0400 Subject: [PATCH 04/33] AUR: deleted md5sum --- package/archlinux/PKGBUILD | 6 +++--- package/build_all.sh | 11 ----------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index 3d35137..78aedd4 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -20,12 +20,12 @@ 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=('0a62fd5e2b89ff7d83f9769d33b6a795c452a8bf09cf2e61ccd8282b40cefd6f') +md5sums=('SKIP') package() { - cd $_pkgname_camelcase-$pkgver + cd $srcdir/src PIP_CONFIG_FILE=/dev/null /usr/bin/pip install --isolated --root="$pkgdir" --ignore-installed --no-deps . # These are already installed by setup.py # install -Dm644 "data/$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop" diff --git a/package/build_all.sh b/package/build_all.sh index 897a517..69004a2 100755 --- a/package/build_all.sh +++ b/package/build_all.sh @@ -36,16 +36,6 @@ build_appimage () { make -C $ROOT package/appimage/VirtScreen-x86_64.AppImage } -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 package/debian/virtscreen_$VERSION-1_all.deb } @@ -53,5 +43,4 @@ build_debian () { override_version build_pypi build_appimage -build_arch build_debian From a97e532b9377edaccde27d130de116f847911555 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Tue, 26 Jun 2018 04:35:32 -0400 Subject: [PATCH 05/33] package/build_all.sh: Deleted and updated Makefile and Travis accordingly --- .travis.yml | 11 ++++++++--- Makefile | 20 +++++++++++++++++++ package/build_all.sh | 46 -------------------------------------------- 3 files changed, 28 insertions(+), 49 deletions(-) delete mode 100755 package/build_all.sh diff --git a/.travis.yml b/.travis.yml index 04944e3..2a0560d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,15 +12,20 @@ script: | echo No test scripts implemented yet. Travis is used only for deploy yet. before_deploy: | - package/build_all.sh $TRAVIS_TAG + if [ -n "$TRAVIS_TAG" ]; then + VERSION=$TRAVIS_TAG make override_version + fi + make package/pypi/*.whl + make package/appimage/*.AppImage + make package/debian/*.deb deploy: - provider: releases api_key: secure: zFbsCIKcsvWU/Yc+9k294Qj8QY48VlkV8DSScP5gz6dQegeUSaSHI/YafherkFQ0B03bIY8yc7roMtDo7HAkEnPptjFhdUiOFI11+xDVb3s7Y8Ek2nV3znQzdtR4CR/94l3in6R3DH+eNA6+6Je/NIWLdVcvRX07RBSfBVdPmnsAyAD9KNTsl8Q4c20HgtLNxfWv2s5eCyD+heCTLYrErEZKZ5vYeeANmWomHvT2ED/4QerpBP8wkh59QXD1S79CF7oyq6X173ZJUQVxdBP+OSXt/mDBAoqf+TV6okawRZn48JluvCWAJ7BceX7t9emd1rVI/s8t3wCP+eMcmNn5g/6UJaCPnTJ5YplTuUWRc63UFSkE0AY8WYcRlrz+/OiXYgQ8LMXfN23aWgarHCbS2vHR3Afu9gpLCoKucr36hKhs3zfjJzVLFFW16mnbaTFcBzfDDRpkvOANB1aZwGVRFpTIWIMjkn0+lxWTC/moIJvQlfRPsC4dN5cDAilRQlguHzayebtGE8X0PuIe9A8bkET3V/y+KPnQiSJ7J+5PNoDSdqRAE4IKvVOLEyHtlqBVkvIHKnugUnWPIZ21gm5RemMEj9/YGa8Efwz7PIKtJJ3kFMGDYKVlIKyB+rg/TFWNdo6jjevnWM6y4SfVI3kFyjA+mp31o6nshrQy0zVQpd8= file: - - package/debian/virtscreen_$TRAVIS_TAG-1_all.deb - - package/appimage/VirtScreen-x86_64.AppImage + - package/debian/*.deb + - package/appimage/*.AppImage skip_cleanup: true on: tags: true diff --git a/Makefile b/Makefile index 64cf5e9..29c78dd 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ # See https://packaging.python.org/tutorials/distributing-packages/#packaging-your-project # for python packaging reference. +VERSION ?= 0.2.4 DOCKER_NAME=kbumsik/virtscreen DOCKER_RUN=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME) @@ -78,5 +79,24 @@ arch-clean: cd package/archlinux -rm -rf pkg src *.tar* .SRCINFO +# 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/_common.sh + # Clean packages clean: appimage-clean arch-clean deb-clean wheel-clean diff --git a/package/build_all.sh b/package/build_all.sh deleted file mode 100755 index 69004a2..0000000 --- a/package/build_all.sh +++ /dev/null @@ -1,46 +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 package/pypi/virtscreen-$VERSION-py2.py3-none-any.whl -} - -build_appimage () { - make -C $ROOT package/appimage/VirtScreen-x86_64.AppImage -} - -build_debian () { - make -C $ROOT package/debian/virtscreen_$VERSION-1_all.deb -} - -override_version -build_pypi -build_appimage -build_debian From 7dcf8a8bde72689da1f0eff93b2597fcdf230559 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Tue, 26 Jun 2018 05:02:30 -0400 Subject: [PATCH 06/33] Debian: cleanup build system --- Makefile | 11 +++++----- .../debian/{Makefile.virtualenv => Makefile} | 0 package/debian/contents.sh | 6 ------ package/debian/control | 6 +++--- package/debian/control.virtualenv | 16 -------------- package/debian/copy_debian.sh | 9 ++------ package/debian/debmake.sh | 21 +++++++------------ package/debian/debuild.sh | 12 ++++------- package/debian/rules | 12 ----------- 9 files changed, 22 insertions(+), 71 deletions(-) rename package/debian/{Makefile.virtualenv => Makefile} (100%) delete mode 100755 package/debian/contents.sh delete mode 100644 package/debian/control.virtualenv delete mode 100755 package/debian/rules diff --git a/Makefile b/Makefile index 29c78dd..628b9a6 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,6 @@ VERSION ?= 0.2.4 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) .ONESHELL: @@ -43,14 +42,14 @@ appimage-clean: .PHONY: deb-contents deb-clean package/debian/%.deb: - $(DOCKER_RUN_DEB) /app/debmake.sh virtualenv - $(DOCKER_RUN_DEB) /app/copy_debian.sh virtualenv - $(DOCKER_RUN_DEB) /app/debuild.sh virtualenv - $(DOCKER_RUN_DEB) chown -R $(shell id -u):$(shell id -u) /app/build + $(DOCKER_RUN) package/debian/debmake.sh + $(DOCKER_RUN) package/debian/copy_debian.sh + $(DOCKER_RUN) package/debian/debuild.sh + $(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/debian/build cp package/debian/build/virtscreen*.deb package/debian deb-contents: - $(DOCKER_RUN_DEB) /app/contents.sh + $(DOCKER_RUN) dpkg -c package/debian/*.deb deb-clean: rm -rf package/debian/build package/debian/*.deb diff --git a/package/debian/Makefile.virtualenv b/package/debian/Makefile similarity index 100% rename from package/debian/Makefile.virtualenv rename to package/debian/Makefile 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 index 91d1d33..75d8c93 100755 --- a/package/debian/copy_debian.sh +++ b/package/debian/copy_debian.sh @@ -1,11 +1,6 @@ #!/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 +cp -f $DIR/control $DIR/build/virtscreen-$PKGVER/debian/ +cp -f $DIR/README.Debian $DIR/build/virtscreen-$PKGVER/debian/ diff --git a/package/debian/debmake.sh b/package/debian/debmake.sh index 55d46a8..e4472ea 100755 --- a/package/debian/debmake.sh +++ b/package/debian/debmake.sh @@ -1,20 +1,15 @@ #!/bin/bash +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source $DIR/_common.sh -source _common.sh - -mkdir build -cd build -# Download +mkdir -p package/debian/build +cd package/debian/build +# Download, rename, and copy files 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 +cp -f ../../Makefile Makefile +# call debmake +debmake -b':sh' diff --git a/package/debian/debuild.sh b/package/debian/debuild.sh index 27e359e..68afab2 100755 --- a/package/debian/debuild.sh +++ b/package/debian/debuild.sh @@ -1,11 +1,7 @@ #!/bin/bash +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source $DIR/_common.sh -source _common.sh - -cd build +cd package/debian/build cd virtscreen-$PKGVER -if [ $1 = "virtualenv" ]; then - dpkg-buildpackage -b -else - debuild -fi +dpkg-buildpackage -b 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 From 2bf8dedf9d56080a9257a50982ee1e2b97a1a7c7 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Tue, 26 Jun 2018 13:12:34 -0400 Subject: [PATCH 07/33] Debian: upgraded to AppImage based package --- Makefile | 18 +++++++------- launch_env.sh | 13 ---------- package/appimage/AppRun | 1 - package/debian/.gitignore | 3 +++ package/debian/Makefile | 16 ++---------- package/debian/_common.sh | 7 ------ package/debian/build.sh | 46 +++++++++++++++++++++++++++++++++++ package/debian/copy_debian.sh | 6 ----- package/debian/debmake.sh | 15 ------------ package/debian/debuild.sh | 7 ------ 10 files changed, 60 insertions(+), 72 deletions(-) delete mode 100755 launch_env.sh create mode 100644 package/debian/.gitignore delete mode 100644 package/debian/_common.sh create mode 100755 package/debian/build.sh delete mode 100755 package/debian/copy_debian.sh delete mode 100755 package/debian/debmake.sh delete mode 100755 package/debian/debuild.sh diff --git a/Makefile b/Makefile index 628b9a6..7e6a975 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ wheel-clean: # For AppImage packaging, https://github.com/AppImage/AppImageKit/wiki/Creating-AppImages .PHONY: appimage-clean +.SECONDARY: package/appimage/VirtScreen-x86_64.AppImage package/appimage/%.AppImage: $(DOCKER_RUN) package/appimage/build.sh @@ -38,21 +39,20 @@ package/appimage/%.AppImage: appimage-clean: -rm -rf package/appimage/virtscreen.AppDir package/appimage/VirtScreen-x86_64.AppImage -# For Debian packaging, https://www.debian.org/doc/manuals/debmake-doc/ch08.en.html#setup-py +# 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 -package/debian/%.deb: - $(DOCKER_RUN) package/debian/debmake.sh - $(DOCKER_RUN) package/debian/copy_debian.sh - $(DOCKER_RUN) package/debian/debuild.sh - $(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/debian/build - cp package/debian/build/virtscreen*.deb package/debian +package/debian/%.deb: package/appimage/VirtScreen-x86_64.AppImage + $(DOCKER_RUN) package/debian/build.sh + $(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/debian deb-contents: $(DOCKER_RUN) dpkg -c package/debian/*.deb deb-clean: - rm -rf package/debian/build package/debian/*.deb + 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 @@ -95,7 +95,7 @@ override-version: package/archlinux/PKGBUILD # Debian perl -pi -e "s/PKGVER=\d+\.\d+\.\d+/PKGVER=$(VERSION)/" \ - package/debian/_common.sh + package/debian/build.sh # Clean packages clean: appimage-clean arch-clean deb-clean wheel-clean 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/AppRun b/package/appimage/AppRun index 2512c2b..7ef9a30 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 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 index 02e1778..22b3f8b 100644 --- a/package/debian/Makefile +++ b/package/debian/Makefile @@ -5,21 +5,9 @@ all: 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 \ + mkdir -p $(DESTDIR)$(prefix)/bin + install -m 755 VirtScreen-x86_64.AppImage \ $(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 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..7d8de0e --- /dev/null +++ b/package/debian/build.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +PKGVER=0.2.4 +# 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 +wget -q https://github.com/kbumsik/VirtScreen/archive/$PKGVER.tar.gz +tar -xzmf $PKGVER.tar.gz +mv VirtScreen-$PKGVER virtscreen-$PKGVER +mv $PKGVER.tar.gz virtscreen-$PKGVER.tar.gz +cp $ROOT/package/debian/Makefile \ + $ROOT/package/debian/virtscreen-$PKGVER/Makefile +cd $ROOT/package/debian/virtscreen-$PKGVER +debmake -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-x86_64.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/copy_debian.sh b/package/debian/copy_debian.sh deleted file mode 100755 index 75d8c93..0000000 --- a/package/debian/copy_debian.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source $DIR/_common.sh - -cp -f $DIR/control $DIR/build/virtscreen-$PKGVER/debian/ -cp -f $DIR/README.Debian $DIR/build/virtscreen-$PKGVER/debian/ diff --git a/package/debian/debmake.sh b/package/debian/debmake.sh deleted file mode 100755 index e4472ea..0000000 --- a/package/debian/debmake.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source $DIR/_common.sh - -mkdir -p package/debian/build -cd package/debian/build -# Download, rename, and copy files -wget -q https://github.com/kbumsik/VirtScreen/archive/$PKGVER.tar.gz -tar -xzmf $PKGVER.tar.gz -mv VirtScreen-$PKGVER virtscreen-$PKGVER -mv $PKGVER.tar.gz virtscreen-$PKGVER.tar.gz -cd virtscreen-$PKGVER -cp -f ../../Makefile Makefile -# call debmake -debmake -b':sh' diff --git a/package/debian/debuild.sh b/package/debian/debuild.sh deleted file mode 100755 index 68afab2..0000000 --- a/package/debian/debuild.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source $DIR/_common.sh - -cd package/debian/build -cd virtscreen-$PKGVER -dpkg-buildpackage -b From f5884ae9a1673775de55b3f48c3c11798a17852d Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Tue, 26 Jun 2018 14:35:53 -0400 Subject: [PATCH 08/33] AppImage: added command line arguments --- package/appimage/AppRun | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/appimage/AppRun b/package/appimage/AppRun index 7ef9a30..f8e633e 100755 --- a/package/appimage/AppRun +++ b/package/appimage/AppRun @@ -11,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 $@ From 8393dab1a5619187bba44bcb428a9f10843386a1 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Tue, 26 Jun 2018 16:27:37 -0400 Subject: [PATCH 09/33] README: added FUreatures and CLI usage --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f5c89a..8e470fb 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,61 @@ VirtScreen is an easy-to-use Linux GUI app that creates a virtual secondary scre 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. +## 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 +### GUI (default) + Upon installation (see Installing section to install), there will be a desktop entry called `VirtScreen` ![desktop entry](https://raw.githubusercontent.com/kbumsik/VirtScreen/master/data/desktop_entry.png) -Or you can run it using a command line: +### CLI-only option + +You can run VirtScreen with `virtscreen` (or `./VirtScreen-x86_64.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 From c09fffe6e8378305687949efc7d9508295814bcf Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Thu, 28 Jun 2018 02:19:32 -0400 Subject: [PATCH 10/33] Backend: switched to asyncio from Twisted --- package/archlinux/PKGBUILD | 2 +- setup.py | 3 +- virtscreen/virtscreen.py | 172 ++++++++++++++++++------------------- 3 files changed, 88 insertions(+), 89 deletions(-) diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index 78aedd4..b356f39 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -8,7 +8,7 @@ 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') +depends=('xorg-xrandr' 'x11vnc' 'python-pyqt5' 'python-quamash-git' 'python-netifaces') makedepends=('python-pip') optdepends=( 'arandr: for display settings option' diff --git a/setup.py b/setup.py index f1355a7..c2b7f0c 100644 --- a/setup.py +++ b/setup.py @@ -145,8 +145,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 diff --git a/virtscreen/virtscreen.py b/virtscreen/virtscreen.py index 8d49b77..5262587 100755 --- a/virtscreen/virtscreen.py +++ b/virtscreen/virtscreen.py @@ -11,9 +11,11 @@ import time import json import shutil import argparse +import shlex from pathlib import Path from enum import Enum from typing import List, Dict, Callable +import asyncio # Import OpenGL library for Nvidia driver # https://github.com/Ultimaker/Cura/pull/131#issuecomment-176088664 @@ -26,8 +28,7 @@ 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 quamash import QEventLoop from netifaces import interfaces, ifaddresses, AF_INET # ------------------------------------------------------------------------------- @@ -75,92 +76,88 @@ class SubprocessWrapper: pass def check_output(self, arg) -> None: - return subprocess.check_output(arg.split(), stderr=subprocess.STDOUT).decode('utf-8') + 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(arg.split(), input=input, stdout=subprocess.PIPE, + return subprocess.run(shlex.split(arg), input=input, stdout=subprocess.PIPE, check=check, stderr=subprocess.STDOUT).stdout.decode('utf-8') # ------------------------------------------------------------------------------- -# Twisted class +# Asynchronous subprocess wrapper 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 +class AsyncSubprocess(): + class Protocol(asyncio.SubprocessProtocol): + def __init__(self, outer): + self.outer = outer + self.transport: asyncio.SubprocessTransport + + def connection_made(self, transport): + print("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 + print("stdin is closed. (we probably did it)") + elif fd == 1: # stdout + print("The child closed their stdout.") + elif fd == 2: # stderr + print("The child closed their stderr.") + + def connection_lost(self, exc): + print("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: + print("Unknown exit") + self.outer.ended(1) + return + print("processEnded, status", return_code) + self.outer.ended(return_code) + + 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 - # 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 + self.transport: asyncio.SubprocessTransport + self.protocol: self.Protocol + + async def _run(self, arg: str, loop: asyncio.AbstractEventLoop): + self.transport, self.protocol = await loop.subprocess_exec( + lambda: self.Protocol(self), *shlex.split(arg), env=os.environ) def run(self, arg: str): - """Spawn a process + """Spawn a process. Arguments: arg {str} -- arguments in string """ + loop = asyncio.get_event_loop() + loop.create_task(self._run(arg, loop)) - 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) + def close(self): + """Kill a spawned process.""" + self.transport.send_signal(signal.SIGINT) # ------------------------------------------------------------------------------- @@ -428,7 +425,7 @@ class Backend(QObject): self._vncUsePassword: bool = False self._vncState: self.VNCState = self.VNCState.OFF # Primary screen and mouse posistion - self.vncServer: ProcessProtocol + self.vncServer: AsyncSubprocess # Check config file # and initialize if needed need_init = False @@ -589,11 +586,11 @@ class Backend(QObject): patter_disconnected = re.compile(r"^.*client_count: 0*$", re.M) # define callbacks - def _onConnected(): + def _connected(): print("VNC started.") self.vncState = self.VNCState.WAITING - def _onReceived(data): + def _received(data): data = data.decode("utf-8") if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data): print("VNC connected.") @@ -602,7 +599,7 @@ class Backend(QObject): print("VNC disconnected.") self.vncState = self.VNCState.WAITING - def _onEnded(exitCode): + def _ended(exitCode): if exitCode is not 0: self.vncState = self.VNCState.ERROR self.onError.emit('X11VNC: Error occurred.\n' @@ -626,7 +623,7 @@ class Backend(QObject): 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) + self.vncServer = AsyncSubprocess(_connected, _received, _received, _ended, logfile) try: virt = self.xrandr.get_virtual_screen() except RuntimeError as e: @@ -643,13 +640,13 @@ class Backend(QObject): @pyqtSlot(str) def openDisplaySetting(self, app: str = "arandr"): # define callbacks - def _onConnected(): + def _connected(): print("External Display Setting opened.") - def _onReceived(data): + def _received(data): pass - def _onEnded(exitCode): + def _ended(exitCode): print("External Display Setting closed.") self.onDisplaySettingClosed.emit() if exitCode is not 0: @@ -660,10 +657,10 @@ class Backend(QObject): self.onError.emit('Wrong display settings program') return program_list = [data[app]['args'], "arandr"] - program = ProcessProtocol(_onConnected, _onReceived, _onReceived, _onEnded, None) + program = AsyncSubprocess(_connected, _received, _received, _ended, None) running_program = '' for arg in program_list: - if not shutil.which(arg.split()[0]): + if not shutil.which(shlex.split(arg)[0]): continue running_program = arg program.run(arg) @@ -679,10 +676,10 @@ class Backend(QObject): def stopVNC(self, force=False): if force: # Usually called from atexit(). - self.vncServer.kill() + 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.kill() + self.vncServer.close() else: self.onError.emit("stopVNC called while it is not running") @@ -809,6 +806,8 @@ def check_env(msg: Callable[[str], None]) -> None: def main_gui(): 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 @@ -842,9 +841,11 @@ def main_gui(): dialog("Failed to load QML") sys.exit(1) sys.exit(app.exec_()) - reactor.run() + with loop: + loop.run_forever() def main_cli(args: argparse.Namespace): + loop = asyncio.get_event_loop() for key, value in vars(args).items(): print(key, ": ", value) # Check the environment @@ -886,9 +887,8 @@ def main_cli(args: argparse.Namespace): 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() + loop.run_forever() if __name__ == '__main__': main() From 2ea15b8943673a507f4025005ce5554db069b9af Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Thu, 28 Jun 2018 08:14:53 -0400 Subject: [PATCH 11/33] README: added asyncio --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e470fb..6130561 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 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 From 96c6066a9123dfbe0b4f9ea6ff69c143b592c022 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Thu, 28 Jun 2018 10:12:08 -0400 Subject: [PATCH 12/33] split single virtscreen.py into submodules --- setup.py | 4 +- virtscreen/__init__.py | 1 - virtscreen/display.py | 108 +++++ virtscreen/main.py | 189 +++++++++ virtscreen/path.py | 38 ++ virtscreen/process.py | 99 +++++ virtscreen/qt_backend.py | 355 ++++++++++++++++ virtscreen/virtscreen.py | 894 --------------------------------------- virtscreen/xrandr.py | 138 ++++++ 9 files changed, 929 insertions(+), 897 deletions(-) create mode 100644 virtscreen/display.py create mode 100755 virtscreen/main.py create mode 100644 virtscreen/path.py create mode 100644 virtscreen/process.py create mode 100644 virtscreen/qt_backend.py delete mode 100755 virtscreen/virtscreen.py create mode 100644 virtscreen/xrandr.py diff --git a/setup.py b/setup.py index c2b7f0c..a6b2abb 100644 --- a/setup.py +++ b/setup.py @@ -136,7 +136,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 @@ -195,7 +195,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/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/main.py b/virtscreen/main.py new file mode 100755 index 0000000..9b273ae --- /dev/null +++ b/virtscreen/main.py @@ -0,0 +1,189 @@ +#!/usr/bin/python3 + +# Python standard packages +import sys +import os +import signal +import json +import shutil +import argparse +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 + + +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) + 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(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_()) + with loop: + loop.run_forever() + +def main_cli(args: argparse.Namespace): + loop = asyncio.get_event_loop() + 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) + backend.startVNC(config['vnc']['port']) + loop.run_forever() + +if __name__ == '__main__': + main() diff --git a/virtscreen/path.py b/virtscreen/path.py new file mode 100644 index 0000000..52ccc8c --- /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 + +# 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" diff --git a/virtscreen/process.py b/virtscreen/process.py new file mode 100644 index 0000000..b3b197b --- /dev/null +++ b/virtscreen/process.py @@ -0,0 +1,99 @@ +"""Subprocess wrapper""" + +import subprocess +import asyncio +import signal +import shlex +import os + + +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): + print("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 + print("stdin is closed. (we probably did it)") + elif fd == 1: # stdout + print("The child closed their stdout.") + elif fd == 2: # stderr + print("The child closed their stderr.") + + def connection_lost(self, exc): + print("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: + print("Unknown exit") + self.outer.ended(1) + return + print("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..1c7e465 --- /dev/null +++ b/virtscreen/qt_backend.py @@ -0,0 +1,355 @@ +"""GUI backend""" + +import json +import re +import subprocess +import os +import shutil +import atexit +import time + +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): + 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 + # 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 _connected(): + print("VNC started.") + 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): + 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 _ended(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 = AsyncSubprocess(_connected, _received, _received, _ended, 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 _connected(): + print("External Display Setting opened.") + + def _received(data): + pass + + def _ended(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 = 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.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.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.onError.emit("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 5262587..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 -import shlex -from pathlib import Path -from enum import Enum -from typing import List, Dict, 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) - -# 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 -from quamash import QEventLoop -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(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') - - -# ------------------------------------------------------------------------------- -# Asynchronous subprocess wrapper class -# ------------------------------------------------------------------------------- -class AsyncSubprocess(): - class Protocol(asyncio.SubprocessProtocol): - def __init__(self, outer): - self.outer = outer - self.transport: asyncio.SubprocessTransport - - def connection_made(self, transport): - print("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 - print("stdin is closed. (we probably did it)") - elif fd == 1: # stdout - print("The child closed their stdout.") - elif fd == 2: # stderr - print("The child closed their stderr.") - - def connection_lost(self, exc): - print("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: - print("Unknown exit") - self.outer.ended(1) - return - print("processEnded, status", return_code) - self.outer.ended(return_code) - - 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: self.Protocol - - async def _run(self, arg: str, loop: asyncio.AbstractEventLoop): - self.transport, self.protocol = await loop.subprocess_exec( - lambda: self.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) - - -# ------------------------------------------------------------------------------- -# 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: AsyncSubprocess - # 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 _connected(): - print("VNC started.") - 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): - 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 _ended(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 = AsyncSubprocess(_connected, _received, _received, _ended, 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 _connected(): - print("External Display Setting opened.") - - def _received(data): - pass - - def _ended(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 = AsyncSubprocess(_connected, _received, _received, _ended, None) - running_program = '' - for arg in program_list: - if not shutil.which(shlex.split(arg)[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.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.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) - 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(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_()) - with loop: - loop.run_forever() - -def main_cli(args: argparse.Namespace): - loop = asyncio.get_event_loop() - 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) - backend.startVNC(config['vnc']['port']) - loop.run_forever() - -if __name__ == '__main__': - main() diff --git a/virtscreen/xrandr.py b/virtscreen/xrandr.py new file mode 100644 index 0000000..d794454 --- /dev/null +++ b/virtscreen/xrandr.py @@ -0,0 +1,138 @@ +"""XRandr parser""" + +import re +import atexit +import subprocess +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)) + 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) + 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() From 28dabf2271cc9900ac2f533e849a73c174e63bb6 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Thu, 28 Jun 2018 10:26:41 -0400 Subject: [PATCH 13/33] Changed main.py to __main__.py --- Makefile | 4 ++++ setup.py | 2 +- virtscreen/{main.py => __main__.py} | 0 3 files changed, 5 insertions(+), 1 deletion(-) rename virtscreen/{main.py => __main__.py} (100%) diff --git a/Makefile b/Makefile index 7e6a975..c690812 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,10 @@ DOCKER_RUN_TTY=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME .ONESHELL: +# Run script +run: + python3 -m virtscreen + # Docker tools .PHONY: docker docker-build diff --git a/setup.py b/setup.py index a6b2abb..9db64e6 100644 --- a/setup.py +++ b/setup.py @@ -195,7 +195,7 @@ setup( # executes the function `main` from this package when invoked: entry_points={ # Optional 'console_scripts': [ - 'virtscreen = virtscreen.main:main', + 'virtscreen = virtscreen.__main__:main', ], }, diff --git a/virtscreen/main.py b/virtscreen/__main__.py similarity index 100% rename from virtscreen/main.py rename to virtscreen/__main__.py From 706a8d9ddfb44e9fbdc2dc61c44f4be8a71b3528 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Fri, 29 Jun 2018 18:03:14 -0400 Subject: [PATCH 14/33] Icon: added a new SVG icon --- data/icon.xcf | Bin 7357 -> 0 bytes data/icon_full.svg | 155 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) delete mode 100644 data/icon.xcf create mode 100644 data/icon_full.svg diff --git a/data/icon.xcf b/data/icon.xcf deleted file mode 100644 index ba7b0fe2017e8b150cf3b7eeaa80d5392494720b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7357 zcmeHMU1(!P6h8Mhe{B<6x3OE=u0$8zE;hAwn{C(Ks=ZfztS?4f^d)JN7)_JXrqF%R zD~diS3JR_-f*=Uef}k5v#NE{wQBd(gK~#M6Ma0JyblLGcb7zv9o2(S6b+?-X-@V_= zoS8Y3IcH|>q)=R5No*|U6Zv8(N02vw_nAPh2Mhy`eLxSeVB&F-a|VEjb>^bU0FMI! z;00h%an{>UWX|Eu!s$r5T+S@#mShH3WEls#09FX`+xmEOYyKNB)V3o;^N(+CF}KbU zSszMnc(^y;v@+?m#iQVM8r++2S`VU~UzN^V;D4mkNSW3TM7HU2JA!s@RDJ!nxn(`8 zbUp|FK=GfTAC}y(E})$YuAOYN^|P}1^>o@ksB|8Neo*#nS_azx%5CSRr7Y{% zqp;ymH7+kdfc)>=cI*Z>oA@xs_@dHjaMSkkKD2X2=`^@$H~0-~Ds1U5z+02Wtz;I9XNf+x8iuwsE6}IJc`M45I7k)#XuLh@$g~YqOyn_z}ah zfd`t)g6Kau0=$A@pwG|&n2mH4t620PaD??81Yrwe7pbdujDodU3Q5dRxaNn$S^`=E zS`yF@er!_<<90#;iq}FkSPRhpT9?F7%_Kjho`9Z!o`4=OB%tTRw)NWl0{*e?<+Pq- zp6}zB_q)kIhkH4#eV6?hqZilu=p96&KjQXsTCX&`>cnd|x2z)mRb?-w_3FthXI5Iv0SGyoW*#0eqlmc^-HZkVlGAW~EwL;p|;GN(dqTLSI8byQ9SY zY=|o{KB};2b8(c2-AXik2`409S?nPkLyzM72PdZi=nQ@hJ=7aM4SWXt&J8(%Z|OF% zkH$qG-7aw*O$tVgrhMoYDY`OnsAKo8&c)UIdw(MR_zn@Jl<1Z?Dk9X1E5*0jjtKm_ zWB2Z@HCOTPZD0%ZAhu&Gbo1)3cLlvm^wWeGz|P?quTrwUZ|FTEN*r%4dg+K@^w6|~ z8-s2vwu?iZ7ou5wFwTk~9g}Y_57P|xrk(PS&Pd&zzcZ(tfQ^#W#WT=PQf8R_0IF-fU*^iNb1Sy}ZOlA%JhD|IeH7X*R@_kuMa{Cd0OoUvL@! zHt=3^*(m->8NCHa0rvs;8)!5KtOL&hud#m1Ex7+)7$4MpJ64Ck+@JR9GT)&ahQ9$= CH+DJz diff --git a/data/icon_full.svg b/data/icon_full.svg new file mode 100644 index 0000000..0a27d39 --- /dev/null +++ b/data/icon_full.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + From 3fe258a96b8d80b352da6fe2b7a3f29415a46754 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Fri, 29 Jun 2018 18:10:12 -0400 Subject: [PATCH 15/33] README: added the new SVG file --- README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6130561..0a13889 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,20 @@ -# VirtScreen +

+ +
+ VirtScreen +

-> Make your iPad/tablet/computer as a secondary monitor on Linux. +

+ Make your iPad/tablet/computer as a secondary monitor on Linux. +

-![gif example](https://raw.githubusercontent.com/kbumsik/VirtScreen/master/data/gif_example.gif) + + +## Description VirtScreen is an easy-to-use Linux GUI app that creates a virtual secondary screen and shares it through VNC. From d2ebf4bb0d0152e7fe57d9c043733121cc783073 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Fri, 29 Jun 2018 18:18:49 -0400 Subject: [PATCH 16/33] Icon: deleted unnecessary header --- data/icon_full.svg | 3 --- 1 file changed, 3 deletions(-) diff --git a/data/icon_full.svg b/data/icon_full.svg index 0a27d39..2a4d41e 100644 --- a/data/icon_full.svg +++ b/data/icon_full.svg @@ -1,6 +1,3 @@ - - - Date: Fri, 29 Jun 2018 19:22:09 -0400 Subject: [PATCH 17/33] Icon: Applied the new icon to the app --- README.md | 2 - data/icon_full.svg | 119 +++++++++------ virtscreen/assets/main.qml | 6 +- virtscreen/icon/full_256x256.png | Bin 0 -> 21580 bytes virtscreen/icon/icon.png | Bin 28835 -> 0 bytes virtscreen/icon/icon_tablet_off.png | Bin 28835 -> 0 bytes virtscreen/icon/icon_tablet_on.png | Bin 111529 -> 0 bytes virtscreen/icon/systray_no_tablet.svg | 154 +++++++++++++++++++ virtscreen/icon/systray_tablet_off.svg | 180 +++++++++++++++++++++++ virtscreen/icon/systray_tablet_on.svg | 196 +++++++++++++++++++++++++ virtscreen/path.py | 2 +- 11 files changed, 606 insertions(+), 53 deletions(-) create mode 100644 virtscreen/icon/full_256x256.png delete mode 100644 virtscreen/icon/icon.png delete mode 100644 virtscreen/icon/icon_tablet_off.png delete mode 100644 virtscreen/icon/icon_tablet_on.png create mode 100644 virtscreen/icon/systray_no_tablet.svg create mode 100644 virtscreen/icon/systray_tablet_off.svg create mode 100644 virtscreen/icon/systray_tablet_on.svg diff --git a/README.md b/README.md index 0a13889..2897756 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,6 @@ VirtScreen is based on [PyQt5](https://www.riverbankcomputing.com/software/pyqt/ Upon installation (see Installing section to install), there will be a desktop entry called `VirtScreen` -![desktop entry](https://raw.githubusercontent.com/kbumsik/VirtScreen/master/data/desktop_entry.png) - ### CLI-only option You can run VirtScreen with `virtscreen` (or `./VirtScreen-x86_64.AppImage` if you use the AppImage package) with additional arguments. diff --git a/data/icon_full.svg b/data/icon_full.svg index 2a4d41e..b0ce4e7 100644 --- a/data/icon_full.svg +++ b/data/icon_full.svg @@ -9,11 +9,11 @@ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="128.91515mm" height="122.79081mm" - viewBox="0 0 128.91514 122.79081" + viewBox="0 0 128.91515 122.79081" version="1.1" id="svg8" inkscape:version="0.92.2 2405546, 2018-03-11" - sodipodi:docname="icon.svg"> + sodipodi:docname="icon_full.svg"> + image/svg+xml - + @@ -110,43 +118,60 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" - transform="translate(-54.81748,-56.581023)"> - - - - - - - - - + transform="translate(-26.029924,-31.875429)"> + + + + + + + + + diff --git a/virtscreen/assets/main.qml b/virtscreen/assets/main.qml index c2c1f74..7e10c3b 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.svg" : + backend.virtScreenCreated ? "../icon/systray_tablet_off.svg" : + "../icon/systray_no_tablet.svg" visible: true property bool clicked: false diff --git a/virtscreen/icon/full_256x256.png b/virtscreen/icon/full_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..f206a27f62801bcb2ce928864c4e924db75a1764 GIT binary patch literal 21580 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 literal 0 HcmV?d00001 diff --git a/virtscreen/icon/icon.png b/virtscreen/icon/icon.png deleted file mode 100644 index ff65365d2943d8fc13c9f8270168d07c5cd9cd8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28835 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 DjF#r8 diff --git a/virtscreen/icon/systray_no_tablet.svg b/virtscreen/icon/systray_no_tablet.svg new file mode 100644 index 0000000..a59bfa9 --- /dev/null +++ b/virtscreen/icon/systray_no_tablet.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/virtscreen/icon/systray_tablet_off.svg b/virtscreen/icon/systray_tablet_off.svg new file mode 100644 index 0000000..8d2aba3 --- /dev/null +++ b/virtscreen/icon/systray_tablet_off.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/virtscreen/icon/systray_tablet_on.svg b/virtscreen/icon/systray_tablet_on.svg new file mode 100644 index 0000000..d4ba9b0 --- /dev/null +++ b/virtscreen/icon/systray_tablet_on.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/virtscreen/path.py b/virtscreen/path.py index 52ccc8c..de94aec 100644 --- a/virtscreen/path.py +++ b/virtscreen/path.py @@ -31,7 +31,7 @@ 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" +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" From 451ada820bbac4d7c8d71c0799c1b68f52d592cf Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Fri, 29 Jun 2018 20:33:35 -0400 Subject: [PATCH 18/33] README: updated How to Use --- README.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2897756..d3a7e86 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,10 @@ VirtScreen is based on [PyQt5](https://www.riverbankcomputing.com/software/pyqt/ ## How to use -### GUI (default) - -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 @@ -94,7 +95,7 @@ Download a `.deb` package from [releases page](https://github.com/kbumsik/VirtSc ```shell sudo apt-get update sudo apt-get install x11vnc -sudo dpkg -i virtscreen_0.2.4-1_all.deb +sudo dpkg -i virtscreen_0.2.4-1_all.deb rm virtscreen_0.2.4-1_all.deb ``` @@ -108,14 +109,7 @@ yaourt virtscreen ### Python `pip` -Although not recommended, 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 From 9b8c1a71a482a4053c9a1f818fb53c9f88e89a6d Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Fri, 29 Jun 2018 20:35:38 -0400 Subject: [PATCH 19/33] main.py: delted Twisted code, added Qt application name --- virtscreen/__main__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/virtscreen/__main__.py b/virtscreen/__main__.py index 9b273ae..9d71542 100755 --- a/virtscreen/__main__.py +++ b/virtscreen/__main__.py @@ -29,6 +29,7 @@ from .path import HOME_PATH, ICON_PATH, MAIN_QML_PATH, CONFIG_PATH 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' @@ -77,6 +78,7 @@ def main() -> None: sys.exit(1) def check_env(msg: Callable[[str], None]) -> None: + """Check enveironments before start""" if os.environ['XDG_SESSION_TYPE'].lower() == 'wayland': msg("Currently Wayland is not supported") sys.exit(1) @@ -113,14 +115,9 @@ def main_gui(): 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.setApplicationName("VirtScreen") 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. From 2fd2119a7a20dc7b29957d33579af6001f577e91 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Fri, 29 Jun 2018 20:37:23 -0400 Subject: [PATCH 20/33] Icon: fix icon wasn't a perfect square --- data/icon_full.svg | 149 ++++++++++--------- data/systray_icon.svg | 180 +++++++++++++++++++++++ virtscreen/assets/main.qml | 6 +- virtscreen/icon/full_256x256.png | Bin 21580 -> 21418 bytes virtscreen/icon/systray_no_tablet.png | Bin 0 -> 21304 bytes virtscreen/icon/systray_no_tablet.svg | 154 ------------------- virtscreen/icon/systray_tablet_off.png | Bin 0 -> 19813 bytes virtscreen/icon/systray_tablet_off.svg | 180 ----------------------- virtscreen/icon/systray_tablet_on.png | Bin 0 -> 20037 bytes virtscreen/icon/systray_tablet_on.svg | 196 ------------------------- 10 files changed, 260 insertions(+), 605 deletions(-) create mode 100644 data/systray_icon.svg create mode 100644 virtscreen/icon/systray_no_tablet.png delete mode 100644 virtscreen/icon/systray_no_tablet.svg create mode 100644 virtscreen/icon/systray_tablet_off.png delete mode 100644 virtscreen/icon/systray_tablet_off.svg create mode 100644 virtscreen/icon/systray_tablet_on.png delete mode 100644 virtscreen/icon/systray_tablet_on.svg diff --git a/data/icon_full.svg b/data/icon_full.svg index b0ce4e7..7e5732e 100644 --- a/data/icon_full.svg +++ b/data/icon_full.svg @@ -7,9 +7,9 @@ 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="128.91515mm" + width="122.79081mm" height="122.79081mm" - viewBox="0 0 128.91515 122.79081" + viewBox="0 0 122.79081 122.79081" version="1.1" id="svg8" inkscape:version="0.92.2 2405546, 2018-03-11" @@ -40,16 +40,6 @@ offset="1" id="stop4553" /> - + gradientTransform="scale(0.26458334)" /> + image/svg+xml - + @@ -118,60 +118,65 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" - transform="translate(-26.029924,-31.875429)"> - - - - + transform="translate(53.98708,-31.875429)"> - - - - + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/virtscreen/assets/main.qml b/virtscreen/assets/main.qml index 7e10c3b..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/systray_tablet_on.svg" : - backend.virtScreenCreated ? "../icon/systray_tablet_off.svg" : - "../icon/systray_no_tablet.svg" + 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/icon/full_256x256.png b/virtscreen/icon/full_256x256.png index f206a27f62801bcb2ce928864c4e924db75a1764..03d75e9415952069b430978b1d9bd7542e03b3e9 100644 GIT binary patch literal 21418 zcmZU*1yq#L*Dg+XBi+){pfn61NP~1Uh=d48cMK&;OLs^}gMtzg14yTUloEq1_r67hN>Y31}6F=CI$f> z`pw$A$_f33j&LM%rIdekFktj@(($6y zjKIRgD#5tKD>;CF^p>$Qbuf($GVKw`~8#_iXg~w?0pPeh&*fO4O?60w^o|ctAdr_Z5g3P*MSE`f}cwdocBt& zjqm}yeHd7e1u@2LKFxxYXNJLoV5*9S$oQR#WZQAWBN@m`q9NGmZ)}d#R3wwByL( zfO;M%{kN_c#o&=u{{xn_ha3a{zl)*|!=Ir-i(j3z_5`}v-p zsJtgb`7v}p93x`!J#B0(i-yptpg)YkXe`X%GlKE*)~9nmMnhBm@PXK1S$IA%#^S#Y z<=_j%)4fpPXfp>EcqA!NNi;MX(nZ07I1mq1n~!EQul}EnnBb8#|FcmGy3zj+NeYHf z%NIe0B12vMmkdj2@QMJeNvB!Q^kWejY8$IL<HPm8>SIt5l-g7BDL#brfDA=?<$EecXMP!Yr`Ka4%z{X&lY>kBn(bY)wmaLn zD~xEimW4A}1e|(QRYpVGduoPI|If40q0MDN>9LCcu8+D|Mx~m>EF>43?BVI!!T96E{3BO1>yg{65qC=`X8ay zQt^KU-rCrUGQ{*f6>3r{6hAYF= zK%FdHH;xQ-8w`K6{fZVa zk*pPZV@L-4FVTc>1(t%U-Ls=6Y5%)Ky;}Y@uaOlc&2MGl&OQPE8QGfQ=d=}pZ^wo| zmsD5~&FymkQJx3C`G~E+H-zEOI4yZH7KE$$|CUx@sZvV_Em*{ta@OXwWnT(vW&l~AK4+E>! z@-Erd{4w3w*h_QljKyRNJ111^gs`1-0g8%u@bp3guXz}L7GH!95vuHm^XaV_cwh%~ zCH}^yBbK7Cp?vSx2nFRj?@9MN4*;ePzb1#g*H;WO9+CdxJdj>}cp$2gLRGP06rMZVq|5C=*kI&eS9t5%-3^?44Tv1SjEP`-b%rs?0Z$<# zKg$Q-irWUQUpH{={&EqskyYpg!R5e%&fsuDC~|0xzAKWK0HJh|9qDG`jEmYR<3yA- zhJA~M&bEJ?>h#425K`eRyzkW0Zzs_k{gVjxk_x)n59gv@$ORa|_y!E^79f=M`%T#8 zxgm=hSpyi!9x+YrP$GH`(U2dTtuKK6?C$qWW{rnhQF?{`S|Y2Rk&WSdPe`5$I8D+b zdN%*l3f+W1;&XL_}SFo2&rN^ur{`)^e zX}koZ_Sbq>@NYz)yW19%pJE^X2Z`+bw%yFOZ)>M$CG$a{>``WeO*kv`bWeXhI^;&_ zaaH9GJ_6_BA6{KAIR^OdcIrqgUaydqIP569J4U^j1P(h6`-C$|2gf}7*GfJDxOEm; zAI$7NTi6>ZrSn+r`8GDSF)a#amhonrTlwcZCoDtXftFV0!m zapX%ePu9Xrc%b}(?f0&)T({IQSV>b(yBDdSU*`0ZAO6+r$^G4ehSE#XZ_%2q zp3VK4xl@eIx5BPOz+qQ{$+qm1h3$+HZg)@^A@R)}7Zic|1g_-GN+u_#qyX!uP!M3m zs$(m5PG$5kM6xY<^TKr8J1~IWUd$pHbDwJUNqXQ=0i;wyUZ zbMN1H;dV#=K##Bxg|ap{S_A|fe2;NRQV7_Vp(*MRVE<*rOj9euUnE-sL6v&IR9fyYbQ(m7o-ZvtTWEsr7;(7m+YhaGgR4FP8%koP?i6n)YZ1d+PK4>gtg zjshFTniB^GjkUH67@rTyv-ithlCT7+bu(o4pW|>)y)~#Y=6$v7_zCVIoEMRKp2%I= zSvR4!tQDgAO!D^W8*r-nrQaJLLn5wyjXEORL1)YCVaU^c zy!oM*OAz=RxOV|{Ards}$fT)|kH?+jLz_Yna02S_$=2h|=y}si?q-1{ADLBbL~tJE zJSw5y%B$OLu2)+@5H93LQG~l#=djgAGyILRT#CNVIx_pOc+RuQVLj6Z>*P5KqBcIP zJmxFw-_{Ju(@vv6{b*5!dWLO^+57j83jSJJU3Kg_dXtPP){~~X6uV#Wor;pQ1)M~- z>qqQVo6Z)Q{FKE#ZR>$L>#|2O@lu%ZNIvh`)G;S?%*{#IYAW~9VvuidBGKY0+D+0P zw5pTHXc{me)Or|8@FIR z$9*1>$FNoGdYo(QaJ!9n%0&*R*|L{EKYUw#^k>&8CC)x4W@}upuHH;g#5&BL?hB)* zLo-FXjVz@~p!vwlw}X_QqwD`rFE*1{f7{RqZ@#deFZQ=! zR?+8Cn$Wj2Teb62lZpnFGuncwLfL$eWQ(5Ql=Vmb**5;<`+7p|d3dKwmM>s%H;~#z zaBTjU>%M5)dOu(l^83f^DhmdQYTFIW)=|c;sWA&`gK7QvG~~cPjR5w?V7ybNjyb@F z-(wxlYhC3<9{Nsqn1AsPchszN@0YvJb#?b_DwF2%789^wA*{%!6)Mhq_J;JYn3sqv zLZ70lS0Gl2%fGq|6B+^@^m1YyuRtPRd_EyNB$99^f|W*s7EE#%u^|3oZVy_%#H&$$ z_{)vsDaf8?PNKt;@N&fZ26Wn&p-+GyVz@9*d~NZ1{=q6w!CxJ+6CACIn?$PG+}D{7 z+4sF;rUKnYqwc0plI$&Riht$3##mpCe)m_X&&J-#7SmewRAR8K5U)xE{V*G29E~en~!pzec6^?nKVE1CJQEHmmei$EdK=TuY+E)}B zBK5v56qERlPU0!<*Q{ZHAUE(wLV7-rV2T=tkszRqYEj6k3i*TG85^ZQN;cc%*fjh* zN*0AZmMt#D0z_gbSBH4;7ERa-(@niP@ZGOvIr6f_VmChbq)0uFu$Kwwnxf3vG;BI} z(r){RQb4LnfXE`jHa2#oLF3Sv#Ko`dhS~1>>+H>AuUAV7CDqVba^y=_3VmUy$CX_dE zEx8`N;^@fxYwQC!hP5bgQK@s=h0%7QTE{iagfk{>--m*9JAks%OpV#e-qTk-EBNF} z)J)MyDvm|XYOG$sqiVo~^L$!+9Tfjr|8Lo_vWIGX(DT$n zyuL1cQYm6xjXrI?aQaP^sN#kGUv)EGIdUr0Z~RU9lsPHBKPBy6uUC!EOwd0>^jN9m zN7zWNGZ;;sWQvhgPq@V?8SX9eczdV{Wqz9PPRsYt-`sR&inM4juHFw=7tz31G3Ncc zUo=9xY6!uTyw;4{3yXRYVNLd!IycJ{MB+W&J9Xgd4eZS4&->O zh)K_1{Pz4D?+~s>cFChJdb;r0gq~y3B9yj%ua? zwOth3&27nAl>#$kTAx&^ee#we$8$abSv^bD8#BvJxLWOH*+CWz1-I*uc`NkWfzal; zU#{b?+bbq1>1i9Xq8^na{7&y|V zbDE(TYL+)=2CB7OWPKd6Z>Q7%8LjcHfn0Wf-3p*!14=^7(_t!0q}iEa(83V$8@X0V zHteqFO*(m2ys@6Tb96ea)>jtkd9t3t`y3xsqDXRX90^KW>fE4ye_!$4TIf#ybVk^E zJ=4Nj^dh&1=iTSN8a+G5npwgePx1?C9@lcDXO+La^ zyDcnY-jEPk=|u-#KlVn>RLU3&{aU#As1=?(7j{KEPqTUkYfQrugWI75YTKn*{8!;e zp!Q2aw@M_?JkbL0jb75jE2@1zi!kPT~YVNq$7d=RPa<9D^mk!IaDvTn@ z(}NADn86Fd2PaAsPe#U;KFs{?3&vkgyU=8&d3sGdI9g$I#m={sL>DzIpFHX%MbT_e z@4;-4|5iMUBZy37t^4g7Vl?~gR=;aT#LMK8y!+hoEQB5pah0;xe*!EOZwz@LjrH0d zM;y*DX#OT9v9BRjEu)f_DFV-^O1{%)f@9`vK;7zI?ht>y(0w%H+MdB}$KL*ew+yJm z)w@cX6O&32NU$z`Ih&zQq4LJuWtPx=BzN-*9 z_9F~S-G#w0ibJj(LyzaO&n5u48T<%zho{Iec!C#8F-X0uf3&v)u6%tbVluqy>6}KJfZ(YatvHSb-qPz2M^#80DzRr|W^mC_H;j z`uC@>^`}4=O{S9~x}R%I6tb2Gfa@Q=oGS$=#$z*D7v`9em2y9O%a?z;rl2nNAD6f% zqnLm@XXx3@zr)TK_r0(06)$EI;6sp;lPL1?5JAo0v|+i237_6;+0lCfW$$u2%wO>m zxA2l77_FBVMm6V&?nRcAIG^);z_IcIXcV6gH&{=W)rBw7KY!?O1Agpbd%da7Ls@DZ zq-<{{f)O#*eJ&op^-W|EztD6#I~?T$NB5fqt!vGUbH~{hi)R`Q9msk`Pe2X|7PXbJ z#r)-jd)glfnh`DoE^GTR&DoHZUyZCTi!)aeP)r^$A^y-f_PP{r;Hq8{m|H{E@G4i5 zUNV7i9UXH<>AlWv+2;Fqu0<(W6+_LD$2~l_r{z(gxnHbE=M}(SCjI`z!fCrq##LtU zIqI_&ehun+7qp@TbU!`*Vtma}Dk#-p-CHYixHEj{@`=vFdT7Gc45{$21@ypZtk{03 zM+)~Z0hK7l>FK(91C@C_Mv>d8_D%7Phu2%j?XG9QSqWKIzff=5F6ad_^9_{$j@(nl zJaP)M?+A|7%ir^9>@@ZQ952C)#8J!#;A8{Jsp8Y6O?SB6NuntLBx@CCt1`jBY7jJr?Af3+lS=afi#QD!?*YA?yDBcT^+!o zYjY9t2`hj$-iH2qQ?{~CLgkKPi{vuH4%7jLVYzD5GqGv-N+iAxp`al~3JW4+mShuw z%f3jekm6JZJhyl#9nEk5y^Q2H%}pz-V-%#l$~|UcpbRi9icsFl!uPiUY=d@_xnc2$#(J-`^ z*$SY{IFxVustRO|80Hl`#4RNKu`ldY7!KdoKLx@7{5RlVuin_2;|Il$#9aA?yJaTL zx)viJD+1MyU#K`xLkXxdd0Y*h>BsL^?^(7j$8vasuyl!Ff6Y-uahzY-)4a0~W{g<= z@`Iup%4AqGRFP8g9<+lg;N(-|fIR^%mF~fZ@@7AzZt}vCS|M4K4a1Yc7WYIs*y#o4>IZA%&FFY8G$TvKN{O4#oSA* zdnNv01pB=O9^-a41|21$BM$Yl(|G&){fQ}^3m&f!umk<8HQ%`?f=A~~A zTbXRg>6xvsjEI;(xgY`fh^G}v_@F()hXEd-^5DZVDzzZVev8Nx$-e!Dd(hGFUp(mi zx7$?Dkm7~#SAMH+1z!rxDq3Kf8_I&k}kibi+H+&TLL@n6PU<2SeL7SiXc z=f%yTKHZVHkcu}u>Lx9Sofuy^JZNjH@$qZHA&5OIsb}dEF(_iO`gPawt!a#JGe}ClX z*LQ0be7(a$NU*9)WQtMwOsq_&8+U!O$u?TD#`Rq9ZG7@ltp86bfCZUNrs^ie<%}Ms zH|eT_y6v04O%qpx$sR+SnYa!l;+|MG;NXb)D7aizUtW+fjbnYrMI;WCzkdDsPJpFC zC-iAnMc$*WPhib;?x%e2BVbzl*b%-6V)F2wz~(1R9o%U++25E$x?T1jG=GouXefC6L_Y~VH2o64Y9ly-`Obx`tLYfm)uO- zkaIc=H1`3wNOkH*0};@Nc^J?Wuje+GP7kkIeF5HX@l9`vc4JyBtP6}Ho>S);*H<4Z z1=rPpIQs%K{>_HmgnRIrTz^$_|J*brO`kxtAvRZO|MAS!f<|3^x+y3cl12I&qu_4c zGVCuz&m76%2{(moAxH-&lvoq(bsbpx&rc08#jin0VXuYMa8JKRf*D%V^fKi?=L<4u z2>Ohj6%S|gD6lvLZ+hu@fMP%}zpLA7(=WHc+4>VW5RIKUmi~^hBv02u6Z_ zQwfrS32_4%xdfnud#w&h(5d*r0kVt zeK^ZH=!l2FDaaiY8Jf9~4$X$>73!K=pvZzdhoy5Pe1gjq2E^#bR345X#h=^M1<9Vk zrZVOnt9@keL@d+}PzSrv5*i}s$)3#(-TgC5R(qA*T_|5hVbgEbo?N8|I_iNpv~M0_ z`P?4B8gu-W080YYUnZTAZSeqyX7Pq&4v*2@PjY&n^aNK1@hufnW_v>!hSyhhI`Dwi zIm|Wbu|h`>^4r}pXed%Nh_Mt%Bj(tNTv7Tu1erE5cKKA0tK{ITh9a6W&HHk9t2wZb zUMT8wd;+erLVt8ts7)a%&QAk`&KU&O`-YpMI^h$Z$ zfneelc}tO6XFLD5DY#=u@3MH@UMgN7H3E&Q^d)1G(sJ^z#w-=mEE#zGDt@p*4HzMm zZdNsafE>zktS{3}4biv%Lpe)AS;U$Rp-ay`dT58bvlf z1Gb>r&aV^RlmTu6hpQ}_r)gU@%a9cjZ=)fj2Q!oG5+? z{92mEN#bXRLZ*4e`u;--$y5n!quFHDq?Q_=Au$f2Mh)}nbVt_MA{*x`B|@hHy01oHT}J^%e?P=PB) zJMkCxyD6LT(-98AgsN2jg!eH|B9&;;FfK5quU--%2Bc0e4*zVemCEuTU5P4pzR6yF z7>5vCC@<(4)sMCzI6Xm!{AXGRVUp4)RFdswsvi(-`A6WUID{rGXmjfLR(y?#?-O{0 z{E5;Zb=T4T{0CcDXl)EBrOy>th4k&;vubLM7Vj?V%~e=1hUmLg(hX+xuK#%!WSJC1 z3t>D|i71ZFCoFLkV_~L$-rQP3`7rdW;a6HD?<2o?pQ26DgX@u5(og&un>06T7D^4} z{HF_;k{(w>Ae*ZX7n%uszij4&@4wx#N<~aSA1|P-R3~^K>+|C7UZA)|YlcTu0Vkf9OTJh@G>9^)A_dofYfU1PVS(Z{lJa#&YYUl>`0mIoSz1 z6b52g^BxMT*I2Ltn4(3MQtkutT5JTqowH83o?m)SUX5MKfrO($bV#!Xe6zX7&j6R3 zwV`r4B=3&pt0a`sF~s$9&635gKAadn6lzkYBbyCAQ`Jp&69=zwYHB8rLA~ttSpYAK z8oG#AH^l-E(vCqNr?K12VIrH)07_M6$qT4?Z_cLB_Jq26fqZQlU~H01vT;JlU@ai7 z_>%~?hRI=k2`lgOonlXxInoc-$~vODB4oNY+!DE*WWTrWg>uxuHiElENgpmfJ8RNb zrc=0-C?G%8uS+pX{*|lpu?ELG5Oc;ofspV}ZHs_MSOj#ZKUOs3FqO9p{kp?GswpX> z87TP28e;KuEBm616cJwZ>EJuha{K^HigkpxX&Pc-TQJD4KiBHx~Nq(8%8MOJ38HF8Lg z$*9UN1q&>>+=`VivPftz8?|(>nL-+BgIY@6@jjF zPU6CEs>KR+p>+%w9;9vORydXv+vs>|+((Kvh|2NE+C7|Ga4HFvKEAFF$RMzO5ynB* zhpDJOi-IicQL?Ahq6g;!UdF;w)3-hwK~rhAJuSC*LyC3*YKLz~=!hu!|RUzyAlb8`3mV`fr zrXi@>4)K&q|Nf#MXzTHF8e$t$a&B&HpVhPc%qYp)jhD@i24htHY|8IHS_)TVzOI|w z3V0;e^n@zu>N{&^Ykuvg`lUs&Ev`8cTtA%rPrSs4RlIJ0Ab=6+Tp0g$8looLsxZHh zUH8#MCv9cjxgh>qb32}BljbL-*9U0DoYLE%SR&WP%PxmL+}@qUOHV^O9WL430?n$d ziE8$uB0=e^_3zOBW5kWrEeNIxD7OKcM!$|xFGc)~z1zeIj_k4t!Y_+kBB;FwZjT_2 zRjJH$JzEa{Yh!`NcwK!Wptfeu(#C|B4sHvLjnJVXaiZDiOUclY_6NqZYPgWPW`#Tb ztmZ%beA5tPo;-ixsFEr4-2VGfg7&D*lu8w&_l)#Y>>w%iB{A5P%Xc~F}GWVlr#e7?jKfBE{anN667>UEcmzu0GIjcAozUGB}`}&8@ zb@Z8pZiAo&3uJ-#x~Wbi1cZ)bEL;yiFx&@UvVD<1o`K$y~~hm zxv{6r*1=JlNj4xnWpQ4PO2Z{MX|~j06e`guz?-5*XY&QyZp0jki~xK3 z*DO_z>G!lu#RTz6@Q~Nidh2ZqdV>t67qRglxyK5V!EtXaFWGK`;%PriD7)@Z47+`* zk4G_8HT=q?qTV4YUse^)e#&!~!Y@n3=!DB~dLJ|oc{i0SBRqKk%XVXdne|SgevA7x zH<_&Rzs4*18Y@R)K_+(T7T&Ky%7$0%9B9~kVxW!QE9wFc_(VObm&HH%l1X02Xjjx^ zG|U=Y8C-uat<~7dCEZXH+&_-yEHL;DJPhe_GE2tFhjrKZqP44UCZ#+4sd{$$&Q+c%gBWRy5HT$%f; z(T#1H`RPBd&)n9xa8+;^a!odyA*5!*I}^AvV%%j$XUD4^Z=jFM+L14^-M-{3Qz3W2 z)OUhZU?ZQk(pCr#h3`-8*xI`dM@oR<9_@0>la4o+5y_$e(L#C zlo*#0u&o%8KoqX&S_be0jPYYGd|wJ3%F@xIjWjb!#q>-8e$A*cA4Tfo)(BF*2UaD8 zH<*h)-%0G&TOor^TiX!nutq`!ex}9{jtVc@W{z`?j=j1vV4RA^u9(pX4<92|6BoUD z8~1%zAcZSF%^b^s!P$NomPP#I6YCcrczAJsXcK8Q6@1VZwn?PXbP1+4lM-A=p2mSY zpU?F+_8bJ_JMW!0cClx)4!v2XVsm;H+S&FHHXh{?GCG5B`J8|oE;{BCdGHKSOXya- z6QLJ10Uz3_1Rg_^P)v1Kl8BK&;xhB(!zN=>962TtOwojL*`DosRL)l%gqAQJVsxX* zzjx5J_Zy{Qe7;@1qTK~o4Ek^;IqCTDTNvYEUnFQ+=aOxyyr31uozIqm zcN;J2#uxDW)%I35w2qhq(gLu--*aSJKVU9W)q==T&Azv|%RHUYnxhew1)}rSLkU6n zUdQlhh>Ev=lyS~PIOCyx6xc=o$->GmXyL6XyLSqp(UM)KG6JPj2XG^23)J+V-NdjX zHCPjnh*mE^8+U93!;KJ5^NBg4@Y`79(eP{U`uHD*N--TkY%DsV=^w?Yx!1P4u!B_9)#JScQm`WQW zBxhb_r2{@+RQ2w&ef7*Tp;z2<I7q}^0-9$o8c zG;l_TbZ!VzQsu?*KJrh#*SCxe9DA^Nw$UiBZ-s8Jrh{)goQl+SWtu zYf=SbetPo4I=@o?!;3Kwf`7M<4UA`;nc7|ged)JQ%{&pa;s?GDa z=+hg%5lT{iO@V~GA*xi$3EXUfAYx6kn11Jaf#;arLBFt2k65!UkO(usyqR)0A5+NH zq1iKhgfpuim3Fgyz+d|9!{hBYruY~UKTNPD6;~s-Dh+-%LN=yO2dzA!mAsJDCdzrI zN<}|!!+o!rUg(UqY0yept=f^+$t}cLOr@$Qzc1!hRU8#7}bu`v(M-NgacxB^nqj^F2jm|FN>)qy=B z{4ZJQaa>GXn~%UGrWPd&)u)-Jql=-%c zrK@EZ^p3^>AX3ZzOtR8#FXvm>iRZB~-B9iO+b|8|>(v<-@oP3aGI*_DGqlFY_wK!Z zHHBGA{pQtYXTHN$!Fm$^*>+XnED>FwCFw2RU7Drt481o^Y--Nk9AuFDO`RK9DJoF) zxQwD|MzA@T*4Uy6+hiFcL`S!>wlt^AQj^0OqSZ`F;CV@E=4&h%^Xlg_s=rJTM;_TA z;K_@tO2_;M!!J5|={(DTa{KYvIP;D+j&aj9>ga0X*9I6n7>Kwc1%=)*k>x0O47Z50 z;P;^cxIri(*!z|Cp8Jn)szAFKT<@iTrA;uSVQ)raxh>mIws(97BQqL%$^`9Pu2w55 zRGzQC+&u!%?PELdpPO;joWK4!t!iu?Sn}#ki|fSweAd-q`(~~w#ts33f2g^VH}Gn} ziok_hHM3@5{gdamhPuyZ01;rLw@(+c+5Fy8{@7XpX*0Lm>%)H*){n#?|}Ood6Ntn zp3Q;rWQk9Czx#SenUzxU`9SUYjSRxRX7|{9>+^q*tC-of_l9;f)8c)WMKJN*0GYU! zdSXddY3ZC1_qC49ogR(D^GT;NDw~QWt;rbofZM|OS`1fgz}5yO1Zxbc^G@4_&SXQL zdM0N%OHmkLMUoW0{IA|@+ZcEiX9Evu$*r_J`j#FY(l1I!7@TQ*+kXa#-9V-BsVwas z9^K(YB%wAYgc;Q|^~_n2J30;lK7PrTb={-9n2T89j*Yy6n~FB5dN3Z35;Ndvcnl%v zMbdxE{OrWXw_}5FEV;lysb=fxosJ$^Ny8#^xW!w3^_yyP0Pkq#JMD`sFN+7*E}nHp z8txpehJ|Go>dkua^B*1ONs;xQ*`#Xka?C4+ZRb8(Rx*NRxA{eYaY6UBvNo1B;b3-f z5+CNE3W>Cisoh6|bX}UUA4RH-a2M6lS zA(+T5fpldD;;Z_px{}; z?vk*^o^n?jV01Jg*uoHJPOOE(cj3GJ4dH-~AZN1{G2sF2G2tiCDI9WT3`*EcM(Jx_n?WTD0qU*$!yz+SE=bsZK2|`Oo zPxg+SZ+-izrnq*@Wx5T~cezue2Y2b}DJ>U=zF(?(w;+fP#N+kU)4Xcd=(2-VN)+f0 zU9G*H?05_4To$+OsPKcR8C83F717jWG@C-*qx)N-M`ePte$p6J4o!y(07G8>^*b|c zzR+X1H*lEICcK%oOBe~Zz=(j}tsnde3J5-TDH{_xL|L>UsGH)x^U7y`yUbp@WzA9r z;^^Wupuctha`we=2Vl)tVTEJB*YiQWYvF+bJ~V|zZol`>Yq{UNy-~?#Beq5np6)<- z9Iez(5J&g$;{(tn6dk%Rr=)1(tV*BMv{lWu@JA4yVq;LQ>vvzVNtsOVl}^ie-SSSi zPi(Daa-Lp+ZY3<(f~Fyar_b8Xqh~rmcZ!L>-?9rmMrX3tLN-VdyS!l{CDf;n+YlX= zn4vdUQm0332m*$#_^8hax0K?6wLPoSXE()fsb3yZl~AG$;|X$4%Il=}hlz>bcRt^1 zVc;=*YIRHha=q&w4h;v^2tKXqnylXNT&z`(OxoBX3~RQC>^RVKw;GKPH-o^3ZfBD9 z^r?@gx11p@ZHQS|ahm(vG3a^t=M+U04sdXS5LWC2lkfxp6GGT7vm*RNEM&WK6t~C{ z6EL177E09vdLw~20Rz~}kVS>a%GB$>=lYXF&EB_VEf&ftv6#Sx2XZa&qVfd8bN;Y2 z%kbSDGlqZ_E6$Kr6_{v$q3R%Ka`$J)2{Z-R0wdqNv8znb*Uuh{MPBZmVi-Pm}Fcz1%tfWYb2`WC|XKm%yv zCu0NHB4Ej(k~j6!+6ws1mBl`pHH!ZkI@F2AreH|S4>>=EQhw`Kz$DuCcM({~S!1Tg zy3KzuOJqk5)AtPyPOJ@Ng(B4f%uj{= zgX}NFn1WEauB3@=pQ+#Tt&_d9O#}-Bp9h@LEAXXmvVvJ4PE@}^WaRLKG;o&DT58%(m^V)M_w3C}_u~^J+4r4CZxQ z8S6O3)35J15IBg@)(k3%0mVM=Nr(Cl1SY+tm(o19)@K+C`IWI?Z~WLJO9bNqbgf9e zYPhH(sfjmf9K=KQ7vPUM|MD_12)neDJVuE_0C)_j?1^IRIi|WSX;Y8Vr5ZsAS1sDU z^g!8cH1^Z6hO@s%zfgc#%a&Wpq<_MHzP zkD7%P@(|)b6?=EY7EM9n}%fRA+TDo6^J&(ii#5T`xI7fA13p&-Xk&9Zw z3OA5i3YCnbjexKy!UMgYa_p4jwq<-l`z4U45mNl;1_q>5n|GrU+CIl^BdjL+<+}A( z4)2kwa}XFcffyl+f1s1v{Nf;V#tvgX1kwOPvi_ez&{D&s=%|!Js%|hH9 zvDl)>g_wp9I!-;%6C2cB#zP7rKv;=ibdgY5nR}_x5_^7lZx{MFJDuzUI&SMJ<_p^v zh#pvii4IEKw#kz-Pw>rOyg!EQW%L&;y79x(yblUZPcAH|GP>zF#2A6GKt~a*0X6;k z9zNo3-)Y@#BGqD2SC7aH>*N+2#q+DEgPs((n1iPAwVzgZU6hFW4uoq&qwBogI>y2= zl)e!jzr@{u@We8#Cjz+s=Bn+Q=J#A2!%;l(B~MaW90iwx zz+48Q!P_mX$SF)X^*@>qy5_ub)w(80t>A&O*y(vraCl9dFpKEXaZCE3v^450xkoI^JL!tDgpS0v22&teH!&dx6HU=hQwgTQ$URed1zS>rg(X9xlWf&@7Q^z|g zquA3?jCS15s>4WjoOq7e@n@FBC@Z?Aea}`}%4DczG8Qorlg2f0Ey;s*d8pyd4c3yw zQ`IC4_>E8cn7#NBe_HaZ^V7w~#{s_(EmjI#B|naF3QC92MOfJ5Lb872fMX*^`{);< zyWCOOTCW_HE9&6YQXH@=(H~4(f=*MB^+(j8t`{z1Ct(Ptnq_uJji85R9~~adJaDY~ zBY1ysjJ!|ADfCxnTV=ZUUyj$JwYK0gG*gg*?{fNM1Zw2!i)*$$s#_+UE>b?NJwvpf}s4!-8^hc*Lf+LE!u= zNl?1=N!U-1kX1V5(Yt^`mui@N5Q6)0^b5{J>t(b2AUfD#Pw^{btX9tt-`FhV+ukIX zF*MU8$78+l1r%(teqO_;iW)yT)0oL;M}H8gI-Br5lg$)RsuueDL^P^p^^-E0NUHN+ zxJaQ==+3*w#vGbL%iWMhs6hZAp(gU2LEsJj#hEi76!%N>CLylpHB7&{a%d=UZ@^00 zPT`~3Ip?dyHLK-AoLBgq%I@)m#5wY>!{RaMZ@~DITnxS;U+?ufHk<7C9%`l5BBDPi zyT8^Gws9C;J_DDMAFuwnSyJ#T^nVHpzTkx~-LoZwqO6&dL6`D|(~t-`TV;Hes@?{^ zhzqor{!gcG^9k_XJ`s7++(3lyrW9tV)I&aR=ew02pzyiP+>PFK01TeLU!FNsy`LO1 zX92ixz`=K0zR?nB-k-w&^uh!98iFbJsS((d!pt~3*MCvK{gS5(Z(?~dR^�$jkph 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 diff --git a/virtscreen/icon/systray_no_tablet.png b/virtscreen/icon/systray_no_tablet.png new file mode 100644 index 0000000000000000000000000000000000000000..92be6dabf2aaf85e1303e70b965725ba5d61ecff GIT binary patch literal 21304 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 literal 0 HcmV?d00001 diff --git a/virtscreen/icon/systray_no_tablet.svg b/virtscreen/icon/systray_no_tablet.svg deleted file mode 100644 index a59bfa9..0000000 --- a/virtscreen/icon/systray_no_tablet.svg +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - diff --git a/virtscreen/icon/systray_tablet_off.png b/virtscreen/icon/systray_tablet_off.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d850ef456e635279cfeca83d4175c6f34c17d7 GIT binary patch literal 19813 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 rv7L - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - diff --git a/virtscreen/icon/systray_tablet_on.png b/virtscreen/icon/systray_tablet_on.png new file mode 100644 index 0000000000000000000000000000000000000000..686202dd6f2976768248d0763d1e8fce104fb00e GIT binary patch literal 20037 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 literal 0 HcmV?d00001 diff --git a/virtscreen/icon/systray_tablet_on.svg b/virtscreen/icon/systray_tablet_on.svg deleted file mode 100644 index d4ba9b0..0000000 --- a/virtscreen/icon/systray_tablet_on.svg +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - From facb96ca194b1a2a65133938564ec24d80243998 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Fri, 29 Jun 2018 21:08:54 -0400 Subject: [PATCH 21/33] Icon: updated virtscreen.png --- data/virtscreen.png | Bin 28835 -> 21418 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/virtscreen.png b/data/virtscreen.png index ff65365d2943d8fc13c9f8270168d07c5cd9cd8b..03d75e9415952069b430978b1d9bd7542e03b3e9 100644 GIT binary patch literal 21418 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�$jkph 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 aAOv#mjy_Y|Dn6qujw>lW@&1G Date: Wed, 22 Aug 2018 14:07:25 -0400 Subject: [PATCH 22/33] Version bump: v0.2.5. PyPI: update setup.py metadata. Debian: added --yes option for debmake --- package/archlinux/PKGBUILD | 16 ++++++++++++---- package/debian/build.sh | 4 ++-- setup.py | 7 +++---- virtscreen/assets/config.default.json | 2 +- virtscreen/assets/data.json | 2 +- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index b356f39..dbfec40 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -1,15 +1,15 @@ # Maintainer: Bumsik Kim _pkgname_camelcase=VirtScreen pkgname=virtscreen -pkgver=0.2.4 +pkgver=0.2.5 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-quamash-git' 'python-netifaces') -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' ) @@ -24,9 +24,17 @@ source=(src::git+https://github.com/kbumsik/$_pkgname_camelcase.git#tag=$pkgver) noextract=() 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 $srcdir/src - PIP_CONFIG_FILE=/dev/null /usr/bin/pip install --isolated --root="$pkgdir" --ignore-installed --no-deps . + 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/debian/build.sh b/package/debian/build.sh index 7d8de0e..9ec30b1 100755 --- a/package/debian/build.sh +++ b/package/debian/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -PKGVER=0.2.4 +PKGVER=0.2.5 # Required for debmake DEBEMAIL="k.bumsik@gmail.com" DEBFULLNAME="Bumsik Kim" @@ -18,7 +18,7 @@ mv $PKGVER.tar.gz virtscreen-$PKGVER.tar.gz cp $ROOT/package/debian/Makefile \ $ROOT/package/debian/virtscreen-$PKGVER/Makefile cd $ROOT/package/debian/virtscreen-$PKGVER -debmake -b':sh' +debmake --yes -b':sh' # copy files to build # debmake files diff --git a/setup.py b/setup.py index 9db64e6..9f2f092 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.2.5', # 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 diff --git a/virtscreen/assets/config.default.json b/virtscreen/assets/config.default.json index bdf67b2..34eb435 100644 --- a/virtscreen/assets/config.default.json +++ b/virtscreen/assets/config.default.json @@ -1,5 +1,5 @@ { - "version": "0.2.4", + "version": "0.2.5", "x11vncVersion": "0.9.15", "theme_color": 8, "virt": { diff --git a/virtscreen/assets/data.json b/virtscreen/assets/data.json index 4380a48..140980c 100644 --- a/virtscreen/assets/data.json +++ b/virtscreen/assets/data.json @@ -1,5 +1,5 @@ { - "version": "0.2.4", + "version": "0.2.5", "x11vncOptions": { "-ncache": { "value": "-ncache", From 3e997c596acff92cc6c0e85147d319d011024da0 Mon Sep 17 00:00:00 2001 From: Luke Anderson Date: Sun, 4 Nov 2018 01:44:40 +0100 Subject: [PATCH 23/33] Added os.environ.get calls to prevent the assumption that the environment variables are set, which causes a KeyError. --- virtscreen/__main__.py | 2 +- virtscreen/qt_backend.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/virtscreen/__main__.py b/virtscreen/__main__.py index 9d71542..0117c66 100755 --- a/virtscreen/__main__.py +++ b/virtscreen/__main__.py @@ -79,7 +79,7 @@ def main() -> None: def check_env(msg: Callable[[str], None]) -> None: """Check enveironments before start""" - if os.environ['XDG_SESSION_TYPE'].lower() == 'wayland': + if os.environ.get('XDG_SESSION_TYPE', '').lower() == 'wayland': msg("Currently Wayland is not supported") sys.exit(1) if not HOME_PATH: diff --git a/virtscreen/qt_backend.py b/virtscreen/qt_backend.py index 1c7e465..a2896c8 100644 --- a/virtscreen/qt_backend.py +++ b/virtscreen/qt_backend.py @@ -85,7 +85,7 @@ class Backend(QObject): else: value["available"] = False # Default Display settings app for a Desktop Environment - desktop_environ = os.environ['XDG_CURRENT_DESKTOP'].lower() + desktop_environ = os.environ.get('XDG_CURRENT_DESKTOP', '').lower() for key, value in data['displaySettingApps'].items(): for de in value['XDG_CURRENT_DESKTOP']: if de in desktop_environ: From 9047091dd029e20ff95e3c3f974860054f94ddc3 Mon Sep 17 00:00:00 2001 From: Luke Anderson Date: Sun, 4 Nov 2018 01:53:39 +0100 Subject: [PATCH 24/33] Fixed parsing of configuration file so that desktop environment is correctly correlated with the display setting app, according to the data.json configuration. --- virtscreen/assets/data.json | 2 +- virtscreen/qt_backend.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/virtscreen/assets/data.json b/virtscreen/assets/data.json index 140980c..5ef5b88 100644 --- a/virtscreen/assets/data.json +++ b/virtscreen/assets/data.json @@ -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/qt_backend.py b/virtscreen/qt_backend.py index a2896c8..70f01bd 100644 --- a/virtscreen/qt_backend.py +++ b/virtscreen/qt_backend.py @@ -87,9 +87,8 @@ class Backend(QObject): # Default Display settings app for a Desktop Environment desktop_environ = os.environ.get('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 + 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)) From f27db06b173c0a22f812888574c1a53ac4f8991a Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Tue, 6 Nov 2018 21:06:50 +0900 Subject: [PATCH 25/33] Disable overriding PATH environment variable #19 --- virtscreen/path.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/virtscreen/path.py b/virtscreen/path.py index de94aec..1b5d070 100644 --- a/virtscreen/path.py +++ b/virtscreen/path.py @@ -7,25 +7,24 @@ from pathlib import Path # 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 +# Setting home path +# Rewrite $HOME env for consistency. 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 +try: + os.environ['HOME'] = str(Path.home()) + # os.environ['PATH'] = os.confstr("CS_PATH") # Sanitize $PATH, Deleted by Issue #19. -# 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: + # 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" -BASE_PATH = os.path.dirname(__file__) +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" From ebbbf97cdfc380dcf1c84b969ae5868134301c7a Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 7 Nov 2018 04:44:37 +0900 Subject: [PATCH 26/33] Use python logging module #8 --- Makefile | 15 ++++++++-- virtscreen/__main__.py | 61 +++++++++++++++++++++++++++---------- virtscreen/path.py | 1 + virtscreen/process.py | 15 +++++----- virtscreen/qt_backend.py | 65 +++++++++++++++++++++------------------- virtscreen/xrandr.py | 7 +++-- 6 files changed, 106 insertions(+), 58 deletions(-) diff --git a/Makefile b/Makefile index c690812..31f6b79 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,21 @@ DOCKER_RUN_TTY=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME .ONESHELL: +.PHONY: run debug run-appimage debug-appimage + # Run script run: python3 -m virtscreen +debug: + QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 python3 -m virtscreen --log=DEBUG + +run-appimage: package/appimage/VirtScreen-x86_64.AppImage + $< + +debug-appimage: package/appimage/VirtScreen-x86_64.AppImage + QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 $< --log=DEBUG + # Docker tools .PHONY: docker docker-build @@ -36,14 +47,14 @@ wheel-clean: .PHONY: appimage-clean .SECONDARY: package/appimage/VirtScreen-x86_64.AppImage -package/appimage/%.AppImage: +package/appimage/VirtScreen-x86_64.AppImage: $(DOCKER_RUN) package/appimage/build.sh $(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/appimage appimage-clean: -rm -rf package/appimage/virtscreen.AppDir package/appimage/VirtScreen-x86_64.AppImage -# For Debian packaging, https://www.debian.org/doc/manuals/maint-guide/index.en.html +# 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 diff --git a/virtscreen/__main__.py b/virtscreen/__main__.py index 0117c66..ed9546d 100755 --- a/virtscreen/__main__.py +++ b/virtscreen/__main__.py @@ -7,6 +7,8 @@ import signal import json import shutil import argparse +import logging +from logging.handlers import RotatingFileHandler from typing import Callable import asyncio @@ -25,8 +27,12 @@ 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 +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""" @@ -63,22 +69,46 @@ def main() -> None: 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=DEBUG.\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()) + # 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}') # Start main - args = parser.parse_args() - if any(vars(args).values()): + if any(args.values()): main_cli(args) else: main_gui() - print('Program should not reach here.') + error('Program should not reach here.') sys.exit(1) def check_env(msg: Callable[[str], None]) -> None: - """Check enveironments before start""" + """Check environments before start""" if os.environ.get('XDG_SESSION_TYPE', '').lower() == 'wayland': msg("Currently Wayland is not supported") sys.exit(1) @@ -94,6 +124,7 @@ def check_env(msg: Callable[[str], None]) -> None: if not shutil.which('x11vnc'): msg("x11vnc is not installed.") sys.exit(1) + # Check if xrandr is correctly parsed. try: test = XRandR() except RuntimeError as e: @@ -105,7 +136,7 @@ def main_gui(): 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: @@ -114,7 +145,7 @@ def main_gui(): dialog("Cannot detect system tray on this system.") sys.exit(1) check_env(dialog) - + app.setApplicationName("VirtScreen") app.setWindowIcon(QIcon(ICON_PATH)) os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material" @@ -138,38 +169,36 @@ def main_gui(): def main_cli(args: argparse.Namespace): loop = asyncio.get_event_loop() - 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" + 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. + # 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: + if not args['auto']: args_virt = ['portrait', 'hidpi'] for prop in args_virt: - if vars(args)[prop]: + if args[prop]: config['virt'][prop] = True args_position = ['left', 'right', 'above', 'below'] - tmp_args = {k: vars(args)[k] for k in args_position} + tmp_args = {k: 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)") + 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): - print('Error: ', msg) + error(msg) sys.exit(1) backend.onError.connect(handle_error) backend.createVirtScreen(config['virt']['device'], config['virt']['width'], diff --git a/virtscreen/path.py b/virtscreen/path.py index 1b5d070..3d7cabf 100644 --- a/virtscreen/path.py +++ b/virtscreen/path.py @@ -29,6 +29,7 @@ BASE_PATH = os.path.dirname(__file__) # Location of this script 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" diff --git a/virtscreen/process.py b/virtscreen/process.py index b3b197b..6d00040 100644 --- a/virtscreen/process.py +++ b/virtscreen/process.py @@ -5,6 +5,7 @@ import asyncio import signal import shlex import os +import logging class SubprocessWrapper: @@ -30,7 +31,7 @@ class _Protocol(asyncio.SubprocessProtocol): self.transport: asyncio.SubprocessTransport def connection_made(self, transport): - print("connectionMade!") + logging.info("connectionMade!") self.outer.connected() self.transport = transport transport.get_pipe_transport(0).close() # No more input @@ -47,14 +48,14 @@ class _Protocol(asyncio.SubprocessProtocol): def pipe_connection_lost(self, fd, exc): if fd == 0: # stdin - print("stdin is closed. (we probably did it)") + logging.info("stdin is closed. (we probably did it)") elif fd == 1: # stdout - print("The child closed their stdout.") + logging.info("The child closed their stdout.") elif fd == 2: # stderr - print("The child closed their stderr.") + logging.info("The child closed their stderr.") def connection_lost(self, exc): - print("Subprocess connection lost.") + logging.info("Subprocess connection lost.") def process_exited(self): if self.outer.logfile is not None: @@ -62,10 +63,10 @@ class _Protocol(asyncio.SubprocessProtocol): self.transport.close() return_code = self.transport.get_returncode() if return_code is None: - print("Unknown exit") + logging.error("Unknown exit") self.outer.ended(1) return - print("processEnded, status", return_code) + logging.info(f"processEnded, status {return_code}") self.outer.ended(return_code) diff --git a/virtscreen/qt_backend.py b/virtscreen/qt_backend.py index 70f01bd..84557b9 100644 --- a/virtscreen/qt_backend.py +++ b/virtscreen/qt_backend.py @@ -7,6 +7,7 @@ import os import shutil import atexit import time +import logging from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS from PyQt5.QtGui import QCursor @@ -74,7 +75,7 @@ class Backend(QObject): p = SubprocessWrapper() arg = 'x11vnc -opts' ret = p.run(arg) - options = tuple(m.group(1) for m in re.finditer("\s*(-\w+)\s+", ret)) + 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) @@ -93,6 +94,10 @@ class Backend(QObject): with open(CONFIG_PATH, 'w') as f: f.write(json.dumps(config, indent=4, sort_keys=True)) + def promptError(self, msg): + logging.error(msg) + self.onError.emit(msg) + # Qt properties @pyqtProperty(str, constant=True) def settings(self): @@ -118,7 +123,7 @@ class Backend(QObject): try: return QQmlListProperty(DisplayProperty, self, [DisplayProperty(x) for x in self.xrandr.screens]) except RuntimeError as e: - self.onError.emit(str(e)) + self.promptError(str(e)) return QQmlListProperty(DisplayProperty, self, []) @pyqtProperty(bool, notify=onVncUsePasswordChanged) @@ -148,28 +153,28 @@ class Backend(QObject): @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...") + logging.info("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')) + self.promptError(str(e.cmd) + '\n' + e.stdout.decode('utf-8')) return except RuntimeError as e: - self.onError.emit(str(e)) + self.promptError(str(e)) return self.virtScreenCreated = True @pyqtSlot() def deleteVirtScreen(self): - print("Deleting the Virtual Screen...") + logging.info("Deleting the Virtual Screen...") if self.vncState is not self.VNCState.OFF: - self.onError.emit("Turn off the VNC server first") + self.promptError("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)) + self.promptError(str(e)) return self.virtScreenCreated = False @@ -181,11 +186,11 @@ class Backend(QObject): 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')) + self.promptError(str(e.cmd) + '\n' + e.stdout.decode('utf-8')) return self.vncUsePassword = True else: - self.onError.emit("Empty password") + self.promptError("Empty password") @pyqtSlot() def deleteVNCPassword(self): @@ -193,16 +198,16 @@ class Backend(QObject): os.remove(X11VNC_PASSWORD_PATH) self.vncUsePassword = False else: - self.onError.emit("Failed deleting the password file") + self.promptError("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.") + self.promptError("Virtual Screen not crated.") return if self.vncState is not self.VNCState.OFF: - self.onError.emit("VNC Server is already running.") + self.promptError("VNC Server is already running.") return # regex used in callbacks patter_connected = re.compile(r"^.*Got connection from client.*$", re.M) @@ -210,27 +215,27 @@ class Backend(QObject): # define callbacks def _connected(): - print("VNC started.") + logging.info("VNC started.") 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): - print("VNC connected.") + logging.info("VNC connected.") self.vncState = self.VNCState.CONNECTED if (self._vncState is self.VNCState.CONNECTED) and patter_disconnected.search(data): - print("VNC disconnected.") + logging.info("VNC disconnected.") self.vncState = self.VNCState.WAITING def _ended(exitCode): if exitCode is not 0: self.vncState = self.VNCState.ERROR - self.onError.emit('X11VNC: Error occurred.\n' + 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 - print("VNC Exited.") + logging.info("VNC Exited.") atexit.unregister(self.stopVNC) # load settings with open(CONFIG_PATH, 'r') as f: @@ -250,7 +255,7 @@ class Backend(QObject): try: virt = self.xrandr.get_virtual_screen() except RuntimeError as e: - self.onError.emit(str(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}" @@ -264,20 +269,20 @@ class Backend(QObject): def openDisplaySetting(self, app: str = "arandr"): # define callbacks def _connected(): - print("External Display Setting opened.") + logging.info("External Display Setting opened.") def _received(data): pass def _ended(exitCode): - print("External Display Setting closed.") + logging.info("External Display Setting closed.") self.onDisplaySettingClosed.emit() if exitCode is not 0: - self.onError.emit(f'Error opening "{running_program}".') + 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.onError.emit('Wrong display settings program') + self.promptError('Wrong display settings program') return program_list = [data[app]['args'], "arandr"] program = AsyncSubprocess(_connected, _received, _received, _ended, None) @@ -288,12 +293,12 @@ class Backend(QObject): 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.') + 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): @@ -304,7 +309,7 @@ class Backend(QObject): if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED): self.vncServer.close() else: - self.onError.emit("stopVNC called while it is not running") + self.promptError("stopVNC called while it is not running") @pyqtSlot() def clearCache(self): diff --git a/virtscreen/xrandr.py b/virtscreen/xrandr.py index d794454..e0e5da7 100644 --- a/virtscreen/xrandr.py +++ b/virtscreen/xrandr.py @@ -3,6 +3,7 @@ import re import atexit import subprocess +import logging from typing import List from .display import Display @@ -53,9 +54,9 @@ class XRandR(SubprocessWrapper): screen.height = int(match.group(7)) screen.x_offset = int(match.group(8)) screen.y_offset = int(match.group(9)) - print("Display information:") + logging.info("Display information:") for s in self.screens: - print("\t", s) + 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" @@ -108,7 +109,7 @@ class XRandR(SubprocessWrapper): def create_virtual_screen(self, width, height, portrait=False, hidpi=False, pos='') -> None: self._update_screens() - print("creating: ", self.virt) + 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'] From 00388dcb0a88cb9d3a2d11068a87290f7db7007c Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 7 Nov 2018 05:19:37 +0900 Subject: [PATCH 27/33] Enable status printing in CLI mode #8 --- virtscreen/__main__.py | 4 ++-- virtscreen/qt_backend.py | 25 +++++++++++++++---------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/virtscreen/__main__.py b/virtscreen/__main__.py index ed9546d..58858df 100755 --- a/virtscreen/__main__.py +++ b/virtscreen/__main__.py @@ -70,7 +70,7 @@ def main() -> None: 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=DEBUG.\n' + 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): @@ -177,7 +177,7 @@ def main_cli(args: argparse.Namespace): sys.exit(1) # By instantiating the backend, additional verifications of config # file will be done. - backend = Backend() + backend = Backend(logger=print) # Get settings with open(CONFIG_PATH, 'r') as f: config = json.load(f) diff --git a/virtscreen/qt_backend.py b/virtscreen/qt_backend.py index 84557b9..97988d3 100644 --- a/virtscreen/qt_backend.py +++ b/virtscreen/qt_backend.py @@ -8,6 +8,7 @@ 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 @@ -41,7 +42,7 @@ class Backend(QObject): onDisplaySettingClosed = pyqtSignal() onError = pyqtSignal(str) - def __init__(self, parent=None): + def __init__(self, parent=None, logger=logging.info, error_logger=logging.error): super(Backend, self).__init__(parent) # Virtual screen properties self.xrandr: XRandR = XRandR() @@ -51,6 +52,9 @@ class Backend(QObject): 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 @@ -95,7 +99,7 @@ class Backend(QObject): f.write(json.dumps(config, indent=4, sort_keys=True)) def promptError(self, msg): - logging.error(msg) + self.log_error(msg) self.onError.emit(msg) # Qt properties @@ -153,7 +157,7 @@ class Backend(QObject): @pyqtSlot(str, int, int, bool, bool) def createVirtScreen(self, device, width, height, portrait, hidpi, pos=''): self.xrandr.virt_name = device - logging.info("Creating a Virtual Screen...") + self.log("Creating a Virtual Screen...") try: self.xrandr.create_virtual_screen(width, height, portrait, hidpi, pos) except subprocess.CalledProcessError as e: @@ -163,10 +167,11 @@ class Backend(QObject): self.promptError(str(e)) return self.virtScreenCreated = True + self.log("The Virtual Screen successfully created.") @pyqtSlot() def deleteVirtScreen(self): - logging.info("Deleting the Virtual Screen...") + 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 @@ -215,16 +220,16 @@ class Backend(QObject): # define callbacks def _connected(): - logging.info("VNC started.") + 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): - logging.info("VNC connected.") + self.log("VNC connected.") self.vncState = self.VNCState.CONNECTED if (self._vncState is self.VNCState.CONNECTED) and patter_disconnected.search(data): - logging.info("VNC disconnected.") + self.log("VNC disconnected.") self.vncState = self.VNCState.WAITING def _ended(exitCode): @@ -235,7 +240,7 @@ class Backend(QObject): self.vncState = self.VNCState.OFF # TODO: better handling error state else: self.vncState = self.VNCState.OFF - logging.info("VNC Exited.") + self.log("VNC Exited.") atexit.unregister(self.stopVNC) # load settings with open(CONFIG_PATH, 'r') as f: @@ -269,13 +274,13 @@ class Backend(QObject): def openDisplaySetting(self, app: str = "arandr"): # define callbacks def _connected(): - logging.info("External Display Setting opened.") + self.log("External Display Setting opened.") def _received(data): pass def _ended(exitCode): - logging.info("External Display Setting closed.") + self.log("External Display Setting closed.") self.onDisplaySettingClosed.emit() if exitCode is not 0: self.promptError(f'Error opening "{running_program}".') From a3e65b8270dd7004cc468cd701a2a4d5eb541e07 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 7 Nov 2018 05:31:22 +0900 Subject: [PATCH 28/33] Add make debug --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 31f6b79..aeb8189 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ debug-appimage: package/appimage/VirtScreen-x86_64.AppImage docker: $(DOCKER_RUN_TTY) /bin/bash - + docker-build: docker build -f Dockerfile -t $(DOCKER_NAME) . From 4a2e7d0c546d56aab7cd85736874faba863137dd Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 7 Nov 2018 06:17:26 +0900 Subject: [PATCH 29/33] Remove versions in packages name --- .travis.yml | 8 ++++---- Makefile | 23 +++++++++++++++-------- README.md | 10 +++++----- package/appimage/.gitignore | 4 ++-- package/debian/Makefile | 2 +- package/debian/build.sh | 2 +- package/pypi/.gitignore | 1 + 7 files changed, 29 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2a0560d..4e692cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,16 +16,16 @@ before_deploy: | VERSION=$TRAVIS_TAG make override_version fi make package/pypi/*.whl - make package/appimage/*.AppImage - make package/debian/*.deb + 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/*.deb - - package/appimage/*.AppImage + - package/debian/virtscreen.deb + - package/appimage/VirtScreen.AppImage skip_cleanup: true on: tags: true diff --git a/Makefile b/Makefile index aeb8189..e99d246 100644 --- a/Makefile +++ b/Makefile @@ -6,10 +6,15 @@ 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) +PKG_APPIMAGE=package/appimage/VirtScreen.AppImage +PKG_DEBIAN=package/debian/virtscreen.deb + .ONESHELL: .PHONY: run debug run-appimage debug-appimage +all: package/pypi/*.whl $(PKG_APPIMAGE) $(PKG_DEBIAN) + # Run script run: python3 -m virtscreen @@ -17,10 +22,10 @@ run: debug: QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 python3 -m virtscreen --log=DEBUG -run-appimage: package/appimage/VirtScreen-x86_64.AppImage +run-appimage: $(PKG_APPIMAGE) $< -debug-appimage: package/appimage/VirtScreen-x86_64.AppImage +debug-appimage: $(PKG_APPIMAGE) QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 $< --log=DEBUG # Docker tools @@ -45,25 +50,27 @@ wheel-clean: # For AppImage packaging, https://github.com/AppImage/AppImageKit/wiki/Creating-AppImages .PHONY: appimage-clean -.SECONDARY: package/appimage/VirtScreen-x86_64.AppImage +.SECONDARY: $(PKG_APPIMAGE) -package/appimage/VirtScreen-x86_64.AppImage: +$(PKG_APPIMAGE): $(DOCKER_RUN) package/appimage/build.sh + $(DOCKER_RUN) mv package/appimage/VirtScreen-x86_64.AppImage $@ $(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/appimage appimage-clean: - -rm -rf package/appimage/virtscreen.AppDir package/appimage/VirtScreen-x86_64.AppImage + -rm -rf package/appimage/virtscreen.AppDir $(PKG_APPIMAGE) # For Debian packaging, https://www.debian.org/doc/manuals/maint-guide/index.en.html # https://www.debian.org/doc/manuals/debmake-doc/ch08.en.html#setup-py .PHONY: deb-contents deb-clean -package/debian/%.deb: package/appimage/VirtScreen-x86_64.AppImage +$(PKG_DEBIAN): $(PKG_APPIMAGE) $(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) dpkg -c package/debian/*.deb +deb-contents: $(PKG_DEBIAN) + $(DOCKER_RUN) dpkg -c $< deb-clean: rm -rf package/debian/build package/debian/*.deb package/debian/*.buildinfo \ diff --git a/README.md b/README.md index d3a7e86..cb22819 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ VirtScreen is based on [PyQt5](https://www.riverbankcomputing.com/software/pyqt/ ### CLI-only option -You can run VirtScreen with `virtscreen` (or `./VirtScreen-x86_64.AppImage` if you use the AppImage package) with additional arguments. +You can run VirtScreen with `virtscreen` (or `./VirtScreen.AppImage` if you use the AppImage package) with additional arguments. ```bash usage: virtscreen [-h] [--auto] [--left] [--right] [--above] [--below] @@ -83,10 +83,10 @@ virtscreen --below --portrait --hipdi # Below, portrait, HiDPI mode. Download a `.AppImage` package from [releases page](https://github.com/kbumsik/VirtScreen/releases). Then make it executable: ```shell -chmod a+x VirtScreen-x86_64.AppImage +chmod a+x VirtScreen.AppImage ``` -Then you can run it by double click the file or `./VirtScreen-x86_64.AppImage` in terminal. +Then you can run it by double click the file or `./VirtScreen.AppImage` in terminal. ### Debian (Ubuntu) @@ -95,8 +95,8 @@ Download a `.deb` package from [releases page](https://github.com/kbumsik/VirtSc ```shell sudo apt-get update sudo apt-get install x11vnc -sudo dpkg -i virtscreen_0.2.4-1_all.deb -rm virtscreen_0.2.4-1_all.deb +sudo dpkg -i virtscreen.deb +rm virtscreen.deb ``` ### Arch Linux (AUR) 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/debian/Makefile b/package/debian/Makefile index 22b3f8b..b9629da 100644 --- a/package/debian/Makefile +++ b/package/debian/Makefile @@ -6,7 +6,7 @@ all: SHELL = /bin/bash install: mkdir -p $(DESTDIR)$(prefix)/bin - install -m 755 VirtScreen-x86_64.AppImage \ + install -m 755 VirtScreen.AppImage \ $(DESTDIR)$(prefix)/bin/virtscreen # Copy desktop entry and icon install -m 644 -D virtscreen.desktop \ diff --git a/package/debian/build.sh b/package/debian/build.sh index 9ec30b1..06ad577 100755 --- a/package/debian/build.sh +++ b/package/debian/build.sh @@ -30,7 +30,7 @@ cp $ROOT/package/debian/Makefile \ cp $ROOT/package/debian/{control,README.Debian} \ $ROOT/package/debian/build/debian/ # binary and data files -cp $ROOT/package/appimage/VirtScreen-x86_64.AppImage \ +cp $ROOT/package/appimage/VirtScreen.AppImage \ $ROOT/package/debian/build/ cp $ROOT/virtscreen.desktop \ $ROOT/package/debian/build/ diff --git a/package/pypi/.gitignore b/package/pypi/.gitignore index 0f66ccb..5b08461 100644 --- a/package/pypi/.gitignore +++ b/package/pypi/.gitignore @@ -1 +1,2 @@ virtscreen*.whl +*.tar.gz From 88079ad98a7b781cbf1730a8974ba7e890315894 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 7 Nov 2018 16:56:21 +0900 Subject: [PATCH 30/33] Makefile: Added tar.gz archive --- .gitignore | 3 +++ Makefile | 12 ++++++++++-- package/debian/build.sh | 6 ++---- 3 files changed, 15 insertions(+), 6 deletions(-) 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/Makefile b/Makefile index e99d246..590ac88 100644 --- a/Makefile +++ b/Makefile @@ -8,12 +8,13 @@ DOCKER_RUN_TTY=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME PKG_APPIMAGE=package/appimage/VirtScreen.AppImage PKG_DEBIAN=package/debian/virtscreen.deb +ARCHIVE=virtscreen-$(VERSION).tar.gz .ONESHELL: .PHONY: run debug run-appimage debug-appimage -all: package/pypi/*.whl $(PKG_APPIMAGE) $(PKG_DEBIAN) +all: package/pypi/*.whl $(ARCHIVE) $(PKG_APPIMAGE) $(PKG_DEBIAN) # Run script run: @@ -28,6 +29,12 @@ 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 @@ -64,7 +71,7 @@ appimage-clean: # https://www.debian.org/doc/manuals/debmake-doc/ch08.en.html#setup-py .PHONY: deb-contents deb-clean -$(PKG_DEBIAN): $(PKG_APPIMAGE) +$(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 @@ -121,3 +128,4 @@ override-version: # Clean packages clean: appimage-clean arch-clean deb-clean wheel-clean + -rm -f $(ARCHIVE) diff --git a/package/debian/build.sh b/package/debian/build.sh index 06ad577..8212035 100755 --- a/package/debian/build.sh +++ b/package/debian/build.sh @@ -11,10 +11,8 @@ ROOT=$SCRIPT_DIR/../.. # Generate necessary files for package building (generated by debmake) cd $ROOT/package/debian -wget -q https://github.com/kbumsik/VirtScreen/archive/$PKGVER.tar.gz -tar -xzmf $PKGVER.tar.gz -mv VirtScreen-$PKGVER virtscreen-$PKGVER -mv $PKGVER.tar.gz virtscreen-$PKGVER.tar.gz +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 From 6759ac6ae24ed3288af3823227fc96ccd0c33b74 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 7 Nov 2018 16:58:29 +0900 Subject: [PATCH 31/33] Version bump: 0.3.0 --- Makefile | 2 +- package/archlinux/PKGBUILD | 2 +- package/debian/build.sh | 2 +- setup.py | 2 +- virtscreen/assets/config.default.json | 2 +- virtscreen/assets/data.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 590ac88..89a9ad5 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # See https://packaging.python.org/tutorials/distributing-packages/#packaging-your-project # for python packaging reference. -VERSION ?= 0.2.4 +VERSION ?= 0.3.0 DOCKER_NAME=kbumsik/virtscreen DOCKER_RUN=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME) diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index dbfec40..600d8ec 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Bumsik Kim _pkgname_camelcase=VirtScreen pkgname=virtscreen -pkgver=0.2.5 +pkgver=0.3.0 pkgrel=1 pkgdesc="Make your iPad/tablet/computer as a secondary monitor on Linux" arch=("i686" "x86_64") diff --git a/package/debian/build.sh b/package/debian/build.sh index 8212035..38ce6e2 100755 --- a/package/debian/build.sh +++ b/package/debian/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -PKGVER=0.2.5 +PKGVER=0.3.0 # Required for debmake DEBEMAIL="k.bumsik@gmail.com" DEBFULLNAME="Bumsik Kim" diff --git a/setup.py b/setup.py index 9f2f092..144e370 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.5', # Required + version='0.3.0', # Required # This is a one-line description or tagline of what your project does. This # corresponds to the "Summary" metadata field: diff --git a/virtscreen/assets/config.default.json b/virtscreen/assets/config.default.json index 34eb435..3c018c2 100644 --- a/virtscreen/assets/config.default.json +++ b/virtscreen/assets/config.default.json @@ -1,5 +1,5 @@ { - "version": "0.2.5", + "version": "0.3.0", "x11vncVersion": "0.9.15", "theme_color": 8, "virt": { diff --git a/virtscreen/assets/data.json b/virtscreen/assets/data.json index 5ef5b88..1d5dc0e 100644 --- a/virtscreen/assets/data.json +++ b/virtscreen/assets/data.json @@ -1,5 +1,5 @@ { - "version": "0.2.5", + "version": "0.3.0", "x11vncOptions": { "-ncache": { "value": "-ncache", From bd115a29f25dd90c41ce36aaee00bf69ec54a87f Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Fri, 9 Nov 2018 18:15:51 +0900 Subject: [PATCH 32/33] FIXED: abort program when ~/.config/virtscreen does not exist --- virtscreen/__main__.py | 59 ++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/virtscreen/__main__.py b/virtscreen/__main__.py index 58858df..5237741 100755 --- a/virtscreen/__main__.py +++ b/virtscreen/__main__.py @@ -79,6 +79,34 @@ def main() -> None: 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' @@ -99,31 +127,6 @@ def main() -> None: logging.info('logging enabled') del args['log'] logging.info(f'{args}') - # Start main - if any(args.values()): - main_cli(args) - else: - main_gui() - error('Program should not reach here.') - sys.exit(1) - -def check_env(msg: Callable[[str], None]) -> None: - """Check environments before start""" - if os.environ.get('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) # Check if xrandr is correctly parsed. try: test = XRandR() @@ -131,7 +134,7 @@ def check_env(msg: Callable[[str], None]) -> None: msg(str(e)) sys.exit(1) -def main_gui(): +def main_gui(args: argparse.Namespace): QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) app = QApplication(sys.argv) loop = QEventLoop(app) @@ -144,7 +147,7 @@ def main_gui(): if not QSystemTrayIcon.isSystemTrayAvailable(): dialog("Cannot detect system tray on this system.") sys.exit(1) - check_env(dialog) + check_env(args, dialog) app.setApplicationName("VirtScreen") app.setWindowIcon(QIcon(ICON_PATH)) @@ -170,7 +173,7 @@ def main_gui(): def main_cli(args: argparse.Namespace): loop = asyncio.get_event_loop() # Check the environment - check_env(print) + 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.") From 9637d628165cd46db0873dbd82af2bfd91d0b7d2 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Fri, 9 Nov 2018 18:19:52 +0900 Subject: [PATCH 33/33] Version bump: v0.3.1 --- Makefile | 2 +- package/archlinux/PKGBUILD | 2 +- package/debian/build.sh | 2 +- setup.py | 2 +- virtscreen/assets/config.default.json | 2 +- virtscreen/assets/data.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 89a9ad5..9ccaa05 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # See https://packaging.python.org/tutorials/distributing-packages/#packaging-your-project # for python packaging reference. -VERSION ?= 0.3.0 +VERSION ?= 0.3.1 DOCKER_NAME=kbumsik/virtscreen DOCKER_RUN=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME) diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index 600d8ec..b5ae795 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Bumsik Kim _pkgname_camelcase=VirtScreen pkgname=virtscreen -pkgver=0.3.0 +pkgver=0.3.1 pkgrel=1 pkgdesc="Make your iPad/tablet/computer as a secondary monitor on Linux" arch=("i686" "x86_64") diff --git a/package/debian/build.sh b/package/debian/build.sh index 38ce6e2..dea5e40 100755 --- a/package/debian/build.sh +++ b/package/debian/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -PKGVER=0.3.0 +PKGVER=0.3.1 # Required for debmake DEBEMAIL="k.bumsik@gmail.com" DEBFULLNAME="Bumsik Kim" diff --git a/setup.py b/setup.py index 144e370..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.3.0', # 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: diff --git a/virtscreen/assets/config.default.json b/virtscreen/assets/config.default.json index 3c018c2..ae1978c 100644 --- a/virtscreen/assets/config.default.json +++ b/virtscreen/assets/config.default.json @@ -1,5 +1,5 @@ { - "version": "0.3.0", + "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 1d5dc0e..ec975a5 100644 --- a/virtscreen/assets/data.json +++ b/virtscreen/assets/data.json @@ -1,5 +1,5 @@ { - "version": "0.3.0", + "version": "0.3.1", "x11vncOptions": { "-ncache": { "value": "-ncache",