Compare commits
12 Commits
84d746859c
...
gui
| Author | SHA1 | Date | |
|---|---|---|---|
| 768d878981 | |||
| afccfbbc05 | |||
| b25cb6b278 | |||
| e92be07078 | |||
| bc15980ba5 | |||
| 5490b665f3 | |||
| 3c184743d0 | |||
| 46bf8aec49 | |||
| b07d7f3b03 | |||
| 39d46fa9e1 | |||
| 4bbe14f8f2 | |||
| 9e990cf58d |
@@ -0,0 +1,2 @@
|
|||||||
|
profiles/
|
||||||
|
*.json
|
||||||
@@ -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]
|
|
||||||
+165
-41
@@ -1,5 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
VERBOSE=1
|
||||||
|
|
||||||
|
function log {
|
||||||
|
if [ $VERBOSE -eq 1 ]; then
|
||||||
|
echo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Make sure a config file was provided
|
# Make sure a config file was provided
|
||||||
@@ -25,73 +32,190 @@ if ! command -v kscreen-doctor &>/dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
#######################
|
|
||||||
# Parse JSON + restore
|
|
||||||
#######################
|
|
||||||
|
|
||||||
# Extract outputs list
|
# 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")
|
outputs=$(jq -c '.outputs[]' "$PROFILE")
|
||||||
|
|
||||||
# 1. Restore enabled/disabled + basic properties
|
# 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
|
while IFS= read -r out; do
|
||||||
id=$(echo "$out" | jq -r '.id')
|
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')
|
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')
|
posx=$(echo "$out" | jq -r '.pos.x')
|
||||||
posy=$(echo "$out" | jq -r '.pos.y')
|
posy=$(echo "$out" | jq -r '.pos.y')
|
||||||
rotation=$(echo "$out" | jq -r '.rotation')
|
rotation=$(echo "$out" | jq -r '.rotation')
|
||||||
scale=$(echo "$out" | jq -r '.scale')
|
scale=$(echo "$out" | jq -r '.scale')
|
||||||
mode=$(echo "$out" | jq -r '.currentModeId')
|
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 bool_allow_map
|
||||||
|
bool_allow_map["true"]="allow"
|
||||||
|
bool_allow_map["false"]="disallow"
|
||||||
|
|
||||||
|
declare -A rgb_range_map
|
||||||
|
rgb_range_map["0"]="automatic"
|
||||||
|
rgb_range_map["full"]="full"
|
||||||
|
rgb_range_map["limited"]="limited"
|
||||||
|
|
||||||
|
declare -A rotation_map
|
||||||
|
rotation_map["1"]="normal"
|
||||||
|
rotation_map["2"]="left"
|
||||||
|
rotation_map["4"]="inverted"
|
||||||
|
rotation_map["8"]="right"
|
||||||
|
|
||||||
|
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
|
||||||
|
apply_attribute $name "overscan" $overscan
|
||||||
|
apply_attribute $name "hdr" $hdr $bool_enable_map
|
||||||
|
apply_attribute $name "brightness" $brightness
|
||||||
|
|
||||||
|
if [ "$maxBpc" == 0 ]; then
|
||||||
|
maxBpc="automatic"
|
||||||
|
fi
|
||||||
|
apply_attribute $name "maxbpc" $maxBpc
|
||||||
|
apply_attribute $name "ddcCi" $ddcCi $bool_allow_map
|
||||||
|
|
||||||
|
|
||||||
|
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)" )
|
||||||
|
apply_attribute $name "mirror" $replication_source_name
|
||||||
|
else
|
||||||
|
apply_attribute $name "mirror" "none"
|
||||||
|
fi
|
||||||
|
|
||||||
priority=$(echo "$out" | jq -r '.priority')
|
priority=$(echo "$out" | jq -r '.priority')
|
||||||
|
|
||||||
# Enable/disable
|
if [ "$iccProfilePath" != "" ]; then
|
||||||
if [[ "$enabled" == "true" ]]; then
|
apply_attribute $name "iccProfilePath" $iccProfilePath
|
||||||
kscreen-doctor "output.$id.enable"
|
|
||||||
else
|
|
||||||
kscreen-doctor "output.$id.disable"
|
|
||||||
continue
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Mode (Resolution + refresh)
|
|
||||||
kscreen-doctor "output.$id.mode.$mode"
|
apply_attribute $name "mode" $mode
|
||||||
|
|
||||||
# Position
|
# Position
|
||||||
kscreen-doctor "output.$id.position.$posx,$posy"
|
# CMD="kscreen-doctor output.$name.position.$posx,$posy"
|
||||||
|
# log "[CMD]" $CMD
|
||||||
|
# $CMD
|
||||||
|
fullPosition="$posx,$posy"
|
||||||
|
apply_attribute $name "position" $fullPosition
|
||||||
|
|
||||||
# Scale
|
apply_attribute $name "scale" $scale
|
||||||
kscreen-doctor "output.$id.scale.$scale"
|
|
||||||
|
|
||||||
# Rotation (map from JSON names to kscreen-doctor options)
|
# Rotation (map from JSON names to kscreen-doctor options)
|
||||||
case "$rotation" in
|
# CMD=""
|
||||||
"1") kscreen-doctor "output.$id.rotation.normal" ;;
|
# case "$rotation" in
|
||||||
"2") kscreen-doctor "output.$id.rotation.left" ;;
|
# "1") CMD="kscreen-doctor output.$name.rotation.normal" ;;
|
||||||
"4") kscreen-doctor "output.$id.rotation.inverted" ;;
|
# "2") CMD="kscreen-doctor output.$name.rotation.left" ;;
|
||||||
"8") kscreen-doctor "output.$id.rotation.right" ;;
|
# "4") CMD="kscreen-doctor output.$name.rotation.inverted" ;;
|
||||||
esac
|
# "8") CMD="kscreen-doctor output.$name.rotation.right" ;;
|
||||||
|
# esac
|
||||||
|
# log "[CMD]" $CMD
|
||||||
|
# $CMD
|
||||||
|
apply_attribute $name "rotation" $rotation $rotation_map
|
||||||
|
|
||||||
# Primary / Not Primary
|
# Primary / Not Primary
|
||||||
echo $priority
|
|
||||||
if [ $priority -eq 1 ]; then
|
if [ $priority -eq 1 ]; then
|
||||||
kscreen-doctor "output.$id.primary"
|
CMD="kscreen-doctor output.$name.primary"
|
||||||
|
log "[CMD]" $CMD
|
||||||
|
$CMD
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
CMD="kscreen-doctor output.$name.priority.$priority"
|
||||||
|
log "[CMD]" $CMD
|
||||||
|
$CMD
|
||||||
|
|
||||||
done <<< "$outputs"
|
done <<< "$outputs"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_profile_to_outputs "$outputs"
|
||||||
#########################
|
|
||||||
# 2. Restore clone groups
|
|
||||||
#########################
|
|
||||||
|
|
||||||
clone_groups=$(jq -c '.clones[]?' "$PROFILE")
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
echo "Display configuration restored."
|
echo "Display configuration restored."
|
||||||
@@ -1,108 +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
|
|
||||||
|
|
||||||
|
|
||||||
#######################
|
|
||||||
# Parse JSON + ordering
|
|
||||||
#######################
|
|
||||||
|
|
||||||
# Extract output entries based on positions
|
|
||||||
origin_out=$(jq -c '.outputs[] | select(.pos.x == 0 and .pos.y == 0 and .enabled == true)' "$PROFILE")
|
|
||||||
non_origin_outs=$(jq -c '.outputs[] | select(.pos.x != 0 or .pos.y != 0 or .enable == false)' "$PROFILE")
|
|
||||||
|
|
||||||
#######################
|
|
||||||
# Function: Apply output
|
|
||||||
#######################
|
|
||||||
apply_output() {
|
|
||||||
local out="$1"
|
|
||||||
|
|
||||||
id=$(echo "$out" | jq -r '.id')
|
|
||||||
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')
|
|
||||||
enabled=$(echo "$out" | jq -r '.enabled')
|
|
||||||
|
|
||||||
# Enable/disable
|
|
||||||
if [[ "$enabled" == "true" ]]; then
|
|
||||||
kscreen-doctor "output.$id.enable"
|
|
||||||
else
|
|
||||||
kscreen-doctor "output.$id.disable"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Mode
|
|
||||||
kscreen-doctor "output.$id.mode.$mode"
|
|
||||||
|
|
||||||
# Position
|
|
||||||
kscreen-doctor "output.$id.position.$posx,$posy"
|
|
||||||
|
|
||||||
# Scale
|
|
||||||
kscreen-doctor "output.$id.scale.$scale"
|
|
||||||
|
|
||||||
# Rotation
|
|
||||||
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
|
|
||||||
if [ "$priority" -eq 1 ]; then
|
|
||||||
kscreen-doctor "output.$id.primary"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ -n "$origin_out" ]]; then
|
|
||||||
apply_output "$origin_out"
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS= read -r out; do
|
|
||||||
apply_output "$out"
|
|
||||||
done <<< "$non_origin_outs"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#########################
|
|
||||||
# 3. Restore clone groups
|
|
||||||
#########################
|
|
||||||
|
|
||||||
clone_groups=$(jq -c '.clones[]?' "$PROFILE")
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
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
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
@@ -0,0 +1,21 @@
|
|||||||
|
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()
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
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.")
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
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()
|
||||||
Reference in New Issue
Block a user