mirror of
https://github.com/kbumsik/VirtScreen.git
synced 2025-02-12 19:31:50 +00:00
#7: Added CLI options
This commit is contained in:
parent
439769ca21
commit
30ffe32f82
1 changed files with 143 additions and 29 deletions
|
@ -1,10 +1,19 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
# Python standard packages
|
# Python standard packages
|
||||||
import sys, os, subprocess, signal, re, atexit, time, json, shutil
|
import sys
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import signal
|
||||||
|
import re
|
||||||
|
import atexit
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List, Dict
|
from typing import List, Dict, Callable
|
||||||
# PyQt5 packages
|
# PyQt5 packages
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
|
from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
|
||||||
|
@ -356,13 +365,23 @@ class XRandR(SubprocessWrapper):
|
||||||
self._update_screens()
|
self._update_screens()
|
||||||
return self.virt
|
return self.virt
|
||||||
|
|
||||||
def create_virtual_screen(self, width, height, portrait=False, hidpi=False) -> None:
|
def create_virtual_screen(self, width, height, portrait=False, hidpi=False, pos='') -> None:
|
||||||
print("creating: ", self.virt)
|
|
||||||
self._update_screens()
|
self._update_screens()
|
||||||
|
print("creating: ", self.virt)
|
||||||
self._add_screen_mode(width, height, portrait, hidpi)
|
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(f"xrandr --output {self.virt.name} --mode {self.mode_name}")
|
||||||
self.check_output("sleep 5")
|
self.check_output("sleep 5")
|
||||||
self.check_output(f"xrandr --output {self.virt.name} --preferred")
|
self.check_output(f"xrandr --output {self.virt.name} {pos}")
|
||||||
self._update_screens()
|
self._update_screens()
|
||||||
|
|
||||||
def delete_virtual_screen(self) -> None:
|
def delete_virtual_screen(self) -> None:
|
||||||
|
@ -530,11 +549,11 @@ class Backend(QObject):
|
||||||
|
|
||||||
# Qt Slots
|
# Qt Slots
|
||||||
@pyqtSlot(str, int, int, bool, bool)
|
@pyqtSlot(str, int, int, bool, bool)
|
||||||
def createVirtScreen(self, device, width, height, portrait, hidpi):
|
def createVirtScreen(self, device, width, height, portrait, hidpi, pos=''):
|
||||||
self.xrandr.virt_name = device
|
self.xrandr.virt_name = device
|
||||||
print("Creating a Virtual Screen...")
|
print("Creating a Virtual Screen...")
|
||||||
try:
|
try:
|
||||||
self.xrandr.create_virtual_screen(width, height, portrait, hidpi)
|
self.xrandr.create_virtual_screen(width, height, portrait, hidpi, pos)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
||||||
return
|
return
|
||||||
|
@ -672,7 +691,7 @@ class Backend(QObject):
|
||||||
if force:
|
if force:
|
||||||
# Usually called from atexit().
|
# Usually called from atexit().
|
||||||
self.vncServer.kill()
|
self.vncServer.kill()
|
||||||
time.sleep(2) # Make sure X11VNC shutdown before execute next atexit().
|
time.sleep(3) # Make sure X11VNC shutdown before execute next atexit().
|
||||||
if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED):
|
if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED):
|
||||||
self.vncServer.kill()
|
self.vncServer.kill()
|
||||||
else:
|
else:
|
||||||
|
@ -691,41 +710,83 @@ class Backend(QObject):
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
# Main Code
|
# Main Code
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
def main():
|
def main() -> None:
|
||||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
parser = argparse.ArgumentParser(
|
||||||
app = QApplication(sys.argv)
|
formatter_class=argparse.RawTextHelpFormatter,
|
||||||
|
description='Make your iPad/tablet/computer as a secondary monitor on Linux.\n\n'
|
||||||
from PyQt5.QtWidgets import QSystemTrayIcon, QMessageBox
|
'You can start VirtScreen in the following two modes:\n\n'
|
||||||
|
' - GUI mode: A system tray icon will appear when no argument passed.\n'
|
||||||
if not QSystemTrayIcon.isSystemTrayAvailable():
|
' You need to use this first to configure a virtual screen.\n'
|
||||||
QMessageBox.critical(None, "VirtScreen",
|
' - CLI mode: After configured the virtual screen, you can start VirtScreen\n'
|
||||||
"Cannot detect system tray on this system.")
|
' 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')
|
||||||
|
args = parser.parse_args()
|
||||||
|
if any(vars(args).values()):
|
||||||
|
main_cli(args)
|
||||||
|
else:
|
||||||
|
main_gui()
|
||||||
|
print('Program should not reach here.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if os.environ['XDG_SESSION_TYPE'] == 'wayland':
|
def check_env(msg: Callable[[str], None]) -> None:
|
||||||
QMessageBox.critical(None, "VirtScreen",
|
if os.environ['XDG_SESSION_TYPE'].lower() == 'wayland':
|
||||||
"Currently Wayland is not supported")
|
msg("Currently Wayland is not supported")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not HOME_PATH:
|
if not HOME_PATH:
|
||||||
QMessageBox.critical(None, "VirtScreen",
|
msg("Cannot detect home directory.")
|
||||||
"Cannot detect home directory.")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not os.path.exists(HOME_PATH):
|
if not os.path.exists(HOME_PATH):
|
||||||
try:
|
try:
|
||||||
os.makedirs(HOME_PATH)
|
os.makedirs(HOME_PATH)
|
||||||
except:
|
except:
|
||||||
QMessageBox.critical(None, "VirtScreen",
|
msg("Cannot create ~/.config/virtscreen")
|
||||||
"Cannot create ~/.config/virtscreen")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not shutil.which('x11vnc'):
|
if not shutil.which('x11vnc'):
|
||||||
QMessageBox.critical(None, "VirtScreen",
|
msg("x11vnc is not installed.")
|
||||||
"x11vnc is not installed.")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
try:
|
try:
|
||||||
test = XRandR()
|
test = XRandR()
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
QMessageBox.critical(None, "VirtScreen", str(e))
|
msg(str(e))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
def main_gui():
|
||||||
|
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# Check environment first
|
||||||
|
from PyQt5.QtWidgets import QMessageBox, QSystemTrayIcon
|
||||||
|
def dialog(message: str) -> None:
|
||||||
|
QMessageBox.critical(None, "VirtScreen", message)
|
||||||
|
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
|
dialog("Cannot detect system tray on this system.")
|
||||||
|
sys.exit(1)
|
||||||
|
check_env(dialog)
|
||||||
|
|
||||||
# Replace Twisted reactor with qt5reactor
|
# Replace Twisted reactor with qt5reactor
|
||||||
import qt5reactor # pylint: disable=E0401
|
import qt5reactor # pylint: disable=E0401
|
||||||
qt5reactor.install()
|
qt5reactor.install()
|
||||||
|
@ -744,10 +805,63 @@ def main():
|
||||||
engine = QQmlApplicationEngine()
|
engine = QQmlApplicationEngine()
|
||||||
engine.load(QUrl(MAIN_QML_PATH))
|
engine.load(QUrl(MAIN_QML_PATH))
|
||||||
if not engine.rootObjects():
|
if not engine.rootObjects():
|
||||||
QMessageBox.critical(None, "VirtScreen", "Failed to load QML")
|
dialog("Failed to load QML")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
reactor.run()
|
reactor.run()
|
||||||
|
|
||||||
|
def main_cli(args: argparse.Namespace):
|
||||||
|
for key, value in vars(args).items():
|
||||||
|
print(key, ": ", value)
|
||||||
|
# Check the environment
|
||||||
|
check_env(print)
|
||||||
|
if not os.path.exists(CONFIG_PATH):
|
||||||
|
print("Configuration file does not exist.\n"
|
||||||
|
"Configure a virtual screen using GUI first.")
|
||||||
|
sys.exit(1)
|
||||||
|
# By instantiating the backend, additional verifications of config
|
||||||
|
# file will be done.
|
||||||
|
backend = Backend()
|
||||||
|
# Get settings
|
||||||
|
with open(CONFIG_PATH, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
# Override settings from arguments
|
||||||
|
position = ''
|
||||||
|
if not args.auto:
|
||||||
|
args_virt = ['portrait', 'hidpi']
|
||||||
|
for prop in args_virt:
|
||||||
|
if vars(args)[prop]:
|
||||||
|
config['virt'][prop] = True
|
||||||
|
args_position = ['left', 'right', 'above', 'below']
|
||||||
|
tmp_args = {k: vars(args)[k] for k in args_position}
|
||||||
|
if not any(tmp_args.values()):
|
||||||
|
print("Choose a position relative to the primary monitor. (e.g. --left)")
|
||||||
|
sys.exit(1)
|
||||||
|
for key, value in tmp_args.items():
|
||||||
|
if value:
|
||||||
|
position = key
|
||||||
|
# Turn settings object into VNC arguments format
|
||||||
|
vnc_option = ''
|
||||||
|
for key, value in config['x11vncOptions'].items():
|
||||||
|
if value['available'] and value['enabled']:
|
||||||
|
vnc_option += key + ' '
|
||||||
|
if value['arg'] is not None:
|
||||||
|
vnc_option += str(value['arg']) + ' '
|
||||||
|
# Create virtscreen and Start VNC
|
||||||
|
def handle_error(msg):
|
||||||
|
print('Error: ', msg)
|
||||||
|
sys.exit(1)
|
||||||
|
backend.onError.connect(handle_error)
|
||||||
|
backend.createVirtScreen(config['virt']['device'], config['virt']['width'],
|
||||||
|
config['virt']['height'], config['virt']['portrait'],
|
||||||
|
config['virt']['hidpi'], position)
|
||||||
|
def handle_vnc_changed(state):
|
||||||
|
if state is backend.VNCState.OFF:
|
||||||
|
sys.exit(0)
|
||||||
|
backend.onVncStateChanged.connect(handle_vnc_changed)
|
||||||
|
from twisted.internet import reactor # pylint: disable=E0401
|
||||||
|
backend.startVNC(config['vnc']['port'], vnc_option)
|
||||||
|
reactor.run()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
Loading…
Reference in a new issue