feat(scripts): reuse python functions #158
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=""
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user