Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
use std::collections::HashMap;
use objc2_core_graphics::{
CGEvent, CGEventFlags, CGEventSource, CGEventSourceStateID, CGEventTapLocation, CGKeyCode,
};
use super::keycode_cache;
use crate::Key;
/// Manages keyboard state and posts keyboard events to the system.
pub struct Keyboard {
/// Cache of character-to-keycode mappings for the current keyboard layout.
cache: HashMap<char, CGKeyCode>,
}
impl Keyboard {
pub fn new() -> Self {
Self {
cache: keycode_cache::build_cache(),
}
}
/// Sends a key down event for the given key.
pub fn key_down(&self, key: &Key) -> Result<(), String> {
post_key_down(self.resolve_keycode(key)?)
}
/// Sends a key up event for the given key.
pub fn key_up(&self, key: &Key) -> Result<(), String> {
post_key_up(self.resolve_keycode(key)?)
}
/// Simulates typing text by sending Quartz events.
pub fn type_text(&self, text: &str) -> Result<(), String> {
let source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState);
// Send one character at a time for better compatibility with various applications.
for ch in text.chars() {
// For now, send each character using the unicode method. This is easier than using
// virtual key codes, but may not be supported in all applications.
//
// TODO(vorporeal): when sending an ASCII character, send it using virtual key codes
// for better compatibility.
type_unicode_char(ch, source.as_deref())?;
}
Ok(())
}
/// Resolves a Key to a CGKeyCode.
///
/// The key can be:
/// - A keycode (platform-specific virtual keycode)
/// - A character (looked up via the current keyboard layout)
fn resolve_keycode(&self, key: &Key) -> Result<CGKeyCode, String> {
match key {
Key::Keycode(code) => CGKeyCode::try_from(*code).map_err(|_| {
format!(
"Invalid keycode {code}: must be in range 0..={}",
CGKeyCode::MAX
)
}),
Key::Char(ch) => self
.cache
.get(ch)
.copied()
.ok_or_else(|| format!("No keycode found for character '{}'", ch)),
}
}
}
/// Posts a key down event for the given virtual keycode.
fn post_key_down(keycode: CGKeyCode) -> Result<(), String> {
let source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState);
let event = CGEvent::new_keyboard_event(source.as_deref(), keycode, true)
.ok_or_else(|| format!("Failed to create key down event for keycode {}", keycode))?;
CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&event));
Ok(())
}
/// Posts a key up event for the given virtual keycode.
fn post_key_up(keycode: CGKeyCode) -> Result<(), String> {
let source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState);
let event = CGEvent::new_keyboard_event(source.as_deref(), keycode, false)
.ok_or_else(|| format!("Failed to create key up event for keycode {}", keycode))?;
CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&event));
Ok(())
}
/// Generates a Quartz event signifying the typing of a single Unicode character.
fn type_unicode_char(ch: char, source: Option<&CGEventSource>) -> Result<(), String> {
let mut buf = [0u16; 2];
let encoded = ch.encode_utf16(&mut buf);
// Create a key down event (virtual key code 0 is used as a placeholder).
let key_down = CGEvent::new_keyboard_event(source, 0, true)
.ok_or("Failed to create key down event for TypeText.")?;
// Set the unicode string on the event.
// Safety: encoded is a valid UTF-16 buffer with the correct length.
unsafe {
CGEvent::keyboard_set_unicode_string(
Some(&key_down),
encoded.len() as u64,
encoded.as_ptr(),
);
}
// Clear any modifier flags that might interfere.
CGEvent::set_flags(Some(&key_down), CGEventFlags::empty());
// Post the key down event.
CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&key_down));
// Create and post a corresponding key up event.
let key_up = CGEvent::new_keyboard_event(source, 0, false)
.ok_or("Failed to create key up event for TypeText.")?;
CGEvent::set_flags(Some(&key_up), CGEventFlags::empty());
CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&key_up));
Ok(())
}
@@ -0,0 +1,179 @@
//! Character-to-keycode cache builder.
//!
//! This module provides a way to translate characters to macOS virtual keycodes.
//! The translation depends on the current keyboard layout.
//!
//! The Carbon APIs used for translation (`TISCopyCurrentKeyboardInputSource`,
//! `UCKeyTranslate`) are not thread-safe, so we build the cache on the main thread
//! using GCD dispatch.
use std::collections::HashMap;
use core_foundation::base::{CFType, CFTypeRef, TCFType};
use core_foundation::data::CFData;
use dispatch2::run_on_main;
use objc2_core_graphics::CGKeyCode;
// Carbon Text Input Services types and functions.
#[allow(non_camel_case_types)]
type TISInputSourceRef = CFTypeRef;
#[link(name = "Carbon", kind = "framework")]
unsafe extern "C" {
fn TISCopyCurrentKeyboardInputSource() -> TISInputSourceRef;
fn TISCopyCurrentKeyboardLayoutInputSource() -> TISInputSourceRef;
fn TISGetInputSourceProperty(source: TISInputSourceRef, key: CFTypeRef) -> CFTypeRef;
// Property key for getting the keyboard layout data.
static kTISPropertyUnicodeKeyLayoutData: CFTypeRef;
fn LMGetKbdType() -> u8;
}
// Unicode Utilities types and functions.
#[repr(C)]
#[allow(non_camel_case_types)]
struct UCKeyboardLayout {
_opaque: [u8; 0],
}
#[link(name = "Carbon", kind = "framework")]
unsafe extern "C" {
fn UCKeyTranslate(
layout: *const UCKeyboardLayout,
virtual_key_code: u16,
key_action: u16,
modifier_key_state: u32,
keyboard_type: u32,
key_translate_options: u32,
dead_key_state: *mut u32,
max_string_length: usize,
actual_string_length: *mut usize,
unicode_string: *mut u16,
) -> i32;
}
// UCKeyTranslate constants.
const K_UC_KEY_ACTION_DOWN: u16 = 0;
const K_UC_KEY_TRANSLATE_NO_DEAD_KEYS_BIT: u32 = 1 << 0;
// Shift modifier for UCKeyTranslate (shift bit position is 1 in the modifier state).
const SHIFT_MODIFIER: u32 = 1 << 1;
/// Builds a character-to-keycode cache for the current keyboard layout.
///
/// This function dispatches to the main thread to call Carbon APIs safely.
/// TODO(QUALITY-271): Store the modifier keys as well.
pub fn build_cache() -> HashMap<char, CGKeyCode> {
run_on_main(|_| build_cache_on_main_thread())
}
/// Builds the cache on the main thread where Carbon APIs are safe to call.
fn build_cache_on_main_thread() -> HashMap<char, CGKeyCode> {
let mut cache = HashMap::new();
// Get the keyboard layout data.
let layout_data = unsafe { get_keyboard_layout_data() };
let Some(layout_data) = layout_data else {
log::warn!("Failed to get keyboard layout data for keycode cache");
return cache;
};
let layout_ptr = layout_data.as_ptr() as *const UCKeyboardLayout;
let keyboard_type = unsafe { LMGetKbdType() } as u32;
// Iterate through all possible keycodes (0-127) and build the mapping.
for keycode in 0u16..128 {
// Get the character for this keycode without modifiers.
if let Some(ch) = translate_keycode(layout_ptr, keycode, 0, keyboard_type)
&& !is_control_char(ch)
{
cache.entry(ch).or_insert(keycode as CGKeyCode);
}
// Get the character for this keycode with shift held.
if let Some(ch) = translate_keycode(layout_ptr, keycode, SHIFT_MODIFIER, keyboard_type)
&& !is_control_char(ch)
{
cache.entry(ch).or_insert(keycode as CGKeyCode);
}
}
cache
}
/// Gets the keyboard layout data from the current input source.
unsafe fn get_keyboard_layout_data() -> Option<CFData> {
// TISCopy* functions follow CF "Copy" semantics - caller owns the reference.
// Wrap in CFType so they're released when dropped.
let source = unsafe { CFType::wrap_under_create_rule(TISCopyCurrentKeyboardInputSource()) };
let mut layout_data = unsafe {
TISGetInputSourceProperty(source.as_CFTypeRef(), kTISPropertyUnicodeKeyLayoutData)
};
// Some keyboard layouts (e.g., Japanese, Chinese) don't have layout data on the
// regular input source. Try the keyboard layout input source instead.
let _layout_source;
if layout_data.is_null() {
// Keep this alive until we're done with layout_data.
_layout_source =
unsafe { CFType::wrap_under_create_rule(TISCopyCurrentKeyboardLayoutInputSource()) };
layout_data = unsafe {
TISGetInputSourceProperty(
_layout_source.as_CFTypeRef(),
kTISPropertyUnicodeKeyLayoutData,
)
};
}
if layout_data.is_null() {
return None;
}
// The returned CFData is not retained, so we need to retain it.
Some(unsafe { CFData::wrap_under_get_rule(layout_data as _) })
}
/// Translates a keycode to a character using UCKeyTranslate.
fn translate_keycode(
layout: *const UCKeyboardLayout,
keycode: u16,
modifier_state: u32,
keyboard_type: u32,
) -> Option<char> {
let mut dead_key_state: u32 = 0;
let mut string_length: usize = 0;
let mut unicode_string = [0u16; 4];
let result = unsafe {
UCKeyTranslate(
layout,
keycode,
K_UC_KEY_ACTION_DOWN,
modifier_state,
keyboard_type,
K_UC_KEY_TRANSLATE_NO_DEAD_KEYS_BIT,
&mut dead_key_state,
unicode_string.len(),
&mut string_length,
unicode_string.as_mut_ptr(),
)
};
if result != 0 || string_length == 0 {
return None;
}
// Convert the first UTF-16 code unit to a char.
// We only handle single-code-unit characters for simplicity.
char::decode_utf16(unicode_string[..string_length].iter().copied())
.next()
.and_then(|r| r.ok())
}
/// Returns true if the character is a control character (non-printable).
fn is_control_char(ch: char) -> bool {
// C0 control characters (0x00-0x1F) and C1 control characters (0x7F-0x9F)
let code = ch as u32;
code <= 0x1F || (0x7F..=0x9F).contains(&code)
}
+83
View File
@@ -0,0 +1,83 @@
mod keyboard;
mod keycode_cache;
mod mouse;
mod screenshot;
mod util;
use async_trait::async_trait;
use warpui::r#async::Timer;
use crate::{Action, ActionResult, Options};
pub fn is_supported_on_current_platform() -> bool {
true
}
pub struct Actor {
keyboard: keyboard::Keyboard,
mouse: mouse::Mouse,
}
impl Actor {
pub fn new() -> Self {
Self {
keyboard: keyboard::Keyboard::new(),
mouse: mouse::Mouse::new(),
}
}
}
#[async_trait]
impl super::Actor for Actor {
fn platform(&self) -> Option<super::Platform> {
Some(super::Platform::Mac)
}
async fn perform_actions(
&mut self,
actions: &[Action],
options: Options,
) -> Result<ActionResult, String> {
for action in actions {
match action {
Action::Wait(duration) => {
Timer::after(*duration).await;
}
Action::MouseDown { button, at } => {
self.mouse.move_to(*at).await?;
self.mouse.button_down(button)?;
}
Action::MouseUp { button } => self.mouse.button_up(button)?,
Action::MouseMove { to } => self.mouse.move_to(*to).await?,
Action::MouseWheel {
at,
direction,
distance,
} => {
self.mouse.move_to(*at).await?;
self.mouse.scroll(direction, distance)?;
}
Action::TypeText { text } => {
self.keyboard.type_text(text)?;
}
Action::KeyDown { key } => {
self.keyboard.key_down(key)?;
}
Action::KeyUp { key } => {
self.keyboard.key_up(key)?;
}
}
}
let screenshot = if let Some(params) = options.screenshot_params {
Some(screenshot::take(params)?)
} else {
None
};
Ok(ActionResult {
screenshot,
cursor_position: Some(self.mouse.current_position()?),
})
}
}
+271
View File
@@ -0,0 +1,271 @@
use instant::Instant;
use std::time::Duration;
use objc2_core_foundation::CGPoint;
use objc2_core_graphics::{
CGEvent, CGEventSource, CGEventSourceStateID, CGEventTapLocation, CGEventType, CGMouseButton,
CGScrollEventUnit,
};
use pathfinder_geometry::vector::Vector2I;
use warpui::r#async::Timer;
use crate::{MouseButton, ScrollDirection, ScrollDistance};
use super::util::main_display_scale_factor;
const POSITION_POLL_INTERVAL: Duration = Duration::from_micros(500);
const POSITION_TIMEOUT: Duration = Duration::from_millis(100);
/// Converts physical coordinates to CGEvent point coordinates.
///
/// On Retina/HiDPI displays, physical coordinates differ from the "point" coordinates
/// used by macOS APIs like CGEvent. This function scales physical coordinates down
/// by the display's backing scale factor.
pub fn to_cgpoint(target: Vector2I) -> CGPoint {
let scale = main_display_scale_factor();
CGPoint {
x: target.x() as f64 / scale,
y: target.y() as f64 / scale,
}
}
/// Converts CGEvent point coordinates to physical coordinates.
pub fn from_cgpoint(point: CGPoint) -> Vector2I {
let scale = main_display_scale_factor();
Vector2I::new((point.x * scale) as i32, (point.y * scale) as i32)
}
/// Manages mouse state and posts mouse events to the system.
pub struct Mouse {
held_buttons: HeldButtons,
}
impl Mouse {
pub fn new() -> Self {
Self {
held_buttons: HeldButtons::default(),
}
}
pub async fn move_to(&mut self, target: Vector2I) -> Result<(), String> {
let (event_type, cg_button) = if let Some(held) = self.held_buttons.primary_down() {
(mouse_dragged_event_type(&held), (&held).into())
} else {
(CGEventType::MouseMoved, CGMouseButton::Left)
};
self.post_event(event_type, to_cgpoint(target), cg_button)?;
self.wait_for_position(target).await
}
pub fn button_down(&mut self, button: &MouseButton) -> Result<(), String> {
let point = self.current_position_cgpoint()?;
self.held_buttons.set_down(button, true);
self.post_event(mouse_down_event_type(button), point, button.into())
}
pub fn button_up(&mut self, button: &MouseButton) -> Result<(), String> {
let point = self.current_position_cgpoint()?;
self.held_buttons.set_down(button, false);
self.post_event(mouse_up_event_type(button), point, button.into())
}
pub fn current_position(&mut self) -> Result<Vector2I, String> {
let cg_point = self.current_position_cgpoint()?;
Ok(from_cgpoint(cg_point))
}
/// Scrolls the mouse wheel in the given direction by the given distance.
pub fn scroll(
&mut self,
direction: &ScrollDirection,
distance: &ScrollDistance,
) -> Result<(), String> {
let source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState);
// Determine scroll unit and amount based on distance type.
let (unit, amount) = match distance {
ScrollDistance::Pixels(pixels) => (CGScrollEventUnit::Pixel, *pixels),
ScrollDistance::Clicks(clicks) => (CGScrollEventUnit::Line, *clicks),
};
// Determine which axis and sign to use based on direction.
// Positive values scroll up/left, negative values scroll down/right.
let (wheel1, wheel2) = match direction {
ScrollDirection::Up => (amount, 0),
ScrollDirection::Down => (-amount, 0),
ScrollDirection::Left => (0, amount),
ScrollDirection::Right => (0, -amount),
};
// The function signature is:
// new_scroll_wheel_event2(source, units, wheel_count, wheel1, wheel2, wheel3)
// wheel_count indicates how many wheel values are valid (1, 2, or 3).
let wheel_count = if wheel2 != 0 { 2 } else { 1 };
let event = CGEvent::new_scroll_wheel_event2(
source.as_deref(),
unit,
wheel_count,
wheel1,
wheel2,
0,
)
.ok_or_else(|| {
format!(
"Failed to create scroll wheel event (direction={:?}, distance={:?}). \
The cause is unknown.",
direction, distance
)
})?;
CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&event));
Ok(())
}
}
// Private implementation details.
impl Mouse {
/// Waits for the mouse to reach the target position, polling until it arrives
/// or times out.
async fn wait_for_position(&mut self, target: Vector2I) -> Result<(), String> {
let start = Instant::now();
loop {
let current = self.current_position()?;
if current == target {
return Ok(());
}
if start.elapsed() >= POSITION_TIMEOUT {
log::warn!(
"Mouse position wait timed out. Target: ({}, {}), Current: ({}, {})",
target.x(),
target.y(),
current.x(),
current.y()
);
return Err(format!(
"Timed out waiting for mouse to move to ({}, {}). Current position: ({}, {})",
target.x(),
target.y(),
current.x(),
current.y()
));
}
Timer::after(POSITION_POLL_INTERVAL).await;
}
}
fn current_position_cgpoint(&mut self) -> Result<CGPoint, String> {
let event = CGEvent::new(None)
.ok_or("Failed to query current cursor position. The cause is unknown.")?;
let pos = CGEvent::location(Some(&event));
Ok(pos)
}
fn post_event(
&mut self,
event_type: CGEventType,
point: CGPoint,
button: CGMouseButton,
) -> Result<(), String> {
let source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState);
let event = CGEvent::new_mouse_event(source.as_deref(), event_type, point, button)
.ok_or_else(|| {
format!(
"Failed to create mouse event (type={:?}, position=({}, {}), button={:?}). \
The cause is unknown.",
event_type, point.x, point.y, button
)
})?;
CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&event));
Ok(())
}
}
// ----------------------------------------------------------------------------
// Button state tracking
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Default)]
struct HeldButtons {
left: bool,
right: bool,
middle: bool,
back: bool,
forward: bool,
}
impl HeldButtons {
/// Returns the "primary" held button (preferring left > right > middle).
fn primary_down(self) -> Option<MouseButton> {
if self.left {
Some(MouseButton::Left)
} else if self.right {
Some(MouseButton::Right)
} else if self.middle {
Some(MouseButton::Middle)
} else if self.back {
Some(MouseButton::Back)
} else if self.forward {
Some(MouseButton::Forward)
} else {
None
}
}
fn set_down(&mut self, button: &MouseButton, down: bool) {
match button {
MouseButton::Left => self.left = down,
MouseButton::Right => self.right = down,
MouseButton::Middle => self.middle = down,
MouseButton::Back => self.back = down,
MouseButton::Forward => self.forward = down,
}
}
}
// ----------------------------------------------------------------------------
// Event type helpers
// ----------------------------------------------------------------------------
impl From<&MouseButton> for CGMouseButton {
fn from(button: &MouseButton) -> Self {
match button {
MouseButton::Left => CGMouseButton::Left,
MouseButton::Right => CGMouseButton::Right,
MouseButton::Middle => CGMouseButton::Center,
MouseButton::Back => CGMouseButton(3),
MouseButton::Forward => CGMouseButton(4),
}
}
}
fn mouse_down_event_type(button: &MouseButton) -> CGEventType {
match button {
MouseButton::Left => CGEventType::LeftMouseDown,
MouseButton::Right => CGEventType::RightMouseDown,
MouseButton::Middle | MouseButton::Back | MouseButton::Forward => {
CGEventType::OtherMouseDown
}
}
}
fn mouse_up_event_type(button: &MouseButton) -> CGEventType {
match button {
MouseButton::Left => CGEventType::LeftMouseUp,
MouseButton::Right => CGEventType::RightMouseUp,
MouseButton::Middle | MouseButton::Back | MouseButton::Forward => CGEventType::OtherMouseUp,
}
}
fn mouse_dragged_event_type(button: &MouseButton) -> CGEventType {
match button {
MouseButton::Left => CGEventType::LeftMouseDragged,
MouseButton::Right => CGEventType::RightMouseDragged,
MouseButton::Middle | MouseButton::Back | MouseButton::Forward => {
CGEventType::OtherMouseDragged
}
}
}
+48
View File
@@ -0,0 +1,48 @@
use command::blocking::Command;
use super::util::main_display_scale_factor;
use crate::ScreenshotParams;
/// Captures a screenshot of the main display using the built-in macOS
/// `screencapture` CLI.
pub fn take(params: ScreenshotParams) -> Result<crate::Screenshot, String> {
let output_dir = tempfile::tempdir()
.map_err(|e| format!("Failed to create temporary directory for screenshot: {e}"))?;
let output_path = output_dir.path().join("screenshot.png");
let mut cmd = Command::new("/usr/sbin/screencapture");
cmd.args([
"-x", // Do not play sounds.
"-tpng", // Capture to PNG format.
"-m", // Only capture the main display (not all displays).
]);
if let Some(region) = params.region {
region.validate()?;
// -R x,y,w,h captures a specific rectangle in point coordinates.
// Convert from physical pixel coordinates to point coordinates.
let scale = main_display_scale_factor();
let x = (region.top_left.x() as f64 / scale) as i32;
let y = (region.top_left.y() as f64 / scale) as i32;
let w = ((region.bottom_right.x() - region.top_left.x()) as f64 / scale) as i32;
let h = ((region.bottom_right.y() - region.top_left.y()) as f64 / scale) as i32;
cmd.arg("-R").arg(format!("{x},{y},{w},{h}"));
}
let output = cmd
.arg(&output_path)
.output()
.map_err(|e| format!("Failed to run screencapture: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let detail = if stderr.trim().is_empty() {
format!("exit code {}", output.status)
} else {
format!("exit code {}: {}", output.status, stderr.trim())
};
return Err(format!("screencapture failed with {detail}"));
}
crate::screenshot_utils::load_and_process_screenshot(&output_path, params)
}
+14
View File
@@ -0,0 +1,14 @@
/// Returns the backing scale factor of the main display.
///
/// This is used to convert between pixel coordinates (as returned by screenshot tools)
/// and point coordinates (as used by CGEvent and screencapture).
pub fn main_display_scale_factor() -> f64 {
use dispatch2::run_on_main;
use objc2_app_kit::NSScreen;
run_on_main(|mtm| {
NSScreen::mainScreen(mtm)
.map(|screen| screen.backingScaleFactor())
.unwrap_or(1.0)
})
}