Subject: Re: This feature is not about "sharing", or a "native" anything.

From: Alvaro Ramirez <[email-masked]>
Date: Thu, 20 Feb 2025 11:58:21 +0000

Eli Zaretskii <[email-masked]> writes:

>> From: Alvaro Ramirez <[email-masked]>
>> Cc: [email-masked]
>> Date: Wed, 19 Feb 2025 12:02:39 +0000
>>
>> Richard Stallman <[email-masked]> writes:
>>
>> > Let's replace "share" with "manipulate" now, with the idea 
>> > that
>> > we can
>> > change the word a few weeks from now -- after discussing 
>> > about
>> > the
>> > best word to use.
>> >
>> > "Manipulate" fits what the feature does, and it is clearly
>> > better than
>> > "share".  If we find a better word, we can change to that.
>>
>> How about "send-to" as package prefix instead of "share"? The
>> context menu would also be "Send to...".
>>
>> "Send to" feels like a great candidate, as it would apply
>> generally across different platforms.
>
> Yes, I think it's better (just sand the same proposal, before 
> even
> reading this).

Sounds good. Thank you.

Attaching the latest iteration of the patch (renamed
0004-Add-Send-to-context-menu-item-to-mouse-el.patch)..

New since 0003-Add-context-menu-share-to-mouse.el.patch:

- Replacing the word "share" throughout patch and using 
  "send"/"send to" instead.

>From 7e1f671568fde6e5f3f3381a634f6ba6077105bd Mon Sep 17 00:00:00 2001
From: xenodium <[email-masked]>
Date: Thu, 13 Feb 2025 17:30:01 +0000
Subject: [PATCH] Add "Send to..." context menu item to mouse.el

* lisp/send-to.el: New package implements sending to apps or services.

* lisp/mouse.el (context-menu-send-to): Add "Send to..." context menu.

* lisp/term/ns-win.el (ns-send-items): Expose native macOS send API.

* src/nsfns.m (ns-send-items): Implement native macOS sending.

* etc/NEWS: Announce the new feature.
---
 etc/NEWS            |   7 ++
 lisp/mouse.el       |  17 +++-
 lisp/send-to.el     | 216 ++++++++++++++++++++++++++++++++++++++++++++
 lisp/term/ns-win.el |   1 +
 src/nsfns.m         |  62 +++++++++++++
 5 files changed, 302 insertions(+), 1 deletion(-)
 create mode 100644 lisp/send-to.el

diff --git a/etc/NEWS b/etc/NEWS
index dea24adb3c9..a1798322ad6 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -280,6 +280,13 @@ customize help text for tabs displayed on the tab-bar.  Help text is
 normally shown in the echo area or via tooltips.  See the variable's
 docstring for arguments passed to a help-text function.
 
+** Mouse
+
+---
+*** context-menu-mode now includes a "Send to..." menu item.
+The menu item enables sending current file(s) or region text to external
+(non-Emacs) apps or services. See send-to.el for customisations.
+
 ** Project
 
 ---
diff --git a/lisp/mouse.el b/lisp/mouse.el
index 1f0ca6a51b6..4076097c90c 100644
--- a/lisp/mouse.el
+++ b/lisp/mouse.el
@@ -30,6 +30,7 @@
 ;;; Code:
 
 (eval-when-compile (require 'rect))
+(eval-when-compile (require 'send-to))
 
 ;; Indent track-mouse like progn.
 (put 'track-mouse 'lisp-indent-function 0)
@@ -393,7 +394,8 @@ context-menu-functions
                                     context-menu-region
                                     context-menu-middle-separator
                                     context-menu-local
-                                    context-menu-minor)
+                                    context-menu-minor
+                                    context-menu-send-to)
   "List of functions that produce the contents of the context menu.
 Each function receives the menu and the mouse click event as its arguments
 and should return the same menu with changes such as added new menu items."
@@ -536,6 +538,19 @@ context-menu-minor
                   (cdr mode))))
   menu)
 
