1#[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
31pub(crate) const MAX_REPLY: usize = 8 * 1024 * 1024;
33
34pub(crate) const MAX_REQUEST: usize = 8 * 1024;
36
37const MAX_DISPATCHES: usize = 64;
40
41const MAX_IN_FLIGHT: usize = 8;
44
45pub(crate) const MAX_TEXT: usize = 4 * 1024;
47
48pub(crate) const MAX_LIST: usize = 4096;
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct HyprlandWorkspace {
54 pub id: Option<i64>,
57 pub name: String,
60 pub monitor: String,
62 pub windows: u32,
64 pub focused: bool,
66 pub active: bool,
68 pub urgent: bool,
70}
71
72#[derive(Clone, Debug, PartialEq)]
74pub struct HyprlandMonitor {
75 pub id: i64,
77 pub name: String,
79 pub description: String,
81 pub focused: bool,
83 pub active_workspace: String,
85 pub x: i32,
87 pub y: i32,
89 pub width: i32,
91 pub height: i32,
93 pub scale: f64,
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct HyprlandToplevel {
100 pub address: String,
104 pub class: String,
106 pub title: String,
108 pub workspace: String,
110 pub monitor: String,
113 pub floating: bool,
115 pub fullscreen: bool,
118 pub focused: bool,
120 pub urgent: bool,
123}
124
125#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct HyprlandEvent {
129 pub name: String,
131 pub data: String,
134}
135
136impl HyprlandEvent {
137 pub fn parse(&self, fields: usize) -> Vec<&str> {
141 self.data.splitn(fields, ',').collect()
142 }
143}
144
145#[derive(Clone)]
148pub struct Hyprland {
149 service: Option<Rc<Service>>,
151 owner: Owner,
154 workspaces: ReadSignal<Vec<HyprlandWorkspace>>,
155 monitors: ReadSignal<Vec<HyprlandMonitor>>,
156 toplevels: ReadSignal<Vec<HyprlandToplevel>>,
157}
158
159impl Hyprland {
160 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 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 #[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 pub fn workspaces(&self) -> ReadSignal<Vec<HyprlandWorkspace>> {
220 self.workspaces
221 }
222
223 pub fn monitors(&self) -> ReadSignal<Vec<HyprlandMonitor>> {
225 self.monitors
226 }
227
228 pub fn toplevels(&self) -> ReadSignal<Vec<HyprlandToplevel>> {
230 self.toplevels
231 }
232}
233
234pub(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#[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 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 "moveworkspace" | "renameworkspace" | "changeworkspaceid" | "monitoradded"
296 | "monitorremoved" | "configreloaded" => &[Workspaces, Monitors, Clients],
297 "openwindow" | "closewindow" | "movewindow" => &[Workspaces, Clients],
298 "windowtitle" | "changefloatingmode" | "fullscreen" => &[Clients],
299 "activewindow" => &[Clients, ActiveWindow],
301 _ => &[],
302 }
303 }
304}
305
306#[derive(Clone, Copy, PartialEq, Eq)]
308enum Reading {
309 Idle,
310 Busy,
311 Stale,
314}
315
316type Lists = (
317 Signal<Vec<HyprlandWorkspace>>,
318 Signal<Vec<HyprlandMonitor>>,
319 Signal<Vec<HyprlandToplevel>>,
320);
321
322pub(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 events: Option<Events>,
334 reading: [Reading; 4],
336 burst_of: [Option<u64>; 4],
340 bursts: Vec<Burst>,
342 next_burst: u64,
343 requests: Vec<Request>,
345 next_request: u64,
346 request_failure_logged: bool,
349 raw: Raw,
350 urgent: HashSet<String>,
353 handlers: Vec<Handler>,
355 next_handler: u64,
356 lua: Option<bool>,
359 status_asked: bool,
361 focusing: Vec<String>,
363 queued: VecDeque<(Asked, String)>,
365 recheck: Option<Recheck>,
368 reconnected: bool,
370}
371
372struct Burst {
374 id: u64,
375 started: usize,
376 out: usize,
378}
379
380struct Recheck {
383 urgent: HashSet<String>,
384 from: u64,
387 windows: bool,
388 active: bool,
389}
390
391enum Sent {
393 Out,
394 Full,
396 Failed,
397}
398
399#[derive(Clone, Copy, Debug, PartialEq, Eq)]
401enum Asked {
402 List(Query),
403 Status,
404 Dispatch,
405}
406
407#[derive(Clone)]
409struct Handler {
410 id: u64,
411 at: &'static Location<'static>,
413 handler: Rc<dyn Fn(&HyprlandEvent)>,
414}
415
416struct Unregister(Rc<Service>, u64);
418
419impl Drop for Unregister {
420 fn drop(&mut self) {
421 let handler = self.0.remove_handler(self.1);
422 drop(handler);
424 }
425}
426
427#[derive(Default)]
429struct Raw {
430 workspaces: Vec<RawWorkspace>,
431 monitors: Vec<RawMonitor>,
432 windows: Vec<RawWindow>,
433 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 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 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 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 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 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 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 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 state.status_asked = false;
654 state.focusing.clear();
655 }
656 Asked::Dispatch => {}
657 },
658 }
659 }
660 }
661
662 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 if stream.write(command)? < command.len() {
674 return Err(std::io::Error::other("the request didn't fit the socket"));
675 }
676 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 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 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 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 let address = || format!("0x{data}");
762 urgency |= match name {
763 "urgent" => state.urgent.len() < MAX_LIST && state.urgent.insert(address()),
764 "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 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 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 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 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 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 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 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 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 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 fn burst(&mut self) -> u64 {
1038 self.next_burst += 1;
1039 self.next_burst
1040 }
1041
1042 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 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 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 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
1150fn 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#[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 #[serde(rename = "specialWorkspace", default)]
1226 special: Option<WorkspaceRef>,
1227}
1228
1229impl RawMonitor {
1230 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#[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#[derive(Deserialize)]
1274struct ActiveWindow {
1275 #[serde(default, deserialize_with = "some_text")]
1276 address: Option<String>,
1277}
1278
1279fn 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
1288fn 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 Err(rustix::io::Errno::INPROGRESS) => Err(ErrorKind::WouldBlock.into()),
1299 Err(errno) => Err(errno.into()),
1300 }
1301}
1302
1303fn 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
1311fn 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 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#[derive(Deserialize)]
1334struct Status {
1335 #[serde(rename = "configProvider")]
1337 config_provider: String,
1338}
1339
1340fn 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}