ags(runner): add new functions and organize existing

This commit is contained in:
retrozinndev
2025-05-11 20:53:19 -03:00
parent ae1d29bc89
commit c574fa87f9
3 changed files with 253 additions and 225 deletions
+233 -202
View File
@@ -1,13 +1,77 @@
import { AstalIO, timeout, Variable } from "astal"; import { AstalIO, timeout } from "astal";
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow";
import { updateApps } from "../scripts/apps"; import { updateApps } from "../scripts/apps";
import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget"; import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget";
import { Windows } from "../windows"; import { Windows } from "../windows";
export let runnerInstance: (Gtk.Window|null) = null; export namespace Runner {
export type RunnerProps = {
halign?: Gtk.Align;
valign?: Gtk.Align;
width?: number;
height?: number;
entryPlaceHolder?: string;
initialText?: string;
showResultsPlaceHolderOnStartup?: boolean;
};
export function startRunnerDefault(initialText?: string) { 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;
/** ran on runner open */
readonly init?: () => void;
/** handle the user input to return results (does not include plugin's prefix) */
readonly handle: (inputText: string) => (ResultWidget|Array<ResultWidget>|null|undefined);
/** ran on runner close */
readonly onClose?: () => void;
/** hide other plugins when using this plugin */
prioritize?: boolean;
}
export let instance: (Widget.Window|null) = null;
let gtkEntry: (Widget.Entry|null) = null;
const plugins = new Set<Runner.Plugin>();
export function close() { instance?.close(); }
export function regExMatch(search: string, item: string): boolean {
search = search.replace(/[\\^$.*?()[\]{}|]/g, "\\$&");
return new RegExp(`${search.split('').map(c =>
`.*(${c.toLowerCase()}|${c.toUpperCase()}).*`).join('')}`
).test(item);
}
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.delete(plugin);
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 setEntryText(text: string): void {
gtkEntry?.set_text(text);
gtkEntry?.set_position(gtkEntry.textLength);
gtkEntry?.grab_focus_without_selecting();
}
export function openDefault(initialText?: string) {
return Runner.openRunner({ return Runner.openRunner({
entryPlaceHolder: "Start typing...", entryPlaceHolder: "Start typing...",
showResultsPlaceHolderOnStartup: false, showResultsPlaceHolderOnStartup: false,
@@ -16,222 +80,189 @@ export function startRunnerDefault(initialText?: string) {
() => [ () => [
new ResultWidget({ new ResultWidget({
icon: "application-x-executable-symbolic", icon: "application-x-executable-symbolic",
title: "Run your applications", title: "Use your applications",
description: "Type the name of the application to search" description: "Search for any app installed in your computer",
closeOnClick: false,
onClick: () => gtkEntry?.grab_focus()
} as ResultWidgetProps), } as ResultWidgetProps),
new ResultWidget({ new ResultWidget({
icon: "media-playback-start-symbolic", icon: "edit-paste-symbolic",
title: "Control media", title: "See your clipboard history",
description: "Use prefix ':' to run" description: "Start your search with '>' to go through your clipboard history",
closeOnClick: false,
onClick: () => setEntryText('>')
} as ResultWidgetProps),
new ResultWidget({
icon: "image-x-generic-symbolic",
title: "Change your wallpaper",
description: "Add '#' at the start to search through the wallpapers folder!",
closeOnClick: false,
onClick: () => setEntryText('#'),
} as ResultWidgetProps), } as ResultWidgetProps),
new ResultWidget({ new ResultWidget({
icon: "utilities-terminal-symbolic", icon: "utilities-terminal-symbolic",
title: "Run shell commands", title: "Run shell commands",
description: "Start typing with '!' prefix to run shell commands" description: "Add '!' before your command to run it (pro tip: add a second '!' to show command output)",
closeOnClick: false,
onClick: () => setEntryText('!!')
} as ResultWidgetProps),
new ResultWidget({
icon: "media-playback-start-symbolic",
title: "Control media",
description: "Type ':' to control playing media",
closeOnClick: false,
onClick: () => setEntryText(':')
} as ResultWidgetProps), } as ResultWidgetProps),
new ResultWidget({ 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,
onClick: () => setEntryText('?')
} as ResultWidgetProps) } as ResultWidgetProps)
]); ]);
} }
export namespace Runner { export function openRunner(props?: RunnerProps, placeholder?: () => Array<ResultWidget>): Widget.Window {
export type RunnerProps = { let onClickTimeout: (AstalIO.Time|undefined);
halign?: Gtk.Align;
valign?: Gtk.Align;
width?: number;
height?: number;
entryPlaceHolder?: string;
initialText?: string;
showResultsPlaceHolderOnStartup?: boolean;
};
export function close() { runnerInstance?.close(); } gtkEntry = new Widget.Entry({
className: "search",
const plugins = new Set<Runner.Plugin>([]); placeholderText: props?.entryPlaceHolder || "",
onChanged: (self) => {
export interface Plugin { updateResultsList(self.text);
/** prefix to call the plugin. if undefined, will be triggered like applications plugin */ resultsList.get_row_at_index(0) &&
readonly prefix?: string; resultsList.select_row(resultsList.get_row_at_index(0));
/** name of the plugin. e.g.: websearch, shell */ },
readonly name?: string; onActivate: (entry) => {
/** ran on runner open */ const resultWidget = resultsList.get_selected_row()?.get_child();
readonly init?: () => void; if(resultWidget instanceof ResultWidget) {
/** handle the user input to return results (does not include plugin's prefix) */ entry.isFocus = false;
readonly handle: (inputText: string) => (ResultWidget|Array<ResultWidget>|null|undefined); resultWidget.onClick();
/** ran on runner close */ resultWidget.closeOnClick && Runner.close();
readonly onClose?: () => void;
/** hide other plugins when using this plugin **/
prioritize?: boolean;
}
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.delete(plugin);
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 openRunner(props?: RunnerProps, placeholder?: () => Array<ResultWidget>): (Gtk.Window|null) {
let subs: Array<() => void> = [];
const entryText: Variable<string> = new Variable<string>("");
let onClickTimeout: (AstalIO.Time|undefined);
const searchEntry = new Widget.Entry({
className: "search",
onChanged: (entry) => entryText.set(entry.text),
placeholderText: props?.entryPlaceHolder || "",
onActivate: (entry) => {
const resultWidget = resultsList.get_selected_row()?.get_child();
if(resultWidget instanceof ResultWidget) {
entry.isFocus = false;
resultWidget.onClick();
resultWidget.closeOnClick && Runner.close();
}
},
primary_icon_name: "system-search"
} as Widget.EntryProps);
const resultsList: Gtk.ListBox = new Gtk.ListBox({
visible: true,
expand: true
} as Gtk.ListBox.ConstructorProps);
if(props?.showResultsPlaceHolderOnStartup && placeholder) {
const placeholderWidgets = placeholder();
placeholderWidgets.map(widget =>
resultsList.insert(widget, -1));
}
function cleanResults() {
resultsList.get_children().map((listItem) => {
resultsList.remove(listItem);
listItem.destroy();
});
}
function getPluginResults(input: string): Array<ResultWidget> {
let calledPlugins: Array<Plugin> = getPlugins().filter((plugin) =>
plugin.prefix ? (input.startsWith(plugin.prefix) ? true : false) : true
).sort((plugin) => plugin.prefix != null ? 0 : 1);
for(const plugin of calledPlugins) {
if(plugin.prioritize) {
calledPlugins = [ plugin ];
break;
}
} }
},
primary_icon_name: "system-search"
} as Widget.EntryProps);
return calledPlugins.map(plugin => plugin.handle( const resultsList: Gtk.ListBox = new Gtk.ListBox({
plugin.prefix ? input.replace(plugin.prefix, "") : input) visible: true,
).filter(value => value !== undefined && value !== null).flat(1); expand: true
} } as Gtk.ListBox.ConstructorProps);
function updateResultsList(entryText: string) { if(props?.showResultsPlaceHolderOnStartup && placeholder) {
const widgets: Array<ResultWidget> = []; const placeholderWidgets = placeholder();
placeholderWidgets.map(widget =>
// Remove all previous results resultsList.insert(widget, -1));
cleanResults();
widgets.push(...getPluginResults(entryText))
// Insert placeholder if there are no results
if(placeholder && widgets.length === 0)
widgets.push(...placeholder());
// Insert results inside GtkListBox
widgets.map((resultWidget: ResultWidget) => {
resultsList.insert(resultWidget, -1);
resultsList.connect("row-activated", (_, row: Gtk.ListBoxRow) => {
const rWidget = row.get_child();
if(rWidget instanceof ResultWidget) {
if(onClickTimeout) return;
// Timeout, so it doesn't fire the event a hundred times :skull:
onClickTimeout = timeout(500, () => onClickTimeout = undefined);
rWidget.onClick();
rWidget.closeOnClick && Runner.close();
}
});
});
}
subs.push(entryText().subscribe((text: string) => {
updateResultsList(text.trim());
resultsList.select_row(resultsList.get_row_at_index(0));
}));
if(!runnerInstance)
runnerInstance = Windows.createWindowForFocusedMonitor((mon: number): (Widget.Window) => PopupWindow({
namespace: "runner",
monitor: mon,
widthRequest: props?.width ?? 750,
heightRequest: props?.height ?? 450,
marginTop: 240,
anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM,
setup: () => {
// Init plugins
plugins.forEach(plugin => plugin.init && plugin.init());
if(props?.initialText) {
searchEntry.set_text(props.initialText);
searchEntry.set_position(searchEntry.textLength);
}
},
onKeyPressEvent: (_, event: Gdk.Event) => {
const keyVal = event.get_keyval()[1];
if(!searchEntry.has_focus && keyVal !== Gdk.KEY_F5
&& keyVal !== Gdk.KEY_Down && keyVal !== Gdk.KEY_Up
&& keyVal !== Gdk.KEY_Return) {
searchEntry.grab_focus_without_selecting();
return;
}
event.get_keyval()[1] === Gdk.KEY_F5 &&
updateApps();
},
onDestroy: () => {
subs.map(sub => sub());
[...plugins.values()].map(plugin =>
plugin && plugin.onClose && plugin.onClose());
runnerInstance = null;
},
child: new Widget.Box({
className: "runner main",
orientation: Gtk.Orientation.VERTICAL,
expand: false,
valign: Gtk.Align.START,
children: [
searchEntry,
new Widget.Scrollable({
className: "results-scrollable",
vscroll: Gtk.PolicyType.AUTOMATIC,
hscroll: Gtk.PolicyType.NEVER,
expand: true,
propagateNaturalHeight: true,
maxContentHeight: props?.height ?? 450,
child: resultsList
})
]
} as Widget.BoxProps)
} as PopupWindowProps))();
return runnerInstance;
} }
function cleanResults() {
resultsList.get_children().map((listItem) => {
resultsList.remove(listItem);
});
}
function getPluginResults(input: string): Array<ResultWidget> {
let calledPlugins: Array<Plugin> = getPlugins().filter((plugin) =>
plugin.prefix ? (input.startsWith(plugin.prefix) ? true : false) : true
).sort((plugin) => plugin.prefix != null ? 0 : 1);
for(const plugin of calledPlugins) {
if(plugin.prioritize) {
calledPlugins = [ plugin ];
break;
}
}
return calledPlugins.map(plugin => plugin.handle(
plugin.prefix ? input.replace(plugin.prefix, "") : input)
).filter(value => value !== undefined && value !== null).flat(1);
}
function updateResultsList(entryText: string) {
const widgets: Array<ResultWidget> = [];
// Remove all previous results
cleanResults();
widgets.push(...getPluginResults(entryText))
// Insert placeholder if there are no results
if(placeholder && widgets.length === 0)
widgets.push(...placeholder());
// Insert results inside GtkListBox
widgets.map((resultWidget: ResultWidget) => {
resultsList.insert(resultWidget, -1);
resultsList.connect("row-activated", (_, row: Gtk.ListBoxRow) => {
const rWidget = row.get_child();
if(rWidget instanceof ResultWidget) {
if(onClickTimeout) return;
// Timeout, so it doesn't fire the event a hundred times :skull:
onClickTimeout = timeout(500, () => onClickTimeout = undefined);
rWidget.onClick();
rWidget.closeOnClick && Runner.close();
}
});
});
}
if(!instance)
instance = Windows.createWindowForFocusedMonitor((mon: number): (Widget.Window) => PopupWindow({
namespace: "runner",
monitor: mon,
widthRequest: props?.width ?? 750,
heightRequest: props?.height ?? 450,
marginTop: 240,
anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM,
setup: () => {
// Init plugins
plugins.forEach(plugin => plugin.init && plugin.init());
if(props?.initialText)
Runner.setEntryText(props.initialText);
},
onKeyPressEvent: (_, event: Gdk.Event) => {
const keyVal = event.get_keyval()[1];
if(!gtkEntry!.has_focus && keyVal !== Gdk.KEY_F5
&& keyVal !== Gdk.KEY_Down && keyVal !== Gdk.KEY_Up
&& keyVal !== Gdk.KEY_Return) {
gtkEntry!.grab_focus_without_selecting();
return;
}
event.get_keyval()[1] === Gdk.KEY_F5 &&
updateApps();
},
onDestroy: () => {
gtkEntry = null;
[...plugins.values()].map(plugin =>
plugin && plugin.onClose && plugin.onClose());
instance = null;
},
child: new Widget.Box({
className: "runner main",
orientation: Gtk.Orientation.VERTICAL,
expand: false,
valign: Gtk.Align.START,
children: [
gtkEntry,
new Widget.Scrollable({
className: "results-scrollable",
vscroll: Gtk.PolicyType.AUTOMATIC,
hscroll: Gtk.PolicyType.NEVER,
expand: true,
propagateNaturalHeight: true,
maxContentHeight: props?.height ?? 450,
child: resultsList
})
]
} as Widget.BoxProps)
} as PopupWindowProps))();
return instance!;
}
} }
+1 -3
View File
@@ -26,9 +26,7 @@ export 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 => // not the best way to search, but it works
new RegExp(`${search.split('').map(c => Runner.regExMatch(search, file.split('/')[file.split('/').length-1])
`.*(${c.toLowerCase()}|${c.toUpperCase()}).*`).join('')}`
).test(file.split('/')[file.split('/').length-1])
).map(path => new ResultWidget({ ).map(path => new ResultWidget({
title: path.split('/')[path.split('/').length-1].replace(/\..*$/, ""), title: path.split('/')[path.split('/').length-1].replace(/\..*$/, ""),
onClick: () => Wallpaper.getDefault().setWallpaper(path) onClick: () => Wallpaper.getDefault().setWallpaper(path)
+19 -20
View File
@@ -2,9 +2,9 @@ import { Wireplumber } from "./volume";
import { Windows } from "../windows"; import { Windows } from "../windows";
import { restartInstance } from "./reload-handler"; import { restartInstance } from "./reload-handler";
import { runnerInstance, startRunnerDefault } from "../runner/Runner";
import { showWorkspaceNumbers } from "../widget/bar/Workspaces"; import { showWorkspaceNumbers } from "../widget/bar/Workspaces";
import { timeout } from "astal"; import { timeout } from "astal";
import { Runner } from "../runner/Runner";
export function handleArguments(request: string): any { export function handleArguments(request: string): any {
@@ -31,9 +31,9 @@ export function handleArguments(request: string): any {
`${name}: ${Windows.isVisible(name) ? "open" : "closed" }`).join('\n'); `${name}: ${Windows.isVisible(name) ? "open" : "closed" }`).join('\n');
case "runner": case "runner":
!runnerInstance ? !Runner.instance ?
startRunnerDefault(args[1] || undefined) Runner.openDefault(args[1] || undefined)
: runnerInstance.close(); : Runner.close();
return "Opening runner..." return "Opening runner..."
case "show-ws-numbers": case "show-ws-numbers":
@@ -154,22 +154,21 @@ Options:
} }
function getHelp(): string { function getHelp(): string {
return ` return `Manage Astal Windows and do more stuff. From
Manage Astal Windows and do more stuff. From retrozinndev's Hyprland Dots, using Astal and AGS by Aylur.
retrozinndev's Hyprland Dots, using Astal and AGS by Aylur.
Options: Options:
open [window_name]: sets specified window's visibility to true. open [window_name]: sets specified window's visibility to true.
close [window_name]: sets specified window's visibility to false. close [window_name]: sets specified window's visibility to false.
toggle [window_name]: toggles visibility of specified window. toggle [window_name]: toggles visibility of specified window.
windows: shows available windows to control. windows: shows available windows to control.
reload: creates a new astal instance and removes this one. reload: creates a new astal instance and removes this one.
volume: wireplumber volume controller, see "volume help". volume: wireplumber volume controller, see "volume help".
runner: open the application runner. runner [initial_text]: open the application runner.
show-ws-numbers: show or hide workspace numbers in bar. show-ws-numbers: show or hide workspace numbers in bar.
h, help: shows this help message. h, help: shows this help message.
2025 (c) retrozinndev's Hyprland-Dots, licensed under the MIT License. 2025 (c) retrozinndev's Hyprland-Dots, licensed under the MIT License.
https://github.com/retrozinndev/Hyprland-Dots https://github.com/retrozinndev/Hyprland-Dots
`.trim(); `.split('\n').map(l => l.replace(/^ {8}/, "")).join('\n');
} }