feat: add private shell control protocol
This commit is contained in:
1 parent
a836b55eb9
commit
75ae44c1d7
12 files changed
+727
No files matched your search
Generated
+9
@@ -1630,12 +1630,21 @@ version = "0.1.0"
|
|||||||
name = "ferese-protocols"
|
name = "ferese-protocols"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.13.2",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-scanner",
|
"wayland-scanner",
|
||||||
"wayland-server",
|
"wayland-server",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ferese-shell-control-probe"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"ferese-protocols",
|
||||||
|
"wayland-client",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ferese-shell-spike"
|
name = "ferese-shell-spike"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ members = [
|
|||||||
"crates/ferese-protocols",
|
"crates/ferese-protocols",
|
||||||
"tools/ferese-effects-probe",
|
"tools/ferese-effects-probe",
|
||||||
"tools/ferese-malformed-client",
|
"tools/ferese-malformed-client",
|
||||||
|
"tools/ferese-shell-control-probe",
|
||||||
"tools/ferese-shell-spike",
|
"tools/ferese-shell-spike",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
@@ -20,6 +21,7 @@ license = "MIT"
|
|||||||
rust-version = "1.87"
|
rust-version = "1.87"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
bitflags = "2.9.4"
|
||||||
calloop = { version = "0.14.4", features = ["signals"] }
|
calloop = { version = "0.14.4", features = ["signals"] }
|
||||||
libc = "0.2.176"
|
libc = "0.2.176"
|
||||||
smithay = { version = "0.7.0", default-features = false, features = [
|
smithay = { version = "0.7.0", default-features = false, features = [
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ impl LaunchConfig {
|
|||||||
client_capabilities.insert(ClientCapabilities::EFFECTS);
|
client_capabilities.insert(ClientCapabilities::EFFECTS);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if argument == "--grant-shell-control" {
|
||||||
|
client_capabilities.insert(ClientCapabilities::SHELL_CONTROL);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Some(value) = argument
|
if let Some(value) = argument
|
||||||
.to_str()
|
.to_str()
|
||||||
.and_then(|argument| argument.strip_prefix("--backend="))
|
.and_then(|argument| argument.strip_prefix("--backend="))
|
||||||
@@ -164,6 +168,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicitly_grants_shell_control_to_the_launched_private_client() {
|
||||||
|
let config = LaunchConfig::parse(
|
||||||
|
[
|
||||||
|
OsString::from("--grant-shell-control"),
|
||||||
|
OsString::from("ferese-shell"),
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(config.client, [OsString::from("ferese-shell")]);
|
||||||
|
assert!(
|
||||||
|
config
|
||||||
|
.client_capabilities
|
||||||
|
.contains(ClientCapabilities::SHELL_CONTROL)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!config
|
||||||
|
.client_capabilities
|
||||||
|
.contains(ClientCapabilities::EFFECTS)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_an_unknown_backend() {
|
fn rejects_an_unknown_backend() {
|
||||||
let error =
|
let error =
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ mod input;
|
|||||||
mod ipc;
|
mod ipc;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod private_client;
|
mod private_client;
|
||||||
|
mod shell_control;
|
||||||
mod stacking;
|
mod stacking;
|
||||||
mod state;
|
mod state;
|
||||||
mod window_rules;
|
mod window_rules;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub(crate) struct ClientCapabilities(u8);
|
|||||||
|
|
||||||
impl ClientCapabilities {
|
impl ClientCapabilities {
|
||||||
pub(crate) const EFFECTS: Self = Self(1 << 0);
|
pub(crate) const EFFECTS: Self = Self(1 << 0);
|
||||||
|
pub(crate) const SHELL_CONTROL: Self = Self(1 << 1);
|
||||||
|
|
||||||
pub(crate) fn contains(self, capability: Self) -> bool {
|
pub(crate) fn contains(self, capability: Self) -> bool {
|
||||||
self.0 & capability.0 == capability.0
|
self.0 & capability.0 == capability.0
|
||||||
@@ -69,9 +70,15 @@ mod tests {
|
|||||||
let mut capabilities = ClientCapabilities::default();
|
let mut capabilities = ClientCapabilities::default();
|
||||||
assert!(capabilities.is_empty());
|
assert!(capabilities.is_empty());
|
||||||
assert!(!capabilities.contains(ClientCapabilities::EFFECTS));
|
assert!(!capabilities.contains(ClientCapabilities::EFFECTS));
|
||||||
|
assert!(!capabilities.contains(ClientCapabilities::SHELL_CONTROL));
|
||||||
|
|
||||||
capabilities.insert(ClientCapabilities::EFFECTS);
|
capabilities.insert(ClientCapabilities::EFFECTS);
|
||||||
assert!(!capabilities.is_empty());
|
assert!(!capabilities.is_empty());
|
||||||
assert!(capabilities.contains(ClientCapabilities::EFFECTS));
|
assert!(capabilities.contains(ClientCapabilities::EFFECTS));
|
||||||
|
assert!(!capabilities.contains(ClientCapabilities::SHELL_CONTROL));
|
||||||
|
|
||||||
|
capabilities.insert(ClientCapabilities::SHELL_CONTROL);
|
||||||
|
assert!(capabilities.contains(ClientCapabilities::EFFECTS));
|
||||||
|
assert!(capabilities.contains(ClientCapabilities::SHELL_CONTROL));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
use ferese_layout::WindowId;
|
||||||
|
use ferese_protocols::shell::v1::server::{
|
||||||
|
ferese_shell_manager_v1::{self, FereseShellManagerV1},
|
||||||
|
ferese_shell_v1::{self, FereseShellV1},
|
||||||
|
};
|
||||||
|
use smithay::{
|
||||||
|
reexports::wayland_server::{
|
||||||
|
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||||
|
},
|
||||||
|
wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{Ferese, private_client::ClientCapabilities, state::ClientState};
|
||||||
|
|
||||||
|
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||||
|
display.create_global::<Ferese, FereseShellManagerV1, _>(1, ());
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
shell.overview_state(u32::from(state.overview_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>,
|
||||||
|
) {
|
||||||
|
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::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.activate_managed_window(id) {
|
||||||
|
state.set_overview_active(false);
|
||||||
|
} else {
|
||||||
|
send_request_failed(
|
||||||
|
shell,
|
||||||
|
ferese_shell_v1::FailedRequest::SelectOverviewWindow,
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ferese_shell_v1::Request::Destroy => {}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ferese {
|
||||||
|
pub(crate) fn send_shell_snapshots(&mut self) {
|
||||||
|
let resources = std::mem::take(&mut self.shell_resources);
|
||||||
|
|
||||||
|
for weak in resources {
|
||||||
|
if let Ok(shell) = weak.upgrade() {
|
||||||
|
self.send_shell_snapshot(&shell);
|
||||||
|
self.shell_resources.push(shell.downgrade());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_shell_snapshot(&mut self, shell: &FereseShellV1) {
|
||||||
|
self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1);
|
||||||
|
let serial = self.shell_snapshot_serial;
|
||||||
|
let snapshots = self.managed_window_snapshots();
|
||||||
|
|
||||||
|
shell.snapshot_begin(serial);
|
||||||
|
for snapshot in snapshots {
|
||||||
|
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,
|
||||||
|
snapshot.title,
|
||||||
|
snapshot.state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
shell.snapshot_end(serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> {
|
||||||
|
let mut snapshots = self
|
||||||
|
.window_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(window, id)| {
|
||||||
|
let workspace = self.workspaces.workspace_for_window(*id)?.0;
|
||||||
|
let toplevel = window.toplevel()?;
|
||||||
|
let (app_id, title) = with_states(toplevel.wl_surface(), |states| {
|
||||||
|
let attributes = states
|
||||||
|
.data_map
|
||||||
|
.get::<XdgToplevelSurfaceData>()
|
||||||
|
.expect("xdg toplevel state exists")
|
||||||
|
.lock()
|
||||||
|
.expect("xdg toplevel state is not poisoned");
|
||||||
|
|
||||||
|
(
|
||||||
|
attributes.app_id.clone().unwrap_or_default(),
|
||||||
|
attributes.title.clone().unwrap_or_default(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let mut state = ferese_shell_v1::WindowState::empty();
|
||||||
|
|
||||||
|
if self.focused_window == Some(*id) {
|
||||||
|
state |= ferese_shell_v1::WindowState::Focused;
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.workspaces
|
||||||
|
.workspace_for_window(*id)
|
||||||
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
|
.is_some_and(|workspace| workspace.fullscreen == Some(*id))
|
||||||
|
{
|
||||||
|
state |= ferese_shell_v1::WindowState::Fullscreen;
|
||||||
|
}
|
||||||
|
if self.is_floating_window(window) {
|
||||||
|
state |= ferese_shell_v1::WindowState::Floating;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(ManagedWindowSnapshot {
|
||||||
|
id: *id,
|
||||||
|
workspace,
|
||||||
|
app_id,
|
||||||
|
title,
|
||||||
|
state,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
snapshots.sort_by_key(|snapshot| snapshot.id.0);
|
||||||
|
snapshots
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_overview_active(&mut self, active: bool) {
|
||||||
|
if self.overview_active == active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.overview_active = active;
|
||||||
|
self.shell_resources.retain(|resource| {
|
||||||
|
resource.upgrade().is_ok_and(|shell| {
|
||||||
|
shell.overview_state(u32::from(active));
|
||||||
|
true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
crate::backends::direct::render_all(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum ShellWindowAction {
|
||||||
|
Activate,
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ManagedWindowSnapshot {
|
||||||
|
id: WindowId,
|
||||||
|
workspace: u64,
|
||||||
|
app_id: String,
|
||||||
|
title: String,
|
||||||
|
state: ferese_shell_v1::WindowState,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_id(id: u64) -> (u32, u32) {
|
||||||
|
((id >> 32) as u32, id as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_id(hi: u32, lo: u32) -> WindowId {
|
||||||
|
WindowId((u64::from(hi) << 32) | u64::from(lo))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_request_failed(
|
||||||
|
shell: &FereseShellV1,
|
||||||
|
request: ferese_shell_v1::FailedRequest,
|
||||||
|
id: WindowId,
|
||||||
|
) {
|
||||||
|
let (window_hi, window_lo) = split_id(id.0);
|
||||||
|
|
||||||
|
shell.request_failed(
|
||||||
|
request,
|
||||||
|
window_hi,
|
||||||
|
window_lo,
|
||||||
|
"unknown or unavailable managed window".to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -154,6 +154,13 @@ pub struct Ferese {
|
|||||||
_ipc_socket: Option<crate::ipc::IpcSocketGuard>,
|
_ipc_socket: Option<crate::ipc::IpcSocketGuard>,
|
||||||
pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>,
|
pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>,
|
||||||
pub(crate) pending_screencopies: Vec<crate::handlers::screencopy::PendingScreencopy>,
|
pub(crate) pending_screencopies: Vec<crate::handlers::screencopy::PendingScreencopy>,
|
||||||
|
pub(crate) shell_resources: Vec<
|
||||||
|
smithay::reexports::wayland_server::Weak<
|
||||||
|
ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
pub(crate) shell_snapshot_serial: u32,
|
||||||
|
pub(crate) overview_active: bool,
|
||||||
next_window_id: u64,
|
next_window_id: u64,
|
||||||
next_output_id: u64,
|
next_output_id: u64,
|
||||||
last_animation_tick: Instant,
|
last_animation_tick: Instant,
|
||||||
@@ -360,6 +367,7 @@ impl Ferese {
|
|||||||
let display_handle = display.handle();
|
let display_handle = display.handle();
|
||||||
crate::handlers::screencopy::init_global(&display_handle);
|
crate::handlers::screencopy::init_global(&display_handle);
|
||||||
crate::effects::init_global(&display_handle);
|
crate::effects::init_global(&display_handle);
|
||||||
|
crate::shell_control::init_global(&display_handle);
|
||||||
let alpha_modifier_state = AlphaModifierState::new::<Self>(&display_handle);
|
let alpha_modifier_state = AlphaModifierState::new::<Self>(&display_handle);
|
||||||
let compositor_state = CompositorState::new::<Self>(&display_handle);
|
let compositor_state = CompositorState::new::<Self>(&display_handle);
|
||||||
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
|
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
|
||||||
@@ -470,6 +478,9 @@ impl Ferese {
|
|||||||
_ipc_socket: None,
|
_ipc_socket: None,
|
||||||
pending_dmabuf_imports: Vec::new(),
|
pending_dmabuf_imports: Vec::new(),
|
||||||
pending_screencopies: Vec::new(),
|
pending_screencopies: Vec::new(),
|
||||||
|
shell_resources: Vec::new(),
|
||||||
|
shell_snapshot_serial: 0,
|
||||||
|
overview_active: false,
|
||||||
next_window_id: 1,
|
next_window_id: 1,
|
||||||
next_output_id: 1,
|
next_output_id: 1,
|
||||||
last_animation_tick: start_time,
|
last_animation_tick: start_time,
|
||||||
@@ -1132,6 +1143,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
self.scrolling_world_x = scrolling_world_x;
|
self.scrolling_world_x = scrolling_world_x;
|
||||||
self.sync_window_stacking();
|
self.sync_window_stacking();
|
||||||
|
self.send_shell_snapshots();
|
||||||
|
|
||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
}
|
}
|
||||||
@@ -1570,6 +1582,59 @@ impl Ferese {
|
|||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
|
||||||
|
if !self.window_ids.values().any(|window_id| *window_id == id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.animations_enabled {
|
||||||
|
self.closing_windows.entry(id).or_default();
|
||||||
|
crate::backends::direct::render_all(self);
|
||||||
|
} else {
|
||||||
|
self.send_window_close(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool {
|
||||||
|
let Some(window) = self
|
||||||
|
.window_ids
|
||||||
|
.iter()
|
||||||
|
.find_map(|(window, window_id)| (*window_id == id).then(|| window.clone()))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(workspace) = self.workspaces.workspace_for_window(id) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(output) = self.output_workspaces.output_for_workspace(workspace) {
|
||||||
|
self.activate_output_workspace(output, workspace);
|
||||||
|
} else if let Some(output) = self.output_workspaces.focused_output() {
|
||||||
|
if self
|
||||||
|
.output_workspaces
|
||||||
|
.assign_workspace(output, workspace)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.activate_output_workspace(output, workspace);
|
||||||
|
} else if self.workspaces.activate(workspace).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.workspaces.focus_window(id).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.focused_window = Some(id);
|
||||||
|
self.raise_window(&window, true);
|
||||||
|
self.relayout();
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn closing_visual(&self, id: WindowId) -> (f64, f32) {
|
pub(crate) fn closing_visual(&self, id: WindowId) -> (f64, f32) {
|
||||||
let Some(animation) = self.closing_windows.get(&id) else {
|
let Some(animation) = self.closing_windows.get(&id) else {
|
||||||
return (1.0, 1.0);
|
return (1.0, 1.0);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ client = ["dep:wayland-client"]
|
|||||||
server = ["dep:wayland-server"]
|
server = ["dep:wayland-server"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bitflags.workspace = true
|
||||||
wayland-backend.workspace = true
|
wayland-backend.workspace = true
|
||||||
wayland-client = { workspace = true, optional = true }
|
wayland-client = { workspace = true, optional = true }
|
||||||
wayland-scanner.workspace = true
|
wayland-scanner.workspace = true
|
||||||
|
|||||||
@@ -55,3 +55,57 @@ pub mod effects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Private shell control and managed-window metadata protocol.
|
||||||
|
pub mod shell {
|
||||||
|
/// Version 1 of the Ferese shell control protocol.
|
||||||
|
pub mod v1 {
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
pub use generated::client;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub use generated::server;
|
||||||
|
|
||||||
|
mod generated {
|
||||||
|
#![allow(
|
||||||
|
dead_code,
|
||||||
|
non_camel_case_types,
|
||||||
|
non_upper_case_globals,
|
||||||
|
non_snake_case,
|
||||||
|
unused_imports,
|
||||||
|
unused_unsafe,
|
||||||
|
unused_variables,
|
||||||
|
clippy::all
|
||||||
|
)]
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
pub mod client {
|
||||||
|
use wayland_client;
|
||||||
|
use wayland_client::protocol::*;
|
||||||
|
|
||||||
|
pub mod __interfaces {
|
||||||
|
use wayland_client::protocol::__interfaces::*;
|
||||||
|
|
||||||
|
wayland_scanner::generate_interfaces!("../../protocols/ferese-shell-v1.xml");
|
||||||
|
}
|
||||||
|
use self::__interfaces::*;
|
||||||
|
|
||||||
|
wayland_scanner::generate_client_code!("../../protocols/ferese-shell-v1.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub mod server {
|
||||||
|
use wayland_server;
|
||||||
|
use wayland_server::protocol::*;
|
||||||
|
|
||||||
|
pub mod __interfaces {
|
||||||
|
use wayland_server::protocol::__interfaces::*;
|
||||||
|
|
||||||
|
wayland_scanner::generate_interfaces!("../../protocols/ferese-shell-v1.xml");
|
||||||
|
}
|
||||||
|
use self::__interfaces::*;
|
||||||
|
|
||||||
|
wayland_scanner::generate_server_code!("../../protocols/ferese-shell-v1.xml");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="ferese_shell_v1">
|
||||||
|
<copyright>
|
||||||
|
Copyright 2026 Ferese contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
</copyright>
|
||||||
|
|
||||||
|
<interface name="ferese_shell_manager_v1" version="1">
|
||||||
|
<description summary="create a capability-scoped Ferese shell controller">
|
||||||
|
This private global is visible only to clients granted the shell-control
|
||||||
|
capability. It does not assign a Wayland surface role.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="get_shell">
|
||||||
|
<arg name="id" type="new_id" interface="ferese_shell_v1"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor"/>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="ferese_shell_v1" version="1">
|
||||||
|
<description summary="managed-window metadata and shell control">
|
||||||
|
Window and workspace identifiers are unsigned 64-bit values encoded as
|
||||||
|
high and low 32-bit words. Each snapshot atomically replaces the prior
|
||||||
|
managed-window view after snapshot_end.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="activate_window">
|
||||||
|
<arg name="window_hi" type="uint"/>
|
||||||
|
<arg name="window_lo" type="uint"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="close_window">
|
||||||
|
<arg name="window_hi" type="uint"/>
|
||||||
|
<arg name="window_lo" type="uint"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="enter_overview"/>
|
||||||
|
<request name="exit_overview"/>
|
||||||
|
|
||||||
|
<request name="select_overview_window">
|
||||||
|
<arg name="window_hi" type="uint"/>
|
||||||
|
<arg name="window_lo" type="uint"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor"/>
|
||||||
|
|
||||||
|
<enum name="window_state" bitfield="true">
|
||||||
|
<entry name="focused" value="1"/>
|
||||||
|
<entry name="urgent" value="2"/>
|
||||||
|
<entry name="fullscreen" value="4"/>
|
||||||
|
<entry name="floating" value="8"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<enum name="failed_request">
|
||||||
|
<entry name="activate_window" value="0"/>
|
||||||
|
<entry name="close_window" value="1"/>
|
||||||
|
<entry name="select_overview_window" value="2"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<event name="snapshot_begin">
|
||||||
|
<arg name="serial" type="uint"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="window">
|
||||||
|
<arg name="window_hi" type="uint"/>
|
||||||
|
<arg name="window_lo" type="uint"/>
|
||||||
|
<arg name="workspace_hi" type="uint"/>
|
||||||
|
<arg name="workspace_lo" type="uint"/>
|
||||||
|
<arg name="app_id" type="string"/>
|
||||||
|
<arg name="title" type="string"/>
|
||||||
|
<arg name="state" type="uint" enum="window_state"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="snapshot_end">
|
||||||
|
<arg name="serial" type="uint"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="overview_state">
|
||||||
|
<arg name="active" type="uint" summary="one when overview is active"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="request_failed">
|
||||||
|
<arg name="request" type="uint" enum="failed_request"/>
|
||||||
|
<arg name="window_hi" type="uint"/>
|
||||||
|
<arg name="window_lo" type="uint"/>
|
||||||
|
<arg name="reason" type="string"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="capabilities">
|
||||||
|
<description summary="marks completion of initial protocol setup"/>
|
||||||
|
<arg name="version" type="uint"/>
|
||||||
|
</event>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "ferese-shell-control-probe"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] }
|
||||||
|
wayland-client.workspace = true
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use ferese_protocols::shell::v1::client::{
|
||||||
|
ferese_shell_manager_v1::FereseShellManagerV1,
|
||||||
|
ferese_shell_v1::{self, FereseShellV1},
|
||||||
|
};
|
||||||
|
use wayland_client::{
|
||||||
|
Connection, Dispatch, QueueHandle, WEnum, delegate_noop, protocol::wl_registry,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let expect_hidden = std::env::args_os().any(|argument| argument == "--expect-hidden");
|
||||||
|
let connection = Connection::connect_to_env()?;
|
||||||
|
let mut queue = connection.new_event_queue();
|
||||||
|
let qh = queue.handle();
|
||||||
|
connection.display().get_registry(&qh, ());
|
||||||
|
let mut state = ProbeState::default();
|
||||||
|
|
||||||
|
queue.roundtrip(&mut state)?;
|
||||||
|
if expect_hidden {
|
||||||
|
if state.manager.is_some() {
|
||||||
|
return Err("public client could see ferese_shell_manager_v1".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("PASS public client cannot see ferese_shell_manager_v1");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let manager = state
|
||||||
|
.manager
|
||||||
|
.clone()
|
||||||
|
.ok_or("trusted client could not see ferese_shell_manager_v1")?;
|
||||||
|
state.shell = Some(manager.get_shell(&qh, ()));
|
||||||
|
|
||||||
|
while !state.initialized() {
|
||||||
|
queue.blocking_dispatch(&mut state)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shell = state.shell.clone().expect("shell initialized");
|
||||||
|
shell.enter_overview();
|
||||||
|
while state.overview_active != Some(true) {
|
||||||
|
queue.blocking_dispatch(&mut state)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.exit_overview();
|
||||||
|
while state.overview_active != Some(false) || state.overview_events < 3 {
|
||||||
|
queue.blocking_dispatch(&mut state)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.activate_window(u32::MAX, u32::MAX);
|
||||||
|
while !state.invalid_window_rejected {
|
||||||
|
queue.blocking_dispatch(&mut state)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"PASS shell-control snapshot={} windows={} overview round-trip and stale-id rejection",
|
||||||
|
state.snapshot_serial.unwrap_or_default(),
|
||||||
|
state.window_count
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ProbeState {
|
||||||
|
manager: Option<FereseShellManagerV1>,
|
||||||
|
shell: Option<FereseShellV1>,
|
||||||
|
capabilities: Option<u32>,
|
||||||
|
snapshot_serial: Option<u32>,
|
||||||
|
snapshot_complete: bool,
|
||||||
|
window_count: usize,
|
||||||
|
overview_active: Option<bool>,
|
||||||
|
overview_events: usize,
|
||||||
|
invalid_window_rejected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProbeState {
|
||||||
|
fn initialized(&self) -> bool {
|
||||||
|
self.capabilities == Some(1)
|
||||||
|
&& self.snapshot_complete
|
||||||
|
&& self.overview_active == Some(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<wl_registry::WlRegistry, ()> for ProbeState {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
registry: &wl_registry::WlRegistry,
|
||||||
|
event: wl_registry::Event,
|
||||||
|
_data: &(),
|
||||||
|
_connection: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
let wl_registry::Event::Global {
|
||||||
|
name,
|
||||||
|
interface,
|
||||||
|
version,
|
||||||
|
} = event
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if interface == "ferese_shell_manager_v1" {
|
||||||
|
state.manager = Some(registry.bind(name, version.min(1), qh, ()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<FereseShellV1, ()> for ProbeState {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
_shell: &FereseShellV1,
|
||||||
|
event: ferese_shell_v1::Event,
|
||||||
|
_data: &(),
|
||||||
|
_connection: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
ferese_shell_v1::Event::Capabilities { version } => {
|
||||||
|
state.capabilities = Some(version);
|
||||||
|
}
|
||||||
|
ferese_shell_v1::Event::SnapshotBegin { serial } => {
|
||||||
|
state.snapshot_serial = Some(serial);
|
||||||
|
state.snapshot_complete = false;
|
||||||
|
state.window_count = 0;
|
||||||
|
}
|
||||||
|
ferese_shell_v1::Event::Window { .. } => {
|
||||||
|
state.window_count += 1;
|
||||||
|
}
|
||||||
|
ferese_shell_v1::Event::SnapshotEnd { serial } => {
|
||||||
|
state.snapshot_complete = state.snapshot_serial == Some(serial);
|
||||||
|
}
|
||||||
|
ferese_shell_v1::Event::OverviewState { active } => {
|
||||||
|
state.overview_active = Some(active != 0);
|
||||||
|
state.overview_events += 1;
|
||||||
|
}
|
||||||
|
ferese_shell_v1::Event::RequestFailed {
|
||||||
|
request,
|
||||||
|
window_hi,
|
||||||
|
window_lo,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
state.invalid_window_rejected = request
|
||||||
|
== WEnum::Value(ferese_shell_v1::FailedRequest::ActivateWindow)
|
||||||
|
&& window_hi == u32::MAX
|
||||||
|
&& window_lo == u32::MAX;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate_noop!(ProbeState: ignore FereseShellManagerV1);
|
||||||
Reference in new issue
Block a user