我写了个MacOS下的输入法切换工具:macism

MacOS Input Source Manager

This tool is used to manage MacOS input source from command line, useful to be integrated with vim and emacs (e.g. emacs-smart-input-source). It is based on the codes from kawa.

macism 's main advantage over other similar tools is that it can reliably select CJKV( Chinese/Japanese/Korean/Vietnamese) input source, while with other tools (such as input-source-switcher, im-select from smartim, swim), when you switch to CJKV input source, you will see that the input source icon has already changed in the menu bar, but unless you activate other applications and then switch back, you input source is actually still the same as before.

macism solve other tools’ problem by reading the shortcut key for switching input sources from the system preference, and then emulate the triggering of the shortcut key.

6 个赞

brew安装的tap地址变了

brew uninstall macism
brew untap laishulu/macism
brew tap laishulu/homebrew
brew install macism
1 个赞

我是直接借鉴了emacs mac port里面的切换输入法的代码,在emacs里增加了2个函数,通过lisp调用进行输入法切换。

1 个赞

你自己定制编译了emacs?

sis里面,如果是Emacs Mac Port,也是直接调用API (GitHub - laishulu/emacs-smart-input-source: Less manual switch for native or OS input source (input method).),无疑是最佳的方法。奈何官方发行版,或者emacs-plus,都不支持。

我的emacs都是自己编译最新的,有时候会切到感兴趣的分支上自己编译,比如以前的nativecomp和现在的igc分支。在代码上加上切换输入法的代码。

切换输入法这块,你要是提交到官方代码中,让大家都能享受到,那是大功一件啊。

从emacs mac port抄过来的代码,稍微改了一下,不好意思提代码。自己用用就好了。

别这么谦虚啊,耽误造福大众 :joy:
正常提交,然后credit里面指出EMP,就OK了嘛

很需要啊,可否公布一下实现的方案?感谢感谢

凑合着用吧,object c的语法看不懂,为了编译过去注释了一堆代码,不过用了2、3年了没什么问题,主要是实现了mac-input-source这个函数,用来切换输入法。

配合evil是这样用的:

(if (and (eq system-type 'darwin) (fboundp 'mac-input-source))
    (progn
      (defvar last-ime (mac-input-source))
      (defun emacs-ime-disable ()
        ;; (start-process "set-input-source" nil "/usr/local/bin/macism" "com.apple.keylayout.ABC"))
        (setq last-ime (mac-input-source))
        (mac-select-input-source "com.apple.keylayout.ABC")
        )

      (defun emacs-ime-enable ()
        ;; (start-process "set-input-source" nil "/usr/local/bin/macism" "im.rime.inputmethod.Squirrel.Rime"))
        (mac-select-input-source last-ime)
        )

      (add-hook 'evil-insert-state-entry-hook 'emacs-ime-enable)
      (add-hook 'evil-insert-state-exit-hook 'emacs-ime-disable)
      ))

代码如下:

modified   src/nsfns.m
@@ -47,6 +47,7 @@ Updated by Christian Limpach ([email protected])
 #ifdef NS_IMPL_COCOA
 #include <IOKit/graphics/IOGraphicsLib.h>
 #include "macfont.h"
+#include <Carbon/Carbon.h>
 #endif
 
 #ifdef HAVE_NS
@@ -3097,6 +3098,514 @@ The position is returned as a cons cell (X . Y) of the
   return Qnil;
 }
 
