aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt5
-rw-r--r--data/icons/com.fluorine.manager.desktop (renamed from data/com.fluorine.manager.desktop)0
-rw-r--r--data/icons/com.fluorine.manager.metainfo.xml (renamed from data/com.fluorine.manager.metainfo.xml)0
-rw-r--r--data/icons/com.fluorine.manager.png (renamed from data/com.fluorine.manager.png)bin51022 -> 51022 bytes
-rw-r--r--docker/Dockerfile2
-rwxr-xr-xdocker/build-inner.sh36
-rw-r--r--libs/basic_games_native/src/basicgameplugin.cpp4
-rw-r--r--libs/nak/Cargo.lock63
-rw-r--r--libs/nak/Cargo.toml1
-rw-r--r--libs/nak/src/icons.rs195
-rw-r--r--libs/nak/src/lib.rs1
-rw-r--r--libs/nak_ffi/CMakeLists.txt2
-rw-r--r--libs/nak_ffi/Cargo.lock63
-rw-r--r--libs/nak_ffi/include/nak_ffi.h17
-rw-r--r--libs/nak_ffi/src/lib.rs51
-rw-r--r--libs/uibase/src/CMakeLists.txt4
-rw-r--r--libs/uibase/src/utility.cpp98
-rw-r--r--src/src/mainwindow.cpp3
-rw-r--r--src/src/moapplication.cpp2
19 files changed, 463 insertions, 84 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 700e55f..21e13f9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -154,12 +154,13 @@ add_subdirectory(libs/libbsarch) # shared + static, needs dds-header
add_subdirectory(libs/7zip) # builds 7z.so from source (Linux only)
add_subdirectory(libs/archive) # shared lib, needs dl
add_subdirectory(libs/lootcli) # executable, needs Qt6 + boost + libloot + curl
-add_subdirectory(libs/uibase) # shared lib, needs Qt6 + spdlog
if(BUILD_NAK_FFI)
- add_subdirectory(libs/nak_ffi)
+ add_subdirectory(libs/nak_ffi) # Rust FFI lib, before uibase (icon extraction)
endif()
+add_subdirectory(libs/uibase) # shared lib, needs Qt6 + spdlog + nak_ffi (Linux)
+
if(BUILD_BSA_FFI)
add_subdirectory(libs/bsa_ffi)
endif()
diff --git a/data/com.fluorine.manager.desktop b/data/icons/com.fluorine.manager.desktop
index ff62c76..ff62c76 100644
--- a/data/com.fluorine.manager.desktop
+++ b/data/icons/com.fluorine.manager.desktop
diff --git a/data/com.fluorine.manager.metainfo.xml b/data/icons/com.fluorine.manager.metainfo.xml
index 814607f..814607f 100644
--- a/data/com.fluorine.manager.metainfo.xml
+++ b/data/icons/com.fluorine.manager.metainfo.xml
diff --git a/data/com.fluorine.manager.png b/data/icons/com.fluorine.manager.png
index 3a9fbdd..3a9fbdd 100644
--- a/data/com.fluorine.manager.png
+++ b/data/icons/com.fluorine.manager.png
Binary files differ
diff --git a/docker/Dockerfile b/docker/Dockerfile
index d197569..5ab99dc 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -35,7 +35,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Python
python3 python3-pip python3-pyqt6 \
# Misc
- icoutils zip unzip \
+ zip unzip \
&& rm -rf /var/lib/apt/lists/*
# ── Qt 6.10 via aqtinstall ──
diff --git a/docker/build-inner.sh b/docker/build-inner.sh
index 0f6fcdb..0c0fbc7 100755
--- a/docker/build-inner.sh
+++ b/docker/build-inner.sh
@@ -50,9 +50,7 @@ cp -f "${RUNDIR}/ModOrganizer" "${OUT_DIR}/ModOrganizer-core"
LOOTCLI="build/libs/lootcli/src/lootcli"
[ -f "${LOOTCLI}" ] && cp -f "${LOOTCLI}" "${OUT_DIR}/"
-for tool in wrestool icotool; do
- command -v "${tool}" >/dev/null 2>&1 && cp -f "$(command -v "${tool}")" "${OUT_DIR}/"
-done
+# wrestool/icotool no longer needed — icon extraction is built into libnak_ffi.so (pelite)
# ── MO2 plugins (.so) ──
find build/libs -type f \( \
@@ -216,7 +214,7 @@ strip --strip-unneeded "${OUT_DIR}/ModOrganizer-core" 2>/dev/null || true
find "${OUT_DIR}/plugins" -name "*.so" -exec strip --strip-unneeded {} \; 2>/dev/null || true
find "${OUT_DIR}/dlls" -name "*.so" -o -name "*.dll" | xargs -r strip --strip-unneeded 2>/dev/null || true
find "${OUT_DIR}/lib" -name "*.so" -exec strip --strip-unneeded {} \; 2>/dev/null || true
-for tool in wrestool icotool lootcli; do
+for tool in lootcli; do
[ -f "${OUT_DIR}/${tool}" ] && strip --strip-unneeded "${OUT_DIR}/${tool}" 2>/dev/null || true
done
@@ -269,6 +267,21 @@ if [ "${HERE_REAL}" != "${DST_REAL}" ]; then
fi
fi
+# ── Install icon + desktop file for Wayland taskbar/decoration ──
+ICON_SRC="${BIN_DST}/icons/com.fluorine.manager.png"
+ICON_DST="${HOME}/.local/share/icons/hicolor/256x256/apps/com.fluorine.manager.png"
+DESKTOP_SRC="${BIN_DST}/icons/com.fluorine.manager.desktop"
+DESKTOP_DST="${HOME}/.local/share/applications/com.fluorine.manager.desktop"
+if [ -f "${ICON_SRC}" ] && [ ! -f "${ICON_DST}" ]; then
+ mkdir -p "$(dirname "${ICON_DST}")"
+ cp -f "${ICON_SRC}" "${ICON_DST}"
+fi
+if [ -f "${DESKTOP_SRC}" ] && [ ! -f "${DESKTOP_DST}" ]; then
+ mkdir -p "$(dirname "${DESKTOP_DST}")"
+ sed "s|^Exec=fluorine-manager|Exec=${BIN_DST}/fluorine-manager|" "${DESKTOP_SRC}" > "${DESKTOP_DST}"
+ chmod +x "${DESKTOP_DST}"
+fi
+
# Run from the synced location.
RUN="${BIN_DST}"
@@ -295,9 +308,10 @@ Plugins = qt6plugins
QTCONF
# ── Desktop integration files ──
-cp -f /src/data/com.fluorine.manager.desktop "${OUT_DIR}/"
-cp -f /src/data/com.fluorine.manager.png "${OUT_DIR}/"
-cp -f /src/data/com.fluorine.manager.metainfo.xml "${OUT_DIR}/"
+mkdir -p "${OUT_DIR}/icons"
+cp -f /src/data/icons/com.fluorine.manager.desktop "${OUT_DIR}/icons/"
+cp -f /src/data/icons/com.fluorine.manager.png "${OUT_DIR}/icons/"
+cp -f /src/data/icons/com.fluorine.manager.metainfo.xml "${OUT_DIR}/icons/"
# ── Determine build mode ──
# BUILD_MODE is passed from build.sh: tarball (default), installer, appimage, all
@@ -366,7 +380,7 @@ case "${CHOICE}" in
# Create desktop shortcut
mkdir -p "${DESKTOP_DIR}" "${ICON_DIR}"
- cp -f "${INSTALL_DIR}/com.fluorine.manager.png" "${ICON_DIR}/"
+ cp -f "${INSTALL_DIR}/icons/com.fluorine.manager.png" "${ICON_DIR}/"
cat > "${DESKTOP_DIR}/com.fluorine.manager.desktop" <<DESKTOP_EOF
[Desktop Entry]
@@ -458,9 +472,9 @@ build_appimage() {
cp -a "${APPDIR}/usr/bin/qt6plugins"/. "${APPDIR}/usr/plugins/"
fi
- cp -f "${OUT_DIR}/com.fluorine.manager.desktop" "${APPDIR}/usr/share/applications/"
- cp -f "${OUT_DIR}/com.fluorine.manager.png" "${APPDIR}/usr/share/icons/hicolor/256x256/apps/"
- cp -f "${OUT_DIR}/com.fluorine.manager.metainfo.xml" "${APPDIR}/usr/share/metainfo/"
+ cp -f "${OUT_DIR}/icons/com.fluorine.manager.desktop" "${APPDIR}/usr/share/applications/"
+ cp -f "${OUT_DIR}/icons/com.fluorine.manager.png" "${APPDIR}/usr/share/icons/hicolor/256x256/apps/"
+ cp -f "${OUT_DIR}/icons/com.fluorine.manager.metainfo.xml" "${APPDIR}/usr/share/metainfo/"
mkdir -p "${APPDIR}/usr/share/icons"
for theme_dir in /usr/share/icons/*; do
diff --git a/libs/basic_games_native/src/basicgameplugin.cpp b/libs/basic_games_native/src/basicgameplugin.cpp
index 8c73e4c..042f899 100644
--- a/libs/basic_games_native/src/basicgameplugin.cpp
+++ b/libs/basic_games_native/src/basicgameplugin.cpp
@@ -1,6 +1,7 @@
#include "basicgameplugin.h"
#include "steamutils.h"
+#include <uibase/utility.h>
#include <uibase/versioninfo.h>
#include <QDateTime>
@@ -166,7 +167,8 @@ bool BasicGamePlugin::isInstalled() const
QIcon BasicGamePlugin::gameIcon() const
{
- return QIcon();
+ return MOBase::iconForExecutable(
+ gameDirectory().absoluteFilePath(binaryName()));
}
QDir BasicGamePlugin::gameDirectory() const
diff --git a/libs/nak/Cargo.lock b/libs/nak/Cargo.lock
index 5bb63b4..03b8ffd 100644
--- a/libs/nak/Cargo.lock
+++ b/libs/nak/Cargo.lock
@@ -80,6 +80,21 @@ dependencies = [
]
[[package]]
+name = "dataview"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0"
+dependencies = [
+ "derive_pod",
+]
+
+[[package]]
+name = "derive_pod"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8"
+
+[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -307,6 +322,7 @@ name = "nak_rust"
version = "0.1.0"
dependencies = [
"chrono",
+ "pelite",
"serde",
"serde_json",
"ureq",
@@ -314,6 +330,12 @@ dependencies = [
]
[[package]]
+name = "no-std-compat"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
+
+[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -329,6 +351,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
+name = "pelite"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685"
+dependencies = [
+ "dataview",
+ "libc",
+ "no-std-compat",
+ "pelite-macros",
+ "winapi",
+]
+
+[[package]]
+name = "pelite-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b"
+
+[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -656,6 +697,22 @@ dependencies = [
]
[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -665,6 +722,12 @@ dependencies = [
]
[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/libs/nak/Cargo.toml b/libs/nak/Cargo.toml
index 3b3c460..c51a804 100644
--- a/libs/nak/Cargo.toml
+++ b/libs/nak/Cargo.toml
@@ -13,3 +13,4 @@ serde_json = "1"
walkdir = "2"
chrono = "0.4"
ureq = "2"
+pelite = "0.10"
diff --git a/libs/nak/src/icons.rs b/libs/nak/src/icons.rs
new file mode 100644
index 0000000..940d0d1
--- /dev/null
+++ b/libs/nak/src/icons.rs
@@ -0,0 +1,195 @@
+//! Extract icons from Windows PE (.exe/.dll) files using pelite.
+//!
+//! Returns raw ICO file bytes that can be loaded directly by Qt's QIcon/QImage.
+
+use std::path::Path;
+
+use pelite::resources::{Entry, Name, Resources};
+
+/// Extract the best (largest) icon from a PE executable as raw ICO bytes.
+///
+/// Returns `None` if the file can't be read, isn't a valid PE, or has no icons.
+pub fn extract_icon(exe_path: &Path) -> Option<Vec<u8>> {
+ let data = std::fs::read(exe_path).ok()?;
+
+ // Try PE32+ (64-bit) first, fall back to PE32 (32-bit)
+ if let Ok(icon) = extract_from_pe64(&data) {
+ return Some(icon);
+ }
+ if let Ok(icon) = extract_from_pe32(&data) {
+ return Some(icon);
+ }
+
+ None
+}
+
+fn extract_from_pe64(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
+ use pelite::pe64::{Pe, PeFile};
+ let pe = PeFile::from_bytes(data)?;
+ let resources = pe.resources()?;
+ build_ico_from_resources(&resources)
+}
+
+fn extract_from_pe32(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
+ use pelite::pe32::{Pe, PeFile};
+ let pe = PeFile::from_bytes(data)?;
+ let resources = pe.resources()?;
+ build_ico_from_resources(&resources)
+}
+
+/// Build an ICO file from PE resources, picking the group icon with the
+/// largest total pixel area.
+fn build_ico_from_resources(
+ resources: &Resources,
+) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
+ let root = resources.root()?;
+
+ // Find RT_GROUP_ICON directory (type 14)
+ let group_icon_dir = root
+ .get_dir(Name::Id(14))
+ .map_err(|_| "no RT_GROUP_ICON")?;
+
+ let mut best_group: Option<(i32, Vec<u8>)> = None;
+
+ for dir_entry in group_icon_dir.entries() {
+ // Each entry is a group; get its subdirectory (language variants)
+ let sub_dir = match dir_entry.entry() {
+ Ok(Entry::Directory(d)) => d,
+ _ => continue,
+ };
+
+ // Get the first language variant's data
+ let data_entry = match sub_dir.entries().next() {
+ Some(e) => match e.entry() {
+ Ok(Entry::DataEntry(d)) => d,
+ _ => continue,
+ },
+ None => continue,
+ };
+
+ let grp_data = match data_entry.bytes() {
+ Ok(d) => d,
+ Err(_) => continue,
+ };
+
+ // Parse GRPICONDIR header: reserved(2) + type(2) + count(2) = 6 bytes
+ if grp_data.len() < 6 {
+ continue;
+ }
+ let count = u16::from_le_bytes([grp_data[4], grp_data[5]]) as usize;
+ // Each GRPICONDIRENTRY is 14 bytes
+ if grp_data.len() < 6 + count * 14 {
+ continue;
+ }
+
+ // Calculate total pixel area for ranking
+ let mut total_area: i32 = 0;
+ for i in 0..count {
+ let offset = 6 + i * 14;
+ let w = if grp_data[offset] == 0 {
+ 256i32
+ } else {
+ grp_data[offset] as i32
+ };
+ let h = if grp_data[offset + 1] == 0 {
+ 256i32
+ } else {
+ grp_data[offset + 1] as i32
+ };
+ total_area += w * h;
+ }
+
+ if let Ok(ico_bytes) = build_ico_file(resources, grp_data, count) {
+ if best_group.is_none() || total_area > best_group.as_ref().unwrap().0 {
+ best_group = Some((total_area, ico_bytes));
+ }
+ }
+ }
+
+ best_group
+ .map(|(_, bytes)| bytes)
+ .ok_or_else(|| "no valid icon group found".into())
+}
+
+/// Build a complete ICO file from a GRPICONDIR and the corresponding RT_ICON resources.
+fn build_ico_file(
+ resources: &Resources,
+ grp_data: &[u8],
+ count: usize,
+) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
+ let root = resources.root()?;
+
+ // RT_ICON = type 3
+ let icon_dir = root.get_dir(Name::Id(3)).map_err(|_| "no RT_ICON")?;
+
+ // Collect individual icon image data
+ struct IconEntry<'a> {
+ header: [u8; 8], // first 8 bytes of GRPICONDIRENTRY (w, h, colors, reserved, planes, bpp)
+ data: &'a [u8],
+ }
+ let mut entries: Vec<IconEntry> = Vec::new();
+
+ for i in 0..count {
+ let grp_offset = 6 + i * 14;
+ let icon_id =
+ u16::from_le_bytes([grp_data[grp_offset + 12], grp_data[grp_offset + 13]]) as u32;
+
+ // Find the RT_ICON with this ID
+ let icon_sub = match icon_dir.get_dir(Name::Id(icon_id)) {
+ Ok(d) => d,
+ Err(_) => continue,
+ };
+
+ let data_entry = match icon_sub.entries().next() {
+ Some(e) => match e.entry() {
+ Ok(Entry::DataEntry(d)) => d,
+ _ => continue,
+ },
+ None => continue,
+ };
+
+ let image_data = match data_entry.bytes() {
+ Ok(d) => d,
+ Err(_) => continue,
+ };
+
+ let mut header = [0u8; 8];
+ header.copy_from_slice(&grp_data[grp_offset..grp_offset + 8]);
+
+ entries.push(IconEntry {
+ header,
+ data: image_data,
+ });
+ }
+
+ if entries.is_empty() {
+ return Err("no icon entries found".into());
+ }
+
+ let entry_count = entries.len() as u16;
+ let header_size = 6 + (entry_count as usize) * 16; // ICONDIR + ICONDIRENTRYs
+
+ let mut ico = Vec::new();
+
+ // ICONDIR header
+ ico.extend_from_slice(&0u16.to_le_bytes()); // reserved
+ ico.extend_from_slice(&1u16.to_le_bytes()); // type = ICO
+ ico.extend_from_slice(&entry_count.to_le_bytes());
+
+ // Write ICONDIRENTRY records (16 bytes each)
+ let mut data_offset = header_size as u32;
+ for entry in &entries {
+ ico.extend_from_slice(&entry.header); // width, height, colors, reserved, planes, bpp (8 bytes)
+ let size = entry.data.len() as u32;
+ ico.extend_from_slice(&size.to_le_bytes()); // dwBytesInRes (4 bytes)
+ ico.extend_from_slice(&data_offset.to_le_bytes()); // dwImageOffset (4 bytes)
+ data_offset += size;
+ }
+
+ // Write image data
+ for entry in &entries {
+ ico.extend_from_slice(entry.data);
+ }
+
+ Ok(ico)
+}
diff --git a/libs/nak/src/lib.rs b/libs/nak/src/lib.rs
index 59f41c6..13bc90e 100644
--- a/libs/nak/src/lib.rs
+++ b/libs/nak/src/lib.rs
@@ -13,4 +13,5 @@ pub mod steam;
pub mod utils;
pub mod deps;
+pub mod icons;
pub mod installers;
diff --git a/libs/nak_ffi/CMakeLists.txt b/libs/nak_ffi/CMakeLists.txt
index 6fde501..bdfac9a 100644
--- a/libs/nak_ffi/CMakeLists.txt
+++ b/libs/nak_ffi/CMakeLists.txt
@@ -18,6 +18,8 @@ add_custom_command(
WORKING_DIRECTORY ${NAK_FFI_DIR}
COMMENT "Building NaK FFI library (Rust)"
DEPENDS ${NAK_FFI_DIR}/Cargo.toml ${NAK_FFI_DIR}/src/lib.rs
+ ${NAK_FFI_DIR}/../nak/Cargo.toml ${NAK_FFI_DIR}/../nak/src/lib.rs
+ ${NAK_FFI_DIR}/../nak/src/icons.rs
)
add_custom_target(nak_ffi_build DEPENDS ${NAK_FFI_LIB})
diff --git a/libs/nak_ffi/Cargo.lock b/libs/nak_ffi/Cargo.lock
index 55ab8db..e8be402 100644
--- a/libs/nak_ffi/Cargo.lock
+++ b/libs/nak_ffi/Cargo.lock
@@ -80,6 +80,21 @@ dependencies = [
]
[[package]]
+name = "dataview"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0"
+dependencies = [
+ "derive_pod",
+]
+
+[[package]]
+name = "derive_pod"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8"
+
+[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -314,6 +329,7 @@ name = "nak_rust"
version = "0.1.0"
dependencies = [
"chrono",
+ "pelite",
"serde",
"serde_json",
"ureq",
@@ -321,6 +337,12 @@ dependencies = [
]
[[package]]
+name = "no-std-compat"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
+
+[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -336,6 +358,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
+name = "pelite"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685"
+dependencies = [
+ "dataview",
+ "libc",
+ "no-std-compat",
+ "pelite-macros",
+ "winapi",
+]
+
+[[package]]
+name = "pelite-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b"
+
+[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -663,6 +704,22 @@ dependencies = [
]
[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -672,6 +729,12 @@ dependencies = [
]
[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/libs/nak_ffi/include/nak_ffi.h b/libs/nak_ffi/include/nak_ffi.h
index cc66d9e..e1a9185 100644
--- a/libs/nak_ffi/include/nak_ffi.h
+++ b/libs/nak_ffi/include/nak_ffi.h
@@ -176,6 +176,23 @@ char *nak_ensure_dxvk_conf(void);
char *nak_get_dxvk_conf_path(void);
/* ========================================================================
+ * Tier 8: PE Icon Extraction
+ * ======================================================================== */
+
+/** Result of icon extraction */
+typedef struct {
+ uint8_t *data; /**< Raw ICO file bytes (NULL if extraction failed) */
+ size_t len; /**< Length in bytes (0 if extraction failed) */
+} NakIconData;
+
+/** Extract the best icon from a Windows PE executable (.exe/.dll).
+ * Returns raw ICO bytes. Free with nak_icon_data_free(). */
+NakIconData nak_extract_exe_icon(const char *exe_path);
+
+/** Free icon data returned by nak_extract_exe_icon */
+void nak_icon_data_free(NakIconData icon);
+
+/* ========================================================================
* General
* ======================================================================== */
diff --git a/libs/nak_ffi/src/lib.rs b/libs/nak_ffi/src/lib.rs
index 65592fa..c45481b 100644
--- a/libs/nak_ffi/src/lib.rs
+++ b/libs/nak_ffi/src/lib.rs
@@ -615,6 +615,57 @@ pub extern "C" fn nak_get_dxvk_conf_path() -> *mut c_char {
}
// ============================================================================
+// Tier 8: PE Icon Extraction
+// ============================================================================
+
+/// Result of icon extraction
+#[repr(C)]
+pub struct NakIconData {
+ /// Raw ICO file bytes (caller must free with nak_icon_data_free)
+ pub data: *mut u8,
+ /// Length of the data in bytes (0 if extraction failed)
+ pub len: usize,
+}
+
+/// Extract the best icon from a Windows PE executable (.exe/.dll).
+///
+/// Returns NakIconData with the raw ICO file bytes.
+/// If extraction fails, data is null and len is 0.
+/// Caller must free the result with nak_icon_data_free().
+#[no_mangle]
+pub unsafe extern "C" fn nak_extract_exe_icon(exe_path: *const c_char) -> NakIconData {
+ let path_str = unsafe { from_cstr(exe_path) };
+ if path_str.is_empty() {
+ return NakIconData {
+ data: ptr::null_mut(),
+ len: 0,
+ };
+ }
+
+ match nak_rust::icons::extract_icon(std::path::Path::new(path_str)) {
+ Some(bytes) => {
+ let len = bytes.len();
+ let mut boxed = bytes.into_boxed_slice();
+ let ptr = boxed.as_mut_ptr();
+ std::mem::forget(boxed);
+ NakIconData { data: ptr, len }
+ }
+ None => NakIconData {
+ data: ptr::null_mut(),
+ len: 0,
+ },
+ }
+}
+
+/// Free icon data returned by nak_extract_exe_icon
+#[no_mangle]
+pub unsafe extern "C" fn nak_icon_data_free(icon: NakIconData) {
+ if !icon.data.is_null() && icon.len > 0 {
+ let _ = unsafe { Vec::from_raw_parts(icon.data, icon.len, icon.len) };
+ }
+}
+
+// ============================================================================
// General: String free
// ============================================================================
diff --git a/libs/uibase/src/CMakeLists.txt b/libs/uibase/src/CMakeLists.txt
index fff3ac3..3015be3 100644
--- a/libs/uibase/src/CMakeLists.txt
+++ b/libs/uibase/src/CMakeLists.txt
@@ -187,6 +187,10 @@ target_link_libraries(uibase
PUBLIC Qt6::Widgets Qt6::Network Qt6::QuickWidgets
PRIVATE spdlog::spdlog_header_only Qt6::Qml Qt6::Quick)
+if(NOT WIN32 AND TARGET mo2::nak_ffi)
+ target_link_libraries(uibase PRIVATE mo2::nak_ffi)
+endif()
+
# installation
install(TARGETS uibase EXPORT uibaseTargets
LIBRARY DESTINATION lib
diff --git a/libs/uibase/src/utility.cpp b/libs/uibase/src/utility.cpp
index 33f227d..e6811af 100644
--- a/libs/uibase/src/utility.cpp
+++ b/libs/uibase/src/utility.cpp
@@ -21,6 +21,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include <uibase/utility.h>
#include <uibase/log.h>
#include <uibase/report.h>
+#ifndef _WIN32
+#include <nak_ffi.h>
+#endif
#include <QApplication>
#include <QBuffer>
#include <QCollator>
@@ -943,80 +946,39 @@ QIcon iconForExecutable(const QString& filePath)
cache.insert(cacheKey, icon);
return icon;
#else
- // Try to extract icon resources from PE executables via icoutils/wrestool.
- // If this fails, return a generic executable icon.
+ // Extract icon from PE executable via NaK (pelite-based, no external tools).
QIcon icon;
- const QString wrestool = QStandardPaths::findExecutable("wrestool");
- if (!wrestool.isEmpty()) {
- const QString cacheRoot =
- QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
- "/exe_icons";
- QDir().mkpath(cacheRoot);
-
- const QString outDirPath = cacheRoot + "/" + QString::number(qHash(cacheKey), 16);
- QDir outDir(outDirPath);
- if (!outDir.exists()) {
- QDir().mkpath(outDirPath);
- }
-
- auto findBestIcon = [&](const QString& dirPath) -> QIcon {
- QString bestFile;
- int bestArea = -1;
-
- QDirIterator it(dirPath,
- QStringList() << "*.png"
- << "*.ico"
- << "*.bmp",
- QDir::Files, QDirIterator::Subdirectories);
- while (it.hasNext()) {
- const QString path = it.next();
- QImageReader reader(path);
- const QSize size = reader.size();
- const int area = size.width() * size.height();
- if (area > bestArea) {
- bestArea = area;
- bestFile = path;
- } else if (bestArea < 0) {
- // Unknown image size (e.g. multi-size ico); keep first viable file.
- bestFile = path;
- }
- }
-
- if (!bestFile.isEmpty()) {
- QIcon candidate(bestFile);
- if (!candidate.pixmap(24, 24).isNull() || !candidate.pixmap(32, 32).isNull()) {
- return candidate;
- }
- }
-
- return QIcon();
- };
-
- // Reuse previous extraction if present.
- icon = findBestIcon(outDirPath);
-
- if (icon.isNull()) {
- const QList<QStringList> tries = {
- {"-x", "--type=group_icon", "-o", outDirPath, fi.absoluteFilePath()},
- {"-x", "--type=14", "-o", outDirPath, fi.absoluteFilePath()},
- {"-x", "-t14", "-o", outDirPath, fi.absoluteFilePath()},
- };
+ {
+ const QByteArray pathUtf8 = fi.absoluteFilePath().toUtf8();
+ NakIconData icoData = nak_extract_exe_icon(pathUtf8.constData());
+ if (icoData.data && icoData.len > 0) {
+ QByteArray ba(reinterpret_cast<const char*>(icoData.data),
+ static_cast<qsizetype>(icoData.len));
+ QBuffer buf(&ba);
+ buf.open(QIODevice::ReadOnly);
+ QImageReader reader(&buf, "ico");
- for (const auto& args : tries) {
- QProcess p;
- p.start(wrestool, args);
- if (!p.waitForFinished(4000)) {
- p.kill();
- continue;
- }
- if (p.exitStatus() == QProcess::NormalExit && p.exitCode() == 0) {
- icon = findBestIcon(outDirPath);
- if (!icon.isNull()) {
- break;
+ // ICO files contain multiple sizes/depths — read all and add to QIcon
+ // so Qt picks the best one for each requested size.
+ if (reader.canRead()) {
+ const int imageCount = reader.imageCount();
+ if (imageCount > 1) {
+ for (int i = 0; i < imageCount; ++i) {
+ reader.jumpToImage(i);
+ QImage img = reader.read();
+ if (!img.isNull()) {
+ icon.addPixmap(QPixmap::fromImage(img));
+ }
+ }
+ } else {
+ QImage img = reader.read();
+ if (!img.isNull()) {
+ icon = QIcon(QPixmap::fromImage(img));
}
}
}
+ nak_icon_data_free(icoData);
}
}
diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp
index c6d119b..457a3dd 100644
--- a/src/src/mainwindow.cpp
+++ b/src/src/mainwindow.cpp
@@ -263,7 +263,8 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore,
QApplication::setEffectEnabled(Qt::UI_AnimateCombo, false);
QApplication::setEffectEnabled(Qt::UI_AnimateTooltip, false);
QApplication::setEffectEnabled(Qt::UI_FadeTooltip, false);
- QApplication::setDesktopFileName(QString("com.fluorine.manager"));
+ // setDesktopFileName moved to MOApplication constructor (must be set before
+ // first window surface for Wayland app_id to take effect)
#ifdef MO2_WEBENGINE
QWebEngineProfile::defaultProfile()->setPersistentCookiesPolicy(
diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp
index 3009a45..45b79a8 100644
--- a/src/src/moapplication.cpp
+++ b/src/src/moapplication.cpp
@@ -229,6 +229,8 @@ MOApplication::MOApplication(int& argc, char** argv) : QApplication(argc, argv)
// Ensure the app name is always "ModOrganizer" regardless of the binary
// filename (e.g. "ModOrganizer.bin" inside an AppImage).
setApplicationName("ModOrganizer");
+ setDesktopFileName(QStringLiteral("com.fluorine.manager"));
+ setWindowIcon(QIcon(":/MO/gui/app_icon"));
qputenv("QML_DISABLE_DISK_CACHE", "true");