mirror of
https://github.com/kbumsik/VirtScreen.git
synced 2025-02-12 11:21:53 +00:00
218 lines
8.6 KiB
Python
Executable file
218 lines
8.6 KiB
Python
Executable file
#!/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()
|