Initial public release v0.1.0 - Linux Wabbajack Modlist Application

Jackify provides native Linux support for Wabbajack modlist installation
   and management with automated Steam integration and Proton configuration.

   Key Features:
   - Almost Native Linux implementation (texconv.exe run via proton)
   - Automated Steam shortcut creation and Proton prefix management
   - Both CLI and GUI interfaces, with Steam Deck optimization

   Supported Games:
   - Skyrim Special Edition
   - Fallout 4
   - Fallout New Vegas
   - Oblivion, Starfield, Enderal, and diverse other games

   Technical Architecture:
   - Clean separation between frontend and backend services
   - Powered by jackify-engine 0.3.x for Wabbajack-matching modlist installation
This commit is contained in:
Omni
2025-09-05 20:46:24 +01:00
commit cd591c14e3
445 changed files with 40398 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
"""
GUI Dialogs Package
Custom dialogs for the Jackify GUI application.
"""
from .completion_dialog import NextStepsDialog
from .success_dialog import SuccessDialog
__all__ = ['NextStepsDialog', 'SuccessDialog']

View File

@@ -0,0 +1,200 @@
"""
Completion Dialog
Custom completion dialog that shows the same detailed completion message
as the CLI frontend, formatted for GUI display.
"""
import logging
from pathlib import Path
from typing import Optional
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit,
QWidget, QSpacerItem, QSizePolicy
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap, QIcon
logger = logging.getLogger(__name__)
class NextStepsDialog(QDialog):
"""
Custom completion dialog showing detailed next steps after modlist configuration.
Displays the same information as the CLI completion message but in a proper GUI format.
"""
def __init__(self, modlist_name: str, parent=None):
"""
Initialize the Next Steps dialog.
Args:
modlist_name: Name of the configured modlist
parent: Parent widget
"""
super().__init__(parent)
self.modlist_name = modlist_name
self.setWindowTitle("Next Steps")
self.setModal(True)
self.setFixedSize(600, 400)
# Set the Wabbajack icon if available
self._set_dialog_icon()
self._setup_ui()
logger.info(f"NextStepsDialog created for modlist: {modlist_name}")
def _set_dialog_icon(self):
"""Set the dialog icon to Wabbajack icon if available"""
try:
# Try to use the same icon as the main application
icon_path = Path(__file__).parent.parent.parent.parent.parent / "Files" / "wabbajack-icon.png"
if icon_path.exists():
icon = QIcon(str(icon_path))
self.setWindowIcon(icon)
except Exception as e:
logger.debug(f"Could not set dialog icon: {e}")
def _setup_ui(self):
"""Set up the dialog user interface"""
layout = QVBoxLayout(self)
layout.setSpacing(16)
layout.setContentsMargins(20, 20, 20, 20)
# Header with icon and title
self._setup_header(layout)
# Main content area
self._setup_content(layout)
# Action buttons
self._setup_buttons(layout)
def _setup_header(self, layout):
"""Set up the dialog header with title"""
header_layout = QHBoxLayout()
# Title
title_label = QLabel("Next Steps:")
title_label.setStyleSheet(
"QLabel { "
" font-size: 18px; "
" font-weight: bold; "
" color: #2c3e50; "
" margin-bottom: 10px; "
"}"
)
header_layout.addWidget(title_label)
# Add some space
header_layout.addStretch()
layout.addLayout(header_layout)
def _setup_content(self, layout):
"""Set up the main content area with next steps"""
# Create content area
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
content_layout.setSpacing(12)
# Add the detailed next steps text (matching CLI completion message)
steps_text = self._build_completion_text()
content_text = QTextEdit()
content_text.setPlainText(steps_text)
content_text.setReadOnly(True)
content_text.setStyleSheet(
"QTextEdit { "
" background-color: #f8f9fa; "
" border: 1px solid #dee2e6; "
" border-radius: 6px; "
" padding: 12px; "
" font-family: 'Segoe UI', Arial, sans-serif; "
" font-size: 12px; "
" line-height: 1.5; "
"}"
)
content_layout.addWidget(content_text)
layout.addWidget(content_widget)
def _setup_buttons(self, layout):
"""Set up the action buttons"""
button_layout = QHBoxLayout()
button_layout.setSpacing(12)
# Add stretch to center buttons
button_layout.addStretch()
# Return button (goes back to menu)
return_btn = QPushButton("Return")
return_btn.setFixedSize(100, 35)
return_btn.clicked.connect(self.accept) # This will close dialog and return to menu
return_btn.setStyleSheet(
"QPushButton { "
" background-color: #3498db; "
" color: white; "
" border: none; "
" border-radius: 4px; "
" font-weight: bold; "
" padding: 8px 16px; "
"} "
"QPushButton:hover { "
" background-color: #2980b9; "
"} "
"QPushButton:pressed { "
" background-color: #21618c; "
"}"
)
button_layout.addWidget(return_btn)
button_layout.addSpacing(10)
# Exit button (closes the application)
exit_btn = QPushButton("Exit")
exit_btn.setFixedSize(100, 35)
exit_btn.clicked.connect(self.reject) # This will close dialog and potentially exit app
exit_btn.setStyleSheet(
"QPushButton { "
" background-color: #95a5a6; "
" color: white; "
" border: none; "
" border-radius: 4px; "
" font-weight: bold; "
" padding: 8px 16px; "
"} "
"QPushButton:hover { "
" background-color: #7f8c8d; "
"} "
"QPushButton:pressed { "
" background-color: #6c7b7d; "
"}"
)
button_layout.addWidget(exit_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
def _build_completion_text(self) -> str:
"""
Build the completion text matching the CLI version from menu_handler.py.
Returns:
Formatted completion text string
"""
# Match the CLI completion text from menu_handler.py lines 627-631
completion_text = f"""✓ Configuration completed successfully!
Modlist Install and Configuration complete!:
• You should now be able to Launch '{self.modlist_name}' through Steam.
• Congratulations and enjoy the game!
Detailed log available at: ~/Jackify/logs/Configure_New_Modlist_workflow.log"""
return completion_text

View File

@@ -0,0 +1,328 @@
"""
Protontricks Error Dialog
Dialog shown when protontricks is not found, with options to install via Flatpak or get native installation guidance.
"""
from pathlib import Path
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QSizePolicy, QTextEdit, QProgressBar
)
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QPixmap, QIcon, QFont
from .. import shared_theme
class FlatpakInstallThread(QThread):
"""Thread for installing Flatpak protontricks"""
finished = Signal(bool, str) # success, message
def __init__(self, detection_service):
super().__init__()
self.detection_service = detection_service
def run(self):
success, message = self.detection_service.install_flatpak_protontricks()
self.finished.emit(success, message)
class ProtontricksErrorDialog(QDialog):
"""
Dialog shown when protontricks is not found
Provides options to install via Flatpak or get native installation guidance
"""
def __init__(self, detection_service, parent=None):
super().__init__(parent)
self.detection_service = detection_service
self.setWindowTitle("Protontricks Required")
self.setModal(True)
self.setFixedSize(550, 520)
self.install_thread = None
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
# Card background
card = QFrame(self)
card.setObjectName("protontricksCard")
card.setFrameShape(QFrame.StyledPanel)
card.setFrameShadow(QFrame.Raised)
card.setMinimumWidth(500)
card.setMinimumHeight(400)
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
card_layout = QVBoxLayout(card)
card_layout.setSpacing(16)
card_layout.setContentsMargins(28, 28, 28, 28)
card.setStyleSheet(
"QFrame#protontricksCard { "
" background: #2d2323; "
" border-radius: 12px; "
" border: 2px solid #e74c3c; "
"}"
)
# Error icon
icon_label = QLabel()
icon_label.setAlignment(Qt.AlignCenter)
icon_label.setText("!")
icon_label.setStyleSheet(
"QLabel { "
" font-size: 36px; "
" font-weight: bold; "
" color: #e74c3c; "
" margin-bottom: 4px; "
"}"
)
card_layout.addWidget(icon_label)
# Error title
title_label = QLabel("Protontricks Not Found")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet(
"QLabel { "
" font-size: 20px; "
" font-weight: 600; "
" color: #e74c3c; "
" margin-bottom: 2px; "
"}"
)
card_layout.addWidget(title_label)
# Error message
message_text = QTextEdit()
message_text.setReadOnly(True)
message_text.setPlainText(
"Protontricks is required for Jackify to function properly. "
"It manages Wine prefixes for Steam games and is essential for modlist installation and configuration.\n\n"
"Choose an installation method below:"
)
message_text.setMinimumHeight(100)
message_text.setMaximumHeight(120)
message_text.setStyleSheet(
"QTextEdit { "
" font-size: 15px; "
" color: #e0e0e0; "
" background: transparent; "
" border: none; "
" line-height: 1.3; "
" margin-bottom: 6px; "
"}"
)
card_layout.addWidget(message_text)
# Progress bar (initially hidden)
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
self.progress_bar.setStyleSheet(
"QProgressBar { "
" border: 1px solid #555; "
" border-radius: 4px; "
" background: #23272e; "
" text-align: center; "
"} "
"QProgressBar::chunk { "
" background-color: #4fc3f7; "
" border-radius: 3px; "
"}"
)
card_layout.addWidget(self.progress_bar)
# Status label (initially hidden)
self.status_label = QLabel()
self.status_label.setVisible(False)
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet(
"QLabel { "
" font-size: 14px; "
" color: #4fc3f7; "
" margin: 8px 0; "
"}"
)
card_layout.addWidget(self.status_label)
# Button layout
button_layout = QVBoxLayout()
button_layout.setSpacing(12)
# Flatpak install button
self.flatpak_btn = QPushButton("Install via Flatpak (Recommended)")
self.flatpak_btn.setFixedHeight(40)
self.flatpak_btn.clicked.connect(self._install_flatpak)
self.flatpak_btn.setStyleSheet(
"QPushButton { "
" background-color: #4fc3f7; "
" color: white; "
" border: none; "
" border-radius: 6px; "
" font-weight: bold; "
" font-size: 14px; "
" padding: 8px 16px; "
"} "
"QPushButton:hover { "
" background-color: #3498db; "
"} "
"QPushButton:pressed { "
" background-color: #2980b9; "
"} "
"QPushButton:disabled { "
" background-color: #555; "
" color: #888; "
"}"
)
button_layout.addWidget(self.flatpak_btn)
# Native install guidance button
self.native_btn = QPushButton("Show Native Installation Instructions")
self.native_btn.setFixedHeight(40)
self.native_btn.clicked.connect(self._show_native_guidance)
self.native_btn.setStyleSheet(
"QPushButton { "
" background-color: #95a5a6; "
" color: white; "
" border: none; "
" border-radius: 6px; "
" font-weight: bold; "
" font-size: 14px; "
" padding: 8px 16px; "
"} "
"QPushButton:hover { "
" background-color: #7f8c8d; "
"} "
"QPushButton:pressed { "
" background-color: #6c7b7d; "
"}"
)
button_layout.addWidget(self.native_btn)
card_layout.addLayout(button_layout)
# Bottom button layout
bottom_layout = QHBoxLayout()
bottom_layout.setSpacing(12)
# Re-detect button
self.redetect_btn = QPushButton("Re-detect")
self.redetect_btn.setFixedSize(120, 36)
self.redetect_btn.clicked.connect(self._redetect)
self.redetect_btn.setStyleSheet(
"QPushButton { "
" background-color: #27ae60; "
" color: white; "
" border: none; "
" border-radius: 4px; "
" font-weight: bold; "
" padding: 8px 16px; "
"} "
"QPushButton:hover { "
" background-color: #229954; "
"} "
"QPushButton:pressed { "
" background-color: #1e8449; "
"}"
)
bottom_layout.addWidget(self.redetect_btn)
bottom_layout.addStretch()
# Exit button
exit_btn = QPushButton("Exit Jackify")
exit_btn.setFixedSize(120, 36)
exit_btn.clicked.connect(self._exit_app)
exit_btn.setStyleSheet(
"QPushButton { "
" background-color: #e74c3c; "
" color: white; "
" border: none; "
" border-radius: 4px; "
" font-weight: bold; "
" padding: 8px 16px; "
"} "
"QPushButton:hover { "
" background-color: #c0392b; "
"} "
"QPushButton:pressed { "
" background-color: #a93226; "
"}"
)
bottom_layout.addWidget(exit_btn)
card_layout.addLayout(bottom_layout)
layout.addStretch()
layout.addWidget(card, alignment=Qt.AlignCenter)
layout.addStretch()
def _install_flatpak(self):
"""Install protontricks via Flatpak"""
# Disable buttons during installation
self.flatpak_btn.setEnabled(False)
self.native_btn.setEnabled(False)
self.redetect_btn.setEnabled(False)
# Show progress
self.progress_bar.setVisible(True)
self.progress_bar.setRange(0, 0) # Indeterminate progress
self.status_label.setVisible(True)
self.status_label.setText("Installing Flatpak protontricks...")
# Start installation thread
self.install_thread = FlatpakInstallThread(self.detection_service)
self.install_thread.finished.connect(self._on_install_finished)
self.install_thread.start()
def _on_install_finished(self, success, message):
"""Handle installation completion"""
# Hide progress
self.progress_bar.setVisible(False)
# Re-enable buttons
self.flatpak_btn.setEnabled(True)
self.native_btn.setEnabled(True)
self.redetect_btn.setEnabled(True)
if success:
self.status_label.setText("✓ Installation successful!")
self.status_label.setStyleSheet("QLabel { color: #27ae60; font-size: 14px; margin: 8px 0; }")
# Auto-redetect after successful installation
self._redetect()
else:
self.status_label.setText(f"✗ Installation failed: {message}")
self.status_label.setStyleSheet("QLabel { color: #e74c3c; font-size: 14px; margin: 8px 0; }")
def _show_native_guidance(self):
"""Show native installation guidance"""
from ..services.message_service import MessageService
guidance = self.detection_service.get_installation_guidance()
MessageService.information(self, "Native Installation", guidance, safety_level="low")
def _redetect(self):
"""Re-detect protontricks"""
self.detection_service.clear_cache()
is_installed, installation_type, details = self.detection_service.detect_protontricks(use_cache=False)
if is_installed:
self.status_label.setText("✓ Protontricks found!")
self.status_label.setStyleSheet("QLabel { color: #27ae60; font-size: 14px; margin: 8px 0; }")
self.status_label.setVisible(True)
self.accept() # Close dialog successfully
else:
self.status_label.setText("✗ Protontricks still not found")
self.status_label.setStyleSheet("QLabel { color: #e74c3c; font-size: 14px; margin: 8px 0; }")
self.status_label.setVisible(True)
def _exit_app(self):
"""Exit the application"""
self.reject()
import sys
sys.exit(1)
def closeEvent(self, event):
"""Handle dialog close event"""
if self.install_thread and self.install_thread.isRunning():
self.install_thread.terminate()
self.install_thread.wait()
event.accept()

