atagen

monolith

public
Star0
Watch0

Commit 02d5a5d5b17e

tarball

atagen <boss@atagen.co>2026-10-16 22:26 UTC

6 files changed+167-3
monolith: support xdg-activation, primary selection and data control

Spawned programs get an activation token carrying their launch context, so a window that comes from a process outside the spawn's ancestry, such as one started over D-Bus, still lands where the launch began. A token only grants focus if monolith made it or the requesting surface held the keyboard, and only within 10 seconds. Anything else marks the window urgent instead of stealing focus.
Modified file: crates/layout/src/lib.rs+18 additions
@@ -682,6 +682,24 @@ impl Layout {
682682 }
683683 }
684684
685+ /// Moves a window to `tag` on `o`, marking the tag urgent when the window
686+ /// ends up out of sight.
687+ pub fn relocate(&mut self, w: WindowId, o: Option<OutputId>, tag: TagId) {
688+ if let Some(o) = o {
689+ self.send_to_output(w, o);
690+ }
691+ self.set_tag(w, tag);
692+ self.mark_urgent(w);
693+ }
694+
695+ pub fn mark_urgent(&mut self, w: WindowId) {
696+ if !self.visible(w)
697+ && let Some(tag) = self.windows.get(w).map(|win| win.tag)
698+ {
699+ self.tags[tag].urgent = true;
700+ }
701+ }
702+
685703 pub fn send_to_output(&mut self, w: WindowId, o: OutputId) {
686704 let Some(win) = self.windows.get(w) else {
687705 return;
Added file: crates/monolith/src/activation.rs+88 additions
@@ -0,0 +1,88 @@
1+//! xdg-activation. Tokens carry launch context, so a program started
2+//! through a launcher or D-Bus lands where the user was when it began, and
3+//! they decide whether an activation may take focus.
4+
5+use crate::state::State;
6+use monolith_layout::{OutputId, TagId};
7+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
8+use smithay::wayland::xdg_activation::{
9+ XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
10+};
11+use std::time::Duration;
12+
13+/// Tokens older than this may still place a window but not focus it.
14+const FRESH: Duration = Duration::from_secs(10);
15+
16+struct Context {
17+ output: Option<OutputId>,
18+ tag: TagId,
19+ /// Created by monolith or by a client while it held the keyboard.
20+ may_focus: bool,
21+}
22+
23+impl XdgActivationHandler for State {
24+ fn activation_state(&mut self) -> &mut XdgActivationState {
25+ &mut self.activation_state
26+ }
27+
28+ fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
29+ let keyboard = self.seat.get_keyboard();
30+ let focused = keyboard.as_ref().and_then(|k| k.current_focus());
31+ let may_focus = data.surface.is_some() && data.surface == focused;
32+ let (output, tag) = self.launch_context();
33+ data.user_data.insert_if_missing(|| Context {
34+ output,
35+ tag,
36+ may_focus,
37+ });
38+ true
39+ }
40+
41+ fn request_activation(
42+ &mut self,
43+ _token: XdgActivationToken,
44+ data: XdgActivationTokenData,
45+ surface: WlSurface,
46+ ) {
47+ let Some(id) = self.window_id(&surface) else {
48+ return;
49+ };
50+ let ctx = data.user_data.get::<Context>();
51+ if self.awaiting.contains(&id)
52+ && let Some(ctx) = ctx
53+ && !self.placed_by_rule.contains(&id)
54+ {
55+ self.layout.relocate(id, ctx.output, ctx.tag);
56+ }
57+ let may_focus = ctx.is_some_and(|c| c.may_focus) && data.timestamp.elapsed() < FRESH;
58+ if may_focus && self.layout.visible(id) {
59+ self.layout.focus(id);
60+ } else {
61+ self.layout.mark_urgent(id);
62+ }
63+ self.apply_layout();
64+ }
65+}
66+
67+impl State {
68+ /// Where a launch starting now should put its window.
69+ pub fn launch_context(&self) -> (Option<OutputId>, TagId) {
70+ let output = self.layout.active_output();
71+ let tag = output.map_or(self.layout.homeless(), |o| self.layout.launch_tag(o));
72+ (output, tag)
73+ }
74+
75+ /// A token for a program monolith starts, carrying the launch context
76+ /// in case its window comes from a process that is not its descendant.
77+ pub fn launch_token(&mut self) -> String {
78+ let (output, tag) = self.launch_context();
79+ let data = XdgActivationTokenData::default();
80+ data.user_data.insert_if_missing(|| Context {
81+ output,
82+ tag,
83+ may_focus: true,
84+ });
85+ let (token, _) = self.activation_state.create_external_token(data);
86+ token.to_string()
87+ }
88+}
Modified file: crates/monolith/src/commands.rs+4 additions−2 deletions
@@ -173,10 +173,12 @@ impl State {
173173 if let Some(sock) = &self.ipc.path {
174174 cmd.env("MONOLITH_SOCKET", sock);
175175 }
176+ let token = self.launch_token();
177+ cmd.env("XDG_ACTIVATION_TOKEN", &token)
178+ .env("DESKTOP_STARTUP_ID", &token);
176179 let mut child = cmd.spawn().map_err(|e| format!("{program}: {e}"))?;
177180 tracing::info!(pid = child.id(), ?argv, "spawned");
178- let output = self.layout.active_output();
179- let tag = output.map_or(self.layout.homeless(), |o| self.layout.launch_tag(o));
181+ let (output, tag) = self.launch_context();
180182 self.launches.record(child.id(), output, tag);
181183 std::thread::spawn(move || child.wait());
182184 Ok(())
Modified file: crates/monolith/src/handlers.rs+27 additions−1 deletions
@@ -26,6 +26,9 @@ use smithay::wayland::dmabuf::{
2626 };
2727 use smithay::wayland::output::OutputHandler;
2828 use smithay::wayland::selection::SelectionHandler;
29+use smithay::wayland::selection::primary_selection::{
30+ PrimarySelectionHandler, PrimarySelectionState, set_primary_focus,
31+};
2932 use smithay::wayland::selection::data_device::{
3033 DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus,
3134 };
@@ -186,7 +189,8 @@ impl SeatHandler for State {
186189
187190 fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
188191 let client = focused.and_then(|s| self.display.get_client(s.id()).ok());
189- set_data_device_focus(&self.display, seat, client);
192+ set_data_device_focus(&self.display, seat, client.clone());
193+ set_primary_focus(&self.display, seat, client);
190194 }
191195 }
192196
@@ -200,6 +204,28 @@ impl DataDeviceHandler for State {
200204 }
201205 }
202206
207+impl PrimarySelectionHandler for State {
208+ fn primary_selection_state(&mut self) -> &mut PrimarySelectionState {
209+ &mut self.primary_selection_state
210+ }
211+}
212+
213+impl smithay::wayland::selection::wlr_data_control::DataControlHandler for State {
214+ fn data_control_state(
215+ &mut self,
216+ ) -> &mut smithay::wayland::selection::wlr_data_control::DataControlState {
217+ &mut self.wlr_data_control_state
218+ }
219+}
220+
221+impl smithay::wayland::selection::ext_data_control::DataControlHandler for State {
222+ fn data_control_state(
223+ &mut self,
224+ ) -> &mut smithay::wayland::selection::ext_data_control::DataControlState {
225+ &mut self.ext_data_control_state
226+ }
227+}
228+
203229 impl DndGrabHandler for State {}
204230
205231 impl WaylandDndGrabHandler for State {
Modified file: crates/monolith/src/main.rs+1 additions
@@ -1,3 +1,4 @@
1+mod activation;
12 mod anim;
23 mod commands;
34 mod cursor;
Modified file: crates/monolith/src/state.rs+29 additions
@@ -28,12 +28,16 @@ use smithay::wayland::idle_inhibit::IdleInhibitManagerState;
2828 use smithay::wayland::idle_notify::IdleNotifierState;
2929 use smithay::wayland::output::OutputManagerState;
3030 use smithay::wayland::selection::data_device::DataDeviceState;
31+use smithay::wayland::selection::ext_data_control::DataControlState as ExtDataControlState;
32+use smithay::wayland::selection::primary_selection::PrimarySelectionState;
33+use smithay::wayland::selection::wlr_data_control::DataControlState as WlrDataControlState;
3134 use smithay::wayland::session_lock::SessionLockManagerState;
3235 use smithay::wayland::shell::wlr_layer::{Layer, WlrLayerShellState};
3336 use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
3437 use smithay::wayland::shell::xdg::{XdgShellState, XdgToplevelSurfaceData};
3538 use smithay::wayland::shm::ShmState;
3639 use smithay::wayland::socket::ListeningSocketSource;
40+use smithay::wayland::xdg_activation::XdgActivationState;
3741 use std::collections::{HashMap, HashSet};
3842 use std::ffi::OsString;
3943 use std::path::PathBuf;
@@ -80,6 +84,8 @@ pub struct State {
8084 /// switches between tiled, floating and fullscreen.
8185 pub last_kind: HashMap<WindowId, (Kind, Rect, OutputId)>,
8286 pub awaiting: HashSet<WindowId>,
87+ /// Windows a rule sent somewhere, which activation tokens leave alone.
88+ pub placed_by_rule: HashSet<WindowId>,
8389 /// A layer surface given the keyboard by a click, like a launcher.
8490 pub layer_focus: Option<WlSurface>,
8591 pub lock: crate::lock::Lock,
@@ -90,6 +96,10 @@ pub struct State {
9096 pub compositor_state: CompositorState,
9197 pub xdg_shell_state: XdgShellState,
9298 pub layer_shell_state: WlrLayerShellState,
99+ pub activation_state: XdgActivationState,
100+ pub primary_selection_state: PrimarySelectionState,
101+ pub wlr_data_control_state: WlrDataControlState,
102+ pub ext_data_control_state: ExtDataControlState,
93103 pub session_lock_state: SessionLockManagerState,
94104 pub idle_notifier: IdleNotifierState<State>,
95105 pub _idle_inhibit: IdleInhibitManagerState,
@@ -152,6 +162,7 @@ impl State {
152162 layout.gaps = gaps(&config);
153163 let binds = Binds::new(&config);
154164 let animation = config.animation.clone();
165+ let primary_selection_state = PrimarySelectionState::new::<State>(&dh);
155166 Ok(Self {
156167 start: Instant::now(),
157168 socket_name,
@@ -184,6 +195,7 @@ impl State {
184195 borders: Default::default(),
185196 last_kind: HashMap::new(),
186197 awaiting: HashSet::new(),
198+ placed_by_rule: HashSet::new(),
187199 layer_focus: None,
188200 lock: Default::default(),
189201 inhibitors: HashSet::new(),
@@ -192,6 +204,18 @@ impl State {
192204 compositor_state: CompositorState::new::<State>(&dh),
193205 xdg_shell_state: XdgShellState::new::<State>(&dh),
194206 layer_shell_state: WlrLayerShellState::new::<State>(&dh),
207+ activation_state: XdgActivationState::new::<State>(&dh),
208+ wlr_data_control_state: WlrDataControlState::new::<State, _>(
209+ &dh,
210+ Some(&primary_selection_state),
211+ |_| true,
212+ ),
213+ ext_data_control_state: ExtDataControlState::new::<State, _>(
214+ &dh,
215+ Some(&primary_selection_state),
216+ |_| true,
217+ ),
218+ primary_selection_state,
195219 session_lock_state: SessionLockManagerState::new::<State, _>(&dh, |_| true),
196220 idle_notifier: IdleNotifierState::new(&dh, event_loop.handle()),
197221 _idle_inhibit: IdleInhibitManagerState::new::<State>(&dh),
@@ -262,6 +286,9 @@ impl State {
262286 None => launch.map(|l| l.1),
263287 };
264288 let id = self.layout.add_window(output, tag, None);
289+ if rule.tag.is_some() || rule.output.is_some() {
290+ self.placed_by_rule.insert(id);
291+ }
265292 if rule.floating == Some(true) {
266293 self.layout.toggle_floating(id);
267294 }
@@ -304,6 +331,7 @@ impl State {
304331 }
305332
306333 fn first_buffer(&mut self, id: WindowId) {
334+ self.placed_by_rule.remove(&id);
307335 if self.awaiting.remove(&id) {
308336 self.anim.spawned(id, now_ns());
309337 self.apply_layout();
@@ -319,6 +347,7 @@ impl State {
319347 self.leave_ghost(id, &window);
320348 }
321349 self.layout.remove_window(id);
350+ self.placed_by_rule.remove(&id);
322351 self.apply_layout();
323352 }
324353 }