diff --git a/src/OpenMaui.AppImage/Program.cs b/src/OpenMaui.AppImage/Program.cs
index 25be88f..5740a2c 100644
--- a/src/OpenMaui.AppImage/Program.cs
+++ b/src/OpenMaui.AppImage/Program.cs
@@ -2,6 +2,7 @@ using System.CommandLine;
using System.Diagnostics;
using System.Reflection;
using System.Text;
+using System.Xml.Linq;
namespace OpenMaui.AppImage;
@@ -88,7 +89,7 @@ class Program
}
}
-public class AppImageOptions
+public record AppImageOptions
{
public required DirectoryInfo InputDirectory { get; init; }
public required FileInfo OutputFile { get; init; }
@@ -145,6 +146,17 @@ public class AppImageBuilder
}
}
+ // Auto-detect icon if not specified
+ if (options.IconPath == null || !options.IconPath.Exists)
+ {
+ var detectedIcon = FindIcon(options.InputDirectory.FullName, execName);
+ if (detectedIcon != null)
+ {
+ options = options with { IconPath = new FileInfo(detectedIcon) };
+ Console.WriteLine($" Auto-detected icon: {Path.GetFileName(detectedIcon)}");
+ }
+ }
+
// Create temporary AppDir structure
var tempDir = Path.Combine(Path.GetTempPath(), $"appimage-{Guid.NewGuid():N}");
var appDir = Path.Combine(tempDir, $"{options.AppName}.AppDir");
@@ -301,11 +313,21 @@ if [ -n ""$APPIMAGE"" ]; then
APPIMAGE_BASENAME=$(basename ""$APPIMAGE"")
if [ ! -f ""$INSTALLED_MARKER/$APPIMAGE_BASENAME"" ] || [ ""$SHOW_INSTALLER"" = ""1"" ]; then
if command -v zenity &> /dev/null; then
+ # Find app icon
+ SANITIZED=$(echo ""$APPIMAGE_NAME"" | tr ' ' '_')
+ ICON_PATH=""""
+ for ext in svg png ico; do
+ [ -f ""$HERE/${{SANITIZED}}.${{ext}}"" ] && ICON_PATH=""$HERE/${{SANITIZED}}.${{ext}}"" && break
+ done
+
+ ICON_OPT=""""
+ [ -n ""$ICON_PATH"" ] && ICON_OPT=""--window-icon=$ICON_PATH""
+
CHOICE=$(zenity --question --title=""$APPIMAGE_NAME"" \
--text=""$APPIMAGE_NAME\nVersion $APPIMAGE_VERSION\n\n$APPIMAGE_COMMENT\n\nWould you like to install this application?"" \
--ok-label=""Install"" --cancel-label=""Run Without Installing"" \
--extra-button=""Cancel"" \
- --width=350 --icon-name=application-x-executable 2>/dev/null; echo $?)
+ --width=350 $ICON_OPT 2>/dev/null; echo $?)
case ""$CHOICE"" in
0) # Install clicked
@@ -313,7 +335,7 @@ if [ -n ""$APPIMAGE"" ]; then
if [ $? -eq 0 ]; then
zenity --info --title=""Installation Complete"" \
--text=""$APPIMAGE_NAME has been installed.\n\nYou can find it in your application menu."" \
- --width=300 2>/dev/null
+ --width=300 $ICON_OPT 2>/dev/null
fi
;;
1) # Run Without Installing
@@ -436,6 +458,211 @@ X-AppImage-Version={options.Version}
return false;
}
+ private string? FindIcon(string inputDir, string execName)
+ {
+ // First, try to find and parse the .csproj to get MauiIcon
+ var csprojIcon = TryGetIconFromCsproj(inputDir, execName);
+ if (csprojIcon != null)
+ return csprojIcon;
+
+ // Fallback to searching for icon files directly
+ var extensions = new[] { ".svg", ".png", ".ico" };
+
+ var searchPatterns = new[]
+ {
+ "appicon_combined", // Combined icon for Linux desktop
+ "appicon", // MAUI standard: appicon.svg
+ "AppIcon", // AppIcon.svg
+ execName, // ShellDemo.svg
+ execName.ToLowerInvariant(), // shelldemo.svg
+ "icon", // icon.svg
+ "logo", // logo.svg
+ };
+
+ var searchDirs = new[]
+ {
+ Path.Combine(inputDir, "Resources", "AppIcon"),
+ Path.Combine(inputDir, "Resources", "Splash"),
+ inputDir,
+ Path.Combine(inputDir, "Resources"),
+ Path.Combine(inputDir, "Resources", "Images"),
+ };
+
+ foreach (var dir in searchDirs)
+ {
+ if (!Directory.Exists(dir)) continue;
+
+ foreach (var pattern in searchPatterns)
+ {
+ foreach (var ext in extensions)
+ {
+ var iconPath = Path.Combine(dir, pattern + ext);
+ if (File.Exists(iconPath))
+ return iconPath;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private string? TryGetIconFromCsproj(string inputDir, string execName)
+ {
+ // Find the project directory (go up from publish directory)
+ var projectDir = FindProjectDirectory(inputDir, execName);
+ if (projectDir == null)
+ return null;
+
+ // Find .csproj file
+ var csprojFiles = Directory.GetFiles(projectDir, "*.csproj");
+ if (csprojFiles.Length == 0)
+ return null;
+
+ var csprojPath = csprojFiles[0];
+ Console.WriteLine($" Found project: {Path.GetFileName(csprojPath)}");
+
+ try
+ {
+ var doc = XDocument.Load(csprojPath);
+ var ns = doc.Root?.Name.Namespace ?? XNamespace.None;
+
+ // Find MauiIcon element
+ var mauiIcon = doc.Descendants("MauiIcon").FirstOrDefault();
+ if (mauiIcon == null)
+ return null;
+
+ var includeAttr = mauiIcon.Attribute("Include")?.Value;
+ var foregroundAttr = mauiIcon.Attribute("ForegroundFile")?.Value;
+ var colorAttr = mauiIcon.Attribute("Color")?.Value ?? "#FFFFFF";
+ var bgColorAttr = mauiIcon.Attribute("BackgroundColor")?.Value;
+
+ if (string.IsNullOrEmpty(includeAttr))
+ return null;
+
+ // Resolve paths (convert backslashes to forward slashes for Linux)
+ var bgPath = Path.Combine(projectDir, includeAttr.Replace('\\', '/'));
+ var fgPath = !string.IsNullOrEmpty(foregroundAttr)
+ ? Path.Combine(projectDir, foregroundAttr.Replace('\\', '/'))
+ : null;
+
+ if (!File.Exists(bgPath))
+ return null;
+
+ // If we have both background and foreground, composite them
+ if (fgPath != null && File.Exists(fgPath))
+ {
+ var compositedIcon = CompositeIcon(bgPath, fgPath, bgColorAttr, colorAttr);
+ if (compositedIcon != null)
+ {
+ Console.WriteLine($" Composited icon from MauiIcon (bg + fg)");
+ return compositedIcon;
+ }
+ }
+
+ // Otherwise, just use the background with the BackgroundColor
+ if (!string.IsNullOrEmpty(bgColorAttr))
+ {
+ Console.WriteLine($" Using MauiIcon background: {Path.GetFileName(bgPath)}");
+ }
+
+ return bgPath;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($" Warning: Could not parse csproj: {ex.Message}");
+ return null;
+ }
+ }
+
+ private string? FindProjectDirectory(string inputDir, string execName)
+ {
+ // The publish directory is typically: ProjectDir/bin/Release/net9.0/linux-arm64/publish
+ // We need to go up to find the project directory
+
+ var dir = new DirectoryInfo(inputDir);
+
+ // Go up looking for a .csproj file
+ while (dir != null)
+ {
+ var csprojFiles = dir.GetFiles("*.csproj");
+ if (csprojFiles.Length > 0)
+ return dir.FullName;
+
+ // Also check if we find a matching project name
+ if (dir.GetFiles($"{execName}.csproj").Length > 0)
+ return dir.FullName;
+
+ dir = dir.Parent;
+
+ // Don't go too far up (max 10 levels)
+ if (dir?.FullName.Split(Path.DirectorySeparatorChar).Length < 3)
+ break;
+ }
+
+ return null;
+ }
+
+ private string? CompositeIcon(string bgPath, string fgPath, string? bgColor, string fgColor)
+ {
+ try
+ {
+ // Read the foreground SVG to extract the path
+ var fgContent = File.ReadAllText(fgPath);
+ var fgDoc = XDocument.Parse(fgContent);
+
+ // Find path elements in the foreground
+ var svgNs = XNamespace.Get("http://www.w3.org/2000/svg");
+ var pathElements = fgDoc.Descendants(svgNs + "path")
+ .Concat(fgDoc.Descendants("path"))
+ .ToList();
+
+ if (pathElements.Count == 0)
+ return null;
+
+ // Extract path data
+ var pathData = string.Join(" ", pathElements
+ .Select(p => p.Attribute("d")?.Value)
+ .Where(d => !string.IsNullOrEmpty(d)));
+
+ if (string.IsNullOrEmpty(pathData))
+ return null;
+
+ // Get the viewBox or size from foreground
+ var fgRoot = fgDoc.Root;
+ var fgViewBox = fgRoot?.Attribute("viewBox")?.Value ?? "0 0 24 24";
+
+ // Parse viewBox to get dimensions
+ var vbParts = fgViewBox.Split(' ');
+ var fgWidth = vbParts.Length >= 3 ? double.Parse(vbParts[2]) : 24;
+ var fgHeight = vbParts.Length >= 4 ? double.Parse(vbParts[3]) : 24;
+
+ // Create composited SVG (256x256 with rounded corners)
+ var finalBgColor = bgColor ?? "#512BD4"; // Default MAUI purple
+ var scale = 160.0 / Math.Max(fgWidth, fgHeight); // Scale to fit in 160px centered in 256px
+ var offsetX = (256 - fgWidth * scale) / 2;
+ var offsetY = (256 - fgHeight * scale) / 2;
+
+ var compositedSvg = $@"
+";
+
+ // Write to temp file
+ var tempIcon = Path.Combine(Path.GetTempPath(), $"appicon_{Guid.NewGuid():N}.svg");
+ File.WriteAllText(tempIcon, compositedSvg);
+
+ return tempIcon;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($" Warning: Could not composite icon: {ex.Message}");
+ return null;
+ }
+ }
+
private async Task FindAppImageTool()
{
var candidates = new[]