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

Compare commits

...

86 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
Bumsik Kim
bae43c8fd2
Applied v0.2.0 in app data 2018-06-03 13:24:49 -04:00
Bumsik Kim
920fd56ea8
Deb for Ubuntu 16.04 is successful 2018-06-03 11:55:15 -04:00
Bumsik Kim
710864a44d
Moved virtual screen device to advanced 2018-05-31 19:53:17 -04:00
Bumsik Kim
7f3448a25e
Selectable x11vnc options in QML 2018-05-31 02:48:07 -04:00
Bumsik Kim
b2a54b7b87
Check if x11vnc is installed 2018-05-31 01:50:06 -04:00
Bumsik Kim
a106d380a2
Moved qml folder to assets 2018-05-31 01:25:18 -04:00
Bumsik Kim
34f8847f67
Preference: Selecting preferred display settings app 2018-05-30 18:36:23 -04:00
Bumsik Kim
6436dbd8ff
QML: open Display settings app using config file 2018-05-30 17:13:06 -04:00
Bumsik Kim
04290f76db
data.json: moved from data folder to qml 2018-05-30 14:51:40 -04:00
Bumsik Kim
0353e60f28
Auto-detect possible x11vnc options 2018-05-29 23:42:32 -04:00
Bumsik Kim
1ac430cdc0
AppWindow: Improved releiability of auto-closing window 2018-05-27 00:44:32 -04:00
Bumsik Kim
41c483764b
First complete Debian packaging system 2018-05-25 14:35:57 -04:00
Bumsik Kim
605f906371
Setup.py: Fixed wrong desktop entry path 2018-05-23 23:30:25 -04:00
Bumsik Kim
ca5af0235a
Fixed: 'StopVNC() called' prompt when clicking Display Setting button 2018-05-23 19:46:20 -04:00
Bumsik Kim
90894d3c41
v0.1.3 release 2018-05-23 19:40:15 -04:00
Bumsik Kim
b8caf68ea8
setup.py: install .desktop files, re-organized doc files to data 2018-05-23 18:49:20 -04:00
Bumsik Kim
be02f3dd4a
Added KDE display setting 2018-05-23 17:12:21 -04:00
Bumsik Kim
6db530301c
README: updated Ubuntu dependancies 2018-05-23 16:21:17 -04:00
Bumsik Kim
4e7d0f485a
Config: Changed HOME_PATH from ~/.virtscreen to ~/.config/virtscreen 2018-05-23 16:00:29 -04:00
Bumsik Kim
2bd1ce68f8
Deleted Snapcraft 2018-05-22 21:26:41 -04:00
Bumsik Kim
b8c2ff27c4
Experimental Debian building system 2018-05-22 21:26:17 -04:00
Bumsik Kim
11cc7add7b
Improved file organization 2018-05-21 18:51:50 -04:00
Bumsik Kim
395df777d1
launch.sh: fixed python binary to system-wide 2018-05-21 18:22:16 -04:00
Bumsik Kim
57b4094ec7
AUR: added missing pieces for 0.1.2 2018-05-21 18:21:33 -04:00
Bumsik Kim
38d8b902e5
Added .vscode to gitignore 2018-05-21 18:20:23 -04:00
Bumsik Kim
fbd0a3d1fd
Deleted .vscode 2018-05-21 18:16:53 -04:00
Bumsik Kim
d51bc3934f
Initial snap.yaml 2018-05-21 15:46:36 -04:00
Bumsik Kim
6c8394f594
AUR: updated to 0.1.2 2018-05-21 15:09:42 -04:00
Bumsik Kim
a88525fdd8
FIX: twisted reactor not defined 2018-05-21 14:40:17 -04:00
Bumsik Kim
46769793e7
AUR: use system-wide python 2018-05-21 06:01:19 -04:00
Bumsik Kim
78b33e9133
Updated AUR install in README 2018-05-21 04:54:18 -04:00
Bumsik Kim
277392e91e
Updated AUR package 2018-05-21 04:24:07 -04:00
54 changed files with 2222 additions and 919 deletions

