diff --git a/app/src/pane_group/focus_state.rs b/app/src/pane_group/focus_state.rs index 78ccefeb5f..e2d5a39c68 100644 --- a/app/src/pane_group/focus_state.rs +++ b/app/src/pane_group/focus_state.rs @@ -1,3 +1,7 @@ +use std::collections::HashMap; + +#[cfg(target_os = "macos")] +use warpui::platform::mac::Window; use warpui::{AppContext, Entity, ModelContext, ModelHandle}; use super::pane::{PaneId, TerminalPaneId}; @@ -12,6 +16,9 @@ pub struct PaneGroupFocusState { active_session_id: Option, in_split_pane: bool, is_focused_pane_maximized: bool, + /// Per-pane keyboard input source IDs (macOS only). + /// Maps pane ID to the last known input source ID (e.g., "com.apple.keylayout.ABC"). + pane_input_sources: HashMap, } #[derive(Debug, Clone)] @@ -38,11 +45,21 @@ impl PaneGroupFocusState { active_session_id: Option, in_split_pane: bool, ) -> Self { + // Initialize the map and store the current input source for the initial pane. + let mut pane_input_sources = HashMap::new(); + #[cfg(target_os = "macos")] + { + if let Some(source_id) = Window::get_current_input_source_id() { + pane_input_sources.insert(focused_pane_id, source_id); + } + } + Self { focused_pane_id, active_session_id, in_split_pane, is_focused_pane_maximized: false, + pane_input_sources, } } @@ -93,6 +110,11 @@ impl PaneGroupFocusState { pub(super) fn set_focused_pane(&mut self, pane_id: PaneId, ctx: &mut ModelContext) { let old_focused = self.focused_pane_id; if old_focused != pane_id { + // Save the current input source for the departing pane, + // then restore the input source for the incoming pane. + self.save_input_source_for_pane(old_focused); + self.restore_input_source_for_pane(pane_id); + self.focused_pane_id = pane_id; // When focus changes, clear maximize state self.is_focused_pane_maximized = false; @@ -103,6 +125,38 @@ impl PaneGroupFocusState { } } + /// Saves the current system input source ID for the given pane. + /// On macOS, this is a no-op when called from a background thread + /// (Carbon TIS APIs require the main thread). + #[cfg(target_os = "macos")] + fn save_input_source_for_pane(&mut self, pane_id: PaneId) { + if let Some(source_id) = Window::get_current_input_source_id() { + self.pane_input_sources.insert(pane_id, source_id); + } + } + + #[cfg(not(target_os = "macos"))] + fn save_input_source_for_pane(&mut self, _pane_id: PaneId) { + // No-op on non-macOS platforms + } + + /// Restores the previously saved input source for the given pane. + /// If the pane has no saved input source (e.g., it's newly created), + /// inherits the current system input source. + /// On macOS, this is a no-op when called from a background thread + /// (Carbon TIS APIs require the main thread). + #[cfg(target_os = "macos")] + fn restore_input_source_for_pane(&mut self, pane_id: PaneId) { + if let Some(source_id) = self.pane_input_sources.get(&pane_id) { + Window::select_input_source(source_id); + } + } + + #[cfg(not(target_os = "macos"))] + fn restore_input_source_for_pane(&mut self, _pane_id: PaneId) { + // No-op on non-macOS platforms + } + /// Sets the active terminal session and emits an ActiveSessionChanged event. pub(super) fn set_active_session( &mut self, diff --git a/crates/warpui/src/platform/mac/objc/keycode.m b/crates/warpui/src/platform/mac/objc/keycode.m index 32b2dc5958..44c7c888b2 100644 --- a/crates/warpui/src/platform/mac/objc/keycode.m +++ b/crates/warpui/src/platform/mac/objc/keycode.m @@ -158,6 +158,45 @@ UniChar TranslatedUnicodeCharFromKeyCode(CFDataRef layout_data, UInt16 key_code, } } +// Returns the current keyboard input source ID (e.g., "com.apple.keylayout.ABC"), +// or nil if not on the main thread or if the input source cannot be determined. +// Carbon TIS APIs must be called from the main thread. +NSString* get_current_input_source_id(void) { + if (![NSThread isMainThread]) return nil; + TISInputSourceRef source = TISCopyCurrentKeyboardInputSource(); + if (!source) return nil; + CFStringRef source_id = TISGetInputSourceProperty(source, kTISPropertyInputSourceID); + CFStringRef result = source_id ? CFStringCreateCopy(kCFAllocatorDefault, source_id) : nil; + CFRelease(source); + return result ? (NSString*)CFBridgingRelease(result) : nil; +} + +// Selects the keyboard input source with the given source ID. +// No-ops silently if not on the main thread — Carbon TIS APIs require the main thread. +void select_input_source(NSString* source_id) { + if (![NSThread isMainThread]) return; + if (!source_id) return; + CFStringRef keys[] = { kTISPropertyInputSourceID }; + CFTypeRef values[] = { (__bridge CFStringRef)source_id }; + CFDictionaryRef dict = CFDictionaryCreate( + kCFAllocatorDefault, + (const void**)keys, + (const void**)values, + 1, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks + ); + if (!dict) return; + CFArrayRef sources = TISCreateInputSourceList(dict, false); + CFRelease(dict); + if (!sources) return; + if (CFArrayGetCount(sources) > 0) { + TISInputSourceRef source = (TISInputSourceRef)CFArrayGetValueAtIndex(sources, 0); + TISSelectInputSource(source); + } + CFRelease(sources); +} + NSArray* charToKeyCodes(NSString* keyChar) { if (keycodeDict == nil) { keycodeDict = [[NSMutableDictionary alloc] init]; diff --git a/crates/warpui/src/platform/mac/window.rs b/crates/warpui/src/platform/mac/window.rs index 34f03a7a17..871c18801e 100644 --- a/crates/warpui/src/platform/mac/window.rs +++ b/crates/warpui/src/platform/mac/window.rs @@ -476,6 +476,8 @@ extern "C" { ); fn open_url(urlString: &NSString); fn set_titlebar_height(window: &NSWindow, height: f64); + fn get_current_input_source_id() -> *mut NSString; + fn select_input_source(source_id: &NSString); } pub type FrameCaptureCallback = Box; @@ -743,6 +745,31 @@ impl Window { } } + /// Returns the current keyboard input source ID (e.g., "com.apple.keylayout.ABC"), + /// or `None` if it cannot be determined. + pub fn get_current_input_source_id() -> Option { + // SAFETY: `get_current_input_source_id` is an FFI call into `keycode.m` + // that reads the active TIS input source. + unsafe { + let ptr = get_current_input_source_id(); + if ptr.is_null() { + None + } else { + Some((*ptr).to_string()) + } + } + } + + /// Selects the keyboard input source with the given source ID. + /// The source ID is a string like "com.apple.keylayout.ABC". + pub fn select_input_source(source_id: &str) { + // SAFETY: `select_input_source` is an FFI call into `keycode.m` + // that selects the given TIS input source. + unsafe { + select_input_source(&NSString::from_str(source_id)); + } + } + pub fn open_url(url: &str) { // SAFETY: `open_url` reads the string for the duration of the call. unsafe {