use crate::host_words::{
BATTERY, CHARGING, CLICK_PRESSED, CLICK_X, CLICK_Y, CLIPBOARD_COUNT, DARK_MODE, DISPLAY_DPI, DISPLAY_HEIGHT,
DISPLAY_WIDTH, ESCAPE_KEPT, GPU_DRAWING, GPU_PAINTS, KEY_CODE_HELD, KEY_DOWN, KEY_PRESSED, MOUSE_DOWN, MOUSE_WHEEL,
MOUSE_X, MOUSE_Y, ONLINE, SYSTEM_VALUES, TIME_OF_DAY, VIEW_HEIGHT, VIEW_WIDTH, WINDOW_HEIGHT, WINDOW_OPEN,
WINDOW_WIDTH,
};
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
const NOTIFICATION_TITLE: &str = "warp";
const READING_LIFETIME: Duration = Duration::from_secs(1);
const NATIVE_VIEW: (i64, i64) = (640, 480);
const INTERNET_ADDRESS: &str = "1.1.1.1:53";
static WINDOW_INPUT: [AtomicU32; 4] = [const { AtomicU32::new(0) }; 4];
static WHEEL: AtomicI64 = AtomicI64::new(0);
pub fn wheel() -> i64 {
WHEEL.load(Ordering::Relaxed)
}
pub fn tell_wheel(steps: i64) {
WHEEL.store(steps, Ordering::Relaxed);
}
static WINDOW_SHOWN: AtomicBool = AtomicBool::new(false);
pub fn tell_window_open(open: bool) {
WINDOW_SHOWN.store(open, Ordering::Relaxed);
}
pub fn window_shows() -> bool {
WINDOW_SHOWN.load(Ordering::Relaxed)
}
static WINDOW_OPENER: OnceLock<fn()> = OnceLock::new();
static WINDOW_ASKED: AtomicBool = AtomicBool::new(false);
pub fn tell_window_opener(open: fn()) {
let _ = WINDOW_OPENER.set(open);
}
fn window_open() -> bool {
if let Some(open) = WINDOW_OPENER.get() {
if !window_shows() && !WINDOW_ASKED.swap(true, Ordering::Relaxed) {
open();
}
}
window_shows()
}
pub fn native_view() -> (i64, i64) {
NATIVE_VIEW
}
static WINDOW_SIZE: Mutex<Option<(i64, i64)>> = Mutex::new(None);
static WINDOW_RESIZER: OnceLock<fn(i64, i64)> = OnceLock::new();
const DISPLAY_READ_ONLY: &str = "a program cannot resize the display: set window.width or canvas.width instead";
pub fn tell_window_resizer(resize: fn(i64, i64)) {
let _ = WINDOW_RESIZER.set(resize);
}
pub fn tell_window_size(width: i64, height: i64) {
*WINDOW_SIZE.lock().unwrap() = Some((width, height));
}
fn window_size() -> (i64, i64) {
WINDOW_SIZE.lock().unwrap().unwrap_or(NATIVE_VIEW)
}
const DEFAULT_DPI: i64 = 96;
static DPI_SET: Mutex<Option<i64>> = Mutex::new(None);
fn set_display_dpi(dpi: i64) -> Result<(), String> {
if dpi <= 0 {
return Err(format!("display.dpi = {dpi} dpi: a display has a positive number of dots per inch"));
}
*DPI_SET.lock().unwrap() = Some(dpi);
READINGS.with(|readings| readings.borrow_mut().remove(DISPLAY_DPI));
Ok(())
}
pub fn write(name: &str, value: i64) -> Result<(), String> {
let (width, height) = match name {
WINDOW_WIDTH => (value, window_size().1),
WINDOW_HEIGHT => (window_size().0, value),
DISPLAY_DPI => return set_display_dpi(value),
DISPLAY_WIDTH | DISPLAY_HEIGHT => return Err(DISPLAY_READ_ONLY.to_string()),
other => {
return Err(format!("{other} cannot be set; settable: {WINDOW_WIDTH}, {WINDOW_HEIGHT}, {DISPLAY_DPI}"))
}
};
if width <= 0 || height <= 0 {
return Err(format!("a window of {width}×{height} points cannot show: its sides must be positive"));
}
tell_window_size(width, height);
if let Some(resize) = WINDOW_RESIZER.get() {
resize(width, height);
}
Ok(())
}
static KEY_PRESSES: Mutex<VecDeque<i64>> = Mutex::new(VecDeque::new());
static ESCAPE_KEPT_BY_PROGRAM: AtomicBool = AtomicBool::new(false);
pub fn escape_kept() -> bool {
ESCAPE_KEPT_BY_PROGRAM.load(Ordering::Relaxed)
}
const KEYS_KEPT: usize = 64;
const KEY_INPUT: usize = 3;
const BUTTON_INPUT: usize = 2;
static CLICKS: Mutex<VecDeque<(f32, f32)>> = Mutex::new(VecDeque::new());
static CLICK_READ: Mutex<(f32, f32)> = Mutex::new((0.0, 0.0));
static DRAWING_ON_GPU: AtomicBool = AtomicBool::new(false);
static GPU_PAINTER: OnceLock<fn() -> bool> = OnceLock::new();
pub fn tell_gpu_painter(paints: fn() -> bool) {
let _ = GPU_PAINTER.set(paints);
}
pub fn tell_gpu_drawing(on_gpu: bool) {
DRAWING_ON_GPU.store(on_gpu, Ordering::Relaxed);
}
pub fn window_input() -> [f32; 4] {
WINDOW_INPUT.each_ref().map(|value| f32::from_bits(value.load(Ordering::Relaxed)))
}
pub fn tell_window_input(input: [f32; 4]) {
tell_window_key(input[KEY_INPUT]);
if input[BUTTON_INPUT] != 0.0 && window_input()[BUTTON_INPUT] == 0.0 {
kept_push(&mut CLICKS.lock().unwrap(), (input[0], input[1]));
}
for (value, number) in WINDOW_INPUT.iter().zip(input) {
value.store(number.to_bits(), Ordering::Relaxed);
}
}
fn kept_push<T>(presses: &mut VecDeque<T>, press: T) {
if presses.len() == KEYS_KEPT {
presses.pop_front();
}
presses.push_back(press);
}
static KEYS_HELD: Mutex<Vec<i64>> = Mutex::new(Vec::new());
pub fn tell_keys_held(codes: &[i64]) {
*KEYS_HELD.lock().unwrap() = codes.to_vec();
}
pub fn key_held(code: i64) -> bool {
held_among(code, window_input()[KEY_INPUT] as i64, &KEYS_HELD.lock().unwrap())
}
pub fn held_among(code: i64, key: i64, held: &[i64]) -> bool {
match code {
0 => key == 0 && held.is_empty(),
_ => key == code || held.contains(&code),
}
}
pub fn tell_window_key(key: f32) {
if key != 0.0 && key != window_input()[KEY_INPUT] {
kept_push(&mut KEY_PRESSES.lock().unwrap(), key as i64);
}
WINDOW_INPUT[KEY_INPUT].store(key.to_bits(), Ordering::Relaxed);
}
thread_local! {
static READINGS: RefCell<HashMap<String, (Instant, i64)>> = RefCell::new(HashMap::new());
}
pub fn read(name: &str) -> Result<i64, String> {
if name == TIME_OF_DAY {
return Ok(crate::system_signals::millisecond_of_day());
}
if name == WINDOW_OPEN {
return Ok(window_open() as i64);
}
if name == KEY_DOWN {
return Ok((window_input()[KEY_INPUT] != 0.0) as i64);
}
if name == KEY_CODE_HELD {
return Ok(window_input()[KEY_INPUT] as i64);
}
if name == CLICK_PRESSED {
let click = CLICKS.lock().unwrap().pop_front();
if let Some(click) = click {
*CLICK_READ.lock().unwrap() = click;
}
return Ok(click.is_some() as i64);
}
if name == CLICK_X || name == CLICK_Y {
let (x, y) = *CLICK_READ.lock().unwrap();
return Ok(if name == CLICK_X { x } else { y } as i64);
}
if name == ESCAPE_KEPT {
ESCAPE_KEPT_BY_PROGRAM.store(true, Ordering::Relaxed);
return Ok(1);
}
if name == GPU_PAINTS {
return Ok(GPU_PAINTER.get().is_some_and(|paints| paints()) as i64);
}
if name == KEY_PRESSED {
return Ok(KEY_PRESSES.lock().unwrap().pop_front().unwrap_or(0));
}
if name == WINDOW_WIDTH || name == WINDOW_HEIGHT {
let (width, height) = window_size();
return Ok(if name == WINDOW_WIDTH { width } else { height });
}
if name == MOUSE_WHEEL {
return Ok(wheel());
}
if name == GPU_DRAWING {
return Ok(DRAWING_ON_GPU.load(Ordering::Relaxed) as i64);
}
if let Some(at) = [MOUSE_X, MOUSE_Y, MOUSE_DOWN].iter().position(|pointer| *pointer == name) {
return Ok(window_input()[at] as i64);
}
let kept = READINGS.with(|readings| {
readings.borrow().get(name).filter(|(when, _)| when.elapsed() < READING_LIFETIME).map(|(_, value)| *value)
});
if let Some(value) = kept {
return Ok(value);
}
let value = read_now(name)?;
READINGS.with(|readings| readings.borrow_mut().insert(name.to_string(), (Instant::now(), value)));
Ok(value)
}
fn read_now(name: &str) -> Result<i64, String> {
match name {
BATTERY => battery().map(|(percent, _)| percent),
CHARGING => battery().map(|(_, charging)| charging as i64),
ONLINE => Ok(online() as i64),
DARK_MODE => dark_mode().map(|dark| dark as i64),
CLIPBOARD_COUNT => clipboard_count(),
VIEW_WIDTH => Ok(NATIVE_VIEW.0),
VIEW_HEIGHT => Ok(NATIVE_VIEW.1),
DISPLAY_WIDTH => display_size().map(|(width, _)| width),
DISPLAY_HEIGHT => display_size().map(|(_, height)| height),
DISPLAY_DPI => Ok(DPI_SET.lock().unwrap().unwrap_or_else(|| display_dpi().unwrap_or(DEFAULT_DPI))),
other => Err(format!("{other} is no system value; known: {}", SYSTEM_VALUES.map(|(name, _)| name).join(", "))),
}
}
#[cfg(target_os = "macos")]
fn display_size() -> Result<(i64, i64), String> {
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGMainDisplayID() -> u32;
fn CGDisplayPixelsWide(display: u32) -> usize;
fn CGDisplayPixelsHigh(display: u32) -> usize;
}
let (width, height) = unsafe {
let display = CGMainDisplayID();
(CGDisplayPixelsWide(display) as i64, CGDisplayPixelsHigh(display) as i64)
};
match width {
0 => Err("display.width: this Mac has no display".to_string()),
_ => Ok((width, height)),
}
}
#[cfg(not(target_os = "macos"))]
fn display_size() -> Result<(i64, i64), String> {
let report = output("xrandr", &["--current"])?;
let current = report.split("current ").nth(1).ok_or("display.width: no display (xrandr tells no screen)")?;
let mut sides = current.split(',').next().unwrap_or_default().split(" x ").map(|side| side.trim().parse::<i64>());
match (sides.next(), sides.next()) {
(Some(Ok(width)), Some(Ok(height))) => Ok((width, height)),
_ => Err(format!("display.width: xrandr's screen is unreadable: {current}")),
}
}
#[cfg(target_os = "macos")]
fn display_dpi() -> Result<i64, String> {
#[repr(C)]
struct Size {
width: f64,
height: f64,
}
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGMainDisplayID() -> u32;
fn CGDisplayScreenSize(display: u32) -> Size;
fn CGDisplayCopyDisplayMode(display: u32) -> *mut std::ffi::c_void;
fn CGDisplayModeGetPixelWidth(mode: *mut std::ffi::c_void) -> usize;
fn CGDisplayModeRelease(mode: *mut std::ffi::c_void);
}
let (pixels, millimetres) = unsafe {
let display = CGMainDisplayID();
let mode = CGDisplayCopyDisplayMode(display);
let pixels = if mode.is_null() { 0 } else { CGDisplayModeGetPixelWidth(mode) };
if !mode.is_null() {
CGDisplayModeRelease(mode);
}
(pixels as f64, CGDisplayScreenSize(display).width)
};
dots_per_inch(pixels, millimetres)
}
#[cfg(not(target_os = "macos"))]
fn display_dpi() -> Result<i64, String> {
let report = output("xrandr", &["--current"])?;
let screen = report.lines().find(|line| line.contains(" connected") && line.contains("mm x ")).unwrap_or_default();
let words: Vec<&str> = screen.split_whitespace().collect();
let pixels =
words.iter().find_map(|word| word.split_once('x').filter(|(_, rest)| rest.contains('+'))?.0.parse().ok());
let millimetres = words.iter().find_map(|word| word.strip_suffix("mm")?.parse().ok());
dots_per_inch(pixels.unwrap_or(0.0), millimetres.unwrap_or(0.0))
}
const MILLIMETRES_PER_INCH: f64 = 25.4;
fn dots_per_inch(pixels: f64, millimetres: f64) -> Result<i64, String> {
match pixels > 0.0 && millimetres > 0.0 {
true => Ok((pixels * MILLIMETRES_PER_INCH / millimetres).round() as i64),
false => Err("display.dpi: the screen tells no physical size".to_string()),
}
}
fn online() -> bool {
std::net::UdpSocket::bind("0.0.0.0:0").and_then(|socket| socket.connect(INTERNET_ADDRESS)).is_ok()
}
fn output(program: &str, arguments: &[&str]) -> Result<String, String> {
let output = std::process::Command::new(program)
.args(arguments)
.output()
.map_err(|problem| format!("{program}: {problem}"))?;
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[cfg(target_os = "macos")]
fn battery() -> Result<(i64, bool), String> {
let report = output("pmset", &["-g", "batt"])?;
let line = report.lines().find(|line| line.contains('%')).ok_or("battery: this Mac has no battery")?;
let percent = line
.split('%')
.next()
.and_then(|before| before.rsplit(|letter: char| !letter.is_ascii_digit()).next())
.and_then(|digits| digits.parse().ok());
let charging = line.contains("; charging") || line.contains("; charged") || report.contains("'AC Power'");
Ok((percent.ok_or_else(|| format!("battery: cannot read {line:?}"))?, charging))
}
#[cfg(target_os = "linux")]
fn battery() -> Result<(i64, bool), String> {
let supply = std::fs::read_dir("/sys/class/power_supply")
.map_err(|problem| format!("battery: {problem}"))?
.flatten()
.map(|entry| entry.path())
.find(|path| path.join("capacity").exists())
.ok_or("battery: this machine has no battery")?;
let read = |file: &str| {
std::fs::read_to_string(supply.join(file))
.map(|text| text.trim().to_string())
.map_err(|problem| format!("battery: {problem}"))
};
let percent = read("capacity")?.parse().map_err(|_| "battery: cannot read its capacity".to_string())?;
Ok((percent, matches!(read("status")?.as_str(), "Charging" | "Full")))
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn battery() -> Result<(i64, bool), String> {
Err("battery: not readable on this platform yet".to_string())
}
#[cfg(target_os = "macos")]
fn dark_mode() -> Result<bool, String> {
Ok(output("defaults", &["read", "-g", "AppleInterfaceStyle"])?.trim() == "Dark")
}
#[cfg(target_os = "linux")]
fn dark_mode() -> Result<bool, String> {
Ok(output("gsettings", &["get", "org.gnome.desktop.interface", "color-scheme"])?.contains("dark"))
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn dark_mode() -> Result<bool, String> {
Err("dark mode: not readable on this platform yet".to_string())
}
#[cfg(target_os = "macos")]
fn clipboard_count() -> Result<i64, String> {
use std::ffi::{c_char, c_void, CStr};
type Object = *mut c_void;
const APPKIT: &CStr = c"/System/Library/Frameworks/AppKit.framework/AppKit";
let symbol = |name: &CStr| -> Result<*mut c_void, String> {
let found = unsafe { libc::dlsym(libc::RTLD_DEFAULT, name.as_ptr()) };
if found.is_null() {
Err(format!("clipboard: no {name:?}"))
} else {
Ok(found)
}
};
if unsafe { libc::dlopen(APPKIT.as_ptr(), libc::RTLD_LAZY) }.is_null() {
return Err("clipboard: cannot load AppKit".to_string());
}
let class: extern "C" fn(*const c_char) -> Object = unsafe { std::mem::transmute(symbol(c"objc_getClass")?) };
let selector: extern "C" fn(*const c_char) -> Object = unsafe { std::mem::transmute(symbol(c"sel_registerName")?) };
let send = symbol(c"objc_msgSend")?;
let send_for_object: extern "C" fn(Object, Object) -> Object = unsafe { std::mem::transmute(send) };
let send_for_count: extern "C" fn(Object, Object) -> isize = unsafe { std::mem::transmute(send) };
let pasteboard = send_for_object(class(c"NSPasteboard".as_ptr()), selector(c"generalPasteboard".as_ptr()));
if pasteboard.is_null() {
return Err("clipboard: no pasteboard".to_string());
}
Ok(send_for_count(pasteboard, selector(c"changeCount".as_ptr())) as i64)
}
#[cfg(not(target_os = "macos"))]
fn clipboard_count() -> Result<i64, String> {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
clipboard_text()?.hash(&mut hasher);
Ok(hasher.finish() as i64)
}
pub fn notify(text: &str) -> Result<(), String> {
#[cfg(target_os = "macos")]
let shown = std::process::Command::new("osascript")
.args([
"-e",
"on run argv",
"-e",
&format!("display notification (item 1 of argv) with title \"{NOTIFICATION_TITLE}\""),
"-e",
"end run",
text,
])
.output();
#[cfg(not(target_os = "macos"))]
let shown = std::process::Command::new("notify-send").args([NOTIFICATION_TITLE, text]).output();
match shown {
Ok(output) if output.status.success() => Ok(()),
Ok(output) => Err(format!("notify: {}", String::from_utf8_lossy(&output.stderr).trim())),
Err(problem) => Err(format!("notify: no notifier on this machine ({problem})")),
}
}
pub fn write_clipboard(text: &str) -> Result<(), String> {
#[cfg(target_os = "macos")]
return input("pbcopy", &[], text);
#[cfg(not(target_os = "macos"))]
input("wl-copy", &[], text)
.or_else(|_| input("xclip", &["-selection", "clipboard", "-i"], text))
.map_err(|problem| format!("clipboard: needs wl-copy or xclip ({problem})"))
}
fn input(program: &str, arguments: &[&str], text: &str) -> Result<(), String> {
use std::io::Write;
let mut child = std::process::Command::new(program)
.args(arguments)
.stdin(std::process::Stdio::piped())
.spawn()
.map_err(|problem| format!("{program}: {problem}"))?;
child
.stdin
.take()
.expect("a piped input")
.write_all(text.as_bytes())
.map_err(|problem| format!("{program}: {problem}"))?;
let status = child.wait().map_err(|problem| format!("{program}: {problem}"))?;
status.success().then_some(()).ok_or_else(|| format!("{program} failed: {status}"))
}
pub fn clipboard_text() -> Result<String, String> {
#[cfg(target_os = "macos")]
return output("pbpaste", &[]);
#[cfg(not(target_os = "macos"))]
output("wl-paste", &["--no-newline"])
.or_else(|_| output("xclip", &["-selection", "clipboard", "-o"]))
.map_err(|problem| format!("clipboard: needs wl-paste or xclip ({problem})"))
}