From 02fd2e97f2c8a53bf2344e6fa8b14769cb15ee38 Mon Sep 17 00:00:00 2001
From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
Date: Thu, 16 Jan 2025 16:35:37 +1100
Subject: refactor: move ts to src
Also move popupwindow to own file
---
src/modules/launcher.tsx | 391 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 391 insertions(+)
create mode 100644 src/modules/launcher.tsx
(limited to 'src/modules/launcher.tsx')
diff --git a/src/modules/launcher.tsx b/src/modules/launcher.tsx
new file mode 100644
index 0000000..2fc6eef
--- /dev/null
+++ b/src/modules/launcher.tsx
@@ -0,0 +1,391 @@
+import { bind, execAsync, Gio, GLib, register, timeout, Variable } from "astal";
+import { Astal, Gtk, Widget } from "astal/gtk3";
+import fuzzysort from "fuzzysort";
+import type AstalApps from "gi://AstalApps";
+import AstalHyprland from "gi://AstalHyprland";
+import { launcher as config } from "../../config";
+import { Apps } from "../services/apps";
+import Math, { type HistoryItem } from "../services/math";
+import { getAppCategoryIcon } from "../utils/icons";
+import { launch } from "../utils/system";
+import { setupCustomTooltip } from "../utils/widgets";
+import PopupWindow from "../widgets/popupwindow";
+
+type Mode = "apps" | "files" | "math";
+
+interface Subcommand {
+ icon: string;
+ name: string;
+ description: string;
+ command: (...args: string[]) => void;
+}
+
+const getIconFromMode = (mode: Mode) => {
+ switch (mode) {
+ case "apps":
+ return "apps";
+ case "files":
+ return "folder";
+ case "math":
+ return "calculate";
+ }
+};
+
+const getEmptyTextFromMode = (mode: Mode) => {
+ switch (mode) {
+ case "apps":
+ return "No apps found";
+ case "files":
+ return GLib.find_program_in_path("fd") === null ? "File search requires `fd`" : "No files found";
+ case "math":
+ return "Type an expression";
+ }
+};
+
+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: string[]) => {
+ let app: Gio.DesktopAppInfo | null = null;
+ let astalApp: AstalApps.Application | undefined;
+ for (const name of names) {
+ app = Gio.DesktopAppInfo.new(`${name}.desktop`);
+ if (app) {
+ astalApp = Apps.get_list().find(a => a.entry === `${name}.desktop`);
+ if (app.get_icon() && astalApp) break;
+ else app = null; // Set app to null if no icon or matching AstalApps#Application
+ }
+ }
+
+ if (!app) console.error(`Launcher - Unable to find app for "${names.join(", ")}"`);
+
+ return app ? (
+
+ ) : null;
+};
+
+const PinnedApps = () => {config.pins.map(PinnedApp)};
+
+const SearchEntry = ({ entry }: { entry: Widget.Entry }) => (
+
+ self.hook(entry, "notify::text-length", () =>
+ // Timeout to avoid flickering when replacing entire text (cause it'll set len to 0 then back to > 0)
+ timeout(1, () => (self.shown = entry.textLength > 0 ? "entry" : "placeholder"))
+ )
+ }
+ >
+
+ {entry}
+
+);
+
+const Result = ({
+ icon,
+ materialIcon,
+ label,
+ sublabel,
+ onClicked,
+}: {
+ icon?: string;
+ materialIcon?: string;
+ label: string;
+ sublabel?: string;
+ onClicked: (self: Widget.Button) => void;
+}) => (
+
+);
+
+const SubcommandResult = ({
+ entry,
+ subcommand,
+ args,
+}: {
+ entry: Widget.Entry;
+ subcommand: Subcommand;
+ args: string[];
+}) => (
+ {
+ subcommand.command(...args);
+ entry.set_text("");
+ }}
+ />
+);
+
+const AppResult = ({ app }: { app: AstalApps.Application }) => (
+ launchAndClose(self, app)}
+ />
+);
+
+const MathResult = ({ math, isHistory, entry }: { math: HistoryItem; isHistory?: boolean; entry: Widget.Entry }) => (
+ {
+ if (isHistory) {
+ Math.get_default().select(math);
+ entry.set_text(math.equation);
+ entry.grab_focus();
+ entry.set_position(-1);
+ } else {
+ execAsync(`wl-copy -- ${math.result}`).catch(console.error);
+ entry.set_text("");
+ }
+ }}
+ />
+);
+
+const FileResult = ({ path }: { path: string }) => (
+ openFileAndClose(self, path)}
+ />
+);
+
+const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable }) => {
+ const empty = Variable(true);
+
+ return (
+ (t ? "empty" : "list"))}
+ >
+
+
+
+ {
+ const subcommands: Record = {
+ apps: {
+ icon: "apps",
+ name: "Apps",
+ description: "Search for apps",
+ command: () => mode.set("apps"),
+ },
+ files: {
+ icon: "folder",
+ name: "Files",
+ description: "Search for files",
+ command: () => mode.set("files"),
+ },
+ math: {
+ icon: "calculate",
+ name: "Math",
+ description: "Do math calculations",
+ command: () => mode.set("math"),
+ },
+ todo: {
+ icon: "checklist",
+ name: "Todo",
+ description: "Create a todo in ",
+ command: (...args) => {
+ // TODO: todo service or maybe use external app
+ },
+ },
+ };
+ const subcommandList = Object.keys(subcommands);
+
+ const updateEmpty = () => empty.set(self.get_children().length === 0);
+
+ const appSearch = () => {
+ const apps = Apps.fuzzy_query(entry.text);
+ if (apps.length > config.maxResults) apps.length = config.maxResults;
+ for (const app of apps) self.add();
+ };
+
+ const calculate = () => {
+ if (entry.text) {
+ self.add();
+ self.add();
+ }
+ for (const item of Math.get_default().history)
+ self.add();
+ };
+
+ const fileSearch = () =>
+ execAsync(["fd", ...config.fdOpts, entry.text, HOME])
+ .then(out => {
+ const paths = out.split("\n").filter(path => path);
+ if (paths.length > config.maxResults) paths.length = config.maxResults;
+ self.foreach(ch => ch.destroy());
+ for (const path of paths) self.add();
+ })
+ .catch(e => {
+ // Ignore execAsync error
+ if (!(e instanceof Gio.IOErrorEnum || e instanceof GLib.SpawnError)) console.error(e);
+ })
+ .finally(updateEmpty);
+
+ self.hook(entry, "activate", () => {
+ if (mode.get() === "math") {
+ if (entry.text.startsWith("clear")) Math.get_default().clear();
+ else Math.get_default().commit();
+ }
+ self.get_children()[0]?.activate();
+ });
+ self.hook(entry, "changed", () => {
+ if (!entry.text && mode.get() === "apps") return;
+
+ // Files has delay cause async so it does some stuff by itself
+ const ignoreFileAsync = entry.text.startsWith(">") || mode.get() !== "files";
+ if (ignoreFileAsync) self.foreach(ch => ch.destroy());
+
+ if (entry.text.startsWith(">")) {
+ const args = entry.text.split(" ");
+ for (const { target } of fuzzysort.go(args[0].slice(1), subcommandList, { all: true }))
+ self.add(
+
+ );
+ } else if (mode.get() === "apps") appSearch();
+ else if (mode.get() === "math") calculate();
+ else if (mode.get() === "files") fileSearch();
+
+ if (ignoreFileAsync) updateEmpty();
+ });
+ }}
+ />
+
+ );
+};
+
+const LauncherContent = ({
+ mode,
+ showResults,
+ entry,
+}: {
+ mode: Variable;
+ showResults: Variable;
+ entry: Widget.Entry;
+}) => (
+ `launcher ${m}`)}>
+
+
+
+
+
+ !s)}
+ transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
+ transitionDuration={150}
+ >
+
+
+
+
+
+
+);
+
+@register()
+export default class Launcher extends PopupWindow {
+ readonly mode: Variable;
+
+ constructor() {
+ const entry = () as Widget.Entry;
+ const mode = Variable("apps");
+ const showResults = Variable.derive([bind(entry, "textLength"), mode], (t, m) => t > 0 || m !== "apps");
+
+ super({
+ name: "launcher",
+ anchor: Astal.WindowAnchor.TOP,
+ keymode: Astal.Keymode.EXCLUSIVE,
+ onKeyPressEvent(_, event) {
+ const keyval = event.get_keyval()[1];
+ // Focus entry on typing
+ if (!entry.isFocus && keyval >= 32 && keyval <= 126) {
+ entry.text += String.fromCharCode(keyval);
+ entry.grab_focus();
+ entry.set_position(-1);
+
+ // Consume event, if not consumed it will duplicate character in entry
+ return true;
+ }
+ },
+ child: ,
+ });
+
+ this.mode = mode;
+
+ this.connect("show", () => (this.marginTop = AstalHyprland.get_default().focusedMonitor.height / 4));
+
+ // Clear search on hide if not in math mode
+ this.connect("hide", () => mode.get() !== "math" && entry.set_text(""));
+
+ this.connect("destroy", () => showResults.drop());
+ }
+
+ open(mode: Mode) {
+ this.mode.set(mode);
+ this.show();
+ }
+}
--
cgit v1.2.3-freya