mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
0.4.9
Linux相关版本适配
This commit is contained in:
192
LanMountainDesktop/Services/LinuxDesktopEntryInstaller.cs
Normal file
192
LanMountainDesktop/Services/LinuxDesktopEntryInstaller.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
internal static class LinuxDesktopEntryInstaller
|
||||
{
|
||||
private const string DesktopFileName = "LanMountainDesktop.desktop";
|
||||
private const string IconFileName = "lanmountaindesktop.png";
|
||||
private const string IconName = "lanmountaindesktop";
|
||||
|
||||
public static void EnsureInstalled()
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var executablePath = ResolveExecutablePath();
|
||||
if (string.IsNullOrWhiteSpace(executablePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dataHome = ResolveDataHome();
|
||||
if (string.IsNullOrWhiteSpace(dataHome))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var applicationsDir = Path.Combine(dataHome, "applications");
|
||||
var iconDir = Path.Combine(dataHome, "icons", "hicolor", "256x256", "apps");
|
||||
|
||||
Directory.CreateDirectory(applicationsDir);
|
||||
Directory.CreateDirectory(iconDir);
|
||||
|
||||
var desktopTargetPath = Path.Combine(applicationsDir, DesktopFileName);
|
||||
var iconTargetPath = Path.Combine(iconDir, IconFileName);
|
||||
|
||||
TryCopyBundledIcon(iconTargetPath);
|
||||
|
||||
var desktopEntryContent = BuildDesktopEntryContent(executablePath);
|
||||
WriteFileIfChanged(desktopTargetPath, desktopEntryContent);
|
||||
|
||||
TryRunCommand("chmod", "+x", executablePath);
|
||||
TryRunCommand("chmod", "+x", desktopTargetPath);
|
||||
TryRunCommand("update-desktop-database", applicationsDir);
|
||||
TryRunCommand("gtk-update-icon-cache", Path.Combine(dataHome, "icons", "hicolor"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep startup resilient if desktop integration fails.
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveExecutablePath()
|
||||
{
|
||||
var processPath = Environment.ProcessPath;
|
||||
if (!string.IsNullOrWhiteSpace(processPath))
|
||||
{
|
||||
return processPath;
|
||||
}
|
||||
|
||||
var commandLineArgs = Environment.GetCommandLineArgs();
|
||||
if (commandLineArgs.Length > 0 && !string.IsNullOrWhiteSpace(commandLineArgs[0]))
|
||||
{
|
||||
return commandLineArgs[0];
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string ResolveDataHome()
|
||||
{
|
||||
var dataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
|
||||
if (!string.IsNullOrWhiteSpace(dataHome))
|
||||
{
|
||||
return dataHome.Trim();
|
||||
}
|
||||
|
||||
var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
if (string.IsNullOrWhiteSpace(homePath))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return Path.Combine(homePath, ".local", "share");
|
||||
}
|
||||
|
||||
private static void TryCopyBundledIcon(string iconTargetPath)
|
||||
{
|
||||
foreach (var candidatePath in EnumerateIconSourceCandidates())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(candidatePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
File.Copy(candidatePath, iconTargetPath, overwrite: true);
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore failures and continue trying fallbacks.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] EnumerateIconSourceCandidates()
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
return
|
||||
[
|
||||
Path.Combine(baseDirectory, "share", "icons", "hicolor", "256x256", "apps", IconFileName),
|
||||
Path.Combine(baseDirectory, IconFileName)
|
||||
];
|
||||
}
|
||||
|
||||
private static string BuildDesktopEntryContent(string executablePath)
|
||||
{
|
||||
var escapedExecutablePath = executablePath.Replace("\"", "\\\"", StringComparison.Ordinal);
|
||||
return
|
||||
"[Desktop Entry]\n" +
|
||||
"Type=Application\n" +
|
||||
"Version=1.0\n" +
|
||||
"Name=LanMountainDesktop\n" +
|
||||
"Comment=LanMountainDesktop desktop shell\n" +
|
||||
$"Exec=\"{escapedExecutablePath}\" %U\n" +
|
||||
$"Icon={IconName}\n" +
|
||||
"Terminal=false\n" +
|
||||
"Categories=Utility;Education;\n" +
|
||||
"StartupWMClass=LanMountainDesktop\n";
|
||||
}
|
||||
|
||||
private static void WriteFileIfChanged(string filePath, string content)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var existing = File.ReadAllText(filePath);
|
||||
if (string.Equals(existing, content, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to attempt writing the content.
|
||||
}
|
||||
|
||||
File.WriteAllText(filePath, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
}
|
||||
|
||||
private static void TryRunCommand(string fileName, params string[] arguments)
|
||||
{
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true
|
||||
};
|
||||
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = process.WaitForExit(2_500);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore missing command or update failures.
|
||||
}
|
||||
}
|
||||
}
|
||||
371
LanMountainDesktop/Services/LinuxDesktopEntryService.cs
Normal file
371
LanMountainDesktop/Services/LinuxDesktopEntryService.cs
Normal file
@@ -0,0 +1,371 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using LanMountainDesktop.Models;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public sealed class LinuxDesktopEntryService
|
||||
{
|
||||
private static readonly Regex FieldCodeRegex =
|
||||
new(@"%[fFuUdDnNickvm]", RegexOptions.Compiled);
|
||||
|
||||
public StartMenuFolderNode Load()
|
||||
{
|
||||
var root = new StartMenuFolderNode("All Apps", string.Empty);
|
||||
if (!OperatingSystem.IsLinux())
|
||||
{
|
||||
return root;
|
||||
}
|
||||
|
||||
var seenDesktopIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var applicationsRoot in EnumerateApplicationsRoots())
|
||||
{
|
||||
foreach (var desktopFilePath in EnumerateDesktopFilesSafe(applicationsRoot))
|
||||
{
|
||||
if (!TryParseDesktopEntry(desktopFilePath, applicationsRoot, out var appEntry))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seenDesktopIds.Add(appEntry.RelativePath))
|
||||
{
|
||||
root.Apps.Add(appEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.Apps.Sort((left, right) =>
|
||||
string.Compare(left.DisplayName, right.DisplayName, CultureInfo.CurrentCulture, CompareOptions.IgnoreCase));
|
||||
return root;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateApplicationsRoots()
|
||||
{
|
||||
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var dataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
|
||||
if (string.IsNullOrWhiteSpace(dataHome) && !string.IsNullOrWhiteSpace(homeDirectory))
|
||||
{
|
||||
dataHome = Path.Combine(homeDirectory, ".local", "share");
|
||||
}
|
||||
|
||||
var dataDirs = (Environment.GetEnvironmentVariable("XDG_DATA_DIRS") ?? "/usr/local/share:/usr/share")
|
||||
.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
var candidates = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(dataHome))
|
||||
{
|
||||
candidates.Add(Path.Combine(dataHome, "applications"));
|
||||
}
|
||||
|
||||
foreach (var dataDir in dataDirs)
|
||||
{
|
||||
candidates.Add(Path.Combine(dataDir, "applications"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(homeDirectory))
|
||||
{
|
||||
candidates.Add(Path.Combine(homeDirectory, ".local", "share", "flatpak", "exports", "share", "applications"));
|
||||
}
|
||||
|
||||
candidates.Add("/var/lib/flatpak/exports/share/applications");
|
||||
candidates.Add("/var/lib/snapd/desktop/applications");
|
||||
|
||||
return candidates
|
||||
.Where(path => !string.IsNullOrWhiteSpace(path) && Directory.Exists(path))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateDesktopFilesSafe(string applicationsRoot)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Directory.EnumerateFiles(applicationsRoot, "*.desktop", SearchOption.AllDirectories);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseDesktopEntry(string desktopFilePath, string applicationsRoot, out StartMenuAppEntry appEntry)
|
||||
{
|
||||
appEntry = null!;
|
||||
|
||||
Dictionary<string, string> fields;
|
||||
try
|
||||
{
|
||||
fields = ReadDesktopEntryFields(desktopFilePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fields.TryGetValue("Type", out var entryType) ||
|
||||
!string.Equals(entryType, "Application", StringComparison.OrdinalIgnoreCase) ||
|
||||
GetBooleanField(fields, "NoDisplay") ||
|
||||
GetBooleanField(fields, "Hidden"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var displayName = GetPreferredName(fields);
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fields.TryGetValue("Exec", out var execValue) ||
|
||||
!TryParseExec(execValue, out var launchExecutable, out var launchArguments))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fields.TryGetValue("TryExec", out var tryExecValue) &&
|
||||
!string.IsNullOrWhiteSpace(tryExecValue) &&
|
||||
!CommandExists(tryExecValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var desktopFileId = BuildDesktopFileId(desktopFilePath, applicationsRoot);
|
||||
var iconValue = fields.TryGetValue("Icon", out var iconFieldValue)
|
||||
? iconFieldValue
|
||||
: string.Empty;
|
||||
var workingDirectory = Path.IsPathRooted(launchExecutable)
|
||||
? Path.GetDirectoryName(launchExecutable)
|
||||
: null;
|
||||
|
||||
appEntry = new StartMenuAppEntry
|
||||
{
|
||||
DisplayName = displayName.Trim(),
|
||||
FilePath = desktopFilePath,
|
||||
RelativePath = desktopFileId,
|
||||
IconPngBytes = LinuxIconService.TryGetIconPngBytes(iconValue, Path.GetDirectoryName(desktopFilePath)),
|
||||
LaunchExecutable = launchExecutable,
|
||||
LaunchArguments = launchArguments,
|
||||
WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ReadDesktopEntryFields(string desktopFilePath)
|
||||
{
|
||||
var fields = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var inDesktopEntrySection = false;
|
||||
foreach (var rawLine in File.ReadLines(desktopFilePath))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith('[') && line.EndsWith(']'))
|
||||
{
|
||||
inDesktopEntrySection = string.Equals(line, "[Desktop Entry]", StringComparison.OrdinalIgnoreCase);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inDesktopEntrySection)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var separatorIndex = line.IndexOf('=');
|
||||
if (separatorIndex <= 0 || separatorIndex >= line.Length - 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = line[..separatorIndex].Trim();
|
||||
var value = line[(separatorIndex + 1)..].Trim();
|
||||
fields[key] = value;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static bool GetBooleanField(IReadOnlyDictionary<string, string> fields, string key)
|
||||
{
|
||||
return fields.TryGetValue(key, out var value) &&
|
||||
bool.TryParse(value, out var result) &&
|
||||
result;
|
||||
}
|
||||
|
||||
private static string GetPreferredName(IReadOnlyDictionary<string, string> fields)
|
||||
{
|
||||
if (TryGetLocalizedField(fields, "Name", out var localizedName))
|
||||
{
|
||||
return localizedName;
|
||||
}
|
||||
|
||||
return fields.TryGetValue("Name", out var fallbackName)
|
||||
? fallbackName
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private static bool TryGetLocalizedField(IReadOnlyDictionary<string, string> fields, string baseKey, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
var uiCulture = CultureInfo.CurrentUICulture;
|
||||
var candidates = new[]
|
||||
{
|
||||
$"{baseKey}[{uiCulture.Name}]",
|
||||
$"{baseKey}[{uiCulture.TwoLetterISOLanguageName}]"
|
||||
};
|
||||
|
||||
foreach (var key in candidates)
|
||||
{
|
||||
if (fields.TryGetValue(key, out var localizedValue) &&
|
||||
!string.IsNullOrWhiteSpace(localizedValue))
|
||||
{
|
||||
value = localizedValue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildDesktopFileId(string desktopFilePath, string applicationsRoot)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(applicationsRoot, desktopFilePath)
|
||||
.Replace(Path.DirectorySeparatorChar, '-')
|
||||
.Replace(Path.AltDirectorySeparatorChar, '-');
|
||||
|
||||
return relativePath.Trim();
|
||||
}
|
||||
|
||||
private static bool TryParseExec(string execValue, out string launchExecutable, out List<string> launchArguments)
|
||||
{
|
||||
launchExecutable = string.Empty;
|
||||
launchArguments = [];
|
||||
|
||||
var tokens = TokenizeExec(execValue);
|
||||
if (tokens.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var cleanedTokens = new List<string>(tokens.Count);
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalizedToken = token.Replace("%%", "%", StringComparison.Ordinal);
|
||||
if (normalizedToken.Length == 2 && normalizedToken[0] == '%')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
normalizedToken = FieldCodeRegex.Replace(normalizedToken, string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalizedToken))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cleanedTokens.Add(normalizedToken);
|
||||
}
|
||||
|
||||
if (cleanedTokens.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
launchExecutable = cleanedTokens[0];
|
||||
launchArguments = cleanedTokens.Skip(1).ToList();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<string> TokenizeExec(string execValue)
|
||||
{
|
||||
var tokens = new List<string>();
|
||||
var current = new StringBuilder();
|
||||
var inQuotes = false;
|
||||
char quoteChar = '\0';
|
||||
|
||||
foreach (var c in execValue)
|
||||
{
|
||||
if ((c == '"' || c == '\'') &&
|
||||
(!inQuotes || quoteChar == c))
|
||||
{
|
||||
if (inQuotes)
|
||||
{
|
||||
inQuotes = false;
|
||||
quoteChar = '\0';
|
||||
}
|
||||
else
|
||||
{
|
||||
inQuotes = true;
|
||||
quoteChar = c;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char.IsWhiteSpace(c) && !inQuotes)
|
||||
{
|
||||
if (current.Length > 0)
|
||||
{
|
||||
tokens.Add(current.ToString());
|
||||
current.Clear();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
current.Append(c);
|
||||
}
|
||||
|
||||
if (current.Length > 0)
|
||||
{
|
||||
tokens.Add(current.ToString());
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private static bool CommandExists(string command)
|
||||
{
|
||||
var trimmedCommand = command.Trim();
|
||||
if (string.IsNullOrWhiteSpace(trimmedCommand))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Path.IsPathRooted(trimmedCommand))
|
||||
{
|
||||
return File.Exists(trimmedCommand);
|
||||
}
|
||||
|
||||
var pathEntries = (Environment.GetEnvironmentVariable("PATH") ?? string.Empty)
|
||||
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
foreach (var pathEntry in pathEntries)
|
||||
{
|
||||
try
|
||||
{
|
||||
var candidate = Path.Combine(pathEntry, trimmedCommand);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore malformed PATH entries.
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
214
LanMountainDesktop/Services/LinuxIconService.cs
Normal file
214
LanMountainDesktop/Services/LinuxIconService.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
internal static class LinuxIconService
|
||||
{
|
||||
private static readonly string[] SupportedRasterExtensions =
|
||||
[
|
||||
".png",
|
||||
".ico"
|
||||
];
|
||||
|
||||
private static readonly Regex SizeDirectoryRegex =
|
||||
new(@"(?<size>\d{1,4})x\d{1,4}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly ConcurrentDictionary<string, string?> IconPathCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static byte[]? TryGetIconPngBytes(string? iconKey, string? desktopFileDirectory = null)
|
||||
{
|
||||
if (!OperatingSystem.IsLinux() || string.IsNullOrWhiteSpace(iconKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var candidatePath in ResolveIconCandidates(iconKey.Trim(), desktopFileDirectory))
|
||||
{
|
||||
if (TryReadIconBytes(candidatePath, out var bytes))
|
||||
{
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ResolveIconCandidates(string iconKey, string? desktopFileDirectory)
|
||||
{
|
||||
if (Path.HasExtension(iconKey))
|
||||
{
|
||||
var directPath = ExpandHome(iconKey);
|
||||
if (Path.IsPathRooted(directPath))
|
||||
{
|
||||
yield return directPath;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(desktopFileDirectory))
|
||||
{
|
||||
yield return Path.GetFullPath(Path.Combine(desktopFileDirectory, directPath));
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
var resolvedThemePath = ResolveThemedIconPath(iconKey);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedThemePath))
|
||||
{
|
||||
yield return resolvedThemePath;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveThemedIconPath(string iconName)
|
||||
{
|
||||
return IconPathCache.GetOrAdd(iconName, static key => FindBestMatchingIconPath(key));
|
||||
}
|
||||
|
||||
private static string? FindBestMatchingIconPath(string iconName)
|
||||
{
|
||||
var candidates = new List<(string Path, int Score)>();
|
||||
foreach (var iconRoot in EnumerateIconRoots())
|
||||
{
|
||||
foreach (var extension in SupportedRasterExtensions)
|
||||
{
|
||||
foreach (var candidatePath in EnumerateFilesSafe(iconRoot, iconName + extension))
|
||||
{
|
||||
candidates.Add((candidatePath, ScoreIconPath(candidatePath)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidates
|
||||
.OrderByDescending(candidate => candidate.Score)
|
||||
.ThenBy(candidate => candidate.Path.Length)
|
||||
.Select(candidate => candidate.Path)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateIconRoots()
|
||||
{
|
||||
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var dataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
|
||||
if (string.IsNullOrWhiteSpace(dataHome) && !string.IsNullOrWhiteSpace(homeDirectory))
|
||||
{
|
||||
dataHome = Path.Combine(homeDirectory, ".local", "share");
|
||||
}
|
||||
|
||||
var dataDirs = (Environment.GetEnvironmentVariable("XDG_DATA_DIRS") ?? "/usr/local/share:/usr/share")
|
||||
.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
var candidates = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(dataHome))
|
||||
{
|
||||
candidates.Add(Path.Combine(dataHome, "icons"));
|
||||
candidates.Add(Path.Combine(dataHome, "pixmaps"));
|
||||
}
|
||||
|
||||
foreach (var dataDir in dataDirs)
|
||||
{
|
||||
candidates.Add(Path.Combine(dataDir, "icons"));
|
||||
candidates.Add(Path.Combine(dataDir, "pixmaps"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(homeDirectory))
|
||||
{
|
||||
candidates.Add(Path.Combine(homeDirectory, ".icons"));
|
||||
candidates.Add(Path.Combine(homeDirectory, ".local", "share", "flatpak", "exports", "share", "icons"));
|
||||
}
|
||||
|
||||
candidates.Add("/var/lib/flatpak/exports/share/icons");
|
||||
candidates.Add("/var/lib/snapd/desktop/icons");
|
||||
|
||||
return candidates
|
||||
.Where(path => !string.IsNullOrWhiteSpace(path) && Directory.Exists(path))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateFilesSafe(string rootPath, string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Directory.EnumerateFiles(rootPath, fileName, SearchOption.AllDirectories);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadIconBytes(string filePath, out byte[] bytes)
|
||||
{
|
||||
bytes = [];
|
||||
try
|
||||
{
|
||||
var extension = Path.GetExtension(filePath);
|
||||
if (!SupportedRasterExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) ||
|
||||
!File.Exists(filePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes = File.ReadAllBytes(filePath);
|
||||
return bytes.Length > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static int ScoreIconPath(string filePath)
|
||||
{
|
||||
var score = 0;
|
||||
var extension = Path.GetExtension(filePath);
|
||||
if (extension.Equals(".png", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
score += 4_000;
|
||||
}
|
||||
else if (extension.Equals(".ico", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
score += 2_000;
|
||||
}
|
||||
|
||||
if (filePath.Contains($"{Path.DirectorySeparatorChar}hicolor{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
score += 8_000;
|
||||
}
|
||||
|
||||
if (filePath.Contains($"{Path.DirectorySeparatorChar}apps{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
score += 1_000;
|
||||
}
|
||||
|
||||
var match = SizeDirectoryRegex.Match(filePath);
|
||||
if (match.Success &&
|
||||
int.TryParse(match.Groups["size"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var size))
|
||||
{
|
||||
score += Math.Min(size, 512);
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private static string ExpandHome(string path)
|
||||
{
|
||||
if (!path.StartsWith("~", StringComparison.Ordinal))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
if (string.IsNullOrWhiteSpace(homeDirectory))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.Length == 1
|
||||
? homeDirectory
|
||||
: Path.Combine(homeDirectory, path[2..]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user