chore(runner): try a new approach to show results, make plugins return properties instead of the actual widget

returning the results as objects seems to be a better approach, also, the new way of showing results doesn't work for some reason(i didn't discover it yet lol)
This commit is contained in:
retrozinndev
2025-07-23 20:41:23 -03:00
parent 69b098b6bd
commit 467de2235a
9 changed files with 278 additions and 242 deletions
+88 -117
View File
@@ -1,9 +1,10 @@
import { Astal, Gdk, Gtk } from "ags/gtk4"; import { Astal, Gdk, Gtk } from "ags/gtk4";
import { timeout } from "ags/time";
import { PopupWindow } from "../widget/PopupWindow"; import { PopupWindow } from "../widget/PopupWindow";
import { updateApps } from "../scripts/apps"; import { updateApps } from "../scripts/apps";
import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget"; import { ResultWidget, ResultWidgetProps } from "./widgets/ResultWidget";
import { Windows } from "../windows"; import { Windows } from "../windows";
import { createState, For } from "ags";
import { timeout } from "ags/time";
import GObject from "ags/gobject"; import GObject from "ags/gobject";
import AstalHyprland from "gi://AstalHyprland"; import AstalHyprland from "gi://AstalHyprland";
@@ -22,18 +23,21 @@ export type RunnerProps = {
showResultsPlaceHolderOnStartup?: boolean; showResultsPlaceHolderOnStartup?: boolean;
}; };
type Result = ResultWidgetProps;
export interface Plugin { export interface Plugin {
/** prefix to call the plugin. if undefined, will be triggered like applications plugin */ /** prefix to call the plugin. if undefined, will be triggered like applications plugin */
readonly prefix?: string; readonly prefix?: string;
/** name of the plugin. e.g.: websearch, shell */ /** name of the plugin. e.g.: websearch, shell */
readonly name?: string; readonly name?: string;
/** ran on runner open */ /** runs when runner opens */
readonly init?: () => void; readonly init?: () => void;
/** handle the user input to return results (does not include plugin's prefix) */ /** handle the user input to return results (does not include plugin's prefix) */
readonly handle: (inputText: string) => (ResultWidget|Array<ResultWidget>|null|undefined); readonly handle: (inputText: string) => (Result|Array<Result>|null|undefined);
/** ran on runner close */ /** runs when runner closes */
readonly onClose?: () => void; readonly onClose?: () => void;
/** hide other plugins when using this plugin */ /** prioritize this plugin's results over other results.
* (hides other results that aren't from this plugin on list) */
prioritize?: boolean; prioritize?: boolean;
} }
@@ -88,105 +92,61 @@ export function openDefault(initialText?: string) {
entryPlaceHolder: "Start typing...", entryPlaceHolder: "Start typing...",
initialText, initialText,
resultsLimit: 24 resultsLimit: 24
} as Runner.RunnerProps, } as Runner.RunnerProps, [
() => [ {
new ResultWidget({
icon: "application-x-executable-symbolic", icon: "application-x-executable-symbolic",
title: "Use your applications", title: "Use your applications",
description: "Search for any app installed in your computer", description: "Search for any app installed in your computer",
closeOnClick: false, closeOnClick: false,
onClick: () => gtkEntry?.grab_focus() actionClick: () => gtkEntry?.grab_focus()
} as ResultWidgetProps), },
new ResultWidget({ {
icon: "edit-paste-symbolic", icon: "edit-paste-symbolic",
title: "See your clipboard history", title: "See your clipboard history",
description: "Start your search with '>' to go through your clipboard history", description: "Start your search with '>' to go through your clipboard history",
closeOnClick: false, closeOnClick: false,
onClick: () => setEntryText('>') actionClick: () => setEntryText('>')
} as ResultWidgetProps), },
new ResultWidget({ {
icon: "image-x-generic-symbolic", icon: "image-x-generic-symbolic",
title: "Change your wallpaper", title: "Change your wallpaper",
description: "Add '#' at the start to search through the wallpapers folder!", description: "Add '#' at the start to search through the wallpapers folder!",
closeOnClick: false, closeOnClick: false,
onClick: () => setEntryText('#'), actionClick: () => setEntryText('#'),
} as ResultWidgetProps), },
new ResultWidget({ {
icon: "utilities-terminal-symbolic", icon: "utilities-terminal-symbolic",
title: "Run shell commands", title: "Run shell commands",
description: "Add '!' before your command to run it (pro tip: add a second '!' to show command output)", description: "Add '!' before your command to run it (pro tip: add a second '!' to show command output)",
closeOnClick: false, closeOnClick: false,
onClick: () => setEntryText('!!') actionClick: () => setEntryText('!!')
} as ResultWidgetProps), },
new ResultWidget({ {
icon: "media-playback-start-symbolic", icon: "media-playback-start-symbolic",
title: "Control media", title: "Control media",
description: "Type ':' to control playing media", description: "Type ':' to control playing media",
closeOnClick: false, closeOnClick: false,
onClick: () => setEntryText(':') actionClick: () => setEntryText(':')
} as ResultWidgetProps), },
new ResultWidget({ {
icon: "applications-internet-symbolic", icon: "applications-internet-symbolic",
title: "Search the Web", title: "Search the Web",
description: "Start typing with '?' prefix to search the web", description: "Start typing with '?' prefix to search the web",
closeOnClick: false, closeOnClick: false,
onClick: () => setEntryText('?') actionClick: () => setEntryText('?')
} as ResultWidgetProps) }
]); ]);
} }
export function openRunner(props: RunnerProps, placeholder?: () => Array<ResultWidget>): Astal.Window { export function openRunner(props: RunnerProps, placeholder?: Array<Result>): Astal.Window {
let onClickTimeout: (AstalIO.Time|undefined);
const connections: Map<GObject.Object, number> = new Map();
props.width ??= 780; props.width ??= 780;
props.height ??= 420; props.height ??= 420;
gtkEntry = <Gtk.SearchEntry class={"search"} placeholderText={props.entryPlaceHolder ?? ""} const connections: Map<GObject.Object, number> = new Map();
onSearchChanged={async (self) => { const [results, setResults] = createState([] as Array<Result>);
updateResultsList(self.text); let clickTimeout: AstalIO.Time|undefined;
resultsList.get_row_at_index(0) &&
resultsList.select_row(resultsList.get_row_at_index(0));
if(self.text.trim().length < 1 && !mainBox.get_style_context().has_class("empty-input")) { function getPluginResults(input: string): Array<Result> {
mainBox.get_style_context().add_class("empty-input");
return;
}
mainBox.get_style_context().has_class("empty-input") &&
mainBox.get_style_context().remove_class("empty-input");
}} onActivate={() => {
const resultWidget = resultsList.get_selected_row()?.get_child();
if(resultWidget instanceof ResultWidget) {
resultWidget.onClick();
resultWidget.closeOnClick && Runner.close();
}
}}
/> as Gtk.SearchEntry;
const mainBox = <Gtk.Box class={`runner main ${props.showResultsPlaceHolderOnStartup ?
"empty" : ""}`} orientation={Gtk.Orientation.VERTICAL} hexpand={true}
valign={Gtk.Align.START}>
{gtkEntry}
<Gtk.ScrolledWindow class={"results-scrollable"} vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
hscrollbarPolicy={Gtk.PolicyType.NEVER} hexpand={true} vexpand={true}
visible={props.showResultsPlaceHolderOnStartup ?? false}
propagateNaturalHeight={true} maxContentHeight={props.height}>
<Gtk.ListBox hexpand={true} vexpand={true} />
</Gtk.ScrolledWindow>
</Gtk.Box> as Gtk.Box;
const scrollable = mainBox.get_last_child() as Gtk.ScrolledWindow;
const resultsList = scrollable.get_first_child() as Gtk.ListBox;
if(props?.showResultsPlaceHolderOnStartup && placeholder) {
const placeholderGtks = placeholder();
placeholderGtks.map(widget =>
resultsList.insert(widget, -1));
}
function getPluginResults(input: string): Array<ResultWidget> {
let calledPlugins: Array<Plugin> = getPlugins().filter((plugin) => let calledPlugins: Array<Plugin> = getPlugins().filter((plugin) =>
plugin.prefix ? (input.startsWith(plugin.prefix) ? true : false) : true plugin.prefix ? (input.startsWith(plugin.prefix) ? true : false) : true
).sort((plugin) => plugin.prefix != null ? 0 : 1); ).sort((plugin) => plugin.prefix != null ? 0 : 1);
@@ -208,53 +168,26 @@ export function openRunner(props: RunnerProps, placeholder?: () => Array<ResultW
: results; : results;
} }
function updateResultsList(entryText: string) { function updateResultsList(input: string) {
const widgets: Array<ResultWidget> = []; const newResults: Array<Result> = [];
// Remove all previous results
resultsList.remove_all();
widgets.push(...getPluginResults(entryText))
// Insert placeholder if there are no results // Insert placeholder if there are no results
if(placeholder && widgets.length === 0) if(placeholder && results.get().length === 0)
widgets.push(...placeholder()); newResults.push(...placeholder);
// Insert results inside GtkListBox getPluginResults(input).forEach((result) => {
widgets.map((resultGtk: ResultWidget) => { newResults.unshift(result);
resultsList.insert(resultGtk, -1);
const conns: Array<number> = [];
conns.push(
resultsList.connect("row-activated", (_, row: Gtk.ListBoxRow) => {
const rGtk = row.get_child();
if(rGtk instanceof ResultWidget) {
if(onClickTimeout) return;
// Timeout, so it doesn't fire the event a hundred times :skull:
onClickTimeout = timeout(500, () => onClickTimeout = undefined);
rGtk.onClick();
rGtk.closeOnClick && Runner.close();
}
}),
resultsList.connect("destroy", () =>
conns.forEach((id) => resultsList.disconnect(id))
)
);
}); });
widgets.length > 0 ? setResults(newResults);
(!scrollable.visible && scrollable.show())
: scrollable.hide();
} }
if(!instance) if(!instance)
instance = Windows.getDefault().createWindowForFocusedMonitor((mon: number) => <PopupWindow instance = Windows.getDefault().createWindowForFocusedMonitor((mon: number) =>
namespace={"runner"} monitor={mon} widthRequest={props.width} heightRequest={props.height} <PopupWindow namespace={"runner"} monitor={mon} widthRequest={props.width}
heightRequest={props.height} exclusivity={Astal.Exclusivity.IGNORE} halign={Gtk.Align.CENTER}
marginTop={(AstalHyprland.get_default().get_monitor(mon)?.height / 2) - (props.height! / 2)} marginTop={(AstalHyprland.get_default().get_monitor(mon)?.height / 2) - (props.height! / 2)}
exclusivity={Astal.Exclusivity.IGNORE} halign={Gtk.Align.CENTER} valign={Gtk.Align.START} valign={Gtk.Align.START} $={() => {
$={() => {
plugins.forEach(plugin => plugins.forEach(plugin =>
plugin.init?.()); plugin.init?.());
@@ -270,19 +203,57 @@ export function openRunner(props: RunnerProps, placeholder?: () => Array<ResultW
keyval === Gdk.KEY_F5 && keyval === Gdk.KEY_F5 &&
updateApps(); updateApps();
}} onDestroy={() => { }} onCloseRequest={() => {
connections.forEach((id, obj) => GObject.signal_handler_is_connected(obj, id) && connections.forEach((id, obj) => GObject.signal_handler_is_connected(obj, id) &&
obj.disconnect(id)); obj.disconnect(id));
gtkEntry = null; gtkEntry = null;
[...plugins.values()].forEach(plugin => [...plugins.values()].forEach(plugin => plugin?.onClose?.());
plugin && plugin.onClose && plugin.onClose());
instance = null; instance = null;
}}> }}>
{mainBox} <Gtk.Box class={`runner main ${props.showResultsPlaceHolderOnStartup ?
</PopupWindow> as Astal.Window)(); "empty" : ""}`} orientation={Gtk.Orientation.VERTICAL} hexpand
valign={Gtk.Align.START} visible>
<Gtk.SearchEntry class={"search"} placeholderText={props.entryPlaceHolder ?? ""}
$={(self) => gtkEntry = self}
onSearchChanged={(self) => {
updateResultsList(self.text);
const listbox = self.parent.get_last_child()?.get_first_child()?.get_first_child() as Gtk.ListBox;
listbox.get_row_at_index(0) &&
listbox.select_row(listbox.get_row_at_index(0));
}} onActivate={(self) => {
const listbox = self.parent.get_last_child()?.get_first_child()?.get_first_child() as Gtk.ListBox;
const resultWidget = listbox.get_selected_row()?.get_child();
if(resultWidget instanceof ResultWidget) {
resultWidget.actionClick();
resultWidget.closeOnClick && Runner.close();
}
}}
/>
<Gtk.ScrolledWindow class={"results-scrollable"} vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
hscrollbarPolicy={Gtk.PolicyType.NEVER} hexpand vexpand propagateNaturalHeight
visible={props.showResultsPlaceHolderOnStartup ?? false}
maxContentHeight={props.height}>
<Gtk.ListBox hexpand vexpand visible onRowActivated={(_, row) => {
if(row instanceof ResultWidget && !clickTimeout) {
clickTimeout = timeout(250, () => clickTimeout = undefined);
row.actionClick?.();
}
}}>
<For each={results}>
{(res: Result) => <ResultWidget {...res} visible />}
</For>
</Gtk.ListBox>
</Gtk.ScrolledWindow>
</Gtk.Box>
</PopupWindow> as Astal.Window
)();
return instance!; return instance!;
} }
+3 -6
View File
@@ -1,5 +1,3 @@
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import AstalApps from "gi://AstalApps";
import { execApp, getAstalApps, lookupIcon, updateApps } from "../../scripts/apps"; import { execApp, getAstalApps, lookupIcon, updateApps } from "../../scripts/apps";
import { Runner } from "../Runner"; import { Runner } from "../Runner";
@@ -9,13 +7,12 @@ export const PluginApps = {
// asynchronously-refresh apps list on init // asynchronously-refresh apps list on init
init: async () => updateApps(), init: async () => updateApps(),
handle: (text: string) => { handle: (text: string) => {
return getAstalApps().fuzzy_query(text).map((app: AstalApps.Application) => return getAstalApps().fuzzy_query(text).map(app => ({
new ResultWidget({
title: app.get_name(), title: app.get_name(),
description: app.get_description(), description: app.get_description(),
icon: lookupIcon(app.iconName) ? app.iconName : "application-x-executable-symbolic", icon: lookupIcon(app.iconName) ? app.iconName : "application-x-executable-symbolic",
onClick: () => execApp(app) actionClick: () => execApp(app)
} as ResultWidgetProps) })
); );
} }
} as Runner.Plugin; } as Runner.Plugin;
+33
View File
@@ -0,0 +1,33 @@
import { Gtk } from "ags/gtk4";
import { Clipboard } from "../../scripts/clipboard";
import { Runner } from "../Runner";
import { jsx } from "ags/gtk4/jsx-runtime";
export const PluginClipboard = {
prefix: '>',
prioritize: true,
handle: (search) => {
if(Clipboard.getDefault().history.length < 1)
return {
icon: "edit-paste-symbolic",
title: "Clipboard is empty",
description: "Copy something and it will be shown right here!"
};
return Clipboard.getDefault().history.filter(item =>
// not the best way to search, but it works
Runner.regExMatch(search, item.id) || Runner.regExMatch(search, item.preview)).map((item) => ({
icon: jsx(Gtk.Label, {
label: `${item.id}`,
css: "font-size: 16px; margin-right: 8px; font-weight: 600;"
}),
title: item.preview,
actionClick: () => Clipboard.getDefault().selectItem(item).catch((err: Error) => {
console.error(`Runner(Plugin/Clipboard): An error occurred while selecting clipboard item. Stderr:\n${
err.message ? `${err.message}\n` : ""}Stack: ${err.stack}`
);
})
}));
}
} as Runner.Plugin;
-28
View File
@@ -1,28 +0,0 @@
import { Gtk } from "ags/gtk4";
import { Clipboard } from "../../scripts/clipboard";
import { ResultWidget } from "../../widget/runner/ResultWidget";
import { Runner } from "../Runner";
export const PluginClipboard = {
prefix: '>',
prioritize: true,
handle: (search) => {
if(Clipboard.getDefault().history.length < 1)
return <ResultWidget icon={"edit-paste-symbolic"} title={"Clipboard is empty"}
description={"Copy something and it will be shown right here!"}
/>;
return Clipboard.getDefault().history.filter(item => // not the best way to search, but it works
Runner.regExMatch(search, item.id) || Runner.regExMatch(search, item.preview)).map((item) =>
<ResultWidget icon={<Gtk.Label label={`${item.id}`}
css={"font-size: 16px; margin-right: 8px; font-weight: 600;"} />}
title={item.preview} onClick={() => Clipboard.getDefault().selectItem(item).catch((err: Error) => {
console.error(`Runner(Plugin/Clipboard): An error occurred while selecting clipboard item. Stderr:\n${
err.message ? `${err.message}\n` : ""}Stack: ${err.stack}`
);
})
}
/>);
}
} as Runner.Plugin;
+14 -17
View File
@@ -1,21 +1,19 @@
import { createBinding, createComputed } from "ags"; import { createBinding, createComputed } from "ags";
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import { Runner } from "../Runner"; import { Runner } from "../Runner";
import AstalMpris from "gi://AstalMpris";
import { player } from "../../widget/bar/Media"; import { player } from "../../widget/bar/Media";
import AstalMpris from "gi://AstalMpris";
export const PluginMedia = { export const PluginMedia = {
prefix: ":", prefix: ":",
handle() { handle: () => !player.get().available ? {
if(!player.get().available) return new ResultWidget({
icon: "folder-music-symbolic", icon: "folder-music-symbolic",
title: "Couldn't find any players", title: "Couldn't find any players",
closeOnClick: false, closeOnClick: false,
description: "No media / player found with mpris" description: "No media / player found with mpris"
} as ResultWidgetProps); } : [
{
return [
new ResultWidget({
icon: createBinding(player.get(), "playbackStatus").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ? icon: createBinding(player.get(), "playbackStatus").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ?
"media-playback-pause-symbolic" "media-playback-pause-symbolic"
: "media-playback-start-symbolic"), : "media-playback-start-symbolic"),
@@ -27,9 +25,9 @@ export const PluginMedia = {
], (title, artist, status) => `${ status === AstalMpris.PlaybackStatus.PLAYING ? ], (title, artist, status) => `${ status === AstalMpris.PlaybackStatus.PLAYING ?
"Pause" : "Play" "Pause" : "Play"
} ${title} | ${artist}`), } ${title} | ${artist}`),
onClick: () => player.get().play_pause() actionClick: () => player.get().play_pause()
} as ResultWidgetProps), },
new ResultWidget({ {
icon: "media-skip-backward-symbolic", icon: "media-skip-backward-symbolic",
closeOnClick: false, closeOnClick: false,
title: createComputed([ title: createComputed([
@@ -38,9 +36,9 @@ export const PluginMedia = {
], (title, artist) => ], (title, artist) =>
`Go Previous ${ title ? title : player.get().busName }${ artist ? ` | ${artist}` : "" }` `Go Previous ${ title ? title : player.get().busName }${ artist ? ` | ${artist}` : "" }`
), ),
onClick: () => player.get().canGoPrevious && player.get().previous() actionClick: () => player.get().canGoPrevious && player.get().previous()
} as ResultWidgetProps), },
new ResultWidget({ {
icon: "media-skip-forward-symbolic", icon: "media-skip-forward-symbolic",
closeOnClick: false, closeOnClick: false,
title: createComputed([ title: createComputed([
@@ -49,8 +47,7 @@ export const PluginMedia = {
], (title, artist) => ], (title, artist) =>
`Go Next ${ title ? title : player.get().busName }${ artist ? ` | ${artist}` : "" }` `Go Next ${ title ? title : player.get().busName }${ artist ? ` | ${artist}` : "" }`
), ),
onClick: () => player.get().canGoNext && player.get().next() actionClick: () => player.get().canGoNext && player.get().next()
} as ResultWidgetProps) }
] ]
},
} as Runner.Plugin; } as Runner.Plugin;
+4 -5
View File
@@ -1,4 +1,3 @@
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import { Runner } from "../Runner"; import { Runner } from "../Runner";
import { Notifications } from "../../scripts/notifications"; import { Notifications } from "../../scripts/notifications";
@@ -17,7 +16,7 @@ export const PluginShell = (() => {
return { return {
prefix: '!', prefix: '!',
prioritize: true, prioritize: true,
handle: (input: string): ResultWidget => { handle: (input) => {
let showOutputNotif: boolean = false; let showOutputNotif: boolean = false;
if(input.startsWith('!')) { if(input.startsWith('!')) {
input = input.replace('!', ""); input = input.replace('!', "");
@@ -26,8 +25,8 @@ export const PluginShell = (() => {
const command = input ? GLib.shell_parse_argv(input) : undefined; const command = input ? GLib.shell_parse_argv(input) : undefined;
return new ResultWidget({ return {
onClick: () => { actionClick: () => {
if(!command || !command[0]) return; if(!command || !command[0]) return;
const proc = procLauncher.spawnv([ shell, "-c", `${input}` ]); const proc = procLauncher.spawnv([ shell, "-c", `${input}` ]);
@@ -56,7 +55,7 @@ export const PluginShell = (() => {
title: `Run ${input ? ` \`${input}\`` : `with ${shell.split('/')[shell.split('/').length-1]}`}`, title: `Run ${input ? ` \`${input}\`` : `with ${shell.split('/')[shell.split('/').length-1]}`}`,
description: (input || showOutputNotif) && `${input ? `${shell}\t` : ""}${ showOutputNotif ? "(showing output on notification)" : "" }`, description: (input || showOutputNotif) && `${input ? `${shell}\t` : ""}${ showOutputNotif ? "(showing output on notification)" : "" }`,
icon: "utilities-terminal-symbolic" icon: "utilities-terminal-symbolic"
} as ResultWidgetProps) };
} }
} as Runner.Plugin } as Runner.Plugin
})(); })();
+7 -7
View File
@@ -1,6 +1,5 @@
import { Wallpaper } from "../../scripts/wallpaper"; import { Wallpaper } from "../../scripts/wallpaper";
import { Runner } from "../Runner"; import { Runner } from "../Runner";
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import Gio from "gi://Gio?version=2.0"; import Gio from "gi://Gio?version=2.0";
@@ -26,18 +25,19 @@ class _PluginWallpapers implements Runner.Plugin {
handle(search: string) { handle(search: string) {
if(this.#files!.length > 0) if(this.#files!.length > 0)
return this.#files!.filter(file => // not the best way to search, but it works return this.#files!.filter(file =>
// also not the best way to search, but it works
Runner.regExMatch(search, file.split('/')[file.split('/').length-1]) Runner.regExMatch(search, file.split('/')[file.split('/').length-1])
).map(path => new ResultWidget({ ).map(path => ({
title: path.split('/')[path.split('/').length-1].replace(/\..*$/, ""), title: path.split('/')[path.split('/').length-1].replace(/\..*$/, ""),
onClick: () => Wallpaper.getDefault().setWallpaper(path) actionClick: () => Wallpaper.getDefault().setWallpaper(path)
} as ResultWidgetProps)); }));
return new ResultWidget({ return {
title: "No wallpapers found!", title: "No wallpapers found!",
description: "Define the $WALLPAPERS variable on Hyprland or create a ~/wallpapers directory", description: "Define the $WALLPAPERS variable on Hyprland or create a ~/wallpapers directory",
icon: "image-missing-symbolic" icon: "image-missing-symbolic"
} as ResultWidgetProps); };
} }
} }
+4 -6
View File
@@ -1,7 +1,7 @@
import AstalHyprland from "gi://AstalHyprland"; import AstalHyprland from "gi://AstalHyprland";
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import { Runner } from "../Runner"; import { Runner } from "../Runner";
const searchEngines = { const searchEngines = {
duckduckgo: "https://duckduckgo.com/?q=", duckduckgo: "https://duckduckgo.com/?q=",
google: "https://google.com/search?q=", google: "https://google.com/search?q=",
@@ -15,15 +15,13 @@ export const PluginWebSearch = {
name: "Web Search", name: "Web Search",
prioritize: true, prioritize: true,
handle: (search: string): ResultWidget => { handle: (search) => ({
return new ResultWidget({
icon: "system-search-symbolic", icon: "system-search-symbolic",
title: search || "Type your search...", title: search || "Type your search...",
description: `Search the Web`, description: `Search the Web`,
onClick: () => AstalHyprland.get_default().dispatch( actionClick: () => AstalHyprland.get_default().dispatch(
"exec", "exec",
`xdg-open \"${engine + search}\"` `xdg-open \"${engine + search}\"`
) )
} as ResultWidgetProps); })
}
} as Runner.Plugin; } as Runner.Plugin;
+69
View File
@@ -0,0 +1,69 @@
import { Accessor, With } from "ags";
import { register } from "ags/gobject";
import { Gtk } from "ags/gtk4";
import Pango from "gi://Pango?version=1.0";
import { variableToBoolean } from "../../scripts/utils";
export { ResultWidget, ResultWidgetProps };
type ResultWidgetProps = {
icon?: string | Accessor<string> | JSX.Element | Accessor<JSX.Element>;
title: string | Accessor<string>;
description?: string | Accessor<string>;
closeOnClick?: boolean;
setup?: () => void;
actionClick?: () => void;
visible?: boolean;
};
@register({ GTypeName: "ResultWidget" })
class ResultWidget extends Gtk.Box {
public readonly actionClick: () => void;
public readonly setup?: () => void;
public icon?: (string | Accessor<string> | JSX.Element | Accessor<JSX.Element>);
public closeOnClick: boolean = true;
constructor(props: ResultWidgetProps) {
super();
this.add_css_class("result");
this.visible = props.visible ?? true;
this.hexpand = true;
this.icon = props.icon;
this.setup = props.setup;
this.closeOnClick = props.closeOnClick ?? true;
this.actionClick = () => props.actionClick?.();
if(this.icon !== undefined) {
if(this.icon instanceof Accessor) {
if(typeof this.icon.get() === "string") {
this.prepend(<Gtk.Image iconName={
this.icon as Accessor<string>
} /> as Gtk.Image);
} else {
this.prepend(<Gtk.Box>
<With value={this.icon as Accessor<Gtk.Widget>}>
{(widget) => widget}
</With>
</Gtk.Box> as Gtk.Box);
}
} else {
if(typeof this.icon === "string")
this.prepend(<Gtk.Image iconName={this.icon as string} /> as Gtk.Image);
else
this.prepend(this.icon as Gtk.Widget);
}
}
this.append(<Gtk.Box orientation={Gtk.Orientation.VERTICAL} valign={Gtk.Align.CENTER}>
<Gtk.Label class={"title"} xalign={0} ellipsize={Pango.EllipsizeMode.END}
label={props.title} />
<Gtk.Label class={"description"} visible={variableToBoolean(props.description)}
ellipsize={Pango.EllipsizeMode.END} xalign={0} label={props.description ?? ""} />
</Gtk.Box> as Gtk.Box);
}
}