✨ feat: a lot of new stuff!
support for default bluetooth adapter, notification popup position in configuration, code improvements
This commit is contained in:
+2
-2
@@ -28,10 +28,10 @@ export function getAstalApps(): AstalApps.Apps {
|
||||
/** handles running with uwsm if it's installed */
|
||||
export function execApp(app: AstalApps.Application|string, dispatchExecArgs?: string) {
|
||||
const executable = (typeof app === "string") ? app
|
||||
: app.executable.replace(/(%f|%F|%u|%U|%i|%c|%k)/g, "");
|
||||
: app.executable.replace(/%[fFcuUik]/g, "");
|
||||
|
||||
AstalHyprland.get_default().dispatch("exec",
|
||||
`${dispatchExecArgs ? `${dispatchExecArgs} ` : ""}${uwsmIsActive ? "uwsm app -- " : ""}${executable}`
|
||||
`${dispatchExecArgs ? `${dispatchExecArgs} ` : ""}${uwsmIsActive ? "uwsm-app -- " : ""}${executable}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { createRoot, getScope, Scope } from "ags";
|
||||
import GObject, { getter, gtype, register, setter } from "ags/gobject";
|
||||
import AstalBluetooth from "gi://AstalBluetooth";
|
||||
|
||||
|
||||
/** AstalBluetooth helper (implements the default adapter feature) */
|
||||
@register({ GTypeName: "Bluetooth" })
|
||||
export class Bluetooth extends GObject.Object {
|
||||
private static instance: Bluetooth;
|
||||
private astalBl = AstalBluetooth.get_default();
|
||||
|
||||
#connections: Map<GObject.Object, Array<number>|number> = new Map();
|
||||
#adapter: AstalBluetooth.Adapter|null = this.astalBl.adapter ?? null;
|
||||
#scope!: Scope;
|
||||
#isAvailable: boolean = false;
|
||||
|
||||
@getter(Boolean)
|
||||
get isAvailable() { return this.#isAvailable; }
|
||||
|
||||
@getter(gtype<AstalBluetooth.Adapter|null>(AstalBluetooth.Adapter))
|
||||
get adapter() { return this.#adapter; }
|
||||
|
||||
@setter(gtype<AstalBluetooth.Adapter|null>(AstalBluetooth.Adapter))
|
||||
set adapter(newAdapter: AstalBluetooth.Adapter|null) {
|
||||
this.#adapter = newAdapter;
|
||||
this.notify("adapter");
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
createRoot((_) => {
|
||||
this.#scope = getScope();
|
||||
|
||||
this.#connections.set(
|
||||
AstalBluetooth.get_default(),
|
||||
AstalBluetooth.get_default().connect("adapter-added", (self, adapter) => {
|
||||
if(self.adapters.length === 1) // adapter was just added
|
||||
this.adapter = adapter;
|
||||
})
|
||||
);
|
||||
|
||||
this.#connections.set(
|
||||
AstalBluetooth.get_default(),
|
||||
AstalBluetooth.get_default().connect("adapter-removed", (self, adapter) => {
|
||||
if(self.adapters.length < 1) {
|
||||
this.adapter = null;
|
||||
this.#isAvailable = false;
|
||||
this.notify("is-available");
|
||||
}
|
||||
|
||||
if(this.#adapter?.address !== adapter.address)
|
||||
return;
|
||||
|
||||
// the removed adapter was the default
|
||||
|
||||
if(self.adapters.length < 1) {
|
||||
this.adapter = null;
|
||||
this.#isAvailable = false;
|
||||
this.notify("is-available");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.#adapter = self.adapters[0];
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static getDefault(): Bluetooth {
|
||||
if(!this.instance)
|
||||
this.instance = new Bluetooth();
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
vfunc_dispose(): void {
|
||||
this.#scope.dispose();
|
||||
}
|
||||
}
|
||||
+51
-31
@@ -1,6 +1,7 @@
|
||||
import { execAsync } from "ags/process";
|
||||
import { getter, register, signal } from "ags/gobject";
|
||||
import { Gdk } from "ags/gtk4";
|
||||
import { createRoot, getScope, Scope } from "ags";
|
||||
import { makeDirectory } from "./utils";
|
||||
import { Notifications } from "./notifications";
|
||||
import { time } from "./utils";
|
||||
@@ -10,10 +11,8 @@ import GLib from "gi://GLib?version=2.0";
|
||||
import Gio from "gi://Gio?version=2.0";
|
||||
|
||||
|
||||
export { Recording };
|
||||
|
||||
@register({ GTypeName: "Recording" })
|
||||
class Recording extends GObject.Object {
|
||||
export class Recording extends GObject.Object {
|
||||
private static instance: Recording;
|
||||
|
||||
@signal() started() {};
|
||||
@@ -21,6 +20,7 @@ class Recording extends GObject.Object {
|
||||
|
||||
#recording: boolean = false;
|
||||
#path: string = "~/Recordings";
|
||||
#recordingScope?: Scope;
|
||||
|
||||
/** Default extension: mp4(h264) */
|
||||
#extension: string = "mp4";
|
||||
@@ -64,7 +64,22 @@ class Recording extends GObject.Object {
|
||||
this.notify("extension");
|
||||
}
|
||||
|
||||
/** Recording output file name. %NULL if screen is not being recorded */
|
||||
@getter(String)
|
||||
public get recordingTime() {
|
||||
if(!this.#recording || !this.#startedAt)
|
||||
return "not recording";
|
||||
|
||||
const startedAtSeconds = time.get().to_unix() - Recording.getDefault().startedAt!;
|
||||
if(startedAtSeconds <= 0) return "00:00";
|
||||
|
||||
const seconds = Math.floor(startedAtSeconds % 60);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
return `${hours > 0 ? `${hours < 10 ? '0' : ""}${hours}` : ""}${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`;
|
||||
}
|
||||
|
||||
/** Recording output file name. null if screen is not being recorded */
|
||||
public get output() { return this.#output; }
|
||||
|
||||
/** Currently unsupported property */
|
||||
@@ -90,41 +105,51 @@ class Recording extends GObject.Object {
|
||||
}
|
||||
|
||||
public startRecording(area?: Gdk.Rectangle) {
|
||||
if(this.recording)
|
||||
if(this.#recording)
|
||||
throw new Error("Screen Recording is already running!");
|
||||
|
||||
this.#output = `${time.get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension || "mp4"}`;
|
||||
this.#recording = true;
|
||||
this.notify("recording");
|
||||
this.emit("started");
|
||||
makeDirectory(this.path);
|
||||
createRoot(() => {
|
||||
this.#recordingScope = getScope();
|
||||
|
||||
const cancellable = Gio.Cancellable.new();
|
||||
cancellable.cancel = () => {};
|
||||
this.#output = `${time.get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension || "mp4"}`;
|
||||
this.#recording = true;
|
||||
this.notify("recording");
|
||||
this.emit("started");
|
||||
makeDirectory(this.path);
|
||||
|
||||
const areaString = `${area?.x ?? 0},${area?.y ?? 0} ${area?.width ?? 1}x${area?.height ?? 1}`;
|
||||
const areaString = `${area?.x ?? 0},${area?.y ?? 0} ${area?.width ?? 1}x${area?.height ?? 1}`;
|
||||
|
||||
this.#process = Gio.Subprocess.new([
|
||||
"wf-recorder",
|
||||
...(area ? [ `-g`, areaString ] : []),
|
||||
"-f",
|
||||
`${this.path}/${this.output!}`
|
||||
], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
|
||||
this.#process = Gio.Subprocess.new([
|
||||
"wf-recorder",
|
||||
...(area ? [ `-g`, areaString ] : []),
|
||||
"-f",
|
||||
`${this.path}/${this.output!}`
|
||||
], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
|
||||
|
||||
this.#process.wait_async(cancellable, () => {
|
||||
this.stopRecording();
|
||||
this.#process.wait_async(null, () => {
|
||||
this.stopRecording();
|
||||
});
|
||||
|
||||
this.#startedAt = time.get().to_unix();
|
||||
this.notify("started-at");
|
||||
|
||||
const timeSub = time.subscribe(() => {
|
||||
this.notify("recording-time");
|
||||
});
|
||||
|
||||
this.#recordingScope.onCleanup(timeSub);
|
||||
});
|
||||
|
||||
this.#startedAt = time.get().to_unix();
|
||||
}
|
||||
|
||||
public stopRecording() {
|
||||
if(!this.#process) return;
|
||||
if(!this.#process || !this.#recording) return;
|
||||
|
||||
!this.#process.get_if_exited() && execAsync([
|
||||
"kill", "-s", "SIGTERM", this.#process.get_identifier()!
|
||||
]);
|
||||
|
||||
this.#recordingScope?.dispose();
|
||||
|
||||
const path = this.#path;
|
||||
const output = this.#output;
|
||||
|
||||
@@ -138,13 +163,8 @@ class Recording extends GObject.Object {
|
||||
Notifications.getDefault().sendNotification({
|
||||
actions: [
|
||||
{
|
||||
text: "View",
|
||||
onAction: () => {
|
||||
execAsync(["nautilus", "-s", output!, path]);
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "Open",
|
||||
text: "View", // will be hidden(can be triggered by clicking in the notification)
|
||||
id: "view",
|
||||
onAction: () => {
|
||||
execAsync(["xdg-open", `${path}/${output}`]);
|
||||
}
|
||||
|
||||
+42
-6
@@ -1,6 +1,6 @@
|
||||
import { createPoll } from "ags/time";
|
||||
import { exec, execAsync } from "ags/process";
|
||||
import { Accessor, For, With } from "ags";
|
||||
import { Accessor, For, getScope, onCleanup, With } from "ags";
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { getSymbolicIcon } from "./apps";
|
||||
|
||||
@@ -217,7 +217,7 @@ export function addSliderMarksFromMinMax(slider: Astal.Slider, amountOfMarks: nu
|
||||
}
|
||||
|
||||
/** initialize and sub class properties with accessors */
|
||||
export function construct(klass: object, props: Record<any, any|Accessor<any>>): Array<() => void> {
|
||||
export function construct<Class extends object>(klass: Class, props: Record<any, any|Accessor<any>>): Array<() => void> {
|
||||
|
||||
const subs: Array<() => void> = [];
|
||||
const isGObject = klass instanceof GObject.Object;
|
||||
@@ -228,16 +228,52 @@ export function construct(klass: object, props: Record<any, any|Accessor<any>>):
|
||||
if(v === undefined) return;
|
||||
if(v instanceof Accessor) {
|
||||
subs.push(v.subscribe(() => {
|
||||
klass[k as keyof typeof klass] = v.get() as never;
|
||||
if(isGObject) klass.notify(k);
|
||||
klass[k as keyof Class] = v.get() as Class[keyof Class];
|
||||
if(isGObject)
|
||||
klass.notify(k.replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`
|
||||
));
|
||||
}));
|
||||
|
||||
klass[k as keyof typeof klass] = v.get() as never;
|
||||
klass[k as keyof Class] = v.get() as Class[keyof Class];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
klass[k as keyof typeof klass] = v as never;
|
||||
klass[k as keyof Class] = v as Class[keyof Class];
|
||||
});
|
||||
|
||||
return subs;
|
||||
}
|
||||
|
||||
/** open connections to gobjects that are closed when the scope
|
||||
* is disposed
|
||||
* @experimental
|
||||
* */
|
||||
export function createConnetions<
|
||||
GObj extends GObject.Object,
|
||||
Signals extends GObj["$signals"],
|
||||
Signal extends keyof Signals,
|
||||
Callback extends Signals[Signal]
|
||||
>(...conns: Array<[GObj, Signal, Callback]>): void {
|
||||
const scope = getScope();
|
||||
|
||||
const connections: Map<GObj, Array<number>> = new Map();
|
||||
|
||||
scope.onCleanup(() => connections.forEach((ids, gobj) =>
|
||||
ids.forEach(id => gobj.disconnect(id))
|
||||
));
|
||||
|
||||
function add(gobj: GObj, id: number): void {
|
||||
if(connections.has(gobj)) {
|
||||
connections.get(gobj)!.push(id);
|
||||
return;
|
||||
}
|
||||
|
||||
connections.set(gobj, [id]);
|
||||
}
|
||||
|
||||
conns.forEach(([gobj, sig, callback]) => {
|
||||
// type stuff
|
||||
add(gobj, gobj.connect(sig as string, callback as never));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user