diff --git a/virtscreen/virtscreen.py b/virtscreen/virtscreen.py index 049b425..0413c7c 100755 --- a/virtscreen/virtscreen.py +++ b/virtscreen/virtscreen.py @@ -1,10 +1,19 @@ #!/usr/bin/python3 # 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 enum import Enum -from typing import List, Dict +from typing import List, Dict, Callable # PyQt5 packages from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS @@ -356,13 +365,23 @@ class XRandR(SubprocessWrapper): self._update_screens() return self.virt - def create_virtual_screen(self, width, height, portrait=False, hidpi=False) -> None: - print("creating: ", self.virt) + def create_virtual_screen(self, width, height, portrait=False, hidpi=False, pos='') -> None: self._update_screens() + print("creating: ", self.virt) self._add_screen_mode(width, height, portrait, hidpi) + arg_pos = ['left', 'right', 'above', 'below'] + xrandr_pos = ['--left-of', '--right-of', '--above', '--below'] + if pos and pos in arg_pos: + # convert pos for xrandr + pos = xrandr_pos[arg_pos.index(pos)] + pos += ' ' + self.primary.name + elif not pos: + pos = '--preferred' + else: + raise RuntimeError("Incorrect position option selected.") self.check_output(f"xrandr --output {self.virt.name} --mode {self.mode_name}") self.check_output("sleep 5") - self.check_output(f"xrandr --output {self.virt.name} --preferred") + self.check_output(f"xrandr --output {self.virt.name} {pos}") self._update_screens() def delete_virtual_screen(self) -> None: @@ -530,11 +549,11 @@ class Backend(QObject): # Qt Slots @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 print("Creating a Virtual Screen...") 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: self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8')) return @@ -672,7 +691,7 @@ class Backend(QObject): if force: # Usually called from atexit(). 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): self.vncServer.kill() else: @@ -691,41 +710,83 @@ class Backend(QObject): # ------------------------------------------------------------------------------- # Main Code # ------------------------------------------------------------------------------- -def main(): - QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) - app = QApplication(sys.argv) +def main() -> None: + parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, + description='Make your iPad/tablet/computer as a secondary monitor on Linux.\n\n' + 'You can start VirtScreen in the following two modes:\n\n' + ' - GUI mode: A system tray icon will appear when no argument passed.\n' + ' You need to use this first to configure a virtual screen.\n' + ' - CLI mode: After configured the virtual screen, you can start VirtScreen\n' + ' in CLI mode if you do not want a GUI, by passing any arguments\n', + epilog='example:\n' + 'virtscreen # GUI mode. You need to use this first\n' + ' to configure the screen\n' + 'virtscreen --auto # CLI mode. Scrren will be created using previous\n' + ' settings (from both GUI mode and CLI mode)\n' + 'virtscreen --left # CLI mode. On the left to the primary monitor\n' + 'virtscreen --below # CLI mode. Below the primary monitor.\n' + 'virtscreen --below --portrait # Below, and portrait mode.\n' + 'virtscreen --below --portrait --hipdi # Below, portrait, HiDPI mode.\n') + parser.add_argument('--auto', action='store_true', + help='create a virtual screen automatically using previous\n' + 'settings (from both GUI mode and CLI mode)') + parser.add_argument('--left', action='store_true', + help='a virtual screen will be created left to the primary\n' + 'monitor') + parser.add_argument('--right', action='store_true', + help='right to the primary monitor') + parser.add_argument('--above', '--up', action='store_true', + help='above the primary monitor') + parser.add_argument('--below', '--down', action='store_true', + help='below the primary monitor') + parser.add_argument('--portrait', action='store_true', + help='Portrait mode. Width and height of the screen are swapped') + parser.add_argument('--hidpi', action='store_true', + help='HiDPI mode. Width and height are doubled') + args = parser.parse_args() + if any(vars(args).values()): + main_cli(args) + else: + main_gui() + print('Program should not reach here.') + sys.exit(1) - 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") +def check_env(msg: Callable[[str], None]) -> None: + if os.environ['XDG_SESSION_TYPE'].lower() == 'wayland': + msg("Currently Wayland is not supported") sys.exit(1) if not HOME_PATH: - QMessageBox.critical(None, "VirtScreen", - "Cannot detect home directory.") + msg("Cannot detect home directory.") sys.exit(1) if not os.path.exists(HOME_PATH): try: os.makedirs(HOME_PATH) except: - QMessageBox.critical(None, "VirtScreen", - "Cannot create ~/.config/virtscreen") + msg("Cannot create ~/.config/virtscreen") sys.exit(1) if not shutil.which('x11vnc'): - QMessageBox.critical(None, "VirtScreen", - "x11vnc is not installed.") + msg("x11vnc is not installed.") sys.exit(1) try: test = XRandR() except RuntimeError as e: - QMessageBox.critical(None, "VirtScreen", str(e)) + msg(str(e)) sys.exit(1) + +def main_gui(): + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + app = QApplication(sys.argv) + + # Check environment first + from PyQt5.QtWidgets import QMessageBox, QSystemTrayIcon + def dialog(message: str) -> None: + QMessageBox.critical(None, "VirtScreen", message) + if not QSystemTrayIcon.isSystemTrayAvailable(): + dialog("Cannot detect system tray on this system.") + sys.exit(1) + check_env(dialog) + # Replace Twisted reactor with qt5reactor import qt5reactor # pylint: disable=E0401 qt5reactor.install() @@ -744,10 +805,63 @@ def main(): engine = QQmlApplicationEngine() engine.load(QUrl(MAIN_QML_PATH)) if not engine.rootObjects(): - QMessageBox.critical(None, "VirtScreen", "Failed to load QML") + dialog("Failed to load QML") sys.exit(1) sys.exit(app.exec_()) reactor.run() +def main_cli(args: argparse.Namespace): + for key, value in vars(args).items(): + print(key, ": ", value) + # Check the environment + check_env(print) + if not os.path.exists(CONFIG_PATH): + print("Configuration file does not exist.\n" + "Configure a virtual screen using GUI first.") + sys.exit(1) + # By instantiating the backend, additional verifications of config + # file will be done. + backend = Backend() + # Get settings + with open(CONFIG_PATH, 'r') as f: + config = json.load(f) + # Override settings from arguments + position = '' + if not args.auto: + args_virt = ['portrait', 'hidpi'] + for prop in args_virt: + if vars(args)[prop]: + config['virt'][prop] = True + args_position = ['left', 'right', 'above', 'below'] + tmp_args = {k: vars(args)[k] for k in args_position} + if not any(tmp_args.values()): + print("Choose a position relative to the primary monitor. (e.g. --left)") + sys.exit(1) + for key, value in tmp_args.items(): + if value: + position = key + # 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__': main()