1mod client;
8pub mod clipboard;
9mod memory;
10mod platform;
11#[cfg(feature = "server")]
12mod server;
13mod state;
14mod wire;
15mod worker;
16
17pub use client::{Client, Event};
18pub use memory::PixelBuffer;
19#[cfg(feature = "server")]
20pub use server::{NativeWindowServer, Registration, ServerEvent};
21pub use worker::WorkerWindow;
22
23use std::io;
24use std::path::{Path, PathBuf};
25use std::time::Duration;
26
27pub const PROTOCOL_NAME: &str = "org.knott.aen-window";
28pub const PROTOCOL_VERSION: u16 = 3;
29pub const MAX_DIMENSION: u32 = 8192;
30pub const MAX_FRAME_BYTES: usize = 32 * 1024 * 1024;
31pub const MAX_MEMORY_BYTES: usize = 128 * 1024 * 1024;
32pub const MAX_CLIENTS: usize = 32;
33pub const MAX_APP_ID_BYTES: usize = 64;
34pub const MAX_TITLE_BYTES: usize = 128;
35pub const MAX_TEXT_COMMIT_BYTES: usize = 256;
37pub const TRANSFER_TIMEOUT: Duration = Duration::from_secs(3);
38pub const MOD_SHIFT: u32 = 1;
39pub const MOD_CTRL: u32 = 2;
40pub const MOD_ALT: u32 = 4;
41pub const MOD_SUPER: u32 = 8;
42
43#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
46pub struct ClientOptions {
47 pub relative_pointer: bool,
50 pub pointer_confinement: bool,
53}
54impl ClientOptions {
55 pub(crate) const fn bits(self) -> u32 {
56 self.relative_pointer as u32 | ((self.pointer_confinement as u32) << 1)
57 }
58 pub(crate) fn from_bits(bits: u32) -> io::Result<Self> {
59 if bits & !3 != 0 {
60 return Err(invalid("unknown native window options"));
61 }
62 Ok(Self {
63 relative_pointer: bits & 1 != 0,
64 pointer_confinement: bits & 2 != 0,
65 })
66 }
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub struct PointerConfinementRequest {
73 pub request_serial: u64,
74 pub frame_serial: u64,
75 pub confined: bool,
76}
77impl PointerConfinementRequest {
78 pub fn validate(self) -> io::Result<()> {
79 if self.request_serial == 0 || self.frame_serial == 0 {
80 return Err(invalid("invalid native pointer confinement request"));
81 }
82 Ok(())
83 }
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct Create {
88 pub app_id: String,
89 pub title: String,
90 pub preferred_width: u32,
91 pub preferred_height: u32,
92}
93impl Create {
94 pub fn validate(&self) -> io::Result<()> {
95 if self.app_id.is_empty()
96 || self.app_id.len() > MAX_APP_ID_BYTES
97 || !self
98 .app_id
99 .bytes()
100 .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
101 || self.title.is_empty()
102 || self.title.len() > MAX_TITLE_BYTES
103 || self.title.chars().any(char::is_control)
104 {
105 return Err(invalid("invalid native window identity"));
106 }
107 frame_bytes(self.preferred_width, self.preferred_height)?;
108 Ok(())
109 }
110}
111
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub struct Configure {
114 pub window_id: u64,
115 pub configure_serial: u64,
116 pub width: u32,
117 pub height: u32,
118 pub scale: u32,
119}
120impl Configure {
121 pub fn validate(&self) -> io::Result<()> {
122 if self.window_id == 0 || self.configure_serial == 0 || self.scale != 1 {
123 return Err(invalid("invalid native window configuration"));
124 }
125 frame_bytes(self.width, self.height)?;
126 Ok(())
127 }
128}
129
130#[derive(Clone, Debug)]
131pub struct Frame {
132 pub frame_serial: u64,
133 pub configure_serial: u64,
134 pub width: u32,
135 pub height: u32,
136 pub pixels: PixelBuffer,
139}
140pub type ServerFrame = Frame;
141impl Frame {
142 pub fn validate(&self) -> io::Result<()> {
143 if self.frame_serial == 0
144 || self.configure_serial == 0
145 || self.pixels.len() != frame_bytes(self.width, self.height)?
146 {
147 return Err(invalid("invalid native window frame"));
148 }
149 Ok(())
150 }
151}
152
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154pub enum TouchPhase {
155 Down,
157 Motion,
159 Up,
161}
162
163#[derive(Clone, Debug, Eq, PartialEq)]
164pub struct Input {
165 pub frame_serial: u64,
166 pub event: InputEvent,
167}
168#[derive(Clone, Debug, Eq, PartialEq)]
169pub enum InputEvent {
170 PointerConfinement {
174 request_serial: u64,
175 active: bool,
176 },
177 Focus {
178 focused: bool,
179 },
180 Motion {
181 x: i32,
182 y: i32,
183 },
184 RelativeMotion {
188 dx: i32,
189 dy: i32,
190 time_usec: u64,
191 },
192 Button {
193 button: u32,
194 pressed: bool,
195 x: i32,
196 y: i32,
197 },
198 Axis {
199 horizontal: i32,
200 vertical: i32,
201 x: i32,
202 y: i32,
203 },
204 Leave,
205 Key {
206 keycode: u32,
207 keysym: u32,
208 pressed: bool,
209 repeat: bool,
210 modifiers: u32,
211 text: Option<char>,
212 },
213 TextCommit {
215 text: String,
216 },
217 Touch {
219 id: i32,
220 phase: TouchPhase,
221 x: i32,
222 y: i32,
223 },
224 TouchCancel,
226}
227impl Input {
228 pub fn validate(&self) -> io::Result<()> {
229 if self.frame_serial == 0 {
230 return Err(invalid("input requires an adopted frame"));
231 }
232 match &self.event {
233 InputEvent::PointerConfinement {
234 request_serial: 0, ..
235 } => Err(invalid("invalid native pointer confinement status")),
236 InputEvent::Key {
237 keycode,
238 pressed,
239 repeat,
240 modifiers,
241 text,
242 ..
243 } if *keycode > 0xffff
244 || (*repeat && !*pressed)
245 || *modifiers & !15 != 0
246 || (*text).is_some_and(char::is_control)
247 || (!*pressed && (*text).is_some()) =>
248 {
249 Err(invalid("invalid native window key event"))
250 }
251 InputEvent::TextCommit { text }
252 if text.is_empty()
253 || text.len() > MAX_TEXT_COMMIT_BYTES
254 || text.chars().any(char::is_control) =>
255 {
256 Err(invalid("invalid native window text commit"))
257 }
258 InputEvent::Button { button, .. } if *button == 0 || *button > 0xffff => {
259 Err(invalid("invalid native window button"))
260 }
261 InputEvent::Touch { id, phase, x, y }
262 if *id < 0 || (*phase == TouchPhase::Up && (*x != 0 || *y != 0)) =>
263 {
264 Err(invalid("invalid native window touch event"))
265 }
266 _ => Ok(()),
267 }
268 }
269}
270
271pub fn frame_bytes(width: u32, height: u32) -> io::Result<usize> {
272 if width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION {
273 return Err(invalid("native window dimensions exceed limits"));
274 }
275 let bytes = (width as usize)
276 .checked_mul(height as usize)
277 .and_then(|n| n.checked_mul(4))
278 .filter(|n| *n <= MAX_FRAME_BYTES)
279 .ok_or_else(|| invalid("native window frame exceeds limit"))?;
280 Ok(bytes)
281}
282
283pub fn socket_for_shell(shell: &Path) -> PathBuf {
284 let mut path = shell.as_os_str().to_owned();
285 path.push(".windows");
286 path.into()
287}
288
289pub fn socket_from_env() -> io::Result<PathBuf> {
291 if let Some(path) = std::env::var_os("AEN_WINDOW_SOCKET") {
292 return Ok(path.into());
293 }
294 if let Some(path) = std::env::var_os("KNOTT_COMPOSITOR_SHELL_SOCKET") {
295 return Ok(socket_for_shell(Path::new(&path)));
296 }
297 let runtime = std::env::var_os("XDG_RUNTIME_DIR")
298 .ok_or_else(|| invalid("no native window endpoint is configured"))?;
299 Ok(socket_for_shell(
300 &PathBuf::from(runtime).join("knottx-shell.sock"),
301 ))
302}
303
304pub(crate) fn invalid(message: &'static str) -> io::Error {
305 io::Error::new(io::ErrorKind::InvalidData, message)
306}