1use crate::host_words::{
18 BATTERY, CHARGING, CLICK_PRESSED, CLICK_X, CLICK_Y, CLIPBOARD_COUNT, DARK_MODE, DISPLAY_DPI, DISPLAY_HEIGHT,
19 DISPLAY_WIDTH, ESCAPE_KEPT, GPU_DRAWING, GPU_PAINTS, KEY_CODE_HELD, KEY_DOWN, KEY_PRESSED, MOUSE_DOWN, MOUSE_WHEEL,
20 MOUSE_X, MOUSE_Y, ONLINE, SYSTEM_VALUES, TIME_OF_DAY, VIEW_HEIGHT, VIEW_WIDTH, WINDOW_HEIGHT, WINDOW_OPEN,
21 WINDOW_WIDTH,
22};
23use std::cell::RefCell;
24use std::collections::{HashMap, VecDeque};
25use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
26use std::sync::{Mutex, OnceLock};
27use std::time::{Duration, Instant};
28
29const NOTIFICATION_TITLE: &str = "warp";
30const READING_LIFETIME: Duration = Duration::from_secs(1);
31const NATIVE_VIEW: (i64, i64) = (640, 480);
33const INTERNET_ADDRESS: &str = "1.1.1.1:53";
35
36static WINDOW_INPUT: [AtomicU32; 4] = [const { AtomicU32::new(0) }; 4];
39
40static WHEEL: AtomicI64 = AtomicI64::new(0);
42
43pub fn wheel() -> i64 {
44 WHEEL.load(Ordering::Relaxed)
45}
46
47pub fn tell_wheel(steps: i64) {
48 WHEEL.store(steps, Ordering::Relaxed);
49}
50
51static WINDOW_SHOWN: AtomicBool = AtomicBool::new(false);
53
54pub fn tell_window_open(open: bool) {
55 WINDOW_SHOWN.store(open, Ordering::Relaxed);
56}
57
58pub fn window_shows() -> bool {
59 WINDOW_SHOWN.load(Ordering::Relaxed)
60}
61
62static WINDOW_OPENER: OnceLock<fn()> = OnceLock::new();
64static WINDOW_ASKED: AtomicBool = AtomicBool::new(false);
66
67pub fn tell_window_opener(open: fn()) {
68 let _ = WINDOW_OPENER.set(open);
69}
70
71fn window_open() -> bool {
73 if let Some(open) = WINDOW_OPENER.get() {
74 if !window_shows() && !WINDOW_ASKED.swap(true, Ordering::Relaxed) {
75 open();
76 }
77 }
78 window_shows()
79}
80
81pub fn native_view() -> (i64, i64) {
83 NATIVE_VIEW
84}
85
86static WINDOW_SIZE: Mutex<Option<(i64, i64)>> = Mutex::new(None);
88static WINDOW_RESIZER: OnceLock<fn(i64, i64)> = OnceLock::new();
90const DISPLAY_READ_ONLY: &str = "a program cannot resize the display: set window.width or canvas.width instead";
91
92pub fn tell_window_resizer(resize: fn(i64, i64)) {
93 let _ = WINDOW_RESIZER.set(resize);
94}
95
96pub fn tell_window_size(width: i64, height: i64) {
98 *WINDOW_SIZE.lock().unwrap() = Some((width, height));
99}
100
101fn window_size() -> (i64, i64) {
103 WINDOW_SIZE.lock().unwrap().unwrap_or(NATIVE_VIEW)
104}
105
106const DEFAULT_DPI: i64 = 96;
108static DPI_SET: Mutex<Option<i64>> = Mutex::new(None);
110
111fn set_display_dpi(dpi: i64) -> Result<(), String> {
112 if dpi <= 0 {
113 return Err(format!("display.dpi = {dpi} dpi: a display has a positive number of dots per inch"));
114 }
115 *DPI_SET.lock().unwrap() = Some(dpi);
116 READINGS.with(|readings| readings.borrow_mut().remove(DISPLAY_DPI));
117 Ok(())
118}
119
120pub fn write(name: &str, value: i64) -> Result<(), String> {
123 let (width, height) = match name {
124 WINDOW_WIDTH => (value, window_size().1),
125 WINDOW_HEIGHT => (window_size().0, value),
126 DISPLAY_DPI => return set_display_dpi(value),
127 DISPLAY_WIDTH | DISPLAY_HEIGHT => return Err(DISPLAY_READ_ONLY.to_string()),
128 other => {
129 return Err(format!("{other} cannot be set; settable: {WINDOW_WIDTH}, {WINDOW_HEIGHT}, {DISPLAY_DPI}"))
130 }
131 };
132 if width <= 0 || height <= 0 {
133 return Err(format!("a window of {width}×{height} points cannot show: its sides must be positive"));
134 }
135 tell_window_size(width, height);
136 if let Some(resize) = WINDOW_RESIZER.get() {
137 resize(width, height);
138 }
139 Ok(())
140}
141
142static KEY_PRESSES: Mutex<VecDeque<i64>> = Mutex::new(VecDeque::new());
145static ESCAPE_KEPT_BY_PROGRAM: AtomicBool = AtomicBool::new(false);
147
148pub fn escape_kept() -> bool {
149 ESCAPE_KEPT_BY_PROGRAM.load(Ordering::Relaxed)
150}
151const KEYS_KEPT: usize = 64;
152const KEY_INPUT: usize = 3;
153const BUTTON_INPUT: usize = 2;
154static CLICKS: Mutex<VecDeque<(f32, f32)>> = Mutex::new(VecDeque::new());
157static CLICK_READ: Mutex<(f32, f32)> = Mutex::new((0.0, 0.0));
158
159static DRAWING_ON_GPU: AtomicBool = AtomicBool::new(false);
161static GPU_PAINTER: OnceLock<fn() -> bool> = OnceLock::new();
163
164pub fn tell_gpu_painter(paints: fn() -> bool) {
165 let _ = GPU_PAINTER.set(paints);
166}
167
168pub fn tell_gpu_drawing(on_gpu: bool) {
169 DRAWING_ON_GPU.store(on_gpu, Ordering::Relaxed);
170}
171
172pub fn window_input() -> [f32; 4] {
173 WINDOW_INPUT.each_ref().map(|value| f32::from_bits(value.load(Ordering::Relaxed)))
174}
175
176pub fn tell_window_input(input: [f32; 4]) {
177 tell_window_key(input[KEY_INPUT]);
178 if input[BUTTON_INPUT] != 0.0 && window_input()[BUTTON_INPUT] == 0.0 {
180 kept_push(&mut CLICKS.lock().unwrap(), (input[0], input[1]));
181 }
182 for (value, number) in WINDOW_INPUT.iter().zip(input) {
183 value.store(number.to_bits(), Ordering::Relaxed);
184 }
185}
186
187fn kept_push<T>(presses: &mut VecDeque<T>, press: T) {
189 if presses.len() == KEYS_KEPT {
190 presses.pop_front();
191 }
192 presses.push_back(press);
193}
194
195static KEYS_HELD: Mutex<Vec<i64>> = Mutex::new(Vec::new());
197
198pub fn tell_keys_held(codes: &[i64]) {
199 *KEYS_HELD.lock().unwrap() = codes.to_vec();
200}
201
202pub fn key_held(code: i64) -> bool {
204 held_among(code, window_input()[KEY_INPUT] as i64, &KEYS_HELD.lock().unwrap())
205}
206
207pub fn held_among(code: i64, key: i64, held: &[i64]) -> bool {
209 match code {
210 0 => key == 0 && held.is_empty(),
211 _ => key == code || held.contains(&code),
212 }
213}
214
215pub fn tell_window_key(key: f32) {
217 if key != 0.0 && key != window_input()[KEY_INPUT] {
219 kept_push(&mut KEY_PRESSES.lock().unwrap(), key as i64);
220 }
221 WINDOW_INPUT[KEY_INPUT].store(key.to_bits(), Ordering::Relaxed);
222}
223
224thread_local! {
225 static READINGS: RefCell<HashMap<String, (Instant, i64)>> = RefCell::new(HashMap::new());
226}
227
228pub fn read(name: &str) -> Result<i64, String> {
230 if name == TIME_OF_DAY {
232 return Ok(crate::system_signals::millisecond_of_day());
233 }
234 if name == WINDOW_OPEN {
235 return Ok(window_open() as i64);
236 }
237 if name == KEY_DOWN {
238 return Ok((window_input()[KEY_INPUT] != 0.0) as i64);
239 }
240 if name == KEY_CODE_HELD {
241 return Ok(window_input()[KEY_INPUT] as i64);
242 }
243 if name == CLICK_PRESSED {
244 let click = CLICKS.lock().unwrap().pop_front();
245 if let Some(click) = click {
246 *CLICK_READ.lock().unwrap() = click;
247 }
248 return Ok(click.is_some() as i64);
249 }
250 if name == CLICK_X || name == CLICK_Y {
251 let (x, y) = *CLICK_READ.lock().unwrap();
252 return Ok(if name == CLICK_X { x } else { y } as i64);
253 }
254 if name == ESCAPE_KEPT {
255 ESCAPE_KEPT_BY_PROGRAM.store(true, Ordering::Relaxed);
256 return Ok(1);
257 }
258 if name == GPU_PAINTS {
259 return Ok(GPU_PAINTER.get().is_some_and(|paints| paints()) as i64);
260 }
261 if name == KEY_PRESSED {
262 return Ok(KEY_PRESSES.lock().unwrap().pop_front().unwrap_or(0));
263 }
264 if name == WINDOW_WIDTH || name == WINDOW_HEIGHT {
265 let (width, height) = window_size();
266 return Ok(if name == WINDOW_WIDTH { width } else { height });
267 }
268 if name == MOUSE_WHEEL {
269 return Ok(wheel());
270 }
271 if name == GPU_DRAWING {
272 return Ok(DRAWING_ON_GPU.load(Ordering::Relaxed) as i64);
273 }
274 if let Some(at) = [MOUSE_X, MOUSE_Y, MOUSE_DOWN].iter().position(|pointer| *pointer == name) {
275 return Ok(window_input()[at] as i64);
276 }
277 let kept = READINGS.with(|readings| {
278 readings.borrow().get(name).filter(|(when, _)| when.elapsed() < READING_LIFETIME).map(|(_, value)| *value)
279 });
280 if let Some(value) = kept {
281 return Ok(value);
282 }
283 let value = read_now(name)?;
284 READINGS.with(|readings| readings.borrow_mut().insert(name.to_string(), (Instant::now(), value)));
285 Ok(value)
286}
287
288fn read_now(name: &str) -> Result<i64, String> {
289 match name {
290 BATTERY => battery().map(|(percent, _)| percent),
291 CHARGING => battery().map(|(_, charging)| charging as i64),
292 ONLINE => Ok(online() as i64),
293 DARK_MODE => dark_mode().map(|dark| dark as i64),
294 CLIPBOARD_COUNT => clipboard_count(),
295 VIEW_WIDTH => Ok(NATIVE_VIEW.0),
296 VIEW_HEIGHT => Ok(NATIVE_VIEW.1),
297 DISPLAY_WIDTH => display_size().map(|(width, _)| width),
298 DISPLAY_HEIGHT => display_size().map(|(_, height)| height),
299 DISPLAY_DPI => Ok(DPI_SET.lock().unwrap().unwrap_or_else(|| display_dpi().unwrap_or(DEFAULT_DPI))),
300 other => Err(format!("{other} is no system value; known: {}", SYSTEM_VALUES.map(|(name, _)| name).join(", "))),
301 }
302}
303
304#[cfg(target_os = "macos")]
306fn display_size() -> Result<(i64, i64), String> {
307 #[link(name = "CoreGraphics", kind = "framework")]
308 unsafe extern "C" {
309 fn CGMainDisplayID() -> u32;
310 fn CGDisplayPixelsWide(display: u32) -> usize;
311 fn CGDisplayPixelsHigh(display: u32) -> usize;
312 }
313 let (width, height) = unsafe {
315 let display = CGMainDisplayID();
316 (CGDisplayPixelsWide(display) as i64, CGDisplayPixelsHigh(display) as i64)
317 };
318 match width {
319 0 => Err("display.width: this Mac has no display".to_string()),
320 _ => Ok((width, height)),
321 }
322}
323
324#[cfg(not(target_os = "macos"))]
326fn display_size() -> Result<(i64, i64), String> {
327 let report = output("xrandr", &["--current"])?;
328 let current = report.split("current ").nth(1).ok_or("display.width: no display (xrandr tells no screen)")?;
329 let mut sides = current.split(',').next().unwrap_or_default().split(" x ").map(|side| side.trim().parse::<i64>());
330 match (sides.next(), sides.next()) {
331 (Some(Ok(width)), Some(Ok(height))) => Ok((width, height)),
332 _ => Err(format!("display.width: xrandr's screen is unreadable: {current}")),
333 }
334}
335
336#[cfg(target_os = "macos")]
338fn display_dpi() -> Result<i64, String> {
339 #[repr(C)]
340 struct Size {
341 width: f64,
342 height: f64,
343 }
344 #[link(name = "CoreGraphics", kind = "framework")]
345 unsafe extern "C" {
346 fn CGMainDisplayID() -> u32;
347 fn CGDisplayScreenSize(display: u32) -> Size;
348 fn CGDisplayCopyDisplayMode(display: u32) -> *mut std::ffi::c_void;
349 fn CGDisplayModeGetPixelWidth(mode: *mut std::ffi::c_void) -> usize;
350 fn CGDisplayModeRelease(mode: *mut std::ffi::c_void);
351 }
352 let (pixels, millimetres) = unsafe {
354 let display = CGMainDisplayID();
355 let mode = CGDisplayCopyDisplayMode(display);
356 let pixels = if mode.is_null() { 0 } else { CGDisplayModeGetPixelWidth(mode) };
357 if !mode.is_null() {
358 CGDisplayModeRelease(mode);
359 }
360 (pixels as f64, CGDisplayScreenSize(display).width)
361 };
362 dots_per_inch(pixels, millimetres)
363}
364
365#[cfg(not(target_os = "macos"))]
367fn display_dpi() -> Result<i64, String> {
368 let report = output("xrandr", &["--current"])?;
369 let screen = report.lines().find(|line| line.contains(" connected") && line.contains("mm x ")).unwrap_or_default();
370 let words: Vec<&str> = screen.split_whitespace().collect();
371 let pixels =
372 words.iter().find_map(|word| word.split_once('x').filter(|(_, rest)| rest.contains('+'))?.0.parse().ok());
373 let millimetres = words.iter().find_map(|word| word.strip_suffix("mm")?.parse().ok());
374 dots_per_inch(pixels.unwrap_or(0.0), millimetres.unwrap_or(0.0))
375}
376
377const MILLIMETRES_PER_INCH: f64 = 25.4;
378
379fn dots_per_inch(pixels: f64, millimetres: f64) -> Result<i64, String> {
380 match pixels > 0.0 && millimetres > 0.0 {
381 true => Ok((pixels * MILLIMETRES_PER_INCH / millimetres).round() as i64),
382 false => Err("display.dpi: the screen tells no physical size".to_string()),
383 }
384}
385
386fn online() -> bool {
387 std::net::UdpSocket::bind("0.0.0.0:0").and_then(|socket| socket.connect(INTERNET_ADDRESS)).is_ok()
388}
389
390fn output(program: &str, arguments: &[&str]) -> Result<String, String> {
391 let output = std::process::Command::new(program)
392 .args(arguments)
393 .output()
394 .map_err(|problem| format!("{program}: {problem}"))?;
395 Ok(String::from_utf8_lossy(&output.stdout).into_owned())
396}
397
398#[cfg(target_os = "macos")]
400fn battery() -> Result<(i64, bool), String> {
401 let report = output("pmset", &["-g", "batt"])?;
403 let line = report.lines().find(|line| line.contains('%')).ok_or("battery: this Mac has no battery")?;
404 let percent = line
405 .split('%')
406 .next()
407 .and_then(|before| before.rsplit(|letter: char| !letter.is_ascii_digit()).next())
408 .and_then(|digits| digits.parse().ok());
409 let charging = line.contains("; charging") || line.contains("; charged") || report.contains("'AC Power'");
410 Ok((percent.ok_or_else(|| format!("battery: cannot read {line:?}"))?, charging))
411}
412
413#[cfg(target_os = "linux")]
414fn battery() -> Result<(i64, bool), String> {
415 let supply = std::fs::read_dir("/sys/class/power_supply")
416 .map_err(|problem| format!("battery: {problem}"))?
417 .flatten()
418 .map(|entry| entry.path())
419 .find(|path| path.join("capacity").exists())
420 .ok_or("battery: this machine has no battery")?;
421 let read = |file: &str| {
422 std::fs::read_to_string(supply.join(file))
423 .map(|text| text.trim().to_string())
424 .map_err(|problem| format!("battery: {problem}"))
425 };
426 let percent = read("capacity")?.parse().map_err(|_| "battery: cannot read its capacity".to_string())?;
427 Ok((percent, matches!(read("status")?.as_str(), "Charging" | "Full")))
428}
429
430#[cfg(not(any(target_os = "macos", target_os = "linux")))]
431fn battery() -> Result<(i64, bool), String> {
432 Err("battery: not readable on this platform yet".to_string())
433}
434
435#[cfg(target_os = "macos")]
436fn dark_mode() -> Result<bool, String> {
437 Ok(output("defaults", &["read", "-g", "AppleInterfaceStyle"])?.trim() == "Dark")
439}
440
441#[cfg(target_os = "linux")]
442fn dark_mode() -> Result<bool, String> {
443 Ok(output("gsettings", &["get", "org.gnome.desktop.interface", "color-scheme"])?.contains("dark"))
444}
445
446#[cfg(not(any(target_os = "macos", target_os = "linux")))]
447fn dark_mode() -> Result<bool, String> {
448 Err("dark mode: not readable on this platform yet".to_string())
449}
450
451#[cfg(target_os = "macos")]
453fn clipboard_count() -> Result<i64, String> {
454 use std::ffi::{c_char, c_void, CStr};
455 type Object = *mut c_void;
456 const APPKIT: &CStr = c"/System/Library/Frameworks/AppKit.framework/AppKit";
457 let symbol = |name: &CStr| -> Result<*mut c_void, String> {
458 let found = unsafe { libc::dlsym(libc::RTLD_DEFAULT, name.as_ptr()) };
459 if found.is_null() {
460 Err(format!("clipboard: no {name:?}"))
461 } else {
462 Ok(found)
463 }
464 };
465 if unsafe { libc::dlopen(APPKIT.as_ptr(), libc::RTLD_LAZY) }.is_null() {
466 return Err("clipboard: cannot load AppKit".to_string());
467 }
468 let class: extern "C" fn(*const c_char) -> Object = unsafe { std::mem::transmute(symbol(c"objc_getClass")?) };
470 let selector: extern "C" fn(*const c_char) -> Object = unsafe { std::mem::transmute(symbol(c"sel_registerName")?) };
471 let send = symbol(c"objc_msgSend")?;
472 let send_for_object: extern "C" fn(Object, Object) -> Object = unsafe { std::mem::transmute(send) };
473 let send_for_count: extern "C" fn(Object, Object) -> isize = unsafe { std::mem::transmute(send) };
474 let pasteboard = send_for_object(class(c"NSPasteboard".as_ptr()), selector(c"generalPasteboard".as_ptr()));
475 if pasteboard.is_null() {
476 return Err("clipboard: no pasteboard".to_string());
477 }
478 Ok(send_for_count(pasteboard, selector(c"changeCount".as_ptr())) as i64)
479}
480
481#[cfg(not(target_os = "macos"))]
483fn clipboard_count() -> Result<i64, String> {
484 use std::hash::{Hash, Hasher};
485 let mut hasher = std::collections::hash_map::DefaultHasher::new();
486 clipboard_text()?.hash(&mut hasher);
487 Ok(hasher.finish() as i64)
488}
489
490pub fn notify(text: &str) -> Result<(), String> {
493 #[cfg(target_os = "macos")]
494 let shown = std::process::Command::new("osascript")
495 .args([
496 "-e",
497 "on run argv",
498 "-e",
499 &format!("display notification (item 1 of argv) with title \"{NOTIFICATION_TITLE}\""),
500 "-e",
501 "end run",
502 text,
503 ])
504 .output();
505 #[cfg(not(target_os = "macos"))]
506 let shown = std::process::Command::new("notify-send").args([NOTIFICATION_TITLE, text]).output();
507 match shown {
508 Ok(output) if output.status.success() => Ok(()),
509 Ok(output) => Err(format!("notify: {}", String::from_utf8_lossy(&output.stderr).trim())),
510 Err(problem) => Err(format!("notify: no notifier on this machine ({problem})")),
511 }
512}
513
514pub fn write_clipboard(text: &str) -> Result<(), String> {
516 #[cfg(target_os = "macos")]
517 return input("pbcopy", &[], text);
518 #[cfg(not(target_os = "macos"))]
519 input("wl-copy", &[], text)
520 .or_else(|_| input("xclip", &["-selection", "clipboard", "-i"], text))
521 .map_err(|problem| format!("clipboard: needs wl-copy or xclip ({problem})"))
522}
523
524fn input(program: &str, arguments: &[&str], text: &str) -> Result<(), String> {
526 use std::io::Write;
527 let mut child = std::process::Command::new(program)
528 .args(arguments)
529 .stdin(std::process::Stdio::piped())
530 .spawn()
531 .map_err(|problem| format!("{program}: {problem}"))?;
532 child
533 .stdin
534 .take()
535 .expect("a piped input")
536 .write_all(text.as_bytes())
537 .map_err(|problem| format!("{program}: {problem}"))?;
538 let status = child.wait().map_err(|problem| format!("{program}: {problem}"))?;
539 status.success().then_some(()).ok_or_else(|| format!("{program} failed: {status}"))
540}
541
542pub fn clipboard_text() -> Result<String, String> {
544 #[cfg(target_os = "macos")]
545 return output("pbpaste", &[]);
546 #[cfg(not(target_os = "macos"))]
547 output("wl-paste", &["--no-newline"])
548 .or_else(|_| output("xclip", &["-selection", "clipboard", "-o"]))
549 .map_err(|problem| format!("clipboard: needs wl-paste or xclip ({problem})"))
550}