aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-22 22:10:23 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-22 22:10:23 +0300
commita8be6117f6975341a48b149f71d93c11e9efc8d8 (patch)
tree2c46f741555ed844172336ed6883c0c4831f1010
parentf23e3779dc6827739fed34437519f548fcb9d76b (diff)
refactor: simplify macos input source ownership
replaces contributions from sf-jin-ku
-rw-r--r--src/platform/macos.rs167
1 files changed, 50 insertions, 117 deletions
diff --git a/src/platform/macos.rs b/src/platform/macos.rs
index 161354d7..e9c89ede 100644
--- a/src/platform/macos.rs
+++ b/src/platform/macos.rs
@@ -14,7 +14,6 @@ use super::{
const PROC_PGRP_ONLY: u32 = 2;
const SERVER_NOFILE_LIMIT_TARGET: libc::rlim_t = 8192;
-const CF_STRING_ENCODING_UTF8: u32 = 0x0800_0100;
pub(crate) fn should_draw_host_cursor_by_default() -> bool {
false
@@ -75,7 +74,6 @@ type CfTypeRef = *const libc::c_void;
type CfStringRef = *const libc::c_void;
type OsStatus = libc::c_int;
type Boolean = libc::c_uchar;
-type CfIndex = isize;
#[link(name = "Carbon", kind = "framework")]
extern "C" {
@@ -106,23 +104,6 @@ extern "C" {
#[link_name = "CFEqual"]
fn cf_equal(left: CfTypeRef, right: CfTypeRef) -> Boolean;
- #[link_name = "CFStringGetCStringPtr"]
- fn cf_string_get_cstring_ptr(value: CfStringRef, encoding: u32) -> *const libc::c_char;
-
- #[link_name = "CFStringGetLength"]
- fn cf_string_get_length(value: CfStringRef) -> CfIndex;
-
- #[link_name = "CFStringGetMaximumSizeForEncoding"]
- fn cf_string_get_maximum_size_for_encoding(length: CfIndex, encoding: u32) -> CfIndex;
-
- #[link_name = "CFStringGetCString"]
- fn cf_string_get_cstring(
- value: CfStringRef,
- buffer: *mut libc::c_char,
- buffer_size: CfIndex,
- encoding: u32,
- ) -> Boolean;
-
#[link_name = "kCFRunLoopDefaultMode"]
static CF_RUN_LOOP_DEFAULT_MODE: CfStringRef;
@@ -153,124 +134,76 @@ pub(crate) fn pump_input_source_runloop() {
}
#[derive(Debug)]
-pub(crate) struct InputSourceRestore {
- previous: NonNull<TisInputSource>,
-}
+struct RetainedInputSource(NonNull<TisInputSource>);
-impl Drop for InputSourceRestore {
- fn drop(&mut self) {
- // SAFETY: `previous` is a retained TIS input source created by
- // `TISCopyCurrentKeyboardInputSource`; selecting it and releasing that
- // retain follows the Carbon Input Source Services ownership contract.
- unsafe {
- let previous = self.previous.as_ptr();
- let status = tis_select_input_source(previous);
- cf_release(previous.cast());
- if status != 0 {
- tracing::debug!(
- status,
- "failed to restore host input source after prefix mode"
- );
- }
- }
+impl RetainedInputSource {
+ /// Takes ownership of a retained reference returned by a TIS `Copy` function.
+ unsafe fn from_copy(raw: TisInputSourceRef) -> Option<Self> {
+ NonNull::new(raw as *mut TisInputSource).map(Self)
}
-}
-pub(crate) fn switch_to_ascii_input_source() -> Option<InputSourceRestore> {
- // SAFETY: TISCopy* functions return retained references or null. Each
- // retained reference is either transferred into `InputSourceRestore` or
- // released before returning; TISSelectInputSource accepts live TIS refs.
- unsafe {
- let current =
- NonNull::new(tis_copy_current_keyboard_input_source() as *mut TisInputSource)?;
- let Some(ascii) = NonNull::new(
- tis_copy_current_ascii_capable_keyboard_layout_input_source() as *mut TisInputSource,
- ) else {
- cf_release(current.as_ptr().cast());
- return None;
- };
-
- if input_source_ids_equal(current.as_ptr(), ascii.as_ptr()) {
- cf_release(current.as_ptr().cast());
- cf_release(ascii.as_ptr().cast());
- return None;
- }
-
- let debug_ids = tracing::enabled!(tracing::Level::DEBUG).then(|| {
- (
- input_source_id(current.as_ptr()),
- input_source_id(ascii.as_ptr()),
- )
- });
-
- let status = tis_select_input_source(ascii.as_ptr());
- cf_release(ascii.as_ptr().cast());
- if status != 0 {
- cf_release(current.as_ptr().cast());
- tracing::debug!(status, "failed to switch host input source for prefix mode");
- return None;
- }
+ fn select(&self) -> OsStatus {
+ // SAFETY: this wrapper keeps the retained input source alive for the call.
+ unsafe { tis_select_input_source(self.0.as_ptr()) }
+ }
- if let Some((Some(from), Some(to))) = debug_ids {
- tracing::debug!(from, to, "switched host input source for prefix mode");
+ fn has_same_id(&self, other: &Self) -> bool {
+ // SAFETY: TIS property values stay valid while their input sources are alive;
+ // both wrappers outlive this comparison.
+ unsafe {
+ let left = tis_get_input_source_property(self.0.as_ptr(), TIS_PROPERTY_INPUT_SOURCE_ID);
+ let right =
+ tis_get_input_source_property(other.0.as_ptr(), TIS_PROPERTY_INPUT_SOURCE_ID);
+ !left.is_null() && !right.is_null() && cf_equal(left, right) != 0
}
-
- Some(InputSourceRestore { previous: current })
}
}
-unsafe fn input_source_ids_equal(left: TisInputSourceRef, right: TisInputSourceRef) -> bool {
- let left_property = tis_get_input_source_property(left, TIS_PROPERTY_INPUT_SOURCE_ID);
- let right_property = tis_get_input_source_property(right, TIS_PROPERTY_INPUT_SOURCE_ID);
- !left_property.is_null()
- && !right_property.is_null()
- && cf_equal(left_property, right_property) != 0
+impl Drop for RetainedInputSource {
+ fn drop(&mut self) {
+ // SAFETY: `from_copy` gives this wrapper ownership of one retain.
+ unsafe { cf_release(self.0.as_ptr().cast()) }
+ }
}
-unsafe fn input_source_id(input_source: TisInputSourceRef) -> Option<String> {
- let property =
- tis_get_input_source_property(input_source, TIS_PROPERTY_INPUT_SOURCE_ID) as CfStringRef;
- cf_string_to_string(property)
+#[derive(Debug)]
+pub(crate) struct InputSourceRestore {
+ previous: RetainedInputSource,
}
-unsafe fn cf_string_to_string(value: CfStringRef) -> Option<String> {
- if value.is_null() {
- return None;
+impl Drop for InputSourceRestore {
+ fn drop(&mut self) {
+ let status = self.previous.select();
+ if status != 0 {
+ tracing::debug!(
+ status,
+ "failed to restore host input source after prefix mode"
+ );
+ }
}
+}
- let direct = cf_string_get_cstring_ptr(value, CF_STRING_ENCODING_UTF8);
- if !direct.is_null() {
- return std::ffi::CStr::from_ptr(direct)
- .to_str()
- .ok()
- .map(str::to_owned);
- }
+pub(crate) fn switch_to_ascii_input_source() -> Option<InputSourceRestore> {
+ // SAFETY: both Carbon `Copy` functions transfer one retain to the caller.
+ let current =
+ unsafe { RetainedInputSource::from_copy(tis_copy_current_keyboard_input_source())? };
+ let ascii = unsafe {
+ RetainedInputSource::from_copy(
+ tis_copy_current_ascii_capable_keyboard_layout_input_source(),
+ )?
+ };
- let length = cf_string_get_length(value);
- if length < 0 {
- return None;
- }
- let max_bytes = cf_string_get_maximum_size_for_encoding(length, CF_STRING_ENCODING_UTF8);
- if max_bytes < 0 {
+ if current.has_same_id(&ascii) {
return None;
}
- let buffer_len = usize::try_from(max_bytes).ok()?.checked_add(1)?;
- let mut buffer = vec![0 as libc::c_char; buffer_len];
- let buffer_size = CfIndex::try_from(buffer.len()).ok()?;
- if cf_string_get_cstring(
- value,
- buffer.as_mut_ptr(),
- buffer_size,
- CF_STRING_ENCODING_UTF8,
- ) == 0
- {
+
+ let status = ascii.select();
+ if status != 0 {
+ tracing::debug!(status, "failed to switch host input source for prefix mode");
return None;
}
- std::ffi::CStr::from_ptr(buffer.as_ptr())
- .to_str()
- .ok()
- .map(str::to_owned)
+ Some(InputSourceRestore { previous: current })
}
pub fn raise_server_nofile_limit() {