Skip to content
Portal Control Protocol

Application

Every running process with a Wayland connection to the compositor gets an Application primitive. This is the top-level container for all PCP data about a process: its identity, its capability tier, the surfaces it owns, and its current runtime state.

The Application primitive exists for the lifetime of the process. When a process connects to the compositor, it appears. When it disconnects, the primitive is removed.

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Application",
"type": "object",
"required": ["id", "process", "surface_tree", "capability_tier", "state"],
"properties": {
"id": {
"type": "string",
"pattern": "^app:wl-[0-9]+$",
"description": "Unique identifier, stable while the app is running. Derived from Wayland client ID."
},
"desktop_id": {
"type": ["string", "null"],
"description": "From .desktop file (best effort). E.g., 'org.gnome.Evolution'"
},
"display_name": {
"type": ["string", "null"],
"description": "Human-readable app name from .desktop file Name key."
},
"icon_name": {
"type": ["string", "null"],
"description": "Icon name from .desktop file Icon key."
},
"process": {
"type": "object",
"required": ["pid", "executable"],
"properties": {
"pid": { "type": "integer", "description": "Process ID" },
"executable": { "type": "string", "description": "Absolute path to executable" },
"cmdline": {
"type": "array",
"items": { "type": "string" },
"description": "Full command line arguments"
},
"cgroup": {
"type": ["string", "null"],
"description": "Cgroup path, useful for identifying sandboxed apps (Flatpak, etc.)"
},
"sandbox": {
"type": ["string", "null"],
"enum": [null, "flatpak", "snap", "bubblewrap", "firejail"],
"description": "Sandbox technology, if any."
}
}
},
"surface_tree": {
"type": "array",
"items": { "type": "string", "pattern": "^surf:[a-z0-9-]+$" },
"description": "IDs of top-level surfaces (windows) owned by this app."
},
"capability_tier": {
"type": "string",
"enum": ["native", "atspi2", "structural", "minimal"],
"description": "Deterministic tier based on probing."
},
"capability_manifest": {
"type": ["object", "null"],
"description": "For Tier 1 (native) apps only. Full CapabilityManifest."
},
"derived_capabilities": {
"type": ["array", "null"],
"items": { "$ref": "Capability" },
"description": "For Tier 2 apps only. Capabilities derived by the adapter."
},
"state": {
"type": "object",
"required": ["focused", "has_visible_surface", "started_at"],
"properties": {
"focused": { "type": "boolean", "description": "True if any surface from this app has keyboard focus." },
"has_visible_surface": { "type": "boolean", "description": "True if at least one surface is visible (not minimized/occluded on all outputs)." },
"started_at": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp when the app connected to the compositor." },
"unresponsive": { "type": "boolean", "default": false, "description": "True if the app has not responded to pings within the timeout period." },
"unresponsive_since": { "type": ["string", "null"], "format": "date-time" }
}
}
}
}
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Application {
pub id: AppId,
pub desktop_id: Option<String>,
pub display_name: Option<String>,
pub icon_name: Option<String>,
pub process: ProcessInfo,
pub surface_tree: Vec<SurfaceId>,
pub capability_tier: CapabilityTier,
pub capability_manifest: Option<CapabilityManifest>,
pub derived_capabilities: Option<Vec<DerivedCapability>>,
pub state: AppState,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AppId(pub String);
// Format: "app:wl-{wayland_client_id}"
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessInfo {
pub pid: u32,
pub executable: String,
pub cmdline: Vec<String>,
pub cgroup: Option<String>,
pub sandbox: Option<SandboxType>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SandboxType {
Flatpak,
Snap,
Bubblewrap,
Firejail,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum CapabilityTier {
/// App with shipped CapabilityManifest
Native,
/// Standard toolkit app with full AT-SPI2 accessibility tree
Atspi2,
/// Partial AT-SPI2 (window frame + basic widgets, e.g., Wine)
Structural,
/// Compositor surface metadata only, no accessibility tree
Minimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppState {
pub focused: bool,
pub has_visible_surface: bool,
pub started_at: DateTime<Utc>,
pub unresponsive: bool,
pub unresponsive_since: Option<DateTime<Utc>>,
}

Format app:wl-{N} where N is the Wayland client ID. Stable for the lifetime of the process. If the same app restarts, it gets a new ID.

The cgroup field helps identify sandboxed applications. The sandbox enum covers the four common Linux sandboxing technologies. When sandbox is null, the process runs unsandboxed.

The tier is determined automatically by the compositor at startup. It is not declared by the app (except Tier 1, which requires opting in via a manifest). The four tiers, from highest capability to lowest:

Tier Value Description
1 native Ships a CapabilityManifest, implements the PCP server trait. Full semantic capabilities with typed parameters and returns.
2 atspi2 Standard toolkit app with a full AT-SPI2 accessibility tree. No developer effort required.
2b structural Partial AT-SPI2 support (window frame and basic widgets). Typical of Wine applications.
3 minimal Compositor surface metadata only. No accessibility tree. Input simulation is the only interaction path.

The compositor maintains the state object in real time. focused flips whenever keyboard focus moves to or from one of the app’s surfaces. has_visible_surface accounts for minimization, occlusion, and output assignment. The unresponsive flag is set when the app fails to respond to Wayland pings within the configured timeout.

{
"id": "app:wl-47",
"desktop_id": "org.gnome.Evolution",
"display_name": "Evolution Mail",
"icon_name": "evolution-mail",
"process": {
"pid": 12345,
"executable": "/usr/bin/evolution",
"cmdline": ["evolution", "--component=mail"],
"cgroup": null,
"sandbox": null
},
"surface_tree": ["surf:47-w1"],
"capability_tier": "atspi2",
"capability_manifest": null,
"derived_capabilities": [
{
"id": "mail.search",
"name": "Search Mail",
"description": "Search across mailboxes using the search bar",
"category": "TEXT_QUERY",
"confidence": "high",
"source_element": "atspi:47-w1-entry-search",
"adapter_id": "email-client-v1"
},
{
"id": "mail.compose",
"name": "Compose New Email",
"description": "Open the compose window",
"category": "ELEMENT_INTERACTION",
"confidence": "high",
"source_element": "atspi:47-w1-btn-compose",
"adapter_id": "email-client-v1"
}
],
"state": {
"focused": true,
"has_visible_surface": true,
"started_at": "2026-05-08T14:00:00Z",
"unresponsive": false,
"unresponsive_since": null
}
}

This example shows a Tier 2 application. The capability_manifest is null because Evolution does not ship a manifest. Instead, the PCP adapter derived two capabilities (mail.search and mail.compose) by inspecting the AT-SPI2 accessibility tree. Each derived capability references the element it was discovered from (source_element) and the adapter that produced it.

Last updated: