1mod 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
29pub(crate) const RETRY: Duration = Duration::from_secs(2);
31
32#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
34pub struct Workspace {
35 pub id: i64,
38 pub name: String,
40 pub output: String,
42 pub focused: bool,
44 pub visible: bool,
46 pub urgent: bool,
48}
49
50#[derive(Clone, Debug, PartialEq, Deserialize)]
53#[serde(from = "RawOutput")]
54pub struct SwayOutput {
55 pub name: String,
57 pub focused: bool,
59 pub active: bool,
61 pub current_workspace: Option<String>,
63 pub x: i32,
65 pub y: i32,
67 pub width: u32,
69 pub height: u32,
71 pub scale: f64,
74}
75
76#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub enum SwayEventKind {
119 Workspace,
122 Output,
124 Mode,
126 Window,
129 BarconfigUpdate,
131 Binding,
133 Shutdown,
135 Tick,
137 BarStateUpdate,
139 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 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#[derive(Clone, Debug, PartialEq, Eq)]
183pub struct SwayEvent {
184 pub kind: SwayEventKind,
186 pub data: String,
190}
191
192pub(crate) struct Socket {
194 pub(crate) path: PathBuf,
195 pub(crate) retry: Duration,
197}
198
199#[derive(Clone)]
202pub struct Sway {
203 service: Option<Rc<Service>>,
205 workspaces: ReadSignal<Vec<Workspace>>,
206 owner: Owner,
208}
209
210impl Sway {
211 #[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 pub fn workspaces(&self) -> ReadSignal<Vec<Workspace>> {
228 self.workspaces
229 }
230
231 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 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 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 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 #[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 self.owner.signal(Unlisten(Rc::clone(service), id));
307 }
308}
309
310struct Listener {
312 id: u64,
313 kinds: Vec<SwayEventKind>,
314 handler: Rc<dyn Fn(&SwayEvent)>,
315 at: &'static Location<'static>,
317}
318
319struct Unlisten(Rc<Service>, u64);
321
322impl Drop for Unlisten {
323 fn drop(&mut self) {
324 drop(self.0.unlisten(self.1));
326 }
327}
328
329pub(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
350pub(crate) struct Service {
352 shared: Rc<Shared>,
353 state: RefCell<Option<State>>,
354}
355
356struct State {
357 workspaces: Signal<Vec<Workspace>>,
358 outputs: Option<Signal<Vec<SwayOutput>>>,
361 changed_at: Option<Instant>,
362 listeners: Vec<Listener>,
364 next_listener: u64,
366 connection: Option<Connection>,
367 outage_logged: bool,
369}
370
371struct Connection {
372 stream: UnixStream,
373 reader: RegistrationToken,
375 writer: Option<RegistrationToken>,
378 inbox: Vec<u8>,
380 outbox: Vec<u8>,
382 fetches: VecDeque<Option<Instant>>,
385 subscribed: Vec<&'static str>,
388 stale: bool,
391 awaiting: usize,
393}
394
395#[derive(Deserialize)]
397struct Outcome {
398 success: bool,
399 #[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 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 self.connect(true);
433 }
434 workspaces.read_only()
435 }
436
437 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 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 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 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 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 fn resubscribe(self: &Rc<Self>) {
524 if let Some(state) = self.state.borrow_mut().as_mut() {
525 self.drop_connection(state);
526 }
527 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 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 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 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 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 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 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 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 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 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 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 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
761fn 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 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 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 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 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 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 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 if outputs_changed {
896 connection.request(ipc::GET_OUTPUTS, b"");
897 }
898 Ok(events)
899 }
900}