14
.gitignore vendored
View file

@ -1,10 +1,20 @@
# Pycharm # IDE and text editor
.idea .idea
.vscode
# Compiled files from Qt # Compiled files from Qt
*.qmlc *.qmlc
*.xcf
# Python linter
.pylintrc
# files & folders for development use
debug
# Archive file
*.tar.gz
################################################################################
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]

View file

@ -1,4 +0,0 @@
[MASTER]
extension-pkg-whitelist=
PyQt5,
netifaces

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

1
.vscode/.gitignore vendored
View file

@ -1 +0,0 @@
tags

23
.vscode/launch.json vendored
View file

@ -1,23 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"pythonPath": "${config:python.pythonPath}"
},
{
"name": "Python: Terminal (integrated)",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"pythonPath": "${config:python.pythonPath}"
}
]
}

View file

@ -1,7 +0,0 @@
{
"python.venvPath": "${workspaceFolder}/ENV",
"[python]": {
"editor.formatOnSave": true
},
"python.pythonPath": "${workspaceFolder}/ENV/bin/python"
}

29
Dockerfile Normal file
View file

@ -0,0 +1,29 @@
# Or bionic
FROM ubuntu:bionic
LABEL author="Bumsik Kim <k.bumsik@gmail.com>"
RUN apt-get update && \
apt-get install -y python3-all python3-pip python3-wheel fakeroot debmake debhelper fakeroot wget tar curl && \
apt-get autoremove -y && \
ln /usr/bin/python3 /usr/bin/python && \
ln /usr/bin/pip3 /usr/bin/pip && \
rm -rf /var/cache/apt/archives/*.deb && \
pip install virtualenv && \
pip install --upgrade pip setuptools
# 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 && \
bash ~/miniconda.sh -b -p ~/miniconda && \
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

@ -3,3 +3,7 @@ include *.md
# Include the license file # Include the license file
include LICENSE.txt include LICENSE.txt
# Include data directories
include data/virtscreen.png
include virtscreen.desktop

136
Makefile
View file

@ -1,33 +1,131 @@
# 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
python setup.py bdist_wheel --universal PKG_DEBIAN=package/debian/virtscreen.deb
ARCHIVE=virtscreen-$(VERSION).tar.gz
python-install:
python setup.py install --user
pip-upload:
twine upload dist/*
.ONESHELL: .ONESHELL:
arch-update: .PHONY: run debug run-appimage debug-appimage
all: package/pypi/*.whl $(ARCHIVE) $(PKG_APPIMAGE) $(PKG_DEBIAN)
# Run script
run:
python3 -m virtscreen
debug:
QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 python3 -m virtscreen --log=DEBUG
run-appimage: $(PKG_APPIMAGE)
$<
debug-appimage: $(PKG_APPIMAGE)
QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 $< --log=DEBUG
# tar.gz
.PHONY: archive
archive $(ARCHIVE):
git archive --format=tar.gz --prefix=virtscreen-$(VERSION)/ -o $@ HEAD
# Docker tools
.PHONY: docker docker-build
docker:
$(DOCKER_RUN_TTY) /bin/bash
docker-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:
rm -rf package/debian/build package/debian/*.deb package/debian/*.buildinfo \
package/debian/*.changes
# For AUR: https://wiki.archlinux.org/index.php/Python_package_guidelines
# and: https://wiki.archlinux.org/index.php/Creating_packages
.PHONY: arch-upload arch-clean
arch-upload: package/archlinux/.SRCINFO
cd package/archlinux
git clone ssh://aur@aur.archlinux.org/virtscreen.git
cp PKGBUILD virtscreen
cp .SRCINFO virtscreen
cd virtscreen
git add --all
git commit
git push
cd ..
rm -rf virtscreen
package/archlinux/.SRCINFO:
cd package/archlinux cd package/archlinux
makepkg --printsrcinfo > .SRCINFO makepkg --printsrcinfo > .SRCINFO
arch-install: arch-update
cd package/archlinux
makepkg -si
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 override-version:
rm -rf build dist virtscreen.egg-info # 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)

121
README.md
View file

@ -1,49 +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://github.com/kbumsik/VirtScreen/blob/d2387d3321bd4d110d890ca87703196df203dc89/icon/gif_example.gif?raw=true) <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.
## Installation & running ## Features
### Installing dependancies * 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
You need [`x11vnc`](https://github.com/LibVNC/x11vnc) and `xrandr`. To install (example on Ubuntu): ## How to use
```bash
$ sudo apt-get install x11vnc
```
### Installing package 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.
#### Using `pip` ### CLI-only option
You can run VirtScreen with `virtscreen` (or `./VirtScreen.AppImage` if you use the AppImage package) with additional arguments.
```bash ```bash
$ pip install 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.
``` ```
#### From the Git repository directly ## 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)
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)
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
$ python setup.py install # add --user option if you have permission problem yaourt virtscreen
``` ```
### Python `pip`
### How to run Although not recommended, you may install it using `pip`. In this case, you need to install the dependancy (`xrandr` and `x11vnc`) manually.
Simply run `virtscreen` after installation:
```bash ```bash
$ virtscreen sudo pip install virtscreen
``` ```
If you want to run it directly from the Git repository:
```bash
$ ./launch.sh
```
Note that any files related to VirtScreen, including password and log, will be stored in `~/.virtscreen` directory.

BIN
data/desktop_entry.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View file

Before

Width:  |  Height:  |  Size: 7.8 MiB

After

Width:  |  Height:  |  Size: 7.8 MiB

Before After
Before After

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

BIN
data/virtscreen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,4 +0,0 @@
#!/bin/sh
# Script to run virtscreen locally
# This is not intended to be included in the distributed package
python 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.1
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.1.tar.gz
sha256sums = c584fe68ef296bced2ef5f3d88ffe81de1039c3062531c34547eeabd8c2f186d
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.1 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,22 @@ 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=('c584fe68ef296bced2ef5f3d88ffe81de1039c3062531c34547eeabd8c2f186d') 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.
python 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
python 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 .
install -Dm644 "$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop" # These are already installed by setup.py
install -Dm644 "$pkgname/icon/icon.png" "$pkgdir/usr/share/pixmaps/$pkgname.png" # install -Dm644 "data/$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop"
# 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

@ -0,0 +1,8 @@
virtscreen for Debian
Please edit this to provide information specific to
this virtscreen Debian package.
(Automatically generated by debmake Version 4.2.9)
-- Bumsik Kim <k.bumsik@gmail.com> Fri, 25 May 2018 17:28:18 +0000

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

16
package/debian/control Normal file
View file

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

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.1.1', # 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
@ -169,7 +167,7 @@ setup(
# If using Python 2.6 or earlier, then these have to be included in # If using Python 2.6 or earlier, then these have to be included in
# MANIFEST.in as well. # MANIFEST.in as well.
package_data={ package_data={
'virtscreen': ['icon/*.png', 'qml/*.qml', 'data/config.default.json'], 'virtscreen': ['icon/*.png', 'assets/*.qml', 'assets/*.json'],
}, },
# Although 'package_data' is the preferred approach, in some case you may # Although 'package_data' is the preferred approach, in some case you may
@ -177,8 +175,15 @@ setup(
# http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files
# #
# In this case, 'data_file' will be installed into '<sys.prefix>/my_data' # In this case, 'data_file' will be installed into '<sys.prefix>/my_data'
data_files=[
# data_files=[('my_data', ['data/data_file'])], # Optional # Desktop entries spec:
# https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/
('share/applications', ['virtscreen.desktop']),
# $XDG_DATA_DIRS/icons
# https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout
('share/icons', ['data/virtscreen.png']),
# ('share/man/man1', ['man/virtscreen.1'])
], # Optional
# To provide executable scripts, use entry points in preference to the # To provide executable scripts, use entry points in preference to the
# "scripts" keyword. Entry points provide cross-platform support and allow # "scripts" keyword. Entry points provide cross-platform support and allow
@ -189,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,10 +1,8 @@
#!/usr/bin/env xdg-open
[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=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

@ -10,7 +10,7 @@ ApplicationWindow {
id: window id: window
visible: false visible: false
flags: Qt.FramelessWindowHint flags: Qt.FramelessWindowHint
title: "Basic layouts" title: "VirtScreen"
property int theme_color: settings.theme_color property int theme_color: settings.theme_color
Material.theme: Material.Light Material.theme: Material.Light
@ -22,6 +22,10 @@ ApplicationWindow {
height: 540 height: 540
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
@ -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" 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
@ -277,6 +284,28 @@ ApplicationWindow {
} }
} }
Loader {
id: displayOptionsLoader
active: false
source: "DisplayOptionsDialog.qml"
onLoaded: {
item.onClosed.connect(function() {
displayOptionsLoader.active = false;
});
}
}
Loader {
id: vncOptionsLoader
active: false
source: "VncOptionsDialog.qml"
onLoaded: {
item.onClosed.connect(function() {
vncOptionsLoader.active = false;
});
}
}
SwipeView { SwipeView {
anchors.top: tabBar.bottom anchors.top: tabBar.bottom
anchors.bottom: parent.bottom anchors.bottom: parent.bottom

View file

@ -0,0 +1,71 @@
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Controls.Material 2.3
import QtQuick.Layouts 1.3
Dialog {
title: "Display Options"
focus: true
modal: true
visible: true
standardButtons: Dialog.Ok
x: (window.width - width) / 2
y: (window.width - height) / 2
width: popupWidth
height: 250
ColumnLayout {
anchors.fill: parent
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
Label { id: deviceLabel; text: "Device"; }
ComboBox {
id: deviceComboBox
anchors.left: deviceLabel.right
anchors.right: parent.right
anchors.leftMargin: 100
textRole: "name"
model: backend.screens
currentIndex: {
if (settings.virt.device) {
for (var i = 0; i < model.length; i++) {
if (model[i].name == settings.virt.device) {
return i;
}
}
}
settings.virt.device = '';
return -1;
}
onActivated: function(index) {
settings.virt.device = model[index].name;
}
delegate: ItemDelegate {
width: deviceComboBox.width
text: modelData.name
font.weight: deviceComboBox.currentIndex === index ? Font.Bold : Font.Normal
enabled: modelData.connected ? false : true
}
}
}
Text {
Layout.fillWidth: true
font { pixelSize: 14 }
wrapMode: Text.WordWrap
text: "<b>Warning</b>: Edit only if 'VIRTUAL1' is not available. " +
"If so, please note that the virtual screen may be " +
"unstable/unavailable depending on a graphic " +
"card and its driver."
}
RowLayout {
// Empty layout
Layout.fillHeight: true
}
}
onAccepted: {}
onRejected: {}
}

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 {
@ -59,27 +59,14 @@ ColumnLayout {
} }
} }
RowLayout { RowLayout {
anchors.left: parent.left Layout.alignment: Qt.AlignRight
anchors.right: parent.right Button {
Label { id: deviceLabel; text: "Device"; } text: "Advanced"
ComboBox { font.capitalization: Font.MixedCase
id: deviceComboBox onClicked: displayOptionsLoader.active = true;
anchors.left: deviceLabel.right background.opacity : 0
anchors.right: parent.right onHoveredChanged: hovered ? background.opacity = 0.4
anchors.leftMargin: 100 :background.opacity = 0;
textRole: "name"
model: backend.screens
currentIndex: backend.virtScreenIndex
onActivated: function(index) {
backend.virtScreenIndex = index
}
delegate: ItemDelegate {
width: deviceComboBox.width
text: modelData.name
font.weight: deviceComboBox.currentIndex === index ? Font.DemiBold : Font.Normal
highlighted: ListView.isCurrentItem
enabled: modelData.connected ? false : true
}
} }
} }
} }
@ -110,6 +97,7 @@ ColumnLayout {
window.autoClose = false; window.autoClose = false;
if (backend.vncState != Backend.OFF) { if (backend.vncState != Backend.OFF) {
console.log("vnc is running"); console.log("vnc is running");
stopVNC();
var restoreVNC = true; var restoreVNC = true;
if (autostart) { if (autostart) {
autostart = false; autostart = false;
@ -123,11 +111,10 @@ ColumnLayout {
autostart = true; autostart = true;
} }
if (restoreVNC) { if (restoreVNC) {
backend.startVNC(settings.vnc.port); startVNC();
} }
}); });
backend.stopVNC(); backend.openDisplaySetting(settings.displaySettingApp);
backend.openDisplaySetting();
} }
} }
} }

View file

@ -0,0 +1,81 @@
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Controls.Material 2.3
import QtQuick.Layouts 1.3
Dialog {
title: "VNC Options"
focus: true
modal: true
visible: true
standardButtons: Dialog.Ok
x: (window.width - width) / 2
y: (window.width - height) / 2
width: popupWidth
height: 350
Component.onCompleted: {
var request = new XMLHttpRequest();
request.open('GET', 'data.json');
request.onreadystatechange = function(event) {
if (request.readyState == XMLHttpRequest.DONE) {
var data = JSON.parse(request.responseText).x11vncOptions;
// merge data and settings
for (var key in data) {
Object.assign(data[key], settings.x11vncOptions[key]);
}
var repeater = vncOptionsRepeater;
repeater.model = Object.keys(data).map(function(k){return data[k]});
}
};
request.send();
}
ColumnLayout {
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 {
id: vncOptionsRepeater
RowLayout {
enabled: modelData.available
Label {
Layout.fillWidth: true
text: modelData.description + ' (' + modelData.value + ')'
}
Switch {
checked: modelData.available ? modelData.enabled : false
onCheckedChanged: {
settings.x11vncOptions[modelData.value].enabled = checked;
}
}
}
}
}
RowLayout {
// Empty layout
Layout.fillHeight: true
}
}
onAccepted: {}
onRejected: {}
}

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"
@ -45,6 +49,17 @@ ColumnLayout {
onClicked: passwordDialog.open() onClicked: passwordDialog.open()
} }
} }
RowLayout {
Layout.alignment: Qt.AlignRight
Button {
text: "Advanced"
font.capitalization: Font.MixedCase
onClicked: vncOptionsLoader.active = true;
background.opacity : 0
onHoveredChanged: hovered ? background.opacity = 0.4
:background.opacity = 0;
}
}
} }
} }
RowLayout { RowLayout {
@ -64,7 +79,7 @@ ColumnLayout {
autostart = checked; autostart = checked;
if ((checked == true) && (backend.vncState == Backend.OFF) && if ((checked == true) && (backend.vncState == Backend.OFF) &&
backend.virtScreenCreated) { backend.virtScreenCreated) {
backend.startVNC(settings.vnc.port); startVNC();
} }
} }
} }
@ -73,28 +88,26 @@ ColumnLayout {
GroupBox { GroupBox {
title: "Available IP addresses" title: "Available IP addresses"
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: 150 Layout.fillHeight: true
ColumnLayout { implicitHeight: 145
ListView {
id: ipListView
anchors.fill: parent anchors.fill: parent
ListView { clip: true
id: ipListView ScrollBar.vertical: ScrollBar {
anchors.fill: parent parent: ipListView.parent
clip: true anchors.top: ipListView.top
ScrollBar.vertical: ScrollBar { anchors.right: ipListView.right
parent: ipListView.parent anchors.bottom: ipListView.bottom
anchors.top: ipListView.top policy: ScrollBar.AlwaysOn
anchors.right: ipListView.right }
anchors.bottom: ipListView.bottom model: network.ipAddresses
policy: ScrollBar.AlwaysOn delegate: TextEdit {
} text: modelData
model: backend.ipAddresses readOnly: true
delegate: TextEdit { selectByMouse: true
text: modelData anchors.horizontalCenter: parent.horizontalCenter
readOnly: true font.pixelSize: 14
selectByMouse: true
anchors.horizontalCenter: parent.horizontalCenter
font.pointSize: 12
}
} }
} }
} }

View file

@ -0,0 +1,39 @@
{
"version": "0.3.1",
"x11vncVersion": "0.9.15",
"theme_color": 8,
"virt": {
"device": "VIRTUAL1",
"width": 1368,
"height": 1024,
"portrait": false,
"hidpi": false
},
"vnc": {
"port": 5900,
"autostart": false
},
"displaySettingApp": "arandr",
"x11vncOptions": {
"-ncache": {
"available": null,
"enabled": false,
"arg": 10
},
"-multiptr": {
"available": null,
"enabled": true,
"arg": null
},
"-repeat": {
"available": null,
"enabled": true,
"arg": null
}
},
"customX11vncArgs": {
"enabled": false,
"value": ""
},
"presets": []
}

View file

@ -0,0 +1,40 @@
{
"version": "0.3.1",
"x11vncOptions": {
"-ncache": {
"value": "-ncache",
"description": "Client side caching",
"long_description": "Enables cache"
},
"-multiptr": {
"value": "-multiptr",
"description": "Show mouse pointer",
"long_description": "This also enables input per-client."
},
"-repeat": {
"value": "-repeat",
"description": "Keyboard auto repeating",
"long_description": "Enables X server key auto repeat"
}
},
"displaySettingApps": {
"gnome": {
"value": "gnome",
"name": "GNOME",
"args": "gnome-control-center display",
"XDG_CURRENT_DESKTOP": ["gnome", "unity"]
},
"kde": {
"value": "kde",
"name": "KDE",
"args": "kcmshell5 kcm_kscreen",
"XDG_CURRENT_DESKTOP": ["kde"]
},
"arandr": {
"value": "arandr",
"name": "ARandR",
"args": "arandr",
"XDG_CURRENT_DESKTOP": [""]
}
}
}

View file

@ -4,15 +4,36 @@ 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 () {
backend.createVirtScreen(settings.virt.device, settings.virt.width,
settings.virt.height, settings.virt.portrait,
settings.virt.hidpi);
}
function startVNC () {
saveSettings();
backend.startVNC(settings.vnc.port);
}
function stopVNC () {
backend.stopVNC();
}
function switchVNC () { function switchVNC () {
if ((backend.vncState == Backend.OFF) && backend.virtScreenCreated) { if ((backend.vncState == Backend.OFF) && backend.virtScreenCreated) {
backend.startVNC(settings.vnc.port); startVNC();
} }
} }
@ -36,10 +57,18 @@ Item {
} }
} }
// virtscreen.py Cursor class.
Cursor {
id: cursor
}
// Timer object and function // Timer object and function
Timer { Timer {
id: timer id: timer
function setTimeout(cb, delayTime) { function setTimeout(cb, delayTime) {
if (timer.running) {
console.log('Timer is already running!');
}
timer.interval = delayTime; timer.interval = delayTime;
timer.repeat = false; timer.repeat = false;
timer.triggered.connect(cb); timer.triggered.connect(cb);
@ -81,37 +110,31 @@ 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();
window.raise(); window.raise();
window.requestActivate(); window.requestActivate();
timer.setTimeout (function() {
sysTrayIcon.clicked = false;
}, 200);
} }
} }
// 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
onMessageClicked: console.log("Message clicked")
Component.onCompleted: { Component.onCompleted: {
// without delay, the message appears in a wierd place // without delay, the message appears in a wierd place
timer.setTimeout (function() { timer.setTimeout (function() {
@ -127,6 +150,9 @@ Item {
return; return;
} }
sysTrayIcon.clicked = true; sysTrayIcon.clicked = true;
timer.setTimeout (function() {
sysTrayIcon.clicked = false;
}, 200);
mainLoader.active = true; mainLoader.active = true;
} }
@ -163,8 +189,7 @@ Item {
// Give a very short delay to show busyDialog. // Give a very short delay to show busyDialog.
timer.setTimeout (function() { timer.setTimeout (function() {
if (!backend.virtScreenCreated) { if (!backend.virtScreenCreated) {
backend.createVirtScreen(settings.virt.width, settings.virt.height, createVirtScreen();
settings.virt.portrait, settings.virt.hidpi);
} else { } else {
// If auto start enabled, stop VNC first then // If auto start enabled, stop VNC first then
if (autostart && (backend.vncState != Backend.OFF)) { if (autostart && (backend.vncState != Backend.OFF)) {
@ -177,7 +202,7 @@ Item {
autostart = true; autostart = true;
} }
}); });
backend.stopVNC(); stopVNC();
} else { } else {
backend.deleteVirtScreen(); backend.deleteVirtScreen();
} }
@ -191,7 +216,7 @@ Item {
backend.vncState == Backend.OFF ? "Start VNC Server" : "Stop VNC Server" backend.vncState == Backend.OFF ? "Start VNC Server" : "Stop VNC Server"
enabled: autostart ? false : enabled: autostart ? false :
backend.virtScreenCreated ? true : false backend.virtScreenCreated ? true : false
onTriggered: backend.vncState == Backend.OFF ? backend.startVNC(settings.vnc.port) : backend.stopVNC() onTriggered: backend.vncState == Backend.OFF ? startVNC() : stopVNC()
} }
MenuItem { MenuItem {
separator: true separator: true
@ -204,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();
} }
} }

View file

@ -13,8 +13,46 @@ 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: 250
Component.onCompleted: {
var request = new XMLHttpRequest();
request.open('GET', 'data.json');
request.onreadystatechange = function(event) {
if (request.readyState == XMLHttpRequest.DONE) {
var data = JSON.parse(request.responseText).displaySettingApps;
var combobox = displaySettingAppComboBox;
combobox.model = Object.keys(data).map(function(k){return data[k]});
combobox.currentIndex = Object.keys(data).indexOf(settings.displaySettingApp);
}
};
request.send();
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
Label { id: displaySettingAppLabel; text: "Display setting program"; }
ComboBox {
id: displaySettingAppComboBox
anchors.left: displaySettingAppLabel.right
anchors.right: parent.right
anchors.leftMargin: 10
textRole: "name"
onActivated: function(index) {
settings.displaySettingApp = model[index].value;
}
delegate: ItemDelegate {
width: parent.width
text: modelData.name
font.weight: displaySettingAppComboBox.currentIndex === index ? Font.Bold : Font.Normal
}
}
}
RowLayout { RowLayout {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@ -52,6 +90,11 @@ Dialog {
} }
} }
} }
RowLayout {
// Empty layout
Layout.fillHeight: true
}
} }
onAccepted: {} onAccepted: {}
onRejected: {} onRejected: {}

View file

@ -1,16 +0,0 @@
{
"version": 0.1,
"theme_color": 8,
"virt": {
"device": "VIRTUAL1",
"width": 1368,
"height": 1024,
"portrait": false,
"hidpi": false
},
"vnc": {
"port": 5900,
"autostart": false
},
"presets": []
}

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,678 +0,0 @@
#!/usr/bin/env python
import sys, os, subprocess, signal, re, atexit, time, json, shutil
from pathlib import Path
from enum import Enum
from typing import List, Dict
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
from PyQt5.QtGui import QIcon, QCursor
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine, QQmlListProperty
from 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
del os.environ['HOME'] # Delete $HOME env for security reason. This will make
# Path.home() to look up in the password directory (pwd module)
os.environ['PATH'] = os.confstr("CS_PATH") # Sanitize $PATH
# Setting home path and base path
HOME_PATH = str(Path.home())
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
DEFAULT_CONFIG_PATH = BASE_PATH + "/data/config.default.json"
ICON_PATH = BASE_PATH + "/icon/icon.png"
QML_PATH = BASE_PATH + "/qml"
MAIN_QML_PATH = QML_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
def run(self, arg: str):
"""Spawn a process
Arguments:
arg {str} -- arguments in string
"""
args = arg.split()
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):
DEFAULT_VIRT_SCREEN = "VIRTUAL1"
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_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.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_idx is None) and (screen.name == self.DEFAULT_VIRT_SCREEN):
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.virt_idx == self.primary_idx:
raise RuntimeError("Virtual screen must be selected other than the primary screen")
if self.virt_idx is None:
for idx, screen in enumerate(self.screens):
if not screen.connected and not screen.active:
self.virt_idx = idx
break
if self.virt_idx is None:
raise RuntimeError("There is no available devices for virtual screen")
self.virt = self.screens[self.virt_idx]
self.primary = self.screens[self.primary_idx]
def _add_screen_mode(self, width, height, portrait, hidpi) -> None:
# 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._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:
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
self._virtScreenIndex: int = self.xrandr.virt_idx
# 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
# Qt properties
@pyqtProperty(str, constant=True)
def settings(self):
try:
with open(CONFIG_PATH, "r") as f:
return f.read()
except FileNotFoundError:
with open(DEFAULT_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):
return QQmlListProperty(DisplayProperty, self, [DisplayProperty(x) for x in self.xrandr.screens])
@pyqtProperty(int, notify=onVirtScreenIndexChanged)
def virtScreenIndex(self):
return self._virtScreenIndex
@virtScreenIndex.setter
def virtScreenIndex(self, virtScreenIndex):
print("Changing virt to ", virtScreenIndex)
self.xrandr.virt_idx = virtScreenIndex
self.xrandr.virt = self.xrandr.screens[self.xrandr.virt_idx]
self._virtScreenIndex = virtScreenIndex
@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):
self._primaryProp = DisplayProperty(self.xrandr.get_primary_screen())
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(int, int, bool, bool)
def createVirtScreen(self, width, height, portrait, hidpi):
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
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
self.xrandr.delete_virtual_screen()
self.virtScreenCreated = False
@pyqtSlot(str)
def createVNCPassword(self, password):
if password:
password += '\n' + password + '\n\n' # verify + confirm
p = SubprocessWrapper()
try:
p.run(f"x11vnc -storepasswd {X11VNC_PASSWORD_PATH}", input=password, check=True)
except subprocess.CalledProcessError as e:
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
return
self.vncUsePassword = True
else:
self.onError.emit("Empty password")
@pyqtSlot()
def deleteVNCPassword(self):
if os.path.isfile(X11VNC_PASSWORD_PATH):
os.remove(X11VNC_PASSWORD_PATH)
self.vncUsePassword = False
else:
self.onError.emit("Failed deleting the password file")
@pyqtSlot(int)
def startVNC(self, port):
# Check if a virtual screen created
if not self.virtScreenCreated:
self.onError.emit("Virtual Screen not crated.")
return
if self.vncState is not self.VNCState.OFF:
self.onError.emit("VNC Server is already running.")
return
# regex used in callbacks
patter_connected = re.compile(r"^.*Got connection from client.*$", re.M)
patter_disconnected = re.compile(r"^.*client_count: 0*$", re.M)
# define callbacks
def _onConnected():
print("VNC started.")
self.vncState = self.VNCState.WAITING
def _onReceived(data):
data = data.decode("utf-8")
if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data):
print("VNC connected.")
self.vncState = self.VNCState.CONNECTED
if (self._vncState is self.VNCState.CONNECTED) and patter_disconnected.search(data):
print("VNC disconnected.")
self.vncState = self.VNCState.WAITING
def _onEnded(exitCode):
if exitCode is not 0:
self.vncState = self.VNCState.ERROR
self.onError.emit('X11VNC: Error occurred.\nDouble 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)
virt = self.xrandr.get_virtual_screen()
clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}"
arg = f"x11vnc -multiptr -repeat -rfbport {port} -clip {clip}"
if self.vncUsePassword:
arg += f" -rfbauth {X11VNC_PASSWORD_PATH}"
self.vncServer.run(arg)
# auto stop on exit
atexit.register(self.stopVNC, force=True)
@pyqtSlot()
def openDisplaySetting(self):
# 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}".')
program_list = ["gnome-control-center display", "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 ~/.virtscreen")
sys.exit(1)
import qt5reactor # pylint: disable=E0401
qt5reactor.install()
from twisted.internet import utils, 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()