diff --git a/ags/app.ts b/ags/app.ts index 1cc563b..43cd558 100644 --- a/ags/app.ts +++ b/ags/app.ts @@ -13,6 +13,7 @@ import { Runner } from "./runner/Runner"; import { PluginApps } from "./runner/plugins/apps"; import { PluginShell } from "./runner/plugins/shell"; import { PluginWebSearch } from "./runner/plugins/websearch"; +import { PluginMedia } from "./runner/plugins/media"; let osdTimer: (Time|undefined); @@ -20,13 +21,14 @@ let osdTimer: (Time|undefined); const runnerPlugins: Array = [ PluginApps, PluginShell, - PluginWebSearch + PluginWebSearch, + PluginMedia ]; App.start({ instanceName: "astal", requestHandler(request: string, response: (result: any) => void) { - console.log(`[LOG] Arguments received: ${request}`); + // console.log(`[LOG] Arguments received: ${request}`); response(handleArguments(request)); }, main() { diff --git a/ags/runner/plugins/media.ts b/ags/runner/plugins/media.ts new file mode 100644 index 0000000..2bd4226 --- /dev/null +++ b/ags/runner/plugins/media.ts @@ -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; diff --git a/ags/scripts/apps.ts b/ags/scripts/apps.ts index 6b70a36..5356c95 100644 --- a/ags/scripts/apps.ts +++ b/ags/scripts/apps.ts @@ -1,7 +1,9 @@ import { Astal } from "astal/gtk3"; + import AstalApps from "gi://AstalApps"; import AstalHyprland from "gi://AstalHyprland"; + const astalApps: AstalApps.Apps = new AstalApps.Apps(); let appsList: Array = astalApps.get_list(); diff --git a/ags/scripts/arg-handler.ts b/ags/scripts/arg-handler.ts index 6a3948f..767bb23 100644 --- a/ags/scripts/arg-handler.ts +++ b/ags/scripts/arg-handler.ts @@ -1,11 +1,14 @@ import { Gtk } from "astal/gtk3"; -import { Windows } from "../windows"; -import { restartInstance } from "./reload-handler"; + import { Wireplumber } from "./volume"; +import { Windows } from "../windows"; + +import { restartInstance } from "./reload-handler"; import { startRunnerDefault } from "../runner/Runner"; import { showWorkspaceNumbers } from "../widget/bar/Workspaces"; import { timeout } from "astal"; + export function handleArguments(request: string): any { const args: Array = request.split(" "); switch(args[0]) { @@ -30,8 +33,10 @@ export function handleArguments(request: string): any { return "Opening runner..." case "show-ws-numbers": - showWorkspaceNumbers.set(true); - timeout(2000, () => showWorkspaceNumbers.set(false)); + if(!showWorkspaceNumbers.get()) { + showWorkspaceNumbers.set(true); + timeout(2200, () => showWorkspaceNumbers.set(false)); + } return "Showing numbers"; default: @@ -156,7 +161,7 @@ Options: reload: creates a new astal instance and removes this one. volume: wireplumber volume controller, see "volume help". 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. 2025 (c) retrozinndev's Hyprland-Dots, licensed under the MIT License. diff --git a/ags/scripts/notifications.ts b/ags/scripts/notifications.ts index 21d8032..0767f64 100644 --- a/ags/scripts/notifications.ts +++ b/ags/scripts/notifications.ts @@ -1,7 +1,7 @@ import { AstalIO, GObject, property, register, signal, timeout } from "astal"; import AstalNotifd from "gi://AstalNotifd"; -export const +export let NOTIFICATION_TIMEOUT_URGENT: number = 0, NOTIFICATION_TIMEOUT_NORMAL: number = 4000, NOTIFICATION_TIMEOUT_LOW: number = 2000; @@ -22,7 +22,7 @@ class Notifications extends GObject.Object { #notifications: Array = []; #history: Array = []; - #connections: Array; + #connections: Array = []; #historyLimit: number = 10; @@ -60,7 +60,7 @@ class Notifications extends GObject.Object { constructor() { super(); - this.#connections = [ + this.#connections.push( AstalNotifd.get_default().connect("notified", (notifd, id) => { const notification = notifd.get_notification(id); const notifTimeout = notification.urgency === AstalNotifd.Urgency.LOW ? @@ -106,7 +106,7 @@ class Notifications extends GObject.Object { this.removeNotification(id); this.addHistory(notifd.get_notification(id)); }) - ]; + ); this.run_dispose = () => { super.run_dispose(); @@ -128,8 +128,10 @@ class Notifications extends GObject.Object { this.#history.length === this.#historyLimit && this.removeHistory(this.#history[this.#history.length - 1]); - const newArray = this.#history.length > 0 ? this.#history.reverse().filter((item) => item.id !== notif.id) : []; - newArray.push({ + this.#history.map((notifb, i) => + notifb.id === notif.id && this.#history.splice(i, 1)); + + this.#history.unshift({ id: notif.id, appName: notif.appName, body: notif.body, @@ -138,21 +140,18 @@ class Notifications extends GObject.Object { time: notif.time, image: notif.image ? notif.image : undefined } as HistoryNotification); - this.#history = newArray.reverse(); + this.notify("history"); this.emit("history-added", this.#history[0]); onAdded && onAdded(notif); } public clearHistory(): void { - for(let i = 0; i < this.history.length; i++) { - const notif = this.history[this.history.length-1]; - - if(this.#history.pop()) { - this.emit("history-removed", notif.id); - this.notify("history"); - } - } + this.#history.reverse().map((notif) => { + this.#history.pop() + this.emit("history-removed", notif.id); + this.notify("history"); + }); } public removeHistory(notif: (HistoryNotification|number)): void { diff --git a/ags/scripts/recording.ts b/ags/scripts/recording.ts index 97ba291..0ae88ea 100644 --- a/ags/scripts/recording.ts +++ b/ags/scripts/recording.ts @@ -2,6 +2,7 @@ import { execAsync, GLib, GObject, property, register, signal } from "astal"; import { Connectable } from "astal/binding"; import { Gdk } from "astal/gtk3"; import { getDateTime } from "./time"; +import { getUserDirs } from "./utils"; @register({ GTypeName: "ScreenRecording" }) class Recording extends GObject.Object implements Connectable { @@ -17,11 +18,13 @@ class Recording extends GObject.Object implements Connectable { #recording: boolean = false; #path: string = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS) || `${GLib.get_home_dir()}/Recordings`; + /** Default extension: mp4(h264) */ #extension: string = "mp4"; #recordAudio: boolean = false; #monitor: (number|null) = null; #area: (Gdk.Rectangle|null) = null; + #pid: (number|null) = null; @property(Boolean) public get recording() { return this.#recording; } @@ -67,19 +70,21 @@ class Recording extends GObject.Object implements Connectable { } 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.emit("started"); execAsync([ - "wf-recorder", - `${Boolean(area) ? - `-g ${area?.x || 0},${area?.y || 0} ${area?.width || 1}x${area?.height || 1}` - : ""}`, + `sh ${ GLib.get_user_config_dir()}/ags/scripts/sh/recording.sh`, + `${ area ? `-g ${area?.x || 0},${area?.y || 0} ${area?.width || 1}x${area?.height || 1}` : "" }`, `-f ${output}` - ]).then(() => { - this.emit("stopped", `${this.path}/${output}`); - this.#recording = false; - this.notify("recording"); + ]).then(async (stdout: string) => { + const pid: number = Number.parseInt( + (await execAsync(`echo ${stdout} | head -n 1`)).split(':')[1]); + + this.#pid = pid; }); } diff --git a/ags/scripts/sh/recording.sh b/ags/scripts/sh/recording.sh new file mode 100644 index 0000000..0b0ba02 --- /dev/null +++ b/ags/scripts/sh/recording.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +wf-recorder $@ & +echo "PID: $!" +exit 0 diff --git a/ags/widget/runner/ResultWidget.ts b/ags/widget/runner/ResultWidget.ts index c7877e2..74ca302 100644 --- a/ags/widget/runner/ResultWidget.ts +++ b/ags/widget/runner/ResultWidget.ts @@ -1,13 +1,13 @@ -import { register } from "astal"; +import { Binding, register } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import { Runner } from "../../runner/Runner"; export { ResultWidget, ResultWidgetProps }; type ResultWidgetProps = { - icon?: string; - title: string; - description?: string; + icon?: string | Binding; + title: string | Binding; + description?: string | Binding; closeOnClick?: boolean; setup?: () => void; onClick?: () => void; @@ -16,7 +16,7 @@ type ResultWidgetProps = { @register({ GTypeName: "ResultWidget" }) class ResultWidget extends Widget.Box { public readonly onClick: (() => void); - public readonly icon: (string|undefined); + public readonly icon: (string | Binding | undefined); public readonly setup: ((() => void)|undefined); public readonly closeOnClick: boolean = true; @@ -55,7 +55,9 @@ class ResultWidget extends Widget.Box { } as Widget.LabelProps), new Widget.Label({ className: "description", - visible: Boolean(props.description), + visible: (props.description instanceof Binding) ? + props.description.as(Boolean) + : Boolean(props.description), truncate: true, xalign: 0, label: props.description || ""