Compare commits

15 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
dawson b25cb6b278 still working on refactoring things 2025-12-07 16:18:01 -07:00
dawson e92be07078 super close. Added all the available attribtes that I saw, but some need some input refining. I'm in the process of making a single function for all kscreen applications to go through but something is wrong and it's late. 2025-12-06 02:20:26 -07:00
Dawson Matthews bc15980ba5 added a list of all kscreen attributes that could be added. not all are recorded in the json generated by kscreen-doctor though. need to find another way to get those values. 2025-12-05 14:25:25 -07:00
dawson 5490b665f3 changed mode from name to actual values 2025-12-04 23:23:48 -07:00
dawson 3c184743d0 The missing changes from this morning 2025-12-04 17:53:19 -07:00
dawson 46bf8aec49 more fixes... 2025-12-04 07:43:25 -07:00
dawson b07d7f3b03 removed existing profiles and fixed id to name 2025-12-04 07:21:15 -07:00
11 changed files with 361 additions and 6631 deletions
+3 -1
View File
@@ -1 +1,3 @@
profiles/*.json
profiles/
*.json
__pycache__/
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
# Make sure a config file was provided
if [ $# -lt 1 ]; then
echo "Usage: $0 <config file>"
exit
fi
# Attempt to parse the config file
INPUT_FILE=$1
echo "Parsing config info from $INPUT_FILE..."
TEST=$(jq '.outputs[]' $INPUT_FILE)
echo $TEST[0]
+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()
-120
View File
@@ -1,120 +0,0 @@
#!/usr/bin/env bash
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")
echo "enabled:"
echo $enabled_outputs
disabled_outputs=$(jq -c '.outputs[] | select(.enabled == false)' "$PROFILE")
echo "disabled:"
echo $disabled_outputs
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
while IFS= read -r out; do
id=$(echo "$out" | jq -r '.id')
name=$(echo "$out" | jq -r '.name')
enabled=$(echo "$out" | jq -r '.enabled')
# Enable/disable
if [[ "$enabled" == "true" ]]; then
kscreen-doctor "output.$name.enable"
else
kscreen-doctor "output.$name.disable"
fi
done <<< "$outputs"
}
enable_outputs $enabled_outputs
enable_outputs $disabled_outputs
function load_profile_to_outputs {
local outputs=$1
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=$(echo "$out" | jq -r '.currentModeId')
priority=$(echo "$out" | jq -r '.priority')
# Mode (Resolution + refresh)
kscreen-doctor "output.$id.mode.$mode"
# Position
kscreen-doctor "output.$id.position.$posx,$posy"
# Scale
kscreen-doctor "output.$id.scale.$scale"
# Rotation (map from JSON names to kscreen-doctor options)
case "$rotation" in
"1") kscreen-doctor "output.$id.rotation.normal" ;;
"2") kscreen-doctor "output.$id.rotation.left" ;;
"4") kscreen-doctor "output.$id.rotation.inverted" ;;
"8") kscreen-doctor "output.$id.rotation.right" ;;
esac
# Primary / Not Primary
echo $priority
if [ $priority -eq 1 ]; then
kscreen-doctor "output.$id.primary"
fi
done <<< "$outputs"
}
load_profile_to_outputs $outputs
#########################
# 2. Restore clone groups
#########################
clone_groups=$(jq -c '.clones[]?' "$PROFILE")
function restore_clone_groups {
local clone_groups=$1
while IFS= read -r clone; do
primary=$(echo "$clone" | jq -r '.[0]')
others=$(echo "$clone" | jq -r '.[]' | tail -n +2)
for o in $others; do
kscreen-doctor "output.$o.clone.$primary"
done
done <<< "$clone_groups"
}
restore_clone_groups $clone_groups
echo "Display configuration restored."
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1297
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
# All Kscreen Doctor attributes
- [x] primary
- [x] priority
- [x] enable
- [x] disable
- [x] mode
- [x] position
- [x] scale
- [x] orientation / rotation
- [x] overscan (0-100)
- [x] vrrpolicy (never / always / automatic)
- [x] rgbrange (automatic / full / limited)
- [x] hdr (enable / disable / toggle)
- [x] sdr-brightness (50-10000)
- [x] wcg (enable / disable / toggle)
- [x] iccprofile (path)
- [ ] sdrGamut (0-100)
- [ ] maxBrightnessOverride (disable / int)
- [ ] maxAverageBrightnessOverride (disable / int)
- [ ] minBrightnessOverride (disable / int)
- [ ] colorProfileSource (sRBG / ICC / EDID)
- [x] brightness (0-100)
- [ ] colorPowerTradeoff (preferEfficiency / preferAccuracy)
- [ ] dimming (0-100)
- [x] mirror ( none / output )
- [x] ddcCi (allow / disallow)
- [x] maxbpc (automatic / 6-16)
- [ ] edrPolicy (never / always)
- [ ] sharpness (0 - 100)
-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