Skip to main content

crabshell_services/
hyprland.rs

1//! Hyprland's workspaces, monitors and windows, over its two IPC sockets
2//! in `$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/`:
3//! `.socket.sock` takes one request per connection, such as
4//! `j/workspaces`, answers and closes, and `.socket2.sock` sends events
5//! as lines of `NAME>>DATA`.
6
7#[cfg(test)]
8mod tests;
9
10use std::cell::{RefCell, RefMut};
11use std::collections::{HashSet, VecDeque};
12use std::io::{ErrorKind, Read, Write};
13use std::net::Shutdown;
14use std::os::fd::AsFd;
15use std::os::unix::net::UnixStream;
16use std::panic::{self, AssertUnwindSafe, Location};
17use std::path::Path;
18use std::rc::Rc;
19use std::sync::Once;
20
21use calloop::{Interest, RegistrationToken};
22use crabshell_reactive::{Owner, ReadSignal, Signal};
23use crabshell_ui::Cx;
24use serde::de::DeserializeOwned;
25use serde::{Deserialize, Deserializer};
26
27use crate::Shared;
28use crate::dbus::set_changed;
29use crate::lines::{Lines, panic_message};
30
31/// The largest reply read from one request; a larger one is dropped.
32pub(crate) const MAX_REPLY: usize = 8 * 1024 * 1024;
33
34/// The longest request sent; a dispatch past it is refused.
35pub(crate) const MAX_REQUEST: usize = 8 * 1024;
36
37/// The most dispatches waiting to be sent or answered; one past it is
38/// refused, so a config dispatching in a loop can't grow the queue.
39const MAX_DISPATCHES: usize = 64;
40
41/// The most requests out at once. Hyprland's listen backlog holds 10, and
42/// a connect to a full one would block the loop.
43const MAX_IN_FLIGHT: usize = 8;
44
45/// The longest text kept, cut at a character.
46pub(crate) const MAX_TEXT: usize = 4 * 1024;
47
48/// The most workspaces, monitors or windows kept; the rest are left out.
49pub(crate) const MAX_LIST: usize = 4096;
50
51/// One of Hyprland's workspaces, as `j/workspaces` describes it.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct HyprlandWorkspace {
54  /// Its number. Named and special workspaces have none from Hyprland
55  /// 0.57 on, and a number below zero before it.
56  pub id: Option<i64>,
57  /// Its name, such as `1`, `web` or `special:magic`: the key to give
58  /// `children_keyed`.
59  pub name: String,
60  /// The name of the monitor it is on, such as `DP-1`.
61  pub monitor: String,
62  /// How many windows it holds.
63  pub windows: u32,
64  /// Whether it is shown on the focused monitor.
65  pub focused: bool,
66  /// Whether it is shown on its monitor, focused or not.
67  pub active: bool,
68  /// Whether a window on it asks for attention.
69  pub urgent: bool,
70}
71
72/// One of Hyprland's monitors, as `j/monitors` describes it.
73#[derive(Clone, Debug, PartialEq)]
74pub struct HyprlandMonitor {
75  /// Hyprland's number for it.
76  pub id: i64,
77  /// Its name, such as `DP-1`, as the output's name.
78  pub name: String,
79  /// Its make, model and serial, as Hyprland puts them.
80  pub description: String,
81  /// Whether it has the focus.
82  pub focused: bool,
83  /// The name of the workspace it shows.
84  pub active_workspace: String,
85  /// Where its left edge is, in the layout's logical pixels.
86  pub x: i32,
87  /// Where its top edge is, in the layout's logical pixels.
88  pub y: i32,
89  /// Its mode's width, in pixels before scaling.
90  pub width: i32,
91  /// Its mode's height, in pixels before scaling.
92  pub height: i32,
93  /// Its scale, such as 1.5.
94  pub scale: f64,
95}
96
97/// One of Hyprland's windows, as `j/clients` describes it.
98#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct HyprlandToplevel {
100  /// Its address, such as `0x55d4c8a3b0c0`: the key to give
101  /// `children_keyed`, and what a dispatcher names it by, as
102  /// `address:0x55d4c8a3b0c0`.
103  pub address: String,
104  /// Its class, the app id.
105  pub class: String,
106  /// Its title.
107  pub title: String,
108  /// The name of the workspace it is on.
109  pub workspace: String,
110  /// The name of the monitor it is on, or `""` while Hyprland doesn't
111  /// list that monitor.
112  pub monitor: String,
113  /// Whether it floats.
114  pub floating: bool,
115  /// Whether it is fullscreen; a maximized window isn't, except on
116  /// Hyprland 0.41 and older, which say only that it covers the screen.
117  pub fullscreen: bool,
118  /// Whether it has the keyboard focus.
119  pub focused: bool,
120  /// Whether it asks for attention: from its `urgent` event until it is
121  /// focused.
122  pub urgent: bool,
123}
124
125/// One line from Hyprland's event socket, such as `workspacev2>>2,2`,
126/// as Quickshell's `HyprlandEvent`.
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct HyprlandEvent {
129  /// What happened, such as `workspacev2` or `submap`.
130  pub name: String,
131  /// What Hyprland sent with it, such as `2,2`: its fields joined by
132  /// commas.
133  pub data: String,
134}
135
136impl HyprlandEvent {
137  /// The data split into at most `fields` fields, as Quickshell's
138  /// `parse(n)`: the last keeps its commas, as a window title in
139  /// `windowtitlev2>>5a1,vim, the editor` does.
140  pub fn parse(&self, fields: usize) -> Vec<&str> {
141    self.data.splitn(fields, ',').collect()
142  }
143}
144
145/// The Hyprland service, from `cx.hyprland()`. A clone is cheap and
146/// shares the one connection.
147#[derive(Clone)]
148pub struct Hyprland {
149  /// `None` when the shell provided no services.
150  service: Option<Rc<Service>>,
151  /// The asking context's owner, which [`on_event`](Self::on_event)'s
152  /// handlers go with.
153  owner: Owner,
154  workspaces: ReadSignal<Vec<HyprlandWorkspace>>,
155  monitors: ReadSignal<Vec<HyprlandMonitor>>,
156  toplevels: ReadSignal<Vec<HyprlandToplevel>>,
157}
158
159impl Hyprland {
160  /// Switches to the workspace `target`, as Hyprland names it: a number
161  /// such as `2`, or `name:web`. Hyprland reads a dispatch as Lua when its
162  /// config is Lua, as `hyprland.lua` can be from 0.55 on and always is
163  /// from 0.57 on, and as `workspace 2` otherwise. So the first call asks
164  /// Hyprland's status, and switches wait for its answer. Under Lua
165  /// `target` goes as a quoted Lua string, so no name runs as code; in the
166  /// old syntax it goes as it is, so a target with whitespace or a control
167  /// character is logged and not sent. Hyprland's refusal is logged.
168  pub fn focus_workspace(&self, target: &str) {
169    match &self.service {
170      Some(service) => service.focus_workspace(target),
171      None => tracing::warn!(target, "no Hyprland to dispatch to"),
172    }
173  }
174
175  /// Sends Hyprland `dispatch command`, as `hyprctl dispatch` does, on a
176  /// connection of its own, and never waits for it. Hyprland's refusal is
177  /// logged, and so is a command over 8 KiB, which isn't sent.
178  ///
179  /// When Hyprland's config is Lua, as `hyprland.lua` can be from 0.55 on
180  /// and always is from 0.57 on, `command` is a Lua expression that
181  /// Hyprland runs inside the compositor, such as
182  /// `hl.dsp.focus({ workspace = "2" })`. Otherwise it is a dispatcher and
183  /// its arguments, such as `workspace 2`. Either way, never put outside
184  /// text, such as a window's title or class or a workspace's name, into
185  /// it: under Lua what it says would run. Switch workspaces with
186  /// [`focus_workspace`](Self::focus_workspace), which quotes them.
187  pub fn dispatch(&self, command: &str) {
188    match &self.service {
189      Some(service) => service.dispatch(command),
190      None => tracing::warn!(command, "no Hyprland to dispatch to"),
191    }
192  }
193
194  /// Calls `handler` with every event Hyprland sends, as Quickshell's
195  /// `rawEvent`, until the context `cx.hyprland()` was called on goes.
196  /// The lists follow once Hyprland answers the reads an event starts,
197  /// after `handler` runs.
198  ///
199  /// It runs on the main thread. If it panics, that is logged with the
200  /// event and where `on_event` was called, and it isn't called again.
201  ///
202  /// Called on a handle whose context went, it logs that and does
203  /// nothing.
204  #[track_caller]
205  pub fn on_event(&self, handler: impl Fn(&HyprlandEvent) + 'static) {
206    let at = Location::caller();
207    if self.owner.is_disposed() {
208      return tracing::warn!(
209        "Hyprland on_event (called at {at}) on a context that is gone does nothing"
210      );
211    }
212    if let Some(service) = &self.service {
213      let id = service.add_handler(Rc::new(handler), at);
214      self.owner.signal(Unregister(Rc::clone(service), id));
215    }
216  }
217  /// Every workspace, numbered ones by number after the named ones.
218  /// Empty while Hyprland isn't connected.
219  pub fn workspaces(&self) -> ReadSignal<Vec<HyprlandWorkspace>> {
220    self.workspaces
221  }
222
223  /// Every monitor, in Hyprland's order.
224  pub fn monitors(&self) -> ReadSignal<Vec<HyprlandMonitor>> {
225    self.monitors
226  }
227
228  /// Every mapped window, in Hyprland's order.
229  pub fn toplevels(&self) -> ReadSignal<Vec<HyprlandToplevel>> {
230    self.toplevels
231  }
232}
233
234/// `cx.hyprland()`.
235pub(crate) fn hyprland(cx: &Cx) -> Hyprland {
236  let Some(service) = cx.service::<Service>() else {
237    static LOGGED: Once = Once::new();
238    LOGGED.call_once(|| {
239      tracing::warn!("cx.hyprland() needs the services the shell provides; it shows nothing");
240    });
241    return Hyprland {
242      service: None,
243      owner: cx.owner(),
244      workspaces: cx.signal(Vec::new()).read_only(),
245      monitors: cx.signal(Vec::new()).read_only(),
246      toplevels: cx.signal(Vec::new()).read_only(),
247    };
248  };
249  let (workspaces, monitors, toplevels) = service.start();
250  Hyprland {
251    service: Some(service),
252    owner: cx.owner(),
253    workspaces: workspaces.read_only(),
254    monitors: monitors.read_only(),
255    toplevels: toplevels.read_only(),
256  }
257}
258
259/// A list read whole with one request.
260#[derive(Clone, Copy, Debug, PartialEq, Eq)]
261enum Query {
262  Workspaces,
263  Monitors,
264  Clients,
265  ActiveWindow,
266}
267
268impl Query {
269  const ALL: [Query; 4] = [
270    Query::Workspaces,
271    Query::Monitors,
272    Query::Clients,
273    Query::ActiveWindow,
274  ];
275
276  fn command(self) -> &'static str {
277    match self {
278      Query::Workspaces => "j/workspaces",
279      Query::Monitors => "j/monitors",
280      Query::Clients => "j/clients",
281      Query::ActiveWindow => "j/activewindow",
282    }
283  }
284
285  /// What reads again after the event `name`. Hyprland sends an event's
286  /// two versions together, and both count, for a Hyprland that lacks
287  /// one.
288  fn after(name: &str) -> &'static [Query] {
289    use Query::{ActiveWindow, Clients, Monitors, Workspaces};
290    match name.strip_suffix("v2").unwrap_or(name) {
291      "workspace" | "focusedmon" | "createworkspace" | "destroyworkspace" | "activespecial" => {
292        &[Workspaces, Monitors]
293      }
294      // Windows name their workspace and monitor too.
295      "moveworkspace" | "renameworkspace" | "changeworkspaceid" | "monitoradded"
296      | "monitorremoved" | "configreloaded" => &[Workspaces, Monitors, Clients],
297      "openwindow" | "closewindow" | "movewindow" => &[Workspaces, Clients],
298      "windowtitle" | "changefloatingmode" | "fullscreen" => &[Clients],
299      // A focused window's new class comes only as `activewindow`.
300      "activewindow" => &[Clients, ActiveWindow],
301      _ => &[],
302    }
303  }
304}
305
306/// Where the reading of one list stands.
307#[derive(Clone, Copy, PartialEq, Eq)]
308enum Reading {
309  Idle,
310  Busy,
311  /// Busy, and an event came meanwhile, which the answer may predate, so
312  /// it reads again when done.
313  Stale,
314}
315
316type Lists = (
317  Signal<Vec<HyprlandWorkspace>>,
318  Signal<Vec<HyprlandMonitor>>,
319  Signal<Vec<HyprlandToplevel>>,
320);
321
322/// The one Hyprland connection of a shell, started on first use.
323pub(crate) struct Service {
324  shared: Rc<Shared>,
325  state: RefCell<Option<State>>,
326}
327
328struct State {
329  workspaces: Signal<Vec<HyprlandWorkspace>>,
330  monitors: Signal<Vec<HyprlandMonitor>>,
331  toplevels: Signal<Vec<HyprlandToplevel>>,
332  /// The event socket, while it is open.
333  events: Option<Events>,
334  /// By [`Query`].
335  reading: [Reading; 4],
336  /// By [`Query`], the burst a list's read in flight was started for, if
337  /// one was: a show waits for the rest of it. A read again after a later
338  /// event belongs to none, so no list that keeps reading holds the others.
339  burst_of: [Option<u64>; 4],
340  /// The bursts whose reads aren't all answered.
341  bursts: Vec<Burst>,
342  next_burst: u64,
343  /// Requests waiting for their reply.
344  requests: Vec<Request>,
345  next_request: u64,
346  /// Whether a request that couldn't connect was logged, so a missing
347  /// request socket logs once.
348  request_failure_logged: bool,
349  raw: Raw,
350  /// The windows that asked for attention since they were last focused,
351  /// by address, as only events say.
352  urgent: HashSet<String>,
353  /// What `on_event` was given, in the order it was.
354  handlers: Vec<Handler>,
355  next_handler: u64,
356  /// Whether Hyprland reads a dispatch as Lua, as it does when its config
357  /// is, once `j/status` said.
358  lua: Option<bool>,
359  /// Whether `j/status` was asked and not answered yet.
360  status_asked: bool,
361  /// What `focus_workspace` was given before the syntax was known.
362  focusing: Vec<String>,
363  /// Requests waiting for fewer than [`MAX_IN_FLIGHT`] to be out.
364  queued: VecDeque<(Asked, String)>,
365  /// The urgent windows from before the event socket was connected
366  /// again, which the events Hyprland dropped may have focused or closed.
367  recheck: Option<Recheck>,
368  /// Whether the event socket was connected again since its last event.
369  reconnected: bool,
370}
371
372/// The reads one batch of events, or a connect, started.
373struct Burst {
374  id: u64,
375  started: usize,
376  /// How many aren't answered yet.
377  out: usize,
378}
379
380/// Urgency to check against the first window list and focused window
381/// read after a reconnect.
382struct Recheck {
383  urgent: HashSet<String>,
384  /// The first request sent after the reconnect: earlier ones' answers
385  /// predate it.
386  from: u64,
387  windows: bool,
388  active: bool,
389}
390
391/// What became of a request [`Service::send`] tried.
392enum Sent {
393  Out,
394  /// Hyprland's backlog is full, so it waits.
395  Full,
396  Failed,
397}
398
399/// What a request asks Hyprland.
400#[derive(Clone, Copy, Debug, PartialEq, Eq)]
401enum Asked {
402  List(Query),
403  Status,
404  Dispatch,
405}
406
407/// A config's raw-event handler.
408#[derive(Clone)]
409struct Handler {
410  id: u64,
411  /// Where `on_event` was called, for the log if it panics.
412  at: &'static Location<'static>,
413  handler: Rc<dyn Fn(&HyprlandEvent)>,
414}
415
416/// Drops a handler when the context it was given in goes.
417struct Unregister(Rc<Service>, u64);
418
419impl Drop for Unregister {
420  fn drop(&mut self) {
421    let handler = self.0.remove_handler(self.1);
422    // Dropped outside the borrow: the handler may hold a `Hyprland`.
423    drop(handler);
424  }
425}
426
427/// What Hyprland last said, the lists are built from.
428#[derive(Default)]
429struct Raw {
430  workspaces: Vec<RawWorkspace>,
431  monitors: Vec<RawMonitor>,
432  windows: Vec<RawWindow>,
433  /// The focused window's address.
434  active: Option<String>,
435}
436
437struct Events {
438  stream: UnixStream,
439  reader: RegistrationToken,
440  lines: Lines,
441}
442
443struct Request {
444  id: u64,
445  asked: Asked,
446  stream: UnixStream,
447  reader: RegistrationToken,
448  reply: Vec<u8>,
449}
450
451impl Service {
452  pub(crate) fn new(shared: Rc<Shared>) -> Service {
453    Service {
454      shared,
455      state: RefCell::new(None),
456    }
457  }
458
459  /// The lists, connecting to Hyprland if this is the first ask.
460  fn start(self: &Rc<Self>) -> Lists {
461    if self.state.borrow().is_none() {
462      let root = &self.shared.root;
463      *self.state.borrow_mut() = Some(State {
464        workspaces: root.signal(Vec::new()),
465        monitors: root.signal(Vec::new()),
466        toplevels: root.signal(Vec::new()),
467        events: None,
468        reading: [Reading::Idle; 4],
469        burst_of: [None; 4],
470        bursts: Vec::new(),
471        next_burst: 0,
472        requests: Vec::new(),
473        next_request: 0,
474        request_failure_logged: false,
475        raw: Raw::default(),
476        urgent: HashSet::new(),
477        handlers: Vec::new(),
478        next_handler: 0,
479        lua: None,
480        status_asked: false,
481        focusing: Vec::new(),
482        queued: VecDeque::new(),
483        reconnected: false,
484        recheck: None,
485      });
486      match &self.shared.config.hyprland {
487        None => tracing::info!(
488          "HYPRLAND_INSTANCE_SIGNATURE or XDG_RUNTIME_DIR is unset, so there is no Hyprland to show"
489        ),
490        Some(dir) => {
491          self.connect(dir);
492        }
493      }
494    }
495    let state = self.state.borrow();
496    let state = state.as_ref().expect("invariant: started above");
497    (state.workspaces, state.monitors, state.toplevels)
498  }
499
500  /// Opens the event socket, then reads every list, so no change between
501  /// the two goes unseen. A failure is logged and never retried, so a
502  /// stale signature never wakes the shell. `false` if it failed.
503  fn connect(self: &Rc<Self>, dir: &Path) -> bool {
504    let Some(event_loop) = self.shared.event_loop() else {
505      return false;
506    };
507    let path = dir.join(".socket2.sock");
508    let opened = open(&path).and_then(|stream| {
509      let fd = stream.as_fd().try_clone_to_owned()?;
510      Ok((stream, fd))
511    });
512    let (stream, fd) = match opened {
513      Ok(opened) => opened,
514      Err(error) => {
515        let path = path.display();
516        tracing::warn!(%path, %error, "can't connect to Hyprland, so its lists stay empty");
517        return false;
518      }
519    };
520    let service = Rc::clone(self);
521    let read = Box::new(move || service.on_events());
522    let Some(reader) = event_loop.insert_fd(fd, Interest::READ, read) else {
523      return false;
524    };
525    let mut state = self.state.borrow_mut();
526    let state = state
527      .as_mut()
528      .expect("invariant: started before connecting");
529    state.events = Some(Events {
530      stream,
531      reader,
532      lines: Lines::default(),
533    });
534    let burst = state.burst();
535    for query in Query::ALL {
536      self.read(state, query, burst);
537    }
538    true
539  }
540
541  /// Reads `query`'s list, or once more after the read under way.
542  fn read(self: &Rc<Self>, state: &mut State, query: Query, burst: u64) {
543    let reading = &mut state.reading[query as usize];
544    if *reading != Reading::Idle {
545      *reading = Reading::Stale;
546      return;
547    }
548    *reading = Reading::Busy;
549    state.burst_of[query as usize] = Some(burst);
550    match state.bursts.iter_mut().find(|open| open.id == burst) {
551      Some(open) => {
552        open.started += 1;
553        open.out += 1;
554      }
555      None => state.bursts.push(Burst {
556        id: burst,
557        started: 1,
558        out: 1,
559      }),
560    }
561    self.request(state, query.command(), Asked::List(query));
562  }
563
564  fn dispatch(self: &Rc<Self>, command: &str) {
565    let request = format!("dispatch {command}");
566    if request.len() > MAX_REQUEST {
567      return tracing::warn!("a dispatch over 8 KiB is too long to send");
568    }
569    let mut state = self.state.borrow_mut();
570    let state = state.as_mut().expect("invariant: started by cx.hyprland()");
571    let sent = state.requests.iter().map(|request| request.asked);
572    let waiting = sent.chain(state.queued.iter().map(|(asked, _)| *asked));
573    if waiting.filter(|asked| *asked == Asked::Dispatch).count() >= MAX_DISPATCHES {
574      return tracing::warn!(
575        command,
576        "64 dispatches wait for Hyprland, so this one isn't sent"
577      );
578    }
579    if self.shared.config.hyprland.is_none() {
580      return tracing::warn!(command, "no Hyprland to dispatch to");
581    }
582    self.request(state, &request, Asked::Dispatch);
583  }
584
585  fn focus_workspace(self: &Rc<Self>, target: &str) {
586    if self.shared.config.hyprland.is_none() {
587      return tracing::warn!(target, "no Hyprland to dispatch to");
588    }
589    let mut guard = self.state.borrow_mut();
590    let state = guard.as_mut().expect("invariant: started by cx.hyprland()");
591    if let Some(lua) = state.lua {
592      drop(guard);
593      return self.switch(lua, target);
594    }
595    if state.focusing.len() >= MAX_DISPATCHES {
596      return tracing::warn!(
597        target,
598        "64 switches wait for Hyprland's status, so this one isn't sent"
599      );
600    }
601    state.focusing.push(target.to_owned());
602    if !state.status_asked {
603      state.status_asked = true;
604      self.request(state, "j/status", Asked::Status);
605    }
606  }
607
608  /// Dispatches the switch to `target` in the syntax Hyprland reads.
609  fn switch(self: &Rc<Self>, lua: bool, target: &str) {
610    if lua {
611      return self.dispatch(&format!(
612        "hl.dsp.focus({{ workspace = {} }})",
613        lua_string(target)
614      ));
615    }
616    // The old syntax sends it as it is.
617    if target.chars().any(|c| c.is_whitespace() || c.is_control()) {
618      return tracing::warn!(
619        target,
620        "a workspace with whitespace or a control character in its name goes unquoted to \
621         Hyprland's old syntax, so the shell can't switch to it"
622      );
623    }
624    self.dispatch(&format!("workspace {target}"));
625  }
626
627  /// Sends `command` on a connection of its own, after what waits. At
628  /// most [`MAX_IN_FLIGHT`] are out, and a connect never waits: while
629  /// Hyprland's backlog is full, the rest wait for a request to end or
630  /// another to be made.
631  fn request(self: &Rc<Self>, state: &mut State, command: &str, asked: Asked) {
632    state.queued.push_back((asked, command.to_owned()));
633    self.send_queued(state);
634  }
635
636  /// Sends what waits, while fewer than [`MAX_IN_FLIGHT`] requests are
637  /// out and Hyprland's backlog has room.
638  fn send_queued(self: &Rc<Self>, state: &mut State) {
639    while state.requests.len() < MAX_IN_FLIGHT {
640      let Some((asked, command)) = state.queued.pop_front() else {
641        return;
642      };
643      match self.send(state, &command, asked) {
644        Sent::Out => {}
645        Sent::Full => return state.queued.push_front((asked, command)),
646        Sent::Failed => match asked {
647          Asked::List(query) => {
648            state.reading[query as usize] = Reading::Idle;
649            state.answered(query);
650          }
651          Asked::Status => {
652            // The switches waiting for it go, as a dispatch would.
653            state.status_asked = false;
654            state.focusing.clear();
655          }
656          Asked::Dispatch => {}
657        },
658      }
659    }
660  }
661
662  /// Connects and sends `command`. `Failed`, logged once, if Hyprland
663  /// can't be reached.
664  fn send(self: &Rc<Self>, state: &mut State, command: &str, asked: Asked) -> Sent {
665    let (Some(dir), Some(event_loop)) = (&self.shared.config.hyprland, self.shared.event_loop())
666    else {
667      return Sent::Failed;
668    };
669    let path = dir.join(".socket.sock");
670    let command = command.as_bytes();
671    let opened = open(&path).and_then(|mut stream| {
672      // One write: Hyprland takes what has arrived as the whole request.
673      if stream.write(command)? < command.len() {
674        return Err(std::io::Error::other("the request didn't fit the socket"));
675      }
676      // Hyprland 0.56 and older read until a short read or the end.
677      stream.shutdown(Shutdown::Write)?;
678      let fd = stream.as_fd().try_clone_to_owned()?;
679      Ok((stream, fd))
680    });
681    let (stream, fd) = match opened {
682      Ok(opened) => opened,
683      Err(error) if error.kind() == ErrorKind::WouldBlock => return Sent::Full,
684      Err(error) => {
685        if !std::mem::replace(&mut state.request_failure_logged, true) {
686          let path = path.display();
687          tracing::warn!(%path, %error, "can't send Hyprland a request");
688        }
689        return Sent::Failed;
690      }
691    };
692    state.request_failure_logged = false;
693    let id = state.next_request;
694    state.next_request += 1;
695    let service = Rc::clone(self);
696    let read = Box::new(move || service.on_reply(id));
697    let Some(reader) = event_loop.insert_fd(fd, Interest::READ, read) else {
698      return Sent::Failed;
699    };
700    state.requests.push(Request {
701      id,
702      asked,
703      stream,
704      reader,
705      reply: Vec::new(),
706    });
707    Sent::Out
708  }
709
710  /// Whether the lists are still there: their owner, the root, isn't
711  /// disposed.
712  fn alive(&self) -> bool {
713    let state = self.state.borrow();
714    let alive = state
715      .as_ref()
716      .and_then(|state| state.workspaces.try_with(|_| ()));
717    alive.is_some()
718  }
719
720  /// Reads what came on the event socket, and reads again what the
721  /// events changed.
722  fn on_events(self: &Rc<Self>) {
723    if !self.alive() {
724      return self.close();
725    }
726    let mut chunk = [0; 64 * 1024];
727    let mut guard = self.state.borrow_mut();
728    let state = guard
729      .as_mut()
730      .expect("invariant: started before connecting");
731    let Some(events) = state.events.as_mut() else {
732      return;
733    };
734    let lines = match events.stream.read(&mut chunk) {
735      Ok(0) => return self.closed(guard, "Hyprland closed its event socket"),
736      Ok(read) => events
737        .lines
738        .push(&chunk[..read], &"Hyprland's event socket"),
739      Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {
740        return;
741      }
742      Err(error) => return self.closed(guard, &error.to_string()),
743    };
744    if !lines.is_empty() {
745      state.reconnected = false;
746    }
747    // A burst of events reads each list once.
748    let mut stale = [false; 4];
749    let mut events = Vec::new();
750    let mut urgency = false;
751    for line in lines {
752      let Some((name, data)) = line.split_once(">>") else {
753        let line = cut(line);
754        tracing::warn!(line, "a line from Hyprland's event socket without `>>`");
755        continue;
756      };
757      for &query in Query::after(name) {
758        stale[query as usize] = true;
759      }
760      // Events name a window without JSON's `0x`.
761      let address = || format!("0x{data}");
762      urgency |= match name {
763        "urgent" => state.urgent.len() < MAX_LIST && state.urgent.insert(address()),
764        // Focus ends a window's urgency, as in Hyprland.
765        "activewindowv2" | "closewindow" => state.urgent.remove(&address()),
766        _ => false,
767      };
768      events.push(HyprlandEvent {
769        name: cut(name.to_owned()),
770        data: cut(data.to_owned()),
771      });
772    }
773    if urgency {
774      state.show();
775    }
776    let burst = state.burst();
777    for query in Query::ALL
778      .into_iter()
779      .filter(|&query| stale[query as usize])
780    {
781      self.read(state, query, burst);
782    }
783    drop(guard);
784    for event in events {
785      self.deliver(&event);
786    }
787  }
788
789  /// Calls each raw-event handler with `event`, with nothing borrowed.
790  fn deliver(&self, event: &HyprlandEvent) {
791    let handlers = match self.state.borrow().as_ref() {
792      Some(state) => state.handlers.clone(),
793      None => return,
794    };
795    for Handler { id, at, handler } in handlers {
796      let registered = self
797        .state
798        .borrow()
799        .as_ref()
800        .is_some_and(|state| state.handlers.iter().any(|handler| handler.id == id));
801      if !registered {
802        // An earlier handler's context went.
803        continue;
804      }
805      if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| handler(event))) {
806        let message = panic_message(payload.as_ref());
807        let name = &event.name;
808        tracing::error!("Hyprland on_event (called at {at}) panicked on {name}: {message}");
809        drop(self.remove_handler(id));
810      }
811    }
812  }
813
814  fn add_handler(
815    &self,
816    handler: Rc<dyn Fn(&HyprlandEvent)>,
817    at: &'static Location<'static>,
818  ) -> u64 {
819    let mut state = self.state.borrow_mut();
820    let state = state.as_mut().expect("invariant: started by cx.hyprland()");
821    let id = state.next_handler;
822    state.next_handler += 1;
823    state.handlers.push(Handler { id, at, handler });
824    id
825  }
826
827  /// Takes out the handler `id`, for the caller to drop once nothing is
828  /// borrowed.
829  fn remove_handler(&self, id: u64) -> Option<Handler> {
830    let mut state = self.state.borrow_mut();
831    let handlers = &mut state.as_mut()?.handlers;
832    let at = handlers.iter().position(|handler| handler.id == id)?;
833    Some(handlers.remove(at))
834  }
835
836  /// Reads what came of a reply, and acts on it once Hyprland closed the
837  /// connection.
838  fn on_reply(self: &Rc<Self>, id: u64) {
839    if !self.alive() {
840      return self.close();
841    }
842    let mut chunk = [0; 64 * 1024];
843    let mut guard = self.state.borrow_mut();
844    let state = guard.as_mut().expect("invariant: started before reading");
845    let Some(at) = state.requests.iter().position(|request| request.id == id) else {
846      return;
847    };
848    let request = &mut state.requests[at];
849    let failed = match request.stream.read(&mut chunk) {
850      Ok(0) => None,
851      Ok(read) if request.reply.len() + read > MAX_REPLY => Some(String::from("over 8 MiB")),
852      Ok(read) => return request.reply.extend_from_slice(&chunk[..read]),
853      Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {
854        return;
855      }
856      Err(error) => Some(error.to_string()),
857    };
858    let request = state.requests.remove(at);
859    if let Some(event_loop) = self.shared.event_loop() {
860      event_loop.remove(request.reader);
861    }
862    self.send_queued(state);
863    let query = match request.asked {
864      Asked::List(query) => query,
865      Asked::Dispatch => {
866        let reply = String::from_utf8_lossy(&request.reply);
867        if failed.is_some() || reply.trim() != "ok" {
868          let reply = cut(reply.into_owned());
869          tracing::warn!(reply, failed, "Hyprland refused a dispatch");
870        }
871        return;
872      }
873      Asked::Status => {
874        state.status_asked = false;
875        let lua = failed
876          .is_none()
877          .then(|| reads_lua(&request.reply))
878          .flatten();
879        if lua.is_none() {
880          let reply = cut(String::from_utf8_lossy(&request.reply).into_owned());
881          tracing::warn!(
882            reply,
883            failed,
884            "Hyprland's status names no config; dispatching in the old syntax"
885          );
886        }
887        let lua = lua.unwrap_or(false);
888        state.lua = Some(lua);
889        let focusing = std::mem::take(&mut state.focusing);
890        drop(guard);
891        for target in focusing {
892          self.switch(lua, &target);
893        }
894        return;
895      }
896    };
897    match failed {
898      Some(why) => {
899        tracing::warn!(
900          command = query.command(),
901          why,
902          "Hyprland's reply was dropped"
903        );
904      }
905      None => {
906        state.raw.apply(query, &request.reply);
907        state.recheck(query, request.id);
908      }
909    }
910    state.answered(query);
911    if state.reading[query as usize] == Reading::Stale {
912      // For a later event, so no show waits for it.
913      state.reading[query as usize] = Reading::Busy;
914      self.request(state, query.command(), Asked::List(query));
915    } else {
916      state.reading[query as usize] = Reading::Idle;
917    }
918    state.show();
919  }
920
921  /// The event socket closed for `why`. Hyprland closes a client that
922  /// falls 64 events behind and runs on, so while its socket is there,
923  /// this connects once again and reads every list, keeping them
924  /// meanwhile. A connection that closes again before an event came is
925  /// lost.
926  fn closed(self: &Rc<Self>, mut guard: RefMut<'_, Option<State>>, why: &str) {
927    let state = guard
928      .as_mut()
929      .expect("invariant: started before connecting");
930    let dir = self.shared.config.hyprland.as_deref();
931    let Some(dir) = dir.filter(|dir| !state.reconnected && dir.join(".socket2.sock").exists())
932    else {
933      return self.lost(state, why);
934    };
935    tracing::warn!(why, "Hyprland closed its event socket; connecting again");
936    state.reconnected = true;
937    state.recheck = Some(Recheck {
938      urgent: state.urgent.clone(),
939      from: state.next_request,
940      windows: false,
941      active: false,
942    });
943    if let (Some(events), Some(event_loop)) = (state.events.take(), self.shared.event_loop()) {
944      event_loop.remove(events.reader);
945    }
946    drop(guard);
947    if !self.connect(dir)
948      && let Some(state) = self.state.borrow_mut().as_mut()
949    {
950      self.lost(state, "Hyprland's event socket can't be reached again");
951    }
952  }
953
954  /// Drops every connection and empties the lists. Hyprland doesn't
955  /// come back under the same signature, so nothing reconnects.
956  fn lost(&self, state: &mut State, why: &str) {
957    tracing::warn!(why, "lost Hyprland; its lists are empty");
958    self.drop_connections(state);
959    state.raw = Raw::default();
960    state.urgent.clear();
961    state.show();
962  }
963
964  /// Closes every connection for good, once the lists are gone.
965  fn close(&self) {
966    if let Some(state) = self.state.borrow_mut().as_mut() {
967      self.drop_connections(state);
968    }
969  }
970
971  fn drop_connections(&self, state: &mut State) {
972    let events = state.events.take();
973    let requests = std::mem::take(&mut state.requests);
974    state.reading = [Reading::Idle; 4];
975    state.burst_of = [None; 4];
976    state.bursts.clear();
977    state.queued.clear();
978    state.status_asked = false;
979    state.focusing.clear();
980    let Some(event_loop) = self.shared.event_loop() else {
981      return;
982    };
983    let readers = events.map(|events| events.reader).into_iter();
984    for reader in readers.chain(requests.into_iter().map(|request| request.reader)) {
985      event_loop.remove(reader);
986    }
987  }
988}
989
990impl State {
991  /// After a reconnect, ends the urgency of a window from before it that
992  /// `query`'s answer, to the request `id`, says was focused or closed.
993  fn recheck(&mut self, query: Query, id: u64) {
994    let State {
995      recheck,
996      urgent,
997      raw,
998      ..
999    } = self;
1000    let Some(check) = recheck.as_mut().filter(|check| id >= check.from) else {
1001      return;
1002    };
1003    match query {
1004      Query::Clients => {
1005        let listed: HashSet<&str> = raw
1006          .windows
1007          .iter()
1008          .map(|window| window.address.as_str())
1009          .collect();
1010        for address in check
1011          .urgent
1012          .iter()
1013          .filter(|address| !listed.contains(address.as_str()))
1014        {
1015          urgent.remove(address);
1016        }
1017        check.windows = true;
1018      }
1019      Query::ActiveWindow => {
1020        if let Some(active) = raw
1021          .active
1022          .as_ref()
1023          .filter(|active| check.urgent.contains(*active))
1024        {
1025          urgent.remove(active);
1026        }
1027        check.active = true;
1028      }
1029      Query::Workspaces | Query::Monitors => return,
1030    }
1031    if check.windows && check.active {
1032      *recheck = None;
1033    }
1034  }
1035
1036  /// A new burst of reads.
1037  fn burst(&mut self) -> u64 {
1038    self.next_burst += 1;
1039    self.next_burst
1040  }
1041
1042  /// `query`'s read for a burst, if it was one, is answered.
1043  fn answered(&mut self, query: Query) {
1044    let Some(id) = self.burst_of[query as usize].take() else {
1045      return;
1046    };
1047    if let Some(at) = self.bursts.iter().position(|open| open.id == id) {
1048      self.bursts[at].out -= 1;
1049      if self.bursts[at].out == 0 {
1050        self.bursts.remove(at);
1051      }
1052    }
1053  }
1054
1055  /// Builds the lists from what Hyprland last said, and sets each that
1056  /// changed. Nothing shows while a burst has some reads answered and
1057  /// others not, so a frame never mixes one list's answer to an event
1058  /// with another's from before it.
1059  fn show(&self) {
1060    if self.bursts.iter().any(|open| open.out < open.started) {
1061      return;
1062    }
1063    let raw = &self.raw;
1064    let focused = raw.monitors.iter().find(|monitor| monitor.focused);
1065    let urgent: Vec<_> = raw
1066      .windows
1067      .iter()
1068      .filter(|window| self.urgent.contains(&window.address))
1069      .map(|window| &window.workspace)
1070      .collect();
1071    let mut workspaces: Vec<_> = raw
1072      .workspaces
1073      .iter()
1074      .map(|workspace| HyprlandWorkspace {
1075        id: workspace.at.id,
1076        name: workspace.at.name.clone(),
1077        monitor: workspace.monitor.clone(),
1078        windows: workspace.windows,
1079        focused: focused.is_some_and(|monitor| monitor.shows(&workspace.at)),
1080        active: raw
1081          .monitors
1082          .iter()
1083          .any(|monitor| monitor.name == workspace.monitor && monitor.shows(&workspace.at)),
1084        urgent: urgent.iter().any(|at| at.is(&workspace.at)),
1085      })
1086      .collect();
1087    // Named ones first, as Hyprland before 0.57 numbers them below zero.
1088    workspaces.sort_by_key(|workspace| workspace.id.unwrap_or(i64::MIN));
1089    let monitors = raw.monitors.iter().map(|monitor| HyprlandMonitor {
1090      id: monitor.id,
1091      name: monitor.name.clone(),
1092      description: monitor.description.clone(),
1093      focused: monitor.focused,
1094      active_workspace: monitor.active.name.clone(),
1095      x: monitor.x,
1096      y: monitor.y,
1097      width: monitor.width,
1098      height: monitor.height,
1099      scale: monitor.scale,
1100    });
1101    let toplevels = raw.windows.iter().map(|window| HyprlandToplevel {
1102      address: window.address.clone(),
1103      class: window.class.clone(),
1104      title: window.title.clone(),
1105      workspace: window.workspace.name.clone(),
1106      monitor: raw
1107        .monitors
1108        .iter()
1109        .find(|monitor| monitor.id == window.monitor)
1110        .map_or_else(String::new, |monitor| monitor.name.clone()),
1111      floating: window.floating,
1112      fullscreen: window.fullscreen.is_fullscreen(),
1113      focused: raw.active.as_ref() == Some(&window.address),
1114      urgent: self.urgent.contains(&window.address),
1115    });
1116    set_changed(self.workspaces, workspaces);
1117    set_changed(self.monitors, monitors.collect());
1118    set_changed(self.toplevels, toplevels.collect());
1119  }
1120}
1121
1122impl Raw {
1123  /// Keeps what `reply` says for `query`. A reply that doesn't parse is
1124  /// logged and the last one kept; an entry that doesn't is left out.
1125  fn apply(&mut self, query: Query, reply: &[u8]) {
1126    match query {
1127      Query::Workspaces => {
1128        if let Some(list) = list("workspace", reply) {
1129          self.workspaces = list;
1130        }
1131      }
1132      Query::Monitors => {
1133        if let Some(list) = list("monitor", reply) {
1134          self.monitors = list;
1135        }
1136      }
1137      Query::Clients => {
1138        if let Some(list) = list("window", reply) {
1139          self.windows = list;
1140        }
1141      }
1142      Query::ActiveWindow => match serde_json::from_slice::<ActiveWindow>(reply) {
1143        Ok(active) => self.active = active.address,
1144        Err(error) => tracing::warn!(%error, "a bad focused window from Hyprland"),
1145      },
1146    }
1147  }
1148}
1149
1150/// The entries of a JSON list of `what`s, at most [`MAX_LIST`], leaving
1151/// out those that don't parse. `None`, logged, for a reply that isn't a
1152/// list.
1153fn list<T: DeserializeOwned>(what: &str, reply: &[u8]) -> Option<Vec<T>> {
1154  let entries: Vec<serde_json::Value> = serde_json::from_slice(reply)
1155    .inspect_err(|error| tracing::warn!(%error, "a bad {what} list from Hyprland"))
1156    .ok()?;
1157  if entries.len() > MAX_LIST {
1158    let listed = entries.len();
1159    tracing::warn!(
1160      listed,
1161      "Hyprland listed more {what}s than the {MAX_LIST} kept"
1162    );
1163  }
1164  let mut bad = 0;
1165  let list = entries
1166    .into_iter()
1167    .take(MAX_LIST)
1168    .filter_map(|entry| serde_json::from_value(entry).inspect_err(|_| bad += 1).ok())
1169    .collect();
1170  if bad > 0 {
1171    tracing::warn!(
1172      bad,
1173      "Hyprland described some {what}s badly; they are left out"
1174    );
1175  }
1176  Some(list)
1177}
1178
1179/// A workspace as a monitor or a window names it: by number while it has
1180/// one, and from Hyprland 0.57 on by address otherwise.
1181#[derive(Deserialize)]
1182struct WorkspaceRef {
1183  id: Option<i64>,
1184  #[serde(default, deserialize_with = "some_text")]
1185  address: Option<String>,
1186  #[serde(deserialize_with = "text")]
1187  name: String,
1188}
1189
1190impl WorkspaceRef {
1191  fn is(&self, other: &WorkspaceRef) -> bool {
1192    match (self.id, other.id) {
1193      (Some(id), Some(other)) => id == other,
1194      _ => self.address.is_some() && self.address == other.address,
1195    }
1196  }
1197}
1198
1199#[derive(Deserialize)]
1200struct RawWorkspace {
1201  #[serde(flatten)]
1202  at: WorkspaceRef,
1203  #[serde(deserialize_with = "text")]
1204  monitor: String,
1205  windows: u32,
1206}
1207
1208#[derive(Deserialize)]
1209struct RawMonitor {
1210  id: i64,
1211  #[serde(deserialize_with = "text")]
1212  name: String,
1213  #[serde(deserialize_with = "text")]
1214  description: String,
1215  focused: bool,
1216  x: i32,
1217  y: i32,
1218  width: i32,
1219  height: i32,
1220  scale: f64,
1221  #[serde(rename = "activeWorkspace")]
1222  active: WorkspaceRef,
1223  /// The special workspace shown over `active`, which names none as
1224  /// `{"id":0,"name":""}`, or from 0.57 on with an empty address.
1225  #[serde(rename = "specialWorkspace", default)]
1226  special: Option<WorkspaceRef>,
1227}
1228
1229impl RawMonitor {
1230  /// Whether it shows `workspace`, as its active or its special one.
1231  fn shows(&self, workspace: &WorkspaceRef) -> bool {
1232    self.active.is(workspace)
1233      || self
1234        .special
1235        .as_ref()
1236        .is_some_and(|special| special.is(workspace))
1237  }
1238}
1239
1240#[derive(Deserialize)]
1241struct RawWindow {
1242  #[serde(deserialize_with = "text")]
1243  address: String,
1244  #[serde(deserialize_with = "text")]
1245  class: String,
1246  #[serde(deserialize_with = "text")]
1247  title: String,
1248  workspace: WorkspaceRef,
1249  monitor: i64,
1250  floating: bool,
1251  fullscreen: Fullscreen,
1252}
1253
1254/// A window's fullscreen state: a flag in older Hyprland, then its mode,
1255/// where 1 is maximized and 2 fullscreen.
1256#[derive(Deserialize)]
1257#[serde(untagged)]
1258enum Fullscreen {
1259  Flag(bool),
1260  Mode(i64),
1261}
1262
1263impl Fullscreen {
1264  fn is_fullscreen(&self) -> bool {
1265    match *self {
1266      Fullscreen::Flag(flag) => flag,
1267      Fullscreen::Mode(mode) => mode & 2 != 0,
1268    }
1269  }
1270}
1271
1272/// `j/activewindow`'s reply: the focused window, or `{}` for none.
1273#[derive(Deserialize)]
1274struct ActiveWindow {
1275  #[serde(default, deserialize_with = "some_text")]
1276  address: Option<String>,
1277}
1278
1279/// A string from Hyprland, cut to [`MAX_TEXT`].
1280fn text<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
1281  String::deserialize(deserializer).map(cut)
1282}
1283
1284fn some_text<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
1285  text(deserializer).map(Some)
1286}
1287
1288/// A non-blocking connection to the Unix socket at `path`, made without
1289/// waiting: a listen backlog that is full is `WouldBlock`.
1290fn open(path: &Path) -> std::io::Result<UnixStream> {
1291  use rustix::net::{AddressFamily, SocketAddrUnix, SocketFlags, SocketType};
1292  let flags = SocketFlags::NONBLOCK | SocketFlags::CLOEXEC;
1293  let fd = rustix::net::socket_with(AddressFamily::UNIX, SocketType::STREAM, flags, None)?;
1294  match rustix::net::connect(&fd, &SocketAddrUnix::new(path)?) {
1295    Ok(()) => Ok(UnixStream::from(fd)),
1296    // A Unix socket never connects in the background; were one to, it
1297    // would be as full.
1298    Err(rustix::io::Errno::INPROGRESS) => Err(ErrorKind::WouldBlock.into()),
1299    Err(errno) => Err(errno.into()),
1300  }
1301}
1302
1303/// `text` cut to [`MAX_TEXT`] bytes at a character.
1304fn cut(mut text: String) -> String {
1305  if text.len() > MAX_TEXT {
1306    text.truncate(text.floor_char_boundary(MAX_TEXT));
1307  }
1308  text
1309}
1310
1311/// `text` as a Lua string literal: quoted, with backslashes, quotes and
1312/// control characters escaped, so nothing in it runs.
1313fn lua_string(text: &str) -> String {
1314  let mut quoted = String::from("\"");
1315  for character in text.chars() {
1316    match character {
1317      '\\' | '"' => {
1318        quoted.push('\\');
1319        quoted.push(character);
1320      }
1321      // Three digits, so a digit after it isn't read as part of it.
1322      control if control.is_ascii_control() => {
1323        quoted.push_str(&format!("\\{:03}", u32::from(control)));
1324      }
1325      other => quoted.push(other),
1326    }
1327  }
1328  quoted.push('"');
1329  quoted
1330}
1331
1332/// What `j/status` says, from Hyprland 0.55 on.
1333#[derive(Deserialize)]
1334struct Status {
1335  /// `lua` or `hyprlang`.
1336  #[serde(rename = "configProvider")]
1337  config_provider: String,
1338}
1339
1340/// Whether the Hyprland that sent `reply` to `j/status` reads dispatches
1341/// as Lua, as it does when its config is. Hyprland before 0.55 knows no
1342/// status and reads the old syntax. `None` when the reply says neither.
1343fn reads_lua(reply: &[u8]) -> Option<bool> {
1344  if reply.trim_ascii() == b"unknown request" {
1345    return Some(false);
1346  }
1347  let Status { config_provider } = serde_json::from_slice(reply).ok()?;
1348  match config_provider.as_str() {
1349    "lua" => Some(true),
1350    "hyprlang" => Some(false),
1351    _ => None,
1352  }
1353}