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

Use python logging module #8

This commit is contained in:
Bumsik Kim 2018-11-07 04:44:37 +09:00
parent f27db06b17
commit ebbbf97cdf
No known key found for this signature in database
GPG key ID: E31041C8EC5B01C6
6 changed files with 106 additions and 58 deletions

View file

@ -8,10 +8,21 @@ DOCKER_RUN_TTY=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME
.ONESHELL: .ONESHELL:
.PHONY: run debug run-appimage debug-appimage
# Run script # Run script
run: run:
python3 -m virtscreen python3 -m virtscreen
debug:
QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 python3 -m virtscreen --log=DEBUG
run-appimage: package/appimage/VirtScreen-x86_64.AppImage
$<
debug-appimage: package/appimage/VirtScreen-x86_64.AppImage
QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 $< --log=DEBUG
# Docker tools # Docker tools
.PHONY: docker docker-build .PHONY: docker docker-build
@ -36,14 +47,14 @@ wheel-clean:
.PHONY: appimage-clean .PHONY: appimage-clean
.SECONDARY: package/appimage/VirtScreen-x86_64.AppImage .SECONDARY: package/appimage/VirtScreen-x86_64.AppImage
package/appimage/%.AppImage: package/appimage/VirtScreen-x86_64.AppImage:
$(DOCKER_RUN) package/appimage/build.sh $(DOCKER_RUN) package/appimage/build.sh
$(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/appimage $(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/appimage
appimage-clean: appimage-clean:
-rm -rf package/appimage/virtscreen.AppDir package/appimage/VirtScreen-x86_64.AppImage -rm -rf package/appimage/virtscreen.AppDir package/appimage/VirtScreen-x86_64.AppImage
# For Debian packaging, https://www.debian.org/doc/manuals/maint-guide/index.en.html # 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 # https://www.debian.org/doc/manuals/debmake-doc/ch08.en.html#setup-py
.PHONY: deb-contents deb-clean .PHONY: deb-contents deb-clean

View file

@ -7,6 +7,8 @@ import signal
import json import json
import shutil import shutil
import argparse import argparse
import logging
from logging.handlers import RotatingFileHandler
from typing import Callable from typing import Callable
import asyncio import asyncio
@ -25,8 +27,12 @@ from quamash import QEventLoop
from .display import DisplayProperty from .display import DisplayProperty
from .xrandr import XRandR from .xrandr import XRandR
from .qt_backend import Backend, Cursor, Network from .qt_backend import Backend, Cursor, Network
from .path import HOME_PATH, ICON_PATH, MAIN_QML_PATH, CONFIG_PATH 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: def main() -> None:
"""Start main program""" """Start main program"""
@ -63,22 +69,46 @@ def main() -> None:
help='Portrait mode. Width and height of the screen are swapped') help='Portrait mode. Width and height of the screen are swapped')
parser.add_argument('--hidpi', action='store_true', parser.add_argument('--hidpi', action='store_true',
help='HiDPI mode. Width and height are doubled') help='HiDPI mode. Width and height are doubled')
parser.add_argument('--log', type=str,
help='Python logging level, For example, --log=DEBUG.\n'
'Only used for reporting bugs and debugging')
# Add signal handler # Add signal handler
def on_exit(self, signum=None, frame=None): def on_exit(self, signum=None, frame=None):
sys.exit(0) sys.exit(0)
for sig in [signal.SIGINT, signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]: for sig in [signal.SIGINT, signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]:
signal.signal(sig, on_exit) signal.signal(sig, on_exit)
args = vars(parser.parse_args())
# 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}')
# Start main # Start main
args = parser.parse_args() if any(args.values()):
if any(vars(args).values()):
main_cli(args) main_cli(args)
else: else:
main_gui() main_gui()
print('Program should not reach here.') error('Program should not reach here.')
sys.exit(1) sys.exit(1)
def check_env(msg: Callable[[str], None]) -> None: def check_env(msg: Callable[[str], None]) -> None:
"""Check enveironments before start""" """Check environments before start"""
if os.environ.get('XDG_SESSION_TYPE', '').lower() == 'wayland': if os.environ.get('XDG_SESSION_TYPE', '').lower() == 'wayland':
msg("Currently Wayland is not supported") msg("Currently Wayland is not supported")
sys.exit(1) sys.exit(1)
@ -94,6 +124,7 @@ def check_env(msg: Callable[[str], None]) -> None:
if not shutil.which('x11vnc'): if not shutil.which('x11vnc'):
msg("x11vnc is not installed.") msg("x11vnc is not installed.")
sys.exit(1) sys.exit(1)
# Check if xrandr is correctly parsed.
try: try:
test = XRandR() test = XRandR()
except RuntimeError as e: except RuntimeError as e:
@ -105,7 +136,7 @@ def main_gui():
app = QApplication(sys.argv) app = QApplication(sys.argv)
loop = QEventLoop(app) loop = QEventLoop(app)
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
# Check environment first # Check environment first
from PyQt5.QtWidgets import QMessageBox, QSystemTrayIcon from PyQt5.QtWidgets import QMessageBox, QSystemTrayIcon
def dialog(message: str) -> None: def dialog(message: str) -> None:
@ -114,7 +145,7 @@ def main_gui():
dialog("Cannot detect system tray on this system.") dialog("Cannot detect system tray on this system.")
sys.exit(1) sys.exit(1)
check_env(dialog) check_env(dialog)
app.setApplicationName("VirtScreen") app.setApplicationName("VirtScreen")
app.setWindowIcon(QIcon(ICON_PATH)) app.setWindowIcon(QIcon(ICON_PATH))
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material" os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
@ -138,38 +169,36 @@ def main_gui():
def main_cli(args: argparse.Namespace): def main_cli(args: argparse.Namespace):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
for key, value in vars(args).items():
print(key, ": ", value)
# Check the environment # Check the environment
check_env(print) check_env(print)
if not os.path.exists(CONFIG_PATH): if not os.path.exists(CONFIG_PATH):
print("Configuration file does not exist.\n" error("Configuration file does not exist.\n"
"Configure a virtual screen using GUI first.") "Configure a virtual screen using GUI first.")
sys.exit(1) sys.exit(1)
# By instantiating the backend, additional verifications of config # By instantiating the backend, additional verifications of config
# file will be done. # file will be done.
backend = Backend() backend = Backend()
# Get settings # Get settings
with open(CONFIG_PATH, 'r') as f: with open(CONFIG_PATH, 'r') as f:
config = json.load(f) config = json.load(f)
# Override settings from arguments # Override settings from arguments
position = '' position = ''
if not args.auto: if not args['auto']:
args_virt = ['portrait', 'hidpi'] args_virt = ['portrait', 'hidpi']
for prop in args_virt: for prop in args_virt:
if vars(args)[prop]: if args[prop]:
config['virt'][prop] = True config['virt'][prop] = True
args_position = ['left', 'right', 'above', 'below'] args_position = ['left', 'right', 'above', 'below']
tmp_args = {k: vars(args)[k] for k in args_position} tmp_args = {k: args[k] for k in args_position}
if not any(tmp_args.values()): if not any(tmp_args.values()):
print("Choose a position relative to the primary monitor. (e.g. --left)") error("Choose a position relative to the primary monitor. (e.g. --left)")
sys.exit(1) sys.exit(1)
for key, value in tmp_args.items(): for key, value in tmp_args.items():
if value: if value:
position = key position = key
# Create virtscreen and Start VNC # Create virtscreen and Start VNC
def handle_error(msg): def handle_error(msg):
print('Error: ', msg) error(msg)
sys.exit(1) sys.exit(1)
backend.onError.connect(handle_error) backend.onError.connect(handle_error)
backend.createVirtScreen(config['virt']['device'], config['virt']['width'], backend.createVirtScreen(config['virt']['device'], config['virt']['width'],

View file

@ -29,6 +29,7 @@ BASE_PATH = os.path.dirname(__file__) # Location of this script
X11VNC_LOG_PATH = HOME_PATH + "/x11vnc_log.txt" X11VNC_LOG_PATH = HOME_PATH + "/x11vnc_log.txt"
X11VNC_PASSWORD_PATH = HOME_PATH + "/x11vnc_passwd" X11VNC_PASSWORD_PATH = HOME_PATH + "/x11vnc_passwd"
CONFIG_PATH = HOME_PATH + "/config.json" CONFIG_PATH = HOME_PATH + "/config.json"
LOGGING_PATH = HOME_PATH + "/log.txt"
# Path in the program path # Path in the program path
ICON_PATH = BASE_PATH + "/icon/full_256x256.png" ICON_PATH = BASE_PATH + "/icon/full_256x256.png"
ASSETS_PATH = BASE_PATH + "/assets" ASSETS_PATH = BASE_PATH + "/assets"

View file

@ -5,6 +5,7 @@ import asyncio
import signal import signal
import shlex import shlex
import os import os
import logging
class SubprocessWrapper: class SubprocessWrapper:
@ -30,7 +31,7 @@ class _Protocol(asyncio.SubprocessProtocol):
self.transport: asyncio.SubprocessTransport self.transport: asyncio.SubprocessTransport
def connection_made(self, transport): def connection_made(self, transport):
print("connectionMade!") logging.info("connectionMade!")
self.outer.connected() self.outer.connected()
self.transport = transport self.transport = transport
transport.get_pipe_transport(0).close() # No more input transport.get_pipe_transport(0).close() # No more input
@ -47,14 +48,14 @@ class _Protocol(asyncio.SubprocessProtocol):
def pipe_connection_lost(self, fd, exc): def pipe_connection_lost(self, fd, exc):
if fd == 0: # stdin if fd == 0: # stdin
print("stdin is closed. (we probably did it)") logging.info("stdin is closed. (we probably did it)")
elif fd == 1: # stdout elif fd == 1: # stdout
print("The child closed their stdout.") logging.info("The child closed their stdout.")
elif fd == 2: # stderr elif fd == 2: # stderr
print("The child closed their stderr.") logging.info("The child closed their stderr.")
def connection_lost(self, exc): def connection_lost(self, exc):
print("Subprocess connection lost.") logging.info("Subprocess connection lost.")
def process_exited(self): def process_exited(self):
if self.outer.logfile is not None: if self.outer.logfile is not None:
@ -62,10 +63,10 @@ class _Protocol(asyncio.SubprocessProtocol):
self.transport.close() self.transport.close()
return_code = self.transport.get_returncode() return_code = self.transport.get_returncode()
if return_code is None: if return_code is None:
print("Unknown exit") logging.error("Unknown exit")
self.outer.ended(1) self.outer.ended(1)
return return
print("processEnded, status", return_code) logging.info(f"processEnded, status {return_code}")
self.outer.ended(return_code) self.outer.ended(return_code)

View file

@ -7,6 +7,7 @@ import os
import shutil import shutil
import atexit import atexit
import time import time
import logging
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
from PyQt5.QtGui import QCursor from PyQt5.QtGui import QCursor
@ -74,7 +75,7 @@ class Backend(QObject):
p = SubprocessWrapper() p = SubprocessWrapper()
arg = 'x11vnc -opts' arg = 'x11vnc -opts'
ret = p.run(arg) ret = p.run(arg)
options = tuple(m.group(1) for m in re.finditer("\s*(-\w+)\s+", ret)) options = tuple(m.group(1) for m in re.finditer(r"\s*(-\w+)\s+", ret))
# Set/unset available x11vnc options flags in config # Set/unset available x11vnc options flags in config
with open(CONFIG_PATH, 'r') as f, open(DATA_PATH, 'r') as f_data: with open(CONFIG_PATH, 'r') as f, open(DATA_PATH, 'r') as f_data:
config = json.load(f) config = json.load(f)
@ -93,6 +94,10 @@ class Backend(QObject):
with open(CONFIG_PATH, 'w') as f: with open(CONFIG_PATH, 'w') as f:
f.write(json.dumps(config, indent=4, sort_keys=True)) f.write(json.dumps(config, indent=4, sort_keys=True))
def promptError(self, msg):
logging.error(msg)
self.onError.emit(msg)
# Qt properties # Qt properties
@pyqtProperty(str, constant=True) @pyqtProperty(str, constant=True)
def settings(self): def settings(self):
@ -118,7 +123,7 @@ class Backend(QObject):
try: try:
return QQmlListProperty(DisplayProperty, self, [DisplayProperty(x) for x in self.xrandr.screens]) return QQmlListProperty(DisplayProperty, self, [DisplayProperty(x) for x in self.xrandr.screens])
except RuntimeError as e: except RuntimeError as e:
self.onError.emit(str(e)) self.promptError(str(e))
return QQmlListProperty(DisplayProperty, self, []) return QQmlListProperty(DisplayProperty, self, [])
@pyqtProperty(bool, notify=onVncUsePasswordChanged) @pyqtProperty(bool, notify=onVncUsePasswordChanged)
@ -148,28 +153,28 @@ class Backend(QObject):
@pyqtSlot(str, int, int, bool, bool) @pyqtSlot(str, int, int, bool, bool)
def createVirtScreen(self, device, width, height, portrait, hidpi, pos=''): def createVirtScreen(self, device, width, height, portrait, hidpi, pos=''):
self.xrandr.virt_name = device self.xrandr.virt_name = device
print("Creating a Virtual Screen...") logging.info("Creating a Virtual Screen...")
try: try:
self.xrandr.create_virtual_screen(width, height, portrait, hidpi, pos) 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.promptError(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
return return
except RuntimeError as e: except RuntimeError as e:
self.onError.emit(str(e)) self.promptError(str(e))
return return
self.virtScreenCreated = True self.virtScreenCreated = True
@pyqtSlot() @pyqtSlot()
def deleteVirtScreen(self): def deleteVirtScreen(self):
print("Deleting the Virtual Screen...") logging.info("Deleting the Virtual Screen...")
if self.vncState is not self.VNCState.OFF: if self.vncState is not self.VNCState.OFF:
self.onError.emit("Turn off the VNC server first") self.promptError("Turn off the VNC server first")
self.virtScreenCreated = True self.virtScreenCreated = True
return return
try: try:
self.xrandr.delete_virtual_screen() self.xrandr.delete_virtual_screen()
except RuntimeError as e: except RuntimeError as e:
self.onError.emit(str(e)) self.promptError(str(e))
return return
self.virtScreenCreated = False self.virtScreenCreated = False
@ -181,11 +186,11 @@ class Backend(QObject):
try: try:
p.run(f"x11vnc -storepasswd {X11VNC_PASSWORD_PATH}", input=password, check=True) p.run(f"x11vnc -storepasswd {X11VNC_PASSWORD_PATH}", input=password, check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8')) self.promptError(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
return return
self.vncUsePassword = True self.vncUsePassword = True
else: else:
self.onError.emit("Empty password") self.promptError("Empty password")
@pyqtSlot() @pyqtSlot()
def deleteVNCPassword(self): def deleteVNCPassword(self):
@ -193,16 +198,16 @@ class Backend(QObject):
os.remove(X11VNC_PASSWORD_PATH) os.remove(X11VNC_PASSWORD_PATH)
self.vncUsePassword = False self.vncUsePassword = False
else: else:
self.onError.emit("Failed deleting the password file") self.promptError("Failed deleting the password file")
@pyqtSlot(int) @pyqtSlot(int)
def startVNC(self, port): def startVNC(self, port):
# Check if a virtual screen created # Check if a virtual screen created
if not self.virtScreenCreated: if not self.virtScreenCreated:
self.onError.emit("Virtual Screen not crated.") self.promptError("Virtual Screen not crated.")
return return
if self.vncState is not self.VNCState.OFF: if self.vncState is not self.VNCState.OFF:
self.onError.emit("VNC Server is already running.") self.promptError("VNC Server is already running.")
return return
# regex used in callbacks # regex used in callbacks
patter_connected = re.compile(r"^.*Got connection from client.*$", re.M) patter_connected = re.compile(r"^.*Got connection from client.*$", re.M)
@ -210,27 +215,27 @@ class Backend(QObject):
# define callbacks # define callbacks
def _connected(): def _connected():
print("VNC started.") logging.info("VNC started.")
self.vncState = self.VNCState.WAITING self.vncState = self.VNCState.WAITING
def _received(data): def _received(data):
data = data.decode("utf-8") data = data.decode("utf-8")
if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data): if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data):
print("VNC connected.") logging.info("VNC connected.")
self.vncState = self.VNCState.CONNECTED self.vncState = self.VNCState.CONNECTED
if (self._vncState is self.VNCState.CONNECTED) and patter_disconnected.search(data): if (self._vncState is self.VNCState.CONNECTED) and patter_disconnected.search(data):
print("VNC disconnected.") logging.info("VNC disconnected.")
self.vncState = self.VNCState.WAITING self.vncState = self.VNCState.WAITING
def _ended(exitCode): def _ended(exitCode):
if exitCode is not 0: if exitCode is not 0:
self.vncState = self.VNCState.ERROR self.vncState = self.VNCState.ERROR
self.onError.emit('X11VNC: Error occurred.\n' self.promptError('X11VNC: Error occurred.\n'
'Double check if the port is already used.') 'Double check if the port is already used.')
self.vncState = self.VNCState.OFF # TODO: better handling error state self.vncState = self.VNCState.OFF # TODO: better handling error state
else: else:
self.vncState = self.VNCState.OFF self.vncState = self.VNCState.OFF
print("VNC Exited.") logging.info("VNC Exited.")
atexit.unregister(self.stopVNC) atexit.unregister(self.stopVNC)
# load settings # load settings
with open(CONFIG_PATH, 'r') as f: with open(CONFIG_PATH, 'r') as f:
@ -250,7 +255,7 @@ class Backend(QObject):
try: try:
virt = self.xrandr.get_virtual_screen() virt = self.xrandr.get_virtual_screen()
except RuntimeError as e: except RuntimeError as e:
self.onError.emit(str(e)) self.promptError(str(e))
return return
clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}" clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}"
arg = f"x11vnc -rfbport {port} -clip {clip} {options}" arg = f"x11vnc -rfbport {port} -clip {clip} {options}"
@ -264,20 +269,20 @@ class Backend(QObject):
def openDisplaySetting(self, app: str = "arandr"): def openDisplaySetting(self, app: str = "arandr"):
# define callbacks # define callbacks
def _connected(): def _connected():
print("External Display Setting opened.") logging.info("External Display Setting opened.")
def _received(data): def _received(data):
pass pass
def _ended(exitCode): def _ended(exitCode):
print("External Display Setting closed.") logging.info("External Display Setting closed.")
self.onDisplaySettingClosed.emit() self.onDisplaySettingClosed.emit()
if exitCode is not 0: if exitCode is not 0:
self.onError.emit(f'Error opening "{running_program}".') self.promptError(f'Error opening "{running_program}".')
with open(DATA_PATH, 'r') as f: with open(DATA_PATH, 'r') as f:
data = json.load(f)['displaySettingApps'] data = json.load(f)['displaySettingApps']
if app not in data: if app not in data:
self.onError.emit('Wrong display settings program') self.promptError('Wrong display settings program')
return return
program_list = [data[app]['args'], "arandr"] program_list = [data[app]['args'], "arandr"]
program = AsyncSubprocess(_connected, _received, _received, _ended, None) program = AsyncSubprocess(_connected, _received, _received, _ended, None)
@ -288,12 +293,12 @@ class Backend(QObject):
running_program = arg running_program = arg
program.run(arg) program.run(arg)
return return
self.onError.emit('Failed to find a display settings program.\n' self.promptError('Failed to find a display settings program.\n'
'Please install ARandR package.\n' 'Please install ARandR package.\n'
'(e.g. sudo apt-get install arandr)\n' '(e.g. sudo apt-get install arandr)\n'
'Please issue a feature request\n' 'Please issue a feature request\n'
'if you wish to add a display settings\n' 'if you wish to add a display settings\n'
'program for your Desktop Environment.') 'program for your Desktop Environment.')
@pyqtSlot() @pyqtSlot()
def stopVNC(self, force=False): def stopVNC(self, force=False):
@ -304,7 +309,7 @@ class Backend(QObject):
if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED): if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED):
self.vncServer.close() self.vncServer.close()
else: else:
self.onError.emit("stopVNC called while it is not running") self.promptError("stopVNC called while it is not running")
@pyqtSlot() @pyqtSlot()
def clearCache(self): def clearCache(self):

