Repository: oliverschwendener/ueli (ts)

Issue:
[Bug] Failed to generate a cache for icon extractor
**Describe the bug**
I installed the Appimage on Majaro and after I started the App I see only the Ueli commands.
The following Error appears in the logs:
`[8.8.2024, 20:39:34][ERROR] Failed to generate a cache for icon extractor. Reason: TypeError: {(intermediate value)(intermediate value)}[e] is not a function`

**To Reproduce**
Steps to reproduce the behavior:
1. Install Manjaro
1. Install the App via Appimage
1. Start the App
2. check the logs

**Expected behavior**
Search result of Apps and later for files and other stuff

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Environment**
 - OS: Latest Manjaro Linux 6.6.32-1-MANJARO 1 SMP PREEMPT_DYNAMIC Mon May 27 03:41:25 UTC 2024 x86_64 GNU/Linu


**Additional context**
Add any other context about the problem here.


Pull request:
fix(ApplicationSearch): Add support for more desktop environments
Fixes #1154, adds support for KDE, MATE, XFCE, and Pantheon desktops

Also includes fix for small bug in `IniFileParser`

Patch:
diff --git a/docs/Extensions/ApplicationSearch/README.md b/docs/Extensions/ApplicationSearch/README.md
index 33a818c2..9669a3e8 100644
--- a/docs/Extensions/ApplicationSearch/README.md
+++ b/docs/Extensions/ApplicationSearch/README.md
@@ -18,14 +18,30 @@ This extension allows you to search for applications installed on your computer.
 
 ### Linux
 
--   Application Folders: the list of folders where Ueli will look for applications.
+-   Application Folders: the list of folders where Ueli will look for `.desktop` files.
 
 ## About this extension
 
