Skip to main content

crabshell_services/
sway.rs

1//! Sway's workspaces, outputs and events, over Sway's IPC socket.
2
3mod ipc;
4#[cfg(test)]
5mod tests;
6
7use std::cell::RefCell;
8use std::collections::VecDeque;
9use std::fmt::Display;
10use std::io::{ErrorKind, Read, Write};
11use std::os::fd::AsFd;
12use std::os::unix::net::UnixStream;
13use std::panic::{self, AssertUnwindSafe, Location};
14use std::path::PathBuf;
15use std::rc::Rc;
16use std::sync::Once;
17use std::time::{Duration, Instant};
18
19use calloop::timer::TimeoutAction;
20use calloop::{Interest, RegistrationToken};
21use crabshell_reactive::{Owner, ReadSignal, Signal, untrack};
22use crabshell_ui::Cx;
23use serde::Deserialize;
24use serde::de::IgnoredAny;
25
26use crate::Shared;
27use crate::lines::panic_message;
28
29/// How long the service waits before each reconnect.
30pub(crate) const RETRY: Duration = Duration::from_secs(2);
31
32/// One of Sway's workspaces, as `GET_WORKSPACES` describes it.
33#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
34pub struct Workspace {
35  /// Sway's id for it, which stays the same while it exists: the key to
36  /// give `children_keyed`.
37  pub id: i64,
38  /// Its name, such as `1` or `3:web`.
39  pub name: String,
40  /// The output it is on, such as `DP-1`.
41  pub output: String,
42  /// Whether it has the keyboard focus.
43  pub focused: bool,
44  /// Whether it is shown on its output.
45  pub visible: bool,
46  /// Whether a window on it asks for attention.
47  pub urgent: bool,
48}
49
50/// One of Sway's outputs, a monitor, as `GET_OUTPUTS` describes it and
51/// as Quickshell's `I3Monitor` does.
52#[derive(Clone, Debug, PartialEq, Deserialize)]
53#[serde(from = "RawOutput")]
54pub struct SwayOutput {
55  /// Its name, such as `DP-1`.
56  pub name: String,
57  /// Whether it shows the focused workspace.
58  pub focused: bool,
59  /// Whether it is enabled. One that isn't has no workspace and no size.
60  pub active: bool,
61  /// The name of the workspace it shows. `None` while it isn't active.
62  pub current_workspace: Option<String>,
63  /// Where its top-left corner is in the layout, in logical pixels.
64  pub x: i32,
65  /// See [`x`](Self::x).
66  pub y: i32,
67  /// Its size in logical pixels: its mode's size divided by its scale.
68  pub width: u32,
69  /// See [`width`](Self::width).
70  pub height: u32,
71  /// Its scale, such as 1.5. 1 for an output that isn't active, which
72  /// Sway gives -1 and SwayFX none.
73  pub scale: f64,
74}
75
76/// An output as Sway sends it. A non-desktop one, such as a VR headset,
77/// comes without `active`, `rect` or `scale`.
78#[derive(Deserialize)]
79struct RawOutput {
80  name: String,
81  #[serde(default)]
82  focused: bool,
83  #[serde(default)]
84  active: bool,
85  current_workspace: Option<String>,
86  #[serde(default)]
87  rect: RawRect,
88  scale: Option<f64>,
89}
90
91#[derive(Default, Deserialize)]
92struct RawRect {
93  x: i32,
94  y: i32,
95  width: u32,
96  height: u32,
97}
98
99impl From<RawOutput> for SwayOutput {
100  fn from(raw: RawOutput) -> SwayOutput {
101    SwayOutput {
102      name: raw.name,
103      focused: raw.focused,
104      active: raw.active,
105      current_workspace: raw.current_workspace,
106      x: raw.rect.x,
107      y: raw.rect.y,
108      width: raw.rect.width,
109      height: raw.rect.height,
110      scale: raw.scale.filter(|scale| *scale > 0.0).unwrap_or(1.0),
111    }
112  }
113}
114
115/// A kind of event Sway sends, for [`Sway::on_event`]. sway-ipc(7) says
116/// what each one's data holds.
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub enum SwayEventKind {
119  /// A workspace was made, emptied, focused, moved, renamed or marked
120  /// urgent.
121  Workspace,
122  /// An output was added, removed or changed.
123  Output,
124  /// The binding mode changed, as to `resize`.
125  Mode,
126  /// A window opened, closed or moved, or its focus, title or state
127  /// changed.
128  Window,
129  /// A bar's config changed.
130  BarconfigUpdate,
131  /// A key or mouse binding ran.
132  Binding,
133  /// Sway is exiting.
134  Shutdown,
135  /// A program sent a tick; also sent once right after subscribing.
136  Tick,
137  /// A bar was shown or hidden by its modifier.
138  BarStateUpdate,
139  /// An input device was added, removed or changed.
140  Input,
141}
142
143impl SwayEventKind {
144  const ALL: [SwayEventKind; 10] = [
145    SwayEventKind::Workspace,
146    SwayEventKind::Output,
147    SwayEventKind::Mode,
148    SwayEventKind::Window,
149    SwayEventKind::BarconfigUpdate,
150    SwayEventKind::Binding,
151    SwayEventKind::Shutdown,
152    SwayEventKind::Tick,
153    SwayEventKind::BarStateUpdate,
154    SwayEventKind::Input,
155  ];
156
157  /// Its message type and the name `SUBSCRIBE` takes, from sway-ipc(7).
158  fn wire(self) -> (u32, &'static str) {
159    match self {
160      SwayEventKind::Workspace => (ipc::WORKSPACE_EVENT, "workspace"),
161      SwayEventKind::Output => (ipc::OUTPUT_EVENT, "output"),
162      SwayEventKind::Mode => (0x8000_0002, "mode"),
163      SwayEventKind::Window => (0x8000_0003, "window"),
164      SwayEventKind::BarconfigUpdate => (0x8000_0004, "barconfig_update"),
165      SwayEventKind::Binding => (0x8000_0005, "binding"),
166      SwayEventKind::Shutdown => (0x8000_0006, "shutdown"),
167      SwayEventKind::Tick => (0x8000_0007, "tick"),
168      SwayEventKind::BarStateUpdate => (0x8000_0014, "bar_state_update"),
169      SwayEventKind::Input => (0x8000_0015, "input"),
170    }
171  }
172
173  fn from_code(code: u32) -> Option<SwayEventKind> {
174    SwayEventKind::ALL
175      .into_iter()
176      .find(|kind| kind.wire().0 == code)
177  }
178}
179
180/// One event from Sway, as Quickshell's `I3Event`, for
181/// [`Sway::on_event`].
182#[derive(Clone, Debug, PartialEq, Eq)]
183pub struct SwayEvent {
184  /// Which kind it is.
185  pub kind: SwayEventKind,
186  /// Its JSON as Sway sent it, an object sway-ipc(7) describes for each
187  /// kind: a mode event's has `change`, the mode's name, and
188  /// `pango_markup`.
189  pub data: String,
190}
191
192/// Where Sway listens.
193pub(crate) struct Socket {
194  pub(crate) path: PathBuf,
195  /// How long to wait before each reconnect: [`RETRY`], less in tests.
196  pub(crate) retry: Duration,
197}
198
199/// The Sway service, from `cx.sway()`. A clone is cheap and shares the
200/// one connection.
201#[derive(Clone)]
202pub struct Sway {
203  /// `None` when the shell provided no services.
204  service: Option<Rc<Service>>,
205  workspaces: ReadSignal<Vec<Workspace>>,
206  /// The asking context's owner, for what a config makes through it.
207  owner: Owner,
208}
209
210impl Sway {
211  /// The service if a config has started it with `cx.sway()`, without
212  /// starting it, so the shell can read [`changed_at`](Self::changed_at)
213  /// and a bar that shows no workspaces never connects.
214  #[doc(hidden)]
215  pub fn started(cx: &Cx) -> Option<Sway> {
216    let service = cx.service::<Service>()?;
217    let workspaces = service.state.borrow().as_ref()?.workspaces.read_only();
218    Some(Sway {
219      service: Some(service),
220      workspaces,
221      owner: cx.owner(),
222    })
223  }
224
225  /// Every workspace on every output, in Sway's order. Empty while Sway
226  /// isn't connected.
227  pub fn workspaces(&self) -> ReadSignal<Vec<Workspace>> {
228    self.workspaces
229  }
230
231  /// Every output, enabled or not, in Sway's order, kept current from
232  /// Sway's output and workspace events. Empty while Sway isn't
233  /// connected. Sway is asked for them on the first call, so a bar that
234  /// shows only workspaces never wakes for an output's change.
235  pub fn outputs(&self) -> ReadSignal<Vec<SwayOutput>> {
236    match &self.service {
237      Some(service) => service.outputs(),
238      None => self.owner.signal(Vec::new()).read_only(),
239    }
240  }
241
242  /// When the workspace event that led to the current list arrived, so
243  /// the shell can log how long a switch took to reach the screen.
244  /// `None` for a list that followed no event, such as the first.
245  pub fn changed_at(&self) -> Option<Instant> {
246    let state = self.service.as_ref()?.state.borrow();
247    state.as_ref()?.changed_at
248  }
249
250  /// Switches to the workspace called `name`, as clicking it in swaybar
251  /// does. A name Sway reads as a keyword, such as `next`, is logged and
252  /// not sent, and so is a call while Sway isn't connected. Sway's
253  /// refusal is logged too.
254  pub fn focus_workspace(&self, name: &str) {
255    let Some(command) = ipc::workspace_command(name) else {
256      tracing::warn!(name, "Sway can't switch to this workspace by name");
257      return;
258    };
259    self.dispatch(&command);
260  }
261
262  /// Runs `command` as Sway commands, as `swaymsg` and Quickshell's
263  /// `I3.dispatch` do, such as `layout tabbed` or `[app_id=foot] kill`.
264  /// It is sent as written, so quote a name that has spaces. A call while
265  /// Sway isn't connected is logged and dropped, and so is a command over
266  /// 4 MiB. Sway's refusal is logged too.
267  pub fn dispatch(&self, command: &str) {
268    if command.len() > ipc::MAX_PAYLOAD {
269      let bytes = command.len();
270      return tracing::warn!(bytes, "a Sway command over 4 MiB isn't sent");
271    }
272    let service = self.service.as_ref();
273    if !service.is_some_and(|service| service.send(ipc::RUN_COMMAND, command.as_bytes())) {
274      tracing::warn!(
275        command,
276        "can't run a Sway command while Sway isn't connected"
277      );
278    }
279  }
280
281  /// Calls `handler` with each event of the `kinds` given, as Quickshell's
282  /// `I3IpcListener`, for as long as the context `cx.sway()` was called on
283  /// lives. Sway is asked only for kinds some handler wants, so a handler
284  /// of mode changes never wakes the shell for a window's.
285  ///
286  /// Sway can't unsubscribe, so once the last handler of a kind goes, the
287  /// next event of that kind makes the service connect again without it.
288  /// Events in between may be missed.
289  ///
290  /// It runs on the main thread. If it panics, that is logged with where
291  /// `on_event` was called, and it isn't called again. Called on a handle
292  /// whose context went, it logs that and does nothing.
293  #[track_caller]
294  pub fn on_event(&self, kinds: &[SwayEventKind], handler: impl Fn(&SwayEvent) + 'static) {
295    let at = Location::caller();
296    if self.owner.is_disposed() {
297      return tracing::warn!(
298        "Sway on_event (called at {at}) on a context that is gone does nothing"
299      );
300    }
301    let Some(service) = &self.service else {
302      return;
303    };
304    let id = service.listen(kinds, Rc::new(handler), at);
305    // Dropped with the asking context's owner, which takes the handler away.
306    self.owner.signal(Unlisten(Rc::clone(service), id));
307  }
308}
309
310/// A config's handler of some kinds of events, from `on_event`.
311struct Listener {
312  id: u64,
313  kinds: Vec<SwayEventKind>,
314  handler: Rc<dyn Fn(&SwayEvent)>,
315  /// Where `on_event` was called, for the log if it panics.
316  at: &'static Location<'static>,
317}
318
319/// Takes a listener away when its context's owner drops this.
320struct Unlisten(Rc<Service>, u64);
321
322impl Drop for Unlisten {
323  fn drop(&mut self) {
324    // Dropped outside the borrow: the handler may hold a `Sway`.
325    drop(self.0.unlisten(self.1));
326  }
327}
328
329/// `cx.sway()`.
330pub(crate) fn sway(cx: &Cx) -> Sway {
331  let Some(service) = cx.service::<Service>() else {
332    static LOGGED: Once = Once::new();
333    LOGGED.call_once(|| {
334      tracing::warn!("cx.sway() needs the services the shell provides; it shows no workspaces");
335    });
336    return Sway {
337      service: None,
338      workspaces: cx.signal(Vec::new()).read_only(),
339      owner: cx.owner(),
340    };
341  };
342  let workspaces = service.start();
343  Sway {
344    service: Some(service),
345    workspaces,
346    owner: cx.owner(),
347  }
348}
349
350/// The one Sway connection of a shell, started on first use.
351pub(crate) struct Service {
352  shared: Rc<Shared>,
353  state: RefCell<Option<State>>,
354}
355
356struct State {
357  workspaces: Signal<Vec<Workspace>>,
358  /// `None` until a config asks for the outputs, so a bar that shows only
359  /// workspaces never subscribes to output events.
360  outputs: Option<Signal<Vec<SwayOutput>>>,
361  changed_at: Option<Instant>,
362  /// The config's handlers, in the order given.
363  listeners: Vec<Listener>,
364  /// The id the next listener gets.
365  next_listener: u64,
366  connection: Option<Connection>,
367  /// Whether the current outage was logged, so retries stay quiet.
368  outage_logged: bool,
369}
370
371struct Connection {
372  stream: UnixStream,
373  /// The loop source that waits for something to read.
374  reader: RegistrationToken,
375  /// The loop source that waits for room to write, while the socket
376  /// holds back part of `outbox`.
377  writer: Option<RegistrationToken>,
378  /// Bytes read that don't make a whole message yet.
379  inbox: Vec<u8>,
380  /// Bytes not written yet.
381  outbox: Vec<u8>,
382  /// One entry per `GET_WORKSPACES` sent and not yet answered, in order:
383  /// when the event behind it arrived, or `None` for the first.
384  fetches: VecDeque<Option<Instant>>,
385  /// The events this connection subscribed to. Sway has no way to
386  /// unsubscribe.
387  subscribed: Vec<&'static str>,
388  /// Whether an event came that nothing wants, as after the last handler
389  /// of its kind went.
390  stale: bool,
391  /// Requests sent that Sway hasn't answered yet.
392  awaiting: usize,
393}
394
395/// Sway's answer to a `SUBSCRIBE` or to one command.
396#[derive(Deserialize)]
397struct Outcome {
398  success: bool,
399  /// Why not, when `success` is false.
400  #[serde(default)]
401  error: String,
402}
403
404impl Service {
405  pub(crate) fn new(shared: Rc<Shared>) -> Service {
406    Service {
407      shared,
408      state: RefCell::new(None),
409    }
410  }
411
412  /// The list, connecting to Sway if this is the first ask.
413  fn start(self: &Rc<Self>) -> ReadSignal<Vec<Workspace>> {
414    if let Some(state) = &*self.state.borrow() {
415      return state.workspaces.read_only();
416    }
417    let workspaces = self.shared.root.signal(Vec::new());
418    *self.state.borrow_mut() = Some(State {
419      workspaces,
420      outputs: None,
421      changed_at: None,
422      listeners: Vec::new(),
423      next_listener: 0,
424      connection: None,
425      outage_logged: false,
426    });
427    if self.shared.config.sway.is_none() {
428      tracing::info!("SWAYSOCK is unset, so the bar shows no workspaces");
429    } else {
430      // A failed first connect isn't retried: SWAYSOCK is stale, or
431      // another compositor runs, and retries would wake the shell.
432      self.connect(true);
433    }
434    workspaces.read_only()
435  }
436
437  /// The outputs, asking Sway for them if this is the first ask.
438  fn outputs(self: &Rc<Self>) -> ReadSignal<Vec<SwayOutput>> {
439    let mut state = self.state.borrow_mut();
440    let state = state.as_mut().expect("invariant: started by cx.sway()");
441    if let Some(outputs) = state.outputs {
442      return outputs.read_only();
443    }
444    let outputs = self.shared.root.signal(Vec::new());
445    state.outputs = Some(outputs);
446    let wanted = state.wanted();
447    if let Some(connection) = state.connection.as_mut() {
448      connection.subscribe(&wanted);
449      connection.request(ipc::GET_OUTPUTS, b"");
450      self.flush_with(state);
451    }
452    outputs.read_only()
453  }
454
455  /// Adds a handler, subscribing to the kinds no other wants yet. Its id.
456  fn listen(
457    self: &Rc<Self>,
458    kinds: &[SwayEventKind],
459    handler: Rc<dyn Fn(&SwayEvent)>,
460    at: &'static Location<'static>,
461  ) -> u64 {
462    let mut state = self.state.borrow_mut();
463    let state = state.as_mut().expect("invariant: started by cx.sway()");
464    state.next_listener += 1;
465    let id = state.next_listener;
466    state.listeners.push(Listener {
467      id,
468      kinds: kinds.to_vec(),
469      handler,
470      at,
471    });
472    let wanted = state.wanted();
473    if let Some(connection) = state.connection.as_mut() {
474      connection.subscribe(&wanted);
475      self.flush_with(state);
476    }
477    id
478  }
479
480  /// Takes a handler away, handing it back to drop outside the borrow.
481  fn unlisten(&self, id: u64) -> Option<Listener> {
482    let mut state = self.state.borrow_mut();
483    let listeners = &mut state.as_mut()?.listeners;
484    let index = listeners.iter().position(|listener| listener.id == id)?;
485    Some(listeners.remove(index))
486  }
487
488  fn listening(&self, id: u64) -> bool {
489    let state = self.state.borrow();
490    let mut listeners = state.iter().flat_map(|state| &state.listeners);
491    listeners.any(|listener| listener.id == id)
492  }
493
494  /// Hands each event to the handlers of its kind, with nothing borrowed,
495  /// since a handler may call the service. A handler that panics is logged
496  /// with where `on_event` was called, and taken away.
497  fn deliver(&self, events: Vec<SwayEvent>) {
498    for event in &events {
499      let handlers: Vec<_> = {
500        let state = self.state.borrow();
501        let listeners = state.iter().flat_map(|state| &state.listeners);
502        listeners
503          .filter(|listener| listener.kinds.contains(&event.kind))
504          .map(|listener| (listener.id, Rc::clone(&listener.handler), listener.at))
505          .collect()
506      };
507      for (id, handler, at) in handlers {
508        // A handler before this one may have taken this one's context.
509        if !self.listening(id) {
510          continue;
511        }
512        if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| handler(event))) {
513          let message = panic_message(payload.as_ref());
514          tracing::error!("Sway on_event (called at {at}) panicked: {message}");
515          drop(self.unlisten(id));
516        }
517      }
518    }
519  }
520
521  /// Connects again for only the events still wanted, since Sway has no
522  /// way to unsubscribe. The lists stay until the new ones arrive.
523  fn resubscribe(self: &Rc<Self>) {
524    if let Some(state) = self.state.borrow_mut().as_mut() {
525      self.drop_connection(state);
526    }
527    // `disconnect` says why if this fails.
528    if !self.connect(false)
529      && let Some(state) = self.state.borrow_mut().as_mut()
530    {
531      self.disconnect(state, &"Sway took no new connection");
532    }
533  }
534
535  /// Connects and asks for the list and its changes. `false` if Sway
536  /// isn't there, logged when `log` unless a drop already was.
537  fn connect(self: &Rc<Self>, log: bool) -> bool {
538    let (Some(socket), Some(event_loop)) = (&self.shared.config.sway, self.shared.event_loop())
539    else {
540      return false;
541    };
542    let opened = UnixStream::connect(&socket.path).and_then(|stream| {
543      stream.set_nonblocking(true)?;
544      let fd = stream.as_fd().try_clone_to_owned()?;
545      Ok((stream, fd))
546    });
547    let mut state = self.state.borrow_mut();
548    let state = state
549      .as_mut()
550      .expect("invariant: started before connecting");
551    let (stream, fd) = match opened {
552      Ok(opened) => opened,
553      Err(error) => {
554        if log && !state.outage_logged {
555          let path = socket.path.display();
556          tracing::warn!(
557              %path,
558              %error,
559              "can't connect to Sway, so the bar shows no workspaces"
560          );
561        }
562        return false;
563      }
564    };
565    let service = Rc::clone(self);
566    let read = Box::new(move || service.on_readable());
567    let Some(reader) = event_loop.insert_fd(fd, Interest::READ, read) else {
568      return false;
569    };
570    if std::mem::take(&mut state.outage_logged) {
571      tracing::info!("connected to Sway again");
572    }
573    let mut connection = Connection {
574      stream,
575      reader,
576      writer: None,
577      inbox: Vec::new(),
578      outbox: Vec::new(),
579      fetches: VecDeque::from([None]),
580      subscribed: Vec::new(),
581      stale: false,
582      awaiting: 0,
583    };
584    // Subscribe first, so no change between the two goes unseen.
585    connection.subscribe(&state.wanted());
586    connection.request(ipc::GET_WORKSPACES, b"");
587    if state.outputs.is_some() {
588      connection.request(ipc::GET_OUTPUTS, b"");
589    }
590    state.connection = Some(connection);
591    self.flush_with(&mut *state);
592    true
593  }
594
595  /// Tries to connect every `retry` until it works or the list is gone.
596  fn retry_later(self: &Rc<Self>) {
597    let (Some(socket), Some(event_loop)) = (&self.shared.config.sway, self.shared.event_loop())
598    else {
599      return;
600    };
601    let retry = socket.retry;
602    let service = Rc::clone(self);
603    event_loop.insert_timer(
604      Instant::now() + retry,
605      Box::new(move || {
606        if !service.alive() || service.connect(true) {
607          TimeoutAction::Drop
608        } else {
609          TimeoutAction::ToDuration(retry)
610        }
611      }),
612    );
613  }
614
615  /// Whether the list is still there: its owner, the root, isn't disposed.
616  fn alive(&self) -> bool {
617    let state = self.state.borrow();
618    let alive = state
619      .as_ref()
620      .and_then(|state| state.workspaces.try_with(|_| ()));
621    alive.is_some()
622  }
623
624  /// Reads what Sway sent and acts on each whole message, then hands the
625  /// events to the config's handlers.
626  fn on_readable(self: &Rc<Self>) {
627    if !self.alive() {
628      return self.close();
629    }
630    let mut chunk = [0; 64 * 1024];
631    let mut guard = self.state.borrow_mut();
632    let state = guard
633      .as_mut()
634      .expect("invariant: started before connecting");
635    let Some(connection) = state.connection.as_mut() else {
636      return;
637    };
638    let taken = match connection.stream.read(&mut chunk) {
639      Ok(0) => Err("Sway closed the socket".to_owned()),
640      Ok(read) => {
641        connection.inbox.extend_from_slice(&chunk[..read]);
642        state.take_messages()
643      }
644      Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {
645        Ok(Vec::new())
646      }
647      Err(error) => Err(error.to_string()),
648    };
649    let events = match taken {
650      Ok(events) => events,
651      Err(why) => return self.disconnect(state, &why),
652    };
653    self.flush_with(state);
654    let wanted = state.wanted();
655    // Only for a subscription nothing wants now, and not while Sway owes
656    // replies: it drops a client that hangs up with requests unread.
657    let stale = state.connection.as_ref().is_some_and(|connection| {
658      connection.stale
659        && connection.awaiting == 0
660        && connection
661          .subscribed
662          .iter()
663          .any(|name| !wanted.contains(name))
664    });
665    drop(guard);
666    if stale {
667      self.resubscribe();
668    }
669    self.deliver(events);
670  }
671
672  /// Writes what the socket takes, and waits for room for the rest.
673  fn flush_with(self: &Rc<Self>, state: &mut State) {
674    let (Some(connection), Some(event_loop)) =
675      (state.connection.as_mut(), self.shared.event_loop())
676    else {
677      return;
678    };
679    while !connection.outbox.is_empty() {
680      match connection.stream.write(&connection.outbox) {
681        Ok(written) => {
682          connection.outbox.drain(..written);
683        }
684        Err(error) if error.kind() == ErrorKind::Interrupted => {}
685        Err(error) if error.kind() == ErrorKind::WouldBlock => break,
686        Err(error) => return self.disconnect(state, &error),
687      }
688    }
689    if connection.outbox.len() > ipc::MAX_PAYLOAD {
690      // Each event queues a fetch, so a Sway that never reads would
691      // grow the outbox and `fetches` without end.
692      return self.disconnect(state, &"Sway reads nothing the bar sends");
693    }
694    if connection.outbox.is_empty() {
695      if let Some(writer) = connection.writer.take() {
696        event_loop.remove(writer);
697      }
698    } else if connection.writer.is_none() {
699      let fd = match connection.stream.as_fd().try_clone_to_owned() {
700        Ok(fd) => fd,
701        Err(error) => return self.disconnect(state, &error),
702      };
703      let service = Rc::clone(self);
704      let write = Box::new(move || service.flush());
705      connection.writer = event_loop.insert_fd(fd, Interest::WRITE, write);
706    }
707  }
708
709  /// Sends one message. `false` while Sway isn't connected.
710  fn send(self: &Rc<Self>, kind: u32, payload: &[u8]) -> bool {
711    let mut state = self.state.borrow_mut();
712    let Some(state) = state.as_mut() else {
713      return false;
714    };
715    let Some(connection) = state.connection.as_mut() else {
716      return false;
717    };
718    connection.request(kind, payload);
719    self.flush_with(state);
720    true
721  }
722
723  fn flush(self: &Rc<Self>) {
724    let mut state = self.state.borrow_mut();
725    if let Some(state) = state.as_mut() {
726      self.flush_with(state);
727    }
728  }
729
730  /// Drops the connection, empties the list and reconnects later.
731  fn disconnect(self: &Rc<Self>, state: &mut State, why: &dyn Display) {
732    tracing::warn!(%why, "lost the connection to Sway; reconnecting");
733    state.outage_logged = true;
734    self.drop_connection(state);
735    state.changed_at = None;
736    replace(state.workspaces, Vec::new());
737    if let Some(outputs) = state.outputs {
738      replace(outputs, Vec::new());
739    }
740    self.retry_later();
741  }
742
743  /// Closes the connection for good, once the list is gone.
744  fn close(&self) {
745    if let Some(state) = self.state.borrow_mut().as_mut() {
746      self.drop_connection(state);
747    }
748  }
749
750  fn drop_connection(&self, state: &mut State) {
751    let connection = state.connection.take();
752    if let (Some(connection), Some(event_loop)) = (connection, self.shared.event_loop()) {
753      event_loop.remove(connection.reader);
754      if let Some(writer) = connection.writer {
755        event_loop.remove(writer);
756      }
757    }
758  }
759}
760
761/// Sets `signal` to `list` unless it holds that already, so nothing reads
762/// an unchanged list again. Whether it changed.
763fn replace<T: PartialEq + 'static>(signal: Signal<Vec<T>>, list: Vec<T>) -> bool {
764  let changed = untrack(|| signal.try_with(|old| *old != list)) == Some(true);
765  if changed {
766    signal.set(list);
767  }
768  changed
769}
770
771impl Connection {
772  /// Subscribes to the events in `wanted` it hasn't yet.
773  fn subscribe(&mut self, wanted: &[&'static str]) {
774    let missing: Vec<_> = wanted
775      .iter()
776      .filter(|name| !self.subscribed.contains(name))
777      .copied()
778      .collect();
779    if !missing.is_empty() {
780      let payload = serde_json::to_vec(&missing).expect("invariant: a list of names serializes");
781      self.request(ipc::SUBSCRIBE, &payload);
782      self.subscribed.extend(missing);
783    }
784  }
785
786  /// Queues a request, whose reply Sway then owes.
787  fn request(&mut self, kind: u32, payload: &[u8]) {
788    ipc::encode(kind, payload, &mut self.outbox);
789    self.awaiting += 1;
790  }
791}
792
793impl State {
794  /// The events the connection needs: workspace ones for the list, output
795  /// ones once a config reads the outputs, and the handlers' kinds.
796  fn wanted(&self) -> Vec<&'static str> {
797    let mut wanted = vec!["workspace"];
798    if self.outputs.is_some() {
799      wanted.push("output");
800    }
801    for kind in self.listeners.iter().flat_map(|listener| &listener.kinds) {
802      let name = kind.wire().1;
803      if !wanted.contains(&name) {
804        wanted.push(name);
805      }
806    }
807    wanted
808  }
809
810  /// Acts on each whole message in the inbox, and returns the events the
811  /// config's handlers want. An error means the stream can't be read any
812  /// further.
813  fn take_messages(&mut self) -> Result<Vec<SwayEvent>, String> {
814    let mut events = Vec::new();
815    let Some(connection) = self.connection.as_mut() else {
816      return Ok(events);
817    };
818    let now = Instant::now();
819    let mut outputs_changed = false;
820    while let Some((kind, payload)) = ipc::decode(&mut connection.inbox)? {
821      // Events have the high bit set; anything else answers a request.
822      if kind & 0x8000_0000 == 0 {
823        connection.awaiting = connection.awaiting.saturating_sub(1);
824      }
825      match kind {
826        ipc::GET_WORKSPACES => {
827          let event = connection.fetches.pop_front().flatten();
828          match serde_json::from_slice::<Vec<Workspace>>(&payload) {
829            Ok(list) => {
830              if replace(self.workspaces, list) {
831                self.changed_at = event;
832              }
833            }
834            Err(error) => tracing::warn!(%error, "a bad workspace list from Sway"),
835          }
836        }
837        ipc::GET_OUTPUTS => match serde_json::from_slice::<Vec<SwayOutput>>(&payload) {
838          Ok(list) => {
839            if let Some(outputs) = self.outputs {
840              replace(outputs, list);
841            }
842          }
843          Err(error) => tracing::warn!(%error, "a bad output list from Sway"),
844        },
845        ipc::SUBSCRIBE => match serde_json::from_slice::<Outcome>(&payload) {
846          Ok(Outcome { success: true, .. }) => {}
847          Ok(Outcome { error, .. }) => {
848            tracing::warn!(
849                %error,
850                "Sway refused the subscription; the list won't update"
851            );
852          }
853          Err(error) => tracing::warn!(%error, "a bad subscribe reply from Sway"),
854        },
855        ipc::RUN_COMMAND => match serde_json::from_slice::<Vec<Outcome>>(&payload) {
856          Ok(outcomes) => {
857            for Outcome { error, .. } in outcomes.into_iter().filter(|outcome| !outcome.success) {
858              tracing::warn!(%error, "Sway refused a command");
859            }
860          }
861          Err(error) => tracing::warn!(%error, "a bad command reply from Sway"),
862        },
863        code => {
864          let Some(kind) = SwayEventKind::from_code(code) else {
865            tracing::debug!(kind = code, "an unexpected message from Sway");
866            continue;
867          };
868          if kind == SwayEventKind::Workspace {
869            connection.fetches.push_back(Some(now));
870            connection.request(ipc::GET_WORKSPACES, b"");
871          }
872          // A workspace event can move an output's workspace or focus.
873          let outputs = self.outputs.is_some()
874            && matches!(kind, SwayEventKind::Workspace | SwayEventKind::Output);
875          outputs_changed |= outputs;
876          let listened = self
877            .listeners
878            .iter()
879            .any(|listener| listener.kinds.contains(&kind));
880          if listened {
881            match String::from_utf8(payload) {
882              Ok(data) if serde_json::from_str::<IgnoredAny>(&data).is_ok() => {
883                events.push(SwayEvent { kind, data });
884              }
885              _ => tracing::warn!(?kind, "a bad event from Sway"),
886            }
887          } else if kind != SwayEventKind::Workspace && !outputs {
888            connection.stale = true;
889          }
890        }
891      }
892    }
893    // One fetch for every event read together, since the reply is all of
894    // them.
895    if outputs_changed {
896      connection.request(ipc::GET_OUTPUTS, b"");
897    }
898    Ok(events)
899  }
900}