launcher: fd based file search

This commit is contained in:
2 * r + 2 * t 2025-01-14 19:24:34 +11:00
parent 9c8d9aa975
commit e8328bbb1c
2 changed files with 48 additions and 5 deletions

View file

@ -5,6 +5,7 @@ import type AstalApps from "gi://AstalApps";
import AstalHyprland from "gi://AstalHyprland"; import AstalHyprland from "gi://AstalHyprland";
import { Apps } from "../services/apps"; import { Apps } from "../services/apps";
import Math, { type HistoryItem } from "../services/math"; import Math, { type HistoryItem } from "../services/math";
import { HOME } from "../utils/constants";
import { getAppCategoryIcon } from "../utils/icons"; import { getAppCategoryIcon } from "../utils/icons";
import { launch } from "../utils/system"; import { launch } from "../utils/system";
import { PopupWindow, setupCustomTooltip, TransitionType } from "../utils/widgets"; import { PopupWindow, setupCustomTooltip, TransitionType } from "../utils/widgets";
@ -19,6 +20,7 @@ interface Subcommand {
} }
const maxSearchResults = 15; const maxSearchResults = 15;
const fdOptions = ["-a", "-t", "f", "-E", ".git"];
const browser = [ const browser = [
"firefox", "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(); const toplevel = self.get_toplevel();
if (toplevel instanceof Widget.Window) toplevel.hide(); if (toplevel instanceof Widget.Window) toplevel.hide();
};
const launchAndClose = (self: JSX.Element, astalApp: AstalApps.Application) => {
close(self);
launch(astalApp); 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[] }) => { const PinnedApp = ({ names }: { names: string[] }) => {
let app: Gio.DesktopAppInfo | null = null; let app: Gio.DesktopAppInfo | null = null;
let astalApp: AstalApps.Application | undefined; let astalApp: AstalApps.Application | undefined;
@ -137,7 +152,7 @@ const Result = ({
)} )}
{sublabel ? ( {sublabel ? (
<box vertical valign={Gtk.Align.CENTER} className="has-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} /> <label hexpand truncate maxWidthChars={1} className="sublabel" xalign={0} label={sublabel} />
</box> </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 Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> }) => {
const empty = Variable(true); const empty = Variable(true);
@ -248,9 +271,10 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
}; };
const subcommandList = Object.keys(subcommands); const subcommandList = Object.keys(subcommands);
const updateEmpty = () => empty.set(self.get_children().length === 0);
const appSearch = () => { const appSearch = () => {
const apps = Apps.fuzzy_query(entry.text); const apps = Apps.fuzzy_query(entry.text);
empty.set(apps.length === 0);
if (apps.length > maxSearchResults) apps.length = maxSearchResults; if (apps.length > maxSearchResults) apps.length = maxSearchResults;
for (const app of apps) self.add(<AppResult app={app} />); 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} />); 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", () => { self.hook(entry, "activate", () => {
if (!entry.text) return; if (!entry.text) return;
if (mode.get() === "math") { if (mode.get() === "math") {
@ -274,7 +312,10 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
}); });
self.hook(entry, "changed", () => { self.hook(entry, "changed", () => {
if (!entry.text && mode.get() === "apps") return; 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(">")) { if (entry.text.startsWith(">")) {
const args = entry.text.split(" "); 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() === "apps") appSearch();
else if (mode.get() === "math") calculate(); else if (mode.get() === "math") calculate();
else if (mode.get() === "files") fileSearch();
empty.set(self.get_children().length === 0); if (ignoreFileAsync) updateEmpty();
}); });
}} }}
/> />

View file

@ -1,4 +1,5 @@
import { exec, GLib } from "astal"; import { exec, GLib } from "astal";
export const HOME = GLib.get_home_dir();
export const CACHE_DIR = GLib.get_user_cache_dir() + "/caelestia"; export const CACHE_DIR = GLib.get_user_cache_dir() + "/caelestia";
exec(`mkdir -p '${CACHE_DIR}'`); exec(`mkdir -p '${CACHE_DIR}'`);