+/* From CFData to a lisp string.  Always returns a unibyte string.  */
+Lisp_Object
+cfdata_to_lisp (CFDataRef data)
+{
+  CFIndex len = CFDataGetLength (data);
+  Lisp_Object result = make_uninit_string (len);
+
+  CFDataGetBytes (data, CFRangeMake (0, len), SDATA (result));
+
+  return result;
+}
+
+/* From CFString to a lisp string.  Returns a unibyte string
+   containing a UTF-8 byte sequence.  */
+Lisp_Object
+cfstring_to_lisp_nodecode (CFStringRef string)
+{
+  Lisp_Object result = Qnil;
+  CFDataRef data;
+  const char *s = CFStringGetCStringPtr (string, kCFStringEncodingUTF8);
+
+  if (s)
+    {
+      CFIndex i, length = CFStringGetLength (string);
+
+      for (i = 0; i < length; i++)
+	if (CFStringGetCharacterAtIndex (string, i) == 0)
+	  break;
+
+      if (i == length)
+	return make_unibyte_string (s, strlen (s));
+    }
+
+  data = CFStringCreateExternalRepresentation (NULL, string,
+					       kCFStringEncodingUTF8, '?');
+  if (data)
+    {
+      result = cfdata_to_lisp (data);
+      CFRelease (data);
+    }
+
+  return result;
+}
+
+/* From CFString to a lisp string.  Never returns a unibyte string
+   (even if it only contains ASCII characters).
+   This may cause GC during code conversion. */
+Lisp_Object
+cfstring_to_lisp (CFStringRef string)
+{
+  // eassert (!mac_gui_thread_p ());
+
+  Lisp_Object result = cfstring_to_lisp_nodecode (string);
+
+  if (!NILP (result))
+    {
+      result = code_convert_string_norecord (result, Qutf_8, 0);
+      /* This may be superfluous.  Just to make sure that the result
+	 is a multibyte string.  */
+      result = string_to_multibyte (result);
+    }
+
+  return result;
+}
+
+/* C string to CFString.  */
+CFStringRef
+cfstring_create_with_utf8_cstring (const char *c_str)
+{
+  CFStringRef str;
+
+  str = CFStringCreateWithCString (NULL, c_str, kCFStringEncodingUTF8);
+  if (str == NULL)
+    /* Failed to interpret as UTF 8.  Fall back on Mac Roman.  */
+    str = CFStringCreateWithCString (NULL, c_str, kCFStringEncodingMacRoman);
+
+  return str;
+}
+
+/* Lisp string containing a UTF-8 byte sequence to CFString.  Unlike
+   cfstring_create_with_utf8_cstring, this function preserves NUL
+   characters.  */
+CFStringRef
+cfstring_create_with_string_noencode (Lisp_Object s)
+{
+  CFStringRef string = CFStringCreateWithBytes (NULL, SDATA (s), SBYTES (s),
+						kCFStringEncodingUTF8, false);
+
+  if (string == NULL)
+    /* Failed to interpret as UTF 8.  Fall back on Mac Roman.  */
+    string = CFStringCreateWithBytes (NULL, SDATA (s), SBYTES (s),
+				      kCFStringEncodingMacRoman, false);
+
+  return string;
+}
+
+/* Lisp string to CFString.  */
+CFStringRef
+cfstring_create_with_string (Lisp_Object s)
+{
+  // eassert (!mac_gui_thread_p ());
+  if (STRING_MULTIBYTE (s))
+    {
+      char *p, *end = SSDATA (s) + SBYTES (s);
+
+      for (p = SSDATA (s); p < end; p++)
+	if (!isascii (*p))
+	  {
+	    s = ENCODE_UTF_8 (s);
+	    break;
+	  }
+      return cfstring_create_with_string_noencode (s);
+    }
+  else
+    return CFStringCreateWithBytes (NULL, SDATA (s), SBYTES (s),
+				    kCFStringEncodingMacRoman, false);
+}
+
+/* Create and return a TISInputSource object from a Lisp
+   representation of the input source SOURCE.  Return NULL if a
+   TISInputSource object cannot be created.  */
+static TISInputSourceRef
+mac_create_input_source_from_lisp (Lisp_Object source)
+{
+  TISInputSourceRef __block result = NULL;
+
+  if (SYMBOLP (source))
+    {
+      if (NILP (source) || EQ (source, Qkeyboard))
+        result = TISCopyCurrentKeyboardInputSource ();
+      else if (EQ (source, Qkeyboard_layout))
+        result = TISCopyCurrentKeyboardLayoutInputSource ();
+      else if (EQ (source, Qascii_capable_keyboard))
+        result = TISCopyCurrentASCIICapableKeyboardInputSource ();
+      else if (EQ (source, Qascii_capable_keyboard_layout))
+        result = TISCopyCurrentASCIICapableKeyboardLayoutInputSource ();
+      else if (EQ (source, Qkeyboard_layout_override))
+        result = TISCopyInputMethodKeyboardLayoutOverride ();
+      else
+      if (EQ (source, Qt))
+        {
+          CFLocaleRef locale = CFLocaleCopyCurrent ();
+
+          if (locale)
+            {
+              CFStringRef language =
+                CFLocaleGetValue (locale, kCFLocaleLanguageCode);
+
+              if (language)
+                result = TISCopyInputSourceForLanguage (language);
+              CFRelease (locale);
+            }
+        }
+    }
+  else if (STRINGP (source))
+    {
+      CFStringRef string = cfstring_create_with_string (source);
+
+      if (string)
+        {
+          CFArrayRef sources = NULL;
+          CFDictionaryRef properties =
+            CFDictionaryCreate (NULL,
+                                (const void **) &kTISPropertyInputSourceID,
+                                (const void **) &string, 1,
+                                &kCFTypeDictionaryKeyCallBacks,
+                                &kCFTypeDictionaryValueCallBacks);
+
+          if (properties)
+            {
+              sources = TISCreateInputSourceList (properties, false);
+              if (sources == NULL)
+                sources = TISCreateInputSourceList (properties, true);
+              CFRelease (properties);
+            }
+          if (sources)
+            {
+              if (CFArrayGetCount (sources) > 0)
+                result = ((TISInputSourceRef)
+                          CFRetain (CFArrayGetValueAtIndex (sources, 0)));
+              CFRelease (sources);
+            }
+          else
+            {
+              CFStringRef language =
+                CFLocaleCreateCanonicalLanguageIdentifierFromString (NULL,
+                                                                     string);
+
+              if (language)
+                {
+                  result = TISCopyInputSourceForLanguage (language);
+                  CFRelease (language);
+                }
+            }
+          CFRelease (string);
+        }
+    }
+
+  return result;
+}
+
+/* Return a Lisp representation of the input souce SOURCE, optionally
+   with its properties if FORMAT is non-nil.  */
+static Lisp_Object
+mac_input_source_properties (TISInputSourceRef source, Lisp_Object format)
+{
+  struct {
+    CFStringRef cf;
+    Lisp_Object sym;
+  } keys[] = {
+    {kTISPropertyInputSourceCategory,		QCcategory},
+    {kTISPropertyInputSourceType,		QCtype},
+    {kTISPropertyInputSourceIsASCIICapable,	QCascii_capable_p},
+    {kTISPropertyInputSourceIsEnableCapable,	QCenable_capable_p},
+    {kTISPropertyInputSourceIsSelectCapable,	QCselect_capable_p},
+    {kTISPropertyInputSourceIsEnabled,		QCenabled_p},
+    {kTISPropertyInputSourceIsSelected,		QCselected_p},
+    /* kTISPropertyInputSourceID (used as the main key) */
+    {kTISPropertyBundleID,			QCbundle_id},
+    {kTISPropertyInputModeID,			QCinput_mode_id},
+    {kTISPropertyLocalizedName,			QClocalized_name},
+    {kTISPropertyInputSourceLanguages,		QClanguages},
+    /* kTISPropertyUnicodeKeyLayoutData (unused) */
+    /* kTISPropertyIconRef (unused) */
+    /* kTISPropertyIconImageURL (handled separately) */
+  };
+  Lisp_Object result = Qnil;
+  CFStringRef __block source_id;
+
+  // mac_within_gui (^{
+      source_id = TISGetInputSourceProperty (source, kTISPropertyInputSourceID);
+    // });
+  if (source_id)
+    {
+      result = cfstring_to_lisp (source_id);
+      int i;
+
+      if (!NILP (format))
+	{
+	  Lisp_Object plist = Qnil;
+
+	  if (EQ (format, Qt)
+	      || (SYMBOLP (format) ? EQ (format, QCicon_image_file)
+		  : !NILP (Fmemq (QCicon_image_file, format))))
+	    {
+	      CFURLRef __block url;
+
+	      // mac_within_gui (^{
+		  url = TISGetInputSourceProperty (source,
+						   kTISPropertyIconImageURL);
+		// });
+	      if (url)
+		{
+		  CFStringRef str = NULL;
+
+		  /* Workaround for wrong icon image URL on OS X 10.10. */
+		  // if (mac_operating_system_version.major == 10
+		  //     && mac_operating_system_version.minor == 10)
+		  //   {
+		  //     CFURLRef fixed =
+		  //       mac_tis_create_fixed_icon_image_url (url);
+
+		  //     if (fixed)
+		  //       {
+		  //         CFRelease (url);
+		  //         url = fixed;
+		  //       }
+		  //   }
+
+		  url = CFURLCopyAbsoluteURL (url);
+		  if (url)
+		    {
+		      str = CFURLCopyFileSystemPath (url, kCFURLPOSIXPathStyle);
+		      CFRelease (url);
+		    }
+		  if (str)
+		    {
+		      plist = Fcons (QCicon_image_file,
+				     Fcons (cfstring_to_lisp (str), plist));
+		      CFRelease (str);
+		    }
+		}
+	    }
+
+	  for (i = ARRAYELTS (keys); i > 0; i--)
+	    if (EQ (format, Qt)
+		|| (SYMBOLP (format) ? EQ (format, keys[i-1].sym)
+		    : !NILP (Fmemq (keys[i-1].sym, format))))
+	      {
+		CFStringRef key = keys[i-1].cf;
+		CFTypeRef __block value;
+
+		// mac_within_gui (^{
+		    value = TISGetInputSourceProperty (source, key);
+		  // });
+		// if (value)
+		//   plist = Fcons (keys[i-1].sym,
+		// 		 Fcons (cfobject_to_lisp (value, 0, -1),
+		// 			plist));
+	      }
+
+	  if (!EQ (format, Qt) && SYMBOLP (format) && CONSP (plist))
+	    plist = XCAR (XCDR (plist));
+	  result = Fcons (result, plist);
+	}
+    }
+
+  return result;
+}
+
+DEFUN ("mac-input-source", Fmac_input_source, Smac_input_source, 0, 2, 0,
+       doc: /* Return ID optionally with properties of input source SOURCE.
+Optional 1st arg SOURCE specifies an input source.  It can be a symbol
+or a string.  If it is a symbol, it has the following meaning:
+
+nil or `keyboard'
+    The currently-selected keyboard input source.
+`keyboard-layout'
+    The keyboard layout currently being used.
+`ascii-capable-keyboard'
+    The most-recently-used ASCII-capable keyboard input source.
+`ascii-capable-keyboard-layout'
+    The most-recently-used ASCII-capable keyboard layout.
+`keyboard-layout-override'
+    Currently-selected input method's keyboard layout override.
+    This may return nil.
+t
+    The input source that should be used to input the language of the
+    current user setting.  This may return nil.
+
+If SOURCE is a string, it is interpreted as either an input source ID,
+which should be an element of the result of `(mac-input-source-list
+t)', or a language code in the BCP 47 format.  Return nil if the
+specified input source ID does not exist or no enabled input source is
+available for the specified language.
+
+Optional 2nd arg FORMAT must be a symbol or a list of symbols, and
+controls the format of the result.
+
+If FORMAT is nil or unspecified, then the result is a string of input
+source ID, which is the unique reverse DNS name associated with the
+input source.
+
+If FORMAT is t, then the result is a cons (ID . PLIST) of an input
+source ID string and a property list containing the following names
+and values:
+
+`:category'
+    The category of input source.  The possible values are
+    "TISCategoryKeyboardInputSource", "TISCategoryPaletteInputSource",
+    and "TISCategoryInkInputSource".
+`:type'
+    The specific type of input source.  The possible values are
+    "TISTypeKeyboardLayout", "TISTypeKeyboardInputMethodWithoutModes",
+    "TISTypeKeyboardInputMethodModeEnabled",
+    "TISTypeKeyboardInputMode", "TISTypeCharacterPalette",
+    "TISTypeKeyboardViewer", and "TISTypeInk".
+`:ascii-capable-p'
+    Whether the input source identifies itself as ASCII-capable.
+`:enable-capable-p'
+    Whether the input source can ever be programmatically enabled.
+`:select-capable-p'
+    Whether the input source can ever be programmatically selected.
+`:enabled-p'
+    Whether the input source is currently enabled.
+`:selected-p'
+    Whether the input source is currently selected.
+`:bundle-id'
+    The reverse DNS BundleID associated with the input source.
+`:input-mode-id'
+    A particular usage class for input modes.
+`:localized-name'
+    The localized name for UI purposes.
+`:languages'
+    Codes for languages that can be input using the input source.
+    Languages codes are in the BCP 47 format.  The first element is
+    the language for which the input source is intended.
+`:icon-image-file' (optional)
+    The file containing the image to be used as the input source icon.
+
+The value corresponding to a name ending with "-p" is nil or t.  The
+value for `:languages' is a vector of strings.  The other values are
+strings.
+
+If FORMAT is a list of symbols, then it is interpreted as a list of
+properties above.  The result is a cons (ID . PLIST) as in the case of
+t, but PLIST only contains the properties given in FORMAT.
+
+If FORMAT is a symbol, then it is interpreted as a property above and
+the result is a cons (ID . VALUE) of an input source ID string and a
+value corresponding to the property.  */)
+  (Lisp_Object source, Lisp_Object format)
+{
+  Lisp_Object result = Qnil;
+  TISInputSourceRef input_source;
+
+  check_window_system (NULL);
+  // mac_check_input_source (source, false);
+  // if (!(SYMBOLP (format) || mac_symbol_list_p (format)))
+  if (!(SYMBOLP (format)))
+    error ("FORMAT must be a symbol or a list of symbols");
+
+  block_input ();
+  input_source = mac_create_input_source_from_lisp (source);
+  if (input_source)
+    {
+      result = mac_input_source_properties (input_source, format);
+      CFRelease (input_source);
+    }
+  unblock_input ();
+
+  return result;
+}
+
+DEFUN ("mac-input-source-list", Fmac_input_source_list, Smac_input_source_list, 0, 2, 0,
+       doc: /* Return a list of input sources.
+If optional 1st arg TYPE is nil or unspecified, then all enabled input
+sources are listed.  If TYPE is `ascii-capable-keyboard', then all
+ASCII compatible enabled input sources are listed.  If TYPE is t, then
+all installed input sources, whether enabled or not, are listed, but
+this can have significant memory impact.
+
+Optional 2nd arg FORMAT must be a symbol or a list of symbols, and
+controls the format of the result.  See `mac-input-source' for their
+meanings.  */)
+  (Lisp_Object type, Lisp_Object format)
+{
+  Lisp_Object result = Qnil;
+  CFArrayRef __block list = NULL;
+
+  check_window_system (NULL);
+  // if (!(NILP (type) || EQ (type, Qt) || EQ (type, Qascii_capable_keyboard)))
+    // error ("TYPE must be nil, t, or `ascii-capable-keyboard'");
+  // if (!(SYMBOLP (format) || mac_symbol_list_p (format)))
+  if (!(SYMBOLP (format)))
+    error ("FORMAT must be a symbol or a list of symbols");
+
+  block_input ();
+  // mac_within_gui (^{
+      if (EQ (type, Qascii_capable_keyboard))
+	list = TISCreateASCIICapableInputSourceList ();
+      else
+	list = TISCreateInputSourceList (NULL, !NILP (type));
+    // });
+  if (list)
+    {
+      CFIndex index, count = CFArrayGetCount (list);
+
+      for (index = 0; index < count; index++)
+	{
+	  Lisp_Object properties =
+	    mac_input_source_properties (((TISInputSourceRef)
+					  CFArrayGetValueAtIndex (list, index)),
+					 format);
+
+	  result = Fcons (properties, result);
+	}
+      CFRelease (list);
+    }
+  unblock_input ();
+
+  return result;
+}
+
+DEFUN ("mac-select-input-source", Fmac_select_input_source, Smac_select_input_source, 1, 2, 0,
+       doc: /* Select the input source SOURCE.
+SOURCE is either a symbol or a string (see `mac-input-source').
+Specifying nil results in re-selecting the current keyboard input
+source and thus that is not meaningful.  So, unlike
+`mac-input-source', SOURCE is not optional.
+
+If optional 2nd arg SET-KEYBOARD-LAYOUT-OVERRIDE-P is non-nil, then
+SOURCE is set as the keyboard layout override rather than the new
+current keyboard input source.
+
+Return t if SOURCE could be successfully selected.  Otherwise, return
+nil.  */)
+  (Lisp_Object source, Lisp_Object set_keyboard_layout_override_p)
+{
+  Lisp_Object __block result = Qnil;
+  TISInputSourceRef input_source;
+
+  if (NILP (source))
+      return Qnil;
+
+  check_window_system (NULL);
+
+  block_input ();
+  input_source = mac_create_input_source_from_lisp (source);
+  if (input_source)
+    {
+      if (NILP (set_keyboard_layout_override_p))
+        {
+          if (TISSelectInputSource (input_source) == noErr)
+            result = Qt;
+        }
+      else
+        {
+          if (TISSetInputMethodKeyboardLayoutOverride (input_source) == noErr)
+            result = Qt;
+        }
+      CFRelease (input_source);
+    }
+
+  unblock_input ();
+
+  return result;
+}
 /* ==========================================================================
 
     Class implementations
@@ -3198,6 +3707,22 @@ - (Lisp_Object)lispString
   DEFSYM (Qdark, "dark");
   DEFSYM (Qlight, "light");
 
+  DEFSYM (Qkeyboard, "keyboard");
+  DEFSYM (Qkeyboard_layout, "keyboard-layout");
+  DEFSYM (Qascii_capable_keyboard, "ascii-capable-keyboard");
+  DEFSYM (Qascii_capable_keyboard_layout, "ascii-capable-keyboard-layout");
+  DEFSYM (Qkeyboard_layout_override, "keyboard-layout-override");
+  DEFSYM (QCascii_capable_p, ":ascii-capable-p");
+  DEFSYM (QCenable_capable_p, ":enable-capable-p");
+  DEFSYM (QCselect_capable_p, ":select-capable-p");
+  DEFSYM (QCenabled_p, ":enabled-p");
+  DEFSYM (QCselected_p, ":selected-p");
+  DEFSYM (QCbundle_id, ":bundle-id");
+  DEFSYM (QCinput_mode_id, ":input-mode-id");
+  DEFSYM (QClocalized_name, ":localized-name");
+  DEFSYM (QClanguages, ":languages");
+  DEFSYM (QCicon_image_file, ":icon-image-file");
+
   DEFVAR_LISP ("ns-icon-type-alist", Vns_icon_type_alist,
                doc: /* Alist of elements (REGEXP . IMAGE) for images of icons associated to frames.
 If the title of a frame matches REGEXP, then IMAGE.tiff is
@@ -3276,6 +3801,9 @@ - (Lisp_Object)lispString
 
   defsubr (&Sx_show_tip);
   defsubr (&Sx_hide_tip);
+  defsubr (&Smac_input_source);
+  defsubr (&Smac_input_source_list);
+  defsubr (&Smac_select_input_source);
 
 #if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
   defsubr (&Ssystem_move_file_to_trash);

3 个赞

看上去这个方案不用 emacs-rime,可以直接用系统输入法?

就是为了用系统输入法,个人不喜欢用emacs-rime,觉得内存开销比较大。

试一下去,感谢你的分享!

基本是照着emacs mac port搬过来的,有不明白的地方可以去看看emacs mac port的代码,不过emacs mac port的输入法切换相关代码不在nsfns.m里面,我是为了方便,不增加源文件放到nsfns.m里了。

我改的部分可以放心merge,这几年的代码merge都没有冲突。master分支前几天的代码编译运行正常。

最新的 master @31 下源代码变化了,我根据你贴的重新弄了个 patch 用起来啦,感谢。

3 个赞

懒人好希望有人把这个补丁提交到emacs官方repo :joy:

其实提交给Emacs-plus也行了,用户量挺多

我提了个PR,你看一下是否可以这么改,这样 emacs-plus patch 后才可以用。

忘记在那些工具类的函数前加上static了,可以去掉编译时的warning,还有个别变量定义了但是没用。另外object c的语法不太明白,直接注释掉了,应该都删掉的。