I'm still working on all these motherfuckin' PAGES!
Let me start by saying that, Pholidota, for all it is, works shoddily, and this is a slightly non-functional version of it. Nonetheless, have fun with it if you so please. I don't particularly enjoy using Git, so I figured I'd share Pholidota in possibly the most horrific fucking way imaginable. Enjoy!
{
"flags":["FramelessWindowHint", "WindowTransparentForInput", "WindowStaysOnTopHint", "X11BypassWindowManagerHint"],
"attributes":["WA_TranslucentBackground"],
"color":[0, 0, 0, 128]
}
from scripts.pholidota import Pholidota
if __name__ == "__main__":
pholidota = Pholidota()
pholidota.exec()
from scripts.controlPanel import ControlPanel
from PySide6 import QtWidgets
from PySide6 import QtCore
from typing import Tuple
from typing import List
import configparser
import atexit
import uuid
import json
import dbus
import sys
import os
# The main class of the program, Pholidota - does the heavy lifting.
class Pholidota(QtWidgets.QApplication):
# I erect the class instance.
def __init__(self):
super().__init__([])
self.setApplicationName("Pholidota")
self.setApplicationDisplayName("Pholidota")
if self.getDesktopType() is True:
self.sessionBus = dbus.SessionBus()
self.uuid = None
self.createOverlayRule()
# LAST IN FIRST OUT - WE FUCKING LOVE AND HATE THE GODDAMN STACK.
atexit.register(self.sessionBus.close)
atexit.register(self.destroyOverlayRule)
self.controlPanel = ControlPanel(self, self.loadOverlaySettings())
self.controlPanel.show()
# Check if the user is running KDE Plasma and Wayland.
@staticmethod
def getDesktopType() -> bool:
desktopEnvironment = os.environ.get("XDG_CURRENT_DESKTOP")
displayServer = os.environ.get("XDG_SESSION_TYPE")
if desktopEnvironment == "KDE" and displayServer == "wayland":
return True
else:
return False
# Path to any data files made simple.
@staticmethod
def getDataPath(directory: str, file: str) -> str:
if getattr(sys, "frozen", False):
basePath = sys._MEIPASS
else:
basePath = os.path.abspath(".")
fullPath = os.path.join(basePath, directory, file)
return fullPath
# Path to Plasma's custom window rules.
@staticmethod
def getRulesPath() -> str:
kWinRulesPath = os.path.expanduser("~/.config/kwinrulesrc")
return kWinRulesPath
# Grabs the overlay settings for the ControlPanel and Overlay to use.
def loadOverlaySettings(self) -> Tuple[int, List[QtCore.Qt.WidgetAttribute]]:
settingsPath = self.getDataPath("data", "overlaySettings.json")
with open(settingsPath) as file:
overlaySettings = json.load(file)
color = overlaySettings["color"]
attributes = []
flags = 0
for overlayFlag in overlaySettings["flags"]:
flags |= getattr(QtCore.Qt.WindowType, overlayFlag)
for overlayAttribute in overlaySettings["attributes"]:
attributes.append(getattr(QtCore.Qt.WidgetAttribute, overlayAttribute))
return flags, color, attributes
# Creates a custom overlay rule for KDE Plasma + Wayland.
# Enforces always-on-top behavior for any overlays created by Pholidota.
def createOverlayRule(self) -> None:
self.uuid = str(uuid.uuid4()) # 5.3 x 10^36 possible V4 UUIDs.
# Rule variables for ease of readability and anti-fuckup-ness.
generalSection = "General"
generalOptionCount = "count"
generalOptionRules = "rules"
kWinService = "org.kde.KWin"
kWinObjectPath = "/KWin"
# Rules for the UUID section of kwinrulesrc.
# Okay, so here's the rub - We have to force the overlay to sit above other windows.
# This breaks in fullscreen, so we have to define the overlay's layer as an overlay.
# Lastly, the taskbar stays full-opacity, so we have to force the overlay to be fullscreen.
# Unfortunately, when alt-tabbing this shit does not work which actually makes a lot of sense - No clue how to fix that.
uuidRules = {
"Description": "Temporary window settings for Pholidota.",
"above": "true",
"aboverule": "2",
"fullscreen": "true",
"fullscreenrule": "2",
"layer": "overlay",
"layerrule": "2",
"title": "Pholidota : Overlay — Pholidota",
"titlematch": "1",
"types": "1",
"wmclass": "Pholidota",
"wmclassmatch": "1",
}
kWinRulesPath = self.getRulesPath()
configurationEditor = configparser.ConfigParser()
# If kwinrulesrc doesn't exist, we must breathe life into it otherwise program kaboom!
if not os.path.exists(kWinRulesPath):
with open(kWinRulesPath, "w") as plasmaWindowRules:
plasmaWindowRules.write("")
with open(kWinRulesPath, "r") as plasmaWindowRules:
configurationEditor.read_file(plasmaWindowRules)
if configurationEditor.has_section(generalSection):
# Up the rule count to accomodate the new rule.
oldCount = configurationEditor[generalSection][generalOptionCount]
newCount = str(int(oldCount) + 1)
# Append the new rule to the end of the rules string.
oldRules = configurationEditor[generalSection][generalOptionRules]
newRules = oldRules + "," + self.uuid
# Update to the general section's rule count and rules with the new rules.
configurationEditor[generalSection][generalOptionCount] = newCount
configurationEditor[generalSection][generalOptionRules] = newRules
else:
configurationEditor[generalSection] = {
generalOptionCount: "1", # configparser hates non-strings.
generalOptionRules: self.uuid,
}
# Create a new section for the uuid rule and pass it the dictionary.
configurationEditor[self.uuid] = uuidRules
with open(kWinRulesPath, "w") as plasmaWindowRules:
configurationEditor.write(plasmaWindowRules, space_around_delimiters=False)
# Ask KDE to reload its configuration with the updated kwinrulesrc file.
kWinProxy = self.sessionBus.get_object(kWinService, kWinObjectPath)
kWinInterface = dbus.Interface(kWinProxy, kWinService)
kWinInterface.reconfigure()
# Deletes and cleans up the custom overlay rule for KDE Plasma + Wayland.
def destroyOverlayRule(self) -> None:
# Protect against edge-case voodoo.
if self.uuid is None:
return
# Rule variables for ease of readability and anti-fuckup-ness.
generalSection = "General"
generalOptionCount = "count"
generalOptionRules = "rules"
kWinService = "org.kde.KWin"
kWinObjectPath = "/KWin"
# Variables to ensure kwinrulesrc was actually cleaned!
cleanedGeneral = False
cleanedUUID = False
kWinRulesPath = self.getRulesPath()
configurationEditor = configparser.ConfigParser()
with open(kWinRulesPath, "r") as plasmaWindowRules:
configurationEditor.read_file(plasmaWindowRules)
if configurationEditor.has_section(generalSection):
# Up the rule count to represent the old rule.
newCount = configurationEditor[generalSection][generalOptionCount]
oldCount = int(newCount) - 1
# If the count is zero, there are no other rules - KABOOM goes [General]!
if oldCount <= 0:
configurationEditor.remove_section(generalSection)
else:
# Cast it back ONLY if we need to.
oldCount = str(oldCount)
# Parse the new rules to remove the appended UUID from the rules string.
newRules = configurationEditor[generalSection][generalOptionRules]
splitRules = newRules.split(",")
# Ensure the UUID is inside the rules, if it isn't... it probably isn't my fault.
if self.uuid in splitRules:
splitRules.remove(self.uuid)
oldRules = ",".join(splitRules)
# Update to the general section's rule count and rules with the old rules.
configurationEditor[generalSection][
generalOptionCount
] = oldCount
configurationEditor[generalSection][
generalOptionRules
] = oldRules
cleanedGeneral = True
if configurationEditor.has_section(self.uuid):
# Nuke the temporary overlay window rules section.
configurationEditor.remove_section(self.uuid)
cleanedUUID = True
# Make sure we actually cleaned ANYTHING up before making it real.
if cleanedGeneral or cleanedUUID:
with open(kWinRulesPath, "w") as plasmaWindowRules:
configurationEditor.write(
plasmaWindowRules, space_around_delimiters=False
)
# Ask KDE to reload its configuration with the updated kwinrulesrc file.
kWinProxy = self.sessionBus.get_object(kWinService, kWinObjectPath)
kWinInterface = dbus.Interface(kWinProxy, kWinService)
kWinInterface.reconfigure()
from scripts.overlay import Overlay
from functools import partial
from PySide6 import QtWidgets
from PySide6 import QtCore
from PySide6 import QtGui
from typing import Tuple
from typing import List
# Used for controlling the whole program - overlays, closing, settings, etc.
class ControlPanel(QtWidgets.QWidget):
#
def __init__(
self,
pholidota: QtWidgets.QApplication,
overlayConfig: Tuple[int, List[int], List[QtCore.Qt.WidgetAttribute]],
):
super().__init__()
#
self.setWindowTitle("Pholidota : ControlPanel")
self.setGeometry(128, 128, 64, 128)
#
self.pholidota = pholidota
self.layout = QtWidgets.QGridLayout(self)
self.layout.setVerticalSpacing(10)
self.layout.setHorizontalSpacing(10)
#
self.overlay = None
self.overlayFlags = overlayConfig[0]
self.overlayColor = QtGui.QColor(*overlayConfig[1])
self.overlayAttributes = overlayConfig[2]
# TODO - Make a method to create these buttons or use style sheets.
# TODO - Add controls for increasing/decreasing the overlay transparency.
# The button to create the overlay!
self.createButton = QtWidgets.QPushButton("Create Overlay")
self.createButton.setMinimumHeight(32)
self.layout.addWidget(self.createButton, 0, 0, 1, 0)
self.createButton.clicked.connect(self.createOverlay)
# The button to destroy the overlay!
self.destroyButton = QtWidgets.QPushButton("Destroy Overlay")
self.destroyButton.setMinimumHeight(32)
self.destroyButton.setDisabled(True)
self.layout.addWidget(self.destroyButton, 1, 0, 1, 0)
self.destroyButton.clicked.connect(self.destroyOverlay)
# The button to increment the overlay's opacity!
self.incrementButton = QtWidgets.QPushButton("+")
self.incrementButton.setMinimumHeight(32)
self.incrementButton.setDisabled(True)
self.layout.addWidget(self.incrementButton, 2, 0)
self.incrementButton.clicked.connect(
partial(self.adjustOverlayOpacity, "increment")
)
# The visual representation of the overlay's opacity!
self.opacityDisplay = QtWidgets.QLabel()
self.opacityDisplay.setText(str(round(self.overlayColor.alphaF() * 100)) + "%")
self.opacityDisplay.setMinimumHeight(32)
self.opacityDisplay.setMinimumWidth(32)
self.opacityDisplay.setDisabled(True)
self.layout.addWidget(
self.opacityDisplay, 2, 1, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter
)
# The button to decrement the overlay's opacity!
self.decrementButton = QtWidgets.QPushButton("-")
self.decrementButton.setMinimumHeight(32)
self.decrementButton.setDisabled(True)
self.layout.addWidget(self.decrementButton, 2, 2)
self.decrementButton.clicked.connect(
partial(self.adjustOverlayOpacity, "decrement")
)
# The button to close the application!
self.closeButton = QtWidgets.QPushButton("Close Application")
self.closeButton.setMinimumHeight(32)
self.layout.addWidget(self.closeButton, 3, 0, 1, 0)
self.closeButton.clicked.connect(self.closePholidota)
# Create an overlay on the screen.
@QtCore.Slot()
def createOverlay(self):
#
if self.overlay is None:
# Enable the overlay-centric buttons!
self.destroyButton.setEnabled(True)
self.incrementButton.setEnabled(True)
self.opacityDisplay.setEnabled(True)
self.decrementButton.setEnabled(True)
self.overlay = Overlay(self.overlayColor)
self.overlay.setWindowFlags(self.overlayFlags)
for widgetAttribute in self.overlayAttributes:
self.overlay.setAttribute(widgetAttribute)
self.overlay.show()
# Destroy the overlay on the screen.
@QtCore.Slot()
def destroyOverlay(self):
#
if self.overlay is not None:
# Disable the overlay-centric buttons!
self.destroyButton.setDisabled(True)
self.incrementButton.setDisabled(True)
self.opacityDisplay.setDisabled(True)
self.decrementButton.setDisabled(True)
# Blow that motherfucker sky high.
self.overlay.deleteLater()
self.overlay = None
# Ask the overlay to increment or decrement its opacity.
@QtCore.Slot()
def adjustOverlayOpacity(self, operation: str):
#
if self.overlay is not None and (
operation == "increment" or operation == "decrement"
):
# Get the float directly and add or subtract % step's to it.
oldOpacity = self.overlay.color.alphaF()
opacityStep = 0.05
# Should never hit the default case, but stranger things have transpired.
match operation:
case "increment":
newOpacity = oldOpacity + opacityStep
case "decrement":
newOpacity = oldOpacity - opacityStep
case default:
newOpacity = oldOpacity
# Ensure that opacity is never outside the normalized bounds.
# I really don't care if the user sets it to fully black - not my problem.
if newOpacity > 1.0:
newOpacity = 1.0
elif newOpacity < 0.0:
newOpacity = 0.0
# Recolor the overlay and ask it to repaint itself.
self.overlayColor.setAlphaF(newOpacity)
self.overlay.color.setAlphaF(newOpacity)
self.opacityDisplay.setText(str(round(newOpacity * 100)) + "%")
self.overlay.update()
# Close Pholidota and make sure everything is cleaned up.
@QtCore.Slot()
def closePholidota(self):
self.destroyOverlay() # Call me a hypochondriac and cry me a river.
self.pholidota.quit()
from PySide6 import QtWidgets
from PySide6 import QtCore
from PySide6 import QtGui
# The Window that acts as an artifical dimmer on the screen.
class Overlay(QtWidgets.QWidget):
#
def __init__(self, color):
super().__init__()
self.setWindowTitle("Pholidota : Overlay")
self.setGeometry(0, 0, 1920, 1080)
# TODO - Move this base color into the overlaySettings.
# TODO - Maybe make it editable in the control panel too.
self.color = color
# Paint the whole overlay - I have to do this because
# filling it normally does not work properly.
def paintEvent(self, event):
with QtGui.QPainter(self) as painter:
painter.setPen(QtCore.Qt.PenStyle.NoPen)
painter.setBrush(self.color)
painter.drawRect(self.rect())