perf(ags/runner): better code for runner, show results from multiple plugins, use interface to make plugins and more perfomance implementations

This commit is contained in:
retrozinndev
2025-03-12 14:11:42 -03:00
parent 0ce13b47be
commit f0cec3ff84
7 changed files with 244 additions and 220 deletions
+14
View File
@@ -9,8 +9,19 @@ import { Time, timeout } from "astal/time";
import { OSD, OSDModes, setOSDMode } from "./window/OSD";
import { ControlCenter } from "./window/ControlCenter";
import { Runner } from "./window/Runner";
import { PluginApps } from "./scripts/runner/apps";
import { PluginShell } from "./scripts/runner/shell";
import { PluginWebSearch } from "./scripts/runner/websearch";
let osdTimer: (Time|undefined);
const runnerPlugins: Array<Runner.Plugin> = [
new PluginApps(),
new PluginShell(),
new PluginWebSearch()
];
App.start({
instanceName: "astal",
requestHandler(request: string, response: (result: any) => void) {
@@ -26,6 +37,9 @@ App.start({
Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () =>
!Windows.isVisible(ControlCenter) && triggerOSD(OSDModes.SINK));
console.log(`[LOG] Adding runner plugins`);
runnerPlugins.map(plugin => Runner.addPlugin(plugin));
}
});
+8 -2
View File
@@ -2,9 +2,14 @@ import AstalHyprland from "gi://AstalHyprland";
import { getAstalApps } from "../apps";
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import AstalApps from "gi://AstalApps";
import { Runner } from "../../window/Runner";
export function handleApplications(search: string): (Array<ResultWidget>|null) {
return getAstalApps().fuzzy_query(search).map((app: AstalApps.Application) =>
export class PluginApps implements Runner.Plugin {
// Do not provide prefix, so it's always ran.
public readonly name = "Apps";
public handle(text: string) {
return getAstalApps().fuzzy_query(text).map((app: AstalApps.Application) =>
new ResultWidget({
title: app.get_name(),
description: app.get_description(),
@@ -13,3 +18,4 @@ export function handleApplications(search: string): (Array<ResultWidget>|null) {
} as ResultWidgetProps)
) || null;
}
}
+9 -5
View File
@@ -1,14 +1,18 @@
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import AstalHyprland from "gi://AstalHyprland";
import { GLib } from "astal";
import { Runner } from "../../window/Runner";
export function handleShell(command: string): ResultWidget {
const userShell = GLib.getenv("SHELL") || "/usr/bin/env bash";
export class PluginShell implements Runner.Plugin {
public readonly prefix = '!';
#shell = GLib.getenv("SHELL") || "/usr/bin/env sh";
public handle(command: string): ResultWidget {
return new ResultWidget({
onClick: () => AstalHyprland.get_default().dispatch("exec", `${userShell} -c "${command}"`),
onClick: () => AstalHyprland.get_default().dispatch("exec", `${this.#shell} -c "${command}"`),
title: `Run: \`${command}\``,
description: userShell,
description: this.#shell,
icon: "utilities-terminal-symbolic"
} as ResultWidgetProps);
} as ResultWidgetProps)
}
}
+27 -29
View File
@@ -1,42 +1,40 @@
import AstalHyprland from "gi://AstalHyprland";
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import { Runner } from "../../window/Runner";
export enum SearchEngine {
GOOGLE,
DUCKDUCKGO,
YAHOO
const searchEngines = {
duckduckgo: "https://duckduckgo.com/?q=",
google: "https://google.com/search?q=",
yahoo: "https://search.yahoo.com/search?p="
};
let engine: string = searchEngines.google;
export class PluginWebSearch implements Runner.Plugin {
#engineString: string;
public readonly prefix = '?';
public readonly name = "Web Search";
constructor() {
switch(engine) {
case searchEngines.google:
this.#engineString = "Google";
case searchEngines.yahoo:
this.#engineString = "Yahoo";
case searchEngines.duckduckgo:
this.#engineString = "DuckDuckGo";
default: this.#engineString = "Web";
}
export const SearchEngineMap: Map<SearchEngine, string> = new Map([
[ SearchEngine.DUCKDUCKGO, "https://duckduckgo.com/?q=" ],
[ SearchEngine.GOOGLE, "https://google.com/search?q=" ],
[ SearchEngine.YAHOO, "https://search.yahoo.com/search?p=" ]
]);
let searchEngine: SearchEngine = SearchEngine.GOOGLE;
export function handleWebSearch(search: string): ResultWidget {
let engineString: string;
switch(searchEngine as SearchEngine) {
case SearchEngine.GOOGLE:
engineString = "Google";
case SearchEngine.YAHOO:
engineString = "Yahoo";
case SearchEngine.DUCKDUCKGO:
engineString = "DuckDuckGo";
default: engineString = "Web";
}
public handle(search: string): ResultWidget {
return new ResultWidget({
icon: "system-search-symbolic",
title: search || "",
description: `Search with ${engineString}`,
description: `Search with ${this.#engineString}`,
onClick: () => AstalHyprland.get_default().dispatch(
"exec",
`xdg-open "${SearchEngineMap.get(searchEngine)! + search.replaceAll(" ", "%20")}"`
`xdg-open \"${engine + search}\"`
)
} as ResultWidgetProps);
}
}
+11 -3
View File
@@ -21,7 +21,14 @@ export function NotificationWidget(notification: AstalNotifd.Notification|number
notification
: AstalNotifd.get_default().get_notification(notification);
return new Widget.Box({
return new Widget.EventBox({
onClick: () => {
if(notification.actions.length >= 1) {
notification.invoke(notification.actions[0]!.id);
onClose && onClose(notification);
}
},
child: new Widget.Box({
className: `notification ${getUrgencyString(notification)}`,
homogeneous: false,
expand: false,
@@ -105,7 +112,7 @@ export function NotificationWidget(notification: AstalNotifd.Notification|number
new Widget.Box({
className: "actions button-row",
hexpand: true,
visible: notification.actions.length > 0,
visible: notification.actions.length > 1,
children: notification.actions.map((action: AstalNotifd.Action) =>
new Widget.Button({
className: "action",
@@ -119,5 +126,6 @@ export function NotificationWidget(notification: AstalNotifd.Notification|number
)
} as Widget.BoxProps)
]
} as Widget.BoxProps)
} as Widget.BoxProps),
} as Widget.EventBoxProps);
}
+2 -2
View File
@@ -18,10 +18,10 @@ export const ControlCenter: Widget.Window = PopupWindow({
halign: Gtk.Align.END,
valign: Gtk.Align.START,
visible: false,
vexpand: true,
expand: true,
child: new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
vexpand: true,
expand: true,
children: [
new Widget.Box({
className: "control-center-container",
+47 -53
View File
@@ -2,9 +2,6 @@ import { AstalIO, timeout, Variable } from "astal";
import { Gdk, Gtk, Widget } from "astal/gtk3";
import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow";
import { updateApps } from "../scripts/apps";
import { handleShell } from "../scripts/runner/shell";
import { handleWebSearch } from "../scripts/runner/websearch";
import { handleApplications } from "../scripts/runner/apps";
import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget";
export let runnerInstance: (Widget.Window|null) = null;
@@ -50,26 +47,48 @@ export namespace Runner {
resultsPlaceholder?: () => Array<ResultWidget>;
};
export const prefixes = new Map<string, (entry: string) => (ResultWidget|Array<ResultWidget>|null)>([
[ "!", handleShell ],
[ "?", handleWebSearch ],
]);
const plugins = new Set<Runner.Plugin>([]);
export interface Plugin {
/** prefix to call the plugin. if undefined, will be triggered like applications plugin */
readonly prefix?: string;
/** name of the plugin. e.g.: websearch, shell */
readonly name?: string;
/** handle the user input to return results (does not contain prefix) */
readonly handle: (inputText: string) => (ResultWidget|Array<ResultWidget>|null|undefined);
}
export function addPlugin(plugin: Runner.Plugin, force?: boolean) {
if(!force && plugin.prefix && plugins.has(plugin))
throw new Error(`Runner plugin with prefix ${plugin.prefix} already exists`);
plugins.add(plugin);
}
export function getPlugins(): Array<Runner.Plugin> {
return [...plugins.values()];
}
/** Removes a plugin from the runner plugin list
* @returns true if plugin was removed or false if plugin wasn't found
*/
export function removePlugin(plugin: Plugin): boolean {
return plugins.delete(plugin);
}
export function RunnerWindow(props?: RunnerProps): (Widget.Window|null) {
let subs: Array<() => void> = [];
const entryText: Variable<string> = new Variable<string>("");
let results: (Array<ResultWidget>|null) = props?.resultsPlaceholder ? props.resultsPlaceholder() : null;
let selectedResultIndex = 0;
const searchEntry = new Widget.Entry({
className: "search",
onChanged: (entry) => entryText.set(entry.text),
placeholderText: props?.entryPlaceHolder || "",
onActivate: (_) => {
onActivate: (entry) => {
const resultWidget = resultsList.get_selected_row()?.get_child();
if(resultWidget instanceof ResultWidget) {
resultWidget.onClick();
_.isFocus = false;
entry.isFocus = false;
}
},
primary_icon_name: "system-search"
@@ -80,30 +99,28 @@ export namespace Runner {
expand: true
} as Gtk.ListBox.ConstructorProps);
subs.push(entryText().subscribe((text: string) => {
const trimmedText = text.trim();
const pluginResult: (ResultWidget|Array<ResultWidget>|null|undefined) = handlePrefix(
trimmedText)?.(trimmedText.replace(trimmedText.charAt(0), ""));
results = Boolean(pluginResult) ?
(!Array.isArray(pluginResult) ?
[ pluginResult! ]
: pluginResult)
: null;
function updateResultsList(entryText: string) {
const calledPlugins: Array<Plugin> = getPlugins().filter((plugin) => plugin.prefix && entryText.startsWith(plugin.prefix) ?
plugin : null).concat(getPlugins().filter(plugin => plugin.prefix === undefined));
if(resultsList.get_children().length > 0) {
const widgets: Array<ResultWidget> = calledPlugins.map(plugin => plugin.handle(
plugin.prefix ? entryText.replace(plugin.prefix, "") : entryText
)).filter(value => value !== undefined && value !== null).flat(1);
// Remove all previous results
resultsList.get_children().map((listItem: Gtk.Widget) => {
resultsList.remove(listItem);
listItem.destroy();
});
}
if(results && results.length > 0 && searchEntry.text.trim().length > 0) {
results.map((resultWidget: ResultWidget) => {
// Insert placeholder if somehow no results are found
if((!entryText || !widgets || widgets.length === 0) && props?.resultsPlaceholder)
widgets.push(...props.resultsPlaceholder());
// Insert results inside GtkListBox
widgets.map((resultWidget: ResultWidget) => {
resultsList.insert(resultWidget, -1);
const listBoxChild = resultsList.get_row_at_index(resultsList.get_children().length - 1)!;
const resWidget = listBoxChild.get_child();
if(listBoxChild && resWidget instanceof ResultWidget) {
resultsList.connect("row-activated", (_, row: Gtk.ListBoxRow) => {
const rWidget = row.get_child()!;
if(rWidget instanceof ResultWidget) {
@@ -114,20 +131,12 @@ export namespace Runner {
}
}
});
}
});
} else {
if(props?.resultsPlaceholder) {
const widgets = props.resultsPlaceholder();
resultsList.get_children().map((res) =>
resultsList.remove(res));
widgets.map((widget) => resultsList.insert(widget, -1));
}
}
selectedResultIndex = 0;
resultsList.select_row(resultsList.get_row_at_index(selectedResultIndex));
subs.push(entryText().subscribe((text: string) => {
updateResultsList(text.trim());
resultsList.select_row(resultsList.get_row_at_index(0));
}));
if(!runnerInstance)
@@ -170,19 +179,4 @@ export namespace Runner {
return runnerInstance;
}
export function handlePrefix(text: string): (((a: string) => (Array<ResultWidget>|ResultWidget|null)) | null) {
const prefix = text.charAt(0);
let result: (((a: string) => ResultWidget|Array<ResultWidget>|null)|null) = null;
if(/([a-z]|[A-Z]|[0-9])/.test(prefix))
result = handleApplications;
[...prefixes.keys()].map((curPrefix: string) => {
if(curPrefix === prefix)
result = prefixes.get(curPrefix)!;
});
return result;
}
}