mirror of
https://github.com/kbumsik/VirtScreen.git
synced 2025-03-09 15:40:18 +00:00
initial program
This commit is contained in:
parent
5917fd833c
commit
1571514662
7 changed files with 487 additions and 0 deletions
4
.pylintrc
Normal file
4
.pylintrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
[MASTER]
|
||||
extension-pkg-whitelist=
|
||||
PyQt5,
|
||||
netifaces
|
1
.vscode/.gitignore
vendored
Normal file
1
.vscode/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
tags
|
23
.vscode/launch.json
vendored
Normal file
23
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
// 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}"
|
||||
}
|
||||
]
|
||||
}
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"python.venvPath": "${workspaceFolder}/ENV",
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"python.pythonPath": "${workspaceFolder}/ENV/bin/python"
|
||||
}
|
BIN
icon.png
Normal file
BIN
icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
PyQt5==5.10.1
|
||||
Twisted==17.9.0
|
||||
qt5reactor==0.5
|
||||
netifaces==0.10.6
|
448
virtscreen.py
Executable file
448
virtscreen.py
Executable file
|
@ -0,0 +1,448 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import os, re
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
from PyQt5.QtWidgets import (QAction, QApplication, QCheckBox, QComboBox,
|
||||
QDialog, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit,
|
||||
QMessageBox, QMenu, QPushButton, QSpinBox, QStyle, QSystemTrayIcon,
|
||||
QTextEdit, QVBoxLayout, QListWidget)
|
||||
from twisted.internet import protocol
|
||||
from netifaces import interfaces, ifaddresses, AF_INET
|
||||
import subprocess
|
||||
import atexit, signal
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Display properties
|
||||
#-------------------------------------------------------------------------------
|
||||
class DisplayProperty:
|
||||
def __init__(self):
|
||||
self.name: str
|
||||
self.width: int
|
||||
self.height: int
|
||||
self.x_offset: int
|
||||
self.y_offset: int
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Screen adjustment class
|
||||
#-------------------------------------------------------------------------------
|
||||
class XRandR:
|
||||
def __init__(self):
|
||||
self.mode_name: str
|
||||
self.scrren_suffix = "_virt"
|
||||
# Thoese will be created in set_virtual_screen()
|
||||
self.virt = DisplayProperty()
|
||||
self.virt.name = "VIRTUAL1"
|
||||
# Primary display
|
||||
self.primary = DisplayProperty()
|
||||
self._update_primary_screen()
|
||||
|
||||
def _call(self, arg) -> None:
|
||||
with open(os.devnull, "w") as nulldev:
|
||||
subprocess.call(arg.split(), stdout=nulldev, stderr=nulldev)
|
||||
|
||||
def _check_call(self, arg) -> None:
|
||||
with open(os.devnull, "w") as nulldev:
|
||||
subprocess.check_call(arg.split(), stdout=nulldev, stderr=nulldev)
|
||||
|
||||
def _run(self, arg: str) -> str:
|
||||
return subprocess.run(arg.split(), stdout=subprocess.PIPE).stdout.decode('utf-8')
|
||||
|
||||
def _add_screen_mode(self) -> None:
|
||||
args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}"
|
||||
try:
|
||||
self._check_call(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_call(f"xrandr --newmode {self.mode_name} {mode}")
|
||||
# Add mode again
|
||||
self._check_call(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 _update_primary_screen(self) -> None:
|
||||
output = self._run("xrandr")
|
||||
match = re.search(r"^(\w*)\s+.*primary\s*(\d+)x(\d+)\+(\d+)\+(\d+).*$", output, re.M)
|
||||
self.primary.name = match.group(1)
|
||||
self.primary.width = int(match.group(2))
|
||||
self.primary.height = int(match.group(3))
|
||||
self.primary.x_offset = int(match.group(4))
|
||||
self.primary.y_offset = int(match.group(5))
|
||||
|
||||
def _update_virtual_screen(self) -> None:
|
||||
output = self._run("xrandr")
|
||||
match = re.search(r"^" + self.virt.name + r"\s+.*\s+(\d+)x(\d+)\+(\d+)\+(\d+).*$", output, re.M)
|
||||
self.virt.width = int(match.group(1))
|
||||
self.virt.height = int(match.group(2))
|
||||
self.virt.x_offset = int(match.group(3))
|
||||
self.virt.y_offset = int(match.group(4))
|
||||
|
||||
def _signal_handler(self, signum=None, frame=None) -> None:
|
||||
self.delete_virtual_screen()
|
||||
os._exit(0)
|
||||
|
||||
def set_virtual_screen(self, width, height, portrait=False, hidpi=False):
|
||||
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.width
|
||||
self.virt.height = 2 * self.virt.height
|
||||
self.mode_name = str(self.virt.width) + "x" + str(self.virt.height) + self.scrren_suffix
|
||||
|
||||
def create_virtual_screen(self, position) -> None:
|
||||
self._add_screen_mode()
|
||||
self._check_call(f"xrandr --output {self.virt.name} --mode {self.mode_name}")
|
||||
self._check_call("sleep 5")
|
||||
self._check_call(f"xrandr --output {self.virt.name} {position} {self.primary.name}")
|
||||
self._update_primary_screen()
|
||||
self._update_virtual_screen()
|
||||
|
||||
def delete_virtual_screen(self) -> None:
|
||||
self._call(f"xrandr --output {self.virt.name} --off")
|
||||
self._call(f"xrandr --delmode {self.virt.name} {self.mode_name}")
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Twisted class
|
||||
#-------------------------------------------------------------------------------
|
||||
class ProcessProtocol(protocol.ProcessProtocol):
|
||||
def __init__(self, onOutReceived, onErrRecevied, onProcessEnded):
|
||||
self.onOutReceived = onOutReceived
|
||||
self.onErrRecevied = onErrRecevied
|
||||
self.onProcessEnded = onProcessEnded
|
||||
|
||||
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.transport.closeStdin() # No more input
|
||||
|
||||
def outReceived(self, data):
|
||||
print("outReceived! with %d bytes!" % len(data))
|
||||
self.onOutReceived(data)
|
||||
|
||||
def errReceived(self, data):
|
||||
print("outReceived! with %d bytes!" % len(data))
|
||||
self.onErrRecevied(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):
|
||||
exitCode = reason.value.exitCode
|
||||
if exitCode is None:
|
||||
print("Unknown exit")
|
||||
self.onProcessEnded(1)
|
||||
return
|
||||
print("processEnded, status", exitCode)
|
||||
print("quitting")
|
||||
self.onProcessEnded(exitCode)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Qt Window class
|
||||
#-------------------------------------------------------------------------------
|
||||
class Window(QDialog):
|
||||
def __init__(self):
|
||||
super(Window, self).__init__()
|
||||
# Create objects
|
||||
self.createDisplayGroupBox()
|
||||
self.createVNCGroupBox()
|
||||
self.createActions()
|
||||
self.createTrayIcon()
|
||||
self.xrandr = XRandR()
|
||||
# Additional attributes
|
||||
self.isVNCRunning = False
|
||||
# Update UI
|
||||
self.update_ip_address()
|
||||
# Put togather
|
||||
mainLayout = QVBoxLayout()
|
||||
mainLayout.addWidget(self.displayGroupBox)
|
||||
mainLayout.addWidget(self.VNCGroupBox)
|
||||
self.setLayout(mainLayout)
|
||||
# Events
|
||||
self.trayIcon.activated.connect(self.iconActivated)
|
||||
self.startVNCButton.pressed.connect(self.startPressed)
|
||||
self.VNCMessageListWidget.model().rowsInserted.connect(
|
||||
self.VNCMessageListWidget.scrollToBottom)
|
||||
# Show
|
||||
icon = QIcon("icon.png")
|
||||
self.trayIcon.setIcon(icon)
|
||||
self.setWindowIcon(icon)
|
||||
self.trayIcon.show()
|
||||
self.trayIcon.setToolTip("VirtScreen")
|
||||
self.setWindowTitle("VirtScreen")
|
||||
self.resize(400, 300)
|
||||
|
||||
def setVisible(self, visible):
|
||||
"""Override of setVisible(bool)
|
||||
|
||||
Arguments:
|
||||
visible {bool} -- true to show, false to hide
|
||||
"""
|
||||
self.openAction.setEnabled(self.isMaximized() or not visible)
|
||||
super(Window, self).setVisible(visible)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Override of closeEvent()
|
||||
|
||||
Arguments:
|
||||
event {QCloseEvent} -- QCloseEvent
|
||||
"""
|
||||
if self.trayIcon.isVisible():
|
||||
self.hide()
|
||||
self.showMessage()
|
||||
event.ignore()
|
||||
else:
|
||||
QApplication.instance().quit()
|
||||
|
||||
@pyqtSlot()
|
||||
def startPressed(self):
|
||||
if self.isVNCRunning:
|
||||
self.VNCServer.kill()
|
||||
else:
|
||||
self.startVNC()
|
||||
|
||||
@pyqtSlot('QSystemTrayIcon::ActivationReason')
|
||||
def iconActivated(self, reason):
|
||||
if reason in (QSystemTrayIcon.Trigger, QSystemTrayIcon.DoubleClick):
|
||||
if self.isVisible():
|
||||
self.hide()
|
||||
else:
|
||||
self.showNormal()
|
||||
elif reason == QSystemTrayIcon.MiddleClick:
|
||||
self.showMessage()
|
||||
|
||||
@pyqtSlot()
|
||||
def showMessage(self):
|
||||
icon = QSystemTrayIcon.MessageIcon(QSystemTrayIcon.Information)
|
||||
self.trayIcon.showMessage("VirtScreen is running",
|
||||
"The program will keep running in the system tray. To \n"
|
||||
"terminate the program, choose \"Quit\" in the \n"
|
||||
"context menu of the system tray entry.",
|
||||
icon,
|
||||
7 * 1000)
|
||||
|
||||
def createDisplayGroupBox(self):
|
||||
self.displayGroupBox = QGroupBox("Virtual Display Settings")
|
||||
|
||||
# First row
|
||||
positionLabel = QLabel("Position:")
|
||||
|
||||
self.displayPositionComboBox = QComboBox()
|
||||
self.displayPositionComboBox.addItem("Right", "--right-of")
|
||||
self.displayPositionComboBox.addItem("Left", "--left-of")
|
||||
self.displayPositionComboBox.addItem("Above", "--above")
|
||||
self.displayPositionComboBox.addItem("Below", "--below")
|
||||
self.displayPositionComboBox.setCurrentIndex(0)
|
||||
|
||||
self.displayPortraitCheckBox = QCheckBox("Portrait Mode")
|
||||
self.displayPortraitCheckBox.setChecked(False)
|
||||
|
||||
# Second row
|
||||
resolutionLabel = QLabel("Resolution:")
|
||||
|
||||
self.displayWidthSpinBox = QSpinBox()
|
||||
self.displayWidthSpinBox.setRange(640, 1920)
|
||||
self.displayWidthSpinBox.setSuffix(" px")
|
||||
self.displayWidthSpinBox.setValue(1368)
|
||||
|
||||
xLabel = QLabel("x")
|
||||
|
||||
self.displayHeightSpinBox = QSpinBox()
|
||||
self.displayHeightSpinBox.setRange(360, 1080)
|
||||
self.displayHeightSpinBox.setSuffix(" px")
|
||||
self.displayHeightSpinBox.setValue(1024)
|
||||
|
||||
self.displayHIDPICheckBox = QCheckBox("HiDPI (2x resolution)")
|
||||
self.displayHIDPICheckBox.setChecked(False)
|
||||
|
||||
# Putting them togather
|
||||
layout = QGridLayout()
|
||||
# Display Position row
|
||||
layout.addWidget(positionLabel, 0, 0)
|
||||
layout.addWidget(self.displayPositionComboBox, 0, 1, 1, 2)
|
||||
layout.addWidget(self.displayPortraitCheckBox, 0, 6, 1, 2, Qt.AlignLeft)
|
||||
# Resolution row
|
||||
layout.addWidget(resolutionLabel, 1, 0)
|
||||
layout.addWidget(self.displayWidthSpinBox, 1, 1, 1, 2)
|
||||
layout.addWidget(xLabel, 1, 3, Qt.AlignHCenter)
|
||||
layout.addWidget(self.displayHeightSpinBox, 1, 4, 1, 2)
|
||||
layout.addWidget(self.displayHIDPICheckBox, 1, 6, 1, 2, Qt.AlignLeft)
|
||||
# Set strectch
|
||||
layout.setColumnStretch(1, 0)
|
||||
layout.setColumnStretch(3, 0)
|
||||
# layout.setRowStretch(4, 1)
|
||||
|
||||
self.displayGroupBox.setLayout(layout)
|
||||
|
||||
def createVNCGroupBox(self):
|
||||
self.VNCGroupBox = QGroupBox("VNC Server")
|
||||
|
||||
portLabel = QLabel("Port:")
|
||||
self.VNCPortSpinBox = QSpinBox()
|
||||
self.VNCPortSpinBox.setRange(5900, 6000)
|
||||
self.VNCPortSpinBox.setValue(5900)
|
||||
|
||||
IPLabel = QLabel("Connect a VNC client to one of:")
|
||||
self.VNCIPListWidget = QListWidget()
|
||||
|
||||
self.startVNCButton = QPushButton("Start VNC Server")
|
||||
self.startVNCButton.setDefault(False)
|
||||
|
||||
messageLabel = QLabel("Server Messages")
|
||||
self.VNCMessageListWidget = QListWidget()
|
||||
self.VNCMessageListWidget.setEnabled(False)
|
||||
|
||||
# Set Overall layout
|
||||
layout = QVBoxLayout()
|
||||
portLayout = QHBoxLayout()
|
||||
portLayout.addWidget(portLabel)
|
||||
portLayout.addWidget(self.VNCPortSpinBox)
|
||||
portLayout.addStretch()
|
||||
layout.addLayout(portLayout)
|
||||
layout.addWidget(IPLabel)
|
||||
layout.addWidget(self.VNCIPListWidget)
|
||||
layout.addWidget(self.startVNCButton)
|
||||
layout.addSpacing(15)
|
||||
layout.addWidget(messageLabel)
|
||||
layout.addWidget(self.VNCMessageListWidget)
|
||||
self.VNCGroupBox.setLayout(layout)
|
||||
|
||||
def createActions(self):
|
||||
self.minimizeAction = QAction("&Start sharing", self)
|
||||
self.minimizeAction.triggered.connect(self.hide)
|
||||
|
||||
self.maximizeAction = QAction("S&top sharing", self)
|
||||
self.maximizeAction.triggered.connect(self.showMaximized)
|
||||
|
||||
self.openAction = QAction("&Open VirtScreen", self)
|
||||
self.openAction.triggered.connect(self.showNormal)
|
||||
|
||||
self.quitAction = QAction("&Quit", self)
|
||||
self.quitAction.triggered.connect(QApplication.instance().quit)
|
||||
|
||||
def createTrayIcon(self):
|
||||
self.trayIconMenu = QMenu(self)
|
||||
self.trayIconMenu.addAction(self.minimizeAction)
|
||||
self.trayIconMenu.addAction(self.maximizeAction)
|
||||
self.trayIconMenu.addSeparator()
|
||||
self.trayIconMenu.addAction(self.openAction)
|
||||
self.trayIconMenu.addSeparator()
|
||||
self.trayIconMenu.addAction(self.quitAction)
|
||||
|
||||
self.trayIcon = QSystemTrayIcon(self)
|
||||
self.trayIcon.setContextMenu(self.trayIconMenu)
|
||||
|
||||
def update_ip_address(self):
|
||||
self.VNCIPListWidget.clear()
|
||||
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:
|
||||
self.VNCIPListWidget.addItem(link['addr'])
|
||||
|
||||
def startVNC(self):
|
||||
def _onReceived(data):
|
||||
data = data.decode("utf-8")
|
||||
for line in data.splitlines():
|
||||
self.VNCMessageListWidget.addItem(line)
|
||||
def _onEnded(exitCode):
|
||||
self.startVNCButton.setEnabled(False)
|
||||
self.xrandr.delete_virtual_screen()
|
||||
self.isVNCRunning = False
|
||||
self.VNCMessageListWidget.setEnabled(False)
|
||||
self.startVNCButton.setText("Start VNC Server")
|
||||
self.startVNCButton.setEnabled(True)
|
||||
# Setting UI before starting
|
||||
self.VNCMessageListWidget.clear()
|
||||
self.startVNCButton.setEnabled(False)
|
||||
self.startVNCButton.setText("Running...")
|
||||
# Create virtual screen
|
||||
width = self.displayWidthSpinBox.value()
|
||||
height = self.displayHeightSpinBox.value()
|
||||
portrait = self.displayPortraitCheckBox.isChecked()
|
||||
hidpi = self.displayHIDPICheckBox.isChecked()
|
||||
position = self.displayPositionComboBox.currentData()
|
||||
self.xrandr.set_virtual_screen(width, height, portrait, hidpi)
|
||||
self.xrandr.create_virtual_screen(position)
|
||||
# Run VNC server
|
||||
self.isVNCRunning = True
|
||||
self.VNCServer = ProcessProtocol(_onReceived, _onReceived, _onEnded)
|
||||
port = self.VNCPortSpinBox.value()
|
||||
virt = self.xrandr.virt
|
||||
clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}"
|
||||
arg = f"x11vnc -multiptr -repeat -rfbport {port} -clip {clip}"
|
||||
self.VNCServer.run(arg)
|
||||
# Change UI
|
||||
self.VNCMessageListWidget.setEnabled(True)
|
||||
self.startVNCButton.setEnabled(True)
|
||||
self.startVNCButton.setText("Stop")
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Main Code
|
||||
#-------------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||
QMessageBox.critical(None, "Systray",
|
||||
"I couldn't detect any system tray on this system.")
|
||||
sys.exit(1)
|
||||
|
||||
if os.environ['XDG_SESSION_TYPE'] == 'wayland':
|
||||
QMessageBox.critical(None, "Wayland Session",
|
||||
"Currently Wayland is not supported")
|
||||
sys.exit(1)
|
||||
|
||||
import qt5reactor # pylint: disable=E0401
|
||||
qt5reactor.install()
|
||||
from twisted.internet import utils, reactor # pylint: disable=E0401
|
||||
|
||||
QApplication.setQuitOnLastWindowClosed(False)
|
||||
window = Window()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
reactor.run()
|
Loading…
Add table
Add a link
Reference in a new issue