From ec43d7b57ac3998f0152faae972bfc97d8031e6c Mon Sep 17 00:00:00 2001 From: Doloro1978 Date: Sun, 13 Sep 2026 19:47:01 +0100 Subject: [PATCH] things --- Cargo.lock | 113 ++++++ crates/common/src/config.rs | 2 + crates/common/src/display.rs | 5 +- crates/common/src/messages/services/mod.rs | 2 + .../common/src/messages/services/pipewire.rs | 36 ++ crates/modules/src/audio.rs | 106 ++++++ crates/modules/src/lib.rs | 2 + crates/services/Cargo.toml | 2 + crates/services/src/lib.rs | 2 + crates/services/src/pipewire.rs | 321 ++++++++++++++++++ flake.nix | 12 +- test.toml | 4 +- 12 files changed, 602 insertions(+), 5 deletions(-) create mode 100644 crates/common/src/messages/services/pipewire.rs create mode 100644 crates/modules/src/audio.rs create mode 100644 crates/services/src/pipewire.rs diff --git a/Cargo.lock b/Cargo.lock index 4bb4c26..7701d39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2335,6 +2335,69 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2765,6 +2828,48 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pipewire-native" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4da5bc428cfd0d93a1f2c7f996bb2a7ce62151f773fea900a636e5c3aeaa67" +dependencies = [ + "bitflags 2.13.2", + "libc", + "parking_lot", + "paste", + "pipewire-native-macros", + "pipewire-native-spa", + "pkg-config", + "tinyjson", +] + +[[package]] +name = "pipewire-native-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a3b131b8b239675111a3d501d877fec5c8fda9239fef4132a764c103dc0cf7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pipewire-native-spa" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f7901f35d9a00e9ced18907b4a4c79aa7293028274311fad001b048f644dd50" +dependencies = [ + "bitflags 2.13.2", + "cc", + "libc", + "libloading", + "num", + "pipewire-native-macros", + "pkg-config", +] + [[package]] name = "pkg-config" version = "0.3.34" @@ -3396,6 +3501,8 @@ dependencies = [ "common", "hyprland", "iced", + "pipewire-native", + "pipewire-native-spa", "reqwest", "serde", "serde_json", @@ -3822,6 +3929,12 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tinyjson" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a" + [[package]] name = "tinystr" version = "0.8.4" diff --git a/crates/common/src/config.rs b/crates/common/src/config.rs index b1e72e0..b1885cd 100644 --- a/crates/common/src/config.rs +++ b/crates/common/src/config.rs @@ -16,6 +16,7 @@ pub struct BarbarConfig { #[derive(serde::Deserialize, Serialize, Clone, Copy, Debug)] #[serde(rename_all = "lowercase")] pub enum Modules { + Audio, Clock, Weather, Workspaces, @@ -24,6 +25,7 @@ pub enum Modules { impl Modules { pub fn to_string(&self) -> &str { match self { + Modules::Audio => "audio", Modules::Clock => "clock", Modules::Weather => "weather", Modules::Workspaces => "workspaces", diff --git a/crates/common/src/display.rs b/crates/common/src/display.rs index 9d71ad0..d79d2ce 100644 --- a/crates/common/src/display.rs +++ b/crates/common/src/display.rs @@ -3,11 +3,13 @@ //! Keeps the bar surface's geometry and renderer flags in one place, so `core` //! only reads config and hands the result straight to the daemon. -use iced::Pixels; +use iced::{Font, Pixels}; use iced_layershell::reexport::{Anchor, KeyboardInteractivity}; use iced_layershell::settings::{LayerShellSettings, Settings, StartMode}; use serde::{Deserialize, Serialize}; +/// Bar text font, matching quickshell's `CaskaydiaCove NFM`. +const DEFAULT_FONT: &str = "CaskaydiaCove NFM"; /// Bar height in logical px when `[display].height` is omitted. const DEFAULT_HEIGHT: u32 = 36; /// Default text size in logical px. @@ -75,6 +77,7 @@ impl Display { pub fn settings(&self) -> Settings { Settings { layer_settings: self.layer_settings(), + default_font: Font::with_name(DEFAULT_FONT), default_text_size: Pixels(self.default_text_size), antialiasing: self.antialiasing, ..Default::default() diff --git a/crates/common/src/messages/services/mod.rs b/crates/common/src/messages/services/mod.rs index 8690b19..4bb301a 100644 --- a/crates/common/src/messages/services/mod.rs +++ b/crates/common/src/messages/services/mod.rs @@ -2,8 +2,10 @@ mod datetime; mod hyprland; +mod pipewire; mod weather; pub use datetime::*; pub use hyprland::*; +pub use pipewire::*; pub use weather::*; diff --git a/crates/common/src/messages/services/pipewire.rs b/crates/common/src/messages/services/pipewire.rs new file mode 100644 index 0000000..680588b --- /dev/null +++ b/crates/common/src/messages/services/pipewire.rs @@ -0,0 +1,36 @@ +//! PipeWire audio messages. + +use crate::Service; + +/// Route key a module subscribes under to receive the audio state. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum PipewireMsg { + State, +} + +impl PipewireMsg { + /// Route key a module subscribes under to receive this service's state. + pub fn key(self) -> Service { + Service(match self { + Self::State => "pipewire.state", + }) + } +} + +/// One audio node's level, as PipeWire reports it. +#[derive(Clone, Copy, PartialEq, Debug, Default)] +pub struct AudioNode { + /// Linear volume fraction: 1.0 is 100%, above 1.0 is boost. + pub volume: f32, + /// Node is muted; `volume` is still the level it will unmute to. + pub muted: bool, +} + +/// Default sink and default source state, published whenever either changes. +#[derive(Clone, Copy, PartialEq, Debug, Default)] +pub struct PipewireState { + /// Default playback device. + pub sink: AudioNode, + /// Default capture device. + pub source: AudioNode, +} diff --git a/crates/modules/src/audio.rs b/crates/modules/src/audio.rs new file mode 100644 index 0000000..2aaa3f8 --- /dev/null +++ b/crates/modules/src/audio.rs @@ -0,0 +1,106 @@ +//! Audio module: the default sink and source as icons, drawn only while +//! something is muted — the signal the quickshell Audio widget gives. + +use iced::widget::{container, row, space, text}; +use iced::{border, Alignment, Element, Length, Task, Theme}; + +use common::{AudioNode, BarModule, ModuleEffect, PipewireMsg, PipewireState, Service, Wire}; + +/// Glyphs exactly as the Quickshell Audio widget draws them: the speaker is a +/// Material `md-volume` icon, the mic a Font Awesome one. +const SINK_ON: &str = "\u{f057e}"; // md-volume_high +const SINK_MUTED: &str = "\u{f0581}"; // md-volume_off +const MIC_ON: &str = "\u{f130}"; // fa-microphone +const MIC_MUTED: &str = "\u{f131}"; // fa-microphone_slash + +/// Quickshell's geometry at its 20 px reference bar: a 24 px slot per icon +/// and 4 px between them. Heights and the pill fill the bar through layout, +/// but font size is a fixed px value iced cannot derive from the space it is +/// given — if the bar ever leaves 20 px, scale the two sizes here. +const REF_SLOT_W: f32 = 24.0; +const REF_SPACING: f32 = 4.0; +const REF_RADIUS: f32 = 3.5; +/// The speaker is re-drawn in the mono face, whose ink is 1168/1496 the size +/// of the proportional face quickshell uses, so it is pre-compensated. +const REF_SINK_SIZE: f32 = 20.0 * 1496.0 / 1168.0; +const REF_MIC_SIZE: f32 = 30.0; + +pub struct Audio { + state: PipewireState, +} + +impl Audio { + pub fn new() -> Self { + Self { + state: PipewireState::default(), + } + } +} + +impl BarModule for Audio { + fn id(&self) -> &'static str { + "audio" + } + + fn view(&self, _window_id: Option) -> Element<'_, Wire> { + let (sink, source) = (self.state.sink, self.state.source); + if !sink.muted && !source.muted { + // Nothing is muted, so there is nothing to show. + return space::horizontal().into(); + } + + let icons = row![ + icon(glyph(sink, SINK_ON, SINK_MUTED), REF_SINK_SIZE), + icon(glyph(source, MIC_ON, MIC_MUTED), REF_MIC_SIZE), + ] + .height(Length::Fill) + .spacing(REF_SPACING); + + container(icons) + .height(Length::Fill) + .style(pill) + .into() + } + + fn update(&mut self, msg: Wire) -> Task { + if let Some(state) = msg.downcast::() { + self.state = *state; + } + Task::none() + } + + fn services(&self) -> Vec { + vec![PipewireMsg::State.key()] + } +} + +/// One glyph centred in a fixed-width slot that fills the bar's height. +fn icon<'a>(glyph: &'static str, size: f32) -> Element<'a, Wire> { + container( + text(glyph) + .size(size) + .height(Length::Fill) + .align_y(Alignment::Center), + ) + .center_x(Length::Fixed(REF_SLOT_W)) + .height(Length::Fill) + .into() +} + +/// The muted glyph when the node is muted, its normal glyph otherwise. +fn glyph(node: AudioNode, on: &'static str, muted: &'static str) -> &'static str { + if node.muted { + muted + } else { + on + } +} + +/// The quickshell pill: rounded, a shade lighter than the bar behind it. +fn pill(theme: &Theme) -> container::Style { + container::Style { + background: Some(theme.extended_palette().background.weak.color.into()), + border: border::rounded(REF_RADIUS), + ..container::Style::default() + } +} diff --git a/crates/modules/src/lib.rs b/crates/modules/src/lib.rs index de5cc9c..296b227 100644 --- a/crates/modules/src/lib.rs +++ b/crates/modules/src/lib.rs @@ -1,6 +1,7 @@ //! Bar module registry: constructs every enabled module. Adding a module is //! a new file under `src/` plus one entry in `all()` — core never changes. +mod audio; mod clock; mod weather; mod workspaces; @@ -13,6 +14,7 @@ pub use weather::*; /// One instance of every enabled module, ready to insert by `id()`. pub fn all(module_config: Table) -> Vec> { vec![ + Box::new(audio::Audio::new()), Box::new(clock::Clock::new(Some(module_config.clone()))), Box::new(weather::WeatherModule::default()), Box::new(workspaces::Workspaces::new(Some(module_config))), diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml index b9817e4..5408099 100644 --- a/crates/services/Cargo.toml +++ b/crates/services/Cargo.toml @@ -16,4 +16,6 @@ chrono = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } reqwest = { version = "0.13.5", features = ["json"] } +pipewire-native = "0.1.4" +pipewire-native-spa = "0.1.4" hyprland = { git = "https://github.com/hyprland-community/hyprland-rs", branch = "master", default-features = false, features = ["listener", "tokio"] } diff --git a/crates/services/src/lib.rs b/crates/services/src/lib.rs index 265da28..5b8e04a 100644 --- a/crates/services/src/lib.rs +++ b/crates/services/src/lib.rs @@ -8,6 +8,7 @@ use common::{Inbox, Service, Wire}; pub mod compositors; pub mod datetime; +pub mod pipewire; pub mod weather; /// Conversion from a route key + its inbox to the subscription backing it. @@ -28,6 +29,7 @@ impl IntoSubscription for Service { compositors::hyprland::HyprlandService::run(inbox) } k if k.starts_with(weather::NAMESPACE) => weather::WeatherService::run(inbox), + k if k.starts_with(pipewire::NAMESPACE) => pipewire::PipewireService::run(inbox), _ => { tracing::warn!(service = key, "no service impl for route key"); Subscription::none() diff --git a/crates/services/src/pipewire.rs b/crates/services/src/pipewire.rs new file mode 100644 index 0000000..0c75f7e --- /dev/null +++ b/crates/services/src/pipewire.rs @@ -0,0 +1,321 @@ +//! PipeWire service: follows the default sink and source — WirePlumber names +//! them in its `default` metadata — and republishes their volume/mute +//! whenever PipeWire reports a change. +//! +//! PipeWire drives its own main loop, so the connection lives on a dedicated +//! thread; that thread hands state to the async side over a channel. + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use common::{AudioNode, Endpoint, Inbox, PipewireState, Wire}; +use iced::futures::{channel::mpsc, SinkExt}; +use iced::Subscription; +use pipewire_native as pw; +use pipewire_native::context::Context; +use pipewire_native::core::Core; +use pipewire_native::main_loop::MainLoop; +use pipewire_native::properties::Properties; +use pipewire_native::proxy::metadata::{Metadata, MetadataEvents}; +use pipewire_native::proxy::node::{Node, NodeEvents}; +use pipewire_native::proxy::registry::{Registry, RegistryEvents}; +use pipewire_native::proxy::{HasProxy, ProxyEvents}; +use pipewire_native::some_closure; +use pipewire_native::types; +use pipewire_native_spa as spa; +use tokio::sync::mpsc as tokio_mpsc; + +/// Route-key namespace owned by this service; keys are `"pipewire."`. +pub const NAMESPACE: &str = "pipewire."; + +/// Node classes the service follows. +const SINK: &str = "Audio/Sink"; +const SOURCE: &str = "Audio/Source"; + +/// WirePlumber's metadata object holding the default nodes. +const DEFAULTS: &str = "default"; +const DEFAULT_SINK: &str = "default.audio.sink"; +const DEFAULT_SOURCE: &str = "default.audio.source"; + +pub struct PipewireService; + +impl PipewireService { + pub fn run(inbox: Inbox) -> Subscription { + Subscription::run_with(inbox, |inbox| { + let key = inbox.key(); + // We dont need to rx any msgs from modules + let mut _rx = inbox.take().expect("inbox receiver is taken once"); + + iced::stream::channel(0, move |mut sender: mpsc::Sender| async move { + // The PipeWire thread owns the connection and drops it once + // this channel goes away, which stops its loop. + let (tx, mut rx) = tokio_mpsc::unbounded_channel(); + std::thread::spawn(move || connect(tx)); + while let Some(state) = rx.recv().await { + tracing::debug!(?state, "pipewire state changed"); + let wire = Wire::topic(Endpoint::service(key), key, state); + if sender.send(wire).await.is_err() { + tracing::warn!(key, "pipewire send failed; stopping service"); + return; + } + } + }) + }) + } +} + +/// Connects, follows the default nodes, and runs PipeWire's loop until the +/// subscriber (or PipeWire itself) goes away. +fn connect(tx: tokio_mpsc::UnboundedSender) { + pw::init(); + let conn = match Connection::open() { + Ok(conn) => conn, + Err(err) => { + tracing::warn!(%err, "pipewire unavailable; audio state stays unknown"); + return; + } + }; + + let track = Arc::new(Mutex::new(Track::new(conn, tx))); + watch(&track); + let main_loop = track.lock().unwrap().conn.main_loop.clone(); + + tracing::info!("pipewire connected"); + main_loop.run(); + tracing::info!("pipewire disconnected"); +} + +/// The connection chain: PipeWire tears the session down if any link of it +/// goes away, so all of it lives for as long as the loop runs. +struct Connection { + main_loop: MainLoop, + _context: Context, + _core: Core, + registry: Registry, +} + +impl Connection { + fn open() -> std::io::Result { + let main_loop = MainLoop::new(&Properties::new()) + .ok_or_else(|| std::io::Error::other("pipewire main loop unavailable"))?; + let context = Context::new(&main_loop, Properties::new())?; + let core = context.connect(None)?; + let registry = core.registry()?; + Ok(Self { + main_loop, + _context: context, + _core: core, + registry, + }) + } +} + +/// Subscribes to every sink/source node and to the `default` metadata, so +/// PipeWire pushes volume/mute changes instead of us polling for them. +fn watch(track: &Arc>) { + let registry = track.lock().unwrap().conn.registry.clone(); + registry.add_listener(RegistryEvents { + global: some_closure!([registry ^(track)] id, _perms, type_, version, props, { + match type_ { + types::interface::NODE => { + let class = props.get("media.class").unwrap_or_default(); + if class != SINK && class != SOURCE { + return; + } + if let Some(name) = props.get("node.name") { + bind_node(track, ®istry, id, type_, version, name.to_string()); + } + } + types::interface::METADATA if props.get("metadata.name") == Some(DEFAULTS) => { + bind_defaults(track, ®istry, id, type_, version); + } + _ => {} + } + }), + global_remove: some_closure!([^(track)] id, { + let mut track = track.lock().unwrap(); + if let Some(name) = track.ids.remove(&id) { + track.nodes.remove(&name); + track.publish(); + } + }), + }); +} + +/// Binds a sink/source node and follows its `Props` param (volume + mute). +fn bind_node( + track: &Arc>, + registry: &Registry, + id: pw::Id, + type_: &str, + version: u32, + name: String, +) { + let Ok(object) = registry.bind(id, type_, version) else { + return; + }; + let Some(proxy) = object.downcast_proxy::() else { + return; + }; + let Some(node) = proxy.object() else { + return; + }; + + node.add_listener(NodeEvents { + param: some_closure!([^(track, name)] _seq, id, _index, _next, pod, { + if id != spa::param::ParamType::Props { + return; + } + let mut track = track.lock().unwrap(); + merge(track.nodes.entry(name.clone()).or_default(), pod); + track.publish(); + }), + ..Default::default() + }); + + // A local proxy is only usable once the server has bound it, and asking + // for params is a method call. + proxy.add_listener(ProxyEvents { + bound_props: some_closure!([^(node)] _id, _props, { + let _ = node.subscribe_params(&[spa::param::ParamType::Props]); + let _ = node.enum_params(0, Some(spa::param::ParamType::Props), 0, u32::MAX, None); + }), + ..Default::default() + }); + + tracing::debug!(name, "following pipewire node"); + let mut track = track.lock().unwrap(); + track.ids.insert(id, name); + track.keep.push(object); +} + +/// Binds WirePlumber's `default` metadata and follows which nodes are the +/// current sink and source. +fn bind_defaults( + track: &Arc>, + registry: &Registry, + id: pw::Id, + type_: &str, + version: u32, +) { + let Ok(object) = registry.bind(id, type_, version) else { + return; + }; + let Some(proxy) = object.downcast_proxy::() else { + return; + }; + let Some(metadata) = proxy.object() else { + return; + }; + + metadata.add_listener(MetadataEvents { + property: some_closure!([^(track)] _subject, key, _type, value, { + let (Some(key), Some(value)) = (key, value) else { + return; + }; + let Some(name) = default_node(value) else { + return; + }; + let mut track = track.lock().unwrap(); + match key { + DEFAULT_SINK => track.sink = Some(name), + DEFAULT_SOURCE => track.source = Some(name), + _ => return, + } + track.publish(); + }), + }); + + tracing::debug!("following pipewire default metadata"); + track.lock().unwrap().keep.push(object); +} + +/// Merges the volume/mute carried by a `SPA_PARAM_Props` pod into `state`; +/// anything the pod leaves out keeps its previous value. +fn merge(state: &mut AudioNode, pod: &spa::pod::RawPodOwned) { + let mut parser = spa::pod::parser::Parser::new(pod.data()); + let _ = parser.pop_object::(|props, _id| { + for (key, _flags, value) in props { + match key { + spa::param::props::Prop::Volume => { + if let Ok(volume) = value.decode::() { + state.volume = volume; + } + } + spa::param::props::Prop::Mute => { + if let Ok(muted) = value.decode::() { + state.muted = muted; + } + } + _ => {} + } + } + Ok(()) + }); +} + +/// WirePlumber stores a default as `{"name":""}`. +fn default_node(value: &str) -> Option { + #[derive(serde::Deserialize)] + struct Default { + name: String, + } + serde_json::from_str::(value).ok().map(|d| d.name) +} + +/// What the PipeWire callbacks share: the connection, the audio state they +/// observe, and the channel home. +struct Track { + conn: Connection, + /// Volume/mute of every sink and source, keyed by `node.name`. + nodes: HashMap, + /// Registry id -> `node.name`, so a removed node can be forgotten. + ids: HashMap, + /// `node.name` of the default sink/source, per the metadata. + sink: Option, + source: Option, + /// Bound proxies, kept alive so their listeners stay registered. + keep: Vec>, + tx: tokio_mpsc::UnboundedSender, + /// Last state published, so redundant updates are dropped. + sent: Option, +} + +impl Track { + fn new(conn: Connection, tx: tokio_mpsc::UnboundedSender) -> Self { + Self { + conn, + nodes: HashMap::new(), + ids: HashMap::new(), + sink: None, + source: None, + keep: Vec::new(), + tx, + sent: None, + } + } + + /// Publishes the default sink/source, if the state actually changed. + fn publish(&mut self) { + let state = PipewireState { + sink: self.node(&self.sink), + source: self.node(&self.source), + }; + if self.sent == Some(state) { + return; + } + self.sent = Some(state); + if self.tx.send(state).is_err() { + tracing::info!("pipewire subscriber gone; disconnecting"); + self.conn.main_loop.quit(); + } + } + + /// State of the named node; zeroes while the default is unknown. + fn node(&self, name: &Option) -> AudioNode { + name.as_ref() + .and_then(|name| self.nodes.get(name)) + .copied() + .unwrap_or_default() + } +} diff --git a/flake.nix b/flake.nix index 66e7d97..12e0112 100644 --- a/flake.nix +++ b/flake.nix @@ -75,6 +75,9 @@ nativeBuildInputs = [ pkgs.pkg-config + # libspa-0.2.pc: pipewire-native bakes the SPA plugin directory + # from it, and loads libspa-support from there at runtime. + pkgs.pipewire.dev ] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.mold @@ -115,7 +118,9 @@ nativeBuildInputs = [ pkgs.makeWrapper ]; postBuild = '' wrapProgram $out/bin/core \ - --prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath guiLibs} + --prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath guiLibs} \ + --set SPA_PLUGIN_DIR ${pkgs.pipewire}/lib/spa-0.2 \ + --set PIPEWIRE_CONFIG_DIR ${pkgs.pipewire}/share/pipewire ''; meta.mainProgram = "core"; }; @@ -144,10 +149,13 @@ # every dependency on each switch. shellHook = '' export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.lib.makeLibraryPath guiLibs}" + # pipewire-native dlopens SPA plugins and reads client.conf. + export SPA_PLUGIN_DIR="${pkgs.pipewire}/lib/spa-0.2" + export PIPEWIRE_CONFIG_DIR="${pkgs.pipewire}/share/pipewire" ''; # Extra inputs can be added here; cargo and rustc are provided by default. - packages = guiLibs ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.mold ]; + packages = guiLibs ++ [ pkgs.pipewire.dev ] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.mold ]; }; } )) diff --git a/test.toml b/test.toml index 221ec22..50a2bcd 100644 --- a/test.toml +++ b/test.toml @@ -2,7 +2,7 @@ # Output to pin the bar to; omit to follow the active output. monitor = "DP-2" # Bar height in logical px (default 36); reserved exclusively. -height = 24 +height = 20 # MSAA for triangle primitives (does not affect quad borders). antialiasing = true # Default text size in logical px (default 16). @@ -15,7 +15,7 @@ spacing = 8.0 [order] left = ["workspaces", "weather"] middle = [] -right = ["clock"] +right = ["audio", "clock"] [modules.clock] show_seconds = false