unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
From: j@mremus.net
To: Lars Ingebrigtsen <larsi@gnus.org>
Cc: 44973@debbugs.gnu.org
Subject: bug#44973: Add a macOS global hotkey function
Date: Tue, 29 Dec 2020 20:10:00 -0800	[thread overview]
Message-ID: <CAAgndd4nkkQZ0aDeXds_QSbSuKbzuzJ0U0tof+ocsN4BaUTdcg@mail.gmail.com> (raw)
In-Reply-To: <87lfdrhj4e.fsf@gnus.org>


[-- Attachment #1.1: Type: text/plain, Size: 2131 bytes --]

Hi Lars,

Here is the patch to bind a global hotkey in mac. As long as Emacs is set as
"trusted" in macOS preferences, the user can bind a two-key hotkey of the
form [modifier-key] or a single-key modifier [function key], for example
(mac-bind-global-hotkey [f1] 'tetris). Binding a three-key
combo is left to a future patch.

The code is copied from w32-register-hot-key as much as possible.

The routine intentionally does not focus the window after the hotkey is
hit; the
user must call a function like (x-focus-frame nil) to focus the frame.

I also wanted to mention that after discovering x-focus-frame, and testing
my
patch and shkd more, I realized that my patch is in fact very similar to
shkd in
functionality. The main differences are that my patch allows hotkeys to be
declared directly in emacs (which I like), and that this patch acts
directly on
an open window (e.g. making it easier to make a hotkey to yank into an
already-open buffer). On the other hand skhd would call emacsclient, which
interacts with the server, even if no frame is open, offering its own
advantages.

Anyways, I'll leave this patch for your consideration as to whether it's
the right
thing to include in emacs!

P.S. I looked into what other code in emacs might be using blocks. I believe
nsxwidget.m also has a block in nsxwidget_webkit_execute_script. I'm
guessing
it's working in our cases because we're ultimately building with the Xcode C
compiler.

Thanks

On Sun, Dec 20, 2020 at 8:35 PM Lars Ingebrigtsen <larsi@gnus.org> wrote:

> j@mremus.net writes:
>
> > But no matter what I do, it always crashes the program. I think my
> > first problem is not knowing how to call elisp (run_hooks, safe_call,
> > etc?) correctly, but second, I suspect if this is crashing due to a
> > threading issue.
>
> I am not at all familiar with the ns functions, but looking at the other
> .m files, it looks like you should be able to just say
>
>   call0 (intern ("some-function"));
>
> or something like that?  (Modulo threading stuff.)
>
> --
> (domestic pets only, the antidote for overdose, milk.)
>    bloggy blog: http://lars.ingebrigtsen.no
>

[-- Attachment #1.2: Type: text/html, Size: 3049 bytes --]

[-- Attachment #2: mac_bind_global_hotkey.diff --]
[-- Type: application/octet-stream, Size: 14266 bytes --]

diff --git a/lisp/loadup.el b/lisp/loadup.el
index 568b9fe40d..f9be0f3645 100644
--- a/lisp/loadup.el
+++ b/lisp/loadup.el
@@ -335,6 +335,7 @@
       (when (featurep 'charprop)
         (load "international/mule-util")
         (load "international/ucs-normalize")
+        (load "ns-fns")
         (load "term/ns-win"))))
 (if (fboundp 'x-create-frame)
     ;; Do it after loading term/foo-win.el since the value of the
diff --git a/lisp/ns-fns.el b/lisp/ns-fns.el
new file mode 100644
index 0000000000..54aefc8cb0
--- /dev/null
+++ b/lisp/ns-fns.el
@@ -0,0 +1,40 @@
+;;; ns-fns.el --- Lisp routines for NeXT/Open/GNUstep/macOS window system  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 1993-1994, 2005-2020 Free Software Foundation, Inc.
+
+;; Keywords: internal
+;; Package: emacs
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;
+;;; Commentary:
+
+
+;;; Code:
+(defgroup ns nil
+  "GNUstep/macOS specific features."
+  :group 'environment)
+
+(defun mac-handle-global-hotkey (event)
+  "Handles global hotkey presses, running EVENT.
+Not intended to be called directly"
+  (interactive "e")
+
+  (apply (cdr event)))
+
+(provide 'ns-fns)
+;;; ns-fns.el ends here
diff --git a/src/keyboard.c b/src/keyboard.c
index 2e0143379a..59a17d4e48 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -6010,6 +6010,11 @@ make_lispy_event (struct input_event *event)
 	return list2 (res, list2 (event->frame_or_window, location));
       }
 
+#ifdef NS_IMPL_COCOA
+      case GLOBAL_HOTKEY_EVENT:
+          return list2 (Qmac_global_hotkey, event->arg);
+#endif
+
     case USER_SIGNAL_EVENT:
       /* A user signal.  */
       {
@@ -11759,6 +11764,10 @@ syms_of_keyboard (void)
   DEFSYM (Qcommand_execute, "command-execute");
   DEFSYM (Qinternal_echo_keystrokes_prefix, "internal-echo-keystrokes-prefix");
 
+#ifdef NS_IMPL_COCOA
+  DEFSYM (Qmac_global_hotkey, "mac-global-hotkey");
+#endif
+
   accent_key_syms = Qnil;
   staticpro (&accent_key_syms);
 
@@ -12459,6 +12468,11 @@ keys_of_keyboard (void)
 
   initial_define_lispy_key (Vspecial_event_map, "delete-frame",
 			    "handle-delete-frame");
+
+#ifdef NS_IMPL_COCOA
+  initial_define_lispy_key (Vspecial_event_map, "mac-global-hotkey",
+                            "mac-handle-global-hotkey");
+#endif
 #ifdef HAVE_NTGUI
   initial_define_lispy_key (Vspecial_event_map, "end-session",
 			    "kill-emacs");
diff --git a/src/nsfns.m b/src/nsfns.m
index c7956497c4..7aeb84e880 100644
--- a/src/nsfns.m
+++ b/src/nsfns.m
@@ -67,6 +67,169 @@ Updated by Christian Limpach (chris@nice.ch)
 
    ========================================================================== */
 
+#ifdef NS_IMPL_COCOA
+const char *const lispy_to_mac_function_keys[] =
+  {
+    "up", "down", "left", "right",          /* NSUpArrowfunctionKey    0xF700 */
+    "f1", "f2", "f3", "f4", "f5",           /* NSF1FunctionKey         0xF704 */
+    "f6", "f7", "f8", "f9", "f10",
+    "f11", "f12", "f13", "f14", "f15",
+    "f16", "f17", "f18", "f19", "f20",
+    "f21", "f22", "f23", "f24", "f25",
+    "f26", "f27", "f28", "f29", "f30",
+    "f31", "f32", "f33", "f34", "f35",      /* NSF35FunctionKey        0xF726 */
+    "insert", "delete", "home",             /* NSInsertfunctionKey     0xF727 */
+    "begin", "end", "prior", "next",
+    "print", "scroll", "pause",
+    0,                                      /* NSSysReqFunctionKey     0xF731 */
+    "break", "reset",
+    0,
+    "menu",
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    "select", "execute", "undo", "redo",
+    "find", "help", "mode-change"           /* NSModeSwitchFunctionKey 0xF747 */
+  };
+
+/* Lookup virtual keycode from string representing the name of a
+   non-ascii keystroke into the corresponding virtual key, using
+   lispy_function_keys.  */
+/* Adapted from w32fns.c */
+static unsigned
+lookup_vk_code (char *key)
+{
+  unsigned i;
+
+  for (i = 0; i < 71; i++)
+    if (lispy_to_mac_function_keys[i]
+	&& strcmp (lispy_to_mac_function_keys[i], key) == 0)
+      return (i | 0xF700);
+
+  /* Alphanumerics map to themselves.  */
+  if (key[1] == 0)
+    {
+      if ((key[0] >= 'A' && key[0] <= 'Z')
+          || (key[0] >= '0' && key[0] <= '9'))
+        return key[0];
+      if (key[0] >= 'a' && key[0] <= 'z')
+        return toupper(key[0]);
+    }
+
+  // tab, enter, and backspace are special cases
+  if (strcmp (key, "tab") == 0)
+    return 9;
+  if (strcmp (key, "enter") == 0)
+    return 13;
+   if (strcmp (key, "escape") == 0)
+    return 27;
+  if (strcmp (key, "backspace") == 0)
+    return 127;
+
+  return -1;
+}
+
+static Lisp_Object
+mac_parse_and_add_hotkey (Lisp_Object key, Lisp_Object lispfn, int hook)
+{
+  /* Copied from Fdefine_key and store_in_keymap.  */
+  register Lisp_Object c;
+  unsigned vk_code = 0;
+  int lisp_modifiers = 0;
+  NSEventModifierFlags mac_modifiers = 0;
+  Lisp_Object res = Qnil;
+  char* vkname;
+
+  CHECK_VECTOR (key);
+
+  if (ASIZE (key) != 1)
+    return Qnil;
+
+  c = AREF (key, 0);
+
+  if (CONSP (c) && lucid_event_type_list_p (c))
+    c = Fevent_convert_list (c);
+
+  if (! FIXNUMP (c) && ! SYMBOLP (c))
+    error ("Key definition is invalid");
+
+  if (! FUNCTIONP(lispfn))
+    error ("HOOK argument is not a function");
+
+  /* Work out the base key and the modifiers.  */
+  if (SYMBOLP (c))
+    {
+
+      c = parse_modifiers (c);
+      lisp_modifiers = XFIXNUM (Fcar (Fcdr (c)));
+      c = Fcar (c);
+      if (!SYMBOLP (c))
+	emacs_abort ();
+
+      vkname = SSDATA (SYMBOL_NAME (c));
+      if (vkname[0] == 0)
+        error("Key definition is invalid");
+      else
+        vk_code = lookup_vk_code (vkname);
+    }
+  else if (FIXNUMP (c))
+    {
+      lisp_modifiers = XFIXNUM (c) & ~CHARACTERBITS;
+      /* Many ascii characters are their own virtual key code.  */
+      vk_code = XFIXNUM (c) & CHARACTERBITS;
+    }
+
+  if (vk_code < 0 || vk_code > 0xF747)
+    return Qnil;
+  else if ((vk_code >= 0xF700) && (vk_code <= 0xF747))
+    mac_modifiers = mac_modifiers | NSEventModifierFlagFunction;
+
+  /* Bind key combinations based on modifier mappings.  */
+  if (((lisp_modifiers & hyper_modifier)
+       && EQ (ns_command_modifier, Qhyper))
+      || ((lisp_modifiers & super_modifier)
+          && EQ (ns_command_modifier, Qsuper))
+      || ((lisp_modifiers & meta_modifier)
+          && EQ (ns_command_modifier, Qmeta))
+      || ((lisp_modifiers & ctrl_modifier)
+          && EQ (ns_command_modifier, Qcontrol))
+      )
+    {
+      mac_modifiers = mac_modifiers | NSEventModifierFlagCommand;
+    }
+
+  if (((lisp_modifiers & hyper_modifier)
+       && EQ (ns_alternate_modifier, Qhyper))
+      || ((lisp_modifiers & super_modifier)
+          && EQ (ns_alternate_modifier, Qsuper))
+      || ((lisp_modifiers & meta_modifier)
+          && EQ (ns_alternate_modifier, Qmeta))
+      || ((lisp_modifiers & ctrl_modifier)
+          && EQ (ns_alternate_modifier, Qcontrol))
+      )
+    {
+      mac_modifiers = mac_modifiers | NSEventModifierFlagOption;
+    }
+
+  if (((lisp_modifiers & hyper_modifier)
+       && EQ (ns_control_modifier, Qhyper))
+      || ((lisp_modifiers & super_modifier)
+          && EQ (ns_control_modifier, Qsuper))
+      || ((lisp_modifiers & meta_modifier)
+          && EQ (ns_control_modifier, Qmeta))
+      || ((lisp_modifiers & ctrl_modifier)
+          && EQ (ns_control_modifier, Qcontrol))
+      )
+    {
+      mac_modifiers = mac_modifiers | NSEventModifierFlagControl;
+    }
+
+  // Bind function key
+  if ((mac_modifiers & NSEventModifierFlagDeviceIndependentFlagsMask) > 0)
+    mac_bind_key(mac_modifiers, vk_code, lispfn);
+
+  return key;
+}
+#endif
+
 /* Let the user specify a Nextstep display with a Lisp object.
    OBJECT may be nil, a frame or a terminal object.
    nil stands for the selected frame--or, if that is not a Nextstep frame,
@@ -2984,6 +3147,56 @@ The position is returned as a cons cell (X . Y) of the
   return Qnil;
 }
 
+#ifdef NS_IMPL_COCOA
+static Lisp_Object mac_registered_hotkeys;
+
+DEFUN ("mac-bind-global-hotkey",
+       Fmac_bind_global_hotkey,
+       Smac_bind_global_hotkey, 2, 2, 0,
+       doc: /* Bind KEY combination as a global hotkey, and run HOOK upon
+invokation. This function assigns a hotkey that will run an elisp function
+from anywhere in MacOS outside Emacs.  The return value is t if registering
+the hotkey was successful, otherwise nil.  */)
+ (Lisp_Object key, Lisp_Object hook)
+{
+  id handler;
+
+  key = mac_parse_and_add_hotkey (key, hook, 1);
+
+  if (!NILP (key) && NILP (Fmemq (Fcons(key,hook), mac_registered_hotkeys)))
+    {
+      Lisp_Object item = Fmemq (Qnil, mac_registered_hotkeys);
+
+      if (NILP (item))
+        mac_registered_hotkeys = Fcons (Fcons(key,hook), mac_registered_hotkeys);
+      else
+        XSETCAR (item, Fcons(key,hook));
+    }
+
+  return key;
+}
+
+DEFUN ("mac-show-bound-hotkeys", Fmac_show_bound_hotkeys,
+       Smac_show_bound_hotkeys, 0, 0, 0,
+       doc: /* Return list of registered hot-key IDs.  */)
+  (void)
+{
+  return mac_registered_hotkeys;
+}
+
+DEFUN ("mac-clear-hotkeys", Fmac_clear_hotkeys,
+       Smac_clear_hotkeys, 0, 0, 0,
+       doc: /* Unbind all global hotkeys */)
+  (void)
+{
+  mac_registered_hotkeys = Qnil;
+
+  mac_clear_hotkeys();
+
+  return Qt;
+}
+#endif
+
 /* ==========================================================================
 
     Class implementations
@@ -3136,6 +3349,11 @@ - (Lisp_Object)lispString
   defsubr (&Sns_set_mouse_absolute_pixel_position);
   defsubr (&Sns_mouse_absolute_pixel_position);
   defsubr (&Sns_show_character_palette);
+#ifdef NS_IMPL_COCOA
+  defsubr (&Smac_bind_global_hotkey);
+  defsubr (&Smac_show_bound_hotkeys);
+  defsubr (&Smac_clear_hotkeys);
+#endif
   defsubr (&Sx_display_mm_width);
   defsubr (&Sx_display_mm_height);
   defsubr (&Sx_display_screens);
@@ -3160,6 +3378,10 @@ - (Lisp_Object)lispString
   defsubr (&Sx_show_tip);
   defsubr (&Sx_hide_tip);
 
+#ifdef NS_IMPL_COCOA
+  staticpro (&mac_registered_hotkeys);
+  mac_registered_hotkeys = Qnil;
+#endif
   as_status = 0;
   as_script = Qnil;
   staticpro (&as_script);
diff --git a/src/nsterm.h b/src/nsterm.h
index f292993d8f..dfade9004b 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -379,6 +379,7 @@ #define NS_DRAW_TO_BUFFER 1
 #ifdef NS_IMPL_COCOA
   BOOL shouldKeepRunning;
   BOOL isFirst;
+  NSStatusItem *theItem;
 #endif
 #ifdef NS_IMPL_GNUSTEP
   BOOL applicationDidFinishLaunchingCalled;
@@ -1230,6 +1231,11 @@ #define NSAPP_DATA2_RUNFILEDIALOG 11
 extern void ns_init_events (struct input_event *);
 extern void ns_finish_events (void);
 
+#ifdef NS_IMPL_COCOA
+typedef enum NSEventModifierFlags NSEventModifierFlags;
+extern void mac_bind_key (enum NSEventModifierFlags modifier, unsigned vkey, Lisp_Object lispfn);
+extern void mac_clear_hotkeys (void);
+#endif
 
 #ifdef NS_IMPL_GNUSTEP
 extern char gnustep_base_version[];  /* version tracking */
diff --git a/src/nsterm.m b/src/nsterm.m
index fa38350a2f..c556eb3a05 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -9721,6 +9721,58 @@ Convert an X font name (XLFD) to an NS font name.
   return ret;
 }
 
+#ifdef NS_IMPL_COCOA
+// Store event handler event ids.
+NSMutableArray *hotkey_ids;
+
+void
+mac_bind_key (NSEventModifierFlags modifier, unsigned vkey,
+                       Lisp_Object lispfn)
+{
+  id handler;
+
+  // Check if Emacs is trusted.
+  NSDictionary *options = @{(__bridge id) kAXTrustedCheckOptionPrompt: @YES};
+  Boolean trusted = AXIsProcessTrustedWithOptions((CFDictionaryRef)options);
+
+  if (hotkey_ids == nil)
+    hotkey_ids = [[NSMutableArray alloc] initWithCapacity: 1];
+
+  if (trusted) {
+    handler = [NSEvent
+                addGlobalMonitorForEventsMatchingMask: (NSEventMaskKeyDown)
+                                              handler:^(NSEvent *event) {
+        if ((event.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask) == modifier) {
+          if([[event.charactersIgnoringModifiers capitalizedString]
+               characterAtIndex:0] == vkey) {
+            struct frame *emacsframe = SELECTED_FRAME ();
+            NSEvent *theEvent = [NSApp currentEvent];
+
+            emacs_event->kind = GLOBAL_HOTKEY_EVENT;
+            emacs_event->arg = lispfn;
+            EV_TRAILER(theEvent);
+          }
+        }
+      }];
+
+    [hotkey_ids addObject: handler];
+  }
+  else {
+    error("Emacs app isn't trusted. Enable in system settings.");
+  }
+}
+
+void
+mac_clear_hotkeys (void)
+{
+
+  for (id hotkey in hotkey_ids) {
+    [NSEvent removeMonitor: hotkey];
+  }
+
+  [hotkey_ids removeAllObjects];
+}
+#endif
 
 void
 syms_of_nsterm (void)
@@ -9935,6 +9987,7 @@ Nil means use fullscreen the old (< 10.7) way.  The old way works better with
   DEFSYM (QCmouse, ":mouse");
 
 #ifdef NS_IMPL_COCOA
+  DEFSYM (Qglobal_hotkey_hook, "global-hotkey-hook");
   Fprovide (Qcocoa, Qnil);
   syms_of_macfont ();
 #else
diff --git a/src/termhooks.h b/src/termhooks.h
index d18b750c3a..b9c32a1ccd 100644
--- a/src/termhooks.h
+++ b/src/termhooks.h
@@ -188,6 +188,11 @@ #define EMACS_TERMHOOKS_H
   USER_SIGNAL_EVENT,		/* A user signal.
                                    code is a number identifying it,
                                    index into lispy_user_signals.  */
+#ifdef NS_IMPL_COCOA
+  GLOBAL_HOTKEY_EVENT,          /* An event for when a hotkey is pressed
+                                   outside of emacs that is setup to execute
+                                   an elisp function.  */
+#endif
 
   /* Help events.  Member `frame_or_window' of the input_event is the
      frame on which the event occurred, and member `arg' contains

  reply	other threads:[~2020-12-30  4:10 UTC|newest]

Thread overview: 19+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-11-30 21:01 bug#44973: Add a macOS global hotkey function j
2020-12-08 20:39 ` Lars Ingebrigtsen
2020-12-21  0:40   ` j
2020-12-21  4:34     ` Lars Ingebrigtsen
2020-12-30  4:10       ` j [this message]
2020-12-30 11:01         ` Alan Third
2020-12-30 17:08         ` Eli Zaretskii
2021-01-04  5:13           ` j
2021-01-04 17:09             ` Eli Zaretskii
2021-01-05  8:25               ` Lars Ingebrigtsen
2021-01-05  8:26                 ` Lars Ingebrigtsen
2021-01-05 15:04                   ` Eli Zaretskii
2021-01-05 15:04                 ` Eli Zaretskii
2021-01-06  5:15                 ` Richard Stallman
2021-01-06  5:02               ` Richard Stallman
2020-12-21  8:12     ` Alan Third
2020-12-21 16:18       ` Eli Zaretskii
2020-12-21 16:34         ` Alan Third
2020-12-22  5:20           ` Richard Stallman

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.gnu.org/software/emacs/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=CAAgndd4nkkQZ0aDeXds_QSbSuKbzuzJ0U0tof+ocsN4BaUTdcg@mail.gmail.com \
    --to=j@mremus.net \
    --cc=44973@debbugs.gnu.org \
    --cc=larsi@gnus.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).