View File

@@ -0,0 +1,239 @@
"""
Success Dialog
Celebration dialog shown when workflows complete successfully.
Features trophy icon, personalized messaging, and time tracking.
"""
import logging
from pathlib import Path
from typing import Optional
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QWidget,
QSpacerItem, QSizePolicy, QFrame, QApplication
)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QPixmap, QIcon, QFont
logger = logging.getLogger(__name__)
class SuccessDialog(QDialog):
"""
Celebration dialog shown when workflows complete successfully.
Features:
- Trophy icon
- Personalized success message
- Time taken display
- Next steps guidance
- Return and Exit buttons
"""
def __init__(self, modlist_name: str, workflow_type: str, time_taken: str, game_name: str = None, parent=None):
super().__init__(parent)
self.modlist_name = modlist_name
self.workflow_type = workflow_type
self.time_taken = time_taken
self.game_name = game_name
self.setWindowTitle("Success!")
self.setWindowModality(Qt.NonModal)
self.setAttribute(Qt.WA_ShowWithoutActivating, True)
self.setFixedSize(500, 420)
self.setWindowFlag(Qt.WindowDoesNotAcceptFocus, True)
self.setStyleSheet("QDialog { background: #181818; color: #fff; border-radius: 12px; }" )
layout = QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
# --- Card background for content ---
card = QFrame(self)
card.setObjectName("successCard")
card.setFrameShape(QFrame.StyledPanel)
card.setFrameShadow(QFrame.Raised)
card.setFixedWidth(440)
card_layout = QVBoxLayout(card)
card_layout.setSpacing(12)
card_layout.setContentsMargins(28, 28, 28, 28)
card.setStyleSheet(
"QFrame#successCard { "
" background: #23272e; "
" border-radius: 12px; "
" border: 1px solid #353a40; "
"}"
)
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Trophy icon (smaller, more subtle)
trophy_label = QLabel()
trophy_label.setAlignment(Qt.AlignCenter)
trophy_icon_path = Path(__file__).parent.parent.parent.parent.parent / "Files" / "trophy.png"
if trophy_icon_path.exists():
pixmap = QPixmap(str(trophy_icon_path)).scaled(36, 36, Qt.KeepAspectRatio, Qt.SmoothTransformation)
trophy_label.setPixmap(pixmap)
else:
trophy_label.setText("")
trophy_label.setStyleSheet(
"QLabel { "
" font-size: 28px; "
" margin-bottom: 4px; "
"}"
)
card_layout.addWidget(trophy_label)
# Success title (less saturated green)
title_label = QLabel("Success!")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet(
"QLabel { "
" font-size: 22px; "
" font-weight: 600; "
" color: #2ecc71; "
" margin-bottom: 2px; "
"}"
)
card_layout.addWidget(title_label)
# Personalized success message (modlist name in Jackify Blue, but less bold)
message_text = self._build_success_message()
modlist_name_html = f'<span style="color:#3fb7d6; font-size:17px; font-weight:500;">{self.modlist_name}</span>'
if self.workflow_type == "install":
message_html = f"<span style='font-size:15px;'>{modlist_name_html} installed successfully!</span>"
else:
message_html = message_text
message_label = QLabel(message_html)
message_label.setAlignment(Qt.AlignCenter)
message_label.setWordWrap(True)
message_label.setStyleSheet(
"QLabel { "
" font-size: 15px; "
" color: #e0e0e0; "
" line-height: 1.3; "
" margin-bottom: 6px; "
" max-width: 400px; "
" min-width: 200px; "
" word-wrap: break-word; "
"}"
)
message_label.setTextFormat(Qt.RichText)
card_layout.addWidget(message_label)
# Time taken
time_label = QLabel(f"Completed in {self.time_taken}")
time_label.setAlignment(Qt.AlignCenter)
time_label.setStyleSheet(
"QLabel { "
" font-size: 12px; "
" color: #b0b0b0; "
" font-style: italic; "
" margin-bottom: 10px; "
"}"
)
card_layout.addWidget(time_label)
# Next steps guidance
next_steps_text = self._build_next_steps()
next_steps_label = QLabel(next_steps_text)
next_steps_label.setAlignment(Qt.AlignCenter)
next_steps_label.setWordWrap(True)
next_steps_label.setStyleSheet(
"QLabel { "
" font-size: 13px; "
" color: #b0b0b0; "
" line-height: 1.2; "
" padding: 6px; "
" background-color: transparent; "
" border-radius: 6px; "
" border: none; "
"}"
)
card_layout.addWidget(next_steps_label)
layout.addStretch()
layout.addWidget(card, alignment=Qt.AlignCenter)
layout.addStretch()
# Action buttons
btn_row = QHBoxLayout()
self.return_btn = QPushButton("Return")
self.exit_btn = QPushButton("Exit")
btn_row.addWidget(self.return_btn)
btn_row.addWidget(self.exit_btn)
layout.addLayout(btn_row)
# Now set up the timer/countdown logic AFTER buttons are created
self.return_btn.setEnabled(False)
self.exit_btn.setEnabled(False)
self._countdown = 3
self._orig_return_text = self.return_btn.text()
self._timer = QTimer(self)
self._timer.timeout.connect(self._update_countdown)
self._update_countdown()
self._timer.start(1000)
self.return_btn.clicked.connect(self.accept)
self.exit_btn.clicked.connect(QApplication.quit)
# Set the Wabbajack icon if available
self._set_dialog_icon()
logger.info(f"SuccessDialog created for {workflow_type}: {modlist_name} (completed in {time_taken})")
def _set_dialog_icon(self):
"""Set the dialog icon to Wabbajack icon if available"""
try:
# Try to use the same icon as the main application
icon_path = Path(__file__).parent.parent.parent.parent.parent / "Files" / "wabbajack-icon.png"
if icon_path.exists():
icon = QIcon(str(icon_path))
self.setWindowIcon(icon)
except Exception as e:
logger.debug(f"Could not set dialog icon: {e}")
def _setup_ui(self):
"""Set up the dialog user interface"""
pass # This method is no longer needed as __init__ handles UI setup
def _setup_buttons(self, layout):
"""Set up the action buttons"""
pass # This method is no longer needed as __init__ handles button setup
def _build_success_message(self) -> str:
"""
Build the personalized success message based on workflow type.
Returns:
Formatted success message string
"""
workflow_messages = {
"install": f"{self.modlist_name} installed successfully!",
"configure_new": f"{self.modlist_name} configured successfully!",
"configure_existing": f"{self.modlist_name} configuration updated successfully!",
"tuxborn": f"Tuxborn installation completed successfully!",
}
return workflow_messages.get(self.workflow_type, f"{self.modlist_name} completed successfully!")
def _build_next_steps(self) -> str:
"""
Build the next steps guidance based on workflow type.
Returns:
Formatted next steps string
"""
game_display = self.game_name or self.modlist_name
if self.workflow_type == "tuxborn":
return f"You can now launch Tuxborn from Steam and enjoy your modded {game_display} experience!"
else:
return f"You can now launch {self.modlist_name} from Steam and enjoy your modded {game_display} experience!"
def _update_countdown(self):
if self._countdown > 0:
self.return_btn.setText(f"{self._orig_return_text} ({self._countdown}s)")
self.return_btn.setEnabled(False)
self.exit_btn.setEnabled(False)
self._countdown -= 1
else:
self.return_btn.setText(self._orig_return_text)
self.return_btn.setEnabled(True)
self.exit_btn.setEnabled(True)
self._timer.stop()

