-Ferese combines a scrolling window manager with its own menu bar, control center
-and native Settings app. Built in Rust on Smithay, it brings configurable motion
-and a cohesive desktop interface to a keyboard-driven workflow.
+Ferese is built in Rust on Smithay, with its own menu bar, control center
+and native Settings app.
-## A desktop that moves with you
+## Features
-- **Scrolling columns:** a horizontal window strip with configurable widths and
- focus behavior. Tree tiling is available per workspace.
-- **Coordinated motion:** animated focus, resize, workspace transitions and
- overview, with adjustable speed and reduced-motion support.
-- **An integrated shell:** workspace selector, status popovers, notification
- controls and a compact control center.
-- **Native Settings:** appearance, wallpaper, input, motion and more, with
- automatic saving, validation and undo.
-- **Your own look:** solid or translucent shell surfaces, backdrop blur,
- rounded corners, soft shadows and optional gradient focus rings.
-- **Multiple displays:** per-output workspaces, fractional scaling, monitor
- hotplug and laptop-lid workspace migration.
-- **Live configuration:** update TOML without restarting the desktop.
- Invalid changes keep the last working configuration.
-- **Session locking:** `ext-session-lock-v1` with swaylock or swaylock-effects
- through `ferese-lock`.
+- Scrolling columns, tree tiling and floating windows
+- Workspace overview
+- Configurable animations
+- Built-in bar and control center
+- Native Settings app
+- Desktop widgets
+- Themes, blur and gradient borders
+- Multi-monitor support and fractional scaling
+- Live configuration reload
+- Background services and session locking
## Try it
@@ -46,9 +39,17 @@ target/release/ferese --backend nested --grant-effects --grant-shell-control --
target/release/ferese-shell
```
-Use a recent Rust toolchain (1.93 or newer for the shell), Smithay's native
-build dependencies, and `foot` for the default terminal. Settings wallpaper
-browsing uses `zenity`; entering an image path works without it.
+Requirements (Fedora package names):
+
+- Rust 1.93+ and Cargo.
+- Build tools: `gcc`, `gcc-c++`, `cmake`, `make`, `pkgconf-pkg-config`.
+- Wayland and input: `wayland-devel`, `libxkbcommon-devel`, `libinput-devel`.
+- Graphics: `libdrm-devel`, `mesa-libgbm-devel`, `mesa-libEGL-devel`.
+- Session and fonts: `systemd-devel`, `libseat-devel`, `fontconfig-devel`.
+- Installation: `desktop-file-utils`, `dbus`.
+- Default terminal: `foot` (or configure another terminal).
+- Optional: `zenity` for wallpaper browsing, `swayidle` for idle actions,
+ and `swaylock` or `swaylock-effects` for locking.
For a login-screen session alongside your existing desktop:
@@ -64,7 +65,7 @@ session logs and rollback.
## Get around
-These are built-in defaults; your configuration can override them.
+Built-in defaults (your configuration can override them):
| Shortcut | Action |
| --- | --- |
@@ -82,22 +83,9 @@ These are built-in defaults; your configuration can override them.
Open **Control Center → Settings**, or run `ferese-settings`.
Advanced options live in `~/.config/ferese/config.toml` (or under
-`$XDG_CONFIG_HOME`). Start with the [example config](packaging/config.toml).
-
-```toml
-[scrolling]
-default_column_width = 0.5
-focus_strategy = "paged"
-
-[[window_rules]]
-app_id = "dev.ferese.Settings"
-floating = true
-```
-
-Saving the file reloads it automatically. Run `feresectl reload-config`
-for an explicit reload with validation feedback. The
-[configuration reference](docs/configuration.md) covers themes, window rules,
-bindings, output profiles, session services and locking.
+`$XDG_CONFIG_HOME`). Changes reload automatically. The complete
+[configuration reference](docs/configuration.md) covers every supported option;
+the [example config](packaging/config.toml) is a starting point.
## Development
@@ -109,15 +97,5 @@ cargo fmt --all --check
Use release builds when evaluating animation and startup performance.
`RUST_LOG=ferese=debug` enables detailed logs;
`FERESE_TRACE_PERFORMANCE=1` enables per-output performance summaries.
-Development probes under `tools/` are local-only and are not part of the
-published Cargo workspace.
-
-- [Architecture and window-manager specification](docs/ferese-window-manager-spec.md)
-- [Shell design](docs/ferese-shell-design.md)
-- [Theme and UI specification](docs/ferese-theme-ui-spec.md)
-- [Animation validation](docs/animation-validation.md)
-- [Nested resize testing](docs/nested-resize-testing.md)
-- [Native-client soak testing](docs/native-soak-testing.md)
-
Test hardware behavior on your own setup, especially suspend/resume and locking.
Keep a working desktop session available while testing changes.
diff --git a/compositor/src/config.rs b/compositor/src/config.rs
index df9af7c..4f01ee0 100644
--- a/compositor/src/config.rs
+++ b/compositor/src/config.rs
@@ -16,6 +16,8 @@ use crate::window_rules::{self, WindowRule, WindowRuleConfig};
#[derive(Debug, Default, Deserialize)]
pub struct Config {
+ #[serde(default)]
+ desktop_widgets: ferese_core::desktop::DesktopWidgets,
#[serde(default)]
pub(crate) autostart: Vec,
#[serde(default)]
@@ -270,6 +272,9 @@ impl Config {
}
pub(crate) fn runtime_config(&self) -> Result {
+ self.desktop_widgets
+ .validate()
+ .map_err(ConfigError::InvalidBinding)?;
for daemon in &self.autostart {
if daemon
.command
@@ -1712,6 +1717,26 @@ pub(crate) fn config_path() -> Option {
mod tests {
use super::*;
+ #[test]
+ fn desktop_clock_settings_are_validated_before_live_publication() {
+ assert!(
+ parse(
+ "[desktop_widgets.clock]\nenabled = true\nanchor = 'center'\ntime_format = '%H:%M'"
+ )
+ .runtime_config()
+ .is_ok()
+ );
+ for source in [
+ "[desktop_widgets.clock]\nwidth = 8192",
+ "[desktop_widgets.clock]\nopacity = nan",
+ "[desktop_widgets.clock]\ntime_format = '%'",
+ "[desktop_widgets.clock]\ntime_zone = 'invalid/zone'",
+ ] {
+ assert!(parse(source).runtime_config().is_err());
+ }
+ assert!(Config::parse_source("[desktop_widgets.clock]\nanchor = 'wrong'").is_err());
+ }
+
fn parse(source: &str) -> Config {
toml::from_str(source).unwrap()
}
diff --git a/crates/ferese-core/Cargo.toml b/crates/ferese-core/Cargo.toml
index 9bc3210..84fcaed 100644
--- a/crates/ferese-core/Cargo.toml
+++ b/crates/ferese-core/Cargo.toml
@@ -7,3 +7,5 @@ rust-version.workspace = true
[dependencies]
ferese-layout = { path = "../ferese-layout" }
+serde = { workspace = true, features = ["derive"] }
+jiff.workspace = true
diff --git a/crates/ferese-core/src/desktop.rs b/crates/ferese-core/src/desktop.rs
new file mode 100644
index 0000000..78cb55c
--- /dev/null
+++ b/crates/ferese-core/src/desktop.rs
@@ -0,0 +1,407 @@
+//! Shared desktop-widget configuration: validated before live publication.
+use serde::Deserialize;
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+#[serde(default, deny_unknown_fields)]
+pub struct DesktopWidgets {
+ pub clock: Clock,
+ pub notes: Vec,
+}
+
+impl DesktopWidgets {
+ pub fn validate(&self) -> Result<(), String> {
+ self.clock.validate()?;
+ if self.notes.len() > 32 {
+ return Err("At most 32 sticky notes are supported.".into());
+ }
+ let mut ids = std::collections::HashSet::new();
+ for note in &self.notes {
+ note.validate()?;
+ if !ids.insert(¬e.id) {
+ return Err("Sticky note IDs must be unique.".into());
+ }
+ }
+ Ok(())
+ }
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq)]
+#[serde(default, deny_unknown_fields)]
+pub struct StickyNote {
+ pub id: String,
+ pub enabled: bool,
+ pub interactive: bool,
+ pub title: String,
+ pub text: String,
+ pub outputs: Vec,
+ pub anchor: Anchor,
+ pub margin_x: i32,
+ pub margin_y: i32,
+ pub width: u32,
+ pub height: u32,
+ pub font_family: Option,
+ pub text_size: f32,
+ pub title_size: f32,
+ pub color: Option,
+ pub background: Option,
+ pub opacity: f32,
+ pub padding: f32,
+ pub gap: f32,
+ pub radius: f32,
+ pub alignment: Alignment,
+}
+
+impl Default for StickyNote {
+ fn default() -> Self {
+ Self {
+ id: "note".into(),
+ enabled: true,
+ interactive: true,
+ title: "Note".into(),
+ text: String::new(),
+ outputs: Vec::new(),
+ anchor: Anchor::TopRight,
+ margin_x: 48,
+ margin_y: 80,
+ width: 320,
+ height: 240,
+ font_family: None,
+ text_size: 16.,
+ title_size: 18.,
+ color: None,
+ background: None,
+ opacity: 0.9,
+ padding: 20.,
+ gap: 8.,
+ radius: 16.,
+ alignment: Alignment::Left,
+ }
+ }
+}
+
+impl StickyNote {
+ pub fn on_output(&self, name: Option<&str>) -> bool {
+ self.enabled
+ && (self.outputs.is_empty()
+ || name.is_some_and(|name| self.outputs.iter().any(|s| s == name)))
+ }
+ /// Text/style changes repaint in place, never remap the desktop card.
+ pub fn same_surface(&self, other: &Self) -> bool {
+ self.id == other.id
+ && self.enabled == other.enabled
+ && self.interactive == other.interactive
+ && self.outputs == other.outputs
+ && self.anchor == other.anchor
+ && self.margin_x == other.margin_x
+ && self.margin_y == other.margin_y
+ && self.width == other.width
+ && self.height == other.height
+ }
+ pub fn validate(&self) -> Result<(), String> {
+ let invalid = |field: &str| {
+ Err(format!(
+ "desktop_widgets.notes[{}].{field} is invalid",
+ self.id
+ ))
+ };
+ if self.id.is_empty()
+ || self.id.len() > 64
+ || !self
+ .id
+ .bytes()
+ .all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
+ {
+ return invalid("id");
+ }
+ if self.text.len() > 16384 || self.title.len() > 256 {
+ return invalid("text/title");
+ }
+ if !(120..=1200).contains(&self.width) || !(80..=1200).contains(&self.height) {
+ return invalid("width/height");
+ }
+ if !(0..=8192).contains(&self.margin_x) || !(0..=8192).contains(&self.margin_y) {
+ return invalid("margin");
+ }
+ for (name, value, min, max) in [
+ ("text_size", self.text_size, 8., 96.),
+ ("title_size", self.title_size, 8., 96.),
+ ("opacity", self.opacity, 0., 1.),
+ ("padding", self.padding, 0., 64.),
+ ("gap", self.gap, 0., 64.),
+ ("radius", self.radius, 0., 128.),
+ ] {
+ if !value.is_finite() || !(min..=max).contains(&value) {
+ return invalid(name);
+ }
+ }
+ for color in [&self.color, &self.background].into_iter().flatten() {
+ if !color.is_empty()
+ && color != "theme"
+ && !color.strip_prefix('#').is_some_and(|hex| {
+ matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit())
+ })
+ {
+ return invalid("color/background");
+ }
+ }
+ if self.font_family.as_ref().is_some_and(|s| s.len() > 128)
+ || self.outputs.len() > 32
+ || self.outputs.iter().any(|s| s.is_empty() || s.len() > 128)
+ {
+ return invalid("font/outputs");
+ }
+ Ok(())
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub enum Anchor {
+ #[default]
+ TopLeft,
+ TopCenter,
+ TopRight,
+ CenterLeft,
+ Center,
+ CenterRight,
+ BottomLeft,
+ BottomCenter,
+ BottomRight,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub enum Alignment {
+ Left,
+ #[default]
+ Center,
+ Right,
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq)]
+#[serde(default, deny_unknown_fields)]
+pub struct Clock {
+ pub enabled: bool,
+ /// Empty means every output; otherwise exact connector names.
+ pub outputs: Vec,
+ pub anchor: Anchor,
+ pub margin_x: i32,
+ pub margin_y: i32,
+ pub width: u32,
+ pub height: u32,
+ pub font_family: Option,
+ pub bold: bool,
+ pub time_size: f32,
+ pub date_size: f32,
+ pub time_format: String,
+ pub date_format: String,
+ pub time_zone: Option,
+ pub show_date: bool,
+ pub lowercase: bool,
+ pub color: Option,
+ pub date_color: Option,
+ pub opacity: f32,
+ pub alignment: Alignment,
+ pub gap: f32,
+ pub padding: f32,
+ pub background: Option,
+ pub radius: f32,
+}
+
+impl Default for Clock {
+ fn default() -> Self {
+ Self {
+ enabled: false,
+ outputs: Vec::new(),
+ anchor: Anchor::TopLeft,
+ margin_x: 64,
+ margin_y: 80,
+ width: 440,
+ height: 160,
+ font_family: None,
+ bold: false,
+ time_size: 72.,
+ date_size: 18.,
+ time_format: "%-I:%M %p".into(),
+ date_format: "%A, %-d %B".into(),
+ time_zone: None,
+ show_date: true,
+ lowercase: true,
+ color: None,
+ date_color: None,
+ opacity: 0.9,
+ alignment: Alignment::Center,
+ gap: 4.,
+ padding: 12.,
+ background: None,
+ radius: 16.,
+ }
+ }
+}
+
+impl Clock {
+ pub fn on_output(&self, name: Option<&str>) -> bool {
+ self.enabled
+ && (self.outputs.is_empty()
+ || name.is_some_and(|name| self.outputs.iter().any(|output| output == name)))
+ }
+
+ pub fn labels(&self, now: &jiff::Zoned) -> Result<(String, String), String> {
+ let zoned = match &self.time_zone {
+ Some(zone) if !zone.is_empty() => {
+ now.with_time_zone(jiff::tz::TimeZone::get(zone).map_err(|e| e.to_string())?)
+ }
+ _ => now.clone(),
+ };
+ let time =
+ jiff::fmt::strtime::format(&self.time_format, &zoned).map_err(|e| e.to_string())?;
+ let date =
+ jiff::fmt::strtime::format(&self.date_format, &zoned).map_err(|e| e.to_string())?;
+ Ok(if self.lowercase {
+ (time.to_lowercase(), date.to_lowercase())
+ } else {
+ (time, date)
+ })
+ }
+
+ pub fn validate(&self) -> Result<(), String> {
+ let invalid = |name: &str| Err(format!("desktop_widgets.clock.{name} is invalid"));
+ if !(64..=1600).contains(&self.width) || !(32..=800).contains(&self.height) {
+ return invalid("width/height");
+ }
+ if !(0..=8192).contains(&self.margin_x) || !(0..=8192).contains(&self.margin_y) {
+ return invalid("margin_x/margin_y");
+ }
+ for (name, value, min, max) in [
+ ("time_size", self.time_size, 8., 240.),
+ ("date_size", self.date_size, 8., 96.),
+ ("opacity", self.opacity, 0., 1.),
+ ("gap", self.gap, 0., 64.),
+ ("padding", self.padding, 0., 64.),
+ ("radius", self.radius, 0., 128.),
+ ] {
+ if !value.is_finite() || !(min..=max).contains(&value) {
+ return invalid(name);
+ }
+ }
+ for color in [&self.color, &self.date_color, &self.background]
+ .into_iter()
+ .flatten()
+ {
+ if color.is_empty() {
+ continue;
+ }
+ if !color.strip_prefix('#').is_some_and(|hex| {
+ matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit())
+ }) {
+ return invalid("color");
+ }
+ }
+ if self.font_family.as_ref().is_some_and(|s| s.len() > 128)
+ || self.time_format.len() > 128
+ || self.date_format.len() > 128
+ || self.outputs.len() > 32
+ || self.outputs.iter().any(|s| s.is_empty() || s.len() > 128)
+ {
+ return invalid("font/format/outputs");
+ }
+ self.labels(&jiff::Zoned::now())
+ .map(|_| ())
+ .map_err(|e| format!("desktop_widgets.clock: {e}"))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ #[test]
+ fn notes_reject_duplicate_ids_and_unbounded_content() {
+ let note = StickyNote::default();
+ note.validate().unwrap();
+ assert!(
+ DesktopWidgets {
+ clock: Clock::default(),
+ notes: vec![note.clone(), note.clone()]
+ }
+ .validate()
+ .is_err()
+ );
+ assert!(
+ StickyNote {
+ text: "x".repeat(16385),
+ ..note.clone()
+ }
+ .validate()
+ .is_err()
+ );
+ assert!(
+ StickyNote {
+ opacity: f32::NAN,
+ ..note.clone()
+ }
+ .validate()
+ .is_err()
+ );
+ assert!(note.same_surface(&StickyNote {
+ text: "edited".into(),
+ ..note.clone()
+ }));
+ }
+ #[test]
+ fn defaults_are_safe_and_opt_in() {
+ let clock = Clock::default();
+ clock.validate().unwrap();
+ assert!(!clock.on_output(Some("HDMI-A-1")));
+ }
+ #[test]
+ fn output_filters_do_not_guess_monitor_names() {
+ let clock = Clock {
+ enabled: true,
+ outputs: vec!["DP-2".into()],
+ ..Clock::default()
+ };
+ assert!(clock.on_output(Some("DP-2")));
+ assert!(!clock.on_output(Some("eDP-1")));
+ assert!(!clock.on_output(None));
+ }
+ #[test]
+ fn rejects_bad_sizes_colors_formats_and_zones() {
+ for clock in [
+ Clock {
+ opacity: f32::NAN,
+ ..Clock::default()
+ },
+ Clock {
+ width: 8192,
+ ..Clock::default()
+ },
+ Clock {
+ color: Some("red".into()),
+ ..Clock::default()
+ },
+ Clock {
+ time_format: "%".into(),
+ ..Clock::default()
+ },
+ Clock {
+ time_zone: Some("not/a-zone".into()),
+ ..Clock::default()
+ },
+ ] {
+ assert!(clock.validate().is_err());
+ }
+ }
+ #[test]
+ fn formats_timezone_and_lowercase() {
+ let now: jiff::Zoned = "2026-09-26T21:05:00+00:00[UTC]".parse().unwrap();
+ let clock = Clock {
+ time_zone: Some("Africa/Lagos".into()),
+ date_format: "%-d %b".into(),
+ ..Clock::default()
+ };
+ assert_eq!(
+ clock.labels(&now).unwrap(),
+ ("10:05 pm".into(), "26 sep".into())
+ );
+ }
+}
diff --git a/crates/ferese-core/src/lib.rs b/crates/ferese-core/src/lib.rs
index 486edf7..41b894a 100644
--- a/crates/ferese-core/src/lib.rs
+++ b/crates/ferese-core/src/lib.rs
@@ -1,3 +1,4 @@
+pub mod desktop;
mod output;
mod workspace;
diff --git a/docs/adr/0001-shell-toolkit.md b/docs/adr/0001-shell-toolkit.md
deleted file mode 100644
index 3b9417a..0000000
--- a/docs/adr/0001-shell-toolkit.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# ADR 0001: libcosmic for the Ferese shell
-
-Status: provisional, pending the M8 lifecycle spike
-Date: 2026-09-25
-
-## Decision
-
-Use libcosmic for the Ferese shell UI if the M8 lifecycle spike passes. Pin the
-dependency to commit `03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0` until a suitable
-stable release exists.
-
-Keep the spike in `tools/ferese-shell-spike`; it is not the production shell.
-The production shell will continue to use public layer-shell for surface roles
-and Ferese-private protocols only for control and semantic effects.
-
-## Why
-
-libcosmic is a Rust-native toolkit designed for Wayland desktop components. Its
-current API can create layer-shell surfaces without an ordinary xdg-toplevel,
-select an active or explicit output, request keyboard interactivity, observe
-configure/focus/output events, and destroy surfaces through the runtime.
-
-The previous GPUI direction depended on an older release and did not provide a
-convincing first-class path for Ferese shell surfaces.
-
-## Constraints found during source review
-
-- The selected revision declares Rust 1.93, so the isolated spike overrides the
- workspace's Rust 1.87 minimum. Ferese core crates keep their existing MSRV.
-- libcosmic exposes the created `WlSurface` in public Wayland layer events. This
- is necessary but may not be sufficient to bind `ferese-effects-v1` on the
- toolkit's event queue.
-- The spike must therefore prove private semantic-effects attachment before the
- production shell adopts libcosmic. If that cannot be done through supported
- APIs, Ferese will place a thin native Wayland surface adapter beneath the UI
- or revisit this decision; it will not replace public layer-shell with a
- private surface role.
-
-## Validation gate
-
-The provisional decision becomes accepted only after the spike demonstrates:
-
-1. public layer-surface creation on the intended output;
-2. output discovery and updates;
-3. configure and resize delivery;
-4. on-demand keyboard focus and focus release;
-5. clean compositor-driven and application-driven shutdown; and
-6. `ferese-effects-v1` attachment to the toolkit-owned surface without an
- unsupported libcosmic or Iced fork.
-
-## Spike result
-
-Items 1 through 5 pass against nested Ferese. Item 6 passes after libcosmic
-publishes the surface in a focus event: the spike reconstructs a supported
-`wayland-client` connection handle, binds the capability-filtered effects
-global, and assigns `panel` to the same `wl_surface`.
-
-The decision remains provisional because libcosmic does not currently publish
-its layer-surface creation event to the application. A normal non-focusable top
-bar therefore has no deterministic pre-map surface handle. Production work may
-proceed with libcosmic widgets and application state, but the top-bar surface
-path must first gain either:
-
-1. a supported upstream surface-created callback; or
-2. the thin native Wayland surface adapter already allowed by the M8 plan.
-
-Ferese will not make the persistent top bar steal keyboard focus merely to
-obtain its surface handle.
diff --git a/docs/animation-validation.md b/docs/animation-validation.md
deleted file mode 100644
index 0c7c98d..0000000
--- a/docs/animation-validation.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Animation regression checks
-
-Run `cargo test -p ferese-animation -p ferese --offline` for transition,
-resize requests, reduced motion, stacking, and refresh-rate regression tests.
-
-For visual validation, use a disposable Ferese session with three tiled windows
-containing text and at least one floating window. Test both the nested and DRM
-backends. Record the session at its native refresh rate when comparing builds;
-single screenshots cannot establish smoothness.
-
-| Scenario | Expected result |
-| --- | --- |
-| Super+F on the middle column, then exit | Neighbours remain behind the expanding window. The shrinking window stays above them until the transition finishes. |
-| Toggle Super+F repeatedly at 40–80 ms intervals | Every reversal starts at the presented frame. Corners, border and shadow follow the same zoom progress. The final state matches the last input. |
-| Cycle width, then immediately Super+F | The old column-width spring cannot override fullscreen width. |
-| Exit fullscreen and immediately resize or scroll | The current frame is retained when the target changes; scrolling resumes without a position jump. |
-| Drag a floating window; resize from its top-left corner | The window follows the pointer directly. The opposite edge stays anchored. Delayed client commits do not move or raise it. |
-| Zoom responsive and slow-redrawing clients | Each zoom requests the destination size once. Client redraws do not generate intermediate resize requests or restart the motion. |
-| Zoom the leftmost and rightmost columns in and out | Each outer edge moves monotonically toward its destination; neither side trembles or reverses near the screen edge. |
-| Disable borders and shadows, then zoom | Rounded corner pixels repaint through the final frame; no corner remnants remain. |
-| Repeat on 60 Hz and 144 Hz outputs; move focus between them | Similar elapsed-time motion with no double advancement. Check frame timing on actual DRM hardware. |
-| Enable reduced motion | Geometry and decorations reach their final values together. |
-
-Zoom requests the destination client size once and scales the available buffer
-along the transition. This avoids repeated content reflow during motion. It is
-not a buffer snapshot or a guarantee of perfectly sharp intermediate text.
diff --git a/docs/configuration.md b/docs/configuration.md
index 503fc14..3b8e267 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1,327 +1,171 @@
-# Configuration and session reference
+# Configuration
-## Live configuration
+Edit `~/.config/ferese/config.toml` (or `$XDG_CONFIG_HOME/ferese/config.toml`),
+or open **Control Center → Settings**. Start with the [example](../packaging/config.toml).
+All supported configuration sections are listed below; [desktop widgets](desktop-widgets.md)
+covers the complete clock and sticky-note fields separately.
-`ferese-settings` opens the native settings application, also available from
-Control Center → Settings after installation. It includes appearance presets,
-wallpaper selection, menu bar geometry, window behavior, motion, keyboard/mouse,
-custom shortcut editing, login items, and existing display profiles. Changes save
-automatically; text fields save on Enter or when leaving the field. Sliders save
-when released. Settings also refreshes after external config edits, preserving
-active drafts. Display profiles, touchpad options, and login items update live.
-Wallpaper browsing uses Zenity's native GTK file chooser (`zenity` must be
-installed); entering an image path directly remains available.
+## Saving and validation
-The app validates changes with `ferese --check-config PATH` before atomically
-replacing your file, preserves comments and unrelated keys, and keeps a
-`config.toml.settings-backup`. Undo restores the previous save; external edits
-require Reload before saving. Search filters settings categories. To preview
-against a disposable config without changing your desktop config:
+Settings saves automatically: text on Enter/focus loss, sliders when released.
+Undo restores the previous save; Reload picks up external edits without merging
+unfinished drafts. Wallpaper browsing needs `zenity`; a path can also be entered.
+Settings keeps `config.toml.settings-backup` before saving.
+
+File edits reload automatically, including atomic editor saves. Invalid changes
+keep the last working configuration. Appearance, wallpaper, motion, input,
+bindings, rules, layouts, displays, widgets and login items update live.
```sh
-cargo build -p ferese -p ferese-settings --locked
-target/debug/ferese-settings --config /tmp/ferese-settings-preview.toml
+feresectl reload-config # reload with error feedback
+ferese --check-config PATH # validate without applying
+ferese-settings --config PATH # edit a separate preview config
```
-The GUI is not a replacement for every advanced config option: adding display
-profiles, arranging outputs, new shortcut definitions, and window rules remain
-in `config.toml`. Theme presets update palette and focus-gradient colors without
-replacing custom rules, bindings, wallpaper, or layout settings.
+Defaults below are built-in defaults, not your personal or packaged overrides.
+Dimensions are logical pixels unless noted; numbers must be finite. Optional
+values are omitted, not `null`. Colors use `#RRGGBB` or `#RRGGBBAA`.
+Live config is limited to 60 KiB.
-New installations float Settings by default. To do the same in an existing
-config, add a `[[window_rules]]` entry with `app_id = "dev.ferese.Settings"`
-and `floating = true`.
+## Layout
-Saving `~/.config/ferese/config.toml` (or `$XDG_CONFIG_HOME/ferese/config.toml`)
-reloads it automatically. Atomic editor saves are supported and bursts are
-debounced. Invalid edits keep the last working compositor and shell config;
-the rejection is logged. Idle watching does not reread the file.
+| `[layout]` key | Type / values | Default | Meaning |
+| --- | --- | --- | --- |
+| `mode` | `"scrolling"`, `"tree"` | `"scrolling"` | Workspace layout |
+| `inner_gap` | number ≥ 0 | `10` | Between windows |
+| `outer_gap` | number ≥ 0 | `4` | Around workspace edges |
+| `smart_gaps` | boolean | `false` | Remove outer gaps for one tiled window |
-Use `feresectl reload-config` to reload manually and report validation errors.
-Live sources are limited to 60 KiB for safe delivery over the private protocol.
+| `[scrolling]` key | Type / values | Default | Meaning |
+| --- | --- | --- | --- |
+| `default_column_width` | proportion > 0; or `"full"` | `0.5` | Width of new columns |
+| `focus_strategy` | `"minimal"`, `"center_on_focus"`, `"paged"` | `"minimal"` | Viewport movement on focus |
+| `width_presets` | array of widths | `[0.3333333333333333, 0.5, 0.6666666666666666, "full"]` | Super+R cycle; empty uses defaults |
-Appearance, fonts, wallpaper, animations, keybindings, keyboard layout/repeat,
-gaps, scrolling focus policy/default widths, and bar geometry reload live.
-Wallpaper decoding stays off the render thread and keeps the old image until
-the replacement is ready. Layout-mode changes convert existing workspaces;
-default-width changes update columns using the previous default, preserving
-custom widths. Changed window rules are reapplied to existing matching windows.
-Output profiles and touchpad changes apply immediately. Display changes retain
-output/workspace identity; configurations with no usable output are rejected.
-The compositor and shell need one initial update/restart to enable this feature.
+`minimal` scrolls only to reveal the focused window. `center_on_focus` centers
+it. `paged` packs columns into viewport-sized pages: halves form pairs, thirds
+form triples; mixed widths and client minimum sizes determine actual boundaries.
+Changing default width preserves manually resized columns.
-## Session services and locking
+## Motion
-`[[autostart]]` entries run after the compositor's public socket is ready:
+| Section / key | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `animations.enabled` | boolean | `true` | Enable motion |
+| `animations.reduced_motion` | boolean | `false` | Disable animated motion |
+| `animations.speed` | number > 0 | `1` | Higher is faster; `0.75` is slower |
+| `animations.spring.mass` | number > 0 | `1` | Window-motion spring mass |
+| `animations.spring.stiffness` | number > 0 | `700` | Spring stiffness |
+| `animations.spring.damping` | number ≥ 0 | `53` | Spring damping |
+| `animations.viewport_spring.mass` | number > 0 | `1` | Scrolling spring mass |
+| `animations.viewport_spring.stiffness` | number > 0 | `320` | Scrolling stiffness |
+| `animations.viewport_spring.damping_ratio` | number > 0 | `1` | `1` is critically damped |
-```toml
-[[autostart]]
-command = ["awari"]
-enabled = true
-restart = true
+## Appearance
-[[autostart]]
-command = ["swayidle", "-w", "timeout", "600", "ferese-lock", "before-sleep", "ferese-lock", "lock", "ferese-lock"]
-restart = true
-```
+| Section / key | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `appearance.corner_radius` | number ≥ 0 | `14` | Shell material/card radius |
+| `appearance.inactive_dim.enabled` | boolean | `false` | Dim unfocused windows |
+| `appearance.inactive_dim.amount` | number 0–1 | `0.15` | Darkening strength |
+| `appearance.inactive_dim.duration_ms` | number ≥ 0 | `150` | Dimming and focus-ring transition; 0 snaps |
+| `theme.typography.font_family` | string | system sans-serif | Shell, Settings and overview font |
+| `theme.background.path` | string | omitted | Wallpaper image path |
+| `theme.background.mode` | `"fill"`, `"fit"` | `"fill"` | Crop or letterbox |
+| `theme.material.style` | `"solid"`, `"translucent"` | `"solid"` | Shell background material |
+| `theme.material.blur_radius` | number ≥ 0 | `12` | Translucent backdrop blur, capped at 32; 0 disables |
-Services are session-owned, reaped, and restarted with a five-second retry delay.
-They receive the correct public Wayland display without private shell privileges.
-Nested previews skip them unless `nested = true` is explicitly configured.
-Autostart changes reconcile live: unchanged commands keep their processes,
-new/enabled items start, and disabled/removed commands stop without blocking rendering.
-Run foreground daemons here, not commands that fork themselves into the background.
+| `[theme.colors]` key | Default | Meaning |
+| --- | --- | --- |
+| `surface_base` | `"#111821"` | Material/card base color |
+| `text_primary` | `"#F4F7FB"` | Main text |
+| `text_muted` | `"#7F8A98"` | Secondary text |
+| `accent` | `"#5B8CFF"` | Active controls and solid focus ring |
+| `border` | `"#FFFFFF18"` | Unfocused window border |
+| `shadow` | `"#00000055"` | Window shadow color |
-Ferese implements `ext-session-lock-v1`. While locked, only lock surfaces over an
-opaque fallback are rendered, normal input/shortcuts and IPC are blocked, and
-screencopy is unavailable. DRM confirmation waits for safe page flips on active
-outputs. A missing/crashed locker leaves the session locked; it is not an unlock.
-`ferese-lock` runs swaylock/swaylock-effects with optional
-`~/.config/ferese/swaylock.conf`, waiting for lock confirmation before daemonizing.
-The compositor authenticates no passwords itself; authentication belongs to the locker.
-Validate password unlock in a disposable nested session before enabling automatic
-locking on a primary session. Crash recovery currently requires ending that session
-from another VT; there is deliberately no unauthenticated IPC unlock.
+| `[theme.geometry]` key | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `border_width` | number ≥ 0 | `1` | Window border thickness |
+| `focus_ring_width` | number ≥ 0 | `2` | Focused border thickness |
+| `window_radius` | number ≥ 0 | `14` | Managed-window corners |
+| `top_bar_height` | number > 0 | `28` | Menu-bar height |
+| `top_bar_margin_top` | integer ≥ 0 | `0` | Space above bar |
+| `top_bar_window_gap` | integer ≥ 0 | `0` | Clearance below bar |
+| `top_bar_margin_horizontal` | integer ≥ 0 | `0` | Bar side margins |
+| `top_bar_radius` | number ≥ 0 | `0` | Bar corners |
+| `panel_padding` | number ≥ 0 | `12` | Bar inner padding |
+| `control_gap` | number ≥ 0 | `12` | Right-side control spacing |
+| Section / key | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `theme.surface.bar.background` | color | `"#1C202EF2"` | Shell fallback background; compositor materials use `surface_base` |
+| `theme.surface.bar.opacity` | number 0–1 | omitted | Material opacity defaults to `0.78`; fallback retains background alpha |
+| `theme.surface.bar.text_primary` | color | `"#F0F3FA"` | Bar text/icons |
+| `theme.surface.bar.text_muted` | color | `"#AAB4C7"` | Inactive bar foreground |
+| `theme.surface.popover.opacity` | number 0–1 | `0.96` | Shell popover fill alpha |
+| `theme.shadow.soft.offset_y` | number | `4` | Window shadow vertical offset |
+| `theme.shadow.soft.blur` | number ≥ 0 | `18` | Window shadow softness |
+| `theme.shadow.soft.opacity` | number 0–1 | `0.20` | Window shadow strength |
-## Configuration reference
+Solid materials omit blur; translucent materials blur behind the surface color.
+True fullscreen removes decorations. Focus transitions never scale the content.
-Ferese reads `$XDG_CONFIG_HOME/ferese/config.toml`, or
-`~/.config/ferese/config.toml` when `XDG_CONFIG_HOME` is unset. New scrolling
-columns use half the viewport by default. To make every newly opened column use
-the full viewport width:
-
-```toml
-[scrolling]
-default_column_width = "full"
-```
-
-`default_column_width = 1.0` is equivalent. `Super+M` switches the active
-workspace between scrolling and tree modes.
-
-Outer gaps remain visible with one window by default. To intentionally remove
-them for a lone tiled window:
-
-```toml
-[layout]
-outer_gap = 4.0
-smart_gaps = true
-```
-
-Managed-window borders use theme tokens and overlay the window edge without
-adding inner padding or changing client geometry:
-
-```toml
-[theme.colors]
-border = "#FFFFFF18"
-accent = "#5B8CFF"
-shadow = "#00000055"
-
-[theme.geometry]
-border_width = 1.0
-focus_ring_width = 2.0
-window_radius = 14.0
-
-[theme.shadow.soft]
-offset_y = 4.0
-blur = 18.0
-opacity = 0.20
-
-[theme.material]
-style = "solid" # solid or translucent
-blur_radius = 12.0 # logical pixels; translucent only, 0 disables blur
-
-[appearance.inactive_dim]
-enabled = true # opt-in; disabled by default
-amount = 0.15 # dark tint over unfocused windows, 0–1; 0 disables
-duration_ms = 150 # 0 snaps immediately; reduced motion also snaps
-```
-
-Inactive dimming follows window focus with a smooth, interruption-safe fade.
-Focus-ring paint (including gradients) and width use the same `duration_ms`,
-even when dimming is disabled. Focus changes never scale or move the window.
-Shell buttons fade their hover tint over 120 ms. Both transitions respect
-`animations.speed`, `animations.enabled`, and `animations.reduced_motion`;
-click actions remain immediate and settled hover states do not animate.
-It keeps window opacity intact, respects animated geometry and rounded corners,
-and leaves shell bars/popovers untouched. Overview temporarily removes dimming.
-
-Material roles use the configured surface color and role-specific shadows.
-Solid is the default. Translucent adds backdrop blur behind the same surface
-color, without grain, glossy lighting, saturation boosts, or inset highlights.
-The radius is clamped to 32 logical pixels. A shader/allocation failure falls
-back to a plain translucent fill. Shell popovers report rounded card regions;
-their outer padding and gaps stay transparent. Each surface shares one capture
-across its cards, so cards do not sample already blurred neighboring cards.
-
-```toml
-[appearance]
-corner_radius = 14.0
-```
-
-Shell shadows use restrained logical-pixel presets: panel `1/5/4%`, popover
-and menu `2/8/7%`, HUD `1/4/4%`, and notification/modal `3/10/9%`
-(vertical offset / blur / opacity). Private effects capabilities
-remain required; public background-effect requests cannot select these materials.
-
-Fullscreen windows are borderless and shadowless. Other managed windows use GPU
-rounded clipping, matching rounded focus rings, and an analytic soft shadow.
-These effects overlay or surround the allocation without adding client padding.
-
-Scrolling keeps windows in stable workspace coordinates and animates the
-viewport only when focus must be revealed. Set
-`focus_strategy = "center_on_focus"` under `[scrolling]` to center focused
-columns instead.
-
-Use `focus_strategy = "paged"` to pack consecutive columns into viewport-sized
-pages. Half-width columns form pairs, thirds form triples, and full-width
-columns occupy their own page. Mixed widths and client minimum sizes use their
-actual allocated widths, including gaps. Focusing within a page keeps it still;
-crossing a page boundary scrolls to that page. Explicit centering remains in
-effect until focus changes or the output resizes.
-
-Scrolling controls use `Super+R` to cycle configured width presets, `Super+C`
-to center the focused column, `Super+[` to consume the focused window into an
-adjacent column, and `Super+]` to expel it. `Super+Ctrl+H/J/K/L` resizes in the
-corresponding direction.
-
-`Super+Tab` toggles the compositor overview. Its live window previews retain
-their client surfaces, `Super+H/J/K/L` moves the selection using the presented
-preview geometry, and clicking a preview activates it without forwarding the
-selection click to the application.
-
-The shell provides a configurable libcosmic menu bar backed
-by the private shell-control connection. Build and preview it nested with:
-
-```bash
-cargo build --release -p ferese -p ferese-shell
-target/release/ferese --backend nested --grant-effects --grant-shell-control -- \
- target/release/ferese-shell
-```
-
-The Ferese mark enters overview. A workspace selector sits on the left;
-status controls and day/time sit on the right. Bar geometry, spacing, colors
-and material style are configurable and reload live.
-
-The compositor decodes wallpaper in parallel with startup. Small shell surfaces prefer the lightweight
-software renderer, with GPU rendering available as a fallback. Set `ICED_BACKEND=wgpu` to explicitly use GPU
-rendering for shell content. Use release builds when evaluating startup and
-animation performance; debug image processing is considerably slower.
-
-Resize presentation configures the destination once and coordinates the
-matching client commits with the workspace motion (300 ms maximum wait).
-Application content stays at native pixel size; animated bounds crop/reveal
-it rather than stretching text. A resize-only previous-frame snapshot fades
-out over 80 ms, with a 64 MiB total snapshot budget and automatic release.
-All monitors share one elapsed-time animation clock, and window content,
-clips, borders and shadows use the same physical-pixel edges.
-
-Window borders and focus rings use solid colors by default. Optional linear
-gradients can be configured independently:
+Optional `[theme.focus_ring.gradient]` and `[theme.border.gradient]` use the same
+keys: required `from` and `to` colors, and `angle` (number, default `0`). Angles
+are clockwise: 0 is left-to-right, 90 top-to-bottom. Omit the gradient section
+for solid `accent` or `border` colors.
```toml
[theme.focus_ring.gradient]
from = "#e5c890"
to = "#b98d58"
angle = 135.0
-
-# Optional unfocused-window gradient:
-[theme.border.gradient]
-from = "#3e3e43"
-to = "#242429"
-angle = 90.0
```
-Angles are clockwise in window coordinates: 0 is left-to-right and 90 is
-top-to-bottom. Both endpoints accept `#RRGGBB` or `#RRGGBBAA`. Remove either
-gradient section to restore its solid `theme.colors.accent` or `border` color.
-Gradients share the existing rounded-border shader and add no animation or
-extra render pass. Configuration changes reload live.
+## Input
-Overview has a horizontally scrollable workspace strip with live window
-thumbnails and an accent outline on the workspace shown by each output.
-Click a workspace to change the window grid without leaving overview; click
-a window to focus it and exit, or press Escape to dismiss. Only the workspace
-strip has a tinted surface; the window grid sits directly over the wallpaper.
-Strip labels use `[theme.typography].font_family` and rasterize at output scale.
-The strip fades independently and preserves its opacity on rapid reversals,
-including when the selected workspace is empty.
+| `[input]` key | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `focus_follows_mouse` | boolean | `false` | Focus windows under the pointer |
+| `xkb_layout` | nonempty string | `"us"` | XKB layout |
+| `xkb_variant` | string | `""` | XKB variant |
+| `xkb_options` | string array | `[]` | XKB options |
+| `repeat_rate` | integer > 0 | `25` | Repeats per second |
+| `repeat_delay_ms` | integer ≥ 0 | `600` | Delay before repeat |
-Ferese renders `[theme.background]` wallpapers directly: `path` selects the
-image and `mode = "fill"` or `"fit"` controls crop/letterboxing. One decoded
-image and one texture per GPU context serve every output; resizing requires
-no image reload or full-screen shell buffers. The launched shell receives
-`FERESE_COMPOSITOR_WALLPAPER=1` to disable its redundant wallpaper surfaces.
-When no valid image path is configured, the shell's existing fallback remains.
+`[input.touchpad]` has three boolean keys, all defaulting to `true`: `tap`,
+`natural_scroll` and `disable_while_typing`. These apply to DRM devices; nested
+previews use the host's physical input settings.
-Keyboard and direct-session touchpad settings reload live:
+## Commands and bindings
-```toml
-[input]
-focus_follows_mouse = false
-xkb_layout = "us"
-xkb_variant = ""
-xkb_options = []
-repeat_rate = 25
-repeat_delay_ms = 600
+`[commands]` maps arbitrary names to nonempty argument arrays. The built-in
+`terminal = ["foot"]` can be overridden. Commands run directly, without a shell.
-[input.touchpad]
-tap = true
-natural_scroll = true
-disable_while_typing = true
-```
+| `[[bindings]]` key | Type / values | Default |
+| --- | --- | --- |
+| `keys` | chord string, e.g. `"Super+Enter"` | required |
+| `match` | `"keysym"`, `"physical"` | `"keysym"` |
+| `action` | action name below | required unless disabled |
+| `argument` | string | required only for actions listed below |
+| `disabled` | boolean | `false` |
-Touchpad settings apply to libinput devices in the direct DRM session. In the
-nested backend, the host compositor remains responsible for physical touchpad
-configuration.
+Modifiers: `Super`/`Logo`/`Mod4`, `Ctrl`/`Control`, `Alt`, `Shift`.
+Keys use XKB keysyms (`Enter`, letters, `[`/`]`) or physical XKB names (`AD06`).
+A binding replaces the same normalized chord/match mode. Duplicate bindings are
+invalid. A disabled binding must omit `action` and `argument`.
-The direct DRM backend selects the first output profile whose listed monitors
-are all connected. Match either the connector name reported by `get-outputs`
-or its persistent EDID-derived identity:
-
-```toml
-[[output_profiles]]
-name = "docked"
-
-[[output_profiles.outputs]]
-match = "HDMI-A-1"
-mode = "3840x2160@119.998"
-scale = 1.6
-position = [0, 0]
-
-[[output_profiles.outputs]]
-match = "eDP-1"
-enabled = false
-
-[[output_profiles]]
-name = "laptop"
-
-[[output_profiles.outputs]]
-match = "eDP-1"
-mode = "2880x1800@120"
-scale = 1.6
-position = [0, 0]
-```
-
-Unspecified connected outputs remain enabled with their preferred mode, scale
-`1.0`, normal transform, and automatic horizontal placement. Supported
-transforms are `normal`, `rotate_90`, `rotate_180`, `rotate_270`, `flipped`,
-and their rotated flipped variants. Output configuration is applied at startup,
-on hotplug, and live reload, including modes, scale, transform, and position.
-
-On DRM, closing the laptop lid disables internal panels when a usable external
-output is available. With no usable external output, the panel stays enabled;
-Ferese does not change the system's suspend policy. Workspaces are evacuated
-to a remaining output without merging their windows, and return to their
-original output when the lid reopens or a disconnected monitor returns.
-Normal use of an evacuated workspace does not cancel its automatic return.
-
-Commands are argv arrays and are started directly, without a shell. The
-default `Super+Enter` binding runs the built-in `terminal = ["foot"]` command:
+| Actions | Argument |
+| --- | --- |
+| `spawn` | Name in `[commands]` |
+| `focus`, `move`, `resize` | `"left"`, `"right"`, `"up"`, `"down"` |
+| `workspace`, `move-to-workspace` | Workspace number string, 1–255 |
+| `close`, `exit`, `toggle-maximized`, `toggle-fullscreen`, `toggle-layout`, `cycle-column-width`, `center-column`, `consume`, `expel`, `toggle-floating`, `toggle-overview` | None |
```toml
[commands]
-terminal = ["foot", "--app-id", "terminal"]
+terminal = ["foot"]
[[bindings]]
keys = "Super+Enter"
@@ -329,115 +173,105 @@ action = "spawn"
argument = "terminal"
```
-A configured binding replaces the built-in binding with the same normalized
-chord and match mode. Disable a default explicitly with no action:
+Defaults: Super+Enter terminal; Super+Q close; Super+H/J/K/L focus;
+Super+Shift+H/J/K/L move; Super+Ctrl+H/J/K/L resize; Super+1–9 workspace;
+Super+Shift+1–9 move to workspace; Super+R width cycle; Super+C center;
+Super+[/] consume/expel; Super+F maximize; Super+Shift+F fullscreen;
+Super+M layout; Super+Shift+Space floating; Super+Tab overview;
+Super+Shift+E immediate logout.
-```toml
-[[bindings]]
-keys = "Super+Q"
-disabled = true
-```
+## Window rules
-Bindings follow the active layout by default. To bind a keyboard position
-instead, set `match = "physical"` and use an XKB physical key name such as
-`AD06`. Invalid chords, actions, arguments, duplicate bindings, and missing
-command references stop startup with an error.
+| `[[window_rules]]` key | Type | Default / meaning |
+| --- | --- | --- |
+| `app_id` | nonempty string | Optional exact match; case-insensitive, `.desktop` suffix ignored |
+| `title` | nonempty string | Optional exact, case-sensitive match |
+| `transient` | boolean | Optional parent-dialog match |
+| `workspace` | integer > 0 | Leave placement unchanged |
+| `floating` | boolean | Leave placement unchanged |
+| `width`, `height` | numbers > 0 | Application-chosen floating size |
+| `fullscreen` | boolean | Leave fullscreen state unchanged |
-Initial window rules match `app_id`, exact title, and/or transient status. All
-matching rules are applied in declaration order; later rules override only the
-fields they specify. Width or height implies floating placement when `floating`
-is omitted:
-
-With `floating = true` and no explicit dimensions, the application chooses its
-initial size and Ferese centers that size on its workspace's output. Small
-dialogs are not forced into a workspace-sized container.
+At least one matcher is required; supplied matchers must all match. Rules apply
+in order, with later fields overriding earlier ones. Dimensions imply floating
+when `floating` is omitted. Floating apps without dimensions choose their own
+size and open centered on the output. Title changes do not trigger new rules;
+config rule changes apply to existing windows.
```toml
[[window_rules]]
-app_id = "org.example.Editor"
-workspace = 3
+app_id = "dev.ferese.Settings"
floating = true
-width = 900.0
-height = 600.0
-fullscreen = false
```
-Rules are finalized on the first toplevel commit, before its buffer is
-presented, and are not re-applied when an application later changes its title.
+## Displays
-`feresectl` controls the running compositor through the same-user socket at
-`$XDG_RUNTIME_DIR/ferese/control.sock`. For example:
+`[[output_profiles]]` has a required unique nonempty `name` and one or more
+`[[output_profiles.outputs]]` entries. The first profile whose listed monitors
+are connected wins. Find connector names and persistent identities using
+`feresectl get-outputs`.
-```bash
-cargo run -p feresectl -- focus left
-cargo run -p feresectl -- workspace 2
-cargo run -p feresectl -- get-workspaces
-cargo run -p feresectl -- get-outputs
+| `[[output_profiles.outputs]]` key | Type | Default |
+| --- | --- | --- |
+| `match` | nonempty connector or persistent identity string | required; unique within profile |
+| `enabled` | boolean | `true` |
+| `mode` | `"WIDTHxHEIGHT"` or `"WIDTHxHEIGHT@HZ"` | Preferred mode |
+| `scale` | number > 0 | `1` |
+| `transform` | enum below | `"normal"` |
+| `position` | `[integer, integer]` | Automatic horizontal placement |
+
+Transforms: `normal`, `rotate_90`, `rotate_180`, `rotate_270`, `flipped`,
+`flipped_90`, `flipped_180`, `flipped_270`. Unspecified connected displays stay
+enabled with defaults. Disabling every usable output is rejected.
+
+```toml
+[[output_profiles]]
+name = "docked"
+
+[[output_profiles.outputs]]
+match = "HDMI-A-1"
+scale = 1.5
+position = [0, 0]
```
-The v0 IPC slice also supports `move`, `resize`, `move-to-workspace`,
-`toggle-floating`, `toggle-maximized`, `toggle-fullscreen`, `toggle-layout`, `cycle-column-width`,
-`center-column`, `consume`, `expel`, `close`, `get-focused-window`, and
-`get-outputs`. Requests use versioned, 1 MiB-limited length-prefixed JSON. The
-runtime directory and socket are restricted to modes `0700` and `0600`, and the
-server rejects peers whose effective UID differs from the compositor's. IPC has
-a bounded request queue and accepts at most 64 concurrent client connections.
-`get-outputs` includes disabled-but-connected monitors, connector and persistent
-identity matchers, current and available modes with refresh rates, fractional
-scale, transform, logical position and size, and physical dimensions.
+With a usable external display, closing the lid disables internal panels.
+Workspaces move to a remaining display and return when their original output
+returns. Ferese does not change the system's suspend policy.
-Text-input-v3 is available to applications by default. Input-method-v2 is
-hidden unless the session explicitly opts in, because an input method can
-request privileged keyboard access. Enable it only when launching a trusted
-IME for the session:
+## Status controls
-```bash
-FERESE_ENABLE_INPUT_METHOD=1 cargo run -p ferese
+| `[status]` key | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `battery_percentage` | boolean | `true` | Show percentage beside icon |
+| `low_battery_threshold` | integer 0–100 | `20` | Warning-color threshold |
+| `settings_command` | argument array | `["ferese-settings"]` | Settings launcher; `[]` hides the action |
+
+## Login items and locking
+
+| `[[autostart]]` key | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `command` | nonempty argument array | required | Foreground process, not a self-daemonizing command |
+| `enabled` | boolean | `true` | Start service |
+| `restart` | boolean | `true` | Restart after exit, with a five-second retry delay |
+| `nested` | boolean | `false` | Also run in nested previews |
+
+Unchanged services keep running on reload; removed/disabled items stop. To lock
+after five minutes and before sleep:
+
+```toml
+[[autostart]]
+command = ["swayidle", "-w", "timeout", "300", "ferese-lock", "before-sleep", "ferese-lock", "lock", "ferese-lock"]
```
-`Ctrl+Alt+Escape` always releases an active client shortcut inhibitor.
+`ferese-lock` uses swaylock/swaylock-effects and optional
+`~/.config/ferese/swaylock.conf`. Test unlock in a nested preview before enabling
+automatic locking. A crashed locker leaves the session locked; recovery requires
+ending the session from another TTY.
-Keyboard-shortcut inhibition is hidden unless a trusted VM or remote-desktop
-session explicitly enables it:
+## Optional session protocols
-```bash
-FERESE_ENABLE_SHORTCUT_INHIBIT=1 cargo run -p ferese
-```
-
-The emergency `Ctrl+Alt+Escape` binding remains available while inhibition is
-active.
-
-Screen capture is hidden by default. A session that has already made an
-explicit capture-policy decision can expose screencopy-v1 to its clients with:
-
-```bash
-FERESE_ENABLE_SCREENCOPY=1 cargo run -p ferese
-```
-
-The current M5 implementation supports one-shot output and region capture into
-ARGB8888 shared-memory buffers. Cursor overlays are included only when the
-capture client requests them.
-
-### Direct DRM session
-
-Run the hardware backend from a spare TTY, outside another graphical session:
-
-```bash
-RUST_LOG=ferese=debug cargo run -p ferese -- --backend=drm -- foot
-```
-
-Do not run that command from a terminal inside the active desktop: the direct
-backend requests control of the seat and primary DRM device. Use
-Ctrl+Alt+F1–F12 to switch virtual terminals.
-
-M4 validation requires the direct session to:
-
-- start `foot` and render the same animated tiling workload as the nested path;
-- retain keyboard and pointer input;
-- pause when switching away and redraw after switching back; and
-- log page-flip timing and any missed presentation deadlines on the reference
- integrated GPU.
-
-The direct path creates an independent render pipeline per connected output and
-rescans connectors on hotplug. Physical unplug/replug acceptance testing on the
-reference hardware is still pending.
+These are launch-time environment switches, not TOML keys. Enable only for
+trusted clients: `FERESE_ENABLE_INPUT_METHOD=1`,
+`FERESE_ENABLE_SHORTCUT_INHIBIT=1`, `FERESE_ENABLE_SCREENCOPY=1`.
+Text input is available by default; Ctrl+Alt+Escape releases an active shortcut
+inhibitor. Use `feresectl --help` for runtime control commands.
diff --git a/docs/desktop-widgets.md b/docs/desktop-widgets.md
new file mode 100644
index 0000000..3796232
--- /dev/null
+++ b/docs/desktop-widgets.md
@@ -0,0 +1,129 @@
+# Desktop widgets
+
+The shell's desktop clock sits above the wallpaper and behind normal windows.
+Drag the clock to reposition it; it reserves no workspace space.
+All desktop widgets share dragging: grab the clock or a note's title. Release
+to save the position, or press Escape to cancel.
+Dragging stops at other visible widgets on the same output; edges can slide
+along each other. Disabled widgets and widgets on other outputs do not block it.
+Each matching output gets a small, transparent surface—not a full-screen canvas.
+Lock-screen rendering remains separate and does not expose the desktop clock.
+
+Enable it in Settings → Desktop widgets or in your Ferese config:
+
+```toml
+[desktop_widgets.clock]
+enabled = true
+outputs = [] # every output; or ["DP-2"] using your own connector
+anchor = "top_left"
+margin_x = 64 # distance from anchored left/right edge
+margin_y = 80 # distance from anchored top/bottom edge
+width = 440
+height = 160
+font_family = "" # empty/omitted follows your desktop font
+bold = false
+time_size = 72.0
+date_size = 18.0
+time_format = "%-I:%M %p" # 12-hour; "%H:%M" for 24-hour, "%H:%M:%S" for seconds
+date_format = "%A, %-d %B"
+time_zone = "" # system zone; or e.g. "Africa/Lagos", "UTC"
+show_date = true
+lowercase = true
+color = "" # theme primary text; or "#ffffff", "#ffffffcc"
+date_color = "" # theme muted text
+opacity = 0.9 # multiplies text and optional background alpha
+alignment = "center" # left, center, right
+gap = 4.0
+padding = 12.0
+background = "" # transparent; or e.g. "#10101080" for a solid tint
+radius = 16.0 # optional background corner radius
+```
+
+Positions: `top_left`, `top_center`, `top_right`, `center_left`, `center`,
+`center_right`, `bottom_left`, `bottom_center`, `bottom_right`.
+Margins affect anchored axes only: a centered horizontal position ignores
+`margin_x`; a centered vertical position ignores `margin_y`.
+All geometry and text sizes use logical pixels and follow the output's scale.
+Increase the widget width/height for longer formats or larger text.
+
+Saving reloads appearance and labels live. Placement, size, output filters and
+enable/disable changes recreate only clock surfaces; normal windows and the bar
+remain intact. Disconnecting an output destroys its clock; reconnecting creates
+one if its name matches the filter. Invalid configuration keeps the last working
+configuration. Theme changes apply to colors/fonts that have no explicit override.
+
+Formatting uses [Jiff's strftime-compatible formats](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html).
+Seconds update on the shell's existing timer. Without seconds, unchanged labels
+produce no additional clock buffer damage; there is no widget animation loop.
+Widget placement can be changed by dragging or through Settings and TOML.
+Sticky notes also support editing directly on the desktop.
+
+## Clock limits
+
+All clock keys are optional. The example enables the clock (disabled by default);
+the other values illustrate defaults or their empty-string equivalents.
+`outputs` is an array of strings; `enabled`, `bold`, `show_date`, `lowercase` are
+booleans; `width`, `height`, and margins are integers. Other sizes/opacity are numbers.
+
+| Fields | Accepted range |
+| --- | --- |
+| `width`, `height` | 64–1600, 32–800 |
+| `margin_x`, `margin_y` | 0–8192 |
+| `time_size`, `date_size` | 8–240, 8–96 |
+| `opacity` | 0–1 |
+| `gap`, `padding`, `radius` | 0–64, 0–64, 0–128 |
+| `font_family`, `time_format`, `date_format` | At most 128 bytes each |
+| `outputs` | At most 32 exact output names, each 1–128 bytes |
+
+Formats must be valid; `time_zone` must be empty or a recognized IANA name.
+Colors are empty (inherit), `#RRGGBB` or `#RRGGBBAA`; all numeric values must be finite.
+
+## Sticky notes
+
+Settings → Desktop widgets → Add note creates a card. Drag its title to move it;
+click Edit for its multiline editor, then Done to finish. Text autosaves after
+500 ms without typing. Escape cancels a drag or finishes editing. Position saves
+on release; dragging changes the anchor to `top_left` and saves pixel margins.
+Title, size and style are also editable in Settings. Notes persist in the config.
+These are plain-text notes, not Markdown or HTML.
+
+```toml
+[[desktop_widgets.notes]]
+id = "today"
+title = "Today"
+text = """
+Review the pull request
+Take a break
+"""
+anchor = "top_right"
+margin_x = 48
+margin_y = 80
+```
+
+Repeat the table for additional notes; IDs must be unique. Cards stay behind windows
+and take keyboard focus only when clicked. Set `interactive = false` for a fully
+click-through card. Long text wraps; increase height if needed. Notes have no idle
+animation loop. Clock and notes share a compact drag preview moved by the
+compositor, not an output-sized canvas. It is released when dragging ends.
+
+| Key | Type | Default / accepted values |
+| --- | --- | --- |
+| `id` | string | `"note"`; unique, 1–64 ASCII letters/digits/`-`/`_` |
+| `enabled` | boolean | `true` |
+| `interactive` | boolean | `true`; drag/edit controls; `false` is click-through |
+| `title` | string | `"Note"`; empty hides it; maximum 256 bytes |
+| `text` | string | `""`; multiline plain text, maximum 16 KiB |
+| `outputs` | string array | `[]` means all; at most 32 exact names, each 1–128 bytes |
+| `anchor` | string | `"top_right"`; same nine positions as clock |
+| `margin_x`, `margin_y` | integer | `48`, `80`; 0–8192; anchored edges only |
+| `width`, `height` | integer | `320`, `240`; 120–1200, 80–1200 |
+| `font_family` | string | omitted/empty follows desktop font; maximum 128 bytes |
+| `text_size`, `title_size` | number | `16`, `18`; 8–96 |
+| `color` | string | omitted/empty/`"theme"` follows primary text; or hex color |
+| `background` | string | omitted/`"theme"` follows surface base; empty is transparent; or hex color |
+| `opacity` | number | `0.9`; 0–1, multiplies text/background alpha |
+| `padding`, `gap` | number | `20`, `8`; 0–64 |
+| `radius` | number | `16`; 0–128 |
+| `alignment` | string | `"left"`; `left`, `center`, `right` |
+
+At most 32 notes; the entire live config is limited to 60 KiB.
diff --git a/docs/ferese-shell-design.md b/docs/ferese-shell-design.md
deleted file mode 100644
index eb887c6..0000000
--- a/docs/ferese-shell-design.md
+++ /dev/null
@@ -1,454 +0,0 @@
-# Ferese Shell Design Specification
-
-Status: Draft for implementation
-Target: Ferese desktop tier
-Normative owner: shell behavior and interaction
-
-Companion documents:
-- `ferese-window-manager-spec.md` — compositor mechanism and policy
-- `ferese-theme-ui-spec.md` — visual tokens and exact component geometry
-
-## 1. Purpose
-
-`ferese-shell` provides the integrated desktop interface around Ferese-managed
-windows.
-
-The initial shell consists of:
-
-- one floating top bar per output;
-- an apps-only launcher;
-- overview controls;
-- top-bar popovers and a compact control center;
-- notifications and notification history; and
-- volume/brightness OSD.
-
-Ferese deliberately provides no persistent dock or bottom application bar.
-
-The shell MUST remain optional. If it crashes, managed windows, layout trees,
-workspaces, focus topology, and client sessions remain valid.
-
-## 2. Document boundaries
-
-This document owns:
-
-```text
-shell visibility
-layer-shell roles
-exclusive-zone behavior
-focus acquisition/restoration
-launcher behavior
-top-bar actions
-notification/DND behavior
-modal interaction
-fullscreen shell behavior
-shell restart behavior
-```
-
-It MUST NOT define raw colors, pixel heights, radii, blur amounts, icon sizes,
-font sizes, or shadows. Those belong to `ferese-theme-ui-spec.md`.
-
-## 3. Surface architecture
-
-The shell uses public layer-shell for normal shell surfaces.
-
-```text
-background(output) background layer, optional
-top_bar(output) top layer, exclusive zone
-launcher(output) overlay layer, exclusive_zone = 0
-notification_stack(output) overlay layer, exclusive_zone = 0
-osd(output) overlay layer, exclusive_zone = 0
-popover(output) overlay/top, exclusive_zone = 0
-modal(output) overlay layer, exclusive_zone = 0
-```
-
-`ferese-shell-v1` is a private control protocol only. It MUST NOT duplicate
-layer-shell geometry or roles.
-
-Its initial responsibilities are:
-
-```text
-window list and lifecycle
-application identity metadata
-focused / urgent state
-activate managed window
-request close
-enter / exit overview
-select overview window / workspace
-```
-
-## 4. Top bar
-
-A top bar exists on every active output unless disabled.
-
-Its conceptual regions are:
-
-```text
-LEFT
- Ferese mark
- workspace switcher
- focused application
-
-CENTER
- clock / date
-
-RIGHT
- notifications
- sound
- power
- control-center entry
-```
-
-The top bar is the only persistent shell surface that reserves workspace space.
-
-Workspace controls MUST use compositor workspace truth and ordinary Ferese
-workspace policy.
-
-The top bar SHOULD remain non-keyboard-interactive during ordinary use.
-
-## 5. Fullscreen behavior
-
-Entering fullscreen hides the top bar visually.
-
-The underlying tiled workspace layout region MUST remain unchanged while
-fullscreen is active.
-
-A temporary top-bar reveal over fullscreen:
-
-```text
-uses overlay presentation
-uses exclusive_zone = 0
-does not resize the fullscreen client
-does not mutate the underlying workspace layout
-```
-
-Leaving fullscreen restores the ordinary top-bar presentation without
-reconfiguring hidden tiled windows merely because the bar reappears.
-
-## 6. Shell reservation lease
-
-If `ferese-shell` disconnects unexpectedly, Ferese SHOULD retain the most recent
-valid Ferese-owned top-bar reservation for a short bounded grace period.
-
-If the shell reconnects before expiry with a compatible reservation, tiled
-windows MUST NOT reflow.
-
-If the grace period expires, Ferese removes the reservation and performs at most
-one ordinary layout reflow.
-
-Default:
-
-```text
-reservation_grace_ms = 2000
-```
-
-## 7. Launcher
-
-The initial launcher is apps-only.
-
-Default invocation:
-
-```text
-Super + Space
-```
-
-The launcher MAY also be opened by a Ferese control in the top bar.
-
-The launcher:
-
-- receives keyboard focus immediately;
-- discovers XDG desktop applications;
-- searches by application name and normalized desktop identity;
-- supports pointer and keyboard navigation;
-- launches selected applications with XDG activation;
-- dismisses on Escape; and
-- restores the previous valid managed-window focus on dismissal.
-
-Initial placeholder:
-
-```text
-Search apps…
-```
-
-The launcher MUST NOT expose dead tabs for files, settings, commands, or recents.
-
-Future search providers MAY extend it without changing the base interaction
-model.
-
-## 8. Application identity and launch state
-
-The launcher consumes the compositor's normalized application identity and launch
-association model.
-
-It MUST NOT assume strings such as:
-
-```text
-firefox
-org.mozilla.firefox
-Mozilla Firefox
-```
-
-are the same application unless desktop-entry or activation metadata establishes
-that relationship.
-
-Ferese does not maintain a persistent running-application strip.
-
-## 9. Top-bar popovers
-
-A top-bar control may own one anchored popover.
-
-Only one primary top-bar popover SHOULD be open per output at a time.
-
-Initial popovers MAY include:
-
-```text
-calendar / time
-sound
-battery / power status
-notifications
-control center
-```
-
-Network and Bluetooth configuration are not part of the initial desktop tier and
-MUST NOT appear as required or placeholder rows.
-
-Opening another primary popover closes the previous one on that output.
-
-## 10. Control center
-
-The initial control center MAY expose only services backed by real providers:
-
-```text
-sound
-battery / power status
-Do Not Disturb
-notification state
-Ferese Settings
-```
-
-Unavailable capabilities are omitted.
-
-## 11. Do Not Disturb
-
-When DND is off:
-
-```text
-eligible notifications may create transient toasts
-notifications enter history according to normal policy
-```
-
-When DND is on:
-
-```text
-normal transient toasts are suppressed
-notifications still enter history
-unread state still updates
-```
-
-A future explicitly defined critical-urgency policy MAY bypass DND.
-
-No application-name heuristic may bypass DND.
-
-DND state persists across shell restart.
-
-## 12. Notifications
-
-`ferese-shell` implements `org.freedesktop.Notifications`.
-
-Initial support:
-
-```text
-summary
-body
-icon metadata
-expiry
-replacement
-close
-actions
-urgency metadata
-```
-
-Notification content is untrusted.
-
-Transient toasts:
-
-- do not reserve workspace space;
-- appear beneath the top bar;
-- obey DND;
-- do not take keyboard focus by default.
-
-## 13. Notification center
-
-Notification history opens from the top bar.
-
-The initial implementation SHOULD use a large anchored popover rather than a
-permanent side drawer.
-
-The center may take shell keyboard focus while interactive and MUST restore the
-previous managed-window focus when dismissed.
-
-## 14. OSD
-
-OSD handles transient state such as:
-
-```text
-volume
-brightness
-microphone mute
-keyboard backlight
-```
-
-OSD surfaces:
-
-```text
-use overlay layer
-use exclusive_zone = 0
-never take keyboard focus
-dismiss automatically
-```
-
-## 15. Overview
-
-`Super+Tab` enters overview.
-
-Managed-window previews remain compositor-owned live scene nodes.
-
-The shell provides:
-
-```text
-selection affordances
-application/window labels
-workspace controls
-optional later search UI
-```
-
-The shell MUST NOT implement overview by continuously copying each managed
-window into the shell process.
-
-Window activation and close actions use `ferese-shell-v1`.
-
-## 16. Modal interaction
-
-Shell modals are reserved for:
-
-```text
-destructive confirmation
-security-sensitive confirmation
-irreversible shell action
-```
-
-Ordinary settings MUST NOT use shell modals.
-
-When a shell modal opens:
-
-```text
-SeatState.keyboard_focus -> FocusTarget::Shell(modal)
-```
-
-The compositor presents the modal above a scrim.
-
-Input outside the modal is consumed by the scrim and MUST NOT reach managed
-applications.
-
-On dismissal, the previous valid managed-window focus is restored.
-
-Ferese does not ship a polkit authentication agent in the initial desktop tier.
-
-## 17. Shell restart
-
-On restart:
-
-```text
-managed windows remain unchanged
-workspace assignments remain unchanged
-layout trees remain unchanged
-managed-window focus remains valid where possible
-```
-
-The restarted shell reconstructs:
-
-```text
-top bars
-workspace indicators
-focused-app metadata
-notification service/state
-DND state
-```
-
-from compositor truth and persistent shell state.
-
-## 18. Shell configuration
-
-Representative behavior-only configuration:
-
-```toml
-[shell]
-enabled = true
-reservation_grace_ms = 2000
-
-[shell.top_bar]
-enabled = true
-hide_in_fullscreen = true
-show_workspaces = true
-show_focused_app = true
-show_clock = true
-
-[shell.launcher]
-enabled = true
-binding = "Super+Space"
-
-[shell.overview]
-enabled = true
-binding = "Super+Tab"
-
-[shell.notifications]
-enabled = true
-do_not_disturb = false
-```
-
-Visual measurements do not belong here.
-
-## 19. First shell implementation slice
-
-```text
-top bar
-launcher
-one top-bar popover
-OSD
-```
-
-Demo:
-
-```text
-1. Start Ferese.
-2. Top bar appears.
-3. Workspace and focused-app state are correct.
-4. Press Super+Space.
-5. Launcher opens and receives focus.
-6. Search for Foot and launch it.
-7. Foot enters the current workspace through normal tiling policy.
-8. Open Sound from the top bar.
-9. Sound popover anchors to its invoking control.
-10. Enter fullscreen.
-11. Top bar hides without changing the underlying tiled layout region.
-12. Reveal top bar temporarily as an overlay.
-13. Exit fullscreen.
-14. Top bar returns to normal reserved presentation.
-15. Restart ferese-shell.
-16. Tiled windows do not reflow during the reservation grace period.
-```
-
-## 20. Completion criteria
-
-The initial shell is complete when:
-
-1. every active output has a functional top bar;
-2. launcher search and application launching work;
-3. workspace indicators reflect compositor truth;
-4. focused application state is accurate;
-5. top-bar popovers anchor correctly;
-6. notifications and DND work;
-7. OSD works without taking focus;
-8. overview uses compositor-owned live windows;
-9. fullscreen hide/reveal does not alter underlying layout geometry;
-10. shell restart does not disturb managed windows; and
-11. all visual presentation is supplied through the theme/UI token system.
diff --git a/docs/ferese-theme-ui-spec.md b/docs/ferese-theme-ui-spec.md
deleted file mode 100644
index 1e822cf..0000000
--- a/docs/ferese-theme-ui-spec.md
+++ /dev/null
@@ -1,822 +0,0 @@
-# Ferese Theme and UI Specification
-
-Status: Draft for implementation
-Target: Ferese desktop tier
-Normative owner: visual tokens and component geometry
-
-Companion documents:
-- `ferese-window-manager-spec.md` — compositor mechanism and policy
-- `ferese-shell-design.md` — shell behavior and interaction
-
-## 1. Purpose
-
-This document defines the Ferese visual language.
-
-It owns:
-
-- desktop background treatment;
-- color tokens;
-- semantic surface tokens;
-- typography;
-- iconography;
-- spacing;
-- exact top-bar geometry;
-- launcher geometry;
-- popover/menu/modal geometry;
-- notification geometry;
-- OSD geometry;
-- settings UI primitives;
-- motion tokens; and
-- glass, translucent, and solid visual profiles.
-
-It does not define shell focus, fullscreen behavior, workspace policy, launcher
-behavior, notification policy, or compositor security.
-
-Ferese intentionally has no persistent dock or bottom application bar.
-
-## 2. Design language
-
-Ferese UI should feel:
-
-```text
-quiet
-precise
-soft
-material
-low-latency
-high-contrast
-spatially consistent
-```
-
-The shell MUST avoid:
-
-- excessive glow;
-- RGB/gamer styling;
-- ornamental gradients;
-- gratuitous transparency;
-- oversized shadows;
-- arbitrary component styling;
-- large chrome that steals workspace area; and
-- pixel-for-pixel imitation of another operating system.
-
-Ferese must still look complete with blur disabled.
-
-## 3. Theme model
-
-Components request semantic surface roles:
-
-```text
-surface.panel
-surface.panel_elevated
-surface.popover
-surface.menu
-surface.notification
-surface.hud
-surface.modal
-```
-
-The active profile maps each role to material parameters.
-
-Themes change appearance, not component structure or behavior.
-
-## 4. Material styles
-
-```rust
-pub enum MaterialStyle {
- Glass,
- Translucent,
- Solid,
-}
-```
-
-### Glass
-
-Backdrop blur plus restrained tint, saturation, brightness, and optional noise.
-
-### Translucent
-
-Alpha-composited semantic tint with no backdrop blur.
-
-### Solid
-
-Opaque semantic surface with identical geometry.
-
-Changing material style MUST NOT move, resize, or restructure shell components.
-
-## 5. Built-in profiles
-
-```text
-ferese-glass
-ferese-translucent
-ferese-solid
-```
-
-All share:
-
-```text
-component geometry
-spacing
-typography
-icon metrics
-interaction states
-motion semantics
-```
-
-## 6. Background
-
-Supported:
-
-```text
-solid color
-image fill
-image fit
-```
-
-```toml
-[theme.background]
-path = "/path/to/wallpaper.png"
-mode = "fill" # fill or fit
-```
-
-Ferese SHOULD NOT globally blur the wallpaper.
-
-Optional restrained adjustments MAY include:
-
-```text
-brightness
-dimming
-saturation
-```
-
-Default fallback:
-
-```text
-#0B0F14
-```
-
-## 7. Color tokens
-
-Representative dark profile:
-
-```toml
-[theme.colors]
-desktop_background = "#0B0F14"
-
-surface_base = "#111821"
-surface_raised = "#18212C"
-surface_elevated = "#202B37"
-
-text_primary = "#F4F7FB"
-text_secondary = "#B7C0CB"
-text_muted = "#7F8A98"
-text_disabled = "#58616D"
-
-accent = "#5B8CFF"
-accent_soft = "#8EAFFF"
-accent_text = "#FFFFFF"
-
-success = "#4CC38A"
-warning = "#F0B45B"
-danger = "#F06A6A"
-
-border = "#FFFFFF18"
-border_strong = "#FFFFFF2A"
-
-shadow = "#00000055"
-scrim = "#05070AB8"
-```
-
-## 8. Surface tokens
-
-Representative glass profile:
-
-```toml
-[theme.surface.panel]
-style = "glass"
-opacity = 0.78
-blur = 24.0
-saturation = 1.08
-brightness = 1.02
-noise = 0.012
-border_opacity = 0.10
-shadow = "soft"
-
-[theme.surface.panel_elevated]
-style = "glass"
-opacity = 0.84
-blur = 28.0
-saturation = 1.10
-brightness = 1.03
-noise = 0.014
-border_opacity = 0.12
-shadow = "elevated"
-
-[theme.surface.popover]
-style = "glass"
-opacity = 0.84
-blur = 28.0
-saturation = 1.10
-brightness = 1.03
-noise = 0.014
-border_opacity = 0.12
-shadow = "elevated"
-
-[theme.surface.hud]
-style = "glass"
-opacity = 0.88
-blur = 30.0
-saturation = 1.06
-brightness = 1.04
-noise = 0.010
-border_opacity = 0.12
-shadow = "elevated"
-```
-
-## 9. Geometry tokens
-
-```toml
-[theme.geometry]
-base_unit = 8.0
-
-window_radius = 14.0
-
-top_bar_height = 38.0
-top_bar_margin_top = 4.0
-top_bar_margin_horizontal = 10.0
-top_bar_radius = 19.0
-
-launcher_width = 560.0
-launcher_radius = 20.0
-
-popover_radius = 16.0
-menu_radius = 14.0
-modal_radius = 18.0
-hud_radius = 18.0
-
-panel_padding = 12.0
-control_gap = 8.0
-section_gap = 16.0
-```
-
-Only the top bar consumes persistent shell space.
-
-## 10. Typography
-
-```toml
-[theme.typography]
-font_family = "sans-serif"
-```
-
-Initial roles:
-
-```text
-caption 11-12 px
-body 13-14 px
-body-strong 13-14 px medium
-label 12-13 px medium
-title 16-18 px semibold
-display 22-28 px semibold
-```
-
-Use the system UI sans-serif by default.
-
-The configured family applies consistently to shell text. Icon assets use the
-theme icon set and are sized from the same scale tokens rather than text glyph
-metrics.
-
-Clocks and changing numeric values SHOULD use tabular numerals where available.
-
-## 11. Iconography
-
-System icons are monochrome vectors.
-
-```text
-visual box 16-20 px
-stroke approximately 1.5-2 px
-style simple and optically balanced
-```
-
-Application icons retain original artwork.
-
-System icons MUST NOT use arbitrary gradients.
-
-## 12. Top bar
-
-Default composition:
-
-```text
-╭──────────────────────────────────────────────────────────────╮
-│ F 1 2 3 4 Zed 09:41 🔊 ◉ │
-╰──────────────────────────────────────────────────────────────╯
-```
-
-Surface role:
-
-```text
-surface.panel
-```
-
-### Workspace state
-
-Inactive workspaces use muted text.
-
-Active workspace uses a compact filled or accent-backed indicator.
-
-### Focused application
-
-May display:
-
-```text
-application icon
-application name
-```
-
-Full changing window titles are not part of the default bar.
-
-### Clock
-
-Compact:
-
-```text
-09:41
-```
-
-Optional wide:
-
-```text
-Mon, Sep 23 09:41
-```
-
-## 13. Launcher
-
-The launcher is the primary application-selection surface.
-
-Default placement:
-
-```text
-centered horizontally
-upper-middle or center of usable workspace
-```
-
-Surface role:
-
-```text
-surface.panel_elevated
-```
-
-Default geometry:
-
-```text
-width theme.geometry.launcher_width
-max height 70% of usable output
-radius theme.geometry.launcher_radius
-```
-
-Initial content:
-
-```text
-╭──────────────────────────────────────╮
-│ Search apps… │
-├──────────────────────────────────────┤
-│ │
-│ Zed Firefox Foot │
-│ Files Settings Music │
-│ │
-╰──────────────────────────────────────╯
-```
-
-Initial implementation is apps-only.
-
-## 14. Launcher search field
-
-```text
-height 44-48 px
-radius 12 px
-```
-
-Placeholder:
-
-```text
-Search apps…
-```
-
-The search field is the visual focus.
-
-## 15. Launcher application grid
-
-Recommended:
-
-```text
-4-6 columns depending on launcher width
-```
-
-Entries show:
-
-```text
-application icon
-one-line application name
-```
-
-Long names truncate with ellipsis.
-
-## 16. Top-bar popovers
-
-Surface:
-
-```text
-surface.popover
-```
-
-Recommended:
-
-```text
-width 280-360 px
-padding 12 px
-radius theme.geometry.popover_radius
-anchor gap 8 px
-```
-
-They visually align with the invoking top-bar control.
-
-## 17. Control center
-
-Representative:
-
-```text
-╭──────────────────────────────╮
-│ Sound │
-│ ━━━━━━━━━━━●━━━━━━ │
-│ │
-│ Battery 92% │
-│ Do Not Disturb Off │
-│ Notifications 3 │
-│ │
-│ Ferese Settings │
-╰──────────────────────────────╯
-```
-
-Network/Bluetooth rows are not part of the initial visual contract.
-
-## 18. Menus
-
-Menus use:
-
-```text
-surface.menu
-```
-
-Row:
-
-```text
-icon label shortcut
-```
-
-Disabled items use `text_disabled`.
-
-## 19. Modals
-
-Modals use:
-
-```text
-surface.modal
-```
-
-Default width:
-
-```text
-360-480 px
-```
-
-Background scrim uses `theme.colors.scrim`.
-
-Modal visuals do not imply a Ferese authentication agent.
-
-## 20. Notifications
-
-Toast surface:
-
-```text
-surface.notification
-```
-
-Recommended width:
-
-```text
-320-360 px
-```
-
-Structure:
-
-```text
-icon
-summary
-body
-time
-optional actions
-```
-
-Newest appears nearest the top bar.
-
-## 21. OSD
-
-Surface:
-
-```text
-surface.hud
-```
-
-Example:
-
-```text
-╭──────────────────────╮
-│ 🔊 ━━━━━━━●━━━━━ │
-╰──────────────────────╯
-```
-
-Default placement is centered or lower-middle.
-
-## 22. Overview chrome
-
-Managed windows remain live compositor surfaces.
-
-Theme/UI owns only:
-
-```text
-selection ring
-labels
-workspace strip
-scrim
-overview control surfaces
-```
-
-Selected preview uses an accent ring, not a large glow.
-
-## 23. Interaction states
-
-Controls support:
-
-```text
-rest
-hover
-pressed
-focused
-selected
-disabled
-urgent where applicable
-```
-
-Hover changes color/contrast only.
-
-Pressed controls MAY use a tiny local scale response around `0.98`.
-
-There is no dock-style magnification behavior.
-
-## 24. Focus rings
-
-Keyboard focus:
-
-```text
-2 px accent ring
-2 px visual separation where needed
-```
-
-Focus rings MUST NOT alter component geometry.
-
-## 25. Shadows
-
-Levels:
-
-```text
-none
-soft
-elevated
-```
-
-Representative:
-
-```toml
-[theme.shadow.soft]
-offset_y = 4.0
-blur = 18.0
-opacity = 0.20
-
-[theme.shadow.elevated]
-offset_y = 10.0
-blur = 36.0
-opacity = 0.28
-```
-
-Top bar uses `soft`.
-
-Launcher, popovers, notifications, modals, and HUDs typically use `elevated`.
-
-## 26. Borders
-
-Default:
-
-```text
-1 logical px
-```
-
-Glass uses subtle highlight borders.
-
-Solid may use slightly stronger borders.
-
-Borders MUST NOT resemble neon outlines.
-
-## 27. Motion tokens
-
-```text
-spatial spring
-bounded easing
-```
-
-Approximate ranges:
-
-```text
-micro feedback 80-120 ms
-popover/menu 140-200 ms
-launcher 180-240 ms
-notification 160-220 ms
-```
-
-Managed-window and overview motion remains compositor spring-driven.
-
-## 28. Reduced motion
-
-Reduced motion:
-
-```text
-removes decorative scale
-shortens/removes large shell translation
-uses opacity where spatial context is unnecessary
-does not animate blur intensity
-```
-
-## 29. Fullscreen visual presentation
-
-When fullscreen is active, the top bar is visually absent.
-
-Temporary reveal uses the same visual tokens without implying any layout change.
-
-## 30. Settings application
-
-Ferese Settings consumes the same tokens.
-
-Representative layout:
-
-```text
-╭──────────────────────────────────────────────╮
-│ sidebar │ content │
-│ │ │
-│ General │ Appearance │
-│ Appearance │ │
-│ Tiling │ live preview │
-│ Workspaces │ controls │
-│ Shortcuts │ │
-│ Input │ │
-╰──────────────────────────────────────────────╯
-```
-
-## 31. Appearance settings
-
-Expose:
-
-```text
-material style
-accent
-corner radius
-shadow intensity
-glass blur
-glass opacity
-animation speed
-reduced motion
-```
-
-Material style:
-
-```text
-Glass
-Translucent
-Solid
-```
-
-Changing style MUST preserve geometry.
-
-## 32. Theme configuration
-
-```toml
-[theme]
-profile = "ferese-glass"
-mode = "dark"
-
-[theme.colors]
-accent = "#5B8CFF"
-
-[theme.material]
-style = "glass"
-
-[theme.material.glass]
-blur = 24.0
-opacity = 0.80
-saturation = 1.08
-noise = 0.012
-
-[theme.geometry]
-window_radius = 14.0
-top_bar_height = 38.0
-top_bar_radius = 19.0
-launcher_width = 560.0
-launcher_radius = 20.0
-
-[theme.motion]
-speed = 1.0
-reduced_motion = false
-```
-
-## 33. Validation
-
-Reject:
-
-```text
-non-finite dimensions
-negative radii
-negative blur
-opacity outside 0..1
-invalid colors
-unknown material style
-invalid font sizes
-invalid motion factors
-```
-
-Theme changes use the same transactional Ferese configuration path.
-
-## 34. Custom theme scope
-
-Themes MAY change:
-
-```text
-colors
-material parameters
-radii
-shadows
-typography
-accent
-icon tint
-motion scale
-validated component geometry tokens
-```
-
-Themes MUST NOT change:
-
-```text
-top-bar behavior
-launcher behavior
-focus behavior
-workspace behavior
-notification policy
-Wayland roles
-security behavior
-```
-
-Ferese intentionally does not expose arbitrary CSS.
-
-## 35. Performance degradation
-
-Priority:
-
-```text
-1. geometry correctness
-2. input latency
-3. text/icon clarity
-4. animation continuity
-5. shadow quality
-6. blur quality
-7. decorative effects
-```
-
-Allowed degradation:
-
-```text
-glass
- -> lower-resolution glass
- -> translucent
- -> solid
-```
-
-Component geometry remains unchanged.
-
-## 36. Visual acceptance criteria
-
-Implementation succeeds when:
-
-1. top bar, launcher, popovers, notifications, OSD, overview chrome, and Settings
- share one spacing/material/typography system;
-2. glass, translucent, and solid profiles preserve geometry;
-3. typography remains readable on bright and dark backgrounds;
-4. icons have consistent optical weight;
-5. launcher and popovers look anchored and intentional;
-6. focus/selection/urgent states remain distinct without relying on glow;
-7. disabling blur does not make the desktop look unfinished;
-8. reduced-motion mode remains coherent; and
-9. the desktop feels complete without a persistent dock or bottom application bar.
diff --git a/docs/ferese-window-manager-spec.md b/docs/ferese-window-manager-spec.md
deleted file mode 100644
index ec27b57..0000000
--- a/docs/ferese-window-manager-spec.md
+++ /dev/null
@@ -1,1678 +0,0 @@
-# Ferese Window Manager Specification
-
-Status: Draft for implementation
-Target: Ferese v0 core and desktop-tier roadmap
-Language: Rust
-Display protocol: Wayland
-
-## 1. Purpose
-
-Ferese is a Wayland scrolling compositor for a conventional, keyboard-friendly
-desktop. It combines predictable window management with compositor-owned rounded
-geometry, shadows, animation, transparency, and semantic surface materials.
-
-This document is the implementation source of truth for Ferese v0 and records
-the gated desktop-tier roadmap. The terms **MUST**, **MUST NOT**, **SHOULD**,
-**SHOULD NOT**, and **MAY** are normative within the tier being specified.
-
-## 2. Product principles
-
-1. Window-management behavior is predictable and familiar.
-2. The compositor owns composition effects; clients request semantic intent.
-3. Core state has one authoritative representation.
-4. Rendering and animation never block input.
-5. Optional shell processes may fail without terminating the compositor.
-6. Effects degrade gracefully when performance requires it.
-7. The nested compositor is the primary development environment.
-8. Security-sensitive operations are unavailable to untrusted clients.
-
-### 2.1 Companion specification ownership
-
-Ferese uses three normative documents with non-overlapping ownership:
-
-| Concern | Normative owner |
-| --- | --- |
-| compositor mechanism, Wayland policy, focus, layout, rendering, IPC, security | this document |
-| shell interaction, visibility, launcher, notifications, DND, shell focus | `ferese-shell-design.md` |
-| colors, semantic surface tokens, typography, icons, and exact shell component geometry | `ferese-theme-ui-spec.md` |
-
-The shell specification MUST reference visual tokens instead of duplicating
-pixel, radius, color, shadow, blur, or typography values. The theme/UI
-specification MUST NOT redefine focus, fullscreen, workspace, launcher,
-notification, or security behavior.
-
-If companion documents conflict, the owner in this table is authoritative for
-that concern.
-
-## 3. Scope
-
-### 3.1 v0 core requirements
-
-Ferese v0 is the smallest independently usable native-Wayland release. It MUST
-provide:
-
-- native Wayland compositing;
-- automatic scrolling columns with optional tree tiling;
-- floating and fullscreen windows;
-- multiple workspaces and outputs;
-- keyboard and pointer input;
-- native Wayland clipboard, primary selection, and drag-and-drop;
-- client-side and server-side decoration negotiation;
-- rounded clipping, borders, shadows, and transparency;
-- frame-timestamp-driven window and workspace animations;
-- damage-aware rendering;
-- validated live configuration reload;
-- authenticated local IPC and a command-line client;
-- direct application launching;
-- standard layer-shell support under the policy in Section 8;
-- a direct DRM/KMS session; and
-- the required native protocol baseline in Section 8.1.
-
-The completed v0 MUST support routine native-Wayland software-development work
-without regular compositor restarts. The graphical Ferese shell is not required
-for basic window management or application launching.
-
-### 3.2 Desktop tier
-
-The desktop tier builds on the v0 core and includes:
-
-- semantic surface materials with glass, translucent, and solid rendering;
-- the Ferese top bar, overview, launcher UI, workspace UI, OSD, and notifications;
-- the graphical settings application;
-- portal-backed screenshots and screen sharing; and
-- X11 compatibility through `xwayland-satellite` unless an architectural
- decision record selects another design.
-
-These features MUST NOT delay the v0 core. Each MUST degrade or fail without
-terminating the compositor or invalidating managed-window state.
-
-### 3.3 Non-goals
-
-The following are outside v0:
-
-- a file manager, display manager, or lock screen;
-- network, Bluetooth, or package-management UI in v0 or the initial desktop-tier shell;
-- a general plugin or scripting system, or arbitrary CSS/component-replacement theme engine;
-- desktop widgets;
-- advanced dynamic layout algorithms;
-- exhaustive support for every Wayland protocol or toolkit edge case; and
-- pixel-for-pixel imitation of another operating system.
-
-Ferese MAY display a configured color or image, but a standalone wallpaper
-daemon is not part of v0.
-
-## 4. Architecture
-
-### 4.1 Processes
-
-| Executable | Responsibility |
-| --- | --- |
-| `ferese` | Compositing, policy, input, scene construction, and rendering |
-| `ferese-shell` | Top bar, overview controls, launcher, workspace UI, notifications, and OSD |
-| `ferese-settings` | Graphical configuration editor |
-| `feresectl` | Command-line IPC client |
-
-`ferese` MUST remain usable if another Ferese process exits. Shell and settings
-code MUST NOT run inside the compositor.
-
-```text
-Wayland clients (plus optional X11 satellite)
- |
- v
- protocol adapters
- |
- v
- authoritative core state
- |
- +----> layout targets
- +----> animation state
- |
- v
- scene graph
- |
- v
- damage-aware renderer
- |
- v
- nested window or DRM/KMS output
-```
-
-Protocol handlers MUST translate requests into domain operations. Business
-logic MUST NOT accumulate in Smithay callbacks.
-
-### 4.2 Technology baseline
-
-The initial implementation uses Rust, Smithay, calloop, Smithay's GLES renderer,
-`glow`, targeted GLSL shaders, DRM/KMS, GBM, EGL, dma-buf, libinput, xkbcommon,
-libseat, serde, TOML, serde JSON, clap, tracing, and GPUI.
-
-Dependencies MAY change when an architectural decision record explains the
-reason and migration impact.
-
-### 4.3 Repository layout
-
-```text
-ferese/
-|-- Cargo.toml
-|-- crates/
-| |-- ferese-core/
-| |-- ferese-layout/
-| |-- ferese-render/
-| |-- ferese-animation/
-| |-- ferese-config/
-| |-- ferese-ipc/
-| |-- ferese-protocols/
-| `-- ferese-shell-protocol/
-|-- compositor/
-|-- shell/
-|-- settings/
-|-- feresectl/
-`-- protocols/
- |-- ferese-effects-v1.xml
- `-- ferese-shell-v1.xml
-```
-
-`ferese-layout` MUST be pure Rust and MUST NOT depend on Smithay or Wayland
-objects.
-
-### 4.4 Domain operations
-
-Wayland callbacks, input handlers, IPC handlers, configuration reload, and shell
-requests MUST enter core policy through explicit domain operations rather than
-mutating unrelated managers directly. A representative shape is:
-
-```rust
-pub enum Command {
- Focus(Direction),
- Move(Direction),
- Resize(Direction, f32),
- SwitchWorkspace(WorkspaceId),
- MoveToWorkspace(WindowId, WorkspaceId),
- ToggleFloating(WindowId),
- ToggleFullscreen(WindowId),
- SetConfig(ConfigChange),
-}
-
-pub enum Event {
- WindowMapped(WindowMapData),
- WindowUnmapped(WindowId),
- OutputConnected(OutputDescriptor),
- OutputDisconnected(OutputId),
- SurfaceCommitted(SurfaceCommit),
-}
-
-pub struct WindowMapData {
- pub window: WindowId,
- pub surface: SurfaceId,
- pub app_id: Option,
- pub title: Option,
- pub parent: Option,
- pub requested_fullscreen: bool,
- pub constraints: SizeConstraints,
-}
-```
-
-A command or event is applied as one state transaction. Commands express
-requested policy changes; events describe facts received from protocol or
-backend adapters and include the metadata needed to apply policy. Neither type
-MUST be used for emitted notifications.
-
-The transaction MUST either
-complete with all Section 5 invariants restored or leave authoritative state
-unchanged. Rendering, protocol configure requests, IPC events, and persistence
-are consequences emitted after the state transition; they MUST NOT become a
-second source of policy state.
-
-### 4.5 Threading and execution
-
-The calloop thread owns the Wayland display, input dispatch, authoritative
-`FereseState`, policy transactions, configure scheduling, and scene publication.
-No other thread may mutate compositor state or Wayland objects.
-
-Shader compilation, image decoding, configuration file I/O, IPC serialization,
-and other bounded background work MAY run on workers. Workers receive immutable
-snapshots or owned messages and return results through bounded channels. The
-calloop thread validates each result against current identifiers and generations
-before applying it. Rendering MAY run on the calloop thread initially; if moved
-to a render thread, scene snapshots and buffer ownership MUST cross an explicit
-handoff and never require input dispatch to wait for GPU completion.
-
-Blocking filesystem, process-wait, D-Bus, portal, and client IPC operations MUST
-NOT run on the calloop thread.
-
-## 5. Authoritative state
-
-The compositor MUST maintain one state root:
-
-```rust
-pub struct FereseState {
- pub outputs: OutputManager,
- pub workspaces: WorkspaceManager,
- pub windows: WindowManager,
- pub seat: SeatState,
- pub layout: LayoutManager,
- pub animations: AnimationManager,
- pub config: Config,
-}
-```
-
-Smithay objects SHOULD remain behind adapter types rather than become the domain
-model.
-
-### 5.1 Identifiers
-
-Outputs, workspaces, windows, layout nodes, and the logical seat MUST have stable
-internal identifiers. An identifier MUST NOT be reused during one compositor
-lifetime.
-
-```rust
-pub struct WindowId(u64);
-pub struct WorkspaceId(u64);
-pub struct OutputId(u64);
-pub struct NodeId(u64);
-pub struct SeatId(u64);
-pub struct SurfaceId(u64);
-```
-
-Ferese v0 exposes exactly one logical seat. Multiple physical keyboards, mice,
-and touch devices MAY feed that seat. Multi-seat policy is outside v0 and MUST
-NOT be implied by the domain model.
-
-### 5.2 Windows
-
-```rust
-pub struct Window {
- pub id: WindowId,
- pub app_id: Option,
- pub title: Option,
- pub workspace: WorkspaceId,
- pub placement: WindowPlacement,
- pub fullscreen: bool,
- pub constraints: SizeConstraints,
- pub urgent: bool,
- pub material: WindowMaterial,
-}
-
-pub enum WindowPlacement {
- Layout,
- Floating { rect: RectF },
-}
-```
-
-Fullscreen is a presentation state layered over the preserved layout or floating
-placement. Entering fullscreen MUST NOT remove a managed window from its
-workspace layout or a floating window from its floating list. At most one window
-may be fullscreen on a workspace. While fullscreen is active, that window is the
-sole normal managed-window content presented for the workspace, although
-permitted shell overlays and popups may still appear. Leaving fullscreen
-restores the preserved placement without reconstructing it.
-
-A window MUST NOT independently store an output. Its output is derived from its
-workspace, preventing stale ownership after workspace migration. Authoritative
-keyboard focus belongs to `SeatState` and MUST NOT be duplicated on every
-window or workspace. Layout hierarchy MUST NOT be embedded in `Window`.
-
-### 5.3 Workspaces
-
-```rust
-pub struct Workspace {
- pub id: WorkspaceId,
- pub name: String,
- pub output: Option,
- pub layout: WorkspaceLayout,
- pub floating: Vec,
- pub last_focused: Option,
-}
-
-pub struct SeatState {
- pub id: SeatId,
- pub keyboard_focus: FocusTarget,
- pub pointer_focus: Option,
- pub focused_output: Option,
- pub focused_workspace: Option,
-}
-
-pub enum FocusTarget {
- None,
- Window(WindowId),
- Shell(SurfaceId),
-}
-```
-
-`Workspace::last_focused` is restoration/history state only. It MUST NOT be
-interpreted as current keyboard focus.
-
-These invariants MUST hold after every state transaction:
-
-1. Every managed window belongs to exactly one workspace.
-2. Every attached workspace belongs to exactly one connected output; only a
- workspace awaiting an output may contain `None`.
-3. An output exposes exactly one active workspace.
-4. A window with `WindowPlacement::Layout` appears exactly once in its active
- workspace layout and not in the floating list.
-5. A window with `WindowPlacement::Floating` appears exactly once in the
- floating list and not in the workspace layout.
-6. Fullscreen does not alter the window's underlying placement membership.
-7. A `FocusTarget::Window` refers to a managed window on
- `seat.focused_workspace`; a `FocusTarget::Shell` refers to a live, authorized
- interactive shell surface.
-8. `Workspace::last_focused`, when present, refers to a window belonging to that
- workspace.
-9. At most one window is fullscreen on a workspace at a time.
-
-Workspace migration MUST be atomic. If an output disappears, its workspaces move
-to a deterministic remaining output. If none remains, they stay detached until
-an output appears.
-
-Numeric workspace names are unbounded, but the default bindings directly address
-`1` through `9` and create them lazily. Named workspaces MAY be added after v0.
-
-### 5.4 Focus semantics
-
-The single v0 seat owns authoritative keyboard focus, pointer focus, focused
-workspace, and focused output. A workspace stores only `last_focused` so focus
-can be restored when that workspace becomes active.
-
-When switching to a workspace that is already visible on another output, Ferese
-MUST focus that output and workspace instead of moving or swapping the workspace.
-Explicit workspace-move commands are responsible for relocating workspaces.
-
-Mapping a normal application while another window is fullscreen MUST preserve
-the fullscreen window and place the new window into the underlying layout or
-floating list without revealing or focusing it. An urgent indication MAY be
-shown. A transient with a valid parent is floating by default, centered over and
-constrained to the parent's visible output, and follows the parent's workspace.
-Activation policy, not mapping alone, determines whether a transient receives
-focus.
-
-Window rules MAY match normalized `app_id`, title, and transient metadata and
-set initial workspace, floating state, dimensions, or fullscreen state. Rules
-MUST be evaluated deterministically before insertion. Title matching SHOULD be
-avoided when a stable `app_id` exists.
-
-### 5.5 Application identity and launch association
-
-Ferese exposes normalized application identity to trusted shell clients without
-making application identity part of layout policy.
-
-```rust
-pub struct ApplicationIdentity {
- pub desktop_id: Option,
- pub app_id: Option,
- pub startup_wm_class: Option,
-}
-
-pub struct LaunchAssociation {
- pub id: LaunchId,
- pub desktop_id: Option,
- pub activation_token: Option,
- pub child_pid: Option,
- pub started_at: Instant,
-}
-```
-
-Application identity resolution MUST prefer explicit desktop-entry association,
-then normalized Wayland `app_id`, then `StartupWMClass`/X11 class metadata when
-available. Ferese MUST NOT guess that unrelated strings are equivalent merely
-because they are similar. Case and an optional `.desktop` suffix MAY be
-normalized only where desktop-entry semantics make that comparison unambiguous.
-
-A shell-initiated launch MUST be tracked until the resulting window is
-associated or the launch expires. XDG activation information is the preferred
-association; process ancestry and normalized application identity MAY be used as
-fallbacks. The association exists to drive shell state such as `launching`,
-`running`, and focus. It MUST NOT bypass ordinary window rules or workspace
-policy.
-
-## 6. Layout
-
-Each workspace has an independent layout mode. `Scrolling` is the default;
-`Tree` is an optional traditional tiling mode. Changing modes MUST preserve
-window membership, focus history, floating geometry, and fullscreen state.
-
-```rust
-pub enum WorkspaceLayout {
- Scrolling(ScrollingLayout),
- Tree(LayoutTree),
-}
-
-pub struct ScrollingLayout {
- pub columns: Vec,
- pub active_column: Option,
- pub viewport_x: f64,
-}
-
-pub struct Column {
- pub windows: Vec,
- pub active: usize,
- pub width: ColumnWidth,
- pub heights: Vec,
-}
-
-pub enum ColumnWidth {
- Proportion(f32),
- Fixed(f32),
- Full,
-}
-```
-
-Scrolling layouts maintain these invariants after every transaction:
-
-1. Empty columns do not exist, and every managed non-floating window appears in
- exactly one column.
-2. `active_column` is `None` exactly when the workspace has no columns;
- otherwise it and every column's `active` index are in bounds.
-3. Proportional and fixed widths are positive and finite. Height weights are
- positive, finite, match the column's window count, and normalize
- deterministically.
-4. `viewport_x` is finite. It is an unbounded world-space offset and MUST NOT be
- clamped to the populated column strip.
-
-Scrolling columns form a horizontal strip that MAY be wider than the usable
-output. Windows within a column divide its height; there is no vertical
-scrolling. Window geometry uses stable workspace coordinates and presentation
-subtracts the workspace viewport:
-
-```text
-screen_x = window_world_x - viewport_x
-```
-
-Navigation animates one viewport spring per visible scrolling workspace.
-Existing windows MUST NOT receive independent horizontal motion for the same
-navigation. Retargeting while the viewport is moving preserves its current
-interpolated position and velocity; viewport moves are not queued.
-
-Viewport translation uses a dedicated critically damped spring, separate from
-window geometry:
-
-```text
-mass = 1.0
-stiffness = 320.0
-damping_ratio = 1.0
-damping = 2 * damping_ratio * sqrt(stiffness * mass) # approximately 35.8
-```
-
-It MUST NOT visibly overshoot. Rapid navigation retargets this same spring
-immediately and may coalesce commands received before the next presented frame.
-Same-direction velocity is preserved. Direction reversal preserves the current
-position, reduces opposing velocity when necessary, and reverses without a
-snap.
-
-New normal windows insert immediately after the focused column at their final
-world position and become focused. The compositor then computes the minimum
-viewport movement needed to expose the new column. A small local reveal using
-opacity, scale, or an x offset MAY supplement this motion, but the viewport is
-the primary animation.
-
-When the focused column closes, focus moves to an adjacent column and the
-viewport retargets to reveal it. Closing a non-focused column MUST NOT move the
-viewport. Manual column resize also MUST NOT retarget it; a subsequent
-navigation, open, or focused-close operation may do so.
-
-During an in-flight viewport transition, resize leaves the viewport target and
-motion intact while affected world geometry uses the normal window spring. The
-composed transform is:
-
-```text
-screen_x = animated_world_x - animated_viewport_x
-```
-
-Resize MUST NOT restart or cancel the viewport spring. If reflow would make the
-focused window completely unreachable, Ferese MAY apply only the minimum
-visibility correction, not normal comfort-region recentering.
-
-The default reveal strategy avoids scrolling when the focused column is already
-fully visible. Otherwise it moves only enough to place the column in a comfort
-region approximately 20% through 80% of the viewport. It MUST NOT center every
-focus change. `focus_strategy = "center_on_focus"` selects that alternative,
-and the explicit center command always centers the focused column. When a large
-column cannot fit the comfort region, Ferese SHOULD retain 40–80 logical pixels
-of the previous column where possible.
-
-Each scrolling workspace owns its viewport. Outputs animate their hosted
-workspaces independently; neither columns nor a viewport span outputs.
-
-Opening, closing, moving, or resizing a window MUST preserve a stable visual
-anchor: columns before the focused column do not jump merely because a later
-column changed. Each workspace stores and restores its own viewport. Output
-resize or migration preserves that viewport without imposing strip bounds. Constraint
-resolution MUST prefer the focused column, preserve user-selected widths where
-possible, and report conflicts rather than silently compressing every column to
-fit. These stable anchors, deterministic constraints, and retained neighboring
-context are required Ferese behavior, not optional polish.
-
-The first normal window creates the first column. A new normal window creates a
-column after the focused column by default. Commands and drag targets can insert
-before or after a column, move a window into an existing column, extract it into
-a new column, cycle standard widths, set a precise width, or redistribute
-heights within a column. Full-width columns occupy the viewport but do not alter
-neighboring column widths.
-
-### 5.1 Scrolling column zoom motion
-
-Cycling a scrolling column between a proportional width and full width is a
-layout morph, not a camera-scale effect. The focused column's visual allocation
-animates with the normal geometry spring while the scrolling viewport uses its
-dedicated critically damped spring. Ferese sends the final client size once and
-may scale and clip the latest valid buffer inside the animated allocation until
-the matching commit arrives.
-
-For two half-width columns, zooming the left column expands it in place and
-pushes the right column out through the right edge. Zooming the right column
-retargets the viewport so it expands into the viewport while the left column
-leaves through the left edge. Zoom-out continuously reverses these
-relationships: a right neighbor returns from the right and a left neighbor
-returns from the left. A neighbor remains a live window and MUST NOT fade,
-teleport, use a screenshot substitute, or receive an unrelated translation.
-
-The presented horizontal position remains the composition:
-
-```text
-screen_x = animated_world_x - animated_viewport_x
-```
-
-Width-cycle viewport targets expose the final scrolling layout. If the complete
-column strip fits, the viewport returns to the strip origin. A full-width
-focused column aligns to the usable viewport; a smaller focused column in a
-larger strip keeps as much real left-side context as fits. This retargeting is
-specific to column zoom. Pointer-driven and directional manual resize continue
-toward the existing viewport target and do not trigger comfort-region
-recentering.
-
-Zoom is interruptible. A reversed width cycle or focus change retargets active
-geometry and viewport springs from their current positions and velocities; it
-MUST NOT finish or snap through the previous target first. Hit testing follows
-presented geometry. Reduced motion shortens the settle while preserving spatial
-direction, and disabling animations snaps directly to the final layout.
-
-Tests MUST cover half-plus-half zoom and zoom-out for either focused column,
-neighbor exit and return direction, bounded focused-anchor drift, interrupted
-and reversed transitions, focus changes during motion, geometry/viewport
-composition, one final configure rather than per-frame configure traffic,
-animated hit testing, reduced motion, and animation-disabled behavior.
-
-Mode conversion is deterministic. Scrolling-to-tree conversion builds
-horizontal structure from columns and vertical structure from windows within a
-column, preserving reading order and size proportions. Tree-to-scrolling
-conversion traverses visible leaves in left-to-right, top-to-bottom order and
-initially creates one column per leaf. Conversion preserves membership and
-focus, but users should not expect mode-specific grouping metadata to survive a
-round trip.
-
-Tree mode uses the following node model:
-
-```rust
-pub enum Node {
- Window(WindowId),
- Split {
- axis: Axis,
- ratio: f32,
- first: NodeId,
- second: NodeId,
- },
- Stack {
- children: Vec,
- active: usize,
- },
-}
-
-pub enum Axis {
- Horizontal,
- Vertical,
-}
-```
-
-Split ratios MUST be finite and clamped so descendants satisfy their minimum
-sizes where possible. If all constraints cannot be met, the focused window takes
-priority and the conflict MUST be logged.
-
-The engine consumes a workspace layout, usable logical geometry, size
-constraints, and mode-specific configuration. It returns a deterministic
-`WindowId -> RectF` map, viewport metadata, and constraint warnings. Identical
-input MUST produce identical output.
-
-The following tree invariants MUST hold after every layout transaction:
-
-1. The tree is acyclic and every reachable node has exactly one parent except
- the root, which has none.
-2. Both children of a split exist and are distinct.
-3. A stack contains at least two children and its `active` index is in bounds.
-4. Removing a child collapses a one-child split or stack into its remaining
- child; removing the last child removes that branch.
-5. Every tiled window is represented by exactly one reachable leaf and every
- reachable window leaf refers to a tiled window in the same workspace.
-6. The focused leaf inside a stack is the active child's recursively focused
- window; directional operations enter a stack through its active child.
-
-### 6.1 Tree-mode insertion
-
-1. The first tiled window becomes the root.
-2. A later window splits the focused leaf.
-3. For `default_split = "automatic"`, use a horizontal split when the focused
- rectangle is wider than tall and a vertical split otherwise.
-4. The new window becomes focused after its first mapped commit.
-
-### 6.2 Required operations
-
-Ferese MUST support directional focus, movement and resizing; scrolling-column
-insertion, extraction, grouping, width cycling, and viewport centering; optional
-tree split direction and divider resize; floating and fullscreen toggles;
-floating move/resize; and layout-aware drag-and-drop.
-
-Drag targets are mode-specific and MUST show a live preview. In scrolling mode,
-left/right inserts a column and top/bottom/center inserts into a column. In tree
-mode, center creates or joins a stack and an edge creates a split.
-
-### 6.3 Gaps
-
-Shared gap defaults live under `[layout]`; mode-specific behavior lives under
-`[scrolling]` and `[tiling]`. `inner_gap` separates managed windows and
-`outer_gap` separates the layout from output bounds. Outer gaps are retained by
-default. With the explicit opt-in `smart_gaps = true`, a lone managed window has
-no outer gap.
-
-## 7. Geometry and animation
-
-Ferese MUST distinguish buffer, surface-local, logical-output, and physical
-coordinates. Layout and input policy use logical coordinates. Rendering performs
-physical conversion. Fractional scaling MUST avoid cumulative rounding errors
-between adjacent tiles. The renderer MUST snap shared logical boundaries once
-per output and derive adjacent physical rectangles from those same snapped
-edges; it MUST NOT independently round each tile's origin and size.
-
-A managed window has three geometry domains that MUST remain distinct:
-
-1. **logical geometry:** the authoritative layout destination;
-2. **visual geometry:** where the compositor currently presents the window while
- an animation is in progress; and
-3. **client geometry:** the most recently configured and committed client content
- size.
-
-A representative model is:
-
-```rust
-pub struct AnimatedRect {
- pub current: RectF,
- pub target: RectF,
- pub velocity: RectF,
-}
-
-pub struct ClientGeometry {
- pub last_configured_size: Size,
- pub committed_size: Size,
-}
-
-pub struct WindowGeometry {
- pub visual: AnimatedRect,
- pub client: ClientGeometry,
-}
-```
-
-`visual.target` is the authoritative logical layout rectangle. The target becomes
-authoritative immediately after the state transaction even if the client has not
-yet committed a buffer matching its new size.
-
-For a non-interactive layout transition Ferese MUST:
-
-1. compute and store the final target geometry;
-2. send a configure for the final client size rather than one configure per
- animation frame;
-3. continue presenting the most recent committed client content while visual
- geometry interpolates toward the target;
-4. map pointer coordinates through the inverse visual transform before
- delivering surface-local input; and
-5. accept a newly committed target-sized buffer without restarting the visual
- transition.
-
-The renderer MAY use a short-lived snapshot of the previously committed content
-to avoid a visible source-size discontinuity. Whether using a live committed
-buffer or snapshot, client content MUST remain aligned with the visual geometry
-used for hit testing.
-
-During a non-interactive resize, committed content is scaled independently on
-each axis to fill the visual content box and clipped to that box, anchored at
-its top-left content origin. This avoids the oversized crop produced by uniform
-cover scaling when only one dimension changes. Ferese MUST NOT expose
-uninitialized regions. When a matching commit arrives it replaces the scaled
-source without restarting the animation. A commit is matching when its
-acknowledged configure and effective buffer geometry match the requested client
-size after scale and transform.
-
-If a client has not acknowledged and committed the final configure within 500
-ms, Ferese MUST stop waiting for visual completion, retain the target layout,
-and continue presenting the latest valid content using the same scale-and-clip
-policy. The timeout is diagnostic rather than permission to send repeated
-configures. A client commit larger than its assigned tile is clipped to the
-tile; it MUST NOT overlap adjacent managed windows or change layout geometry.
-
-During an interactive resize, configure events MAY follow pointer motion subject
-to normal coalescing and client responsiveness. Keyboard navigation MUST use the
-target layout topology, not transient visual geometry.
-
-When animation is disabled or reduced motion is active, `visual.current` snaps
-to `visual.target`. Geometry uses spring animation; opacity, dimming, blur
-intensity, and overlays use bounded easing. Updates MUST use presentation or
-monotonic frame timestamps and MUST NOT assume a fixed refresh rate.
-
-The default geometry spring uses mass `1.0`, stiffness `700.0`, and damping
-`53.0`, which is approximately critically damped. Geometry defaults MUST NOT
-overshoot shared tile boundaries. User-configured underdamped geometry springs
-MAY overshoot numerically, but their rendered content remains clipped to the
-animated allocation and MUST NOT cover adjacent windows.
-
-Animations MUST stop within configured tolerances. Hidden animations SHOULD stop
-requesting frames. Opening, closing, or moving a window MUST never block input.
-
-## 8. Wayland behavior
-
-### 8.1 Required native protocols
-
-Before the native desktop is usable, Ferese MUST support the following core
-protocols and widely deployed extensions:
-
-- compositor, subcompositor, shared memory, and outputs;
-- xdg-shell;
-- xdg-activation;
-- seats, keyboard, pointer, and available touch devices;
-- data-device clipboard and drag-and-drop;
-- primary selection;
-- xdg-decoration;
-- relative pointer and pointer constraints;
-- presentation timing;
-- viewporter and fractional scaling;
-- layer-shell;
-- screencopy with the capture authorization policy from Section 13;
-- cursor-shape-v1;
-- text-input-v3 and input-method-v2;
-- keyboard-shortcuts-inhibit-v1;
-- idle-notify-v1 and idle-inhibit-v1;
-- xdg-foreign-v2;
-- single-pixel-buffer-v1; and
-- Linux dma-buf where supported.
-
-Layer-shell is a public standard role and MUST NOT require a Ferese private
-capability. Ferese MUST enforce exclusive-zone, focus, namespace, and layer
-policy so ordinary panels, launchers, and notification daemons can use it
-without receiving unrestricted compositor authority. Input-method access,
-screencopy, and shortcut inhibition MUST use explicit user/session policy;
-binding a public global alone MUST NOT silently grant capture or persistent
-keyboard interception.
-
-Virtual input and session-lock MAY be added after their security and failure
-policies are specified. Portal integration is not a Wayland protocol: the
-desktop tier MUST separately provide an XDG Desktop Portal backend and PipeWire
-integration for interactive screenshots and screen sharing.
-
-### 8.2 Decorations
-
-Ferese MUST support client-side decorations. If a client negotiates server-side
-decorations, Ferese MUST provide usable move, resize, and close interactions.
-Shadows, masks, and focus borders are compositor effects and may apply in either
-mode.
-
-Tiled and fullscreen windows default to borderless server-side decoration so
-application title bars do not consume layout space. Floating windows and
-dialogs default to client-side decoration until Ferese provides a complete
-compositor-drawn floating frame.
-
-### 8.3 XWayland
-
-The desktop tier SHOULD use `xwayland-satellite`, launched on demand, so X11
-clients become ordinary xdg-shell toplevels from Ferese's perspective. The
-layout engine and `Window` abstraction MUST remain protocol-agnostic. Clipboard
-and primary-selection interoperability depend on the corresponding public
-Wayland protocols remaining available to the satellite.
-
-An embedded X11 window manager requires an architectural decision record and
-MUST justify the additional selection, focus, stacking, and override-redirect
-policy. Tests MUST cover X11 menus, dialogs, games, and applications that request
-absolute positioning; the satellite approach reduces compositor complexity but
-does not guarantee every X11 positioning convention.
-
-## 9. Scene and rendering
-
-Ferese MUST build an explicit scene from authoritative state and current
-animation values. Nodes SHOULD cover surfaces, popups, decorations, shadows,
-masks, borders, materials, shell UI, drag icons, and cursors.
-
-Scene order is background, windows back-to-front, shell/overlays, drag icons,
-then cursor. Within a window, the shadow is behind clipped background and client
-content; the border is above. Rounded clipping MUST be established before
-drawing clipped content or through an equivalent offscreen pass. Client buffers
-MUST NOT be modified to implement effects.
-
-Rendering MUST be damage-driven. Damage includes commits, movement, animation,
-shadows, borders, material invalidation, shell, cursor, and output changes.
-Moving or removing a node MUST damage both old and new bounds. Expansion MUST
-include shadow, blur-kernel, and sampling extents.
-
-Direct scanout SHOULD be possible when one compatible fullscreen surface covers
-an output and no transform, overlay, capture, conversion, or effect requires
-composition.
-
-Rounded corners use a GPU mask or equivalent. Fullscreen windows use zero radius.
-Shadows SHOULD use analytic shaders, cached textures, or reduced-resolution
-intermediates rather than full-resolution per-frame blur.
-
-## 10. Material system
-
-This section specifies compositor support for desktop-tier semantic surfaces. It
-is not a v0 release gate. Theme token values and exact component geometry belong
-to `ferese-theme-ui-spec.md`.
-
-Initial semantic surface roles are:
-
-```text
-surface.panel
-surface.panel_elevated
-surface.popover
-surface.menu
-surface.notification
-surface.hud
-surface.modal
-```
-
-Clients request semantic roles rather than raw shader values.
-
-The compositor resolves each role through the active material style:
-
-```rust
-pub enum MaterialStyle {
- Glass,
- Translucent,
- Solid,
-}
-```
-
-`ferese-effects-v1` is private to trusted Ferese components. It MAY set or clear
-a semantic surface role. It MUST NOT expose shader code, unrestricted capture,
-arbitrary renderer state, or unbounded effect values. Its global MUST be
-advertised only to Wayland clients granted the `effects` capability by the
-private-client mechanism in Section 13.
-
-For `Glass`, the compositor-owned rendering pipeline is:
-
-```text
-lower scene content
- -> capture expanded region
- -> downsample
- -> blur
- -> upsample
- -> saturation and brightness
- -> tint and optional noise
- -> semantic surface foreground
-```
-
-Blur MUST exclude the requesting surface and all content above it. Sampling
-bounds MUST include the kernel radius.
-
-For `Translucent`, the compositor applies semantic tint and alpha without
-backdrop capture. For `Solid`, the compositor uses an opaque semantic surface
-color. Changing material style MUST NOT change component geometry, focus, input
-regions, layer-shell roles, or shell behavior.
-
-A blur cache entry MUST identify its output and scale, visible and sample
-regions, semantic role and resolved material parameters, contributing
-lower-layer generations, and result texture. Changes to content, stacking,
-geometry, scale, transform, resolved material, workspace, or output MUST
-invalidate affected entries. A global generation MAY be used initially;
-region-aware invalidation SHOULD replace it before performance tuning is
-complete.
-
-### 10.1 Built-in visual profile
-
-The desktop tier provides one restrained Ferese visual language with glass,
-translucent, and solid profiles. Exact values for colors, radii, shadows,
-typography, spacing, and material parameters are normative only in
-`ferese-theme-ui-spec.md`.
-
-Focused and foreground content MUST remain distinguishable without transparency.
-Blur, borders, and shadows SHOULD establish hierarchy rather than compete for
-attention. Settings, top bar, overview chrome, notifications, launcher, popovers,
-and OSD MUST consume the same semantic surface and spacing tokens.
-
-## 11. Shell and overview
-
-This section specifies compositor-facing desktop-tier shell behavior. Detailed
-interaction is normative in `ferese-shell-design.md`; visual tokens and exact
-component geometry are normative in `ferese-theme-ui-spec.md`.
-
-`ferese-shell` owns the top bar, overview controls, launcher, workspace UI,
-notifications, and volume and brightness OSD. Screenshot UI MAY be added after
-capture policy and implementation are complete. The shell uses a
-capability-scoped private Wayland connection plus authenticated IPC.
-
-Ferese deliberately has no persistent dock or bottom application bar.
-
-Overview MUST NOT require copying each managed window into the shell process.
-The compositor owns the live window presentation and enters an overview scene
-mode in which existing window scene nodes receive temporary visual transforms.
-`ferese-shell` supplies controls such as labels, selection affordances,
-workspace UI, and later search.
-
-The overview binding is `Super+Tab`. Entering overview MUST animate windows from
-their current visible geometry into overview geometry. Pointer and keyboard
-selection MUST target the compositor-owned live window nodes. Dismissing
-overview MUST animate those same nodes back to the current layout targets.
-Workspace switching from overview MUST obey Section 5.4.
-
-### 11.1 Public layer-shell
-
-`ferese-shell` MUST use standard layer-shell for the top bar, launcher,
-workspace UI, notifications, popovers, and OSD surfaces. Such surfaces are not
-managed application windows and MUST NOT enter tiling, workspace focus history,
-or normal application task switching.
-
-Layer-shell is a public standard role and remains available to ordinary
-third-party panels, launchers, notification daemons, and wallpapers under
-Section 8 policy. Binding layer-shell MUST NOT grant Ferese-specific metadata,
-overview control, capture, virtual input, or semantic-surface capability.
-
-The compositor enforces layer, exclusive-zone, keyboard-interactivity,
-input-region, namespace, and output policy.
-
-### 11.2 Top bar, fullscreen, and reservation
-
-The Ferese top bar is the only persistent Ferese shell surface that reserves
-workspace space.
-
-Fullscreen is a presentation state over the preserved underlying workspace
-layout. Entering fullscreen MUST hide the Ferese top bar visually without
-changing the underlying workspace usable layout region. Leaving fullscreen MUST
-restore the bar without reconfiguring hidden managed windows merely because the
-bar reappears.
-
-A temporary top-bar reveal over fullscreen MUST use overlay presentation and
-`exclusive_zone = 0`. It MUST NOT resize the fullscreen client or alter the
-underlying workspace layout.
-
-If `ferese-shell` disconnects unexpectedly, the compositor SHOULD retain the
-last valid Ferese-owned top-bar reservation for a short bounded grace period.
-If the shell reconnects with a compatible reservation before the lease expires,
-managed windows MUST NOT reflow. If the grace period expires, Ferese removes the
-reservation and performs at most one ordinary layout reflow.
-
-### 11.3 Private shell control protocol
-
-`ferese-shell-v1` is a private control and metadata protocol. It MUST NOT
-duplicate layer-shell roles, exclusive zones, or general surface placement.
-
-Its initial responsibilities are limited to:
-
-```text
-managed-window list and lifecycle
-application identity metadata
-focused and urgent state
-activate managed window
-request close
-enter / exit overview
-select overview window / workspace
-```
-
-The protocol requires the `shell-control` capability from Section 13. Requests
-MUST be validated against current stable identifiers and flow through the same
-domain operations as keyboard, pointer, and IPC actions.
-
-### 11.4 Launcher and application launching
-
-The initial Ferese launcher is apps-only. Its default binding is `Super+Space`.
-It discovers XDG desktop applications, searches application names and normalized
-application identity, and launches selected applications.
-
-The initial search placeholder is:
-
-```text
-Search apps…
-```
-
-Files, recents, settings, and command providers MAY be added later. The shell
-MUST NOT display dead tabs for unavailable providers.
-
-Before launch, the shell MUST obtain an xdg-activation token tied to the
-triggering user interaction and make the token available to the child. The child
-receives the public Wayland display and normal session environment. It MUST NOT
-inherit a private Wayland descriptor, IPC capability, or bearer secret.
-
-A private descriptor deliberately inherited when the compositor starts a trusted
-component is the sole exception during that component's initial exec; the
-component MUST mark it close-on-exec before it can launch any child.
-
-### 11.5 Shell focus and modals
-
-Interactive shell surfaces MAY receive `FocusTarget::Shell`. The persistent top
-bar SHOULD remain non-keyboard-interactive during ordinary use.
-
-The launcher receives keyboard focus while open and MUST restore the previous
-valid managed-window focus on dismissal where possible. Interactive popovers and
-modals follow the same restoration rule.
-
-A shell modal is appropriate only for destructive confirmation,
-security-sensitive confirmation, or an irreversible shell action. While a modal
-is active, the compositor presents a scrim and directs keyboard focus to the
-modal. Input outside the modal is consumed by the scrim and MUST NOT reach
-managed applications.
-
-Destroying a shell surface or disconnecting the shell MUST never leave a dead
-`FocusTarget::Shell`.
-
-Ferese does not ship a polkit authentication agent in the initial desktop tier.
-System authentication requiring polkit uses an external agent until a separate
-authentication-agent security specification exists.
-
-### 11.6 Notifications and Do Not Disturb
-
-`ferese-shell` MUST implement the standard `org.freedesktop.Notifications` D-Bus
-service for basic notifications. The desktop tier MUST support summary, body,
-icon metadata, expiry, replacement, close, actions, and urgency metadata.
-
-Notification content is untrusted: markup, image paths, and action identifiers
-MUST be parsed and rendered without command execution.
-
-When Do Not Disturb is disabled, eligible notifications MAY create transient
-toasts and enter notification history. When Do Not Disturb is enabled, normal
-transient toasts are suppressed but notifications still enter history and
-unread state still updates. A future explicitly defined critical-urgency policy
-MAY bypass DND; application-name heuristics MUST NOT.
-
-Network and Bluetooth control UI are outside the initial desktop-tier shell and
-MUST NOT appear as required or placeholder controls.
-
-If the shell restarts, it MUST reacquire the notification service and reconstruct
-shell state without affecting managed windows.
-
-## 12. Configuration
-
-The primary file is `$XDG_CONFIG_HOME/ferese/config.toml`, falling back to
-`~/.config/ferese/config.toml`.
-
-Configuration has one authoritative representation. Namespace ownership is:
-
-```text
-[theme.*] visual tokens/material mapping ferese-theme-ui-spec.md
-[shell.*] shell behavior ferese-shell-design.md
-
-[animations] compositor animation policy this document
-[layout] shared layout policy this document
-[scrolling] scrolling layout policy this document
-[tiling] optional tree-layout policy this document
-[input] input policy this document
-[commands] launch command vectors this document
-[[bindings]] compositor bindings this document
-[[window_rules]] window placement rules this document
-[[output_profiles]] direct-session output policy this document
-```
-
-There is no separate legacy visual namespace. Blur, opacity, radii, shadows,
-surface colors, typography, and shell component geometry belong only to
-`[theme.*]`.
-
-Representative core configuration:
-
-```toml
-[animations]
-enabled = true
-reduced_motion = false
-speed = 1.0
-
-[animations.spring]
-stiffness = 700.0
-damping = 53.0
-mass = 1.0
-
-[animations.viewport_spring]
-stiffness = 320.0
-damping_ratio = 1.0
-mass = 1.0
-
-[layout]
-mode = "scrolling"
-inner_gap = 10.0
-outer_gap = 4.0
-smart_gaps = false
-
-[scrolling]
-default_column_width = 0.5
-# Use 1.0 or "full" to open every new column at the viewport width.
-width_presets = [0.333333, 0.5, 0.666667, 1.0]
-focus_strategy = "minimal"
-neighbor_context = 48.0
-new_window = "column_after_focused"
-
-[tiling]
-default_split = "automatic"
-
-[input]
-focus_follows_mouse = false
-xkb_layout = "us"
-xkb_variant = ""
-xkb_options = []
-repeat_rate = 25
-repeat_delay_ms = 600
-
-[input.touchpad]
-tap = true
-natural_scroll = true
-disable_while_typing = true
-
-[commands]
-terminal = ["foot"]
-
-[[bindings]]
-keys = "Super+Enter"
-action = "spawn"
-argument = "terminal"
-
-[[bindings]]
-keys = "Super+Q"
-action = "close"
-
-[[bindings]]
-keys = "Super+Tab"
-action = "show-overview"
-
-[[bindings]]
-keys = "Super+H"
-match = "keysym"
-action = "focus"
-argument = "left"
-
-[[bindings]]
-keys = "Super+Space"
-action = "show-launcher"
-
-# Example: remove a built-in binding.
-[[bindings]]
-keys = "Super+Q"
-disabled = true
-
-[[window_rules]]
-app_id = "org.example.DialogApp"
-floating = true
-
-[[output_profiles]]
-name = "docked"
-
-[[output_profiles.outputs]]
-match = "HDMI-A-1"
-mode = "3840x2160@120"
-scale = 1.6
-position = [0, 0]
-
-[[output_profiles.outputs]]
-match = "eDP-1"
-enabled = false
-```
-
-Command values are argument arrays, never shell command strings. Binding records
-use a normalized key chord, match mode, action, and optional argument. `keysym`
-matching is the default and follows the active XKB layout. `physical` matching
-uses XKB physical key names and is independent of the selected layout.
-
-The complete built-in set is listed in Section 14; when a user supplies a record
-for the same chord and match mode it replaces that default. A record with
-`disabled = true` and no action explicitly unbinds the matching default.
-
-Duplicate normalized bindings, unknown actions, invalid arguments, and
-references to missing commands MUST fail validation. Every built-in binding
-MUST be expressible, replaceable, and removable through this schema.
-
-`ferese-config` MUST assemble all enabled namespace schemas and validate the
-entire candidate configuration before applying any part of it. No companion
-component may maintain an independent competing copy of visual or shell
-configuration.
-
-The launcher and overview actions are desktop-tier defaults and MUST be installed
-only when their shell features are available. Their absence from a v0-only
-installation is not a validation error.
-
-Unknown fields SHOULD warn. Invalid types and invalid domain values MUST fail the
-owning schema's validation.
-
-Reload MUST debounce duplicate events, read the entire file, parse, validate,
-construct, and apply the difference as one transaction. Invalid input MUST NOT
-replace active configuration. Errors MUST be logged and emitted through IPC.
-
-Settings and IPC MUST use the same validation path. Persistent changes MUST be
-written atomically using a temporary file and rename.
-
-## 13. IPC and security
-
-### 13.1 Transport and trust
-
-The user-control socket is `$XDG_RUNTIME_DIR/ferese/control.sock`. Its parent
-directory MUST be owned by the user with mode `0700`; the socket MUST use `0600`.
-The compositor MUST reject peers whose effective UID, read from OS peer
-credentials, differs from its own.
-
-Same-UID checks authorize user control only; they do not identify a trusted UI
-client. Privileged Wayland access is capability-scoped per client. Initial
-capabilities are:
-
-```text
-effects
-shell-control
-capture
-virtual-input
-```
-
-Trusted UI processes MUST receive a private Wayland connection created by the
-compositor. Ferese creates a socket pair, registers the server end as a Wayland
-client, and passes the client end only through an inherited descriptor such as
-`WAYLAND_SOCKET`. Privilege MUST NOT depend on PID, executable name, command-line
-arguments, environment secrets, or a discoverable private socket path.
-
-The compositor associates an explicit capability set with each private Wayland
-client and filters private globals and requests against that set.
-
-`ferese-effects-v1` requires `effects`. `ferese-shell-v1` requires
-`shell-control`. `ferese-shell` receives only the capabilities required by
-enabled shell features. `ferese-settings` MAY receive `effects` when spawned as
-a trusted Ferese component; its configuration changes still use the normal
-validated configuration/IPC path.
-
-Public layer-shell does not require a private capability. The private `capture`
-capability permits unattended capture only for a trusted component. Public
-screencopy clients remain subject to interactive or configured capture policy
-and do not acquire `capture` merely by binding a public global.
-
-If a trusted component launches another privileged Ferese component, it MUST ask
-the compositor to create and pass a new capability-scoped private connection;
-it MUST NOT forward its own Wayland descriptor or capabilities.
-
-Ferese does not ship a polkit authentication agent in the initial desktop tier.
-Security-sensitive actions requiring system authentication MUST use an external
-agent until a separate authentication-agent security specification exists.
-
-### 13.2 Framing
-
-IPC v0 uses unsigned 32-bit big-endian length-prefixed UTF-8 JSON:
-
-```text
-4-byte payload length | JSON payload bytes
-```
-
-The maximum payload is 1 MiB. Oversized, truncated, invalid UTF-8, or invalid
-JSON frames close the connection after a rate-limited diagnostic.
-
-```json
-{
- "version": 1,
- "id": 42,
- "type": "command",
- "command": "focus",
- "args": { "direction": "left" }
-}
-```
-
-Responses echo `id` and contain exactly one of `result` or `error`:
-
-```json
-{
- "version": 1,
- "id": 42,
- "result": {}
-}
-```
-
-Errors have stable codes. Unknown fields MAY be ignored; unknown commands and
-unsupported versions MUST return errors.
-
-### 13.3 Commands and events
-
-User-control commands are:
-
-```text
-focus move
-resize workspace
-move-to-workspace toggle-floating
-toggle-fullscreen toggle-layout
-cycle-column-width center-column
-consume expel
-close
-reload-config get-focused-window
-get-workspaces get-outputs
-get-config set-config
-show-overview hide-overview
-show-launcher hide-launcher
-```
-
-Managed-window enumeration, application identity metadata, shell activation,
-close, and overview-selection operations used by `ferese-shell` belong to the
-private `ferese-shell-v1` protocol rather than widening normal user-control IPC.
-
-Events include `window-opened`, `window-closed`, `window-focused`,
-`window-moved`, `workspace-changed`, `output-added`, `output-removed`,
-`config-changed`, and `config-error`.
-
-Subscribers have bounded queues. The server MUST disconnect a subscriber whose
-queue overflows rather than block the compositor.
-
-The IPC server MUST bound concurrent connection workers as well as queued
-requests. Reaching either limit rejects the excess client without blocking the
-compositor event loop or allocating another worker thread.
-
-## 14. Input
-
-Ferese MUST support click-to-focus, configurable focus-follows-pointer, floating
-move/resize, tiled rearrangement and divider resizing, pointer constraints,
-relative pointer, cursor-shape, text input, configurable XKB layout and repeat,
-touchpad tap/natural-scroll policy, and configurable bindings. Default bindings
-match keysyms, so `Super+H/J/K/L` follows the active keyboard layout. Users MAY
-select physical matching per binding when spatial key positions are desired.
-
-A focused client MAY inhibit compositor shortcuts only while its requesting
-surface is visible and holds keyboard focus. Ferese MUST retain an emergency
-escape binding that cannot be inhibited. Input-method activation MUST follow the
-focused text-input request and MUST NOT grant the input-method client general
-shortcut or capture authority.
-
-```text
-Super+Enter terminal
-Super+Q close window
-Super+H/J/K/L focus left/down/up/right
-Super+Shift+H/J/K/L move left/down/up/right
-Super+Ctrl+H/J/K/L resize left/down/up/right
-Super+1..9 switch workspace
-Super+Shift+1..9 move window to workspace
-Super+F toggle fullscreen
-Super+M toggle scrolling/tree layout
-Super+R cycle scrolling column width
-Super+C center scrolling column
-Super+[ consume window into adjacent column
-Super+] expel window from column
-Super+Shift+Space toggle floating
-Super+Space launcher
-Super+Tab overview
-```
-
-The launcher and overview bindings are desktop-tier defaults and MUST be
-installed only when their actions are available. Their absence from a v0-only
-installation is not a binding error.
-
-Bare `Super` MUST NOT activate overview. Interactive grabs MUST have explicit
-cancellation. Losing a device, seat, surface, or output during a grab MUST end it
-safely.
-
-## 15. Outputs
-
-Each connected output has a mode, scale, transform, logical position, usable
-region, and one active workspace. v0 MAY choose preferred modes automatically
-but MUST handle hotplug without losing windows.
-
-The direct backend selects the first configured output profile for which every
-listed output matcher is connected. A matcher MAY be a DRM connector name or
-the persistent identity reported by `get-outputs`. Each entry MAY set enabled
-state, `WIDTHxHEIGHT[@REFRESH]` mode, positive fractional scale, transform, and
-logical position. Unspecified connected outputs remain enabled using preferred
-mode, scale `1.0`, normal transform, and automatic horizontal placement.
-Invalid profiles MUST fail startup validation. An unavailable valid mode MUST
-produce a warning and fall back to the preferred mode.
-
-`get-outputs` MUST enumerate disabled as well as enabled connected outputs and
-report connector and persistent identity, active profile, current and available
-modes and refresh rates, scale, transform, logical geometry when enabled, and
-physical dimensions when known.
-
-On startup, the first output creates and activates workspace `1`. Selecting an
-uncreated numeric workspace from `1` through `9` creates it on the focused
-output. A newly connected output receives the lowest unused numeric workspace
-name and activates it; this MUST NOT silently move an existing workspace.
-
-Output removal selects a target from the remaining outputs in this order:
-
-1. the v0 seat's focused output, if it remains connected;
-2. nearest output to the removed output by logical-center distance;
-3. lowest stable `OutputId` as a tie-breaker.
-
-A workspace MAY move between outputs but MUST NOT appear on two outputs at once.
-Switching to a workspace already visible on another output changes the seat's
-focused output/workspace to that existing presentation and MUST NOT implicitly
-move or swap workspaces.
-
-When an output is removed, the migration target keeps its current active
-workspace unless the removed output held the seat focus. In that case, the
-removed output's active workspace becomes active on the target and the seat's
-managed-window focus is restored there where possible.
-
-Ferese MUST derive a persistent output identity from stable connector and EDID
-properties and record the workspaces evacuated during the compositor lifetime.
-When that output returns, those workspaces return to it atomically if they still
-reside on their automatic migration target and have not been explicitly moved
-since disconnection. Its previous active workspace is restored when available.
-Explicit user movement wins; Ferese MUST NOT take a workspace back from a new
-assignment. If no recorded workspace can return, normal new-output assignment
-applies.
-
-## 16. Performance and observability
-
-The target is sustained output refresh at 60 Hz on integrated graphics during
-ordinary use. Animation MUST also work at higher and variable refresh rates.
-
-Ferese SHOULD track frame, render, layout and blur duration; damaged pixels;
-missed presentation deadlines; surface, semantic-material, and blur-region counts; and active
-animations. Performance tracing MUST be optional. Suggested targets are
-`ferese::wayland`, `ferese::layout`, `ferese::render`, `ferese::input`,
-`ferese::ipc`, and `ferese::xwayland`.
-
-## 17. Testing
-
-### 17.1 Layout
-
-Unit tests MUST cover scrolling-column insertion, extraction, grouping, focus,
-viewport clamping and stable anchors; tree insertion, deletion, movement,
-ratios, constraints, resize and stacks; floating transitions, fullscreen
-placement preservation, workspace migration and restoration, gaps, shared-edge
-rounding, layout-mode conversion, and deterministic geometry. They MUST NOT
-require Wayland. Property tests SHOULD prove arbitrary operation sequences do
-not duplicate or lose windows and preserve scrolling column membership or tree
-acyclicity, valid parentage and stack cardinality as applicable.
-
-### 17.2 Configuration
-
-Tests MUST cover defaults, partial files, unknown fields, invalid and non-finite
-values, duplicate key chords, unknown binding actions, missing command
-references, keysym versus physical matching, default unbinding, window-rule
-ordering, theme/shell namespace ownership, reload rollback, serialization, and atomic updates.
-
-### 17.3 IPC and security
-
-Tests MUST cover partial reads, multiple frames per read, oversized frames,
-unsupported versions, subscriptions, slow clients, peer-UID rejection, and
-denial of privileged commands without capability, `shell-control` filtering, and
-public layer-shell remaining unprivileged. Launch tests MUST prove that
-children receive the public display and activation token but cannot inherit any
-private Wayland or privileged IPC descriptor.
-
-### 17.4 Rendering and integration
-
-Pure tests MUST cover clipping, shadow bounds, damage expansion, blur sampling,
-cache invalidation, transforms, animated hit testing, and refresh-rate-independent
-interpolation. Geometry tests MUST cover mismatches between logical target,
-visual geometry, configured client size, and committed client size. Nested
-integration tests SHOULD cover configure/commit behavior, inverse-transformed
-input during animation, clipboard, popups, drag-and-drop, focus, fullscreen,
-xdg-activation, output removal, private-client capability filtering, overview
-scene transitions, top-bar reservation restart grace, application-identity association,
-DND behavior, notification replacement/actions, and shell restart.
-
-## 18. Milestones
-
-A milestone is complete only when its acceptance criteria pass. M0 through M6
-form the v0 release path. M7 through M10 are independently gated desktop-tier
-work and MUST NOT delay the v0 release.
-
-### M0: Native window on screen
-
-Implement the nested backend, public Wayland socket, output, seat, xdg-shell,
-basic renderer, and commit handling.
-
-Acceptance: `foot` opens and redraws; keyboard, pointer, and popups work; closing
-a client leaves the compositor running.
-
-### M1: Essential native protocols
-
-Implement clipboard, primary selection, drag-and-drop, decoration negotiation,
-xdg-activation, relative pointer, pointer constraints, presentation timing, and
-scaling.
-
-Acceptance: text copies between native clients; drag-and-drop completes;
-decorated and undecorated clients can move and resize; scaled hit testing matches
-rendered content; a user-initiated test launch receives a valid activation token.
-
-### M2: Layout and workspaces
-
-Implement identifiers, default scrolling columns, optional layout trees,
-insertion, focus, movement, resize, gaps, floating, fullscreen, and workspaces.
-
-Acceptance: five terminals populate a horizontally scrollable column strip
-without overlap; focus reveals columns without unnecessary recentering; tree
-mode remains selectable; navigation works in all directions; close reflows the
-active layout; modes restore correctly; randomized tests preserve all
-invariants.
-
-### M3: Animated geometry
-
-Implement logical, visual, and client geometry separation plus timestamp-driven
-animation.
-
-Acceptance: layout changes do not jump; one non-interactive reflow does not send
-per-frame configure events; a client may commit its final size mid-animation
-without restarting the transition; inverse-transformed hit testing matches the
-visible surface; input stays responsive.
-
-### M4: Early hardware session
-
-Implement the smallest useful libseat, DRM/KMS, GBM, libinput, and session-
-lifecycle path for one output. Record page-flip presentation timestamps and
-missed-deadline metrics. Keep the nested backend for development.
-
-Acceptance: Ferese starts from a TTY, renders the M3 workload directly, survives
-session deactivate/reactivate, and drives animation from measured presentation
-timing. Frame pacing and animation are measured on the reference integrated GPU
-before blur or shell work begins.
-
-### M5: Native desktop interoperability
-
-Implement layer-shell, cursor-shape, text-input/input-method, shortcut inhibit,
-idle notify/inhibit, xdg-foreign, single-pixel-buffer, screencopy authorization,
-multi-output hotplug, and the remaining Section 8.1 protocols.
-
-Acceptance: representative native panels, launchers, notification daemons, IMEs,
-VM/remote-desktop clients, and screenshot clients exercise their protocols;
-output removal and return obey Section 15; security-sensitive requests are
-denied without the required policy decision.
-
-### M6: Shippable v0 core
-
-Implement rounded clipping, shadows, active borders, transparency, damage
-expansion, validated textual configuration, input settings, window rules,
-direct application launching, IPC, and `feresectl`. Complete native-client crash,
-soak, and performance testing.
-
-Acceptance: masks and damage are correct; configuration reload is transactional;
-defaults can be replaced or unbound; `feresectl` commands work reliably; a
-configured terminal launches without shell parsing; malformed clients do not
-affect availability; a 24-hour native-client soak has no compositor crash or
-unbounded resource growth; all Section 3.1 requirements pass.
-
-### M7: Semantic surface materials
-
-Before choosing a shell toolkit, build a minimal `wayland-client` probe that
-creates a layer-shell surface and requests `surface.panel` through
-`ferese-effects-v1`. Then implement semantic role resolution for glass,
-translucent, and solid styles plus backdrop capture, blur, tint, saturation,
-optional noise, caching, and invalidation.
-
-Acceptance: the probe can switch among glass, translucent, and solid without
-changing geometry; glass blurs only content behind its surface; backdrop changes
-invalidate correctly; the effects global is visible only to a client with the
-`effects` capability; a public client cannot bind it; the demo meets its frame
-budget on the reference integrated GPU.
-
-### M8: Overview and shell
-
-First spike the selected shell toolkit's layer-surface creation, output discovery,
-focus, configure, and shutdown behavior. The initial selection is libcosmic,
-pinned to a reviewed Git revision because it has not published a current crates.io
-release. If the spike fails its lifecycle tests, use a thin native Wayland shell
-adapter or record a new toolkit decision before proceeding. Implement
-the compositor overview scene, minimal `ferese-shell-v1` control/metadata
-protocol, top bar, apps-only launcher UI, workspace UI, OSD, and the standard
-notifications D-Bus service.
-
-Acceptance: normal shell surfaces use public layer-shell; `Super+Tab` controls
-overview; managed windows remain compositor-owned live scene nodes; selection
-follows visual geometry; the apps-only launcher uses application identity and
-XDG activation; DND and notification replacement/actions work; fullscreen hides
-the top bar without changing underlying layout geometry; shell restart preserves
-the top-bar reservation through its grace period; each trusted process receives
-only its assigned capabilities.
-
-### M9: Settings and portals
-
-Build Theme/Appearance, Tiling, Shortcuts, Input, Workspaces, Displays, and About.
-Implement the portal backend and PipeWire integration for screenshots and screen
-sharing.
-
-Acceptance: controls preview live; invalid changes do not replace active config;
-changes survive restart; shortcut editing can replace and unbind defaults; the
-Displays page represents hotplug and return behavior; portal requests require an
-interactive authorization path and produce working screenshots/streams.
-
-### M10: X11 compatibility and desktop stabilization
-
-Integrate `xwayland-satellite` on demand and complete desktop-tier crash, soak,
-compatibility, and performance testing. Replacing the satellite with an embedded
-X11 window manager requires the architectural decision from Section 8.3.
-
-Acceptance: representative X11 applications tile and float; clipboard works both
-ways; menus, dialogs, games, and absolute-position requests have documented test
-results; a 24-hour mixed-client soak has no compositor crash or unbounded
-resource growth; all Section 3.2 desktop-tier requirements pass.
-
-## 19. First implementation slice
-
-```text
-nested Smithay compositor
- -> two xdg-shell terminals
- -> deterministic two-column scrolling layout
- -> logical/visual/client geometry separation
- -> spring reflow with coalesced final configure
-```
-
-The slice succeeds when A and B form columns automatically; closing B
-immediately updates the logical layout while A expands visually; A receives no
-per-frame configure flood; pointer input remains aligned through inverse
-transforms; shared edges round identically; terminal changes damage the correct
-regions; and all interaction remains responsive.
-
-M4 follows this nested slice immediately so frame pacing is validated on real
-hardware. Semantic materials, GPUI, Settings, XWayland, notifications, portals, and the
-graphical shell MUST NOT delay it or the v0 core.
-
-## 20. Definition of v0
-
-Ferese v0 is complete when a user can:
-
-1. start a direct Ferese session;
-2. run ordinary native Wayland applications and layer-shell utilities;
-3. copy, paste, drag, and use application popups;
-4. scroll, focus, move, resize, group, float, fullscreen, and close windows,
- with optional tree tiling;
-5. use multiple workspaces and outputs;
-6. configure input, bindings, rules, gaps, corners, borders, shadows, and
- animations through validated live reload;
-7. launch configured applications directly;
-8. use an IME, inhibit shortcuts where allowed, take an authorized screenshot,
- and resume after output removal or session switching;
-9. control permitted behavior through `feresectl`; and
-10. perform routine native-Wayland programming work without regular compositor
- restarts.
-
-The intended v0 result is a low-latency, independently usable scrolling
-compositor with optional tree tiling.
-The desktop tier adds a coherent Ferese shell, semantic surface materials,
-graphical settings,
-portals, and X11 compatibility without redefining core correctness. Visual
-effects support hierarchy and usability; they do not replace correctness,
-responsiveness, or security.
diff --git a/docs/installation.md b/docs/installation.md
index 98510a0..5850648 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -1,73 +1,48 @@
-# Fedora / SDDM installation
+# Installation
-From the checkout, run the installer as your normal user:
+## Build and install
+
+On Fedora with SDDM, install a Rust toolchain (1.93 or newer), native build
+dependencies, `desktop-file-utils`, D-Bus and `foot`. Wallpaper browsing also
+needs `zenity`; locking needs swaylock or swaylock-effects.
+
+From the checkout, run as your normal user:
```sh
./scripts/install.sh
```
-It builds all four release binaries with the locked dependencies, then uses
-`sudo` (or `pkexec`) for installation. Install the Rust toolchain, native build
-dependencies, `desktop-file-utils`, and D-Bus first. Cargo reports missing native
-libraries during the build. The shell requires Rust 1.93 or newer.
-
-Useful options:
+The script builds locked release binaries, then requests elevation to install.
+It preserves your configuration and other desktop sessions. Log out and select
+**Ferese** at the login screen. Upgrades require logging out and back in too.
```sh
./scripts/install.sh --dry-run
-./scripts/install.sh --offline --release-id my-demo
-./scripts/install.sh --skip-build --release-id existing-build
+./scripts/install.sh --offline --release-id my-build
+./scripts/install.sh --skip-build
```
-The script works from any directory and preserves your user configuration.
-Use `--help` for all options. To test and install manually:
+Use `--help` for all options. New configs can be copied from
+`/usr/local/lib/ferese/current/config.example.toml` to
+`~/.config/ferese/config.toml`. See [Configuration](configuration.md).
-```sh
-cargo test -p ferese -p feresectl -p ferese-animation -p ferese-core -p ferese-layout -p ferese-shell -p ferese-settings --locked
-cargo build --release --locked -p ferese -p ferese-shell -p ferese-settings -p feresectl
-sudo bash scripts/install-session.sh YOUR_RELEASE_ID
-```
+## Preview and logs
-The installer adds **Ferese** to SDDM without changing Plasma or
-the default session. It installs immutable, versioned directories in
-`/usr/local/lib/ferese/releases/` and launchers in `/usr/local/bin/`.
-`current` selects the installed build; `previous` records the prior selection
-on upgrades. Installation refuses to replace unmanaged launchers/session files.
+Run `ferese-session --nested` inside an existing Wayland desktop. For a hardware
+session, use the login screen or a spare TTY; never run Ferese as root or start
+its DRM backend inside an active graphical session.
-Keep your existing `~/.config/ferese/config.toml`. For a new configuration, copy
-`/usr/local/lib/ferese/current/config.example.toml` there. The existing compositor
-defaults handle connected monitors automatically; add output profiles only after
-checking `feresectl get-outputs` on the actual DRM session.
-
-Use `ferese-session --nested` for an installed-build preview. Log out of Plasma
-and select Ferese in SDDM for real hardware use. `ferese-session` can also run
-from a spare logged-in TTY. It must run as your normal user, never as root.
-Foot and `dbus-run-session` must be installed.
-
-Session logs are written under `${XDG_STATE_HOME:-~/.local/state}/ferese/`.
-The launcher sets Wayland desktop variables, starts D-Bus if none is available,
-and grants shell capabilities only to the compositor-launched shell process.
-It does not enable public screencopy, privileged input methods, or shortcut
-inhibition by default.
+Logs are stored in `${XDG_STATE_HOME:-~/.local/state}/ferese/`. Keep an existing
+desktop available while testing scaling, monitor hotplug, suspend and locking.
## Logout and recovery
-Save your work, then press **Super+Shift+E** or run `feresectl exit` to log out.
-Ferese stops its event loop and terminates its shell child during shutdown.
-This is immediate logout, without a save-confirmation dialog.
+Save your work before **Super+Shift+E** or `feresectl exit`: logout is immediate.
+If frozen, switch to another TTY with Ctrl+Alt+F1–F12, terminate the specific
+Ferese process, and select your other desktop on the next login. Restarting SDDM
+also ends other sessions, so avoid it with unsaved work.
-If the compositor becomes unresponsive, switch to another TTY using
-Ctrl+Alt+F1–F12 and terminate that specific Ferese process. Select Plasma on the
-next login. Do not restart SDDM while a desktop contains unsaved work.
-
-For a binary rollback, inspect `/usr/local/lib/ferese/previous` and select that
-release with the `current` symlink while Ferese is stopped. Existing releases
-are retained. To hide the login option, move
+Installed binaries live in `/usr/local/lib/ferese/releases/`. While Ferese is
+stopped, select a retained release with the `current` symlink; `previous` points
+to the prior installation. To hide the session option, move
`/usr/share/wayland-sessions/ferese.desktop` out of that directory.
-
-## First hardware session
-
-Verify terminal/editor/browser, clipboard, fullscreen, fractional scaling,
-monitor disconnect/reconnect, VT switching away/back, and logout. Suspend/resume
-and session locking are not certified by the nested smoke test. Keep Plasma
-available until real development sessions and these checks pass.
diff --git a/docs/native-soak-testing.md b/docs/native-soak-testing.md
deleted file mode 100644
index 0ebd43c..0000000
--- a/docs/native-soak-testing.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# Native-client soak testing
-
-The M6 soak runner repeatedly creates and destroys native Wayland clients while
-exercising focus, resize, column width, fullscreen, floating, and workspace
-paths. Every tenth iteration also terminates a client abruptly. If a local
-malformed-client probe is available, it opens three isolated connections that
-send malformed Wayland messages. The runner requires
-Ferese to disconnect each malformed client and then checks that the compositor
-and authenticated IPC socket remain responsive. It also records compositor RSS
-growth.
-
-Build Ferese and `feresectl`, start the compositor, and run:
-
-```bash
-FERESE_TRACE_PERFORMANCE=1 cargo run -p ferese -- foot
-```
-
-From a terminal connected to Ferese, or from the host with the Ferese socket
-selected, run:
-
-```bash
-WAYLAND_DISPLAY=wayland-1 \
-FERESE_PID="$(pgrep -n -x ferese)" \
-./scripts/soak-native.sh
-```
-
-The default duration is 24 hours. A shorter validation run is:
-
-```bash
-WAYLAND_DISPLAY=wayland-1 \
-FERESE_PID="$(pgrep -n -x ferese)" \
-FERESE_SOAK_SECONDS=300 \
-./scripts/soak-native.sh
-```
-
-Development probes are local-only, not distributed in the Cargo workspace.
-Supply an existing executable with `FERESE_MALFORMED_CLIENT` to enable protocol
-checks. An explicitly configured missing probe is an error; without a probe,
-the runner reports that protocol checks are skipped.
-
-The local probe can also be run independently against a disposable Ferese session:
-
-```bash
-WAYLAND_DISPLAY=wayland-1 /path/to/ferese-malformed-client
-target/debug/feresectl get-outputs
-```
-
-Do not point the malformed-client probe at the host compositor socket.
-
-Environment controls:
-
-- `FERESE_SOAK_SECONDS`: run duration, default `86400`;
-- `FERESE_SOAK_DELAY`: delay between operations, default `0.15` seconds;
-- `FERESE_SOAK_CLIENT`: native client executable, default `foot`;
-- `FERESECTL`: path to `feresectl`, default `target/debug/feresectl`;
-- `FERESE_MALFORMED_CLIENT`: path to the malformed protocol probe, default
- `target/debug/ferese-malformed-client`;
-- `FERESE_PID`: compositor PID; automatic lookup is only a fallback;
-- `FERESE_SOAK_LOG`: result log, default `/tmp/ferese-soak-.log`.
-- `FERESE_SOAK_MAX_RSS_GROWTH_KIB`: maximum RSS growth, default `131072`
- (128 MiB);
-- `FERESE_SOAK_MAX_FD_GROWTH`: maximum open-file-descriptor growth, default
- `32`;
-- `FERESE_SOAK_MAX_THREAD_GROWTH`: maximum thread growth, default `8`.
-
-Run the same workload once with the nested backend and once from a direct DRM
-session. Samples are taken after spawned clients are reaped. A passing run
-requires the compositor to remain alive, IPC queries to succeed, and RSS, file
-descriptor, and thread growth to stay within the declared budgets. Tighten the
-defaults for release hardware when its normal cache behavior is known. Preserve
-the soak log and the `ferese::render` summaries with the release artifacts.
diff --git a/docs/nested-resize-testing.md b/docs/nested-resize-testing.md
deleted file mode 100644
index 16fef0a..0000000
--- a/docs/nested-resize-testing.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Nested live-resize regression check
-
-The shell enables libcosmic's `wgpu` feature. Without it, iced uses tiny-skia
-to resample and rasterize the wallpaper on the CPU. In an unoptimized build,
-this blocks the UI/render thread shared with the top bar for seconds. Prompt
-layer configures, buffer stretching, and compositor vsync cannot fix that
-client-side bottleneck. Software rendering remains available as a fallback.
-
-## Reproduce and compare
-
-```sh
-cargo build --locked -p ferese -p ferese-shell
-WAYLAND_DEBUG=1 FERESE_TRACE_PERFORMANCE=1 \
- target/debug/ferese --backend nested --grant-effects --grant-shell-control -- \
- target/debug/ferese-shell 2>/tmp/ferese-resize.log
-```
-
-Use a configured wallpaper. Drag the host window's edges in both directions,
-then maximize/restore it repeatedly. Check that the wallpaper and bar follow
-the window, including at fractional host scaling. Repeat with
-`ICED_BACKEND=wgpu` to require GPU rendering (rather than silently accepting
-fallback), and with `ICED_BACKEND=tiny-skia` to compare software rendering.
-Use a release build if software fallback is needed for regular use.
-
-Trace the chain, not just the compositor's frame rate:
-
-1. Host `xdg_toplevel.configure` becomes Winit `Resized`.
-2. Ferese updates the output, arranges layers, and sends
- `zwlr_layer_surface_v1.configure` for the new logical dimensions.
-3. The shell acknowledges configure and eventually attaches/commits a new
- buffer. A fast acknowledgement does **not** imply a finished render.
-4. Committed buffers change surface damage; Ferese renders the damaged output
- and submits through EGL to the host compositor.
-
-`FERESE_TRACE_PERFORMANCE` measures Ferese's rendering/submission, not shell
-rasterization. Wayland traces distinguish software SHM buffers from GPU
-buffers (Mesa Vulkan on the tested system).
-
-## Measured validation (KDE Wayland, 175% scale)
-
-With temporary instrumentation, 24 host maximize/restore requests spaced
-250 ms apart generated 26 Winit resize events including initialization. The
-same debug binaries were tested with each renderer forced:
-
-| Measurement | tiny-skia | wgpu |
-| --- | ---: | ---: |
-| Median configure acknowledgement | 1.35 ms | 2.27 ms |
-| Median Ferese render + submit | 0.82 ms | 1.10 ms |
-| Wallpaper buffer interval | 5919 ms (only two buffers) | 9.85 ms median |
-| Bar buffers observed | 1 | 996 |
-
-The CPU capture ran for 14 seconds and the GPU capture for 18 seconds;
-counts are diagnostic, not equal-duration throughput benchmarks. GPU maximum
-buffer intervals were about 200 ms for wallpaper and 158 ms for the bar, so
-the median does not imply every frame met a refresh deadline. These figures
-include protocol tracing overhead. The temporary resize/timing hooks were
-removed after testing; no synthetic resizing runs in production.
-
-The fix does not alter output coordinates, layer exclusion, damage tracking,
-frame scheduling, or EGL swap settings. It also applies to the same shell
-running under DRM, though this validation used the nested backend.
diff --git a/packaging/config.toml b/packaging/config.toml
index 406aeb2..2f327b7 100644
--- a/packaging/config.toml
+++ b/packaging/config.toml
@@ -27,3 +27,13 @@ floating = true
[[window_rules]]
title = "Choose a wallpaper"
floating = true
+
+# Optional desktop clock; all dimensions are logical pixels.
+[desktop_widgets.clock]
+enabled = false
+anchor = "top_left"
+margin_x = 64
+margin_y = 80
+time_size = 72.0
+date_size = 18.0
+opacity = 0.9
diff --git a/shell/ferese-settings/src/main.rs b/shell/ferese-settings/src/main.rs
index 1e8d1a1..434765c 100644
--- a/shell/ferese-settings/src/main.rs
+++ b/shell/ferese-settings/src/main.rs
@@ -61,6 +61,9 @@ enum Message {
Undo,
Preset(usize),
PickWallpaper,
+ AddNote,
+ NoteAction(String, widget::text_editor::Action),
+ SaveNote(String, u64),
WallpaperPicked(Result