feat: expose shell workspace metadata

This commit is contained in:
Samuel Onoja committed 2026-09-25 06:50:26 +01:00
1 parent 3384cfde0f
commit 0c6fefacc1
5 files changed
+204 -27

No files matched your search

+114 -12
View File
@@ -1,3 +1,4 @@
use ferese_core::{OutputId, WorkspaceId};
use ferese_layout::WindowId;
use ferese_protocols::shell::v1::server::{
ferese_shell_manager_v1::{self, FereseShellManagerV1},
@@ -91,6 +92,20 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
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::EnterOverview => state.set_overview_active(true),
ferese_shell_v1::Request::ExitOverview => state.set_overview_active(false),
ferese_shell_v1::Request::SelectOverviewWindow {
@@ -103,7 +118,7 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
send_request_failed(
shell,
ferese_shell_v1::FailedRequest::SelectOverviewWindow,
id,
id.0,
);
}
}
@@ -128,9 +143,40 @@ impl Ferese {
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 outputs = self.output_snapshots();
let workspaces = self.workspace_snapshots();
let snapshots = self.managed_window_snapshots();
shell.snapshot_begin(serial);
for output in 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,
workspace_hi,
workspace_lo,
u32::from(output.focused),
);
}
for workspace in 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,
u32::from(workspace.active),
);
}
for snapshot in snapshots {
let (window_hi, window_lo) = split_id(snapshot.id.0);
let (workspace_hi, workspace_lo) = split_id(snapshot.workspace);
@@ -148,6 +194,48 @@ impl Ferese {
shell.snapshot_end(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> {
let mut workspaces = self
.workspaces
.iter()
.map(|workspace| {
let output = self.output_workspaces.output_for_workspace(workspace.id);
let active = output.is_some_and(|output| {
self.output_workspaces.active_workspace(output) == Some(workspace.id)
});
WorkspaceSnapshot {
id: workspace.id,
output,
name: workspace.name.clone(),
active,
}
})
.collect::<Vec<_>>();
workspaces.sort_by_key(|workspace| workspace.id.0);
workspaces
}
fn managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> {
let mut snapshots = self
.window_ids
@@ -212,7 +300,7 @@ impl Ferese {
};
if !succeeded {
send_request_failed(shell, request, id);
send_request_failed(shell, request, id.0);
}
}
@@ -242,26 +330,40 @@ struct ManagedWindowSnapshot {
state: ferese_shell_v1::WindowState,
}
struct OutputSnapshot {
id: OutputId,
name: String,
active_workspace: WorkspaceId,
focused: bool,
}
struct WorkspaceSnapshot {
id: WorkspaceId,
output: Option<OutputId>,
name: String,
active: bool,
}
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))
WindowId(join_u64(hi, lo))
}
fn send_request_failed(
shell: &FereseShellV1,
request: ferese_shell_v1::FailedRequest,
id: WindowId,
) {
let (window_hi, window_lo) = split_id(id.0);
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,
window_hi,
window_lo,
"unknown or unavailable managed window".to_owned(),
object_hi,
object_lo,
"unknown or unavailable shell object".to_owned(),
);
}
+38 -5
View File
@@ -114,7 +114,7 @@ pub struct Ferese {
pub space: Space<Window>,
pub workspaces: WorkspaceSet,
pub output_workspaces: OutputWorkspaceMap,
output_ids: HashMap<Output, OutputId>,
pub(crate) output_ids: HashMap<Output, OutputId>,
output_identity_ids: HashMap<String, OutputId>,
pub window_ids: HashMap<Window, WindowId>,
pub window_geometry: HashMap<WindowId, WindowGeometry>,
@@ -239,18 +239,20 @@ impl Ferese {
geometry.size.h,
);
match self
let registration = self
.output_workspaces
.connect(output_id, geometry, fallback_workspace)
{
.connect(output_id, geometry, fallback_workspace);
match registration {
Ok(workspace) => {
self.output_ids.insert(output.clone(), output_id);
if self.output_workspaces.focused_output() == Some(output_id) {
self.activate_output_workspace(output_id, workspace);
}
self.send_shell_snapshots();
}
Err(error) => {
tracing::error!(%error, output = %output.name(), "failed to register output")
tracing::error!(%error, output = %output.name(), "failed to register output");
}
}
}
@@ -1665,6 +1667,37 @@ impl Ferese {
true
}
pub(crate) fn activate_managed_workspace(&mut self, workspace: WorkspaceId) -> bool {
if self.workspaces.workspace(workspace).is_none() {
return false;
}
let output = self
.output_workspaces
.output_for_workspace(workspace)
.or_else(|| self.output_workspaces.focused_output());
let Some(output) = output else {
return false;
};
if self
.output_workspaces
.output_for_workspace(workspace)
.is_none()
&& self
.output_workspaces
.assign_workspace(output, workspace)
.is_err()
{
return false;
}
self.activate_output_workspace(output, workspace);
self.relayout();
self.restore_keyboard_focus();
true
}
pub(crate) fn closing_visual(&self, id: WindowId) -> (f64, f32) {
let Some(animation) = self.closing_windows.get(&id) else {
return (1.0, 1.0);
+4
View File
@@ -0,0 +1,4 @@
fn main() {
println!("cargo::rerun-if-changed=../../protocols/ferese-effects-v1.xml");
println!("cargo::rerun-if-changed=../../protocols/ferese-shell-v1.xml");
}
+29 -5
View File
@@ -37,9 +37,9 @@
<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.
Window, workspace, and output identifiers are unsigned 64-bit values
encoded as high and low 32-bit words. Each snapshot atomically replaces
the prior shell view after snapshot_end.
</description>
<request name="activate_window">
@@ -52,6 +52,11 @@
<arg name="window_lo" type="uint"/>
</request>
<request name="activate_workspace">
<arg name="workspace_hi" type="uint"/>
<arg name="workspace_lo" type="uint"/>
</request>
<request name="enter_overview"/>
<request name="exit_overview"/>
@@ -73,6 +78,7 @@
<entry name="activate_window" value="0"/>
<entry name="close_window" value="1"/>
<entry name="select_overview_window" value="2"/>
<entry name="activate_workspace" value="3"/>
</enum>
<event name="snapshot_begin">
@@ -89,6 +95,24 @@
<arg name="state" type="uint" enum="window_state"/>
</event>
<event name="output">
<arg name="output_hi" type="uint"/>
<arg name="output_lo" type="uint"/>
<arg name="name" type="string"/>
<arg name="active_workspace_hi" type="uint"/>
<arg name="active_workspace_lo" type="uint"/>
<arg name="focused" type="uint" summary="one when this is the focused output"/>
</event>
<event name="workspace">
<arg name="workspace_hi" type="uint"/>
<arg name="workspace_lo" type="uint"/>
<arg name="output_hi" type="uint" summary="zero when the workspace has no output"/>
<arg name="output_lo" type="uint" summary="zero when the workspace has no output"/>
<arg name="name" type="string"/>
<arg name="active" type="uint" summary="one when visible on its output"/>
</event>
<event name="snapshot_end">
<arg name="serial" type="uint"/>
</event>
@@ -99,8 +123,8 @@
<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="object_hi" type="uint"/>
<arg name="object_lo" type="uint"/>
<arg name="reason" type="string"/>
</event>
+19 -5
View File
@@ -53,8 +53,10 @@ fn main() -> Result<(), Box<dyn Error>> {
}
println!(
"PASS shell-control snapshot={} windows={} overview round-trip and stale-id rejection",
"PASS shell-control snapshot={} outputs={} workspaces={} windows={} overview round-trip and stale-id rejection",
state.snapshot_serial.unwrap_or_default(),
state.output_count,
state.workspace_count,
state.window_count
);
Ok(())
@@ -68,6 +70,8 @@ struct ProbeState {
snapshot_serial: Option<u32>,
snapshot_complete: bool,
window_count: usize,
output_count: usize,
workspace_count: usize,
overview_active: Option<bool>,
overview_events: usize,
invalid_window_rejected: bool,
@@ -77,6 +81,8 @@ impl ProbeState {
fn initialized(&self) -> bool {
self.capabilities == Some(1)
&& self.snapshot_complete
&& self.output_count > 0
&& self.workspace_count > 0
&& self.overview_active == Some(false)
}
}
@@ -122,10 +128,18 @@ impl Dispatch<FereseShellV1, ()> for ProbeState {
state.snapshot_serial = Some(serial);
state.snapshot_complete = false;
state.window_count = 0;
state.output_count = 0;
state.workspace_count = 0;
}
ferese_shell_v1::Event::Window { .. } => {
state.window_count += 1;
}
ferese_shell_v1::Event::Output { .. } => {
state.output_count += 1;
}
ferese_shell_v1::Event::Workspace { .. } => {
state.workspace_count += 1;
}
ferese_shell_v1::Event::SnapshotEnd { serial } => {
state.snapshot_complete = state.snapshot_serial == Some(serial);
}
@@ -135,14 +149,14 @@ impl Dispatch<FereseShellV1, ()> for ProbeState {
}
ferese_shell_v1::Event::RequestFailed {
request,
window_hi,
window_lo,
object_hi,
object_lo,
..
} => {
state.invalid_window_rejected = request
== WEnum::Value(ferese_shell_v1::FailedRequest::ActivateWindow)
&& window_hi == u32::MAX
&& window_lo == u32::MAX;
&& object_hi == u32::MAX
&& object_lo == u32::MAX;
}
_ => {}
}