Publish topic guides to the wiki and document tabbed windows

Publish the guides from this repository to the GitHub Wiki. Organize them into Usage, Configuration and Development, with 15 separate configuration topics and redirects for the former website guide URLs.

Add a tabbed-windows guide covering grouping, switching tabs and taking windows out. Document the scrolling indicator's defaults and limits, and link the guide from the README, shortcuts and IPC pages. Update from main, including the latest tabbed-layout changes.

Separate Activities setup, configuration and publishing documentation. Review the guide prose with the unslop skill, keeping existing examples and heading links. The wiki workflow uses GitHub's built-in token to publish documentation changes on main.

Validation:

- 20 documentation tests pass, including README links and Activities/tab-indicator field coverage.
- The publisher builds 46 wiki pages; the site build and Python syntax checks pass.
- All 115 code examples from main are preserved.
- The Python suite passes: 90 tests passed and 31 opt-in integration tests skipped, with the system GI module available.
This commit is contained in:
Samuel Onoja authored and GitHub committed 2026-10-13 00:32:23 +01:00
1 parent eaeabebe73
commit eb84104940
37 files changed
+2178 -1510

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: actions/configure-pages@v5
- name: Build website and handbook
- name: Build homepage and documentation redirects
run: |
python3 -m venv .site-venv
.site-venv/bin/pip install -r site/requirements.txt
+48
View File
@@ -0,0 +1,48 @@
name: Publish documentation to GitHub Wiki
on:
push:
branches: [main]
paths:
- 'docs/**'
- 'site/README.md'
- 'crates/ferese-theme/README.md'
- 'scripts/build-wiki.py'
- 'scripts/tests/test_wiki.py'
- 'site/requirements.txt'
- '.github/workflows/wiki.yml'
workflow_dispatch:
permissions:
contents: write
concurrency:
group: wiki
cancel-in-progress: false
jobs:
publish:
if: github.repository == 'ferese-wm/ferese' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: main
persist-credentials: false
- name: Install Markdown parser and validate publisher
run: |
python3 -m venv .site-venv
.site-venv/bin/pip install -r site/requirements.txt
.site-venv/bin/python -m unittest discover -s scripts/tests -p test_wiki.py -v
.site-venv/bin/python scripts/build-wiki.py
- name: Publish changed wiki pages
env:
GH_TOKEN: ${{ github.token }}
run: |
set -eu
gh auth setup-git --hostname github.com
git clone --depth 1 https://github.com/ferese-wm/ferese.wiki.git build/wiki-checkout
.site-venv/bin/python scripts/build-wiki.py --output build/wiki-checkout
git -C build/wiki-checkout add -- '*.md' .ferese-pages.json
if git -C build/wiki-checkout diff --cached --quiet; then
echo 'Wiki is already current'
exit 0
fi
git -C build/wiki-checkout -c user.name='github-actions[bot]' -c user.email='41898282+github-actions[bot]@users.noreply.github.com' commit -m "Publish documentation from $(git rev-parse --short HEAD)"
git -C build/wiki-checkout push origin HEAD
+1
View File
@@ -7,5 +7,6 @@
# Generated GitHub Pages site
/build/site/
/build/wiki/
/.site-venv/
__pycache__/
+18 -18
View File
@@ -5,33 +5,33 @@
<p align="center"><strong>A configurable Wayland desktop</strong></p>
<p align="center">
<a href="docs/installation.md">Install</a> ·
<a href="docs/configuration.md">Configure</a> ·
<a href="docs/screenshots.md">Screenshots</a> ·
<a href="docs/README.md">Documentation</a>
<a href="https://github.com/ferese-wm/ferese/wiki/Installation">Install</a> ·
<a href="https://github.com/ferese-wm/ferese/wiki/Configuration">Configure</a> ·
<a href="https://github.com/ferese-wm/ferese/wiki/Screenshots">Screenshots</a> ·
<a href="https://github.com/ferese-wm/ferese/wiki">Documentation</a>
</p>
Ferese combines a window manager and desktop shell with one configuration.
![Ferese desktop with Morph's music view open over a sunset wallpaper](docs/images/screenshots/ferese-morph.webp)
[View more screenshots](docs/screenshots.md)
[View more screenshots](https://github.com/ferese-wm/ferese/wiki/Screenshots)
## Features
- Scrolling columns, window groups, tree tiling and floating windows with saved size and position
- Scrolling columns, [tabbed window groups](https://github.com/ferese-wm/ferese/wiki/Tabbed-Windows), tree tiling and floating windows with saved size and position
- Integrated shell with notifications, media controls, Settings and Control Center
- [Custom panels](docs/panel-configuration.md) with seven styles, editable groups and items, and top or bottom placement
- [Live Activities](docs/activities.md) for build status, running commands and progress from connected apps
- [Custom panels](https://github.com/ferese-wm/ferese/wiki/Panel-Configuration) with seven styles, editable groups and items, and top or bottom placement
- [Live Activities](https://github.com/ferese-wm/ferese/wiki/Live-Activities) for build status, running commands and progress from connected apps
- Workspace overview and recent-window switching
- [Continuum animations](docs/configuration.md#animations): interruptible springs, gesture momentum and shared motion controls
- [Continuum animations](https://github.com/ferese-wm/ferese/wiki/Configuration-Animations): interruptible springs, gesture momentum and shared motion controls
- Live wallpapers and blur, custom KDL themes, and Light/Dark/Auto modes
- Multiple monitors, fractional scaling and lower refresh rates on low battery
- Wi-Fi, Bluetooth and audio controls
- Custom keyboard shortcuts and touchpad gestures
- Screenshot annotation, screen sharing and recording
- Lock screen and authentication dialogs
- [Keep awake](docs/configuration.md#idle-inhibition) during fullscreen media playback or for selected apps
- [Keep awake](https://github.com/ferese-wm/ferese/wiki/Configuration-Idle-Inhibition) during fullscreen media playback or for selected apps
- Continuous corners for windows and shell
## How Ferese compares
@@ -44,7 +44,7 @@ compares current upstream features and names separate tools where they are neede
| Tiling layouts | Scrolling columns and tree tiling; switch per workspace | [Scrolling, dwindle, master and monocle; per-workspace layouts](https://github.com/hyprwm/Hyprland#features) | Scrolling columns |
| Floating windows | Built in | Built in | Built in |
| Workspace overview | Built in | Plugins such as [HyprExpo](https://github.com/sandwichfarm/hyprexpo) | Built in |
| Animations | [Continuum](docs/animation-model.md): interruptible springs, gesture momentum; speed and reduced-motion settings shared by the compositor and shell | Bézier and spring curves | [Spring and easing animations; custom shaders](https://niri-wm.github.io/niri/Configuration:-Animations.html) |
| Animations | [Continuum](https://github.com/ferese-wm/ferese/wiki/Animation-Model): interruptible springs, gesture momentum; speed and reduced-motion settings shared by the compositor and shell | Bézier and spring curves | [Spring and easing animations; custom shaders](https://niri-wm.github.io/niri/Configuration:-Animations.html) |
| Corner shapes | Continuous corners across windows and shell | [Rounded corners and squircles](https://wiki.hypr.land/configuring/core/config-options/#decoration) | [Rounded corners with optional clipping](https://niri-wm.github.io/niri/Configuration:-Window-Rules.html#geometry-corner-radius) |
| Live configuration reload | Yes | Yes | Yes |
| Monitor mirroring | Built in | [Built in](https://wiki.hypr.land/configuring/core/monitors/) | Separate tool, such as [wl-mirror](https://niri-wm.github.io/niri/Screencasting.html#screen-mirroring) |
@@ -59,9 +59,9 @@ also include desktop controls; their features depend on the shell and tools you
## Install
Fedora and Arch package builds are covered in [Packages and releases](docs/packaging.md).
Fedora and Arch package builds are covered in [Packages and releases](https://github.com/ferese-wm/ferese/wiki/Packages-and-Releases).
Start with the [dependencies](docs/installation.md#requirements), then clone Ferese and
Start with the [dependencies](https://github.com/ferese-wm/ferese/wiki/Installation#requirements), then clone Ferese and
build it as your normal user:
```sh
@@ -72,7 +72,7 @@ cd ferese
Once it’s installed, log out and choose **Ferese** at your login screen, or run
`ferese-session` from a TTY or a greeter that accepts a command. The [installation
guide](docs/installation.md) also walks you through previews, updates and recovery.
guide](https://github.com/ferese-wm/ferese/wiki/Installation) also walks you through previews, updates and recovery.
## Configure
@@ -80,7 +80,7 @@ Configure Ferese through **Control Center → Settings** or `~/.config/ferese/co
Changes apply live, and if an edit is invalid, the last working configuration stays
in place.
The [configuration reference](docs/configuration.md) and [example
The [configuration reference](https://github.com/ferese-wm/ferese/wiki/Configuration) and [example
config](packaging/config.kdl) cover appearance, window layouts,
displays and startup apps.
@@ -111,12 +111,12 @@ Shortcuts** whenever you want to change a shortcut or gesture.
| Three-finger swipe up / down | Next / previous workspace |
| Three-finger swipe left / right | Focus the window to the right / left |
See [all shortcuts and mouse actions](docs/shortcuts.md) for window and workspace controls.
See [all shortcuts and mouse actions](https://github.com/ferese-wm/ferese/wiki/Shortcuts-and-Gestures) for window and workspace controls.
## Documentation
The [documentation index](docs/README.md) lists the desktop guides.
[Development instructions](docs/development.md) cover building, testing and contributing.
The [documentation index](https://github.com/ferese-wm/ferese/wiki) lists the desktop guides.
[Development instructions](https://github.com/ferese-wm/ferese/wiki/Development) cover building, testing and contributing.
Ferese is written in Rust using Smithay. Development happens [on
GitHub](https://github.com/ferese-wm/ferese). If something goes wrong, [open an
+28 -7
View File
@@ -1,26 +1,47 @@
# Documentation
## Get started
## Usage
- [Installation](installation.md): install dependencies, log in, preview changes, update and recover a session.
- [Configuration](configuration.md): configure layouts, input, shortcuts and displays in KDL.
- [Panel configuration](panel-configuration.md): choose a panel style, arrange groups and items, and configure the Morph hub.
- [Why Ferese](why-ferese.md): window layouts and a shared theme for the desktop.
- [Screenshots](screenshots.md): see the themes, overview and shell controls.
## Use the desktop
- [Live Activities](activities.md): show running commands in the panel, configure detection and troubleshoot missing tasks.
- [Shortcuts and gestures](shortcuts.md): keyboard navigation, moving and resizing with Super-drag, and window grouping.
- [Tabbed windows](tabbed-windows.md): group windows, select tabs and switch scrolling columns between tabs and rows.
- [Lock screen](locking.md): change its appearance, set up automatic locking and configure display sleep.
- [Screen sharing and recording](screen-sharing.md): choose a source and stop sharing.
- [Desktop portals](portals.md): services available to applications.
- [feresectl and IPC events](ipc.md): query the desktop, target a session and follow changes.
## Develop Ferese
## Configuration
- [Introduction](configuration.md): KDL syntax, loading and validation.
- [Layout](configuration/layout.md).
- [Animations](configuration/animations.md).
- [Appearance](configuration/appearance.md).
- [Workspaces](configuration/workspaces.md).
- [Input](configuration/input.md).
- [Commands and bindings](configuration/bindings.md).
- [Window rules](configuration/window-rules.md).
- [Outputs](configuration/outputs.md).
- [Panels](configuration/panel.md).
- [Panel options and examples](panel-configuration.md).
- [Activities](configuration/activities.md).
- [Status controls](configuration/status.md).
- [Startup, locking and logout](configuration/startup-and-locking.md).
- [Idle inhibition](configuration/idle-inhibition.md).
- [Session protocols](configuration/session-protocols.md).
- [Xwayland](configuration/xwayland.md).
## Development
- [Packages and releases](packaging.md): build Fedora and Arch packages and publish a tagged release.
- [Development](development.md): build Ferese, run tests and check portal contracts.
- [Publishing activities](development/activities.md): IPC schema, ownership, limits and templates.
- [Animation model](animation-model.md): geometry, spring motion, gestures and prediction.
- [Desktop reconciliation](desktop-reconciliation.md): topology publication and presentation ownership.
- [Output management tests](output-management-testing.md): profiles, hotplug and DRM failures.
- [Nested resize checks](nested-resize-testing.md).
- [Client soak testing](native-soak-testing.md).
- [Website development](../site/README.md).
- [Theme components](../crates/ferese-theme/README.md): shared native UI styles.
+17 -174
View File
@@ -135,10 +135,12 @@ feresectl run --title "Backup" -- ./backup.sh
feresectl run --title "Tests" -- npm test
```
Arguments after `--` are passed to the command. It keeps its terminal input,
output, working directory, environment, terminal process group and exit/signal
status, and still runs if activity publishing fails. Completion follows the command's exit. A background descendant holding
stdout open does not keep the wrapper alive indefinitely.
Arguments after `--` are passed to the command. The command keeps its terminal
input, output, working directory, environment, terminal process group and
exit/signal status, and still runs if activity publishing fails.
Completion follows the command's exit. A background descendant holding stdout
open does not keep the wrapper alive indefinitely.
### Cargo progress
@@ -178,105 +180,22 @@ percentage show an animated arc around their icon. Reduced motion makes progress
changes immediate and uses a static track. Activities do not automatically open
the expanded panel.
A visible task's result stays for 1.5 seconds by default. Do not disturb limits
ordinary running activities to one, hides the badge and suppresses result
flashes. When there are no eligible tasks, the regular Activities item has no
A visible task's result stays for the configured [result display time](configuration/activities.md#settings).
Do not disturb limits ordinary running activities to one, hides the badge and
suppresses result flashes. When there are no eligible tasks, the regular Activities item has no
content and Morph returns to its other content.
## Configure Activities
In **Settings → Panel & Shell → Appearance**, **Visible activities** chooses one
or two slots in Morph's collapsed and peek states. **Show activity count** controls
the `+n` badge; hiding it still lets you open the full list.
For source and timing settings, edit the top-level `activities` block in
`~/.config/ferese/config.kdl`. Merge these fields into an existing block rather
than adding a second one:
```kdl
activities {
max-visible 2
show-badge #true
start-delay-ms 300
result-flash-ms 1500
sources {
shell-hook #true
process-watch #false
notification-bridge #false
}
}
```
| Setting | Effect |
| --- | --- |
| `max-visible` | One or two compact activity slots. |
| `show-badge` | Show `+n` for tasks beyond those slots. |
| `start-delay-ms` | Minimum delay for a new task; a rule can require a longer delay. |
| `result-flash-ms` | How long an eligible completion result remains visible. |
| `shell-hook` | Accept command start/end events from an installed shell hook. |
| `process-watch` | Opt-in process sampling when hooks aren't available; process disappearance removes the task without inferring success or failure. |
| `notification-bridge` | Present notifications as activities instead of duplicate toasts, respecting Do not disturb and notification popup settings. |
The process watcher and notification bridge stay off unless you enable them.
Media comes from MPRIS; downloads and call statuses need an integration that
publishes them. Microphone or camera use alone does not create a call activity.
Changes reload live. Invalid edits leave the last valid configuration in use;
check an edit with `ferese --check-config ~/.config/ferese/config.kdl`.
For command patterns, custom titles, per-rule delays and completion messages,
see [Command rules](#command-rules).
[Configure Activities](configuration/activities.md).
### Turn off automatic command detection
Set `shell-hook #false` inside `activities.sources`. Leave `process-watch` off
as well if you don't want process detection. You can still use `feresectl run`
and direct publishers.
To skip shell and Activities setup during installation, pass
`./scripts/install.sh --no-activities`. This skips setup for that installation;
it doesn't remove an existing hook or disable an already configured source.
[Turn off automatic command detection](configuration/activities.md#turn-off-automatic-command-detection).
### Command rules
```kdl
activities {
max-visible 2
show-badge #true
start-delay-ms 300
result-flash-ms 1500
sources {
shell-hook #true
process-watch #false
notification-bridge #false
}
rule "cargo" {
match #"^cargo (build|check|test|run|clippy)\b"#
title "cargo {1}"
icon "hammer"
priority 100
progress "indeterminate"
min-duration-ms 1500
ignore-parents "rust-analyzer"
dedupe-by "cwd"
flash-on-fail #true
result ok="Finished" fail="Command failed"
}
}
```
User rules run in declaration order, then unreplaced built-ins in Cargo,
npm/pnpm, make, git-push, docker-build order. A user rule replaces the entire
built-in with the same name. One command selects one rule, pinned for its lifetime.
`ignore-parents` accepts multiple names. Regex input/pattern/program sizes are
bounded. Command and cwd metadata are used only during matching; nonmatches are
discarded and never logged. Grouping hashes cwd within owner/rule. Invocations
retain independent lifetimes and progress is never averaged.
The global/rule visibility delay is their maximum. Completion before that delay,
including failure, never flashes. Eligible results last 1500 ms by default;
`flash-on-fail #false` removes failures directly. Configuration is validated
before application; an invalid reload preserves the last valid configuration.
Diagnostics include source offsets for activity configuration errors.
[Command rules](configuration/activities.md#command-rules).
## If nothing appears
@@ -314,93 +233,17 @@ feresectl activity cancel diagnostic
```
The lease removes this diagnostic after ten seconds if you omit the cancel.
Ownership verification needs Linux 6.5 or newer. See
[Publisher ownership and lifecycle](#publisher-ownership-and-lifecycle)
for ownership, limits and expiry behavior.
See [Publisher ownership and lifecycle](development/activities.md#publisher-ownership-and-lifecycle)
for the Linux requirement, ownership checks, limits and expiry behavior.
## Publish your own status
Run these commands from the same shell. The ID `download` belongs to that shell,
so another terminal cannot update it:
```sh
feresectl activity start download --source download --icon download \
--title "Downloading update" --progress 0.2
sleep 2
feresectl activity update download --progress 0.7
feresectl activity end download --result ok --message "Downloaded"
```
This example publishes status only; it doesn't perform a download. Your program
supplies real progress and completion results.
See [Publishing activities](development/activities.md#publish-your-own-status) for the CLI examples.
## Publisher ownership and lifecycle
Use the same socket selection as every other control command: `--socket`, then
`FERESE_SOCKET`, then the session runtime default. Separate nested sessions keep
separate brokers. One-shot commands use their verified immediate parent (normally
the shell), not the CLI connection, as lifetime owner. IDs are local to that owner.
A direct publisher and `run` use the publishing process itself. The host obtains
kernel peer credentials and a socket-bound pidfd, checks the parent relationship,
process birth and UID, and monitors owner exit. This requires Linux 6.5 or newer;
an unverifiable owner is rejected instead of trusting a JSON PID.
```sh
feresectl activity start build --icon hammer --title "Build"
feresectl activity update build --progress 0.6
feresectl activity end build --result ok --message "Built"
feresectl activity start tea --icon timer --title "Tea" --remaining-ms 180000
feresectl run --title "cargo test" -- cargo test
```
`start` prints a JSON handle containing owner, session and generation. The
`--handle` option on update/end/cancel checks that exact invocation. Omitting it
resolves the local ID only within the same verified owner. Duplicate starts are
rejected. Repeated identical accepted results keep the original result deadline;
a removed or superseded generation is stale. Register again after broker restart.
The compositor assigns a terminal window at activity creation using the
publisher's process ancestry. List selection follows the activity ID across
reordering. Recording and low battery keep their protected priority, including
the combined recording/low-battery presentation.
Each owner may hold three records, including delayed and terminal entries. The
session defaults to 128 records, a priority ceiling of 160 and two compact slots.
New publications are limited to ten per owner per second. Replaceable updates
are limited to 30 per owner per second; rejected updates get
an explicit error. The bar coalesces snapshots through a bounded latest-value
subscription while the broker retains accepted results. Terminal/cancel dispatch waits for bounded dispatcher capacity
on the socket worker. A CLI disconnect does not remove an owner's records.
Running owner death and expired leases remove entries without assigning a result.
Results survive owner exit until their original deadline. Leases are positive and
at most five minutes; valid refreshes renew only the lease.
Activities stay on their creation output, initially the focused output. On output
removal they move to the first surviving output, or become unpresented until an
output is available. Host timestamps use `CLOCK_BOOTTIME`, including suspend.
Countdown wire timestamps are ignored and replaced with host sample time.
See [Ownership and lifecycle](development/activities.md#publisher-ownership-and-lifecycle).
## Extensions and templates
The version-1 activity schema is carried inside the existing versioned,
length-prefixed JSON `activity-publish` command. `activity-get` and long-poll
`activity-watch` provide snapshots. See `ferese_ipc::activities` and
`ferese_activities::Data`; there is no second socket or JSON-lines protocol.
Extensions publish typed Progress, Timer, Media, or List data. The bar controls
rendering. Values are indeterminate, finite progress in `[0,1]`, bounded text, or
countdown durations. Titles/text are limited to 256 UTF-8 bytes; lists to eight
rows. Icons are symbolic names; unknown names use the Ferese symbol, and paths
are rejected. No extension widgets, markup, colors, shaders or drawing programs
are accepted.
Use `--template` with a typed JSON template, e.g.
`'{"kind":"list","data":{"rows":["Indexing files","Waiting for server"]}}'`.
Capture/call integrations use `--source capture` and describe the state they
actually observe. Process names, microphone use, or camera use do not infer calls
or downloads. Existing MPRIS state supplies Media rows and transport controls;
recording/battery reuse the shell's existing protected state and watchers.
The optional notification bridge uses existing notification state, ranks below
all other ordinary sources, and respects DND and popup privacy. When enabled, a
notice becomes an activity rather than a duplicate toast. It is off by default.
See [Templates and the IPC schema](development/activities.md#extensions-and-templates).
+2 -1
View File
@@ -1,4 +1,5 @@
# Continuum: Ferese’s animation model
When you resize a window, Ferese has to coordinate where the layout wants it, where it currently appears, and what size the application has actually drawn. Those can differ during a transition: the layout may already have requested a larger window while the application is preparing its next buffer and the visible frame is still expanding. Continuum manages that visible movement so the application and your next input can proceed while the animation finishes. It provides the spring implementation and motion settings used by both the compositor and the shell.
How that movement behaves depends on what you’re doing. While dragging, the window follows the pointer directly; adding a spring there would make it trail your hand. When you release a gesture, however, a spring takes over with the velocity of the movement you just made. If you change the destination before it settles, Ferese can retain the current position and velocity rather than restart the movement from rest. Large movements use zero bounce by default, so they settle without oscillating around the destination.
@@ -135,4 +136,4 @@ Rendering checks need to follow the interruptions people can make while using th
Those checks establish state and rendering behavior, but input-to-presentation latency and smoothness at 60, 120, 144, and 240 Hz still need measurements on the target hardware. The solver’s correctness cannot tell us whether a particular frame arrived in time to be displayed.
For user settings and KDL examples, see [Animation configuration](configuration.md#animations).
For user settings and KDL examples, see [Animation configuration](configuration/animations.md#animations).
+25 -1104
View File
File diff suppressed because it is too large. Load diff
+152
View File
@@ -0,0 +1,152 @@
# Configure Activities
Select Morph in **Settings → Panel & Shell** to find **Visible activities** and
**Show activity count** in its appearance controls. These set the compact slots
and `+n` badge; hiding the badge still lets you open the full list.
Set sources, timing and command rules in the top-level `activities` block in
`~/.config/ferese/config.kdl`. Merge fields into an existing block rather than
adding a second one. This example enables the shell hook:
```kdl
activities {
max-visible 2
show-badge #true
start-delay-ms 300
result-flash-ms 1500
sources {
shell-hook #true
process-watch #false
notification-bridge #false
}
}
```
For hook installation and everyday use, see [Live Activities](../activities.md).
For the IPC schema and publisher lifetimes, see [Publishing activities](../development/activities.md).
## Settings
Omitted fields use these built-in defaults. Integer ranges include both endpoints.
| Key | Type / accepted values | Default | Meaning |
| --- | --- | --- | --- |
| `max-visible` | Integer 1–2 | `2` | Compact activity slots |
| `show-badge` | Boolean | `#true` | Show `+n` for tasks beyond those slots |
| `max-total` | Integer 1–128 | `128` | Session record limit, including delayed and completed entries |
| `owner-ceiling` | Integer 0–255 | `160` | Highest base priority used to rank ordinary activities |
| `start-delay-ms` | Integer 0–60000 | `300` | Minimum delay before a task appears; a rule can require a longer delay |
| `result-flash-ms` | Integer 0–10000 | `1500` | How long an eligible completion result remains visible |
## Sources
Set these booleans inside `activities.sources`:
| Key | Built-in default | Meaning |
| --- | --- | --- |
| `shell-hook` | `#false` | Accept command start/end events from an installed shell hook |
| `process-watch` | `#false` | Sample processes when hooks are unavailable; disappearance removes the task without inferring success or failure |
| `notification-bridge` | `#false` | Show notifications as activities instead of duplicate toasts, respecting Do not disturb and notification popup settings |
All three sources are off in the built-in defaults. First-time installer setup
enables `shell-hook` unless you explicitly set it to `#false`. The process watcher
and notification bridge stay off unless you enable them.
Media comes from MPRIS. Downloads and call statuses need an integration that
publishes them; microphone or camera use alone does not create a call activity.
Changes reload live. Invalid edits leave the last valid configuration in use.
Check an edit with `ferese --check-config ~/.config/ferese/config.kdl`.
## Turn off automatic command detection
Set `shell-hook #false` inside `activities.sources`. Leave `process-watch` off
as well if you don't want process detection. You can still use `feresectl run`
and direct publishers.
To skip shell and Activities setup during installation, pass
`./scripts/install.sh --no-activities`. This skips setup for that installation;
it doesn't remove an existing hook or disable an already configured source.
## Command rules
A named `rule` matches command text and supplies its title, icon, priority and
completion messages. Rules are matched in declaration order, then unreplaced
built-ins in Cargo, npm/pnpm, make, git-push, docker-build order. A user rule
replaces the entire built-in with the same name. One command selects one rule,
which stays attached to that invocation until it ends.
```kdl
activities {
max-visible 2
show-badge #true
start-delay-ms 300
result-flash-ms 1500
sources {
shell-hook #true
process-watch #false
notification-bridge #false
}
rule "cargo" {
match #"^cargo (build|check|test|run|clippy)\b"#
title "cargo {1}"
icon "hammer"
priority 100
progress "indeterminate"
min-duration-ms 1500
ignore-parents "rust-analyzer"
dedupe-by "cwd"
flash-on-fail #true
result ok="Finished" fail="Command failed"
}
}
```
These defaults apply to a new custom rule. Built-in rules have their own values,
shown in the [source catalog](../../crates/ferese-config/activities.kdl).
| Key | Type / accepted values | Default for a custom rule |
| --- | --- | --- |
| Rule name | String, up to 256 UTF-8 bytes, no control characters; unique | Required positional argument |
| `match` | Valid regular expression, up to 2048 UTF-8 bytes | Required |
| `title` | String, up to 256 UTF-8 bytes, no control characters | Required |
| `icon` | Symbolic name, up to 64 ASCII letters, digits or hyphens | `"ferese"` |
| `priority` | Integer 0–255; ranking caps it at `owner-ceiling` | `100` |
| `progress` | `"indeterminate"` or `"cargo-json"` | `"indeterminate"` |
| `min-duration-ms` | Integer 0–60000 | `0` |
| `ignore-parents` | Up to 16 names, each up to 128 UTF-8 bytes | No ignored parents |
| `dedupe-by` | `"cwd"`, or omit for no grouping | Unset |
| `flash-on-fail` | Boolean | `#true` |
| `result.ok` | String, up to 256 UTF-8 bytes, no control characters | `"Finished"` |
| `result.fail` | String, up to 256 UTF-8 bytes, no control characters | `"Command failed"` |
Use `result ok="Finished" fail="Command failed"` inside the rule to change both
messages. If you provide `result`, both fields are required. Title placeholders
refer to regex captures: `{0}` is the whole match, `{1}` the first group, and so
on. Invalid capture references reject the configuration. An expanded title over
256 bytes or containing control characters is discarded during matching.
`ignore-parents` accepts multiple names in one node, such as
`ignore-parents "rust-analyzer" "editor"`. `dedupe-by "cwd"` groups tasks by a
hash of the working directory within the same owner and rule. Invocations retain
independent lifetimes, and progress is never averaged.
`progress "cargo-json"` enables the adapter for matching `feresectl run`
commands. Shell hooks do not wrap commands or parse their output automatically;
see [Cargo progress](../activities.md#cargo-progress).
## Visibility and matching limits
The visibility delay is the larger of `start-delay-ms` and `min-duration-ms`.
Completion before that delay, including failure, never flashes. Eligible results
last 1500 ms by default; `flash-on-fail #false` removes failures directly.
At most 64 rules may remain after unreplaced built-ins are added. Command input
is limited to 4096 UTF-8 bytes, working directories to 1024 bytes and parent lists
to 16 entries. The matcher sets a 256 KiB compiled-size limit and a 256 KiB DFA-size limit.
Command and cwd metadata are used only during matching; nonmatches are discarded
and never logged.
Configuration is validated before application; an invalid reload preserves the
last valid configuration. Diagnostics include source offsets for activity
configuration errors.
+136
View File
@@ -0,0 +1,136 @@
# Animations
Continuum is Ferese's shared compositor and shell motion system. Set animation
speed in Settings → Windows → Motion or edit the `animations` block in
`~/.config/ferese/config.kdl`. Changes apply live. Start with `speed` if you only
want faster or slower transitions; you do not need to change the springs.
```kdl
animations {
enabled #true
reduced-motion #false
speed 1.0
spring {
duration-ms 240.0
bounce 0.0
overshoot #false
}
viewport-spring {
duration-ms 350.0
bounce 0.0
overshoot #false
}
}
```
This example uses the built-in defaults. The packaged config sets `speed 0.9`.
You can omit either spring block to keep its defaults.
| Section / key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `animations.enabled` | boolean | `true` | Enable motion |
| `animations.reduced-motion` | boolean | `false` | Disable animated motion |
| `animations.speed` | number > 0 | `1` | Higher is faster; `0.75` is slower |
| `animations.spring.duration-ms` | number > 0 | `240` | Window and shell spring response |
| `animations.viewport-spring.duration-ms` | number > 0 | `350` | Scrolling and workspace spring response |
Both `spring` and `viewport-spring` accept these keys:
| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `duration-ms` | number > 0 | `240` or `350`, as above | Larger values give a slower response; not a completion deadline |
| `bounce` | number strictly between −1 and 1 | `0` | Zero is critical damping; positive values require overshoot |
| `overshoot` | boolean | `false` | Allow this spring to cross its target |
Use duration and bounce to configure spring physics. `mass`, `stiffness`,
`damping` and `damping-ratio` are internal solver parameters, not configuration
keys. Removed or misspelled animation keys are rejected. Invalid edits leave
the last accepted configuration active.
## Speed and reduced motion
`speed` changes window motion, scrolling, workspace transitions, overview,
theme transitions and shell popup, notification and hover transitions. `0.5`
gives an animation twice as much time; `2.0` gives it half as much. Overview
uses 60% of this speed when opening or closing, including when selecting a
window. There is no separate overview-speed setting.
Set `reduced-motion #true` or `enabled #false` to make these transitions
immediate. Reduced motion takes precedence over `enabled #true`; setting
`speed 0` is invalid. These settings control Ferese's animations, not animations
inside other applications.
Morph animates its shape with its own spring and fades between views, while
following the shared speed and reduced-motion policy; see the
[panel guide](../panel-configuration.md#morph) for its states and size controls.
Shell animations sample motion on each surface's redraw, paced by its Wayland
frame callbacks. They request animation frames only while moving.
Menus, modals, notifications and hover fades use this path instead of a fixed
16 ms animation timer. Clock, notification-expiry and service updates keep their
own schedules.
Overview can reverse while moving, and workspace changes slide in their navigation
direction. Workspace and scrolling gestures carry their release velocity into
the settling spring. Scrolling gestures resist at viewport limits and spring back.
Window opening starts when content is available; closing waits for the application
to unmap, then animates its retained image. Input and application close requests
remain responsive while the animations finish.
Live windows, overview thumbnails and retained images share a window identity
and presentation state. Entering overview inherits the window's visible position
and velocity, including motion from a workspace slide. Resize images use the same
content mapping in overview and remain visible if the window closes during a resize.
Content, clipping and borders use the same squircle outline at the output's scale.
## Spring tuning
`spring` controls window position and size motion, fullscreen/maximize zoom and
overview entrance and dismissal, opening/closing windows, focus emphasis, shell
popups and notifications. `viewport-spring` controls the scrolling viewport,
workspace slides and the column-width animation that runs with scrolling.
Continuum shares the analytic spring solver and motion settings between the
compositor and shell. Small opacity changes (hover, dimming, theme fades) may
still use timed transitions.
Focus emphasis and shadows have separate spring responses. Emphasis uses 80% of
the main spring's response time; shadows use 120%. These ratios preserve the
configured damping ratio and overshoot policy, and follow `speed` and reduced
motion. They are built-in tuning values, not additional configuration keys.
Focused windows use the theme's full shadow. Unfocused shadows use 75% of its
vertical offset, 90% of its blur radius and 80% of its opacity, with spring motion
between those values. Shadow motion does not change the content or border geometry.
Spring settling time depends on distance, velocity and tolerances. `duration-ms`
controls the response, not a fixed completion deadline. Both default springs are
critically damped; the viewport has a slower response than windows and shell popups.
To opt into bounce for one spring:
```kdl
animations {
spring {
duration-ms 300.0
bounce 0.2
overshoot #true
}
}
```
Bounce defaults to zero and must be strictly between -1 and 1. Positive bounce
requires `overshoot #true` in that spring block. Negative bounce adds overdamping.
Without overshoot, Ferese stops a spring at its first target crossing. Opacity
remains bounded even when geometry can overshoot. Overshoot is never enabled globally.
For the solver, mass is fixed at `1`. With `T = duration-ms / 1000`, Continuum uses
`omega = 2*pi/T`, `stiffness = omega^2` and `damping = 2*omega*zeta`.
`zeta = 1-bounce` for nonnegative bounce, otherwise `zeta = 1/(1+bounce)`.
This conversion does not guarantee the same perceptual duration or settling time
as Apple's springs.
Duration and speed must be finite and greater than zero. Bounce must be finite.
Values that overflow or underflow the derived coefficients are rejected. The
allowed bounce range keeps damping positive, so undamped oscillation cannot
prevent popup cleanup.
+137
View File
@@ -0,0 +1,137 @@
# Appearance
Settings → Appearance offers six paired themes: **Ferese Blue** (default),
**Catppuccin**, **Gruvbox**, **Rosé Pine**, **Tokyo Night**, and **Everforest**.
Ferese Blue uses the default accent, `#3D7BE6`, and has light and dark variants.
Choose Light, Dark, or Auto in Settings or Control Center. Auto follows local
07:00 and 19:00 boundaries unless you change its schedule.
Selections store preset names. Partial KDL files can override shared colors or
one appearance.
| Section / key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `theme.mode` | `"light"`, `"dark"`, `"auto"` | `"dark"` | Selected appearance mode |
| `theme.family` | family id | `"ferese-blue"` | Paired theme; automatically uses its light or dark variant |
| `theme.split` | boolean | `false` | Use separate light/dark families; retains both selections when disabled |
| `theme.light.family`, `theme.dark.family` | family id | `"ferese-blue"` | Stored selections used when split is enabled |
| `theme.custom-themes.<id>.file` | path | unset | Imported KDL family with explicit light and/or dark sections |
| `theme.light.preset` | preset name | `"ferese-blue-light"` | Light palette |
| `theme.dark.preset` | preset name | `"ferese-blue"` | Dark palette |
| `theme.file` | path | unset | Shared partial KDL theme |
| `theme.light.file`, `theme.dark.file` | path | unset | Partial KDL theme for one appearance |
| `theme.accent` | hex color | unset | Accent override, adjusted for readability in each appearance |
| `theme.schedule.source` | `"system"`, `"schedule"` | `"schedule"` | Follow GTK/GNOME appearance or a custom schedule; sunrise/sunset is deferred |
| `theme.schedule.timezone` | `"system"` or IANA timezone | `"system"` | Auto schedule timezone |
| `theme.schedule.light-at`, `theme.schedule.dark-at` | `HH:MM` | `"07:00"`, `"19:00"` | Auto boundaries; times must differ |
| `theme.accessibility.increase-contrast` | boolean | `false` | Strengthen text and borders |
| `theme.accessibility.reduce-transparency` | boolean | `false` | Solid surfaces; skip background blur |
| `appearance.focus-effect.enabled` | boolean | `true` | Apply window opacity and dimming; disabling preserves the saved settings |
| `appearance.focus-effect.active-opacity` | number 0–1 | `1` | Active application opacity, including fullscreen |
| `appearance.focus-effect.inactive-opacity` | number 0–1 | `1` | Other application window opacity |
| `appearance.focus-effect.inactive-dim` | number 0–1 | `0` | Darkening strength for inactive windows; 0 disables |
| `appearance.focus-effect.duration-ms` | number ≥ 0 | `150` | Shared opacity and dimming transition; 0 snaps |
| `theme.typography.font-family` | string | `"Inter"` | Shell, Settings and overview font |
| `theme.background.path` | string | matching light/dark wallpaper | Shared wallpaper; an explicit path overrides both bundled defaults |
| `theme.light.background.path`, `theme.dark.background.path` | string | shared path, or matching default | Wallpaper for one appearance; overrides the shared path |
| `theme.background.mode` | `"fill"`, `"fit"` | `"fill"` | Crop or letterbox |
| `theme.light.background.mode`, `theme.dark.background.mode` | `"fill"`, `"fit"` | shared placement | Image placement for one appearance |
| `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.tint-strength` | number 0–1 | Dark `0.5`, light `1` | Color strength over the blurred backdrop |
| `theme.material.blur-radius` | number ≥ 0 | `12` | Translucent backdrop blur, capped at 32; 0 disables |
Window opacity follows the active application. Giving keyboard focus to a shell
menu does not change it. Overview selection changes the highlight, while window
opacity still follows activation; dimming fades out as overview opens. Fullscreen
windows use the same opacity settings. Opening and closing fades multiply window
opacity. Reduced motion disables the focus transition.
Settings → Wallpaper shows the current image and both bundled defaults. Use
**Apply to** to change both modes or just one. **Match appearance** restores the
bundled light and dark pair. Custom images can also be selected separately in KDL:
```kdl
theme {
light {
background { path "~/Pictures/day.png"; }
}
dark {
background { path "~/Pictures/night.jpg"; }
}
}
```
The wallpaper follows manual light/dark changes and automatic appearance changes.
A shared `theme.background.path` applies to both modes unless a per-mode path
is set.
| `theme.colors` key | Default | Meaning |
| --- | --- | --- |
| `surface-base` | `"#111821"` | Material/card base color |
| `surface-raised` | `"#1E2530"` | Raised surfaces, including the Morph hub |
| `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 |
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` | Shell surfaces and controls, apart from panel shapes; fractional radii are preserved; 0 makes them square |
| `control-gap` | number ≥ 0 | `12` | Shared control spacing token; panel spacing belongs to its groups |
| Section / key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `theme.surface.bar.background` | color | `"#111821"` | Regular panel background color; Morph uses `surface-raised` |
| `theme.surface.bar.text-primary` | color | `"#F4F7FB"` | Panel text/icons |
| `theme.surface.bar.text-muted` | color | `"#8793A2"` | Inactive panel 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.
Window focus borders and selected controls use an accent gradient by default.
Built-in themes supply the second color; when a theme has one accent color,
Ferese derives a nearby lighter or darker shade. Changing the accent replaces
the automatic gradient. Inactive window borders derive their gradient from the
border color and keep its transparency.
Automatic accent endpoints are adjusted for contrast against the theme's surfaces.
In Settings → Appearance, choose **Gradient** or **Solid** for **Accent style**
and **Inactive border style**. In KDL, `theme.focus-ring.style` and
`theme.border.style` accept `"auto"` (default) or `"solid"`. Solid disables the
gradient, including any explicit endpoints, without deleting them from the config.
For custom endpoints, set `theme.focus-ring.gradient` or `theme.border.gradient`.
Both require `from` and `to` colors; `angle` is optional and defaults to `0`.
Angles are clockwise: 0 is left-to-right, 90 top-to-bottom. Explicit gradients
take precedence over automatically derived colors. The automatic angle is 135°.
```kdl
theme {
focus-ring {
gradient {
from "#e5c890"
to "#b98d58"
angle 135.0
}
}
}
```
For solid colors:
```kdl
theme {
focus-ring { style "solid"; }
border { style "solid"; }
}
```
+154
View File
@@ -0,0 +1,154 @@
# 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.
| `binding` key | Type / values | Default |
| --- | --- | --- |
| `keys` | chord or gesture, e.g. `"Super+Enter"`, `"Swipe3Up"` | required |
| `match` | `"keysym"`, `"physical"` | `"keysym"` |
| `action` | action name below | required unless disabled |
| `argument` | string | required only for actions listed below |
| `disabled` | boolean | `false` |
Modifiers: `Super`/`Logo`/`Mod4`, `Ctrl`/`Control`, `Alt`, `Shift`.
Keys use XKB keysyms (`Enter`, letters, `[`/`]`) or physical XKB names (`AD06`).
A binding replaces the same normalized chord/match mode. Duplicate bindings are
invalid. A disabled binding must omit `action` and `argument`.
| Actions | Argument |
| --- | --- |
| `spawn` | Name in `commands` |
| `binding-mode` | Named mode, or `"default"` to leave it |
| `focus`, `move`, `resize`, `join-tab` | `"left"`, `"right"`, `"up"`, `"down"` |
| `workspace`, `move-to-workspace` | Workspace number string, 1–255 |
| `workspace-next`, `workspace-previous` | None; next/previous workspace on this monitor |
| `workspace-back-and-forth` | None; return to the previous workspace on this monitor |
| `focus-last-window` | None; return to the last focused window |
| `focus-floating` | None; focus the last focused window in the opposite tiled/floating layer on this workspace |
| `focus-mru-next`, `focus-mru-previous` | None; cycle windows in focus order |
| `none` | None; ignore this trigger |
| `close`, `exit`, `toggle-maximized`, `toggle-fullscreen`, `toggle-layout`, `cycle-column-width`, `center-column`, `consume`, `expel`, `toggle-column-tabbed`, `leave-tab`, `next-tab`, `previous-tab`, `toggle-floating`, `toggle-overview` | None |
```kdl
commands {
terminal "foot"
}
binding keys="Super+Enter" action="spawn" argument="terminal"
binding keys="Super+V" action="focus-floating"
```
See [Tabbed windows](../tabbed-windows.md) for grouping actions and
[Layout configuration](layout.md#tabbed-columns) for scrolling tab settings.
`focus-floating` switches focus between tiled and floating windows without
changing placement. It stays on the active workspace and does nothing when the
opposite layer has no candidate. `toggle-floating` changes window placement.
## Binding modes
Named modes provide a separate keyboard map for resizing, launching, or other
sets of actions. No modes are enabled by default. For example:
```kdl
binding "Super+Shift+r" "binding-mode" "resize"
binding "Super+Shift+Return" "binding-mode" "launch"
binding-mode "resize" {
binding "h" "resize" "left"
binding "j" "resize" "down"
binding "k" "resize" "up"
binding "l" "resize" "right"
binding "Return" "binding-mode" "default"
}
binding-mode "launch" {
binding "t" "spawn" "terminal"
binding "r" "binding-mode" "resize"
binding "Return" "binding-mode" "default"
}
```
Release the entry chord's modifiers before using an unmodified mode binding.
Modifiers keep their usual exact matching semantics; entering a mode never
changes the application's modifier state. Escape exits with any modifiers held
and cannot be overridden inside a mode. The bar shows the active name and Escape
hint, including over fullscreen windows, and restores its ordinary contents on
exit. Long names truncate to keep the exit hint visible. Mode changes reach the
shell through its existing snapshot protocol.
Entering a mode closes panel popovers and workspace tooltips. An expanded Morph
panel collapses and releases keyboard focus. Hover, activity updates, and queued
menu requests cannot reopen it while the mode indicator is displayed.
Live Activities continue updating their data, but leave the hidden Morph panel's
geometry unchanged until the mode exits.
Mode bindings replace the default keyboard map while active. Unmatched keys
reach the focused application; default compositor bindings and portal shortcuts
do not run as fallbacks. Gestures keep the default configuration. Normal shortcut
inhibition still applies, with Escape available to leave the mode. A mode remains
active across actions and focus/workspace changes until explicitly changed or
exited; launching an application does not implicitly leave it.
Each mode must have a unique name of 1–48 ASCII letters, digits, underscores, or
hyphens. `default` is reserved. Mode bindings accept the same actions and physical
key matching as ordinary bindings; swipes, disabled records, duplicate chords,
and Escape bindings are rejected, including physical keys remapped to Escape.
References to undefined modes reject the configuration before it is applied.
An accepted change to bindings, modes, commands, or input settings returns to
default and closes any focus preview. Unrelated reloads preserve the mode;
rejected reloads keep the previous configuration and mode. Locking, input capture,
and session suspension also exit.
The first press owns its matching release across these transitions. Held keys
are never reinterpreted by a new mode; consumed releases stay consumed. Repeated
presses retain their original action and repeat only while its chord still
matches. A mode/configuration transition stops that action's repeats.
`feresectl keybindings` lists the active keyboard map.
Defaults: Super+Enter terminal; Super+Q close; Super+H/J/K/L focus;
Super+Shift+H/J/K/L move; Super+Ctrl+H/J/K/L resize; Super+1–9 workspace;
Super+Shift+1–9 move to workspace; Super+R width cycle; Super+C center;
Super+[/] consume/expel; Super+Shift+T stacked/tabbed column; Super+Ctrl+[/] previous/next tab;
Super+F maximize; Super+Shift+F fullscreen;
Super+M layout; Super+Shift+Space floating; Super+V focus tiled/floating; Super+Tab overview;
Super+Escape previous visited workspace;
Super+BackSpace last focused window; Alt+Tab/Alt+Shift+Tab MRU switching;
Super+Shift+S area screenshot; Print Screen whole-screen screenshot;
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. The starting position
selects an edge or corner using a 3×3 grid; the center selects the nearest corner.
The selected edges stay fixed for the whole drag. These work even when an app has
no title bar or resize border.
For a floating window, Super+Shift+H/J/K/L moves left/down/up/right by 32 logical
pixels. Super+Ctrl+H/L shrinks/grows its width; Super+Ctrl+K/J shrinks/grows its
height by the same step. Keyboard adjustments respect client size limits and
keep the window reachable in its output's work area. Fullscreen and maximized
floating windows ignore these actions.
Mouse moves and resizes snap to work-area edges and nearby visible window edges at
10 logical pixels, then resist movement until 20 pixels from the attached edge.
Window edges are eligible only when the windows overlap along the other axis,
with a 10-pixel allowance. Hold Shift during a drag to bypass snapping.
The screenshot shortcut runs `ferese-screenshot`: drag to select an area, or
press Escape to cancel. Captures open in Satty for annotation. Press Enter to
save the edited PNG under your Pictures directory in `Screenshots` and copy it
to the clipboard; Escape discards the capture. It requires `slurp`, `satty`, and
`wl-copy` (from `wl-clipboard`); the capture itself is done by the compositor
through `feresectl screenshot`. Override the `screenshot` command to use another
screenshot tool.
Print Screen runs `ferese-screenshot --full` and captures the active monitor
without a selector, falling back to the sole enabled output when focus is
unavailable. It never silently captures every output. Fn+PrtSc works when the
keyboard emits the Print Screen key; Fn is handled by the keyboard firmware.
Override `screenshot-full` to customize this command. Both modes open Satty with
the same save and copy workflow.
`ferese-screenshot --all` captures every enabled output as one image.
Satty opens as a floating window sized for the captured image and constrained
to the workspace.
+42
View File
@@ -0,0 +1,42 @@
# Idle inhibition
Ferese keeps the session awake during fullscreen playback when an application
reports `Playing` through MPRIS. Pausing or stopping playback releases this
automatic inhibition. A window must have content visible on an awake display;
switching away, closing it, locking the session or sleeping its display releases
inhibition too.
Disable the default fullscreen playback policy with:
```kdl
idle-inhibit {
fullscreen-playback #false
}
```
Use window rules for presentations, games or other apps that should keep the
session awake while visible:
```kdl
window-rule app-id="org.example.Presentation" idle-inhibit="visible"
```
| `idle-inhibit` rule | Behavior |
| --- | --- |
| `visible` | Inhibit while visible, regardless of playback state |
| `fullscreen` | Inhibit while visible and fullscreen |
| `playing` | Inhibit while visible and its MPRIS player reports `Playing` |
| `fullscreen-playing` | Inhibit while visible, fullscreen and its MPRIS player reports `Playing` |
| `none` | Disable automatic inhibition for this window |
MPRIS identifies an application, not an individual video window. Ferese matches
the player's process ID first, then its `DesktopEntry` against the window's app
ID. If several windows match, automatic playback inhibition stays off. MPRIS
also does not distinguish audio from video, so fullscreen audio playback can
inhibit too. Players without MPRIS can request inhibition through the existing
Wayland or portal protocols; a fullscreen window alone does not prove playback.
These settings control automatic inhibition. They do not override an
application's own Wayland or portal request, and they do not block manual
locking or suspend. Run `feresectl idle-inhibition` to inspect the current
inhibition state and discovered players. Changes take effect on config reload.
+53
View File
@@ -0,0 +1,53 @@
# 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 |
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
using directional keyboard focus brings the selected window into view.
In Overview (`Super+Tab`), click a window preview or a miniature inside a workspace
card to activate that exact window. Hover highlights the target; directional
focus keys move the selection, Enter activates it, and Escape dismisses Overview.
Clicking a workspace card's background switches workspaces while keeping Overview
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
previews use the host's physical input settings.
Workspace swipes and left/right window-focus swipes follow your fingers and settle
when released. Up goes to the next workspace on the
current monitor, down to the previous; left focuses the window to the right,
and right focuses the window to the left. Workspace swipes skip workspaces owned
by other monitors and stop at the first/last workspace. Short or cancelled navigation
swipes slide back; diagonal swipes do not navigate. Navigation pauses while locked, during
window grabs, or when an application inhibits shortcuts.
Assign swipes in **Settings → Keyboard & mouse → Shortcuts** using the same actions and arguments as
keyboard bindings. Choose a "Customize swipe" button to add an override. The
gesture keys are `Swipe3Up`, `Swipe3Down`, `Swipe3Left`, and `Swipe3Right`; 4 or 5
fingers are also supported. Changes reload live:
```kdl
binding keys="Swipe3Up" action="toggle-overview"
binding keys="Swipe3Left" action="move" argument="left"
binding keys="Swipe3Down" disabled=#true
```
`spawn` uses a named entry from `commands`, just like keyboard shortcuts. To
disable a direction in Settings, set its action to `none` and leave Argument
empty. In KDL, `disabled=#true` removes the default binding. Swipes do not use
keyboard modifiers or physical key matching. The trigger is automatically
recognized from its name. Gesture navigation uses native touchpad events in a
hardware session.
Adjust recognition distance in **Settings → Keyboard & mouse**, or with
`swipe-threshold 80` under `input.touchpad` (integer 16–1000 logical pixels).
+93
View File
@@ -0,0 +1,93 @@
# Layout
| `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 |
| `scrolling` key | Type / values | Default | Meaning |
| --- | --- | --- | --- |
| `default-column-display` | `"stacked"`, `"tabbed"` | `"stacked"` | Display mode of new columns; reload preserves existing column choices |
| `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` leaves fully visible columns in place and scrolls only enough to reveal
a hidden edge, ignoring differences of up to 0.5 logical pixels. Full-width
columns align with the viewport. Wider columns reveal their left edge when
approached from the left and their right edge when approached from the right;
an existing view inside a wider column stays in place. `center_on_focus` centers
the focused column. `paged` packs columns into viewport-sized pages: halves form pairs, thirds
form triples; mixed widths and effective minimum sizes determine actual boundaries.
Changing default width preserves manually resized columns.
## Tabbed columns
To open new columns as tabs:
```kdl
scrolling {
default-column-display "tabbed"
}
```
Your existing columns stay as they are. Press **Super + Shift + T** to switch
the focused column.
See [Tabbed windows](../tabbed-windows.md) for grouping, tab selection and the default shortcuts.
## Scrolling tab indicator
| `scrolling.tab-indicator` key | Type / range | Default |
| --- | --- | --- |
| `width` | number, 1–16 | `4` |
| `segment-height` | number, `width`–128 | `24` |
| `segment-gap` | number, 0–64 | `4` |
| `hit-width` | number, `width`–64 | `12` |
| `gap-threshold` | number, `width`–128 | `8` |
| `list-width` | number, 80–640 | `220` |
| `row-height` | number, (`font-size` + 6)–80 | `28` |
| `padding` | number, 0–24 | `4` |
| `font-size` | number, 8–32 | `12` |
| `outline-width` | number, 0–8 | `0.5` |
| `flash-duration-ms` | integer, 0–60000 | `1000` |
| `active-color` | optional color | Theme accent |
| `inactive-color` | optional color | Theme border |
| `background-color` | optional color | Theme raised surface |
| `text-color` | optional color | Theme primary text |
| `outline-color` | optional color | Theme border |
Configure the scrolling indicator under `scrolling.tab-indicator`:
```kdl
scrolling {
tab-indicator {
width 4
segment-height 24
segment-gap 4
hit-width 12
gap-threshold 8
list-width 220
row-height 28
padding 4
font-size 12
outline-width 0.5
flash-duration-ms 1000
}
}
```
Sizes are logical pixels. The hover list fits its longest title, up to
`list-width`, with an 80px minimum. Its width stays fixed while open.
`outline-width 0` hides the outline; positive widths draw at least one physical
pixel. `flash-duration-ms 0` disables keyboard feedback.
Leave colors unset to use your theme, or override them with `#RRGGBB` or
`#RRGGBBAA` values.
The selected row has a faint highlight using the text color. The rail has
fully rounded ends; the title list follows the shell corner radius.
The hover list and keyboard feedback pill follow the shell material opacity.
Solid material or Reduce transparency makes their backgrounds opaque. Text and
rail markers retain their own opacity. These controls use flat fills without
blur or shadows. Changes apply on configuration reload.
+150
View File
@@ -0,0 +1,150 @@
# Displays
Each `output-profile` needs a unique, nonempty `name` and at least one `output`
entry. A profile matches when every required monitor is connected and its
optional `lid-closed` condition matches. The profile with the most required
monitors wins; ties use config order. If none matches, Ferese extends across all
usable connected monitors. Find selectors with `feresectl outputs`.
| `output-profile` key | Type | Default |
| --- | --- | --- |
| `name` | unique nonempty string | required |
| `layout` | `internal-only`, `external-only`, `extend`, `mirror` | `extend` |
| `lid-closed` | boolean | either lid state |
| `mirror-source` | selector of an enabled output in this profile | first enabled hardware key |
| `lid-policy` | `dock-or-suspend`, `ignore` | `dock-or-suspend` |
| `confirm-timeout` | seconds; `0` disables confirmation | `15` |
| `output-profile → output` key | Type | Default |
| --- | --- | --- |
| `match` | nonempty connector or persistent identity string | required; unique within profile |
| `required` | boolean | `true`; set `false` for an optional monitor |
| `enabled` | boolean | `true` |
| `mode` | `"WIDTHxHEIGHT"` or `"WIDTHxHEIGHT@HZ"` | Preferred mode |
| `auto-refresh` | boolean | `false` |
| `scale` | number > 0 | `1` |
| `transform` | enum below | `"normal"` |
| `position` | `[integer, integer]` | Automatic horizontal placement |
Transforms: `normal`, `rotate-90`, `rotate-180`, `rotate-270`, `flipped`,
`flipped-90`, `flipped-180`, `flipped-270`. In extend and mirror profiles,
unlisted monitors stay enabled with defaults. Internal-only and external-only
filter by connector type. A layout that would disable the final usable display
falls back to a working display. Profiles that explicitly disable all their
listed outputs are invalid.
Monitor identities use EDID manufacturer, product/model and serial, rather than
a hash of the entire EDID. Without a usable serial, the identity includes the
GPU's sysfs hardware path and connector. For duplicate serials, Ferese adds the
same path and connector to distinguish the monitors. If both arrive together,
both identities include those details. If the second arrives later, the first
keeps its identity and only the second gets the path and connector. Ferese
remembers that choice for the compositor session. Without serials, Ferese
identifies monitors by port and cannot tell them apart if you swap their ports.
Connector names and old `drm-edid:` selectors still work. Ambiguous or
overlapping selectors are rejected during live reload. A disconnected identity
can remain in config; it prevents matching only when its entry has
`required=#true`.
```kdl
output-profile "desk" layout="external-only" {
output "DP-1" mode="2560x1440@60" scale=1.25
output "eDP-1" required=#false enabled=#false
}
output-profile "mobile" layout="internal-only" {
output "eDP-1" scale=1.75
}
output-profile "presentation" layout="mirror" mirror-source="eDP-1" {
output "eDP-1" scale=1.75
output "HDMI-A-1" mode="1920x1080@60"
}
```
Mirror uses the source's logical size, workspace, layers and pointer. Targets
own no workspaces or separate pointer region. Ferese fits the complete source
into each target's transformed physical mode, preserving aspect ratio and
letterboxing instead of cropping. Target scale does not resize the source
layout. Each target has its own hardware cadence; only the source releases
client frame callbacks and presentation feedback. Mirrored monitors are listed
by `outputs`, but expose one logical output to clients and the shell. If a required mirror target disconnects, Ferese selects another matching
profile or extends across all usable connected monitors. Leaving mirror
restores evacuated workspaces.
```sh
feresectl outputs
feresectl output-profiles
feresectl toggle-display-mode
feresectl output-layout extend
feresectl output-profile presentation
feresectl output-confirm
feresectl output-profile auto
feresectl output-internal off
feresectl output-revert
```
The display Fn key (`XF86Display`) and the bar’s display icon open the same
display-mode chooser. The icon appears while an external monitor is connected.
`output-layout` selects a layout without requiring a named profile and keeps
the matching profile’s mode, scale and transform settings.
A manual choice overrides automatic selection until the monitor set or lid
state changes. Reload keeps it only if its profile still exists and matches.
Manual changes revert after `confirm-timeout` seconds unless confirmed;
`output-revert` restores immediately. Reverting restores the last confirmed modes,
scales, transforms and positions, even if you make several changes before
confirming. The timeout still applies if the client that made the change exits. Automatic hotplug/lid changes never wait for confirmation.
Disabling the last usable internal panel is rejected.
```kdl
output-profile name="docked" {
output match="HDMI-A-1" scale=1.5 {
position 0 0
}
}
```
Settings → Displays offers the connected display's supported refresh rates at
its configured resolution. Choosing a rate saves it and disables Auto. Choosing
Auto saves the highest supported rate as the normal mode and enables automatic
switching. These choices apply immediately for the active profile and persist
across restarts.
With `auto-refresh=#true`, a discharging system battery below 30% switches the
display to a supported mode within 1 Hz of 60 Hz at the same resolution. The
configured normal mode returns on external power or when the battery reaches
35%. Power is checked every five seconds; mode changes only run when the policy
changes. Unavailable battery data restores normal refresh. If no matching 60 Hz
mode exists, normal refresh is retained. Rejected DRM mode changes retain the
current output. A refresh-rate change can briefly blank the display (about one
second on some panels), including automatic changes below 30%, recovery at 35%,
and restoration on AC. Session activation refreshes lid and connector state
before rendering; unchanged outputs retain their handles and workspaces.
Hotplug bursts settle for 150 ms before one reconciliation. Ferese records changes that arrive while the session is inactive and applies
them on activation.
```kdl
output-profile laptop {
output eDP-1 mode="2880x1800@120" scale=1.75 auto-refresh=#true
}
```
With a usable external display, closing the lid disables internal panels after
external activation succeeds. Without one, Ferese asks logind to suspend,
respecting sleep inhibitors. `lid-policy="ignore"` disables this behavior for
the selected profile. Resume reads lid state from logind, with ACPI fallback,
before reconciling fresh hardware. Ferese releases its logind lid inhibitor on
session pause, so logind handles the lid while another session or TTY is active.
Workspaces keep their home output and return when it reconnects, unless you
reassigned or deleted them after evacuation. Floating
windows are remapped and clamped to the surviving display.
On atomic DRM, Ferese tests the complete configuration for each GPU before
changing it.
Smithay performs the real commits; logical changes follow successful hardware
application. Failed commits attempt restoration, and outputs that cannot be
restored are removed from logical state. Each GPU commits separately. If a later GPU fails, Ferese attempts to restore
the earlier GPUs. Legacy KMS lacks atomic
test-only validation. Hardware failures can still leave the session without a working monitor; Ferese tries preferred modes to recover a display instead of
intentionally turning off the last working one.
+51
View File
@@ -0,0 +1,51 @@
# Panel composition
Ferese uses one panel configuration across all outputs, with each output laying
out its own workspace controls and focused-window title. Open **Settings →
Panel & Shell** to choose a style and position, then **Edit panel** to arrange
items in Start, Center and End groups. The composition preview stays visible
while you edit, and changes save automatically.
Choose style, opacity and corners for the whole panel; groups control layout and
spacing. The available styles are Floating, Capsules, Notch, Edge to edge,
Fused islands, Adaptive and Morph. Capsules and Fused islands join neighboring
**groups within a zone**, so a zone with one group remains a single island.
Adaptive follows window state independently on each output, while Morph combines
the panel into a centered hub with expandable workspace, media and status views.
Most panels default to 36 logical pixels high, Morph to 34, and detached styles
use 8-pixel margins unless your configuration overrides them. The packaged
configuration includes its own geometry overrides. For the complete options and
examples, see the [panel configuration guide](../panel-configuration.md).
```kdl
panel "main" {
edge "top"
style "floating"
geometry { height 36; edge-margin 8; window-clearance 4; }
start {
group "navigation" {
item "overview" kind="overview" overflow="never"
item "workspaces" kind="workspaces" style="dots" overflow="never"
}
}
center {
group "title" {
item "title" kind="focused-window" overflow="never"
}
}
end {
group "status" {
item "quick-settings" kind="quick-settings"
item "battery" kind="battery" percentage=#true
item "clock" kind="clock" representation="compact"
}
}
}
```
If you omit the panel block, Ferese uses its default composition; if you write
one, its groups and items define the contents, and empty zones stay empty. Only
one panel block is accepted, and group and item IDs must be unique within it.
Use `edge "bottom"` to move the same arrangement to the bottom, with popups
opening toward the desktop and edge-relative corners mirroring vertically.
+22
View File
@@ -0,0 +1,22 @@
# Optional session protocols
These are launch-time environment switches, not KDL keys. Enable only for
trusted clients: `FERESE_ENABLE_INPUT_METHOD=1`,
`FERESE_ENABLE_SHORTCUT_INHIBIT=1`, `FERESE_ENABLE_SCREENCOPY=1`.
The installed session launcher enables screencopy for screenshots unless
`FERESE_ENABLE_SCREENCOPY=0` is explicitly set in its environment. Setting it
to `0` disables every path that can read the screen, including the built-in
screenshot command, not just the Wayland protocol. A session launched by hand
rather than through `ferese-session` needs `FERESE_ENABLE_SCREENCOPY=1` in its
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.
Shell rounding is controlled in Settings → Appearance → Shell corner radius.
The shell and compositor use the same squircle profile for fills, borders, shadows
and blur masks. Small controls cap the radius to fit their size; circles and pills
keep circular outlines. The measured profile has small tangent and curvature
discontinuities at its internal segment joins. Window rounding remains under
Settings → Windows. Shell rounding comes from `theme.geometry.shell-radius`,
which defaults to 14 px. Panel corners follow their selected style and height,
with optional overrides in the [panel configuration](../panel-configuration.md#background-outline-and-corners).
+39
View File
@@ -0,0 +1,39 @@
# Login items and locking
| `autostart` key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `command` | nonempty argument array | required | Foreground process, not a self-daemonizing command |
| `enabled` | boolean | `true` | Start service |
| `restart` | boolean | `true` | Restart after exit, with a five-second retry delay |
| `nested` | boolean | `false` | Also run in nested previews |
Unchanged services keep running on reload; removed/disabled items stop. To lock
after five minutes and before sleep:
```kdl
autostart {
command "swayidle" "-w" "timeout" "300" "ferese-lock" "before-sleep" "ferese-lock" "lock" "ferese-lock"
}
```
The locked display dims after 30 seconds and sleeps after 120 seconds of
inactivity. Settings → Lock screen changes `lock-screen.dim-after-seconds` and
`lock-screen.sleep-after-seconds`; zero disables each action. Input wakes the
display without unlocking. Display sleep does not suspend the computer.
`ferese-lock` is the native PAM-authenticated locker. It follows your wallpaper,
colors, font and shell corner radius. Its command returns successfully only after
the compositor confirms the lock; the UI keeps running until authentication
succeeds. `--foreground` keeps the command attached to its UI process.
Use `ferese-lock --preview` for an ordinary window that never locks or checks a
password. See [Native locker](../locking.md) for setup and verification. Test real
unlock in a nested compositor before enabling automatic locking. A crashed
locker leaves the session locked; recovery requires ending that session from
another TTY.
## 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.
+59
View File
@@ -0,0 +1,59 @@
# Status controls
| `status` key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `keybinding-guide` | boolean | `true` | Show the active shortcut guide at login until disabled in Settings → Keyboard & mouse → Shortcuts. |
| `low-battery-threshold` | integer 0–100 | `20` | Warning-color threshold |
| `settings-command` | argument array | `["ferese-settings"]` | Settings launcher; `[]` hides the action |
Configure the panel in **Panel & Shell** or its KDL block, as described in the
[panel guide](../panel-configuration.md). The panel's appearance applies to every
group, while popup and notification backgrounds follow the shared shell theme.
Transparent gaps between islands pass clicks through to the windows beneath.
## Now Playing
The Media item can show artwork, the track title and a play/pause button wherever
you place it in the panel. Click the artwork or title to open playback controls
for MPRIS players; Morph presents playback in its Music view.
Scroll over the control to change the player's volume, or
middle-click to raise its window when supported. The menu has previous/next
buttons, a seek slider when supported, and a player chooser. It uses the same
colors, transparency and animations as the other bar menus.
Automatic selection prefers the player that most recently started playing.
When none is playing, it selects the most recently active paused player. Players
already running at startup have no known activity order; ties use their bus names.
Stopped players hide the control. Pin a player through the chooser to keep it
selected, or ignore it to exclude it. These choices last until that player leaves
the bus or the media service reconnects. `Choose automatically` clears the pin.
The bar and media keys share one MPRIS monitor. Playback updates come from D-Bus
signals. Progress updates only while the menu is open and playback is running:
once a second, or every three seconds on battery. Album art loads while the
bar or menu is visible, on a worker with download, decode and cache limits. A placeholder
keeps its space while loading or when art is unavailable.
The default Play/Pause, Next and Previous keys use `media-play-pause`,
`media-next` and `media-previous`. These actions take no arguments. Custom
bindings can use them too:
```kdl
binding "Super+P" "media-play-pause"
```
Inspect players or control the selected one from the terminal:
```sh
feresectl media
feresectl media play-pause
feresectl media next
feresectl media previous
feresectl media pin org.mpris.MediaPlayer2.example
feresectl media ignore org.mpris.MediaPlayer2.example
feresectl media unignore org.mpris.MediaPlayer2.example
feresectl media auto
```
`unignore` restores an ignored player even when the bar control is hidden. Apps
without MPRIS support do not appear here.
+69
View File
@@ -0,0 +1,69 @@
# Window rules
Native authentication and display-sharing dialogs float by default. Explicit
window rules can override that behavior.
| `window-rule` key | Type | Default / meaning |
| --- | --- | --- |
| `app-id` | nonempty string | Optional exact match; case-insensitive, `.desktop` suffix ignored |
| `title` | nonempty string | Optional exact, case-sensitive match |
| `transient` | boolean | Optional parent-dialog match |
| `workspace` | integer > 0 | Leave placement unchanged |
| `floating` | boolean | Leave placement unchanged |
| `width`, `height` | numbers > 0 | Application-chosen floating size |
| `min-width`, `min-height` | numbers > 0 | Replaces the client's advertised minimum size for that axis |
| `fullscreen` | boolean | Leave fullscreen state unchanged |
| `block-out-from-screencasts` | boolean | Exclude from captures; enabled by default for Ferese authentication dialogs |
| `idle-inhibit` | `none`, `visible`, `fullscreen`, `playing`, `fullscreen-playing` | Automatic fullscreen playback; see [Idle inhibition](idle-inhibition.md#idle-inhibition) |
`window-rule app-id="spotify" min-width=500 min-height=400` replaces what the
app itself reports as its smallest size, so a configured column width wins over
an app that refuses to shrink (`min-width=1` removes the floor). Each axis is
independent; an omitted axis keeps the client's minimum.
`window-rule app-id="org.example.Private" block-out-from-screencasts=#true`
keeps matching windows visible on the display but omits them (including their
popups, subsurfaces, shadows and overview previews) from monitor and region
captures. The background or windows behind them remain visible in the capture.
Transient children inherit protection even on another output. Direct window
capture fails, ending an active window stream. Custom cursors from a client with
a protected window are also omitted until the cursor image is replaced.
Detached close-animation snapshots are omitted from captures.
This applies to portal sharing, recording, native capture protocols and screenshots:
these routes share capture buffers. The rule is checked against current app-ID,
title and parent metadata, and updates on config reload. Other placement rules
retain their existing lifecycle. Later matching rules may set the field to `#false`,
including for `dev.ferese.Authentication`; a protected parent still protects its
children. A protection change cancels deferred screenshots and window frames.
Frames already delivered to a capture client cannot be recalled.
Every rule needs at least one matcher, and supplied matchers must all match.
Rules apply in order; later fields override earlier ones. Dimensions imply
floating when `floating` is omitted. Floating apps without dimensions choose
their own size. Ferese first tries to center transient windows on the visible
part of their parent, then tries saved geometry, then the position with the least
summed overlap.
Equal-overlap candidates favor the focused window's center. If the size cannot
fit, a per-output cascade advances by 32 logical pixels and wraps to the work-area
origin. Placement respects layer-shell exclusive zones and effective minimum sizes;
an oversized window keeps its size with its top-left corner reachable.
Successful move/resize completion saves ordinary floating geometry by app ID in
`$XDG_STATE_HOME/ferese/floating.json` (normally `~/.local/state/ferese/floating.json`).
Geometry is stored with the output name and as fractions of its work area, so
placement survives resolution changes. Restores are skipped if the output is
missing or less than 25% of the window would be visible. A worker saves the
latest snapshot, combining pending updates. Completing a drag updates memory
immediately without waiting for storage.
Transient dialogs and cancelled drags do not update this memory. Floating → tiled
→ floating also restores the window's last floating geometry. Nested previews
keep drag memory in process and do not write the user's saved placements.
Title changes do not reapply placement rules. Capture privacy and idle inhibition
follow current app ID, title and parent metadata. Config rule changes apply to
existing windows.
```kdl
window-rule app-id="dev.ferese.Settings" floating=#true
```
+46
View File
@@ -0,0 +1,46 @@
# Workspaces
Each monitor has an independent list of workspaces, numbered from 1. Its bar,
overview strip, and numeric shortcuts use that same order. Numbers change when
empty workspaces are removed; internal workspace IDs stay stable.
One empty workspace is always available at the end of each monitor's list.
Opening or moving a window there creates another spare. An empty workspace stays
while displayed or participating in a swipe, then disappears after you leave it.
Selecting a number beyond the current list selects the trailing empty workspace.
Window-rule workspace numbers also refer to positions on the window's monitor.
`Super+Escape` returns to the previous surviving workspace on the focused monitor.
History does not keep abandoned empty workspaces alive. With no previous workspace,
the action does nothing. Disconnecting a monitor clears its history; occupied
workspaces move to another monitor and can return when it reconnects.
Enable **Settings → Windows → Toggle back with the same workspace shortcut**, or:
```kdl
workspaces {
auto-back-and-forth #true
}
```
This defaults to `#false`. When enabled, pressing a `workspace` binding for the
current workspace returns to the previous one. Explicit `feresectl workspace N`
commands and overview selection still select the requested workspace directly.
Use `feresectl workspace-back-and-forth` for an explicit toggle, or bind
`workspace-back-and-forth` to another key or gesture. The action takes no argument.
`Super+BackSpace` focuses the last focused window, including windows on other
workspaces or monitors. Repeating it toggles between the two most recently
focused windows. Layer surfaces such as launchers do not replace this history.
`Alt+Tab` previews windows in focus order using overview. Keep Alt held and press
Tab to advance, or Shift+Tab to go backward. Release Alt to focus the selection;
Escape cancels without changing focus. The order stays fixed until selection is
committed. Closed windows are skipped and newly opened windows join the next
cycle. Preview transitions follow the configured animation speed and reduced
motion settings.
Bind `focus-last-window`, `focus-mru-next`, or `focus-mru-previous` to customize
these shortcuts. MRU bindings with Alt held preview until Alt is released;
bindings without Alt, gestures, and the corresponding `feresectl` commands
switch immediately. All three actions take no arguments.
+89
View File
@@ -0,0 +1,89 @@
# X11 support (xwayland-satellite)
Ferese can run legacy X11-only applications through
[xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite).
```kdl
xwayland {
enabled #true
startup on-demand
path "xwayland-satellite"
}
```
| Key | Values | Default | Meaning |
| --- | --- | --- | --- |
| `enabled` | `true` / `false` | `true` | Run the bridge at all. `false` is a complete opt-out. |
| `startup` | `on-demand` / `eager` | `on-demand` | `on-demand` starts Satellite the first time an application connects to the X11 socket. `eager` starts it during session startup. |
| `path` | path to an executable | `xwayland-satellite` | Which binary to run. |
These keys require a new session. Reloading configuration does not restart or
reallocate the X11 service; `feresectl xwayland status` reports
`restart_required` when the running configuration no longer matches the file.
## Requirements and failure behavior
`xwayland-satellite` and an `Xwayland` binary it can find must be installed.
Satellite must be built with its `systemd` feature enabled
(`cargo build --release --features systemd`); that feature sends the `READY=1`
notification Ferese waits for. Upstream's default feature set is empty, so a
default build never sends it and every start ends in a startup failure after
Ferese's fixed 10-second budget, even though the process is running. Being
spawned is not readiness. If any of these are missing, the session still starts
as a normal Wayland desktop and `feresectl xwayland status` explains why X11 is
unavailable. Ferese never publishes a `DISPLAY` value that does not work, so
applications fail cleanly instead of hanging against an endpoint that accepts
nothing.
In a nested session, Ferese's own environment is preserved for backend
initialization, but applications it launches receive the new Ferese endpoints.
The host's `DISPLAY` is never exported to the activation environment of a nested
session, so it cannot leak into services started by your login session.
## Diagnostics and recovery
```text
feresectl xwayland status
feresectl xwayland retry
```
`status` reports whether the service is idle, starting, running, backing off, or
failed, plus the display, the Satellite PID, and whether readiness was verified.
A failed service is not restarted automatically: Ferese retries a small bounded
number of times, then stops and waits, so a broken binary cannot produce a
restart loop. Run `feresectl xwayland retry` once the cause is fixed. The
snapshot never contains the X11 cookie, so it is safe to paste into a bug
report.
`retry` can also refuse to run. If the previous service group cannot be proven
gone — for example it survived the stop sequence — Ferese keeps its cleanup
record instead of starting a second generation that would compete with
survivors for the same display, and the retry fails with a message naming what
still holds the X11 display. The endpoint is not republished in the meantime.
Run the retry again once that group has finished exiting.
Each start uses a fresh, private notification socket and only a `READY=1`
message from the Satellite process that was actually spawned is accepted, so a
stale or unrelated notification cannot be mistaken for readiness.
## Limitations
- X11 applications are ordinary Wayland windows as far as layout, focus, and
window rules are concerned. Rules match the application identity Satellite
reports, so they are configured the same way as for native clients.
- Native Wayland preferences are kept as they are. Ferese does not force GTK,
Qt, Firefox, or Electron onto X11 to demonstrate support.
- Portal dialogs requested with an `x11:` parent are shown **unparented**. The
chooser or consent prompt still opens and authorization is unchanged; Ferese
does not yet translate an X11 window ID into a Wayland export, and inventing a
parent would be a security problem.
- Per-monitor fractional scaling of X11 applications depends on the selected
Satellite build and its Xsettings policy. Text sharpness, pointer coordinates,
and popup placement should be checked on each output profile rather than
assumed.
- Drag and drop between native and X11 applications depends on the Satellite
build and is not part of the initial support claim.
- Sandboxed applications may need explicit permission to reach the display. A
flatpak with only `fallback-x11` gets no X11 socket inside a Wayland session and
cannot read the private `XAUTHORITY` file under `$XDG_RUNTIME_DIR`, so it exits
without a window while the service itself reports healthy. See
[Sandboxed applications](../installation.md#sandboxed-applications).
+1 -1
View File
@@ -63,7 +63,7 @@ and then undoing migrations to requested outputs that never became usable.
- `DesktopPlan` is pure data: the staged `OutputWorkspaceMap`, reserved workspace
IDs, affected output IDs and focus dirtiness. It copies neither window layouts
nor client buffers nor the whole compositor. Window layout is not duplicated.
nor client buffers nor the whole compositor.
- `DesktopOutput` adapts actual backend resources to that planner. Its Wayland
handles, mode, scale and transform stay in the compositor, outside the pure map.
- `DesktopTransition` holds short-lived resource retirement/redraw bookkeeping and
+8 -8
View File
@@ -15,9 +15,9 @@ 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.
Run CI manually when a change needs it. Ordinary pushes and pull requests do not
start build runners, and website deployment is manual. Release tags and pull
requests labeled `release` run the release checks and package builds.
Workspace checks and website deployment run manually. Documentation changes on
main publish to the wiki automatically. Release tags and pull requests labeled
`release` run the release checks and package builds.
The workspace workflow checks formatting, lints and tests on Ubuntu, including
the shell, Settings, portal and CLI. It builds the desktop binaries, runs the
@@ -29,15 +29,15 @@ host. Hardware checks remain separate.
- [Live Activities checks](#live-activities-checks): core, CLI, shell hooks, installer setup and native rendering.
- [Desktop reconciliation](desktop-reconciliation.md): topology publication, hardware failure and presentation ownership.
- [Output management tests](output-management-testing.md): profiles, hotplug, mirroring, lid handling and failed DRM commits.
- [Nested resize checks](nested-resize-testing.md): wallpaper and bar responsiveness.
- [Client soak testing](native-soak-testing.md): repeated client lifecycle and resource growth.
## Live Activities checks
See [Live Activities](activities.md) for setup, configuration and the publisher
protocol.
See [Live Activities](activities.md) for setup and use,
[Activities configuration](configuration/activities.md) for settings, and
[Publishing activities](development/activities.md) for the IPC protocol.
Hidden items and measurement widgets do not animate. Reduced motion snaps
progress and uses a static track. Workspace buttons retain their own actions;
@@ -133,7 +133,7 @@ cargo test --release --locked -p ferese session_lock::tests -- --nocapture
```
These tests create private runtime directories and Wayland clients. They run in
normal test suites and in the compositor lifecycle CI job, without a host desktop or DRM.
normal test suites and in the workspace CI job, without a host desktop or DRM.
They cover confirmation, rejected unlocks, owner death and replacement, output
removal, keyboard focus and idle timers. Physical display protection still needs
hardware testing.
@@ -146,7 +146,7 @@ cargo test --release --locked -p ferese resume::tests -- --nocapture
These tests need `dbus-daemon`. They use a private bus to test missed resumes,
logind replacement and listener shutdown during connection setup, signal waits
and retries. They run in the compositor lifecycle CI job and never contact system logind.
and retries. They run in the workspace CI job and never contact system logind.
Recovery requests fresh hardware and theme state without resetting idle activity.
Hardware suspend/resume still needs testing on a direct session.
+104
View File
@@ -0,0 +1,104 @@
# Publishing activities
Use the activity IPC to publish progress, timers, media or lists in the panel.
For shell setup and everyday use, see [Live Activities](../activities.md).
The [configuration reference](../configuration/activities.md) covers settings and rules.
## Publish your own status
Run these commands from the same shell. The ID `download` belongs to that shell,
so another terminal cannot update it:
```sh
feresectl activity start download --source download --icon download \
--title "Downloading update" --progress 0.2
sleep 2
feresectl activity update download --progress 0.7
feresectl activity end download --result ok --message "Downloaded"
```
This example publishes status only; it doesn't perform a download. Your program
supplies real progress and completion results.
## Publisher ownership and lifecycle
Use the same socket selection as every other control command: `--socket`, then
`FERESE_SOCKET`, then the session runtime default. Separate nested sessions keep
separate brokers. One-shot commands use their verified immediate parent (normally
the shell), not the CLI connection, as lifetime owner. IDs are local to that owner.
A direct publisher and `run` use the publishing process itself.
The host verifies ownership using kernel peer credentials and a socket-bound pidfd. It checks the parent relationship, process birth and UID, and monitors
owner exit. This requires Linux 6.5 or newer; an unverifiable owner is rejected
instead of trusting a JSON PID.
```sh
feresectl activity start build --icon hammer --title "Build"
feresectl activity update build --progress 0.6
feresectl activity end build --result ok --message "Built"
feresectl activity start tea --icon timer --title "Tea" --remaining-ms 180000
feresectl run --title "cargo test" -- cargo test
```
`start` prints a JSON handle containing owner, session and generation. The
`--handle` option on update/end/cancel checks that exact invocation. Omitting it
resolves the local ID only within the same verified owner. Duplicate starts are
rejected. Repeated identical accepted results keep the original result deadline;
a removed or superseded generation is stale. Register again after broker restart.
The compositor assigns a terminal window at activity creation using the
publisher's process ancestry. List selection follows the activity ID across
reordering. Recording and low battery keep their protected priority, including
the combined recording/low-battery presentation.
### Limits and delivery
Each owner may hold three records, including delayed and terminal entries. The
session record, priority and compact-slot limits follow [Activities configuration](../configuration/activities.md).
New publications are limited to ten per owner per second. Replaceable updates
are limited to 30 per owner per second; rejected updates get
an explicit error.
The bar keeps only the latest snapshot in its bounded subscription; the broker
retains accepted results. Terminal and cancel requests wait for bounded
dispatcher capacity on the socket worker. A CLI disconnect does not remove an
owner's records.
### Expiry and output changes
Ferese removes running activities without a result when their owner exits or
their lease expires.
Results survive owner exit until their original deadline. Leases are positive and
at most five minutes; valid refreshes renew only the lease.
Activities stay on their creation output, initially the focused output. On output
removal they move to the first surviving output, or become unpresented until an
output is available. Host timestamps use `CLOCK_BOOTTIME`, including suspend.
Countdown wire timestamps are ignored and replaced with host sample time.
## Extensions and templates
The version-1 activity schema is carried inside the existing versioned,
length-prefixed JSON `activity-publish` command. `activity-get` and long-poll
`activity-watch` provide snapshots. See [`ferese_ipc::activities`](../../crates/ferese-ipc/src/activities.rs)
and [`ferese_activities::Data`](../../crates/ferese-activities/src/lib.rs); there is no
second socket or JSON-lines protocol.
Extensions publish typed Progress, Timer, Media, or List data. The bar controls
rendering. Values are indeterminate, finite progress in `[0,1]`, bounded text, or
countdown durations. Titles/text are limited to 256 UTF-8 bytes; lists to eight
rows. Icons are symbolic names; unknown names use the Ferese symbol, and paths
are rejected. No extension widgets, markup, colors, shaders or drawing programs
are accepted.
Use `--template` with a typed JSON template, for example:
`'{"kind":"list","data":{"rows":["Indexing files","Waiting for server"]}}'`.
Capture/call integrations use `--source capture` and describe the state they
actually observe. Process names, microphone use, or camera use do not infer calls
or downloads. Existing MPRIS state supplies Media rows and transport controls;
recording/battery reuse the shell's existing protected state and watchers.
The optional notification bridge uses existing notification state, ranks below
all other ordinary sources, and respects DND and popup privacy. When enabled, a
notice becomes an activity rather than a duplicate toast. It is off by default.
+2 -2
View File
@@ -88,7 +88,7 @@ xwayland {
```
Start a new session, then check `feresectl xwayland status`. See
[X11 support](configuration.md#x11-support-xwayland-satellite) for the full key
[X11 support](configuration/xwayland.md#x11-support-xwayland-satellite) for the full key
list, diagnostics, and known limitations.
#### Sandboxed applications
@@ -342,7 +342,7 @@ Installer options:
`--bundle` replaces `--skip-build` and installs a complete bundle instead of loose
files in `target/release/`. Build options cannot be combined with `--bundle`.
The front-end dry run prints commands. To inspect the filesystem changes planned
The `install.sh` dry run prints commands. To inspect the filesystem changes planned
for an existing bundle, run:
```sh
+6 -6
View File
@@ -1,9 +1,5 @@
# feresectl and IPC events
`feresectl keybindings` lists the active binding mode's shortcuts. Outside a
mode it lists the configured default shortcuts. Escape is always available to
leave a mode; see [Binding modes](configuration.md#binding-modes).
`feresectl` controls a running Ferese session. Run `feresectl --help` for commands
and `feresectl COMMAND --help` for arguments.
@@ -27,6 +23,10 @@ feresectl media
feresectl activity list
```
`feresectl keybindings` lists the active binding mode's shortcuts. Outside a
mode it lists the configured default shortcuts. Escape is always available to
leave a mode; see [Binding modes](configuration/bindings.md#binding-modes).
The `get-*` spellings remain hidden aliases for the corresponding queries and
print a deprecation warning on stderr. They use the same output rules: scripts
that previously parsed their JSON must add `-j`. New scripts should use the
@@ -59,7 +59,7 @@ these actions.
`toggle-column-tabbed` changes a scrolling column between stacked rows and tabs.
In either layout, `join-tab left|right|up|down` joins the focused window to a
neighbouring tab group. `next-tab` and `previous-tab` switch tabs, and `leave-tab`
splits the focused tab out. See [Commands and bindings](configuration.md#commands-and-bindings)
splits the focused tab out. See [Tabbed windows](tabbed-windows.md)
for grouping behavior and shortcuts.
## Live Activities
@@ -67,7 +67,7 @@ for grouping behavior and shortcuts.
Use `feresectl run --title "Tests" -- cargo test` to publish a command's lifetime,
or `feresectl activity start`, `update`, `end` and `cancel` to publish status from
an integration. The [Live Activities guide](activities.md) covers setup and
examples; its [publisher section](activities.md#publisher-ownership-and-lifecycle) explains ownership,
examples; the [publishing guide](development/activities.md#publisher-ownership-and-lifecycle) explains ownership,
leases and the `activity-publish`, `activity-get` and `activity-watch` commands.
`feresectl activity setup --config PATH` updates a local config for shell
+5 -4
View File
@@ -128,7 +128,7 @@ The panel uses the shared material settings under `theme.material`, including
translucency and backdrop blur. Morph uses the theme's `surface-raised` color
and suppresses the material shadow. **Reduce Transparency** takes precedence
over a panel opacity override, making the background opaque and removing blur.
See [Appearance](configuration.md#appearance) for the shared theme controls.
See [Appearance](configuration/appearance.md#appearance) for the shared theme controls.
Set `border #true` to draw the panel outline. Its color comes from
`theme.colors.border`, and Capsules also uses that color for its dividers.
@@ -187,7 +187,7 @@ group a separate height, style or set of corners.
### Item catalog
The Add item catalog contains these twelve kinds. Service-dependent controls
The Add item catalog contains these built-in kinds. Service-dependent controls
appear when their service or hardware is available, and a focused-window item
disappears when that output has no window title to show.
@@ -197,6 +197,7 @@ disappears when that output has no window title to show.
| Workspaces | `workspaces` | `style`: `numbers`, `dots`, `tabs`, `window-stacks`, `app-icons`; default `dots` |
| Focused window | `focused-window` | Use the shared `visible` field to show or hide the title |
| Media | `media` | Full, Compact and Icon representations |
| Activities | `activities` | Full, Compact and Icon representations; see [Activities configuration](configuration/activities.md) |
| Quick Settings | `quick-settings` | Opens Control Center |
| Network | `network` | Opens Wi-Fi controls |
| Audio | `audio` | Opens audio controls |
@@ -224,7 +225,7 @@ item "desk-clock" kind="clock" {
### Adaptation and overflow
In the inspector, **Full**, **Compact** and **Icon** correspond to `wide`,
`compact` and `icon`. Media and Clock support all three; Battery with percentage
`compact` and `icon`. Activities, Media and Clock support all three; Battery with percentage
supports Full and Icon. Workspaces and Focused window have one form that can
shrink, while other controls use icons. An unsupported preference falls back
to the item's normal form.
@@ -254,7 +255,7 @@ Notifications or Controls view. Escape or the close button collapses it.
With an Activities item configured, running tasks can occupy the collapsed and
peek states, and the hub gains an Activities tab. **Visible activities** and
**Show activity count** in Appearance control its compact slots and `+n` badge;
see [Activities configuration](activities.md#configure-activities).
see [Activities configuration](configuration/activities.md).
The Workspaces view shows miniature window layouts. Click a tile to switch
workspace, use Tab and the arrow keys to navigate it, or drag a miniature window
+3 -3
View File
@@ -65,7 +65,7 @@ switches tabs; the usual left/right focus keys still switch columns.
In tree mode, **Super + [** joins the focused window to a neighbouring window
as a tab. Click a title in the tab bar or use **Super + Ctrl + [ / ]** to switch
tabs. **Super + ]** splits the focused tab out beside the group. For a specific
neighbour, bind `join-tab` with a direction; see [Commands and bindings](configuration.md#commands-and-bindings).
neighbour, bind `join-tab` with a direction; see [Tabbed windows](tabbed-windows.md).
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
@@ -110,7 +110,7 @@ Keep before the confirmation timeout or Revert to restore the last confirmed
configuration. Bind `toggle-display-mode` if your keyboard uses another key.
In Satty, Enter saves the edited screenshot and copies it to the clipboard;
Escape discards it. See [Configuration](configuration.md#commands-and-bindings)
Escape discards it. See [Configuration](configuration/bindings.md#commands-and-bindings)
for dependencies and custom screenshot commands.
## Touchpad gestures
@@ -176,7 +176,7 @@ These are examples, not defaults. To disable a built-in binding:
binding "Super+Q" disabled=#true
```
See [Commands and bindings](configuration.md#commands-and-bindings) for action
See [Commands and bindings](configuration/bindings.md#commands-and-bindings) for action
names, arguments, and physical-key matching.
In scrolling columns, hover over the left-edge tab indicator for window titles.
+77
View File
@@ -0,0 +1,77 @@
# Tabbed windows
Tabs keep several windows in the same space, with one visible at a time.
## Create a group
In scrolling layout, press **Super + [** to put the focused window in the column
to its left, or the one to its right if there is no left neighbour. Press
**Super + Shift + T** to turn that column into tabs. Press it again to return to
stacked rows with their previous sizes.
In tree layout, **Super + [** joins the focused window to a neighbour as a tab.
Ferese looks left, right, up, then down for a window to join.
If you want to choose the direction, bind `join-tab` to `left`, `right`, `up`, or
`down`; see [Commands and bindings](configuration/bindings.md). In scrolling
layout, this also turns the destination column into tabs, even when the two
windows are already in the same column. The ordinary `consume` action keeps the
destination column's stacked or tabbed choice.
## Switch tabs
| Shortcut | Action |
| --- | --- |
| Super + Ctrl + [ | Previous tab |
| Super + Ctrl + ] | Next tab |
| Super + J / K | Next / previous row or tab in a scrolling column |
| Super + H / L | Previous / next scrolling column |
The previous/next tab shortcuts wrap at either end of the group. Switching tabs
in a scrolling column keeps its width and scroll position. Vertical resizing
has no effect while the column is tabbed.
In tree layout, click a title in the bar above the window to select it.
Scrolling columns have a segmented rail on the left: click a segment, or hover
over the rail to see a list of titles. You can click a title in that list too.
The list stays open as you move between it and the rail. Scroll through it when
there are too many titles to fit on screen.
Keyboard tab switching briefly shows the selected tab's number and title near
the top of the window, as `n/N · title`.
## Take a window out
Press **Super + ]** to move the focused tab into its own column or split, to the
right of the group. You can also use the `leave-tab` action. Closing a window,
making it floating, or moving it to another workspace removes it from the group.
New windows open beside the group.
Tab controls disappear when only one window remains, or when the selected window
is maximized or fullscreen. Leave fullscreen before grouping or switching tabs;
fullscreen belongs to the individual window. Switching between tree and scrolling
layouts keeps all windows but separates the groups.
## Indicator and appearance
The scrolling rail and title list sit over the desktop without taking space from
the window. By default, the rail sits in the left gap if there are at least 8
logical pixels available; otherwise it sits just inside the window. Its click
area is wider than the visible rail. On short windows, the segments shrink to fit.
The rail uses the theme's accent for the selected tab. The title list follows the
shell's colors, corner radius and opacity. To change its size, colors or keyboard
feedback duration, see [Layout configuration](configuration/layout.md#tabbed-columns).
That page also shows how to open new columns as tabs by default.
## feresectl
These commands work on the focused window:
```sh
feresectl toggle-column-tabbed
feresectl join-tab left
feresectl next-tab
feresectl previous-tab
feresectl leave-tab
```
+2 -2
View File
@@ -1,6 +1,6 @@
# Why Ferese
Alongside a window manager, you need Wi-Fi, audio and display controls, notifications and a lock screen. Choosing separate desktop components remains useful when you want that independence, though keeping them consistent becomes another job when their settings and behaviour are maintained separately.
Alongside a window manager, you need Wi-Fi, audio and display controls, notifications and a lock screen. You can choose these components separately, but keeping their settings and behaviour consistent takes extra work.
Ferese includes a Wayland compositor, desktop shell, Settings, Control Center and a lock screen with shared configuration and theming. It keeps window management configurable while defining how a desktop change applies across those components.
@@ -34,4 +34,4 @@ A window’s content, clipping and border need to follow the same presented boun
## Testing the whole interaction
A shared theme does not resolve a late application buffer, and a correct spring equation does not make a missed frame arrive on time. Resize coordination, output timing and interrupted transitions still need testing and measurements on the hardware where the desktop runs.
Resize coordination, output timing and interrupted transitions need testing and measurements on the hardware where the desktop runs.
+44 -159
View File
@@ -1,172 +1,57 @@
#!/usr/bin/env python3
"""Build Ferese's static website and handbook from the project Markdown docs."""
from pathlib import Path
"""Build the Ferese homepage and redirect former handbook URLs to the wiki."""
from html import escape
import importlib.util
import json
import re
from pathlib import Path
import shutil
from urllib.parse import urlsplit
from markdown_it import MarkdownIt
ROOT = Path(__file__).resolve().parent.parent
OUTPUT = ROOT / 'build' / 'site'
REPO = 'https://github.com/ferese-wm/ferese'
PAGES = [
('index', 'Introduction', ROOT / 'site/content/index.md'),
('screenshots', 'Screenshots', ROOT / 'docs/screenshots.md'),
('why-ferese', 'Why Ferese', ROOT / 'docs/why-ferese.md'),
('installation', 'Installation', ROOT / 'docs/installation.md'),
('packaging', 'Packages and releases', ROOT / 'docs/packaging.md'),
('configuration', 'Configuration', ROOT / 'docs/configuration.md'),
('panel-configuration', 'Panel configuration', ROOT / 'docs/panel-configuration.md'),
('animation-model', 'Animation model', ROOT / 'docs/animation-model.md'),
('shortcuts', 'Shortcuts and gestures', ROOT / 'docs/shortcuts.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')
if OUTPUT.exists():
shutil.rmtree(OUTPUT)
OUTPUT.mkdir(parents=True)
for name in ('index.html', 'home.css', 'handbook.css', 'styles.css', 'app.js'):
shutil.copy2(ROOT / 'site' / name, OUTPUT / name)
(OUTPUT / 'assets').mkdir(exist_ok=True)
shutil.copytree(ROOT / 'site/assets', OUTPUT / 'assets', dirs_exist_ok=True)
shutil.copytree(ROOT / 'docs/images', OUTPUT / 'assets', dirs_exist_ok=True)
shutil.copytree(ROOT / 'assets/branding', OUTPUT / 'assets/branding')
(OUTPUT / 'docs').mkdir(exist_ok=True)
(OUTPUT / '.nojekyll').touch()
search_index = []
OUTPUT = ROOT / 'build/site'
spec = importlib.util.spec_from_file_location('wiki', ROOT / 'scripts/build-wiki.py')
wiki = importlib.util.module_from_spec(spec)
spec.loader.exec_module(wiki)
def rewrite_link(href, source):
url = urlsplit(href)
if url.scheme or url.netloc or not url.path:
return href
basename = Path(url.path).stem
if url.path.endswith('.md') and basename in {page[0] for page in PAGES}:
return f'{basename}.html' + (f'#{url.fragment}' if url.fragment else '')
path = (source.parent / url.path).resolve()
if path.is_relative_to(ROOT / 'docs/images'):
return '../assets/' + path.relative_to(ROOT / 'docs/images').as_posix()
destination = path.relative_to(ROOT)
return f'{REPO}/blob/main/{destination}' + (f'#{url.fragment}' if url.fragment else '')
for page_number, (slug, title, source) in enumerate(PAGES):
tokens = md.parse(source.read_text())
toc, ids, sections = [], set(), []
current = None
for i, token in enumerate(tokens):
if token.type == 'heading_open':
heading = tokens[i + 1].content
base = re.sub(r'[^\w\s-]', '', heading.lower()).strip()
base = re.sub(r'[\s_]+', '-', base)
anchor = base
suffix = 1
while anchor in ids:
anchor = f'{base}-{suffix}'
suffix += 1
ids.add(anchor)
token.attrSet('id', anchor)
if token.tag != 'h1':
toc.append((anchor, heading))
current = {'page': title, 'heading': heading, 'url': f'{slug}.html#{anchor}', 'text': ''}
sections.append(current)
elif current and token.type in ('inline', 'fence', 'code_block'):
current['text'] += token.content + ' '
for child in token.children or []:
if child.type == 'link_open':
child.attrSet('href', rewrite_link(child.attrGet('href'), source))
elif child.type == 'image':
image = urlsplit(child.attrGet('src'))
if not image.scheme and not image.netloc:
path = (source.parent / image.path).resolve()
relative = path.relative_to(ROOT / 'docs/images')
if not path.is_file():
raise FileNotFoundError(path)
child.attrSet('src', f'../assets/{relative.as_posix()}')
child.attrSet('loading', 'lazy')
child.attrSet('decoding', 'async')
search_index.extend(sections)
body = md.renderer.render(tokens, md.options, {})
body = body.replace('<table>', '<div class="table-scroll" tabindex="0" role="region" aria-label="Reference table"><table>').replace('</table>', '</table></div>')
navigation = ''.join(f'<a href="{s}.html"' + (' aria-current="page"' if s == slug else '') + f'>{escape(t)}</a>' for s, t, _ in PAGES)
contents = ''.join(f'<a href="#{anchor}">{escape(heading)}</a>' for anchor, heading in toc)
pager = ''
if page_number:
prev = PAGES[page_number - 1]
pager += f'<a class="pager-previous" href="{prev[0]}.html"><small>Previous</small>← {escape(prev[1])}</a>'
if page_number + 1 < len(PAGES):
nxt = PAGES[page_number + 1]
pager += f'<a class="pager-next" href="{nxt[0]}.html"><small>Next</small>{escape(nxt[1])} →</a>'
mobile_contents = (
f'<details class="mobile-contents"><summary>On this page</summary><nav aria-label="Page sections">{contents}</nav></details>'
if contents else ''
)
doc = f'''<!doctype html>
def redirect_page(destination):
return f'''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#111821">
<title>{escape(title)} — Ferese handbook</title>
<meta name="description" content="{escape(title)} in the Ferese handbook: setup, configuration, and guides for your Wayland desktop.">
<link rel="icon" href="../assets/branding/ferese.svg" type="image/svg+xml">
<link rel="icon" href="../assets/favicon.ico" sizes="16x16 32x32 48x48">
<link rel="icon" href="../assets/favicon-32.png" type="image/png" sizes="32x32">
<link rel="stylesheet" href="../styles.css">
<link rel="stylesheet" href="../handbook.css">
<script src="../app.js" defer></script>
<title>Ferese documentation</title>
<link rel="canonical" href="{escape(destination, quote=True)}">
<meta http-equiv="refresh" content="0;url={escape(destination, quote=True)}">
<script>location.replace({json.dumps(destination)} + location.hash);</script>
</head>
<body class="docs-page">
<svg class="icon-definitions" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><defs>
<symbol id="i-arrow" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></symbol>
<symbol id="i-external" viewBox="0 0 24 24"><path d="M7 17 17 7M7 7h10v10"/></symbol>
</defs></svg>
<a class="skip" href="#main">Skip to content</a>
<header class="site-header">
<div class="header-inner page-width">
<a class="brand" href="../" aria-label="Ferese home"><img src="../assets/branding/ferese.svg" width="34" height="34" alt=""><span>Ferese</span></a>
<nav aria-label="Main navigation"><a href="../#make-room">The desktop</a><a href="../#appearance">Appearance</a><a href="./" aria-current="page">Handbook</a></nav>
<div class="header-actions"><a class="small-button" href="installation.html">Get Ferese</a></div>
</div>
</header>
<div class="docs-layout page-width">
<aside class="docs-sidebar" aria-label="Handbook navigation">
<div class="docs-search" hidden>
<label class="sr-only" for="docs-search">Search the handbook</label>
<input id="docs-search" type="search" placeholder="Search the handbook…" autocomplete="off" aria-controls="search-results">
<div class="search-results" id="search-results" hidden></div>
<p class="sr-only" id="search-status" role="status"></p>
</div>
<details class="handbook-navigation" open>
<summary>Browse the handbook <svg class="icon" aria-hidden="true"><use href="#i-arrow"/></svg></summary>
<nav aria-label="Documentation">{navigation}</nav>
</details>
<div class="sidebar-links">
<a href="{REPO}/issues">Report an issue <svg class="icon" aria-hidden="true"><use href="#i-external"/></svg></a>
<a href="../">Back to Ferese</a>
</div>
</aside>
<main class="docs-article" id="main">
<div class="breadcrumb"><a href="./">Handbook</a><span aria-hidden="true">/</span><span>{escape(title)}</span></div>
{mobile_contents}
<article class="prose">{body}</article>
<a class="edit-link" href="{REPO}/blob/main/{source.relative_to(ROOT)}">View this guide on GitHub <svg class="icon" aria-hidden="true"><use href="#i-external"/></svg></a>
<nav class="docs-pager" aria-label="Previous and next guide">{pager}</nav>
</main>
<aside class="docs-toc" aria-label="On this page">{'<p>On this page</p><nav>' + contents + '</nav>' if contents else ''}</aside>
</div>
<footer class="site-footer page-width">
<a class="brand" href="../" aria-label="Ferese home"><img src="../assets/branding/ferese.svg" width="28" height="28" alt=""><span>Ferese</span></a>
<span>Built in the open for Linux.</span>
<nav aria-label="Footer navigation"><a href="./">Handbook</a><a href="{REPO}">GitHub <svg class="icon" aria-hidden="true"><use href="#i-external"/></svg></a></nav>
</footer>
</body>
</html>'''
(OUTPUT / 'docs' / f'{slug}.html').write_text(doc)
(OUTPUT / 'docs/search-index.json').write_text(json.dumps(search_index, ensure_ascii=False))
print(f'Built website and {len(PAGES)} handbook pages at {OUTPUT}')
<body><p>This guide is on the <a href="{escape(destination, quote=True)}">Ferese wiki</a>.</p></body>
</html>
'''
def main():
if OUTPUT.exists():
shutil.rmtree(OUTPUT)
OUTPUT.mkdir(parents=True)
for name in ('index.html', 'home.css', 'styles.css'):
shutil.copy2(ROOT / 'site' / name, OUTPUT / name)
(OUTPUT / 'assets').mkdir(exist_ok=True)
shutil.copytree(ROOT / 'site/assets', OUTPUT / 'assets', dirs_exist_ok=True)
shutil.copytree(ROOT / 'docs/images', OUTPUT / 'assets', dirs_exist_ok=True)
shutil.copytree(ROOT / 'assets/branding', OUTPUT / 'assets/branding')
(OUTPUT / '.nojekyll').touch()
handbook = OUTPUT / 'docs'
handbook.mkdir()
(handbook / 'index.html').write_text(redirect_page(wiki.WIKI))
for source in wiki.guide_sources():
if not source.is_relative_to(ROOT / 'docs') or source.stem == 'README':
continue
destination = wiki.WIKI + '/' + wiki.page_name(source)
output = handbook / source.relative_to(ROOT / 'docs').with_suffix('.html')
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(redirect_page(destination))
print(f'Built homepage and documentation redirects at {OUTPUT}')
if __name__ == '__main__':
main()
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""Publish repository guides as GitHub Wiki Markdown, preserving their prose."""
import argparse
import json
from pathlib import Path
import re
import subprocess
from urllib.parse import unquote, urlsplit
from markdown_it import MarkdownIt
ROOT = Path(__file__).resolve().parent.parent
REPO = "https://github.com/ferese-wm/ferese"
WIKI = REPO + "/wiki"
MANIFEST = ".ferese-pages.json"
MD = MarkdownIt("commonmark").enable("table")
NAMES = {
"README": "Home",
"activities": "Live-Activities",
"ipc": "IPC",
"locking": "Lock-Screen",
"packaging": "Packages-and-Releases",
"portals": "Desktop-Portals",
"screen-sharing": "Screen-Sharing-and-Recording",
"shortcuts": "Shortcuts-and-Gestures",
}
ALIASES = {
"Appearance": [("Configuration-Appearance", "", "Appearance")],
"Desktop-Widgets": [("Configuration", "", "Configuration")],
"Displays": [("Configuration-Outputs", "", "Displays")],
"Input-and-Bindings": [
("Configuration-Input", "", "Input"),
("Configuration-Bindings", "", "Commands and bindings"),
],
"Layout-and-Motion": [
("Configuration-Layout", "", "Layout"),
("Configuration-Animations", "", "Animation configuration"),
("Animation-Model", "", "Animation model"),
],
"Services-and-Locking": [
("Configuration-Startup-And-Locking", "", "Login items"),
("Lock-Screen", "", "Lock screen"),
("Desktop-Portals", "", "Desktop portals"),
],
"Window-Rules": [("Configuration-Window-Rules", "", "Window rules")],
}
# Inline links/images and reference definitions. Only destinations recognized by
# the Markdown parser are rewritten; examples in code blocks remain untouched.
LINK = re.compile(r"(?P<code>(?P<ticks>`+).*?(?P=ticks))|(?P<prefix>\]\(|^ {0,3}\[[^]\n]+\]:\s*)(?P<url><[^>\n]+>|[^\s)]+)", re.MULTILINE)
def page_name(source):
if source == ROOT / "site/README.md":
return "Website-Development"
if source == ROOT / "crates/ferese-theme/README.md":
return "Theme-Components"
if source.parent == ROOT / "docs/development":
return "Development-" + "-".join(word.capitalize() for word in source.stem.split("-"))
if source.parent == ROOT / "docs/configuration":
return "Configuration-" + "-".join(word.capitalize() for word in source.stem.split("-"))
return NAMES.get(source.stem, "-".join(word.capitalize() for word in source.stem.split("-")))
def heading_ids(text):
counts, result = {}, set()
tokens = MD.parse(text)
for index, token in enumerate(tokens):
if token.type != "heading_open":
continue
children = tokens[index + 1].children or []
title = "".join(child.content for child in children if child.type in ("text", "code_inline", "image"))
base = re.sub(r"[^\w\s-]", "", title.lower()).replace(" ", "-")
count = counts.get(base, 0)
anchor = base if count == 0 else f"{base}-{count}"
while anchor in result:
count += 1
anchor = f"{base}-{count}"
counts[base] = count + 1
result.add(anchor)
return result
def destinations(text):
return {
child.attrGet("href" if child.type == "link_open" else "src")
for token in MD.parse(text)
for child in token.children or []
if child.type in ("link_open", "image")
}
def rewrite(text, source, pages, revision):
replacements = {}
for href in destinations(text):
url = urlsplit(href)
if url.scheme or url.netloc:
continue
target = (source.parent / unquote(url.path)).resolve() if url.path else source
if url.path and (not target.is_relative_to(ROOT) or not target.is_file()):
raise ValueError(f"{source.relative_to(ROOT)}: missing link target {href}")
if target in pages:
if url.fragment and unquote(url.fragment) not in heading_ids(target.read_text()):
raise ValueError(f"{source.relative_to(ROOT)}: missing heading {href}")
destination = WIKI + "/" + pages[target]
elif target.suffix == ".md":
raise ValueError(f"{source.relative_to(ROOT)}: unpublished guide {href}")
else:
relative = target.relative_to(ROOT).as_posix()
destination = f"{REPO}/raw/{revision}/{relative}" if target.suffix in (".png", ".jpg", ".webp", ".svg") else f"{REPO}/blob/{revision}/{relative}"
if not url.path:
destination = ""
if url.query:
destination += "?" + url.query
if url.fragment:
destination += "#" + url.fragment
replacements[href] = destination
def replace(match):
if match["code"] is not None:
return match[0]
raw = match["url"]
angled = raw.startswith("<")
href = raw[1:-1] if angled else raw
if href not in replacements:
return match[0]
replacement = replacements[href]
return match["prefix"] + ("<" + replacement + ">" if angled else replacement)
# Use parser line ranges to protect both fenced and indented code blocks.
lines = text.splitlines(keepends=True)
protected = set()
for token in MD.parse(text):
if token.type in ("fence", "code_block"):
protected.update(range(*token.map))
return "".join(line if number in protected else LINK.sub(replace, line) for number, line in enumerate(lines))
def guide_sources():
return (sorted((ROOT / "docs").glob("*.md"))
+ sorted((ROOT / "docs/configuration").glob("*.md"))
+ sorted((ROOT / "docs/development").glob("*.md"))
+ [ROOT / "site/README.md", ROOT / "crates/ferese-theme/README.md"])
def render(revision):
sources = guide_sources()
pages = {source.resolve(): page_name(source) for source in sources}
if len(set(pages.values())) != len(pages):
raise ValueError("Wiki page names must be unique")
output = {}
for source, name in pages.items():
text = rewrite(source.read_text(), source, pages, revision)
output[name + ".md"] = text
for alias, links in ALIASES.items():
targets = []
for name, anchor, title in links:
if name + ".md" not in output:
raise ValueError(f"Alias points to absent page {name}")
if anchor and anchor not in heading_ids(output[name + ".md"]):
raise ValueError(f"Alias points to absent heading {name}#{anchor}")
targets.append(f"- [{title}]({WIKI}/{name}" + ("#" + anchor if anchor else "") + ")")
output[alias + ".md"] = "# " + alias.replace("-", " ") + "\n\nSee the current guides:\n\n" + "\n".join(targets) + "\n"
home = output["Home.md"]
# The documentation index is also the navigation source. Keep descriptions
# on Home, and use its grouped link list for the Wiki sidebar.
sidebar = [f"[Home]({WIKI})"]
for line in home.splitlines():
if line.startswith("## "):
sidebar.extend(["", line, ""])
elif line.startswith("- "):
link = re.match(r"- \[[^]]+\]\([^)]+\)", line)
if link:
sidebar.append(link[0])
output["_Sidebar.md"] = "\n".join(sidebar) + "\n"
output["Home.md"] += f"\nEdit the guide sources in the [main repository]({REPO}/tree/main/docs).\n"
return output
def write_pages(output, destination):
destination.mkdir(parents=True, exist_ok=True)
manifest = destination / MANIFEST
previous = json.loads(manifest.read_text()) if manifest.exists() else []
if not isinstance(previous, list) or any(not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9_-]+\.md", name) for name in previous):
raise ValueError("Invalid wiki page manifest")
for name in previous:
if name not in output:
(destination / name).unlink(missing_ok=True)
for name, text in output.items():
(destination / name).write_text(text)
manifest.write_text(json.dumps(sorted(output), indent=2) + "\n")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, default=ROOT / "build/wiki")
args = parser.parse_args()
destination = args.output.resolve()
if destination == ROOT or destination.is_relative_to(ROOT / "docs"):
parser.error("output must not overwrite documentation sources")
revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
output = render(revision) # Validate every guide before touching destination.
write_pages(output, destination)
print(f"Built {len(output)} wiki pages at {destination}")
if __name__ == "__main__":
main()
+271
View File
@@ -0,0 +1,271 @@
"""Wiki publication must preserve examples and resolve links before writing."""
import importlib.util
import json
import re
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[2]
PARSER_AVAILABLE = importlib.util.find_spec('markdown_it') is not None
if PARSER_AVAILABLE:
spec = importlib.util.spec_from_file_location('wiki', ROOT / 'scripts/build-wiki.py')
wiki = importlib.util.module_from_spec(spec)
spec.loader.exec_module(wiki)
@unittest.skipUnless(PARSER_AVAILABLE, 'Install site/requirements.txt for wiki checks')
class WikiTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
(self.root / 'docs').mkdir()
self.source = self.root / 'docs/intro.md'
self.source.write_text('# Intro\n\n## Local heading\n')
self.config = self.root / 'docs/configuration.md'
self.config.write_text('# Configuration\n\n## With `code` & punctuation!\n\n## Again\n\n## Again\n')
self.pages = {self.source: 'Intro', self.config: 'Configuration'}
self.patch = mock.patch.object(wiki, 'ROOT', self.root)
self.patch.start()
self.addCleanup(self.patch.stop)
def rewrite(self, text):
return wiki.rewrite(text, self.source, self.pages, 'deadbeef')
def test_guide_links_and_duplicate_heading_anchors(self):
text = '[Reference](configuration.md#with-code--punctuation) and [again](configuration.md#again-1).\n'
result = self.rewrite(text)
self.assertIn(wiki.WIKI + '/Configuration#with-code--punctuation', result)
self.assertIn(wiki.WIKI + '/Configuration#again-1', result)
def test_local_anchor_and_external_link_unchanged(self):
text = '[Here](#local-heading) and [Elsewhere](https://example.org/a?q=1#fragment).\n'
self.assertEqual(self.rewrite(text), text)
def test_source_files_and_images_are_pinned_to_source_commit(self):
(self.root / 'docs/image.webp').write_bytes(b'image')
(self.root / 'example.kdl').write_text('layout {}')
result = self.rewrite('![Desktop](image.webp) [Example](../example.kdl)\n')
self.assertIn(wiki.REPO + '/raw/deadbeef/docs/image.webp', result)
self.assertIn(wiki.REPO + '/blob/deadbeef/example.kdl', result)
def test_fenced_indented_and_inline_code_are_preserved(self):
example = '[Config](configuration.md)'
text = f'{example}\n\n```md\n{example}\n```\n\n {example}\n\nUse `{example}` as an example.\n'
result = self.rewrite(text)
self.assertIn(wiki.WIKI + '/Configuration', result)
self.assertIn(f'```md\n{example}\n```', result)
self.assertIn(f' {example}', result)
self.assertIn(f'`{example}`', result)
def test_reference_links_and_angle_destinations(self):
result = self.rewrite('[Config][ref]\n\n[ref]: <configuration.md> "Title"\n')
self.assertIn('[ref]: <' + wiki.WIKI + '/Configuration> "Title"', result)
def test_missing_file_missing_anchor_and_unpublished_markdown_fail(self):
for target in ('missing.md', 'configuration.md#missing'):
with self.subTest(target=target), self.assertRaises(ValueError):
self.rewrite('[Missing](' + target + ')')
(self.root / 'unpublished.md').write_text('# Unpublished')
with self.assertRaises(ValueError):
self.rewrite('[Missing](../unpublished.md)')
def test_failed_validation_does_not_replace_any_destination_page(self):
destination = self.root / 'published'
destination.mkdir()
(destination / 'Home.md').write_text('Previously published')
with mock.patch.object(sys, 'argv', ['build-wiki.py', '--output', str(destination)]), mock.patch.object(wiki, 'render', side_effect=ValueError('bad link')), mock.patch.object(wiki.subprocess, 'check_output', return_value='deadbeef'):
with self.assertRaises(ValueError):
wiki.main()
self.assertEqual((destination / 'Home.md').read_text(), 'Previously published')
self.assertEqual(list(destination.iterdir()), [destination / 'Home.md'])
def test_stale_generated_pages_removed_and_manual_pages_preserved(self):
destination = self.root / 'published'
wiki.write_pages({'Old.md': '# Old', 'Home.md': '# Home'}, destination)
(destination / 'Manual.md').write_text('Manual page')
wiki.write_pages({'Home.md': '# New home'}, destination)
self.assertFalse((destination / 'Old.md').exists())
self.assertEqual((destination / 'Manual.md').read_text(), 'Manual page')
self.assertEqual(json.loads((destination / wiki.MANIFEST).read_text()), ['Home.md'])
def test_manifest_cannot_delete_files_outside_destination(self):
destination = self.root / 'published'
destination.mkdir()
(destination / wiki.MANIFEST).write_text('["../keep.md"]')
(self.root / 'keep.md').write_text('Keep')
with self.assertRaises(ValueError):
wiki.write_pages({'Home.md': '# New'}, destination)
self.assertEqual((self.root / 'keep.md').read_text(), 'Keep')
self.assertFalse((destination / 'Home.md').exists())
def test_repeated_generation_has_identical_content(self):
destination = self.root / 'published'
pages = {'Home.md': '# Home', '_Sidebar.md': '[Home](Home)'}
wiki.write_pages(pages, destination)
before = {p.name: p.read_bytes() for p in destination.iterdir()}
wiki.write_pages(pages, destination)
self.assertEqual(before, {p.name: p.read_bytes() for p in destination.iterdir()})
def test_every_repository_guide_is_published_and_navigable(self):
with mock.patch.object(wiki, 'ROOT', ROOT):
output = wiki.render('deadbeef')
sources = wiki.guide_sources()
sidebar = output['_Sidebar.md']
self.assertEqual([line for line in sidebar.splitlines() if line.startswith('## ')], ['## Usage', '## Configuration', '## Development'])
self.assertNotIn('Wiki-Publishing.md', output)
self.assertNotIn('Getting-Started.md', output)
for source in sources:
name = wiki.page_name(source)
with self.subTest(source=source):
self.assertIn(name + '.md', output)
if name != 'Home':
self.assertIn(wiki.WIKI + '/' + name, sidebar)
for name, content in output.items():
for href in wiki.destinations(content):
self.assertFalse(href.endswith('.md') and not href.startswith('https://'), (name, href))
def test_configuration_topics_have_distinct_pages_and_direct_navigation(self):
with mock.patch.object(wiki, 'ROOT', ROOT):
output = wiki.render('deadbeef')
for path in (ROOT / 'docs/configuration').glob('*.md'):
with self.subTest(topic=path.name):
page = wiki.page_name(path)
self.assertTrue(page.startswith('Configuration-'))
self.assertIn(page + '.md', output)
self.assertIn(wiki.WIKI + '/' + page + ')', output['_Sidebar.md'])
self.assertIn('## Layout', output['Configuration.md'])
self.assertNotIn('default-column-width', output['Configuration.md'])
self.assertIn('default-column-width', output['Configuration-Layout.md'])
self.assertIn('### Binding modes', output['Configuration.md'])
self.assertIn('## Binding modes', output['Configuration-Bindings.md'])
self.assertIn(wiki.WIKI + '/Configuration-Bindings', output['Input-and-Bindings.md'])
def test_activity_settings_have_separate_configuration_page(self):
with mock.patch.object(wiki, 'ROOT', ROOT):
output = wiki.render('deadbeef')
settings = output['Configuration-Activities.md']
usage = output['Live-Activities.md']
sidebar_configuration = output['_Sidebar.md'].split('## Configuration', 1)[1].split('## Development', 1)[0]
self.assertIn(wiki.WIKI + '/Configuration-Activities)', sidebar_configuration)
self.assertIn('shell-hook #true', settings)
self.assertIn('rule "cargo"', settings)
self.assertIn('## Command rules', settings)
self.assertNotIn('rule "cargo"', usage)
self.assertIn('## Configure Activities', usage)
self.assertIn(wiki.WIKI + '/Configuration-Activities#command-rules', usage)
self.assertIn('## Start using it', usage)
self.assertIn('## Publisher ownership and lifecycle', usage)
def test_tabbed_windows_usage_and_settings_are_published_separately(self):
with mock.patch.object(wiki, 'ROOT', ROOT):
output = wiki.render('deadbeef')
usage = output['Tabbed-Windows.md']
layout = output['Configuration-Layout.md']
bindings = output['Configuration-Bindings.md']
navigation = output['_Sidebar.md'].split('## Usage', 1)[1].split('## Configuration', 1)[0]
self.assertIn(wiki.WIKI + '/Tabbed-Windows)', navigation)
self.assertIn('segmented rail', usage)
self.assertIn('feresectl join-tab left', usage)
self.assertIn(wiki.WIKI + '/Configuration-Layout#tabbed-columns', usage)
self.assertIn('default-column-display "tabbed"', layout)
self.assertNotIn('default-column-display', output['Configuration.md'])
for action in ('toggle-column-tabbed', 'join-tab', 'leave-tab', 'next-tab', 'previous-tab'):
with self.subTest(action=action):
self.assertIn('`' + action + '`', bindings)
for page in ('Shortcuts-and-Gestures.md', 'IPC.md', 'Configuration-Bindings.md'):
self.assertIn(wiki.WIKI + '/Tabbed-Windows', output[page])
def test_tab_indicator_reference_documents_every_configuration_field(self):
source = (ROOT / 'compositor/src/config/tab_indicator.rs').read_text()
reference = (ROOT / 'docs/configuration/layout.md').read_text()
body = re.search(r'struct TabIndicatorConfig\s*\{([^}]+)\}', source).group(1)
for field in re.findall(r'^\s+(\w+):', body, re.MULTILINE):
with self.subTest(field=field):
self.assertIn('| `' + field.replace('_', '-') + '` |', reference)
def test_activity_publishing_has_its_own_development_page(self):
with mock.patch.object(wiki, 'ROOT', ROOT):
output = wiki.render('deadbeef')
developer = output['Development-Activities.md']
usage = output['Live-Activities.md']
navigation = output['_Sidebar.md'].split('## Development', 1)[1]
self.assertIn(wiki.WIKI + '/Development-Activities)', navigation)
self.assertIn('Linux 6.5', developer)
self.assertIn('socket-bound pidfd', ' '.join(developer.split()))
self.assertNotIn('socket-bound pidfd', usage)
for anchor in ('publish-your-own-status', 'publisher-ownership-and-lifecycle', 'extensions-and-templates'):
with self.subTest(anchor=anchor):
self.assertIn(anchor, wiki.heading_ids(usage))
self.assertIn(wiki.WIKI + '/Development-Activities#' + anchor, usage)
self.assertIn(anchor, wiki.heading_ids(developer))
def test_activity_reference_documents_every_configuration_field(self):
source = (ROOT / 'crates/ferese-config/src/activities.rs').read_text()
reference = (ROOT / 'docs/configuration/activities.md').read_text()
special = {'name': 'Rule name', 'pattern': '`match`', 'sources': '## Sources', 'rules': '## Command rules', 'result': '`result.ok`'}
for name in ('Settings', 'Sources', 'Rule', 'ResultText'):
body = re.search(r'pub struct ' + name + r'\s*\{([^}]+)\}', source).group(1)
for field in re.findall(r'pub (\w+):', body):
label = special.get(field, '`' + ('result.' if name == 'ResultText' else '') + field.replace('_', '-') + '`')
with self.subTest(struct=name, field=field):
self.assertIn(label, reference)
def test_readme_wiki_links_resolve_to_published_pages(self):
from html.parser import HTMLParser
from urllib.parse import urlsplit
class Links(HTMLParser):
def __init__(self):
super().__init__()
self.hrefs = set()
def handle_starttag(self, tag, attrs):
if tag == 'a':
self.hrefs.update(value for key, value in attrs if key == 'href')
with mock.patch.object(wiki, 'ROOT', ROOT):
output = wiki.render('deadbeef')
readme = (ROOT / 'README.md').read_text()
links = Links()
links.feed(readme)
for href in wiki.destinations(readme) | links.hrefs:
if not href.startswith(wiki.WIKI):
continue
relative = href[len(wiki.WIKI):].lstrip('/')
url = urlsplit(relative)
page = (url.path or 'Home') + '.md'
with self.subTest(href=href):
self.assertIn(page, output)
if url.fragment:
self.assertIn(url.fragment, wiki.heading_ids(output[page]))
def test_legacy_configuration_anchors_point_to_published_topics(self):
with mock.patch.object(wiki, 'ROOT', ROOT):
output = wiki.render('deadbeef')
introduction = output['Configuration.md']
for href in wiki.destinations(introduction):
prefix = wiki.WIKI + '/Configuration-'
if not href.startswith(prefix):
continue
page, _, anchor = href[len(wiki.WIKI) + 1:].partition('#')
self.assertIn(page + '.md', output)
if anchor:
self.assertIn(anchor, wiki.heading_ids(output[page + '.md']))
self.assertIn(wiki.WIKI + '/Configuration-Bindings#binding-modes', introduction)
def test_homepage_redirects_keep_old_paths_and_fragments(self):
subprocess.run([sys.executable, str(ROOT / 'scripts/build-site.py')], check=True, capture_output=True)
destination = ROOT / 'build/site/docs/configuration.html'
content = destination.read_text()
self.assertIn(wiki.WIKI + '/Configuration', content)
self.assertIn('location.hash', content)
self.assertIn('http-equiv="refresh"', content)
self.assertIn('<a href="' + wiki.WIKI + '/Configuration">', content)
self.assertTrue((ROOT / 'build/site/assets/screenshots/ferese-media.webp').is_file())
self.assertIn(wiki.WIKI + '/Configuration-Appearance', (ROOT / 'build/site/docs/configuration/appearance.html').read_text())
if __name__ == '__main__':
unittest.main()
+10 -14
View File
@@ -1,7 +1,7 @@
# Ferese website and handbook
# Ferese website
The website's handbook is built from the project's Markdown guides, so the website
and repository share the same documentation.
The website links to the [GitHub Wiki](https://github.com/ferese-wm/ferese/wiki)
for documentation. Former handbook URLs redirect to the matching wiki pages.
## Local preview
@@ -12,18 +12,14 @@ python3 -m venv .site-venv
python3 -m http.server 8080 --bind 127.0.0.1 --directory build/site
```
Open http://localhost:8080 for the homepage or http://localhost:8080/docs/ for the
handbook. Rebuild and refresh the browser after changing the site sources or guides.
Open http://localhost:8080 for the homepage. Rebuild and refresh the browser
after changing the site sources.
- `site/index.html` and `site/home.css`: homepage and desktop screenshots.
- `site/styles.css`: shared dark appearance, typography and navigation.
- `site/handbook.css`: handbook layout, search and reference tables.
- `site/content/index.md`: handbook introduction.
- `docs/*.md`: guides; published pages are listed in `scripts/build-site.py`.
- `scripts/build-site.py`: renders Markdown, resolves links, generates section
navigation, previous/next links, and the search index.
- `site/app.js`: handbook search and code copying. Reading and navigation work
without JavaScript. Search is local to the browser, with no third-party service.
- `docs/*.md` and its configuration/development topic directories: guide sources published to the wiki.
- `scripts/build-site.py`: copies homepage assets and writes handbook redirects.
- `scripts/build-wiki.py`: validates and generates wiki pages.
## GitHub Pages
@@ -32,8 +28,8 @@ open **Actions → Deploy website to GitHub Pages → Run workflow** and choose
branch to publish. Deployment runs manually, and the expected address is
https://ferese-wm.github.io/ferese/.
The site uses relative links, including for handbook search, so it also works under the
`/ferese/` project path. Each build copies screenshots from `docs/images` into
The homepage uses relative asset links, so it also works under the `/ferese/`
project path. Each build copies screenshots from `docs/images` into
`build/site`, which is ignored by Git.
For deployment setup, follow [GitHub’s custom workflow
+6 -6
View File
@@ -28,15 +28,15 @@
<header class="site-header">
<div class="header-inner page-width">
<a class="brand" href="#main" aria-label="Ferese home"><img src="assets/branding/ferese.svg" width="34" height="34" alt=""><span>Ferese</span></a>
<nav aria-label="Main navigation"><a href="#make-room">The desktop</a><a href="#appearance">Appearance</a><a href="docs/">Handbook</a></nav>
<div class="header-actions"><a class="small-button" href="docs/installation.html">Get Ferese</a></div>
<nav aria-label="Main navigation"><a href="#make-room">The desktop</a><a href="#appearance">Appearance</a><a href="https://github.com/ferese-wm/ferese/wiki">Documentation</a></nav>
<div class="header-actions"><a class="small-button" href="https://github.com/ferese-wm/ferese/wiki/Installation">Get Ferese</a></div>
</div>
</header>
<main id="main">
<section class="hero page-width" aria-labelledby="hero-title">
<h1 id="hero-title">A little more room.</h1>
<p class="hero-description">A configurable Wayland desktop for Linux that brings your windows and everyday controls together.</p>
<div class="hero-actions"><a class="primary-button" href="docs/installation.html">Get Ferese <svg class="icon"><use href="#i-arrow"/></svg></a><a class="inline-link" href="#desktop-preview">Take a look <svg class="icon down"><use href="#i-arrow"/></svg></a></div>
<div class="hero-actions"><a class="primary-button" href="https://github.com/ferese-wm/ferese/wiki/Installation">Get Ferese <svg class="icon"><use href="#i-arrow"/></svg></a><a class="inline-link" href="#desktop-preview">Take a look <svg class="icon down"><use href="#i-arrow"/></svg></a></div>
<div class="desktop-stage" id="desktop-preview">
<div class="desktop-capture" id="desktop-capture">
<img id="desktop-capture-image" src="assets/screenshots/ferese-media.webp" srcset="assets/screenshots/ferese-media-960.webp 960w, assets/screenshots/ferese-media-1440.webp 1440w, assets/screenshots/ferese-media.webp 2880w" sizes="(max-width: 700px) calc(100vw - 40px), (max-width: 1200px) calc(100vw - 80px), 1120px" width="2880" height="1800" alt="The Ferese desktop with a translucent Now Playing popup showing Spotify artwork and playback controls over the blue and violet city wallpaper." fetchpriority="high">
@@ -61,9 +61,9 @@
<h3>A little closer to your music.</h3><p>See what's playing and pause or skip a track, right from your panel.</p>
</article>
</section>
<div class="gallery-link page-width"><a class="inline-link" href="docs/screenshots.html">See all desktop screenshots <svg class="icon"><use href="#i-arrow"/></svg></a></div>
<section class="start-section page-width" aria-labelledby="start-title"><img class="start-logo" src="assets/branding/ferese.svg" width="74" height="74" alt=""><h2 id="start-title">Make yourself at home.</h2><p>Try Ferese in a nested session, or install it as your next desktop.</p><div class="hero-actions"><a class="primary-button" href="docs/installation.html">Get Ferese <svg class="icon"><use href="#i-arrow"/></svg></a><a class="inline-link" href="https://github.com/ferese-wm/ferese">Explore the source <svg class="icon"><use href="#i-external"/></svg></a></div></section>
<div class="gallery-link page-width"><a class="inline-link" href="https://github.com/ferese-wm/ferese/wiki/Screenshots">See all desktop screenshots <svg class="icon"><use href="#i-arrow"/></svg></a></div>
<section class="start-section page-width" aria-labelledby="start-title"><img class="start-logo" src="assets/branding/ferese.svg" width="74" height="74" alt=""><h2 id="start-title">Make yourself at home.</h2><p>Try Ferese in a nested session, or install it as your next desktop.</p><div class="hero-actions"><a class="primary-button" href="https://github.com/ferese-wm/ferese/wiki/Installation">Get Ferese <svg class="icon"><use href="#i-arrow"/></svg></a><a class="inline-link" href="https://github.com/ferese-wm/ferese">Explore the source <svg class="icon"><use href="#i-external"/></svg></a></div></section>
</main>
<footer class="site-footer page-width"><a class="brand" href="#main" aria-label="Ferese home"><img src="assets/branding/ferese.svg" width="28" height="28" alt=""><span>Ferese</span></a><span>Built in the open for Linux.</span><nav aria-label="Footer navigation"><a href="docs/">Handbook</a><a href="https://github.com/ferese-wm/ferese">GitHub <svg class="icon"><use href="#i-external"/></svg></a></nav></footer>
<footer class="site-footer page-width"><a class="brand" href="#main" aria-label="Ferese home"><img src="assets/branding/ferese.svg" width="28" height="28" alt=""><span>Ferese</span></a><span>Built in the open for Linux.</span><nav aria-label="Footer navigation"><a href="https://github.com/ferese-wm/ferese/wiki">Documentation</a><a href="https://github.com/ferese-wm/ferese">GitHub <svg class="icon"><use href="#i-external"/></svg></a></nav></footer>
</body>
</html>