diff --git a/all_attributes.md b/resources/all_attributes.md similarity index 100% rename from all_attributes.md rename to resources/all_attributes.md diff --git a/load-display-profile.sh b/scripts/load-display-profile.sh similarity index 100% rename from load-display-profile.sh rename to scripts/load-display-profile.sh diff --git a/save-display-profile.sh b/scripts/save-display-profile.sh similarity index 100% rename from save-display-profile.sh rename to scripts/save-display-profile.sh diff --git a/src/__pycache__/mainwindow.cpython-314.pyc b/src/__pycache__/mainwindow.cpython-314.pyc new file mode 100644 index 0000000..c1ba515 Binary files /dev/null and b/src/__pycache__/mainwindow.cpython-314.pyc differ diff --git a/src/__pycache__/profile_manager.cpython-314.pyc b/src/__pycache__/profile_manager.cpython-314.pyc new file mode 100644 index 0000000..4eba625 Binary files /dev/null and b/src/__pycache__/profile_manager.cpython-314.pyc differ diff --git a/src/__pycache__/save_dialog.cpython-314.pyc b/src/__pycache__/save_dialog.cpython-314.pyc new file mode 100644 index 0000000..ca8e8bc Binary files /dev/null and b/src/__pycache__/save_dialog.cpython-314.pyc differ diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..fcda406 --- /dev/null +++ b/src/main.py @@ -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() \ No newline at end of file diff --git a/src/mainwindow.py b/src/mainwindow.py new file mode 100644 index 0000000..840a725 --- /dev/null +++ b/src/mainwindow.py @@ -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.") \ No newline at end of file diff --git a/src/profile_manager.py b/src/profile_manager.py new file mode 100644 index 0000000..1928700 --- /dev/null +++ b/src/profile_manager.py @@ -0,0 +1,139 @@ +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""" + # Check common locations + possible_paths = [ + Path(__file__).parent.parent / "scripts/display-script.sh", + Path.home() / "bin/display-script.sh", + Path("/usr/local/bin/display-script.sh"), + ] + + for path in possible_paths: + if path.exists(): + return path + + # For now, return None - we'll handle this gracefully + return None + + def load_metadata(self): + """Load profile metadata from JSON file""" + try: + with open(self.metadata_file, 'r') as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return [] + + def save_metadata(self, profiles): + """Save profile metadata to JSON file""" + with open(self.metadata_file, 'w') as f: + json.dump(profiles, f, indent=2) + + 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) \ No newline at end of file diff --git a/src/save_dialog.py b/src/save_dialog.py new file mode 100644 index 0000000..ffd78b5 --- /dev/null +++ b/src/save_dialog.py @@ -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() \ No newline at end of file