Decode portal wallpapers by content and simplify status bar

Detect wallpaper image formats from their contents so extensionless portal copies render on the desktop and lock screen. Preserve decode limits and cover extensionless files with regression tests.

Remove the Bluetooth icon from the status bar.
This commit is contained in:
Samuel Onoja committed 2026-09-30 14:56:43 +01:00
1 parent 1d7f408c67
commit e05aa31ce3
5 files changed
+61 -6

No files matched your search

Generated
+1
View File
@@ -1663,6 +1663,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"smithay", "smithay",
"tempfile",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"xcursor", "xcursor",
+3
View File
@@ -26,3 +26,6 @@ serde_json.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true
xcursor.workspace = true xcursor.workspace = true
[dev-dependencies]
tempfile.workspace = true
+33 -2
View File
@@ -83,11 +83,20 @@ impl WallpaperState {
let wakeup = wakeup.clone(); let wakeup = wakeup.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let load = || -> Result<image::RgbaImage, String> { let load = || -> Result<image::RgbaImage, String> {
let (width, height) = image::image_dimensions(&path).map_err(|e| e.to_string())?; use image::ImageDecoder;
let mut reader = image::ImageReader::open(&path)
.map_err(|e| e.to_string())?
.with_guessed_format()
.map_err(|e| e.to_string())?;
let mut limits = image::Limits::default();
limits.max_alloc = Some(256 * 1024 * 1024);
reader.limits(limits);
let decoder = reader.into_decoder().map_err(|e| e.to_string())?;
let (width, height) = decoder.dimensions();
if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 { if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 {
return Err("wallpaper exceeds the 256 MiB decode limit".into()); return Err("wallpaper exceeds the 256 MiB decode limit".into());
} }
image::open(path) image::DynamicImage::from_decoder(decoder)
.map(|image| image.into_rgba8()) .map(|image| image.into_rgba8())
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
}; };
@@ -329,6 +338,28 @@ fn image_geometry(
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn portal_wallpaper_decodes_without_a_filename_extension() {
let directory = tempfile::tempdir().unwrap();
let expected = image::RgbaImage::from_pixel(3, 2, image::Rgba([32, 80, 160, 255]));
for name in ["wallpaper-portal", "wallpaper.jpg"] {
let path = directory.path().join(name);
expected.save_with_format(&path, image::ImageFormat::Png).unwrap();
let mut state = WallpaperState::new(WallpaperConfig {
path: Some(path),
mode: WallpaperMode::Fill,
});
let decoded = state
.receiver
.take()
.unwrap()
.recv_timeout(Duration::from_secs(5))
.unwrap()
.unwrap();
assert_eq!(decoded, expected);
}
}
#[test] #[test]
fn failed_upload_retry_wakes_idle_loop_and_does_not_spin_when_output_is_inactive() { fn failed_upload_retry_wakes_idle_loop_and_does_not_spin_when_output_is_inactive() {
use smithay::backend::renderer::ContextId; use smithay::backend::renderer::ContextId;
+23 -2
View File
@@ -169,6 +169,16 @@ fn account_picture(user: &str) -> Option<image::Handle> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn portal_wallpaper_loads_without_a_filename_extension() {
let file = tempfile::NamedTempFile::new().unwrap();
::image::RgbaImage::from_pixel(3, 2, ::image::Rgba([32, 80, 160, 255]))
.save_with_format(file.path(), ::image::ImageFormat::Png)
.unwrap();
assert!(load_wallpaper(file.path(), 0.).is_ok());
assert!(load_wallpaper(file.path(), 2.).is_ok());
}
#[test] #[test]
fn lock_options_share_shell_style_without_changing_security() { fn lock_options_share_shell_style_without_changing_security() {
let directory = tempfile::tempdir().unwrap(); let directory = tempfile::tempdir().unwrap();
@@ -211,11 +221,22 @@ mod tests {
} }
pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle, String> { pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle, String> {
let (width, height) = ::image::image_dimensions(path).map_err(|e| e.to_string())?; use ::image::ImageDecoder;
let mut reader = ::image::ImageReader::open(path)
.map_err(|e| e.to_string())?
.with_guessed_format()
.map_err(|e| e.to_string())?;
let mut limits = ::image::Limits::default();
limits.max_alloc = Some(256 * 1024 * 1024);
reader.limits(limits);
let decoder = reader.into_decoder().map_err(|e| e.to_string())?;
let (width, height) = decoder.dimensions();
if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 { if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 {
return Err("Lock wallpaper exceeds the decode limit".into()); return Err("Lock wallpaper exceeds the decode limit".into());
} }
let pixels = ::image::open(path).map_err(|e| e.to_string())?.into_rgba8(); let pixels = ::image::DynamicImage::from_decoder(decoder)
.map_err(|e| e.to_string())?
.into_rgba8();
let pixels = if blur > 0. { let pixels = if blur > 0. {
::image::imageops::blur(&::image::imageops::thumbnail(&pixels, 640, 640), blur) ::image::imageops::blur(&::image::imageops::thumbnail(&pixels, 640, 640), blur)
} else { } else {
+1 -2
View File
@@ -252,11 +252,10 @@ impl FereseShell {
pub fn view_status_bar(&self) -> Element<'_, cosmic::Action<Message>> { pub fn view_status_bar(&self) -> Element<'_, cosmic::Action<Message>> {
let theme = self.config.theme.for_bar(); let theme = self.config.theme.for_bar();
let metrics = BarMetrics::from(theme); let metrics = BarMetrics::from(theme);
let mut controls = row::with_capacity(7).spacing(1).align_y(Alignment::Center); let mut controls = row::with_capacity(6).spacing(1).align_y(Alignment::Center);
for kind in [ for kind in [
Menu::System, Menu::System,
Menu::Network, Menu::Network,
Menu::Bluetooth,
Menu::Audio, Menu::Audio,
Menu::Recording, Menu::Recording,
Menu::Notifications, Menu::Notifications,