refactored by gemini to python

This commit is contained in:
Dawson Matthews
2026-03-08 18:09:01 -06:00
parent 768d878981
commit acd351f559
8 changed files with 202 additions and 475 deletions
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
import argparse
import json
import subprocess
import sys
import os
VERBOSE = True
def log(*args):
if VERBOSE:
print(*args)
def run_command(cmd):
log(f"[CMD] {cmd}")
try:
subprocess.run(cmd, shell=True, check=True)
except subprocess.CalledProcessError as e:
log(f"Error executing command: {e}")
def get_mode_string(output, mode_id):
for mode in output.get('modes', []):
if mode['id'] == mode_id:
width = mode['size']['width']
height = mode['size']['height']
refresh_rate = round(mode['refreshRate'])
return f"{width}x{height}@{refresh_rate}"
return None
def save_profile(profile_path):
log(f"Saving current display profile to {profile_path}...")
try:
with open(profile_path, 'w') as f:
subprocess.run(['kscreen-doctor', '--json'], stdout=f, check=True)
log("Profile saved successfully.")
except Exception as e:
print(f"Error saving profile: {e}", file=sys.stderr)
sys.exit(1)
def apply_attribute(output_name, attribute, value, value_map=None):
if value is None:
log(f"Output {output_name} has no attribute {attribute}, skipping...")
return
# If it's a list or dict, something might be wrong or it's a complex structure we don't handle this way
if isinstance(value, (list, dict)):
log(f"Output {output_name} attribute {attribute} has complex value {value}, skipping...")
return
str_value = str(value).lower()
if value_map:
if str_value in value_map:
value = value_map[str_value]
else:
log(f"Warning: Value {str_value} not found in map for {attribute}, using as is.")
cmd = f"kscreen-doctor output.{output_name}.{attribute}.{value}"
run_command(cmd)
def load_profile(profile_path):
log(f"Loading display profile from {profile_path}...")
if not os.path.exists(profile_path):
print(f"Profile file not found: {profile_path}", file=sys.stderr)
sys.exit(1)
try:
with open(profile_path, 'r') as f:
profile = json.load(f)
except Exception as e:
print(f"Error reading profile JSON: {e}", file=sys.stderr)
sys.exit(1)
outputs = profile.get('outputs', [])
# Maps from JSON values to kscreen-doctor options
bool_enable_map = {"true": "enable", "false": "disable"}
bool_allow_map = {"true": "allow", "false": "disallow"}
rgb_range_map = {
"0": "automatic", "1": "full", "2": "limited",
"automatic": "automatic", "full": "full", "limited": "limited"
}
rotation_map = {"1": "normal", "2": "left", "4": "inverted", "8": "right"}
vrr_policy_map = {
"0": "never", "1": "always", "2": "automatic",
"never": "never", "always": "always", "automatic": "automatic"
}
# 1. Handle Enable/Disable (Enable first to avoid no-output state)
log("Enabling enabled outputs...")
for out in outputs:
if out.get('enabled'):
run_command(f"kscreen-doctor output.{out['name']}.enable")
log("Disabling disabled outputs...")
for out in outputs:
if not out.get('enabled'):
run_command(f"kscreen-doctor output.{out['name']}.disable")
# 2. Apply other attributes
for out in outputs:
name = out['name']
log(f"Configuring output: {name}")
# WCG
apply_attribute(name, "wcg", out.get('wcg'), bool_enable_map)
# SDR Brightness
apply_attribute(name, "sdr-brightness", out.get('sdr-brightness'))
# VRR Policy
apply_attribute(name, "vrrpolicy", out.get('vrrPolicy'), vrr_policy_map)
# RGB Range
apply_attribute(name, "rgbrange", out.get('rgbRange'), rgb_range_map)
# Overscan
apply_attribute(name, "overscan", out.get('overscan'))
# HDR
apply_attribute(name, "hdr", out.get('hdr'), bool_enable_map)
# Brightness (0.0-1.0 to 0-100)
brightness = out.get('brightness')
if brightness is not None:
apply_attribute(name, "brightness", int(float(brightness) * 100))
# Max BPC
max_bpc = out.get('maxBpc')
if max_bpc == 0:
max_bpc = "automatic"
apply_attribute(name, "maxbpc", max_bpc)
# DDC/CI
apply_attribute(name, "ddcCi", out.get('ddcCiAllowed'), bool_allow_map)
# Mirroring
replication_source_id = out.get('replicationSource', 0)
if replication_source_id != 0:
# Find the name of the replication source
source_name = "none"
for other_out in outputs:
if other_out.get('id') == replication_source_id:
source_name = other_out['name']
break
apply_attribute(name, "mirror", source_name)
else:
apply_attribute(name, "mirror", "none")
# ICC Profile
icc_path = out.get('iccProfilePath')
if icc_path:
apply_attribute(name, "iccProfilePath", icc_path)
# Mode
mode_id = out.get('currentModeId')
if mode_id:
mode_str = get_mode_string(out, mode_id)
if mode_str:
apply_attribute(name, "mode", mode_str)
# Position
pos = out.get('pos')
if pos:
apply_attribute(name, "position", f"{pos['x']},{pos['y']}")
# Scale
apply_attribute(name, "scale", out.get('scale'))
# Rotation
apply_attribute(name, "rotation", out.get('rotation'), rotation_map)
# Priority and Primary
priority = out.get('priority')
if priority is not None:
if priority == 1:
run_command(f"kscreen-doctor output.{name}.primary")
run_command(f"kscreen-doctor output.{name}.priority.{priority}")
log("Display configuration restored.")
def main():
parser = argparse.ArgumentParser(description="KDE Display Profile Manager")
subparsers = parser.add_subparsers(dest="command", required=True)
# Save command
save_parser = subparsers.add_parser("save", help="Save current display configuration to a profile")
save_parser.add_argument("profile", help="Path to the profile file (e.g., profiles/myprofile.json)")
# Load command
load_parser = subparsers.add_parser("load", help="Load a display configuration from a profile")
load_parser.add_argument("profile", help="Path to the profile file")
args = parser.parse_args()
if args.command == "save":
save_profile(args.profile)
elif args.command == "load":
load_profile(args.profile)
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
-21
View File
@@ -1,21 +0,0 @@
import sys
from PyQt6.QtWidgets import QApplication
from PyQt6.QtGui import QIcon
from mainwindow import MainWindow
def main():
app = QApplication(sys.argv)
app.setApplicationName("Display Profile Manager")
app.setOrganizationName("KDE")
app.setOrganizationDomain("kde.org")
# Set application icon (we'll add this later)
# app.setWindowIcon(QIcon.fromTheme("video-display"))
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
-263
View File
@@ -1,263 +0,0 @@
from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QListWidget, QListWidgetItem,
QLabel, QGroupBox, QLineEdit, QMessageBox,
QMenu, QMenuBar)
from PyQt6.QtCore import Qt, QSize
from PyQt6.QtGui import QAction, QKeySequence, QIcon
from save_dialog import SaveProfileDialog
from profile_manager import ProfileManager
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.profile_manager = ProfileManager()
self.init_ui()
self.load_profiles()
def init_ui(self):
self.setWindowTitle("Display Profile Manager")
self.setMinimumSize(800, 600)
# Create menu bar
self.create_menu_bar()
# Create central widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
# Main layout
main_layout = QHBoxLayout(central_widget)
# Left panel - Profile list
left_panel = self.create_left_panel()
main_layout.addWidget(left_panel, 2)
# Right panel - Profile details
right_panel = self.create_right_panel()
main_layout.addWidget(right_panel, 1)
def create_menu_bar(self):
menubar = self.menuBar()
# File menu
file_menu = menubar.addMenu("&File")
save_action = QAction("&Save Current Display", self)
save_action.setShortcut(QKeySequence("Ctrl+S"))
save_action.triggered.connect(self.save_profile)
file_menu.addAction(save_action)
file_menu.addSeparator()
quit_action = QAction("&Quit", self)
quit_action.setShortcut(QKeySequence("Ctrl+Q"))
quit_action.triggered.connect(self.close)
file_menu.addAction(quit_action)
# Edit menu
edit_menu = menubar.addMenu("&Edit")
rename_action = QAction("&Rename Profile", self)
rename_action.setShortcut(QKeySequence("F2"))
rename_action.triggered.connect(self.rename_profile)
edit_menu.addAction(rename_action)
delete_action = QAction("&Delete Profile", self)
delete_action.setShortcut(QKeySequence("Delete"))
delete_action.triggered.connect(self.delete_profile)
edit_menu.addAction(delete_action)
# Help menu
help_menu = menubar.addMenu("&Help")
about_action = QAction("&About", self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def create_left_panel(self):
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
# Title
title_label = QLabel("Saved Profiles")
title_label.setStyleSheet("font-size: 14pt; font-weight: bold;")
left_layout.addWidget(title_label)
# Profile list
self.profile_list = QListWidget()
self.profile_list.itemSelectionChanged.connect(self.on_profile_selected)
self.profile_list.itemDoubleClicked.connect(self.load_selected_profile)
left_layout.addWidget(self.profile_list)
# Buttons
button_layout = QHBoxLayout()
self.save_btn = QPushButton("Save Current")
self.save_btn.clicked.connect(self.save_profile)
button_layout.addWidget(self.save_btn)
self.load_btn = QPushButton("Load")
self.load_btn.clicked.connect(self.load_selected_profile)
self.load_btn.setEnabled(False)
button_layout.addWidget(self.load_btn)
self.delete_btn = QPushButton("Delete")
self.delete_btn.clicked.connect(self.delete_profile)
self.delete_btn.setEnabled(False)
button_layout.addWidget(self.delete_btn)
left_layout.addLayout(button_layout)
return left_widget
def create_right_panel(self):
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
# Profile details group
details_group = QGroupBox("Profile Details")
details_layout = QVBoxLayout(details_group)
# Profile name
name_layout = QHBoxLayout()
name_layout.addWidget(QLabel("Name:"))
self.name_edit = QLineEdit()
self.name_edit.setReadOnly(True)
name_layout.addWidget(self.name_edit)
details_layout.addLayout(name_layout)
# Keyboard shortcut
shortcut_layout = QHBoxLayout()
shortcut_layout.addWidget(QLabel("Shortcut:"))
self.shortcut_label = QLabel("None")
shortcut_layout.addWidget(self.shortcut_label)
shortcut_layout.addStretch()
self.set_shortcut_btn = QPushButton("Set Shortcut")
self.set_shortcut_btn.setEnabled(False)
self.set_shortcut_btn.clicked.connect(self.set_shortcut)
shortcut_layout.addWidget(self.set_shortcut_btn)
details_layout.addLayout(shortcut_layout)
# Profile info
self.info_label = QLabel("Select a profile to view details")
self.info_label.setWordWrap(True)
self.info_label.setAlignment(Qt.AlignmentFlag.AlignTop)
details_layout.addWidget(self.info_label)
details_layout.addStretch()
right_layout.addWidget(details_group)
right_layout.addStretch()
return right_widget
def load_profiles(self):
"""Load and display all saved profiles"""
self.profile_list.clear()
profiles = self.profile_manager.list_profiles()
for profile in profiles:
item = QListWidgetItem(profile['name'])
item.setData(Qt.ItemDataRole.UserRole, profile)
self.profile_list.addItem(item)
def save_profile(self):
"""Show dialog to save current display configuration"""
dialog = SaveProfileDialog(self.profile_manager, self)
if dialog.exec():
profile_name = dialog.get_profile_name()
try:
self.profile_manager.save_profile(profile_name)
self.load_profiles()
QMessageBox.information(self, "Success",
f"Profile '{profile_name}' saved successfully!")
except Exception as e:
QMessageBox.critical(self, "Error",
f"Failed to save profile: {str(e)}")
def load_selected_profile(self):
"""Load the selected profile"""
current_item = self.profile_list.currentItem()
if not current_item:
return
profile = current_item.data(Qt.ItemDataRole.UserRole)
try:
self.profile_manager.load_profile(profile['id'])
QMessageBox.information(self, "Success",
f"Profile '{profile['name']}' loaded successfully!")
except Exception as e:
QMessageBox.critical(self, "Error",
f"Failed to load profile: {str(e)}")
def delete_profile(self):
"""Delete the selected profile"""
current_item = self.profile_list.currentItem()
if not current_item:
return
profile = current_item.data(Qt.ItemDataRole.UserRole)
reply = QMessageBox.question(self, "Confirm Delete",
f"Are you sure you want to delete '{profile['name']}'?",
QMessageBox.StandardButton.Yes |
QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
try:
self.profile_manager.delete_profile(profile['id'])
self.load_profiles()
QMessageBox.information(self, "Success",
f"Profile '{profile['name']}' deleted successfully!")
except Exception as e:
QMessageBox.critical(self, "Error",
f"Failed to delete profile: {str(e)}")
def rename_profile(self):
"""Rename the selected profile"""
current_item = self.profile_list.currentItem()
if not current_item:
return
# Make item editable
self.profile_list.editItem(current_item)
def on_profile_selected(self):
"""Handle profile selection"""
current_item = self.profile_list.currentItem()
has_selection = current_item is not None
self.load_btn.setEnabled(has_selection)
self.delete_btn.setEnabled(has_selection)
self.set_shortcut_btn.setEnabled(has_selection)
if has_selection:
profile = current_item.data(Qt.ItemDataRole.UserRole)
self.name_edit.setText(profile['name'])
self.name_edit.setReadOnly(False)
shortcut = profile.get('shortcut', 'None')
self.shortcut_label.setText(shortcut)
# Display profile info
info_text = f"Created: {profile.get('created', 'Unknown')}\n"
info_text += f"Last Modified: {profile.get('modified', 'Unknown')}"
self.info_label.setText(info_text)
else:
self.name_edit.clear()
self.name_edit.setReadOnly(True)
self.shortcut_label.setText("None")
self.info_label.setText("Select a profile to view details")
def set_shortcut(self):
"""Set keyboard shortcut for selected profile"""
QMessageBox.information(self, "Coming Soon",
"Keyboard shortcut configuration will be implemented next!")
def show_about(self):
"""Show about dialog"""
QMessageBox.about(self, "About Display Profile Manager",
"Display Profile Manager\n\n"
"Manage KDE Plasma display configurations\n\n"
"Save, load, and manage multiple display profiles\n"
"with keyboard shortcuts.")
-114
View File
@@ -1,114 +0,0 @@
import os
import json
import subprocess
from datetime import datetime
from pathlib import Path
import uuid
class ProfileManager:
def __init__(self):
self.config_dir = Path.home() / ".local/share/plasma-display-profiles"
self.config_dir.mkdir(parents=True, exist_ok=True)
self.metadata_file = self.config_dir / "profiles.json"
self.script_path = self.find_script()
# Initialize metadata file if it doesn't exist
if not self.metadata_file.exists():
self.save_metadata([])
def find_script(self):
"""Find the bash script for display management"""
return "scripts/"
def list_profiles(self):
"""Return list of all profiles"""
return self.load_metadata()
def save_profile(self, name):
"""Save current display configuration as a new profile"""
profiles = self.load_metadata()
# Generate unique ID
profile_id = str(uuid.uuid4())
timestamp = datetime.now().isoformat()
# Create profile entry
profile = {
'id': profile_id,
'name': name,
'created': timestamp,
'modified': timestamp,
'script_file': f"{profile_id}.sh",
'shortcut': None
}
# TODO: Call bash script to save display configuration
# For now, create a placeholder script file
script_file = self.config_dir / profile['script_file']
script_file.write_text(f"#!/bin/bash\n# Display profile: {name}\n# Created: {timestamp}\n")
script_file.chmod(0o755)
# If script exists, call it
if self.script_path and self.script_path.exists():
try:
subprocess.run([str(self.script_path), 'save', str(script_file)],
check=True, capture_output=True)
except subprocess.CalledProcessError as e:
raise Exception(f"Script execution failed: {e.stderr.decode()}")
profiles.append(profile)
self.save_metadata(profiles)
def load_profile(self, profile_id):
"""Load a display profile"""
profiles = self.load_metadata()
profile = next((p for p in profiles if p['id'] == profile_id), None)
if not profile:
raise Exception(f"Profile not found: {profile_id}")
script_file = self.config_dir / profile['script_file']
if not script_file.exists():
raise Exception(f"Profile script not found: {script_file}")
# Execute the profile script
try:
if self.script_path and self.script_path.exists():
subprocess.run([str(self.script_path), 'load', str(script_file)],
check=True, capture_output=True)
else:
# Fallback: execute script directly
subprocess.run([str(script_file)], check=True, capture_output=True)
except subprocess.CalledProcessError as e:
raise Exception(f"Failed to load profile: {e.stderr.decode()}")
def delete_profile(self, profile_id):
"""Delete a profile"""
profiles = self.load_metadata()
profile = next((p for p in profiles if p['id'] == profile_id), None)
if not profile:
raise Exception(f"Profile not found: {profile_id}")
# Remove script file
script_file = self.config_dir / profile['script_file']
if script_file.exists():
script_file.unlink()
# Remove from metadata
profiles = [p for p in profiles if p['id'] != profile_id]
self.save_metadata(profiles)
def rename_profile(self, profile_id, new_name):
"""Rename a profile"""
profiles = self.load_metadata()
profile = next((p for p in profiles if p['id'] == profile_id), None)
if not profile:
raise Exception(f"Profile not found: {profile_id}")
profile['name'] = new_name
profile['modified'] = datetime.now().isoformat()
self.save_metadata(profiles)
-77
View File
@@ -1,77 +0,0 @@
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QMessageBox)
from PyQt6.QtCore import Qt
class SaveProfileDialog(QDialog):
def __init__(self, profile_manager, parent=None):
super().__init__(parent)
self.profile_manager = profile_manager
self.init_ui()
def init_ui(self):
self.setWindowTitle("Save Display Profile")
self.setMinimumWidth(400)
layout = QVBoxLayout(self)
# Instruction label
info_label = QLabel("Enter a name for this display profile:")
layout.addWidget(info_label)
# Profile name input
self.name_input = QLineEdit()
self.name_input.setPlaceholderText(self.get_default_name())
self.name_input.textChanged.connect(self.validate_input)
layout.addWidget(self.name_input)
# Hint label
hint_label = QLabel("Leave empty to use default name")
hint_label.setStyleSheet("color: gray; font-size: 9pt;")
layout.addWidget(hint_label)
# Buttons
button_layout = QHBoxLayout()
self.save_btn = QPushButton("Save")
self.save_btn.setDefault(True)
self.save_btn.clicked.connect(self.accept)
cancel_btn = QPushButton("Cancel")
cancel_btn.clicked.connect(self.reject)
button_layout.addStretch()
button_layout.addWidget(cancel_btn)
button_layout.addWidget(self.save_btn)
layout.addLayout(button_layout)
def get_default_name(self):
"""Generate default profile name"""
profiles = self.profile_manager.list_profiles()
# Find the next available number
counter = 1
while True:
name = f"My Profile {counter}"
if not any(p['name'] == name for p in profiles):
return name
counter += 1
def validate_input(self):
"""Validate profile name"""
name = self.name_input.text().strip()
if not name:
self.save_btn.setEnabled(True)
return
# Check for duplicates
profiles = self.profile_manager.list_profiles()
if any(p['name'] == name for p in profiles):
self.save_btn.setEnabled(False)
else:
self.save_btn.setEnabled(True)
def get_profile_name(self):
"""Return the profile name (default or custom)"""
name = self.name_input.text().strip()
return name if name else self.get_default_name()