summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-03-27 18:02:40 +1100
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-03-27 18:02:40 +1100
commit72e349e368f6d4600512354e75551b380cbf70c7 (patch)
tree0668c7b511457c84dc2dd3691e13004943119d27 /src
parentsidebar: change streams unavailable text (diff)
downloadcaelestia-shell-72e349e368f6d4600512354e75551b380cbf70c7.tar.gz
caelestia-shell-72e349e368f6d4600512354e75551b380cbf70c7.tar.bz2
caelestia-shell-72e349e368f6d4600512354e75551b380cbf70c7.zip
sidebar: audio device selector module
Diffstat (limited to 'src')
-rw-r--r--src/modules/sidebar/audio.tsx3
-rw-r--r--src/modules/sidebar/modules/deviceselector.tsx126
2 files changed, 129 insertions, 0 deletions
diff --git a/src/modules/sidebar/audio.tsx b/src/modules/sidebar/audio.tsx
index a5c1651..20a6551 100644
--- a/src/modules/sidebar/audio.tsx
+++ b/src/modules/sidebar/audio.tsx
@@ -1,3 +1,4 @@
+import DeviceSelector from "./modules/deviceselector";
import Media from "./modules/media";
import Streams from "./modules/streams";
@@ -6,5 +7,7 @@ export default () => (
<Media />
<box className="separator" />
<Streams />
+ <box className="separator" />
+ <DeviceSelector />
</box>
);
diff --git a/src/modules/sidebar/modules/deviceselector.tsx b/src/modules/sidebar/modules/deviceselector.tsx
new file mode 100644
index 0000000..03acc97
--- /dev/null
+++ b/src/modules/sidebar/modules/deviceselector.tsx
@@ -0,0 +1,126 @@
+import { bind, execAsync, Variable, type Binding } from "astal";
+import { Astal, Gtk } from "astal/gtk3";
+import AstalWp from "gi://AstalWp";
+
+const Device = ({
+ input,
+ defaultDevice,
+ showDropdown,
+ device,
+}: {
+ input?: boolean;
+ defaultDevice: Binding<AstalWp.Endpoint>;
+ showDropdown: Variable<boolean>;
+ device: AstalWp.Endpoint;
+}) => (
+ <button
+ visible={defaultDevice.get().id !== device.id}
+ cursor="pointer"
+ onClicked={() => {
+ execAsync(`wpctl set-default ${device.id}`).catch(console.error);
+ showDropdown.set(false);
+ }}
+ setup={self => {
+ let last: { d: AstalWp.Endpoint; id: number } | null = {
+ d: defaultDevice.get(),
+ id: defaultDevice
+ .get()
+ .connect("notify::id", () => self.set_visible(defaultDevice.get().id !== device.id)),
+ };
+ self.hook(defaultDevice, (_, d) => {
+ last?.d.disconnect(last.id);
+ self.set_visible(d.id !== device.id);
+ last = {
+ d,
+ id: d.connect("notify::id", () => self.set_visible(d.id !== device.id)),
+ };
+ });
+ self.connect("destroy", () => last?.d.disconnect(last.id));
+ }}
+ >
+ <box className="device">
+ {bind(device, "icon").as(i =>
+ Astal.Icon.lookup_icon(i) ? (
+ <icon className="icon" icon={device.icon} />
+ ) : (
+ <label className="icon" label={input ? "mic" : "media_output"} />
+ )
+ )}
+ <label label={bind(device, "description")} />
+ </box>
+ </button>
+);
+
+const DefaultDevice = ({ input, device }: { input?: boolean; device: AstalWp.Endpoint }) => (
+ <box className="selected">
+ <label className="icon" label={input ? "mic" : "media_output"} />
+ <box vertical>
+ <label
+ truncate
+ xalign={0}
+ label={bind(device, "description").as(d => (input ? "[In] " : "[Out] ") + (d ?? "Unknown"))}
+ />
+ <label
+ xalign={0}
+ className="sublabel"
+ label={bind(device, "volume").as(v => `Volume ${Math.round(v * 100)}%`)}
+ />
+ </box>
+ </box>
+);
+
+const Selector = ({ input, audio }: { input?: boolean; audio: AstalWp.Audio }) => {
+ const showDropdown = Variable(false);
+ const defaultDevice = bind(audio, input ? "defaultMicrophone" : "defaultSpeaker");
+
+ return (
+ <box vertical className="selector">
+ <revealer
+ transitionType={Gtk.RevealerTransitionType.SLIDE_UP}
+ transitionDuration={150}
+ revealChild={bind(showDropdown)}
+ >
+ <box vertical className="list">
+ {bind(audio, input ? "microphones" : "speakers").as(ds =>
+ ds.map(d => (
+ <Device
+ input={input}
+ defaultDevice={defaultDevice}
+ showDropdown={showDropdown}
+ device={d}
+ />
+ ))
+ )}
+ <box className="separator" />
+ </box>
+ </revealer>
+ <button cursor="pointer" onClick={() => showDropdown.set(!showDropdown.get())}>
+ {defaultDevice.as(d => (
+ <DefaultDevice input={input} device={d} />
+ ))}
+ </button>
+ </box>
+ );
+};
+
+const NoWp = () => (
+ <box vexpand homogeneous>
+ <box vertical valign={Gtk.Align.CENTER}>
+ <label label="Device selector unavailable" />
+ <label className="no-wp-prompt" label="WirePlumber is required for this module" />
+ </box>
+ </box>
+);
+
+export default () => {
+ const audio = AstalWp.get_default()?.get_audio();
+
+ if (!audio) return <NoWp />;
+
+ return (
+ <box vertical className="device-selector">
+ <Selector input audio={audio} />
+ <Selector audio={audio} />
+ </box>
+ );
+};