View file

@ -3,6 +3,7 @@
import re import re
import atexit import atexit
import subprocess import subprocess
import logging
from typing import List from typing import List
from .display import Display from .display import Display
@ -53,9 +54,9 @@ class XRandR(SubprocessWrapper):
screen.height = int(match.group(7)) screen.height = int(match.group(7))
screen.x_offset = int(match.group(8)) screen.x_offset = int(match.group(8))
screen.y_offset = int(match.group(9)) screen.y_offset = int(match.group(9))
print("Display information:") logging.info("Display information:")
for s in self.screens: for s in self.screens:
print("\t", s) logging.info(f"\t{s}")
if self.primary_idx is None: if self.primary_idx is None:
raise RuntimeError("There is no primary screen detected.\n" raise RuntimeError("There is no primary screen detected.\n"
"Go to display settings and set\n" "Go to display settings and set\n"
@ -108,7 +109,7 @@ class XRandR(SubprocessWrapper):
def create_virtual_screen(self, width, height, portrait=False, hidpi=False, pos='') -> None: def create_virtual_screen(self, width, height, portrait=False, hidpi=False, pos='') -> None:
self._update_screens() self._update_screens()
print("creating: ", self.virt) logging.info(f"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'] arg_pos = ['left', 'right', 'above', 'below']
xrandr_pos = ['--left-of', '--right-of', '--above', '--below'] xrandr_pos = ['--left-of', '--right-of', '--above', '--below']