feat(ags/runner): add plugin to control media from runner with prefix :

This commit is contained in:
retrozinndev
2025-03-26 16:14:53 -03:00
parent b241574319
commit 4ede01b3b9
8 changed files with 109 additions and 37 deletions
+4 -2
View File
@@ -13,6 +13,7 @@ import { Runner } from "./runner/Runner";
import { PluginApps } from "./runner/plugins/apps"; import { PluginApps } from "./runner/plugins/apps";
import { PluginShell } from "./runner/plugins/shell"; import { PluginShell } from "./runner/plugins/shell";
import { PluginWebSearch } from "./runner/plugins/websearch"; import { PluginWebSearch } from "./runner/plugins/websearch";
import { PluginMedia } from "./runner/plugins/media";
let osdTimer: (Time|undefined); let osdTimer: (Time|undefined);
@@ -20,13 +21,14 @@ let osdTimer: (Time|undefined);
const runnerPlugins: Array<Runner.Plugin> = [ const runnerPlugins: Array<Runner.Plugin> = [
PluginApps, PluginApps,
PluginShell, PluginShell,
PluginWebSearch PluginWebSearch,
PluginMedia
]; ];
App.start({ App.start({
instanceName: "astal", instanceName: "astal",
requestHandler(request: string, response: (result: any) => void) { requestHandler(request: string, response: (result: any) => void) {
console.log(`[LOG] Arguments received: ${request}`); // console.log(`[LOG] Arguments received: ${request}`);
response(handleArguments(request)); response(handleArguments(request));
}, },
main() { main() {
+52
View File
@@ -0,0 +1,52 @@
import { bind, Variable } from "astal";
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import { Runner } from "../Runner";
import AstalMpris from "gi://AstalMpris";
export const PluginMedia = {
prefix: ":",
handle() {
const player = AstalMpris.get_default().players[0];
if(!player) return new ResultWidget({
icon: "folder-music-symbolic",
title: "Couldn't find any players",
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"),
title: Variable.derive([
bind(player, "title"),
bind(player, "artist"),
bind(player, "playbackStatus")
], (title, artist, status) => `${ status === AstalMpris.PlaybackStatus.PLAYING ?
"Pause" : "Play"
} ${title} | ${artist}`)(),
onClick: () => player && player.play_pause()
} as ResultWidgetProps),
new ResultWidget({
icon: "media-skip-backward-symbolic",
title: Variable.derive([
bind(player, "title"),
bind(player, "artist")
], (title, artist) =>
`Go Previous ${ title ? title : player.busName }${ artist ? ` | ${artist}` : "" }`
)(),
onClick: () => player && player.canGoPrevious && player.previous()
} as ResultWidgetProps),
new ResultWidget({
icon: "media-skip-forward-symbolic",
title: Variable.derive([
bind(player, "title"),
bind(player, "artist")
], (title, artist) =>
`Go Next ${ title ? title : player.busName }${ artist ? ` | ${artist}` : "" }`
)(),
onClick: () => player && player.canGoNext && player.next()
} as ResultWidgetProps)
]
},
} as Runner.Plugin;
+2
View File
@@ -1,7 +1,9 @@
import { Astal } from "astal/gtk3"; import { Astal } from "astal/gtk3";
import AstalApps from "gi://AstalApps"; import AstalApps from "gi://AstalApps";
import AstalHyprland from "gi://AstalHyprland"; import AstalHyprland from "gi://AstalHyprland";
const astalApps: AstalApps.Apps = new AstalApps.Apps(); const astalApps: AstalApps.Apps = new AstalApps.Apps();
let appsList: Array<AstalApps.Application> = astalApps.get_list(); let appsList: Array<AstalApps.Application> = astalApps.get_list();
+10 -5
View File
@@ -1,11 +1,14 @@
import { Gtk } from "astal/gtk3"; import { Gtk } from "astal/gtk3";
import { Windows } from "../windows";
import { restartInstance } from "./reload-handler";
import { Wireplumber } from "./volume"; import { Wireplumber } from "./volume";
import { Windows } from "../windows";
import { restartInstance } from "./reload-handler";
import { startRunnerDefault } from "../runner/Runner"; import { startRunnerDefault } from "../runner/Runner";
import { showWorkspaceNumbers } from "../widget/bar/Workspaces"; import { showWorkspaceNumbers } from "../widget/bar/Workspaces";
import { timeout } from "astal"; import { timeout } from "astal";
export function handleArguments(request: string): any { export function handleArguments(request: string): any {
const args: Array<string> = request.split(" "); const args: Array<string> = request.split(" ");
switch(args[0]) { switch(args[0]) {
@@ -30,8 +33,10 @@ export function handleArguments(request: string): any {
return "Opening runner..." return "Opening runner..."
case "show-ws-numbers": case "show-ws-numbers":
showWorkspaceNumbers.set(true); if(!showWorkspaceNumbers.get()) {
timeout(2000, () => showWorkspaceNumbers.set(false)); showWorkspaceNumbers.set(true);
timeout(2200, () => showWorkspaceNumbers.set(false));
}
return "Showing numbers"; return "Showing numbers";
default: default:
@@ -156,7 +161,7 @@ Options:
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: open the application runner.
(show|hide)-ws-numbers: show or hide workspace numbers in bar. show-ws-numbers: show or hide workspace numbers in bar.
help, -h, --help: shows this help message. help, -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.
+14 -15
View File
@@ -1,7 +1,7 @@
import { AstalIO, GObject, property, register, signal, timeout } from "astal"; import { AstalIO, GObject, property, register, signal, timeout } from "astal";
import AstalNotifd from "gi://AstalNotifd"; import AstalNotifd from "gi://AstalNotifd";
export const export let
NOTIFICATION_TIMEOUT_URGENT: number = 0, NOTIFICATION_TIMEOUT_URGENT: number = 0,
NOTIFICATION_TIMEOUT_NORMAL: number = 4000, NOTIFICATION_TIMEOUT_NORMAL: number = 4000,
NOTIFICATION_TIMEOUT_LOW: number = 2000; NOTIFICATION_TIMEOUT_LOW: number = 2000;
@@ -22,7 +22,7 @@ class Notifications extends GObject.Object {
#notifications: Array<AstalNotifd.Notification> = []; #notifications: Array<AstalNotifd.Notification> = [];
#history: Array<HistoryNotification> = []; #history: Array<HistoryNotification> = [];
#connections: Array<number>; #connections: Array<number> = [];
#historyLimit: number = 10; #historyLimit: number = 10;
@@ -60,7 +60,7 @@ class Notifications extends GObject.Object {
constructor() { constructor() {
super(); super();
this.#connections = [ this.#connections.push(
AstalNotifd.get_default().connect("notified", (notifd, id) => { AstalNotifd.get_default().connect("notified", (notifd, id) => {
const notification = notifd.get_notification(id); const notification = notifd.get_notification(id);
const notifTimeout = notification.urgency === AstalNotifd.Urgency.LOW ? const notifTimeout = notification.urgency === AstalNotifd.Urgency.LOW ?
@@ -106,7 +106,7 @@ class Notifications extends GObject.Object {
this.removeNotification(id); this.removeNotification(id);
this.addHistory(notifd.get_notification(id)); this.addHistory(notifd.get_notification(id));
}) })
]; );
this.run_dispose = () => { this.run_dispose = () => {
super.run_dispose(); super.run_dispose();
@@ -128,8 +128,10 @@ class Notifications extends GObject.Object {
this.#history.length === this.#historyLimit && this.#history.length === this.#historyLimit &&
this.removeHistory(this.#history[this.#history.length - 1]); this.removeHistory(this.#history[this.#history.length - 1]);
const newArray = this.#history.length > 0 ? this.#history.reverse().filter((item) => item.id !== notif.id) : []; this.#history.map((notifb, i) =>
newArray.push({ notifb.id === notif.id && this.#history.splice(i, 1));
this.#history.unshift({
id: notif.id, id: notif.id,
appName: notif.appName, appName: notif.appName,
body: notif.body, body: notif.body,
@@ -138,21 +140,18 @@ class Notifications extends GObject.Object {
time: notif.time, time: notif.time,
image: notif.image ? notif.image : undefined image: notif.image ? notif.image : undefined
} as HistoryNotification); } as HistoryNotification);
this.#history = newArray.reverse();
this.notify("history"); this.notify("history");
this.emit("history-added", this.#history[0]); this.emit("history-added", this.#history[0]);
onAdded && onAdded(notif); onAdded && onAdded(notif);
} }
public clearHistory(): void { public clearHistory(): void {
for(let i = 0; i < this.history.length; i++) { this.#history.reverse().map((notif) => {
const notif = this.history[this.history.length-1]; this.#history.pop()
this.emit("history-removed", notif.id);
if(this.#history.pop()) { this.notify("history");
this.emit("history-removed", notif.id); });
this.notify("history");
}
}
} }
public removeHistory(notif: (HistoryNotification|number)): void { public removeHistory(notif: (HistoryNotification|number)): void {
+14 -9
View File
@@ -2,6 +2,7 @@ import { execAsync, GLib, GObject, property, register, signal } from "astal";
import { Connectable } from "astal/binding"; import { Connectable } from "astal/binding";
import { Gdk } from "astal/gtk3"; import { Gdk } from "astal/gtk3";
import { getDateTime } from "./time"; import { getDateTime } from "./time";
import { getUserDirs } from "./utils";
@register({ GTypeName: "ScreenRecording" }) @register({ GTypeName: "ScreenRecording" })
class Recording extends GObject.Object implements Connectable { class Recording extends GObject.Object implements Connectable {
@@ -17,11 +18,13 @@ class Recording extends GObject.Object implements Connectable {
#recording: boolean = false; #recording: boolean = false;
#path: string = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS) || `${GLib.get_home_dir()}/Recordings`; #path: string = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS) || `${GLib.get_home_dir()}/Recordings`;
/** Default extension: mp4(h264) */ /** Default extension: mp4(h264) */
#extension: string = "mp4"; #extension: string = "mp4";
#recordAudio: boolean = false; #recordAudio: boolean = false;
#monitor: (number|null) = null; #monitor: (number|null) = null;
#area: (Gdk.Rectangle|null) = null; #area: (Gdk.Rectangle|null) = null;
#pid: (number|null) = null;
@property(Boolean) @property(Boolean)
public get recording() { return this.#recording; } public get recording() { return this.#recording; }
@@ -67,19 +70,21 @@ class Recording extends GObject.Object implements Connectable {
} }
public startRecording(monitor?: number, area?: Gdk.Rectangle) { public startRecording(monitor?: number, area?: Gdk.Rectangle) {
const output = `${getDateTime().get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension}`; if(this.#recording)
throw new Error("Screen Recording is already running!");
const output = `${getDateTime().get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension || "mp4"}`;
this.#recording = true; this.#recording = true;
this.emit("started"); this.emit("started");
execAsync([ execAsync([
"wf-recorder", `sh ${ GLib.get_user_config_dir()}/ags/scripts/sh/recording.sh`,
`${Boolean(area) ? `${ area ? `-g ${area?.x || 0},${area?.y || 0} ${area?.width || 1}x${area?.height || 1}` : "" }`,
`-g ${area?.x || 0},${area?.y || 0} ${area?.width || 1}x${area?.height || 1}`
: ""}`,
`-f ${output}` `-f ${output}`
]).then(() => { ]).then(async (stdout: string) => {
this.emit("stopped", `${this.path}/${output}`); const pid: number = Number.parseInt(
this.#recording = false; (await execAsync(`echo ${stdout} | head -n 1`)).split(':')[1]);
this.notify("recording");
this.#pid = pid;
}); });
} }
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
wf-recorder $@ &
echo "PID: $!"
exit 0
+8 -6
View File
@@ -1,13 +1,13 @@
import { register } from "astal"; import { Binding, register } from "astal";
import { Gtk, Widget } from "astal/gtk3"; import { Gtk, Widget } from "astal/gtk3";
import { Runner } from "../../runner/Runner"; import { Runner } from "../../runner/Runner";
export { ResultWidget, ResultWidgetProps }; export { ResultWidget, ResultWidgetProps };
type ResultWidgetProps = { type ResultWidgetProps = {
icon?: string; icon?: string | Binding<string | undefined>;
title: string; title: string | Binding<string | undefined>;
description?: string; description?: string | Binding<string | undefined>;
closeOnClick?: boolean; closeOnClick?: boolean;
setup?: () => void; setup?: () => void;
onClick?: () => void; onClick?: () => void;
@@ -16,7 +16,7 @@ type ResultWidgetProps = {
@register({ GTypeName: "ResultWidget" }) @register({ GTypeName: "ResultWidget" })
class ResultWidget extends Widget.Box { class ResultWidget extends Widget.Box {
public readonly onClick: (() => void); public readonly onClick: (() => void);
public readonly icon: (string|undefined); public readonly icon: (string | Binding<string|undefined> | undefined);
public readonly setup: ((() => void)|undefined); public readonly setup: ((() => void)|undefined);
public readonly closeOnClick: boolean = true; public readonly closeOnClick: boolean = true;
@@ -55,7 +55,9 @@ class ResultWidget extends Widget.Box {
} as Widget.LabelProps), } as Widget.LabelProps),
new Widget.Label({ new Widget.Label({
className: "description", className: "description",
visible: Boolean(props.description), visible: (props.description instanceof Binding) ?
props.description.as(Boolean)
: Boolean(props.description),
truncate: true, truncate: true,
xalign: 0, xalign: 0,
label: props.description || "" label: props.description || ""