diff --git a/.gitignore b/.gitignore
index a90a9e3..373a9c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,14 +3,8 @@
*.log
ChatGPT Image*.png
-# Local implementation notes and validation plans
+# Local architecture decision notes
/docs/adr/
-/docs/animation-validation.md
-/docs/nested-resize-testing.md
-/docs/native-soak-testing.md
-/docs/ferese-window-manager-spec.md
-/docs/ferese-theme-ui-spec.md
-/docs/ferese-shell-design.md
# Generated GitHub Pages site
/build/site/
diff --git a/README.md b/README.md
index 22249a7..256d7f4 100644
--- a/README.md
+++ b/README.md
@@ -2,60 +2,35 @@
-A beautiful, fluid Wayland desktop made to feel like yours.
+A Wayland desktop with scrolling layouts and a shared theme.
Install ·
Configure ·
- Widgets ·
- Notifications
+ Documentation
-Ferese is a complete Wayland desktop built around personalization. Its custom
-shell, native settings, live-reloading themes and wallpapers, desktop widgets,
-fluid animations, and scrolling and tiling layouts work together as one
-cohesive experience—beautiful by default and unmistakably yours.
+Ferese is a Linux desktop with scrolling columns, tree tiling, and floating
+windows. It includes a top bar, Settings, notifications, desktop widgets, a
+lock screen, and screen sharing. Change themes and settings without restarting.

