1

What

I need to capture some text which is then inserted at point without using the minibuffer (see "why" for why this is not entirely stupid). My idea was to just pop up a temporary window which is not read-only, but where RET is redefined to close the window and save all text in its buffer in some global variable.

I thought I could start by having a look at how help-mode is defined, but was quickly overwhelmed. Inheriting from help-mode seems like a bad idea since it redefines self-inserting keys like q to do other stuff.

I have read the elisp manual on defining major modes, but my elisp-fu seems to be too weak. Can anyone point me in the right direction or alternatively suggest a better way to achieve what I want (see "why")?

Why

exwm-xim is great, but refuses to work with any window written in qt. I can input Chinese in any window which is not a qt application, but that is not helpful when my browser is written in qt. Since the qt issue seems to be a "won't fix" for exwm-xim and I only need to write short snippets of Chinese or Japanese anyway, my idea was to just hit a key combination to bring up the minibuffer (where xim works), input the characters there, and insert them at point.

However, since the completion suggestions themselves use the minibuffer, inputting kanji or hanzi in the minibuffer disables showing suggestions, making you totally blind. The obvious workaround is to input them in a proper window instead of the minibuffer, which then vanishes once you press RET.

Alternatives

Since I only need this feature in GUI mode, I would also be happy (probably even happier) with a small popup located at or near point instead of an entire window.

Or maybe I have overlooked a way to make Chinese input work correctly in the minibuffer; that would also be fine.

The best solution (and a totally different question) would be to make exwm-xim just work in all windows, but my internet searches so far have yielded nothing on that front. To quote a friend, "I think the set of people who want to write in Chinese on Arch Linux using EXWM is pretty much you."

1 Answer 1

2

Demo of input.el (Non-EXWM)

Overview of input.el


Demo of input.el (EXWM)

