Compare commits

...

8 Commits

Author SHA1 Message Date
Dawson Matthews f274d5c8b3 working with gui 2026-03-08 18:43:09 -06:00
Dawson Matthews 5e0840a59d all attributes apply together 2026-03-08 18:38:37 -06:00
Dawson Matthews 71a71dcc5e gui works 2026-03-08 18:36:45 -06:00
Dawson Matthews 6257e63357 working state 2026-03-08 18:13:21 -06:00
Dawson Matthews 41047adac3 refactored 2026-03-08 18:12:19 -06:00
Dawson Matthews acd351f559 refactored by gemini to python 2026-03-08 18:09:01 -06:00
Dawson Matthews 768d878981 more often uses apply_attribute 2026-02-01 13:55:01 -07:00
Dawson Matthews afccfbbc05 Creating a non-functional GUI 2026-01-23 18:50:07 -07:00
5 changed files with 331 additions and 259 deletions
+1
View File
@@ -1,2 +1,3 @@
profiles/
*.json
__pycache__/
+329
View File
@@ -0,0 +1,329 @@
#!/usr/bin/env python3
import argparse
import json
import subprocess
import sys
import os
from pathlib import Path
# Try to import PySide6 for the GUI
try:
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QListWidget, QPushButton, QInputDialog, QMessageBox, QLabel,
QFileDialog
)
from PySide6.QtCore import Qt
PYSIDE_AVAILABLE = True
except ImportError:
PYSIDE_AVAILABLE = False
VERBOSE = True
DEFAULT_PROFILE_DIR = Path.home() / ".local/share/kde-display-profiles"
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:
log(f"Error saving profile: {e}")
raise e
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):
raise FileNotFoundError(f"Profile file not found: {profile_path}")
try:
with open(profile_path, 'r') as f:
profile = json.load(f)
except Exception as e:
log(f"Error reading profile JSON: {e}")
raise e
outputs = profile.get('outputs', [])
commands = []
# 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"
}
def add_attr(output_name, attribute, value, value_map=None):
if value is None:
return
if isinstance(value, (list, dict)):
return
str_value = str(value).lower()
if value_map:
if str_value in value_map:
value = value_map[str_value]
commands.append(f"output.{output_name}.{attribute}.{value}")
# 1. Handle Enable/Disable
for out in outputs:
status = "enable" if out.get('enabled') else "disable"
commands.append(f"output.{out['name']}.{status}")
# 2. Collect other attributes
for out in outputs:
name = out['name']
# WCG
add_attr(name, "wcg", out.get('wcg'), bool_enable_map)
# SDR Brightness
add_attr(name, "sdr-brightness", out.get('sdr-brightness'))
# VRR Policy
add_attr(name, "vrrpolicy", out.get('vrrPolicy'), vrr_policy_map)
# RGB Range
add_attr(name, "rgbrange", out.get('rgbRange'), rgb_range_map)
# Overscan
add_attr(name, "overscan", out.get('overscan'))
# HDR
add_attr(name, "hdr", out.get('hdr'), bool_enable_map)
# Brightness
brightness = out.get('brightness')
if brightness is not None:
add_attr(name, "brightness", int(float(brightness) * 100))
# Max BPC
max_bpc = out.get('maxBpc')
if max_bpc == 0:
max_bpc = "automatic"
add_attr(name, "maxbpc", max_bpc)
# DDC/CI
add_attr(name, "ddcCi", out.get('ddcCiAllowed'), bool_allow_map)
# Mirroring
replication_source_id = out.get('replicationSource', 0)
if replication_source_id != 0:
source_name = "none"
for other_out in outputs:
if other_out.get('id') == replication_source_id:
source_name = other_out['name']
break
add_attr(name, "mirror", source_name)
else:
add_attr(name, "mirror", "none")
# ICC Profile
icc_path = out.get('iccProfilePath')
if icc_path:
add_attr(name, "iccProfilePath", icc_path)
# Mode
mode_id = out.get('currentModeId')
if mode_id:
mode_str = get_mode_string(out, mode_id)
if mode_str:
add_attr(name, "mode", mode_str)
# Position
pos = out.get('pos')
if pos:
add_attr(name, "position", f"{pos['x']},{pos['y']}")
# Scale
add_attr(name, "scale", out.get('scale'))
# Rotation
add_attr(name, "rotation", out.get('rotation'), rotation_map)
# Priority and Primary
priority = out.get('priority')
if priority is not None:
if priority == 1:
commands.append(f"output.{name}.primary")
commands.append(f"output.{name}.priority.{priority}")
if commands:
full_cmd = ["kscreen-doctor"] + commands
log(f"Running atomic command: {' '.join(full_cmd)}")
try:
subprocess.run(full_cmd, check=True)
except subprocess.CalledProcessError as e:
log(f"Error executing kscreen-doctor: {e}")
raise e
log("Display configuration restored.")
class DisplayProfileManagerGUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("KDE Display Profile Manager")
self.setMinimumSize(400, 300)
# Ensure default directory exists
DEFAULT_PROFILE_DIR.mkdir(parents=True, exist_ok=True)
self.setup_ui()
self.refresh_profiles()
def setup_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
layout.addWidget(QLabel("Available Profiles:"))
self.profile_list = QListWidget()
layout.addWidget(self.profile_list)
btn_layout = QHBoxLayout()
self.load_btn = QPushButton("Load Profile")
self.load_btn.clicked.connect(self.on_load_clicked)
btn_layout.addWidget(self.load_btn)
self.save_btn = QPushButton("Save Current")
self.save_btn.clicked.connect(self.on_save_clicked)
btn_layout.addWidget(self.save_btn)
self.refresh_btn = QPushButton("Refresh")
self.refresh_btn.clicked.connect(self.refresh_profiles)
btn_layout.addWidget(self.refresh_btn)
layout.addLayout(btn_layout)
def refresh_profiles(self):
self.profile_list.clear()
if DEFAULT_PROFILE_DIR.exists():
profiles = sorted(DEFAULT_PROFILE_DIR.glob("*.json"))
for profile in profiles:
self.profile_list.addItem(profile.stem)
def get_default_profile_name(self):
existing_names = []
if DEFAULT_PROFILE_DIR.exists():
existing_names = [p.stem for p in DEFAULT_PROFILE_DIR.glob("*.json")]
i = 1
while f"Profile {i}" in existing_names:
i += 1
return f"Profile {i}"
def on_save_clicked(self):
default_name = self.get_default_profile_name()
name, ok = QInputDialog.getText(self, "Save Profile", "Profile Name:", text=default_name)
if ok and name:
profile_path = DEFAULT_PROFILE_DIR / f"{name}.json"
if profile_path.exists():
reply = QMessageBox.question(self, "Overwrite?",
f"Profile '{name}' already exists. Overwrite?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.No:
return
try:
save_profile(str(profile_path))
self.refresh_profiles()
QMessageBox.information(self, "Success", f"Profile '{name}' saved.")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to save profile: {e}")
def on_load_clicked(self):
selected_item = self.profile_list.currentItem()
if not selected_item:
QMessageBox.warning(self, "No Selection", "Please select a profile to load.")
return
profile_name = selected_item.text()
profile_path = DEFAULT_PROFILE_DIR / f"{profile_name}.json"
try:
load_profile(str(profile_path))
QMessageBox.information(self, "Success", f"Profile '{profile_name}' loaded.")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to load profile: {e}")
def show_gui():
if not PYSIDE_AVAILABLE:
print("Error: PySide6 is not installed. Please install it to use the GUI.", file=sys.stderr)
sys.exit(1)
app = QApplication(sys.argv)
window = DisplayProfileManagerGUI()
window.show()
sys.exit(app.exec())
def main():
parser = argparse.ArgumentParser(description="KDE Display Profile Manager")
subparsers = parser.add_subparsers(dest="command", required=False)
# 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")
# GUI command
subparsers.add_parser("gui", help="Launch the GUI manager")
args = parser.parse_args()
if args.command == "save":
save_profile(args.profile)
elif args.command == "load":
load_profile(args.profile)
elif args.command == "gui" or args.command is None:
show_gui()
if __name__ == "__main__":
main()
-248
View File
@@ -1,248 +0,0 @@
#!/usr/bin/env bash
VERBOSE=1
function log {
if [ $VERBOSE -eq 1 ]; then
echo "$@"
fi
}
set -e
# Make sure a config file was provided
if [ $# -lt 1 ]; then
echo "Usage: $0 <config file>"
exit
fi
PROFILE=$1
if [[ ! -f "$PROFILE" ]]; then
echo "Profile file not found: $PROFILE" >&2
exit 1
fi
if ! command -v jq &>/dev/null; then
echo "jq not installed!" >&2
exit 1
fi
if ! command -v kscreen-doctor &>/dev/null; then
echo "kscreen-doctor not found!" >&2
exit 1
fi
# Extract outputs list
enabled_outputs=$(jq -c '.outputs[] | select(.enabled == true)' "$PROFILE")
disabled_outputs=$(jq -c '.outputs[] | select(.enabled == false)' "$PROFILE")
outputs=$(jq -c '.outputs[]' "$PROFILE")
# Restore enabled/disabled starting with the enabled monitors
# Starting with a disabled monitor might not work if it was
# previously the only enabled monitor
function enable_outputs {
local outputs=$1
# If empty or only whitespace, return early
[[ -z "$outputs" ]] && return
while IFS= read -r out; do
id=$(echo "$out" | jq -r '.id')
log "[VAR] id:" $id
name=$(echo "$out" | jq -r '.name')
log "[VAR] name:" $name
enabled=$(echo "$out" | jq -r '.enabled')
log "[VAR] enabled:" $enabled
# Enable/disable
if [[ "$enabled" == "true" ]]; then
CMD="kscreen-doctor output.$name.enable"
log "[CMD]" $CMD
$CMD
else
CMD="kscreen-doctor output.$name.disable"
log "[CMD]" $CMD
$CMD
fi
done <<< "$outputs"
}
log "Enabling enabled outputs..."
enable_outputs "$enabled_outputs"
log "Disabling disabled outputs..."
enable_outputs "$disabled_outputs"
function apply_attribute {
output_id=$1
attribute=$2
value=$3
value_map=$4
if [ $value != "null" ]; then
if [[ -n "$value_map" ]]; then
log "[PREVALUE] Value supplied for $output_id $attribute: $value"
value=${value_map["$value"]}
log "[POSTVALUE] Value output for $output_id $attribute: $value"
fi
CMD="kscreen-doctor output.$output_id.$attribute.$value"
log "[CMD]" $CMD
$CMD
else
log "Output $output_id has not attribute $attribute, skipping..."
fi
}
function load_profile_to_outputs {
local outputs=$1
# If empty or only whitespace, return early
[[ -z "$outputs" ]] && return
while IFS= read -r out; do
id=$(echo "$out" | jq -r '.id')
name=$(echo "$out" | jq -r '.name')
posx=$(echo "$out" | jq -r '.pos.x')
posy=$(echo "$out" | jq -r '.pos.y')
rotation=$(echo "$out" | jq -r '.rotation')
scale=$(echo "$out" | jq -r '.scale')
mode_id=$(echo "$out" | jq -r '.currentModeId')
brightness=$(echo "$out" | jq -r '.brightness')
brightness=$(awk "BEGIN {printf \"%d\", $brightness * 100}")
ddcCi=$(echo "$out" | jq -r '.ddcCi')
iccProfilePath=$(echo "$out" | jq -r '.iccProfilePath')
# mode_name=$(echo "$out" | jq -r ".modes[] | select(.id == \"$mode_id\") | .name")
# log "[VAR] mode_name:" $mode_name
refresh_rate=$(echo "$out" | jq -r ".modes[] | select(.id == \"$mode_id\") | .refreshRate")
refresh_rate=$(printf "%.0f" "$refresh_rate")
height=$(echo "$out" | jq -r ".modes[] | select(.id == \"$mode_id\") | .size.height")
width=$(echo "$out" | jq -r ".modes[] | select(.id == \"$mode_id\") | .size.width")
mode="${width}x${height}@${refresh_rate}"
replication_source_id=$(echo "$out" | jq -r ".replicationSource")
hdr=$(echo "$out" | jq -r ".hdr")
maxBpc=$(echo "$out" | jq -r ".maxBpc")
overscan=$(echo "$out" | jq -r ".overscan")
rgbRange=$(echo "$out" | jq -r ".rgbRange")
sdrbrightness=$(echo "$out" | jq -r '."sdr-brightness"')
vrrPolicy=$(echo "$out" | jq -r ".vrrPolicy")
wcg=$(echo "$out" | jq -r ".wcg")
declare -A bool_enable_map
bool_enable_map["true"]="enable"
bool_enable_map["false"]="disable"
declare -A rgb_range_map
rgb_range_map["0"]="automatic"
rgb_range_map["full"]="full"
rgb_range_map["limited"]="limited"
apply_attribute $name "wcg" $wcg $bool_enable_map
apply_attribute $name "sdr-brightness" $sdrbrightness
apply_attribute $name "vrrpolicy" $vrrPolicy
apply_attribute $name "rgbrange" $rgbRange $rgb_range_map
CMD="kscreen-doctor output.$name.overscan.$overscan"
log "[CMD]" $CMD
$CMD
if [ "$maxBpc" == 0 ]; then
CMD="kscreen-doctor output.$name.maxbpc.automatic"
else
CMD="kscreen-doctor output.$name.maxbpc.$maxBpc"
fi
log "[CMD]" $CMD
$CMD
if [ "$hdr" == true ]; then
CMD="kscreen-doctor output.$name.hdr.enable"
else
CMD="kscreen-doctor output.$name.hdr.disable"
fi
log "[CMD]" $CMD
$CMD
log "[VAR] replication_source_id: $replication_source_id"
if [ $replication_source_id != 0 ]; then
replication_source_name=$(echo "$outputs.[] | select(.id == \"$replication_source_id\" | .name)" )
log "[VAR] replication_source_name: $replication_source_name"
CMD="kscreen-doctor output.$name.mirror.$replication_source_name"
log "[CMD]" $CMD
$CMD
else
CMD="kscreen-doctor output.$name.mirror.none"
log "[CMD]" $CMD
$CMD
fi
log "[VAR] mode: $mode"
priority=$(echo "$out" | jq -r '.priority')
if [ $ddcCi == true ]; then
CMD="kscreen-doctor output.$name.ddcCi.allow"
else
CMD="kscreen-doctor output.$name.ddcCi.disallow"
fi
log "[CMD]" $CMD
$CMD
if [ "$iccProfilePath" != "" ]; then
CMD="kscreen-doctor output.$name.iccProfilePath.$iccProfilePath"
log "[CMD]" $CMD
$CMD
fi
CMD="kscreen-doctor output.$name.brightness.$brightness"
log "[CMD]" $CMD
$CMD
#
# Mode (Resolution + refresh)
# CMD="kscreen-doctor output.$name.mode.$mode"
CMD="kscreen-doctor output.$name.mode.$mode"
log "[CMD]" $CMD
$CMD
# Position
CMD="kscreen-doctor output.$name.position.$posx,$posy"
log "[CMD]" $CMD
$CMD
# Scale
CMD="kscreen-doctor output.$name.scale.$scale"
log "[CMD]" $CMD
$CMD
# Rotation (map from JSON names to kscreen-doctor options)
CMD=""
case "$rotation" in
"1") CMD="kscreen-doctor output.$name.rotation.normal" ;;
"2") CMD="kscreen-doctor output.$name.rotation.left" ;;
"4") CMD="kscreen-doctor output.$name.rotation.inverted" ;;
"8") CMD="kscreen-doctor output.$name.rotation.right" ;;
esac
log "[CMD]" $CMD
$CMD
# Primary / Not Primary
if [ $priority -eq 1 ]; then
CMD="kscreen-doctor output.$name.primary"
log "[CMD]" $CMD
$CMD
fi
CMD="kscreen-doctor output.$name.priority.$priority"
log "[CMD]" $CMD
$CMD
done <<< "$outputs"
}
load_profile_to_outputs "$outputs"
echo "Display configuration restored."
-10
View File
@@ -1,10 +0,0 @@
#!/bin/bash
# Check for correct usage
if [ $# -lt 1 ]; then
echo "Usage: $0 <config name>"
exit
fi
OUTPUT_CONFIG_NAME=$1
kscreen-doctor --json > $OUTPUT_CONFIG_NAME