mod ipc;
#[cfg(test)]
mod tests;
use std::cell::RefCell;
use std::collections::VecDeque;
use std::fmt::Display;
use std::io::{ErrorKind, Read, Write};
use std::os::fd::AsFd;
use std::os::unix::net::UnixStream;
use std::panic::{self, AssertUnwindSafe, Location};
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Once;
use std::time::{Duration, Instant};
use calloop::timer::TimeoutAction;
use calloop::{Interest, RegistrationToken};
use crabshell_reactive::{Owner, ReadSignal, Signal, untrack};
use crabshell_ui::Cx;
use serde::Deserialize;
use serde::de::IgnoredAny;
use crate::Shared;
use crate::lines::panic_message;
pub(crate) const RETRY: Duration = Duration::from_secs(2);
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct Workspace {
pub id: i64,
pub name: String,
pub output: String,
pub focused: bool,
pub visible: bool,
pub urgent: bool,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(from = "RawOutput")]
pub struct SwayOutput {
pub name: String,
pub focused: bool,
pub active: bool,
pub current_workspace: Option<String>,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
pub scale: f64,
}
#[derive(Deserialize)]
struct RawOutput {
name: String,
#[serde(default)]
focused: bool,
#[serde(default)]
active: bool,
current_workspace: Option<String>,
#[serde(default)]
rect: RawRect,
scale: Option<f64>,
}
#[derive(Default, Deserialize)]
struct RawRect {
x: i32,
y: i32,
width: u32,
height: u32,
}
impl From<RawOutput> for SwayOutput {
fn from(raw: RawOutput) -> SwayOutput {
SwayOutput {
name: raw.name,
focused: raw.focused,
active: raw.active,
current_workspace: raw.current_workspace,
x: raw.rect.x,
y: raw.rect.y,
width: raw.rect.width,
height: raw.rect.height,
scale: raw.scale.filter(|scale| *scale > 0.0).unwrap_or(1.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SwayEventKind {
Workspace,
Output,
Mode,
Window,
BarconfigUpdate,
Binding,
Shutdown,
Tick,
BarStateUpdate,
Input,
}
impl SwayEventKind {
const ALL: [SwayEventKind; 10] = [
SwayEventKind::Workspace,
SwayEventKind::Output,
SwayEventKind::Mode,
SwayEventKind::Window,
SwayEventKind::BarconfigUpdate,
SwayEventKind::Binding,
SwayEventKind::Shutdown,
SwayEventKind::Tick,
SwayEventKind::BarStateUpdate,
SwayEventKind::Input,
];
fn wire(self) -> (u32, &'static str) {
match self {
SwayEventKind::Workspace => (ipc::WORKSPACE_EVENT, "workspace"),
SwayEventKind::Output => (ipc::OUTPUT_EVENT, "output"),
SwayEventKind::Mode => (0x8000_0002, "mode"),
SwayEventKind::Window => (0x8000_0003, "window"),
SwayEventKind::BarconfigUpdate => (0x8000_0004, "barconfig_update"),
SwayEventKind::Binding => (0x8000_0005, "binding"),
SwayEventKind::Shutdown => (0x8000_0006, "shutdown"),
SwayEventKind::Tick => (0x8000_0007, "tick"),
SwayEventKind::BarStateUpdate => (0x8000_0014, "bar_state_update"),
SwayEventKind::Input => (0x8000_0015, "input"),
}
}
fn from_code(code: u32) -> Option<SwayEventKind> {
SwayEventKind::ALL
.into_iter()
.find(|kind| kind.wire().0 == code)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SwayEvent {
pub kind: SwayEventKind,
pub data: String,
}
pub(crate) struct Socket {
pub(crate) path: PathBuf,
pub(crate) retry: Duration,
}
#[derive(Clone)]
pub struct Sway {
service: Option<Rc<Service>>,
workspaces: ReadSignal<Vec<Workspace>>,
owner: Owner,
}
impl Sway {
#[doc(hidden)]
pub fn started(cx: &Cx) -> Option<Sway> {
let service = cx.service::<Service>()?;
let workspaces = service.state.borrow().as_ref()?.workspaces.read_only();
Some(Sway {
service: Some(service),
workspaces,
owner: cx.owner(),
})
}
pub fn workspaces(&self) -> ReadSignal<Vec<Workspace>> {
self.workspaces
}
pub fn outputs(&self) -> ReadSignal<Vec<SwayOutput>> {
match &self.service {
Some(service) => service.outputs(),
None => self.owner.signal(Vec::new()).read_only(),
}
}
pub fn changed_at(&self) -> Option<Instant> {
let state = self.service.as_ref()?.state.borrow();
state.as_ref()?.changed_at
}
pub fn focus_workspace(&self, name: &str) {
let Some(command) = ipc::workspace_command(name) else {
tracing::warn!(name, "Sway can't switch to this workspace by name");
return;
};
self.dispatch(&command);
}
pub fn dispatch(&self, command: &str) {
if command.len() > ipc::MAX_PAYLOAD {
let bytes = command.len();
return tracing::warn!(bytes, "a Sway command over 4 MiB isn't sent");
}
let service = self.service.as_ref();
if !service.is_some_and(|service| service.send(ipc::RUN_COMMAND, command.as_bytes())) {
tracing::warn!(
command,
"can't run a Sway command while Sway isn't connected"
);
}
}
#[track_caller]
pub fn on_event(&self, kinds: &[SwayEventKind], handler: impl Fn(&SwayEvent) + 'static) {
let at = Location::caller();
if self.owner.is_disposed() {
return tracing::warn!(
"Sway on_event (called at {at}) on a context that is gone does nothing"
);
}
let Some(service) = &self.service else {
return;
};
let id = service.listen(kinds, Rc::new(handler), at);
self.owner.signal(Unlisten(Rc::clone(service), id));
}
}
struct Listener {
id: u64,
kinds: Vec<SwayEventKind>,
handler: Rc<dyn Fn(&SwayEvent)>,
at: &'static Location<'static>,
}
struct Unlisten(Rc<Service>, u64);
impl Drop for Unlisten {
fn drop(&mut self) {
drop(self.0.unlisten(self.1));
}
}
pub(crate) fn sway(cx: &Cx) -> Sway {
let Some(service) = cx.service::<Service>() else {
static LOGGED: Once = Once::new();
LOGGED.call_once(|| {
tracing::warn!("cx.sway() needs the services the shell provides; it shows no workspaces");
});
return Sway {
service: None,
workspaces: cx.signal(Vec::new()).read_only(),
owner: cx.owner(),
};
};
let workspaces = service.start();
Sway {
service: Some(service),
workspaces,
owner: cx.owner(),
}
}
pub(crate) struct Service {
shared: Rc<Shared>,
state: RefCell<Option<State>>,
}
struct State {
workspaces: Signal<Vec<Workspace>>,
outputs: Option<Signal<Vec<SwayOutput>>>,
changed_at: Option<Instant>,
listeners: Vec<Listener>,
next_listener: u64,
connection: Option<Connection>,
outage_logged: bool,
}
struct Connection {
stream: UnixStream,
reader: RegistrationToken,
writer: Option<RegistrationToken>,
inbox: Vec<u8>,
outbox: Vec<u8>,
fetches: VecDeque<Option<Instant>>,
subscribed: Vec<&'static str>,
stale: bool,
awaiting: usize,
}
#[derive(Deserialize)]
struct Outcome {
success: bool,
#[serde(default)]
error: String,
}
impl Service {
pub(crate) fn new(shared: Rc<Shared>) -> Service {
Service {
shared,
state: RefCell::new(None),
}
}
fn start(self: &Rc<Self>) -> ReadSignal<Vec<Workspace>> {
if let Some(state) = &*self.state.borrow() {
return state.workspaces.read_only();
}
let workspaces = self.shared.root.signal(Vec::new());
*self.state.borrow_mut() = Some(State {
workspaces,
outputs: None,
changed_at: None,
listeners: Vec::new(),
next_listener: 0,
connection: None,
outage_logged: false,
});
if self.shared.config.sway.is_none() {
tracing::info!("SWAYSOCK is unset, so the bar shows no workspaces");
} else {
self.connect(true);
}
workspaces.read_only()
}
fn outputs(self: &Rc<Self>) -> ReadSignal<Vec<SwayOutput>> {
let mut state = self.state.borrow_mut();
let state = state.as_mut().expect("invariant: started by cx.sway()");
if let Some(outputs) = state.outputs {
return outputs.read_only();
}
let outputs = self.shared.root.signal(Vec::new());
state.outputs = Some(outputs);
let wanted = state.wanted();
if let Some(connection) = state.connection.as_mut() {
connection.subscribe(&wanted);
connection.request(ipc::GET_OUTPUTS, b"");
self.flush_with(state);
}
outputs.read_only()
}
fn listen(
self: &Rc<Self>,
kinds: &[SwayEventKind],
handler: Rc<dyn Fn(&SwayEvent)>,
at: &'static Location<'static>,
) -> u64 {
let mut state = self.state.borrow_mut();
let state = state.as_mut().expect("invariant: started by cx.sway()");
state.next_listener += 1;
let id = state.next_listener;
state.listeners.push(Listener {
id,
kinds: kinds.to_vec(),
handler,
at,
});
let wanted = state.wanted();
if let Some(connection) = state.connection.as_mut() {
connection.subscribe(&wanted);
self.flush_with(state);
}
id
}
fn unlisten(&self, id: u64) -> Option<Listener> {
let mut state = self.state.borrow_mut();
let listeners = &mut state.as_mut()?.listeners;
let index = listeners.iter().position(|listener| listener.id == id)?;
Some(listeners.remove(index))
}
fn listening(&self, id: u64) -> bool {
let state = self.state.borrow();
let mut listeners = state.iter().flat_map(|state| &state.listeners);
listeners.any(|listener| listener.id == id)
}
fn deliver(&self, events: Vec<SwayEvent>) {
for event in &events {
let handlers: Vec<_> = {
let state = self.state.borrow();
let listeners = state.iter().flat_map(|state| &state.listeners);
listeners
.filter(|listener| listener.kinds.contains(&event.kind))
.map(|listener| (listener.id, Rc::clone(&listener.handler), listener.at))
.collect()
};
for (id, handler, at) in handlers {
if !self.listening(id) {
continue;
}
if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| handler(event))) {
let message = panic_message(payload.as_ref());
tracing::error!("Sway on_event (called at {at}) panicked: {message}");
drop(self.unlisten(id));
}
}
}
}
fn resubscribe(self: &Rc<Self>) {
if let Some(state) = self.state.borrow_mut().as_mut() {
self.drop_connection(state);
}
if !self.connect(false)
&& let Some(state) = self.state.borrow_mut().as_mut()
{
self.disconnect(state, &"Sway took no new connection");
}
}
fn connect(self: &Rc<Self>, log: bool) -> bool {
let (Some(socket), Some(event_loop)) = (&self.shared.config.sway, self.shared.event_loop())
else {
return false;
};
let opened = UnixStream::connect(&socket.path).and_then(|stream| {
stream.set_nonblocking(true)?;
let fd = stream.as_fd().try_clone_to_owned()?;
Ok((stream, fd))
});
let mut state = self.state.borrow_mut();
let state = state
.as_mut()
.expect("invariant: started before connecting");
let (stream, fd) = match opened {
Ok(opened) => opened,
Err(error) => {
if log && !state.outage_logged {
let path = socket.path.display();
tracing::warn!(
%path,
%error,
"can't connect to Sway, so the bar shows no workspaces"
);
}
return false;
}
};
let service = Rc::clone(self);
let read = Box::new(move || service.on_readable());
let Some(reader) = event_loop.insert_fd(fd, Interest::READ, read) else {
return false;
};
if std::mem::take(&mut state.outage_logged) {
tracing::info!("connected to Sway again");
}
let mut connection = Connection {
stream,
reader,
writer: None,
inbox: Vec::new(),
outbox: Vec::new(),
fetches: VecDeque::from([None]),
subscribed: Vec::new(),
stale: false,
awaiting: 0,
};
connection.subscribe(&state.wanted());
connection.request(ipc::GET_WORKSPACES, b"");
if state.outputs.is_some() {
connection.request(ipc::GET_OUTPUTS, b"");
}
state.connection = Some(connection);
self.flush_with(&mut *state);
true
}
fn retry_later(self: &Rc<Self>) {
let (Some(socket), Some(event_loop)) = (&self.shared.config.sway, self.shared.event_loop())
else {
return;
};
let retry = socket.retry;
let service = Rc::clone(self);
event_loop.insert_timer(
Instant::now() + retry,
Box::new(move || {
if !service.alive() || service.connect(true) {
TimeoutAction::Drop
} else {
TimeoutAction::ToDuration(retry)
}
}),
);
}
fn alive(&self) -> bool {
let state = self.state.borrow();
let alive = state
.as_ref()
.and_then(|state| state.workspaces.try_with(|_| ()));
alive.is_some()
}
fn on_readable(self: &Rc<Self>) {
if !self.alive() {
return self.close();
}
let mut chunk = [0; 64 * 1024];
let mut guard = self.state.borrow_mut();
let state = guard
.as_mut()
.expect("invariant: started before connecting");
let Some(connection) = state.connection.as_mut() else {
return;
};
let taken = match connection.stream.read(&mut chunk) {
Ok(0) => Err("Sway closed the socket".to_owned()),
Ok(read) => {
connection.inbox.extend_from_slice(&chunk[..read]);
state.take_messages()
}
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {
Ok(Vec::new())
}
Err(error) => Err(error.to_string()),
};
let events = match taken {
Ok(events) => events,
Err(why) => return self.disconnect(state, &why),
};
self.flush_with(state);
let wanted = state.wanted();
let stale = state.connection.as_ref().is_some_and(|connection| {
connection.stale
&& connection.awaiting == 0
&& connection
.subscribed
.iter()
.any(|name| !wanted.contains(name))
});
drop(guard);
if stale {
self.resubscribe();
}
self.deliver(events);
}
fn flush_with(self: &Rc<Self>, state: &mut State) {
let (Some(connection), Some(event_loop)) =
(state.connection.as_mut(), self.shared.event_loop())
else {
return;
};
while !connection.outbox.is_empty() {
match connection.stream.write(&connection.outbox) {
Ok(written) => {
connection.outbox.drain(..written);
}
Err(error) if error.kind() == ErrorKind::Interrupted => {}
Err(error) if error.kind() == ErrorKind::WouldBlock => break,
Err(error) => return self.disconnect(state, &error),
}
}
if connection.outbox.len() > ipc::MAX_PAYLOAD {
return self.disconnect(state, &"Sway reads nothing the bar sends");
}
if connection.outbox.is_empty() {
if let Some(writer) = connection.writer.take() {
event_loop.remove(writer);
}
} else if connection.writer.is_none() {
let fd = match connection.stream.as_fd().try_clone_to_owned() {
Ok(fd) => fd,
Err(error) => return self.disconnect(state, &error),
};
let service = Rc::clone(self);
let write = Box::new(move || service.flush());
connection.writer = event_loop.insert_fd(fd, Interest::WRITE, write);
}
}
fn send(self: &Rc<Self>, kind: u32, payload: &[u8]) -> bool {
let mut state = self.state.borrow_mut();
let Some(state) = state.as_mut() else {
return false;
};
let Some(connection) = state.connection.as_mut() else {
return false;
};
connection.request(kind, payload);
self.flush_with(state);
true
}
fn flush(self: &Rc<Self>) {
let mut state = self.state.borrow_mut();
if let Some(state) = state.as_mut() {
self.flush_with(state);
}
}
fn disconnect(self: &Rc<Self>, state: &mut State, why: &dyn Display) {
tracing::warn!(%why, "lost the connection to Sway; reconnecting");
state.outage_logged = true;
self.drop_connection(state);
state.changed_at = None;
replace(state.workspaces, Vec::new());
if let Some(outputs) = state.outputs {
replace(outputs, Vec::new());
}
self.retry_later();
}
fn close(&self) {
if let Some(state) = self.state.borrow_mut().as_mut() {
self.drop_connection(state);
}
}
fn drop_connection(&self, state: &mut State) {
let connection = state.connection.take();
if let (Some(connection), Some(event_loop)) = (connection, self.shared.event_loop()) {
event_loop.remove(connection.reader);
if let Some(writer) = connection.writer {
event_loop.remove(writer);
}
}
}
}
fn replace<T: PartialEq + 'static>(signal: Signal<Vec<T>>, list: Vec<T>) -> bool {
let changed = untrack(|| signal.try_with(|old| *old != list)) == Some(true);
if changed {
signal.set(list);
}
changed
}
impl Connection {
fn subscribe(&mut self, wanted: &[&'static str]) {
let missing: Vec<_> = wanted
.iter()
.filter(|name| !self.subscribed.contains(name))
.copied()
.collect();
if !missing.is_empty() {
let payload = serde_json::to_vec(&missing).expect("invariant: a list of names serializes");
self.request(ipc::SUBSCRIBE, &payload);
self.subscribed.extend(missing);
}
}
fn request(&mut self, kind: u32, payload: &[u8]) {
ipc::encode(kind, payload, &mut self.outbox);
self.awaiting += 1;
}
}
impl State {
fn wanted(&self) -> Vec<&'static str> {
let mut wanted = vec!["workspace"];
if self.outputs.is_some() {
wanted.push("output");
}
for kind in self.listeners.iter().flat_map(|listener| &listener.kinds) {
let name = kind.wire().1;
if !wanted.contains(&name) {
wanted.push(name);
}
}
wanted
}
fn take_messages(&mut self) -> Result<Vec<SwayEvent>, String> {
let mut events = Vec::new();
let Some(connection) = self.connection.as_mut() else {
return Ok(events);
};
let now = Instant::now();
let mut outputs_changed = false;
while let Some((kind, payload)) = ipc::decode(&mut connection.inbox)? {
if kind & 0x8000_0000 == 0 {
connection.awaiting = connection.awaiting.saturating_sub(1);
}
match kind {
ipc::GET_WORKSPACES => {
let event = connection.fetches.pop_front().flatten();
match serde_json::from_slice::<Vec<Workspace>>(&payload) {
Ok(list) => {
if replace(self.workspaces, list) {
self.changed_at = event;
}
}
Err(error) => tracing::warn!(%error, "a bad workspace list from Sway"),
}
}
ipc::GET_OUTPUTS => match serde_json::from_slice::<Vec<SwayOutput>>(&payload) {
Ok(list) => {
if let Some(outputs) = self.outputs {
replace(outputs, list);
}
}
Err(error) => tracing::warn!(%error, "a bad output list from Sway"),
},
ipc::SUBSCRIBE => match serde_json::from_slice::<Outcome>(&payload) {
Ok(Outcome { success: true, .. }) => {}
Ok(Outcome { error, .. }) => {
tracing::warn!(
%error,
"Sway refused the subscription; the list won't update"
);
}
Err(error) => tracing::warn!(%error, "a bad subscribe reply from Sway"),
},
ipc::RUN_COMMAND => match serde_json::from_slice::<Vec<Outcome>>(&payload) {
Ok(outcomes) => {
for Outcome { error, .. } in outcomes.into_iter().filter(|outcome| !outcome.success) {
tracing::warn!(%error, "Sway refused a command");
}
}
Err(error) => tracing::warn!(%error, "a bad command reply from Sway"),
},
code => {
let Some(kind) = SwayEventKind::from_code(code) else {
tracing::debug!(kind = code, "an unexpected message from Sway");
continue;
};
if kind == SwayEventKind::Workspace {
connection.fetches.push_back(Some(now));
connection.request(ipc::GET_WORKSPACES, b"");
}
let outputs = self.outputs.is_some()
&& matches!(kind, SwayEventKind::Workspace | SwayEventKind::Output);
outputs_changed |= outputs;
let listened = self
.listeners
.iter()
.any(|listener| listener.kinds.contains(&kind));
if listened {
match String::from_utf8(payload) {
Ok(data) if serde_json::from_str::<IgnoredAny>(&data).is_ok() => {
events.push(SwayEvent { kind, data });
}
_ => tracing::warn!(?kind, "a bad event from Sway"),
}
} else if kind != SwayEventKind::Workspace && !outputs {
connection.stale = true;
}
}
}
}
if outputs_changed {
connection.request(ipc::GET_OUTPUTS, b"");
}
Ok(events)
}
}