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