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

From: Visuwesh <[email-masked]>
Date: Thu, 20 Feb 2025 17:54:02 +0530

[வியாழன் பிப்ரவரி 20, 2025] Alvaro Ramirez wrote:

In interest of adapting my personal KDE Connect code as a plug-in for
the new library, I have some comments below.

> 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)

This isn't required since seq is preloaded nowadays IIRC.

> +(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")

Does this need to be a defcustom?  This seems like an internal feature,
and I wonder if a defvar/defcustom is necessary even.

> +(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."

I see that the way to distinguish between a filename and "plain text" is
`file-exists-p'.  Why not make filenames be a file: URL?

What about other URLs such as https:?  IMO, these should be accounted
for since it is quite common to send URLs (to other devices in my case).

> +  :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")

We should document how a filename is distinguished from plain text item
here again.  Also should this be buffer-local so different major-mode
can provide different providers?  The current approach is a bit
difficult for major-mode authors to provide support for this feature.

> +;;;###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')"
                                      ^^^^^ Typo here!
                                      
> +                (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)))))

Why not add (thing-at-point 'existing-filename)?  Addition of this will
also provide rudimentary support for other major-modes via
thing-at-point-provider-alist.