From e8328bbb1cdc31abb0a6fcee8d0957bfcaf99b54 Mon Sep 17 00:00:00 2001
From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
Date: Tue, 14 Jan 2025 19:24:34 +1100
Subject: launcher: fd based file search

---
 modules/launcher.tsx | 52 +++++++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 47 insertions(+), 5 deletions(-)

(limited to 'modules/launcher.tsx')

diff --git a/modules/launcher.tsx b/modules/launcher.tsx
index 5015b5b..b9ab20b 100644
--- a/modules/launcher.tsx
+++ b/modules/launcher.tsx
@@ -5,6 +5,7 @@ import type AstalApps from "gi://AstalApps";
 import AstalHyprland from "gi://AstalHyprland";
 import { Apps } from "../services/apps";
 import Math, { type HistoryItem } from "../services/math";
+import { HOME } from "../utils/constants";
 import { getAppCategoryIcon } from "../utils/icons";
 import { launch } from "../utils/system";
 import { PopupWindow, setupCustomTooltip, TransitionType } from "../utils/widgets";
@@ -19,6 +20,7 @@ interface Subcommand {
 }
 
 const maxSearchResults = 15;
+const fdOptions = ["-a", "-t", "f", "-E", ".git"];
 
 const browser = [
     "firefox",
@@ -56,12 +58,25 @@ const getEmptyTextFromMode = (mode: Mode) => {
     }
 };
 
-const launchAndClose = (self: JSX.Element, astalApp: AstalApps.Application) => {
+const close = (self: JSX.Element) => {
     const toplevel = self.get_toplevel();
     if (toplevel instanceof Widget.Window) toplevel.hide();
+};
+
+const launchAndClose = (self: JSX.Element, astalApp: AstalApps.Application) => {
+    close(self);
     launch(astalApp);
 };
 
+const openFileAndClose = (self: JSX.Element, path: string) => {
+    close(self);
+    execAsync([
+        "bash",
+        "-c",
+        `dbus-send --session --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:"file://${path}" string:"" || xdg-open "${path}"`,
+    ]).catch(console.error);
+};
+
 const PinnedApp = ({ names }: { names: string[] }) => {
     let app: Gio.DesktopAppInfo | null = null;
     let astalApp: AstalApps.Application | undefined;
@@ -137,7 +152,7 @@ const Result = ({
             )}
             {sublabel ? (
                 <box vertical valign={Gtk.Align.CENTER} className="has-sublabel">
-                    <label xalign={0} label={label} />
+                    <label hexpand truncate maxWidthChars={1} xalign={0} label={label} />
                     <label hexpand truncate maxWidthChars={1} className="sublabel" xalign={0} label={sublabel} />
                 </box>
             ) : (
@@ -196,6 +211,14 @@ const MathResult = ({ math, isHistory, entry }: { math: HistoryItem; isHistory?:
     />
 );
 
+const FileResult = ({ path }: { path: string }) => (
+    <Result
+        label={path.split("/").pop()!}
+        sublabel={path.startsWith(HOME) ? "~" + path.slice(HOME.length) : path}
+        onClicked={self => openFileAndClose(self, path)}
+    />
+);
+
 const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> }) => {
     const empty = Variable(true);
 
@@ -248,9 +271,10 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
                     };
                     const subcommandList = Object.keys(subcommands);
 
+                    const updateEmpty = () => empty.set(self.get_children().length === 0);
+
                     const appSearch = () => {
                         const apps = Apps.fuzzy_query(entry.text);
-                        empty.set(apps.length === 0);
                         if (apps.length > maxSearchResults) apps.length = maxSearchResults;
                         for (const app of apps) self.add(<AppResult app={app} />);
                     };
@@ -264,6 +288,20 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
                             self.add(<MathResult isHistory math={item} entry={entry} />);
                     };
 
+                    const fileSearch = () =>
+                        execAsync(["fd", ...fdOptions, entry.text, HOME])
+                            .then(out => {
+                                const paths = out.split("\n").filter(path => path);
+                                if (paths.length > maxSearchResults) paths.length = maxSearchResults;
+                                self.foreach(ch => ch.destroy());
+                                for (const path of paths) self.add(<FileResult path={path} />);
+                            })
+                            .catch(e => {
+                                // Ignore execAsync error
+                                if (!(e instanceof Gio.IOErrorEnum || e instanceof GLib.SpawnError)) console.error(e);
+                            })
+                            .finally(updateEmpty);
+
                     self.hook(entry, "activate", () => {
                         if (!entry.text) return;
                         if (mode.get() === "math") {
@@ -274,7 +312,10 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
                     });
                     self.hook(entry, "changed", () => {
                         if (!entry.text && mode.get() === "apps") return;
-                        self.foreach(ch => ch.destroy());
+
+                        // Files has delay cause async so it does some stuff by itself
+                        const ignoreFileAsync = !entry.text || entry.text.startsWith(">") || mode.get() !== "files";
+                        if (ignoreFileAsync) self.foreach(ch => ch.destroy());
 
                         if (entry.text.startsWith(">")) {
                             const args = entry.text.split(" ");
@@ -288,8 +329,9 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
                                 );
                         } else if (mode.get() === "apps") appSearch();
                         else if (mode.get() === "math") calculate();
+                        else if (mode.get() === "files") fileSearch();
 
-                        empty.set(self.get_children().length === 0);
+                        if (ignoreFileAsync) updateEmpty();
                     });
                 }}
             />
-- 
cgit v1.2.3-freya

