;;; rope-read-mode.el --- Rearrange lines to read text smoothly -*- lexical-binding: t -*-

;; THIS FILE HAS BEEN GENERATED.



;; Copyright 2015-2026 Marco Wahl
;; 
;; Author: Marco Wahl <marcowahlsoft@gmail.com>
;; Maintainer: Marco Wahl <marcowahlsoft@gmail.com>
;; Created: 4 Jan 2015
;; Package-Version: 20260610.2004
;; Package-Revision: 4270080eafa5
;; Keywords: reading, convenience, chill
;; URL: https://gitlab.com/marcowahl/rope-read-mode
;; Package-Requires: ((emacs "24"))
;; 
;; This file is not part of Emacs.
;; 
;; This program 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.
;; 
;; This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.


;;; Commentary:

;; * TL;DR
;; 
;; =rope-read-mode= can reverse every other line of a buffer or in a part
;; of a buffer.  With rope-read-mode reading is like following a rope.
;; 
;; 1. Activate with {M-x rope-read-mode RET}
;; 2. Press p or g.
;; 2.1. Wait for it.
;; 3. Enjoy.
;; 4. Press p, SPC, DEL, r or g.
;; 5. Goto step 3 or:
;; 6. Press q to quit.
;; 
;; note: rope-read-mode depends on programm =gm= of the
;; GraphicsMagick-suite for full functionality .
;; 
;; * Documentation
;; 
;; ** About rope-read
;; 
;; =rope-read-mode= reverses every other line for a text.  With every
;; other line reversed reading can be like following a rope.
;; 
;; read a line in one direction e.g. from left to right and the next line
;; from right to left.
;; 
;; Not so cool with rope read: a reversed line is somewhat harder to read
;; compared with a non-reversed line.
;; 
;; Typically you need to practice for some time to be able to read
;; reversed lines effortlessly.
;; 
;; *** Why rope-read?
;; 
;; - Chill.  =rope-read-mode= allows fluent reading.
;; 
;;   - Find the start of the next line easily.
;; 
;;   - Avoid flurry eye movement.
;; 
;; - Have an alternative view on text.
;; 
;; *** Illustration
;; 
;; [[file:rope-read-illustration.png][file:./rope-read-illustration.png]]
;; 
;; ** Usage
;; 
;; *** Turning it on and off
;; 
;; =M-x rope-read-mode= in a buffer activates rope-read.  No visible
;; change in the buffer is to be expected.  The buffer is set read-only.
;; 
;; Type =M-x rope-read-mode= or press 'q' to quit rope-read.  The buffer
;; writability gets restored.
;; 
;; *** Action
;; 
;; When =rope-read-mode= is on you can press
;; - =C-g= to interrupt =rope-read-mode= anytime
;; - =q= to quit =rope-read-mode=
;; - =?= to open the help buffer
;; - =r= /redraw standard/ to go back to the representation of the buffer
;;   without reversed lines (keeping =rope-read-mode=)
;; - =p= /paragraph/ to reverse every other line starting with the line
;;   below the cursor up to the end of the paragraph (if visible) and
;;   move point there
;; - The next four commands are each followed by reversing every other
;;   line in the visible part.  The keys are taken the same as in
;;   =view-mode=:
;;   - =SPC= to scroll a screen down
;;   - =<backspace>= or =S-SPC= to scroll a screen up
;;   - =v= or =<return>= to scroll one line down
;;   - =V= or =y= to scroll one line up
;; - =g= /get the rope-read/ to trigger reversing every other line for
;;   the currently visible part of the buffer
;; - =d= /downwards/ to reverse every other line starting with the line
;;   below the cursor
;; 
;; *** Customization
;; 
;; **** Keybinding to activate rope-read
;; 
;; For convenience you can bind command =rope-read-mode= to a key.  For
;; example to activate or deactivate =rope-read-mode= by pressing the key
;; seqence C-c R use the line
;; 
;; #+BEGIN_EXAMPLE
;; (keymap-global-set "C-c R" #'rope-read-mode)
;; #+END_EXAMPLE
;; 
;; in your emacs init file.
;; 
;; **** Customize the flipping
;; 
;; You can control the flipping somewhat via customization.
;; See =M-x customize-apropos rope-read=.
;; 
;; *** Image files
;; 
;; The reverse representation of lines is realized with images (if
;; =rope-read-pure-text= is off.)  They get collected in directory
;; rope-read-mode/ in the user-emacs-directory.
;; 
;; You can delete this directory any time.
;; 
;; *** User Feedback
;; 
;; [2016-10-03 Mon 15:04] M: "I use rope read mode occasionally to read
;; info manuals, mail (via gnus) and text of web pages (via eww).  Most
;; of the time I start the mode and then use key 'p' to walk through the
;; text.  Finally I use key 'q' to quit."
;; 
;; ** Preconditions for full rope-read-mode functionality
;; 
;; - Emacs is running under X.
;; - The programm =gm= of the GraphicsMagick-suite is available.
;; 
;; The =gm= program has the job to create images of lines and rotate them.
;; 
;; ** rope-read-mode without image creation
;; 
;; turn option =rope-read-pure-text= on to enable rope-read-mode without
;; creating images.
;; 
;; note that with this setting the characters in every other line get
;; just reversed.  the result might not be as readable as with the
;; flipping of the lines.


;;; Code:


;; Variables for customization

(defcustom rope-read-flip-line-horizontally t
  "When not nil the line in rope-read-mode gets flipped upside
  down.  When nil no upside down flip occurs."
  :group 'rope-read
  :type 'boolean)

(defcustom rope-read-flip-line-vertically t
  "When not nil the line in rope-read-mode gets flipped left
  right.  When nil no left right flip occurs."
  :group 'rope-read
  :type 'boolean)

(defcustom rope-read-pure-text nil
  "When not nil the line gets flipped by pure
character reversing."
  :group 'rope-read
  :type 'boolean)

(defcustom rope-read-mode-lighter
  " ⇌"
  "Text in the mode line to indicate that the mode is on."
  :group 'rope-read
  :type 'string)


;; Variables

(defvar rope-read-overlays nil
  "List of rope-read-overlays.")

(defvar rope-read-olimid-next-unused 0
  "Overlay-image-id that has not been used yet.

  The program must reset this variable reasonably when an id gets
  used.")

(defvar rope-read-image-overlay-path
  (concat user-emacs-directory "rope-read-mode/")
  "Path where the overlay images get stored.")

(defvar rope-read-image-overlay-filename-format-string
  (concat (file-name-directory rope-read-image-overlay-path) "%d.png")
  "Template for the filenames to be written to disk.")

(defvar rope-read-mode nil)
(make-variable-buffer-local 'rope-read-mode)

(defvar rope-read-old-buffer-read-only)
(make-variable-buffer-local 'rope-read-old-buffer-read-only)

(defvar rope-read-transform-fun
  ;; #'rope-read-reol-in-visible-buffer-part-with-images
  #'rope-read-reol
  "The function which transforms a screen for rope-reading.

This indirection is for the comfort of any coder to try
out something new.")

(defvar rope-read-mode-hook nil)


;; Keys

(defvar rope-read-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map " " #'rope-read-next-page)
    (define-key map [?\S-\ ] #'rope-read-prev-page)
    (define-key map (kbd "<backspace>") #'rope-read-prev-page)
    (define-key map (kbd "<return>") #'rope-read-scroll-up-line)
    (define-key map "v" #'rope-read-scroll-up-line)
    (define-key map "y" #'rope-read-scroll-down-line)
    (define-key map "V" #'rope-read-scroll-down-line)
    (define-key map "g" #'rope-read-refresh)
    (define-key map "d" #'rope-read-reol)
    (define-key map "p" #'rope-read-next-paragraph)
    (define-key map "r" #'rope-read-delete-overlays)
    (define-key map "q" #'rope-read-quit)
    (define-key map "?" #'describe-mode)
    map)
  "Keymap for `rope-read-mode'.")


;; The mode

;;;###autoload
(define-minor-mode rope-read-mode
  "Rope Reading mode.

In rope-read-mode every other line gets reversed.  rope-read-mode is a
view only mode.

\\{rope-read-mode-map}

This mode can help to save eye movements.

By reversing every other line the reader often just can dip the
gaze at the end of a line to read on instead of doing the
annoying search for the next line at the other side of the text."
  :lighter rope-read-mode-lighter :keymap rope-read-mode-map
  (if rope-read-mode (rope-read-mode-enable) (rope-read-mode-disable)))

(defun rope-read-mode-enable ()
  (unless (file-exists-p rope-read-image-overlay-path)
    (make-directory rope-read-image-overlay-path))
  (setq rope-read-old-buffer-read-only buffer-read-only
        buffer-read-only t)
  (run-hooks 'rope-read-mode-hook))

(defun rope-read-mode-disable ()
  (rope-read-delete-overlays)
  (setq buffer-read-only rope-read-old-buffer-read-only))


;; Commands

(defun rope-read-delete-overlays ()
  "Delete all overlays currently used with the rope-read-feature."
  (interactive)
  (mapc #'delete-overlay rope-read-overlays)
  (setq rope-read-overlays nil))

(defun rope-read-next-page ()
  "Scroll up one page.
If point is at the bottom bring the line with the cursor to the
top.  This is supposed to ease reading."
  (interactive)
  (rope-read-delete-overlays)
  (if (rope-read-point-at-bottom-p)
      (recenter 0)                      ;
    (scroll-up-command))
  (redisplay t)
  (move-to-window-line 0)
  (funcall rope-read-transform-fun))

(defun rope-read-prev-page ()
  (interactive)
  (rope-read-delete-overlays)
  (scroll-down-command)
  (redisplay t)
  (move-to-window-line 0)
  (funcall rope-read-transform-fun))

(defun rope-read-scroll-line (n)
  "Scroll the buffer N lines and reverse every other visible line."
  (rope-read-delete-overlays)
  (scroll-up-line n)
  (redisplay t)
  (move-to-window-line 0)
  (funcall rope-read-transform-fun))

(defun rope-read-scroll-up-line (n)
  "Scroll the buffer up N lines and reverse every other visible line.

  E.g.  for N = 1 the second-line becomes first."
  (interactive "p")
  (unless n (setq n 1))
  (rope-read-scroll-line n))

(defun rope-read-scroll-down-line (n)
  "Scroll the buffer down N lines and reverse every other line.

  E.g.  for N = 1 the first-line becomes second."
  (interactive "p")
  (unless n (setq n 1))
  (rope-read-scroll-line (- n)))

(defun rope-read-refresh ()
  "Refresh the rope-read-representation for the given window."
  (interactive)
  (rope-read-delete-overlays)
  (redisplay t)
  (move-to-window-line 0)
  (funcall rope-read-transform-fun))

(defun rope-read-quit ()
  (interactive)
  (when rope-read-mode (rope-read-mode 'toggle)))


;; Coordinates calculation

(defun rope-read-y-info-of-line ()
  "Return the top coordinate and the height of the line that contains `(point)'.
This function typically takes a while."
  (let* ((beg (progn (beginning-of-visual-line) (point)))
         (posn-at-point
          (progn
            (posn-at-point (point))))
         (y-top (cdr (posn-x-y posn-at-point)))
         (height (cdr (nth 9 posn-at-point)))
         (end (progn (end-of-visual-line) (point))))
    (goto-char beg)
    (while (and (< (point) (point-max))
                (progn (forward-char)
                       (< (point) end)))
      (setq
       posn-at-point (posn-at-point (point))
       height (max height (cdr (nth 9 posn-at-point)))
       y-top (min y-top (cdr (posn-x-y posn-at-point)))))
    (cons y-top height)))

(defun rope-read-line-height ()
  "Height of the current line."
  (let* ((beg (progn (beginning-of-visual-line) (point)))
         (height (cdr (nth 9 (posn-at-point beg))))
         (end (progn (end-of-visual-line) (point))))
    (goto-char beg)
    (forward-char)
    (while (progn (< (point) end))
      (setq height (max height (cdr (nth 9 (posn-at-point (point))))))
      (forward-char))
    height))

(defun rope-read-line-width ()
  "Width of the current line."
  (end-of-visual-line)
  (car (posn-x-y (posn-at-point (point)))))

(defun rope-read-line-top ()
  "Top coordinate of the current line."
  (end-of-visual-line)
  (cdr (posn-x-y (posn-at-point (point)))))

(defun rope-read-line-widths-and-tops ()
  "Line widths and tops of the window."
  (let ((max-line-number (move-to-window-line -1))
        (line 0)
        widths tops)
    (while (<= line max-line-number)
      (move-to-window-line line)
      (setq tops (cons (rope-read-line-top) tops)
            widths (cons (rope-read-line-width) widths))
      (cl-incf line))
    (cons (vconcat (nreverse widths)) (vconcat (nreverse tops)))))


;; Reverse those lines

(defun rope-read-reol-in-visible-buffer-part-with-images ()
  "Reverse every other line in the visible buffer part."
  (move-to-window-line 0)
  (rope-read-reol))

(defun rope-read-advance-one-visual-line ()
  (beginning-of-visual-line 2))

(defun rope-read-reol ()
  "Reverse every other line in the visible part starting with line after point."
  (interactive)
   (let ((point-at-start (point))
         (last-line
          (progn (move-to-window-line -1)
                 (point))))
     (goto-char point-at-start)
     (beginning-of-visual-line)
     (rope-read-advance-one-visual-line)
     (while (and (< (point) last-line) ; todo: handle case of last line
                 (< (save-excursion (end-of-visual-line) (point))
                    (point-max)))  ; todo: try to handle also the very
                                        ; last line.  the last line is
                                        ; special because it is
                                        ; special for the
                                        ; beginning-of-visual-line
                                        ; command.  no further
                                        ; iteration!
       (unless rope-read-pure-text
	 (rope-read-snap-visual-line-under-olimid-filename))
       (let* ((l-beg   (save-excursion (beginning-of-visual-line) (point)))
              (l-end   (save-excursion (end-of-visual-line) (point)))
              (l-next  (save-excursion
                         (goto-char l-beg) (beginning-of-visual-line 2) (point)))
                                        ; try to use for identify truncation of the line
              (olimid-current (1- rope-read-olimid-next-unused)))
         (push (make-overlay l-beg l-end) rope-read-overlays)
	 (if rope-read-pure-text
        (overlay-put
         (car rope-read-overlays) 'display (reverse (buffer-substring l-beg l-end)))
         (overlay-put
          (car rope-read-overlays) 'display
          (create-image
           (expand-file-name
            (format
             rope-read-image-overlay-filename-format-string
             olimid-current))
           nil nil
           :scale 1.0
           :ascent 'center
           ;; TODO: try to refine.  hint: try
           ;; understand.  is this a font-dependent
           ;; thing?  e.g. :ascent 83 is possible.
           ;; there are further attributes...
           )))
         (when (= l-end l-next)
           (overlay-put (car rope-read-overlays) 'after-string "\n")
           ;; this newline makes the images appear in some cases.
           ;; todo: at least think about doing something similar in
           ;; the analog case of 'before'.
           )
         (goto-char l-next)
         (redisplay t)
         (rope-read-advance-one-visual-line)))
     (forward-line -1)
     (beginning-of-visual-line)))


;; Line snapper

(defun rope-read-snap-visual-line-under-olimid-filename ()
  "Snapshot the visual line with `(point)' flipflopped.

Also consider the line above the line containing `(point)'.  If
the line above is longer then extend the snapshot to use the
length of the line above.  This often eases continuation of
reading for short lines.

The file name for the snapshot contains the number
`rope-read-olimid-next-unused' as index.  Use the source for all
detail."
  (interactive "P")
  (save-excursion
    (let* ((beg (progn (beginning-of-visual-line) (point)))
           (end (progn (end-of-visual-line) (point)))
           (end-above (save-excursion (goto-char beg) (end-of-visual-line 0) (point)))
           (beg-next (progn  (goto-char beg) (beginning-of-visual-line 2) ))
           (width (if (or (= end beg-next) (= end-above beg))
                      (- (nth 2 (window-inside-pixel-edges))
                         (nth 0 (window-inside-pixel-edges)))
                    (- (max (car (posn-x-y (posn-at-point end)))
                            (car (posn-x-y (posn-at-point end-above))))
                       (car (posn-x-y (posn-at-point beg))))))
           (y-info-getter #'rope-read-y-info-of-line)
           (y-top-height (progn (goto-char beg)
                                (funcall y-info-getter)))
           (y-pos-line (car y-top-height))
           (height (cdr y-top-height))
           (x-win-left (nth 0 (window-inside-pixel-edges)))
           (y-win-top (nth 1 (window-inside-pixel-edges)))
           (x-anchor (+ x-win-left))
           (y-anchor (+ y-win-top y-pos-line))
	   (filename (expand-file-name
		      (format
		       rope-read-image-overlay-filename-format-string
		       (1-(setq
			   rope-read-olimid-next-unused
			   (1+ rope-read-olimid-next-unused)))))))
      (shell-command
       (format
	"gm import -window %s -crop %dx%d+%d+%d -quality 50 %s"
	(frame-parameter nil 'window-id)
	width height x-anchor y-anchor
	filename))
      (shell-command
       (format
	"gm convert %s %s %s %s"
	(if rope-read-flip-line-horizontally "-flip" "")
	(if rope-read-flip-line-vertically "-flop" "")
	filename
	filename)))))


;; Paragraph wise rope read
(defun rope-read-reol-in-region (start end)
  "Reverse every other line starting with line with pos START.
Do this at most up to pos END."
  (interactive "r")
  (rope-read-delete-overlays)
  (let ((transient-mark-mode-before transient-mark-mode)) ; T is this better (transient-mark-mode -1)?  see also transient-mark-mode references below.
    (unwind-protect
        (let* ((point-at-start start)
               (point-at-last-window-line (progn (move-to-window-line -1) (point)))
               (point-at-end (min end point-at-last-window-line)))
          (transient-mark-mode -1)
          (goto-char point-at-start)
          (beginning-of-visual-line)
          (rope-read-advance-one-visual-line)
          (while (and (< (point) point-at-end) ; todo: handle case of last line
                      (< (save-excursion (end-of-visual-line) (point))
			 (min point-at-end (point-max)))) ; todo: try to handle also the very
                                        ; last line.  the last line is
                                        ; special because it is
                                        ; special for the
                                        ; beginning-of-visual-line
                                        ; command.  no further
                                        ; iteration!
            (rope-read-snap-visual-line-under-olimid-filename)
            (let* ((l-beg   (save-excursion (beginning-of-visual-line) (point)))
                   (l-end   (save-excursion (end-of-visual-line) (point)))
                   (l-next  (save-excursion
                              (goto-char l-beg) (beginning-of-visual-line 2) (point)))
                                        ; try to use for identify truncation of the line
                   (olimid-current (1- rope-read-olimid-next-unused)))
              (push (make-overlay l-beg l-end) rope-read-overlays)
              (overlay-put
               (car rope-read-overlays) 'display
               (create-image
		(expand-file-name
		 (format
                  rope-read-image-overlay-filename-format-string
                  olimid-current))
		nil nil
		:scale 1.0
		:ascent 'center
		;; TODO: try to refine.  hint: try
		;; understand.  is this a font-dependent
		;; thing?  e.g. :ascent 83 is possible.
		;; there are further attributes...
		))
              (when (= l-end l-next)
		(overlay-put (car rope-read-overlays) 'after-string "\n")
		;; this newline makes the images appear in some cases.
		;; todo: at least think about doing something similar in
		;; the analog case of 'before'.
		)
              (goto-char l-next)
              (redisplay t)
              (rope-read-advance-one-visual-line)))
          (when ( <= point-at-last-window-line (point))
            (beginning-of-line 0)))
      (transient-mark-mode transient-mark-mode-before))))

(defun rope-read-reol-in-region-pure-text (start end)
  "EXPERIMENTAL!
Reverse every other line starting with line with pos START.
Do this at most up to pos END.
Do this by displaying the characters reversed."
  (interactive "r")
  (rope-read-delete-overlays)
  (let*
      ((point-at-last-window-line (progn (move-to-window-line -1) (point)))
       (point-at-end (min end point-at-last-window-line)))
    (goto-char start)
    (beginning-of-visual-line)
    (rope-read-advance-one-visual-line)
    (while (and (< (point) point-at-end) 
                (< (save-excursion (end-of-visual-line) (point))
                   (min point-at-end (point-max)))) 
      (let* ((l-beg (save-excursion (beginning-of-visual-line) (point)))
             (l-end (save-excursion (end-of-visual-line) (point)))
             (l-next (save-excursion
                       (goto-char l-beg)
		       (beginning-of-visual-line 2)
		       (point))))
        (push (make-overlay l-beg l-end) rope-read-overlays)
        (overlay-put
         (car rope-read-overlays) 'display (reverse (buffer-substring l-beg l-end)))
        (when (= l-end l-next)
          (overlay-put (car rope-read-overlays) 'after-string "\n"))
        (goto-char l-next)
        (redisplay t)
        (rope-read-advance-one-visual-line)))
    (when ( <= point-at-last-window-line (point))
      (beginning-of-line 0))))

(defun rope-read-point-at-bottom-p ()
  "Return T if point is in one of the last two lines at bottom."
  (let* ((point-before (point)))
    (save-excursion
      (if (< point-before
             (progn
               (move-to-window-line -2)
               (point)))
          nil t))))

(defun rope-read-next-paragraph ()
  "Apply rope read up to the end of the paragraph and move point there.
If point is in one of the two bottom lines recenter the line with
point to the top."
  (interactive)
  (skip-chars-forward " \t\n\r")
  (when (rope-read-point-at-bottom-p)
    (recenter 0)
    (redisplay))
  (let ((beg (point))
        (end (save-excursion
               (let ((point-in-bottom-line
                      (save-excursion
                        (move-to-window-line -1)
                        (point))))
                 (forward-paragraph)
                 (min (point) point-in-bottom-line)))))
    (if rope-read-pure-text
	(rope-read-reol-in-region-pure-text beg end)
      (rope-read-reol-in-region beg end))))


(provide 'rope-read-mode)


;;; rope-read-mode.el ends here
