mpris and bar control impl

gitignore

fix import error
This commit is contained in:
Samuel Onoja committed 2026-10-05 16:12:24 +01:00
1 parent 73f21e20ce
commit 8a0dd7615e
42 files changed
+4026 -227

No files matched your search

+1
View File
@@ -8,4 +8,5 @@ rust-version.workspace = true
[dependencies]
serde.workspace = true
serde_json.workspace = true
libc.workspace = true
ferese-theme-model = { path = "../ferese-theme-model" }
+1
View File
@@ -5,6 +5,7 @@ use std::{fmt, io};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub mod media;
pub mod theme;
pub const VERSION: u32 = 1;
+146
View File
@@ -0,0 +1,146 @@
use serde::{Deserialize, Serialize};
pub fn now_us() -> u64 {
let mut time = libc::timespec { tv_sec: 0, tv_nsec: 0 };
// CLOCK_MONOTONIC shares one epoch between the compositor and shell.
if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut time) } != 0 {
return 0;
}
(time.tv_sec.max(0) as u64)
.saturating_mul(1_000_000)
.saturating_add(time.tv_nsec.max(0) as u64 / 1_000)
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum Playback {
Playing,
Paused,
#[default]
Stopped,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(default)]
pub struct Player {
pub name: String,
pub owner: String,
pub identity: String,
pub title: String,
pub artist: String,
pub album: String,
pub art_url: Option<String>,
pub track_id: Option<String>,
pub status: Playback,
pub position_us: Option<u64>,
pub sampled_at_us: u64,
pub length_us: Option<u64>,
pub rate: f64,
pub volume: Option<f64>,
pub can_control: bool,
pub can_play: bool,
pub can_pause: bool,
pub can_next: bool,
pub can_previous: bool,
pub can_seek: bool,
pub can_raise: bool,
}
impl Default for Player {
fn default() -> Self {
Self {
name: String::new(),
owner: String::new(),
identity: String::new(),
title: String::new(),
artist: String::new(),
album: String::new(),
art_url: None,
track_id: None,
status: Playback::Stopped,
position_us: None,
sampled_at_us: 0,
length_us: None,
rate: 1.0,
volume: None,
can_control: false,
can_play: false,
can_pause: false,
can_next: false,
can_previous: false,
can_seek: false,
can_raise: false,
}
}
}
impl Player {
pub fn label(&self) -> &str {
if self.title.is_empty() {
&self.identity
} else {
&self.title
}
}
pub fn can_toggle(&self) -> bool {
self.can_control
&& if self.status == Playback::Playing {
self.can_pause
} else {
self.can_play
}
}
pub fn position_at(&self, now_us: u64) -> Option<u64> {
let mut position = self.position_us? as f64;
if self.status == Playback::Playing && self.rate.is_finite() {
position += now_us.saturating_sub(self.sampled_at_us) as f64 * self.rate;
}
let position = position.max(0.0) as u64;
Some(self.length_us.map_or(position, |length| position.min(length)))
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Choice {
pub name: String,
pub owner: String,
pub identity: String,
pub status: Playback,
pub ignored: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Snapshot {
pub revision: u64,
pub selected: Option<Player>,
pub players: Vec<Choice>,
pub pinned: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn position_uses_rate_and_monotonic_sample_without_polling() {
let mut player = Player {
status: Playback::Playing,
position_us: Some(2_000_000),
sampled_at_us: 10_000_000,
length_us: Some(8_000_000),
rate: 1.5,
..Player::default()
};
assert_eq!(player.position_at(12_000_000), Some(5_000_000));
assert_eq!(player.position_at(20_000_000), Some(8_000_000));
assert_eq!(player.position_at(9_000_000), Some(2_000_000));
player.status = Playback::Paused;
assert_eq!(player.position_at(20_000_000), Some(2_000_000));
player.position_us = None;
assert_eq!(player.position_at(20_000_000), None);
}
}
+19 -3
View File
@@ -78,9 +78,16 @@ impl Connection {
command: command.into(),
args,
};
write_frame(&mut self.stream, &request).map_err(|e| e.to_string())?;
let response: Response = read_frame(&mut self.stream).map_err(|e| e.to_string())?;
let response = write_frame(&mut self.stream, &request).and_then(|()| read_frame::<Response>(&mut self.stream));
let response = match response {
Ok(response) => response,
Err(error) => {
let _ = self.stream.shutdown(Shutdown::Both);
return Err(error.to_string());
}
};
if response.id != self.id || response.version != VERSION {
let _ = self.stream.shutdown(Shutdown::Both);
return Err("Unexpected theme IPC response".into());
}
if let Some(error) = response.error {
@@ -98,8 +105,17 @@ impl Connection {
revision: u64,
fallback_families: impl FnOnce() -> Vec<Family>,
) -> Result<Snapshot, String> {
Snapshot::decode(self.wait("theme-watch", json!({"since": revision}))?, fallback_families)
}
/// Wait for a server-side change; dropping the cancellation handle interrupts it.
pub fn wait(&mut self, command: &str, args: Value) -> Result<Value, String> {
self.stream.set_read_timeout(None).map_err(|e| e.to_string())?;
Snapshot::decode(self.call("theme-watch", json!({"since": revision}))?, fallback_families)
let result = self.call(command, args);
self.stream
.set_read_timeout(Some(Duration::from_secs(2)))
.map_err(|e| e.to_string())?;
result
}
}
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="Next">
<path d="M21 4v16"/>
<path d="M6.029 4.285A2 2 0 0 0 3 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z"/>
</svg>

After

Width:  |  Height:  |  Size: 343 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Pause">
<rect x="15" y="12" width="10" height="36" rx="3" fill="currentColor"/><rect x="35" y="12" width="10" height="36" rx="3" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 241 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Play">
<path d="M21 12q-3-2-3 2v32q0 4 3 2l27-16q3-2 0-4Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 171 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="Previous">
<path d="M17.971 4.285A2 2 0 0 1 21 6v12a2 2 0 0 1-3.029 1.715l-9.997-5.998a2 2 0 0 1-.003-3.432z"/>
<path d="M3 20V4"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Media">
<path d="M22 39V14l25-5v25M22 23l25-5" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/><ellipse cx="15" cy="41" rx="8" ry="6" fill="#3d7be6"/><ellipse cx="40" cy="36" rx="8" ry="6" fill="#3d7be6"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

+5
View File
@@ -32,6 +32,11 @@ pub const CONTROL_CENTER: &[u8] = include_bytes!("../assets/icons/status/control
pub const DND: &[u8] = include_bytes!("../assets/icons/status/dnd.svg");
pub const ETHERNET: &[u8] = include_bytes!("../assets/icons/status/ethernet.svg");
pub const LOCK: &[u8] = include_bytes!("../assets/icons/status/lock.svg");
pub const MEDIA: &[u8] = include_bytes!("../assets/icons/status/media.svg");
pub const MEDIA_PLAY: &[u8] = include_bytes!("../assets/icons/status/media-play.svg");
pub const MEDIA_PAUSE: &[u8] = include_bytes!("../assets/icons/status/media-pause.svg");
pub const MEDIA_NEXT: &[u8] = include_bytes!("../assets/icons/status/media-next.svg");
pub const MEDIA_PREVIOUS: &[u8] = include_bytes!("../assets/icons/status/media-previous.svg");
pub const MIC_MUTE: &[u8] = include_bytes!("../assets/icons/status/mic-mute.svg");
pub const MIC: &[u8] = include_bytes!("../assets/icons/status/mic.svg");
pub const MOON: &[u8] = include_bytes!("../assets/icons/status/moon.svg");