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

Compare commits

...

54 commits

Author SHA1 Message Date
Bumsik Kim
9637d62816
Version bump: v0.3.1 2018-11-09 18:36:07 +09:00
Bumsik Kim
bd115a29f2
FIXED: abort program when ~/.config/virtscreen does not exist 2018-11-09 18:15:51 +09:00
Bumsik Kim
6759ac6ae2
Version bump: 0.3.0 2018-11-07 17:50:06 +09:00
Bumsik Kim
88079ad98a
Makefile: Added tar.gz archive 2018-11-07 17:50:01 +09:00
Bumsik Kim
4a2e7d0c54
Remove versions in packages name 2018-11-07 06:17:26 +09:00
Bumsik Kim
a3e65b8270
Add make debug 2018-11-07 05:31:22 +09:00
Bumsik Kim
00388dcb0a
Enable status printing in CLI mode #8 2018-11-07 05:19:37 +09:00
Bumsik Kim
ebbbf97cdf
Use python logging module #8 2018-11-07 04:44:37 +09:00
Bumsik Kim
f27db06b17
Disable overriding PATH environment variable #19 2018-11-06 21:06:50 +09:00
Luke Anderson
9047091dd0 Fixed parsing of configuration file so that desktop environment is correctly correlated with the display setting app, according to the data.json configuration. 2018-11-04 10:57:41 +09:00
Luke Anderson
3e997c596a Added os.environ.get calls to prevent the assumption that the environment variables are set, which causes a KeyError. 2018-11-04 10:57:41 +09:00
Bumsik Kim
f62fa66f51
Version bump: v0.2.5. PyPI: update setup.py metadata. Debian: added --yes option for debmake 2018-08-22 14:54:45 -04:00
Bumsik Kim
facb96ca19
Icon: updated virtscreen.png 2018-06-29 21:08:54 -04:00
Bumsik Kim
2fd2119a7a
Icon: fix icon wasn't a perfect square 2018-06-29 20:37:23 -04:00
Bumsik Kim
9b8c1a71a4
main.py: delted Twisted code, added Qt application name 2018-06-29 20:35:38 -04:00
Bumsik Kim
451ada820b
README: updated How to Use 2018-06-29 20:33:35 -04:00
Bumsik Kim
c31f7054c4
Icon: Applied the new icon to the app 2018-06-29 19:22:09 -04:00
Bumsik Kim
d2ebf4bb0d
Icon: deleted unnecessary header 2018-06-29 18:18:49 -04:00
Bumsik Kim
3fe258a96b
README: added the new SVG file 2018-06-29 18:17:57 -04:00
Bumsik Kim
706a8d9ddf
Icon: added a new SVG icon 2018-06-29 18:03:14 -04:00
Bumsik Kim
28dabf2271
Changed main.py to __main__.py 2018-06-28 10:26:41 -04:00
Bumsik Kim
96c6066a91
split single virtscreen.py into submodules 2018-06-28 10:12:08 -04:00
Bumsik Kim
2ea15b8943
README: added asyncio 2018-06-28 08:14:53 -04:00
Bumsik Kim
c09fffe6e8
Backend: switched to asyncio from Twisted 2018-06-28 07:21:31 -04:00
Bumsik Kim
8393dab1a5
README: added FUreatures and CLI usage 2018-06-26 16:27:37 -04:00
Bumsik Kim
f5884ae9a1
AppImage: added command line arguments 2018-06-26 14:35:53 -04:00
Bumsik Kim
2bf8dedf9d
Debian: upgraded to AppImage based package 2018-06-26 13:12:34 -04:00
Bumsik Kim
7dcf8a8bde
Debian: cleanup build system 2018-06-26 05:02:30 -04:00
Bumsik Kim
a97e532b93
package/build_all.sh: Deleted and updated Makefile and Travis accordingly 2018-06-26 04:35:32 -04:00
Bumsik Kim
19d8e1a180
AUR: deleted md5sum 2018-06-26 04:32:03 -04:00
Bumsik Kim
d357296306
Makefile: cleanup build system 2018-06-26 03:49:45 -04:00
Bumsik Kim
af7b9348a5
AUR: version bump 2018-06-25 14:38:58 -04:00
Bumsik Kim
41af47f65e
README: updated installation section 2018-06-25 14:32:18 -04:00
Bumsik Kim
418072b80c
Version bump 2018-06-25 04:30:45 -04:00
Bumsik Kim
5fabf6a773
Fixed layout bug in QML 2018-06-25 04:06:53 -04:00
Bumsik Kim
a40ea8ec29
\#11: Initial AppImage packaging system 2018-06-24 17:55:15 -04:00
Bumsik Kim
aba7c949b5
Fixed README and setup.py after dropping qtbase5-dev and pyopengl 2018-06-24 14:56:09 -04:00
Bumsik Kim
9b2925b428
#10 import OpenGl library on startup, dropped qtbase5-dev dependancy 2018-06-24 14:36:57 -04:00
Bumsik Kim
458108119d
Version bump: v0.2.2 2018-06-21 05:06:00 -04:00
Bumsik Kim
ec392f50f5
\#10: Added pyopengl, custom shell script for miniconda environment 2018-06-21 05:03:28 -04:00
Bumsik Kim
2665d829a2
README: version bump 2018-06-18 20:59:45 -04:00
Bumsik Kim
ce84f0daa8
AUR: version dump 2018-06-18 20:47:08 -04:00
Bumsik Kim
d1563d14bc
Travis: Fixed bash interpreter 2018-06-18 20:32:42 -04:00
Bumsik Kim
7ebe935805
Travis: Built CI deploy 2018-06-18 02:56:56 -04:00
Bumsik Kim
ec88e497fa
Added Travis CI 2018-06-17 20:14:40 -04:00
Bumsik Kim
8c65910f27
#6: Added support for custom x11vnc arguments 2018-06-10 15:41:35 -04:00
Bumsik Kim
7941f1909a
#5, AUR: Switched from setuptools to pip for package builder 2018-06-09 22:26:22 -04:00
Bumsik Kim
ee0efafa02
More graceful signal handling 2018-06-09 11:30:31 -04:00
Bumsik Kim
9ec6256fc1
Separated Cursor and Network classes out of Backend class 2018-06-09 11:28:34 -04:00
Bumsik Kim
30ffe32f82
#7: Added CLI options 2018-06-08 18:00:15 -04:00
Bumsik Kim
439769ca21
#3: Fixed font sizing on HiDPi screen 2018-06-05 21:31:59 -04:00
Bumsik Kim
3dfa0f92e3
Fix continue 2018-06-05 21:23:21 -04:00
Bumsik Kim
0d01fa0816
#4: Fixed window positioning on HiDPI screen 2018-06-05 21:18:52 -04:00
Bumsik Kim
1d439478bb
Updated packages for v0.2.0 release 2018-06-03 14:04:15 -04:00
54 changed files with 1836 additions and 1106 deletions

3
.gitignore vendored
View file

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

40
.travis.yml Normal file
View file

