Skip to content
Open
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
4b76965
feat: impl varlink service w/ audio APIs
mmstick Mar 12, 2026
7ca1606
build(debian): add libclang-dev to Build-Depends
mmstick Mar 17, 2026
c96571d
tests(audio-server): add dev-dependencies needed to run tests
mmstick Mar 23, 2026
91c6b94
refactor(varlink-server): prefix audio interface methods
mmstick Mar 23, 2026
d43b60f
fix(audio): round volume values to fix volume raise shortcut
mmstick Mar 23, 2026
74a96a2
fix(audio-server): use port type as fallback for headset detection
mmstick Mar 31, 2026
5437b87
fix(audio-server): also consider source routes when detecting headset…
mmstick Mar 31, 2026
f3a6c76
chore: add debug logs
mmstick Apr 1, 2026
628a2a3
fix(audio-server): select headset route which has headset port type
mmstick Apr 1, 2026
aea459b
chore: add a few more debug logs
mmstick Apr 1, 2026
bf0773d
chore: add device to log
mmstick Apr 1, 2026
4e7d3f9
chore: lower delay for ignore of device_headset_check to 1 second aft…
mmstick Apr 1, 2026
9059b17
fix(audio-server): do not skip headset detection for devices availabl…
mmstick Apr 1, 2026
53e4843
fix(audio-server): reset headset check when headset route is unplugged
mmstick Apr 1, 2026
fcd5400
chore(audio-server): remove dbg logging
mmstick Apr 21, 2026
17c66bf
chore(audio-client): re-export zlink
mmstick Apr 21, 2026
15d0917
fix(cosmic-pipewire): RemoveDevice message not emitted
mmstick Apr 21, 2026
10e3223
fix(audio-server): active profile messages not being emitted
mmstick Apr 22, 2026
3e29c5d
fix(audio-server): volume set to 0 when a volume property update is e…
mmstick Apr 22, 2026
3b029d5
fix(audio-server): only show dialog for physical plugs (no bt)
hojjatabdollahi Apr 22, 2026
13f3605
fix(audio-server): detect when a speaker route changes to a headphone
hojjatabdollahi Apr 23, 2026
1a52abc
fix(audio-backend): adjust profile priority 1/2
mmstick May 6, 2026
14866f1
fix(audio-backend): adjust profile priority 2/2
mmstick May 6, 2026
a2871d6
fix(audio-backend): avoid node volume overwrite if set before node wa…
mmstick May 7, 2026
9b15374
Revert "fix(audio-server): detect when a speaker route changes to a h…
mmstick May 7, 2026
38297f3
Revert "fix(audio-server): only show dialog for physical plugs (no bt)"
mmstick May 7, 2026
ba9c394
fix(audio-backend): on default node set, check before assuming a node…
mmstick May 8, 2026
3ab040b
chore(audio-backend): no need to clear active sink/source name
mmstick May 8, 2026
47a168f
fix(cosmic-pipewire): emit default source/sink event even if name is …
mmstick May 8, 2026
34d73c5
fix(audio-server): handle default source/sink event even if name is u…
mmstick May 8, 2026
f7eafa9
chore(audio-server): no need to clear default sink/source node name
mmstick May 8, 2026
59f351a
feat: update MSRV to Rust 1.93
mmstick May 8, 2026
3796fdd
feat: update zlink to 0.5.0
mmstick May 8, 2026
767a1fd
feat(audio-server): add SetRoute varlink method
mmstick May 12, 2026
e7712db
chore: clippy suggestions
mmstick May 12, 2026
b04f24c
feat(audio-server): pass better arguments to `cosmic-osd confirm-head…
mmstick May 12, 2026
ab00f44
fix(audio-server): missing argument to cosmic-osd
mmstick May 12, 2026
5d316c9
fix(audio-server): ensure headset profile route is a source
mmstick May 12, 2026
c2717f1
refactor(audio-server): add Select{Headphone,Headset}Profile methods
mmstick May 12, 2026
559ef3a
perf(audio): set device profiles internally with libpipewire
mmstick May 12, 2026
2a911b3
fix(audio-server): always emit ActiveRoute signals to fix device names
mmstick May 12, 2026
5a7445b
fix: speaker route after headphone profile apply
mmstick May 18, 2026
c9f626e
chore: log volume property updates to pipewire
mmstick May 19, 2026
8bdcbd8
chore(cosmic-pipewire): clear routes when route index 0 is received
mmstick May 19, 2026
5de7fbb
fix(cosmic-pipewire): check route::device when getting active route b…
mmstick May 19, 2026
6a83b38
chore(cosmic-pipewire): add audio-backend target to logs
mmstick May 19, 2026
729dac9
fix(cosmic-pipewire): request to enumerate routes after setting route
mmstick May 19, 2026
aa708f1
fix(cosmic-pipewire): route capacity panic
mmstick May 20, 2026
e4a6e20
fix(audio-server): disable headset profile detection for bluetooth de…
mmstick May 20, 2026
ee5254f
fix(cosmic-pipewire): enumerate profiles after setting a profile
mmstick May 20, 2026
0d81b42
fix(audio-server): on headset profile set, set route if profile is al…
mmstick May 20, 2026
14a8f13
fix(audio-server): on setting headphone profile, attempt to active av…
mmstick May 20, 2026
70513d8
chore: revert profile enumeration request after setting profile
mmstick May 20, 2026
f87f005
fix: clear routes and profiles on 0 index
mmstick Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,328 changes: 820 additions & 508 deletions Cargo.lock

Large diffs are not rendered by default.

57 changes: 31 additions & 26 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,57 @@ name = "cosmic-settings-daemon"
version = "0.1.0"
edition = "2024"
license = "GPL-3.0-or-later"
publish = false

[workspace]
members = ["config", "cosmic-settings-daemon-config", "geonames"]
members = ["audio-client", "audio-core", "audio-server", "config", "cosmic-settings-daemon-config", "geonames", "cosmic-pipewire", "varlink-server"]

[workspace.dependencies]
cosmic-config = { git = "https://github.com/pop-os/libcosmic" }
cosmic-theme = { git = "https://github.com/pop-os/libcosmic" }

# cosmic-config = { path = "../libcosmic/cosmic-config" }
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }

[workspace.dependencies.zlink]
version = "0.5"
default-features = false
features = ["tokio"]

[dependencies]
cosmic-settings-pulse-subscription = { git = "https://github.com/pop-os/cosmic-settings", default-features = false }
acpid_plug = "0.1.2"
anyhow = "1.0.99"
chrono = "0.4.42"
cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp" }
cosmic-config.workspace = true
cosmic-dbus-a11y = { git = "https://github.com/pop-os/dbus-settings-bindings" }
cosmic-settings-audio-server = { path = "./audio-server" }
cosmic-settings-daemon-config = { path = "./cosmic-settings-daemon-config", features = [
"greeter",
] }
cosmic-settings-varlink-server = { path = "./varlink-server" }
cosmic-theme.export = true
cosmic-theme.workspace = true
ctrlc = { version = "3.5.0", features = ["termination"] }
ddc-hi = "0.4.1"
dirs = "6.0.0"
futures = "0.3.31"
futures-util = "0.3.31"
geonames = { path = "geonames" }
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false }
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings" }
log = "0.4.28"
memoize = "0.5.1"
notify = "8.2.0"
notify-rust = "4.11.7"
serde = "1.0.228"
sunrise = "2.1.0"
tokio = { version = "1.47.1", features = ["macros", "net", "rt"] }
udev = "0.9.3"
zbus = { version = "5.11.0", default-features = false, features = ["tokio"] }
tokio-stream = "0.1.17"
sunrise = "2.1.0"
cosmic-config.workspace = true
cosmic-theme.workspace = true
cosmic-theme.export = true
cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp" }
chrono = "0.4.42"
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false }
acpid_plug = "0.1.2"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
udev = "0.9.3"
upower_dbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings" }
cosmic-dbus-a11y = { git = "https://github.com/pop-os/dbus-settings-bindings" }
notify-rust = "4.11.7"
walkdir = "2.5.0"
memoize = "0.5.1"
futures-util = "0.3.31"
futures = "0.3.31"
ctrlc = { version = "3.5.0", features = ["termination"] }
xkb-data = "0.2.1"
geonames = { path = "geonames" }
log = "0.4.28"
env_logger = "0.11.8"
ddc-hi = "0.4.1"
zbus = { version = "5.11.0", default-features = false, features = ["tokio"] }

# For development and testing purposes
# [patch.'https://github.com/pop-os/libcosmic']
Expand Down
35 changes: 35 additions & 0 deletions audio-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "cosmic-settings-audio-client"
version = "1.0.0"
edition = "2024"
license = "MPL-2.0"
publish = true

[features]
codec = ["dep:futures-util", "dep:ron", "dep:tokio", "tokio/net", "tokio-util/codec"]

[dependencies]
cosmic-settings-audio-core = { path = "../audio-core" }
dirs = "6.0.0"
futures-util = { version = "0.3", optional = true}
tokio = { version = "1.0", default-features = false, optional = true}
tokio-util = { version = "0.7" }
serde = "1.0"
ron = { version = "0.12", optional = true }
# Needed by zlink
tracing = "0.1.44"

[dependencies.zlink]
workspace = true
default-features = false
features = ["proxy", "tokio", "tracing"]

[dev-dependencies]
cosmic-settings-audio-client = { path = ".", features = ["codec"] }
cosmic-settings-audio-core = { path = "../audio-core" }
tokio = { version = "1.49.0", features = ["full"] }
zlink = { workspace = true, features = ["idl", "introspection", "tokio", "tracing", "server", "service"] }
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tracing = "0.1.44"
futures-util = "0.3.32"
serde = "1.0.228"
3 changes: 3 additions & 0 deletions audio-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# cosmic-settings-audio-client

Varlink client proxy for clients using the `com.system76.CosmicSettings.Audio` interfaces on `com.system76.CosmicSettings`.
47 changes: 47 additions & 0 deletions audio-client/examples/audio_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2026 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only

use cosmic_settings_audio_client::CosmicAudioProxy;
use futures_util::StreamExt;
use tracing_subscriber::prelude::*;

#[tokio::main(flavor = "current_thread")]
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
let log_format = tracing_subscriber::fmt::format()
.pretty()
.without_time()
.with_line_number(true)
.with_file(true)
.with_target(false)
.with_thread_names(true);

let log_layer = tracing_subscriber::fmt::Layer::default()
.with_writer(std::io::stderr)
.event_format(log_format);

tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_env("RUST_LOG"))
.with(log_layer)
.init();

let mut client = cosmic_settings_audio_client::connect().await.unwrap();

println!(
"default source: {:?}",
client.conn.default_source().await.unwrap()
);
println!(
"default sink: {:?}",
client.conn.default_source().await.unwrap()
);

client
.recv_events()
.await??
.for_each(|result| async move {
eprintln!("{:?}", result.unwrap());
})
.await;

Ok(())
}
80 changes: 80 additions & 0 deletions audio-client/src/codec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2026 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0

use cosmic_settings_audio_core::Event;
use tokio_util::bytes::Buf;

const MAX: usize = 8 * 1024 * 1024;

pub struct EventDecoder;

impl tokio_util::codec::Decoder for EventDecoder {
type Item = Event;
type Error = Error;

fn decode(
&mut self,
src: &mut tokio_util::bytes::BytesMut,
) -> Result<Option<Self::Item>, Self::Error> {
if src.len() < 4 {
// Not enough data to read length marker.
return Ok(None);
}

// Read length marker.
let mut length_bytes = [0u8; 4];
length_bytes.copy_from_slice(&src[..4]);
let length = u32::from_le_bytes(length_bytes) as usize;

// Check that the length is not too large to avoid a denial of
// service attack where the server runs out of memory.
if length > MAX {
return Err(Error::IO(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Frame of length {} is too large.", length),
)));
}

if src.len() < 4 + length {
// The full string has not yet arrived.
//
// We reserve more space in the buffer. This is not strictly
// necessary, but is a good idea performance-wise.
src.reserve(4 + length - src.len());

// We inform the Framed that we need more bytes to form the next
// frame.
return Ok(None);
}

// Use advance to modify src such that it no longer contains
// this frame.
let data = src[4..4 + length].to_vec();
src.advance(4 + length);

ron::de::from_bytes(&data).map(Some).map_err(Error::Ron)
}
}

#[derive(Debug)]
pub enum Error {
IO(std::io::Error),
Ron(ron::de::SpannedError),
}

impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ron(why) => write!(f, "RON deserialize error: {why}"),
Self::IO(why) => write!(f, "I/O stream error: {why}"),
}
}
}

impl std::error::Error for Error {}

impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::IO(error)
}
}
129 changes: 129 additions & 0 deletions audio-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2026 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0

#[cfg(feature = "codec")]
pub mod codec;

pub use zlink;
use zlink::Connection;

pub use cosmic_settings_audio_core::*;
use std::{os::fd::OwnedFd, path::PathBuf};

pub async fn connect() -> zlink::Result<Client> {
zlink::unix::connect(socket_path())
.await
.map(|conn| Client { conn })
}

#[derive(Debug)]
pub struct Client {
pub conn: Connection<zlink::unix::Stream>,
}

impl Client {
#[cfg(feature = "codec")]
pub async fn recv_events(
&mut self,
) -> zlink::Result<
Result<
impl futures_util::Stream<Item = Result<Event, codec::Error>> + Sync + Send + 'static,
Error,
>,
> {
self.conn
.recv_events()
.await
.map(|(result, mut fds)| match result {
Ok(()) => Ok(tokio_util::codec::FramedRead::new(
tokio::net::unix::pipe::Receiver::from_owned_fd(fds.swap_remove(0)).unwrap(),
codec::EventDecoder,
)),
Err(why) => Err(why),
})
}
}

pub fn socket_path() -> PathBuf {
dirs::runtime_dir()
.expect("runtime dir required by varlink service")
.join("com.system76.CosmicSettings")
}

#[zlink::proxy("com.system76.CosmicSettings.Audio")]
pub trait CosmicAudioProxy {
/// Request to listen to audio events through the returned `OwnedFd`.
#[zlink(return_fds)]
async fn recv_events(&mut self) -> zlink::Result<(Result<(), Error>, Vec<OwnedFd>)>;

async fn default_sink(&mut self) -> zlink::Result<Result<Node, Error>>;

async fn default_source(&mut self) -> zlink::Result<Result<Node, Error>>;

async fn sink_mute_toggle(&mut self) -> zlink::Result<Result<Mute, Error>>;

async fn sink_volume_lower(&mut self, step: u32) -> zlink::Result<Result<Volume, Error>>;

async fn sink_volume_raise(&mut self, step: u32) -> zlink::Result<Result<Volume, Error>>;

async fn source_mute_toggle(&mut self) -> zlink::Result<Result<Mute, Error>>;

async fn source_volume_lower(&mut self, step: u32) -> zlink::Result<Result<Volume, Error>>;

async fn source_volume_raise(&mut self, step: u32) -> zlink::Result<Result<Volume, Error>>;

async fn set_default(&mut self, node_id: u32, save: bool) -> zlink::Result<Result<(), Error>>;

/// Select the headphone profile of a device, if available.
async fn select_headphone_profile(
&mut self,
device_id: u32,
) -> zlink::Result<Result<(), Error>>;

/// Select the headset profile of a device, if available.
async fn select_headset_profile(&mut self, device_id: u32) -> zlink::Result<Result<(), Error>>;

/// Change the active profile of an audio device; changing which routes are active.
async fn set_profile(
&mut self,
device_id: u32,
profile_index: u32,
save: bool,
) -> zlink::Result<Result<(), Error>>;

/// Change the active route of an audio device.
async fn set_route(
&mut self,
device_id: u32,
card_profile_device: u32,
route_index: u32,
save: bool,
) -> zlink::Result<Result<(), Error>>;

/// Apply a volume to the default sink node.
async fn set_sink_volume(&mut self, volume: u32) -> zlink::Result<Result<Volume, Error>>;

/// Apply a volume to the default source node.
async fn set_source_volume(&mut self, volume: u32) -> zlink::Result<Result<Volume, Error>>;

/// Apply a mute state to a node by its ID.
async fn set_node_mute(
&mut self,
node_id: u32,
mute: bool,
) -> zlink::Result<Result<Mute, Error>>;

/// Apply a volume to a node by its ID.
async fn set_node_volume(
&mut self,
node_id: u32,
volume: u32,
) -> zlink::Result<Result<Volume, Error>>;

/// Set the balance of a node by its ID. 1.
async fn set_node_volume_balance(
&mut self,
node_id: u32,
balance: Option<f32>,
) -> zlink::Result<Result<Volume, Error>>;
}
15 changes: 15 additions & 0 deletions audio-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "cosmic-settings-audio-core"
version = "1.0.0"
edition = "2024"
license = "MPL-2.0"
publish = true

[dependencies]
dirs = "6.0.0"
ron = "0.12.0"
serde = { version = "1.0.228", features = ["derive"] }

[dependencies.zlink]
workspace = true
features = ["introspection"]
Loading