import { Apps as AppsService } from "@/services/apps"; import { getAppCategoryIcon } from "@/utils/icons"; import { launch } from "@/utils/system"; import { FlowBox } from "@/utils/widgets"; import PopupWindow from "@/widgets/popupwindow"; import { bind, execAsync, Gio, register, Variable } from "astal"; import { App, Astal, Gtk, Widget } from "astal/gtk3"; import { launcher as config } from "config"; import type AstalApps from "gi://AstalApps"; type Mode = "apps" | "files" | "math" | "windows"; interface ModeContent { updateContent(search: string): void; handleActivate(search: string): void; } const close = () => App.get_window("launcher")?.hide(); const getModeIcon = (mode: Mode) => { if (mode === "apps") return "apps"; if (mode === "files") return "folder"; if (mode === "math") return "calculate"; if (mode === "windows") return "select_window"; return "search"; }; const getPrettyMode = (mode: Mode) => { if (mode === "apps") return "Apps"; if (mode === "files") return "Files"; if (mode === "math") return "Math"; if (mode === "windows") return "Windows"; return mode; }; const limitLength = (arr: T[], cfg: { maxResults: Variable }) => cfg.maxResults.get() > 0 && arr.length > cfg.maxResults.get() ? arr.slice(0, cfg.maxResults.get()) : arr; const AppResult = ({ app }: { app: AstalApps.Application }) => ( ); const FileResult = ({ path }: { path: string }) => ( ); @register() class Apps extends Widget.Box implements ModeContent { #content: FlowBox; constructor() { super({ name: "apps", className: "apps" }); this.#content = () as FlowBox; this.add( {this.#content} ); } updateContent(search: string): void { this.#content.foreach(c => c.destroy()); for (const app of limitLength(AppsService.fuzzy_query(search), config.apps)) this.#content.add(); } handleActivate(): void { this.#content.get_child_at_index(0)?.get_child()?.grab_focus(); this.#content.get_child_at_index(0)?.get_child()?.activate(); } } @register() class Files extends Widget.Box implements ModeContent { #content: FlowBox; constructor() { super({ name: "files", className: "files" }); this.#content = () as FlowBox; this.add( {this.#content} ); } updateContent(search: string): void { execAsync(["fd", ...config.files.fdOpts.get(), search, HOME]) .then(out => { this.#content.foreach(c => c.destroy()); const paths = out.split("\n").filter(path => path); for (const path of limitLength(paths, config.files)) this.#content.add(); }) .catch(() => {}); // Ignore errors } handleActivate(): void { this.#content.get_child_at_index(0)?.get_child()?.grab_focus(); this.#content.get_child_at_index(0)?.get_child()?.activate(); } } @register() class Math extends Widget.Box implements ModeContent { constructor() { super({ name: "math", className: "math" }); } updateContent(search: string): void { throw new Error("Method not implemented."); } handleActivate(search: string): void { throw new Error("Method not implemented."); } } @register() class Windows extends Widget.Box implements ModeContent { constructor() { super({ name: "windows", className: "windows" }); } updateContent(search: string): void { throw new Error("Method not implemented."); } handleActivate(search: string): void { throw new Error("Method not implemented."); } } const SearchBar = ({ mode, entry }: { mode: Variable; entry: Widget.Entry }) => ( ); const ModeSwitcher = ({ mode, modes }: { mode: Variable; modes: Mode[] }) => ( {modes.map(m => ( ))} ); @register() export default class Launcher extends PopupWindow { readonly mode: Variable; constructor() { const entry = () as Widget.Entry; const mode = Variable("apps"); const content = { apps: new Apps(), files: new Files(), math: new Math(), windows: new Windows(), }; super({ name: "launcher", anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT, keymode: Astal.Keymode.EXCLUSIVE, borderWidth: 0, 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: ( `launcher ${m}`)} > {Object.values(content)} ), }); this.mode = mode; content[mode.get()].updateContent(entry.get_text()); this.hook(mode, (_, v: Mode) => { entry.set_text(""); content[v].updateContent(entry.get_text()); }); this.hook(entry, "changed", () => content[mode.get()].updateContent(entry.get_text())); this.hook(entry, "activate", () => content[mode.get()].handleActivate(entry.get_text())); // Clear search on hide if not in math mode or creating a todo this.connect("hide", () => mode.get() !== "math" && !entry.text.startsWith(">todo") && entry.set_text("")); } open(mode: Mode) { this.mode.set(mode); this.show(); } }