crcbl_shell/error.rs
1//! Errors crossing the shell seam.
2//!
3//! One enum, unlike `crcbl-hal`'s two. The HAL splits `SurfaceError` out
4//! because `OutOfDate` is *routine* traffic that the frame loop must handle
5//! every resize; the shell has no equivalent — nothing here happens on a normal
6//! frame. Every variant below means a caller made a mistake, or the window
7//! system went away.
8//!
9//! No platform type appears in any variant. Where a backend has detail worth
10//! keeping — a compositor's protocol error string, an X11 error code's name —
11//! it goes into a `String`, for the same reason `crcbl-hal` does it: the
12//! alternative is losing it or leaking `xcb_generic_error_t` into every caller.
13
14use crate::{ShellBackend, WindowId};
15
16/// A shell operation failed.
17#[derive(Debug, thiserror::Error)]
18#[non_exhaustive]
19pub enum ShellError {
20 /// No backend could be opened.
21 ///
22 /// `tried` lists the backends that were attempted in order, so the log line
23 /// distinguishes "there is no `WAYLAND_DISPLAY` and no `DISPLAY`" from "the
24 /// Wayland backend is not compiled into this build". See
25 /// [`open`](crate::open).
26 #[error("no shell backend available (tried: {tried})")]
27 NoBackend {
28 /// Comma-separated backend names, in the order they were attempted.
29 tried: String,
30 },
31
32 /// `CRCBL_SHELL` named a backend this build does not have.
33 ///
34 /// A hard error rather than a fallback: someone who set the variable wanted
35 /// that backend, and silently running on a different one turns a debugging
36 /// session into a wild goose chase.
37 #[error("CRCBL_SHELL={requested:?} is not a backend in this build (have: {available})")]
38 UnknownBackend {
39 /// The value of `CRCBL_SHELL`.
40 requested: String,
41 /// Comma-separated names of the backends that are compiled in.
42 available: String,
43 },
44
45 /// Connecting to the window system failed — no compositor socket, a refused
46 /// X11 connection, a browser with no canvas.
47 #[error("cannot connect to the {backend} display server: {detail}")]
48 Connect {
49 /// Backend that failed to connect.
50 backend: ShellBackend,
51 /// What went wrong, verbatim from the backend.
52 detail: String,
53 },
54
55 /// The connection to the window system was lost mid-session. Nothing the
56 /// shell owns is usable afterwards.
57 ///
58 /// Distinct from [`ShellError::Connect`] because the recovery differs: a
59 /// failed connect is a startup problem a user can fix, and a lost
60 /// connection means the compositor died or the session ended, which is a
61 /// clean-shutdown signal.
62 #[error("{backend} display server connection lost: {detail}")]
63 Disconnected {
64 /// Backend whose connection dropped.
65 backend: ShellBackend,
66 /// What the backend reported.
67 detail: String,
68 },
69
70 /// A [`WindowId`] did not resolve: it was destroyed, never created, or came
71 /// from a different shell.
72 ///
73 /// The generational [`Handle`](crcbl_core::Handle) behind `WindowId` makes
74 /// this a clean failure rather than an operation applied to whatever window
75 /// took the slot — the same argument as `crcbl-hal`'s `InvalidHandle`.
76 #[error("invalid or stale window handle {bits:#x}")]
77 InvalidWindow {
78 /// The handle's packed bits, from
79 /// [`Handle::to_bits`](crcbl_core::Handle::to_bits).
80 bits: u64,
81 },
82
83 /// A [`MonitorId`](crate::MonitorId) named a monitor that is not connected —
84 /// usually one that was unplugged between
85 /// [`monitors`](crate::Shell::monitors) and the call that used its id.
86 #[error("no such monitor: {0}")]
87 NoSuchMonitor(u32),
88
89 /// Creating the window failed.
90 #[error("window creation failed: {0}")]
91 WindowCreation(String),
92
93 /// A descriptor field was out of range or self-inconsistent — a caller bug,
94 /// which is why the message is expected to name the field.
95 #[error("invalid descriptor: {0}")]
96 InvalidDescriptor(String),
97
98 /// The reply named a window with no close request outstanding.
99 ///
100 /// An error rather than a no-op because it is always a state-machine bug in
101 /// the caller: the reply either arrived twice or was addressed to the wrong
102 /// window, and both silently leave a real close request unanswered.
103 #[error("window {window:?} has no outstanding close request")]
104 NoPendingCloseRequest {
105 /// The window the reply named.
106 window: WindowId,
107 },
108
109 /// The operation is real but this backend cannot do it.
110 ///
111 /// Always paired with a [`ShellCaps`](crate::ShellCaps) flag: a caller that
112 /// checks caps first never sees this, and a caller that does not gets an
113 /// error naming the capability it should have checked. Getting this back is
114 /// the signal that a consumer is missing a caps branch — which is exactly
115 /// the platform-sniffing regression `ShellCaps` exists to prevent.
116 #[error("unsupported by the {backend} shell backend: {what}")]
117 Unsupported {
118 /// Backend that refused.
119 backend: ShellBackend,
120 /// What was asked for.
121 what: &'static str,
122 },
123
124 /// Anything else the backend wants to report verbatim.
125 #[error("{0}")]
126 Backend(String),
127
128 /// The window system requires a recent user interaction for this, and there
129 /// has not been one.
130 ///
131 /// # Why this is a named case rather than a backend string
132 ///
133 /// It is a **policy**, not a fault, and the policy is the same one on three
134 /// platforms:
135 ///
136 /// * Wayland's `wl_data_device.set_selection` quotes the serial of the
137 /// input event that caused the copy, and a compositor checks it. A client
138 /// that has received no input on a seat cannot take the clipboard.
139 /// * A browser gates `navigator.clipboard.write()` and Pointer Lock behind
140 /// a *user gesture*, with the same reasoning and the same failure.
141 /// * X11 has no such requirement at all, and neither does Win32.
142 ///
143 /// What it prevents is a background process silently taking the clipboard —
144 /// so a caller must treat it as "ask the user to do that again", never as a
145 /// bug to work around. A backend on a platform without the rule **never
146 /// returns this**, and that is obviously correct rather than obviously
147 /// incomplete.
148 #[error("{backend} needs a recent user interaction for {what}")]
149 NeedsUserInteraction {
150 /// Which backend refused.
151 backend: ShellBackend,
152 /// What was being attempted, for the log line.
153 what: &'static str,
154 },
155}
156
157impl ShellError {
158 /// Builds a [`ShellError::InvalidWindow`] from a window handle.
159 ///
160 /// Backends call this from their handle-lookup path so the error carries the
161 /// same bits that appear in `WindowId`'s `Debug` output.
162 #[must_use]
163 pub fn invalid_window(window: WindowId) -> Self {
164 Self::InvalidWindow {
165 bits: window.to_bits(),
166 }
167 }
168
169 /// Whether the error means the window system is gone and the process should
170 /// shut down rather than retry.
171 #[must_use]
172 pub fn is_fatal(&self) -> bool {
173 matches!(self, Self::Disconnected { .. })
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crcbl_core::Pool;
181
182 #[test]
183 fn messages_name_the_specific_problem() {
184 let error = ShellError::Unsupported {
185 backend: ShellBackend::Wayland,
186 what: "pointer warp",
187 };
188 assert_eq!(
189 error.to_string(),
190 "unsupported by the wayland shell backend: pointer warp"
191 );
192 assert!(!error.is_fatal());
193
194 let error = ShellError::NoBackend {
195 tried: "wayland, x11".to_string(),
196 };
197 assert!(error.to_string().contains("wayland, x11"));
198
199 assert!(
200 ShellError::Disconnected {
201 backend: ShellBackend::X11,
202 detail: "server closed the connection".to_string(),
203 }
204 .is_fatal()
205 );
206 }
207
208 #[test]
209 fn invalid_window_carries_the_handle_bits() {
210 let mut pool: Pool<u8> = Pool::new();
211 let window: WindowId = pool.insert(0).cast();
212 let ShellError::InvalidWindow { bits } = ShellError::invalid_window(window) else {
213 panic!("wrong variant");
214 };
215 assert_eq!(bits, window.to_bits());
216 assert_ne!(bits, 0, "packed handle bits are never zero");
217 }
218}