View File

@@ -0,0 +1,529 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Ulimit Guidance Dialog
Provides guidance for manually increasing file descriptor limits when automatic
increase fails. Offers distribution-specific instructions and commands.
"""
import logging
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QTextEdit, QGroupBox, QTabWidget, QWidget, QScrollArea,
QFrame, QSizePolicy
)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QFont, QIcon
logger = logging.getLogger(__name__)
class UlimitGuidanceDialog(QDialog):
"""Dialog to provide manual ulimit increase guidance when automatic methods fail"""
def __init__(self, resource_manager=None, parent=None):
super().__init__(parent)
self.resource_manager = resource_manager
self.setWindowTitle("File Descriptor Limit Guidance")
self.setModal(True)
self.setMinimumSize(800, 600)
self.resize(900, 700)
# Get current status and instructions
if self.resource_manager:
self.status = self.resource_manager.get_limit_status()
self.instructions = self.resource_manager.get_manual_increase_instructions()
else:
# Fallback if no resource manager provided
from jackify.backend.services.resource_manager import ResourceManager
temp_manager = ResourceManager()
self.status = temp_manager.get_limit_status()
self.instructions = temp_manager.get_manual_increase_instructions()
self._setup_ui()
# Auto-refresh status every few seconds
self.refresh_timer = QTimer()
self.refresh_timer.timeout.connect(self._refresh_status)
self.refresh_timer.start(3000) # Refresh every 3 seconds
def _setup_ui(self):
"""Set up the user interface"""
layout = QVBoxLayout()
self.setLayout(layout)
# Title and current status
self._create_header(layout)
# Main content with tabs
self._create_content_tabs(layout)
# Action buttons
self._create_action_buttons(layout)
# Apply styling
self._apply_styling()
def _create_header(self, layout):
"""Create header with current status"""
header_frame = QFrame()
header_frame.setFrameStyle(QFrame.StyledPanel)
header_layout = QVBoxLayout()
header_frame.setLayout(header_layout)
# Title
title_label = QLabel("File Descriptor Limit Configuration")
title_font = QFont()
title_font.setPointSize(14)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setAlignment(Qt.AlignCenter)
header_layout.addWidget(title_label)
# Status information
self._create_status_section(header_layout)
layout.addWidget(header_frame)
def _create_status_section(self, layout):
"""Create current status display"""
status_layout = QHBoxLayout()
# Current limits
current_label = QLabel(f"Current Limit: {self.status['current_soft']}")
target_label = QLabel(f"Target Limit: {self.status['target_limit']}")
max_label = QLabel(f"Maximum Possible: {self.status['max_possible']}")
# Status indicator
if self.status['target_achieved']:
status_text = "✓ Optimal"
status_color = "#4caf50" # Green
elif self.status['can_increase']:
status_text = "⚠ Can Improve"
status_color = "#ff9800" # Orange
else:
status_text = "✗ Needs Manual Fix"
status_color = "#f44336" # Red
self.status_label = QLabel(f"Status: {status_text}")
self.status_label.setStyleSheet(f"color: {status_color}; font-weight: bold;")
status_layout.addWidget(current_label)
status_layout.addWidget(target_label)
status_layout.addWidget(max_label)
status_layout.addStretch()
status_layout.addWidget(self.status_label)
layout.addLayout(status_layout)
def _create_content_tabs(self, layout):
"""Create tabbed content with different guidance types"""
self.tab_widget = QTabWidget()
# Quick Fix tab
self._create_quick_fix_tab()
# Permanent Fix tab
self._create_permanent_fix_tab()
# Troubleshooting tab
self._create_troubleshooting_tab()
layout.addWidget(self.tab_widget)
def _create_quick_fix_tab(self):
"""Create quick/temporary fix tab"""
widget = QWidget()
layout = QVBoxLayout()
widget.setLayout(layout)
# Explanation
explanation = QLabel(
"Quick fixes apply only to the current terminal session. "
"You'll need to run these commands each time you start Jackify from a new terminal."
)
explanation.setWordWrap(True)
explanation.setStyleSheet("color: #666; font-style: italic; margin-bottom: 10px;")
layout.addWidget(explanation)
# Commands group
commands_group = QGroupBox("Commands to Run")
commands_layout = QVBoxLayout()
commands_group.setLayout(commands_layout)
# Command text
if 'temporary' in self.instructions['methods']:
temp_method = self.instructions['methods']['temporary']
commands_text = QTextEdit()
commands_text.setPlainText('\n'.join(temp_method['commands']))
commands_text.setMaximumHeight(120)
commands_text.setFont(QFont("monospace"))
commands_layout.addWidget(commands_text)
# Note
if 'note' in temp_method:
note_label = QLabel(f"Note: {temp_method['note']}")
note_label.setWordWrap(True)
note_label.setStyleSheet("color: #666; font-style: italic;")
commands_layout.addWidget(note_label)
layout.addWidget(commands_group)
# Current session test
test_group = QGroupBox("Test Current Session")
test_layout = QVBoxLayout()
test_group.setLayout(test_layout)
test_label = QLabel("You can test if the commands worked by running:")
test_layout.addWidget(test_label)
test_command = QTextEdit()
test_command.setPlainText("ulimit -n")
test_command.setMaximumHeight(40)
test_command.setFont(QFont("monospace"))
test_layout.addWidget(test_command)
expected_label = QLabel(f"Expected result: {self.instructions['target_limit']} or higher")
expected_label.setStyleSheet("color: #666;")
test_layout.addWidget(expected_label)
layout.addWidget(test_group)
layout.addStretch()
self.tab_widget.addTab(widget, "Quick Fix")
def _create_permanent_fix_tab(self):
"""Create permanent fix tab"""
widget = QWidget()
layout = QVBoxLayout()
widget.setLayout(layout)
# Explanation
explanation = QLabel(
"Permanent fixes modify system configuration files and require administrator privileges. "
"Changes take effect after logout/login or system reboot."
)
explanation.setWordWrap(True)
explanation.setStyleSheet("color: #666; font-style: italic; margin-bottom: 10px;")
layout.addWidget(explanation)
# Distribution detection
distro_label = QLabel(f"Detected Distribution: {self.instructions['distribution'].title()}")
distro_label.setStyleSheet("font-weight: bold; color: #333;")
layout.addWidget(distro_label)
# Commands group
commands_group = QGroupBox("System Configuration Commands")
commands_layout = QVBoxLayout()
commands_group.setLayout(commands_layout)
# Warning
warning_label = QLabel(
"⚠️ WARNING: These commands require root/sudo privileges and modify system files. "
"Make sure you understand what each command does before running it."
)
warning_label.setWordWrap(True)
warning_label.setStyleSheet("background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 8px; border-radius: 4px; color: #856404;")
commands_layout.addWidget(warning_label)
# Command text
if 'permanent' in self.instructions['methods']:
perm_method = self.instructions['methods']['permanent']
commands_text = QTextEdit()
commands_text.setPlainText('\n'.join(perm_method['commands']))
commands_text.setMinimumHeight(200)
commands_text.setFont(QFont("monospace"))
commands_layout.addWidget(commands_text)
# Note
if 'note' in perm_method:
note_label = QLabel(f"Note: {perm_method['note']}")
note_label.setWordWrap(True)
note_label.setStyleSheet("color: #666; font-style: italic;")
commands_layout.addWidget(note_label)
layout.addWidget(commands_group)
# Verification group
verify_group = QGroupBox("Verification After Reboot/Re-login")
verify_layout = QVBoxLayout()
verify_group.setLayout(verify_layout)
verify_label = QLabel("After rebooting or logging out and back in, verify the change:")
verify_layout.addWidget(verify_label)
verify_command = QTextEdit()
verify_command.setPlainText("ulimit -n")
verify_command.setMaximumHeight(40)
verify_command.setFont(QFont("monospace"))
verify_layout.addWidget(verify_command)
expected_label = QLabel(f"Expected result: {self.instructions['target_limit']} or higher")
expected_label.setStyleSheet("color: #666;")
verify_layout.addWidget(expected_label)
layout.addWidget(verify_group)
layout.addStretch()
self.tab_widget.addTab(widget, "Permanent Fix")
def _create_troubleshooting_tab(self):
"""Create troubleshooting tab"""
widget = QWidget()
layout = QVBoxLayout()
widget.setLayout(layout)
# Create scrollable area for troubleshooting content
scroll = QScrollArea()
scroll_widget = QWidget()
scroll_layout = QVBoxLayout()
scroll_widget.setLayout(scroll_layout)
# Common issues
issues_group = QGroupBox("Common Issues and Solutions")
issues_layout = QVBoxLayout()
issues_group.setLayout(issues_layout)
issues_text = """
<b>Issue:</b> "Operation not permitted" when trying to increase limits<br>
<b>Solution:</b> You may need root privileges or the hard limit may be too low. Try the permanent fix method.
<b>Issue:</b> Changes don't persist after closing terminal<br>
<b>Solution:</b> Use the permanent fix method to modify system configuration files.
<b>Issue:</b> Still getting "too many open files" errors after increasing limits<br>
<b>Solution:</b> Some applications may need to be restarted to pick up the new limits. Try restarting Jackify.
<b>Issue:</b> Can't increase above a certain number<br>
<b>Solution:</b> The hard limit may be set by system administrator or systemd. Check systemd service limits if applicable.
"""
issues_label = QLabel(issues_text)
issues_label.setWordWrap(True)
issues_label.setTextFormat(Qt.RichText)
issues_layout.addWidget(issues_label)
scroll_layout.addWidget(issues_group)
# System information
sysinfo_group = QGroupBox("System Information")
sysinfo_layout = QVBoxLayout()
sysinfo_group.setLayout(sysinfo_layout)
sysinfo_text = f"""
<b>Current Soft Limit:</b> {self.status['current_soft']}<br>
<b>Current Hard Limit:</b> {self.status['current_hard']}<br>
<b>Target Limit:</b> {self.status['target_limit']}<br>
<b>Detected Distribution:</b> {self.instructions['distribution']}<br>
<b>Can Increase Automatically:</b> {"Yes" if self.status['can_increase'] else "No"}<br>
<b>Target Achieved:</b> {"Yes" if self.status['target_achieved'] else "No"}
"""
sysinfo_label = QLabel(sysinfo_text)
sysinfo_label.setWordWrap(True)
sysinfo_label.setTextFormat(Qt.RichText)
sysinfo_label.setFont(QFont("monospace", 9))
sysinfo_layout.addWidget(sysinfo_label)
scroll_layout.addWidget(sysinfo_group)
# Additional resources
resources_group = QGroupBox("Additional Resources")
resources_layout = QVBoxLayout()
resources_group.setLayout(resources_layout)
resources_text = """
<b>For more help:</b><br>
• Check your distribution's documentation for ulimit configuration<br>
• Search for "increase file descriptor limit [your_distribution]"<br>
• Consider asking on your distribution's support forums<br>
• Jackify documentation and issue tracker on GitHub
"""
resources_label = QLabel(resources_text)
resources_label.setWordWrap(True)
resources_label.setTextFormat(Qt.RichText)
resources_layout.addWidget(resources_label)
scroll_layout.addWidget(resources_group)
scroll_layout.addStretch()
scroll.setWidget(scroll_widget)
scroll.setWidgetResizable(True)
layout.addWidget(scroll)
self.tab_widget.addTab(widget, "Troubleshooting")
def _create_action_buttons(self, layout):
"""Create action buttons"""
button_layout = QHBoxLayout()
# Try Again button
self.try_again_btn = QPushButton("Try Automatic Fix Again")
self.try_again_btn.clicked.connect(self._try_automatic_fix)
self.try_again_btn.setEnabled(self.status['can_increase'] and not self.status['target_achieved'])
# Refresh Status button
refresh_btn = QPushButton("Refresh Status")
refresh_btn.clicked.connect(self._refresh_status)
# Close button
close_btn = QPushButton("Close")
close_btn.clicked.connect(self.accept)
close_btn.setDefault(True)
button_layout.addWidget(self.try_again_btn)
button_layout.addWidget(refresh_btn)
button_layout.addStretch()
button_layout.addWidget(close_btn)
layout.addLayout(button_layout)
def _apply_styling(self):
"""Apply dialog styling"""
self.setStyleSheet("""
QDialog {
background-color: #f5f5f5;
}
QGroupBox {
font-weight: bold;
border: 2px solid #cccccc;
border-radius: 5px;
margin-top: 1ex;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
QTextEdit {
background-color: #ffffff;
border: 1px solid #cccccc;
border-radius: 3px;
padding: 5px;
}
QPushButton {
background-color: #007acc;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #005a9e;
}
QPushButton:pressed {
background-color: #004175;
}
QPushButton:disabled {
background-color: #cccccc;
color: #666666;
}
""")
def _try_automatic_fix(self):
"""Try automatic fix again"""
if self.resource_manager:
success = self.resource_manager.apply_recommended_limits()
if success:
self._refresh_status()
from jackify.frontends.gui.services.message_service import MessageService
MessageService.information(
self,
"Success",
"File descriptor limits have been increased successfully!",
safety_level="low"
)
else:
from jackify.frontends.gui.services.message_service import MessageService
MessageService.warning(
self,
"Fix Failed",
"Automatic fix failed. Please try the manual methods shown in the tabs above.",
safety_level="medium"
)
def _refresh_status(self):
"""Refresh current status display"""
try:
if self.resource_manager:
self.status = self.resource_manager.get_limit_status()
else:
from jackify.backend.services.resource_manager import ResourceManager
temp_manager = ResourceManager()
self.status = temp_manager.get_limit_status()
# Update status display in header
header_frame = self.layout().itemAt(0).widget()
if header_frame:
# Find and update status section
header_layout = header_frame.layout()
status_layout = header_layout.itemAt(1).layout()
# Update individual labels
status_layout.itemAt(0).widget().setText(f"Current Limit: {self.status['current_soft']}")
status_layout.itemAt(1).widget().setText(f"Target Limit: {self.status['target_limit']}")
status_layout.itemAt(2).widget().setText(f"Maximum Possible: {self.status['max_possible']}")
# Update status indicator
if self.status['target_achieved']:
status_text = "✓ Optimal"
status_color = "#4caf50" # Green
elif self.status['can_increase']:
status_text = "⚠ Can Improve"
status_color = "#ff9800" # Orange
else:
status_text = "✗ Needs Manual Fix"
status_color = "#f44336" # Red
self.status_label.setText(f"Status: {status_text}")
self.status_label.setStyleSheet(f"color: {status_color}; font-weight: bold;")
# Update try again button
self.try_again_btn.setEnabled(self.status['can_increase'] and not self.status['target_achieved'])
except Exception as e:
logger.warning(f"Error refreshing status: {e}")
def closeEvent(self, event):
"""Handle dialog close event"""
if hasattr(self, 'refresh_timer'):
self.refresh_timer.stop()
event.accept()
# Convenience function for easy use
def show_ulimit_guidance(parent=None, resource_manager=None):
"""
Show the ulimit guidance dialog
Args:
parent: Parent widget for the dialog
resource_manager: Optional ResourceManager instance
Returns:
Dialog result (QDialog.Accepted or QDialog.Rejected)
"""
dialog = UlimitGuidanceDialog(resource_manager, parent)
return dialog.exec()
if __name__ == "__main__":
# Test the dialog
import sys
from PySide6.QtWidgets import QApplication
app = QApplication(sys.argv)
# Create and show dialog
result = show_ulimit_guidance()
sys.exit(result)

View File

@@ -0,0 +1,188 @@
"""
Warning Dialog
Custom warning dialog for destructive actions (e.g., deleting directory contents).
Matches Jackify theming and requires explicit user confirmation.
"""
from pathlib import Path
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QFrame, QSizePolicy, QTextEdit
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap, QIcon, QFont
from .. import shared_theme
class WarningDialog(QDialog):
"""
Jackify-themed warning dialog for dangerous/destructive actions.
Requires user to type 'DELETE' to confirm.
"""
def __init__(self, warning_message: str, parent=None):
super().__init__(parent)
self.setWindowTitle("Warning!")
self.setModal(True)
# Increased height for better text display, scalable for 800p screens
self.setFixedSize(500, 440)
self.confirmed = False
self._setup_ui(warning_message)
def _setup_ui(self, warning_message):
layout = QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
# Card background
card = QFrame(self)
card.setObjectName("warningCard")
card.setFrameShape(QFrame.StyledPanel)
card.setFrameShadow(QFrame.Raised)
card.setMinimumWidth(440)
card.setMinimumHeight(320)
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
card_layout = QVBoxLayout(card)
card_layout.setSpacing(16)
card_layout.setContentsMargins(28, 28, 28, 28)
card.setStyleSheet(
"QFrame#warningCard { "
" background: #2d2323; "
" border-radius: 12px; "
" border: 2px solid #e67e22; "
"}"
)
# Warning icon
icon_label = QLabel()
icon_label.setAlignment(Qt.AlignCenter)
icon_label.setText("!")
icon_label.setStyleSheet(
"QLabel { "
" font-size: 36px; "
" font-weight: bold; "
" color: #e67e22; "
" margin-bottom: 4px; "
"}"
)
card_layout.addWidget(icon_label)
# Warning title
title_label = QLabel("Potentially Destructive Action!")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet(
"QLabel { "
" font-size: 20px; "
" font-weight: 600; "
" color: #e67e22; "
" margin-bottom: 2px; "
"}"
)
card_layout.addWidget(title_label)
# Warning message (use a scrollable text area for long messages)
message_text = QTextEdit()
message_text.setReadOnly(True)
message_text.setPlainText(warning_message)
message_text.setMinimumHeight(80)
message_text.setMaximumHeight(160)
message_text.setStyleSheet(
"QTextEdit { "
" font-size: 15px; "
" color: #e0e0e0; "
" background: transparent; "
" border: none; "
" line-height: 1.3; "
" margin-bottom: 6px; "
" max-width: 400px; "
" min-width: 200px; "
"}"
)
card_layout.addWidget(message_text)
# Confirmation entry
confirm_label = QLabel("Type 'DELETE' to confirm:")
confirm_label.setAlignment(Qt.AlignCenter)
confirm_label.setStyleSheet(
"QLabel { "
" font-size: 13px; "
" color: #e67e22; "
" margin-bottom: 2px; "
"}"
)
card_layout.addWidget(confirm_label)
self.confirm_edit = QLineEdit()
self.confirm_edit.setAlignment(Qt.AlignCenter)
self.confirm_edit.setPlaceholderText("DELETE")
self.confirm_edit.setStyleSheet(
"QLineEdit { "
" font-size: 15px; "
" border: 1px solid #e67e22; "
" border-radius: 6px; "
" padding: 6px; "
" background: #23272e; "
" color: #e67e22; "
"}"
)
card_layout.addWidget(self.confirm_edit)
# Action buttons
button_layout = QHBoxLayout()
button_layout.setSpacing(12)
button_layout.addStretch()
cancel_btn = QPushButton("Cancel")
cancel_btn.setFixedSize(120, 36)
cancel_btn.clicked.connect(self.reject)
cancel_btn.setStyleSheet(
"QPushButton { "
" background-color: #95a5a6; "
" color: white; "
" border: none; "
" border-radius: 4px; "
" font-weight: bold; "
" padding: 8px 16px; "
"} "
"QPushButton:hover { "
" background-color: #7f8c8d; "
"} "
"QPushButton:pressed { "
" background-color: #6c7b7d; "
"}"
)
button_layout.addWidget(cancel_btn)
confirm_btn = QPushButton("Proceed")
confirm_btn.setFixedSize(120, 36)
confirm_btn.clicked.connect(self._on_confirm)
confirm_btn.setStyleSheet(
"QPushButton { "
" background-color: #e67e22; "
" color: white; "
" border: none; "
" border-radius: 4px; "
" font-weight: bold; "
" padding: 8px 16px; "
"} "
"QPushButton:hover { "
" background-color: #d35400; "
"} "
"QPushButton:pressed { "
" background-color: #b34700; "
"}"
)
button_layout.addWidget(confirm_btn)
button_layout.addStretch()
card_layout.addLayout(button_layout)
layout.addStretch()
layout.addWidget(card, alignment=Qt.AlignCenter)
layout.addStretch()
def _on_confirm(self):
if self.confirm_edit.text().strip().upper() == "DELETE":
self.confirmed = True
self.accept()
else:
self.confirm_edit.setText("")
self.confirm_edit.setPlaceholderText("Type DELETE to confirm")
self.confirm_edit.setStyleSheet(self.confirm_edit.styleSheet() + "QLineEdit { background: #3b2323; }")