Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::ffi::CStr;
|
||||
|
||||
use cocoa::base::{id, nil};
|
||||
use core_foundation::{
|
||||
base::TCFType,
|
||||
string::{CFString, CFStringRef},
|
||||
};
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use warp_core::channel::{Channel, ChannelState};
|
||||
|
||||
// Launch Services constants
|
||||
type LSRolesMask = u32;
|
||||
type OSStatus = i32;
|
||||
|
||||
// https://github.com/kornelski/core-services/blob/5572befea9fae3c31310d875240342229afa14ca/src/launch_services.rs#L33
|
||||
const K_LS_ROLES_SHELL: LSRolesMask = 0x00000008;
|
||||
|
||||
extern "C" {
|
||||
// Launch Services bindings
|
||||
fn LSCopyDefaultRoleHandlerForContentType(
|
||||
in_content_type: CFStringRef,
|
||||
in_role: LSRolesMask,
|
||||
) -> CFStringRef;
|
||||
|
||||
fn LSSetDefaultRoleHandlerForContentType(
|
||||
in_content_type: CFStringRef,
|
||||
in_role: LSRolesMask,
|
||||
in_handler_bundle_id: CFStringRef,
|
||||
) -> OSStatus;
|
||||
}
|
||||
|
||||
pub fn can_become_default_terminal() -> bool {
|
||||
unsafe {
|
||||
let bundle_class = class!(NSBundle);
|
||||
let main_bundle: id = msg_send![bundle_class, mainBundle];
|
||||
let bundle_id: id = msg_send![main_bundle, bundleIdentifier];
|
||||
bundle_id != nil && ChannelState::channel() != Channel::Local
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_warp_default_terminal() -> bool {
|
||||
unsafe {
|
||||
let unix_executable_content_type = CFString::new("public.unix-executable");
|
||||
let handler = LSCopyDefaultRoleHandlerForContentType(
|
||||
unix_executable_content_type.as_concrete_TypeRef(),
|
||||
K_LS_ROLES_SHELL,
|
||||
);
|
||||
|
||||
if handler.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(warp_bundle_id) = get_warp_bundle_id() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let handler_string = CFString::wrap_under_create_rule(handler);
|
||||
let current_handler = handler_string.to_string();
|
||||
|
||||
current_handler == warp_bundle_id
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_warp_as_default_terminal() -> Result<(), String> {
|
||||
log::debug!("Setting Warp as default terminal");
|
||||
|
||||
let bundle_id = get_warp_bundle_id().ok_or("No bundle ID".to_string())?;
|
||||
|
||||
set_default_terminal(&bundle_id)
|
||||
}
|
||||
|
||||
fn set_default_terminal(bundle_id: &str) -> Result<(), String> {
|
||||
log::debug!("Setting default terminal to bundle ID: {bundle_id}");
|
||||
|
||||
unsafe {
|
||||
let unix_executable_content_type = CFString::new("public.unix-executable");
|
||||
|
||||
let bundle_id_cf = CFString::new(bundle_id);
|
||||
|
||||
let result = LSSetDefaultRoleHandlerForContentType(
|
||||
unix_executable_content_type.as_concrete_TypeRef(),
|
||||
K_LS_ROLES_SHELL,
|
||||
bundle_id_cf.as_concrete_TypeRef(),
|
||||
);
|
||||
|
||||
match result {
|
||||
0 => Ok(()),
|
||||
_ => Err(format!(
|
||||
"LSSetDefaultRoleHandlerForContentType failed with stats: {result}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets Warp's bundle identifier. This may be `None` if not running as a bundle, i.e. through
|
||||
/// `cargo run` without `cargo bundle`.
|
||||
fn get_warp_bundle_id() -> Option<String> {
|
||||
unsafe {
|
||||
let bundle_class = class!(NSBundle);
|
||||
let main_bundle: id = msg_send![bundle_class, mainBundle];
|
||||
let bundle_id: id = msg_send![main_bundle, bundleIdentifier];
|
||||
|
||||
if bundle_id == nil {
|
||||
return None;
|
||||
}
|
||||
|
||||
let bundle_id_str: *const i8 = msg_send![bundle_id, UTF8String];
|
||||
let bundle_id_cstr = CStr::from_ptr(bundle_id_str);
|
||||
String::from_utf8(bundle_id_cstr.to_bytes().into())
|
||||
.inspect_err(|err| log::error!("Error converting bundle ID to string: {err:#}"))
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
use warpui::{
|
||||
windowing::{StateEvent, WindowManager},
|
||||
Entity, ModelContext, SingletonEntity,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use mac::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod non_mac {
|
||||
pub fn can_become_default_terminal() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_warp_default_terminal() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Sets Warp as the default terminal
|
||||
pub fn set_warp_as_default_terminal() -> Result<(), String> {
|
||||
Err("Not implemented".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use non_mac::*;
|
||||
|
||||
pub struct DefaultTerminal {
|
||||
/// Whether the OS will treat Warp as the default app for scripts/executables.
|
||||
is_warp_default: bool,
|
||||
}
|
||||
|
||||
impl DefaultTerminal {
|
||||
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
|
||||
ctx.subscribe_to_model(
|
||||
&WindowManager::handle(ctx),
|
||||
Self::handle_window_manager_event,
|
||||
);
|
||||
|
||||
// This can be slow to compute due to calling into platform APIs, so in unit
|
||||
// tests, where we shouldn't care, just pretend that we are not.
|
||||
let is_warp_default = if cfg!(test) {
|
||||
false
|
||||
} else {
|
||||
is_warp_default_terminal()
|
||||
};
|
||||
|
||||
Self { is_warp_default }
|
||||
}
|
||||
|
||||
/// This is an OS-level setting. Unlike most other settings, where Warp is the source-of-truth
|
||||
/// for the value of the setting, it can be changed outside of Warp. We monitor if it gets
|
||||
/// changed externally by checking when Warp is focused.
|
||||
fn handle_window_manager_event(&mut self, event: &StateEvent, ctx: &mut ModelContext<Self>) {
|
||||
match event {
|
||||
StateEvent::ValueChanged { current, previous } => {
|
||||
if current.active_window.is_some() && previous.active_window.is_none() {
|
||||
let is_warp_default_now = is_warp_default_terminal();
|
||||
if is_warp_default_now != self.is_warp_default {
|
||||
self.set_is_warp_default(is_warp_default_now, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_is_warp_default(&mut self, value: bool, ctx: &mut ModelContext<Self>) {
|
||||
self.is_warp_default = value;
|
||||
ctx.emit(DefaultTerminalEvent::ValueChanged);
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub fn can_warp_become_default() -> bool {
|
||||
if cfg!(test) {
|
||||
// Determining whether or not we can become the default terminal requires
|
||||
// calling into platform APIs, which can be slow, and we can't actually
|
||||
// set the default terminal in unit tests, so just say we can't.
|
||||
false
|
||||
} else {
|
||||
can_become_default_terminal()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_warp_default(&self) -> bool {
|
||||
self.is_warp_default
|
||||
}
|
||||
|
||||
/// This is a one-way operation. Once we set the default terminal to Warp, we can't really
|
||||
/// "unset" it unless we pick a new default terminal. Picking a new default is complicated.
|
||||
pub fn make_warp_default(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
if let Err(e) = set_warp_as_default_terminal() {
|
||||
log::error!("Error setting Warp as default terminal: {e:#}");
|
||||
} else {
|
||||
self.set_is_warp_default(true, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DefaultTerminalEvent {
|
||||
ValueChanged,
|
||||
}
|
||||
|
||||
impl Entity for DefaultTerminal {
|
||||
type Event = DefaultTerminalEvent;
|
||||
}
|
||||
|
||||
impl SingletonEntity for DefaultTerminal {}
|
||||
Reference in New Issue
Block a user