@ -0,0 +1,40 @@
sudo: required
language: python
python: '3.6'
services:
- docker
install: |
docker pull kbumsik/virtscreen
pip3 install .
script: |
echo No test scripts implemented yet. Travis is used only for deploy yet.
before_deploy: |
if [ -n "$TRAVIS_TAG" ]; then
VERSION=$TRAVIS_TAG make override_version
fi
make package/pypi/*.whl
make package/appimage/VirtScreen.AppImage
make package/debian/virtscreen.deb
deploy:
- provider: releases
api_key:
secure: zFbsCIKcsvWU/Yc+9k294Qj8QY48VlkV8DSScP5gz6dQegeUSaSHI/YafherkFQ0B03bIY8yc7roMtDo7HAkEnPptjFhdUiOFI11+xDVb3s7Y8Ek2nV3znQzdtR4CR/94l3in6R3DH+eNA6+6Je/NIWLdVcvRX07RBSfBVdPmnsAyAD9KNTsl8Q4c20HgtLNxfWv2s5eCyD+heCTLYrErEZKZ5vYeeANmWomHvT2ED/4QerpBP8wkh59QXD1S79CF7oyq6X173ZJUQVxdBP+OSXt/mDBAoqf+TV6okawRZn48JluvCWAJ7BceX7t9emd1rVI/s8t3wCP+eMcmNn5g/6UJaCPnTJ5YplTuUWRc63UFSkE0AY8WYcRlrz+/OiXYgQ8LMXfN23aWgarHCbS2vHR3Afu9gpLCoKucr36hKhs3zfjJzVLFFW16mnbaTFcBzfDDRpkvOANB1aZwGVRFpTIWIMjkn0+lxWTC/moIJvQlfRPsC4dN5cDAilRQlguHzayebtGE8X0PuIe9A8bkET3V/y+KPnQiSJ7J+5PNoDSdqRAE4IKvVOLEyHtlqBVkvIHKnugUnWPIZ21gm5RemMEj9/YGa8Efwz7PIKtJJ3kFMGDYKVlIKyB+rg/TFWNdo6jjevnWM6y4SfVI3kFyjA+mp31o6nshrQy0zVQpd8=
file:
- package/debian/virtscreen.deb
- package/appimage/VirtScreen.AppImage
skip_cleanup: true
on:
tags: true
repo: kbumsik/VirtScreen
- provider: pypi
user: kbumsik
password:
secure: d7ozcWf9/j2mpyYX60o7yo/0dPnTkA/1FxPm6GV3bst264z1NVh4G4+J0o/jIpLKA9lEd5QbBUgnLnNIBGGBeEghYCeof/yZnekCntYd75tIAiaIkwBzaYu3n5wfxpEVUIDngTh+biH4EU4iq+Kxrg/KxMi+MetFWL6EVJgtIUarjr2wkBYmKAOEkNvyXWkIEJqUn0xuQSGmqGyNxRjoAPv+6i9QR7KnTCaEPOrEzwKyxhzOL33acBrmaymRFC7EznmaTIHMzGqBcaj3rljC6Kk5bnepSzncNTT8C4v8MuJZPF+oYPN5n16Xy4odAJlt1+pWsuAbhB6Gk/l5Z0zoKjIIuH2LkMWkm2MDO3qbmuu9qfEWg1Y+MmbhnVQf+1qRO7i0vMt9WP5X6IDPkBeXYibUiFZVwYY2AmBchRCD7XvIL1+0JEGQadtAR8EJWNPKCpRgl3p9WTyMVtGgob/UEzknRJWDAYk4u3R4yiMw+shqdc/osRyjoadVQZFZs/80QqLTBUFkR3XlBfNmyywtu3ux9PNnCEgoPO28K6EWj70UaujN87ByjFQ1b4n+wuWwFkp5PTJYLSHgXI8oR29VB9xk4mmKNU4MnAApokgbs4Gqb3jY6KHm5t/MIMqYcrOrqT8OYqwpvfie1FMLXvvtowcgVnUup7vOAaq9mafZpJI=
distributions: "bdist_wheel"
on:
tags: true
repo: kbumsik/VirtScreen

View file

@ -1,23 +1,29 @@
# Or bionic # Or bionic
FROM ubuntu:latest FROM ubuntu:bionic
LABEL author="Bumsik Kim <k.bumsik@gmail.com>" LABEL author="Bumsik Kim <k.bumsik@gmail.com>"
WORKDIR /app
CMD ["/bin/bash"]
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y python3-all python3-pip fakeroot debmake debhelper fakeroot wget tar && \ apt-get install -y python3-all python3-pip python3-wheel fakeroot debmake debhelper fakeroot wget tar curl && \
apt-get autoremove -y && \ apt-get autoremove -y && \
ln /usr/bin/python3 /usr/bin/python && \ ln /usr/bin/python3 /usr/bin/python && \
ln /usr/bin/pip3 /usr/bin/pip && \ ln /usr/bin/pip3 /usr/bin/pip && \
rm -rf /var/cache/apt/archives/*.deb && \ rm -rf /var/cache/apt/archives/*.deb && \
pip install virtualenv && \ pip install virtualenv && \
pip install --upgrade pip setuptools && \ pip install --upgrade pip setuptools
useradd -c Builder -m -U builder
USER builder
# Get Miniconda and make it the main Python interpreter # Get Miniconda and make it the main Python interpreter
RUN wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh && \ RUN wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh && \
bash ~/miniconda.sh -b -p ~/miniconda && \ bash ~/miniconda.sh -b -p ~/miniconda && \
rm ~/miniconda.sh rm ~/miniconda.sh
# AppImageKit
WORKDIR /opt
RUN wget https://github.com/AppImage/AppImageKit/releases/download/10/appimagetool-x86_64.AppImage && \
chmod a+x appimagetool-x86_64.AppImage && \
./appimagetool-x86_64.AppImage --appimage-extract && \
mv squashfs-root appimagetool && \
rm appimagetool-x86_64.AppImage
ENV PATH=/opt/appimagetool/usr/bin:$PATH
WORKDIR /app
CMD ["/bin/bash"]

View file

@ -6,4 +6,4 @@ include LICENSE.txt
# Include data directories # Include data directories
include data/virtscreen.png include data/virtscreen.png
include data/virtscreen.desktop include virtscreen.desktop

143
Makefile
View file

@ -1,66 +1,93 @@
# See https://packaging.python.org/tutorials/distributing-packages/#packaging-your-project # See https://packaging.python.org/tutorials/distributing-packages/#packaging-your-project
# for python packaging reference. # for python packaging reference.
VERSION ?= 0.3.1
.PHONY: DOCKER_NAME=kbumsik/virtscreen
DOCKER_RUN=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME)
DOCKER_RUN_TTY=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME)
python-wheel: PKG_APPIMAGE=package/appimage/VirtScreen.AppImage
/usr/bin/python3 setup.py bdist_wheel --universal PKG_DEBIAN=package/debian/virtscreen.deb
ARCHIVE=virtscreen-$(VERSION).tar.gz
python-install:
/usr/bin/pip3 install . --user
python-uninstall:
/usr/bin/pip3 uninstall virtscreen
python-clean:
rm -rf build dist virtscreen.egg-info virtscreen/qml/*.qmlc
pip-upload: python-wheel
twine upload dist/*
.ONESHELL: .ONESHELL:
# For Debian packaging, https://www.debian.org/doc/manuals/debmake-doc/ch08.en.html#setup-py .PHONY: run debug run-appimage debug-appimage
deb-docker-build:
docker build -f package/debian/Dockerfile -t debmake .
deb-docker: all: package/pypi/*.whl $(ARCHIVE) $(PKG_APPIMAGE) $(PKG_DEBIAN)
docker run --privileged --interactive --tty -v $(shell pwd)/package/debian:/app debmake /bin/bash
deb-docker-rm: # Run script
docker image rm -f debmake run:
python3 -m virtscreen
deb-make: debug:
docker run --privileged --interactive --tty --rm -v $(shell pwd)/package/debian:/app debmake /app/debmake.sh QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 python3 -m virtscreen --log=DEBUG
deb-build: deb-clean deb-make run-appimage: $(PKG_APPIMAGE)
package/debian/copy_debian.sh $<
docker run --privileged --interactive --tty --rm -v $(shell pwd)/package/debian:/app debmake /app/debuild.sh
deb-contents: debug-appimage: $(PKG_APPIMAGE)
docker run --privileged --interactive --tty --rm -v $(shell pwd)/package/debian:/app debmake /app/contents.sh QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 $< --log=DEBUG
deb-env-make: # tar.gz
docker run --privileged --interactive --tty --rm -v $(shell pwd)/package/debian:/app debmake /app/debmake.sh virtualenv .PHONY: archive
deb-env-build: deb-clean deb-env-make archive $(ARCHIVE):
package/debian/copy_debian.sh virtualenv git archive --format=tar.gz --prefix=virtscreen-$(VERSION)/ -o $@ HEAD
docker run --privileged --interactive --tty --rm -v $(shell pwd)/package/debian:/app debmake /app/debuild.sh virtualenv
# Docker tools
.PHONY: docker docker-build
docker:
$(DOCKER_RUN_TTY) /bin/bash
docker-build:
docker build -f Dockerfile -t $(DOCKER_NAME) .
# Python wheel package for PyPI
.PHONY: wheel-clean
package/pypi/%.whl:
python3 setup.py bdist_wheel --universal
cp dist/* package/pypi
-rm -rf build dist *.egg-info
wheel-clean:
-rm package/pypi/virtscreen*.whl
# For AppImage packaging, https://github.com/AppImage/AppImageKit/wiki/Creating-AppImages
.PHONY: appimage-clean
.SECONDARY: $(PKG_APPIMAGE)
$(PKG_APPIMAGE):
$(DOCKER_RUN) package/appimage/build.sh
$(DOCKER_RUN) mv package/appimage/VirtScreen-x86_64.AppImage $@
$(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/appimage
appimage-clean:
-rm -rf package/appimage/virtscreen.AppDir $(PKG_APPIMAGE)
# For Debian packaging, https://www.debian.org/doc/manuals/maint-guide/index.en.html
# https://www.debian.org/doc/manuals/debmake-doc/ch08.en.html#setup-py
.PHONY: deb-contents deb-clean
$(PKG_DEBIAN): $(PKG_APPIMAGE) $(ARCHIVE)
$(DOCKER_RUN) package/debian/build.sh
$(DOCKER_RUN) mv package/debian/*.deb $@
$(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/debian
deb-contents: $(PKG_DEBIAN)
$(DOCKER_RUN) dpkg -c $<
deb-clean: deb-clean:
rm -rf package/debian/build rm -rf package/debian/build package/debian/*.deb package/debian/*.buildinfo \
package/debian/*.changes
# For AUR: https://wiki.archlinux.org/index.php/Python_package_guidelines # For AUR: https://wiki.archlinux.org/index.php/Python_package_guidelines
# and: https://wiki.archlinux.org/index.php/Creating_packages # and: https://wiki.archlinux.org/index.php/Creating_packages
arch-update: .PHONY: arch-upload arch-clean
cd package/archlinux
makepkg --printsrcinfo > .SRCINFO
arch-install: arch-update arch-upload: package/archlinux/.SRCINFO
cd package/archlinux
makepkg -si
arch-upload: arch-update
cd package/archlinux cd package/archlinux
git clone ssh://aur@aur.archlinux.org/virtscreen.git git clone ssh://aur@aur.archlinux.org/virtscreen.git
cp PKGBUILD virtscreen cp PKGBUILD virtscreen
@ -72,11 +99,33 @@ arch-upload: arch-update
cd .. cd ..
rm -rf virtscreen rm -rf virtscreen
package/archlinux/.SRCINFO:
cd package/archlinux
makepkg --printsrcinfo > .SRCINFO
arch-clean: arch-clean:
cd package/archlinux cd package/archlinux
rm -rf pkg src *.tar* -rm -rf pkg src *.tar* .SRCINFO
launch: # Override version
./launch.sh .PHONY: override-version
clean: arch-clean deb-clean python-clean override-version:
# Update python setup.py
perl -pi -e "s/version=\'\d+\.\d+\.\d+\'/version=\'$(VERSION)\'/" \
setup.py
# Update .json files in the module
perl -pi -e "s/\"version\"\s*\:\s*\"\d+\.\d+\.\d+\"/\"version\"\: \"$(VERSION)\"/" \
virtscreen/assets/data.json
perl -pi -e "s/\"version\"\s*\:\s*\"\d+\.\d+\.\d+\"/\"version\"\: \"$(VERSION)\"/" \
virtscreen/assets/config.default.json
# Arch AUR
perl -pi -e "s/pkgver=\d+\.\d+\.\d+/pkgver=$(VERSION)/" \
package/archlinux/PKGBUILD
# Debian
perl -pi -e "s/PKGVER=\d+\.\d+\.\d+/PKGVER=$(VERSION)/" \
package/debian/build.sh
# Clean packages
clean: appimage-clean arch-clean deb-clean wheel-clean
-rm -f $(ARCHIVE)

110
README.md
View file

@ -1,54 +1,116 @@
# VirtScreen <h1 align="center">
<img src="data/icon_full.svg" width="21%">
<br/>
VirtScreen
</h1>
> Make your iPad/tablet/computer as a secondary monitor on Linux. <h4 align="center">
Make your iPad/tablet/computer as a secondary monitor on Linux.
</h4>
![gif example](https://raw.githubusercontent.com/kbumsik/VirtScreen/master/data/gif_example.gif) <div align="center">
<a href="https://github.com/kbumsik/VirtScreen">
<img src="https://raw.githubusercontent.com/kbumsik/VirtScreen/master/data/gif_example.gif" alt="VirtScreen" width="80%">
</a>
</div>
## Description
VirtScreen is an easy-to-use Linux GUI app that creates a virtual secondary screen and shares it through VNC. VirtScreen is an easy-to-use Linux GUI app that creates a virtual secondary screen and shares it through VNC.
VirtScreen is based on [PyQt5](https://www.riverbankcomputing.com/software/pyqt/intro) and [Twisted](https://twistedmatrix.com) in Python side and uses [x11vnc](https://github.com/LibVNC/x11vnc) and XRandR. VirtScreen is based on [PyQt5](https://www.riverbankcomputing.com/software/pyqt/intro) and [asyncio](https://docs.python.org/3/library/asyncio.html) in Python side and uses [x11vnc](https://github.com/LibVNC/x11vnc) and XRandR.
## Features
* No more typing commands - create a second VNC screen with a few clicks from the GUI.
* ...But there is also command-line only options for CLI lovers.
* Highly configurable - resolutions, portrait mode, and HiDPI mode.
* Works on any Linux Distro with Xorg
* Lightweight
* System Tray Icon
## How to use ## How to use
Upon installation (see Installing section to install), there will be a desktop entry called `VirtScreen` 1. Run the app.
2. Set options (resolution etc.) and enable the virtual screen.
3. Go to VNC tab and then start the VNC server.
4. Run your favorite VNC client app on your second device and connect it to the IP address appeared on the app.
![desktop entry](https://raw.githubusercontent.com/kbumsik/VirtScreen/master/data/desktop_entry.png) ### CLI-only option
Or you can run it using a command line: You can run VirtScreen with `virtscreen` (or `./VirtScreen.AppImage` if you use the AppImage package) with additional arguments.
```bash ```bash
$ virtscreen usage: virtscreen [-h] [--auto] [--left] [--right] [--above] [--below]
[--portrait] [--hidpi]
Make your iPad/tablet/computer as a secondary monitor on Linux.
You can start VirtScreen in the following two modes:
- GUI mode: A system tray icon will appear when no argument passed.
You need to use this first to configure a virtual screen.
- CLI mode: After configured the virtual screen, you can start VirtScreen
in CLI mode if you do not want a GUI, by passing any arguments
optional arguments:
-h, --help show this help message and exit
--auto create a virtual screen automatically using previous
settings (from both GUI mode and CLI mode)
--left a virtual screen will be created left to the primary
monitor
--right right to the primary monitor
--above, --up above the primary monitor
--below, --down below the primary monitor
--portrait Portrait mode. Width and height of the screen are swapped
--hidpi HiDPI mode. Width and height are doubled
example:
virtscreen # GUI mode. You need to use this first
# to configure the screen
virtscreen --auto # CLI mode. Scrren will be created using previous
# settings (from both GUI mode and CLI mode)
virtscreen --left # CLI mode. On the left to the primary monitor
virtscreen --below # CLI mode. Below the primary monitor.
virtscreen --below --portrait # Below, and portrait mode.
virtscreen --below --portrait --hipdi # Below, portrait, HiDPI mode.
``` ```
## Installation ## Installation
### Universal package (AppImage)
Download a `.AppImage` package from [releases page](https://github.com/kbumsik/VirtScreen/releases). Then make it executable:
```shell
chmod a+x VirtScreen.AppImage
```
Then you can run it by double click the file or `./VirtScreen.AppImage` in terminal.
### Debian (Ubuntu) ### Debian (Ubuntu)
A PPA package will be available soon. Download a `.deb` package from [releases page](https://github.com/kbumsik/VirtScreen/releases). Then install it:
```shell
sudo apt-get update
sudo apt-get install x11vnc
sudo dpkg -i virtscreen.deb
rm virtscreen.deb
```
### Arch Linux (AUR) ### Arch Linux (AUR)
There is [`virtscreen` AUR package](https://aur.archlinux.org/packages/virtscreen/) available. Though there are many ways to install the AUR package, one of the easiest way is to use [`aurman`](https://github.com/polygamma/aurman) AUR helper: There is [`virtscreen` AUR package](https://aur.archlinux.org/packages/virtscreen/) available. Though there are many ways to install the AUR package, one of the easiest way is to use [`yaourt`](https://github.com/polygamma/aurman) AUR helper:
```bash ```bash
$ aurman -S virtscreen yaourt virtscreen
``` ```
### Python `pip` ### Python `pip`
If your distro is none of above, you may install it using `pip`. In this case, you need to install the dependancies manually. Although not recommended, you may install it using `pip`. In this case, you need to install the dependancy (`xrandr` and `x11vnc`) manually.
#### Dependancies
You need [`x11vnc`](https://github.com/LibVNC/x11vnc), `xrandr`, and PyQt5 libraries. To install (e.g. on Ubuntu):
```bash
$ sudo apt-get install x11vnc qtbase5-dev # On Debian/Ubuntu, xrandr is included.
```
#### Installing
After you install the dependancies then run:
```bash ```bash
$ sudo pip install virtscreen sudo pip install virtscreen
``` ```

Binary file not shown.

182
data/icon_full.svg Normal file
View file

@ -0,0 +1,182 @@
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="122.79081mm"
height="122.79081mm"
viewBox="0 0 122.79081 122.79081"
version="1.1"
id="svg8"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="icon_full.svg">
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient1066">
<stop
style="stop-color:#6f8a91;stop-opacity:1;"
offset="0"
id="stop1062" />
<stop
style="stop-color:#6f8a91;stop-opacity:0;"
offset="1"
id="stop1064" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4555">
<stop
style="stop-color:#a4b5b9;stop-opacity:1"
offset="0"
id="stop4551" />
<stop
style="stop-color:#8ba1a6;stop-opacity:1"
offset="1"
id="stop4553" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1066"
id="linearGradient1068"
x1="145.00168"
y1="321.47601"
x2="683.82404"
y2="482.69577"
gradientUnits="userSpaceOnUse"
gradientTransform="scale(0.26458334)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4555"
id="linearGradient837"
gradientUnits="userSpaceOnUse"
x1="98.37323"
y1="80.037163"
x2="132.22571"
y2="124.80769"
gradientTransform="translate(0.21652862,-1.4950989)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4555"
id="linearGradient842"
x1="26.029924"
y1="31.875429"
x2="148.82074"
y2="154.66602"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(3.0621348)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.2219349"
inkscape:cx="68.62512"
inkscape:cy="261.78697"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
showborder="false"
inkscape:window-width="2560"
inkscape:window-height="1034"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:object-nodes="false"
inkscape:snap-smooth-nodes="false"
inkscape:object-paths="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(53.98708,-31.875429)">
<path
id="path1014"
d="m 130.43648,59.620133 -74.572192,3.962548 -7.597984,42.012419 10.291879,10.29188 -16.295647,2.88458 35.894448,35.89445 H 154.94506 V 84.128715 Z"
style="fill:none;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
inkscape:connector-curvature="0" />
<g
id="g888"
transform="translate(-83.079173)"
inkscape:export-filename="/home/kbumsik/Dropbox/Projects/VirtScreen/virtscreen/icon/full.png"
inkscape:export-xdpi="52.955105"
inkscape:export-ydpi="52.955105">
<rect
y="31.875429"
x="29.092093"
height="122.79081"
width="122.79081"
id="rect834"
style="opacity:1;fill:url(#linearGradient842);fill-opacity:1;stroke:none;stroke-width:6.35238171;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" />
<path
id="path1009"
d="m 130.43647,59.61999 -74.572189,3.962548 -7.597988,42.012422 10.29188,10.29188 -16.295648,2.88458 35.89445,35.89445 H 151.88271 V 81.066226 Z"
style="fill:url(#linearGradient1068);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
inkscape:connector-curvature="0" />
<g
id="g877">
<g
id="g865">
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8.24230289;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m 55.14522,57.599343 c -3.811016,0 -6.878886,3.518771 -6.878886,7.890732 V 105.59529 H 58.610348 V 67.196961 h 63.754312 v 38.398329 h 10.34401 V 65.490075 c 0,-4.371961 -3.06787,-7.890732 -6.87889,-7.890732 z"
id="path1016"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8.42327976;stroke-linejoin:bevel;stroke-miterlimit:3.20000005;stroke-dasharray:none;stroke-opacity:1"
d="m 39.826996,110.15428 v 2.68424 c 0,4.48975 3.421591,8.10409 7.671187,8.10409 h 85.979207 c 4.2496,0 7.67061,-3.61434 7.67061,-8.10409 v -2.68424 z"
id="path1018"
inkscape:connector-curvature="0" />
</g>
<g
id="g869">
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:7.00793839;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1020"
width="41.864029"
height="53.648178"
x="96.424461"
y="75.294144"
ry="4.7652273" />
<rect
style="opacity:1;fill:url(#linearGradient837);fill-opacity:1;stroke:none;stroke-width:5.28829765;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1022"
width="30.171503"
height="42.388714"
x="102.27074"
y="80.923874"
ry="2.2456675e-14" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

180
data/systray_icon.svg Normal file
View file

@ -0,0 +1,180 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="104.16939mm"
height="104.16939mm"
viewBox="0 0 104.16939 104.16939"
version="1.1"
id="svg8"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="systray_icon.svg">
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient1066">
<stop
style="stop-color:#6f8a91;stop-opacity:1;"
offset="0"
id="stop1062" />
<stop
style="stop-color:#6f8a91;stop-opacity:0;"
offset="1"
id="stop1064" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4555">
<stop
style="stop-color:#a4b5b9;stop-opacity:1"
offset="0"
id="stop4551" />
<stop
style="stop-color:#8ba1a6;stop-opacity:1"
offset="1"
id="stop4553" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1066"
id="linearGradient1068"
x1="145.00168"
y1="321.47601"
x2="683.82404"
y2="482.69577"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-109.95437,-120.47406)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4555"
id="linearGradient837"
gradientUnits="userSpaceOnUse"
x1="98.37323"
y1="80.037163"
x2="132.22571"
y2="124.80769"
gradientTransform="translate(0.21652862,-1.4950989)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4555"
id="linearGradient842"
x1="26.029924"
y1="31.875429"
x2="148.82074"
y2="154.66602"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.8483484,0,0,0.8483484,16.320331,14.144671)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.2219349"
inkscape:cx="-145.53325"
inkscape:cy="226.5969"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
showborder="false"
inkscape:window-width="2560"
inkscape:window-height="1034"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:object-nodes="false"
inkscape:snap-smooth-nodes="false"
inkscape:object-paths="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-nodes="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-38.402805,-41.186142)">
<path
id="path1014"
d="m 130.43648,59.620133 -74.572192,3.962548 -7.597984,42.012419 10.291879,10.29188 -16.295647,2.88458 35.894448,35.89445 H 154.94506 V 84.128715 Z"
style="fill:none;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
inkscape:connector-curvature="0" />
<g
id="g953">
<rect
y="41.186142"
x="38.402805"
height="104.16939"
width="104.16939"
id="rect834"
style="opacity:1;fill:url(#linearGradient842);fill-opacity:1;stroke:none;stroke-width:5.38903284;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" />
<path
id="path1009"
transform="matrix(0.26458333,0,0,0.26458333,29.092093,31.875429)"
d="M 383.0332,104.86133 101.18555,119.83789 72.46875,278.625 111.36719,317.52344 49.777344,328.42578 150.25195,428.90039 H 428.90039 V 150.72852 Z"
style="fill:url(#linearGradient1068);fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
inkscape:connector-curvature="0" />
<g
id="g877">
<g
id="g865">
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8.24230289;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m 55.14522,57.599343 c -3.811016,0 -6.878886,3.518771 -6.878886,7.890732 V 105.59529 H 58.610348 V 67.196961 h 63.754312 v 38.398329 h 10.34401 V 65.490075 c 0,-4.371961 -3.06787,-7.890732 -6.87889,-7.890732 z"
id="path1016"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8.42327976;stroke-linejoin:bevel;stroke-miterlimit:3.20000005;stroke-dasharray:none;stroke-opacity:1"
d="m 39.826996,110.15428 v 2.68424 c 0,4.48975 3.421591,8.10409 7.671187,8.10409 h 85.979207 c 4.2496,0 7.67061,-3.61434 7.67061,-8.10409 v -2.68424 z"
id="path1018"
inkscape:connector-curvature="0" />
</g>
<g
id="g869">
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:7.00793839;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1020"
width="41.864029"
height="53.648178"
x="96.424461"
y="75.294144"
ry="4.7652273" />
<rect
style="opacity:1;fill:url(#linearGradient837);fill-opacity:1;stroke:none;stroke-width:5.28829765;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1022"
width="30.171503"
height="42.388714"
x="102.27074"
y="80.923874"
ry="2.2456675e-14" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

View file

@ -1,4 +0,0 @@
#!/bin/sh
# Script to run virtscreen locally
# This is not intended to be included in the distributed package
virtscreen/virtscreen.py

2
package/appimage/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.AppImage
*.AppDir

14
package/appimage/AppRun Executable file
View file

@ -0,0 +1,14 @@
#!/bin/sh
# This script is only for isolated miniconda environment
# Used in AppImage package
SCRIPTDIR=$(dirname $0)
ENV=$SCRIPTDIR/usr/share/virtscreen/env
export PYTHONPATH=$ENV/lib/python3.6
export LD_LIBRARY_PATH=$ENV/lib
export QT_PLUGIN_PATH=$ENV/lib/python3.6/site-packages/PyQt5/Qt/plugins
export QML2_IMPORT_PATH=$ENV/lib/python3.6/site-packages/PyQt5/Qt/qml
# export QT_QPA_FONTDIR=/usr/share/fonts
# export QT_XKB_CONFIG_ROOT=/usr/share/X11/xkb
$ENV/bin/python3 $ENV/bin/virtscreen $@

32
package/appimage/build.sh Executable file
View file

@ -0,0 +1,32 @@
#!/bin/bash
# Directory
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ROOT=$DIR/../..
cd $ROOT/package/appimage
mkdir virtscreen.AppDir
cd virtscreen.AppDir
# Create virtualenv
install -d usr/share/virtscreen
source $HOME/miniconda/bin/activate && \
conda create -y --copy --prefix usr/share/virtscreen/env python=3.6
# Install VirtScreen using pip
source $HOME/miniconda/bin/activate && \
source activate usr/share/virtscreen/env && \
pip install $ROOT
# Delete unnecessary installed files done by setup.py
rm -rf usr/share/virtscreen/env/lib/python3.6/site-packages/usr
# Copy desktop entry, icon, and AppRun
install -m 644 -D $ROOT/virtscreen.desktop \
usr/share/applications/virtscreen.desktop
install -m 644 -D $ROOT/virtscreen.desktop \
.
install -m 644 -D $ROOT/data/virtscreen.png \
usr/share/pixmaps/virtscreen.png
install -m 644 -D $ROOT/data/virtscreen.png \
.
install -m 755 -D $ROOT/package/appimage/AppRun \
.
cd ..
appimagetool virtscreen.AppDir

View file

@ -1,22 +0,0 @@
pkgbase = virtscreen
pkgdesc = Make your iPad/tablet/computer as a secondary monitor on Linux
pkgver = 0.1.3
pkgrel = 1
url = https://github.com/kbumsik/VirtScreen
arch = i686
arch = x86_64
license = GPL
makedepends = python-setuptools
depends = xorg-xrandr
depends = x11vnc
depends = python-pyqt5
depends = python-twisted
depends = python-netifaces
depends = python-qt5reactor
optdepends = arandr: for display settings option
provides = virtscreen
source = https://github.com/kbumsik/VirtScreen/archive/0.1.3.tar.gz
sha256sums = 79cd7a07fc5eb9d6034812cca39612cb1cbef109bd2c8e939a45e2186a82cac2
pkgname = virtscreen

View file

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

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

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

28
package/debian/Makefile Normal file
View file

@ -0,0 +1,28 @@
prefix = /usr
all:
: # do nothing
SHELL = /bin/bash
install:
mkdir -p $(DESTDIR)$(prefix)/bin
install -m 755 VirtScreen.AppImage \
$(DESTDIR)$(prefix)/bin/virtscreen
# Copy desktop entry and icon
install -m 644 -D virtscreen.desktop \
$(DESTDIR)$(prefix)/share/applications/virtscreen.desktop
install -m 644 -D data/virtscreen.png \
$(DESTDIR)$(prefix)/share/pixmaps/virtscreen.png
clean:
: # do nothing
distclean: clean
uninstall:
: # do nothing
# override_dh_usrlocal:
# : # do nothing
.PHONY: all install clean distclean uninstall

View file

@ -1,40 +0,0 @@
prefix = /usr
all:
: # do nothing
SHELL = /bin/bash
install:
# Create virtualenv
install -d $(DESTDIR)$(prefix)/share/virtscreen
virtualenv $(DESTDIR)$(prefix)/share/virtscreen/env --always-copy
source $(HOME)/miniconda/bin/activate && \
conda create -y --copy --prefix $(DESTDIR)$(prefix)/share/virtscreen/env python=3.6
source $(HOME)/miniconda/bin/activate && \
source activate $(DESTDIR)$(prefix)/share/virtscreen/env && \
pip install .
# Fix hashbang and move executable
sed -i "1s:.*:#!$(prefix)/share/virtscreen/env/bin/python3:" \
$(DESTDIR)$(prefix)/share/virtscreen/env/bin/virtscreen
install -D $(DESTDIR)$(prefix)/share/virtscreen/env/bin/virtscreen \
$(DESTDIR)$(prefix)/bin/virtscreen
# Delete unnecessary installed files done by setup.py
rm -rf $(DESTDIR)$(prefix)/share/virtscreen/env/lib/python3.6/site-packages/usr
# Copy desktop entry and icon
install -m 644 -D data/virtscreen.desktop \
$(DESTDIR)$(prefix)/share/applications/virtscreen.desktop
install -m 644 -D data/virtscreen.png \
$(DESTDIR)$(prefix)/share/pixmaps/virtscreen.png
clean:
: # do nothing
distclean: clean
uninstall:
: # do nothing
# override_dh_usrlocal:
# : # do nothing
.PHONY: all install clean distclean uninstall

View file

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

44
package/debian/build.sh Executable file
View file

@ -0,0 +1,44 @@
#!/bin/bash
PKGVER=0.3.1
# Required for debmake
DEBEMAIL="k.bumsik@gmail.com"
DEBFULLNAME="Bumsik Kim"
export PKGVER DEBEMAIL DEBFULLNAME
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ROOT=$SCRIPT_DIR/../..
# Generate necessary files for package building (generated by debmake)
cd $ROOT/package/debian
cp $ROOT/virtscreen-$PKGVER.tar.gz .
tar -xzmf virtscreen-$PKGVER.tar.gz
cp $ROOT/package/debian/Makefile \
$ROOT/package/debian/virtscreen-$PKGVER/Makefile
cd $ROOT/package/debian/virtscreen-$PKGVER
debmake --yes -b':sh'
# copy files to build
# debmake files
mkdir -p $ROOT/package/debian/build
cp -R $ROOT/package/debian/virtscreen-$PKGVER/debian \
$ROOT/package/debian/build/debian
cp $ROOT/package/debian/Makefile \
$ROOT/package/debian/build/
cp $ROOT/package/debian/{control,README.Debian} \
$ROOT/package/debian/build/debian/
# binary and data files
cp $ROOT/package/appimage/VirtScreen.AppImage \
$ROOT/package/debian/build/
cp $ROOT/virtscreen.desktop \
$ROOT/package/debian/build/
cp -R $ROOT/data \
$ROOT/package/debian/build/
# Build .deb package
cd $ROOT/package/debian/build
dpkg-buildpackage -b
# cleanup
rm -rf $ROOT/package/debian/virtscreen-$PKGVER \
$ROOT/package/debian/*.tar.gz

View file

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

View file

@ -2,15 +2,15 @@ Source: virtscreen
Section: utils Section: utils
Priority: optional Priority: optional
Maintainer: Bumsik Kim <k.bumsik@gmail.com> Maintainer: Bumsik Kim <k.bumsik@gmail.com>
Build-Depends: debhelper (>=9), dh-python, python3-all Build-Depends: debhelper (>=9), python3-all
Standards-Version: 3.9.8 Standards-Version: 3.9.8
Homepage: https://github.com/kbumsik/VirtScreen Homepage: https://github.com/kbumsik/VirtScreen
X-Python3-Version: >= 3.6 X-Python3-Version: >= 3.5
Package: virtscreen Package: virtscreen
Architecture: all Architecture: all
Multi-Arch: foreign Multi-Arch: foreign
Depends: ${misc:Depends}, ${python3:Depends}, x11vnc, python3-pyqt5, qtbase5-dev, python3-twisted, python3-netifaces Depends: ${misc:Depends}, x11vnc
Description: Make your iPad/tablet/computer as a secondary monitor on Linux Description: Make your iPad/tablet/computer as a secondary monitor on Linux
VirtScreen is an easy-to-use Linux GUI app that creates a virtual VirtScreen is an easy-to-use Linux GUI app that creates a virtual
secondary screen and shares it through VNC. secondary screen and shares it through VNC.

View file

@ -1,16 +0,0 @@
Source: virtscreen
Section: utils
Priority: optional
Maintainer: Bumsik Kim <k.bumsik@gmail.com>
Build-Depends: debhelper (>=9), python3-all
Standards-Version: 3.9.8
Homepage: https://github.com/kbumsik/VirtScreen
X-Python3-Version: >= 3.5
Package: virtscreen
Architecture: all
Multi-Arch: foreign
Depends: ${misc:Depends}, x11vnc, qtbase5-dev
Description: Make your iPad/tablet/computer as a secondary monitor on Linux
VirtScreen is an easy-to-use Linux GUI app that creates a virtual
secondary screen and shares it through VNC.

View file

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

View file

@ -1,20 +0,0 @@
#!/bin/bash
source _common.sh
mkdir build
cd build
# Download
wget https://github.com/kbumsik/VirtScreen/archive/$PKGVER.tar.gz
tar -xzmf $PKGVER.tar.gz
# rename packages
mv VirtScreen-$PKGVER virtscreen-$PKGVER
mv $PKGVER.tar.gz virtscreen-$PKGVER.tar.gz
cd virtscreen-$PKGVER
if [ $1 = "virtualenv" ]; then
cp ../../Makefile.virtualenv Makefile
debmake -b':sh'
else
debmake -b':py3'
fi

View file

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

View file

@ -1,12 +0,0 @@
#!/usr/bin/make -f
# You must remove unused comment lines for the released package.
export DH_VERBOSE = 1
%:
dh $@ --with python3 --buildsystem=pybuild
#override_dh_auto_install:
# dh_auto_install -- prefix=/usr
#override_dh_install:
# dh_install --list-missing -X.pyc -X.pyo

2
package/pypi/.gitignore vendored Normal file
View file

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

View file

@ -40,7 +40,7 @@ setup(
# For a discussion on single-sourcing the version across setup.py and the # For a discussion on single-sourcing the version across setup.py and the
# project code, see # project code, see
# https://packaging.python.org/en/latest/single_source_version.html # https://packaging.python.org/en/latest/single_source_version.html
version='0.2.0', # Required version='0.3.1', # Required
# This is a one-line description or tagline of what your project does. This # This is a one-line description or tagline of what your project does. This
# corresponds to the "Summary" metadata field: # corresponds to the "Summary" metadata field:
@ -107,8 +107,8 @@ setup(
# Specify the Python versions you support here. In particular, ensure # Specify the Python versions you support here. In particular, ensure
# that you indicate whether you support Python 2, Python 3 or both. # that you indicate whether you support Python 2, Python 3 or both.
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
# Environment # Environment
'Environment :: X11 Applications', 'Environment :: X11 Applications',
@ -116,8 +116,7 @@ setup(
'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: Linux',
# Framework used # Framework used
'Framework :: Twisted', 'Framework :: AsyncIO',
# 'Framework :: AsyncIO',
], ],
# This field adds keywords for your project which will appear on the # This field adds keywords for your project which will appear on the
@ -136,7 +135,7 @@ setup(
# #
# py_modules=["my_module"], # py_modules=["my_module"],
# #
packages=['virtscreen'], # Required packages=find_packages(), # Required
# This field lists other packages that your project depends on to run. # This field lists other packages that your project depends on to run.
# Any package you put here will be installed by pip when your project is # Any package you put here will be installed by pip when your project is
@ -145,8 +144,7 @@ setup(
# For an analysis of "install_requires" vs pip's requirements files see: # For an analysis of "install_requires" vs pip's requirements files see:
# https://packaging.python.org/en/latest/requirements.html # https://packaging.python.org/en/latest/requirements.html
install_requires=['PyQt5>=5.10.1', install_requires=['PyQt5>=5.10.1',
'Twisted>=17.9.0', 'Quamash>=0.6.0',
'qt5reactor>=0.5',
'netifaces>=0.10.6'], # Optional 'netifaces>=0.10.6'], # Optional
# List additional groups of dependencies here (e.g. development # List additional groups of dependencies here (e.g. development
@ -180,7 +178,7 @@ setup(
data_files=[ data_files=[
# Desktop entries spec: # Desktop entries spec:
# https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/ # https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/
('share/applications', ['data/virtscreen.desktop']), ('share/applications', ['virtscreen.desktop']),
# $XDG_DATA_DIRS/icons # $XDG_DATA_DIRS/icons
# https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout # https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout
('share/icons', ['data/virtscreen.png']), ('share/icons', ['data/virtscreen.png']),
@ -196,7 +194,7 @@ setup(
# executes the function `main` from this package when invoked: # executes the function `main` from this package when invoked:
entry_points={ # Optional entry_points={ # Optional
'console_scripts': [ 'console_scripts': [
'virtscreen = virtscreen.virtscreen:main', 'virtscreen = virtscreen.__main__:main',
], ],
}, },

View file

@ -1,9 +1,8 @@
[Desktop Entry] [Desktop Entry]
Encoding=UTF-8
Type=Application Type=Application
Name=VirtScreen Name=VirtScreen
Comment=Make your iPad/tablet/computer as a secondary monitor on Linux Comment=Make your iPad/tablet/computer as a secondary monitor on Linux
Exec=bash -c "export PATH=$PATH:$HOME/.local/bin; virtscreen" Exec=bash -c "export PATH=\\$PATH:\\$HOME/.local/bin; virtscreen"
Icon=virtscreen Icon=virtscreen
Terminal=false Terminal=false
StartupNotify=false StartupNotify=false

View file

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

218
virtscreen/__main__.py Executable file
View file

@ -0,0 +1,218 @@
#!/usr/bin/python3
# Python standard packages
import sys
import os
import signal
import json
import shutil
import argparse
import logging
from logging.handlers import RotatingFileHandler
from typing import Callable
import asyncio
# Import OpenGL library for Nvidia driver
# https://github.com/Ultimaker/Cura/pull/131#issuecomment-176088664
import ctypes
from ctypes.util import find_library
ctypes.CDLL(find_library('GL'), ctypes.RTLD_GLOBAL)
from PyQt5.QtWidgets import QApplication
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt, QUrl
from quamash import QEventLoop
from .display import DisplayProperty
from .xrandr import XRandR
from .qt_backend import Backend, Cursor, Network
from .path import HOME_PATH, ICON_PATH, MAIN_QML_PATH, CONFIG_PATH, LOGGING_PATH
def error(*args, **kwargs) -> None:
"""Error printing"""
args = ('Error: ', *args)
print(*args, file=sys.stderr, **kwargs)
def main() -> None:
"""Start main program"""
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
description='Make your iPad/tablet/computer as a secondary monitor on Linux.\n\n'
'You can start VirtScreen in the following two modes:\n\n'
' - GUI mode: A system tray icon will appear when no argument passed.\n'
' You need to use this first to configure a virtual screen.\n'
' - CLI mode: After configured the virtual screen, you can start VirtScreen\n'
' in CLI mode if you do not want a GUI, by passing any arguments\n',
epilog='example:\n'
'virtscreen # GUI mode. You need to use this first\n'
' to configure the screen\n'
'virtscreen --auto # CLI mode. Scrren will be created using previous\n'
' settings (from both GUI mode and CLI mode)\n'
'virtscreen --left # CLI mode. On the left to the primary monitor\n'
'virtscreen --below # CLI mode. Below the primary monitor.\n'
'virtscreen --below --portrait # Below, and portrait mode.\n'
'virtscreen --below --portrait --hipdi # Below, portrait, HiDPI mode.\n')
parser.add_argument('--auto', action='store_true',
help='create a virtual screen automatically using previous\n'
'settings (from both GUI mode and CLI mode)')
parser.add_argument('--left', action='store_true',
help='a virtual screen will be created left to the primary\n'
'monitor')
parser.add_argument('--right', action='store_true',
help='right to the primary monitor')
parser.add_argument('--above', '--up', action='store_true',
help='above the primary monitor')
parser.add_argument('--below', '--down', action='store_true',
help='below the primary monitor')
parser.add_argument('--portrait', action='store_true',
help='Portrait mode. Width and height of the screen are swapped')
parser.add_argument('--hidpi', action='store_true',
help='HiDPI mode. Width and height are doubled')
parser.add_argument('--log', type=str,
help='Python logging level, For example, --log=INFO.\n'
'Only used for reporting bugs and debugging')
# Add signal handler
def on_exit(self, signum=None, frame=None):
sys.exit(0)
for sig in [signal.SIGINT, signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]:
signal.signal(sig, on_exit)
args = vars(parser.parse_args())
cli_args = ['auto', 'left', 'right', 'above', 'below', 'portrait', 'hidpi']
# Start main
if any((value and arg in cli_args) for arg, value in args.items()):
main_cli(args)
else:
main_gui(args)
error('Program should not reach here.')
sys.exit(1)
def check_env(args: argparse.Namespace, msg: Callable[[str], None]) -> None:
"""Check environments and arguments before start. This also enable logging"""
if os.environ.get('XDG_SESSION_TYPE', '').lower() == 'wayland':
msg("Currently Wayland is not supported")
sys.exit(1)
# Check ~/.config/virtscreen
if not HOME_PATH: # This is set in path.py
msg("Cannot detect home directory.")
sys.exit(1)
if not os.path.exists(HOME_PATH):
try:
os.makedirs(HOME_PATH)
except:
msg("Cannot create ~/.config/virtscreen")
sys.exit(1)
# Check x11vnc
if not shutil.which('x11vnc'):
msg("x11vnc is not installed.")
sys.exit(1)
# Enable logging
if args['log'] is None:
args['log'] = 'WARNING'
log_level = getattr(logging, args['log'].upper(), None)
if not isinstance(log_level, int):
error('Please choose a correct python logging level')
sys.exit(1)
# When logging level is INFO or lower, print logs in terminal
# Otherwise log to a file
log_to_file = True if log_level > logging.INFO else False
FORMAT = "[%(levelname)s:%(filename)s:%(lineno)s:%(funcName)s()] %(message)s"
logging.basicConfig(level=log_level, format=FORMAT,
**({'filename': LOGGING_PATH} if log_to_file else {}))
if log_to_file:
logger = logging.getLogger()
handler = RotatingFileHandler(LOGGING_PATH, mode='a', maxBytes=1024*4, backupCount=1)
logger.addHandler(handler)
logging.info('logging enabled')
del args['log']
logging.info(f'{args}')
# Check if xrandr is correctly parsed.
try:
test = XRandR()
except RuntimeError as e:
msg(str(e))
sys.exit(1)
def main_gui(args: argparse.Namespace):
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop)
# Check environment first
from PyQt5.QtWidgets import QMessageBox, QSystemTrayIcon
def dialog(message: str) -> None:
QMessageBox.critical(None, "VirtScreen", message)
if not QSystemTrayIcon.isSystemTrayAvailable():
dialog("Cannot detect system tray on this system.")
sys.exit(1)
check_env(args, dialog)
app.setApplicationName("VirtScreen")
app.setWindowIcon(QIcon(ICON_PATH))
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
# Register the Python type. Its URI is 'People', it's v1.0 and the type
# will be called 'Person' in QML.
qmlRegisterType(DisplayProperty, 'VirtScreen.DisplayProperty', 1, 0, 'DisplayProperty')
qmlRegisterType(Backend, 'VirtScreen.Backend', 1, 0, 'Backend')
qmlRegisterType(Cursor, 'VirtScreen.Cursor', 1, 0, 'Cursor')
qmlRegisterType(Network, 'VirtScreen.Network', 1, 0, 'Network')
# Create a component factory and load the QML script.
engine = QQmlApplicationEngine()
engine.load(QUrl(MAIN_QML_PATH))
if not engine.rootObjects():
dialog("Failed to load QML")
sys.exit(1)
sys.exit(app.exec_())
with loop:
loop.run_forever()
def main_cli(args: argparse.Namespace):
loop = asyncio.get_event_loop()
# Check the environment
check_env(args, print)
if not os.path.exists(CONFIG_PATH):
error("Configuration file does not exist.\n"
"Configure a virtual screen using GUI first.")
sys.exit(1)
# By instantiating the backend, additional verifications of config
# file will be done.
backend = Backend(logger=print)
# Get settings
with open(CONFIG_PATH, 'r') as f:
config = json.load(f)
# Override settings from arguments
position = ''
if not args['auto']:
args_virt = ['portrait', 'hidpi']
for prop in args_virt:
if args[prop]:
config['virt'][prop] = True
args_position = ['left', 'right', 'above', 'below']
tmp_args = {k: args[k] for k in args_position}
if not any(tmp_args.values()):
error("Choose a position relative to the primary monitor. (e.g. --left)")
sys.exit(1)
for key, value in tmp_args.items():
if value:
position = key
# Create virtscreen and Start VNC
def handle_error(msg):
error(msg)
sys.exit(1)
backend.onError.connect(handle_error)
backend.createVirtScreen(config['virt']['device'], config['virt']['width'],
config['virt']['height'], config['virt']['portrait'],
config['virt']['hidpi'], position)
def handle_vnc_changed(state):
if state is backend.VNCState.OFF:
sys.exit(0)
backend.onVncStateChanged.connect(handle_vnc_changed)
backend.startVNC(config['vnc']['port'])
loop.run_forever()
if __name__ == '__main__':
main()

View file

@ -23,6 +23,10 @@ ApplicationWindow {
property int margin: 10 property int margin: 10
property int popupWidth: width - 26 property int popupWidth: width - 26
screen: Qt.application.screens[0]
x: screen.virtualX
y: screen.virtualY
// hide screen when loosing focus // hide screen when loosing focus
property bool autoClose: true property bool autoClose: true
property bool ignoreCloseOnce: false property bool ignoreCloseOnce: false
@ -46,7 +50,7 @@ ApplicationWindow {
menuBar: ToolBar { menuBar: ToolBar {
id: toolbar id: toolbar
font.weight: Font.Medium font.weight: Font.Medium
font.pointSize: 11 //parent.font.pointSize + 1 font.pixelSize: height * 0.34
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@ -60,7 +64,7 @@ ApplicationWindow {
ToolButton { ToolButton {
id: menuButton id: menuButton
anchors.right: parent.right Layout.alignment: Qt.AlignRight
text: qsTr("⋮") text: qsTr("⋮")
contentItem: Text { contentItem: Text {
text: parent.text text: parent.text
@ -150,35 +154,38 @@ ApplicationWindow {
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
Text { Text {
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
font { weight: Font.Bold; pointSize: 15 } font { weight: Font.Bold; pixelSize: 20 }
text: "VirtScreen" + " v" + settings.version text: "VirtScreen" + " v" + settings.version
} }
Text { Text {
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
font { pixelSize: 13 }
text: "Make your iPad/tablet/computer<br/>as a secondary monitor.<br/>" text: "Make your iPad/tablet/computer<br/>as a secondary monitor.<br/>"
} }
Text { Text {
font { pixelSize: 14 }
text: "- <a href='https://github.com/kbumsik/VirtScreen'>Project Website</a>" text: "- <a href='https://github.com/kbumsik/VirtScreen'>Project Website</a>"
onLinkActivated: Qt.openUrlExternally(link) onLinkActivated: Qt.openUrlExternally(link)
} }
Text { Text {
font { pixelSize: 14 }
text: "- <a href='https://github.com/kbumsik/VirtScreen/issues'>Issues & Bug Report</a>" text: "- <a href='https://github.com/kbumsik/VirtScreen/issues'>Issues & Bug Report</a>"
onLinkActivated: Qt.openUrlExternally(link) onLinkActivated: Qt.openUrlExternally(link)
} }
Text { Text {
font { pointSize: 10 } font { pixelSize: 14 }
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
lineHeight: 0.7 lineHeight: 0.7
text: "<br/>Copyright © 2018 Bumsik Kim <a href='https://kbumsik.io/'>Homepage</a><br/>" text: "<br/>Copyright © 2018 Bumsik Kim <a href='https://kbumsik.io/'>Homepage</a><br/>"
onLinkActivated: Qt.openUrlExternally(link) onLinkActivated: Qt.openUrlExternally(link)
} }
Text { Text {
font { pointSize: 9 } font { pixelSize: 11 }
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: "This program comes with absolutely no warranty.<br/>" + text: "This program comes with absolutely no warranty.<br/>" +
"See the <a href='https://github.com/kbumsik/VirtScreen/blob/master/LICENSE'>" + "See the <a href='https://github.com/kbumsik/VirtScreen/blob/master/LICENSE'>" +
@ -202,8 +209,7 @@ ApplicationWindow {
TextField { TextField {
id: passwordFIeld id: passwordFIeld
focus: true focus: true
anchors.left: parent.left Layout.fillWidth: true
anchors.right: parent.right
placeholderText: "New Password"; placeholderText: "New Password";
echoMode: TextInput.Password; echoMode: TextInput.Password;
} }
@ -234,7 +240,8 @@ ApplicationWindow {
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
ScrollView { ScrollView {
anchors.fill: parent Layout.fillHeight: true
Layout.fillWidth: true
TextArea { TextArea {
// readOnly: true // readOnly: true
selectByMouse: true selectByMouse: true

View file

@ -52,11 +52,12 @@ Dialog {
} }
Text { Text {
font { pointSize: 10 } Layout.fillWidth: true
font { pixelSize: 14 }
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: "<b>Warning</b>: Edit only if 'VIRTUAL1' is not available<br/>" + text: "<b>Warning</b>: Edit only if 'VIRTUAL1' is not available. " +
"If so, please note that the virtual screen may be<br/>" + "If so, please note that the virtual screen may be " +
"unstable/unavailable depending on a graphic<br/>" + "unstable/unavailable depending on a graphic " +
"card and its driver." "card and its driver."
} }

View file

@ -6,7 +6,7 @@ import VirtScreen.Backend 1.0
ColumnLayout { ColumnLayout {
GroupBox { GroupBox {
title: "Virtual Display" title: "Virtual Screen"
Layout.fillWidth: true Layout.fillWidth: true
enabled: backend.virtScreenCreated ? false : true enabled: backend.virtScreenCreated ? false : true
ColumnLayout { ColumnLayout {

View file

@ -12,7 +12,7 @@ Dialog {
x: (window.width - width) / 2 x: (window.width - width) / 2
y: (window.width - height) / 2 y: (window.width - height) / 2
width: popupWidth width: popupWidth
height: 300 height: 350
Component.onCompleted: { Component.onCompleted: {
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();
@ -33,7 +33,27 @@ Dialog {
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
RowLayout {
TextField {
id: vncCustomArgsTextField
enabled: vncCustomArgsCheckbox.checked
Layout.fillWidth: true
placeholderText: "Custom x11vnc arguments"
onTextEdited: {
settings.customX11vncArgs.value = text;
}
text: vncCustomArgsCheckbox.checked ? settings.customX11vncArgs.value : ""
}
CheckBox {
id: vncCustomArgsCheckbox
checked: settings.customX11vncArgs.enabled
onToggled: {
settings.customX11vncArgs.enabled = checked;
}
}
}
ColumnLayout {
enabled: !vncCustomArgsCheckbox.checked
Repeater { Repeater {
id: vncOptionsRepeater id: vncOptionsRepeater
RowLayout { RowLayout {
@ -50,7 +70,7 @@ Dialog {
} }
} }
} }
}
RowLayout { RowLayout {
// Empty layout // Empty layout
Layout.fillHeight: true Layout.fillHeight: true

View file

@ -3,8 +3,14 @@ import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import VirtScreen.Backend 1.0 import VirtScreen.Backend 1.0
import VirtScreen.Network 1.0
ColumnLayout { ColumnLayout {
// virtscreen.py Network interfaces backend.
Network {
id: network
}
GroupBox { GroupBox {
title: "VNC Server" title: "VNC Server"
Layout.fillWidth: true Layout.fillWidth: true
@ -27,8 +33,6 @@ ColumnLayout {
} }
} }
RowLayout { RowLayout {
anchors.left: parent.left
anchors.right: parent.right
Label { text: "Password"; Layout.fillWidth: true } Label { text: "Password"; Layout.fillWidth: true }
Button { Button {
text: "Delete" text: "Delete"
@ -84,9 +88,8 @@ ColumnLayout {
GroupBox { GroupBox {
title: "Available IP addresses" title: "Available IP addresses"
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: 145 implicitHeight: 145
ColumnLayout {
anchors.fill: parent
ListView { ListView {
id: ipListView id: ipListView
anchors.fill: parent anchors.fill: parent
@ -98,14 +101,13 @@ ColumnLayout {
anchors.bottom: ipListView.bottom anchors.bottom: ipListView.bottom
policy: ScrollBar.AlwaysOn policy: ScrollBar.AlwaysOn
} }
model: backend.ipAddresses model: network.ipAddresses
delegate: TextEdit { delegate: TextEdit {
text: modelData text: modelData
readOnly: true readOnly: true
selectByMouse: true selectByMouse: true
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
font.pointSize: 12 font.pixelSize: 14
}
} }
} }
} }

View file

@ -1,5 +1,5 @@
{ {
"version": "0.2.0", "version": "0.3.1",
"x11vncVersion": "0.9.15", "x11vncVersion": "0.9.15",
"theme_color": 8, "theme_color": 8,
"virt": { "virt": {
@ -31,5 +31,9 @@
"arg": null "arg": null
} }
}, },
"customX11vncArgs": {
"enabled": false,
"value": ""
},
"presets": [] "presets": []
} }

View file

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

View file

@ -4,12 +4,18 @@ import Qt.labs.platform 1.0
import VirtScreen.DisplayProperty 1.0 import VirtScreen.DisplayProperty 1.0
import VirtScreen.Backend 1.0 import VirtScreen.Backend 1.0
import VirtScreen.Cursor 1.0
Item { Item {
property alias window: mainLoader.item property alias window: mainLoader.item
property var settings: JSON.parse(backend.settings) property var settings: JSON.parse(backend.settings)
property bool autostart: settings.vnc.autostart property bool autostart: settings.vnc.autostart
function saveSettings () {
settings.vnc.autostart = autostart;
backend.settings = JSON.stringify(settings, null, 4);
}
function createVirtScreen () { function createVirtScreen () {
backend.createVirtScreen(settings.virt.device, settings.virt.width, backend.createVirtScreen(settings.virt.device, settings.virt.width,
settings.virt.height, settings.virt.portrait, settings.virt.height, settings.virt.portrait,
@ -17,18 +23,8 @@ Item {
} }
function startVNC () { function startVNC () {
var options = ''; saveSettings();
var data = settings.x11vncOptions; backend.startVNC(settings.vnc.port);
for (var key in data) {
if(data[key].available && data[key].enabled) {
options += key + ' ';
if(data[key].arg !== null) {
options += data[key].arg.toString() + ' ';
}
}
}
console.log('options: ', options);
backend.startVNC(settings.vnc.port, options);
} }
function stopVNC () { function stopVNC () {
@ -61,6 +57,11 @@ Item {
} }
} }
// virtscreen.py Cursor class.
Cursor {
id: cursor
}
// Timer object and function // Timer object and function
Timer { Timer {
id: timer id: timer
@ -109,17 +110,14 @@ Item {
} }
}); });
// Move window to the corner of the primary display // Move window to the corner of the primary display
var primary = backend.primary; var cursor_x = (cursor.x / window.screen.devicePixelRatio) - window.screen.virtualX;
var width = primary.width; var cursor_y = (cursor.y / window.screen.devicePixelRatio) - window.screen.virtualY;
var height = primary.height; var x_mid = window.screen.width / 2;
var cursor_x = backend.cursor_x - primary.x_offset; var y_mid = window.screen.height / 2;
var cursor_y = backend.cursor_y - primary.y_offset; var x = window.screen.width - window.width; //(cursor_x > x_mid)? width - window.width : 0;
var x_mid = width / 2; var y = (cursor_y > y_mid)? window.screen.height - window.height : 0;
var y_mid = height / 2; x += window.screen.virtualX;
var x = width - window.width; //(cursor_x > x_mid)? width - window.width : 0; y += window.screen.virtualY;
var y = (cursor_y > y_mid)? height - window.height : 0;
x += primary.x_offset;
y += primary.y_offset;
window.x = x; window.x = x;
window.y = y; window.y = y;
window.show(); window.show();
@ -131,9 +129,9 @@ Item {
// Sytray Icon // Sytray Icon
SystemTrayIcon { SystemTrayIcon {
id: sysTrayIcon id: sysTrayIcon
iconSource: backend.vncState == Backend.CONNECTED ? "../icon/icon_tablet_on.png" : iconSource: backend.vncState == Backend.CONNECTED ? "../icon/systray_tablet_on.png" :
backend.virtScreenCreated ? "../icon/icon_tablet_off.png" : backend.virtScreenCreated ? "../icon/systray_tablet_off.png" :
"../icon/icon.png" "../icon/systray_no_tablet.png"
visible: true visible: true
property bool clicked: false property bool clicked: false
@ -231,8 +229,7 @@ Item {
id: quitAction id: quitAction
text: qsTr("&Quit") text: qsTr("&Quit")
onTriggered: { onTriggered: {
settings.vnc.autostart = autostart; saveSettings();
backend.settings = JSON.stringify(settings, null, 4);
backend.quitProgram(); backend.quitProgram();
} }
} }

108
virtscreen/display.py Normal file
View file

@ -0,0 +1,108 @@
"""Display information data classes"""
from PyQt5.QtCore import QObject, pyqtProperty
class Display(object):
"""Display information"""
__slots__ = ['name', 'primary', 'connected', 'active', 'width', 'height',
'x_offset', 'y_offset']
def __init__(self):
self.name: str = None
self.primary: bool = False
self.connected: bool = False
self.active: bool = False
self.width: int = 0
self.height: int = 0
self.x_offset: int = 0
self.y_offset: int = 0
def __str__(self) -> str:
ret = f"{self.name}"
if self.connected:
ret += " connected"
else:
ret += " disconnected"
if self.primary:
ret += " primary"
if self.active:
ret += f" {self.width}x{self.height}+{self.x_offset}+{self.y_offset}"
else:
ret += f" not active {self.width}x{self.height}"
return ret
class DisplayProperty(QObject):
"""Wrapper around Display class for Qt"""
def __init__(self, display: Display, parent=None):
super(DisplayProperty, self).__init__(parent)
self._display = display
@property
def display(self):
return self._display
@pyqtProperty(str, constant=True)
def name(self):
return self._display.name
@name.setter
def name(self, name):
self._display.name = name
@pyqtProperty(bool, constant=True)
def primary(self):
return self._display.primary
@primary.setter
def primary(self, primary):
self._display.primary = primary
@pyqtProperty(bool, constant=True)
def connected(self):
return self._display.connected
@connected.setter
def connected(self, connected):
self._display.connected = connected
@pyqtProperty(bool, constant=True)
def active(self):
return self._display.active
@active.setter
def active(self, active):
self._display.active = active
@pyqtProperty(int, constant=True)
def width(self):
return self._display.width
@width.setter
def width(self, width):
self._display.width = width
@pyqtProperty(int, constant=True)
def height(self):
return self._display.height
@height.setter
def height(self, height):
self._display.height = height
@pyqtProperty(int, constant=True)
def x_offset(self):
return self._display.x_offset
@x_offset.setter
def x_offset(self, x_offset):
self._display.x_offset = x_offset
@pyqtProperty(int, constant=True)
def y_offset(self):
return self._display.y_offset
@y_offset.setter
def y_offset(self, y_offset):
self._display.y_offset = y_offset

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

38
virtscreen/path.py Normal file
View file

@ -0,0 +1,38 @@
"""File path definitions"""
import os
from pathlib import Path
# Sanitize environment variables
# https://wiki.sei.cmu.edu/confluence/display/c/ENV03-C.+Sanitize+the+environment+when+invoking+external+programs
# Setting home path
# Rewrite $HOME env for consistency. This will make
# Path.home() to look up in the password directory (pwd module)
try:
os.environ['HOME'] = str(Path.home())
# os.environ['PATH'] = os.confstr("CS_PATH") # Sanitize $PATH, Deleted by Issue #19.
# https://www.freedesktop.org/software/systemd/man/file-hierarchy.html
# HOME_PATH will point to ~/.config/virtscreen by default
if ('XDG_CONFIG_HOME' in os.environ) and len(os.environ['XDG_CONFIG_HOME']):
HOME_PATH = os.environ['XDG_CONFIG_HOME']
else:
HOME_PATH = os.environ['HOME'] + '/.config'
HOME_PATH = HOME_PATH + "/virtscreen"
except OSError:
HOME_PATH = '' # This will be checked in _main_.py.
# Setting base path
BASE_PATH = os.path.dirname(__file__) # Location of this script
# Path in ~/.virtscreen
X11VNC_LOG_PATH = HOME_PATH + "/x11vnc_log.txt"
X11VNC_PASSWORD_PATH = HOME_PATH + "/x11vnc_passwd"
CONFIG_PATH = HOME_PATH + "/config.json"
LOGGING_PATH = HOME_PATH + "/log.txt"
# Path in the program path
ICON_PATH = BASE_PATH + "/icon/full_256x256.png"
ASSETS_PATH = BASE_PATH + "/assets"
DATA_PATH = ASSETS_PATH + "/data.json"
DEFAULT_CONFIG_PATH = ASSETS_PATH + "/config.default.json"
MAIN_QML_PATH = ASSETS_PATH + "/main.qml"

100
virtscreen/process.py Normal file
View file

@ -0,0 +1,100 @@
"""Subprocess wrapper"""
import subprocess
import asyncio
import signal
import shlex
import os
import logging
class SubprocessWrapper:
"""Subprocess wrapper class"""
def __init__(self):
pass
def check_output(self, arg) -> None:
return subprocess.check_output(shlex.split(arg), stderr=subprocess.STDOUT).decode('utf-8')
def run(self, arg: str, input: str = None, check=False) -> str:
if input:
input = input.encode('utf-8')
return subprocess.run(shlex.split(arg), input=input, stdout=subprocess.PIPE,
check=check, stderr=subprocess.STDOUT).stdout.decode('utf-8')
class _Protocol(asyncio.SubprocessProtocol):
"""SubprocessProtocol implementation"""
def __init__(self, outer):
self.outer = outer
self.transport: asyncio.SubprocessTransport
def connection_made(self, transport):
logging.info("connectionMade!")
self.outer.connected()
self.transport = transport
transport.get_pipe_transport(0).close() # No more input
def pipe_data_received(self, fd, data):
if fd == 1: # stdout
self.outer.out_recevied(data)
if self.outer.logfile is not None:
self.outer.logfile.write(data)
elif fd == 2: # stderr
self.outer.err_recevied(data)
if self.outer.logfile is not None:
self.outer.logfile.write(data)
def pipe_connection_lost(self, fd, exc):
if fd == 0: # stdin
logging.info("stdin is closed. (we probably did it)")
elif fd == 1: # stdout
logging.info("The child closed their stdout.")
elif fd == 2: # stderr
logging.info("The child closed their stderr.")
def connection_lost(self, exc):
logging.info("Subprocess connection lost.")
def process_exited(self):
if self.outer.logfile is not None:
self.outer.logfile.close()
self.transport.close()
return_code = self.transport.get_returncode()
if return_code is None:
logging.error("Unknown exit")
self.outer.ended(1)
return
logging.info(f"processEnded, status {return_code}")
self.outer.ended(return_code)
class AsyncSubprocess():
"""Asynchronous subprocess wrapper class"""
def __init__(self, connected, out_recevied, err_recevied, ended, logfile=None):
self.connected = connected
self.out_recevied = out_recevied
self.err_recevied = err_recevied
self.ended = ended
self.logfile = logfile
self.transport: asyncio.SubprocessTransport
self.protocol: _Protocol
async def _run(self, arg: str, loop: asyncio.AbstractEventLoop):
self.transport, self.protocol = await loop.subprocess_exec(
lambda: _Protocol(self), *shlex.split(arg), env=os.environ)
def run(self, arg: str):
"""Spawn a process.
Arguments:
arg {str} -- arguments in string
"""
loop = asyncio.get_event_loop()
loop.create_task(self._run(arg, loop))
def close(self):
"""Kill a spawned process."""
self.transport.send_signal(signal.SIGINT)

364
virtscreen/qt_backend.py Normal file
View file

@ -0,0 +1,364 @@
"""GUI backend"""
import json
import re
import subprocess
import os
import shutil
import atexit
import time
import logging
from typing import Callable
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
from PyQt5.QtGui import QCursor
from PyQt5.QtQml import QQmlListProperty
from PyQt5.QtWidgets import QApplication
from netifaces import interfaces, ifaddresses, AF_INET
from .display import DisplayProperty
from .xrandr import XRandR
from .process import AsyncSubprocess, SubprocessWrapper
from .path import (DATA_PATH, CONFIG_PATH, DEFAULT_CONFIG_PATH,
X11VNC_PASSWORD_PATH, X11VNC_LOG_PATH)
class Backend(QObject):
""" Backend class for QML frontend """
class VNCState:
""" Enum to indicate a state of the VNC server """
OFF = 0
ERROR = 1
WAITING = 2
CONNECTED = 3
Q_ENUMS(VNCState)
# Signals
onVirtScreenCreatedChanged = pyqtSignal(bool)
onVncUsePasswordChanged = pyqtSignal(bool)
onVncStateChanged = pyqtSignal(VNCState)
onDisplaySettingClosed = pyqtSignal()
onError = pyqtSignal(str)
def __init__(self, parent=None, logger=logging.info, error_logger=logging.error):
super(Backend, self).__init__(parent)
# Virtual screen properties
self.xrandr: XRandR = XRandR()
self._virtScreenCreated: bool = False
# VNC server properties
self._vncUsePassword: bool = False
self._vncState: self.VNCState = self.VNCState.OFF
# Primary screen and mouse posistion
self.vncServer: AsyncSubprocess
# Info/error logger
self.log: Callable[[str], None] = logger
self.log_error: Callable[[str], None] = error_logger
# Check config file
# and initialize if needed
need_init = False
if not os.path.exists(CONFIG_PATH):
shutil.copy(DEFAULT_CONFIG_PATH, CONFIG_PATH)
need_init = True
# Version check
file_match = True
with open(CONFIG_PATH, 'r') as f_config, open(DATA_PATH, 'r') as f_data:
config = json.load(f_config)
data = json.load(f_data)
if config['version'] != data['version']:
file_match = False
# Override config with default when version doesn't match
if not file_match:
shutil.copy(DEFAULT_CONFIG_PATH, CONFIG_PATH)
need_init = True
# initialize config file
if need_init:
# 1. Available x11vnc options
# Get available x11vnc options from x11vnc first
p = SubprocessWrapper()
arg = 'x11vnc -opts'
ret = p.run(arg)
options = tuple(m.group(1) for m in re.finditer(r"\s*(-\w+)\s+", ret))
# Set/unset available x11vnc options flags in config
with open(CONFIG_PATH, 'r') as f, open(DATA_PATH, 'r') as f_data:
config = json.load(f)
data = json.load(f_data)
for key, value in config["x11vncOptions"].items():
if key in options:
value["available"] = True
else:
value["available"] = False
# Default Display settings app for a Desktop Environment
desktop_environ = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
for key, value in data['displaySettingApps'].items():
if desktop_environ in value['XDG_CURRENT_DESKTOP']:
config["displaySettingApp"] = key
# Save the new config
with open(CONFIG_PATH, 'w') as f:
f.write(json.dumps(config, indent=4, sort_keys=True))
def promptError(self, msg):
self.log_error(msg)
self.onError.emit(msg)
# Qt properties
@pyqtProperty(str, constant=True)
def settings(self):
with open(CONFIG_PATH, "r") as f:
return f.read()
@settings.setter
def settings(self, json_str):
with open(CONFIG_PATH, "w") as f:
f.write(json_str)
@pyqtProperty(bool, notify=onVirtScreenCreatedChanged)
def virtScreenCreated(self):
return self._virtScreenCreated
@virtScreenCreated.setter
def virtScreenCreated(self, value):
self._virtScreenCreated = value
self.onVirtScreenCreatedChanged.emit(value)
@pyqtProperty(QQmlListProperty, constant=True)
def screens(self):
try:
return QQmlListProperty(DisplayProperty, self, [DisplayProperty(x) for x in self.xrandr.screens])
except RuntimeError as e:
self.promptError(str(e))
return QQmlListProperty(DisplayProperty, self, [])
@pyqtProperty(bool, notify=onVncUsePasswordChanged)
def vncUsePassword(self):
if os.path.isfile(X11VNC_PASSWORD_PATH):
self._vncUsePassword = True
else:
if self._vncUsePassword:
self.vncUsePassword = False
return self._vncUsePassword
@vncUsePassword.setter
def vncUsePassword(self, use):
self._vncUsePassword = use
self.onVncUsePasswordChanged.emit(use)
@pyqtProperty(VNCState, notify=onVncStateChanged)
def vncState(self):
return self._vncState
@vncState.setter
def vncState(self, state):
self._vncState = state
self.onVncStateChanged.emit(self._vncState)
# Qt Slots
@pyqtSlot(str, int, int, bool, bool)
def createVirtScreen(self, device, width, height, portrait, hidpi, pos=''):
self.xrandr.virt_name = device
self.log("Creating a Virtual Screen...")
try:
self.xrandr.create_virtual_screen(width, height, portrait, hidpi, pos)
except subprocess.CalledProcessError as e:
self.promptError(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
return
except RuntimeError as e:
self.promptError(str(e))
return
self.virtScreenCreated = True
self.log("The Virtual Screen successfully created.")
@pyqtSlot()
def deleteVirtScreen(self):
self.log("Deleting the Virtual Screen...")
if self.vncState is not self.VNCState.OFF:
self.promptError("Turn off the VNC server first")
self.virtScreenCreated = True
return
try:
self.xrandr.delete_virtual_screen()
except RuntimeError as e:
self.promptError(str(e))
return
self.virtScreenCreated = False
@pyqtSlot(str)
def createVNCPassword(self, password):
if password:
password += '\n' + password + '\n\n' # verify + confirm
p = SubprocessWrapper()
try:
p.run(f"x11vnc -storepasswd {X11VNC_PASSWORD_PATH}", input=password, check=True)
except subprocess.CalledProcessError as e:
self.promptError(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
return
self.vncUsePassword = True
else:
self.promptError("Empty password")
@pyqtSlot()
def deleteVNCPassword(self):
if os.path.isfile(X11VNC_PASSWORD_PATH):
os.remove(X11VNC_PASSWORD_PATH)
self.vncUsePassword = False
else:
self.promptError("Failed deleting the password file")
@pyqtSlot(int)
def startVNC(self, port):
# Check if a virtual screen created
if not self.virtScreenCreated:
self.promptError("Virtual Screen not crated.")
return
if self.vncState is not self.VNCState.OFF:
self.promptError("VNC Server is already running.")
return
# regex used in callbacks
patter_connected = re.compile(r"^.*Got connection from client.*$", re.M)
patter_disconnected = re.compile(r"^.*client_count: 0*$", re.M)
# define callbacks
def _connected():
self.log(f"VNC started. Now connect a VNC client to port {port}.")
self.vncState = self.VNCState.WAITING
def _received(data):
data = data.decode("utf-8")
if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data):
self.log("VNC connected.")
self.vncState = self.VNCState.CONNECTED
if (self._vncState is self.VNCState.CONNECTED) and patter_disconnected.search(data):
self.log("VNC disconnected.")
self.vncState = self.VNCState.WAITING
def _ended(exitCode):
if exitCode is not 0:
self.vncState = self.VNCState.ERROR
self.promptError('X11VNC: Error occurred.\n'
'Double check if the port is already used.')
self.vncState = self.VNCState.OFF # TODO: better handling error state
else:
self.vncState = self.VNCState.OFF
self.log("VNC Exited.")
atexit.unregister(self.stopVNC)
# load settings
with open(CONFIG_PATH, 'r') as f:
config = json.load(f)
options = ''
if config['customX11vncArgs']['enabled']:
options = config['customX11vncArgs']['value']
else:
for key, value in config['x11vncOptions'].items():
if value['available'] and value['enabled']:
options += key + ' '
if value['arg'] is not None:
options += str(value['arg']) + ' '
# Sart x11vnc, turn settings object into VNC arguments format
logfile = open(X11VNC_LOG_PATH, "wb")
self.vncServer = AsyncSubprocess(_connected, _received, _received, _ended, logfile)
try:
virt = self.xrandr.get_virtual_screen()
except RuntimeError as e:
self.promptError(str(e))
return
clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}"
arg = f"x11vnc -rfbport {port} -clip {clip} {options}"
if self.vncUsePassword:
arg += f" -rfbauth {X11VNC_PASSWORD_PATH}"
self.vncServer.run(arg)
# auto stop on exit
atexit.register(self.stopVNC, force=True)
@pyqtSlot(str)
def openDisplaySetting(self, app: str = "arandr"):
# define callbacks
def _connected():
self.log("External Display Setting opened.")
def _received(data):
pass
def _ended(exitCode):
self.log("External Display Setting closed.")
self.onDisplaySettingClosed.emit()
if exitCode is not 0:
self.promptError(f'Error opening "{running_program}".')
with open(DATA_PATH, 'r') as f:
data = json.load(f)['displaySettingApps']
if app not in data:
self.promptError('Wrong display settings program')
return
program_list = [data[app]['args'], "arandr"]
program = AsyncSubprocess(_connected, _received, _received, _ended, None)
running_program = ''
for arg in program_list:
if not shutil.which(arg.split()[0]):
continue
running_program = arg
program.run(arg)
return
self.promptError('Failed to find a display settings program.\n'
'Please install ARandR package.\n'
'(e.g. sudo apt-get install arandr)\n'
'Please issue a feature request\n'
'if you wish to add a display settings\n'
'program for your Desktop Environment.')
@pyqtSlot()
def stopVNC(self, force=False):
if force:
# Usually called from atexit().
self.vncServer.close()
time.sleep(3) # Make sure X11VNC shutdown before execute next atexit().
if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED):
self.vncServer.close()
else:
self.promptError("stopVNC called while it is not running")
@pyqtSlot()
def clearCache(self):
# engine.clearComponentCache()
pass
@pyqtSlot()
def quitProgram(self):
self.blockSignals(True) # This will prevent invoking auto-restart or etc.
QApplication.instance().quit()
class Cursor(QObject):
""" Global mouse cursor position """
def __init__(self, parent=None):
super(Cursor, self).__init__(parent)
@pyqtProperty(int)
def x(self):
cursor = QCursor().pos()
return cursor.x()
@pyqtProperty(int)
def y(self):
cursor = QCursor().pos()
return cursor.y()
class Network(QObject):
""" Backend class for network interfaces """
onIPAddressesChanged = pyqtSignal()
def __init__(self, parent=None):
super(Network, self).__init__(parent)
@pyqtProperty('QStringList', notify=onIPAddressesChanged)
def ipAddresses(self):
for interface in interfaces():
if interface == 'lo':
continue
addresses = ifaddresses(interface).get(AF_INET, None)
if addresses is None:
continue
for link in addresses:
if link is not None:
yield link['addr']

View file

@ -1,764 +0,0 @@
#!/usr/bin/python3
# Python standard packages
import sys, os, subprocess, signal, re, atexit, time, json, shutil
from pathlib import Path
from enum import Enum
from typing import List, Dict
# PyQt5 packages
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
from PyQt5.QtGui import QIcon, QCursor
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine, QQmlListProperty
# Twisted and netifaces
from twisted.internet import protocol, error
from netifaces import interfaces, ifaddresses, AF_INET
# -------------------------------------------------------------------------------
# file path definitions
# -------------------------------------------------------------------------------
# Sanitize environment variables
# https://wiki.sei.cmu.edu/confluence/display/c/ENV03-C.+Sanitize+the+environment+when+invoking+external+programs
# Delete $HOME env for security reason. This will make
# Path.home() to look up in the password directory (pwd module)
if 'HOME' in os.environ:
del os.environ['HOME']
os.environ['HOME'] = str(Path.home())
os.environ['PATH'] = os.confstr("CS_PATH") # Sanitize $PATH
# Setting home path and base path
# https://www.freedesktop.org/software/systemd/man/file-hierarchy.html
# HOME_PATH will point to ~/.config/virtscreen by default
if 'XDG_CONFIG_HOME' in os.environ and os.environ['XDG_CONFIG_HOME']:
HOME_PATH = os.environ['XDG_CONFIG_HOME']
else:
HOME_PATH = os.environ['HOME']
if HOME_PATH is not None:
HOME_PATH = HOME_PATH + "/.config"
if HOME_PATH is not None:
HOME_PATH = HOME_PATH + "/virtscreen"
BASE_PATH = os.path.dirname(__file__)
# Path in ~/.virtscreen
X11VNC_LOG_PATH = HOME_PATH + "/x11vnc_log.txt"
X11VNC_PASSWORD_PATH = HOME_PATH + "/x11vnc_passwd"
CONFIG_PATH = HOME_PATH + "/config.json"
# Path in the program path
ICON_PATH = BASE_PATH + "/icon/icon.png"
ASSETS_PATH = BASE_PATH + "/assets"
DATA_PATH = ASSETS_PATH + "/data.json"
DEFAULT_CONFIG_PATH = ASSETS_PATH + "/config.default.json"
MAIN_QML_PATH = ASSETS_PATH + "/main.qml"
# -------------------------------------------------------------------------------
# Subprocess wrapper
# -------------------------------------------------------------------------------
class SubprocessWrapper:
def __init__(self):
pass
def check_output(self, arg) -> None:
return subprocess.check_output(arg.split(), stderr=subprocess.STDOUT).decode('utf-8')
def run(self, arg: str, input: str = None, check=False) -> str:
if input:
input = input.encode('utf-8')
return subprocess.run(arg.split(), input=input, stdout=subprocess.PIPE,
check=check, stderr=subprocess.STDOUT).stdout.decode('utf-8')
# -------------------------------------------------------------------------------
# Twisted class
# -------------------------------------------------------------------------------
class ProcessProtocol(protocol.ProcessProtocol):
def __init__(self, onConnected, onOutReceived, onErrRecevied, onProcessEnded, logfile=None):
self.onConnected = onConnected
self.onOutReceived = onOutReceived
self.onErrRecevied = onErrRecevied
self.onProcessEnded = onProcessEnded
self.logfile = logfile
# We cannot import this at the top of the file because qt5reactor should
# be installed in the main function first.
from twisted.internet import reactor # pylint: disable=E0401
self.reactor = reactor
def run(self, arg: str):
"""Spawn a process
Arguments:
arg {str} -- arguments in string
"""
args = arg.split()
self.reactor.spawnProcess(self, args[0], args=args, env=os.environ)
def kill(self):
"""Kill a spawned process
"""
self.transport.signalProcess('INT')
def connectionMade(self):
print("connectionMade!")
self.onConnected()
self.transport.closeStdin() # No more input
def outReceived(self, data):
# print("outReceived! with %d bytes!" % len(data))
self.onOutReceived(data)
if self.logfile is not None:
self.logfile.write(data)
def errReceived(self, data):
# print("errReceived! with %d bytes!" % len(data))
self.onErrRecevied(data)
if self.logfile is not None:
self.logfile.write(data)
def inConnectionLost(self):
print("inConnectionLost! stdin is closed! (we probably did it)")
pass
def outConnectionLost(self):
print("outConnectionLost! The child closed their stdout!")
pass
def errConnectionLost(self):
print("errConnectionLost! The child closed their stderr.")
pass
def processExited(self, reason):
exitCode = reason.value.exitCode
if exitCode is None:
print("Unknown exit")
return
print("processEnded, status", exitCode)
def processEnded(self, reason):
if self.logfile is not None:
self.logfile.close()
exitCode = reason.value.exitCode
if exitCode is None:
print("Unknown exit")
self.onProcessEnded(1)
return
print("processEnded, status", exitCode)
print("quitting")
self.onProcessEnded(exitCode)
# -------------------------------------------------------------------------------
# Display properties
# -------------------------------------------------------------------------------
class Display(object):
__slots__ = ['name', 'primary', 'connected', 'active', 'width', 'height', 'x_offset', 'y_offset']
def __init__(self):
self.name: str = None
self.primary: bool = False
self.connected: bool = False
self.active: bool = False
self.width: int = 0
self.height: int = 0
self.x_offset: int = 0
self.y_offset: int = 0
def __str__(self):
ret = f"{self.name}"
if self.connected:
ret += " connected"
else:
ret += " disconnected"
if self.primary:
ret += " primary"
if self.active:
ret += f" {self.width}x{self.height}+{self.x_offset}+{self.y_offset}"
else:
ret += f" not active {self.width}x{self.height}"
return ret
class DisplayProperty(QObject):
def __init__(self, display: Display, parent=None):
super(DisplayProperty, self).__init__(parent)
self._display = display
@property
def display(self):
return self._display
@pyqtProperty(str, constant=True)
def name(self):
return self._display.name
@name.setter
def name(self, name):
self._display.name = name
@pyqtProperty(bool, constant=True)
def primary(self):
return self._display.primary
@primary.setter
def primary(self, primary):
self._display.primary = primary
@pyqtProperty(bool, constant=True)
def connected(self):
return self._display.connected
@connected.setter
def connected(self, connected):
self._display.connected = connected
@pyqtProperty(bool, constant=True)
def active(self):
return self._display.active
@active.setter
def active(self, active):
self._display.active = active
@pyqtProperty(int, constant=True)
def width(self):
return self._display.width
@width.setter
def width(self, width):
self._display.width = width
@pyqtProperty(int, constant=True)
def height(self):
return self._display.height
@height.setter
def height(self, height):
self._display.height = height
@pyqtProperty(int, constant=True)
def x_offset(self):
return self._display.x_offset
@x_offset.setter
def x_offset(self, x_offset):
self._display.x_offset = x_offset
@pyqtProperty(int, constant=True)
def y_offset(self):
return self._display.y_offset
@y_offset.setter
def y_offset(self, y_offset):
self._display.y_offset = y_offset
# -------------------------------------------------------------------------------
# Screen adjustment class
# -------------------------------------------------------------------------------
class XRandR(SubprocessWrapper):
VIRT_SCREEN_SUFFIX = "_virt"
def __init__(self):
super(XRandR, self).__init__()
self.mode_name: str
self.screens: List[Display] = []
self.virt: Display() = None
self.primary: Display() = None
self.virt_name: str = ''
self.virt_idx: int = None
self.primary_idx: int = None
# Primary display
self._update_screens()
def _update_screens(self) -> None:
output = self.run("xrandr")
self.primary = None
self.virt = None
self.screens = []
self.virt_idx = None
self.primary_idx = None
pattern = re.compile(r"^(\S*)\s+(connected|disconnected)\s+((primary)\s+)?"
r"((\d+)x(\d+)\+(\d+)\+(\d+)\s+)?.*$", re.M)
for idx, match in enumerate(pattern.finditer(output)):
screen = Display()
screen.name = match.group(1)
if self.virt_name and screen.name == self.virt_name:
self.virt_idx = idx
screen.primary = True if match.group(4) else False
if screen.primary:
self.primary_idx = idx
screen.connected = True if match.group(2) == "connected" else False
screen.active = True if match.group(5) else False
self.screens.append(screen)
if not screen.active:
continue
screen.width = int(match.group(6))
screen.height = int(match.group(7))
screen.x_offset = int(match.group(8))
screen.y_offset = int(match.group(9))
print("Display information:")
for s in self.screens:
print("\t", s)
if self.primary_idx is None:
raise RuntimeError("There is no primary screen detected.\n"
"Go to display settings and set\n"
"a primary screen\n")
if self.virt_idx == self.primary_idx:
raise RuntimeError("Virtual screen must be selected other than the primary screen")
if self.virt_idx is not None:
self.virt = self.screens[self.virt_idx]
elif self.virt_name and self.virt_idx is None:
raise RuntimeError("No virtual screen name found")
self.primary = self.screens[self.primary_idx]
def _add_screen_mode(self, width, height, portrait, hidpi) -> None:
if not self.virt or not self.virt_name:
raise RuntimeError("No virtual screen selected.\n"
"Go to Display->Virtual Display->Advaced\n"
"To select a device.")
# Set virtual screen property first
self.virt.width = width
self.virt.height = height
if portrait:
self.virt.width = height
self.virt.height = width
if hidpi:
self.virt.width *= 2
self.virt.height *= 2
self.mode_name = str(self.virt.width) + "x" + str(self.virt.height) + self.VIRT_SCREEN_SUFFIX
# Then create using xrandr command
args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}"
try:
self.check_output(args_addmode)
except subprocess.CalledProcessError:
# When failed create mode and then add again
output = self.run(f"cvt {self.virt.width} {self.virt.height}")
mode = re.search(r"^.*Modeline\s*\".*\"\s*(.*)$", output, re.M).group(1)
# Create new screen mode
self.check_output(f"xrandr --newmode {self.mode_name} {mode}")
# Add mode again
self.check_output(args_addmode)
# After adding mode the program should delete the mode automatically on exit
atexit.register(self.delete_virtual_screen)
for sig in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]:
signal.signal(sig, self._signal_handler)
def _signal_handler(self, signum=None, frame=None) -> None:
self.delete_virtual_screen()
os._exit(0)
def get_primary_screen(self) -> Display:
self._update_screens()
return self.primary
def get_virtual_screen(self) -> Display:
self._update_screens()
return self.virt
def create_virtual_screen(self, width, height, portrait=False, hidpi=False) -> None:
print("creating: ", self.virt)
self._update_screens()
self._add_screen_mode(width, height, portrait, hidpi)
self.check_output(f"xrandr --output {self.virt.name} --mode {self.mode_name}")
self.check_output("sleep 5")
self.check_output(f"xrandr --output {self.virt.name} --preferred")
self._update_screens()
def delete_virtual_screen(self) -> None:
self._update_screens()
try:
self.virt.name
self.mode_name
except AttributeError:
return
self.run(f"xrandr --output {self.virt.name} --off")
self.run(f"xrandr --delmode {self.virt.name} {self.mode_name}")
atexit.unregister(self.delete_virtual_screen)
self._update_screens()
# -------------------------------------------------------------------------------
# QML Backend class
# -------------------------------------------------------------------------------
class Backend(QObject):
""" Backend class for QML frontend """
class VNCState:
""" Enum to indicate a state of the VNC server """
OFF = 0
ERROR = 1
WAITING = 2
CONNECTED = 3
Q_ENUMS(VNCState)
# Signals
onVirtScreenCreatedChanged = pyqtSignal(bool)
onVirtScreenIndexChanged = pyqtSignal(int)
onVncUsePasswordChanged = pyqtSignal(bool)
onVncStateChanged = pyqtSignal(VNCState)
onIPAddressesChanged = pyqtSignal()
onDisplaySettingClosed = pyqtSignal()
onError = pyqtSignal(str)
def __init__(self, parent=None):
super(Backend, self).__init__(parent)
# Virtual screen properties
self.xrandr: XRandR = XRandR()
self._virtScreenCreated: bool = False
# VNC server properties
self._vncUsePassword: bool = False
self._vncState: self.VNCState = self.VNCState.OFF
# Primary screen and mouse posistion
self._primaryProp: DisplayProperty
self.vncServer: ProcessProtocol
# Check config file
# and initialize if needed
need_init = False
if not os.path.exists(CONFIG_PATH):
shutil.copy(DEFAULT_CONFIG_PATH, CONFIG_PATH)
need_init = True
# Version check
file_match = True
with open(CONFIG_PATH, 'r') as f_config, open(DATA_PATH, 'r') as f_data:
config = json.load(f_config)
data = json.load(f_data)
if config['version'] != data['version']:
file_match = False
# Override config with default when version doesn't match
if not file_match:
shutil.copy(DEFAULT_CONFIG_PATH, CONFIG_PATH)
need_init = True
# initialize config file
if need_init:
# 1. Available x11vnc options
# Get available x11vnc options from x11vnc first
p = SubprocessWrapper()
arg = 'x11vnc -opts'
ret = p.run(arg)
options = tuple(m.group(1) for m in re.finditer("\s*(-\w+)\s+", ret))
# Set/unset available x11vnc options flags in config
with open(CONFIG_PATH, 'r') as f, open(DATA_PATH, 'r') as f_data:
config = json.load(f)
data = json.load(f_data)
for key, value in config["x11vncOptions"].items():
if key in options:
value["available"] = True
else:
value["available"] = False
# 2. Default Display settings app for a Desktop Environment
desktop_environ = os.environ['XDG_CURRENT_DESKTOP'].lower()
for key, value in data['displaySettingApps'].items():
for de in value['XDG_CURRENT_DESKTOP']:
if de in desktop_environ:
config["displaySettingApp"] = key
# Save the new config
with open(CONFIG_PATH, 'w') as f:
f.write(json.dumps(config, indent=4, sort_keys=True))
# Qt properties
@pyqtProperty(str, constant=True)
def settings(self):
with open(CONFIG_PATH, "r") as f:
return f.read()
@settings.setter
def settings(self, json_str):
with open(CONFIG_PATH, "w") as f:
f.write(json_str)
@pyqtProperty(bool, notify=onVirtScreenCreatedChanged)
def virtScreenCreated(self):
return self._virtScreenCreated
@virtScreenCreated.setter
def virtScreenCreated(self, value):
self._virtScreenCreated = value
self.onVirtScreenCreatedChanged.emit(value)
@pyqtProperty(QQmlListProperty, constant=True)
def screens(self):
try:
return QQmlListProperty(DisplayProperty, self, [DisplayProperty(x) for x in self.xrandr.screens])
except RuntimeError as e:
self.onError.emit(str(e))
return QQmlListProperty(DisplayProperty, self, [])
@pyqtProperty(bool, notify=onVncUsePasswordChanged)
def vncUsePassword(self):
if os.path.isfile(X11VNC_PASSWORD_PATH):
self._vncUsePassword = True
else:
if self._vncUsePassword:
self.vncUsePassword = False
return self._vncUsePassword
@vncUsePassword.setter
def vncUsePassword(self, use):
self._vncUsePassword = use
self.onVncUsePasswordChanged.emit(use)
@pyqtProperty(VNCState, notify=onVncStateChanged)
def vncState(self):
return self._vncState
@vncState.setter
def vncState(self, state):
self._vncState = state
self.onVncStateChanged.emit(self._vncState)
@pyqtProperty('QStringList', notify=onIPAddressesChanged)
def ipAddresses(self):
for interface in interfaces():
if interface == 'lo':
continue
addresses = ifaddresses(interface).get(AF_INET, None)
if addresses is None:
continue
for link in addresses:
if link is not None:
yield link['addr']
@pyqtProperty(DisplayProperty)
def primary(self):
try:
self._primaryProp = DisplayProperty(self.xrandr.get_primary_screen())
except RuntimeError as e:
self.onError.emit(str(e))
return
return self._primaryProp
@pyqtProperty(int)
def cursor_x(self):
cursor = QCursor().pos()
return cursor.x()
@pyqtProperty(int)
def cursor_y(self):
cursor = QCursor().pos()
return cursor.y()
# Qt Slots
@pyqtSlot(str, int, int, bool, bool)
def createVirtScreen(self, device, width, height, portrait, hidpi):
self.xrandr.virt_name = device
print("Creating a Virtual Screen...")
try:
self.xrandr.create_virtual_screen(width, height, portrait, hidpi)
except subprocess.CalledProcessError as e:
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
return
except RuntimeError as e:
self.onError.emit(str(e))
return
self.virtScreenCreated = True
@pyqtSlot()
def deleteVirtScreen(self):
print("Deleting the Virtual Screen...")
if self.vncState is not self.VNCState.OFF:
self.onError.emit("Turn off the VNC server first")
self.virtScreenCreated = True
return
try:
self.xrandr.delete_virtual_screen()
except RuntimeError as e:
self.onError.emit(str(e))
return
self.virtScreenCreated = False
@pyqtSlot(str)
def createVNCPassword(self, password):
if password:
password += '\n' + password + '\n\n' # verify + confirm
p = SubprocessWrapper()
try:
p.run(f"x11vnc -storepasswd {X11VNC_PASSWORD_PATH}", input=password, check=True)
except subprocess.CalledProcessError as e:
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
return
self.vncUsePassword = True
else:
self.onError.emit("Empty password")
@pyqtSlot()
def deleteVNCPassword(self):
if os.path.isfile(X11VNC_PASSWORD_PATH):
os.remove(X11VNC_PASSWORD_PATH)
self.vncUsePassword = False
else:
self.onError.emit("Failed deleting the password file")
@pyqtSlot(int, str)
def startVNC(self, port, options=''):
# Check if a virtual screen created
if not self.virtScreenCreated:
self.onError.emit("Virtual Screen not crated.")
return
if self.vncState is not self.VNCState.OFF:
self.onError.emit("VNC Server is already running.")
return
# regex used in callbacks
patter_connected = re.compile(r"^.*Got connection from client.*$", re.M)
patter_disconnected = re.compile(r"^.*client_count: 0*$", re.M)
# define callbacks
def _onConnected():
print("VNC started.")
self.vncState = self.VNCState.WAITING
def _onReceived(data):
data = data.decode("utf-8")
if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data):
print("VNC connected.")
self.vncState = self.VNCState.CONNECTED
if (self._vncState is self.VNCState.CONNECTED) and patter_disconnected.search(data):
print("VNC disconnected.")
self.vncState = self.VNCState.WAITING
def _onEnded(exitCode):
if exitCode is not 0:
self.vncState = self.VNCState.ERROR
self.onError.emit('X11VNC: Error occurred.\n'
'Double check if the port is already used.')
self.vncState = self.VNCState.OFF # TODO: better handling error state
else:
self.vncState = self.VNCState.OFF
print("VNC Exited.")
atexit.unregister(self.stopVNC)
logfile = open(X11VNC_LOG_PATH, "wb")
self.vncServer = ProcessProtocol(_onConnected, _onReceived, _onReceived, _onEnded, logfile)
try:
virt = self.xrandr.get_virtual_screen()
except RuntimeError as e:
self.onError.emit(str(e))
return
clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}"
arg = f"x11vnc -rfbport {port} -clip {clip} {options}"
if self.vncUsePassword:
arg += f" -rfbauth {X11VNC_PASSWORD_PATH}"
self.vncServer.run(arg)
# auto stop on exit
atexit.register(self.stopVNC, force=True)
@pyqtSlot(str)
def openDisplaySetting(self, app: str = "arandr"):
# define callbacks
def _onConnected():
print("External Display Setting opened.")
def _onReceived(data):
pass
def _onEnded(exitCode):
print("External Display Setting closed.")
self.onDisplaySettingClosed.emit()
if exitCode is not 0:
self.onError.emit(f'Error opening "{running_program}".')
with open(DATA_PATH, 'r') as f:
data = json.load(f)['displaySettingApps']
if app not in data:
self.onError.emit('Wrong display settings program')
return
program_list = [data[app]['args'], "arandr"]
program = ProcessProtocol(_onConnected, _onReceived, _onReceived, _onEnded, None)
running_program = ''
for arg in program_list:
if not shutil.which(arg.split()[0]):
continue
running_program = arg
program.run(arg)
return
self.onError.emit('Failed to find a display settings program.\n'
'Please install ARandR package.\n'
'(e.g. sudo apt-get install arandr)\n'
'Please issue a feature request\n'
'if you wish to add a display settings\n'
'program for your Desktop Environment.')
@pyqtSlot()
def stopVNC(self, force=False):
if force:
# Usually called from atexit().
self.vncServer.kill()
time.sleep(2) # Make sure X11VNC shutdown before execute next atexit().
if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED):
self.vncServer.kill()
else:
self.onError.emit("stopVNC called while it is not running")
@pyqtSlot()
def clearCache(self):
engine.clearComponentCache()
@pyqtSlot()
def quitProgram(self):
self.blockSignals(True) # This will prevent invoking auto-restart or etc.
QApplication.instance().quit()
# -------------------------------------------------------------------------------
# Main Code
# -------------------------------------------------------------------------------
def main():
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QApplication(sys.argv)
from PyQt5.QtWidgets import QSystemTrayIcon, QMessageBox
if not QSystemTrayIcon.isSystemTrayAvailable():
QMessageBox.critical(None, "VirtScreen",
"Cannot detect system tray on this system.")
sys.exit(1)
if os.environ['XDG_SESSION_TYPE'] == 'wayland':
QMessageBox.critical(None, "VirtScreen",
"Currently Wayland is not supported")
sys.exit(1)
if not HOME_PATH:
QMessageBox.critical(None, "VirtScreen",
"Cannot detect home directory.")
sys.exit(1)
if not os.path.exists(HOME_PATH):
try:
os.makedirs(HOME_PATH)
except:
QMessageBox.critical(None, "VirtScreen",
"Cannot create ~/.config/virtscreen")
sys.exit(1)
if not shutil.which('x11vnc'):
QMessageBox.critical(None, "VirtScreen",
"x11vnc is not installed.")
sys.exit(1)
try:
test = XRandR()
except RuntimeError as e:
QMessageBox.critical(None, "VirtScreen", str(e))
sys.exit(1)
# Replace Twisted reactor with qt5reactor
import qt5reactor # pylint: disable=E0401
qt5reactor.install()
from twisted.internet import reactor # pylint: disable=E0401
app.setWindowIcon(QIcon(ICON_PATH))
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
# os.environ["QT_QUICK_CONTROLS_STYLE"] = "Fusion"
# Register the Python type. Its URI is 'People', it's v1.0 and the type
# will be called 'Person' in QML.
qmlRegisterType(DisplayProperty, 'VirtScreen.DisplayProperty', 1, 0, 'DisplayProperty')
qmlRegisterType(Backend, 'VirtScreen.Backend', 1, 0, 'Backend')
# Create a component factory and load the QML script.
engine = QQmlApplicationEngine()
engine.load(QUrl(MAIN_QML_PATH))
if not engine.rootObjects():
QMessageBox.critical(None, "VirtScreen", "Failed to load QML")
sys.exit(1)
sys.exit(app.exec_())
reactor.run()
if __name__ == '__main__':
main()

139
virtscreen/xrandr.py Normal file
View file

@ -0,0 +1,139 @@
"""XRandr parser"""
import re
import atexit
import subprocess
import logging
from typing import List
from .display import Display
from .process import SubprocessWrapper
VIRT_SCREEN_SUFFIX = "_virt"
class XRandR(SubprocessWrapper):
"""XRandr parser class"""
def __init__(self):
super(XRandR, self).__init__()
self.mode_name: str
self.screens: List[Display] = []
self.virt: Display() = None
self.primary: Display() = None
self.virt_name: str = ''
self.virt_idx: int = None
self.primary_idx: int = None
# Primary display
self._update_screens()
def _update_screens(self) -> None:
output = self.run("xrandr")
self.primary = None
self.virt = None
self.screens = []
self.virt_idx = None
self.primary_idx = None
pattern = re.compile(r"^(\S*)\s+(connected|disconnected)\s+((primary)\s+)?"
r"((\d+)x(\d+)\+(\d+)\+(\d+)\s+)?.*$", re.M)
for idx, match in enumerate(pattern.finditer(output)):
screen = Display()
screen.name = match.group(1)
if self.virt_name and screen.name == self.virt_name:
self.virt_idx = idx
screen.primary = True if match.group(4) else False
if screen.primary:
self.primary_idx = idx
screen.connected = True if match.group(2) == "connected" else False
screen.active = True if match.group(5) else False
self.screens.append(screen)
if not screen.active:
continue
screen.width = int(match.group(6))
screen.height = int(match.group(7))
screen.x_offset = int(match.group(8))
screen.y_offset = int(match.group(9))
logging.info("Display information:")
for s in self.screens:
logging.info(f"\t{s}")
if self.primary_idx is None:
raise RuntimeError("There is no primary screen detected.\n"
"Go to display settings and set\n"
"a primary screen\n")
if self.virt_idx == self.primary_idx:
raise RuntimeError("Virtual screen must be selected other than the primary screen")
if self.virt_idx is not None:
self.virt = self.screens[self.virt_idx]
elif self.virt_name and self.virt_idx is None:
raise RuntimeError("No virtual screen name found")
self.primary = self.screens[self.primary_idx]
def _add_screen_mode(self, width, height, portrait, hidpi) -> None:
if not self.virt or not self.virt_name:
raise RuntimeError("No virtual screen selected.\n"
"Go to Display->Virtual Display->Advaced\n"
"To select a device.")
# Set virtual screen property first
self.virt.width = width
self.virt.height = height
if portrait:
self.virt.width = height
self.virt.height = width
if hidpi:
self.virt.width *= 2
self.virt.height *= 2
self.mode_name = str(self.virt.width) + "x" + str(self.virt.height) + VIRT_SCREEN_SUFFIX
# Then create using xrandr command
args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}"
try:
self.check_output(args_addmode)
except subprocess.CalledProcessError:
# When failed create mode and then add again
output = self.run(f"cvt {self.virt.width} {self.virt.height}")
mode = re.search(r"^.*Modeline\s*\".*\"\s*(.*)$", output, re.M).group(1)
# Create new screen mode
self.check_output(f"xrandr --newmode {self.mode_name} {mode}")
# Add mode again
self.check_output(args_addmode)
# After adding mode the program should delete the mode automatically on exit
atexit.register(self.delete_virtual_screen)
def get_primary_screen(self) -> Display:
self._update_screens()
return self.primary
def get_virtual_screen(self) -> Display:
self._update_screens()
return self.virt
def create_virtual_screen(self, width, height, portrait=False, hidpi=False, pos='') -> None:
self._update_screens()
logging.info(f"creating: {self.virt}")
self._add_screen_mode(width, height, portrait, hidpi)
arg_pos = ['left', 'right', 'above', 'below']
xrandr_pos = ['--left-of', '--right-of', '--above', '--below']
if pos and pos in arg_pos:
# convert pos for xrandr
pos = xrandr_pos[arg_pos.index(pos)]
pos += ' ' + self.primary.name
elif not pos:
pos = '--preferred'
else:
raise RuntimeError("Incorrect position option selected.")
self.check_output(f"xrandr --output {self.virt.name} --mode {self.mode_name}")
self.check_output("sleep 5")
self.check_output(f"xrandr --output {self.virt.name} {pos}")
self._update_screens()
def delete_virtual_screen(self) -> None:
self._update_screens()
try:
self.virt.name
self.mode_name
except AttributeError:
return
self.run(f"xrandr --output {self.virt.name} --off")
self.run(f"xrandr --delmode {self.virt.name} {self.mode_name}")
atexit.unregister(self.delete_virtual_screen)
self._update_screens()