+(defun context-menu-send-to (menu _click)
+  "Add a \"Send to...\" context MENU entry on supported platforms."
+  (run-hooks 'activate-menubar-hook 'menu-bar-update-hook)
+  (when (send-to-supported-p)
+    (define-key-after menu [separator-send] menu-bar-separator)
+    (define-key-after menu [send]
+      '(menu-item "Send to..." (lambda ()
+                                 (interactive)
+                                 (send-to))
+                  :help
+                  "Send item (region, buffer file, or dired files) to app or service")))
+  menu)
+
 (defun context-menu-buffers (menu _click)
   "Populate MENU with the buffer submenus to buffer switching."
   (run-hooks 'activate-menubar-hook 'menu-bar-update-hook)
diff --git a/lisp/send-to.el b/lisp/send-to.el
new file mode 100644
index 00000000000..78fd1b03ec3
--- /dev/null
+++ b/lisp/send-to.el
@@ -0,0 +1,216 @@
+;;; send-to.el --- send files to apps or services  -*- lexical-binding: t -*-
+
+;; Copyright (C) 1993-2025 Free Software Foundation, Inc.
+
+;; Maintainer: [email-masked]
+;; Keywords: send, apps
+;; 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:
+
+;; This package provides commands to facilitate sending files to
+;; external apps or services.
+;;
+;; Enable `context-menu-mode' to get a "Send to..." context menu item.
+;;
+;; `send-to' uses `send-to-context-items-function' to
+;; pick which file(s) to send and `send-to-handler-function' to
+;; route handling to external (non-Emacs) app or service.
+
+;;; Code:
+
+(require 'seq)
+
+(declare-function dired-between-files "dired")
+(declare-function dired-get-filename "dired")
+(declare-function dired-get-marked-files "dired")
+(declare-function dired-move-to-filename "dired")
+
+(defgroup send-to nil
+  "Send files or text to external applications or services."
+  :group 'external
+  :version "31.1")
+
+(defcustom send-to-support-checker-function #'send-to--default-support-checker-p
+  "A function returning non-nil when sending is supported by current platform."
+  :type '(function :tag "Function")
+  :group 'send-to
+  :version "31.1")
+
+(defcustom send-to-handler-function #'send-to--default-handler
+  "A function handling `send-to' external routing.
+
+The function receives a list of items to send.
+
+Each item can be a either a filename or plain text."
+  :type '(function :tag "Function")
+  :group 'send-to
+  :version "31.1")
+
+(defcustom send-to-context-items-function #'send-to--default-context-items
+  "A function to collect the items to be sent by `send-to'.
+
+Defaults to `send-to--default-context-items'.
+
+The function returns a list of items, where each item can be a either
+a filename or plain text."
+  :type '(function :tag "Function")
+  :group 'send-to
+  :version "31.1")
+
+;;;###autoload
+(defun send-to-supported-p ()
+  "Return non-nil for platforms where `send-to' is supported."
+  (unless send-to-support-checker-function
+    (error "`send-to-support-checker-function' must be set"))
+  (funcall send-to-support-checker-function))
+
+;;;###autoload
+(defun send-to (&optional items)
+  "Send file(s) or region text to external (non-Emacs) apps or services.
+
+Sending implementation is handled by `send-to-handler-function'.
+
+ITEMS list is automatically populated based on context.
+See `send-to-context-items-function' for details.
+
+ITEMS can be overridden to send a specific list of items."
+  (interactive)
+  (unless send-to-handler-function
+    (error "`send-to-handler-function' must be set"))
+  (unless send-to-context-items-function
+    (error "`send-to-context-items-function' must be set"))
+  (dolist (item items)
+    (unless (stringp item)
+      (error "Item must be a string: %s" item)))
+  (funcall send-to-handler-function
+           (or items
+               (funcall send-to-context-items-function)
+               (user-error "Nothing to send"))))
+
+(defun send-to--format-items (items)
+  "Format ITEMS into a user-presentable message string."
+  (truncate-string-to-width
+   (string-join
+    (seq-map (lambda (item)
+               (if (and (stringp item)
+                        (file-exists-p item))
+                   (format "\"%s\"" (file-name-nondirectory item))
+                 (format "\"%s\"" (truncate-string-to-width item 35 nil nil "…"))))
+             items) " ") 70 nil nil "…"))
+
+(defun send-to--dired-filenames-in-region ()
+  "If in `dired' buffer, return region files.  nil otherwise."
+  (when (and (derived-mode-p 'dired-mode)
+             (use-region-p))
+    (let* ((start (region-beginning))
+           (end (region-end))
+           (marked-files (dired-get-marked-files nil nil nil t))
+           (active-marks-p (if (= (length marked-files) 1)
+                               nil ;; File found at point (not marked)
+                             (not (seq-empty-p marked-files))))
+           (filenames))
+      (when active-marks-p
+        (user-error "Either mark `dired' files or select a region, but not both"))
+      (save-excursion
+        (save-restriction
+          (goto-char start)
+          (while (< (point) end)
+            ;; Skip non-file lines.
+            (while (and (< (point) end) (dired-between-files))
+              (forward-line 1))
+            (when (and (dired-get-filename nil t)
+                       ;; Filename must be in region.
+                       (< (save-excursion
+                            (forward-line 0)
+                            (dired-move-to-filename))
+                          end))
+              (setq filenames (append filenames (list (dired-get-filename nil t)))))
+            (forward-line 1))))
+      filenames)))
+
+(defun send-to--default-support-checker-p ()
+  "Return non-nil for platforms supporting send capability."
+  (or (and (featurep 'ns) (fboundp 'ns-send-items))
+      (executable-find "xdg-open")))
+
+(defun send-to--default-handler (items)
+  "Send ITEMS to external (non-Emacs) apps or services.
+
+ITEMS can be filenames or any text to send.
+
+Text is written to a temporary file before sending."
+  (unless items
+    (error "Nothing to send"))
+  (cond ((and (featurep 'ns) (fboundp 'ns-send-items))
+         (ns-send-items
+          (seq-map #'send-to--convert-item-to-filename items)))
+        ((executable-find "xdg-open")
+         (when (y-or-n-p (format "Open externally: %s ?"
+                                 (send-to--format-items items)))
+           (dolist (item items)
+             (with-temp-buffer
+               (unless (zerop (call-process
+                               "xdg-open" nil (current-buffer) t
+                               (send-to--convert-item-to-filename
+                                item)))
+                 (error "%s" (string-trim (buffer-string))))))))
+        (t
+         (error "Don't know how to sende %s (adjust `send-to-handler-function')"
+                (send-to--format-items items)))))
+
+(defun send-to--convert-item-to-filename (item)
+  "Convert ITEM to a filename.
+
+Unless ITEM is a verifiable filename, save its content to a file and
+return its new timestamped filename."
+  (if (file-exists-p item)
+      item
+    (let ((filename (concat temporary-file-directory
+                            (format-time-string "%F_%H.%M.%S") ".txt")))
+      (with-temp-file filename
+        (insert item))
+      filename)))
+
+(defun send-to--default-context-items ()
+  "Build a list of items to send based on default context.
+
+From a `dired' buffer, chosen items are based on either of these being active:
+
+  - Marked files
+  - Files in region.
+  - File at point.
+
+From any other buffer, either of these two, in order of preference:
+
+  - Active region text.
+  - Buffer file."
+  (cond ((derived-mode-p 'dired-mode)
+         (or
+          (send-to--dired-filenames-in-region)
+          (dired-get-marked-files)))
+        ((use-region-p)
+         (list (buffer-substring-no-properties
+                (region-beginning)
+                (region-end))))
+        ((buffer-file-name)
+         (list (buffer-file-name)))))
+
+(provide 'send-to)
+
+;;; send-to.el ends here
diff --git a/lisp/term/ns-win.el b/lisp/term/ns-win.el
index 46639f2422b..e5f239b94e3 100644
--- a/lisp/term/ns-win.el
+++ b/lisp/term/ns-win.el
@@ -214,6 +214,7 @@ 'mac-option-modifier
 (defvaralias 'mac-right-option-modifier 'ns-right-option-modifier)
 (defvaralias 'mac-function-modifier 'ns-function-modifier)
 (declare-function ns-do-applescript "nsfns.m" (script))
+(declare-function ns-send-items "nsfns.m" (items))
 (defalias 'do-applescript 'ns-do-applescript)
 
 ;;;; Services
diff --git a/src/nsfns.m b/src/nsfns.m
index a2c50468cd1..a3549ff89fc 100644
--- a/src/nsfns.m
+++ b/src/nsfns.m
@@ -3771,6 +3771,65 @@ The position is returned as a cons cell (X . Y) of the
   return Qnil;
 }
 
+#ifdef NS_IMPL_COCOA
+
+static NSPoint
+ns_get_point_relative_coordinates (NSView* view)
+{
+  Lisp_Object coordinate = Fnth (make_fixnum (2), Fposn_at_point (Fpoint (), Qnil));
+
+  Lisp_Object x = Fcar (coordinate);
+  if (NILP (x))
+  {
+    signal_error ("No x coordinate found", coordinate);
+  }
+
+  Lisp_Object y = Fcdr (coordinate);
+  if (NILP (y))
+  {
+    signal_error ("No y coordinate found", coordinate);
+  }
+
+  return [view convertPoint:NSMakePoint (XFIXNUM (x), XFIXNUM (y)) toView:nil];
+}
+
+DEFUN ("ns-send-items",
+       Fns_send_items,
+       Sns_send_items, 1, 1, 0,
+       doc: /* Send a list of items to macOS apps or services. */)
+       (Lisp_Object items)
+{
+  CHECK_LIST (items);
+
+  NSMutableArray *sent = [NSMutableArray array];
+  for (Lisp_Object tail = items; CONSP (tail); tail = XCDR (tail))
+  {
+    Lisp_Object elt = XCAR (tail);
+    if (!(STRINGP (elt)))
+    {
+      signal_error ("Element not a string", elt);
+    }
+    BOOL isDir;
+    if ([[NSFileManager defaultManager] fileExistsAtPath:@(SSDATA(elt)) isDirectory:&isDir]) {
+      [sent addObject:[NSURL fileURLWithPath:@(SSDATA(elt))]];
+      continue;
+    }
+    [sent addObject:@(SSDATA(elt))];
+  }
+
+  struct frame *frame = SELECTED_FRAME ();
+  EmacsView *view = FRAME_NS_VIEW (frame);
+  NSPoint point = ns_get_point_relative_coordinates (view);
+  NSRect displayArea = NSMakeRect (point.x + 15, view.bounds.size.height - point.y + 25, 1, 1);
+
+  NSSharingServicePicker *picker = [[NSSharingServicePicker alloc] initWithItems:sent];
+  [picker showRelativeToRect:displayArea ofView:view preferredEdge:NSRectEdgeMaxY];
+
+  return Qnil;
+}
+
+#endif /* NS_IMPL_COCOA */
+
 /* ==========================================================================
 
     Class implementations
@@ -4003,6 +4062,9 @@ - (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 (&Sns_send_items);
+#endif
   defsubr (&Sx_display_mm_width);
   defsubr (&Sx_display_mm_height);
   defsubr (&Sx_display_screens);
-- 
2.39.5 (Apple Git-154)