731 lines
25 KiB
Rust
731 lines
25 KiB
Rust
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 crate::Ferese;
|
|
use crate::private_client::ClientCapabilities;
|
|
use crate::state::ClientState;
|
|
|
|
pub(crate) fn init_global(display: &DisplayHandle) {
|
|
display.create_global::<Ferese, FereseShellManagerV1, _>(5, ());
|
|
}
|
|
|
|
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<FereseShellManagerV1, ()> for Ferese {
|
|
fn bind(
|
|
_state: &mut Self,
|
|
_display: &DisplayHandle,
|
|
_client: &Client,
|
|
resource: New<FereseShellManagerV1>,
|
|
_global_data: &(),
|
|
data_init: &mut DataInit<'_, Self>,
|
|
) {
|
|
data_init.init(resource, ());
|
|
}
|
|
|
|
fn can_view(client: Client, _global_data: &()) -> bool {
|
|
client
|
|
.get_data::<ClientState>()
|
|
.is_some_and(|state| state.capabilities.contains(ClientCapabilities::SHELL_CONTROL))
|
|
}
|
|
}
|
|
|
|
impl Dispatch<FereseShellManagerV1, ()> 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, ());
|
|
|
|
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<FereseShellV1, ()> for Ferese {
|
|
fn request(
|
|
state: &mut Self,
|
|
_client: &Client,
|
|
shell: &FereseShellV1,
|
|
request: ferese_shell_v1::Request,
|
|
_data: &(),
|
|
_display: &DisplayHandle,
|
|
_data_init: &mut DataInit<'_, Self>,
|
|
) {
|
|
if state.session_lock.active {
|
|
return;
|
|
}
|
|
match request {
|
|
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::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: &(),
|
|
) {
|
|
if state.logout_owner.as_ref() == Some(&shell.id()) {
|
|
state.cancel_logout_confirmation();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn logout_confirmation_matches(pending: Option<u32>, query: Option<u32>, serial: u32, token: u32) -> bool {
|
|
pending == Some(serial) && query == Some(token)
|
|
}
|
|
|
|
fn consume_logout_confirmation(pending: &mut Option<u32>, 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 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::<Vec<_>>();
|
|
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) {
|
|
self.shell_resources.retain(|resource| resource.upgrade().is_ok());
|
|
if self.shell_resources.is_empty() {
|
|
self.last_shell_snapshot = None;
|
|
return;
|
|
}
|
|
if self
|
|
.last_shell_snapshot
|
|
.as_ref()
|
|
.is_some_and(|snapshot| self.shell_snapshot_matches(snapshot))
|
|
{
|
|
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) -> bool {
|
|
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_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
|
|
|| previous.title != title
|
|
|| previous.state != state
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
windows.next().is_none()
|
|
}
|
|
|
|
fn shell_snapshot(&self) -> ShellSnapshot {
|
|
ShellSnapshot {
|
|
outputs: self.output_snapshots(),
|
|
workspaces: self.workspace_snapshots(),
|
|
windows: self.managed_window_snapshots(),
|
|
}
|
|
}
|
|
|
|
fn send_shell_snapshot(&mut self, shell: &FereseShellV1) {
|
|
// 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<OutputSnapshot> {
|
|
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),
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
outputs.sort_by_key(|output| output.id.0);
|
|
outputs
|
|
}
|
|
|
|
fn workspace_snapshots(&self) -> Vec<WorkspaceSnapshot> {
|
|
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 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 managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> {
|
|
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,
|
|
})
|
|
.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 {
|
|
outputs: Vec<OutputSnapshot>,
|
|
workspaces: Vec<WorkspaceSnapshot>,
|
|
windows: Vec<ManagedWindowSnapshot>,
|
|
}
|
|
|
|
impl ShellSnapshot {
|
|
fn send(&self, shell: &FereseShellV1, serial: u32) {
|
|
shell.snapshot_begin(serial);
|
|
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,
|
|
);
|
|
}
|
|
shell.snapshot_end(serial);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
struct ManagedWindowSnapshot {
|
|
id: WindowId,
|
|
workspace: u64,
|
|
app_id: String,
|
|
title: String,
|
|
state: ferese_shell_v1::WindowState,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
struct OutputSnapshot {
|
|
id: OutputId,
|
|
name: String,
|
|
active_workspace: WorkspaceId,
|
|
focused: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
struct WorkspaceSnapshot {
|
|
id: WorkspaceId,
|
|
output: Option<OutputId>,
|
|
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(),
|
|
);
|
|
}
|
|
|
|
#[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 {
|
|
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();
|
|
|
|
for _ in 0..100 {
|
|
assert!(state.shell_snapshot_matches(&original));
|
|
}
|
|
|
|
state
|
|
.workspaces
|
|
.insert_window(WindowId(99), ferese_layout::Axis::Horizontal, 0.5)
|
|
.unwrap();
|
|
assert!(!state.shell_snapshot_matches(&original));
|
|
let occupied = state.shell_snapshot();
|
|
assert!(state.shell_snapshot_matches(&occupied));
|
|
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));
|
|
let assigned = state.shell_snapshot();
|
|
state.output_workspaces.switch_workspace(output_id, next).unwrap();
|
|
assert!(!state.shell_snapshot_matches(&assigned));
|
|
let selected = state.shell_snapshot();
|
|
assert!(state.shell_snapshot_matches(&selected));
|
|
state.unregister_output(&output);
|
|
assert!(!state.shell_snapshot_matches(&selected));
|
|
}
|
|
|
|
#[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 {
|
|
outputs: vec![OutputSnapshot {
|
|
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,
|
|
}],
|
|
};
|
|
assert_eq!(original, original.clone());
|
|
let changes: &[fn(&mut ShellSnapshot)] = &[
|
|
|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));
|
|
}
|
|
}
|
|
}
|