Surface Model: Layer Shell and Multi-Monitor
Wayland Layer Shell
ashell uses the wlr-layer-shell protocol to position itself as a status bar. Key concepts:
- Layer surface: A special Wayland surface that lives in a specific layer (Background, Bottom, Top, Overlay).
- Anchor: Where the surface attaches (top, bottom, left, right edges).
- Exclusive zone: Space reserved by the bar that other windows won’t overlap.
Surface Architecture
For each monitor output, ashell creates a main bar surface, plus a menu surface while a menu is open:
┌─────────────────────────────────────────┐
│ Monitor Output │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Main Layer Surface │ │
│ │ (configured layer, 34px high) │ │
│ │ Namespace: "ashell-main-layer" │ │
│ │ Exclusive zone: yes │ │
│ │ Keyboard: None ↔ OnDemand │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Menu Layer Surface │ │
│ │ (Overlay layer, on demand) │ │
│ │ Namespace: "ashell-menu-layer" │ │
│ │ Exclusive zone: no │ │
│ │ Keyboard: None/OnDemand/Excl. │ │
│ └──────────────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
- Main surface: Always present, displays the bar content. Uses an exclusive zone so windows don’t overlap it. Its keyboard interactivity is switched to
OnDemandwhile a menu is open ifenable_esc_keyis set, so Escape can close the menu. - Menu surface: Created on the Overlay layer when a menu opens, anchored to all four edges, and destroyed when the menu closes. It requests
OnDemandkeyboard interactivity whenenable_esc_keyis set, andExclusivewhile a dialog (e.g. the password prompt) needs keyboard input.
Two additional overlay surfaces are created on demand and are not tied to a specific output: the toast surface for notifications ("ashell-toast-layer") and the OSD surface ("ashell-osd-layer").
Multi-Monitor Configuration
The outputs config field controls which monitors get a bar:
# Default: bar on all monitors
outputs = "All"
# Only on the active monitor
outputs = "Active"
# Specific monitors by name
outputs = { Targets = ["eDP-1", "HDMI-A-1"] }
The Outputs Struct
src/outputs.rs defines the Outputs struct:
#![allow(unused)]
fn main() {
pub struct Outputs {
entries: Vec<(OutputKey, Option<ShellInfo>, Option<OutputId>)>,
toast: Option<OverlaySurface>,
osd: Option<OverlaySurface>,
// ...
}
}
Each entry is a tuple of:
- OutputKey: Monitor name (e.g.,
"eDP-1", or"Fallback"for the default) and description, used to matchoutputstargets - ShellInfo: The bar surface id, its menu, and layout state (if active)
- OutputId: The Wayland output identifier (if known)
Lifecycle
- Startup: The initial surface created by
.layer_shell()inmain.rsis used as a fallback (not tied to any specific output). - Output detected: When Wayland reports a new output, ashell creates surfaces for it (if it matches the config filter).
- Output removed: Surfaces for that output are destroyed.
- Config change: The
syncmethod reconciles surfaces with the new config.
Menu Surface Lifecycle
Menu surfaces are managed by Menu in src/components/menu.rs:
#![allow(unused)]
fn main() {
pub fn open(
&mut self,
menu_type: MenuType,
button_ui_ref: ButtonUIRef,
request_keyboard: bool,
output_id: Option<OutputId>,
) -> Task<app::Message> {
let (menu_id, task) = new_layer_surface(LayerShellSettings {
namespace: "ashell-menu-layer".to_string(),
layer: Layer::Overlay,
keyboard_interactivity, // OnDemand if request_keyboard, else None
output: output_id,
anchor: Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT,
// ...
});
// ...
}
/// Begin the close animation, firing `FinishCloseMenu` once it ends.
pub fn close(&mut self) -> Task<app::Message> { /* ... */ }
/// Destroy the surface after the close animation, opening any queued menu.
pub fn finish_close(&mut self) -> Task<app::Message> { /* ... */ }
}
Closing is two-step: close() starts the close animation (100 ms, skipped when animations are disabled) and emits Message::FinishCloseMenu, whose handler destroys the surface. Toggling a different menu while one is closing queues it until the close finishes.
Bar Positioning
The position config field (default: Top) controls where the bar appears:
Top: Anchored to top edgeBottom: Anchored to bottom edge
The layer config field (default: Bottom) controls the Wayland layer:
Top: Bar appears above normal windowsBottom: Bar appears below floating windows (default preference)Overlay: Bar appears above everything
Note: With the default
Bottomlayer the bar sits below floating windows. UseTopto keep the bar always visible, especially in Niri’s overview mode.