Repository: oliverschwendener/ueli
Pull request #1431 (2025-05-13 11:51:50)

feat(SimpleFileSearch): add the option to exclude hidden files
This PR adds the option to exclude hidden files as requested in #1214.

![image](https://github.com/user-attachments/assets/0a0514ce-5abf-4206-bd3b-3b25cc3feb09)



Patch:
diff --git a/src/common/Extensions/SimpleFileSearch/FolderSetting.ts b/src/common/Extensions/SimpleFileSearch/FolderSetting.ts
index 58b539da..c1ce9c2a 100644
--- a/src/common/Extensions/SimpleFileSearch/FolderSetting.ts
+++ b/src/common/Extensions/SimpleFileSearch/FolderSetting.ts
@@ -2,5 +2,6 @@ export type FolderSetting = {
     id: string;
     path: string;
     recursive: boolean;
+    excludeHiddenFiles?: boolean;
     searchFor: "files" | "folders" | "filesAndFolders";
 };
diff --git a/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts b/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts
index 11e758a0..fad15894 100644
--- a/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts
+++ b/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts
@@ -8,8 +8,8 @@ export interface FileSystemUtility {
     readTextFile(filePath: string, encoding?: BufferEncoding): Promise<string>;
     readJsonFile<T>(filePath: string): Promise<T>;
     readJsonFileSync<T>(filePath: string): T;
-    readDirectory(folderPath: string, recursive?: boolean): Promise<string[]>;
-    readDirectorySync(folderPath: string, recursive?: boolean): string[];
+    readDirectory(folderPath: string, recursive?: boolean, excludeHiddenFiles?: boolean): Promise<string[]>;
+    readDirectorySync(folderPath: string, recursive?: boolean, excludeHiddenFiles?: boolean): string[];
     removeFile(filePath: string): Promise<void>;
     removeFileSync(filePath: string): void;
     writeFile(data: Buffer, filePath: string): Promise<void>;
diff --git a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts
index 93e35849..666e8f58 100644
--- a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts
+++ b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts
@@ -100,13 +100,23 @@ export class NodeJsFileSystemUtility implements FileSystemUtility {
         return (await this.readFile(filePath)).toString(encoding);
     }
 
-    public async readDirectory(folderPath: string, recursive?: boolean): Promise<string[]> {
+    public async readDirectory(
+        folderPath: string,
+        recursive?: boolean,
+        excludeHiddenFiles?: boolean,
+    ): Promise<string[]> {
         const fileNames = await readdir(folderPath, { recursive });
-        return fileNames.map((fileName) => join(folderPath, fileName));
+
+        return fileNames
+            .filter((fileName) => (excludeHiddenFiles ? !fileName.startsWith(".") : true))
+            .map((fileName) => join(folderPath, fileName));
     }
 
-    public readDirectorySync(folderPath: string, recursive?: boolean): string[] {
+    public readDirectorySync(folderPath: string, recursive?: boolean, excludeHiddenFiles?: boolean): string[] {
         const fileNames = readdirSync(folderPath, { recursive });
-        return fileNames.map((fileName) => join(folderPath, fileName.toString()));
+
+        return fileNames
+            .filter((fileName) => (excludeHiddenFiles ? !fileName.toString().startsWith(".") : true))
+            .map((fileName) => join(folderPath, fileName.toString()));
     }
 }
diff --git a/src/main/Extensions/SimpleFileSearch/SimpleFileSearchExtension.ts b/src/main/Extensions/SimpleFileSearch/SimpleFileSearchExtension.ts
index a3b27ba4..de26938b 100644
--- a/src/main/Extensions/SimpleFileSearch/SimpleFileSearchExtension.ts
+++ b/src/main/Extensions/SimpleFileSearch/SimpleFileSearchExtension.ts
@@ -126,7 +126,9 @@ export class SimpleFileSearchExtension implements Extension {
 
     private async getFilePaths(folderSettings: FolderSetting[]): Promise<Record<string, string[]>> {
         const promiseResults = await Promise.allSettled(
-            folderSettings.map(({ path, recursive }) => this.fileSystemUtility.readDirectory(path, recursive)),
+            folderSettings.map(({ path, recursive, excludeHiddenFiles }) =>
+                this.fileSystemUtility.readDirectory(path, recursive, excludeHiddenFiles),
+            ),
         );
 
         const result: Record<string, string[]> = {};
@@ -183,6 +185,7 @@ export class SimpleFileSearchExtension implements Extension {
                 folders: "Folders",
                 path: "Path",
                 recursive: "Recursive",
+                excludeHiddenFiles: "Exclude hidden files",
                 add: "Add",
                 save: "Save",
                 addFolder: "Add folder",
@@ -212,6 +215,7 @@ export class SimpleFileSearchExtension implements Extension {
                 folders: "Ordner",
                 path: "Pfad",
                 recursive: "Rekursiv",
+                excludeHiddenFiles: "Versteckte Dateien ausschliessen",
                 add: "Hinzufügen",
                 save: "Speichern",
                 addFolder: "Ordner hinzufügen",
diff --git a/src/renderer/Extensions/SimpleFileSearch/EditDialog.tsx b/src/renderer/Extensions/SimpleFileSearch/EditDialog.tsx
index 5cd2f2b8..5b932e5d 100644
--- a/src/renderer/Extensions/SimpleFileSearch/EditDialog.tsx
+++ b/src/renderer/Extensions/SimpleFileSearch/EditDialog.tsx
@@ -26,6 +26,7 @@ const createFolderSetting = (): FolderSetting => ({
     id: crypto.randomUUID(),
     path: "",
     recursive: false,
+    excludeHiddenFiles: false,
     searchFor: "filesAndFolders",
 });
 
@@ -34,10 +35,11 @@ const folderSettingToState = (folderSetting: FolderSetting): State => ({
     isValidPath: window.ContextBridge.fileExists(folderSetting.path),
 });
 
-const stateToFolderSetting = ({ id, path, recursive, searchFor }: State): FolderSetting => ({
+const stateToFolderSetting = ({ id, path, recursive, searchFor, excludeHiddenFiles }: State): FolderSetting => ({
     id,
     path,
     recursive,
+    excludeHiddenFiles,
     searchFor,
 });
 
@@ -79,6 +81,13 @@ export const EditDialog = ({ title, trigger, confirm, cancel, folderSetting, onS
         });
     };
 
+    const updateExcludeHiddenFiles = (excludeHiddenFiles: boolean) => {
+        setState({
+            ...state,
+            excludeHiddenFiles,
+        });
+    };
+
     const openFileDialog = async () => {
         const result = await window.ContextBridge.showOpenDialog({ properties: ["openDirectory"] });
 
@@ -135,6 +144,11 @@ export const EditDialog = ({ title, trigger, confirm, cancel, folderSetting, onS
                                 checked={state.recursive}
                                 onChange={(_, { checked }) => updateRecursiveness(checked === true)}
                             />
+                            <Checkbox
+                                label={t("excludeHiddenFiles")}
+                                checked={state.excludeHiddenFiles}
+                                onChange={(_, { checked }) => updateExcludeHiddenFiles(checked === true)}
+                            />
                         </div>
                     </DialogContent>
                     <DialogActions>
diff --git a/src/renderer/Extensions/SimpleFileSearch/FolderListItem.tsx b/src/renderer/Extensions/SimpleFileSearch/FolderListItem.tsx
index efa728b0..50e983ad 100644
--- a/src/renderer/Extensions/SimpleFileSearch/FolderListItem.tsx
+++ b/src/renderer/Extensions/SimpleFileSearch/FolderListItem.tsx
@@ -39,6 +39,18 @@ export const FolderListItem = ({ folderSetting, removeFolderSetting, updateFolde
                                 </Badge>
                             )}
                         </Caption1>
+                        <Caption1>
+                            {t("excludeHiddenFiles")}:{" "}
+                            {folderSetting.excludeHiddenFiles ? (
+                                <Badge appearance="tint" size="small">
+                                    Yes
+                                </Badge>
+                            ) : (
+                                <Badge appearance="tint" size="small" color="informative">
+                                    No
+                                </Badge>
+                            )}
+                        </Caption1>
                         <Caption1>
                             {t("searchFor")}: {t(`searchFor.${folderSetting.searchFor}`)}
                         </Caption1>


Test patch:
diff --git a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts
index d3e6ad96..14c42052 100644
--- a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts
+++ b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts
@@ -169,6 +169,48 @@ describe(NodeJsFileSystemUtility, () => {
 
             expect(actual).toEqual([]);
         });
+
+        it("should exclude hidden files when excludeHiddenFiles is true", async () => {
+            const fileSystemUtility = new NodeJsFileSystemUtility();
+
+            const filePath1 = join(tempFolderPath, "file1.txt");
+            const filePath2 = join(tempFolderPath, ".file2.txt");
+
+            await fileSystemUtility.writeTextFile("test", filePath1);
+            await fileSystemUtility.writeTextFile("test 2", filePath2);
+
+            const actual = await fileSystemUtility.readDirectory(tempFolderPath, false, true);
+
+            expect(actual).toEqual([filePath1]);
+        });
+
+        it("should include hidden files when excludeHiddenFiles is false", async () => {
+            const fileSystemUtility = new NodeJsFileSystemUtility();
+
+            const filePath1 = join(tempFolderPath, "file1.txt");
+            const filePath2 = join(tempFolderPath, ".file2.txt");
+
+            await fileSystemUtility.writeTextFile("test", filePath1);
+            await fileSystemUtility.writeTextFile("test 2", filePath2);
+
+            const actual = await fileSystemUtility.readDirectory(tempFolderPath, false, false);
+
+            expect(actual).toEqual([filePath2, filePath1]);
+        });
+
+        it("should include hidden files when excludeHiddenFiles is undefined", async () => {
+            const fileSystemUtility = new NodeJsFileSystemUtility();
+
+            const filePath1 = join(tempFolderPath, "file1.txt");
+            const filePath2 = join(tempFolderPath, ".file2.txt");
+
+            await fileSystemUtility.writeTextFile("test", filePath1);
+            await fileSystemUtility.writeTextFile("test 2", filePath2);
+
+            const actual = await fileSystemUtility.readDirectory(tempFolderPath, false, undefined);
+
+            expect(actual).toEqual([filePath2, filePath1]);
+        });
     });
 
     describe(NodeJsFileSystemUtility.prototype.readDirectorySync, () => {
@@ -212,6 +254,48 @@ describe(NodeJsFileSystemUtility, () => {
 
             expect(actual).toEqual([]);
         });
+
+        it("should exclude hidden files when excludeHiddenFiles is true", async () => {
+            const fileSystemUtility = new NodeJsFileSystemUtility();
+
+            const filePath1 = join(tempFolderPath, "file1.txt");
+            const filePath2 = join(tempFolderPath, ".file2.txt");
+
+            await fileSystemUtility.writeTextFile("test", filePath1);
+            await fileSystemUtility.writeTextFile("test 2", filePath2);
+
+            const actual = fileSystemUtility.readDirectorySync(tempFolderPath, true, true);
+
+            expect(actual).toEqual([filePath1]);
+        });
+
+        it("should include hidden files when excludeHiddenFiles is false", async () => {
+            const fileSystemUtility = new NodeJsFileSystemUtility();
+
+            const filePath1 = join(tempFolderPath, "file1.txt");
+            const filePath2 = join(tempFolderPath, ".file2.txt");
+
+            await fileSystemUtility.writeTextFile("test", filePath1);
+            await fileSystemUtility.writeTextFile("test 2", filePath2);
+
+            const actual = fileSystemUtility.readDirectorySync(tempFolderPath, true, false);
+
+            expect(actual).toEqual([filePath2, filePath1]);
+        });
+
+        it("should include hidden files when excludeHiddenFiles is undefined", async () => {
+            const fileSystemUtility = new NodeJsFileSystemUtility();
+
+            const filePath1 = join(tempFolderPath, "file1.txt");
+            const filePath2 = join(tempFolderPath, ".file2.txt");
+
+            await fileSystemUtility.writeTextFile("test", filePath1);
+            await fileSystemUtility.writeTextFile("test 2", filePath2);
+
+            const actual = fileSystemUtility.readDirectorySync(tempFolderPath, true, undefined);
+
+            expect(actual).toEqual([filePath2, filePath1]);
+        });
     });
 
     describe(NodeJsFileSystemUtility.prototype.readFile, () => {