use ferese_core::{OutputId, WorkspaceId}; use ferese_layout::WindowId; use ferese_protocols::shell::v1::server::ferese_shell_manager_v1::FereseShellManagerV1; use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1; use ferese_protocols::shell::v1::server::{ferese_shell_manager_v1, ferese_shell_v1}; use smithay::reexports::wayland_server::{ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, }; use std::sync::atomic::{AtomicBool, Ordering}; use crate::Ferese; use crate::private_client::ClientCapabilities; use crate::state::ClientState; pub(crate) fn init_global(display: &DisplayHandle) { display.create_global::(11, ()); } pub(crate) struct ShellData { background_title_updates: AtomicBool, } impl Default for ShellData { fn default() -> Self { Self { background_title_updates: AtomicBool::new(true), } } } pub(crate) fn config_chunks(source: &str) -> Vec<&str> { let mut remaining = source; let mut chunks = Vec::new(); while !remaining.is_empty() { let mut boundary = remaining.len().min(1024); while !remaining.is_char_boundary(boundary) { boundary -= 1; } chunks.push(&remaining[..boundary]); remaining = &remaining[boundary..]; } chunks } pub(crate) fn send_shell_config(shell: &FereseShellV1, source: &str) { shell.config_begin(); for chunk in config_chunks(source) { shell.config_chunk(chunk.to_owned()); } shell.config_end(); } impl GlobalDispatch for Ferese { fn bind( _state: &mut Self, _display: &DisplayHandle, _client: &Client, resource: New, _global_data: &(), data_init: &mut DataInit<'_, Self>, ) { data_init.init(resource, ()); } fn can_view(client: Client, _global_data: &()) -> bool { client.get_data::().is_some_and(|state| { state .capabilities .contains(ClientCapabilities::SHELL_CONTROL) }) } } impl Dispatch for Ferese { fn request( state: &mut Self, _client: &Client, _manager: &FereseShellManagerV1, request: ferese_shell_manager_v1::Request, _data: &(), _display: &DisplayHandle, data_init: &mut DataInit<'_, Self>, ) { match request { ferese_shell_manager_v1::Request::GetShell { id } => { let shell = data_init.init(id, ShellData::default()); shell.capabilities(1); if shell.version() >= 2 && let Some(source) = &state.config_source { send_shell_config(&shell, source); } shell.overview_state(u32::from(state.overview.is_active())); state.send_shell_snapshot(&shell); state.shell_resources.push(shell.downgrade()); } ferese_shell_manager_v1::Request::Destroy => {} _ => unreachable!(), } } } impl Dispatch for Ferese { fn request( state: &mut Self, _client: &Client, shell: &FereseShellV1, request: ferese_shell_v1::Request, data: &ShellData, _display: &DisplayHandle, _data_init: &mut DataInit<'_, Self>, ) { match request { ferese_shell_v1::Request::SetBackgroundTitleUpdates { enabled } => { if data .background_title_updates .swap(enabled != 0, Ordering::Relaxed) != (enabled != 0) && !state.session_lock.active() { state.send_shell_snapshot(shell); } } _ if state.session_lock.active() => {} ferese_shell_v1::Request::Thumbnail { token, window_hi, window_lo, } => { if let Some((width, height, file)) = state.shell_thumbnail(join_id(window_hi, window_lo)) { use std::os::fd::AsFd; shell.thumbnail(token, window_hi, window_lo, width, height, file.as_fd()); } else { shell.thumbnail_unavailable(token, window_hi, window_lo); } } ferese_shell_v1::Request::ActivateWindow { window_hi, window_lo, } => state.handle_shell_window_request( shell, ferese_shell_v1::FailedRequest::ActivateWindow, join_id(window_hi, window_lo), ShellWindowAction::Activate, ), ferese_shell_v1::Request::CloseWindow { window_hi, window_lo, } => state.handle_shell_window_request( shell, ferese_shell_v1::FailedRequest::CloseWindow, join_id(window_hi, window_lo), ShellWindowAction::Close, ), ferese_shell_v1::Request::ActivateWorkspace { workspace_hi, workspace_lo, } => { let workspace = WorkspaceId(join_u64(workspace_hi, workspace_lo)); if !state.activate_managed_workspace(workspace) { send_request_failed( shell, ferese_shell_v1::FailedRequest::ActivateWorkspace, workspace.0, ); } } ferese_shell_v1::Request::MoveWindowToWorkspace { window_hi, window_lo, workspace_hi, workspace_lo, } => { let window = join_id(window_hi, window_lo); let workspace = WorkspaceId(join_u64(workspace_hi, workspace_lo)); if !state.move_managed_window_to_workspace(window, workspace) { send_request_failed( shell, ferese_shell_v1::FailedRequest::MoveWindowToWorkspace, window.0, ); } } ferese_shell_v1::Request::ConfirmLogout { serial } => { let snapshot = state.portal_session.snapshot(); let token = snapshot["query-token"].as_u64().unwrap_or(0) as u32; let revision = snapshot["inhibitor-revision"].as_u64().unwrap_or(0) as u32; state.confirm_portal_logout(shell, serial, token, revision, false); } ferese_shell_v1::Request::ConfirmLogoutWithInhibitors { serial, inhibitor_revision, query_token, force, } => { if force <= 1 { state.confirm_portal_logout( shell, serial, query_token, inhibitor_revision, force == 1, ); } } ferese_shell_v1::Request::CancelLogout { serial } => { if state.logout_owner.as_ref() == Some(&shell.id()) && state.pending_logout == Some(serial) { state.cancel_logout_confirmation(); } } ferese_shell_v1::Request::EnterOverview => state.set_overview_active(true), ferese_shell_v1::Request::ExitOverview => state.set_overview_active(false), ferese_shell_v1::Request::SelectOverviewWindow { window_hi, window_lo, } => { let id = join_id(window_hi, window_lo); if !state.select_overview_window(id) { send_request_failed( shell, ferese_shell_v1::FailedRequest::SelectOverviewWindow, id.0, ); } } ferese_shell_v1::Request::Destroy => { if state.logout_owner.as_ref() == Some(&shell.id()) { state.cancel_logout_confirmation(); } } _ => unreachable!(), } } fn destroyed( state: &mut Self, _client: smithay::reexports::wayland_server::backend::ClientId, shell: &FereseShellV1, _data: &ShellData, ) { if state.logout_owner.as_ref() == Some(&shell.id()) { state.cancel_logout_confirmation(); } } } fn logout_confirmation_matches( pending: Option, query: Option, serial: u32, token: u32, ) -> bool { pending == Some(serial) && query == Some(token) } fn consume_logout_confirmation(pending: &mut Option, serial: u32) -> bool { if *pending == Some(serial) { *pending = None; true } else { false } } impl Ferese { fn confirm_portal_logout( &mut self, shell: &FereseShellV1, serial: u32, token: u32, revision: u32, force: bool, ) { if self.logout_owner.as_ref() != Some(&shell.id()) || !logout_confirmation_matches(self.pending_logout, self.logout_query, serial, token) { return; } match self.portal_session.commit_end(token, revision, force) { Ok(()) => { consume_logout_confirmation(&mut self.pending_logout, serial); self.logout_owner = None; self.logout_query = None; self.end_portal_session(); } Err(error) => { tracing::debug!(%error, "logout requires a new confirmation"); self.cancel_logout_confirmation(); self.request_logout_confirmation(); } } } pub(crate) fn notify_monitor_state(&self) { for shell in self .shell_resources .iter() .filter_map(|shell| shell.upgrade().ok()) .filter(|shell| shell.version() >= 6) { shell.monitor_state_changed(); } } pub(crate) fn toggle_display_mode(&mut self) -> bool { if self.session_lock.active() { return false; } let Some(shell) = self .shell_resources .iter() .filter_map(|shell| shell.upgrade().ok()) .find(|shell| shell.version() >= 6) else { tracing::warn!("display mode chooser requires the updated Ferese shell"); return false; }; shell.toggle_display_mode( self.focused_output() .map_or_else(String::new, |output| output.name()), ); true } pub(crate) fn toggle_launcher(&mut self) -> bool { if self.session_lock.active() { return false; } let Some(shell) = self .shell_resources .iter() .filter_map(|shell| shell.upgrade().ok()) .find(|shell| shell.version() >= 10) else { return false; }; shell.toggle_launcher( self.focused_output() .map_or_else(String::new, |output| output.name()), ); true } pub(crate) fn toggle_keybinding_guide(&mut self) -> bool { if self.session_lock.active() { return false; } let Some(shell) = self .shell_resources .iter() .filter_map(|shell| shell.upgrade().ok()) .find(|shell| shell.version() >= 5) else { tracing::warn!("shortcut hint requires the updated Ferese shell"); return false; }; let output = self .focused_output() .map_or_else(String::new, |output| output.name()); shell.toggle_keybinding_guide(output); true } pub(crate) fn request_logout_confirmation(&mut self) { if self.session_lock.active() { return; } let shells = self .shell_resources .iter() .filter_map(|shell| shell.upgrade().ok()) .filter(|shell| shell.version() >= 3) .collect::>(); if shells.is_empty() { tracing::warn!("logout confirmation requires the updated Ferese shell"); return; } self.cancel_logout_confirmation(); let serial = u32::from(smithay::utils::SERIAL_COUNTER.next_serial()); let shell = &shells[0]; self.logout_query = Some(self.portal_session.begin_query()); self.pending_logout = Some(serial); self.logout_owner = Some(shell.id()); let output_name = self .focused_output() .map_or_else(String::new, |output| output.name()); shell.logout_requested(serial, output_name); } pub(crate) fn cancel_logout_confirmation(&mut self) { if let Some(token) = self.logout_query.take() { self.portal_session.cancel_query(token); } if let Some(serial) = self.pending_logout.take() { for shell in self .shell_resources .iter() .filter_map(|shell| shell.upgrade().ok()) { if self.logout_owner.as_ref() == Some(&shell.id()) && shell.version() >= 3 { shell.logout_cancelled(serial); } } } self.logout_owner = None; } pub(crate) fn send_shell_snapshots(&mut self) { if let Some(transition) = self.desktop_transition.as_mut() { transition.shell_pending = true; return; } if self .direct_backend .as_ref() .is_some_and(|backend| backend.reconciling()) { return; } self.shell_resources .retain(|resource| resource.upgrade().is_ok()); if self.shell_resources.is_empty() { self.last_shell_snapshot = None; return; } let background_title_updates = self.shell_resources.iter().any(|resource| { resource.upgrade().is_ok_and(|shell| { shell .data::() .is_some_and(|data| data.background_title_updates.load(Ordering::Relaxed)) }) }); if self .last_shell_snapshot .as_ref() .is_some_and(|snapshot| self.shell_snapshot_matches(snapshot, background_title_updates)) { return; } let snapshot = self.shell_snapshot(); for resource in &self.shell_resources { if let Ok(shell) = resource.upgrade() { self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1); snapshot.send(&shell, self.shell_snapshot_serial); } } self.last_shell_snapshot = Some(snapshot); } fn shell_snapshot_matches( &self, snapshot: &ShellSnapshot, background_title_updates: bool, ) -> bool { if snapshot.workspace_preview_revision != self.workspace_preview_revision || snapshot.binding_mode.as_deref() != self.binding_modes.active() { return false; } let mut outputs = self.output_workspaces.connected_outputs(); for previous in &snapshot.outputs { if outputs.next() != Some(previous.id) || self.output_names.get(&previous.id) != Some(&previous.name) || self.output_workspaces.active_workspace(previous.id) != Some(previous.active_workspace) || self .output_ids .iter() .find(|(_, id)| **id == previous.id) .and_then(|(output, _)| self.output_bounds_for(output)) != previous.bounds || (self.output_workspaces.focused_output() == Some(previous.id)) != previous.focused { return false; } } if outputs.next().is_some() { return false; } let mut workspaces = self .output_workspaces .workspace_views_iter(&self.workspaces); for previous in &snapshot.workspaces { let Some(view) = workspaces.next() else { return false; }; if previous.id != view.id || previous.output != Some(view.output) || previous.index != view.index || previous.window_count != view.window_count as u32 || previous.visible != view.visible || previous.focused != view.focused { return false; } } if workspaces.next().is_some() { return false; } let mut windows = self .windows .ordered_ids() .filter_map(|id| self.shell_window_data(id)); for previous in &snapshot.windows { let Some((id, workspace, app_id, title, state)) = windows.next() else { return false; }; if previous.id != id || previous.workspace != workspace || previous.app_id != app_id || ((background_title_updates || state.contains(ferese_shell_v1::WindowState::Focused)) && previous.title != title) || previous.state != state || previous.source_geometry != self.shell_window_geometry(id) { return false; } } windows.next().is_none() } fn shell_snapshot(&self) -> ShellSnapshot { ShellSnapshot { binding_mode: self.binding_modes.active().map(str::to_owned), workspace_preview_revision: self.workspace_preview_revision, outputs: self.output_snapshots(), workspaces: self.workspace_snapshots(), windows: self.managed_window_snapshots(), } } fn send_shell_snapshot(&mut self, shell: &FereseShellV1) { if let Some(transition) = self.desktop_transition.as_mut() { transition.shell_pending = true; self.last_shell_snapshot = None; return; } // New subscribers always receive a complete snapshot. Do not update the // broadcast cache here: existing subscribers may still need this state. self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1); self.shell_snapshot() .send(shell, self.shell_snapshot_serial); } fn output_snapshots(&self) -> Vec { let focused = self.output_workspaces.focused_output(); let mut outputs = self .output_ids .iter() .filter_map(|(output, id)| { Some(OutputSnapshot { id: *id, name: output.name(), active_workspace: self.output_workspaces.active_workspace(*id)?, focused: focused == Some(*id), bounds: self.output_bounds_for(output), }) }) .collect::>(); outputs.sort_by_key(|output| output.id.0); outputs } fn workspace_snapshots(&self) -> Vec { self.output_workspaces .workspace_views_iter(&self.workspaces) .map(|view| WorkspaceSnapshot { id: view.id, output: Some(view.output), name: view.index.to_string(), index: view.index, window_count: view.window_count as u32, visible: view.visible, focused: view.focused, }) .collect() } fn shell_window_data( &self, id: WindowId, ) -> Option<(WindowId, u64, &str, &str, ferese_shell_v1::WindowState)> { let workspace = self.workspaces.workspace_for_window(id)?; let record = self.windows.record(id)?; self.windows.window(id)?.toplevel()?; let mut state = ferese_shell_v1::WindowState::empty(); if record.maximized { state |= ferese_shell_v1::WindowState::Maximized; } if self.focused_window == Some(id) { state |= ferese_shell_v1::WindowState::Focused; } if self .workspaces .workspace(workspace) .is_some_and(|workspace| workspace.fullscreen == Some(id)) { state |= ferese_shell_v1::WindowState::Fullscreen; } if matches!( self.workspaces.placement(id), Some(ferese_core::WindowPlacement::Floating { .. }) ) { state |= ferese_shell_v1::WindowState::Floating; } Some((id, workspace.0, &record.app_id, &record.title, state)) } fn shell_window_geometry(&self, id: WindowId) -> Option { let rect = self.windows.geometry(&id)?.visual.target; (rect.width > 0. && rect.height > 0.).then_some(rect) } fn managed_window_snapshots(&self) -> Vec { let previews = self.workspace_preview_rects(); self.windows .ordered_ids() .filter_map(|id| self.shell_window_data(id)) .map( |(id, workspace, app_id, title, state)| ManagedWindowSnapshot { id, workspace, app_id: app_id.to_owned(), title: title.to_owned(), state, source_geometry: self.shell_window_geometry(id), geometry: previews .get(&id) .copied() .or_else(|| self.shell_window_geometry(id)), }, ) .collect() } fn handle_shell_window_request( &mut self, shell: &FereseShellV1, request: ferese_shell_v1::FailedRequest, id: WindowId, action: ShellWindowAction, ) { let succeeded = match action { ShellWindowAction::Activate => self.activate_managed_window(id), ShellWindowAction::Close => self.close_managed_window(id), }; if !succeeded { send_request_failed(shell, request, id.0); } } pub(crate) fn notify_overview_state(&mut self) { let active = self.overview.is_active(); self.shell_resources.retain(|resource| { resource.upgrade().is_ok_and(|shell| { shell.overview_state(u32::from(active)); true }) }); } } #[derive(Clone, Copy)] enum ShellWindowAction { Activate, Close, } #[derive(Clone, Debug, PartialEq)] pub(crate) struct ShellSnapshot { binding_mode: Option, workspace_preview_revision: u64, outputs: Vec, workspaces: Vec, windows: Vec, } impl ShellSnapshot { fn send(&self, shell: &FereseShellV1, serial: u32) { shell.snapshot_begin(serial); if shell.version() >= 9 { shell.binding_mode(self.binding_mode.clone().unwrap_or_default()); } for output in &self.outputs { let (output_hi, output_lo) = split_id(output.id.0); let (workspace_hi, workspace_lo) = split_id(output.active_workspace.0); shell.output( output_hi, output_lo, output.name.clone(), workspace_hi, workspace_lo, u32::from(output.focused), ); } for workspace in &self.workspaces { let (workspace_hi, workspace_lo) = split_id(workspace.id.0); let (output_hi, output_lo) = workspace .output .map(|output| split_id(output.0)) .unwrap_or_default(); shell.workspace( workspace_hi, workspace_lo, output_hi, output_lo, workspace.name.clone(), workspace.index, workspace.window_count, u32::from(workspace.visible), u32::from(workspace.focused), ); } for snapshot in &self.windows { let (window_hi, window_lo) = split_id(snapshot.id.0); let (workspace_hi, workspace_lo) = split_id(snapshot.workspace); shell.window( window_hi, window_lo, workspace_hi, workspace_lo, snapshot.app_id.clone(), snapshot.title.clone(), snapshot.state, ); if shell.version() >= 8 && let Some(rect) = snapshot.geometry { shell.window_geometry( window_hi, window_lo, rect.x, rect.y, rect.width, rect.height, ); } } shell.snapshot_end(serial); } } #[derive(Clone, Debug, PartialEq)] struct ManagedWindowSnapshot { id: WindowId, workspace: u64, app_id: String, title: String, state: ferese_shell_v1::WindowState, geometry: Option, source_geometry: Option, } #[derive(Clone, Debug, PartialEq)] struct OutputSnapshot { bounds: Option, id: OutputId, name: String, active_workspace: WorkspaceId, focused: bool, } #[derive(Clone, Debug, PartialEq)] struct WorkspaceSnapshot { id: WorkspaceId, output: Option, name: String, index: u32, window_count: u32, visible: bool, focused: bool, } fn split_id(id: u64) -> (u32, u32) { ((id >> 32) as u32, id as u32) } fn join_id(hi: u32, lo: u32) -> WindowId { WindowId(join_u64(hi, lo)) } fn join_u64(hi: u32, lo: u32) -> u64 { (u64::from(hi) << 32) | u64::from(lo) } fn send_request_failed(shell: &FereseShellV1, request: ferese_shell_v1::FailedRequest, id: u64) { let (object_hi, object_lo) = split_id(id); shell.request_failed( request, object_hi, object_lo, "unknown or unavailable shell object".to_owned(), ); } impl Ferese { pub(crate) fn dismiss_launcher(&self) { for shell in self .shell_resources .iter() .filter_map(|s| s.upgrade().ok()) .filter(|s| s.version() >= 10) { shell.dismiss_launcher(); } } fn shell_thumbnail(&mut self, id: WindowId) -> Option<(u32, u32, std::fs::File)> { use std::io::{Seek, Write}; if self.session_lock.active() { return None; } let window = self .windows .ids() .iter() .find(|(_, candidate)| **candidate == id)? .0 .clone(); if self.capture_protected(&window) { return None; } let workspace = self.workspaces.workspace_for_window(id)?; let owner = self.output_workspaces.output_for_workspace(workspace)?; let output = self .output_ids .iter() .find(|(_, candidate)| **candidate == owner)? .0 .clone(); let geometry = window.geometry(); if geometry.size.w <= 0 || geometry.size.h <= 0 { return None; } let scale = (320. / f64::from(geometry.size.w)) .min(168. / f64::from(geometry.size.h)) .min(1.); let result = if let Some(backend) = self.nested_backend.clone() { let mut backend = backend.try_borrow_mut().ok()?; crate::render::capture_window_frame(backend.renderer(), &window, geometry, scale, None) } else { let mut backend = self.direct_backend.take()?; let result = backend.capture_window_frame(self, &window, &output, None, scale); self.direct_backend = Some(backend); result }; let mut buffer = result.ok()?; if buffer.width <= 0 || buffer.width > 320 || buffer.height <= 0 || buffer.height > 168 { return None; } // GL ABGR readback is RGBA bytes on the supported little-endian targets. // Iced expects straight rather than premultiplied channels. for pixel in buffer.pixels.chunks_exact_mut(4) { let alpha = u16::from(pixel[3]); for channel in &mut pixel[..3] { *channel = (u16::from(*channel) * 255 + alpha / 2) .checked_div(alpha) .unwrap_or(0) .min(255) as u8; } } let mut file = tempfile::tempfile().ok()?; file.write_all(&buffer.pixels).ok()?; file.rewind().ok()?; Some((buffer.width as u32, buffer.height as u32, file)) } } #[cfg(test)] mod tests { #[test] #[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"] fn snapshot_precheck_detects_membership_selection_and_output_changes() { use smithay::output::{Output, PhysicalProperties, Subpixel}; use smithay::reexports::calloop::EventLoop; use smithay::reexports::wayland_server::Display; use super::*; let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap()); assert!(runtime.starts_with(std::env::temp_dir())); let mut event_loop = EventLoop::try_new().unwrap(); let config = crate::config::Config::default().runtime_config().unwrap(); let mut state = Ferese::new(&mut event_loop, Display::new().unwrap(), config).unwrap(); let output = Output::new( "snapshot-test".into(), PhysicalProperties { serial_number: String::new(), size: (0, 0).into(), subpixel: Subpixel::Unknown, make: "test".into(), model: "test".into(), }, ); output.change_current_state( Some(smithay::output::Mode { size: (800, 600).into(), refresh: 60_000, }), Some(smithay::utils::Transform::Normal), None, Some((0, 0).into()), ); state.space.map_output(&output, (0, 0)); state.register_output(&output, "snapshot-test".into()); let original = state.shell_snapshot(); let initial_cache = state.preview_cache_stats(); for _ in 0..100 { // Building a snapshot for metadata changes must reuse layout geometry. assert_eq!(state.shell_snapshot(), original); assert!(state.shell_snapshot_matches(&original, true)); } assert_eq!(state.preview_cache_stats(), initial_cache); state .workspaces .insert_window(WindowId(99), ferese_layout::Axis::Horizontal, 0.5) .unwrap(); assert!(!state.shell_snapshot_matches(&original, true)); let occupied = state.shell_snapshot(); assert_eq!(state.preview_cache_stats().0, initial_cache.0 + 1); assert!(state.shell_snapshot_matches(&occupied, true)); let output_id = state.output_ids[&output]; let next = state.workspaces.create_workspace(); state .output_workspaces .assign_workspace(output_id, next) .unwrap(); assert!(!state.shell_snapshot_matches(&occupied, true)); let assigned = state.shell_snapshot(); state .output_workspaces .switch_workspace(output_id, next) .unwrap(); assert!(!state.shell_snapshot_matches(&assigned, true)); let selected = state.shell_snapshot(); assert!(state.shell_snapshot_matches(&selected, true)); assert_eq!( state.output_workspaces.active_workspace(output_id), Some(next) ); assert_eq!( state .workspaces .workspace(next) .unwrap() .layout .window_ids() .count(), 0 ); let hidden_before = state.workspace_preview_rects()[&WindowId(99)]; let before = state.preview_cache_stats(); state.output_names.insert(output_id, "Renamed".into()); state.shell_snapshot(); assert_eq!( state.preview_cache_stats(), before, "names do not affect previews" ); state.output_names.insert(output_id, "snapshot-test".into()); // The second workspace is empty; modifying its layout should not solve the hidden one. state .workspaces .workspace_mut(next) .unwrap() .layout .insert(WindowId(100), None, ferese_layout::Axis::Horizontal, 0.5) .unwrap(); state.workspace_preview_rects(); assert_eq!(state.preview_cache_stats().0, before.0 + 1); state .workspaces .workspace_mut(next) .unwrap() .layout .remove(WindowId(100)) .unwrap(); state.workspace_preview_rects(); let selected = state.shell_snapshot(); state .reload_config_source( "layout { inner-gap 30; outer-gap 40; smart-gaps #false; }".into(), ) .unwrap(); assert!( !state.shell_snapshot_matches(&selected, true), "hidden previews must invalidate even with no visible window geometry to change" ); assert_ne!( state.workspace_preview_rects()[&WindowId(99)], hidden_before ); let refreshed = state.shell_snapshot(); assert!(state.shell_snapshot_matches(&refreshed, true)); state .reload_config_source( "layout { inner-gap 30; outer-gap 40; smart-gaps #false; }; input { repeat-rate 35; }".into(), ) .unwrap(); assert_eq!( state.workspace_preview_revision, refreshed.workspace_preview_revision, "unrelated settings do not invalidate layouts" ); state.unregister_output(&output); assert!(!state.shell_snapshot_matches(&refreshed, true)); state.workspace_preview_rects(); assert_eq!( state.preview_cache_stats().1, 0, "removed outputs release cached previews" ); } #[test] fn logout_serial_cannot_authorize_a_different_session_query() { assert!(super::logout_confirmation_matches(Some(42), Some(9), 42, 9)); assert!(!super::logout_confirmation_matches( Some(42), Some(9), 42, 10 )); assert!(!super::logout_confirmation_matches(None, Some(9), 42, 9)); } #[test] fn logout_requires_the_current_confirmation_and_can_be_cancelled() { let mut pending = Some(42); assert!(!super::consume_logout_confirmation(&mut pending, 41)); assert_eq!(pending, Some(42)); assert!(super::consume_logout_confirmation(&mut pending, 42)); assert_eq!(pending, None); assert!(!super::consume_logout_confirmation(&mut pending, 42)); } use super::*; #[test] fn snapshot_deduplication_preserves_shell_visible_changes() { let original = ShellSnapshot { binding_mode: None, workspace_preview_revision: 0, outputs: vec![OutputSnapshot { bounds: None, id: OutputId(1), name: "display".into(), active_workspace: WorkspaceId(1), focused: true, }], workspaces: vec![WorkspaceSnapshot { id: WorkspaceId(1), output: Some(OutputId(1)), name: "1".into(), index: 1, window_count: 1, visible: true, focused: true, }], windows: vec![ManagedWindowSnapshot { id: WindowId(1), workspace: 1, app_id: "terminal".into(), title: "shell".into(), state: ferese_shell_v1::WindowState::Focused, geometry: None, source_geometry: None, }], }; assert_eq!(original, original.clone()); let changes: &[fn(&mut ShellSnapshot)] = &[ |s| s.binding_mode = Some("resize".into()), |s| s.workspace_preview_revision += 1, |s| s.outputs[0].focused = false, |s| s.outputs[0].active_workspace = WorkspaceId(2), |s| s.outputs.clear(), |s| s.workspaces[0].output = None, |s| s.workspaces[0].visible = false, |s| s.workspaces[0].focused = false, |s| s.workspaces[0].index = 2, |s| s.workspaces[0].window_count = 0, |s| s.workspaces[0].name = "renamed".into(), |s| s.windows[0].workspace = 2, |s| s.windows[0].title = "new title".into(), |s| s.windows[0].app_id = "new app".into(), |s| s.windows[0].state |= ferese_shell_v1::WindowState::Fullscreen, |s| s.windows.clear(), ]; for change in changes { let mut updated = original.clone(); change(&mut updated); assert_ne!(original, updated); } } #[test] fn config_chunks_are_bounded_and_preserve_unicode() { let source = "abc🌲é".repeat(9000); let chunks = config_chunks(&source); assert!( chunks .iter() .all(|chunk| !chunk.is_empty() && chunk.len() <= 1024) ); assert_eq!(chunks.concat(), source); assert!(config_chunks("").is_empty()); } #[test] fn window_ids_round_trip_through_protocol_words() { for id in [0, 1, u32::MAX as u64, u64::from(u32::MAX) + 1, u64::MAX] { let (hi, lo) = split_id(id); assert_eq!(join_id(hi, lo), WindowId(id)); } } }