input phase: Inititate it with input-start (= C-c ')

State during input-start


post-input phase: Finalize input with input-done (= C-c ')

enter image description here


do this

  1. Copy the above below snippet to a file ~/input.el
  2. emacs -Q
  3. M-x load-file RET ~/input.el
  4. In the *scratch* buffer, do C-c '.
  5. A new window will pop up with tamil-phonetic input method activated.
  6. In the new input window, type something.
  7. Type C-c ' to exit.
  8. Profit.

input.el

(require 'org-macs)
(require 'subr-x)

(defvar input-start-keys (kbd "C-c '"))

(global-set-key input-start-keys 'input-start)

(defvar input-buffer-preferred-input-method "tamil-phonetic"
  "Input method active in `input-mode'.")

(defvar input-mode-hook nil)

(defvar-local input--sink-beg-marker nil)
(put 'input--sink-beg-marker 'permanent-local t)

(defvar-local input--sink-end-marker nil)
(put 'input--sink-end-marker 'permanent-local t)

(defvar-local sink--overlay nil)
(put 'sink--overlay 'permanent-local t)

(defvar-local input-mode-stashed-window-config nil)
(put 'input-mode-stashed-window-config 'permanent-local t)

(defun input-buffer-locate (beg end)
  "Return buffer editing area between BEG and END.
Return nil if there is no such buffer."
  (catch 'exit
    (dolist (b (buffer-list))
      (with-current-buffer b
        (and (input-buffer-p)
             (= beg input--sink-beg-marker)
             (eq (marker-buffer beg) (marker-buffer input--sink-beg-marker))
             (= end input--sink-end-marker)
             (eq (marker-buffer end) (marker-buffer input--sink-end-marker))
             (throw 'exit b))))))

(defun sink--make-overlay (beg end input-buffer)
  "Create overlay between BEG and END positions and return it.
INPUT-BUFFER is the buffer currently editing area between BEG and
END."
  (let ((overlay (make-overlay beg end)))
    (overlay-put overlay 'face 'secondary-selection)
    (overlay-put overlay 'input-buffer input-buffer)
    (overlay-put overlay 'help-echo
                 "Click with mouse-1 to switch to buffer editing this segment")
    (overlay-put overlay 'face 'secondary-selection)
    (let ((read-only
           (list
            (lambda (&rest _)
              (user-error
               "Cannot modify an area being edited in a dedicated buffer")))))
      (overlay-put overlay 'modification-hooks read-only)
      (overlay-put overlay 'insert-in-front-hooks read-only)
      (overlay-put overlay 'insert-behind-hooks read-only))
    overlay))

(defun input--finalize ()
  "Remove overlay from current source buffer."
  (deactivate-transient-input-method)
  (when (overlayp sink--overlay)
    (delete-overlay sink--overlay)))


;;; Input minor mode

(defvar input-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map "\C-c'" 'input-done)
    (define-key map "\C-c\C-k" 'input-abort)
    map))

(define-minor-mode input-mode
  "Minor mode for inputting strings.

Input is initiated with `input-start` (=`\\[input-start]').

To change the key binding set `input-start-keys'.

The default `input-mode-hook' activates the transient input
method defined in `input-buffer-preferred-input-method'.

\\{input-mode-map}."
  :lighter " Input"
  (setq header-line-format
        (substitute-command-keys
         (propertize "Edit, then exit with `\\[input-done]' or abort with \ `\\[input-abort]'"
                     'face 'highlight))))

(defun my-activate-transient-input-method (input-method)
  (setq default-transient-input-method input-method)
  (setq previous-transient-input-method current-input-method)
  (when previous-transient-input-method
    (deactivate-input-method))
  (activate-input-method default-transient-input-method)
  (setq current-transient-input-method default-transient-input-method))

(defun input--configure ()
  "Configure the input buffer."
  (my-activate-transient-input-method input-buffer-preferred-input-method)
  (add-hook 'kill-buffer-hook #'input--finalize nil 'local))

(add-hook 'input-mode-hook #'input--configure)

(defun input-buffer-p (&optional buffer)
  "Non-nil when current buffer is a input buffer.
If BUFFER is non-nil, test it instead."
  (let ((buffer (org-base-buffer (or buffer (current-buffer)))))
    (and (buffer-live-p buffer)
         (local-variable-p 'input--sink-beg-marker buffer)
         (local-variable-p 'input--sink-end-marker buffer))))

(defun input-abort ()
  "Abort input and return to the sink buffer."
  (interactive)
  (unless (input-buffer-p)
    (error "Not in a sub-editing buffer"))
  (let* ((beg input--sink-beg-marker)
         (end input--sink-end-marker))

    (set-buffer-modified-p nil)
    ;; Switch to source buffer.  Kill sub-editing buffer.
    (let ((edit-buffer (current-buffer))
          (source-buffer (marker-buffer beg)))
      (unless source-buffer
        (error "Source buffer disappeared.  Aborting"))

      (org-switch-to-buffer-other-window source-buffer)
      (delete-other-windows)

      (kill-buffer edit-buffer))
    (goto-char beg)
    ;; Clean up left-over markers and restore window configuration.
    (set-marker beg nil)
    (set-marker end nil)
    (when input-mode-stashed-window-config
      (unwind-protect
          (set-window-configuration input-mode-stashed-window-config)
        (setq input-mode-stashed-window-config nil)))))

(defun input-done ()
  "Kill current sub-editing buffer and return to source buffer."
  (interactive)
  (unless (input-buffer-p)
    (error "Not in a sub-editing buffer"))
  (let* ((beg input--sink-beg-marker)
         (end input--sink-end-marker)
         (rel-pos (- (point) 1))
         (contents (buffer-substring-no-properties (point-min) (point-max))))
    (kill-new contents)
    (message "Copied input text in to kill ring")
    (set-buffer-modified-p nil)
    ;; Switch to source buffer.  Kill sub-editing buffer.
    (let ((input-buffer (current-buffer))
          (sink-buffer (marker-buffer beg)))
      (unless sink-buffer
        (error "Source buffer disappeared.  Aborting"))
      (org-switch-to-buffer-other-window sink-buffer)
      (delete-other-windows)
      (kill-buffer input-buffer))
    ;; Insert modified code.
    (cond
     ((derived-mode-p 'exwm-mode)
      (exwm-input--fake-key ?\C-v))
     (
      (org-with-wide-buffer
       (when (not (equal (buffer-substring beg end)
                         contents))
         (undo-boundary)
         (goto-char beg)
         (save-restriction
           (narrow-to-region beg end)
           (replace-region-contents beg end (lambda () contents))
           (goto-char (point-max)))))
      ;; If we are to return to source buffer, put point at an
      ;; appropriate location.  In particular, if block is hidden, move
      ;; to the beginning of the block opening line.
      (goto-char (+ beg rel-pos))))
    ;; Clean up left-over markers and restore window configuration.
    (set-marker beg nil)
    (set-marker end nil)
    (when input-mode-stashed-window-config
      (unwind-protect
          (set-window-configuration input-mode-stashed-window-config)
        (setq input-mode-stashed-window-config nil)))))

(defun input-start ()
  "Call a special editor for the element at point."
  (interactive)
  (cond
   ((derived-mode-p 'exwm-mode)
    (ignore))
   (t
    (barf-if-buffer-read-only)))
  (setq input-mode-stashed-window-config (current-window-configuration))
  (let* ((name (format "*Input %s*" (buffer-name)))
         (beg (copy-marker (point)))
         (end (copy-marker (point) t))
         (old-edit-buffer (input-buffer-locate beg end))
         (contents ""))
    (if old-edit-buffer
        ;; Move to existing buffer.
        (org-switch-to-buffer-other-window old-edit-buffer)
      (let* (;; Generate a new input buffer.
             (input-buffer (generate-new-buffer name))
             ;; Add an overlay on top of source.
             (overlay (sink--make-overlay beg end input-buffer)))
        ;; Switch to input buffer.
        (delete-other-windows)
        (org-switch-to-buffer-other-window input-buffer)
        (set-buffer-modified-p nil)
        (setq buffer-file-name nil)
        ;; Initialize buffer.
        (text-mode)
        ;; Transmit buffer-local variables for exit function.  It must
        ;; be done after initializing major mode, as this operation
        ;; may reset them otherwise.
        (setq input--sink-beg-marker beg)
        (setq input--sink-end-marker end)
        (setq sink--overlay overlay)
        ;; Start minor mode.
        (input-mode)
        ;; Clear undo information so we cannot undo back to the
        ;; initial empty buffer.
        (buffer-disable-undo (current-buffer))
        (buffer-enable-undo)
        (goto-char (point-max))))))

;; Local Variables:
;; indent-tabs-mode: nil
;; End:


FWIW, the above snippet is derived from org-src.el after stripping down all the org-isms.

Btw, org-src.el is the library that implements editing of Org Babel and other special blocks.

In org-src case you are inputting n various programming (or structural programming) languages; in your case, you are inputting in different human language.

I looked at org-src.el, to see if I can repurpose the default library to suit your current needs; unfortunately org-src.el was too soaked in org-isms that I had to copy stuff over and strip it to its bare essentials. In programmers-speak, the code that you see here is an org-src-core.el around which org-src.el could be built. In practical terms, the code I have shared here "works" or "known to work" (modulo some bugs I may have introduced during "aggressive" stripping or my own "limited" interpretation of what some areas of code are doing .)

IOW, if you know how to edit org babel blocks, you know how to use the above snippet.

So much of what Editing Source Code (The Org Manual)

(info "(org) Editing Source Code")

says holds good.


Three areas where I have done "aggressive" stripping:

  • Edting a region / Pre-filling of input buffer: org-src.el allows editing of a region a block of text. That is, it populates the input buffer with current src-block content. In the present case, the input happens at point. So, the input buffer starts out empty.
  • *Handling of mark-s
  • org-src-window-setup: org-src.el provides much wider set of options.

If my choice of window setup for inputting is not what you like, you need to borrow from org-src-window-setup.


Any further enhancements or bug fixes are left as an exercise to the reader. I have already spent enough brain cycles on what I mistakenly assumed to be a "trivial" exercise.

7
  • I don't know if you need to special manoeuvres to insert in to a qt buffer .... This special manoeuvre is probably the one thing you need to do. Btw, what qt application you are using; is it qutebrowser or some such thing.
    – user31220
    Commented Sep 28, 2022 at 5:08
  • The snippet assumes that the buffer--the sink--is managed by emacs and retrieves marker-s, makes overlays etc. In your case, the qt application---most likely you are editing a textarea or a form input in browser--is NOT an emacs browser. So, when the input is finalized copy the buffer string to clipboard and send a Ctrl+v simulation key to the qt browser. Unless you say what qt application you are using, it is very difficult what might be needed on "inserting the input" part. I hope you will be able to plugin the details.
    – user31220
    Commented Sep 28, 2022 at 5:57
  • Wow, that is a lot of work you did there. I'll try it out as soon as I am back on my emacs machine. Commented Sep 29, 2022 at 10:34
  • The most important qt applications for me are qutebrowser and the element desktop app (a matrix client). I do not quite understand how applications decide whether to let EXWM manage their input or not, and which parts to handle. For example, in qutebrowser, C-a correctly moves to the beginning of the line, while in element, it incorrectly activates the region instead. Hence I assumed qutebrowser would also honour EXWM's input method, but it does not. Commented Sep 29, 2022 at 10:39
  • I have updated the snippet slightly to account for inputting in to exwm-mode buffers (In my case, firefox). I have also added corresponding screenshots. I use gnome and I don't want to pull all the qt dependencies. The only change you have to make is in the (exwm-input--fake-key ?\C-v) part where I send Ctrl+v to the firefox to paste the newly inputted string. Btw, you need to consider configuring the exwm-input-simulation-keys and exwm-input-set-local-simulation-keys. Please report any issues you may face.
    – user31220
    Commented Sep 30, 2022 at 7:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.