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