diff --git a/ags/runner/Runner.ts b/ags/runner/Runner.tsx similarity index 59% rename from ags/runner/Runner.ts rename to ags/runner/Runner.tsx index 92cad32..6b8f8ce 100644 --- a/ags/runner/Runner.ts +++ b/ags/runner/Runner.tsx @@ -1,10 +1,14 @@ -import { AstalIO, GObject, timeout } from "astal"; -import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; -import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; +import { Astal, Gdk, Gtk } from "ags/gtk4"; +import { timeout } from "ags/time"; +import { PopupWindow } from "../widget/PopupWindow"; import { updateApps } from "../scripts/apps"; import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget"; import { Windows } from "../windows"; + +import GObject from "ags/gobject"; import AstalHyprland from "gi://AstalHyprland"; +import AstalIO from "gi://AstalIO"; + export namespace Runner { export type RunnerProps = { @@ -33,8 +37,8 @@ export interface Plugin { prioritize?: boolean; } -export let instance: (Widget.Window|null) = null; -let gtkEntry: (Widget.Entry|null) = null; +export let instance: (Astal.Window|null) = null; +let gtkEntry: (Gtk.SearchEntry|null) = null; const plugins = new Set(); export function close() { instance?.close(); } @@ -74,9 +78,9 @@ export function removePlugin(plugin: Plugin): boolean { export function setEntryText(text: string): void { gtkEntry?.set_text(text); - gtkEntry?.set_position(gtkEntry.textLength); + gtkEntry?.set_position(gtkEntry.text.length); - gtkEntry?.grab_focus_without_selecting(); + gtkEntry?.grab_focus(); } export function openDefault(initialText?: string) { @@ -131,78 +135,57 @@ export function openDefault(initialText?: string) { ]); } -export function openRunner(props: RunnerProps, placeholder?: () => Array): Widget.Window { +export function openRunner(props: RunnerProps, placeholder?: () => Array): Astal.Window { let onClickTimeout: (AstalIO.Time|undefined); const connections: Map = new Map(); props.width ??= 780; props.height ??= 420; - gtkEntry = new Widget.Entry({ - className: "search", - placeholderText: props?.entryPlaceHolder || "", - onChanged: async (self) => { - updateResultsList(self.text); - resultsList.get_row_at_index(0) && - resultsList.select_row(resultsList.get_row_at_index(0)); + gtkEntry = { + updateResultsList(self.text); + 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")) { - mainBox.get_style_context().add_class("empty-input"); - return; - } + if(self.text.trim().length < 1 && !mainBox.get_style_context().has_class("empty-input")) { + 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: (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); + 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 = new Widget.Box({ - className: `runner main ${props.showResultsPlaceHolderOnStartup ? "empty" : ""}`, - orientation: Gtk.Orientation.VERTICAL, - hexpand: true, - valign: Gtk.Align.START, - children: [ - gtkEntry, - new Widget.Scrollable({ - className: "results-scrollable", - vscroll: Gtk.PolicyType.AUTOMATIC, - hscroll: Gtk.PolicyType.NEVER, - expand: true, - visible: props.showResultsPlaceHolderOnStartup ?? false, - propagateNaturalHeight: true, - maxContentHeight: props.height, - child: new Gtk.ListBox({ - visible: true, - expand: true, - } as Gtk.ListBox.ConstructorProps) - }) - ] - } as Widget.BoxProps); + const mainBox = + {gtkEntry} + - const scrollable = mainBox.get_children()[1] as Widget.Scrollable; - const resultsList = scrollable.get_child() as Gtk.ListBox; + + + 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 placeholderWidgets = placeholder(); - placeholderWidgets.map(widget => + const placeholderGtks = placeholder(); + placeholderGtks.map(widget => resultsList.insert(widget, -1)); } - function cleanResults() { - resultsList.get_children().map((listItem) => { - resultsList.remove(listItem); - }); - } - function getPluginResults(input: string): Array { let calledPlugins: Array = getPlugins().filter((plugin) => plugin.prefix ? (input.startsWith(plugin.prefix) ? true : false) : true @@ -229,7 +212,7 @@ export function openRunner(props: RunnerProps, placeholder?: () => Array = []; // Remove all previous results - cleanResults(); + resultsList.remove_all(); widgets.push(...getPluginResults(entryText)) @@ -238,21 +221,21 @@ export function openRunner(props: RunnerProps, placeholder?: () => Array { - resultsList.insert(resultWidget, -1); + widgets.map((resultGtk: ResultWidget) => { + resultsList.insert(resultGtk, -1); const conns: Array = []; conns.push( resultsList.connect("row-activated", (_, row: Gtk.ListBoxRow) => { - const rWidget = row.get_child(); - if(rWidget instanceof ResultWidget) { + 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); - rWidget.onClick(); - rWidget.closeOnClick && Runner.close(); + rGtk.onClick(); + rGtk.closeOnClick && Runner.close(); } }), resultsList.connect("destroy", () => @@ -267,48 +250,39 @@ export function openRunner(props: RunnerProps, placeholder?: () => Array PopupWindow({ - namespace: "runner", - monitor: mon, - widthRequest: props.width, - heightRequest: props.height, - marginTop: (AstalHyprland.get_default().get_monitor(mon)?.height / 2) - (props.height! / 2), - exclusivity: Astal.Exclusivity.IGNORE, - halign: Gtk.Align.CENTER, - valign: Gtk.Align.START, - setup: () => { - // Init plugins - plugins.forEach(plugin => plugin.init && plugin.init()); + instance = Windows.getDefault().createWindowForFocusedMonitor((mon: number) => { + plugins.forEach(plugin => + 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(); + props.initialText && + Runner.setEntryText(props.initialText); + }} actionKeyPressed={(_, keyval) => { + if(!gtkEntry!.has_focus && keyval !== Gdk.KEY_F5 + && keyval !== Gdk.KEY_Down && keyval !== Gdk.KEY_Up + && keyval !== Gdk.KEY_Return) { + gtkEntry!.grab_focus(); return; } - event.get_keyval()[1] === Gdk.KEY_F5 && + keyval === Gdk.KEY_F5 && updateApps(); - }, - onDestroy: () => { - connections.forEach((id, obj) => GObject.signal_handler_is_connected(obj, id) && - obj.disconnect(id)); + }} onDestroy={() => { + connections.forEach((id, obj) => GObject.signal_handler_is_connected(obj, id) && + obj.disconnect(id)); - gtkEntry = null; + gtkEntry = null; - [...plugins.values()].forEach(plugin => - plugin && plugin.onClose && plugin.onClose()); + [...plugins.values()].forEach(plugin => + plugin && plugin.onClose && plugin.onClose()); - instance = null; - }, - child: mainBox - } as PopupWindowProps))(); + instance = null; + }}> + {mainBox} + as Astal.Window)(); return instance!; } diff --git a/ags/runner/plugins/apps.ts b/ags/runner/plugins/apps.ts index ab423e0..15ae8bb 100644 --- a/ags/runner/plugins/apps.ts +++ b/ags/runner/plugins/apps.ts @@ -1,8 +1,7 @@ import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; import AstalApps from "gi://AstalApps"; -import { execApp, getAstalApps, updateApps } from "../../scripts/apps"; +import { execApp, getAstalApps, lookupIcon, updateApps } from "../../scripts/apps"; import { Runner } from "../Runner"; -import { Astal } from "astal/gtk3"; export const PluginApps = { // Do not provide prefix, so it always runs. @@ -14,7 +13,7 @@ export const PluginApps = { new ResultWidget({ title: app.get_name(), description: app.get_description(), - icon: Astal.Icon.lookup_icon(app.iconName) ? app.iconName : "application-x-executable-symbolic", + icon: lookupIcon(app.iconName) ? app.iconName : "application-x-executable-symbolic", onClick: () => execApp(app) } as ResultWidgetProps) ); diff --git a/ags/runner/plugins/clipboard.ts b/ags/runner/plugins/clipboard.ts deleted file mode 100644 index 0e10582..0000000 --- a/ags/runner/plugins/clipboard.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Widget } from "astal/gtk3"; -import { Clipboard } from "../../scripts/clipboard"; -import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; -import { Runner } from "../Runner"; -import { Gio } from "astal"; - - -export const PluginClipboard = { - prefix: '>', - prioritize: true, - handle: (search) => { - if(Clipboard.getDefault().history.length < 1) - return new ResultWidget({ - icon: "edit-paste-symbolic", - title: "Clipboard is empty", - description: "Copy something and it will be shown right here!" - } as ResultWidgetProps); - - 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) => - new ResultWidget({ - icon: new Widget.Label({ - label: item.id.toString(), - css: "font-size: 16px; margin-right: 8px; font-weight: 600;" - } as Widget.LabelProps), - title: item.preview, - onClick: () => Clipboard.getDefault().selectItem(item).catch((err: Gio.IOErrorEnum) => { - console.error(`Runner(Plugin/Clipboard): An error occurred while selecting clipboard item. Stderr:\n${ - err.message ? `${err.message}\n` : ""}Stack: ${err.stack}`); - }) - } as ResultWidgetProps)); - } -} as Runner.Plugin; diff --git a/ags/runner/plugins/clipboard.tsx b/ags/runner/plugins/clipboard.tsx new file mode 100644 index 0000000..39c798a --- /dev/null +++ b/ags/runner/plugins/clipboard.tsx @@ -0,0 +1,28 @@ +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 ; + + 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) => + } + 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; diff --git a/ags/runner/plugins/media.ts b/ags/runner/plugins/media.ts index 54ef694..d87e967 100644 --- a/ags/runner/plugins/media.ts +++ b/ags/runner/plugins/media.ts @@ -1,79 +1,56 @@ -import { bind, Variable } from "astal"; +import { createBinding, createComputed } from "ags"; import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; import { Runner } from "../Runner"; import AstalMpris from "gi://AstalMpris"; +import { player } from "../../widget/bar/Media"; -export const PluginMedia = (() => { - let playTitle: Variable|null; - let previousTitle: Variable|null; - let nextTitle: Variable|null; +export const PluginMedia = { + prefix: ":", + handle() { + if(!player.get().available) return new ResultWidget({ + icon: "folder-music-symbolic", + title: "Couldn't find any players", + closeOnClick: false, + description: "No media / player found with mpris" + } as ResultWidgetProps); - return { - prefix: ":", - - onClose: () => { - playTitle?.drop(); - previousTitle?.drop(); - nextTitle?.drop(); - - previousTitle = null; - playTitle = null; - nextTitle = null; - }, - - handle() { - const player = AstalMpris.get_default().players[0]; - - playTitle = Variable.derive([ - bind(player, "title"), - bind(player, "artist"), - bind(player, "playbackStatus") - ], (title, artist, status) => `${ status === AstalMpris.PlaybackStatus.PLAYING ? - "Pause" : "Play" - } ${title} | ${artist}`); - - previousTitle = Variable.derive([ - bind(player, "title"), - bind(player, "artist") - ], (title, artist) => - `Go Previous ${ title ? title : player.busName }${ artist ? ` | ${artist}` : "" }` - ); - - nextTitle = Variable.derive([ - bind(player, "title"), - bind(player, "artist") - ], (title, artist) => - `Go Next ${ title ? title : player.busName }${ artist ? ` | ${artist}` : "" }` - ); - - if(!player) return new ResultWidget({ - icon: "folder-music-symbolic", - title: "Couldn't find any players", + return [ + new ResultWidget({ + icon: createBinding(player.get(), "playbackStatus").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ? + "media-playback-pause-symbolic" + : "media-playback-start-symbolic"), closeOnClick: false, - description: "No media / player found with mpris" - } as ResultWidgetProps); - return [ - new ResultWidget({ - icon: bind(player, "playbackStatus").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ? - "media-playback-pause-symbolic" - : "media-playback-start-symbolic"), - closeOnClick: false, - title: playTitle(), - onClick: () => player && player.play_pause() - } as ResultWidgetProps), - new ResultWidget({ - icon: "media-skip-backward-symbolic", - closeOnClick: false, - title: previousTitle(), - onClick: () => player && player.canGoPrevious && player.previous() - } as ResultWidgetProps), - new ResultWidget({ - icon: "media-skip-forward-symbolic", - closeOnClick: false, - title: nextTitle(), - onClick: () => player && player.canGoNext && player.next() - } as ResultWidgetProps) - ] - }, - } as Runner.Plugin -})(); + title: createComputed([ + createBinding(player.get(), "title"), + createBinding(player.get(), "artist"), + createBinding(player.get(), "playbackStatus") + ], (title, artist, status) => `${ status === AstalMpris.PlaybackStatus.PLAYING ? + "Pause" : "Play" + } ${title} | ${artist}`), + onClick: () => player.get().play_pause() + } as ResultWidgetProps), + new ResultWidget({ + icon: "media-skip-backward-symbolic", + closeOnClick: false, + title: createComputed([ + createBinding(player.get(), "title"), + createBinding(player.get(), "artist") + ], (title, artist) => + `Go Previous ${ title ? title : player.get().busName }${ artist ? ` | ${artist}` : "" }` + ), + onClick: () => player.get().canGoPrevious && player.get().previous() + } as ResultWidgetProps), + new ResultWidget({ + icon: "media-skip-forward-symbolic", + closeOnClick: false, + title: createComputed([ + createBinding(player.get(), "title"), + createBinding(player.get(), "artist") + ], (title, artist) => + `Go Next ${ title ? title : player.get().busName }${ artist ? ` | ${artist}` : "" }` + ), + onClick: () => player.get().canGoNext && player.get().next() + } as ResultWidgetProps) + ] + }, +} as Runner.Plugin; diff --git a/ags/runner/plugins/shell.ts b/ags/runner/plugins/shell.ts index 89a6a63..364bec7 100644 --- a/ags/runner/plugins/shell.ts +++ b/ags/runner/plugins/shell.ts @@ -1,8 +1,11 @@ import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; -import { Gio, GLib } from "astal"; import { Runner } from "../Runner"; import { Notifications } from "../../scripts/notifications"; +import GLib from "gi://GLib?version=2.0"; +import Gio from "gi://Gio?version=2.0"; + + export const PluginShell = (() => { const shell = GLib.getenv("SHELL") ?? "/bin/sh"; diff --git a/ags/runner/plugins/wallpapers.ts b/ags/runner/plugins/wallpapers.ts index 09b21fd..2169cf8 100644 --- a/ags/runner/plugins/wallpapers.ts +++ b/ags/runner/plugins/wallpapers.ts @@ -1,10 +1,11 @@ -import { Gio } from "astal"; import { Wallpaper } from "../../scripts/wallpaper"; import { Runner } from "../Runner"; import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; +import Gio from "gi://Gio?version=2.0"; -export class PluginWallpapers implements Runner.Plugin { + +class _PluginWallpapers implements Runner.Plugin { prefix = "#"; prioritize = true; #files: (Array|undefined); @@ -39,3 +40,5 @@ export class PluginWallpapers implements Runner.Plugin { } as ResultWidgetProps); } } + +export const PluginWallpapers = new _PluginWallpapers();