-Author: [Oliver Schwendener](https://github.com/oliverschwendener)
+Authors:
+
+-   [Oliver Schwendener](https://github.com/oliverschwendener) (Windows, macOS)
+-   [Kelvin Wu](https://github.com/ke1v) (Linux)
 
 Supported operating systems:
 
 -   Windows
 -   macOS
--   Linux (with Cinnamon or Gnome Desktop environment)
+-   Linux
+
+## Other notes
+
+### Linux
+
+As the extraction of the correct application icon can be complicated on some desktop environments, Ueli currently only shows the correct app icons on the following desktop environments. On other desktop environments a generic app icon will be used.
+
+-   GNOME
+-   KDE
+-   MATE
+-   XFCE
+-   Cinnamon
+-   Pantheon
diff --git a/src/main/Core/ImageGenerator/ImageGeneratorModule.ts b/src/main/Core/ImageGenerator/ImageGeneratorModule.ts
index b21362c1..8c5563ee 100644
--- a/src/main/Core/ImageGenerator/ImageGeneratorModule.ts
+++ b/src/main/Core/ImageGenerator/ImageGeneratorModule.ts
@@ -48,18 +48,24 @@ export class ImageGeneratorModule {
         // To prevent the execution of all icon extractor constructors, we use a function here that is only invoked
         // for the current operating system.
         const operatingSystemSpecificIconExtractors: Record<OperatingSystem, () => FileIconExtractor[]> = {
-            Linux: () => [
-                new LinuxAppIconExtractor(
-                    dependencyRegistry.get("FileSystemUtility"),
-                    dependencyRegistry.get("CommandlineUtility"),
-                    dependencyRegistry.get("IniFileParser"),
-                    dependencyRegistry.get("Logger"),
-                    cacheFileNameGenerator,
-                    cacheFolderPath,
-                    dependencyRegistry.get("App").getPath("home"),
-                    dependencyRegistry.get("EnvironmentVariableProvider"),
-                ),
-            ],
+            Linux: () =>
+                ["Cinnamon", "GNOME", "KDE", "MATE", "XFCE", "Pantheon"].includes(
+                    dependencyRegistry.get("LinuxDesktopEnvironmentResolver").resolve(),
+                )
+                    ? [
+                          new LinuxAppIconExtractor(
+                              dependencyRegistry.get("FileSystemUtility"),
+                              dependencyRegistry.get("CommandlineUtility"),
+                              dependencyRegistry.get("IniFileParser"),
+                              dependencyRegistry.get("Logger"),
+                              cacheFileNameGenerator,
+                              cacheFolderPath,
+                              dependencyRegistry.get("App").getPath("home"),
+                              dependencyRegistry.get("EnvironmentVariableProvider"),
+                              dependencyRegistry.get("LinuxDesktopEnvironmentResolver"),
+                          ),
+                      ]
+                    : [],
             macOS: () => [
                 new MacOsFolderIconExtractor(
                     dependencyRegistry.get("AssetPathResolver"),
diff --git a/src/main/Core/ImageGenerator/Linux/LinuxAppIconExtractor.ts b/src/main/Core/ImageGenerator/Linux/LinuxAppIconExtractor.ts
index ab97b1a9..fe1df1fc 100644
--- a/src/main/Core/ImageGenerator/Linux/LinuxAppIconExtractor.ts
+++ b/src/main/Core/ImageGenerator/Linux/LinuxAppIconExtractor.ts
@@ -2,6 +2,7 @@ import type { CommandlineUtility } from "@Core/CommandlineUtility";
 import type { EnvironmentVariableProvider } from "@Core/EnvironmentVariableProvider";
 import type { FileSystemUtility } from "@Core/FileSystemUtility";
 import type { IniFileParser } from "@Core/IniFileParser";
+import type { LinuxDesktopEnvironment, LinuxDesktopEnvironmentResolver } from "@Core/LinuxDesktopEnvironment";
 import type { Logger } from "@Core/Logger";
 import type { Image } from "@common/Core/Image";
 import { basename, dirname, extname, join } from "path";
@@ -47,16 +48,17 @@ export class LinuxAppIconExtractor implements FileIconExtractor {
         private readonly cacheFileNameGenerator: CacheFileNameGenerator,
         private readonly cacheFolder: string,
         private readonly homePath: string,
-        private readonly environmentVariablePRovider: EnvironmentVariableProvider,
+        private readonly environmentVariableProvider: EnvironmentVariableProvider,
+        private readonly linuxDesktopEnvironmentResolver: LinuxDesktopEnvironmentResolver,
     ) {
         this.baseDirectories = [
             join(this.homePath, ".icons"),
             join(
-                this.environmentVariablePRovider.get("XDG_DATA_HOME") || join(this.homePath, ".local", "share"),
+                this.environmentVariableProvider.get("XDG_DATA_HOME") || join(this.homePath, ".local", "share"),
                 "icons",
             ),
             ...(
-                this.environmentVariablePRovider.get("XDG_DATA_DIRS") ||
+                this.environmentVariableProvider.get("XDG_DATA_DIRS") ||
                 `${join("/", "usr", "local", "share")}:${join("/", "usr", "share")}`
             )
                 .split(":")
@@ -273,15 +275,23 @@ export class LinuxAppIconExtractor implements FileIconExtractor {
     }
 
     private async getIconThemeName(): Promise<string> {
-        const currentDesktopEnvironment = this.environmentVariablePRovider.get("XDG_SESSION_DESKTOP");
-
-        const iconThemeNameMap: Record<"cinnamon" | "gnome", () => Promise<string>> = {
-            cinnamon: () =>
+        const iconThemeNameMap: Partial<Record<LinuxDesktopEnvironment, () => Promise<string>>> = {
+            GNOME: () => this.commandlineUtility.executeCommand("gsettings get org.gnome.desktop.interface icon-theme"),
+            KDE: () =>
+                this.commandlineUtility.executeCommand("kreadconfig5 --file kdeglobals --group Icons --key Theme"),
+            MATE: () => this.commandlineUtility.executeCommand("gsettings get org.mate.interface icon-theme"),
+            XFCE: () => this.commandlineUtility.executeCommand("gsettings get org.gnome.desktop.interface icon-theme"),
+            Cinnamon: () =>
                 this.commandlineUtility.executeCommand("gsettings get org.cinnamon.desktop.interface icon-theme"),
-            gnome: () => this.commandlineUtility.executeCommand("gsettings get org.gnome.desktop.interface icon-theme"),
+            Pantheon: () =>
+                this.commandlineUtility.executeCommand("gsettings get org.gnome.desktop.interface icon-theme"),
         };
 
-        return (await iconThemeNameMap[currentDesktopEnvironment]()).trim().slice(1, -1);
+        const iconThemeName = (await iconThemeNameMap[this.linuxDesktopEnvironmentResolver.resolve()]()).trim();
+
+        return iconThemeName.startsWith("'") && iconThemeName.endsWith("'")
+            ? iconThemeName.slice(1, -1)
+            : iconThemeName;
     }
 
     // https://specifications.freedesktop.org/icon-theme-spec/latest/ar01s05.html
diff --git a/src/main/Core/LinuxDesktopEnvironment/Contract/LinuxDesktopEnvironment.ts b/src/main/Core/LinuxDesktopEnvironment/Contract/LinuxDesktopEnvironment.ts
index 50ad752a..538fa324 100644
--- a/src/main/Core/LinuxDesktopEnvironment/Contract/LinuxDesktopEnvironment.ts
+++ b/src/main/Core/LinuxDesktopEnvironment/Contract/LinuxDesktopEnvironment.ts
@@ -3,11 +3,13 @@
  */
 export type LinuxDesktopEnvironment =
     | "Cinnamon"
-    | "GNOME-Classic"
-    | "GNOME-Flashback"
     | "GNOME"
     | "KDE"
     | "MATE"
+    | "XFCE"
     | "Pantheon"
-    | "X-Cinnamon"
-    | "XFCE";
+    | "COSMIC"
+    | "LXDE"
+    | "LXQt"
+    | "DDE"
+    | "Unity";
diff --git a/src/main/Core/LinuxDesktopEnvironment/LinuxDesktopEnvironmentResolver.ts b/src/main/Core/LinuxDesktopEnvironment/LinuxDesktopEnvironmentResolver.ts
index ac1bad0e..37bbe0c1 100644
--- a/src/main/Core/LinuxDesktopEnvironment/LinuxDesktopEnvironmentResolver.ts
+++ b/src/main/Core/LinuxDesktopEnvironment/LinuxDesktopEnvironmentResolver.ts
@@ -9,10 +9,28 @@ export class LinuxDesktopEnvironmentResolver implements LinuxDesktopEnvironmentR
 
     public resolve(): LinuxDesktopEnvironment | undefined {
         const map: Record<string, LinuxDesktopEnvironment> = {
-            "ubuntu:GNOME": "GNOME",
+            GNOME: "GNOME",
+            "GNOME-Classic": "GNOME",
+            "GNOME-Flashback": "GNOME",
             KDE: "KDE",
+            COSMIC: "COSMIC",
+            LXDE: "LXDE",
+            LXQt: "LXQt",
+            MATE: "MATE",
+            Unity: "Unity",
+            XFCE: "XFCE",
+            Cinnamon: "Cinnamon",
+            "X-Cinnamon": "Cinnamon",
+            Pantheon: "Pantheon",
+            DDE: "DDE",
         };
 
-        return map[this.environmentVariableProvider.get("XDG_CURRENT_DESKTOP")];
+        for (const environment of this.environmentVariableProvider.get("ORIGINAL_XDG_CURRENT_DESKTOP").split(":")) {
+            if (map[environment]) {
+                return map[environment];
+            }
+        }
+
+        return undefined;
     }
 }
diff --git a/src/main/Extensions/ApplicationSearch/ApplicationSearch.ts b/src/main/Extensions/ApplicationSearch/ApplicationSearch.ts
index 5ef5d43f..04192eae 100644
--- a/src/main/Extensions/ApplicationSearch/ApplicationSearch.ts
+++ b/src/main/Extensions/ApplicationSearch/ApplicationSearch.ts
@@ -1,5 +1,4 @@
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
-import type { EnvironmentVariableProvider } from "@Core/EnvironmentVariableProvider";
 import type { Extension } from "@Core/Extension";
 import type { OperatingSystem, SearchResultItem } from "@common/Core";
 import { getExtensionSettingKey } from "@common/Core/Extension";
@@ -26,7 +25,6 @@ export class ApplicationSearch implements Extension {
         private readonly applicationRepository: ApplicationRepository,
         private readonly settings: Settings,
         private readonly assetPathResolver: AssetPathResolver,
-        private readonly environmentVariableProvider: EnvironmentVariableProvider,
     ) {}
 
     public async getSearchResultItems(): Promise<SearchResultItem[]> {
@@ -35,13 +33,7 @@ export class ApplicationSearch implements Extension {
     }
 
     public isSupported(): boolean {
-        const checks: Record<OperatingSystem, () => boolean> = {
-            Linux: () => ["cinnamon", "gnome"].includes(this.environmentVariableProvider.get("XDG_SESSION_DESKTOP")),
-            macOS: () => true,
-            Windows: () => true,
-        };
-
-        return checks[this.operatingSystem]();
+        return true;
     }
 
     public getSettingDefaultValue<T>(key: string): T {
diff --git a/src/main/Extensions/ApplicationSearch/ApplicationSearchModule.ts b/src/main/Extensions/ApplicationSearch/ApplicationSearchModule.ts
index 1bcd0a59..b8648d08 100644
--- a/src/main/Extensions/ApplicationSearch/ApplicationSearchModule.ts
+++ b/src/main/Extensions/ApplicationSearch/ApplicationSearchModule.ts
@@ -61,7 +61,6 @@ export class ApplicationSearchModule implements ExtensionModule {
                 applicationRepositories[dependencyRegistry.get("OperatingSystem")](),
                 settings,
                 dependencyRegistry.get("AssetPathResolver"),
-                dependencyRegistry.get("EnvironmentVariableProvider"),
             ),
             actionHandlers: actionHandlers[dependencyRegistry.get("OperatingSystem")](),
         };
diff --git a/src/main/Extensions/ApplicationSearch/Linux/LinuxApplicationRepository.ts b/src/main/Extensions/ApplicationSearch/Linux/LinuxApplicationRepository.ts
index 5fcb70d8..da8ed8a8 100644
--- a/src/main/Extensions/ApplicationSearch/Linux/LinuxApplicationRepository.ts
+++ b/src/main/Extensions/ApplicationSearch/Linux/LinuxApplicationRepository.ts
@@ -60,7 +60,7 @@ export class LinuxApplicationRepository implements ApplicationRepository {
             (await this.fileSystemUtility.readFile(filePath)).toString(),
         )["Desktop Entry"];
         // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#recognized-keys
-        const desktopEnv = this.environmentVariableProvider.get("XDG_CURRENT_DESKTOP")?.split(":");
+        const desktopEnv = this.environmentVariableProvider.get("ORIGINAL_XDG_CURRENT_DESKTOP")?.split(":");
         if (
             !config ||
             !config["Icon"] ||


Test patch:
diff --git a/src/main/Core/LinuxDesktopEnvironment/LinuxDesktopEnvironmentResolver.test.ts b/src/main/Core/LinuxDesktopEnvironment/LinuxDesktopEnvironmentResolver.test.ts
index 04e69834..6efe806e 100644
--- a/src/main/Core/LinuxDesktopEnvironment/LinuxDesktopEnvironmentResolver.test.ts
+++ b/src/main/Core/LinuxDesktopEnvironment/LinuxDesktopEnvironmentResolver.test.ts
@@ -16,15 +16,54 @@ describe(LinuxDesktopEnvironmentResolver, () => {
 
         expect(new LinuxDesktopEnvironmentResolver(environmentVariableProvider).resolve()).toBe(expected);
         expect(getMock).toHaveBeenCalledOnce();
-        expect(getMock).toHaveBeenCalledWith("XDG_CURRENT_DESKTOP");
+        expect(getMock).toHaveBeenCalledWith("ORIGINAL_XDG_CURRENT_DESKTOP");
     };
 
     it("should return undefined when XDG_CURRENT_DESKTOP environment variable is not set", () =>
         testResolve({ expected: undefined, xdgCurrentDesktop: "" }));
 
-    it("should return GNOME when XDG_CURRENT_DESKTOP is ubuntu:GNOME", () =>
-        testResolve({ expected: "GNOME", xdgCurrentDesktop: "ubuntu:GNOME" }));
+    it("should return the first valid desktop in XDG_CURRENT_DESKTOP", () =>
+        testResolve({ expected: "GNOME", xdgCurrentDesktop: "ubuntu:GNOME:someother:KDE" }));
 
-    it("should return KDE when XDG_CURRENT_DESKTOP is KDE", () =>
+    it("should return GNOME when XDG_CURRENT_DESKTOP contains GNOME", () =>
+        testResolve({ expected: "GNOME", xdgCurrentDesktop: "GNOME" }));
+
+    it("should return GNOME when XDG_CURRENT_DESKTOP contains GNOME-Flashback", () =>
+        testResolve({ expected: "GNOME", xdgCurrentDesktop: "GNOME-Flashback" }));
+
+    it("should return GNOME when XDG_CURRENT_DESKTOP contains GNOME-Classic", () =>
+        testResolve({ expected: "GNOME", xdgCurrentDesktop: "GNOME-Classic" }));
+
+    it("should return KDE when XDG_CURRENT_DESKTOP contains KDE", () =>
+        testResolve({ expected: "KDE", xdgCurrentDesktop: "KDE" }));
+
+    it("should return COSMIC when XDG_CURRENT_DESKTOP contains COSMIC", () =>
+        testResolve({ expected: "COSMIC", xdgCurrentDesktop: "COSMIC" }));
+
+    it("should return LXDE when XDG_CURRENT_DESKTOP contains LXDE", () =>
+        testResolve({ expected: "LXDE", xdgCurrentDesktop: "LXDE" }));
+
+    it("should return LXQt when XDG_CURRENT_DESKTOP contains LXQt", () =>
+        testResolve({ expected: "LXQt", xdgCurrentDesktop: "LXQt" }));
+
+    it("should return MATE when XDG_CURRENT_DESKTOP contains MATE", () =>
+        testResolve({ expected: "MATE", xdgCurrentDesktop: "MATE" }));
+
+    it("should return Unity when XDG_CURRENT_DESKTOP contains Unity", () =>
+        testResolve({ expected: "Unity", xdgCurrentDesktop: "Unity" }));
+
+    it("should return XFCE when XDG_CURRENT_DESKTOP contains XFCE", () =>
+        testResolve({ expected: "XFCE", xdgCurrentDesktop: "XFCE" }));
+
+    it("should return Cinnamon when XDG_CURRENT_DESKTOP contains X-Cinnamon", () =>
+        testResolve({ expected: "Cinnamon", xdgCurrentDesktop: "X-Cinnamon" }));
+
+    it("should return Cinnamon when XDG_CURRENT_DESKTOP contains KDE", () =>
         testResolve({ expected: "KDE", xdgCurrentDesktop: "KDE" }));
+
+    it("should return Pantheon when XDG_CURRENT_DESKTOP contains Pantheon", () =>
+        testResolve({ expected: "Pantheon", xdgCurrentDesktop: "Pantheon" }));
+
+    it("should return DDE when XDG_CURRENT_DESKTOP contains DDE", () =>
+        testResolve({ expected: "DDE", xdgCurrentDesktop: "DDE" }));
 });
diff --git a/src/main/Extensions/ApplicationSearch/ApplicationSearch.test.ts b/src/main/Extensions/ApplicationSearch/ApplicationSearch.test.ts
index f9b0855c..efaf6966 100644
--- a/src/main/Extensions/ApplicationSearch/ApplicationSearch.test.ts
+++ b/src/main/Extensions/ApplicationSearch/ApplicationSearch.test.ts
@@ -1,5 +1,4 @@
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
-import type { EnvironmentVariableProvider } from "@Core/EnvironmentVariableProvider";
 import type { OperatingSystem, SearchResultItem } from "@common/Core";
 import { getExtensionSettingKey } from "@common/Core/Extension";
 import { describe, expect, it, vi } from "vitest";
@@ -34,7 +33,6 @@ describe(ApplicationSearch, () => {
             applicationRepository,
             <Settings>{},
             <AssetPathResolver>{},
-            <EnvironmentVariableProvider>{},
         ).getSearchResultItems();
 
         expect(searchResultItems).toEqual(applications.map((application) => application.toSearchResultItem()));
@@ -56,7 +54,6 @@ describe(ApplicationSearch, () => {
             <ApplicationRepository>{},
             settings,
             <AssetPathResolver>{},
-            <EnvironmentVariableProvider>{},
         );
 
         expect(applicationSearch.getSettingDefaultValue("key1")).toBe("value1");
@@ -68,51 +65,23 @@ describe(ApplicationSearch, () => {
         const testIsSupported = ({
             expected,
             operatingSystem,
-            environmentVariables,
         }: {
             expected: boolean;
             operatingSystem: OperatingSystem;
-            environmentVariables?: Record<string, string>;
         }) => {
-            const environmentVariableProvider = <EnvironmentVariableProvider>{
-                get: (name: string) => environmentVariables[name],
-                getAll: () => environmentVariables,
-            };
-
             expect(
                 new ApplicationSearch(
                     operatingSystem,
                     <ApplicationRepository>{},
                     <Settings>{},
                     <AssetPathResolver>{},
-                    environmentVariableProvider,
                 ).isSupported(),
             ).toBe(expected);
         };
 
         it("should return true on Windows", () => testIsSupported({ expected: true, operatingSystem: "Windows" }));
         it("should return true on macOS", () => testIsSupported({ expected: true, operatingSystem: "macOS" }));
-
-        it("should return true on Linux with cinnamon desktop environment", () =>
-            testIsSupported({
-                expected: true,
-                operatingSystem: "Linux",
-                environmentVariables: { XDG_SESSION_DESKTOP: "cinnamon" },
-            }));
-
-        it("should return true on Linux with gnome desktop environment", () =>
-            testIsSupported({
-                expected: true,
-                operatingSystem: "Linux",
-                environmentVariables: { XDG_SESSION_DESKTOP: "gnome" },
-            }));
-
-        it("should return false on Linux with other desktop environment", () =>
-            testIsSupported({
-                expected: false,
-                operatingSystem: "Linux",
-                environmentVariables: { XDG_SESSION_DESKTOP: "KDE" },
-            }));
+        it("should return true on Linux", () => testIsSupported({ expected: true, operatingSystem: "Linux" }));
     });
 
     it("should support en-US and de-DE translations", () => {
@@ -121,7 +90,6 @@ describe(ApplicationSearch, () => {
             <ApplicationRepository>{},
             <Settings>{},
             <AssetPathResolver>{},
-            <EnvironmentVariableProvider>{},
         ).getI18nResources();
 
         expect(Object.keys(translations)).to.include("en-US");
@@ -136,28 +104,21 @@ describe(ApplicationSearch, () => {
         };
 
         expect(
-            new ApplicationSearch(
-                "Windows",
-                <ApplicationRepository>{},
-                <Settings>{},
-                assetPathResolver,
-                <EnvironmentVariableProvider>{},
-            ).getImage(),
+            new ApplicationSearch("Windows", <ApplicationRepository>{}, <Settings>{}, assetPathResolver).getImage(),
+        ).toEqual({ url: "file://someFilePath" });
+
+        expect(
+            new ApplicationSearch("macOS", <ApplicationRepository>{}, <Settings>{}, assetPathResolver).getImage(),
         ).toEqual({ url: "file://someFilePath" });
 
         expect(
-            new ApplicationSearch(
-                "macOS",
-                <ApplicationRepository>{},
-                <Settings>{},
-                assetPathResolver,
-                <EnvironmentVariableProvider>{},
-            ).getImage(),
+            new ApplicationSearch("Linux", <ApplicationRepository>{}, <Settings>{}, assetPathResolver).getImage(),
         ).toEqual({ url: "file://someFilePath" });
 
         expect(getExtensionAssetPathMock).toHaveBeenCalledWith("ApplicationSearch", "windows-generic-app-icon.png");
         expect(getExtensionAssetPathMock).toHaveBeenCalledWith("ApplicationSearch", "macos-applications.png");
-        expect(getExtensionAssetPathMock).toHaveBeenCalledTimes(2);
+        expect(getExtensionAssetPathMock).toHaveBeenCalledWith("ApplicationSearch", "linux-applications.png");
+        expect(getExtensionAssetPathMock).toHaveBeenCalledTimes(3);
     });
 
     it("should get the correct setting ids that trigger a rescan", () => {
@@ -166,7 +127,6 @@ describe(ApplicationSearch, () => {
             <ApplicationRepository>{},
             <Settings>{},
             <AssetPathResolver>{},
-            <EnvironmentVariableProvider>{},
         ).getSettingKeysTriggeringRescan();
 
         expect(settingKeys).toEqual([