pub mod figma_bridge;
pub mod font;
mod hud_render;
mod icon;
pub mod placement;
#[cfg(target_os = "linux")]
mod tray;
mod types;
pub use types::*;
pub fn render_app_icon_rgba(size: u32) -> Vec<u8> {
icon::render_app_icon_rgba(size)
}
pub fn rasterize_svg(svg_bytes: &[u8], size: u32) -> Option<Vec<u8>> {
let opts = usvg::Options::default();
let tree = usvg::Tree::from_data(svg_bytes, &opts).ok()?;
let mut pixmap = resvg::tiny_skia::Pixmap::new(size, size)?;
let svg_size = tree.size();
let svg_max = svg_size.width().max(svg_size.height()).max(1.0);
let scale = size as f32 / svg_max;
let dx = (size as f32 - svg_size.width() * scale) * 0.5;
let dy = (size as f32 - svg_size.height() * scale) * 0.5;
resvg::render(
&tree,
resvg::tiny_skia::Transform::from_scale(scale, scale).post_translate(dx, dy),
&mut pixmap.as_mut(),
);
let mut out = pixmap.data().to_vec();
for px in out.chunks_exact_mut(4) {
let a = px[3];
if a == 0 {
continue;
}
let inv = 255.0 / a as f32;
px[0] = ((px[0] as f32 * inv).round() as u32).min(255) as u8;
px[1] = ((px[1] as f32 * inv).round() as u32).min(255) as u8;
px[2] = ((px[2] as f32 * inv).round() as u32).min(255) as u8;
}
Some(out)
}
pub fn rasterize_png(png_bytes: &[u8], size: u32) -> Option<Vec<u8>> {
let source = resvg::tiny_skia::Pixmap::decode_png(png_bytes).ok()?;
let mut out_pixmap = resvg::tiny_skia::Pixmap::new(size, size)?;
let src_w = source.width() as f32;
let src_h = source.height() as f32;
let src_max = src_w.max(src_h).max(1.0);
let scale = size as f32 / src_max;
let dx = (size as f32 - src_w * scale) * 0.5;
let dy = (size as f32 - src_h * scale) * 0.5;
let paint = resvg::tiny_skia::PixmapPaint {
opacity: 1.0,
blend_mode: resvg::tiny_skia::BlendMode::Source,
quality: resvg::tiny_skia::FilterQuality::Bilinear,
};
out_pixmap.draw_pixmap(
0,
0,
source.as_ref(),
&paint,
resvg::tiny_skia::Transform::from_scale(scale, scale).post_translate(dx, dy),
None,
);
let mut out = out_pixmap.data().to_vec();
for px in out.chunks_exact_mut(4) {
let a = px[3];
if a == 0 {
continue;
}
let inv = 255.0 / a as f32;
px[0] = ((px[0] as f32 * inv).round() as u32).min(255) as u8;
px[1] = ((px[1] as f32 * inv).round() as u32).min(255) as u8;
px[2] = ((px[2] as f32 * inv).round() as u32).min(255) as u8;
}
Some(out)
}
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows_impl;
#[cfg(target_os = "macos")]
pub fn bootstrap_main<F>(daemon_body: F) -> !
where
F: FnOnce() + Send + 'static,
{
macos::bootstrap_main(daemon_body)
}
#[cfg(target_os = "macos")]
pub fn extract_macos_app_icon_rgba(
bundle_path: &std::path::Path,
size: u32,
) -> Option<Vec<u8>> {
macos::extract_macos_app_icon_rgba(bundle_path, size)
}
#[cfg(target_os = "macos")]
pub fn primary_screen_visible_height() -> Option<f32> {
macos::primary_screen_visible_height()
}
#[cfg(target_os = "macos")]
pub fn promote_to_foreground_application() {
use std::os::raw::c_int;
#[repr(C)]
#[derive(Clone, Copy)]
struct ProcessSerialNumber {
high: u32,
low: u32,
}
const K_CURRENT_PROCESS: u32 = 2;
const K_PROCESS_TRANSFORM_TO_FOREGROUND_APPLICATION: u32 = 1;
#[link(name = "ApplicationServices", kind = "framework")]
unsafe extern "C" {
fn TransformProcessType(
psn: *const ProcessSerialNumber,
transform_type: u32,
) -> c_int;
}
let psn = ProcessSerialNumber {
high: 0,
low: K_CURRENT_PROCESS,
};
let status = unsafe {
TransformProcessType(&psn, K_PROCESS_TRANSFORM_TO_FOREGROUND_APPLICATION)
};
if status != 0 {
log::warn!(
"promote_to_foreground_application: TransformProcessType returned {status}"
);
return;
}
log::info!("macos: promoted process to ForegroundApplication");
std::thread::Builder::new()
.name("vernier-foreground-activate".into())
.spawn(|| {
use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication};
std::thread::sleep(std::time::Duration::from_millis(400));
let running = unsafe { NSRunningApplication::currentApplication() };
unsafe {
running.activateWithOptions(
NSApplicationActivationOptions::ActivateAllWindows,
);
}
log::info!(
"macos: deferred activate fired (NSRunningApplication.activateWithOptions)"
);
})
.expect("spawn vernier-foreground-activate thread");
}
#[cfg(target_os = "macos")]
pub fn focus_macos_app_by_pid(pid: i32) {
use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication};
let Some(app) =
(unsafe { NSRunningApplication::runningApplicationWithProcessIdentifier(pid) })
else {
log::debug!("focus_macos_app_by_pid: no NSRunningApplication for pid {pid}");
return;
};
unsafe {
app.activateWithOptions(NSApplicationActivationOptions::ActivateAllWindows);
}
}
pub fn probe_screen_recording() -> std::sync::mpsc::Receiver<bool> {
#[cfg(target_os = "macos")]
{
macos::probe_screen_recording()
}
#[cfg(not(target_os = "macos"))]
{
let (tx, rx) = std::sync::mpsc::channel();
let _ = tx.send(true);
rx
}
}
pub fn open_screen_recording_settings() {
#[cfg(target_os = "macos")]
{
if let Err(e) = std::process::Command::new("open")
.arg(
"x-apple.systempreferences:com.apple.preference.security\
?Privacy_ScreenCapture",
)
.spawn()
{
log::warn!("open_screen_recording_settings: spawn `open` failed: {e}");
}
}
}
pub trait Platform: Send + Sync {
fn monitors(&self) -> Result<Vec<MonitorInfo>>;
fn focused_app(&self) -> Result<Option<AppIdentity>>;
fn capture_screen(&self, monitor: MonitorId) -> Result<Frame>;
fn capture_screen_native(&self, _monitor: MonitorId) -> Result<NativeFrame> {
Err(PlatformError::Unsupported {
what: "capture_screen_native is not implemented for this platform",
})
}
fn register_hotkey(&self, accelerator: Accelerator, label: &str) -> Result<HotkeyId>;
fn unregister_hotkey(&self, id: HotkeyId) -> Result<()>;
fn create_overlay(&self, monitor: MonitorId) -> Result<OverlayHandle>;
fn create_tray(&self, menu: TrayMenu) -> Result<TrayHandle>;
}
pub trait OverlayOps: Send {
fn show(&mut self);
fn hide(&mut self);
fn toggle(&mut self);
fn is_visible(&self) -> bool;
fn monitor(&self) -> MonitorId;
fn set_tint(&mut self, tint: Color);
fn set_input_capturing(&mut self, capturing: bool);
fn set_hud(&mut self, hud: Option<Hud>);
fn set_background_frame(&mut self, frame: Option<Frame>) {
let _ = frame;
}
fn set_system_pointer_visible(&mut self, visible: bool);
fn set_pointing_hand_cursor(&mut self, pointing: bool) {
let _ = pointing;
}
fn confine_pointer(&mut self, x: i32, y: i32, w: i32, h: i32);
fn release_pointer_confine(&mut self);
}
pub struct OverlayHandle {
inner: Box<dyn OverlayOps>,
}
impl OverlayHandle {
pub fn from_backend<B: OverlayOps + 'static>(b: B) -> Self {
Self { inner: Box::new(b) }
}
pub fn show(&mut self) {
self.inner.show()
}
pub fn hide(&mut self) {
self.inner.hide()
}
pub fn toggle(&mut self) {
self.inner.toggle()
}
pub fn is_visible(&self) -> bool {
self.inner.is_visible()
}
pub fn monitor(&self) -> MonitorId {
self.inner.monitor()
}
pub fn set_tint(&mut self, c: Color) {
self.inner.set_tint(c)
}
pub fn set_input_capturing(&mut self, capturing: bool) {
self.inner.set_input_capturing(capturing)
}
pub fn set_hud(&mut self, hud: Option<Hud>) {
self.inner.set_hud(hud)
}
pub fn set_background_frame(&mut self, frame: Option<Frame>) {
self.inner.set_background_frame(frame)
}
pub fn set_system_pointer_visible(&mut self, visible: bool) {
self.inner.set_system_pointer_visible(visible)
}
pub fn set_pointing_hand_cursor(&mut self, pointing: bool) {
self.inner.set_pointing_hand_cursor(pointing)
}
pub fn confine_pointer(&mut self, x: i32, y: i32, w: i32, h: i32) {
self.inner.confine_pointer(x, y, w, h)
}
pub fn release_pointer_confine(&mut self) {
self.inner.release_pointer_confine()
}
}
pub trait TrayOps: Send {
fn update_menu(&mut self, menu: TrayMenu) -> Result<()>;
fn set_active(&mut self, active: bool);
}
pub struct TrayHandle {
inner: Box<dyn TrayOps>,
}
impl TrayHandle {
pub fn from_backend<B: TrayOps + 'static>(b: B) -> Self {
Self { inner: Box::new(b) }
}
pub fn update_menu(&mut self, menu: TrayMenu) -> Result<()> {
self.inner.update_menu(menu)
}
pub fn set_active(&mut self, active: bool) {
self.inner.set_active(active)
}
}
pub fn init() -> Result<(Box<dyn Platform>, EventReceiver)> {
#[cfg(target_os = "linux")]
{
return linux::init();
}
#[cfg(target_os = "macos")]
{
return macos::init();
}
#[cfg(target_os = "windows")]
{
return windows_impl::init();
}
#[allow(unreachable_code)]
Err(PlatformError::Unsupported {
what: "this platform has no vernier backend",
})
}
#[cfg(all(test, target_os = "macos"))]
mod png_test {
use super::*;
#[test]
fn rasterize_macos_cached_icon() {
let path = "/Users/jon/Library/Caches/vernier/handoff-icons/CleanShot_X.png";
let Ok(bytes) = std::fs::read(path) else {
eprintln!("(skip — cache PNG not present)");
return;
};
let rgba = rasterize_png(&bytes, 128).expect("rasterize_png Some");
assert_eq!(rgba.len(), 128 * 128 * 4);
let nonzero = rgba.chunks_exact(4).filter(|p| p[3] > 0).count();
eprintln!("nonzero alpha pixels: {nonzero}");
assert!(nonzero > 1000);
}
}
#[cfg(all(test, target_os = "macos"))]
mod icon_extract_test {
use super::*;
#[test]
fn extract_each_installed_app() {
for app in &[
"/Applications/Setapp/CleanShot X.app",
"/Applications/Shottr.app",
"/System/Applications/Preview.app",
] {
let result = extract_macos_app_icon_rgba(std::path::Path::new(app), 128);
eprintln!("{app}: {}", match &result {
Some(b) => format!("Some({} bytes, {} non-transparent)",
b.len(),
b.chunks_exact(4).filter(|p| p[3] > 0).count()),
None => "None".into(),
});
}
}
}