-## Why Ferese
+## Features
-- **Flexible layouts:** scrolling columns, tree tiling, floating windows and a
- workspace overview.
-- **A native shell:** menu bar, control center, settings, notifications and
- desktop widgets designed to belong together, including themed authentication prompts.
- Native apps share the [Ferese theme components](crates/ferese-theme/README.md).
-- **Personal by design:** themes, wallpapers, accent colors, blur, borders and
- motion that update without restarting your session.
-- **Ready for real setups:** gestures, multi-monitor support, fractional
- scaling, screenshots, background services and session locking.
-- **Screen sharing:** a native display picker and PipeWire video streams for
- portal-compatible recording and conferencing apps.
+- Scrolling and tree layouts, floating windows, and a workspace overview.
+- Configurable keyboard shortcuts and touchpad gestures.
+- Multiple monitors with fractional scaling.
+- Six theme presets, wallpapers, blur, and adjustable corners.
+- Clock and sticky-note widgets.
+- Screenshots with Satty editing, screen sharing, and top-bar recording.
-
-
-The overview brings windows and workspaces into one view. Read more about
-[why Ferese builds the compositor and shell together](docs/why-ferese.md).
-
-## Make it yours
-
-Use the native Settings app for everyday customization, or edit
-`~/.config/ferese/config.kdl` when you want complete control. Ferese reloads
-changes as you make them and keeps the last working configuration if an edit is
-invalid.
-
-
-
-Choose a preset or build your own look. Window behavior, gestures, shortcuts,
-rules, displays, widgets and login items are configurable too. Start with the
-[configuration guide](docs/configuration.md) or the
-[example config](packaging/config.kdl).
+
## Install
-Install Ferese on Linux with any Wayland-capable login manager, a command-based
-greeter, or a local TTY. Start with the [installation guide](docs/installation.md)
-for system dependencies and Rust 1.95 or newer, then build as your normal user:
+Install the [dependencies](docs/installation.md#requirements), then build as your
+normal user:
```sh
git clone https://github.com/ferese-wm/ferese.git
@@ -63,59 +38,48 @@ cd ferese
./scripts/install.sh
```
-Log out and select **Ferese** at your login screen. For greeters that take a
-session command, use `/usr/local/bin/ferese-session`. The installer preserves
-your configuration and other sessions, and retains the previous release for
-rollback. The guide covers [all startup methods](docs/installation.md#start-a-session),
-optional tools, updates, and recovery.
+Log out, select **Ferese** at your login screen, and sign in. For a TTY or a
+greeter that accepts a command, run `ferese-session`.
+See [Installation](docs/installation.md) for previews, updates, and recovery.
-To preview Ferese inside your current Wayland desktop instead:
+## Configure
-```sh
-cargo build --release --locked -p ferese -p ferese-shell -p ferese-settings -p feresectl -p ferese-lock
-target/release/ferese --backend nested --grant-effects --grant-shell-control -- \
- target/release/ferese-shell
-```
+Open **Control Center → Settings**, or edit `~/.config/ferese/config.kdl`.
+Changes reload automatically; invalid edits keep the last working configuration.
-## Get around
+
-These are the built-in defaults; every shortcut and gesture can be changed.
-The login guide shows your active shortcuts. Disable it in **Settings → Shortcuts**
-when you no longer need it.
+See the [configuration reference](docs/configuration.md) and
+[example config](packaging/config.kdl).
+
+## Default shortcuts
+
+`Super` is usually the Windows key. Change shortcuts and gestures in
+**Settings → Shortcuts**.
| Shortcut | Action |
| --- | --- |
| Super + Enter | Open a terminal |
-| Super + H / J / K / L | Focus a window |
+| Super + H / J / K / L | Focus left / down / up / right |
+| Super + F | Maximize the window |
+| Super + Shift + F | Toggle fullscreen |
| Super + R | Cycle column width |
-| Super + Tab | Open workspace and window overview |
+| Super + Tab | Open overview |
| Super + M | Switch scrolling / tree layout |
+| Print Screen | Capture the active monitor and open Satty |
| Super + Shift + S | Select a screenshot area |
+| Super + Shift + E | Ask to log out |
-Click the bar date for a calendar. Click the battery for power modes and
-confirmed Restart, Power off and Suspend actions.
+See [all shortcuts and mouse actions](docs/shortcuts.md), including Super-drag
+to move and Super-right-drag to resize floating windows.
-Three-finger swipes move between workspaces and windows. Open **Control Center →
-Settings → Shortcuts** to change them or assign gestures to other actions.
+Three-finger swipes up/down switch workspaces; left/right focus windows.
+The shortcut guide appears at login until disabled in Settings.
## Documentation
-- [Why Ferese](docs/why-ferese.md)
-- [Installation and recovery](docs/installation.md)
-- [Configuration reference](docs/configuration.md)
-- [Desktop widgets](docs/desktop-widgets.md)
-- [Notifications](docs/notifications.md)
-- [Native locker](docs/locking.md)
-- [Desktop portals and compatibility](docs/portals.md)
-- [Screen sharing and recording](docs/screen-sharing.md)
+Start with the [documentation index](docs/README.md) for user guides and
+[development instructions](docs/development.md) for builds and testing.
-## Development
-
-```sh
-cargo test --workspace --locked
-cargo fmt --all --check
-```
-
-Ferese is built in Rust on Smithay. Use release builds when evaluating motion
-and startup performance, and keep another desktop session available while
-testing hardware behavior.
+Ferese is written in Rust using Smithay. Report bugs on
+[GitHub](https://github.com/ferese-wm/ferese/issues) with reproduction steps and logs.
diff --git a/compositor/src/state.rs b/compositor/src/state.rs
index 770a608..a0a5c4e 100644
--- a/compositor/src/state.rs
+++ b/compositor/src/state.rs
@@ -65,7 +65,7 @@ use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager;
use crate::backends::direct::DirectBackendState;
use crate::config::{Binding, DaemonConfig, InputSettings, OutputProfile, ThemeSettings};
-use crate::cursor::NamedCursor;
+use crate::cursor::{NamedCursor, cursor_theme, load_named_cursor};
use crate::dimming::DimAnimation;
use crate::gestures::{Swipe, SwipeDirection};
use crate::handlers::screencopy::PendingScreencopy;
@@ -74,13 +74,14 @@ use crate::handlers::screenshot_worker::Worker;
use crate::input::LockedPointerHint;
use crate::input_capture::InputCapture;
use crate::ipc::IpcSocketGuard;
+use crate::overview::OverviewState;
use crate::portal_session::PortalSession;
use crate::portal_shortcuts::PortalShortcuts;
use crate::resize_transaction::ResizeTransaction;
use crate::session_lock::{IdleSettings, Lock};
use crate::shell_control::ShellSnapshot;
use crate::stacking::WindowStack;
-use crate::wallpaper::WallpaperState;
+use crate::wallpaper::{WallpaperConfig, WallpaperState};
use crate::window_rules::{WindowRule, resolve as resolve_window_rules};
use crate::winit::{
BlurProgram, MaterialBuffers, MaterialProgram, NestedBackend, OverviewScrim, ResizeSnapshot, RoundedClipPrograms,
@@ -344,10 +345,10 @@ pub struct Ferese {
}
pub struct RuntimeConfig {
- pub(crate) lock_idle: crate::session_lock::IdleSettings,
- pub(crate) autostart: Vec,
+ pub(crate) lock_idle: IdleSettings,
+ pub(crate) autostart: Vec,
pub(crate) overview_font_family: String,
- pub(crate) wallpaper: crate::wallpaper::WallpaperConfig,
+ pub(crate) wallpaper: WallpaperConfig,
pub layout_mode: LayoutMode,
pub gap_config: GapConfig,
pub input_settings: InputSettings,
@@ -371,9 +372,11 @@ impl Ferese {
config: RuntimeConfig,
) -> Result> {
let display_handle = display.handle();
+
crate::handlers::screencopy::init_global(&display_handle);
crate::effects::init_global(&display_handle);
crate::shell_control::init_global(&display_handle);
+
let alpha_modifier_state = AlphaModifierState::new::(&display_handle);
let compositor_state = CompositorState::new::(&display_handle);
let cursor_shape_state = CursorShapeManagerState::new::(&display_handle);
@@ -427,8 +430,8 @@ impl Ferese {
let socket_name = Self::init_wayland_listener(display, event_loop)?;
let start_time = Instant::now();
- let cursor_theme = crate::cursor::cursor_theme();
- let default_cursor = crate::cursor::load_named_cursor(&cursor_theme, CursorIcon::Default);
+ let cursor_theme = cursor_theme();
+ let default_cursor = load_named_cursor(&cursor_theme, CursorIcon::Default);
let named_cursors = HashMap::from([(CursorIcon::Default, default_cursor)]);
let mut state = Self {
@@ -457,10 +460,10 @@ impl Ferese {
resize_transactions: HashMap::new(),
resize_snapshots: HashMap::new(),
nested_backend: None,
- wallpaper: crate::wallpaper::WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())),
+ wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())),
maximized_windows: HashSet::new(),
maximized_column_widths: HashMap::new(),
- window_stack: crate::stacking::WindowStack::default(),
+ window_stack: WindowStack::default(),
floating_above_fullscreen: HashMap::new(),
natural_floating_pending: HashSet::new(),
window_borders: HashMap::new(),
@@ -473,7 +476,7 @@ impl Ferese {
overview_scrims: HashMap::new(),
material_programs: HashMap::new(),
material_buffers: HashMap::new(),
- portal_shortcuts: crate::portal_shortcuts::PortalShortcuts::default(),
+ portal_shortcuts: PortalShortcuts::default(),
pending_logout: None,
logout_query: None,
logout_owner: None,
@@ -506,20 +509,20 @@ impl Ferese {
cursor_theme,
named_cursors,
intercepted_keys: HashSet::new(),
- swipe: crate::gestures::Swipe::default(),
+ swipe: Swipe::default(),
idle_inhibitors: HashMap::new(),
active_shortcuts_inhibitor: None,
direct_backend: None,
_ipc_socket: None,
pending_dmabuf_imports: Vec::new(),
pending_screencopies: Vec::new(),
- screenshot: crate::handlers::screenshot::Coordinator::new(),
+ screenshot: Coordinator::new(),
screenshot_parts: None,
screenshot_worker: None,
shell_resources: Vec::new(),
shell_snapshot_serial: 0,
last_shell_snapshot: None,
- overview: crate::overview::OverviewState::with_font_family(config.overview_font_family),
+ overview: OverviewState::with_font_family(config.overview_font_family),
next_window_id: 1,
next_output_id: 1,
last_animation_tick: start_time,
@@ -557,6 +560,7 @@ impl Ferese {
state.screenshot_parts = Some(screenshot.parts);
state.screenshot_worker = Some(screenshot.worker);
state._ipc_socket = Some(screenshot._guard);
+
Ok(state)
}
diff --git a/crates/ferese-theme/README.md b/crates/ferese-theme/README.md
index de0b05f..6fac543 100644
--- a/crates/ferese-theme/README.md
+++ b/crates/ferese-theme/README.md
@@ -5,7 +5,7 @@ prompt and screen-sharing/recording chooser.
- `Palette` resolves KDL colors and corners and builds the native toolkit theme.
- `PRESETS` defines the six built-in color schemes.
-- `font` and `text` share bounded font-name storage and explicit font selection.
+- `font` and `text` provide shared font selection and text.
- `controls` supplies surfaces, navigation, font-aware text buttons and input styles.
- `menus` supplies headings, rows, sections, separators, switches and slider styles.
- `calendar` supplies date/time labels, stacked clock digits and calendar grids.
@@ -16,9 +16,8 @@ prompt and screen-sharing/recording chooser.
Keep the attachment alive with its window and make the client background
transparent after attachment succeeds. Other compositors retain the client fill.
-Applications own layout, data, actions and animation timing. Pass their current
-palette and animation progress into the shared components; do not build another
-native theme or copy a common control style into an application.
+Applications supply layout, data, actions, and animation progress. Extend shared
+components when a style is needed across apps.
```rust
let palette = ferese_theme::Palette::from_document(Some(&document));
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..e934802
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,29 @@
+# Documentation
+
+## Get started
+
+- [Installation](installation.md): dependencies, login, previews, updates, and recovery.
+- [Configuration](configuration.md): KDL syntax, layouts, input, shortcuts, and displays.
+- [Why Ferese](why-ferese.md): layouts and the shared desktop theme.
+
+## Use the desktop
+
+- [Shortcuts and gestures](shortcuts.md): keyboard navigation, Super-drag, resize, and window grouping.
+
+- [Desktop widgets](desktop-widgets.md): clocks and sticky notes.
+- [Notifications](notifications.md): popups, history, and Do Not Disturb.
+- [Lock screen](locking.md): appearance, automatic locking, and display sleep.
+- [Screen sharing and recording](screen-sharing.md): choosing sources and stopping a session.
+- [Desktop portals](portals.md): application integration and supported services.
+
+## Develop Ferese
+
+- [Development](development.md): builds, tests, and portal checks.
+- [Compositor architecture](ferese-window-manager-spec.md).
+- [Shell behavior](ferese-shell-design.md).
+- [Theme components](ferese-theme-ui-spec.md).
+- [Animation checks](animation-validation.md).
+- [Nested resize checks](nested-resize-testing.md).
+- [Client soak testing](native-soak-testing.md).
+- [Performance measurements](performance-baseline.md).
+- [Website development](../site/README.md).
diff --git a/docs/animation-validation.md b/docs/animation-validation.md
new file mode 100644
index 0000000..9806999
--- /dev/null
+++ b/docs/animation-validation.md
@@ -0,0 +1,26 @@
+# 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 maximized width. |
+| Exit maximize or 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 faa3c2c..74ee86e 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -2,8 +2,9 @@
Edit `~/.config/ferese/config.kdl` (or `$XDG_CONFIG_HOME/ferese/config.kdl`),
or open **Control Center → Settings**. Start with the [example](../packaging/config.kdl).
-All supported configuration sections are listed below; [desktop widgets](desktop-widgets.md)
-covers the complete clock and sticky-note fields separately.
+This reference covers layouts, appearance, input, and desktop behavior.
+See [Desktop widgets](desktop-widgets.md), [Notifications](notifications.md),
+and [Desktop portals](portals.md) for their additional options.
## KDL syntax
@@ -44,8 +45,7 @@ its command as positional arguments. Duplicate fields are rejected.
The dotted paths in the reference tables below describe nested sections.
Settings preserves comments and custom fields when editing, while normalizing
-indentation. Runtime configuration uses KDL only. KDL parsing requires Rust 1.95
-or newer to build.
+indentation. Configuration uses KDL.
## Saving and validation
@@ -74,15 +74,15 @@ Live config is limited to 60 KiB.
| `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 |
+| `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 |
| `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 |
+| `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 |
`minimal` scrolls only to reveal the focused window. `center_on_focus` centers
it. `paged` packs columns into viewport-sized pages: halves form pairs, thirds
@@ -94,95 +94,73 @@ Changing default width preserves manually resized columns.
| Section / key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `animations.enabled` | boolean | `true` | Enable motion |
-| `animations.reduced_motion` | boolean | `false` | Disable animated 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 |
+| `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 |
## Appearance
Settings → Appearance offers six presets: **Ferese Blue** (default),
**Monochrome**, **Gruvbox**, **Dracula**, **Ayu Light**, and **Monokai**.
-The two-row gallery previews each palette on a miniature desktop; a checkmark
-identifies the active preset.
-Ferese Blue uses the README logo's `#3D7BE6`. Each preset coordinates surfaces,
-text, menu-bar colors, borders and the focus ring. Ayu Light also switches native
-Settings controls to light styling. Presets preserve wallpaper, typography,
-geometry and opacity settings; colors remain individually editable.
-
-The community palettes are desktop adaptations with text shades adjusted for
-readability. Sources: [Gruvbox](https://github.com/morhetz/gruvbox),
-[Dracula](https://github.com/dracula/visual-studio-code),
-[Ayu](https://github.com/ayu-theme/ayu-colors), and
-[classic Monokai](https://github.com/microsoft/vscode/blob/main/extensions/theme-monokai/themes/monokai-color-theme.json).
-Marketplace install counts informed the shortlist; they are not a measurement
-of desktop users. Monochrome and Gruvbox replace similar blue palettes to keep
-these six visually distinct.
-
+Ferese Blue uses the logo accent, `#3D7BE6`. Presets update colors and preserve
+wallpaper, fonts, corners, and opacity. Colors remain individually editable.
| Section / key | Type | Default | Meaning |
| --- | --- | --- | --- |
-| `appearance.corner_radius` | number ≥ 0 | unset | Legacy fallback for shell 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 |
+| `appearance.corner-radius` | number ≥ 0 | unset | Legacy fallback for shell 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 | bundled Ferese wallpaper | Wallpaper image path; an existing selection overrides the default |
| `theme.background.mode` | `"fill"`, `"fit"` | `"fill"` | Crop or letterbox |
| `theme.material.style` | `"solid"`, `"translucent"` | `"solid"` | Shell background material |
| `theme.material.opacity` | number 0–1 | `0.78` | Shared shell background opacity for bars, menus, popovers, notifications and themed dialogs. Text/icons stay opaque; 0 hides the material. Solid mode is always opaque |
-| `theme.material.blur_radius` | number ≥ 0 | `12` | Translucent backdrop blur, capped at 32; 0 disables |
+| `theme.material.blur-radius` | number ≥ 0 | `12` | Translucent backdrop blur, capped at 32; 0 disables |
| `theme.colors` key | Default | Meaning |
| --- | --- | --- |
-| `surface_base` | `"#111821"` | Material/card base color |
-| `text_primary` | `"#F4F7FB"` | Main text |
-| `text_muted` | `"#8793A2"` | Secondary text |
+| `surface-base` | `"#111821"` | Material/card base color |
+| `text-primary` | `"#F4F7FB"` | Main text |
+| `text-muted` | `"#8793A2"` | Secondary text |
| `accent` | `"#3D7BE6"` | Active controls and solid focus ring |
| `border` | `"#FFFFFF18"` | Unfocused window border |
| `shadow` | `"#00000055"` | Window shadow color |
-Ferese derives the text and icon color for filled accent controls from the
-configured theme. It keeps the configured text color and, when necessary, shades
-the accent fill slightly to reach a 4.5:1 contrast ratio. If that would require a
-large color change, it chooses black or white instead. The same rule is shared by
-native dialogs, settings selections, and notification badges. Hover, pressed,
-and disabled accent-button states use the same policy. Small filled controls
-resolve accent transparency against their theme surface so wallpaper cannot
-make their labels unreadable. No separate per-application color setting is needed.
-The target follows [normal-text contrast guidance](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html).
+Filled controls choose a contrasting text color automatically.
| `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, independent of the shell |
-| `shell_radius` | number ≥ 0 | `14` | All shell surfaces, cards, widgets and interaction backgrounds; 0 makes them square |
-| `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 | unset | Legacy fallback for shell radius |
-| `panel_padding` | number ≥ 0 | `12` | Bar inner padding |
-| `control_gap` | number ≥ 0 | `12` | Right-side control spacing |
+| `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, independent of the shell |
+| `shell-radius` | number ≥ 0 | `14` | All shell surfaces, cards, widgets and interaction backgrounds; 0 makes them square |
+| `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 | unset | Legacy fallback for shell radius |
+| `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.text_primary` | color | `"#F0F3FA"` | Bar text/icons |
-| `theme.surface.bar.text_muted` | color | `"#AAB4C7"` | Inactive bar foreground |
-| `theme.shadow.soft.offset_y` | number | `4` | Window shadow vertical offset |
+| `theme.surface.bar.background` | color | `"#1C202EF2"` | Shell fallback background; compositor materials use `surface-base` |
+| `theme.surface.bar.text-primary` | color | `"#F0F3FA"` | Bar text/icons |
+| `theme.surface.bar.text-muted` | color | `"#AAB4C7"` | Inactive bar foreground |
+| `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 |
Solid materials omit blur; translucent materials blur behind the surface color.
True fullscreen removes decorations. Focus transitions never scale the content.
-Optional `theme.focus_ring.gradient` and `theme.border.gradient` use the same
+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.
@@ -199,16 +177,25 @@ theme {
}
```
+## Workspaces
+
+A single-output session starts with workspace 1. Numbered workspace shortcuts create
+workspaces on demand; switching forward past the last workspace also creates the
+next one when the current workspace contains windows. Empty workspaces are
+cleaned up automatically, keeping at most one empty workspace in addition to
+any empty workspace currently shown on another display. Additional displays
+receive their own workspace.
+
## Input
| `input` key | Type | Default | Meaning |
| --- | --- | --- | --- |
-| `focus_follows_mouse` | boolean | `false` | Focus visible windows under the pointer without raising them or scrolling; click or use keyboard focus to reveal a window |
-| `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 |
+| `focus-follows-mouse` | boolean | `false` | Focus visible windows under the pointer without raising them or scrolling; click or use keyboard focus to reveal a window |
+| `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 |
With focus-follows-mouse enabled, hovering a visible part of a window changes
keyboard focus without raising it or moving the scrolling viewport. Clicking or
@@ -221,7 +208,7 @@ Clicking a workspace card's background switches workspaces while keeping Overvie
open. Previews follow layout order and use the configured shell font and colors.
The touchpad device keys are booleans, all defaulting to `true`: `tap`,
-`natural_scroll` and `disable_while_typing`. These apply to DRM devices; nested
+`natural-scroll` and `disable-while-typing`. These apply to DRM devices; nested
previews use the host's physical input settings.
Three-finger swipes navigate on release: up goes to the next workspace on the
@@ -254,6 +241,8 @@ Adjust recognition distance in **Settings → Keyboard & mouse**, or with
## Commands and bindings
+See [Shortcuts and gestures](shortcuts.md) for the full default keymap and mouse actions.
+
`commands` maps arbitrary names to nonempty argument arrays. The built-in
`terminal "foot"` can be overridden. Commands run directly, without a shell.
@@ -292,7 +281,7 @@ 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+S area screenshot; Print Screen whole-screen screenshot;
-Super+Shift+E immediate logout.
+Super+Shift+E logout confirmation.
Hold Super and drag a floating window with the left mouse button to move it.
Hold Super and drag with the right mouse button to resize it from the nearest
@@ -314,19 +303,8 @@ the same save and copy workflow.
`ferese-screenshot --all` captures every enabled output as one image.
-The editor window is sized from the captured image rather than the output, so a
-small area selection does not open a large window. Sizing is advisory: the
-compositor floats the editor at whatever size Satty requests, clamped to the
-workspace.
-
-A capture is assembled from the readback each output produces when it is next
-redrawn, so a request is abandoned if an output has not been redrawn within 30
-seconds. That is reported as an error rather than a partial or empty image, and
-the request holds no resources once it is abandoned. Nothing is left in the
-runtime directory: `feresctl screenshot` writes a private file that the caller
-opens and unlinks, and any file a dead client leaves behind is reclaimed after
-an hour.
-
+Satty opens as a floating window sized for the captured image and constrained
+to the workspace.
## Window rules
@@ -335,7 +313,7 @@ window rules can override that behavior.
| `window-rule` key | Type | Default / meaning |
| --- | --- | --- |
-| `app_id` | nonempty string | Optional exact match; case-insensitive, `.desktop` suffix ignored |
+| `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 |
@@ -369,8 +347,8 @@ are connected wins. Find connector names and persistent identities using
| `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
+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.
```kdl
@@ -390,10 +368,10 @@ returns. Ferese does not change the system's suspend policy.
| `status` key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `keybinding-guide` | boolean | `true` | Show the active shortcut guide at login until disabled in Settings → Shortcuts. |
-| `window_title` | boolean | `true` | Focused window title in the bar center when space allows |
-| `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 |
+| `window-title` | boolean | `true` | Focused window title in the bar center when space allows |
+| `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
@@ -443,22 +421,19 @@ environment for screenshots to work.
Text input is available by default; Ctrl+Alt+Escape releases an active shortcut
inhibitor. Use `feresectl --help` for runtime control commands.
-## Notifications
-
-Settings → Notifications controls popups, Do Not Disturb, and the default timeout. See [Notifications](notifications.md) for KDL options and app behavior.
-
-A single-output session starts with workspace 1. Numbered workspace shortcuts create
-workspaces on demand; switching forward past the last workspace also creates the
-next one when the current workspace contains windows. Empty workspaces are
-cleaned up automatically, keeping at most one empty workspace in addition to
-any empty workspace currently shown on another display. Additional displays
-receive their own workspace.
-
Shell rounding is controlled in Settings → Appearance → Shell corner radius.
Small controls cap the radius to fit their size. Window rounding remains under
-Settings → Windows. `theme.geometry.shell_radius` takes precedence over the old
-`appearance.corner_radius` and then `theme.geometry.top_bar_radius` keys; when
+Settings → Windows. `theme.geometry.shell-radius` takes precedence over the old
+`appearance.corner-radius` and then `theme.geometry.top-bar-radius` keys; when
none are set, the shell uses 14 px. Legacy clock/note radius fields no longer
override shell rounding.
-Power actions use a centered system confirmation. The `exit` key binding (Super+Shift+E by default) requests logout confirmation; `feresectl request-logout` does the same. `feresectl exit` remains an immediate administrative exit.
+## Power and logout
+
+Power actions use a centered confirmation. **Super+Shift+E** and
+`feresectl request-logout` ask to log out. `feresectl exit` ends the session
+immediately.
+
+## Notifications
+
+See [Notifications](notifications.md) for popups, Do Not Disturb, and timeout options.
diff --git a/docs/desktop-widgets.md b/docs/desktop-widgets.md
index 1ff87c2..2e5bd66 100644
--- a/docs/desktop-widgets.md
+++ b/docs/desktop-widgets.md
@@ -1,13 +1,8 @@
# 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.
+Clock and sticky-note widgets sit above the wallpaper and behind windows.
+Drag the clock or a note's title to move it. Release to save, or press Escape to
+cancel. Widgets stop at each other's edges on the same display.
Enable it in Settings → Desktop widgets or in your Ferese config:
@@ -42,42 +37,40 @@ desktop-widgets {
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`.
+`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.
-Pixel is the default clock style: heavy hours above minutes, alternating theme colors, and a small plain date above. Its bundled Cantarell display font can be replaced with the font selector. Minimal retains the plain time/date layout. Pixel zero-pads hours and hides am/pm for the stacked display. Formats with seconds or additional text retain their full formatted label on one line. Both styles shrink time text to fit smaller widget dimensions; an explicit text color overrides the alternating colors.
+Choose **Pixel** for stacked hours and minutes with alternating theme colors,
+or **Minimal** for a plain time/date layout. Pixel zero-pads hours and hides am/pm.
+Formats containing seconds or extra text use a single line. Both styles shrink
+time text to fit. Select a different font in Settings, or set `font-family`.
-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 KDL.
-Sticky notes also support editing directly on the desktop.
+Changes apply live. Colors and fonts follow the theme unless overridden.
+Formatting uses [strftime-compatible formats](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html).
+Use `%H:%M` for 24-hour time or `%-I:%M %p` for 12-hour time.
## Clock limits
+Clock and note corners follow the shared shell radius. Their `radius` fields
+are accepted for compatibility but do not override it.
+
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
+`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 |
+| `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 |
+| `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.
+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
@@ -87,7 +80,8 @@ 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.
-Choose each note’s font in Settings using the installed-font dropdown or enter a family name manually. Default font inherits the interface font. In KDL, use `font-family "Comfortaa"` inside the `note` node.
+Choose a note’s font in Settings or set `font-family "Comfortaa"` in its `note`
+node. Without an override, notes use the interface font.
These are plain-text notes, not Markdown or HTML.
@@ -99,9 +93,7 @@ desktop-widgets {
Repeat the `note` node 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.
+click-through card. Long text wraps; increase height if needed.
| Key | Type | Default / accepted values |
| --- | --- | --- |
@@ -112,10 +104,10 @@ compositor, not an output-sized canvas. It is released when dragging ends.
| `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 |
+| `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 |
+| `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 |
diff --git a/docs/development.md b/docs/development.md
new file mode 100644
index 0000000..d7aa035
--- /dev/null
+++ b/docs/development.md
@@ -0,0 +1,80 @@
+# Development
+
+Install the [build dependencies](installation.md#requirements), then run:
+
+```sh
+cargo build --release --locked --workspace
+cargo test --workspace --locked
+cargo fmt --all --check
+```
+
+Use release builds for performance and animation checks. Preview changes in a
+[nested session](installation.md#preview-and-logs) before testing hardware behavior.
+Keep another desktop available for direct-session recovery.
+
+## Test guides
+
+- [Animation checks](animation-validation.md): transitions, interruption, and hit testing.
+- [Nested resize checks](nested-resize-testing.md): wallpaper and bar responsiveness.
+- [Client soak testing](native-soak-testing.md): repeated client lifecycle and resource growth.
+- [Performance measurements](performance-baseline.md): sampling and historical results.
+
+The [compositor](ferese-window-manager-spec.md), [shell](ferese-shell-design.md),
+and [theme](ferese-theme-ui-spec.md) references describe the main code boundaries.
+
+## Portal checks
+
+Run backend unit tests and compare D-Bus contracts against installed portal XML:
+
+```sh
+cargo test --locked -p xdg-desktop-portal-ferese
+python3 scripts/tests/test_portal_contracts.py
+```
+
+The contract test uses a private bus. It checks signatures, appearance changes,
+invalid-config retention, and caller authorization. It does not prove complete
+compatibility with every app or open consent dialogs.
+
+With release binaries built, run isolated integration checks:
+
+```sh
+FERESE_TEST_SHORTCUTS=1 python3 scripts/tests/test_shortcuts_isolated.py
+FERESE_TEST_WINDOW_CAPTURE=1 python3 scripts/tests/test_window_capture_isolated.py
+FERESE_TEST_INHIBIT=1 python3 scripts/tests/test_inhibit_isolated.py
+FERESE_TEST_RESTORE=1 python3 scripts/tests/test_restore_isolated.py
+FERESE_TEST_WINDOW_STREAM=1 python3 scripts/tests/test_window_stream_isolated.py
+```
+
+These cover shortcut cleanup, window capture, inhibitors, restored permissions,
+and live window streams. Check each script's dependencies before running it.
+They use disposable nested sessions or private buses.
+
+For monitor streaming and lock revocation, install Python GI/GStreamer with
+`pipewiresrc`, `dbus-daemon`, `xdg-desktop-portal`, and `bwrap`, then run:
+
+```sh
+FERESE_TEST_PORTAL=1 \
+FERESE_TEST_PORTAL_BINARY=target/release/xdg-desktop-portal-ferese \
+python3 scripts/tests/test_portal_isolated.py
+```
+
+Add `FERESE_TEST_PORTAL_CONSENT=1` to exercise the picker. Choose the temporary
+display and click **Share**. Tests must target the nested preview, not the host desktop.
+
+Check real encoding and the private InputCapture transport:
+
+```sh
+cargo test --locked -p xdg-desktop-portal-ferese --bin ferese-record encoder_finishes_a_real_webm -- --ignored
+cargo test --locked -p ferese input_capture::tests
+cargo test --locked -p xdg-desktop-portal-ferese eis::tests -- --ignored
+```
+
+Encoding needs the recorder's GStreamer plugins. The transport test needs libei
+and uses synthetic input over a private socket pair. Hardware pointer barriers,
+multiple monitors, display sleep, and live authentication still need session tests.
+
+## Report a bug
+
+Include the commit or installed release, reproduction steps, backend (nested or
+direct), monitor scales/transforms, and relevant logs. Avoid publishing passwords,
+access tokens, or private window content.
diff --git a/docs/ferese-shell-design.md b/docs/ferese-shell-design.md
new file mode 100644
index 0000000..40fea05
--- /dev/null
+++ b/docs/ferese-shell-design.md
@@ -0,0 +1,65 @@
+# Shell behavior
+
+This is a maintainer reference. User options are documented in
+[Configuration](configuration.md). Common UI belongs in
+[ferese-theme](../crates/ferese-theme/README.md).
+
+## Bar and popovers
+
+The shell creates a bar for each output. Workspace buttons use compositor state
+and appear in numeric order. The active window title occupies the center when
+space allows. Date and time form one calendar button. Right-side controls open
+sound, battery, notifications, recording, and Control Center popovers.
+
+A popover is anchored to its control and constrained to the same output.
+Opening another primary popover replaces the current one. Popovers reserve no
+workspace space; the bar reserves its configured height and margins.
+True fullscreen hides the bar without changing the preserved tiled layout.
+
+Use real service availability for controls. Handle missing services and failed
+actions without blocking the UI. Settings opens as a separate application.
+
+## Overview
+
+The compositor presents window previews; the shell supplies labels and workspace
+controls. Do not continuously copy windows into the shell process.
+
+Clicking a workspace background selects that workspace and keeps overview open
+so its windows can be selected. Clicking a window activates that exact window.
+Directional keys change selection, Enter activates it, and Escape dismisses.
+Activation must use the selected window ID, not the window focused before overview.
+
+## Focus and dialogs
+
+The bar and notification popups do not normally take keyboard focus. Interactive
+shell surfaces restore the previous valid app focus when dismissed.
+
+Power actions and logout use a centered system confirmation, including blockers
+when present. Input outside the confirmation must not reach applications.
+Authentication, screen-sharing, and recording dialogs are separate themed windows.
+Use one visible frame, content-sized height, and an output-sized height limit.
+Scrollable lists must leave headings and action buttons accessible.
+
+Attach dialog material to the content card using the shared theme helper.
+Match blur regions to the displayed card throughout opening and resize; do not
+paint an opaque window behind a translucent card.
+
+## Notifications and widgets
+
+The shell provides the standard notification service. Normal popups respect
+Do Not Disturb; critical messages bypass it. History is held in memory and clears
+when the shell exits. See [Notifications](notifications.md).
+
+Clock and note widgets sit behind application windows, reserve no workspace space,
+and save positions after dragging. Only interactive notes take keyboard focus.
+See [Desktop widgets](desktop-widgets.md).
+
+## Lifecycle
+
+Shell failure must not destroy managed windows or workspace state. Reconstruct
+bars and controls from compositor state after reconnecting, and clear dead shell
+focus. Notification history is not reconstructed from disk.
+
+Keep slow service queries, image loading, and filesystem work away from UI event
+handling. Stop animation work once the UI settles. Validate popover placement,
+fullscreen transitions, dialog dismissal, and shell restart in a nested session.
diff --git a/docs/ferese-theme-ui-spec.md b/docs/ferese-theme-ui-spec.md
new file mode 100644
index 0000000..221a143
--- /dev/null
+++ b/docs/ferese-theme-ui-spec.md
@@ -0,0 +1,81 @@
+# Theme components
+
+Ferese's native apps use [ferese-theme](../crates/ferese-theme/README.md) for
+colors, fonts, controls, menus, icons, calendar elements, and dialog materials.
+Use [Configuration → Appearance](configuration.md#appearance) for supported keys
+and defaults.
+
+## Shared appearance
+
+The six presets are Ferese Blue, Monochrome, Gruvbox, Dracula, Ayu Light, and
+Monokai. Ferese Blue uses the logo accent, `#3D7BE6`. Presets change colors while
+preserving wallpaper, typography, corners, and opacity.
+
+The interface font applies to shell text and native dialogs. Clock and note
+widgets can override it. Managed-window corners and shell corners are separate
+settings; small controls cap their corners to fit their size.
+
+## Materials
+
+Two material styles are supported:
+
+| Style | Background |
+| --- | --- |
+| `solid` | Opaque theme surface |
+| `translucent` | Theme tint over blurred background content |
+
+Bars, popovers, notifications, and themed dialogs share `theme.material.opacity`
+and `blur-radius`. Text and icons stay opaque. An opacity of zero hides the
+material; blur radius zero disables blur. Solid mode stays opaque.
+
+```kdl
+theme {
+ colors {
+ accent "#3D7BE6"
+ }
+ material {
+ style "translucent"
+ opacity 0.78
+ blur-radius 12
+ }
+ geometry {
+ shell-radius 14
+ window-radius 14
+ }
+}
+```
+
+The compositor owns blur and tint. `material::ModalMaterial` attaches native
+dialogs to that rendering path. Keep the attachment alive with the window and
+make the attached client background transparent. Outside Ferese, retain a client
+background so the dialog remains usable.
+
+## Components
+
+| Module | Use |
+| --- | --- |
+| `palette` | Resolve configuration colors and build the native toolkit theme |
+| `controls` | Surfaces, navigation, text buttons, and input styles |
+| `menus` | Rows, headings, sections, separators, switches, and sliders |
+| `calendar` | Date/time labels, clock digits, and calendar grids |
+| `icons` | Shared SVG assets, sizing, and tint |
+| `typography` | Shared font selection and text |
+| `contrast` | Readable labels on filled controls |
+| `material` | Attach modal cards to compositor materials |
+
+Applications provide data, layout, actions, and animation progress. Extend shared
+components when a style is needed in multiple apps. Avoid hardcoded app-specific
+fonts, foreground colors, or opacity values.
+
+## UI checks
+
+- Check text and icons in all six presets, including Ayu Light.
+- Check rest, hover, pressed, focused, selected, and disabled controls.
+- Keep accent-button labels readable after typing or changing focus.
+- Keep controls usable with solid materials and blur disabled.
+- Align material regions with the visible card at every output scale.
+- Give dialogs one frame; cap their height and scroll their content, preserving actions.
+- Respect reduced motion and stop requesting animation frames after settling.
+
+Theme changes affect appearance; they do not change focus, workspace ownership,
+input permissions, or authentication policy.
diff --git a/docs/ferese-window-manager-spec.md b/docs/ferese-window-manager-spec.md
new file mode 100644
index 0000000..106c14b
--- /dev/null
+++ b/docs/ferese-window-manager-spec.md
@@ -0,0 +1,109 @@
+# Compositor architecture
+
+This is a maintainer reference. For supported configuration and shortcuts, use
+[Configuration](configuration.md); for builds and checks, use [Development](development.md).
+
+## Processes and crates
+
+| Component | Responsibility |
+| --- | --- |
+| `ferese` | Wayland clients, windows, workspaces, input, outputs, and rendering |
+| `ferese-shell` | Bar, popovers, notifications, widgets, and system confirmations |
+| `ferese-settings` | Configuration editor |
+| `feresectl` | Command-line control |
+| `ferese-lock` | Lock-screen UI and PAM authentication |
+| `ferese-polkit-agent` | System authentication prompts |
+| `xdg-desktop-portal-ferese` | Desktop portal requests and PipeWire streams |
+| `ferese-record` | Video encoding and saving |
+
+The compositor uses Smithay and calloop. The direct backend renders through
+DRM/KMS; the nested backend renders into a window on another Wayland desktop.
+Native UIs use libcosmic. Layout, animation, configuration, IPC, protocols, and
+[theme components](../crates/ferese-theme/README.md) live in separate crates.
+
+## State and ownership
+
+The event loop owns compositor state and Wayland objects. Background workers
+return results through bounded channels; results must be checked against current
+window, output, and request identities before application.
+
+Keep these relationships valid after a state change:
+
+- Each window belongs to one workspace and appears once in its layout or floating list.
+- Each attached workspace belongs to one output; each output has one active workspace.
+- A window's output follows workspace ownership.
+- Fullscreen preserves the underlying layout or floating placement.
+- Focus points to a live window or authorized shell surface.
+- Workspace focus history is distinct from current keyboard focus.
+
+Monitor removal migrates workspaces to a remaining output. Returning monitors
+restore eligible workspaces; explicit user moves take priority. Selecting a
+workspace visible on another monitor focuses that monitor.
+
+## Layout and presentation
+
+Scrolling layouts arrange columns horizontally. Windows within a column divide
+its height. A workspace owns its viewport; presentation subtracts that viewport
+from window positions. Tree layouts use nested splits and stacks. Layout changes
+preserve window membership and floating placement.
+
+Keep layout targets, animated rectangles, and committed client sizes separate.
+A layout transition requests the destination client size rather than sending
+one configure per animation frame. Available content is scaled and clipped until
+the matching client buffer arrives. Hit testing uses the same presented geometry.
+Interrupted animations continue from their current position.
+
+Input and layout use logical pixels. Rendering converts them using the output's
+scale and transform. Application popups use their parent's output and presented
+position when calculating bounds.
+
+## Rendering and frame delivery
+
+Render damage from client commits, animations, effects, cursor motion, and output
+changes. Movement damages both old and new bounds, including shadows and blur.
+Output-specific callbacks follow window ownership. Presentation feedback is
+collected only for surfaces the renderer actually displayed on that output.
+An unchanged frame still permits paced client callbacks without submitting a
+new display buffer.
+
+Animations use elapsed time across outputs, with a timer while transitions remain
+active. An idle focused monitor must not stall another monitor's animation.
+
+The compositor renders blur from content behind the requesting surface. Material
+regions and client content must share coordinates during placement, scaling,
+and animation. See [Theme components](ferese-theme-ui-spec.md).
+
+## Input
+
+Use milliseconds for keyboard, pointer, touch, and gesture protocol events;
+relative-motion timestamps retain microseconds. Keep keyboard focus, pointer
+focus, and window stacking separate. Hover focus does not scroll or raise windows.
+
+Pointer grabs and constraints must end cleanly when their surface disappears.
+A locked pointer's position hint is stored while locked, then resolved against
+current surface geometry and clamped to an output when the lock is released.
+
+## IPC and private protocols
+
+User control uses `$XDG_RUNTIME_DIR/ferese/control.sock`, restricted to the
+session user. The parent directory is private and peers are checked by UID.
+IPC frames are length-prefixed JSON with bounded message sizes, workers, and queues.
+See `feresectl --help` for commands.
+
+Private Wayland capabilities grant shell control or material effects to trusted
+components. Public layer-shell access alone grants neither. Trusted connections
+and descriptors must not leak to launched applications. Capture paths check
+session policy and reject capture while locked.
+
+Session locking must protect every output, including hotplug, and remain secure
+if the lock UI crashes. [Portal integration](portals.md) documents application
+consent and the currently supported services.
+
+## Review and testing
+
+Check output ownership, presented geometry, focus restoration, request lifetimes,
+and bounded work whenever changing a protocol or rendering path. Test both
+backends; a nested result does not establish hardware behavior.
+
+Use the [animation](animation-validation.md), [resize](nested-resize-testing.md),
+and [soak](native-soak-testing.md) checks alongside the workspace tests.
diff --git a/docs/installation.md b/docs/installation.md
index c57e45e..dc061f7 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -6,23 +6,12 @@ TTY. You do not need to replace your existing login manager or desktop.
## Requirements
-You need a working graphics driver with DRM/KMS and EGL support, an authenticated
-local login session with seat access through logind or seatd, D-Bus, and
-Linux-PAM for the native locker. A nested preview needs an existing Wayland
-desktop instead of direct access to the graphics device.
+Use a Wayland-capable graphics driver and a local user session with seat access
+through logind or seatd. A nested preview runs inside an existing Wayland desktop.
-Build with **Rust 1.95 or newer**, a C/C++ toolchain, CMake, pkg-config, and the
-native development libraries below. The installer builds Ferese, its shell,
-Settings, command-line tools, locker, polkit agent, and desktop portal backend together. It does not install system
-packages or enable background services at boot. On systemd systems, the installed
-session helper activates `ferese-session.target` after exporting the compositor's
-Wayland display, and stops it when the shell exits. This keeps
-`graphical-session.target` active so desktop portals can start. The native portal
-is D-Bus activated and supervised by systemd; it stops with the session so a
-previous release cannot remain running after logout. Session startup also refreshes
-an already-running native portal after importing the current display environment.
-Nested previews
-do not alter the host's activation environment or session targets.
+Building requires **Rust 1.95 or newer**, a C/C++ toolchain, CMake, pkg-config,
+and the libraries below. The installer builds all Ferese components; install
+system packages separately.
### Fedora
@@ -60,11 +49,9 @@ sudo apt install build-essential git curl cmake pkg-config libwayland-dev \
gstreamer1.0-plugins-good
```
-These are package recipes, not a claim that every distribution release has been
-tested. Fedora is the currently exercised installation path. Older releases
-may need newer packages or additional repositories. For other distributions,
-install the equivalent libraries; see the upstream
-[Smithay dependencies](https://github.com/Smithay/smithay#system-dependencies).
+Fedora is the tested installation path. Package names and versions may differ
+on other distributions. See [Smithay dependencies](https://github.com/Smithay/smithay#system-dependencies)
+for equivalent libraries.
### Rust toolchain
@@ -98,32 +85,17 @@ The script builds locked release binaries, then requests administrator access
through `sudo` or `pkexec` for installation. Do not run the whole build as root.
The first build needs network access to fetch Rust dependencies.
-The installer adds:
+The installer adds the session launcher, desktop tools, portal backend, login
+entry, icons, default wallpaper, and example config. Releases live under
+`/usr/local/lib/ferese/releases/`; commands live under `/usr/local/bin/`.
+The `current` and `previous` links support updates and rollback.
-- Versioned releases under `/usr/local/lib/ferese/releases/`, with `current` and
- `previous` links for upgrades and rollback.
-- Commands under `/usr/local/bin/`, including `ferese-session`, `ferese-settings`,
- `feresectl`, `ferese-lock`, `ferese-polkit-agent`, `ferese-screenshot`, and
- `xdg-desktop-portal-ferese`, and `ferese-record`.
-- D-Bus activation and Ferese-specific desktop portal registration.
-- A Wayland session entry at `/usr/share/wayland-sessions/ferese.desktop`.
-- A Settings application entry, icons, the default wallpaper, and an example config.
-- `/etc/pam.d/ferese-lock`, using your distribution's authentication stack.
- An existing administrator-supplied policy is preserved.
-
-Your existing configuration and other desktop sessions are preserved. The
-installer does not change your default session or login manager.
-
-Ferese starts its own polkit authentication agent with the shell. Password
-prompts follow the configured font, accent, surface color, and corner radius.
-Authentication still uses polkit's system helper and policy. The agent starts
-only inside a Ferese session; logging into another desktop leaves its agent
-unchanged. Restart your Ferese session after upgrading to use the new prompt.
+Your configuration, other desktop sessions, and existing PAM policy are preserved.
+Ferese starts its authentication agent and portal services with the session.
### Initial configuration
-Ferese has built-in defaults. To start from the example without replacing an
-existing config:
+Ferese has built-in defaults. To start from the example without replacing your config:
```sh
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/ferese"
@@ -144,9 +116,7 @@ command in Settings if you prefer another terminal.
Save your work and log out. Open your login screen's session selector, choose
**Ferese**, and sign in. Any manager that supports Wayland sessions and reads
-`/usr/share/wayland-sessions/` can discover the installed entry. The selector's
-location and whether it appears before or after choosing your account depend
-on the login manager.
+`/usr/share/wayland-sessions/` can discover the installed entry.
If Ferese is missing, confirm that the session entry exists and its launcher is
executable:
@@ -169,15 +139,9 @@ If your greeter asks for a session command instead of reading desktop files, use
/usr/local/bin/ferese-session
```
-For a setup such as greetd, configure that as the command launched **after user
-authentication**, using your greeter's session-command option. Preserve the
-existing authentication and greeter configuration. Do not set Ferese itself as
-the privileged greeter process. See the
-[greetd documentation](https://sr.ht/~kennylevinsen/greetd/) for your chosen frontend.
-
-The launcher starts the compositor and shell together, sets the desktop
-environment variables, and starts a D-Bus session when one is not already
-available. Launch it instead of the bare compositor for a complete desktop.
+For greetd, use this as the session command after authentication; see the
+[greetd documentation](https://sr.ht/~kennylevinsen/greetd/).
+The launcher starts the compositor, shell, and session services together.
### From a TTY
@@ -229,7 +193,8 @@ inside an existing Wayland desktop:
```sh
cargo build --release --locked -p ferese -p ferese-shell -p ferese-settings -p feresectl -p ferese-lock -p xdg-desktop-portal-ferese
-target/release/ferese --backend nested --grant-effects --grant-shell-control -- \
+FERESE_ENABLE_SCREENCOPY=1 target/release/ferese --backend nested \
+ --grant-effects --grant-shell-control -- \
target/release/ferese-shell
```
@@ -283,12 +248,13 @@ Installer options:
`--dry-run` prints the planned commands. `--offline` requires dependencies to be
cached already. `--skip-build` installs the binaries already in `target/release/`;
-use it only after building all six components from the intended revision.
+use it only after building all components from the intended revision.
Run `./scripts/install.sh --help` for all options.
## Logout and recovery
-Save your work before **Super+Shift+E** or `feresectl exit`: logout is immediate.
+Use **Super+Shift+E** or `feresectl request-logout` to ask for logout confirmation.
+`feresectl exit` ends the session immediately; save your work first.
If Ferese freezes, switch to another TTY with **Ctrl+Alt+F1–F12**, sign in, and
identify the affected compositor process:
diff --git a/docs/locking.md b/docs/locking.md
index 1f7e2b4..9aefbf4 100644
--- a/docs/locking.md
+++ b/docs/locking.md
@@ -1,79 +1,43 @@
-# Native locker
+# Lock screen
-Ferese Lock uses `ext-session-lock-v1`, not a fullscreen application overlay.
-Each output receives its own surface, including outputs added while locked.
-The compositor protects the desktop if the client exits or crashes.
+Ferese Lock follows your desktop wallpaper, colors, font, and shell corners.
+Open **Settings → Lock screen** to change its clock, date, wallpaper softness,
+and inactivity delays.
-## Preview
+## Preview and lock
```sh
-cargo build --release --locked -p ferese-lock
-target/release/ferese-lock --preview
+ferese-lock --preview # ordinary window; does not lock or check passwords
+ferese-lock # lock; returns after compositor confirmation
+ferese-lock --foreground # stay attached until unlock
```
-This is an ordinary, closable window. Submitting the field does not call PAM,
-lock, or unlock anything. Do not type your actual password into a visual preview.
+Do not enter your real password in a preview. The account picture comes from
+AccountsService or `~/.face`; a silhouette is used when neither is available.
-Settings → Lock screen provides clock visibility, date visibility, time format,
-wallpaper dimming and softness, inactivity delays, and a safe preview button. Your account picture is loaded from
-AccountsService or `~/.face`, with a user silhouette as a fallback.
+Escape clears the password. Caps Lock is shown beside authentication status.
+Failed attempts show an error and delay retries. Authentication uses the system's
+PAM policy; both password and account validation must succeed.
-The locker reads Ferese's wallpaper, theme colors, typography, and shell radius
-at startup. The wallpaper is softened once and shared across displays. Set softness
-to zero to keep it sharp. Escape clears the password. Caps Lock is shown above authentication
-status. Password verification runs on a worker, and repeated attempts are held
-for two seconds after failure, in addition to delays enforced by PAM.
+## Automatic locking
-## Installation and locking
+Install `swayidle`, then add this to `~/.config/ferese/config.kdl` to lock after
+five minutes and before system sleep:
-The normal `scripts/install.sh` includes the native binary and installs
-`/etc/pam.d/ferese-lock` if absent. Existing administrator policies are preserved.
-Fedora/Arch installations use `system-auth`; Debian-family installations use
-`common-auth` and `common-account`; the fallback uses the system `login` stack.
-There is no setuid binary, password command argument, or shadow-file reader.
-Both authentication and account validation must succeed. Expired credentials
-must be updated outside the locker. Interactive multi-factor enrollment and
-password changes are not supported by this initial password UI.
-
-```sh
-ferese-lock # returns after confirmation, UI stays running
-ferese-lock --foreground # stays attached until unlock
+```kdl
+autostart {
+ command "swayidle" "-w" "timeout" "300" "ferese-lock" "before-sleep" "ferese-lock" "lock" "ferese-lock"
+}
```
-The `swayidle -w` example in [Configuration](configuration.md#login-items-and-locking)
-can invoke the default command. Auto-locking is opt-in. This change does not
-install or start an idle daemon, replace your login manager, or change the Control Center's
-suspend action. Test real password authentication inside a nested Ferese session
-before enabling this on your desktop.
-
-## Security and recovery
-
-Never interpret the preview as proof of a locked desktop. A missing PAM policy
-or unsupported compositor produces an error before a lock request. A failure
-or authentication-worker panic never authorizes unlock. A crash after the lock
-request may leave the session permanently locked; end that session from another
-TTY if necessary. Do not restart the compositor to test crash behavior on a live
-session with unsaved work.
-
-The pinned libcosmic backend emits `Locked` twice: once when requesting a lock
-and once on the actual protocol callback. The startup handshake waits for the
-second notification. Re-audit this behavior whenever upgrading libcosmic. The
-parent times out with an error after 15 seconds and leaves the UI running; it
-never reports success merely because the window appeared.
-
-Validation references: [session-lock protocol](https://wayland.app/protocols/ext-session-lock-v1),
-[PAM authentication](https://man7.org/linux/man-pages/man3/pam_authenticate.3.html),
-and [PAM account checks](https://man7.org/linux/man-pages/man3/pam_acct_mgmt.3.html).
+Test password unlocking before enabling automatic locking. The installer provides
+`/etc/pam.d/ferese-lock` and preserves an existing administrator policy.
## Inactivity and display sleep
-A locked display dims after 30 seconds without input and sleeps after 120 seconds.
-Dimming fades over half a second. Any keyboard, pointer or touch activity restores
-brightness and wakes the display while keeping the session locked. The compositor
-owns this policy, so it remains effective if the locker crashes. Sleep starts only
-after the compositor has confirmed that every display is protected.
-
-Settings → Lock screen adjusts both delays. Zero disables that action:
+Locked displays dim after 30 seconds without input and sleep after 120 seconds.
+Keyboard, pointer, or touch activity wakes them without unlocking. Set either
+delay to zero to disable it, in Settings or KDL:
```kdl
lock-screen {
@@ -82,28 +46,19 @@ lock-screen {
}
```
-These delays are independent of automatic locking. Existing idle inhibitors from
-apps cannot keep an already locked display awake. In a direct session, display
-sleep uses DRM DPMS without removing outputs or rearranging workspaces; nested
-previews show black instead of powering off the host display. This puts displays
-to sleep, not the computer. Suspend remains a separate system power action.
+These delays start after locking, independently of the automatic-lock timer.
+Display sleep does not suspend the computer. A nested preview shows black
+instead of powering off the host display.
-## Practical swaylock comparison
+## Recovery and limits
-| Behavior | Ferese Lock |
-| --- | --- |
-| Secure Wayland locking and protection after a locker crash | Session-lock protocol, compositor-owned fallback |
-| All displays, including hotplug | Separate lock surface for each display |
-| Readiness before an idle manager permits suspend | Default command waits for compositor confirmation |
-| Password verification | PAM authentication and account checks on a worker |
-| Password clearing and Caps Lock feedback | Escape clears; Caps Lock indicator |
-| Authentication failure feedback and retry delay | Error message and bounded retry delay |
-| Wallpaper, font and color customization | Shared Ferese theme and Settings |
-| Inactivity and display power | Compositor dimming and DPMS; input restores brightness |
+The lock covers all displays, including newly connected ones. If the lock UI
+crashes, the compositor keeps the session protected. Recovery may require ending
+that session from another TTY; see [Recovery](installation.md#logout-and-recovery).
-[Swaylock](https://github.com/swaywm/swaylock/blob/master/swaylock.1.scd) provides
-additional appearance flags, layout indicators and readiness-FD CLI options.
-Ferese uses its shared configuration and existing startup acknowledgement instead.
-Like the [swayidle example](https://github.com/swaywm/swayidle/blob/master/swayidle.1.scd),
-locking and display sleep are separate from system suspend. Password changes and
-interactive multi-factor prompts remain outside this password-only interface.
+The password interface does not support password changes or interactive
+multi-factor prompts. Update expired credentials outside the locker.
+
+Developers should validate locking and readiness against
+[ext-session-lock-v1](https://wayland.app/protocols/ext-session-lock-v1).
+See [Development](development.md) for tests.
diff --git a/docs/native-soak-testing.md b/docs/native-soak-testing.md
new file mode 100644
index 0000000..e3fc31e
--- /dev/null
+++ b/docs/native-soak-testing.md
@@ -0,0 +1,67 @@
+# Native-client soak testing
+
+The 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` and start a disposable
+[nested session](installation.md#preview-and-logs). Select its Wayland socket and
+compositor PID explicitly, then run:
+
+```sh
+WAYLAND_DISPLAY=wayland-N FERESE_PID=PID ./scripts/soak-native.sh
+```
+
+Replace `wayland-N` and `PID` with the preview's values. Do not use the host socket
+or rely on automatic PID selection when multiple sessions are running.
+
+The default duration is 24 hours. A shorter validation run is:
+
+```bash
+WAYLAND_DISPLAY=wayland-N \
+FERESE_PID=PID \
+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
new file mode 100644
index 0000000..6e8dde7
--- /dev/null
+++ b/docs/nested-resize-testing.md
@@ -0,0 +1,36 @@
+# Nested resize checks
+
+Build and launch a preview with protocol and render logging:
+
+```sh
+cargo build --release --locked -p ferese -p ferese-shell
+WAYLAND_DEBUG=1 FERESE_TRACE_PERFORMANCE=1 \
+ target/release/ferese --backend nested --grant-effects --grant-shell-control -- \
+ target/release/ferese-shell 2>/tmp/ferese-resize.log
+```
+
+Use a wallpaper, drag each edge of the host window, and repeatedly maximize and
+restore it. Check that the wallpaper and bar follow without long stalls, including
+at fractional scaling.
+
+## Compare renderers
+
+Repeat with `ICED_BACKEND=wgpu` and `ICED_BACKEND=tiny-skia`. The shell enables
+wgpu; software fallback can be slower when resampling wallpaper.
+
+Trace the whole sequence: host resize, layer configure, client acknowledgement,
+new buffer commit, then compositor rendering. A quick acknowledgement does not
+mean the client has finished drawing. `FERESE_TRACE_PERFORMANCE` measures compositor
+work, not shell rasterization.
+
+## Historical check
+
+A KDE Wayland check at 175% scale used debug binaries and protocol tracing.
+Median configure acknowledgement was 1.35 ms with tiny-skia and 2.27 ms with
+wgpu. Wallpaper buffer intervals were 5919 ms with tiny-skia (only two buffers)
+and 9.85 ms median with wgpu. Capture durations differed, so these are diagnostic
+observations rather than throughput benchmarks. GPU intervals also had stalls
+up to about 200 ms.
+
+Repeat on the current build. A nested result does not establish direct-session
+resize or frame-pacing behavior.
diff --git a/docs/notifications.md b/docs/notifications.md
index fbe8610..1ecb169 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -1,37 +1,24 @@
# Notifications
-Ferese Shell provides themed notification cards and an in-session history panel.
-Cards appear beneath the bar on the focused output and follow the selected theme.
-Click the notification icon in the bar to open history, toggle Do Not Disturb, or
-clear notifications. Escape, clicking outside, or the close button closes the history panel.
-The center uses the same anchored popup and animation as the other bar menus.
-Both the center and popup cards follow the shell material: translucent mode uses
-the compositor’s blur and tint, while solid mode stays opaque.
-
-## Send a notification
-
-Notifications use the standard `org.freedesktop.Notifications` D-Bus interface.
-Existing apps and `notify-send` work without Ferese-specific commands:
-
-```sh
-notify-send --app-name=Ferese --icon=dialog-information "Hello" "Your desktop notification"
-```
+Ferese shows compact notification popups and a history panel opened from the bar.
+Their colors, font, corners, and background follow the shell theme.
## Popups and history
-Up to three popup cards are visible at once. History keeps the most recent 100
-notifications in memory and clears when the shell exits. Hovering a popup pauses
-its timer. Critical notifications bypass Do Not Disturb and do not expire unless
-the sender supplies a timeout. App action buttons emit the standard action signal;
-a replacement updates the same notification ID. Transient messages are removed
-from history when they close.
+Up to three popups are visible at once. Hovering pauses a popup's timer.
+History keeps the latest 100 notifications and clears when the shell exits.
+App action buttons work when provided by the sender. Transient messages leave
+history when closed.
-## Settings and Do Not Disturb
+Critical notifications bypass Do Not Disturb and remain visible unless the app
+provides a timeout. Other apps can also request a timeout or no timeout.
-Settings → Notifications controls popup visibility, the initial Do Not Disturb
-state, and the default timeout. The bar's Do Not Disturb toggle is temporary;
-changing the Settings option saves the preference for future sessions. Apps can
-request their own timeout, including no timeout.
+## Settings
+
+**Settings → Notifications** controls popup visibility, the default timeout, and
+the Do Not Disturb preference. The bar toggle changes Do Not Disturb for the
+current session; Settings saves it for future sessions. Hidden popups still enter
+history.
```kdl
notifications {
@@ -41,17 +28,20 @@ notifications {
}
```
-The timeout must be between 1000 and 30000 milliseconds. Hiding popups still
-records notifications in history. Appearance follows the Ferese theme.
+The default timeout accepts 1000–30000 milliseconds.
-## Session integration
+## Send a test notification
-Only one notification server can own the D-Bus interface. The Ferese session
-launcher stops the SwayNotificationCenter user service before starting its shell.
-For a manually started shell, stop that service with `systemctl --user stop
-swaync.service`. If another server already owns the interface, Ferese leaves it
-running and logs that native notifications are unavailable.
+```sh
+notify-send --app-name=Ferese --icon=dialog-information "Hello" "Your desktop notification"
+```
-This implementation supports plain text, app icons, urgency, transient/resident
-hints, actions, replacements, and close signals. It does not advertise rich body
-markup, embedded image data, notification sounds, or history persistence.
+## Troubleshooting
+
+Only one notification server can run on the session bus. The session launcher
+stops `swaync.service` before starting the shell. When launching the shell manually,
+stop any existing notification server first. If another server owns the interface,
+Ferese logs that native notifications are unavailable.
+
+Ferese supports plain text, app icons, actions, replacement, and urgency. Rich
+markup, embedded images, sounds, and saved notification history are not supported.
diff --git a/docs/performance-baseline.md b/docs/performance-baseline.md
index 0973f33..d96064c 100644
--- a/docs/performance-baseline.md
+++ b/docs/performance-baseline.md
@@ -1,217 +1,50 @@
-# Release performance baseline
+# Performance measurements
-Measured 2026-09-27 at commit `08a5866`, using
-`cargo build --release --locked -p ferese -p ferese-shell -p feresectl`.
+Use release builds and a fixed workload when comparing performance. Record the
+commit, output size, scale, refresh rate, graphics driver, and whether the session
+is nested or direct. Keep the nested preview visible: host occlusion changes
+rendering behavior.
-Two disposable nested sessions used separate runtime/config directories, default
-configuration, the bundled wallpaper, and the release shell. Performance logging
-was enabled with `FERESE_TRACE_PERFORMANCE=1`. The nested output reported
-1422 × 1696 physical pixels, 2× scale and 60 Hz. The parent DRM output was a
-2880 × 1800 eDP panel at 120 Hz and 2× scale.
+## Collect a sample
-## Measurements
+```sh
+cargo build --release --locked -p ferese -p ferese-shell -p feresectl
+FERESE_TRACE_PERFORMANCE=1 target/release/ferese --backend nested \
+ --grant-effects --grant-shell-control -- target/release/ferese-shell
+```
-CPU is process CPU time from `/proc/PID/stat` divided by elapsed monotonic time,
-expressed as a percentage of one core. First-run samples lasted 10 seconds;
-second-run samples lasted 5 seconds. These are short observational samples,
-not statistically established improvements or before/after comparisons.
+Compare idle, several windows, overview, Settings navigation, translucent
+popovers, and screen capture separately. Allow startup to settle and repeat each
+sample. Record process CPU, child-process CPU, RSS, render attempts, and damaged
+frames. Keep build work and unrelated activity outside the sampling interval.
-| Scenario | Compositor CPU, run 1 / run 2 | Shell CPU, run 1 / run 2 |
+CPU percentages below use one core as 100%. Short samples are observations,
+not guarantees or hardware-session benchmarks.
+
+## Historical results
+
+On 2026-09-27 at `08a5866`, two nested runs used a 1422 × 1696 output at 2× scale,
+reporting 60 Hz on a 120 Hz host. Samples lasted 5–10 seconds.
+
+| Scenario | Compositor CPU, two runs | Shell CPU, two runs |
| --- | --- | --- |
-| Idle shell, no application window | 5.3% / 5.8% | 1.5% / 1.6% |
+| Idle, no application window | 5.3% / 5.8% | 1.5% / 1.6% |
| Overview with one terminal | 18.6% / 16.2% | 1.3% / 1.4% |
-| After window transitions, terminal still open | 6.6% / 6.4% | 1.5% / 1.4% |
+| After transitions | 6.6% / 6.4% | 1.5% / 1.4% |
-Compositor RSS was approximately 81–82 MiB initially and 91–92 MiB after the
-short exercise. This does not establish a leak or long-term memory stability.
-Main-thread voluntary context switches were approximately 162–165/s at idle
-and 498–505/s in overview; these are scheduling proxies, not hardware wakeups.
+Compositor RSS increased from about 81–82 MiB to 91–92 MiB during the exercise.
+These samples do not establish a leak or long-term stability. A follow-up had
+lower rendering rates, so the original observations did not prove duplicate scheduling.
-A five-second idle log interval contained 60 damaged frames and 531 no-damage
-attempts, approximately 118 attempts/s. A later overview interval contained
-591 damaged frames in 5001 ms. Nested frame scheduling and overview damage
-therefore deserve investigation first. The parent runs at 120 Hz, so the
-mismatch with the nested output's reported 60 Hz must be understood before
-attributing these counts to duplicate scheduling.
+A 20-second status-worker comparison removed five commands per poll (15 to 10).
+Total measured CPU time including child processes fell from 2.41 to 1.90 seconds.
+The timing wrapper was included, and this was one comparison.
-## Behavioral exercise
+For push status delivery (`4bf0757` to `b0583eb`), a synthetic transport test
+measured median delivery delay of 129.398 ms before and 0.130 ms after. It excluded
+queries, UI dispatch, and painting. Visible shell samples suggested lower typical
+CPU, but an after-run spike raised the mean; they did not establish a robust
+average CPU saving.
-Both sessions exited with status 0. The probes created a terminal, entered and
-left overview, and toggled maximize, fullscreen and floating twice each.
-The second session exercised `theme.typography.font_family`, wallpaper fit/fill,
-and replacement image paths through filesystem-driven reloads. Logs confirmed
-accepted reloads and replacement wallpaper decoding. This verifies execution
-and liveness, not pixel-perfect appearance or GPU allocation-failure recovery.
-The first probe used an incorrect font key; only the second probe counts as
-font reload coverage.
-
-Raw local artifacts:
-
-- `/tmp/ferese-profile-7cbrpopn/{results.json,session.log}`
-- `/tmp/ferese-profile-h2ypepn6/{results.json,session.log}`
-- Probe scripts: `/tmp/ferese-profile.py` and `/tmp/ferese-profile-reloads.py`
-
-## Limits and next work
-
-The installed DRM compositor is release `20260927-042025-168287`; its binary hash
-differs from the tested release build. A read-only host sample measured 9.8%
-compositor CPU and 1.7% shell CPU, but overlapped the nested workload and is not
-an idle baseline or validation of the new DRM code. No session was replaced.
-Only one monitor was connected, so multi-monitor behavior remains untested.
-
-Next investigate nested refresh scheduling and repeated overview damage, using
-these scenarios for before/after measurements. Preserve client frame callbacks,
-animation pacing and idle responsiveness. Status command polling remains a
-secondary target; blur invalidation needs a separate translucent-material
-workload before ranking its cost. New-build DRM and multi-monitor validation
-remain necessary before claiming hardware performance improvements.
-
-## Follow-up investigation
-
-A repeat baseline (`/tmp/ferese-profile-n8437l1i`) confirmed 60 Hz after startup
-but ran near 60 attempts/s rather than 118. Compositor CPU was 3.0% idle, 9.2%
-in overview and 4.6% after transitions. The earlier high-rate behavior is not
-consistently reproduced, so it is not sufficient evidence of duplicate redraws.
-The candidate refresh-period guard was discarded after testing.
-
-The guard run (`/tmp/ferese-profile-llcrkvm5`) measured 4.0% idle, 8.6% overview,
-and 3.6% after transitions. Render attempts fell slightly below the nominal
-cadence. This did not establish a benefit, so no frame-pacing change was kept.
-The next investigation is unconditional thumbnail damage in overview.
-
-## Broader profiling (visible preview)
-
-Artifacts: `/tmp/ferese-profile-all-0lhodit4/`; script:
-`/tmp/ferese-profile-all.py`. This run used the unchanged release compositor,
-release shell and release Settings. The user kept the preview visible after an
-earlier hidden-window run proved that host occlusion invalidated render samples.
-Six-second samples are exploratory, not statistically stable benchmarks.
-The captured output was 2856 × 1696 pixels; the preview was resized during the
-session, so these workloads must not be compared as if output size were fixed.
-
-| Scenario | Compositor CPU | Shell CPU | Shell waited-child CPU | Settings CPU |
-| --- | ---: | ---: | ---: | ---: |
-| Idle shell | 4.33% | 1.33% | 7.50% | — |
-| Three terminals | 6.67% | 1.50% | 11.33% | — |
-| Overview, three terminals | 17.50% | 1.17% | 8.17% | — |
-| Settings open, idle | 6.17% | 1.50% | 10.83% | 0.00% |
-| After capture/window transitions | 8.50% | 1.33% | 11.00% | 2.50% |
-
-Percentages use one CPU core as 100%. Child CPU is Linux's accumulated CPU time
-for reaped children, separate from the shell process. This includes the timing
-wrapper: a PATH-local `timeout` wrapper invokes GNU time and the real timeout
-command. Read-only PipeWire queries use the host runtime socket; no status
-control actions were sent. The native installed shell cross-check, without
-instrumentation, measured 2.0% self CPU plus 8.8% waited-child CPU over ten
-seconds. It is an older binary and a different scene, so this corroborates the
-existence of subprocess overhead rather than providing an exact comparison.
-
-The preview recorded 273 successful status command invocations across 21 polls:
-13 per poll. The three login1 capability calls consumed 1.88 seconds total wall
-time, about 90 ms/poll. Three nmcli queries consumed 1.35 seconds total; two wpctl
-queries consumed 0.80 seconds. GNU time's hundredth-second granularity makes
-per-command CPU totals unsuitable for precise attribution. Stable capability
-queries, device metadata and frequently changing values currently share one
-poll cycle. Separate refresh policies or service subscriptions deserve priority.
-
-Settings mapped in 109 ms, measured from process launch until IPC reported it
-focused (not first completed paint). Idle Settings RSS was 108068 KiB, with 38
-threads and 65 descriptors. Zero measured CPU over six seconds means below this
-sample's resolution, not a guarantee of zero work. Startup font enumeration was
-not isolated separately. Settings interaction/search/page-switch profiling is
-still outstanding.
-
-Twenty `feresectl get-outputs` calls had median process-inclusive latency of
-3.05 ms and maximum 3.95 ms. Five captures took 1.13 seconds total, including
-client startup, transfer and PNG encoding. Capture was enabled only in the
-isolated compositor with `FERESE_ENABLE_SCREENCOPY=1`. The first screenshot was
-visually inspected and showed the shell and Settings rendered. It does not
-measure GPU readback independently. Window maximize/fullscreen/layout toggles
-completed, but there was no instrumented input-to-photon latency measurement.
-
-The four-second visible overview perf recording (`cpu-clock:u`, 999 Hz) collected
-471 samples with no lost samples. Shared-object attribution: 47.77% Ferese
-executable (including statically linked dependencies), 30.36% Mesa Gallium,
-14.23% libc, 2.97% Wayland client library. Approximately 96.4% of samples were in
-the main compositor thread. Texture upload/memory-copy paths appear in the
-sampled stacks. This is user-space CPU sampling, not GPU timing; short samples
-and incomplete frame-pointer unwinding limit function-level attribution.
-
-The earlier broad run (`/tmp/ferese-profile-all-boiq_r30`) had a 37-second render
-reporting gap while hidden, no usable perf samples, and a disabled screencopy
-protocol. Its render/overview and capture results are excluded. Its bare idle
-sample was 1.0% CPU, but visibility differences preclude subtracting it from the
-visible shell run.
-
-Next priority: shell status subprocesses. Then investigate sustained overview
-damage and texture uploads with controlled window visibility and dimensions.
-Control-center interaction, widgets, translucent-material workloads, long-term
-memory growth, new-build DRM and multi-monitor runs remain unprofiled. No source
-changes or profiling-note commits were made during this profiling pass.
-
-## Status-worker optimization comparison
-
-Compared the production status module at `08a5866` with the new single-command
-brightness reader and reusable native D-Bus connections. A standalone harness
-started `Service::start(None)`, consumed updates every 250 ms, and ran for 20
-seconds without a UI. Both variants used the same host runtime/services and
-command timing wrapper. Artifacts: `/tmp/ferese-status-compare/`.
-
-Both completed nine polls. Notifications were available in this comparison, so
-the original worker launched 15 commands/poll and the new one launched 10:
-135 versus 90 invocations. Four busctl launches and one brightnessctl launch per
-poll were removed. When notifications are absent, the equivalent counts are
-13 versus 8. Poll frequency, fresh capability queries and post-action generation
-handling remain unchanged; no status-value caching was introduced.
-
-GNU time measured total user+system CPU, including waited children: 2.41 seconds
-before, 1.90 seconds after over 20.01 seconds each (about 21% lower in this single
-comparison). Timing-wrapper overhead is included, so this is not a precise
-uninstrumented CPU improvement estimate. Native D-Bus adds persistent sockets
-and a runtime thread pool; UI samples observed roughly 26–28 shell threads and
-48 descriptors versus roughly 18 threads and 42 descriptors previously.
-
-One UI run showed a transient self-CPU spike; a subsequent perf recording
-identified tiny-skia raster work rather than D-Bus queries among its hot paths.
-The UI-free comparison avoids treating variable UI activity as polling cost.
-
-Validation: workspace suite and explicit private-dbus-daemon test passed. The
-private bus exercised live capability changes, yes/challenge/no/na handling,
-notification-owner appearance/disappearance, error replies, missing methods,
-and a reply delayed past the configured timeout. Brightness parser tests cover
-rounding, zero brightness, invalid classes and malformed/nonfinite values.
-
-### Push status delivery measurement (b0583eb)
-
-Compared release binaries for 4bf0757 and b0583eb with the same nested compositor,
-empty isolated config and a visible preview. Binary SHA-256 hashes differ and
-were checked after forcing a shell rebuild to avoid shared-target artifact reuse.
-No status-command timing wrappers were active. Discarded an initial baseline
-that overlapped compilation. Retained after/before/after runs, each with three
-10-second samples following a three-second startup settle. Artifacts and harness:
-`/tmp/ferese-status-push-measure/` (`summary.json`, `ui.py`, `latency.rs`).
-
-Shell CPU percentages of one core:
-- Before: 1.5, 1.4, 1.6 (median 1.5).
-- After run 1: 0.8, 1.1, 1.1.
-- After run 2: 1.0, 1.1, 7.5 (combined after median 1.1).
-
-The 7.5% sample coincided with a large damaged-pixel burst (44.3 million in a
-five-second interval), higher RSS and two additional descriptors. Its cause was
-not isolated; it is retained, not silently excluded. Combined after mean is
-2.1%, versus 1.5% before, so these short runs do not establish a robust average
-CPU saving. Typical steady-state samples suggest a 0.4 percentage-point reduction.
-Status child CPU remained roughly 6–8% of one core, dominating shell self CPU.
-
-Steady-state damaged frames were about 12/s before versus 6.6–7.6/s in the first
-after run. The repeat had similar quiet intervals plus bursts at 15.6 and 22.7/s.
-Total render attempts stayed around 60/s, confirming active rather than hidden
-preview sampling. No claim is made about DRM-session CPU or total idle wakeups.
-
-An isolated transport harness used the old bounded mpsc + 250ms polling path and
-an extracted copy of the new Updates::stream implementation, with 40 synthetic
-updates per path at 317ms intervals. Worker-to-consumer delay:
-- Before: median 129.398ms, p95 239.957ms, maximum 245.881ms.
-- After: median 0.130ms, p95 0.330ms, maximum 0.425ms.
-This measures delivery only, excluding status queries, UI dispatch and painting.
-The preview processes exited and the temporary source worktree was removed.
+These results predate later changes. Run fresh comparisons for current claims,
+and validate direct rendering and multiple monitors separately.
diff --git a/docs/portals.md b/docs/portals.md
index 227c3e8..4f90803 100644
--- a/docs/portals.md
+++ b/docs/portals.md
@@ -1,174 +1,60 @@
# Desktop portals
-Applications call the standard `org.freedesktop.portal.Desktop` frontend. Ferese's
-native backend implements desktop integration; GTK remains the fallback for
-standard file, print and application dialogs. Install both backends.
+Desktop portals let applications request screen sharing, screenshots, wallpaper
+changes, and other desktop services. Ferese provides themed consent dialogs;
+GTK supplies file, print, and other standard dialogs.
-## Native services
+Install `xdg-desktop-portal`, `xdg-desktop-portal-gtk`, and Ferese's backend through
+[Installation](installation.md). The session launcher starts the services with
+the correct display environment.
-- **Settings:** Ferese's color scheme, accent color, contrast preference and
- reduced-motion setting. Changes are signalled after valid KDL appearance
- changes; malformed saves leave the last valid preferences intact.
-- **Screenshot:** screen, selected-area, selected-window and active-window PNG
- captures, plus a screen color picker. Window captures render the selected
- toplevel and its subsurfaces in isolation, preserving transparency; occluding
- windows, separate popups, shell layers and compositor borders are excluded.
- The native window picker lists window titles and application IDs. Every capture
- asks for consent. Interactive requests offer area selection; area and color
- selection use `slurp`. Captures use Ferese's bounded screenshot IPC.
- The helper must disappear from the compositor before capture starts.
-- **Wallpaper:** a preview and confirmation, then an atomic configuration update.
- Desktop and lock-screen wallpaper can be set independently, or together.
- Ferese copies accepted local images into its own private data directory so
- temporary portal files can disappear safely. Image bytes and decoded dimensions
- are bounded. `theme.background.lock-path` overrides the lock-screen image;
- without it the lock screen follows `theme.background.path`.
-- **Background:** native open-window/app state, change signals and per-instance
- background consent. Legacy autostart requests create managed XDG desktop entries;
- current frontends manage those files themselves. Direct sessions launch standard
- XDG autostart entries after importing the display environment. User overrides,
- Hidden, OnlyShowIn, NotShowIn and TryExec are respected; previews skip autostart.
-- **Lockdown:** standard printing, save-to-disk, application-handler, location,
- camera, microphone and sound-output restrictions. All default to unrestricted.
- Configure boolean `disable-*` fields under `portals { lockdown { ... } }`;
- changes are signalled to the frontend. Invalid policy types retain the current
- policy. Only the portal frontend may write backend policy properties.
-- **USB:** native confirmation showing device vendor/model and requested read-only
- or read/write access. The portal frontend handles device enumeration and opening.
-- **GlobalShortcuts:** session-scoped keyboard shortcuts with native trigger
- selection, conflict checking, activation/release signals and reconfiguration.
- Applications cannot replace Ferese bindings or reserved escape/VT shortcuts.
- Closing a session or losing its compositor connection releases its shortcuts;
- shortcuts do not activate on the lock screen. Saved choices are offered for
- future sessions, with explicit approval before registering them.
-- **Inhibit:** connection-scoped logout, suspend and idle inhibitors, backed by
- logind descriptors and compositor idle inhibition. Native session monitors
- report lock state and Running/QueryEnd/Ending transitions. Ending queries wait
- for acknowledgements for up to one second. Power confirmations list blockers
- and recheck them before acting; authentication failure cancels the query.
- UserSwitch is recorded, but Ferese currently has no user-switch operation.
-- **InputCapture:** approved keyboard and pointer capture over a private EIS
- connection, with standard screen zones and pointer barriers on outside display
- edges. Capture begins at a configured barrier; releasing it restores local
- input. Ctrl+Alt+Escape returns control immediately. Locking, topology/keymap
- changes, receiver disconnects and stalled consumers stop capture. Permission
- is requested for every session; touch capture and remembered grants are not
- offered. This is an input receiver, not remote input injection.
-- **ScreenCast:** monitor and isolated window sharing with a native picker, PipeWire streams and explicit opt-in
- saved permissions. The frontend stores and revokes saved choices. Invalid
- restore data or changed display identity requires fresh consent.
- See [screen sharing](screen-sharing.md) for lifetime and recording details.
+## Supported services
-Native request dialogs use Ferese's theme and are floating windows. Screenshot,
-PickColor, Wallpaper, USB, InputCapture and ScreenCast consent dialogs use Wayland parent identifiers to establish
-a transient relationship with the requesting window.
-Cancelling a request or losing the portal frontend terminates its pending helper.
-
-The packaged `ferese-portals.conf` selects these interfaces explicitly. Existing
-user overrides in `~/.config/xdg-desktop-portal/` may take precedence. Install the
-new compositor, backend and preference files together; screenshot synchronization
-requires the updated compositor IPC.
-
-## GNOME compatibility audit
-
-The audit compares the [GNOME backend's registered interfaces](https://github.com/GNOME/xdg-desktop-portal-gnome/blob/main/data/meson.build)
-with the [standard backend D-Bus contracts](https://flatpak.github.io/xdg-desktop-portal/docs/impl-dbus-interfaces.html).
-Matching D-Bus signatures does not establish complete feature parity.
-
-| Interface | Ferese integration |
+| Service | Behavior |
| --- | --- |
-| Settings | Native, including appearance change signals |
-| Screenshot | Native screen/area/window/active-window capture and PickColor |
-| Wallpaper | Native desktop, lock-screen and combined targets |
-| ScreenCast | Native v4 monitors and isolated windows, logical geometry, live window resizing and opt-in saved display permissions |
-| Access, Account, AppChooser, DynamicLauncher, FileChooser, Notification, Print | Delegated to GTK |
-| Background | Native application state, per-instance consent and XDG autostart |
-| Usb | Native consent; standard frontend handles enumeration and device descriptors |
-| GlobalShortcuts | Native sessions, configurable keyboard triggers, conflict checks and activation/release signals |
-| RemoteDesktop | Deferred: authorized input injection and EIS transport |
-| Clipboard | Deferred with RemoteDesktop |
-| InputCapture | Native v2 keyboard/pointer consent, zones, outside-edge barriers and private EIS receiver transport |
-| Lockdown | Native seven-property policy provider with persisted configuration and change signals |
-| Inhibit | Native inhibitors and session monitors; UserSwitch recorded until a switch operation exists |
+| Settings | Color scheme, accent, contrast, and reduced-motion preferences |
+| Screenshot | Screen, area, window, active-window capture, and color picking |
+| Wallpaper | Preview and approval for desktop, lock screen, or both |
+| ScreenCast | Display and window sharing through PipeWire |
+| Background | Permission to run in the background and startup integration |
+| USB | Device information and approval for requested access |
+| GlobalShortcuts | App shortcut selection, conflict checks, and activation |
+| Inhibit | App requests to delay logout, suspend, or idle actions |
+| InputCapture | Approved keyboard/pointer capture for input-sharing applications |
+| Lockdown | Administrator restrictions on printing, saving, handlers, location, camera, microphone, and sound |
+| FileChooser, Print, Access, Account, AppChooser, DynamicLauncher, Notification | GTK backend |
-The frontend supplies additional APIs such as OpenURI, network monitoring,
-document export and permission storage. Adding a similarly named backend service
-is neither required nor sufficient for those APIs.
+RemoteDesktop and its session Clipboard integration are not implemented.
+This does not affect ordinary application clipboard use.
-Window streams render only the selected toplevel and its subsurfaces, including
-on an inactive workspace. Overlapping apps, shell layers, separate popup windows
-and compositor decorations are excluded. The private capture protocol is available
-only to the native portal executable and checks session opt-in and locking.
-Window resizing renegotiates PipeWire video dimensions. Closing the selected
-window ends sharing; its lifetime ID cannot select a replacement app. Window
-selections require fresh consent and are not saved as display permissions.
-RemoteDesktop and its Clipboard integration remain deferred.
+## Consent and capture
-## Validation
+Screenshot and screen-sharing requests ask for approval. Screen sharing can
+remember display permissions when the application requests it and the user opts
+in. Window sharing always asks again. See [Screen sharing](screen-sharing.md).
+Window captures contain the selected app and its subsurfaces; separate popups,
+other apps, shell layers, and compositor decorations are excluded.
-Build the backend and run unit tests, then check its contracts on a private bus:
+InputCapture starts at an approved display-edge barrier. **Ctrl+Alt+Escape**
+returns local control. Locking, disconnecting the receiver, or changing displays
+or the keyboard layout stops capture. Touch and remembered InputCapture grants
+are not supported. InputCapture forwards local input; it does not inject remote input.
-```sh
-cargo build --locked -p xdg-desktop-portal-ferese
-cargo test --locked -p xdg-desktop-portal-ferese
-python3 scripts/tests/test_portal_contracts.py
-```
+Wallpaper approval copies the image into Ferese's data directory and updates the
+config. Set `theme.background.lock-path` for a separate lock-screen image; otherwise
+it follows the desktop wallpaper.
-With a Wayland host and a built compositor, check shortcut connection cleanup
-and configuration conflicts in an isolated nested compositor:
+Global shortcuts cannot replace Ferese bindings or reserved escape/console keys.
+They stop when the app session closes and never activate on the lock screen.
+Power confirmations show app inhibitors before proceeding. Ferese has no
+user-switch operation.
-```sh
-FERESE_TEST_SHORTCUTS=1 python3 scripts/tests/test_shortcuts_isolated.py
-```
+## Portal selection
-Check window capture isolation, transparency and orientation with overlapping
-clients in a temporary nested compositor:
+The installer writes `/usr/share/xdg-desktop-portal/ferese-portals.conf`.
+Overrides in `~/.config/xdg-desktop-portal/` may take precedence. If a request opens
+the wrong dialog or no source appears, check those overrides and confirm that
+`XDG_CURRENT_DESKTOP` includes `Ferese`.
-```sh
-FERESE_TEST_WINDOW_CAPTURE=1 python3 scripts/tests/test_window_capture_isolated.py
-```
-
-Capture a managed window directly from the CLI with `feresectl screenshot-window
- > capture.png`; `feresectl get-windows` lists IDs.
-
-The contract test compares introspection against the installed official backend
-XML, verifies appearance changes and invalid-save retention, and checks that
-sensitive methods reject callers outside the portal frontend. It does not open
-consent dialogs or capture the host desktop. Interactive consent, fractional-scale
-capture, polkit and hotplug still require a live session test.
-
-Check native inhibitor lifetimes, acknowledgement generations and session-ending
-transitions against a mock logind in an isolated nested compositor:
-
-```sh
-FERESE_TEST_INHIBIT=1 python3 scripts/tests/test_inhibit_isolated.py
-```
-
-The shell now requires version 4 of Ferese's shell protocol; rebuild the shell
-and compositor together when upgrading.
-
-Check restored sharing, mode downgrade, stale-data fallback, output-generation
-validation and frontend-disconnect cleanup with synthetic monitor identities on
-an isolated bus and nested compositor:
-
-```sh
-FERESE_TEST_RESTORE=1 python3 scripts/tests/test_restore_isolated.py
-```
-
-Check live window pixels, overlap isolation, inactive workspaces, resizing and
-closure using a disposable nested compositor and a real PipeWire consumer:
-
-```sh
-FERESE_TEST_WINDOW_STREAM=1 python3 scripts/tests/test_window_stream_isolated.py
-```
-
-Check InputCapture state and its transport against an installed libei receiver:
-
-```sh
-cargo test --locked -p ferese input_capture::tests
-cargo test --locked -p xdg-desktop-portal-ferese eis::tests -- --ignored
-```
-
-The receiver test uses a private socket pair and synthetic input; it never
-captures the host keyboard or changes host focus. Real pointer-barrier crossings
-and interactive consent still need a live session check.
+For capture issues, see [screen-sharing troubleshooting](screen-sharing.md#troubleshooting).
+For backend contracts and integration checks, see [Development](development.md#portal-checks).
diff --git a/docs/screen-sharing.md b/docs/screen-sharing.md
index c53152e..8766648 100644
--- a/docs/screen-sharing.md
+++ b/docs/screen-sharing.md
@@ -1,142 +1,73 @@
# Screen sharing and recording
-Ferese includes a ScreenCast portal backend, `xdg-desktop-portal-ferese`.
-Applications use the standard desktop portal to request a display or window, then receive
-video through PipeWire. A recorder or conferencing application handles encoding,
-saving files, or sending video; those tasks do not run inside the compositor.
+Share a monitor or window from a Wayland-compatible recording or conferencing app,
+or save a video using the top bar. [Installation](installation.md) includes the
+portal and PipeWire dependencies.
## Record from the top bar
-1. Click the screen-recording icon in the top bar to open the display picker.
-2. Choose a monitor in the native picker and click **Share**.
-3. The icon changes to a stop square and shows elapsed time. Click it to stop.
+1. Click the recording icon.
+2. Select a monitor and click **Share**.
+3. Click the stop square to finish.
-The recorder finishes the video before sending a notification with its saved
-path. Videos are saved as VP8 WebM files under
-`Videos/Ferese` (using your configured XDG Videos folder). Audio is off.
-The icon uses the same theme accent, hover effect, and corner radius as the
-other bar controls.
-
-Encoding runs in a separate native `ferese-record` process using GStreamer.
-The shell remains responsive while a file is recorded or finalized. If saving
-fails, an error appears in the notification center; an unfinished `.webm.part` file
-may remain at the path shown in the error.
+Videos are saved as VP8 WebM files in your XDG Videos folder under `Ferese`.
+A notification shows the saved path. Audio and region recording are not supported
+by the built-in recorder; other recording apps can handle audio separately.
+If recording fails, a notification reports the error and any unfinished
+`.webm.part` file.
## Start sharing
-Use a Wayland screen-sharing or recording source in your application. Ferese
-opens a native source picker. Depending on what the application requests, choose
-from displays, windows, or both and press **Share**. Nothing is selected by default.
-Applications that request multiple sources can select up to eight. Display and
-window choices are grouped separately. Windows show their title and application.
-Window streams capture only the selected app's toplevel and subsurfaces, even
-when covered or on another workspace. Separate popups and compositor decorations
-are excluded. Resizing a shared window updates the video size without ending the
-session. Closing that window ends sharing.
-The picker uses your Ferese theme colors, font, and shell corner radius. Its
-background uses the same compositor material as popovers and authentication
-prompts: opaque in solid mode, or blurred and translucent using the shared
-**Appearance → Shell opacity** and blur settings. Text and icons remain opaque.
+Select a Wayland screen-sharing source in your application. In Ferese's picker,
+choose a display or window and click **Share**. Nothing is selected initially.
+Apps requesting multiple sources can select up to eight.
-Other applications keep a **screen sharing** window with a **Stop sharing** button.
-Ferese’s built-in recorder uses the top-bar stop control instead, leaving your
-windows accessible after display selection. The command-line recorder keeps the
-sharing window when it is not launched by the shell. Press **Stop sharing** or
-close the sharing window to revoke an ordinary application’s session. Closing the requesting application,
-locking Ferese, disconnecting the selected display, or changing a monitor's capture
-size also ends sharing. By default, each request asks for consent. If an application requests saved
-permissions, the picker offers an **Allow without asking** switch, off by default.
-Permissions can last while the application runs or until revoked, as requested
-by the app. The standard portal frontend owns permission storage and revocation.
-A changed or missing display, different app, or changed cursor options asks again.
-Displays without a reliable EDID identity, including nested previews, always need
-fresh consent. Window selections also require fresh consent and cannot enable saved
-display permissions. Stopping a stream ends that stream; it does not erase a previously
-saved permission.
-Everything visible on a shared display, including notifications, is included.
+A shared display includes everything visible on it, including notifications.
+A shared window includes only that app's content and subsurfaces, even when
+covered or on another workspace. Separate popups and compositor decorations are
+excluded. Resizing a shared window updates its video size; closing it ends sharing.
-## Installation
+The picker follows your shell colors, font, corners, opacity, and blur.
-The [standard installer](installation.md) builds and installs the backend, its
-D-Bus activation entry, and a Ferese-specific portal preference file. Install
-`pipewire`, a PipeWire session manager such as `wireplumber`,
-`xdg-desktop-portal`, and `xdg-desktop-portal-gtk` using your distribution's package
-manager. The GTK backend supplies standard dialogs such as file selection. Ferese also
-provides native Settings, Screenshot and Wallpaper services; see
-[desktop portals](portals.md).
-The built-in recorder also needs GStreamer’s PipeWire, base, and good plugins
-(`pipewiresrc`, `videoconvert`, `vp8enc`, and `webmmux`). The installation guide
-includes these packages.
+## Stop sharing
-Log into a new Ferese session after installation. The session launcher imports
-its Wayland display into the D-Bus activation environment. Nested previews leave
-the host activation environment alone.
+Click **Stop sharing** in the sharing window, or close that window. The built-in
+recorder uses the top-bar stop button instead. Sharing also ends when the app
+closes or Ferese locks. Disconnecting a shared monitor or changing its capture
+size ends its stream; start sharing again afterward.
-Existing custom window rules can float the picker and sharing indicator:
+## Remember permissions
-```kdl
-window-rule app-id="dev.ferese.ScreenShare" floating=#true
-```
+When an app requests saved display permissions, the picker offers **Allow without
+asking**, off by default. Approval lasts for the duration requested by the app.
+The portal manages stored permissions and revocation.
-The packaged example configuration already includes this rule. Existing user
-configuration is preserved during installation.
+A different app, changed display, or changed cursor option needs approval again.
+Nested previews and displays without a reliable identity always ask. Window
+permissions are not saved. Stopping a stream does not erase a saved permission.
-## Current capabilities
+## Requirements and limits
-- Monitor and isolated window capture, one PipeWire stream per source, up to 30 fps.
-- Window streams keep working across workspace switches and renegotiate on resize.
-- Hidden or embedded pointer, as requested by the application.
-- Logical stream position and size, including fractional output scales.
-- Wayland parent-window relationships for the consent picker.
-- Bounded frame storage and reusable shared-memory buffers.
-- Cancellation during selection, an explicit stop control, and process cleanup.
-- ScreenCast backend version 4 with explicit opt-in saved display permissions.
+Install PipeWire, WirePlumber, `xdg-desktop-portal`, and `xdg-desktop-portal-gtk`.
+The built-in recorder also needs GStreamer's PipeWire, base, and good plugins;
+see the [package lists](installation.md#requirements).
-Region selection and audio capture in Ferese’s recorder are not implemented yet.
-The top-bar recorder currently requests monitors. Other recording applications can handle audio separately.
-Video currently uses CPU copies through the compositor's shared-memory screencopy path, not DMA-BUF zero-copy. High-resolution
-or multiple-display recording can therefore use significant CPU and memory
-bandwidth. A paused stream checks capture availability once per second so locking
-also ends sessions without an active consumer.
+The installer supplies Ferese's portal backend and selects it for screen sharing.
+Log into a new Ferese session after installation.
-## Development checks
-
-Build the backend after installing the native dependencies in the
-[installation guide](installation.md#requirements):
-
-```sh
-cargo build --release --locked -p xdg-desktop-portal-ferese
-cargo test --locked -p xdg-desktop-portal-ferese
-# Exercise real video encoding and playback (requires the plugins above):
-cargo test --locked -p xdg-desktop-portal-ferese --bin ferese-record encoder_finishes_a_real_webm -- --ignored
-```
-
-The opt-in integration test opens a temporary nested compositor and uses a
-private D-Bus session. It requires built Ferese and locker binaries, Python
-GI/GStreamer with `pipewiresrc`, `dbus-daemon`, `xdg-desktop-portal`, and `bwrap`.
-It checks capture delivery and timestamps, cancellation, frontend loss, and lock
-revocation:
-
-```sh
-FERESE_TEST_PORTAL=1 \
-FERESE_TEST_PORTAL_BINARY=target/release/xdg-desktop-portal-ferese \
-python3 scripts/tests/test_portal_isolated.py
-```
-
-Add `FERESE_TEST_PORTAL_CONSENT=1` to exercise the native picker and restricted
-PipeWire connection. Select the temporary display and click **Share** when
-prompted. Tests capture only the nested preview and leave host PAM policies and
-D-Bus activation unchanged.
+Streams run at up to 30 fps and support hidden or embedded cursors. Capture copies
+frames through CPU-accessible buffers, so large or multiple streams can use
+substantial CPU and memory bandwidth.
## Troubleshooting
-Confirm that `XDG_CURRENT_DESKTOP` contains `Ferese`, PipeWire is running, and
-`FERESE_ENABLE_SCREENCOPY` was not set to `0` before launching the session.
+- Confirm PipeWire and WirePlumber are running.
+- Confirm `XDG_CURRENT_DESKTOP` includes `Ferese`.
+- Check that `FERESE_ENABLE_SCREENCOPY` was not set to `0` before session startup.
+- Check for portal overrides in `~/.config/xdg-desktop-portal/`.
-The installed preference is
-`/usr/share/xdg-desktop-portal/ferese-portals.conf`. A user override under
-`~/.config/xdg-desktop-portal/` can take precedence. Its ScreenCast preference
-should name `ferese`:
+The installed preference is `/usr/share/xdg-desktop-portal/ferese-portals.conf`.
+Its ScreenCast entry should select Ferese:
```ini
[preferred]
@@ -144,8 +75,7 @@ default=gtk;
org.freedesktop.impl.portal.ScreenCast=ferese;
```
-If capture dimensions change, stop and start sharing again. Ferese ends the old
-session instead of feeding incorrectly sized frames to the application.
-
-For development, `xdg-desktop-portal-ferese --sources` lists displays available
-on the current `WAYLAND_DISPLAY`. This command does not start a stream.
+`xdg-desktop-portal-ferese --sources` lists displays on the current
+`WAYLAND_DISPLAY` without starting a stream. Nested previews do not replace the
+host desktop's portal environment. See [Development](development.md#portal-checks)
+for isolated sharing tests.
diff --git a/docs/shortcuts.md b/docs/shortcuts.md
new file mode 100644
index 0000000..dab7dd1
--- /dev/null
+++ b/docs/shortcuts.md
@@ -0,0 +1,106 @@
+# Shortcuts and gestures
+
+`Super` is usually the Windows key. These are the built-in defaults. Open
+**Settings → Shortcuts** to change bindings and gestures or disable the login
+guide. Your saved bindings can override these defaults.
+
+## Move and resize with the mouse
+
+| Action | Result |
+| --- | --- |
+| Super + left-button drag on a floating window | Move the window |
+| Super + right-button drag on a floating window | Resize from the nearest corner |
+| Super + Shift + Space | Switch the focused window between tiled and floating |
+
+You can drag from inside the window; a title bar or resize border is not required.
+To place an app freely, toggle it to floating, then Super-drag it.
+
+## Focus, move, and resize
+
+`H`, `J`, `K`, and `L` mean left, down, up, and right.
+
+| Shortcut | Action |
+| --- | --- |
+| Super + H / J / K / L | Focus in that direction |
+| Super + Shift + H / J / K / L | Move the focused window in that direction |
+| Super + Ctrl + H / J / K / L | Resize in that direction |
+| Super + Q | Close the focused window |
+| Super + F | Maximize or restore the window |
+| Super + Shift + F | Enter or leave fullscreen |
+
+Maximize fills the available workspace. Fullscreen also hides the bar and window
+decorations.
+
+## Columns and layouts
+
+| Shortcut | Action |
+| --- | --- |
+| Super + R | Cycle column width |
+| Super + C | Center the focused column |
+| Super + [ | Place the window in a neighbouring column |
+| Super + ] | Extract the window into its own column |
+| Super + M | Switch scrolling / tree layout |
+| Super + Tab | Open or close overview |
+
+In scrolling mode, **Super + [** uses the left neighbour when available, otherwise
+the right. Try it with two terminals to place them vertically in one column.
+**Super + ]** separates the focused window again.
+
+In overview, select a workspace to see its windows, then click the window you
+want. Directional focus keys move selection; Enter activates it and Escape closes
+overview.
+
+## Workspaces
+
+| Shortcut | Action |
+| --- | --- |
+| Super + 1–9 | Switch to that workspace |
+| Super + Shift + 1–9 | Move the window to that workspace |
+
+Workspaces are created as needed. Selecting one visible on another monitor focuses
+that monitor.
+
+## Desktop actions
+
+| Shortcut | Action |
+| --- | --- |
+| Super + Enter | Open the configured terminal (`foot` by default) |
+| Print Screen | Capture the active monitor and open Satty |
+| Super + Shift + S | Select a screenshot area and open Satty |
+| Super + Shift + E | Ask to log out |
+
+In Satty, Enter saves the edited screenshot and copies it to the clipboard;
+Escape discards it. See [Configuration](configuration.md#commands-and-bindings)
+for dependencies and custom screenshot commands.
+
+## Touchpad gestures
+
+| Three-finger swipe | Action on release |
+| --- | --- |
+| Up / down | Next / previous workspace on this monitor |
+| Left / right | Focus the window to the right / left |
+
+Short, diagonal, or cancelled swipes do nothing. Gestures can use the same actions
+as keyboard bindings, including overview or moving a window. Four- and five-finger
+bindings are supported too.
+
+## Add a shortcut
+
+For example, bind a lock command and use a swipe to open overview:
+
+```kdl
+commands {
+ lock "ferese-lock"
+}
+binding "Super+Alt+L" "spawn" "lock"
+binding "Swipe3Up" "toggle-overview"
+```
+
+These are examples, not defaults. To disable a built-in binding:
+
+```kdl
+binding "Super+Q" disabled=#true
+```
+
+See [Commands and bindings](configuration.md#commands-and-bindings) for action
+names, arguments, and physical-key matching.
diff --git a/docs/why-ferese.md b/docs/why-ferese.md
index d5f8388..7dbebc0 100644
--- a/docs/why-ferese.md
+++ b/docs/why-ferese.md
@@ -1,34 +1,26 @@
# Why Ferese
-Omarchy and Ferese come from two different ideas about how a desktop should be
-built. Omarchy brings existing Linux tools together and configures them as one
-environment, which gives it access to mature components that already do their
-jobs well.
+Ferese combines window management and a desktop shell with one configuration.
+Settings, the bar, notifications, widgets, authentication prompts, and the lock
+screen share colors and fonts.
-A setup like that might combine:
+## Choose how windows fit
-```text
-Hyprland + bar + launcher + notifications + lock screen + theme scripts
-```
+Scrolling columns let you keep windows at useful widths and move through them
+horizontally. Tree tiling divides the screen into nested splits. Floating windows
+work alongside either layout, and overview shows windows across workspaces.
-There is nothing wrong with this approach, and being able to replace each part
-is useful, but those parts still have their own configuration, state, rendering,
-and lifecycle. Matching their themes can make them look related, while keeping
-their behavior in sync remains an ongoing part of maintaining the desktop.
+
-With Ferese, we are building the compositor and shell together, so workspaces,
-layout, focus, outputs, input, and materials all come from the same desktop
-state. The shell and Settings use first-party interfaces to work with that state
-instead of reading separate versions of it from several external tools.
+## Change the desktop together
-This does not mean every Ferese component has to run in one process. It means
-that the components share the same concepts, configuration, and protocols, so a
-workspace or surface means the same thing everywhere it appears.
+Choose a theme in Settings, then adjust the wallpaper, colors, font, corners,
+and motion. Changes apply live. Window corners and shell corners can be set
+independently.
-## What this means in practice
+
-You can see the difference in something as simple as corner radius. Ferese has a
-shared setting for shell surfaces:
+For example, this changes shell corners:
```kdl
theme {
@@ -38,41 +30,5 @@ theme {
}
```
-Changing that value updates the bar, Control Center, popovers, notifications,
-widgets, and native lock screen together. Managed windows have their own radius
-because they are a different kind of surface, but each value still has one clear
-meaning wherever it is used.
-
-
-
-In an assembled desktop, making the same visual change may mean editing the
-window manager, bar CSS, launcher theme, notification daemon, and lock screen
-config separately, then keeping those values aligned as the setup changes.
-
-The same difference shows up in how the desktop behaves. Ferese implements
-scrolling as part of its workspace model, renders blur behind shell surfaces in
-the compositor, and builds the overview from the real window scene. Focus,
-fullscreen, workspace geometry, and input all work from the same state rather
-than waiting for separate tools to catch up with one another.
-
-
-
-When a window opens, Ferese can handle the result as one transition:
-
-```text
-window opens → layout updates → viewport moves → input follows
-```
-
-An assembled environment may need to publish a window-manager event, pass it
-through IPC, run a script, update the bar, and start another animation before
-the rest of the desktop reflects the change. Each step can be fast, but it is
-still another boundary that has to be maintained.
-
-## The tradeoff we are making
-
-Building more of the desktop ourselves gives Ferese tighter control over how its
-parts work together, but it also leaves us responsible for more code. Omarchy
-can rely on mature components from across the Linux desktop ecosystem, while
-Ferese has to build that same level of maturity over time, and that is a tradeoff
-we are willing to make to keep the desktop consistent in both behavior and
-configuration.
+See [Configuration](configuration.md) for all options, or
+[Installation](installation.md) to try Ferese in a nested preview.
diff --git a/scripts/build-site.py b/scripts/build-site.py
index 2202965..9336c14 100644
--- a/scripts/build-site.py
+++ b/scripts/build-site.py
@@ -16,10 +16,13 @@ PAGES = [
('why-ferese', 'Why Ferese', ROOT / 'docs/why-ferese.md'),
('installation', 'Installation', ROOT / 'docs/installation.md'),
('configuration', 'Configuration', ROOT / 'docs/configuration.md'),
+ ('shortcuts', 'Shortcuts and gestures', ROOT / 'docs/shortcuts.md'),
('desktop-widgets', 'Desktop widgets', ROOT / 'docs/desktop-widgets.md'),
('notifications', 'Notifications', ROOT / 'docs/notifications.md'),
- ('locking', 'Native locker', ROOT / 'docs/locking.md'),
+ ('locking', 'Lock screen', ROOT / 'docs/locking.md'),
('screen-sharing', 'Screen sharing', ROOT / 'docs/screen-sharing.md'),
+ ('portals', 'Desktop portals', ROOT / 'docs/portals.md'),
+ ('development', 'Development', ROOT / 'docs/development.md'),
]
md = MarkdownIt('commonmark', {'html': False}).enable('table')
OUTPUT.mkdir(parents=True, exist_ok=True)
@@ -101,7 +104,7 @@ for page_number, (slug, title, source) in enumerate(PAGES):
-