Explain runtime invariants and avoid redundant unwraps

This commit is contained in:
Samuel Onoja committed 2026-10-22 01:28:03 +01:00
1 parent 34f2f07301
commit 1b51ba83b4
116 files changed
+1643 -515

No files matched your search

+4 -1
View File
@@ -30,7 +30,10 @@ impl Publisher {
serde_json::json!(job.snapshot.as_ref()), serde_json::json!(job.snapshot.as_ref()),
)); ));
} }
let value = &cached.as_ref().unwrap().1; let value = &cached
.as_ref()
.expect("activity serialization populated the cache for this revision")
.1;
// Lock acquisition revokes queued/in-flight serialization. // Lock acquisition revokes queued/in-flight serialization.
// Hold the guard through delivery to order sends before revocation. // Hold the guard through delivery to order sends before revocation.
deliver(job.epoch, &worker_epoch, job.recipients, value); deliver(job.epoch, &worker_epoch, job.recipients, value);
+116 -31
View File
@@ -387,7 +387,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
state state
.direct_backend .direct_backend
.as_mut() .as_mut()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.topology .topology
.observe_devices(&[( .observe_devices(&[(
primary_path.clone(), primary_path.clone(),
@@ -640,25 +640,43 @@ pub(crate) fn set_output_profile(state: &mut Ferese, name: &str) -> Result<(), S
return Err("profile does not match the connected monitors/lid".into()); return Err("profile does not match the connected monitors/lid".into());
} }
if let Err(error) = validate_live_outputs(state, &state.output_profiles) { if let Err(error) = validate_live_outputs(state, &state.output_profiles) {
state.direct_backend.as_mut().unwrap().manual_outputs = previous; state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.manual_outputs = previous;
return Err(error); return Err(error);
} }
reconcile_outputs(state, false); reconcile_outputs(state, false);
if let Some(error) = state.direct_backend.as_ref().unwrap().output_error.clone() { if let Some(error) = state
state.direct_backend.as_mut().unwrap().manual_outputs = previous; .direct_backend
.as_ref()
.expect("DRM callbacks require an installed direct backend")
.output_error
.clone()
{
state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.manual_outputs = previous;
return Err(error); return Err(error);
} }
if name != "auto" if name != "auto"
&& state && state
.direct_backend .direct_backend
.as_ref() .as_ref()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.desired_outputs .desired_outputs
.profile .profile
.as_deref() .as_deref()
!= Some(name) != Some(name)
{ {
state.direct_backend.as_mut().unwrap().manual_outputs = previous; state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.manual_outputs = previous;
return Err("profile does not match the connected monitors/lid".into()); return Err("profile does not match the connected monitors/lid".into());
} }
arm_output_confirmation(state, known_good, previous); arm_output_confirmation(state, known_good, previous);
@@ -698,12 +716,26 @@ pub(crate) fn set_output_layout(
backend.manual_outputs.internal = None; backend.manual_outputs.internal = None;
backend.manual_outputs.layout = Some(layout); backend.manual_outputs.layout = Some(layout);
if let Err(error) = validate_live_outputs(state, &state.output_profiles) { if let Err(error) = validate_live_outputs(state, &state.output_profiles) {
state.direct_backend.as_mut().unwrap().manual_outputs = previous; state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.manual_outputs = previous;
return Err(error); return Err(error);
} }
reconcile_outputs(state, false); reconcile_outputs(state, false);
if let Some(error) = state.direct_backend.as_ref().unwrap().output_error.clone() { if let Some(error) = state
state.direct_backend.as_mut().unwrap().manual_outputs = previous; .direct_backend
.as_ref()
.expect("DRM callbacks require an installed direct backend")
.output_error
.clone()
{
state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.manual_outputs = previous;
return Err(error); return Err(error);
} }
arm_output_confirmation(state, known_good, previous); arm_output_confirmation(state, known_good, previous);
@@ -732,8 +764,18 @@ pub(crate) fn set_internal_output(state: &mut Ferese, enabled: bool) -> Result<(
backend.manual_outputs.internal = Some(enabled); backend.manual_outputs.internal = Some(enabled);
backend.manual_outputs.layout = None; backend.manual_outputs.layout = None;
reconcile_outputs(state, false); reconcile_outputs(state, false);
if let Some(error) = state.direct_backend.as_ref().unwrap().output_error.clone() { if let Some(error) = state
state.direct_backend.as_mut().unwrap().manual_outputs = previous; .direct_backend
.as_ref()
.expect("DRM callbacks require an installed direct backend")
.output_error
.clone()
{
state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.manual_outputs = previous;
return Err(error); return Err(error);
} }
arm_output_confirmation(state, known_good, previous); arm_output_confirmation(state, known_good, previous);
@@ -750,7 +792,10 @@ fn arm_output_confirmation(
previous: DesiredOutputConfiguration, previous: DesiredOutputConfiguration,
manual: ManualOverride, manual: ManualOverride,
) { ) {
let backend = state.direct_backend.as_mut().unwrap(); let backend = state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend");
if let Some(token) = backend.confirmation_timer.take() { if let Some(token) = backend.confirmation_timer.take() {
state.loop_handle.remove(token); state.loop_handle.remove(token);
} }
@@ -781,7 +826,13 @@ fn arm_output_confirmation(
TimeoutAction::Drop TimeoutAction::Drop
}, },
) { ) {
Ok(token) => state.direct_backend.as_mut().unwrap().confirmation_timer = Some(token), Ok(token) => {
state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.confirmation_timer = Some(token)
}
Err(error) => { Err(error) => {
tracing::warn!(%error, "could not arm monitor confirmation; reverting"); tracing::warn!(%error, "could not arm monitor confirmation; reverting");
let _ = confirm_output_configuration(state, false); let _ = confirm_output_configuration(state, false);
@@ -809,7 +860,13 @@ pub(crate) fn confirm_output_configuration(
)); ));
backend.manual_outputs = manual; backend.manual_outputs = manual;
reconcile_outputs(state, false); reconcile_outputs(state, false);
if let Some(error) = state.direct_backend.as_ref().unwrap().output_error.clone() { if let Some(error) = state
.direct_backend
.as_ref()
.expect("DRM callbacks require an installed direct backend")
.output_error
.clone()
{
return Err(error); return Err(error);
} }
} }
@@ -1256,7 +1313,7 @@ fn redraw(
let outputs = state let outputs = state
.direct_backend .direct_backend
.as_ref() .as_ref()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.devices .devices
.iter() .iter()
.flat_map(|(node, device)| { .flat_map(|(node, device)| {
@@ -1274,7 +1331,7 @@ fn redraw(
let scene = state let scene = state
.direct_backend .direct_backend
.as_ref() .as_ref()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.scene_output(state, output); .scene_output(state, output);
selected.is_none_or(|selected| selected.contains(output) || selected.contains(&scene)) selected.is_none_or(|selected| selected.contains(output) || selected.contains(&scene))
|| state.output_has_animations(&scene) || state.output_has_animations(&scene)
@@ -1293,7 +1350,7 @@ fn redraw(
state state
.direct_backend .direct_backend
.as_mut() .as_mut()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.scheduling_metrics .scheduling_metrics
.redraw(source, selected.is_none(), requested, outputs.len()); .redraw(source, selected.is_none(), requested, outputs.len());
arm_animation_timer(state); arm_animation_timer(state);
@@ -1517,7 +1574,11 @@ fn render_output(
let damage = damage.map_or_else( let damage = damage.map_or_else(
|| { || {
vec![smithay::utils::Rectangle::from_size( vec![smithay::utils::Rectangle::from_size(
output.output.current_mode().unwrap().size, output
.output
.current_mode()
.expect("a rendered output must have a current mode")
.size,
)] )]
}, },
|damage| { |damage| {
@@ -1641,7 +1702,13 @@ fn render_output(
restore_device(state, node, device); restore_device(state, node, device);
if !submitted if !submitted
&& scene_output && scene_output
== state.direct_backend.as_ref().unwrap().devices[&node].outputs[&crtc].output == state
.direct_backend
.as_ref()
.expect("DRM callbacks require an installed direct backend")
.devices[&node]
.outputs[&crtc]
.output
{ {
schedule_frame_callbacks(state, node, crtc, plan.present_at); schedule_frame_callbacks(state, node, crtc, plan.present_at);
} }
@@ -1687,7 +1754,11 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) -> Result<bool, ()> {
.and_then(|scan| transaction::apply_device(state, node, &mut device, scan)); .and_then(|scan| transaction::apply_device(state, node, &mut device, scan));
if let Err(error) = &result { if let Err(error) = &result {
tracing::warn!(?node, %error, "output transaction failed; restored known-good outputs where possible"); tracing::warn!(?node, %error, "output transaction failed; restored known-good outputs where possible");
state.direct_backend.as_mut().unwrap().output_error = Some(error.clone()); state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.output_error = Some(error.clone());
transaction::update_applied_info(&mut device); transaction::update_applied_info(&mut device);
} }
restore_device(state, node, device); restore_device(state, node, device);
@@ -1695,7 +1766,10 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) -> Result<bool, ()> {
} }
fn refresh_connected_info(state: &mut Ferese) { fn refresh_connected_info(state: &mut Ferese) {
let backend = state.direct_backend.as_mut().unwrap(); let backend = state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend");
for device in backend.devices.values_mut() { for device in backend.devices.values_mut() {
if let Ok(scan) = select_outputs( if let Ok(scan) = select_outputs(
device, device,
@@ -1739,7 +1813,10 @@ fn refresh_connected_info(state: &mut Ferese) {
} }
fn remember_applied_configuration(state: &mut Ferese) { fn remember_applied_configuration(state: &mut Ferese) {
let backend = state.direct_backend.as_mut().unwrap(); let backend = state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend");
if backend.topology.reconciling { if backend.topology.reconciling {
// Applied positions are read only after the coherent logical publish. // Applied positions are read only after the coherent logical publish.
backend.topology.remember_applied = true; backend.topology.remember_applied = true;
@@ -1913,17 +1990,20 @@ fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instan
let scene = state let scene = state
.direct_backend .direct_backend
.as_ref() .as_ref()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.scene_output(state, &identity); .scene_output(state, &identity);
let animating = enabled && state.output_has_animations(&scene); let animating = enabled && state.output_has_animations(&scene);
let backend = state.direct_backend.as_mut().unwrap(); let backend = state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend");
let output = backend let output = backend
.devices .devices
.get_mut(&node) .get_mut(&node)
.unwrap() .expect("the selected DRM device remains registered during this operation")
.outputs .outputs
.get_mut(&crtc) .get_mut(&crtc)
.unwrap(); .expect("the selected DRM device remains registered during this operation");
// Preserve recovery of a requested final frame if another output // Preserve recovery of a requested final frame if another output
// advances this animation to rest before the watchdog is dispatched. // advances this animation to rest before the watchdog is dispatched.
let active = enabled let active = enabled
@@ -2032,19 +2112,21 @@ fn arm_animation_timer(state: &mut Ferese) {
state state
.direct_backend .direct_backend
.as_mut() .as_mut()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.scheduling_metrics .scheduling_metrics
.fallback_recoveries += 1; .fallback_recoveries += 1;
state state
.direct_backend .direct_backend
.as_mut() .as_mut()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.devices .devices
.get_mut(&node) .get_mut(&node)
.unwrap() .expect(
"the selected DRM device must remain registered during this operation",
)
.outputs .outputs
.get_mut(&crtc) .get_mut(&crtc)
.unwrap() .expect("the selected CRTC must remain registered during this operation")
.animation_fallback = AnimationFallback::default(); .animation_fallback = AnimationFallback::default();
request_frame(state, node, crtc); request_frame(state, node, crtc);
} }
@@ -2054,7 +2136,10 @@ fn arm_animation_timer(state: &mut Ferese) {
TimeoutAction::Drop TimeoutAction::Drop
}) { }) {
Ok(token) => { Ok(token) => {
let backend = state.direct_backend.as_mut().unwrap(); let backend = state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend");
backend.animation_timer = Some(token); backend.animation_timer = Some(token);
backend.animation_deadline = Some(deadline); backend.animation_deadline = Some(deadline);
backend.scheduling_metrics.fallback_arms += 1; backend.scheduling_metrics.fallback_arms += 1;
+5 -1
View File
@@ -102,7 +102,11 @@ pub(super) fn capture_mirror(
continue; continue;
}; };
let element = super::mirror::fitted_texture( let element = super::mirror::fitted_texture(
output.mirror_capture_texture.as_ref().unwrap().clone(), output
.mirror_capture_texture
.as_ref()
.expect("successful mirror composition populated its capture texture")
.clone(),
&output.output, &output.output,
smithay::backend::renderer::element::Id::new(), smithay::backend::renderer::element::Id::new(),
Default::default(), Default::default(),
+46 -12
View File
@@ -148,7 +148,11 @@ fn read_acpi() -> Option<bool> {
impl Reader { impl Reader {
pub fn set_active(&mut self, active: bool) { pub fn set_active(&mut self, active: bool) {
let generation = self.lease.lock().unwrap().activate(active); let generation = self
.lease
.lock()
.expect("lid policy lease must remain consistent across workers")
.activate(active);
self.suspend_requested = false; self.suspend_requested = false;
if active { if active {
let _ = self.suspend.send(PolicyRequest::Activate(generation)); let _ = self.suspend.send(PolicyRequest::Activate(generation));
@@ -167,7 +171,9 @@ impl Reader {
let Ok(connection) = connection else { return }; let Ok(connection) = connection else { return };
while let Ok(request) = suspend_requests.recv() { while let Ok(request) = suspend_requests.recv() {
if let PolicyRequest::Activate(generation) = request { if let PolicyRequest::Activate(generation) = request {
let current = worker_lease.lock().unwrap(); let current = worker_lease
.lock()
.expect("lid policy lease must remain consistent across workers");
if !current.active || current.generation != generation { if !current.active || current.generation != generation {
continue; continue;
} }
@@ -189,14 +195,17 @@ impl Reader {
reply.body().deserialize::<zbus::zvariant::OwnedFd>() reply.body().deserialize::<zbus::zvariant::OwnedFd>()
}); });
match inhibitor { match inhibitor {
Ok(descriptor) => { Ok(descriptor) => worker_lease
worker_lease.lock().unwrap().accept(generation, descriptor) .lock()
} .expect("lid policy lease must remain consistent across workers")
.accept(generation, descriptor),
Err(error) => tracing::warn!(%error, "could not own logind lid policy"), Err(error) => tracing::warn!(%error, "could not own logind lid policy"),
} }
continue; continue;
} }
let current = worker_lease.lock().unwrap(); let current = worker_lease
.lock()
.expect("lid policy lease must remain consistent across workers");
if !current.active || !current.suspend { if !current.active || !current.suspend {
continue; continue;
} }
@@ -221,7 +230,9 @@ impl Reader {
tracing::debug!("lid suspend prevented by sleep inhibitor"); tracing::debug!("lid suspend prevented by sleep inhibitor");
continue; continue;
} }
let current = worker_lease.lock().unwrap(); let current = worker_lease
.lock()
.expect("lid policy lease must remain consistent across workers");
let requested = current.active && current.suspend; let requested = current.active && current.suspend;
drop(current); drop(current);
if !requested { if !requested {
@@ -237,7 +248,10 @@ impl Reader {
tracing::warn!(%error, "logind rejected lid suspend"); tracing::warn!(%error, "logind rejected lid suspend");
} }
} }
worker_lease.lock().unwrap().activate(false); worker_lease
.lock()
.expect("lid policy lease must remain consistent across workers")
.activate(false);
})?; })?;
let (requests, receiver) = mpsc::channel::<Probe>(); let (requests, receiver) = mpsc::channel::<Probe>();
let (replies, events) = smithay::reexports::calloop::channel::channel(); let (replies, events) = smithay::reexports::calloop::channel::channel();
@@ -306,7 +320,12 @@ fn request(state: &mut Ferese, reactivate: bool, wake: bool) {
} }
} }
backend.lid_reader.suspend_requested = false; backend.lid_reader.suspend_requested = false;
backend.lid_reader.lease.lock().unwrap().suspend = false; backend
.lid_reader
.lease
.lock()
.expect("lid policy lease must remain consistent across workers")
.suspend = false;
let probe = backend.lid.begin(reactivate, wake); let probe = backend.lid.begin(reactivate, wake);
let sent = backend.lid_reader.requests.send(probe).is_ok(); let sent = backend.lid_reader.requests.send(probe).is_ok();
let timer = state.loop_handle.insert_source( let timer = state.loop_handle.insert_source(
@@ -322,7 +341,12 @@ fn request(state: &mut Ferese, reactivate: bool, wake: bool) {
); );
if sent && let Ok(token) = timer { if sent && let Ok(token) = timer {
state.direct_backend.as_mut().unwrap().lid_reader.deadline = Some(token); state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.lid_reader
.deadline = Some(token);
} else { } else {
if let Ok(token) = timer { if let Ok(token) = timer {
state.loop_handle.remove(token); state.loop_handle.remove(token);
@@ -352,7 +376,12 @@ fn complete_refresh(state: &mut Ferese, probe: Probe, value: Option<bool>) {
backend.topology.reconciling = true; backend.topology.reconciling = true;
state.reset_animation_clock(); state.reset_animation_clock();
state.lock_input_activity(); state.lock_input_activity();
state.direct_backend.as_mut().unwrap().topology.reconciling = false; state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.topology
.reconciling = false;
} }
reconcile_outputs(state, reactivate); reconcile_outputs(state, reactivate);
@@ -364,7 +393,12 @@ pub(super) fn apply_suspend_policy(state: &mut Ferese) {
return; return;
}; };
let requested = backend.active && backend.desired_outputs.suspend && backend.lid.closed(); let requested = backend.active && backend.desired_outputs.suspend && backend.lid.closed();
backend.lid_reader.lease.lock().unwrap().suspend = requested; backend
.lid_reader
.lease
.lock()
.expect("lid policy lease must remain consistent across workers")
.suspend = requested;
if backend.lid_reader.suspend_requested != requested { if backend.lid_reader.suspend_requested != requested {
backend.lid_reader.suspend_requested = requested; backend.lid_reader.suspend_requested = requested;
let _ = backend.lid_reader.suspend.send(PolicyRequest::Suspend); let _ = backend.lid_reader.suspend.send(PolicyRequest::Suspend);
+8 -1
View File
@@ -131,7 +131,14 @@ pub(super) fn cursor_on_output(state: &crate::Ferese, output: &smithay::output::
states states
.data_map .data_map
.get::<CursorImageSurfaceData>() .get::<CursorImageSurfaceData>()
.map(|attributes| attributes.lock().unwrap().hotspot) .map(|attributes| {
attributes
.lock()
.expect(
"cursor surface attributes must remain consistent across callbacks",
)
.hotspot
})
.unwrap_or_default() .unwrap_or_default()
}); });
smithay::desktop::utils::bbox_from_surface_tree( smithay::desktop::utils::bbox_from_surface_tree(
@@ -23,13 +23,21 @@ impl<S: Session> SessionDevice<S> {
} }
pub fn device_fd(&self) -> io::Result<DeviceFd> { pub fn device_fd(&self) -> io::Result<DeviceFd> {
self.fd.as_ref().unwrap().try_clone().map(DeviceFd::from) self.fd
.as_ref()
.expect("the session device retains its descriptor until Drop")
.try_clone()
.map(DeviceFd::from)
} }
} }
impl<S: Session> Drop for SessionDevice<S> { impl<S: Session> Drop for SessionDevice<S> {
fn drop(&mut self) { fn drop(&mut self) {
if let Err(error) = self.session.close(self.fd.take().unwrap()) { if let Err(error) = self.session.close(
self.fd
.take()
.expect("the session device retains its descriptor until Drop"),
) {
tracing::warn!( tracing::warn!(
?error, ?error,
"could not release DRM device through the session manager" "could not release DRM device through the session manager"
+76 -24
View File
@@ -176,11 +176,11 @@ fn slot_mut<'a>(state: &'a mut Ferese, path: &Path) -> &'a mut DeviceState {
state state
.direct_backend .direct_backend
.as_mut() .as_mut()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.topology .topology
.devices .devices
.get_mut(path) .get_mut(path)
.unwrap() .expect("the discovery path must have a topology slot")
} }
/// The only entry point for applying the current output configuration to DRM. /// The only entry point for applying the current output configuration to DRM.
@@ -250,7 +250,12 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
} }
for path in paths { for path in paths {
let slot = &state.direct_backend.as_ref().unwrap().topology.devices[&path]; let slot = &state
.direct_backend
.as_ref()
.expect("DRM callbacks require an installed direct backend")
.topology
.devices[&path];
let current_path = observed let current_path = observed
.as_ref() .as_ref()
.ok() .ok()
@@ -261,7 +266,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
state state
.direct_backend .direct_backend
.as_ref() .as_ref()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.devices .devices
.contains_key(node) .contains_key(node)
}); });
@@ -291,7 +296,10 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
} }
let node = match existing { let node = match existing {
Some(node) => node, Some(node) => node,
None => match open_device(state, current_path.unwrap()) { None => match open_device(
state,
current_path.expect("device discovery found a path before attempting to open it"),
) {
Ok(node) => node, Ok(node) => node,
Err(error) => { Err(error) => {
tracing::warn!(%error, ?path, device_path = ?current_path, "DRM device unavailable; will retry"); tracing::warn!(%error, ?path, device_path = ?current_path, "DRM device unavailable; will retry");
@@ -306,10 +314,10 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
let device = state let device = state
.direct_backend .direct_backend
.as_mut() .as_mut()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.devices .devices
.get_mut(&node) .get_mut(&node)
.unwrap(); .expect("the selected DRM device must remain registered during this operation");
device.drm.activate(true).map(|()| { device.drm.activate(true).map(|()| {
for (crtc, output) in &mut device.outputs { for (crtc, output) in &mut device.outputs {
cancel_output_timers(&state.loop_handle, output); cancel_output_timers(&state.loop_handle, output);
@@ -325,7 +333,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
state state
.desktop_transition .desktop_transition
.as_mut() .as_mut()
.unwrap() .expect("desktop transition must be started before publication or retirement")
.redraw .redraw
.insert(output.output.clone()); .insert(output.output.clone());
@@ -350,13 +358,13 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
let mut output = state let mut output = state
.direct_backend .direct_backend
.as_mut() .as_mut()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.devices .devices
.get_mut(&node) .get_mut(&node)
.unwrap() .expect("the selected DRM device must remain registered during this operation")
.outputs .outputs
.remove(&crtc) .remove(&crtc)
.unwrap(); .expect("the selected CRTC must remain registered during this operation");
cancel_output_timers(&state.loop_handle, &mut output); cancel_output_timers(&state.loop_handle, &mut output);
if let Some(global) = output.global.take() { if let Some(global) = output.global.take() {
state.display_handle.disable_global::<Ferese>(global); state.display_handle.disable_global::<Ferese>(global);
@@ -365,7 +373,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
state state
.direct_backend .direct_backend
.as_mut() .as_mut()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.presentation .presentation
.remove(&(node, crtc)); .remove(&(node, crtc));
} }
@@ -376,7 +384,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
let known_good = state let known_good = state
.direct_backend .direct_backend
.as_ref() .as_ref()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.applied_outputs .applied_outputs
.clone(); .clone();
if let Err(error) = refresh_monitor_policy(state).and_then(|()| { if let Err(error) = refresh_monitor_policy(state).and_then(|()| {
@@ -384,14 +392,21 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
validate_desired_outputs(state) validate_desired_outputs(state)
}) { }) {
tracing::warn!(%error, "monitor policy validation failed; retaining applied outputs"); tracing::warn!(%error, "monitor policy validation failed; retaining applied outputs");
state.direct_backend.as_mut().unwrap().output_error = Some(error); state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.output_error = Some(error);
for (path, node) in &ready { for (path, node) in &ready {
slot_mut(state, path).degraded(*node, Instant::now()); slot_mut(state, path).degraded(*node, Instant::now());
} }
} else { } else {
// Activate destinations before disabling former owners, including across GPUs. // Activate destinations before disabling former owners, including across GPUs.
ready.sort_by_key(|(_, node)| { ready.sort_by_key(|(_, node)| {
let backend = state.direct_backend.as_ref().unwrap(); let backend = state
.direct_backend
.as_ref()
.expect("DRM callbacks require an installed direct backend");
let prefix = format!("{}/", drm_device_key(&backend.devices[node].drm)); let prefix = format!("{}/", drm_device_key(&backend.devices[node].drm));
let desired = backend let desired = backend
.desired_outputs .desired_outputs
@@ -416,14 +431,21 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
// Device boundaries are not cross-GPU atomic. Compensate already // Device boundaries are not cross-GPU atomic. Compensate already
// applied devices using their last actual hardware settings. // applied devices using their last actual hardware settings.
if !known_good.outputs.is_empty() { if !known_good.outputs.is_empty() {
let backend = state.direct_backend.as_mut().unwrap(); let backend = state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend");
let requested = backend.desired_outputs.clone(); let requested = backend.desired_outputs.clone();
backend.desired_outputs = backend.desired_outputs =
crate::output_policy::restore_configuration(&known_good, &backend.monitors); crate::output_policy::restore_configuration(&known_good, &backend.monitors);
for previous in succeeded { for previous in succeeded {
let _ = rescan_device(state, previous); let _ = rescan_device(state, previous);
} }
state.direct_backend.as_mut().unwrap().desired_outputs = requested; state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.desired_outputs = requested;
} }
break; break;
} }
@@ -431,7 +453,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
if state if state
.direct_backend .direct_backend
.as_ref() .as_ref()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.output_error .output_error
.is_none() .is_none()
{ {
@@ -441,14 +463,24 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
transaction::recover_last_output(state); transaction::recover_last_output(state);
let mut desktop_changes = publish_runtime_desktop(state); let mut desktop_changes = publish_runtime_desktop(state);
if std::mem::take(&mut state.direct_backend.as_mut().unwrap().topology.layout_dirty) { if std::mem::take(
&mut state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.topology
.layout_dirty,
) {
desktop_changes.layout = true; desktop_changes.layout = true;
desktop_changes.shell = true; desktop_changes.shell = true;
desktop_changes desktop_changes
.outputs .outputs
.extend(state.space.outputs().cloned()); .extend(state.space.outputs().cloned());
} }
let backend = state.direct_backend.as_mut().unwrap(); let backend = state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend");
backend.topology.reconciling = false; backend.topology.reconciling = false;
let retry = backend.topology.retry_deadline(backend.active); let retry = backend.topology.retry_deadline(backend.active);
if std::mem::take(&mut backend.topology.remember_applied) { if std::mem::take(&mut backend.topology.remember_applied) {
@@ -465,7 +497,14 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
reconcile_outputs(state, false); reconcile_outputs(state, false);
TimeoutAction::Drop TimeoutAction::Drop
}) { }) {
Ok(token) => state.direct_backend.as_mut().unwrap().topology.retry_timer = Some(token), Ok(token) => {
state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.topology
.retry_timer = Some(token)
}
Err(error) => tracing::warn!(%error, "could not arm DRM recovery deadline"), Err(error) => tracing::warn!(%error, "could not arm DRM recovery deadline"),
} }
} }
@@ -481,7 +520,7 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
let mut outputs = state let mut outputs = state
.direct_backend .direct_backend
.as_ref() .as_ref()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.devices .devices
.values() .values()
.flat_map(|device| device.outputs.values()) .flat_map(|device| device.outputs.values())
@@ -525,7 +564,13 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
let changes = state let changes = state
.publish_desktop(runtime) .publish_desktop(runtime)
.expect("validated usable output inventory"); .expect("validated usable output inventory");
for device in state.direct_backend.as_mut().unwrap().devices.values_mut() { for device in state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.devices
.values_mut()
{
for output in device.outputs.values_mut() { for output in device.outputs.values_mut() {
if output.mirror_source.is_none() { if output.mirror_source.is_none() {
if output.global.is_none() { if output.global.is_none() {
@@ -579,7 +624,14 @@ pub(super) fn hotplug(state: &mut Ferese) {
reconcile_outputs(state, false); reconcile_outputs(state, false);
TimeoutAction::Drop TimeoutAction::Drop
}) { }) {
Ok(token) => state.direct_backend.as_mut().unwrap().topology.settle_timer = Some(token), Ok(token) => {
state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.topology
.settle_timer = Some(token)
}
Err(error) => { Err(error) => {
tracing::warn!(%error, "could not debounce output events"); tracing::warn!(%error, "could not debounce output events");
reconcile_outputs(state, false); reconcile_outputs(state, false);
+47 -20
View File
@@ -257,7 +257,7 @@ fn retire(state: &mut Ferese, output: &mut DirectOutput, node: DrmNode, crtc: cr
state state
.direct_backend .direct_backend
.as_mut() .as_mut()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.presentation .presentation
.remove(&(node, crtc)); .remove(&(node, crtc));
} }
@@ -296,7 +296,7 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
.staged .staged
.get_mut(&crtc) .get_mut(&crtc)
.or_else(|| self.device.outputs.get_mut(&crtc)) .or_else(|| self.device.outputs.get_mut(&crtc))
.unwrap(); .expect("the selected CRTC has a live or staged output in this transaction");
output output
.surface .surface
.set_connectors(&[selection.connector.handle()]) .set_connectors(&[selection.connector.handle()])
@@ -317,8 +317,15 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
// set_connectors: each tests against the other's old value. Stage a // set_connectors: each tests against the other's old value. Stage a
// replacement surface, retire the old one *before* its first commit // replacement surface, retire the old one *before* its first commit
// (Smithay clears the CRTC on surface drop), then publish metadata. // (Smithay clears the CRTC on surface drop), then publish metadata.
let replacement = self.staged.remove(&crtc).unwrap(); let replacement = self
let previous = self.device.outputs.get_mut(&crtc).unwrap(); .staged
.remove(&crtc)
.expect("the selected CRTC has a live or staged output in this transaction");
let previous = self
.device
.outputs
.get_mut(&crtc)
.expect("the selected CRTC has a live or staged output in this transaction");
previous.surface = replacement.surface; previous.surface = replacement.surface;
previous.dmabuf_feedback = replacement.dmabuf_feedback; previous.dmabuf_feedback = replacement.dmabuf_feedback;
self.replaced.insert(crtc); self.replaced.insert(crtc);
@@ -326,7 +333,7 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
self.staged self.staged
.get_mut(&crtc) .get_mut(&crtc)
.or_else(|| self.device.outputs.get_mut(&crtc)) .or_else(|| self.device.outputs.get_mut(&crtc))
.unwrap() .expect("the selected CRTC has a live or staged output in this transaction")
.surface .surface
.commit_frame() .commit_frame()
.map_err(|error| error.to_string()) .map_err(|error| error.to_string())
@@ -336,7 +343,7 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
self.device self.device
.outputs .outputs
.get_mut(&crtc) .get_mut(&crtc)
.unwrap() .expect("the selected CRTC has a live or staged output in this transaction")
.surface .surface
.clear() .clear()
.map_err(|error| error.to_string()) .map_err(|error| error.to_string())
@@ -384,7 +391,11 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
&replacement.settings, &replacement.settings,
) )
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
let previous = self.device.outputs.get_mut(&crtc).unwrap(); let previous = self
.device
.outputs
.get_mut(&crtc)
.expect("the selected CRTC has a live or staged output in this transaction");
previous.surface = replacement.surface; previous.surface = replacement.surface;
previous.dmabuf_feedback = replacement.dmabuf_feedback; previous.dmabuf_feedback = replacement.dmabuf_feedback;
previous previous
@@ -426,7 +437,7 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
self.state self.state
.desktop_transition .desktop_transition
.as_mut() .as_mut()
.unwrap() .expect("desktop transition must be started before publication or retirement")
.redraw .redraw
.insert(output.output.clone()); .insert(output.output.clone());
output.lock_frame_pending = false; output.lock_frame_pending = false;
@@ -504,7 +515,7 @@ pub(super) fn apply_device(
let mut output = staged let mut output = staged
.remove(crtc) .remove(crtc)
.or_else(|| device.outputs.remove(crtc)) .or_else(|| device.outputs.remove(crtc))
.unwrap(); .expect("the selected CRTC has a live or staged output in this transaction");
if output.connector != selection.connector.handle() || output.identity != selection.identity if output.connector != selection.connector.handle() || output.identity != selection.identity
{ {
retire(state, &mut output, node, *crtc); retire(state, &mut output, node, *crtc);
@@ -551,7 +562,7 @@ pub(super) fn apply_device(
let clock = state let clock = state
.direct_backend .direct_backend
.as_mut() .as_mut()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.presentation .presentation
.entry((node, *crtc)) .entry((node, *crtc))
.or_default(); .or_default();
@@ -561,7 +572,10 @@ pub(super) fn apply_device(
} }
// Retire resources now; the final inventory determines evacuation once. // Retire resources now; the final inventory determines evacuation once.
for crtc in removals { for crtc in removals {
let mut output = device.outputs.remove(&crtc).unwrap(); let mut output = device
.outputs
.remove(&crtc)
.expect("the selected CRTC has a live or staged output in this transaction");
retire(state, &mut output, node, crtc); retire(state, &mut output, node, crtc);
} }
device.connected_outputs = scan.connected_outputs; device.connected_outputs = scan.connected_outputs;
@@ -592,7 +606,7 @@ pub(super) fn retire_disconnected(state: &mut Ferese) {
let missing = state let missing = state
.direct_backend .direct_backend
.as_ref() .as_ref()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.devices .devices
.iter() .iter()
.flat_map(|(node, device)| { .flat_map(|(node, device)| {
@@ -610,10 +624,10 @@ pub(super) fn retire_disconnected(state: &mut Ferese) {
if let Some(mut output) = state if let Some(mut output) = state
.direct_backend .direct_backend
.as_mut() .as_mut()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.devices .devices
.get_mut(&node) .get_mut(&node)
.unwrap() .expect("the selected DRM device must remain registered during this operation")
.outputs .outputs
.remove(&crtc) .remove(&crtc)
{ {
@@ -629,7 +643,7 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
if state if state
.direct_backend .direct_backend
.as_ref() .as_ref()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.devices .devices
.values() .values()
.any(|device| { .any(|device| {
@@ -641,7 +655,10 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
{ {
return; return;
} }
let backend = state.direct_backend.as_ref().unwrap(); let backend = state
.direct_backend
.as_ref()
.expect("DRM callbacks require an installed direct backend");
let requested = backend.desired_outputs.clone(); let requested = backend.desired_outputs.clone();
let original_error = backend.output_error.clone(); let original_error = backend.output_error.clone();
let mut candidates = backend let mut candidates = backend
@@ -652,7 +669,10 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
candidates.sort_by_key(|monitor| (!monitor.internal, monitor.key.clone())); candidates.sort_by_key(|monitor| (!monitor.internal, monitor.key.clone()));
for candidate in candidates { for candidate in candidates {
let backend = state.direct_backend.as_mut().unwrap(); let backend = state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend");
backend.desired_outputs = DesiredOutputConfiguration { backend.desired_outputs = DesiredOutputConfiguration {
profile: None, profile: None,
suspend: requested.suspend, suspend: requested.suspend,
@@ -687,7 +707,7 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
if state if state
.direct_backend .direct_backend
.as_ref() .as_ref()
.unwrap() .expect("DRM callbacks require an installed direct backend")
.devices .devices
.values() .values()
.any(|device| { .any(|device| {
@@ -698,13 +718,20 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
}) })
{ {
remember_applied_configuration(state); remember_applied_configuration(state);
state.direct_backend.as_mut().unwrap().output_error = original_error; state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend")
.output_error = original_error;
tracing::warn!(identity = %candidate.identity, "recovered last usable output with preferred mode"); tracing::warn!(identity = %candidate.identity, "recovered last usable output with preferred mode");
return; return;
} }
} }
} }
let backend = state.direct_backend.as_mut().unwrap(); let backend = state
.direct_backend
.as_mut()
.expect("DRM callbacks require an installed direct backend");
backend.desired_outputs = requested; backend.desired_outputs = requested;
backend.output_error = original_error; backend.output_error = original_error;
} }
+5 -2
View File
@@ -115,8 +115,11 @@ impl Config {
theme: &ferese_config::theme::ResolvedTheme, theme: &ferese_config::theme::ResolvedTheme,
) -> Result<ThemeSettings, ConfigError> { ) -> Result<ThemeSettings, ConfigError> {
let config = Self { let config = Self {
theme: serde_json::from_value(serde_json::to_value(&theme.tokens).unwrap()) theme: serde_json::from_value(
.expect("resolved theme matches runtime schema"), serde_json::to_value(&theme.tokens)
.expect("typed configuration and IPC values must serialize to JSON"),
)
.expect("resolved theme matches runtime schema"),
..Self::default() ..Self::default()
}; };
let mut settings = config.theme_settings()?; let mut settings = config.theme_settings()?;
+33 -11
View File
@@ -132,14 +132,20 @@ impl Loader {
loop { loop {
let (requests, ready) = &*worker; let (requests, ready) = &*worker;
let mut requests = ready let mut requests = ready
.wait_while(requests.lock().unwrap(), |requests| { .wait_while(
!requests.closed && requests.pending.is_none() requests.lock().expect(
}) "cursor decode queue must remain consistent across workers",
.unwrap(); ),
|requests| !requests.closed && requests.pending.is_none(),
)
.expect("cursor decode queue must remain consistent across workers");
if requests.closed { if requests.closed {
break; break;
} }
let request = requests.pending.take().unwrap(); let request = requests
.pending
.take()
.expect("the decode wait completed with a pending cursor request");
requests.in_flight = Some(request); requests.in_flight = Some(request);
drop(requests); drop(requests);
let cursor = load(request); let cursor = load(request);
@@ -155,7 +161,11 @@ impl Loader {
break; break;
} }
signal.wakeup(); signal.wakeup();
worker.0.lock().unwrap().in_flight = None; worker
.0
.lock()
.expect("cursor decode queue must remain consistent across workers")
.in_flight = None;
} }
})?; })?;
Ok(Self { shared, results }) Ok(Self { shared, results })
@@ -163,7 +173,11 @@ impl Loader {
pub(crate) fn request(&self, icon: CursorIcon, scale: i32) { pub(crate) fn request(&self, icon: CursorIcon, scale: i32) {
let request = (icon, scale); let request = (icon, scale);
let mut requests = self.shared.0.lock().unwrap(); let mut requests = self
.shared
.0
.lock()
.expect("cursor decode queue must remain consistent across workers");
if requests.in_flight == Some(request) || requests.pending == Some(request) { if requests.in_flight == Some(request) || requests.pending == Some(request) {
return; return;
} }
@@ -174,7 +188,11 @@ impl Loader {
impl Drop for Loader { impl Drop for Loader {
fn drop(&mut self) { fn drop(&mut self) {
let mut requests = self.shared.0.lock().unwrap(); let mut requests = self
.shared
.0
.lock()
.expect("cursor decode queue must remain consistent across workers");
requests.closed = true; requests.closed = true;
requests.pending = None; requests.pending = None;
self.shared.1.notify_one(); self.shared.1.notify_one();
@@ -256,7 +274,11 @@ impl crate::Ferese {
}) else { }) else {
break; break;
}; };
bytes -= self.named_cursors.remove(&evict).unwrap().bytes; bytes -= self
.named_cursors
.remove(&evict)
.expect("cursor selection retains the requested or default cursor")
.bytes;
} }
self.named_cursors.insert(icon, cursor); self.named_cursors.insert(icon, cursor);
if affects_current { if affects_current {
@@ -299,12 +321,12 @@ impl crate::Ferese {
if !visible { if !visible {
return; return;
} }
let icon = icon.unwrap(); let icon = icon.expect("the hidden-cursor branch returned before image selection");
let cursor = self let cursor = self
.named_cursors .named_cursors
.get(&icon) .get(&icon)
.or_else(|| self.named_cursors.get(&CursorIcon::Default)) .or_else(|| self.named_cursors.get(&CursorIcon::Default))
.unwrap(); .expect("cursor selection retains the requested or default cursor");
let (frame, remaining) = let (frame, remaining) =
cursor.sample(now.saturating_duration_since(self.cursor_animation.epoch)); cursor.sample(now.saturating_duration_since(self.cursor_animation.epoch));
if self.cursor_animation.frame != frame { if self.cursor_animation.frame != frame {
+4 -1
View File
@@ -66,7 +66,10 @@ impl<K: Eq, C: Eq, T> Queue<K, C, T> {
mut complete: impl FnMut(Pending<K, C, T>), mut complete: impl FnMut(Pending<K, C, T>),
) { ) {
for _ in 0..self.pending.len() { for _ in 0..self.pending.len() {
let entry = self.pending.pop_front().unwrap(); let entry = self
.pending
.pop_front()
.expect("the bounded drain removes at most the original queue length");
if remove(&entry) { if remove(&entry) {
complete(entry); complete(entry);
} else { } else {
+110 -27
View File
@@ -109,22 +109,52 @@ impl SurfaceEffectsState {
} }
fn commit_presentation(&self) { fn commit_presentation(&self) {
let Some(pending) = self.pending.lock().unwrap().take() else { let Some(pending) = self
.pending
.lock()
.expect("surface effect state must not be used after a mutator panics")
.take()
else {
return; return;
}; };
*self.role.lock().unwrap() = Some(pending.role); *self
*self.panel.lock().unwrap() = pending.panel; .role
*self.regions.lock().unwrap() = Some(pending.regions); .lock()
*self.region_opacities.lock().unwrap() = pending.region_opacities; .expect("surface effect state must not be used after a mutator panics") =
Some(pending.role);
*self
.panel
.lock()
.expect("surface effect state must not be used after a mutator panics") = pending.panel;
*self
.regions
.lock()
.expect("surface effect state must not be used after a mutator panics") =
Some(pending.regions);
*self
.region_opacities
.lock()
.expect("surface effect state must not be used after a mutator panics") =
pending.region_opacities;
if !self.dismissing.load(Ordering::Acquire) { if !self.dismissing.load(Ordering::Acquire) {
*self.opacity.lock().unwrap() = pending.opacity; *self
.opacity
.lock()
.expect("surface effect state must not be used after a mutator panics") =
pending.opacity;
} }
self.generation.fetch_add(1, Ordering::Release); self.generation.fetch_add(1, Ordering::Release);
} }
fn set_role(&self, role: Option<SemanticRole>) -> bool { fn set_role(&self, role: Option<SemanticRole>) -> bool {
self.pending.lock().unwrap().take(); self.pending
let mut current = self.role.lock().unwrap(); .lock()
.expect("surface effect state must not be used after a mutator panics")
.take();
let mut current = self
.role
.lock()
.expect("surface effect state must not be used after a mutator panics");
if *current != role { if *current != role {
*current = role; *current = role;
self.generation.fetch_add(1, Ordering::Release); self.generation.fetch_add(1, Ordering::Release);
@@ -144,7 +174,11 @@ impl SurfaceEffectsUserData {
} }
fn surface(&self) -> Option<WlSurface> { fn surface(&self) -> Option<WlSurface> {
self.0.lock().unwrap().upgrade().ok() self.0
.lock()
.expect("surface effect state must not be used after a mutator panics")
.upgrade()
.ok()
} }
} }
@@ -182,7 +216,10 @@ impl Dispatch<FereseMaterialManagerV1, ()> for Ferese {
states states
.data_map .data_map
.insert_if_missing_threadsafe(SurfaceEffectsState::new); .insert_if_missing_threadsafe(SurfaceEffectsState::new);
let effects = states.data_map.get::<SurfaceEffectsState>().unwrap(); let effects = states
.data_map
.get::<SurfaceEffectsState>()
.expect("effects state was inserted before this access");
effects.attached.swap(true, Ordering::AcqRel) effects.attached.swap(true, Ordering::AcqRel)
}); });
if attached { if attached {
@@ -248,7 +285,7 @@ pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[f64; 5]>> {
.get::<SurfaceEffectsState>()? .get::<SurfaceEffectsState>()?
.regions .regions
.lock() .lock()
.unwrap() .expect("surface effect state must not be used after a mutator panics")
.clone() .clone()
}) })
} }
@@ -260,7 +297,7 @@ pub(crate) fn surface_panel_presentation(surface: &WlSurface) -> Option<PanelPre
.get::<SurfaceEffectsState>()? .get::<SurfaceEffectsState>()?
.panel .panel
.lock() .lock()
.unwrap() .expect("surface effect state must not be used after a mutator panics")
.clone() .clone()
}) })
} }
@@ -296,7 +333,13 @@ pub(crate) fn surface_region_opacities(surface: &WlSurface) -> Vec<u32> {
states states
.data_map .data_map
.get::<SurfaceEffectsState>() .get::<SurfaceEffectsState>()
.map(|effects| effects.region_opacities.lock().unwrap().clone()) .map(|effects| {
effects
.region_opacities
.lock()
.expect("surface effect state must not be used after a mutator panics")
.clone()
})
.unwrap_or_default() .unwrap_or_default()
}) })
} }
@@ -345,7 +388,10 @@ fn decode_regions(bytes: &[u8]) -> Option<Vec<[f64; 5]>> {
pub(crate) fn surface_role(surface: &WlSurface) -> Option<(SemanticRole, u64)> { pub(crate) fn surface_role(surface: &WlSurface) -> Option<(SemanticRole, u64)> {
with_states(surface, |states| { with_states(surface, |states| {
let effects = states.data_map.get::<SurfaceEffectsState>()?; let effects = states.data_map.get::<SurfaceEffectsState>()?;
let role = *effects.role.lock().unwrap(); let role = *effects
.role
.lock()
.expect("surface effect state must not be used after a mutator panics");
role.map(|role| (role, effects.generation.load(Ordering::Acquire))) role.map(|role| (role, effects.generation.load(Ordering::Acquire)))
}) })
} }
@@ -388,7 +434,7 @@ impl Dispatch<FereseEffectsManagerV1, ()> for Ferese {
states states
.data_map .data_map
.get::<SurfaceEffectsState>() .get::<SurfaceEffectsState>()
.unwrap() .expect("effects state was inserted before this access")
.attached .attached
.swap(true, Ordering::AcqRel) .swap(true, Ordering::AcqRel)
}); });
@@ -404,7 +450,7 @@ impl Dispatch<FereseEffectsManagerV1, ()> for Ferese {
states states
.data_map .data_map
.get::<SurfaceEffectsState>() .get::<SurfaceEffectsState>()
.unwrap() .expect("effects state was inserted before this access")
.presentation_supported .presentation_supported
.store(effects.version() >= 3, Ordering::Release) .store(effects.version() >= 3, Ordering::Release)
}); });
@@ -448,7 +494,9 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
} }
with_states(&surface, |states| { with_states(&surface, |states| {
if let Some(effects) = states.data_map.get::<SurfaceEffectsState>() { if let Some(effects) = states.data_map.get::<SurfaceEffectsState>() {
*effects.pending.lock().unwrap() = Some(PendingPresentation { *effects.pending.lock().expect(
"surface effect state must not be used after a mutator panics",
) = Some(PendingPresentation {
panel: Some(panel), panel: Some(panel),
role: SemanticRole::Panel, role: SemanticRole::Panel,
region_opacities: vec![1000; regions.len()], region_opacities: vec![1000; regions.len()],
@@ -473,7 +521,9 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
}; };
with_states(&surface, |states| { with_states(&surface, |states| {
if let Some(effects) = states.data_map.get::<SurfaceEffectsState>() { if let Some(effects) = states.data_map.get::<SurfaceEffectsState>() {
*effects.pending.lock().unwrap() = Some(PendingPresentation { *effects.pending.lock().expect(
"surface effect state must not be used after a mutator panics",
) = Some(PendingPresentation {
panel: None, panel: None,
role, role,
regions, regions,
@@ -491,7 +541,10 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
if effects.dismissing.load(Ordering::Acquire) { if effects.dismissing.load(Ordering::Acquire) {
return false; return false;
} }
let mut current = effects.opacity.lock().unwrap(); let mut current = effects
.opacity
.lock()
.expect("surface effect state must not be used after a mutator panics");
let opacity = opacity.min(1000); let opacity = opacity.min(1000);
if *current == opacity { if *current == opacity {
return false; return false;
@@ -520,7 +573,10 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
let Some(effects) = states.data_map.get::<SurfaceEffectsState>() else { let Some(effects) = states.data_map.get::<SurfaceEffectsState>() else {
return false; return false;
}; };
let mut old = effects.region_opacities.lock().unwrap(); let mut old = effects
.region_opacities
.lock()
.expect("surface effect state must not be used after a mutator panics");
if *old == opacities { if *old == opacities {
return false; return false;
} }
@@ -540,7 +596,10 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
let Some(effects) = states.data_map.get::<SurfaceEffectsState>() else { let Some(effects) = states.data_map.get::<SurfaceEffectsState>() else {
return false; return false;
}; };
let mut old = effects.regions.lock().unwrap(); let mut old = effects
.regions
.lock()
.expect("surface effect state must not be used after a mutator panics");
if old.as_ref() == Some(&regions) { if old.as_ref() == Some(&regions) {
return false; return false;
} }
@@ -606,11 +665,26 @@ fn detach(surface: &WlSurface) -> bool {
return false; return false;
}; };
let role_changed = effects.set_role(None); let role_changed = effects.set_role(None);
effects.region_opacities.lock().unwrap().clear(); effects
effects.pending.lock().unwrap().take(); .region_opacities
effects.panel.lock().unwrap().take(); .lock()
.expect("surface effect state must not be used after a mutator panics")
.clear();
effects
.pending
.lock()
.expect("surface effect state must not be used after a mutator panics")
.take();
effects
.panel
.lock()
.expect("surface effect state must not be used after a mutator panics")
.take();
effects.dismissing.store(false, Ordering::Release); effects.dismissing.store(false, Ordering::Release);
let mut opacity = effects.opacity.lock().unwrap(); let mut opacity = effects
.opacity
.lock()
.expect("surface effect state must not be used after a mutator panics");
let opacity_changed = *opacity != 1000; let opacity_changed = *opacity != 1000;
*opacity = 1000; *opacity = 1000;
let was_attached = effects.attached.swap(false, Ordering::AcqRel); let was_attached = effects.attached.swap(false, Ordering::AcqRel);
@@ -624,7 +698,12 @@ pub(crate) fn surface_opacity(surface: &WlSurface) -> f32 {
.data_map .data_map
.get::<SurfaceEffectsState>() .get::<SurfaceEffectsState>()
.map_or(1.0, |effects| { .map_or(1.0, |effects| {
*effects.opacity.lock().unwrap() as f32 / 1000.0 *effects
.opacity
.lock()
.expect("surface effect state must not be used after a mutator panics")
as f32
/ 1000.0
}) })
}) })
} }
@@ -645,7 +724,11 @@ pub(crate) fn begin_surface_dismiss(surface: &WlSurface) -> bool {
pub(crate) fn fade_dismissed_surface(surface: &WlSurface, opacity: f64) { pub(crate) fn fade_dismissed_surface(surface: &WlSurface, opacity: f64) {
with_states(surface, |states| { with_states(surface, |states| {
if let Some(effects) = states.data_map.get::<SurfaceEffectsState>() { if let Some(effects) = states.data_map.get::<SurfaceEffectsState>() {
*effects.opacity.lock().unwrap() = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32; *effects
.opacity
.lock()
.expect("surface effect state must not be used after a mutator panics") =
(opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
effects.generation.fetch_add(1, Ordering::Release); effects.generation.fetch_add(1, Ordering::Release);
} }
}); });
+18 -5
View File
@@ -213,9 +213,13 @@ impl SaveThread {
loop { loop {
let job = { let job = {
let (mutex, ready) = &*worker; let (mutex, ready) = &*worker;
let mut state = mutex.lock().unwrap(); let mut state = mutex.lock().expect(
"screenshot writer queue must remain consistent across workers",
);
while state.pending.is_none() && !state.closed { while state.pending.is_none() && !state.closed {
state = ready.wait(state).unwrap(); state = ready.wait(state).expect(
"screenshot writer queue must remain consistent across workers",
);
} }
match state.pending.take() { match state.pending.take() {
Some(job) => job, Some(job) => job,
@@ -237,7 +241,10 @@ impl SaveThread {
fn queue(&self, path: PathBuf, memory: Memory) { fn queue(&self, path: PathBuf, memory: Memory) {
let (mutex, ready) = &*self.shared; let (mutex, ready) = &*self.shared;
mutex.lock().unwrap().pending = Some((path, memory)); mutex
.lock()
.expect("screenshot writer queue must remain consistent across workers")
.pending = Some((path, memory));
ready.notify_one(); ready.notify_one();
} }
} }
@@ -245,7 +252,10 @@ impl SaveThread {
impl Drop for SaveThread { impl Drop for SaveThread {
fn drop(&mut self) { fn drop(&mut self) {
let (mutex, ready) = &*self.shared; let (mutex, ready) = &*self.shared;
mutex.lock().unwrap().closed = true; mutex
.lock()
.expect("screenshot writer queue must remain consistent across workers")
.closed = true;
ready.notify_one(); ready.notify_one();
// Flush the latest snapshot only during compositor shutdown, after // Flush the latest snapshot only during compositor shutdown, after
// the event loop has stopped. Drag completion never joins this thread. // the event loop has stopped. Drag completion never joins this thread.
@@ -265,7 +275,10 @@ impl SaveWorker {
if self.worker.is_none() { if self.worker.is_none() {
self.worker = Some(SaveThread::start(|path, memory| memory.persist(path))?); self.worker = Some(SaveThread::start(|path, memory| memory.persist(path))?);
} }
self.worker.as_ref().unwrap().queue(path, memory.clone()); self.worker
.as_ref()
.expect("screenshot submission initialized the writer before queueing work")
.queue(path, memory.clone());
Ok(()) Ok(())
} }
} }
+3 -1
View File
@@ -177,7 +177,9 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>) std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>)
}); });
let (changed, requested_focus) = { let (changed, requested_focus) = {
let mut previous = previous.lock().unwrap(); let mut previous = previous
.lock()
.expect("layer configure state must remain consistent across callbacks");
let changed = previous.as_ref().map(|(layout, _)| layout) != Some(&current); let changed = previous.as_ref().map(|(layout, _)| layout) != Some(&current);
let requested_focus = policy.keyboard_interactivity == KeyboardInteractivity::OnDemand let requested_focus = policy.keyboard_interactivity == KeyboardInteractivity::OnDemand
&& previous && previous
+14 -3
View File
@@ -250,7 +250,10 @@ impl Dispatch<ZwlrScreencopyFrameV1, Arc<FrameData>> for Ferese {
state.queue_screencopy(frame, data, buffer, true); state.queue_screencopy(frame, data, buffer, true);
} }
zwlr_screencopy_frame_v1::Request::Destroy => { zwlr_screencopy_frame_v1::Request::Destroy => {
data.snapshot.lock().unwrap().take(); data.snapshot
.lock()
.expect("surface rendering state must remain consistent across callbacks")
.take();
} }
_ => unreachable!(), _ => unreachable!(),
} }
@@ -265,11 +268,19 @@ impl Ferese {
buffer: WlBuffer, buffer: WlBuffer,
with_damage: bool, with_damage: bool,
) { ) {
if data.snapshot.lock().unwrap().is_some() { if data
.snapshot
.lock()
.expect("surface rendering state must remain consistent across callbacks")
.is_some()
{
super::window_capture::copy_snapshot(self, frame, data, &buffer); super::window_capture::copy_snapshot(self, frame, data, &buffer);
return; return;
} }
let mut used = data.used.lock().unwrap(); let mut used = data
.used
.lock()
.expect("surface rendering state must remain consistent across callbacks");
if self.session_lock.active() { if self.session_lock.active() {
frame.failed(); frame.failed();
return; return;
+36 -10
View File
@@ -35,7 +35,9 @@ struct Permit {
impl Permit { impl Permit {
fn reserve(budget: &Arc<Mutex<Budget>>, bytes: usize) -> Option<Self> { fn reserve(budget: &Arc<Mutex<Budget>>, bytes: usize) -> Option<Self> {
let mut available = budget.lock().unwrap(); let mut available = budget
.lock()
.expect("surface rendering state must remain consistent across callbacks");
if bytes == 0 if bytes == 0
|| bytes > MAX_BYTES.saturating_sub(available.bytes) || bytes > MAX_BYTES.saturating_sub(available.bytes)
|| available.frames >= MAX_FRAMES || available.frames >= MAX_FRAMES
@@ -53,7 +55,10 @@ impl Permit {
impl Drop for Permit { impl Drop for Permit {
fn drop(&mut self) { fn drop(&mut self) {
let mut budget = self.budget.lock().unwrap(); let mut budget = self
.budget
.lock()
.expect("surface rendering state must remain consistent across callbacks");
budget.bytes -= self.bytes; budget.bytes -= self.bytes;
budget.frames -= 1; budget.frames -= 1;
} }
@@ -167,21 +172,32 @@ fn finish_conversion(state: &Ferese, result: Conversion) {
let Some(data) = frame.data::<Arc<FrameData>>() else { let Some(data) = frame.data::<Arc<FrameData>>() else {
return; return;
}; };
if !frame.is_alive() || *data.used.lock().unwrap() { if !frame.is_alive()
|| *data
.used
.lock()
.expect("surface rendering state must remain consistent across callbacks")
{
return; return;
} }
if state.session_lock.active() if state.session_lock.active()
|| !super::screencopy::capture_allowed() || !super::screencopy::capture_allowed()
|| !state.capture_window_allowed(snapshot.id, snapshot.epoch) || !state.capture_window_allowed(snapshot.id, snapshot.epoch)
{ {
*data.used.lock().unwrap() = true; *data
.used
.lock()
.expect("surface rendering state must remain consistent across callbacks") = true;
frame.failed(); frame.failed();
return; return;
} }
let size = (snapshot.pixels.width as u32, snapshot.pixels.height as u32); let size = (snapshot.pixels.width as u32, snapshot.pixels.height as u32);
let logical_size = snapshot.logical_size; let logical_size = snapshot.logical_size;
*data.snapshot.lock().unwrap() = Some(snapshot); *data
.snapshot
.lock()
.expect("surface rendering state must remain consistent across callbacks") = Some(snapshot);
if manager.is_alive() { if manager.is_alive() {
manager.geometry(&frame, logical_size.0, logical_size.1); manager.geometry(&frame, logical_size.0, logical_size.1);
} }
@@ -256,7 +272,8 @@ impl Dispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
} else if global } else if global
.worker .worker
.try_send(Conversion { .try_send(Conversion {
snapshot: snapshot.unwrap(), snapshot: snapshot
.expect("conversion is queued only after obtaining a capture snapshot"),
frame: resource.clone(), frame: resource.clone(),
manager: manager.clone(), manager: manager.clone(),
}) })
@@ -264,10 +281,11 @@ impl Dispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
{ {
*resource *resource
.data::<Arc<FrameData>>() .data::<Arc<FrameData>>()
.unwrap() .expect("capture frame resources are initialized with their FrameData")
.used .used
.lock() .lock()
.unwrap() = true; .expect("surface rendering state must remain consistent across callbacks") =
true;
manager.busy(&resource); manager.busy(&resource);
resource.failed(); resource.failed();
} }
@@ -386,7 +404,10 @@ pub(super) fn copy_snapshot(
data: &FrameData, data: &FrameData,
buffer: &WlBuffer, buffer: &WlBuffer,
) { ) {
let mut used = data.used.lock().unwrap(); let mut used = data
.used
.lock()
.expect("surface rendering state must remain consistent across callbacks");
if *used { if *used {
resource.post_error( resource.post_error(
frame::Error::AlreadyUsed, frame::Error::AlreadyUsed,
@@ -395,7 +416,12 @@ pub(super) fn copy_snapshot(
return; return;
} }
*used = true; *used = true;
let Some(snapshot) = data.snapshot.lock().unwrap().take() else { let Some(snapshot) = data
.snapshot
.lock()
.expect("surface rendering state must remain consistent across callbacks")
.take()
else {
resource.failed(); resource.failed();
return; return;
}; };
+2 -2
View File
@@ -329,9 +329,9 @@ pub(crate) fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
states states
.data_map .data_map
.get::<XdgToplevelSurfaceData>() .get::<XdgToplevelSurfaceData>()
.unwrap() .expect("an XDG toplevel must retain its Smithay surface data")
.lock() .lock()
.unwrap() .expect("Smithay surface state must remain consistent across callbacks")
.initial_configure_sent .initial_configure_sent
}) })
} }
+1 -1
View File
@@ -142,7 +142,7 @@ impl Ferese {
states states
.data_map .data_map
.get::<XdgToplevelSurfaceData>() .get::<XdgToplevelSurfaceData>()
.is_some_and(|data| data.lock().unwrap().parent.is_some()) .is_some_and(|data| data.lock().expect("Smithay surface metadata must remain consistent across callbacks").parent.is_some())
}); });
crate::window_rules::resolve( crate::window_rules::resolve(
&self.window_rules, &self.window_rules,
+11 -3
View File
@@ -245,8 +245,12 @@ async fn action(
.length_us .length_us
.map_or(position, |length| position.min(length)) .map_or(position, |length| position.min(length))
.min(i64::MAX as u64) as i64; .min(i64::MAX as u64) as i64;
let track = OwnedObjectPath::try_from(view.track_id.clone().unwrap()) let track = OwnedObjectPath::try_from(
.map_err(|error| error.to_string())?; view.track_id
.clone()
.expect("media action validation requires an active track"),
)
.map_err(|error| error.to_string())?;
connection connection
.call_method( .call_method(
Some(player.owner.as_str()), Some(player.owner.as_str()),
@@ -262,7 +266,11 @@ async fn action(
.as_f64() .as_f64()
.filter(|delta| delta.is_finite() && delta.abs() <= 1.0) .filter(|delta| delta.is_finite() && delta.abs() <= 1.0)
.ok_or("Invalid volume change")?; .ok_or("Invalid volume change")?;
let volume = (view.volume.unwrap() + delta).clamp(0.0, 1.0); let volume = (view
.volume
.expect("relative volume validation requires the current volume")
+ delta)
.clamp(0.0, 1.0);
connection connection
.call_method( .call_method(
Some(player.owner.as_str()), Some(player.owner.as_str()),
+16 -3
View File
@@ -1048,7 +1048,10 @@ impl Ferese {
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard"); let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
let modifiers = keyboard.modifier_state(); let modifiers = keyboard.modifier_state();
let raw_symbols = keyboard.with_xkb_state(self, |context| { let raw_symbols = keyboard.with_xkb_state(self, |context| {
let xkb = context.xkb().lock().unwrap(); let xkb = context
.xkb()
.lock()
.expect("keyboard keymap state must remain consistent across callbacks");
xkb.raw_syms_for_key_in_layout(keycode, xkb.active_layout()) xkb.raw_syms_for_key_in_layout(keycode, xkb.active_layout())
.iter() .iter()
.map(|symbol| symbol.raw()) .map(|symbol| symbol.raw())
@@ -1484,7 +1487,11 @@ fn surface_tree_offset(root: &WlSurface, target: &WlSurface) -> Option<Point<i32
states states
.data_map .data_map
.get::<RendererSurfaceStateUserData>() .get::<RendererSurfaceStateUserData>()
.and_then(|data| data.lock().unwrap().view()) .and_then(|data| {
data.lock()
.expect("Smithay surface metadata must remain consistent across callbacks")
.view()
})
.map_or(TraversalAction::SkipChildren, |view| { .map_or(TraversalAction::SkipChildren, |view| {
TraversalAction::DoChildren(*location + view.offset) TraversalAction::DoChildren(*location + view.offset)
}) })
@@ -1494,7 +1501,13 @@ fn surface_tree_offset(root: &WlSurface, target: &WlSurface) -> Option<Point<i32
&& let Some(view) = states && let Some(view) = states
.data_map .data_map
.get::<RendererSurfaceStateUserData>() .get::<RendererSurfaceStateUserData>()
.and_then(|data| data.lock().unwrap().view()) .and_then(|data| {
data.lock()
.expect(
"Smithay surface metadata must remain consistent across callbacks",
)
.view()
})
{ {
found.set(Some(*location + view.offset)); found.set(Some(*location + view.offset));
} }
+23 -6
View File
@@ -174,7 +174,11 @@ impl InputCapture {
self.disable_all(); self.disable_all();
let ids = self.sessions.keys().copied().collect::<Vec<_>>(); let ids = self.sessions.keys().copied().collect::<Vec<_>>();
for id in ids { for id in ids {
self.sessions.get_mut(&id).unwrap().barriers.clear(); self.sessions
.get_mut(&id)
.expect("input capture operations validate their session or waiter before mutation")
.barriers
.clear();
self.enqueue(id, json!({"type":"zones", "zone_set":self.zone_set})); self.enqueue(id, json!({"type":"zones", "zone_set":self.zone_set}));
} }
self.flush(); self.flush();
@@ -246,7 +250,10 @@ impl InputCapture {
failed.push(barrier.id); failed.push(barrier.id);
} }
} }
self.sessions.get_mut(&id).unwrap().barriers = accepted; self.sessions
.get_mut(&id)
.expect("input capture operations validate their session or waiter before mutation")
.barriers = accepted;
Ok(failed) Ok(failed)
} }
@@ -303,7 +310,10 @@ impl InputCapture {
self.next_activation = self.next_activation.wrapping_add(1); self.next_activation = self.next_activation.wrapping_add(1);
let activation = self.next_activation; let activation = self.next_activation;
self.active = Some(id); self.active = Some(id);
let session = self.sessions.get_mut(&id).unwrap(); let session = self
.sessions
.get_mut(&id)
.expect("input capture operations validate their session or waiter before mutation");
session.activation = Some(activation); session.activation = Some(activation);
self.enqueue( self.enqueue(
id, id,
@@ -367,7 +377,9 @@ impl InputCapture {
self.active = None; self.active = None;
self.last_absolute = None; self.last_absolute = None;
self.restore_focus = true; self.restore_focus = true;
let session = self.sessions.get_mut(&id).unwrap(); let session = self.sessions.get_mut(&id).expect(
"input capture operations validate their session or waiter before mutation",
);
let activation = session.activation.take(); let activation = session.activation.take();
self.enqueue( self.enqueue(
id, id,
@@ -380,7 +392,10 @@ impl InputCapture {
pub(crate) fn disable(&mut self, id: u64) -> Result<(), String> { pub(crate) fn disable(&mut self, id: u64) -> Result<(), String> {
self.release(id, None)?; self.release(id, None)?;
let session = self.sessions.get_mut(&id).unwrap(); let session = self
.sessions
.get_mut(&id)
.expect("input capture operations validate their session or waiter before mutation");
if session.enabled { if session.enabled {
session.enabled = false; session.enabled = false;
self.enqueue(id, json!({"type":"disabled"})); self.enqueue(id, json!({"type":"disabled"}));
@@ -436,7 +451,9 @@ impl InputCapture {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for owner in ready { for owner in ready {
let (request, id, response) = self.waiters.remove(&owner).unwrap(); let (request, id, response) = self.waiters.remove(&owner).expect(
"input capture operations validate their session or waiter before mutation",
);
let result = if let Some(session) = self.sessions.get_mut(&id) { let result = if let Some(session) = self.sessions.get_mut(&id) {
json!({"events":session.events.drain(..).collect::<Vec<_>>()}) json!({"events":session.events.drain(..).collect::<Vec<_>>()})
} else { } else {
+5 -2
View File
@@ -1106,7 +1106,10 @@ impl Ferese {
self.refresh_input_capture_zones(); self.refresh_input_capture_zones();
let keyboard = self.seat.get_keyboard().expect("seat has keyboard"); let keyboard = self.seat.get_keyboard().expect("seat has keyboard");
let keymap = keyboard.with_xkb_state(self, |state| { let keymap = keyboard.with_xkb_state(self, |state| {
let xkb = state.xkb().lock().unwrap(); let xkb = state
.xkb()
.lock()
.expect("keyboard keymap state must remain consistent across callbacks");
// SAFETY: the ref-counted keymap does not escape the locked Xkb; // SAFETY: the ref-counted keymap does not escape the locked Xkb;
// only its owned string is retained for the EIS keyboard. // only its owned string is retained for the EIS keyboard.
unsafe { xkb.keymap() } unsafe { xkb.keymap() }
@@ -1708,7 +1711,7 @@ impl Ferese {
.filter_map(|(window, id)| { .filter_map(|(window, id)| {
let toplevel = window.toplevel()?; let toplevel = window.toplevel()?;
let (app_id, title) = with_states(toplevel.wl_surface(), |states| { let (app_id, title) = with_states(toplevel.wl_surface(), |states| {
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().unwrap().lock().unwrap(); let attributes = states.data_map.get::<XdgToplevelSurfaceData>().expect("an XDG toplevel must retain its Smithay surface data").lock().expect("Smithay surface state must remain consistent across callbacks");
( (
attributes.app_id.clone().unwrap_or_default(), attributes.app_id.clone().unwrap_or_default(),
attributes.title.clone().unwrap_or_default(), attributes.title.clone().unwrap_or_default(),
+9 -2
View File
@@ -78,7 +78,9 @@ impl Subscribers {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if let Some(last) = events.last_mut() { if let Some(last) = events.last_mut() {
Arc::get_mut(last).unwrap().last = true; Arc::get_mut(last)
.expect("new initial-event records have not been shared with subscribers")
.last = true;
} }
let initial = self let initial = self
.subscribers .subscribers
@@ -98,7 +100,12 @@ impl Subscribers {
let delivered = if subscriber.initial { let delivered = if subscriber.initial {
subscriber subscriber
.sender .sender
.try_send(initial.as_ref().unwrap().clone()) .try_send(
initial
.as_ref()
.expect("the initial event batch is built before subscription delivery")
.clone(),
)
.is_ok() .is_ok()
} else { } else {
events events
+24 -20
View File
@@ -114,26 +114,30 @@ impl IdentityRegistry {
Some(base) => format!("{base}@{}", escaped(key)), Some(base) => format!("{base}@{}", escaped(key)),
None => format!("drm:{}", escaped(key)), None => format!("drm:{}", escaped(key)),
}; };
let preferred = self let preferred =
.known self.known
.get(key) .get(key)
.filter(|known| { .filter(|known| {
base.as_ref() base.as_ref()
.is_some_and(|base| *known == base || *known == &qualified) .is_some_and(|base| *known == base || *known == &qualified)
|| (base.is_none() && *known == &qualified) || (base.is_none() && *known == &qualified)
}) })
.cloned() .cloned()
.unwrap_or_else(|| { .unwrap_or_else(|| {
if identity if identity
.as_ref() .as_ref()
.is_some_and(|identity| identity.serial.is_some()) .is_some_and(|identity| identity.serial.is_some())
&& !self.ambiguous.contains(base.as_ref().unwrap()) && !self.ambiguous.contains(base.as_ref().expect(
{ "a unique serial-bearing monitor has a computed base identity",
base.clone().unwrap() ))
} else { {
qualified.clone() base.clone().expect(
} "a unique serial-bearing monitor has a computed base identity",
}); )
} else {
qualified.clone()
}
});
let resolved = if used.insert(preferred.clone()) { let resolved = if used.insert(preferred.clone()) {
preferred preferred
} else { } else {
+25 -4
View File
@@ -30,7 +30,11 @@ struct FontRequests {
impl FontRequests { impl FontRequests {
fn request(&self, family: &str) { fn request(&self, family: &str) {
*self.latest.lock().unwrap() = family.to_owned(); *self
.latest
.lock()
.expect("overview font request state must remain consistent across workers") =
family.to_owned();
let _ = self.wake.try_send(()); let _ = self.wake.try_send(());
} }
} }
@@ -59,7 +63,10 @@ pub(crate) fn init_font_loader(
.name("ferese-overview-font".into()) .name("ferese-overview-font".into())
.spawn(move || { .spawn(move || {
while requests.recv().is_ok() { while requests.recv().is_ok() {
let family = worker_latest.lock().unwrap().clone(); let family = worker_latest
.lock()
.expect("overview font request state must remain consistent across workers")
.clone();
// Rescan on a font change so newly installed fonts remain usable. // Rescan on a font change so newly installed fonts remain usable.
let mut database = fontdb::Database::new(); let mut database = fontdb::Database::new();
database.load_system_fonts(); database.load_system_fonts();
@@ -1049,7 +1056,16 @@ impl Ferese {
for (id, target) in overview_layout(window_area(bounds), &windows) { for (id, target) in overview_layout(window_area(bounds), &windows) {
targets.insert( targets.insert(
id, id,
(self.windows.geometry(&id).unwrap().visual.current, target), (
self.windows
.geometry(&id)
.expect(
"overview layout includes only windows with resolved geometry",
)
.visual
.current,
target,
),
); );
} }
continue; continue;
@@ -1074,7 +1090,12 @@ impl Ferese {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for (id, target) in overview_layout(window_area(bounds), &windows) { for (id, target) in overview_layout(window_area(bounds), &windows) {
let normal = self.windows.geometry(&id).unwrap().visual.current; let normal = self
.windows
.geometry(&id)
.expect("overview layout includes only windows with resolved geometry")
.visual
.current;
// Incoming grids start at their overview size. Translation // Incoming grids start at their overview size. Translation
// belongs to the workspace slide, not a second desktop zoom. // belongs to the workspace slide, not a second desktop zoom.
let source = if !sliding.is_empty() { target } else { normal }; let source = if !sliding.is_empty() { target } else { normal };
+4 -1
View File
@@ -146,7 +146,10 @@ impl PortalSession {
pub(crate) fn begin_owned_query(&mut self, owner: u64) -> u32 { pub(crate) fn begin_owned_query(&mut self, owner: u64) -> u32 {
let token = self.begin_query(); let token = self.begin_query();
self.query.as_mut().unwrap().owner = Some(owner); self.query
.as_mut()
.expect("begin_query installs the query before setting its owner")
.owner = Some(owner);
token token
} }
+7 -2
View File
@@ -108,10 +108,15 @@ fn update_shared<E: Element>(
) -> bool { ) -> bool {
// Element queries can read this same backdrop's commit. Snapshot the state, // Element queries can read this same backdrop's commit. Snapshot the state,
// release its lock, then inspect sources. Rendering is the sole writer. // release its lock, then inspect sources. Rendering is the sole writer.
let mut backdrop = shared.lock().unwrap().clone(); let mut backdrop = shared
.lock()
.expect("surface rendering state must remain consistent across callbacks")
.clone();
let sources = elements.iter().filter(|element| element.id() != owner); let sources = elements.iter().filter(|element| element.id() != owner);
let changed = backdrop.update(sources, region, scale); let changed = backdrop.update(sources, region, scale);
*shared.lock().unwrap() = backdrop; *shared
.lock()
.expect("surface rendering state must remain consistent across callbacks") = backdrop;
changed changed
} }
+10 -2
View File
@@ -106,7 +106,11 @@ pub(crate) fn capture_window_frame(
.user_data() .user_data()
.get_or_insert_threadsafe(|| Arc::new(CaptureTarget::default())) .get_or_insert_threadsafe(|| Arc::new(CaptureTarget::default()))
.clone(); .clone();
let reusable = cache.0.lock().unwrap().take(); let reusable = cache
.0
.lock()
.expect("the reusable capture texture cache must remain consistent across callbacks")
.take();
let mut snapshot = render_window_texture( let mut snapshot = render_window_texture(
renderer, renderer,
window, window,
@@ -159,7 +163,11 @@ pub(crate) fn capture_window_frame(
return Err("Incomplete window readback".into()); return Err("Incomplete window readback".into());
} }
let pixels = source[..bytes].to_vec(); let pixels = source[..bytes].to_vec();
*cache.0.lock().unwrap() = Some(snapshot.texture); *cache
.0
.lock()
.expect("the reusable capture texture cache must remain consistent across callbacks") =
Some(snapshot.texture);
Ok(crate::handlers::screenshot::CaptureBuffer { Ok(crate::handlers::screenshot::CaptureBuffer {
width: size.w, width: size.w,
+1 -1
View File
@@ -730,7 +730,7 @@ fn panel_matrices(
match permutation match permutation
.iter() .iter()
.position(|i| *i == old_corner as f32) .position(|i| *i == old_corner as f32)
.unwrap() .expect("the corner transform is a permutation containing every source corner")
{ {
0 => 5, 0 => 5,
1 => 4, 1 => 4,
+9 -2
View File
@@ -222,7 +222,11 @@ impl BlurRenderElement {
self.geometry = parameters.sample_geometry; self.geometry = parameters.sample_geometry;
self.capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer); self.capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer);
self.uniforms = blur_uniforms(parameters).into(); self.uniforms = blur_uniforms(parameters).into();
self.backdrop.lock().unwrap().commit.increment(); self.backdrop
.lock()
.expect("surface rendering state must remain consistent across callbacks")
.commit
.increment();
} }
} }
@@ -232,7 +236,10 @@ impl Element for BlurRenderElement {
} }
fn current_commit(&self) -> CommitCounter { fn current_commit(&self) -> CommitCounter {
self.backdrop.lock().unwrap().commit self.backdrop
.lock()
.expect("surface rendering state must remain consistent across callbacks")
.commit
} }
fn src(&self) -> Rectangle<f64, Buffer> { fn src(&self) -> Rectangle<f64, Buffer> {
+8 -1
View File
@@ -561,7 +561,14 @@ pub(super) fn cursor_elements(
states states
.data_map .data_map
.get::<CursorImageSurfaceData>() .get::<CursorImageSurfaceData>()
.map(|attributes| attributes.lock().unwrap().hotspot) .map(|attributes| {
attributes
.lock()
.expect(
"cursor surface attributes must remain consistent across callbacks",
)
.hotspot
})
.unwrap_or_default() .unwrap_or_default()
}); });
let physical_location = let physical_location =
+16 -4
View File
@@ -39,7 +39,10 @@ struct Cancellation {
impl Cancellation { impl Cancellation {
fn cancel(&self) { fn cancel(&self) {
let mut state = self.state.lock().unwrap(); let mut state = self
.state
.lock()
.expect("resume cancellation state must remain consistent across workers");
state.requested = true; state.requested = true;
let waker = state.waker.take(); let waker = state.waker.take();
self.changed.notify_all(); self.changed.notify_all();
@@ -52,7 +55,10 @@ impl Cancellation {
async fn cancelled(&self) { async fn cancelled(&self) {
future::poll_fn(|context| { future::poll_fn(|context| {
let mut state = self.state.lock().unwrap(); let mut state = self
.state
.lock()
.expect("resume cancellation state must remain consistent across workers");
if state.requested { if state.requested {
return Poll::Ready(()); return Poll::Ready(());
} }
@@ -66,8 +72,14 @@ impl Cancellation {
fn retry(&self, delay: Duration) -> bool { fn retry(&self, delay: Duration) -> bool {
let (state, _) = self let (state, _) = self
.changed .changed
.wait_timeout_while(self.state.lock().unwrap(), delay, |state| !state.requested) .wait_timeout_while(
.unwrap(); self.state
.lock()
.expect("resume cancellation state must remain consistent across workers"),
delay,
|state| !state.requested,
)
.expect("resume cancellation state must remain consistent across workers");
!state.requested !state.requested
} }
} }
+13 -3
View File
@@ -289,7 +289,11 @@ impl Ferese {
surface.alive().then(|| { surface.alive().then(|| {
( (
surface.wl_surface().clone(), surface.wl_surface().clone(),
self.space.output_geometry(output).unwrap().loc.to_f64(), self.space
.output_geometry(output)
.expect("a lock surface is assigned to a mapped output with geometry")
.loc
.to_f64(),
) )
}) })
} }
@@ -366,7 +370,10 @@ impl Ferese {
.map(|(_, surface)| surface) .map(|(_, surface)| surface)
}) })
.map(|surface| surface.wl_surface().clone()); .map(|surface| surface.wl_surface().clone());
let keyboard = self.seat.get_keyboard().unwrap(); let keyboard = self
.seat
.get_keyboard()
.expect("the compositor seat is initialized with a keyboard");
if keyboard.current_focus() != surface { if keyboard.current_focus() != surface {
keyboard.set_focus(self, surface, SERIAL_COUNTER.next_serial()); keyboard.set_focus(self, surface, SERIAL_COUNTER.next_serial());
} }
@@ -493,7 +500,10 @@ impl SessionLockHandler for Ferese {
touch.unset_grab(self); touch.unset_grab(self);
touch.cancel(self); touch.cancel(self);
} }
let keyboard = self.seat.get_keyboard().unwrap(); let keyboard = self
.seat
.get_keyboard()
.expect("the compositor seat is initialized with a keyboard");
keyboard.unset_grab(self); keyboard.unset_grab(self);
keyboard.set_focus(self, None, serial); keyboard.set_focus(self, None, serial);
self.lock_input_activity(); self.lock_input_activity();
+8 -6
View File
@@ -200,9 +200,9 @@ impl Ferese {
let swipe = self let swipe = self
.viewports .viewports
.get_mut(&workspace) .get_mut(&workspace)
.unwrap() .expect("an active workspace gesture retains its viewport state")
.end_gesture(None) .end_gesture(None)
.unwrap(); .expect("an active workspace gesture retains its viewport state");
self.presentation_dependencies self.presentation_dependencies
.restore_viewport(swipe.workspace, swipe.dependencies); .restore_viewport(swipe.workspace, swipe.dependencies);
self.release_presentation_dependencies(); self.release_presentation_dependencies();
@@ -360,10 +360,12 @@ impl Ferese {
} }
} }
} }
let expired = self let expired = self.windows.resizing().any(|id| {
.windows self.windows
.resizing() .transaction(id)
.any(|id| self.windows.transaction(id).unwrap().expired(now)); .expect("the resize candidate retains its active presentation transaction")
.expired(now)
});
self.windows.expire_transactions(now); self.windows.expire_transactions(now);
if expired { if expired {
self.release_presentation_dependencies(); self.release_presentation_dependencies();
+3 -1
View File
@@ -30,7 +30,9 @@ impl Ferese {
let Some(data) = states.data_map.get::<XdgToplevelSurfaceData>() else { let Some(data) = states.data_map.get::<XdgToplevelSurfaceData>() else {
return (true, None); return (true, None);
}; };
let data = data.lock().unwrap(); let data = data
.lock()
.expect("window metadata must remain consistent during privacy checks");
let rule = crate::window_rules::resolve( let rule = crate::window_rules::resolve(
&self.window_rules, &self.window_rules,
data.app_id.as_deref(), data.app_id.as_deref(),
+4 -1
View File
@@ -156,7 +156,10 @@ impl Ferese {
self.windows.update(window, |w| w.maximized = false); self.windows.update(window, |w| w.maximized = false);
} }
if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) { if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) {
let workspace_id = self.workspaces.workspace_for_window(window).unwrap(); let workspace_id = self
.workspaces
.workspace_for_window(window)
.expect("a tiled window belongs to a workspace");
if let Some(workspace) = self.workspaces.workspace_mut(workspace_id) if let Some(workspace) = self.workspaces.workspace_mut(workspace_id)
&& let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout && let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout
{ {
+5 -1
View File
@@ -657,7 +657,11 @@ impl Ferese {
self.resize_metrics.begin( self.resize_metrics.begin(
id, id,
self.workspaces.workspace_for_window(id), self.workspaces.workspace_for_window(id),
&self.windows.record(id).unwrap().app_id, &self
.windows
.record(id)
.expect("the selected window has a registered lifecycle record")
.app_id,
now, now,
); );
self.windows.set_transaction( self.windows.set_transaction(
+7 -2
View File
@@ -57,8 +57,13 @@ impl Ferese {
let Some(mut presentation) = self.current_window_presentation(id) else { let Some(mut presentation) = self.current_window_presentation(id) else {
return; return;
}; };
let record = self.windows.record(id).unwrap(); let record = self
let geometry = record.geometry.unwrap(); .windows
.record(id)
.expect("the selected window has a registered lifecycle record");
let geometry = record
.geometry
.expect("presented windows retain their resolved geometry");
let decorations = geometry.decorations.clamp(0.0, 1.0); let decorations = geometry.decorations.clamp(0.0, 1.0);
let handoff_size = (!presentation.scale_content).then(|| { let handoff_size = (!presentation.scale_content).then(|| {
presentation presentation
+7 -4
View File
@@ -112,7 +112,7 @@ impl Ferese {
if let Some(workspace) = self.focus_swipe().map(|swipe| swipe.workspace) { if let Some(workspace) = self.focus_swipe().map(|swipe| swipe.workspace) {
self.viewports self.viewports
.get_mut(&workspace) .get_mut(&workspace)
.unwrap() .expect("an active workspace gesture retains its viewport state")
.update_gesture(progress); .update_gesture(progress);
} }
@@ -145,9 +145,9 @@ impl Ferese {
let swipe = self let swipe = self
.viewports .viewports
.get_mut(&workspace) .get_mut(&workspace)
.unwrap() .expect("an active workspace gesture retains its viewport state")
.end_gesture(velocity) .end_gesture(velocity)
.unwrap(); .expect("an active workspace gesture retains its viewport state");
self.presentation_dependencies self.presentation_dependencies
.restore_viewport(workspace, swipe.dependencies.clone()); .restore_viewport(workspace, swipe.dependencies.clone());
let neighbor = self.output_bounds().and_then(|bounds| { let neighbor = self.output_bounds().and_then(|bounds| {
@@ -653,7 +653,10 @@ impl Ferese {
{ {
slide.release_with_velocity(true, self.swipe.release_velocity / self.animation_speed); slide.release_with_velocity(true, self.swipe.release_velocity / self.animation_speed);
self.last_animation_tick = Instant::now(); self.last_animation_tick = Instant::now();
self.workspace_slides.insert(owner, previous_slide.unwrap()); self.workspace_slides.insert(
owner,
previous_slide.expect("the reversal branch requires an existing workspace slide"),
);
return; return;
} }
let slide_direction = slide_direction.or_else(|| { let slide_direction = slide_direction.or_else(|| {
@@ -119,7 +119,11 @@ fn tree_reflow_groups(
groups.insert(*id, group); groups.insert(*id, group);
let mut frontier = vec![*id]; let mut frontier = vec![*id];
while let Some(id) = frontier.pop() { while let Some(id) = frontier.pop() {
let bounds = moving.iter().find(|(window, _)| *window == id).unwrap().1; let bounds = moving
.iter()
.find(|(window, _)| *window == id)
.expect("the dependency frontier contains only moving window IDs")
.1;
for (other, candidate) in &moving { for (other, candidate) in &moving {
if !groups.contains_key(other) if !groups.contains_key(other)
&& bounds[0].max(candidate[0]) + 0.001 < bounds[2].min(candidate[2]) && bounds[0].max(candidate[0]) + 0.001 < bounds[2].min(candidate[2])
@@ -448,7 +452,10 @@ impl Ferese {
if self.workspaces.workspace_for_window(*slow) != Some(workspace_id) { if self.workspaces.workspace_for_window(*slow) != Some(workspace_id) {
continue; continue;
} }
let transaction = *self.windows.transaction(slow).unwrap(); let transaction = *self
.windows
.transaction(slow)
.expect("the resize candidate retains its active presentation transaction");
let commit = ClientCommit { let commit = ClientCommit {
window: *slow, window: *slow,
serial: transaction.serial(), serial: transaction.serial(),
@@ -504,7 +511,14 @@ impl Ferese {
self.workspaces.workspace_for_window(**id) == Some(workspace.id) self.workspaces.workspace_for_window(**id) == Some(workspace.id)
&& layout.contains(**id) && layout.contains(**id)
}) })
.map(|id| (*id, *self.windows.transaction(id).unwrap())) .map(|id| {
(
*id,
*self.windows.transaction(id).expect(
"the resize candidate retains its active presentation transaction",
),
)
})
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
previous_viewport.remove(&workspace.id).unwrap_or_default(), previous_viewport.remove(&workspace.id).unwrap_or_default(),
); );
+14 -3
View File
@@ -101,7 +101,12 @@ impl Ferese {
layout: !plan.affected_outputs.is_empty(), layout: !plan.affected_outputs.is_empty(),
focus: plan.focus_changed, focus: plan.focus_changed,
shell: !plan.affected_outputs.is_empty(), shell: !plan.affected_outputs.is_empty(),
outputs: self.desktop_transition.as_ref().unwrap().redraw.clone(), outputs: self
.desktop_transition
.as_ref()
.expect("desktop transition must be started before publication or retirement")
.redraw
.clone(),
}; };
let floats = self let floats = self
.windows .windows
@@ -213,7 +218,10 @@ impl Ferese {
} }
pub(crate) fn finish_desktop_transition(&mut self, mut changes: DesktopChanges) { pub(crate) fn finish_desktop_transition(&mut self, mut changes: DesktopChanges) {
self.desktop_transition.as_mut().unwrap().publishing = true; self.desktop_transition
.as_mut()
.expect("desktop transition must be started before publication or retirement")
.publishing = true;
let activity_output = self.space.outputs().next().map(|output| output.name()); let activity_output = self.space.outputs().next().map(|output| output.name());
if let Some(output) = activity_output if let Some(output) = activity_output
&& (self.activities.store.attach_output(&output) && (self.activities.store.attach_output(&output)
@@ -230,7 +238,10 @@ impl Ferese {
self.reconcile_layer_keyboard_focus(); self.reconcile_layer_keyboard_focus();
self.refresh_lock_outputs(); self.refresh_lock_outputs();
self.refresh_idle_inhibition(); self.refresh_idle_inhibition();
let batch = self.desktop_transition.take().unwrap(); let batch = self
.desktop_transition
.take()
.expect("desktop transition must be started before publication or retirement");
changes.outputs.extend(batch.redraw); changes.outputs.extend(batch.redraw);
// Animation/layout helpers may request snapshots, but only this point // Animation/layout helpers may request snapshots, but only this point
// can emit the complete topology generation. // can emit the complete topology generation.
+10 -4
View File
@@ -236,7 +236,10 @@ impl WindowRegistry<Window> {
if let Some(surface) = surface { if let Some(surface) = surface {
self.surface_ids.insert(surface.clone(), id); self.surface_ids.insert(surface.clone(), id);
self.records.get_mut(&id).unwrap().surface = Some(surface.clone()); self.records
.get_mut(&id)
.expect("the selected window has a registered lifecycle record")
.surface = Some(surface.clone());
self.refresh_metadata(&surface); self.refresh_metadata(&surface);
} }
} }
@@ -245,14 +248,17 @@ impl WindowRegistry<Window> {
let Some(id) = self.id_for_surface(surface) else { let Some(id) = self.id_for_surface(surface) else {
return false; return false;
}; };
let record = self.records.get_mut(&id).unwrap(); let record = self
.records
.get_mut(&id)
.expect("the selected window has a registered lifecycle record");
with_states(surface, |states| { with_states(surface, |states| {
let attributes = states let attributes = states
.data_map .data_map
.get::<XdgToplevelSurfaceData>() .get::<XdgToplevelSurfaceData>()
.unwrap() .expect("an XDG toplevel must retain its Smithay surface data")
.lock() .lock()
.unwrap(); .expect("Smithay surface state must remain consistent across callbacks");
let app_id = attributes.app_id.as_deref().unwrap_or_default(); let app_id = attributes.app_id.as_deref().unwrap_or_default();
let title = attributes.title.as_deref().unwrap_or_default(); let title = attributes.title.as_deref().unwrap_or_default();
let changed = record.app_id != app_id || record.title != title; let changed = record.app_id != app_id || record.title != title;
+4 -4
View File
@@ -28,9 +28,9 @@ impl Ferese {
let attributes = states let attributes = states
.data_map .data_map
.get::<XdgToplevelSurfaceData>() .get::<XdgToplevelSurfaceData>()
.unwrap() .expect("an XDG toplevel must retain its Smithay surface data")
.lock() .lock()
.unwrap(); .expect("Smithay surface state must remain consistent across callbacks");
( (
attributes.app_id.clone(), attributes.app_id.clone(),
attributes.title.clone(), attributes.title.clone(),
@@ -350,9 +350,9 @@ impl Ferese {
let attributes = states let attributes = states
.data_map .data_map
.get::<XdgToplevelSurfaceData>() .get::<XdgToplevelSurfaceData>()
.unwrap() .expect("an XDG toplevel must retain its Smithay surface data")
.lock() .lock()
.unwrap(); .expect("Smithay surface state must remain consistent across callbacks");
( (
attributes.app_id.clone(), attributes.app_id.clone(),
attributes.title.clone(), attributes.title.clone(),
+24 -8
View File
@@ -90,7 +90,9 @@ fn auto_context() -> ferese_config::theme::AutoContext {
Mutex::new(value) Mutex::new(value)
}); });
ferese_config::theme::AutoContext { ferese_config::theme::AutoContext {
system: *appearance.lock().unwrap(), system: *appearance
.lock()
.expect("theme watcher state must remain consistent across workers"),
} }
} }
@@ -264,7 +266,10 @@ impl Engine {
directory.to_owned() directory.to_owned()
}) })
.collect(); .collect();
*self.paths.lock().unwrap() = targets; *self
.paths
.lock()
.expect("theme watcher state must remain consistent across workers") = targets;
if let Some(watcher) = &mut self.watcher { if let Some(watcher) = &mut self.watcher {
for directory in self.watched_directories.difference(&directories) { for directory in self.watched_directories.difference(&directories) {
let _ = watcher.unwatch(directory); let _ = watcher.unwatch(directory);
@@ -349,11 +354,14 @@ pub(crate) fn init(
let (sender, events) = mpsc::sync_channel(1); let (sender, events) = mpsc::sync_channel(1);
let paths = state.theme_engine.paths.clone(); let paths = state.theme_engine.paths.clone();
if let Some(config) = crate::config::config_path() { if let Some(config) = crate::config::config_path() {
paths.lock().unwrap().insert(config); paths
.lock()
.expect("theme watcher state must remain consistent across workers")
.insert(config);
} }
paths paths
.lock() .lock()
.unwrap() .expect("theme watcher state must remain consistent across workers")
.insert(PathBuf::from("/etc/localtime")); .insert(PathBuf::from("/etc/localtime"));
let wakeup = state.loop_signal.clone(); let wakeup = state.loop_signal.clone();
let system_sender = sender.clone(); let system_sender = sender.clone();
@@ -385,7 +393,9 @@ pub(crate) fn init(
.output() .output()
{ {
if let Some(state) = SYSTEM_APPEARANCE.get() { if let Some(state) = SYSTEM_APPEARANCE.get() {
*state.lock().unwrap() = *state
.lock()
.expect("theme watcher state must remain consistent across workers") =
system_appearance_value(&String::from_utf8_lossy(&output.stdout)); system_appearance_value(&String::from_utf8_lossy(&output.stdout));
} }
let _ = system_sender.try_send(()); let _ = system_sender.try_send(());
@@ -398,7 +408,9 @@ pub(crate) fn init(
{ {
let value = system_appearance_value(&line); let value = system_appearance_value(&line);
if let Some(state) = SYSTEM_APPEARANCE.get() { if let Some(state) = SYSTEM_APPEARANCE.get() {
*state.lock().unwrap() = value; *state.lock().expect(
"theme watcher state must remain consistent across workers",
) = value;
} }
let _ = system_sender.try_send(()); let _ = system_sender.try_send(());
system_wakeup.wakeup(); system_wakeup.wakeup();
@@ -434,7 +446,7 @@ fn file_watcher(
&& event.paths.iter().any(|path| { && event.paths.iter().any(|path| {
paths paths
.lock() .lock()
.unwrap() .expect("theme watcher state must remain consistent across workers")
.iter() .iter()
.any(|target| target == path || target.starts_with(path)) .any(|target| target == path || target.starts_with(path))
}) })
@@ -575,7 +587,11 @@ impl Ferese {
} }
} }
if start { if start {
let pending = self.theme_engine.pending.take().unwrap(); let pending = self
.theme_engine
.pending
.take()
.expect("the transition starts only when a theme candidate is pending");
let from = self.theme_engine.live.presented.clone(); let from = self.theme_engine.live.presented.clone();
let previous_wallpaper = pending let previous_wallpaper = pending
.wallpaper .wallpaper
+9 -1
View File
@@ -114,7 +114,15 @@ impl WallpaperState {
let decode_failed = config.path.is_some() && !owned; let decode_failed = config.path.is_some() && !owned;
let receiver = if owned { let receiver = if owned {
Some(decoder.submit(config.path.clone().unwrap(), wakeup.clone())) Some(
decoder.submit(
config
.path
.clone()
.expect("owned wallpaper decoding requires a configured image path"),
wakeup.clone(),
),
)
} else { } else {
decoder.cancel_pending(); decoder.cancel_pending();
None None
+23 -8
View File
@@ -30,15 +30,21 @@ impl Decoder {
loop { loop {
let (queue, ready) = &*worker; let (queue, ready) = &*worker;
let mut queue = ready let mut queue = ready
.wait_while(queue.lock().unwrap(), |queue| { .wait_while(
!queue.closed && queue.pending.is_none() queue.lock().expect(
}) "wallpaper decode queue must remain consistent across workers",
.unwrap(); ),
|queue| !queue.closed && queue.pending.is_none(),
)
.expect("wallpaper decode queue must remain consistent across workers");
if queue.closed { if queue.closed {
break; break;
} }
let request = queue.pending.take().unwrap(); let request = queue
.pending
.take()
.expect("the decode wait completed with a pending wallpaper request");
drop(queue); drop(queue);
let _ = request.result.send(load(&request.path)); let _ = request.result.send(load(&request.path));
if let Some(wakeup) = request.wakeup { if let Some(wakeup) = request.wakeup {
@@ -57,7 +63,10 @@ impl Decoder {
) -> mpsc::Receiver<Result<Pixels, String>> { ) -> mpsc::Receiver<Result<Pixels, String>> {
let (result, receiver) = mpsc::channel(); let (result, receiver) = mpsc::channel();
let (queue, ready) = &*self.shared; let (queue, ready) = &*self.shared;
queue.lock().unwrap().pending = Some(Request { queue
.lock()
.expect("wallpaper decode queue must remain consistent across workers")
.pending = Some(Request {
path, path,
result, result,
wakeup, wakeup,
@@ -67,14 +76,20 @@ impl Decoder {
} }
pub fn cancel_pending(&self) { pub fn cancel_pending(&self) {
self.shared.0.lock().unwrap().pending = None; self.shared
.0
.lock()
.expect("wallpaper decode queue must remain consistent across workers")
.pending = None;
} }
} }
impl Drop for Decoder { impl Drop for Decoder {
fn drop(&mut self) { fn drop(&mut self) {
let (queue, ready) = &*self.shared; let (queue, ready) = &*self.shared;
let mut queue = queue.lock().unwrap(); let mut queue = queue
.lock()
.expect("wallpaper decode queue must remain consistent across workers");
queue.closed = true; queue.closed = true;
queue.pending = None; queue.pending = None;
ready.notify_one(); ready.notify_one();
+1 -1
View File
@@ -185,7 +185,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
let _ = renderer let _ = renderer
.render( .render(
framebuffer, framebuffer,
output.current_mode().unwrap().size, output.current_mode().expect("a rendered output must have a current mode").size,
output.current_transform(), output.current_transform(),
)? )?
.finish()?; .finish()?;
+1 -1
View File
@@ -55,7 +55,7 @@ impl VelocityTracker {
for col in 0..=degree { for col in 0..=degree {
let pivot = (col..=degree) let pivot = (col..=degree)
.max_by(|&a, &b| matrix[a][col].abs().total_cmp(&matrix[b][col].abs())) .max_by(|&a, &b| matrix[a][col].abs().total_cmp(&matrix[b][col].abs()))
.unwrap(); .expect("the inclusive pivot range contains at least the current column");
matrix.swap(col, pivot); matrix.swap(col, pivot);
let divisor = matrix[col][col]; let divisor = matrix[col][col];
if divisor.abs() < 1e-12 { if divisor.abs() < 1e-12 {
+7 -2
View File
@@ -310,13 +310,18 @@ pub(crate) fn parse(node: &KdlNode) -> Result<Value, Error> {
.into_iter() .into_iter()
.filter(|rule| !names.contains(&rule.name)), .filter(|rule| !names.contains(&rule.name)),
); );
values.insert("rules".into(), serde_json::to_value(rules).unwrap()); values.insert(
"rules".into(),
serde_json::to_value(rules)
.expect("typed configuration and IPC values must serialize to JSON"),
);
let settings: Settings = let settings: Settings =
serde_json::from_value(Value::Object(values)).map_err(|error| diagnostic(node, error))?; serde_json::from_value(Value::Object(values)).map_err(|error| diagnostic(node, error))?;
settings settings
.validate() .validate()
.map_err(|error| diagnostic(node, error))?; .map_err(|error| diagnostic(node, error))?;
Ok(serde_json::to_value(settings).unwrap()) Ok(serde_json::to_value(settings)
.expect("typed configuration and IPC values must serialize to JSON"))
} }
fn builtins() -> Vec<Rule> { fn builtins() -> Vec<Rule> {
[ [
+14 -4
View File
@@ -166,7 +166,10 @@ fn object(doc: &KdlDocument, parent: &str) -> Result<Map<String, Value>, Error>
let records = result let records = result
.entry(key) .entry(key)
.or_insert_with(|| Value::Array(Vec::new())); .or_insert_with(|| Value::Array(Vec::new()));
records.as_array_mut().unwrap().push(value); records
.as_array_mut()
.expect("repeated KDL records are stored as arrays")
.push(value);
} else { } else {
insert(&mut result, key, value)?; insert(&mut result, key, value)?;
} }
@@ -255,7 +258,10 @@ pub(crate) fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result<Valu
return Err(Error(format!("{} requires one value", node.name()))); return Err(Error(format!("{} requires one value", node.name())));
} }
Ok(args.into_iter().next().unwrap()) Ok(args
.into_iter()
.next()
.expect("a scalar KDL node was checked to have one argument"))
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -305,7 +311,8 @@ impl Document {
let mut document = self.clone(); let mut document = self.clone();
document.value["theme"] = document.value["theme"] =
serde_json::to_value(&theme.tokens).expect("serializable theme tokens"); serde_json::to_value(&theme.tokens).expect("serializable theme tokens");
document.value["theme"]["appearance"] = serde_json::to_value(theme.appearance).unwrap(); document.value["theme"]["appearance"] = serde_json::to_value(theme.appearance)
.expect("typed configuration and IPC values must serialize to JSON");
document document
} }
@@ -882,7 +889,10 @@ fn kdl_value(value: &Value) -> Result<KdlValue, Error> {
} else if let Some(i) = n.as_u64() { } else if let Some(i) = n.as_u64() {
KdlValue::Integer(i.into()) KdlValue::Integer(i.into())
} else { } else {
KdlValue::Float(n.as_f64().unwrap()) KdlValue::Float(
n.as_f64()
.expect("JSON numbers fit the KDL numeric representation"),
)
} }
} }
_ => return Err(Error("Expected a scalar value".into())), _ => return Err(Error("Expected a scalar value".into())),
+4 -2
View File
@@ -64,10 +64,12 @@ pub fn builtins() -> Vec<Family> {
id: (*id).into(), id: (*id).into(),
name: (*name).into(), name: (*name).into(),
light: Some(Palette::from( light: Some(Palette::from(
&crate::theme::preset(light, Appearance::Light).unwrap(), &crate::theme::preset(light, Appearance::Light)
.expect("built-in theme families must reference registered presets"),
)), )),
dark: Some(Palette::from( dark: Some(Palette::from(
&crate::theme::preset(dark, Appearance::Dark).unwrap(), &crate::theme::preset(dark, Appearance::Dark)
.expect("built-in theme families must reference registered presets"),
)), )),
}) })
.collect() .collect()
+54 -22
View File
@@ -552,8 +552,10 @@ fn token_value(tokens: &Tokens) -> Result<Value, String> {
let mut value = serde_json::to_value(tokens).map_err(|error| error.to_string())?; let mut value = serde_json::to_value(tokens).map_err(|error| error.to_string())?;
// Keep authoring defaults available for merging. The wire format omits // Keep authoring defaults available for merging. The wire format omits
// automatic styles so older snapshots still round-trip unchanged. // automatic styles so older snapshots still round-trip unchanged.
value["focus_ring"]["style"] = serde_json::to_value(tokens.focus_ring.style).unwrap(); value["focus_ring"]["style"] = serde_json::to_value(tokens.focus_ring.style)
value["border"]["style"] = serde_json::to_value(tokens.border.style).unwrap(); .expect("typed configuration and IPC values must serialize to JSON");
value["border"]["style"] = serde_json::to_value(tokens.border.style)
.expect("typed configuration and IPC values must serialize to JSON");
Ok(value) Ok(value)
} }
@@ -673,8 +675,16 @@ pub fn preset(name: &str, appearance: Appearance) -> Result<Tokens, String> {
} else { } else {
[1.; 4] [1.; 4]
}; };
t.colors.surface_raised = hex(blend(rgba(base).unwrap(), shade, 0.055)); t.colors.surface_raised = hex(blend(
t.colors.application_background = hex(blend(rgba(base).unwrap(), shade, 0.025)); rgba(base).expect("resolved theme colors must be valid hexadecimal RGBA"),
shade,
0.055,
));
t.colors.application_background = hex(blend(
rgba(base).expect("resolved theme colors must be valid hexadecimal RGBA"),
shade,
0.025,
));
t.colors.text_primary = text.into(); t.colors.text_primary = text.into();
t.colors.text_muted = muted.into(); t.colors.text_muted = muted.into();
t.colors.accent = accent.into(); t.colors.accent = accent.into();
@@ -708,8 +718,10 @@ pub fn authored_warnings(tokens: &Tokens) -> Vec<String> {
), ),
]; ];
let check = |foreground: &str, background: &str, minimum: f64| { let check = |foreground: &str, background: &str, minimum: f64| {
let foreground = rgba(foreground).unwrap(); let foreground =
let background = rgba(background).unwrap(); rgba(foreground).expect("resolved theme colors must be valid hexadecimal RGBA");
let background =
rgba(background).expect("resolved theme colors must be valid hexadecimal RGBA");
contrast(composite(foreground, background), background) >= minimum contrast(composite(foreground, background), background) >= minimum
}; };
for (name, background) in surfaces { for (name, background) in surfaces {
@@ -813,7 +825,8 @@ fn transform(t: &mut Tokens, accessibility: &Accessibility, warnings: &mut Vec<S
for target in [[0., 0., 0., 1.], [1.; 4]] { for target in [[0., 0., 0., 1.], [1.; 4]] {
for step in 0..=1000 { for step in 0..=1000 {
let distance = f64::from(step) / 1000.; let distance = f64::from(step) / 1000.;
let color = rgba(&hex(blend(requested, target, distance))).unwrap(); let color = rgba(&hex(blend(requested, target, distance)))
.expect("hex output must be a valid RGBA color");
if acceptable(color) && distance < best_distance { if acceptable(color) && distance < best_distance {
best = Some(color); best = Some(color);
best_distance = distance; best_distance = distance;
@@ -858,14 +871,17 @@ fn transform(t: &mut Tokens, accessibility: &Accessibility, warnings: &mut Vec<S
} }
if accessibility.increase_contrast { if accessibility.increase_contrast {
t.colors.border = hex(readable_across( t.colors.border = hex(readable_across(
rgba(&t.colors.border).unwrap(), rgba(&t.colors.border).expect("resolved theme colors must be valid hexadecimal RGBA"),
&backgrounds, &backgrounds,
3., 3.,
)); ));
if backgrounds if backgrounds.iter().any(|background| {
.iter() contrast(
.any(|background| contrast(rgba(&t.colors.border).unwrap(), *background) < 3.) rgba(&t.colors.border)
{ .expect("resolved theme colors must be valid hexadecimal RGBA"),
*background,
) < 3.
}) {
warnings.push("Border cannot reach 3:1 contrast on all configured surfaces".into()); warnings.push("Border cannot reach 3:1 contrast on all configured surfaces".into());
} }
resolve_material_contrast(t, minimum); resolve_material_contrast(t, minimum);
@@ -889,10 +905,14 @@ fn resolve_gradients(tokens: &mut Tokens, preset_end: Option<&str>) {
}; };
let backgrounds = [ let backgrounds = [
rgba(&tokens.colors.surface_base).unwrap(), rgba(&tokens.colors.surface_base)
rgba(&tokens.colors.surface_raised).unwrap(), .expect("resolved theme colors must be valid hexadecimal RGBA"),
rgba(&tokens.colors.application_background).unwrap(), rgba(&tokens.colors.surface_raised)
rgba(&tokens.surface.bar.background).unwrap(), .expect("resolved theme colors must be valid hexadecimal RGBA"),
rgba(&tokens.colors.application_background)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
rgba(&tokens.surface.bar.background)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
]; ];
for (paint, color, endpoint, focus) in [ for (paint, color, endpoint, focus) in [
( (
@@ -915,7 +935,11 @@ fn resolve_gradients(tokens: &mut Tokens, preset_end: Option<&str>) {
let to = endpoint.map_or_else(|| shade(color), str::to_owned); let to = endpoint.map_or_else(|| shade(color), str::to_owned);
let to = if focus { let to = if focus {
hex(readable_across(rgba(&to).unwrap(), &backgrounds, 3.)) hex(readable_across(
rgba(&to).expect("resolved theme colors must be valid hexadecimal RGBA"),
&backgrounds,
3.,
))
} else { } else {
to to
}; };
@@ -931,10 +955,14 @@ fn resolve_material_contrast(tokens: &mut Tokens, minimum: f64) {
if tokens.material.style == "solid" { if tokens.material.style == "solid" {
return; return;
} }
let surface = rgba(&tokens.colors.surface_base).unwrap(); let surface = rgba(&tokens.colors.surface_base)
let bar = rgba(&tokens.surface.bar.background).unwrap(); .expect("resolved theme colors must be valid hexadecimal RGBA");
let raised = rgba(&tokens.colors.surface_raised).unwrap(); let bar = rgba(&tokens.surface.bar.background)
let app = rgba(&tokens.colors.application_background).unwrap(); .expect("resolved theme colors must be valid hexadecimal RGBA");
let raised = rgba(&tokens.colors.surface_raised)
.expect("resolved theme colors must be valid hexadecimal RGBA");
let app = rgba(&tokens.colors.application_background)
.expect("resolved theme colors must be valid hexadecimal RGBA");
let initial = tokens.material.opacity * tokens.material.tint_strength; let initial = tokens.material.opacity * tokens.material.tint_strength;
let readable = |backgrounds: &[[f64; 4]]| { let readable = |backgrounds: &[[f64; 4]]| {
[[0., 0., 0., 1.], [1.; 4]].into_iter().any(|text| { [[0., 0., 0., 1.], [1.; 4]].into_iter().any(|text| {
@@ -962,7 +990,11 @@ fn resolve_material_contrast(tokens: &mut Tokens, minimum: f64) {
(&mut tokens.surface.bar.text_primary, bar.as_slice()), (&mut tokens.surface.bar.text_primary, bar.as_slice()),
(&mut tokens.surface.bar.text_muted, bar.as_slice()), (&mut tokens.surface.bar.text_muted, bar.as_slice()),
] { ] {
*color = hex(readable_across(rgba(color).unwrap(), backgrounds, minimum)); *color = hex(readable_across(
rgba(color).expect("resolved theme colors must be valid hexadecimal RGBA"),
backgrounds,
minimum,
));
} }
} }
+14 -3
View File
@@ -184,7 +184,15 @@ impl OutputWorkspaceMap {
let mut removed = self let mut removed = self
.connected_outputs() .connected_outputs()
.filter(|id| !desired.contains_key(id)) .filter(|id| !desired.contains_key(id))
.map(|id| (id, outputs.outputs.remove(&id).unwrap())) .map(|id| {
(
id,
outputs
.outputs
.remove(&id)
.expect("workspace reconciliation only accesses connected outputs"),
)
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Preserve the original focused workspace when several outputs vanish. // Preserve the original focused workspace when several outputs vanish.
removed.sort_by_key(|(id, _)| (Some(*id) == self.focused, *id)); removed.sort_by_key(|(id, _)| (Some(*id) == self.focused, *id));
@@ -196,7 +204,10 @@ impl OutputWorkspaceMap {
// against the final inventory, where that donor no longer needs it. // against the final inventory, where that donor no longer needs it.
for (id, record) in returning { for (id, record) in returning {
let reclaimed = outputs.reclaim(id, &record); let reclaimed = outputs.reclaim(id, &record);
let state = outputs.outputs.get_mut(&id).unwrap(); let state = outputs
.outputs
.get_mut(&id)
.expect("workspace reconciliation only accesses connected outputs");
state.workspaces.extend(reclaimed.iter().copied()); state.workspaces.extend(reclaimed.iter().copied());
if reclaimed.contains(&record.active) { if reclaimed.contains(&record.active) {
state.activate(record.active); state.activate(record.active);
@@ -635,7 +646,7 @@ impl OutputWorkspaceMap {
self.homes.insert(workspace, output); self.homes.insert(workspace, output);
self.outputs self.outputs
.get_mut(&output) .get_mut(&output)
.unwrap() .expect("workspace reconciliation only accesses connected outputs")
.workspaces .workspaces
.insert(workspace); .insert(workspace);
self.bump_revision(workspace); self.bump_revision(workspace);
+4 -1
View File
@@ -544,7 +544,10 @@ impl WorkspaceSet {
replacements.push((workspace.id, layout)); replacements.push((workspace.id, layout));
} }
for (id, layout) in replacements { for (id, layout) in replacements {
self.workspaces.get_mut(&id).unwrap().layout = layout; self.workspaces
.get_mut(&id)
.expect("replacement layouts were built from existing workspace IDs")
.layout = layout;
} }
self.default_layout_mode = mode; self.default_layout_mode = mode;
self.default_column_width = width; self.default_column_width = width;
+4 -1
View File
@@ -41,7 +41,10 @@ impl OutputWorkspaceMap {
id, id,
output, output,
index: index as u32 + 1, index: index as u32 + 1,
window_count: workspaces.workspace(id).unwrap().window_count(), window_count: workspaces
.workspace(id)
.expect("workspace views enumerate registered workspace IDs")
.window_count(),
visible, visible,
focused: visible && self.focused_output() == Some(output), focused: visible && self.focused_output() == Some(output),
} }
+8 -2
View File
@@ -666,7 +666,10 @@ impl ScrollingLayout {
let viewport_center = bounds.x + bounds.width / 2.0; let viewport_center = bounds.x + bounds.width / 2.0;
let window_center = rect.x + rect.width / 2.0; let window_center = rect.x + rect.width / 2.0;
self.viewport_x += window_center - viewport_center; self.viewport_x += window_center - viewport_center;
let active = self.window_location(window).unwrap().0; let active = self
.window_location(window)
.expect("geometry resolution confirmed the window belongs to a column")
.0;
let inner = finite_nonnegative(gaps.inner); let inner = finite_nonnegative(gaps.inner);
let positions = self let positions = self
.columns .columns
@@ -678,7 +681,10 @@ impl ScrollingLayout {
Some(position) Some(position)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let viewport_width = self.last_column_sizing.unwrap().viewport_width; let viewport_width = self
.last_column_sizing
.expect("geometry resolution records the column sizing before centering")
.viewport_width;
self.record_viewport_basis( self.record_viewport_basis(
&positions, &positions,
inner, inner,
+3 -3
View File
@@ -49,9 +49,9 @@ impl Geometry {
fn from_values(values: [f64; COUNT]) -> Self { fn from_values(values: [f64; COUNT]) -> Self {
Self { Self {
bounds: Rect::new(values[0], values[1], values[2], values[3]), bounds: Rect::new(values[0], values[1], values[2], values[3]),
radii: values[4..8].try_into().unwrap(), radii: [values[4], values[5], values[6], values[7]],
margin: values[8..12].try_into().unwrap(), margin: [values[8], values[9], values[10], values[11]],
thickness: values[12..16].try_into().unwrap(), thickness: [values[12], values[13], values[14], values[15]],
gap: values[16], gap: values[16],
fusion: values[17], fusion: values[17],
} }
+8 -4
View File
@@ -211,10 +211,14 @@ pub fn notch_border(
let mut paths = Vec::new(); let mut paths = Vec::new();
while let Some((a, b)) = segments.pop() { while let Some((a, b)) = segments.pop() {
let mut path = vec![a, b]; let mut path = vec![a, b];
while let Some(index) = segments while let Some(index) = segments.iter().position(|&(a, _)| {
.iter() close(
.position(|&(a, _)| close(a, *path.last().unwrap())) a,
{ *path
.last()
.expect("an outline path starts with two points and only grows"),
)
}) {
path.push(segments.swap_remove(index).1); path.push(segments.swap_remove(index).1);
} }
while let Some(index) = segments.iter().position(|&(_, b)| close(b, path[0])) { while let Some(index) = segments.iter().position(|&(_, b)| close(b, path[0])) {
+27 -16
View File
@@ -183,10 +183,10 @@ pub fn resolve_with_gap(
result.minimum_width = minimum.required_width(); result.minimum_width = minimum.required_width();
// Flexible content yields space without changing the stored item preference. // Flexible content yields space without changing the stored item preference.
for (_, item) in &candidates { for (_, item) in &candidates {
let Some(measured) = measurements let Some(measured) = measurements.get(&item.id) else {
.get(&item.id) continue;
.filter(|measured| measured.minimum.is_some()) };
else { let Some(minimum) = measured.minimum else {
continue; continue;
}; };
if fits(&result) { if fits(&result) {
@@ -199,7 +199,7 @@ pub fn resolve_with_gap(
else { else {
continue; continue;
}; };
let minimum = measured.minimum.unwrap().min(width); let minimum = minimum.min(width);
result.items.insert( result.items.insert(
measured.id.clone(), measured.id.clone(),
Placement::Visible { Placement::Visible {
@@ -352,10 +352,14 @@ pub fn resolve_with_gap(
let mut high = maximum; let mut high = maximum;
for _ in 0..20 { for _ in 0..20 {
let width = (low + high) * 0.5; let width = (low + high) * 0.5;
*result.items.get_mut(&measured.id).unwrap() = Placement::Visible { *result
representation, .items
width, .get_mut(&measured.id)
}; .expect("visible items retain their allocation during width refinement") =
Placement::Visible {
representation,
width,
};
result.update_widths(panel, overflow_width); result.update_widths(panel, overflow_width);
if fits(&result) { if fits(&result) {
low = width low = width
@@ -363,14 +367,21 @@ pub fn resolve_with_gap(
high = width high = width
} }
} }
*result.items.get_mut(&measured.id).unwrap() = Placement::Visible { *result
representation, .items
width: low, .get_mut(&measured.id)
}; .expect("visible items retain their allocation during width refinement") =
Placement::Visible {
representation,
width: low,
};
} }
result result.overflow.sort_by_key(|id| {
.overflow ordered
.sort_by_key(|id| ordered.iter().position(|item| &item.id == id).unwrap()); .iter()
.position(|item| &item.id == id)
.expect("overflow contains only authored item IDs")
});
result.update_widths(panel, overflow_width); result.update_widths(panel, overflow_width);
result result
} }
+75 -36
View File
@@ -108,7 +108,8 @@ pub fn readable_across(
white white
}; };
for step in 1..=100 { for step in 1..=100 {
let color = rgba(&hex(blend(preferred, target, f64::from(step) / 100.))).unwrap(); let color = rgba(&hex(blend(preferred, target, f64::from(step) / 100.)))
.expect("hex output must be a valid RGBA color");
if score(color) >= minimum { if score(color) >= minimum {
return color; return color;
} }
@@ -137,9 +138,12 @@ fn material_foreground(preferred: [f64; 4], backgrounds: [[f64; 4]; 2]) -> [f64;
let outside = luminance(result) < luminance(backgrounds[0]) let outside = luminance(result) < luminance(backgrounds[0])
|| luminance(result) > luminance(backgrounds[1]); || luminance(result) > luminance(backgrounds[1]);
if outside if outside
&& backgrounds && backgrounds.iter().all(|background| {
.iter() contrast(
.all(|background| contrast(rgba(&hex(result)).unwrap(), *background) >= 4.5) rgba(&hex(result)).expect("hex output must be a valid RGBA color"),
*background,
) >= 4.5
})
{ {
result result
} else { } else {
@@ -161,8 +165,8 @@ fn transition_paint(from: &Paint, to: &Paint, from_solid: &str, to_solid: &str,
let (a, b) = endpoints(from, from_solid); let (a, b) = endpoints(from, from_solid);
let (c, d) = endpoints(to, to_solid); let (c, d) = endpoints(to, to_solid);
let color = |a: &str, b: &str| { let color = |a: &str, b: &str| {
let mut a = rgba(a).unwrap(); let mut a = rgba(a).expect("resolved theme colors must be valid hexadecimal RGBA");
let mut b = rgba(b).unwrap(); let mut b = rgba(b).expect("resolved theme colors must be valid hexadecimal RGBA");
for channel in 0..3 { for channel in 0..3 {
a[channel] *= a[3]; a[channel] *= a[3];
b[channel] *= b[3]; b[channel] *= b[3];
@@ -180,13 +184,13 @@ fn transition_paint(from: &Paint, to: &Paint, from_solid: &str, to_solid: &str,
let from_angle = from let from_angle = from
.effective_gradient() .effective_gradient()
.or(to.effective_gradient()) .or(to.effective_gradient())
.unwrap() .expect("at least one transition endpoint has a gradient")
.angle .angle
.rem_euclid(360.); .rem_euclid(360.);
let to_angle = to let to_angle = to
.effective_gradient() .effective_gradient()
.or(from.effective_gradient()) .or(from.effective_gradient())
.unwrap() .expect("at least one transition endpoint has a gradient")
.angle .angle
.rem_euclid(360.); .rem_euclid(360.);
let delta = (to_angle - from_angle + 180.).rem_euclid(360.) - 180.; let delta = (to_angle - from_angle + 180.).rem_euclid(360.) - 180.;
@@ -202,6 +206,10 @@ fn transition_paint(from: &Paint, to: &Paint, from_solid: &str, to_solid: &str,
impl ResolvedTheme { impl ResolvedTheme {
/// Bound contrast across all backdrops instead of interpolating foregrounds. /// Bound contrast across all backdrops instead of interpolating foregrounds.
///
/// # Panics
/// Both themes must contain valid hexadecimal colors and gradient endpoints,
/// as supplied by configuration resolution. Invalid colors panic during blending.
pub fn transition(&self, to: &Self, progress: f64) -> Self { pub fn transition(&self, to: &Self, progress: f64) -> Self {
if to.reduced_motion || self.accessibility != to.accessibility || progress >= 1.0 { if to.reduced_motion || self.accessibility != to.accessibility || progress >= 1.0 {
return to.clone(); return to.clone();
@@ -212,7 +220,13 @@ impl ResolvedTheme {
let p = progress.clamp(0., 1.); let p = progress.clamp(0., 1.);
let p = p * p * (3. - 2. * p); let p = p * p * (3. - 2. * p);
let mut frame = to.clone(); let mut frame = to.clone();
let mix = |a: &str, b: &str| hex(blend(rgba(a).unwrap(), rgba(b).unwrap(), p)); let mix = |a: &str, b: &str| {
hex(blend(
rgba(a).expect("resolved theme colors must be valid hexadecimal RGBA"),
rgba(b).expect("resolved theme colors must be valid hexadecimal RGBA"),
p,
))
};
frame.tokens.colors.surface_base = mix( frame.tokens.colors.surface_base = mix(
&self.tokens.colors.surface_base, &self.tokens.colors.surface_base,
&to.tokens.colors.surface_base, &to.tokens.colors.surface_base,
@@ -256,9 +270,12 @@ impl ResolvedTheme {
frame.appearance = preferred.appearance; frame.appearance = preferred.appearance;
if self.appearance == to.appearance { if self.appearance == to.appearance {
let body = [ let body = [
rgba(&frame.tokens.colors.surface_base).unwrap(), rgba(&frame.tokens.colors.surface_base)
rgba(&frame.tokens.colors.surface_raised).unwrap(), .expect("resolved theme colors must be valid hexadecimal RGBA"),
rgba(&frame.tokens.colors.application_background).unwrap(), rgba(&frame.tokens.colors.surface_raised)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
rgba(&frame.tokens.colors.application_background)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
]; ];
let minimum = if to.accessibility.increase_contrast { let minimum = if to.accessibility.increase_contrast {
7.0 7.0
@@ -266,36 +283,46 @@ impl ResolvedTheme {
4.5 4.5
}; };
frame.tokens.colors.text_primary = hex(readable_across( frame.tokens.colors.text_primary = hex(readable_across(
rgba(&preferred.tokens.colors.text_primary).unwrap(), rgba(&preferred.tokens.colors.text_primary)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
&body, &body,
minimum, minimum,
)); ));
frame.tokens.colors.text_muted = hex(readable_across( frame.tokens.colors.text_muted = hex(readable_across(
rgba(&preferred.tokens.colors.text_muted).unwrap(), rgba(&preferred.tokens.colors.text_muted)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
&body, &body,
minimum, minimum,
)); ));
let bar = rgba(&frame.tokens.surface.bar.background).unwrap(); let bar = rgba(&frame.tokens.surface.bar.background)
.expect("resolved theme colors must be valid hexadecimal RGBA");
frame.tokens.surface.bar.text_primary = hex(readable( frame.tokens.surface.bar.text_primary = hex(readable(
rgba(&preferred.tokens.surface.bar.text_primary).unwrap(), rgba(&preferred.tokens.surface.bar.text_primary)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
bar, bar,
minimum, minimum,
)); ));
frame.tokens.surface.bar.text_muted = hex(readable( frame.tokens.surface.bar.text_muted = hex(readable(
rgba(&preferred.tokens.surface.bar.text_muted).unwrap(), rgba(&preferred.tokens.surface.bar.text_muted)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
bar, bar,
minimum, minimum,
)); ));
frame.tokens.colors.on_accent = hex(readable( frame.tokens.colors.on_accent = hex(readable(
rgba(&preferred.tokens.colors.on_accent).unwrap(), rgba(&preferred.tokens.colors.on_accent)
rgba(&frame.tokens.colors.accent).unwrap(), .expect("resolved theme colors must be valid hexadecimal RGBA"),
rgba(&frame.tokens.colors.accent)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
4.5, 4.5,
)); ));
return frame; return frame;
} }
let base = rgba(&frame.tokens.colors.surface_base).unwrap(); let base = rgba(&frame.tokens.colors.surface_base)
let raised = rgba(&frame.tokens.colors.surface_raised).unwrap(); .expect("resolved theme colors must be valid hexadecimal RGBA");
let app = rgba(&frame.tokens.colors.application_background).unwrap(); let raised = rgba(&frame.tokens.colors.surface_raised)
.expect("resolved theme colors must be valid hexadecimal RGBA");
let app = rgba(&frame.tokens.colors.application_background)
.expect("resolved theme colors must be valid hexadecimal RGBA");
let minimum = [base, raised, app] let minimum = [base, raised, app]
.into_iter() .into_iter()
.map(luminance) .map(luminance)
@@ -308,8 +335,10 @@ impl ResolvedTheme {
frame.tokens.colors.surface_raised = frame.tokens.colors.surface_base.clone(); frame.tokens.colors.surface_raised = frame.tokens.colors.surface_base.clone();
frame.tokens.colors.application_background = frame.tokens.colors.surface_base.clone(); frame.tokens.colors.application_background = frame.tokens.colors.surface_base.clone();
} }
let surface = rgba(&frame.tokens.colors.surface_base).unwrap(); let surface = rgba(&frame.tokens.colors.surface_base)
let bar = rgba(&frame.tokens.surface.bar.background).unwrap(); .expect("resolved theme colors must be valid hexadecimal RGBA");
let bar = rgba(&frame.tokens.surface.bar.background)
.expect("resolved theme colors must be valid hexadecimal RGBA");
let mut opacity = if frame.tokens.material.style == "solid" { let mut opacity = if frame.tokens.material.style == "solid" {
1. 1.
} else { } else {
@@ -318,8 +347,10 @@ impl ResolvedTheme {
for step in 0..=100 { for step in 0..=100 {
let candidate = opacity + (1. - opacity) * f64::from(step) / 100.; let candidate = opacity + (1. - opacity) * f64::from(step) / 100.;
let backgrounds = material_bounds(surface, candidate); let backgrounds = material_bounds(surface, candidate);
let raised = rgba(&frame.tokens.colors.surface_raised).unwrap(); let raised = rgba(&frame.tokens.colors.surface_raised)
let app = rgba(&frame.tokens.colors.application_background).unwrap(); .expect("resolved theme colors must be valid hexadecimal RGBA");
let app = rgba(&frame.tokens.colors.application_background)
.expect("resolved theme colors must be valid hexadecimal RGBA");
let all = [backgrounds[0], backgrounds[1], raised, app]; let all = [backgrounds[0], backgrounds[1], raised, app];
let minimum = all.into_iter().map(luminance).fold(f64::INFINITY, f64::min); let minimum = all.into_iter().map(luminance).fold(f64::INFINITY, f64::min);
let maximum = all.into_iter().map(luminance).fold(0., f64::max); let maximum = all.into_iter().map(luminance).fold(0., f64::max);
@@ -339,38 +370,46 @@ impl ResolvedTheme {
} }
let surface = material_bounds(surface, opacity); let surface = material_bounds(surface, opacity);
let bar = material_bounds(bar, opacity); let bar = material_bounds(bar, opacity);
let raised = rgba(&frame.tokens.colors.surface_raised).unwrap(); let raised = rgba(&frame.tokens.colors.surface_raised)
let app = rgba(&frame.tokens.colors.application_background).unwrap(); .expect("resolved theme colors must be valid hexadecimal RGBA");
let app = rgba(&frame.tokens.colors.application_background)
.expect("resolved theme colors must be valid hexadecimal RGBA");
let body_backgrounds = [surface[0], surface[1], raised, app]; let body_backgrounds = [surface[0], surface[1], raised, app];
let body_bounds = [ let body_bounds = [
*body_backgrounds *body_backgrounds
.iter() .iter()
.min_by(|a, b| luminance(**a).total_cmp(&luminance(**b))) .min_by(|a, b| luminance(**a).total_cmp(&luminance(**b)))
.unwrap(), .expect("material contrast checks contain four background samples"),
*body_backgrounds *body_backgrounds
.iter() .iter()
.max_by(|a, b| luminance(**a).total_cmp(&luminance(**b))) .max_by(|a, b| luminance(**a).total_cmp(&luminance(**b)))
.unwrap(), .expect("material contrast checks contain four background samples"),
]; ];
frame.tokens.colors.text_primary = hex(material_foreground( frame.tokens.colors.text_primary = hex(material_foreground(
rgba(&preferred.tokens.colors.text_primary).unwrap(), rgba(&preferred.tokens.colors.text_primary)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
body_bounds, body_bounds,
)); ));
frame.tokens.colors.text_muted = hex(material_foreground( frame.tokens.colors.text_muted = hex(material_foreground(
rgba(&preferred.tokens.colors.text_muted).unwrap(), rgba(&preferred.tokens.colors.text_muted)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
body_bounds, body_bounds,
)); ));
frame.tokens.surface.bar.text_primary = hex(material_foreground( frame.tokens.surface.bar.text_primary = hex(material_foreground(
rgba(&preferred.tokens.surface.bar.text_primary).unwrap(), rgba(&preferred.tokens.surface.bar.text_primary)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
bar, bar,
)); ));
frame.tokens.surface.bar.text_muted = hex(material_foreground( frame.tokens.surface.bar.text_muted = hex(material_foreground(
rgba(&preferred.tokens.surface.bar.text_muted).unwrap(), rgba(&preferred.tokens.surface.bar.text_muted)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
bar, bar,
)); ));
frame.tokens.colors.on_accent = hex(readable( frame.tokens.colors.on_accent = hex(readable(
rgba(&preferred.tokens.colors.on_accent).unwrap(), rgba(&preferred.tokens.colors.on_accent)
rgba(&frame.tokens.colors.accent).unwrap(), .expect("resolved theme colors must be valid hexadecimal RGBA"),
rgba(&frame.tokens.colors.accent)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
4.5, 4.5,
)); ));
frame frame
+14 -5
View File
@@ -17,6 +17,10 @@ pub struct TileOptions {
pub id: Id, pub id: Id,
} }
/// Shows a theme family using its validated appearance palettes.
///
/// # Panics
/// The family must contain a usable palette with valid hexadecimal colors.
pub fn tile<M: Clone + 'static>( pub fn tile<M: Clone + 'static>(
family: &Family, family: &Family,
options: TileOptions, options: TileOptions,
@@ -36,7 +40,9 @@ pub fn tile<M: Clone + 'static>(
let label_color = family let label_color = family
.palette(variant.unwrap_or(Appearance::Light)) .palette(variant.unwrap_or(Appearance::Light))
.or(family.dark.as_ref()) .or(family.dark.as_ref())
.map(|palette| crate::parse_color(&palette.text).unwrap()) .map(|palette| {
crate::parse_color(&palette.text).expect("theme family text is a validated color")
})
.unwrap_or(palette.text); .unwrap_or(palette.text);
let mut caption = row([]) let mut caption = row([])
.spacing(6) .spacing(6)
@@ -141,7 +147,9 @@ fn preview_with_radius(
std::sync::Mutex<std::collections::HashMap<String, (Family, icon::Handle)>>, std::sync::Mutex<std::collections::HashMap<String, (Family, icon::Handle)>>,
> = std::sync::OnceLock::new(); > = std::sync::OnceLock::new();
let cache = CACHE.get_or_init(Default::default); let cache = CACHE.get_or_init(Default::default);
let mut cache = cache.lock().unwrap(); let mut cache = cache
.lock()
.expect("theme preview cache must not be used after a mutator panics");
if cache.get(&key).is_none_or(|(cached, _)| cached != family) { if cache.get(&key).is_none_or(|(cached, _)| cached != family) {
let handle = icon::from_svg_bytes( let handle = icon::from_svg_bytes(
svg_with_radius(family, variant, active, selected, radius).into_bytes(), svg_with_radius(family, variant, active, selected, radius).into_bytes(),
@@ -207,7 +215,7 @@ fn svg_with_radius(
result.push_str("<clipPath id=\"preview\"><polygon points=\""); result.push_str("<clipPath id=\"preview\"><polygon points=\"");
for [x, y] in points { for [x, y] in points {
use std::fmt::Write as _; use std::fmt::Write as _;
write!(result, "{x:.4},{y:.4} ").unwrap(); write!(result, "{x:.4},{y:.4} ").expect("formatting into an owned String cannot fail");
} }
result.push_str("\"/></clipPath></defs><g clip-path=\"url(#preview)\">"); result.push_str("\"/></clipPath></defs><g clip-path=\"url(#preview)\">");
for appearance in [Appearance::Light, Appearance::Dark] { for appearance in [Appearance::Light, Appearance::Dark] {
@@ -263,10 +271,11 @@ fn svg_with_radius(
.palette(active.unwrap_or(Appearance::Dark)) .palette(active.unwrap_or(Appearance::Dark))
.or(family.dark.as_ref()) .or(family.dark.as_ref())
.or(family.light.as_ref()) .or(family.light.as_ref())
.unwrap(); .expect("a theme family provides at least one appearance palette");
let foreground = ferese_theme_model::hex(ferese_theme_model::readable( let foreground = ferese_theme_model::hex(ferese_theme_model::readable(
[1.; 4], [1.; 4],
ferese_theme_model::rgba(&p.accent).unwrap(), ferese_theme_model::rgba(&p.accent)
.expect("resolved theme colors must be valid hexadecimal RGBA"),
4.5, 4.5,
)); ));
result.push_str(&format!(r#"<circle cx="183" cy="16" r="10" fill="{}"/><path d="M178 16l3 3 6-6" fill="none" stroke="{foreground}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>"#, p.accent)); result.push_str(&format!(r#"<circle cx="183" cy="16" r="10" fill="{}"/><path d="M178 16l3 3 6-6" fill="none" stroke="{foreground}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>"#, p.accent));
+9 -2
View File
@@ -136,7 +136,10 @@ impl TintCache {
.iter() .iter()
.position(|entry| std::ptr::eq(entry.source, source) && entry.colors == colors) .position(|entry| std::ptr::eq(entry.source, source) && entry.colors == colors)
{ {
let entry = self.entries.remove(index).unwrap(); let entry = self
.entries
.remove(index)
.expect("the cache lookup identifies an existing icon entry");
let handle = entry.handle.clone(); let handle = entry.handle.clone();
self.entries.push_back(entry); self.entries.push_back(entry);
return handle; return handle;
@@ -147,7 +150,11 @@ impl TintCache {
if bytes <= TINT_CACHE_BYTES { if bytes <= TINT_CACHE_BYTES {
while self.entries.len() >= TINT_CACHE_ENTRIES || self.bytes + bytes > TINT_CACHE_BYTES while self.entries.len() >= TINT_CACHE_ENTRIES || self.bytes + bytes > TINT_CACHE_BYTES
{ {
self.bytes -= self.entries.pop_front().unwrap().bytes; self.bytes -= self
.entries
.pop_front()
.expect("cache eviction requires an entry while over budget")
.bytes;
} }
self.bytes += bytes; self.bytes += bytes;
self.entries.push_back(TintedIcon { self.entries.push_back(TintedIcon {
+12 -2
View File
@@ -94,8 +94,18 @@ impl Palette {
stop.color.a = opacity; stop.color.a = opacity;
} }
let from = crate::composite(candidate.stops[0].unwrap().color, self.sidebar); let from = crate::composite(
let to = crate::composite(candidate.stops[1].unwrap().color, self.sidebar); candidate.stops[0]
.expect("the constructed linear gradient contains both endpoint stops")
.color,
self.sidebar,
);
let to = crate::composite(
candidate.stops[1]
.expect("the constructed linear gradient contains both endpoint stops")
.color,
self.sidebar,
);
let lower = Color::from_rgb(from.r.min(to.r), from.g.min(to.g), from.b.min(to.b)); let lower = Color::from_rgb(from.r.min(to.r), from.g.min(to.g), from.b.min(to.b));
let upper = Color::from_rgb(from.r.max(to.r), from.g.max(to.g), from.b.max(to.b)); let upper = Color::from_rgb(from.r.max(to.r), from.g.max(to.g), from.b.max(to.b));
let lightness = crate::luminance(foreground); let lightness = crate::luminance(foreground);
+4 -1
View File
@@ -6,7 +6,10 @@ pub fn font(family: Option<&str>) -> cosmic::font::Font {
return cosmic::font::default(); return cosmic::font::default();
}; };
static FONTS: OnceLock<Mutex<HashMap<String, cosmic::font::Font>>> = OnceLock::new(); static FONTS: OnceLock<Mutex<HashMap<String, cosmic::font::Font>>> = OnceLock::new();
let mut fonts = FONTS.get_or_init(Default::default).lock().unwrap(); let mut fonts = FONTS
.get_or_init(Default::default)
.lock()
.expect("shared font cache must not be used after a mutator panics");
if let Some(font) = fonts.get(family) { if let Some(font) = fonts.get(family) {
return *font; return *font;
} }
+3 -1
View File
@@ -423,7 +423,9 @@ pub fn run(
}); });
let status = child.wait()?; let status = child.wait()?;
CHILD.store(0, std::sync::atomic::Ordering::Relaxed); CHILD.store(0, std::sync::atomic::Ordering::Relaxed);
*drain_deadline.lock().unwrap() = *drain_deadline
.lock()
.expect("command output drain deadline must remain consistent across reader and waiter") =
Some(std::time::Instant::now() + std::time::Duration::from_millis(250)); Some(std::time::Instant::now() + std::time::Duration::from_millis(250));
if let Some(reader) = reader { if let Some(reader) = reader {
// Descendants may inherit the pipe after the direct child exits. Drain // Descendants may inherit the pipe after the direct child exits. Drain
+3 -1
View File
@@ -19,7 +19,9 @@ impl Read for Output {
if self if self
.deadline .deadline
.lock() .lock()
.unwrap() .expect(
"command output drain deadline must remain consistent across reader and waiter",
)
.is_some_and(|deadline| Instant::now() >= deadline) .is_some_and(|deadline| Instant::now() >= deadline)
{ {
// EOF lets BufReader return its final unterminated line too. // EOF lets BufReader return its final unterminated line too.
+11 -5
View File
@@ -16,7 +16,9 @@ pub(super) struct Updates(Arc<(Mutex<Pending>, Condvar)>);
impl Updates { impl Updates {
pub fn progress(&self, units: u64) { pub fn progress(&self, units: u64) {
let (lock, wake) = &*self.0; let (lock, wake) = &*self.0;
let mut pending = lock.lock().unwrap(); let mut pending = lock.lock().expect(
"the bounded progress buffer was checked before removing bytes or parsing text",
);
if !pending.closed { if !pending.closed {
pending.units = Some(units); pending.units = Some(units);
wake.notify_one(); wake.notify_one();
@@ -36,9 +38,11 @@ impl Publisher {
std::thread::spawn(move || { std::thread::spawn(move || {
loop { loop {
let (lock, wake) = &*worker.0; let (lock, wake) = &*worker.0;
let mut pending = lock.lock().unwrap(); let mut pending = lock.lock().expect(
"the bounded progress buffer was checked before removing bytes or parsing text",
);
while pending.units.is_none() && pending.end.is_none() { while pending.units.is_none() && pending.end.is_none() {
pending = wake.wait(pending).unwrap(); pending = wake.wait(pending).expect("the bounded progress buffer was checked before removing bytes or parsing text");
} }
let end = pending.end.take(); let end = pending.end.take();
let units = pending.units.take(); let units = pending.units.take();
@@ -54,7 +58,7 @@ impl Publisher {
Publish::Update { Publish::Update {
local: local.clone(), local: local.clone(),
handle: Some(handle.clone()), handle: Some(handle.clone()),
value: Some(Value::Text(format!("{} units compiled", units.unwrap()))), value: Some(Value::Text(format!("{} units compiled", units.expect("the bounded progress buffer was checked before removing bytes or parsing text")))),
lease_ms: None, lease_ms: None,
} }
}; };
@@ -76,7 +80,9 @@ impl Publisher {
pub fn finish(self, result: State) { pub fn finish(self, result: State) {
let (lock, wake) = &*self.updates.0; let (lock, wake) = &*self.updates.0;
{ {
let mut pending = lock.lock().unwrap(); let mut pending = lock.lock().expect(
"the bounded progress buffer was checked before removing bytes or parsing text",
);
pending.closed = true; pending.closed = true;
pending.units = None; pending.units = None;
pending.end = Some(result); pending.end = Some(result);
+18 -4
View File
@@ -15,7 +15,11 @@ fn prepare(source: &str) -> Result<String, Box<dyn Error>> {
serde_json::to_value(vec![panel::Panel::default()])?, serde_json::to_value(vec![panel::Panel::default()])?,
)?; )?;
} }
let panels: Vec<panel::Panel> = serde_json::from_value(doc.get("panels").unwrap().clone())?; let panels: Vec<panel::Panel> = serde_json::from_value(
doc.get("panels")
.expect("setup populated the panel document before validation")
.clone(),
)?;
panel::validate(&panels)?; panel::validate(&panels)?;
for (index, bar) in panels.iter().enumerate() { for (index, bar) in panels.iter().enumerate() {
let groups = [&bar.start, &bar.center, &bar.end] let groups = [&bar.start, &bar.center, &bar.end]
@@ -47,7 +51,11 @@ fn prepare(source: &str) -> Result<String, Box<dyn Error>> {
if let Some(zone) = zone { if let Some(zone) = zone {
doc.add( doc.add(
&format!("panels.{index}.{zone}.groups.0.items"), &format!("panels.{index}.{zone}.groups.0.items"),
item.as_object().unwrap().clone().into_iter().collect(), item.as_object()
.expect("serialized panel items are JSON objects")
.clone()
.into_iter()
.collect(),
)?; )?;
} else { } else {
doc.add( doc.add(
@@ -59,7 +67,11 @@ fn prepare(source: &str) -> Result<String, Box<dyn Error>> {
)?; )?;
} }
} }
let panels = serde_json::from_value::<Vec<panel::Panel>>(doc.get("panels").unwrap().clone())?; let panels = serde_json::from_value::<Vec<panel::Panel>>(
doc.get("panels")
.expect("setup populated the panel document before validation")
.clone(),
)?;
panel::validate(&panels)?; panel::validate(&panels)?;
Ok(doc.to_string()) Ok(doc.to_string())
} }
@@ -72,7 +84,9 @@ pub fn configure(path: &Path) -> Result<(), Box<dyn Error>> {
} }
let backup = path.with_file_name(format!( let backup = path.with_file_name(format!(
"{}.before-activities-{}", "{}.before-activities-{}",
path.file_name().unwrap().to_string_lossy(), path.file_name()
.expect("the configured shell hook path identifies a file")
.to_string_lossy(),
ferese_ipc::activities::now_ms() ferese_ipc::activities::now_ms()
)); ));
file.replace(&desired, Some(&backup), |_| Ok(()))?; file.replace(&desired, Some(&backup), |_| Ok(()))?;
+1 -1
View File
@@ -119,7 +119,7 @@ fn theme_command(
} }
ThemeAction::Mode { mode } => connection.call( ThemeAction::Mode { mode } => connection.call(
"theme-set-mode", "theme-set-mode",
json!({"mode": mode.to_possible_value().unwrap().get_name()}), json!({"mode": mode.to_possible_value().expect("every ThemeMode variant has a Clap value name").get_name()}),
)?, )?,
ThemeAction::Preview { path } => connection.call( ThemeAction::Preview { path } => connection.call(
"theme-preview", "theme-preview",
+22 -5
View File
@@ -210,7 +210,11 @@ pub(crate) async fn authorize(
.ok_or_else(|| zbus::fdo::Error::AccessDenied("Missing caller".into()))?; .ok_or_else(|| zbus::fdo::Error::AccessDenied("Missing caller".into()))?;
let dbus = zbus::fdo::DBusProxy::new(connection).await?; let dbus = zbus::fdo::DBusProxy::new(connection).await?;
let owner = dbus let owner = dbus
.get_name_owner(FRONTEND.try_into().unwrap()) .get_name_owner(
FRONTEND
.try_into()
.expect("the portal frontend has a valid static bus name"),
)
.await .await
.map_err(|_| zbus::fdo::Error::AccessDenied("Desktop portal is unavailable".into()))?; .map_err(|_| zbus::fdo::Error::AccessDenied("Desktop portal is unavailable".into()))?;
if sender.as_str() != owner.as_str() { if sender.as_str() != owner.as_str() {
@@ -608,9 +612,15 @@ async fn picker(prompt: &Prompt) -> Result<Child, String> {
.kill_on_drop(true) .kill_on_drop(true)
.spawn() .spawn()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let mut input = child.stdin.take().unwrap(); let mut input = child
.stdin
.take()
.expect("the child was spawned with piped standard input");
input input
.write_all(&serde_json::to_vec(prompt).unwrap()) .write_all(
&serde_json::to_vec(prompt)
.expect("typed configuration and IPC values must serialize to JSON"),
)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
drop(input); drop(input);
@@ -855,7 +865,10 @@ async fn start_streams(
.spawn() .spawn()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let mut line = String::new(); let mut line = String::new();
let output = child.stdout.take().unwrap(); let output = child
.stdout
.take()
.expect("the child was spawned with piped standard output");
tokio::time::timeout( tokio::time::timeout(
Duration::from_secs(10), Duration::from_secs(10),
BufReader::new(output).read_line(&mut line), BufReader::new(output).read_line(&mut line),
@@ -1129,7 +1142,11 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
let owner = match zbus::fdo::DBusProxy::new(&connection).await { let owner = match zbus::fdo::DBusProxy::new(&connection).await {
Ok(proxy) => proxy Ok(proxy) => proxy
.get_name_owner(FRONTEND.try_into().unwrap()) .get_name_owner(
FRONTEND
.try_into()
.expect("the portal frontend has a valid static bus name"),
)
.await .await
.ok() .ok()
.map(|name| name.to_string()), .map(|name| name.to_string()),
+4 -1
View File
@@ -234,7 +234,10 @@ impl Capture {
} }
frame.destroy(); frame.destroy();
self.state.result.take().unwrap()?; self.state
.result
.take()
.expect("the capture dispatch loop exits after storing its result")?;
let buffer = self let buffer = self
.state .state
+5 -1
View File
@@ -163,7 +163,11 @@ impl cosmic::Application for Consent {
if self.prompt.shortcuts.is_empty() { if self.prompt.shortcuts.is_empty() {
println!("true"); println!("true");
} else { } else {
println!("{}", serde_json::to_string(&self.prompt.shortcuts).unwrap()); println!(
"{}",
serde_json::to_string(&self.prompt.shortcuts)
.expect("typed configuration and IPC values must serialize to JSON")
);
} }
let _ = std::io::stdout().flush(); let _ = std::io::stdout().flush();
return cosmic::iced::exit(); return cosmic::iced::exit();
+8 -2
View File
@@ -229,7 +229,10 @@ pub(crate) async fn consent(
.spawn() .spawn()
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
let pid = child.id().ok_or("Missing consent process ID")?; let pid = child.id().ok_or("Missing consent process ID")?;
let mut input = child.stdin.take().unwrap(); let mut input = child
.stdin
.take()
.expect("the child was spawned with piped standard input");
input input
.write_all(&serde_json::to_vec(&prompt).map_err(|error| error.to_string())?) .write_all(&serde_json::to_vec(&prompt).map_err(|error| error.to_string())?)
@@ -578,7 +581,10 @@ async fn select_window(app: &str, parent: &str) -> Result<Option<u64>, String> {
.spawn() .spawn()
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
let pid = child.id().ok_or("Missing window selector PID")?; let pid = child.id().ok_or("Missing window selector PID")?;
let mut stdin = child.stdin.take().unwrap(); let mut stdin = child
.stdin
.take()
.expect("the child was spawned with piped standard input");
stdin stdin
.write_all(&serde_json::to_vec(&prompt).map_err(|error| error.to_string())?) .write_all(&serde_json::to_vec(&prompt).map_err(|error| error.to_string())?)
.await .await
+5 -3
View File
@@ -157,17 +157,19 @@ impl Inhibit {
let values = HashMap::from([ let values = HashMap::from([
( (
"screensaver-active".into(), "screensaver-active".into(),
Value::from(locked).try_into().unwrap(), Value::from(locked).try_into().expect("the D-Bus value uses an owned supported scalar or collection type"),
), ),
( (
"session-state".into(), "session-state".into(),
Value::from(phase).try_into().unwrap(), Value::from(phase).try_into().expect("the D-Bus value uses an owned supported scalar or collection type"),
), ),
]); ]);
if let Ok(emitter) = SignalEmitter::new(&connection, PATH) if let Ok(emitter) = SignalEmitter::new(&connection, PATH)
&& Self::state_changed( && Self::state_changed(
&emitter, &emitter,
OwnedObjectPath::try_from(path.clone()).unwrap(), OwnedObjectPath::try_from(path.clone()).expect(
"session handles were validated as D-Bus object paths",
),
values, values,
) )
.await .await
+12 -2
View File
@@ -506,7 +506,12 @@ impl InputCapture {
return Ok((1, Options::new())); return Ok((1, Options::new()));
} }
let result = self let result = self
.finish_native_call(connection, &session_handle, &bridge, result.unwrap()) .finish_native_call(
connection,
&session_handle,
&bridge,
result.expect("the completion loop returned with a native call result"),
)
.await?; .await?;
let zones: Vec<(u32, u32, i32, i32)> = serde_json::from_value(result["zones"].clone()) let zones: Vec<(u32, u32, i32, i32)> = serde_json::from_value(result["zones"].clone())
.map_err(|_| invalid("Invalid native capture zones"))?; .map_err(|_| invalid("Invalid native capture zones"))?;
@@ -582,7 +587,12 @@ impl InputCapture {
return Ok((1, Options::new())); return Ok((1, Options::new()));
} }
let result = self let result = self
.finish_native_call(connection, &session_handle, &bridge, result.unwrap()) .finish_native_call(
connection,
&session_handle,
&bridge,
result.expect("the completion loop returned with a native call result"),
)
.await?; .await?;
let failed_barriers: Vec<u32> = serde_json::from_value(result["failed_barriers"].clone()) let failed_barriers: Vec<u32> = serde_json::from_value(result["failed_barriers"].clone())
.map_err(|_| invalid("Invalid native barrier response"))?; .map_err(|_| invalid("Invalid native barrier response"))?;
+5 -2
View File
@@ -31,7 +31,8 @@ impl Lockdown {
} }
pub(crate) async fn watch(self, connection: Connection) { pub(crate) async fn watch(self, connection: Connection) {
let emitter = SignalEmitter::new(&connection, PATH).unwrap(); let emitter = SignalEmitter::new(&connection, PATH)
.expect("the portal exports signals at a valid static object path");
loop { loop {
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
let mut current = self.0.write().await; let mut current = self.0.write().await;
@@ -49,7 +50,9 @@ impl Lockdown {
drop(current); drop(current);
let _ = zbus::fdo::Properties::properties_changed( let _ = zbus::fdo::Properties::properties_changed(
&emitter, &emitter,
"org.freedesktop.impl.portal.Lockdown".try_into().unwrap(), "org.freedesktop.impl.portal.Lockdown"
.try_into()
.expect("the Lockdown interface has a valid static D-Bus name"),
changed, changed,
Cow::Borrowed(&[]), Cow::Borrowed(&[]),
) )
+2 -1
View File
@@ -150,7 +150,8 @@ impl Background {
} }
pub(crate) async fn watch(self, connection: Connection) { pub(crate) async fn watch(self, connection: Connection) {
let emitter = SignalEmitter::new(&connection, PATH).unwrap(); let emitter = SignalEmitter::new(&connection, PATH)
.expect("the portal exports signals at a valid static object path");
loop { loop {
let result = async { let result = async {
let path = ferese_ipc::socket::resolve(None).map_err(|error| error.to_string())?; let path = ferese_ipc::socket::resolve(None).map_err(|error| error.to_string())?;
+10 -2
View File
@@ -250,7 +250,11 @@ impl cosmic::Application for Picker {
.map(|i| &self.prompt.sources[*i].name) .map(|i| &self.prompt.sources[*i].name)
.collect(); .collect();
if self.prompt.window_capture { if self.prompt.window_capture {
println!("{}", serde_json::to_string(&names).unwrap()); println!(
"{}",
serde_json::to_string(&names)
.expect("typed configuration and IPC values must serialize to JSON")
);
} else { } else {
let selection = Selection { let selection = Selection {
names: names.into_iter().cloned().collect(), names: names.into_iter().cloned().collect(),
@@ -262,7 +266,11 @@ impl cosmic::Application for Picker {
audio: self.audio, audio: self.audio,
region: self.region, region: self.region,
}; };
println!("{}", serde_json::to_string(&selection).unwrap()); println!(
"{}",
serde_json::to_string(&selection)
.expect("typed configuration and IPC values must serialize to JSON")
);
} }
let _ = std::io::stdout().flush(); let _ = std::io::stdout().flush();
return cosmic::iced::exit(); return cosmic::iced::exit();
+2 -1
View File
@@ -28,7 +28,8 @@ pub fn event(state: &str, message: Option<&str>, path: Option<&Path>) {
} }
fn option_string(value: String) -> OwnedValue { fn option_string(value: String) -> OwnedValue {
OwnedValue::try_from(Value::from(value)).unwrap() OwnedValue::try_from(Value::from(value))
.expect("the D-Bus value uses an owned supported scalar or collection type")
} }
async fn request<B: serde::Serialize + zbus::zvariant::DynamicType>( async fn request<B: serde::Serialize + zbus::zvariant::DynamicType>(
+5 -2
View File
@@ -44,7 +44,9 @@ impl Appearance {
("color-scheme".into(), self.scheme.into()), ("color-scheme".into(), self.scheme.into()),
( (
"accent-color".into(), "accent-color".into(),
Value::from(self.accent).try_to_owned().unwrap(), Value::from(self.accent)
.try_to_owned()
.expect("the D-Bus value uses an owned supported scalar or collection type"),
), ),
("contrast".into(), self.contrast.into()), ("contrast".into(), self.contrast.into()),
("reduced-motion".into(), self.reduced_motion.into()), ("reduced-motion".into(), self.reduced_motion.into()),
@@ -83,7 +85,8 @@ impl Settings {
} }
pub(crate) async fn watch(self, connection: Connection) { pub(crate) async fn watch(self, connection: Connection) {
let emitter = SignalEmitter::new(&connection, PATH).unwrap(); let emitter = SignalEmitter::new(&connection, PATH)
.expect("the portal exports signals at a valid static object path");
loop { loop {
let connection = tokio::task::spawn_blocking(ferese_ipc::Connection::connect).await; let connection = tokio::task::spawn_blocking(ferese_ipc::Connection::connect).await;
if let Ok(Ok(mut connection)) = connection if let Ok(Ok(mut connection)) = connection
+20 -6
View File
@@ -210,7 +210,8 @@ impl GlobalShortcuts {
session: Arc<Session>, session: Arc<Session>,
bridge: Bridge, bridge: Bridge,
) { ) {
let emitter = SignalEmitter::new(&connection, PATH).unwrap(); let emitter = SignalEmitter::new(&connection, PATH)
.expect("the portal exports signals at a valid static object path");
loop { loop {
tokio::select! { tokio::select! {
_ = session.cancel.wait() => break, _ = session.cancel.wait() => break,
@@ -414,7 +415,12 @@ impl GlobalShortcuts {
self.requests.end(connection, &handle).await; self.requests.end(connection, &handle).await;
Ok(( Ok((
0, 0,
HashMap::from([("shortcuts".into(), Value::from(result).try_into().unwrap())]), HashMap::from([(
"shortcuts".into(),
Value::from(result)
.try_into()
.expect("the D-Bus value uses an owned supported scalar or collection type"),
)]),
)) ))
} }
@@ -453,7 +459,8 @@ impl GlobalShortcuts {
.await; .await;
session.state.lock().await.busy = false; session.state.lock().await.busy = false;
if let Ok(Some(shortcuts)) = result { if let Ok(Some(shortcuts)) = result {
let emitter = SignalEmitter::new(&connection, PATH).unwrap(); let emitter = SignalEmitter::new(&connection, PATH)
.expect("the portal exports signals at a valid static object path");
let _ = Self::shortcuts_changed(&emitter, session_handle, shortcuts).await; let _ = Self::shortcuts_changed(&emitter, session_handle, shortcuts).await;
} }
}); });
@@ -597,7 +604,9 @@ fn reply(response: Result<Option<Shortcuts>, String>) -> (u32, Options) {
0, 0,
HashMap::from([( HashMap::from([(
"shortcuts".into(), "shortcuts".into(),
Value::from(shortcuts).try_into().unwrap(), Value::from(shortcuts)
.try_into()
.expect("the D-Bus value uses an owned supported scalar or collection type"),
)]), )]),
), ),
Ok(None) => (1, Options::new()), Ok(None) => (1, Options::new()),
@@ -629,7 +638,10 @@ async fn dialog(
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.spawn() .spawn()
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
let mut input = child.stdin.take().unwrap(); let mut input = child
.stdin
.take()
.expect("the child was spawned with piped standard input");
input input
.write_all(&serde_json::to_vec(&prompt).map_err(|error| error.to_string())?) .write_all(&serde_json::to_vec(&prompt).map_err(|error| error.to_string())?)
.await .await
@@ -705,7 +717,9 @@ fn save(app: &str, fields: &[ShortcutField]) -> Result<(), String> {
let Some(path) = cache_path(app) else { let Some(path) = cache_path(app) else {
return Ok(()); return Ok(());
}; };
let directory = path.parent().unwrap(); let directory = path
.parent()
.expect("the portal configuration path identifies a file in a directory");
std::fs::create_dir_all(directory).map_err(|error| error.to_string())?; std::fs::create_dir_all(directory).map_err(|error| error.to_string())?;
let mut file = tempfile::NamedTempFile::new_in(directory).map_err(|error| error.to_string())?; let mut file = tempfile::NamedTempFile::new_in(directory).map_err(|error| error.to_string())?;
std::io::Write::write_all( std::io::Write::write_all(
+18 -9
View File
@@ -106,7 +106,7 @@ fn pod(object: spa::pod::Object) -> Vec<u8> {
std::io::Cursor::new(Vec::new()), std::io::Cursor::new(Vec::new()),
&spa::pod::Value::Object(object), &spa::pod::Value::Object(object),
) )
.unwrap() .expect("SPA pod serialization into an owned Vec cannot fail for these object types")
.0 .0
.into_inner() .into_inner()
} }
@@ -230,7 +230,7 @@ pub fn run(
width, width,
height height
}) })
.unwrap() .expect("typed configuration and IPC values must serialize to JSON")
); );
let _ = std::io::stdout().flush(); let _ = std::io::stdout().flush();
} }
@@ -291,8 +291,10 @@ pub fn run(
)); ));
if stream if stream
.update_params(&mut [ .update_params(&mut [
Pod::from_bytes(&buffers).unwrap(), Pod::from_bytes(&buffers)
Pod::from_bytes(&header).unwrap(), .expect("SPA pod bytes were produced by the matching pod serializer"),
Pod::from_bytes(&header)
.expect("SPA pod bytes were produced by the matching pod serializer"),
]) ])
.is_err() .is_err()
{ {
@@ -310,7 +312,9 @@ pub fn run(
*target.chunk_mut().size_mut() = 0; *target.chunk_mut().size_mut() = 0;
return; return;
} }
let frame = data.latest.lock().unwrap(); let frame = data.latest.lock().expect(
"capture frame state must remain consistent across producer and PipeWire callbacks",
);
if (frame.width, frame.height) != data.negotiated { if (frame.width, frame.height) != data.negotiated {
*target.chunk_mut().size_mut() = 0; *target.chunk_mut().size_mut() = 0;
return; return;
@@ -344,7 +348,8 @@ pub fn run(
spa::utils::Direction::Output, spa::utils::Direction::Output,
None, None,
pw::stream::StreamFlags::MAP_BUFFERS | pw::stream::StreamFlags::DRIVER, pw::stream::StreamFlags::MAP_BUFFERS | pw::stream::StreamFlags::DRIVER,
&mut [Pod::from_bytes(&format).unwrap()], &mut [Pod::from_bytes(&format)
.expect("SPA pod bytes were produced by the matching pod serializer")],
)?; )?;
let interval = Duration::from_nanos(1_000_000_000 / 30); let interval = Duration::from_nanos(1_000_000_000 / 30);
@@ -362,7 +367,7 @@ pub fn run(
checked = Instant::now(); checked = Instant::now();
match capture.frame(&name, cursor, &capture_stop, std::mem::take(&mut spare)) { match capture.frame(&name, cursor, &capture_stop, std::mem::take(&mut spare)) {
Ok(frame) if resizable || (frame.width == width && frame.height == height) => { Ok(frame) if resizable || (frame.width == width && frame.height == height) => {
spare = std::mem::replace(&mut *latest.lock().unwrap(), frame).pixels; spare = std::mem::replace(&mut *latest.lock().expect("capture frame state must remain consistent across producer and PipeWire callbacks"), frame).pixels;
fresh.store(capture_streaming.load(Ordering::Relaxed), Ordering::SeqCst); fresh.store(capture_streaming.load(Ordering::Relaxed), Ordering::SeqCst);
} }
Ok(_) => { Ok(_) => {
@@ -385,12 +390,16 @@ pub fn run(
while !stop.load(Ordering::Relaxed) { while !stop.load(Ordering::Relaxed) {
let size = { let size = {
let frame = format_frames.lock().unwrap(); let frame = format_frames.lock().expect(
"capture frame state must remain consistent across producer and PipeWire callbacks",
);
(frame.width, frame.height) (frame.width, frame.height)
}; };
if resizable && size != requested_size { if resizable && size != requested_size {
let format = video_format(size.0, size.1); let format = video_format(size.0, size.1);
if let Err(error) = stream.update_params(&mut [Pod::from_bytes(&format).unwrap()]) { if let Err(error) = stream.update_params(&mut [Pod::from_bytes(&format)
.expect("SPA pod bytes were produced by the matching pod serializer")])
{
renegotiation_error = Some(error); renegotiation_error = Some(error);
stop.store(true, Ordering::Relaxed); stop.store(true, Ordering::Relaxed);
break; break;
+4 -1
View File
@@ -47,7 +47,10 @@ pub fn daemonize() -> Result<(), String> {
} }
fn confirm(child: &mut Child) -> Result<(), String> { fn confirm(child: &mut Child) -> Result<(), String> {
let stdout = child.stdout.take().unwrap(); let stdout = child
.stdout
.take()
.expect("the child was spawned with piped standard output");
let (tx, rx) = std::sync::mpsc::sync_channel(1); let (tx, rx) = std::sync::mpsc::sync_channel(1);
std::thread::spawn(move || { std::thread::spawn(move || {
let mut reader = BufReader::new(stdout); let mut reader = BufReader::new(stdout);
+14 -3
View File
@@ -136,7 +136,10 @@ impl Agent {
let cancelled = Arc::new(AtomicBool::new(false)); let cancelled = Arc::new(AtomicBool::new(false));
{ {
let mut active = self.active.lock().unwrap(); let mut active = self
.active
.lock()
.expect("authentication session ownership must remain consistent across callbacks");
if active.contains_key(&cookie) { if active.contains_key(&cookie) {
return Err(zbus::fdo::Error::Failed("Duplicate request".into())); return Err(zbus::fdo::Error::Failed("Duplicate request".into()));
} }
@@ -166,7 +169,10 @@ impl Agent {
}) })
.await; .await;
self.active.lock().unwrap().remove(&cookie); self.active
.lock()
.expect("authentication session ownership must remain consistent across callbacks")
.remove(&cookie);
match result.map_err(|error| zbus::fdo::Error::Failed(error.to_string()))? { match result.map_err(|error| zbus::fdo::Error::Failed(error.to_string()))? {
Ok(true) => Ok(()), Ok(true) => Ok(()),
Ok(false) => Err(zbus::fdo::Error::Failed("Authentication cancelled".into())), Ok(false) => Err(zbus::fdo::Error::Failed("Authentication cancelled".into())),
@@ -175,7 +181,12 @@ impl Agent {
} }
fn cancel_authentication(&self, cookie: String) { fn cancel_authentication(&self, cookie: String) {
if let Some(cancelled) = self.active.lock().unwrap().get(&cookie) { if let Some(cancelled) = self
.active
.lock()
.expect("authentication session ownership must remain consistent across callbacks")
.get(&cookie)
{
cancelled.store(true, Ordering::Release); cancelled.store(true, Ordering::Release);
} }
} }
+14 -3
View File
@@ -153,11 +153,17 @@ fn read_events(
for line in lines { for line in lines {
let Ok(line) = line else { break }; let Ok(line) = line else { break };
if let Ok(event) = serde_json::from_str(&line) { if let Ok(event) = serde_json::from_str(&line) {
queue.lock().unwrap().push_back(event); queue
.lock()
.expect("authentication prompt queue must remain consistent across workers")
.push_back(event);
} }
} }
// The agent owns this pipe. EOF (including agent death) closes the prompt. // The agent owns this pipe. EOF (including agent death) closes the prompt.
queue.lock().unwrap().push_back(PromptEvent::Cancel); queue
.lock()
.expect("authentication prompt queue must remain consistent across workers")
.push_back(PromptEvent::Cancel);
} }
pub fn run() -> Result<(), Box<dyn std::error::Error>> { pub fn run() -> Result<(), Box<dyn std::error::Error>> {
@@ -348,7 +354,12 @@ impl cosmic::Application for Prompt {
self.material = result.ok(); self.material = result.ok();
} }
Message::Tick => { Message::Tick => {
let events: Vec<_> = self.incoming.lock().unwrap().drain(..).collect(); let events: Vec<_> = self
.incoming
.lock()
.expect("authentication prompt queue must remain consistent across workers")
.drain(..)
.collect();
let mut refocus = false; let mut refocus = false;
for event in events { for event in events {
match event { match event {
+3 -1
View File
@@ -469,7 +469,9 @@ fn run_session(
} else if stopped { } else if stopped {
Ok(SessionOutcome::Cancelled) Ok(SessionOutcome::Cancelled)
} else { } else {
Ok(SessionOutcome::Completed(outcome.unwrap())) Ok(SessionOutcome::Completed(outcome.expect(
"the session loop finished only after receiving a completion outcome",
)))
} }
} }
+67 -14
View File
@@ -68,7 +68,10 @@ enum Error {
impl Pairing { impl Pairing {
fn begin(&self, target: &str) -> Result<u64, String> { fn begin(&self, target: &str) -> Result<u64, String> {
let mut state = self.0.lock().unwrap(); let mut state = self
.0
.lock()
.expect("pairing agent state must remain consistent across callbacks");
if state.target.is_some() { if state.target.is_some() {
return Err("Another device is already pairing.".into()); return Err("Another device is already pairing.".into());
} }
@@ -78,7 +81,10 @@ impl Pairing {
} }
fn finish(&self, generation: u64) { fn finish(&self, generation: u64) {
let mut state = self.0.lock().unwrap(); let mut state = self
.0
.lock()
.expect("pairing agent state must remain consistent across callbacks");
if state.generation == generation { if state.generation == generation {
state.target = None; state.target = None;
state.prompt = None; state.prompt = None;
@@ -87,7 +93,10 @@ impl Pairing {
} }
pub fn cancel_local(&self) -> Option<String> { pub fn cancel_local(&self) -> Option<String> {
let mut state = self.0.lock().unwrap(); let mut state = self
.0
.lock()
.expect("pairing agent state must remain consistent across callbacks");
state.generation = state.generation.wrapping_add(1); state.generation = state.generation.wrapping_add(1);
state.prompt = None; state.prompt = None;
state.reply = None; state.reply = None;
@@ -95,11 +104,18 @@ impl Pairing {
} }
pub fn prompt(&self) -> Option<Prompt> { pub fn prompt(&self) -> Option<Prompt> {
self.0.lock().unwrap().prompt.clone() self.0
.lock()
.expect("pairing agent state must remain consistent across callbacks")
.prompt
.clone()
} }
pub fn reply(&self, id: u64, value: Option<Zeroizing<String>>) { pub fn reply(&self, id: u64, value: Option<Zeroizing<String>>) {
let mut state = self.0.lock().unwrap(); let mut state = self
.0
.lock()
.expect("pairing agent state must remain consistent across callbacks");
if state.prompt.as_ref().is_some_and(|p| p.id == id) { if state.prompt.as_ref().is_some_and(|p| p.id == id) {
if let Some(reply) = state.reply.take() { if let Some(reply) = state.reply.take() {
let _ = reply.send(value); let _ = reply.send(value);
@@ -126,7 +142,10 @@ impl Pairing {
sender: Option<&str>, sender: Option<&str>,
) -> Result<Zeroizing<String>, Error> { ) -> Result<Zeroizing<String>, Error> {
let (id, rx) = { let (id, rx) = {
let mut state = self.0.lock().unwrap(); let mut state = self
.0
.lock()
.expect("pairing agent state must remain consistent across callbacks");
Self::check(&state, device.as_str(), sender)?; Self::check(&state, device.as_str(), sender)?;
if state.reply.is_some() { if state.reply.is_some() {
return Err(Error::Rejected( return Err(Error::Rejected(
@@ -159,7 +178,10 @@ impl Pairing {
text: String, text: String,
sender: Option<&str>, sender: Option<&str>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut state = self.0.lock().unwrap(); let mut state = self
.0
.lock()
.expect("pairing agent state must remain consistent across callbacks");
Self::check(&state, device.as_str(), sender)?; Self::check(&state, device.as_str(), sender)?;
state.serial = state.serial.wrapping_add(1); state.serial = state.serial.wrapping_add(1);
state.prompt = Some(Prompt { state.prompt = Some(Prompt {
@@ -171,7 +193,10 @@ impl Pairing {
} }
fn dismiss(&self, sender: Option<&str>) -> Result<(), Error> { fn dismiss(&self, sender: Option<&str>) -> Result<(), Error> {
let mut state = self.0.lock().unwrap(); let mut state = self
.0
.lock()
.expect("pairing agent state must remain consistent across callbacks");
if sender.is_none() || state.owner.as_deref() != sender { if sender.is_none() || state.owner.as_deref() != sender {
return Err(Error::Rejected("Unknown sender".into())); return Err(Error::Rejected("Unknown sender".into()));
} }
@@ -289,7 +314,10 @@ impl Pairing {
fn release(&self, #[zbus(header)] header: Header<'_>) -> Result<(), Error> { fn release(&self, #[zbus(header)] header: Header<'_>) -> Result<(), Error> {
self.dismiss(header.sender().map(|s| s.as_str()))?; self.dismiss(header.sender().map(|s| s.as_str()))?;
self.cancel_local(); self.cancel_local();
self.0.lock().unwrap().owner = None; self.0
.lock()
.expect("pairing agent state must remain consistent across callbacks")
.owner = None;
Ok(()) Ok(())
} }
} }
@@ -308,17 +336,29 @@ impl Client {
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let mut registered = self.registered.lock().await; let mut registered = self.registered.lock().await;
if registered.as_ref() == Some(&owner) if registered.as_ref() == Some(&owner)
&& self.agent.0.lock().unwrap().owner.as_ref() == Some(&owner) && self
.agent
.0
.lock()
.expect("pairing agent state must remain consistent across callbacks")
.owner
.as_ref()
== Some(&owner)
{ {
return Ok(()); return Ok(());
} }
self.agent.0.lock().unwrap().owner = Some(owner.clone()); self.agent
.0
.lock()
.expect("pairing agent state must remain consistent across callbacks")
.owner = Some(owner.clone());
self.connection self.connection
.object_server() .object_server()
.at(PATH, self.agent.clone()) .at(PATH, self.agent.clone())
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let path = OwnedObjectPath::try_from(PATH).unwrap(); let path = OwnedObjectPath::try_from(PATH)
.expect("the pairing agent has a valid static object path");
proxy( proxy(
&self.connection, &self.connection,
"org.bluez", "org.bluez",
@@ -341,13 +381,26 @@ impl Client {
} }
let result = async { let result = async {
self.register_agent().await?; self.register_agent().await?;
if self.agent.0.lock().unwrap().generation != generation { if self
.agent
.0
.lock()
.expect("pairing agent state must remain consistent across callbacks")
.generation
!= generation
{
return Err("Pairing canceled.".into()); return Err("Pairing canceled.".into());
} }
bluetooth::device_call(&self.connection, path, "Pair").await bluetooth::device_call(&self.connection, path, "Pair").await
} }
.await; .await;
let still_active = self.agent.0.lock().unwrap().generation == generation; let still_active = self
.agent
.0
.lock()
.expect("pairing agent state must remain consistent across callbacks")
.generation
== generation;
self.agent.finish(generation); self.agent.finish(generation);
if !still_active { if !still_active {
return Err("Pairing canceled.".into()); return Err("Pairing canceled.".into());
@@ -170,7 +170,12 @@ impl Client {
pub(crate) async fn discover(&self, path: String, epoch: u64) -> Result<(), String> { pub(crate) async fn discover(&self, path: String, epoch: u64) -> Result<(), String> {
// BlueZ discovery is reference counted per D-Bus caller. Never stop // BlueZ discovery is reference counted per D-Bus caller. Never stop
// discovery owned by another app, and never acquire a lease twice. // discovery owned by another app, and never acquire a lease twice.
if self.discovery.lock().unwrap().contains_key(&path) { if self
.discovery
.lock()
.expect("Bluetooth discovery ownership must remain consistent across callbacks")
.contains_key(&path)
{
return Ok(()); return Ok(());
} }
adapter_call(&self.connection, &path, "StartDiscovery").await?; adapter_call(&self.connection, &path, "StartDiscovery").await?;
@@ -179,13 +184,18 @@ impl Client {
return Err("Discovery canceled.".into()); return Err("Discovery canceled.".into());
} }
let lease = std::time::Instant::now(); let lease = std::time::Instant::now();
self.discovery.lock().unwrap().insert(path.clone(), lease); self.discovery
.lock()
.expect("Bluetooth discovery ownership must remain consistent across callbacks")
.insert(path.clone(), lease);
let leases = self.discovery.clone(); let leases = self.discovery.clone();
let connection = self.connection.clone(); let connection = self.connection.clone();
tokio::spawn(async move { tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(30)).await; tokio::time::sleep(Duration::from_secs(30)).await;
let owned = { let owned = {
let mut leases = leases.lock().unwrap(); let mut leases = leases.lock().expect(
"Bluetooth discovery ownership must remain consistent across callbacks",
);
if leases.get(&path) == Some(&lease) { if leases.get(&path) == Some(&lease) {
leases.remove(&path); leases.remove(&path);
true true
@@ -201,7 +211,12 @@ impl Client {
} }
pub(crate) async fn stop_discovery(&self) -> Result<(), String> { pub(crate) async fn stop_discovery(&self) -> Result<(), String> {
let paths = std::mem::take(&mut *self.discovery.lock().unwrap()); let paths = std::mem::take(
&mut *self
.discovery
.lock()
.expect("Bluetooth discovery ownership must remain consistent across callbacks"),
);
let mut result = Ok(()); let mut result = Ok(());
for (path, _) in paths { for (path, _) in paths {
if let Err(error) = adapter_call(&self.connection, &path, "StopDiscovery").await { if let Err(error) = adapter_call(&self.connection, &path, "StopDiscovery").await {
@@ -212,7 +227,10 @@ impl Client {
} }
pub(crate) fn discovering(&self, path: &str) -> bool { pub(crate) fn discovering(&self, path: &str) -> bool {
self.discovery.lock().unwrap().contains_key(path) self.discovery
.lock()
.expect("Bluetooth discovery ownership must remain consistent across callbacks")
.contains_key(path)
} }
} }
@@ -19,7 +19,9 @@ impl App {
column([]) column([])
.spacing(8) .spacing(8)
.push(self.label("Bluetooth unavailable", 14.)) .push(self.label("Bluetooth unavailable", 14.))
.push(self.connection_note(result.as_ref().unwrap_err())), .push(self.connection_note(result.as_ref().expect_err(
"the unavailable-services branch contains a connection error",
))),
); );
}; };
let mut body = column([]).spacing(6); let mut body = column([]).spacing(6);
@@ -163,7 +163,12 @@ impl App {
} }
if accepted if accepted
&& !super::agent::valid_answer( && !super::agent::valid_answer(
&self.connections.prompt.as_ref().unwrap().kind, &self
.connections
.prompt
.as_ref()
.expect("the pairing reply branch requires an active prompt")
.kind,
&self.connections.pairing_input, &self.connections.pairing_input,
) )
{ {
+6 -1
View File
@@ -229,7 +229,12 @@ impl Client {
fn cleanup(&self) { fn cleanup(&self) {
let target = self.agent.cancel_local(); let target = self.agent.cancel_local();
let paths = std::mem::take(&mut *self.discovery.lock().unwrap()); let paths = std::mem::take(
&mut *self
.discovery
.lock()
.expect("Bluetooth discovery ownership must remain consistent across callbacks"),
);
let connection = self.connection.clone(); let connection = self.connection.clone();
self.runtime.spawn(async move { self.runtime.spawn(async move {
if let Some(path) = target { if let Some(path) = target {
@@ -20,7 +20,9 @@ impl App {
column([]) column([])
.spacing(8) .spacing(8)
.push(self.label("Wi-Fi unavailable", 14.)) .push(self.label("Wi-Fi unavailable", 14.))
.push(self.connection_note(result.as_ref().unwrap_err())), .push(self.connection_note(result.as_ref().expect_err(
"the unavailable-services branch contains a connection error",
))),
); );
}; };
let mut body = column([]).spacing(6); let mut body = column([]).spacing(6);
+10 -3
View File
@@ -258,7 +258,9 @@ impl cosmic::Application for App {
} }
.into(); .into();
let error = initial.as_ref().err().cloned(); let error = initial.as_ref().err().cloned();
let current = initial.unwrap_or_else(|_| Snapshot::parse(String::new()).unwrap()); let current = initial.unwrap_or_else(|_| {
Snapshot::parse(String::new()).expect("an empty Settings document uses valid defaults")
});
let resolved = ferese_theme_client::service::current(); let resolved = ferese_theme_client::service::current();
let font = fonts::interface_font(&resolved.presented.tokens.typography.font_family); let font = fonts::interface_font(&resolved.presented.tokens.typography.font_family);
let native_palette = visuals::Palette::from_resolved(&resolved.presented); let native_palette = visuals::Palette::from_resolved(&resolved.presented);
@@ -540,7 +542,10 @@ impl cosmic::Application for App {
if let Some((id, appearance)) = &self.gallery_focus if let Some((id, appearance)) = &self.gallery_focus
&& self.gallery_position(id, *appearance) == Some((list, index)) && self.gallery_position(id, *appearance) == Some((list, index))
{ {
let (id, appearance) = self.gallery_focus.take().unwrap(); let (id, appearance) = self
.gallery_focus
.take()
.expect("gallery focus was requested before this focus operation");
return cosmic::iced::advanced::widget::operate( return cosmic::iced::advanced::widget::operate(
cosmic::iced::advanced::widget::operation::focusable::focus( cosmic::iced::advanced::widget::operation::focusable::focus(
gallery_id(&id, appearance), gallery_id(&id, appearance),
@@ -1100,7 +1105,9 @@ impl App {
{ {
return self.apply_wallpaper_edits(wallpaper_controls::edits( return self.apply_wallpaper_edits(wallpaper_controls::edits(
wallpaper_controls::Target::Both, wallpaper_controls::Target::Both,
path.rsplit('.').next().unwrap(), path.rsplit('.')
.next()
.expect("splitting a field path always yields its final component"),
value.clone(), value.clone(),
)); ));
} }
+6 -3
View File
@@ -321,10 +321,13 @@ impl App {
.map(|panel| panel.resolved_geometry()) .map(|panel| panel.resolved_geometry())
.unwrap_or_default(), .unwrap_or_default(),
) )
.unwrap(); .expect("typed configuration and IPC values must serialize to JSON");
*default = geometry[field.path.rsplit('.').next().unwrap()] *default =
geometry[field.path.rsplit('.').next().expect(
"splitting a field path always yields its final component",
)]
.as_f64() .as_f64()
.unwrap(); .expect("splitting a field path always yields its final component");
} }
let pinned = self.draft.item(&field.path).is_some(); let pinned = self.draft.item(&field.path).is_some();
let reset = pinned.then_some(Message::PanelEdit(Action::ClearGeometry( let reset = pinned.then_some(Message::PanelEdit(Action::ClearGeometry(
+7 -3
View File
@@ -258,7 +258,7 @@ impl Widget<Message, Theme, Renderer> for Editor<'_> {
.find_map(|(region, bounds)| { .find_map(|(region, bounds)| {
matches!(region, Region::Item(id, _) if id == item).then_some(*bounds) matches!(region, Region::Item(id, _) if id == item).then_some(*bounds)
}) })
.unwrap(); .expect("the active drag branch retains the pressed item and its measured bounds");
state.drag = Some(Drag { state.drag = Some(Drag {
item: item.clone(), item: item.clone(),
origin: cursor, origin: cursor,
@@ -272,7 +272,9 @@ impl Widget<Message, Theme, Renderer> for Editor<'_> {
} }
} }
Event::Mouse(mouse::Event::CursorMoved { .. }) if state.drag.is_some() => { Event::Mouse(mouse::Event::CursorMoved { .. }) if state.drag.is_some() => {
let drag = state.drag.as_mut().unwrap(); let drag = state.drag.as_mut().expect(
"the active drag branch retains the pressed item and its measured bounds",
);
if let Some(cursor) = cursor.position() { if let Some(cursor) = cursor.position() {
drag.cursor = cursor; drag.cursor = cursor;
drag.moved |= (cursor.x - drag.origin.x).hypot(cursor.y - drag.origin.y) >= 6.; drag.moved |= (cursor.x - drag.origin.x).hypot(cursor.y - drag.origin.y) >= 6.;
@@ -291,7 +293,9 @@ impl Widget<Message, Theme, Renderer> for Editor<'_> {
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
if state.drag.is_some() => if state.drag.is_some() =>
{ {
let drag = state.drag.take().unwrap(); let drag = state.drag.take().expect(
"the active drag branch retains the pressed item and its measured bounds",
);
if drag.moved { if drag.moved {
if let Some(target) = cursor if let Some(target) = cursor
.position() .position()
+18 -8
View File
@@ -147,7 +147,7 @@ fn locate(panel: &Panel, id: &ItemId) -> Result<(String, usize), String> {
.groups .groups
.iter() .iter()
.find(|group| group.id == destination.group) .find(|group| group.id == destination.group)
.unwrap(); .expect("the selected destination group belongs to its resolved zone");
if let Some(index) = group.items.iter().position(|item| &item.id == id) { if let Some(index) = group.items.iter().position(|item| &item.id == id) {
return Ok((path, index)); return Ok((path, index));
} }
@@ -309,7 +309,7 @@ pub(super) fn plan(snapshot: &Snapshot, action: Action) -> Result<Vec<Edit>, Str
} }
let group = records(&candidate, &format!("panels.0.{}.groups", zone.key()))[0]["id"] let group = records(&candidate, &format!("panels.0.{}.groups", zone.key()))[0]["id"]
.as_str() .as_str()
.unwrap() .expect("validated panel groups and item kinds have string identifiers")
.to_owned(); .to_owned();
edits.extend(plan( edits.extend(plan(
&candidate, &candidate,
@@ -337,7 +337,13 @@ pub(super) fn plan(snapshot: &Snapshot, action: Action) -> Result<Vec<Edit>, Str
} }
let (path, index) = locate(panel, &id)?; let (path, index) = locate(panel, &id)?;
if field == "style" if field == "style"
&& !matches!(panel.item(&id).unwrap().kind, ItemKind::Workspaces { .. }) && !matches!(
panel
.item(&id)
.expect("the item edit resolved its stable ID before checking its kind")
.kind,
ItemKind::Workspaces { .. }
)
{ {
return Err("Workspace style belongs to Workspaces items.".into()); return Err("Workspace style belongs to Workspaces items.".into());
} }
@@ -349,11 +355,14 @@ pub(super) fn plan(snapshot: &Snapshot, action: Action) -> Result<Vec<Edit>, Str
} }
let path = group_path(panel, &destination)?; let path = group_path(panel, &destination)?;
let name = serde_json::to_value(kind).map_err(|error| error.to_string())?; let name = serde_json::to_value(kind).map_err(|error| error.to_string())?;
let name = name["kind"].as_str().unwrap(); let name = name["kind"]
.as_str()
.expect("validated panel groups and item kinds have string identifiers");
let count = destinations(panel) let count = destinations(panel)
.iter() .iter()
.map(|destination| { .map(|destination| {
let path = group_path(panel, destination).unwrap(); let path = group_path(panel, destination)
.expect("catalog destinations refer to existing panel groups");
records(snapshot, &path).len() records(snapshot, &path).len()
}) })
.sum::<usize>(); .sum::<usize>();
@@ -366,7 +375,7 @@ pub(super) fn plan(snapshot: &Snapshot, action: Action) -> Result<Vec<Edit>, Str
} }
}) })
.find(|id| panel.item(&ItemId(id.clone())).is_none()) .find(|id| panel.item(&ItemId(id.clone())).is_none())
.unwrap(); .expect("one more candidate ID than existing items guarantees an unused ID");
let mut items = records(snapshot, &path); let mut items = records(snapshot, &path);
items.push( items.push(
serde_json::to_value(Item::new(&id, kind)).map_err(|error| error.to_string())?, serde_json::to_value(Item::new(&id, kind)).map_err(|error| error.to_string())?,
@@ -381,7 +390,7 @@ pub(super) fn plan(snapshot: &Snapshot, action: Action) -> Result<Vec<Edit>, Str
let id = (1..=count + 1) let id = (1..=count + 1)
.map(|number| format!("{}-{number}", zone.key())) .map(|number| format!("{}-{number}", zone.key()))
.find(|id| locate_group(panel, &GroupId(id.clone())).is_err()) .find(|id| locate_group(panel, &GroupId(id.clone())).is_err())
.unwrap(); .expect("one more candidate ID than existing items guarantees an unused ID");
let path = format!("panels.0.{}.groups", zone.key()); let path = format!("panels.0.{}.groups", zone.key());
let mut groups = records(snapshot, &path); let mut groups = records(snapshot, &path);
groups.push(serde_json::json!({ groups.push(serde_json::json!({
@@ -432,7 +441,8 @@ pub(super) fn plan(snapshot: &Snapshot, action: Action) -> Result<Vec<Edit>, Str
"padding_vertical" | "padding_horizontal" => { "padding_vertical" | "padding_horizontal" => {
let (zone, index) = locate_group(panel, &id)?; let (zone, index) = locate_group(panel, &id)?;
let mut padding = let mut padding =
serde_json::to_value(zone.definition(panel).groups[index].padding).unwrap(); serde_json::to_value(zone.definition(panel).groups[index].padding)
.expect("typed configuration and IPC values must serialize to JSON");
padding[usize::from(field == "padding_horizontal")] = value; padding[usize::from(field == "padding_horizontal")] = value;
("padding".to_owned(), padding) ("padding".to_owned(), padding)
} }
+9 -4
View File
@@ -140,12 +140,12 @@ impl App {
.into_iter() .into_iter()
.flat_map(|zone| &zone.definition(panel).groups) .flat_map(|zone| &zone.definition(panel).groups)
.find(|group| group.id == destination.group) .find(|group| group.id == destination.group)
.unwrap(); .expect("the inspector selection refers to an item or group in the current panel");
let index = group let index = group
.items .items
.iter() .iter()
.position(|candidate| candidate.id == item.id) .position(|candidate| candidate.id == item.id)
.unwrap(); .expect("the inspector selection refers to an item or group in the current panel");
rows.push(self.panel_divider()) rows.push(self.panel_divider())
.push( .push(
self.inspector_actions( self.inspector_actions(
@@ -293,7 +293,7 @@ impl App {
.groups .groups
.iter() .iter()
.position(|candidate| candidate.id == group.id) .position(|candidate| candidate.id == group.id)
.unwrap(); .expect("the inspector selection refers to an item or group in the current panel");
rows = rows.push( rows = rows.push(
self.inspector_actions( self.inspector_actions(
(index > 0).then(|| Message::PanelEdit(Action::EarlierGroup(group.id.clone()))), (index > 0).then(|| Message::PanelEdit(Action::EarlierGroup(group.id.clone()))),
@@ -335,7 +335,12 @@ impl App {
fn panel_field(&self, item: &Item, field: Field) -> Element<'static, Message> { fn panel_field(&self, item: &Item, field: Field) -> Element<'static, Message> {
let p = crate::visuals::Palette::from_resolved(&self.resolved.presented); let p = crate::visuals::Palette::from_resolved(&self.resolved.presented);
let id = item.id.clone(); let id = item.id.clone();
let name = field.path.rsplit('.').next().unwrap().to_owned(); let name = field
.path
.rsplit('.')
.next()
.expect("splitting a field path always yields its final component")
.to_owned();
let values = serde_json::to_value(item).expect("serializable panel item"); let values = serde_json::to_value(item).expect("serializable panel item");
match field.kind { match field.kind {
Kind::Choice { default, choices } => { Kind::Choice { default, choices } => {
@@ -328,7 +328,9 @@ impl App {
return setting( return setting(
&self.draft, &self.draft,
self.wallpaper.target, self.wallpaper.target,
path.rsplit('.').next().unwrap(), path.rsplit('.')
.next()
.expect("splitting a field path always yields its final component"),
fallback, fallback,
); );
} }
+5 -1
View File
@@ -336,7 +336,11 @@ impl FereseShell {
presentation.rows = rows; presentation.rows = rows;
continue; continue;
} }
let runtime = output.runtime.hub.as_ref().unwrap(); let runtime = output
.runtime
.hub
.as_ref()
.expect("this activity action belongs to an output with a Morph hub");
let name = self let name = self
.output_for_bar(output.bar) .output_for_bar(output.bar)
.map(|output| output.name.as_str()) .map(|output| output.name.as_str())
+5 -1
View File
@@ -67,7 +67,11 @@ impl Compact {
+ count as f32 * (ICON + TEXT_GAP) + count as f32 * (ICON + TEXT_GAP)
+ count.saturating_sub(1) as f32 * (2. * ITEM_GAP + 1.) + count.saturating_sub(1) as f32 * (2. * ITEM_GAP + 1.)
+ if hidden > 0 { + if hidden > 0 {
ITEM_GAP + self.badge.unwrap().1 ITEM_GAP
+ self
.badge
.expect("hidden activity count is nonzero only when its badge was measured")
.1
} else { } else {
0. 0.
}; };
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more