feat(scripts): reuse python functions #158

Merged
Boria138 merged 2 commits from unify into main 2026-05-22 07:15:27 +00:00
17 changed files with 359 additions and 199 deletions

No files matched your search

@@ -145,7 +145,7 @@ pw_enable_idle_inhibit() {
return
fi
PW_INHIBIT_SLR="python3 -m portprotonqt.idle_inhibit"
PW_INHIBIT_SLR="python3 -m portprotonqt.scripts_utils.idle_inhibit"
print_info "Screensaver will be inhibited using D-Bus"
}
@@ -1627,8 +1627,10 @@ stop_portproton () {
find "${PORT_DATA_PATH}/data/prefixes/${PW_PREFIX_NAME}/drive_c/" -maxdepth 1 -type f -name "*.tmp" -delete
pw_auto_create_shortcut
add_in_stop_portproton
if [[ -n "$PW_POWERPROFILECTL_PROFILE" ]] && [[ "$(pw_ppd_dbus_get_profile)" != "$PW_POWERPROFILECTL_PROFILE" ]] ; then
pw_ppd_dbus_set_profile "$PW_POWERPROFILECTL_PROFILE"
if [[ -n "$PW_POWERPROFILECTL_PROFILE" ]] \
&& [[ "$(python3 -m portprotonqt.scripts_utils.power_profiles get)" != "$PW_POWERPROFILECTL_PROFILE" ]]
then
python3 -m portprotonqt.scripts_utils.power_profiles set "$PW_POWERPROFILECTL_PROFILE" &>/dev/null
fi
if [[ $PW_LOG != 1 ]] && [[ -n $START_PW_TIME_IN_GAME ]] ; then
@@ -2251,15 +2253,12 @@ pw_create_gui_png () {
if [[ -z $name_desktop_png ]] ; then
basename_PW_EXE_FILE="$(basename "${PW_EXE_FILE}")"
if echo "$basename_PW_EXE_FILE" | grep -ie 'setup\|install\|\.msi$' &>/dev/null ; then
export PW_ICON_FOR_YAD="${PORT_DATA_PATH}/data/img/setup.png"
export name_desktop_png="setup"
return 0
elif echo "$basename_PW_EXE_FILE" | grep -ie '\.reg$' &>/dev/null ; then
export PW_ICON_FOR_YAD="${PORT_DATA_PATH}/data/img/reg.png"
export name_desktop_png="reg"
return 0
elif echo "$basename_PW_EXE_FILE"| grep -ie '\.bat$' &>/dev/null ; then
export PW_ICON_FOR_YAD="${PORT_DATA_PATH}/data/img/bat.png"
export name_desktop_png="bat"
return 0
fi
@@ -2304,7 +2303,7 @@ pw_create_gui_png () {
fi
fi
resize_png "$PW_EXE_FILE" "${PORTPROTON_NAME}" "48" "128"
resize_png "$PW_EXE_FILE" "${PORTPROTON_NAME}"
PORTPROTON_NAME_PNG="${PORTPROTON_NAME// /_}"
if [[ $PORTPROTON_NAME_PNG =~ [\!\%\$\&\<] ]] ; then
@@ -2313,15 +2312,9 @@ pw_create_gui_png () {
PORTPROTON_NAME_PNG="${PORTPROTON_NAME_PNG//$i/}"
done
fi
if [[ -z "${PW_ICON_FOR_YAD}" ]] ; then
if [[ -n "$(file "${PORT_DATA_PATH}/data/img/${PORTPROTON_NAME_PNG}.png" | grep "${PW_RESIZE_TO} x ${PW_RESIZE_TO}")" ]] ; then
export PW_ICON_FOR_YAD="${PORT_DATA_PATH}/data/img/${PORTPROTON_NAME_PNG}.png"
else
export PW_ICON_FOR_YAD="${PW_GUI_ICON_PATH}/port_proton.png"
fi
fi
}
pw_create_unique_exe () {
BASEDIR_GAME="$(dirname "$PW_EXE_FILE")"
if [[ -d "$BASEDIR_GAME" ]] ; then
@@ -2897,48 +2890,6 @@ start_portproton () {
unset __GLX_VENDOR_LIBRARY_NAME
fi
pw_ppd_dbus_get_profile() {
if ! command -v gdbus &>/dev/null; then
return 1
fi
local result
result=$(gdbus call --system \
--dest net.hadess.PowerProfiles \
--object-path /net/hadess/PowerProfiles \
--method org.freedesktop.DBus.Properties.Get \
'net.hadess.PowerProfiles' 'ActiveProfile' 2>/dev/null)
[[ -n "$result" ]] && echo "$result" | sed "s/.*'\([^']*\)'.*/\1/" || return 1
}
export -f pw_ppd_dbus_get_profile
pw_ppd_dbus_set_profile() {
local profile="$1"
if ! command -v gdbus &>/dev/null; then
return 1
fi
gdbus call --system \
--dest net.hadess.PowerProfiles \
--object-path /net/hadess/PowerProfiles \
--method org.freedesktop.DBus.Properties.Set \
'net.hadess.PowerProfiles' 'ActiveProfile' "<'$profile'>" &>/dev/null
}
export -f pw_ppd_dbus_set_profile
pw_ppd_dbus_has_profile() {
local profile="$1"
if ! command -v gdbus &>/dev/null; then
return 1
fi
local result
result=$(gdbus call --system \
--dest net.hadess.PowerProfiles \
--object-path /net/hadess/PowerProfiles \
--method org.freedesktop.DBus.Properties.GetAll \
'net.hadess.PowerProfiles' 2>/dev/null)
[[ "$result" == *"'$profile'"* ]]
}
export -f pw_ppd_dbus_has_profile
if check_gamescope_session ; then
export PW_GAMEMODERUN_SLR=""
elif [[ "$PW_USE_GAMEMODE" = "1" ]] \
@@ -2947,9 +2898,12 @@ export -f pw_ppd_dbus_has_profile
if pw_is_process_active ananicy ananicy-cpp scx scx_loader falcond ; then
export GAMEMODERUN=0
export PW_GAMEMODERUN_SLR=""
if pw_ppd_dbus_has_profile "performance" && [[ "$(pw_ppd_dbus_get_profile)" != "performance" ]] ; then
export PW_POWERPROFILECTL_PROFILE=$(pw_ppd_dbus_get_profile)
pw_ppd_dbus_set_profile performance
local current_power_profile
current_power_profile=$(python3 -m portprotonqt.scripts_utils.power_profiles get)
if [[ -n "$current_power_profile" && "$current_power_profile" != "performance" ]] \
&& python3 -m portprotonqt.scripts_utils.power_profiles set performance &>/dev/null
then
export PW_POWERPROFILECTL_PROFILE="$current_power_profile"
print_info "Gamemode replaced by power-profiles-daemon/tuned-ppd/tlp-pd via D-Bus to avoid conflicts"
else
export PW_POWERPROFILECTL_PROFILE=""
@@ -4443,11 +4397,11 @@ pw_gamescope_process () {
# RESIZE PNG FILE
resize_png () {
if [[ -z "$1" ]] || [[ -z "$2" ]] || [[ -z "$3" ]] ; then
if [[ -z "$1" ]] || [[ -z "$2" ]] ; then
print_error "no argument specified for resize_png"
return 1
else
local RESIZE_FILE RESIZE_NAME_PNG resize_to resize_to_helper
local RESIZE_FILE RESIZE_NAME_PNG
RESIZE_FILE="$1"
RESIZE_NAME_PNG="${2// /_}"
fi
@@ -4456,44 +4410,26 @@ resize_png () {
then RESIZE_NAME_PNG=${RESIZE_NAME_PNG/*ru.linux_gaming.PortProton\/data\/img\//}
fi
for resize_to in "${@:3}" ; do
if [[ $resize_to == 128 ]]
then resize_to_helper=""
else resize_to_helper="_$resize_to"
fi
if [[ $RESIZE_NAME_PNG =~ [\!\%\$\&\<] ]] ; then
local ICON_NAME_REGEX=(\! % \$ \& \<)
for i in "${ICON_NAME_REGEX[@]}" ; do
RESIZE_NAME_PNG="${RESIZE_NAME_PNG//$i/}"
done
fi
if [[ $RESIZE_NAME_PNG =~ [\!\%\$\&\<] ]] ; then
local ICON_NAME_REGEX=(\! % \$ \& \<)
for i in "${ICON_NAME_REGEX[@]}" ; do
RESIZE_NAME_PNG="${RESIZE_NAME_PNG//$i/}"
done
fi
if [[ -f "${PORT_DATA_PATH}/data/img/${RESIZE_NAME_PNG}.png" ]] \
|| [[ ! -f "${RESIZE_FILE}" ]] \
|| [[ ! ${RESIZE_FILE,,} =~ .exe$ ]]
then
return 0
fi
if [[ -f "${PORT_DATA_PATH}/data/img/${RESIZE_NAME_PNG}${resize_to_helper}.png" ]] \
|| [[ ! -f "${RESIZE_FILE}" ]] \
|| [[ ! ${RESIZE_FILE,,} =~ .exe$ ]]
then
return 0
fi
try_remove_file "${PORT_DATA_PATH}/data/img/launcher.png"
try_remove_file "${PORT_DATA_PATH}/data/img/Launcher.png"
try_remove_file "${PORT_DATA_PATH}/data/img/launcher.png"
try_remove_file "${PORT_DATA_PATH}/data/img/Launcher.png"
if check_flatpak ; then
if ! timeout 3 \
exe-thumbnailer --force-resize -s "$resize_to" "$(readlink -f "${RESIZE_FILE}")" "${PORT_DATA_PATH}/data/img/${RESIZE_NAME_PNG}${resize_to_helper}.png" \
&& [[ "$ALPINE_FP" != "1" ]]
then
print_error "exe-thumbnailer - broken!"
fi
else
print_warning "use portable exe-thumbnailer"
env PYTHONPATH="${PW_PLUGINS_PATH}/portable/lib/python3.9/site-packages/" \
LD_LIBRARY_PATH="${PW_PLUGINS_PATH}/portable/lib/lib64" \
"${PW_WINELIB}/runtime/files/bin/python3.9" \
"${PW_PLUGINS_PATH}/portable/bin/exe-thumbnailer" --force-resize -s "$resize_to" "$(readlink -f "${RESIZE_FILE}")" "${PORT_DATA_PATH}/data/img/${RESIZE_NAME_PNG}${resize_to_helper}.png"
fi
done
python3 -m portprotonqt.scripts_utils.shortcut_tools thumbnail \
"$(readlink -f "${RESIZE_FILE}")" \
"${PORT_DATA_PATH}/data/img/${RESIZE_NAME_PNG}.png"
}
portwine_create_shortcut () {
@@ -4527,19 +4463,12 @@ portwine_output_yad_shortcut () {
export name_desktop="$PW_NAME_DESKTOP_PROXY"
fi
echo "[Desktop Entry]" > "${PORT_DATA_PATH}/${name_desktop}.desktop"
echo "Name=${name_desktop}" >> "${PORT_DATA_PATH}/${name_desktop}.desktop"
if check_flatpak
then echo "Exec=flatpak run ${FLATPAK_ID} --silent \"${PW_EXE_FILE}\"" >> "${PORT_DATA_PATH}/${name_desktop}.desktop"
else echo "Exec=portprotonqt --silent \"${PW_EXE_FILE}\"" >> "${PORT_DATA_PATH}/${name_desktop}.desktop"
if ! python3 -m portprotonqt.scripts_utils.shortcut_tools shortcut \
"${PW_EXE_FILE}" \
"${name_desktop}"
then
return 1
fi
{
echo "Type=Application"
echo "Categories=Game"
echo "StartupNotify=true"
echo "Icon=${PORT_DATA_PATH}/data/img/${name_desktop_png}.png"
} >> "${PORT_DATA_PATH}/${name_desktop}.desktop"
chmod +x "${PORT_DATA_PATH}/${name_desktop}.desktop"
if [[ "${PW_SHORTCUT_MENU}" == "TRUE" ]] ; then
try_remove_file "${HOME}/.local/share/applications/${name_desktop}.desktop"
@@ -4609,7 +4538,7 @@ pw_auto_create_shortcut () {
pw_init_db
export PW_NO_RESTART_PPDB="1"
edit_db_from_gui PW_PREFIX_NAME LAUNCH_URI
unset PW_ICON_FOR_YAD name_desktop_png
unset name_desktop_png
portwine_create_shortcut
fi
fi
@@ -4643,9 +4572,9 @@ pw_auto_create_shortcut () {
fi
print_info "Created link for: $link_name"
PORTPROTON_NAME="$link_name"
resize_png "$PW_EXE_FILE" "${PORTPROTON_NAME}" "48" "128"
resize_png "$PW_EXE_FILE" "${PORTPROTON_NAME}"
export PW_NO_RESTART_PPDB=1
unset PW_ICON_FOR_YAD name_desktop_png
unset name_desktop_png
portwine_create_shortcut
done
}
@@ -6,7 +6,6 @@ export PW_VULKAN_USE="0"
export PW_WINE_USE="PROTON_LG"
export PW_PREFIX_NAME="STUDENT"
export PW_DISABLED_CREATE_DB="1"
export PW_ICON_FOR_YAD="${PORT_DATA_PATH}/data/img/setup.png"
export PW_USE_FSYNC="0"
export PW_USE_ESYNC="0"
export PW_DLL_INSTALL=""
@@ -6,7 +6,6 @@ export PW_VULKAN_USE="0"
export PW_WINE_USE="PROTON_LG"
export PW_PREFIX_NAME="TEACHER"
export PW_DISABLED_CREATE_DB="1"
export PW_ICON_FOR_YAD="${PORT_DATA_PATH}/data/img/setup.png"
export PW_USE_FSYNC="0"
export PW_USE_ESYNC="0"
export PW_DLL_INSTALL=""
+13 -4
View File
@@ -19,6 +19,14 @@ logger = get_logger(__name__)
_portproton_start_command: list[str] | None = None
def _sanitize_icon_name(name: str) -> str:
"""Convert a game name to a safe icon file name."""
sanitized_name = name.replace(" ", "_")
for char in ("!", "%", "$", "&", "<"):
sanitized_name = sanitized_name.replace(char, "")
return sanitized_name
class PortProtonConfig(BaseConfig):
"""PortProton location configuration."""
@@ -356,8 +364,8 @@ def find_game_by_exe(exe_path: str) -> configparser.SectionProxy | None:
def create_desktop_file(
exe_path: str,
game_name: str | None = None,
) -> tuple[str, str] | None:
"""Create desktop entry content and destination path for a game."""
) -> tuple[str, str, str] | None:
"""Create desktop entry content, destination path and icon path for a game."""
portproton_path = get_portproton_location()
if not os.path.isfile(exe_path):
logger.error("Executable not found: %s", exe_path)
@@ -369,7 +377,8 @@ def create_desktop_file(
if not game_name:
game_name = os.path.splitext(os.path.basename(exe_path))[0]
base_path = os.path.join(portproton_path, "data")
icon_path = os.path.join(base_path, "img", f"{game_name}.png")
icon_name = _sanitize_icon_name(game_name)
icon_path = os.path.join(base_path, "img", f"{icon_name}.png")
desktop_path = os.path.join(portproton_path, f"{game_name}.desktop")
os.makedirs(os.path.dirname(icon_path), exist_ok=True)
@@ -391,4 +400,4 @@ def create_desktop_file(
"StartupNotify=true\n"
f"Icon={icon_path}\n"
)
return desktop_entry, desktop_path
return desktop_entry, desktop_path, icon_path
+2 -1
View File
@@ -21,7 +21,8 @@ from portprotonqt.steam_api import (
remove_from_steam,
safe_vdf_load,
)
from portprotonqt.dialogs import AddGameDialog, generate_thumbnail
from portprotonqt.icon_extractor import generate_thumbnail
from portprotonqt.dialogs import AddGameDialog
from portprotonqt.dialogs.base import DraggableDialog
from portprotonqt.theme_manager import ThemeManager
from portprotonqt.logger import get_logger
+1 -1
View File
@@ -5,10 +5,10 @@ New code should import directly from portprotonqt.dialogs package.
"""
# Re-export all dialog classes for backward compatibility
from portprotonqt.icon_extractor import generate_thumbnail
from portprotonqt.dialogs.base import (
GameLaunchDialog,
AddGameDialog,
generate_thumbnail,
FileSelectedSignal,
)
from portprotonqt.dialogs.file_explorer import FileExplorer
+2 -1
View File
@@ -5,7 +5,8 @@ from portprotonqt.dialogs.file_explorer import FileExplorer
from portprotonqt.dialogs.winetricks_dialog import WinetricksDialog
from portprotonqt.dialogs.settings_dialog import ExeSettingsDialog
from portprotonqt.dialogs.proton_manager import ProtonManager
from portprotonqt.dialogs.base import GameLaunchDialog, AddGameDialog, generate_thumbnail, FileSelectedSignal
from portprotonqt.icon_extractor import generate_thumbnail
from portprotonqt.dialogs.base import GameLaunchDialog, AddGameDialog, FileSelectedSignal
__all__ = [
'create_dialog_hints_widget',
+2 -72
View File
@@ -9,7 +9,6 @@ from PySide6.QtWidgets import (
QProgressBar, QSizePolicy, QWidget, QCheckBox
)
from PySide6.QtCore import Qt, QObject, Signal, QTimer, QStandardPaths, QSize
from PIL import Image
import psutil
if TYPE_CHECKING:
@@ -23,80 +22,13 @@ from portprotonqt.downloader import Downloader
from portprotonqt.virtual_keyboard import VirtualKeyboard
from portprotonqt.localization import _
from portprotonqt.image_utils import COVER_IMAGE_EXTENSIONS, set_animated_cover
from portprotonqt.icon_extractor import IconExtractor, IconExtractorError
from portprotonqt.icon_extractor import generate_thumbnail
logger = get_logger(__name__)
theme_manager = ThemeManager()
DISC_IMAGE_EXTENSIONS = (".iso", ".mdf")
def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
"""
Generates a thumbnail for an .exe file.
inputfile: the input file path (%i)
outfile: output filename (%o)
size: determines the thumbnail output size (%s)
"""
logger.debug(f"Starting thumbnail generation: {inputfile} → {outfile}, size={size}, force={force_resize}")
try:
extractor = IconExtractor(inputfile)
logger.debug("IconExtractor created successfully.")
except (RuntimeError, IconExtractorError) as e:
logger.warning(f"Failed to create IconExtractor: {e}")
return False
try:
data = extractor.get_icon()
if data is None:
return False
im = Image.open(data)
logger.debug(f"Extracted icon size {im.size}, formats: {im.format}, frames: {getattr(im, 'n_frames', 1)}")
except Exception as e:
logger.warning(f"Error extracting icon: {e}")
return False
if force_resize:
logger.debug(f"Forcing icon resize to {size}x{size}")
im = im.resize((size, size), Image.Resampling.LANCZOS)
else:
if size > 256:
logger.warning('Requested size larger than 256, set to 256')
size = 256
elif size not in (128, 256):
logger.warning(f'Unsupported size {size}, set to 128')
size = 128
if size == 256:
logger.debug("Saving icon without resize (256x256)")
im.save(outfile, "PNG")
logger.info(f"Icon saved to {outfile}")
return True
frames = getattr(im, 'n_frames', 1)
try:
for frame in range(frames):
im.seek(frame)
if im.size == (size, size):
logger.debug(f"Found frame with size {size}x{size}")
break
except EOFError:
logger.debug("Frames ended before finding required size.")
if im.size != (size, size):
logger.debug(f"Resizing from {im.size} to {size}x{size}")
im = im.resize((size, size))
try:
im.save(outfile, "PNG")
logger.info(f"Thumbnail successfully saved to {outfile}")
return True
except Exception as e:
logger.error(f"Error saving thumbnail: {e}")
return False
class FileSelectedSignal(QObject):
"""Signal class for file selection."""
file_selected = Signal(str)
@@ -538,9 +470,7 @@ class AddGameDialog(DraggableDialog):
result = create_desktop_file(exe_path, name)
if not result:
return None, None
desktop_entry, desktop_path = result
base_path = os.path.join(os.path.dirname(desktop_path), "data")
icon_path = os.path.join(base_path, "img", f"{name}.png")
desktop_entry, desktop_path, icon_path = result
if not self.last_cover_path and not generate_thumbnail(launch_path, icon_path, size=128):
logger.error(f"Failed to generate thumbnail from exe: {launch_path}")
+2 -1
View File
@@ -21,7 +21,8 @@ from portprotonqt.theme_manager import ThemeManager
from portprotonqt.custom_widgets import AutoSizeButton
from portprotonqt.localization import _
from portprotonqt.dialogs.dialog_utils import create_dialog_hints_widget, update_dialog_hints
from portprotonqt.dialogs.base import DraggableDialog, generate_thumbnail, FileSelectedSignal
from portprotonqt.icon_extractor import generate_thumbnail
from portprotonqt.dialogs.base import DraggableDialog, FileSelectedSignal
from portprotonqt.image_utils import COVER_IMAGE_EXTENSIONS
logger = get_logger(__name__)
+30
View File
@@ -12,6 +12,36 @@ RT_ICON = 3
RT_GROUP_ICON = 14
THUMBNAIL_SIZE = 128
def generate_thumbnail(inputfile: str, outfile: str, size: int = 128, force_resize: bool = True) -> bool:
"""
Generates a thumbnail for an .exe file.
inputfile: the input file path
outfile: output filename
size: determines the thumbnail output size
"""
logger.debug("Starting thumbnail generation: %s → %s, size=%s, force=%s", inputfile, outfile, size, force_resize)
try:
extractor = IconExtractor(inputfile)
data = extractor.get_icon()
if data is None:
return False
im = Image.open(data)
if force_resize or im.size != (size, size):
logger.debug("Resizing icon to %sx%s", size, size)
im = im.resize((size, size), Image.Resampling.LANCZOS)
im.save(outfile, "PNG")
logger.info("Thumbnail successfully saved to %s", outfile)
return True
except Exception as e:
logger.error("Error generating thumbnail: %s", e)
return False
class IconExtractorError(Exception):
"""Base exception for icon extraction errors."""
+2 -1
View File
@@ -6,7 +6,8 @@ import subprocess
import psutil
import re
from portprotonqt.logger import get_logger
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog, ExeSettingsDialog, generate_thumbnail
from portprotonqt.icon_extractor import generate_thumbnail
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog, ExeSettingsDialog
from portprotonqt.game_card import GameCard
from portprotonqt.animations import DetailPageAnimations
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout
+10 -1
View File
@@ -12,7 +12,6 @@ install_data(
'game_card.py',
'game_library_manager.py',
'howlongtobeat_api.py',
'idle_inhibit.py',
'image_utils.py',
'icon_extractor.py',
'input_manager.py',
@@ -119,6 +118,16 @@ install_data(
install_dir: pythondir / meson.project_name() / 'debug_utils',
)
# Install scripts_utils package
install_data(
['scripts_utils/__init__.py',
'scripts_utils/idle_inhibit.py',
'scripts_utils/power_profiles.py',
'scripts_utils/shortcut_tools.py',
],
install_dir: pythondir / meson.project_name() / 'scripts_utils',
)
# Install config package
install_data(
['config/__init__.py',
Whitespace-only changes.
@@ -5,6 +5,9 @@ import sys
from collections.abc import Awaitable
from typing import Any
import dbus_fast
import dbus_fast.aio
from portprotonqt.logger import get_logger
from portprotonqt.localization import _
@@ -38,9 +41,6 @@ async def _dbus_call(awaitable: Awaitable[Any]) -> Any:
async def _request_inhibit() -> tuple[Any, int, Any]:
import dbus_fast
import dbus_fast.aio
bus = await _dbus_call(dbus_fast.aio.MessageBus().connect())
introspection = await _dbus_call(bus.introspect(BUS_NAME, BUS_PATH))
proxy = bus.get_proxy_object(BUS_NAME, BUS_PATH, introspection)
@@ -0,0 +1,162 @@
"""Power profile tools for PortProton scripts."""
import argparse
import asyncio
import sys
from typing import Any
from dbus_fast import BusType, Message, Variant
from dbus_fast.aio import MessageBus
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
Endpoint = tuple[str, str]
POWER_PROFILE_ENDPOINTS: tuple[Endpoint, ...] = (
(
"org.freedesktop.UPower.PowerProfiles",
"/org/freedesktop/UPower/PowerProfiles",
),
(
"net.hadess.PowerProfiles",
"/net/hadess/PowerProfiles",
),
)
PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties"
ACTIVE_PROFILE = "ActiveProfile"
PROFILES = "Profiles"
DBUS_TIMEOUT = 10
async def _dbus_call(bus: Any, message: Any) -> Any:
reply = await asyncio.wait_for(bus.call(message), timeout=DBUS_TIMEOUT)
if reply.message_type.name == "ERROR":
return None
return reply.body
async def _get_property(bus: Any, endpoint: Endpoint, name: str) -> Any:
bus_name, bus_path = endpoint
body = await _dbus_call(
bus,
Message(
destination=bus_name,
path=bus_path,
interface=PROPERTIES_INTERFACE,
member="Get",
signature="ss",
body=[bus_name, name],
),
)
if not body:
return None
return _variant_value(body[0])
async def _active_profile(bus: Any) -> tuple[Endpoint, str] | None:
for endpoint in POWER_PROFILE_ENDPOINTS:
try:
profile = await _get_property(bus, endpoint, ACTIVE_PROFILE)
except Exception as error:
logger.debug("Power profile endpoint unavailable: %s", error)
continue
if isinstance(profile, str):
return endpoint, profile
return None
async def get_profile() -> str | None:
"""Return active power profile."""
bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
active_profile = await _active_profile(bus)
if not active_profile:
return None
return active_profile[1]
async def set_profile(profile: str) -> bool:
"""Set active power profile."""
bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
active_profile = await _active_profile(bus)
if not active_profile:
return False
bus_name, bus_path = active_profile[0]
profiles = await _get_property(bus, active_profile[0], PROFILES)
if profile not in _profile_names(profiles):
return False
body = await _dbus_call(
bus,
Message(
destination=bus_name,
path=bus_path,
interface=PROPERTIES_INTERFACE,
member="Set",
signature="ssv",
body=[bus_name, ACTIVE_PROFILE, Variant("s", profile)],
),
)
return body is not None
def _variant_value(value: Any) -> Any:
if isinstance(value, Variant):
return _variant_value(value.value)
if isinstance(value, dict):
return {
_variant_value(key): _variant_value(item)
for key, item in value.items()
}
if isinstance(value, list):
return [_variant_value(item) for item in value]
return value
def _profile_names(profiles: Any) -> set[str]:
if not isinstance(profiles, list):
return set()
names = set()
for profile in profiles:
if isinstance(profile, dict) and isinstance(profile.get("Profile"), str):
names.add(profile["Profile"])
return names
def parse_args() -> argparse.Namespace:
"""Parse arguments for power profile tools."""
parser = argparse.ArgumentParser(description="PortProtonQt power profile tools")
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("get", help="Print active power profile")
set_parser = subparsers.add_parser("set", help="Set active power profile")
set_parser.add_argument("profile", help="Power profile name")
return parser.parse_args()
async def _main() -> int:
args = parse_args()
try:
if args.command == "get":
profile = await get_profile()
if not profile:
return 1
print(profile)
return 0
if args.command == "set":
return 0 if await set_profile(args.profile) else 1
except Exception as error:
logger.debug("Power profile command failed: %s", error)
return 1
return 1
def main() -> int:
return asyncio.run(_main())
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,89 @@
"""Entry point for shortcut and thumbnail generation tools."""
import argparse
import os
import sys
from pathlib import Path
from portprotonqt.cli import normalize_launch_path
def create_shortcut(exe_path: str, game_name: str | None = None) -> bool:
"""Create a PortProtonQt desktop shortcut for an executable."""
from portprotonqt.config import create_desktop_file
from portprotonqt.icon_extractor import generate_thumbnail
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
normalized_path = normalize_launch_path(exe_path)
result = create_desktop_file(normalized_path, game_name)
if not result:
return False
desktop_entry, desktop_path, icon_path = result
if not generate_thumbnail(normalized_path, icon_path, size=128):
logger.error("Failed to generate thumbnail from exe: %s", normalized_path)
desktop_entry = desktop_entry.replace(f"Icon={icon_path}\n", "Icon=\n")
try:
Path(desktop_path).write_text(desktop_entry, encoding="utf-8")
os.chmod(desktop_path, 0o755)
except OSError as error:
logger.error("Failed to save desktop file %s: %s", desktop_path, error)
return False
return True
def create_thumbnail(exe_path: str, output_path: str) -> bool:
"""Create a PNG thumbnail for an executable."""
from portprotonqt.icon_extractor import generate_thumbnail
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
normalized_path = normalize_launch_path(exe_path)
if not normalized_path or not output_path:
return False
try:
output_dir = os.path.dirname(output_path)
if output_dir:
os.makedirs(output_dir, exist_ok=True)
except OSError as error:
logger.error("Failed to prepare thumbnail directory %s: %s", output_path, error)
return False
if not generate_thumbnail(normalized_path, output_path, size=128):
logger.error("Failed to generate thumbnail: %s", normalized_path)
return False
return True
def parse_args():
"""Parse arguments for shortcut tools."""
parser = argparse.ArgumentParser(description="PortProtonQt shortcut tools")
subparsers = parser.add_subparsers(dest="command", required=True)
shortcut_parser = subparsers.add_parser("shortcut", help="Create a desktop shortcut")
shortcut_parser.add_argument("exe_path", help="Path to the executable")
shortcut_parser.add_argument("game_name", help="Name of the game")
thumbnail_parser = subparsers.add_parser("thumbnail", help="Create a PNG thumbnail")
thumbnail_parser.add_argument("exe_path", help="Path to the executable")
thumbnail_parser.add_argument("output_path", help="Path to the output PNG file")
return parser.parse_args()
def main() -> int:
"""Main entry point for shortcut tools."""
args = parse_args()
if args.command == "shortcut":
return 0 if create_shortcut(args.exe_path, args.game_name) else 1
if args.command == "thumbnail":
return 0 if create_thumbnail(args.exe_path, args.output_path) else 1
return 1
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -15,7 +15,7 @@ import websocket
from portprotonqt.logger import get_logger
from portprotonqt.downloader import Downloader
from portprotonqt.dialogs import generate_thumbnail
from portprotonqt.icon_extractor import generate_thumbnail
from portprotonqt.config import (
extract_exec_target_path,
get_portproton_location,