summaryrefslogtreecommitdiff
path: root/custom
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-10-12 11:47:26 -0500
committerCraig Jennings <c@cjennings.net>2025-10-12 11:47:26 -0500
commit092304d9e0ccc37cc0ddaa9b136457e56a1cac20 (patch)
treeea81999b8442246c978b364dd90e8c752af50db5 /custom
changing repositories
Diffstat (limited to 'custom')
-rw-r--r--custom/c-boxes.el407
-rw-r--r--custom/edit-indirect.el440
-rw-r--r--custom/elpa-mirror.el450
-rw-r--r--custom/eplot.el3495
-rw-r--r--custom/gptel-prompts.el418
-rw-r--r--custom/org-checklist.el153
-rw-r--r--custom/pdf-continuous-scroll-mode-latest.el1046
-rw-r--r--custom/pdf-continuous-scroll-mode.el434
-rw-r--r--custom/profile-dotemacs.el200
-rw-r--r--custom/titlecase-data.el721
-rw-r--r--custom/titlecase.el396
-rw-r--r--custom/utilities/vcf-conversion-helpers.el388
12 files changed, 8548 insertions, 0 deletions
diff --git a/custom/c-boxes.el b/custom/c-boxes.el
new file mode 100644
index 00000000..273b783a
--- /dev/null
+++ b/custom/c-boxes.el
@@ -0,0 +1,407 @@
+;;; Boxed comments for C mode.
+;;; Copyright (C) 1991, 1992, 1993, 1994 Free Software Foundation, Inc.
+;;; Francois Pinard <pinard@iro.umontreal.ca>, April 1991.
+;;;
+;;; I often refill paragraphs inside C comments, while stretching or
+;;; shrinking the surrounding box as needed. This is a real pain to
+;;; do by hand. Here is the code I made to ease my life on this,
+;;; usable from within GNU Emacs. It would not be fair giving all
+;;; sources for a product without also giving the means for nicely
+;;; modifying them.
+;;;
+;;; The function rebox-c-comment adjust comment boxes without
+;;; refilling comment paragraphs, while reindent-c-comment adjust
+;;; comment boxes after refilling. Numeric prefixes are used to add,
+;;; remove, or change the style of the box surrounding the comment.
+;;; Since refilling paragraphs in C mode does make sense only for
+;;; comments, this code redefines the M-q command in C mode. I use
+;;; this hack by putting, in my .emacs file:
+;;;
+;;; (setq c-mode-hook
+;;; '(lambda ()
+;;; (define-key c-mode-map "\M-q" 'reindent-c-comment)))
+;;; (autoload 'rebox-c-comment "c-boxes" nil t)
+;;; (autoload 'reindent-c-comment "c-boxes" nil t)
+;;;
+;;; The cursor should be within a comment before any of these
+;;; commands, or else it should be between two comments, in which case
+;;; the command applies to the next comment. When the command is
+;;; given without prefix, the current comment box type is recognized
+;;; and preserved. Given 0 as a prefix, the comment box disappears
+;;; and the comment stays between a single opening `/*' and a single
+;;; closing `*/'. Given 1 or 2 as a prefix, a single or doubled lined
+;;; comment box is forced. Given 3 as a prefix, a Taarna style box is
+;;; forced, but you do not even want to hear about those. When a
+;;; negative prefix is given, the absolute value is used, but the
+;;; default style is changed. Any other value (like C-u alone) forces
+;;; the default box style.
+;;;
+;;; I observed rounded corners first in some code from Warren Tucker
+;;; <wht@n4hgf.mt-park.ga.us>.
+
+(defvar c-box-default-style 'single "*Preferred style for box comments.")
+(defvar c-mode-taarna-style nil "*Non-nil for Taarna team C-style.")
+
+;;; Set or reset the Taarna team's own way for a C style.
+
+(defun taarna-mode ()
+ (interactive)
+ (if c-mode-taarna-style
+ (progn
+
+ (setq c-mode-taarna-style nil)
+ (setq c-indent-level 2)
+ (setq c-continued-statement-offset 2)
+ (setq c-brace-offset 0)
+ (setq c-argdecl-indent 5)
+ (setq c-label-offset -2)
+ (setq c-tab-always-indent t)
+ (setq c-box-default-style 'single)
+ (message "C mode: GNU style"))
+
+ (setq c-mode-taarna-style t)
+ (setq c-indent-level 4)
+ (setq c-continued-statement-offset 4)
+ (setq c-brace-offset -4)
+ (setq c-argdecl-indent 4)
+ (setq c-label-offset -4)
+ (setq c-tab-always-indent t)
+ (setq c-box-default-style 'taarna)
+ (message "C mode: Taarna style")))
+
+;;; Return the minimum value of the left margin of all lines, or -1 if
+;;; all lines are empty.
+
+(defun buffer-left-margin ()
+ (let ((margin -1))
+ (goto-char (point-min))
+ (while (not (eobp))
+ (skip-chars-forward " \t")
+ (if (not (looking-at "\n"))
+ (setq margin
+ (if (< margin 0)
+ (current-column)
+ (min margin (current-column)))))
+ (forward-line 1))
+ margin))
+
+;;; Return the maximum value of the right margin of all lines. Any
+;;; sentence ending a line has a space guaranteed before the margin.
+
+(defun buffer-right-margin ()
+ (let ((margin 0) period)
+ (goto-char (point-min))
+ (while (not (eobp))
+ (end-of-line)
+ (if (bobp)
+ (setq period 0)
+ (backward-char 1)
+ (setq period (if (looking-at "[.?!]") 1 0))
+ (forward-char 1))
+ (setq margin (max margin (+ (current-column) period)))
+ (forward-char 1))
+ margin))
+
+;;; Add, delete or adjust a C comment box. If FLAG is nil, the
+;;; current boxing style is recognized and preserved. When 0, the box
+;;; is removed; when 1, a single lined box is forced; when 2, a double
+;;; lined box is forced; when 3, a Taarna style box is forced. If
+;;; negative, the absolute value is used, but the default style is
+;;; changed. For any other value (like C-u), the default style is
+;;; forced. If REFILL is not nil, refill the comment paragraphs prior
+;;; to reboxing.
+
+(defun rebox-c-comment-engine (flag refill)
+ (save-restriction
+ (let ((undo-list buffer-undo-list)
+ (marked-point (point-marker))
+ (saved-point (point))
+ box-style left-margin right-margin)
+
+ ;; First, find the limits of the block of comments following or
+ ;; enclosing the cursor, or return an error if the cursor is not
+ ;; within such a block of comments, narrow the buffer, and
+ ;; untabify it.
+
+ ;; - insure the point is into the following comment, if any
+
+ (skip-chars-forward " \t\n")
+ (if (looking-at "/\\*")
+ (forward-char 2))
+
+ (let ((here (point)) start end temp)
+
+ ;; - identify a minimal comment block
+
+ (search-backward "/*")
+ (setq temp (point))
+ (beginning-of-line)
+ (setq start (point))
+ (skip-chars-forward " \t")
+ (if (< (point) temp)
+ (progn
+ (goto-char saved-point)
+ (error "text before comment's start")))
+ (search-forward "*/")
+ (setq temp (point))
+ (end-of-line)
+ (if (looking-at "\n")
+ (forward-char 1))
+ (setq end (point))
+ (skip-chars-backward " \t\n")
+ (if (> (point) temp)
+ (progn
+ (goto-char saved-point)
+ (error "text after comment's end")))
+ (if (< end here)
+ (progn
+ (goto-char saved-point)
+ (error "outside any comment block")))
+
+ ;; - try to extend the comment block backwards
+
+ (goto-char start)
+ (while (and (not (bobp))
+ (progn (previous-line 1)
+ (beginning-of-line)
+ (looking-at "[ \t]*/\\*.*\\*/[ \t]*$")))
+ (setq start (point)))
+
+ ;; - try to extend the comment block forward
+
+ (goto-char end)
+ (while (looking-at "[ \t]*/\\*.*\\*/[ \t]*$")
+ (forward-line 1)
+ (beginning-of-line)
+ (setq end (point)))
+
+ ;; - narrow to the whole block of comments
+
+ (narrow-to-region start end))
+
+ ;; Second, remove all the comment marks, and move all the text
+ ;; rigidly to the left to insure the left margin stays at the
+ ;; same place. At the same time, recognize and save the box
+ ;; style in BOX-STYLE.
+
+ (let ((previous-margin (buffer-left-margin))
+ actual-margin)
+
+ ;; - remove all comment marks
+
+ (goto-char (point-min))
+ (replace-regexp "^\\([ \t]*\\)/\\*" "\\1 ")
+ (goto-char (point-min))
+ (replace-regexp "^\\([ \t]*\\)|" "\\1 ")
+ (goto-char (point-min))
+ (replace-regexp "\\(\\*/\\||\\)[ \t]*" "")
+ (goto-char (point-min))
+ (replace-regexp "\\*/[ \t]*/\\*" " ")
+
+ ;; - remove the first and last dashed lines
+
+ (setq box-style 'plain)
+ (goto-char (point-min))
+ (if (looking-at "^[ \t]*-*[.\+\\]?[ \t]*\n")
+ (progn
+ (setq box-style 'single)
+ (replace-match ""))
+ (if (looking-at "^[ \t]*=*[.\+\\]?[ \t]*\n")
+ (progn
+ (setq box-style 'double)
+ (replace-match ""))))
+ (goto-char (point-max))
+ (previous-line 1)
+ (beginning-of-line)
+ (if (looking-at "^[ \t]*[`\+\\]?*[-=]+[ \t]*\n")
+ (progn
+ (if (eq box-style 'plain)
+ (setq box-style 'taarna))
+ (replace-match "")))
+
+ ;; - remove all spurious whitespace
+
+ (goto-char (point-min))
+ (replace-regexp "[ \t]+$" "")
+ (goto-char (point-min))
+ (if (looking-at "\n+")
+ (replace-match ""))
+ (goto-char (point-max))
+ (skip-chars-backward "\n")
+ (if (looking-at "\n\n+")
+ (replace-match "\n"))
+ (goto-char (point-min))
+ (replace-regexp "\n\n\n+" "\n\n")
+
+ ;; - move the text left is adequate
+
+ (setq actual-margin (buffer-left-margin))
+ (if (not (= previous-margin actual-margin))
+ (indent-rigidly (point-min) (point-max)
+ (- previous-margin actual-margin))))
+
+ ;; Third, select the new box style from the old box style and
+ ;; the argument, choose the margins for this style and refill
+ ;; each paragraph.
+
+ ;; - modify box-style only if flag is defined
+
+ (if flag
+ (setq box-style
+ (cond ((eq flag 0) 'plain)
+ ((eq flag 1) 'single)
+ ((eq flag 2) 'double)
+ ((eq flag 3) 'taarna)
+ ((eq flag '-) (setq c-box-default-style 'plain) 'plain)
+ ((eq flag -1) (setq c-box-default-style 'single) 'single)
+ ((eq flag -2) (setq c-box-default-style 'double) 'double)
+ ((eq flag -3) (setq c-box-default-style 'taarna) 'taarna)
+ (t c-box-default-style))))
+
+ ;; - compute the left margin
+
+ (setq left-margin (buffer-left-margin))
+
+ ;; - temporarily set the fill prefix and column, then refill
+
+ (untabify (point-min) (point-max))
+
+ (if refill
+ (let ((fill-prefix (make-string left-margin ? ))
+ (fill-column (- fill-column
+ (if (memq box-style '(single double)) 4 6))))
+ (fill-region (point-min) (point-max))))
+
+ ;; - compute the right margin after refill
+
+ (setq right-margin (buffer-right-margin))
+
+ ;; Fourth, put the narrowed buffer back into a comment box,
+ ;; according to the value of box-style. Values may be:
+ ;; plain: insert between a single pair of comment delimiters
+ ;; single: complete box, overline and underline with dashes
+ ;; double: complete box, overline and underline with equal signs
+ ;; taarna: comment delimiters on each line, underline with dashes
+
+ ;; - move the right margin to account for left inserts
+
+ (setq right-margin (+ right-margin
+ (if (memq box-style '(single double))
+ 2
+ 3)))
+
+ ;; - construct the box comment, from top to bottom
+
+ (goto-char (point-min))
+ (cond ((eq box-style 'plain)
+
+ ;; - construct a plain style comment
+
+ (skip-chars-forward " " (+ (point) left-margin))
+ (insert (make-string (- left-margin (current-column)) ? )
+ "/* ")
+ (end-of-line)
+ (forward-char 1)
+ (while (not (eobp))
+ (skip-chars-forward " " (+ (point) left-margin))
+ (insert (make-string (- left-margin (current-column)) ? )
+ " ")
+ (end-of-line)
+ (forward-char 1))
+ (backward-char 1)
+ (insert " */"))
+ ((eq box-style 'single)
+
+ ;; - construct a single line style comment
+
+ (indent-to left-margin)
+ (insert "/*")
+ (insert (make-string (- right-margin (current-column)) ?-)
+ "-.\n")
+ (while (not (eobp))
+ (skip-chars-forward " " (+ (point) left-margin))
+ (insert (make-string (- left-margin (current-column)) ? )
+ "| ")
+ (end-of-line)
+ (indent-to right-margin)
+ (insert " |")
+ (forward-char 1))
+ (indent-to left-margin)
+ (insert "`")
+ (insert (make-string (- right-margin (current-column)) ?-)
+ "*/\n"))
+ ((eq box-style 'double)
+
+ ;; - construct a double line style comment
+
+ (indent-to left-margin)
+ (insert "/*")
+ (insert (make-string (- right-margin (current-column)) ?=)
+ "=\\\n")
+ (while (not (eobp))
+ (skip-chars-forward " " (+ (point) left-margin))
+ (insert (make-string (- left-margin (current-column)) ? )
+ "| ")
+ (end-of-line)
+ (indent-to right-margin)
+ (insert " |")
+ (forward-char 1))
+ (indent-to left-margin)
+ (insert "\\")
+ (insert (make-string (- right-margin (current-column)) ?=)
+ "*/\n"))
+ ((eq box-style 'taarna)
+
+ ;; - construct a Taarna style comment
+
+ (while (not (eobp))
+ (skip-chars-forward " " (+ (point) left-margin))
+ (insert (make-string (- left-margin (current-column)) ? )
+ "/* ")
+ (end-of-line)
+ (indent-to right-margin)
+ (insert " */")
+ (forward-char 1))
+ (indent-to left-margin)
+ (insert "/* ")
+ (insert (make-string (- right-margin (current-column)) ?-)
+ " */\n"))
+ (t (error "unknown box style")))
+
+ ;; Fifth, retabify, restore the point position, then cleanup the
+ ;; undo list of any boundary since we started.
+
+ ;; - retabify before left margin only (adapted from tabify.el)
+
+ (goto-char (point-min))
+ (while (re-search-forward "^[ \t][ \t][ \t]*" nil t)
+ (let ((column (current-column))
+ (indent-tabs-mode t))
+ (delete-region (match-beginning 0) (point))
+ (indent-to column)))
+
+ ;; - restore the point position
+
+ (goto-char (marker-position marked-point))
+
+ ;; - remove all intermediate boundaries from the undo list
+
+ (if (not (eq buffer-undo-list undo-list))
+ (let ((cursor buffer-undo-list))
+ (while (not (eq (cdr cursor) undo-list))
+ (if (car (cdr cursor))
+ (setq cursor (cdr cursor))
+ (rplacd cursor (cdr (cdr cursor))))))))))
+
+;;; Rebox a C comment without refilling it.
+
+(defun rebox-c-comment (flag)
+ (interactive "P")
+ (rebox-c-comment-engine flag nil))
+
+;;; Rebox a C comment after refilling.
+
+(defun reindent-c-comment (flag)
+ (interactive "P")
+ (rebox-c-comment-engine flag t))
+
diff --git a/custom/edit-indirect.el b/custom/edit-indirect.el
new file mode 100644
index 00000000..307f9695
--- /dev/null
+++ b/custom/edit-indirect.el
@@ -0,0 +1,440 @@
+;;; edit-indirect.el --- Edit regions in separate buffers -*- lexical-binding: t -*-
+
+;; Author: Fanael Linithien <fanael4@gmail.com>
+;; URL: https://github.com/Fanael/edit-indirect
+;; Version: 0.1.10
+;; Package-Requires: ((emacs "24.3"))
+
+;; This file is NOT part of GNU Emacs.
+
+;; SPDX-License-Identifier: BSD-2-clause
+;;
+;; Copyright (c) 2014-2022, Fanael Linithien
+;; All rights reserved.
+;;
+;; Redistribution and use in source and binary forms, with or without
+;; modification, are permitted provided that the following conditions are
+;; met:
+;;
+;; * Redistributions of source code must retain the above copyright
+;; notice, this list of conditions and the following disclaimer.
+;; * Redistributions in binary form must reproduce the above copyright
+;; notice, this list of conditions and the following disclaimer in the
+;; documentation and/or other materials provided with the distribution.
+;;
+;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+;; IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+;; TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+;; PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+;; OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+;; EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+;; PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+;; PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+;; LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+;;; Commentary:
+
+;; Edit regions in separate buffers, like `org-edit-src-code' but for arbitrary
+;; regions.
+;;
+;; See the docstring of `edit-indirect-region' for details.
+
+;;; Code:
+(defgroup edit-indirect nil
+ "Editing regions in separate buffers."
+ :group 'editing)
+
+(defcustom edit-indirect-guess-mode-function #'edit-indirect-default-guess-mode
+ "The function used to guess the major mode of an edit-indirect buffer.
+It's called with the edit-indirect buffer as the current buffer.
+It's called with three arguments, the parent buffer, the beginning
+and the end of the parent buffer region being editing.
+
+Note that the buffer-local value from the parent buffer is used."
+ :type 'function
+ :group 'edit-indirect)
+
+(defcustom edit-indirect-after-creation-hook nil
+ "Functions called after the edit-indirect buffer is created.
+The functions are called with the edit-indirect buffer as the
+current buffer.
+
+Note that the buffer-local value from the parent buffer is used."
+ :type 'hook
+ :group 'edit-indirect)
+
+(defcustom edit-indirect-before-commit-hook nil
+ "Functions called before the edit-indirect buffer is committed.
+The functions are called with the edit-indirect buffer as the
+current buffer.
+
+Note that the buffer-local value from the edit-indirect buffer is
+used."
+ :type 'hook
+ :group 'edit-indirect)
+
+(defcustom edit-indirect-before-commit-functions nil
+ "Functions called before an edit-indirect buffer is committed.
+The functions are called with the parent buffer as the current
+buffer.
+Each function is called with two arguments, the beginning and the
+end of the region to be changed."
+ :type 'hook
+ :group 'edit-indirect)
+
+(defcustom edit-indirect-after-commit-functions nil
+ "Functions called after an edit-indirect buffer has been committed.
+The functions are called with the parent buffer as the current
+buffer.
+Each function is called with two arguments, the beginning and the
+end of the changed region."
+ :type 'hook
+ :group 'edit-indirect)
+
+(defgroup edit-indirect-faces nil
+ "Faces used in `edit-indirect'."
+ :group 'edit-indirect
+ :group 'faces
+ :prefix "edit-indirect")
+
+(defface edit-indirect-edited-region
+ '((t :inherit secondary-selection))
+ "Face used to highlight an indirectly edited region."
+ :group 'edit-indirect-faces)
+
+;; Emacs <= 24.3 has no `define-error'.
+(let* ((user-error-conditions (get 'user-error 'error-conditions))
+ (define-user-error (lambda (name message)
+ (put name 'error-conditions
+ (cons name user-error-conditions))
+ (put name 'error-message message))))
+ (funcall define-user-error 'edit-indirect-overlapping
+ "Indirectly edited regions cannot overlap")
+ (funcall define-user-error 'edit-indirect-read-only
+ "Text is read-only, modify the edit-indirect buffer instead")
+ (funcall define-user-error 'edit-indirect-not-indirect
+ "This is not an edit-indirect buffer"))
+
+(defvar edit-indirect--overlay)
+(defvar edit-indirect--should-quit-window nil)
+(put 'edit-indirect--should-quit-window 'permanent-local t)
+
+;;;###autoload
+(defun edit-indirect-region (beg end &optional display-buffer)
+ "Edit the region BEG..END in a separate buffer.
+The region is copied, without text properties, to a separate
+buffer, called edit-indirect buffer, and
+`edit-indirect-guess-mode-function' is called to set the major
+mode.
+When done, exit with `edit-indirect-commit', which will remove the
+original region and replace it with the edited version; or with
+`edit-indirect-abort', which will drop the modifications.
+
+This differs from `clone-indirect-buffer' with narrowing in that
+the text properties are not shared, so the parent buffer major mode
+and the edit-indirect buffer major mode will not be able to tread
+on each other's toes by setting up potentially conflicting text
+properties, which happens surprisingly often when the font-lock
+mode is used.
+
+Edit-indirect buffers use the `edit-indirect-mode-map' keymap.
+Regions with active edit-indirect buffers use the
+`edit-indirect-overlay-map' keymap.
+
+If there's already an edit-indirect buffer for BEG..END, use that.
+If there's already an edit-indirect buffer active overlapping any
+portion of BEG..END, an `edit-indirect-overlapping' error is
+signaled.
+
+When DISPLAY-BUFFER is non-nil or when called interactively,
+display the edit-indirect buffer in some window and select it.
+
+In any case, return the edit-indirect buffer."
+ (interactive
+ (if (or (use-region-p) (not transient-mark-mode))
+ (prog1 (list (region-beginning) (region-end) t)
+ (deactivate-mark))
+ (user-error "No region")))
+ (let ((buffer (edit-indirect--get-edit-indirect-buffer beg end)))
+ (when display-buffer
+ (edit-indirect--display-buffer buffer))
+ buffer))
+
+(defvar edit-indirect-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map [remap save-buffer] #'edit-indirect-save)
+ (define-key map (kbd "C-c '") #'edit-indirect-commit)
+ (define-key map (kbd "C-c C-c") #'edit-indirect-commit)
+ (define-key map (kbd "C-c C-k") #'edit-indirect-abort)
+ map)
+ "Keymap for edit-indirect buffers.
+
+\\{edit-indirect-mode-map}")
+
+(defvar edit-indirect-overlay-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "RET") #'edit-indirect-display-active-buffer)
+ map)
+ "Keymap for regions with active edit-indirect buffers.
+
+\\{edit-indirect-overlay-map}")
+
+(defun edit-indirect-commit ()
+ "Commit the modifications done in an edit-indirect buffer.
+That is, replace the original region in the parent buffer with the
+contents of the edit-indirect buffer.
+The edit-indirect buffer is then killed.
+
+Can be called only when the current buffer is an edit-indirect
+buffer."
+ (interactive)
+ (edit-indirect--barf-if-not-indirect)
+ (edit-indirect--commit)
+ (edit-indirect--clean-up))
+
+(defun edit-indirect-save ()
+ "Save the modifications done in an edit-indirect buffer.
+That is, replace the original region in the parent buffer with the
+contents of the edit-indirect buffer.
+
+Can be called only when the current buffer is an edit-indirect
+buffer."
+ (interactive)
+ (edit-indirect--barf-if-not-indirect)
+ (edit-indirect--commit))
+
+(defun edit-indirect-abort ()
+ "Abort indirect editing in the current buffer and kill the buffer.
+
+Can be called only when the current buffer is an edit-indirect
+buffer."
+ (interactive)
+ (edit-indirect--barf-if-not-indirect)
+ (edit-indirect--abort))
+
+(defun edit-indirect-buffer-indirect-p (&optional buffer)
+ "Non-nil iff the BUFFER is an edit-indirect buffer.
+BUFFER defaults to the current buffer."
+ (save-current-buffer
+ (when buffer
+ (set-buffer buffer))
+ ;; (not (null)) so we don't leak the overlay to the outside world.
+ (not (null edit-indirect--overlay))))
+
+(defun edit-indirect-default-guess-mode (_parent-buffer _beg _end)
+ "Guess the major mode for an edit-indirect buffer.
+It's done by calling `normal-mode'."
+ (normal-mode))
+
+(defun edit-indirect-display-active-buffer ()
+ "Display the active edit-indirect buffer of the region the point is in."
+ (interactive)
+ (let ((overlay
+ (let ((p (point)))
+ (edit-indirect--search-for-edit-indirect p (1+ p)))))
+ (unless overlay
+ (signal 'edit-indirect-not-indirect '()))
+ (edit-indirect--display-buffer (overlay-get overlay 'edit-indirect-buffer))))
+
+(defvar edit-indirect--overlay nil
+ "The overlay spanning the region of the parent buffer being edited.
+
+It's also used as the variable determining if we're in an
+edit-indirect buffer at all.")
+(make-variable-buffer-local 'edit-indirect--overlay)
+(put 'edit-indirect--overlay 'permanent-local t)
+
+;; Normally this would use `define-minor-mode', but that makes the mode function
+;; interactive, which we don't want, because it's just an implementation detail.
+(defun edit-indirect--mode (overlay)
+ "Turn the `edit-indirect--mode' \"minor mode\" on.
+OVERLAY is the value to set `edit-indirect--overlay' to."
+ (setq edit-indirect--overlay overlay)
+ (add-hook 'kill-buffer-hook #'edit-indirect--abort-on-kill-buffer nil t))
+(with-no-warnings
+ (add-minor-mode
+ 'edit-indirect--overlay " indirect" edit-indirect-mode-map nil #'ignore))
+
+(defun edit-indirect--display-buffer (buffer)
+ "Display the given BUFFER in some window and select it."
+ (with-current-buffer buffer
+ (setq-local edit-indirect--should-quit-window t))
+ (select-window (display-buffer buffer))
+ nil)
+
+(defun edit-indirect--get-edit-indirect-buffer (beg end)
+ "Return an edit-indirect buffer for the region BEG..END.
+If there's already an edit-indirect buffer active overlapping any
+portion of BEG..END, an `edit-indirect-overlapping' error is
+signaled."
+ (let ((old-overlay (edit-indirect--search-for-edit-indirect beg end)))
+ (cond
+ ((null old-overlay)
+ (let ((overlay (edit-indirect--create-overlay beg end)))
+ (edit-indirect--create-indirect-buffer beg end overlay)))
+ ((and (= beg (overlay-start old-overlay))
+ (= end (overlay-end old-overlay)))
+ (overlay-get old-overlay 'edit-indirect-buffer))
+ (t
+ (signal 'edit-indirect-overlapping '())))))
+
+(defun edit-indirect--search-for-edit-indirect (beg end)
+ "Return an existing edit-indirect overlay for some region inside BEG..END.
+If there's no indirectly edited region inside BEG..END, return
+nil."
+ (catch 'done
+ (dolist (overlay (overlays-in beg end))
+ (when (overlay-get overlay 'edit-indirect-buffer)
+ (throw 'done overlay)))
+ nil))
+
+(defmacro edit-indirect--buffer-local-value (buffer variable)
+ "Get the BUFFER local value of VARIABLE.
+VARIABLE shall be a symbol."
+ (unless (symbolp variable)
+ (signal 'wrong-type-argument (list #'symbolp variable)))
+ ;; `with-current-buffer' is used instead of `buffer-local-value' because
+ ;; the latter doesn't give warnings about free variables when
+ ;; byte-compiled.
+ `(with-current-buffer ,buffer ,variable))
+
+(defun edit-indirect--create-indirect-buffer (beg end overlay)
+ "Create an edit-indirect buffer and return it.
+
+BEG..END is the parent buffer region to insert.
+OVERLAY is the overlay, see `edit-indirect--overlay'."
+ (add-hook 'after-change-major-mode-hook #'edit-indirect--rebind-save-hooks)
+ (let ((buffer (generate-new-buffer (format "*edit-indirect %s*" (buffer-name))))
+ (parent-buffer (current-buffer)))
+ (overlay-put overlay 'edit-indirect-buffer buffer)
+ (with-current-buffer buffer
+ (insert-buffer-substring-no-properties parent-buffer beg end)
+ (set-buffer-modified-p nil)
+ (edit-indirect--mode overlay)
+ ;; Use the buffer-local values from the parent buffer. Don't retrieve the
+ ;; values before actual uses in case these variables are changed by some
+ ;; of the many possible hooks.
+ (funcall (edit-indirect--buffer-local-value
+ parent-buffer edit-indirect-guess-mode-function)
+ parent-buffer beg end)
+ (if (local-variable-p 'edit-indirect-after-creation-hook parent-buffer)
+ ;; Copy the parent buffer hook to the indirect buffer instead of
+ ;; let-binding it to avoid running it twice.
+ (setq-local edit-indirect-after-creation-hook
+ (edit-indirect--buffer-local-value
+ parent-buffer edit-indirect-after-creation-hook))
+ ;; No need to do copy anything if the parent buffer has no local value,
+ ;; the global value will be used instead. Just kill the local value in
+ ;; the indirect buffer in case a prior hook set it, because we're not
+ ;; supposed to use it.
+ (kill-local-variable 'edit-indirect-after-creation-hook))
+ (run-hooks 'edit-indirect-after-creation-hook))
+ buffer))
+
+(defun edit-indirect--create-overlay (beg end)
+ "Create the edit-indirect overlay and return it.
+
+BEG and END specify the region the overlay should encompass."
+ (let ((overlay (make-overlay beg end)))
+ (overlay-put overlay 'face 'edit-indirect-edited-region)
+ (overlay-put overlay 'modification-hooks '(edit-indirect--barf-read-only))
+ (overlay-put overlay 'insert-in-front-hooks '(edit-indirect--barf-read-only))
+ (overlay-put overlay 'keymap edit-indirect-overlay-map)
+ overlay))
+
+(defvar edit-indirect--inhibit-read-only nil
+ "Non-nil means disregard read-only status of indirectly-edited region.")
+
+(defun edit-indirect--barf-read-only (_ov _after _beg _end &optional _len)
+ "Signal an error because the text is read-only.
+No error is signaled if `inhibit-read-only' or
+`edit-indirect--inhibit-read-only' is non-nil."
+ (unless (or inhibit-read-only edit-indirect--inhibit-read-only)
+ (signal 'edit-indirect-read-only '())))
+
+(defun edit-indirect--commit ()
+ "Commit the modifications done in an edit-indirect buffer."
+ (run-hooks 'edit-indirect-before-commit-hook)
+ (let ((beg (overlay-start edit-indirect--overlay))
+ (end (overlay-end edit-indirect--overlay))
+ (buffer (current-buffer))
+ (edit-indirect--inhibit-read-only t))
+ (with-current-buffer (overlay-buffer edit-indirect--overlay)
+ (save-excursion
+ (let ((beg-marker (copy-marker beg))
+ (end-marker (copy-marker end)))
+ (edit-indirect--run-hook-with-positions
+ 'edit-indirect-before-commit-functions beg-marker end-marker)
+ (save-match-data
+ (set-match-data (list beg-marker end-marker))
+ (let ((new-data
+ (with-current-buffer buffer
+ (buffer-substring-no-properties 1 (1+ (buffer-size))))))
+ (unless (string= new-data (match-string 0))
+ (replace-match new-data t t))))
+ (edit-indirect--run-hook-with-positions
+ 'edit-indirect-after-commit-functions beg-marker (point))
+ (set-marker beg-marker nil)
+ (set-marker end-marker nil))))
+ (set-buffer-modified-p nil)))
+
+(defun edit-indirect--run-hook-with-positions (hook beg end)
+ "Run HOOK with the specified positions BEG and END.
+HOOK should be a symbol, a hook variable.
+The functions are passed integer positions.
+If a function changes the buffer contents, the next function will be
+called with updated positions."
+ (let ((beg-marker (unless (markerp beg) (copy-marker beg)))
+ (end-marker (unless (markerp end) (copy-marker end))))
+ (run-hook-wrapped hook
+ (lambda (f beg end)
+ (funcall f (marker-position beg) (marker-position end))
+ nil)
+ (or beg-marker beg) (or end-marker end))
+ (when beg-marker (set-marker beg-marker nil))
+ (when end-marker (set-marker end-marker nil))))
+
+(defun edit-indirect--abort ()
+ "Abort indirect edit."
+ (edit-indirect--clean-up))
+
+(defun edit-indirect--clean-up ()
+ "Clean up an edit-indirect buffer."
+ (delete-overlay edit-indirect--overlay)
+ ;; Kill the overlay reference so that `edit-indirect--abort-on-kill-buffer'
+ ;; won't try to call us again.
+ (setq edit-indirect--overlay nil)
+ ;; If we created a window, get rid of it. Kill the buffer we created.
+ (if edit-indirect--should-quit-window
+ (quit-window t)
+ (kill-buffer)))
+
+(defun edit-indirect--rebind-save-hooks ()
+ "Bind our `save-buffer' hooks in the current buffer.
+Does nothing if the current buffer is not an edit-indirect buffer."
+ (when (edit-indirect-buffer-indirect-p)
+ (setq buffer-offer-save t)
+ (add-hook 'write-contents-functions #'edit-indirect--commit-on-save nil t)))
+
+(defun edit-indirect--commit-on-save ()
+ "Commit the indirect edit.
+Should only be called from `write-contents-functions'."
+ (edit-indirect--commit)
+ t)
+
+(defun edit-indirect--abort-on-kill-buffer ()
+ "Abort indirect edit.
+Should be called only from `kill-buffer-hook'."
+ (when edit-indirect--overlay
+ (edit-indirect--abort)))
+
+(defun edit-indirect--barf-if-not-indirect ()
+ "Signal an error if the current buffer is not an edit-indirect buffer.
+The error signaled is `edit-indirect-not-indirect'."
+ (unless edit-indirect--overlay
+ (signal 'edit-indirect-not-indirect '())))
+
+(provide 'edit-indirect)
+;;; edit-indirect.el ends here
diff --git a/custom/elpa-mirror.el b/custom/elpa-mirror.el
new file mode 100644
index 00000000..777293e2
--- /dev/null
+++ b/custom/elpa-mirror.el
@@ -0,0 +1,450 @@
+;;; elpa-mirror.el --- Create local package repository from installed packages
+
+;; Copyright (C) 2014-2020 Chen Bin
+
+;; Author: Chen Bin <chenbin.sh@gmail.com>
+;; URL: http://github.com/redguardtoo/elpa-mirror
+;; Package-Requires: ((emacs "25.1"))
+;; Version: 2.2.2
+;; Keywords: tools
+;;
+;; This file is not part of GNU Emacs.
+
+;;; License:
+
+;; This file is part of elpa-mirror
+;;
+;; elpa-mirror 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.
+;;
+;; elpa-mirror 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This program will create a local package repository by from all
+;; installed packages.
+;;
+;; Please note compile Emacs Lisp file (*.elc) from one version of Emacs
+;; might not work with another version of Emacs. So you need this program
+;; to compile package from local repository.
+;;
+;; This is the ONLY way to have 100% portable Emacs setup.
+;;
+;; Usage in Emacs,
+;; Run `elpamr-create-mirror-for-installed'.
+;;
+;; CLI program tar is required. It's bundled with Windows10/Linux/macOS.
+;;
+;; Usage in Shell,
+;; Emacs --batch -l ~/.emacs.d/init.el
+;; -l ~/any-directory-you-prefer/elpa-mirror.el \
+;; --eval='(setq elpamr-default-output-directory "~/myelpa")' \
+;; --eval='(elpamr-create-mirror-for-installed)
+;;
+;; Use the repository created by elpa-mirror,
+;; - Add `(setq package-archives '(("myelpa" . "~/myelpa/")))` into ~/.emacs
+;; - Restart Emacs
+;;
+;; Tips,
+;; - `elpamr-exclude-packages' excludes packages
+;;
+;; - `elpamr-tar-command-exclude-patterns' excludes file and directories in
+;; package directory.
+;;
+;; - `elpamr-exclude-patterns-filter-function' lets users define a function to
+;; exclude files and directories per package.
+;;
+;; Below setup adds directory "bin/" into package "vagrant-tramp".
+;;
+;; (setq elpamr-exclude-patterns-filter-function
+;; (lambda (package-dir)
+;; (let ((patterns elpamr-tar-command-exclude-patterns))
+;; (when (string-match "vagrant-tramp" package-dir)
+;; (setq patterns (remove "*/bin" patterns)))
+;; patterns)))
+
+;; - You can also setup repositories on Dropbox and Github.
+;; See https://github.com/redguardtoo/elpa-mirror for details.
+;;
+
+;;; Code:
+(require 'package)
+
+(defcustom elpamr-default-output-directory nil
+ "The output directory use by `elpamr-create-mirror-for-installed'."
+ :type '(choice directory (const :tags "None" nil))
+ :group 'elpa-mirror)
+
+(defcustom elpamr-exclude-packages nil
+ "Names of excluded packages."
+ :type '(repeat string)
+ :group 'elpa-mirror)
+
+(defcustom elpamr-tar-command-exclude-patterns
+ '("*.elc"
+ "*~"
+ "*autoloads.el"
+ "*.so"
+ "*.dylib"
+ "*.dll"
+ "*/bin"
+ "*/__pycache__")
+ "Exclude patterns passed tar's `--exclude' option.
+
+The patterns use shell glob syntax, not regexp syntax:
+
+* `*' matches any string, including `/'.
+* `?' matches a single character.
+* `[abc]' or `[a-z]' is a character class.
+* `[^a-z]' or `[!a-z]' is a negated character class.
+* `^' and `$' have a special meaning in BSD tar only.
+* Special characters are quoted with `\\'.
+
+The patterns are anchored, meaning that they always start
+matching at the start of the path. This is done by passing the
+`--anchored' option when running with GNU tar, or pre-pending `^'
+to every pattern when running with BSD tar.
+
+Examples:
+
+* Exclude files/directories that end with `.elc': `*.elc'.
+* Exclude files/directories named `__pycache__': `*/__pycache__'.
+* Exclude `bin' inside the `company' package: `company-*/bin'.
+
+Note that a slash at the start or the end of a pattern will cause
+it to match nothing."
+ :type '(repeat string)
+ :group 'elpa-mirror)
+
+(defcustom elpamr-exclude-patterns-filter-function nil
+ "Filter `elpamr-tar-command-exclude-patterns' before using it per package.
+A function with one parameter which is the package directory.
+It returns the result to replace `elpamr-tar-command-exclude-patterns'."
+ :group 'elpa-mirror
+ :type 'hook)
+
+(defcustom elpamr-tar-executable
+ "tar"
+ "The tar executable used by elpa-mirror.
+It can be BSD tar, but GNU tar is preferred."
+ :type 'string
+ :group 'elpa-mirror)
+
+(defcustom elpamr-finished-hook nil
+ "Hook run when command `elpamr-create-mirror-for-installed' run finished.
+The hook function have one argument: output-directory."
+ :group 'elpa-mirror
+ :type 'hook)
+
+(defcustom elpamr-enable-log nil
+ "Enable log."
+ :type 'boolean
+ :group 'elpa-mirror)
+
+(defvar elpamr--log-buffer "*elpa-mirror log*"
+ "Destination buffer for log messages and command output.")
+
+(defun elpamr--log (format-string &rest args)
+ "Format ARGS with FORMAT-STRING, add the result to the log, and return it.
+The log line will be pre-pended with an asterisk to distinguish it
+from program output."
+ (when elpamr-enable-log
+ (let ((line (apply #'format format-string args)))
+ (with-current-buffer (get-buffer-create elpamr--log-buffer)
+ (insert "* " line "\n"))
+ line)))
+
+(defun elpamr--log-message (format-string &rest args)
+ "Format ARGS with FORMAT-STRING, add the result to the log and display it."
+ (when elpamr-enable-log
+ (apply #'elpamr--log format-string args)
+ (apply #'message format-string args)))
+
+(defun elpamr--log-error (format-string &rest args)
+ "Format ARGS with FORMAT-STRING, add the result to the log and signal an error."
+ (when elpamr-enable-log
+(apply #'elpamr--log format-string args)
+ (apply #'error format-string args)))
+
+(defun elpamr--package-desc (item)
+ "Extract package information from ITEM."
+ (cadr item))
+
+(defun elpamr--is-bsd-tar ()
+ "Are we using BSD tar instead of GNU tar?"
+ (let* ((output (mapconcat #'identity (process-lines elpamr-tar-executable "--version") " "))
+ ;; @see https://github.com/redguardtoo/elpa-mirror/issues/37
+ ;; extra error message insert extra whitespace before "bsdtar"
+ (result (and output (string-match-p "\\(^[ \t]*\\|[ \t]\\)bsdtar" output))))
+ (elpamr--log "Detected tar variant: %s" (if result "BSD" "GNU"))
+ result))
+
+(defun elpamr--create-one-item-for-archive-contents (pkg)
+ "Access PKG extracted from `package-alist' directly."
+ (unless (member (symbol-name (car pkg)) elpamr-exclude-packages)
+ pkg))
+
+(defun elpamr--fullpath (parent file)
+ "Full path of 'PARENT/FILE'."
+ (let* ((result (file-truename (concat (file-name-as-directory parent) file))))
+ (elpamr--log "Converted to full path: %S %S -> %S" parent file result)
+ result))
+
+(defun elpamr--clean-package-description (description)
+ "Clean DESCRIPTION."
+ (replace-regexp-in-string "-\*-.*-\*-" ""
+ (replace-regexp-in-string "\"" "" description t)
+ t))
+
+(defun elpamr--get-dependency (item)
+ "Get ITEM dependency."
+ (package-desc-reqs (elpamr--package-desc item)))
+
+(defun elpamr--get-version (item)
+ "Get ITEM version."
+ (package-desc-version (elpamr--package-desc item)))
+
+(defun elpamr--get-summary (item)
+ "Get ITEM description."
+ (package-desc-summary (elpamr--package-desc item)))
+
+(defun elpamr--one-item-for-archive-contents (final-pkg)
+ "Format FINAL-PKG information into a string for archive-contents."
+ (format " (%s . [%S %S \"%s\" tar])\n"
+ (car final-pkg)
+ (elpamr--get-version final-pkg)
+ (elpamr--get-dependency final-pkg)
+ (elpamr--clean-package-description (elpamr--get-summary final-pkg))))
+
+(defun elpamr--call-process-check (arguments)
+ "Call run tar program with the ARGUMENTS.
+Log and signal an error if it exits with a non-zero status."
+ (let ((exit-status (apply #'call-process
+ elpamr-tar-executable
+ nil
+ (and elpamr-enable-log elpamr--log-buffer)
+ nil
+ arguments)))
+ (cond
+ ((not (= exit-status 0))
+ (elpamr--log-error
+ "Program %s exited with non-zero status %s, see the %s buffer for details"
+ elpamr-tar-executable exit-status elpamr--log-buffer)
+ )
+ (t
+ exit-status))))
+
+(defun elpamr--run-tar (working-dir out-file dir-to-archive is-bsd-tar)
+ "Run tar in order to archive DIR-TO-ARCHIVE into OUT-FILE.
+Paths are relative to WORKING-DIR.
+IS-BSD-TAR should be non-nil if this function should use a
+command compatible with BSD tar instead of GNU tar."
+ ;; We could detect BSD tar inside this function easily, but detecting it once
+ ;; and then passing it as an argument improves performance.
+ (let* ((exclude-opts (mapcar (lambda (s)
+ (concat "--exclude=" (if is-bsd-tar "^" "") s))
+ (if elpamr-exclude-patterns-filter-function
+ (funcall elpamr-exclude-patterns-filter-function
+ dir-to-archive)
+ elpamr-tar-command-exclude-patterns)))
+ ;; set pwd of process
+ (default-directory working-dir)
+ ;; create tar using GNU tar
+ (tar-args
+ `("cf" ,out-file
+ ,@(unless is-bsd-tar '("--anchored"))
+ ,@exclude-opts
+ ;; tar 1.14 NEWS,
+ ;; @see https://git.savannah.gnu.org/cgit/tar.git/plain/NEWS?id=release_1_14
+ ;; * New option --format allows to select the output archive format
+ ;; * The default output format can be selected at configuration time
+ ;; by presetting the environment variable DEFAULT_ARCHIVE_FORMAT.
+ ;; Allowed values are GNU, V7, OLDGNU and POSIX.
+ ,@(unless is-bsd-tar '("--format=gnu"))
+ ;; Improve reproducibility by not storing unnecessary metadata.
+ ;; These options are enough for archives in the GNU format, but if
+ ;; we ever switch to PAX, we'll need to add more (see
+ ;; <http://h2.jaguarpaw.co.uk/posts/reproducible-tar/> and
+ ;; <https://www.gnu.org/software/tar/manual/html_node/PAX-keywords.html>).
+ ,@(unless is-bsd-tar
+ ;; @see https://github.com/redguardtoo/elpa-mirror/issues/41
+ '("--owner=root"
+ "--group=root"
+ "--mtime=1970-01-01 00:00:00 UTC"))
+ "--" ,dir-to-archive))
+ ;; Don't archive macOS' file properties (see
+ ;; <https://superuser.com/q/259703>).
+ (process-environment (if (eq system-type 'darwin)
+ (cons "COPYFILE_DISABLE=" process-environment)
+ process-environment)))
+ (elpamr--log "Running tar: %S %S" elpamr-tar-executable tar-args)
+ (elpamr--call-process-check tar-args)))
+
+;;;###autoload
+(defun elpamr-version ()
+ "Current version."
+ (interactive)
+ (message "2.2.2"))
+
+(defun elpamr--win-executable-find (exe)
+ "Find EXE on windows."
+ (let* ((drivers '("c" "d" "e" "f"))
+ (i 0)
+ j
+ (dirs '(":\\\\cygwin64\\\\bin\\\\"
+ ":\\\\msys64\\\\usr\\\\bin\\\\"))
+ rlt)
+ (while (and (not rlt)
+ (< i (length dirs)))
+ (setq j 0)
+ (while (and (not rlt)
+ (< j (length drivers)))
+ (setq rlt (executable-find (concat (nth j drivers) (nth i dirs) exe)))
+ (setq j (1+ j)))
+ (setq i (1+ i)))
+ (unless rlt
+ ;; nothing found, fall back to exe
+ (setq rlt exe))
+ rlt))
+
+(defun elpamr-double-check-executable ()
+ "Make sure `elpamr-tar-executable' is executable."
+ (when (eq system-type 'windows-nt)
+
+ (cond
+ (elpamr-tar-executable
+ (unless (executable-find elpamr-tar-executable)
+ (setq elpamr-tar-executable (elpamr--win-executable-find elpamr-tar-executable))))
+
+ ((executable-find "tar")
+ (setq elpamr-tar-executable "tar")))))
+
+(defun elpamr-delete-directory (directory)
+ "Delete DIRECTORY."
+ (ignore-errors
+ (delete-directory directory t)))
+
+;;;###autoload
+(defun elpamr-create-mirror-for-installed (&optional output-directory recreate-directory)
+ "Export installed packages into a new directory.
+Create the html files for the mirror site.
+
+The first valid directory found from the below list
+will be used as mirror package's output directory:
+1. Argument: OUTPUT-DIRECTORY
+2. Variable: `elpamr-default-output-directory'
+3. Ask user to provide.
+
+When RECREATE-DIRECTORY is non-nil, OUTPUT-DIRECTORY
+will be deleted and recreated."
+ (interactive)
+
+ ;; find tar program on Windows if GNU tar from Cygwin/MYSYS2 is installed
+ ;; and current `elpamr-tar-executable' is NOT executable.
+ (elpamr-double-check-executable)
+
+ (let (final-pkg-list)
+
+ ;; Erase the log (in case of multiple consecutive calls to this function).
+ (when elpamr-enable-log
+ (with-current-buffer (get-buffer-create elpamr--log-buffer)
+ (erase-buffer)))
+
+ ;; Since Emacs 27, `package-initialize' is optional.
+ ;; but we still need it to initialize `package-alist'.
+ (unless package-alist (package-initialize))
+
+ ;; Quote from manual about package-alist:
+ ;; Alist of all packages available for activation.
+ ;; Each element has the form (PKG . DESCS), where PKG is a package
+ ;; name (a symbol) and DESCS is a non-empty list of `package-desc' structure,
+ ;; sorted by decreasing versions.
+ ;; Sorted for reproducibility.
+ (setq final-pkg-list
+ (let ((sorted-package-alist
+ (sort (copy-sequence package-alist)
+ (lambda (a b)
+ (string< (symbol-name (car a))
+ (symbol-name (car b)))))))
+ (delq nil (mapcar #'elpamr--create-one-item-for-archive-contents
+ sorted-package-alist))))
+
+ ;; set output directory
+ (setq output-directory
+ (cond ((and output-directory
+ (stringp output-directory))
+ (file-name-as-directory output-directory))
+ ((and elpamr-default-output-directory
+ (stringp elpamr-default-output-directory))
+ (file-name-as-directory elpamr-default-output-directory))
+ (t (read-directory-name "Output directory: "))))
+
+ ;; Delete output directory if we need a clean output directory
+ (when (and recreate-directory
+ (file-directory-p output-directory))
+ (elpamr--log-message "Re-creating %s" output-directory)
+ (elpamr-delete-directory output-directory))
+
+ ;; Create output directory if it does not exist.
+ (unless (file-directory-p output-directory)
+ (make-directory output-directory t))
+
+ (when (and (> (length final-pkg-list) 0)
+ output-directory
+ (file-directory-p output-directory))
+ (let ((tmp-dir (make-temp-file "elpa" t))
+ (is-bsd-tar (elpamr--is-bsd-tar))
+ (cnt 0))
+
+ (dolist (package final-pkg-list)
+ (let* ((pkg-dir (package-desc-dir (car (cdr package))))
+ (name (package-desc-name (car (cdr package))))
+ (version-str (package-version-join (package-desc-version (car (cdr package)))))
+ (name-fixed (format "%s-%s" name version-str))
+ (package-parent-dir-temp-p (not (string-match (concat "^" (file-truename package-user-dir))
+ pkg-dir)))
+ (package-parent-dir (cond
+ (package-parent-dir-temp-p
+ tmp-dir)
+ (t
+ package-user-dir))))
+
+ (when package-parent-dir-temp-p
+ (copy-directory pkg-dir (elpamr--fullpath tmp-dir name-fixed)))
+
+ (elpamr--run-tar package-parent-dir
+ (file-relative-name (concat (elpamr--fullpath output-directory name-fixed) ".tar")
+ package-parent-dir)
+ name-fixed
+ is-bsd-tar)
+ (message "Creating *.tar... %2d%% (%s)"
+ (/ (* cnt 100) (length final-pkg-list))
+ pkg-dir))
+ (setq cnt (1+ cnt)))
+ ;; clean up temp directory
+ (elpamr-delete-directory tmp-dir))
+
+ ;; output archive-contents
+ (elpamr--log-message "Creating archive-contents...")
+ (with-temp-buffer
+ (let* ((print-level nil)
+ (print-length nil))
+ (insert "(1\n")
+ (dolist (final-pkg final-pkg-list)
+ ;; each package occupies one line
+ (insert (elpamr--one-item-for-archive-contents final-pkg)))
+ (insert ")"))
+ (write-file (elpamr--fullpath output-directory "archive-contents")))
+ (run-hook-with-args 'elpamr-finished-hook output-directory)
+ (elpamr--log-message "DONE! Output directory: %s" output-directory))))
+
+(provide 'elpa-mirror)
+;;; elpa-mirror.el ends here
diff --git a/custom/eplot.el b/custom/eplot.el
new file mode 100644
index 00000000..c00a3e31
--- /dev/null
+++ b/custom/eplot.el
@@ -0,0 +1,3495 @@
+;;; eplot.el --- Manage and Edit Wordpress Posts -*- lexical-binding: t -*-
+
+;; Copyright (C) 2024 Free Software Foundation, Inc.
+
+;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
+;; Keywords: charts
+;; Package: eplot
+;; Version: 1.0
+;; Package-Requires: ((emacs "29.0.59") (pcsv "0.0"))
+
+;; eplot 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 2, or (at your option)
+;; any later version.
+
+;;; Commentary:
+
+;; The main entry point is `M-x eplot' in a buffer with time series
+;; data.
+;;
+;; If installing manually, put something like the following in your
+;; Emacs init file (but adjust the path to where you've put eplot):
+;;
+;; (push "~/src/eplot/" load-path)
+;; (autoload 'eplot "eplot" nil t)
+;; (autoload 'eplot-mode "eplot" nil t)
+;; (unless (assoc "\\.plt" auto-mode-alist)
+;; (setq auto-mode-alist (cons '("\\.plt" . eplot-mode) auto-mode-alist)))
+
+;; This requires the pcsv package to parse CSV files.
+
+;;; Code:
+
+(require 'svg)
+(require 'cl-lib)
+(require 'face-remap)
+(require 'eieio)
+(require 'iso8601)
+(require 'transient)
+
+(defvar eplot--user-defaults nil)
+(defvar eplot--chart-headers nil)
+(defvar eplot--plot-headers nil)
+(defvar eplot--transient-settings nil)
+
+
+(defvar eplot--colors
+ '("aliceblue" "antiquewhite" "aqua" "aquamarine" "azure" "beige" "bisque"
+ "black" "blanchedalmond" "blue" "blueviolet" "brown" "burlywood"
+ "cadetblue" "chartreuse" "chocolate" "coral" "cornflowerblue" "cornsilk"
+ "crimson" "cyan" "darkblue" "darkcyan" "darkgoldenrod" "darkgray"
+ "darkgreen" "darkgrey" "darkkhaki" "darkmagenta" "darkolivegreen"
+ "darkorange" "darkorchid" "darkred" "darksalmon" "darkseagreen"
+ "darkslateblue" "darkslategray" "darkslategrey" "darkturquoise"
+ "darkviolet" "deeppink" "deepskyblue" "dimgray" "dimgrey" "dodgerblue"
+ "firebrick" "floralwhite" "forestgreen" "fuchsia" "gainsboro" "ghostwhite"
+ "gold" "goldenrod" "gray" "green" "greenyellow" "grey" "honeydew" "hotpink"
+ "indianred" "indigo" "ivory" "khaki" "lavender" "lavenderblush" "lawngreen"
+ "lemonchiffon" "lightblue" "lightcoral" "lightcyan" "lightgoldenrodyellow"
+ "lightgray" "lightgreen" "lightgrey" "lightpink" "lightsalmon"
+ "lightseagreen" "lightskyblue" "lightslategray" "lightslategrey"
+ "lightsteelblue" "lightyellow" "lime" "limegreen" "linen" "magenta"
+ "maroon" "mediumaquamarine" "mediumblue" "mediumorchid" "mediumpurple"
+ "mediumseagreen" "mediumslateblue" "mediumspringgreen" "mediumturquoise"
+ "mediumvioletred" "midnightblue" "mintcream" "mistyrose" "moccasin"
+ "navajowhite" "navy" "oldlace" "olive" "olivedrab" "orange" "orangered"
+ "orchid" "palegoldenrod" "palegreen" "paleturquoise" "palevioletred"
+ "papayawhip" "peachpuff" "peru" "pink" "plum" "powderblue" "purple" "red"
+ "rosybrown" "royalblue" "saddlebrown" "salmon" "sandybrown" "seagreen"
+ "seashell" "sienna" "silver" "skyblue" "slateblue" "slategray" "slategrey"
+ "snow" "springgreen" "steelblue" "tan" "teal" "thistle" "tomato"
+ "turquoise" "violet" "wheat" "white" "whitesmoke" "yellow" "yellowgreen"))
+
+(defun eplot-set (header value)
+ "Set the default value of HEADER to VALUE.
+To get a list of all possible HEADERs, use the `M-x
+eplot-list-chart-headers' command.
+
+Also see `eplot-reset'."
+ (let ((elem (or (assq header eplot--chart-headers)
+ (assq header eplot--plot-headers))))
+ (unless elem
+ (error "No such header type: %s" header))
+ (eplot--add-default header value)))
+
+(defun eplot--add-default (header value)
+ ;; We want to preserve the order defaults have been added, so that
+ ;; we can apply them in the same order. This makes a difference
+ ;; when we're dealing with specs that have inheritence.
+ (setq eplot--user-defaults (delq (assq header eplot--user-defaults)
+ eplot--user-defaults))
+ (setq eplot--user-defaults (list (cons header value))))
+
+(defun eplot-reset (&optional header)
+ "Reset HEADER to defaults.
+If HEADER is nil or not present, reset everything to defaults."
+ (if header
+ (setq eplot--user-defaults (delq (assq header eplot--user-defaults)
+ eplot--user-defaults))
+ (setq eplot--user-defaults nil)))
+
+(unless (assoc "\\.plt" auto-mode-alist)
+ (setq auto-mode-alist (cons '("\\.plt" . eplot-mode) auto-mode-alist)))
+
+;;; eplot modes.
+
+(defvar-keymap eplot-mode-map
+ "C-c C-c" #'eplot-update-view-buffer
+ "C-c C-p" #'eplot-switch-view-buffer
+ "C-c C-e" #'eplot-list-chart-headers
+ "C-c C-v" #'eplot-customize
+ "C-c C-l" #'eplot-create-controls
+ "TAB" #'eplot-complete)
+
+;; # is working overtime in the syntax here:
+;; It can be a color like Color: #e0e0e0, and
+;; it can be a setting like 33 # Label: Apples,
+;; when it starts a line it's a comment.
+(defvar eplot-font-lock-keywords
+ `(("^[ \t\n]*#.*" . font-lock-comment-face)
+ ("^[^ :\n]+:" . font-lock-keyword-face)
+ ("#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]\\([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]\\)?" . font-lock-variable-name-face)
+ ("#.*" . font-lock-builtin-face)))
+
+(define-derived-mode eplot-mode text-mode "eplot"
+ "Major mode for editing charts.
+Use the \\[eplot-list-chart-headers] command to get a list of all
+possible chart headers."
+ (setq-local completion-at-point-functions
+ (cons 'eplot--complete-header completion-at-point-functions))
+ (setq-local font-lock-defaults
+ '(eplot-font-lock-keywords nil nil nil)))
+
+(defun eplot-complete ()
+ "Complete headers."
+ (interactive)
+ (cond
+ ((let ((completion-fail-discreetly t))
+ (completion-at-point))
+ ;; Completion was performed; nothing else to do.
+ nil)
+ (t (indent-relative))))
+
+(defun eplot--complete-header ()
+ (or
+ ;; Complete headers names.
+ (and (or (looking-at ".*:")
+ (and (looking-at "[ \t]*$")
+ (save-excursion
+ (beginning-of-line)
+ (not (looking-at "\\(.+\\):")))))
+ (lambda ()
+ (let ((headers (mapcar
+ (lambda (h)
+ (if (looking-at ".*:")
+ (capitalize (symbol-name (car h)))
+ (concat (capitalize (symbol-name (car h))) ": ")))
+ (save-excursion
+ ;; If we're after the headers, then we want
+ ;; to complete over the plot headers. Otherwise,
+ ;; complete over the chart headers.
+ (if (and (not (bobp))
+ (progn
+ (forward-line -1)
+ (re-search-backward "^[ \t]*$" nil t)))
+ eplot--plot-headers
+ eplot--chart-headers))))
+ (completion-ignore-case t))
+ (completion-in-region (pos-bol) (line-end-position) headers)
+ 'completion-attempted)))
+ ;; Complete header values.
+ (let ((hname nil))
+ (and (save-excursion
+ (and (looking-at "[ \t]*$")
+ (progn
+ (beginning-of-line)
+ (and (looking-at "\\(.+\\):")
+ (setq hname (intern (downcase (match-string 1)))))))
+ (lambda ()
+ (let ((valid (plist-get
+ (cdr (assq hname (append eplot--plot-headers
+ eplot--chart-headers)))
+ :valid))
+ (completion-ignore-case t))
+ (completion-in-region
+ (save-excursion
+ (search-backward ":" (pos-bol) t)
+ (skip-chars-forward ": \t")
+ (point))
+ (line-end-position)
+ (mapcar #'symbol-name valid))
+ 'completion-attempted)))))))
+
+(define-minor-mode eplot-minor-mode
+ "Minor mode to issue commands from an eplot data buffer."
+ :lighter " eplot")
+
+(defvar-keymap eplot-minor-mode-map
+ "H-l" #'eplot-eval-and-update)
+
+(defvar-keymap eplot-view-mode-map
+ "s" #'eplot-view-write-file
+ "w" #'eplot-view-write-scaled-file
+ "c" #'eplot-view-customize
+ "l" #'eplot-create-controls)
+
+(define-derived-mode eplot-view-mode special-mode "eplot view"
+ "Major mode for displaying eplots."
+ (setq-local revert-buffer-function #'eplot-update
+ cursor-type nil))
+
+(defun eplot-view-write-file (file &optional width)
+ "Write the current chart to a file.
+If you type in a file name that ends with something else than \"svg\",
+ImageMagick \"convert\" will be used to convert the image first.
+
+If writing to a PNG file, \"rsvg-conver\" will be used instead if
+it exists as this usually gives better results."
+ (interactive "FWrite to file name: ")
+ (when (and (file-exists-p file)
+ (not (yes-or-no-p "File exists, overwrite? ")))
+ (error "Not overwriting the file"))
+ (save-excursion
+ (goto-char (point-min))
+ (let ((match
+ (text-property-search-forward 'display nil
+ (lambda (_ e)
+ (and (consp e)
+ (eq (car e) 'image))))))
+ (unless match
+ (error "Can't find an image in the current buffer"))
+ (let ((svg (plist-get (cdr (prop-match-value match)) :data))
+ (tmp " *eplot convert*")
+ (executable (if width "rsvg-convert" "convert"))
+ sfile ofile)
+ (unless svg
+ (error "Invalid image in the current buffer"))
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (svg-print svg)
+ (if (string-match-p "\\.svg\\'" file)
+ (write-region (point-min) (point-max) file)
+ (if (and (string-match-p "\\.png\\'" file)
+ (executable-find "rsvg-convert"))
+ (setq executable "rsvg-convert")
+ (unless (executable-find executable)
+ (error "%s isn't installed; can only save svg files"
+ executable)))
+ (when (and (equal executable "rsvg-convert")
+ (not (string-match-p "\\.png\\'" file))
+ (not (executable-find "convert")))
+ (error "Can only write PNG files when scaling because \"convert\" isn't installed"))
+ (unwind-protect
+ (progn
+ (setq sfile (make-temp-file "eplot" nil ".svg")
+ ofile (make-temp-file "eplot" nil ".png"))
+ (write-region (point-min) (point-max) sfile nil 'silent)
+ ;; We don't use `call-process-region', because
+ ;; convert doesn't seem to like that?
+ (let ((code (if (equal executable "rsvg-convert")
+ (apply
+ #'call-process
+ executable nil (get-buffer-create tmp) nil
+ `(,(format "--output=%s"
+ (expand-file-name ofile))
+ ,@(and width
+ `(,(format "--width=%d" width)
+ "--keep-aspect-ratio"))
+ ,sfile))
+ (call-process
+ executable nil (get-buffer-create tmp) nil
+ sfile file))))
+ (eplot--view-error code tmp)
+ (when (file-exists-p ofile)
+ (if (string-match-p "\\.png\\'" file)
+ (rename-file ofile file)
+ (let ((code (call-process "convert" nil tmp nil
+ ofile file)))
+ (eplot--view-error code tmp))))
+ (message "Wrote %s" file)))
+ ;; Clean-up.
+ (when (get-buffer tmp)
+ (kill-buffer tmp))
+ (when (file-exists-p sfile)
+ (delete-file sfile))
+ (when (file-exists-p ofile)
+ (delete-file sfile)))))))))
+
+(defun eplot--view-error (code tmp)
+ (unless (zerop code)
+ (error "Error code %d: %s"
+ code
+ (with-current-buffer tmp
+ (while (search-forward "[ \t\n]+" nil t)
+ (replace-match " "))
+ (string-trim (buffer-string))))))
+
+(defun eplot-view-write-scaled-file (width file)
+ "Write the current chart to a rescaled to a file.
+The rescaling is done by \"rsvg-convert\", which has to be
+installed. Rescaling is done when rendering, so this should give
+you a clear, non-blurry version of the chart at any size."
+ (interactive "nWidth: \nFWrite to file: ")
+ (eplot-view-write-file file width))
+
+(defun eplot-view-customize ()
+ "Customize the settings for the chart in the current buffer."
+ (interactive)
+ (with-suppressed-warnings ((interactive-only eplot-customize))
+ (eplot-customize)))
+
+(defvar eplot--data-buffer nil)
+(defvar eplot--current-chart nil)
+
+(defun eplot ()
+ "Plot the data in the current buffer."
+ (interactive)
+ (eplot-update-view-buffer))
+
+(defun eplot-with-headers (header-file)
+ "Plot the data in the current buffer using headers from a file."
+ (interactive "fHeader file: ")
+ (eplot-update-view-buffer
+ (with-temp-buffer
+ (insert-file-contents header-file)
+ (eplot--parse-headers))))
+
+(defun eplot-make-plot (headers &rest datas)
+ "Return an SVG based on DATA.
+DATA should be pairs of headers, then followed by the plot data."
+ (with-temp-buffer
+ ;; Insert the headers.
+ (dolist (line headers)
+ (insert (format "%s:" (pop line)))
+ (dolist (elem line)
+ (insert (format " %s" elem)))
+ (insert "\n"))
+ ;; Then insert all the plot data sets.
+ (dolist (data datas)
+ (insert "\n")
+ (dolist (line data)
+ (dolist (elem line)
+ (insert (format "%s" elem) " "))
+ (insert "\n")))
+ ;;(setq a (buffer-string)) (debug)
+ (eplot--render (eplot--parse-buffer) t)))
+
+(defun eplot-switch-view-buffer ()
+ "Switch to the eplot view buffer and render the chart."
+ (interactive)
+ (eplot-update-view-buffer nil t))
+
+(defun eplot-update-view-buffer (&optional headers switch)
+ "Update the eplot view buffer based on the current data buffer."
+ (interactive)
+ ;; This is mainly useful during implementation.
+ (if (and (eq major-mode 'emacs-lisp-mode)
+ (get-buffer-window "*eplot*" t))
+ (with-current-buffer "*eplot*"
+ (eplot-update)
+ (when-let ((win (get-buffer-window "*eplot*" t)))
+ (set-window-point win (point-min))))
+ ;; Normal case.
+ (let* ((eplot--user-defaults (eplot--settings-table))
+ (data (eplot--parse-buffer))
+ (data-buffer (current-buffer))
+ (window (selected-window)))
+ (unless data
+ (user-error "No data in the current buffer"))
+ (setq data (eplot--inject-headers data headers))
+ (if (get-buffer-window "*eplot*" t)
+ (set-buffer "*eplot*")
+ (if switch
+ (pop-to-buffer-same-window "*eplot*")
+ (pop-to-buffer "*eplot*")))
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (unless (eq major-mode 'eplot-view-mode)
+ (eplot-view-mode))
+ (setq-local eplot--data-buffer data-buffer)
+ (let ((chart (eplot--render data)))
+ (with-current-buffer data-buffer
+ (setq-local eplot--current-chart chart)))
+ (insert "\n")
+ (when-let ((win (get-buffer-window "*eplot*" t)))
+ (set-window-point win (point-min))))
+ (select-window window))))
+
+(defun eplot--settings-table ()
+ (if (not eplot--transient-settings)
+ eplot--user-defaults
+ (append eplot--user-defaults eplot--transient-settings)))
+
+(defun eplot--inject-headers (data headers)
+ ;; It's OK not to separate the plot headers from the chart
+ ;; headers. Collect them here, if any.
+ (when-let ((plot-headers
+ (cl-loop for elem in (mapcar #'car eplot--plot-headers)
+ for value = (eplot--vs elem headers)
+ when value
+ collect (progn
+ ;; Remove these headers from the data
+ ;; headers so that we don't get errors
+ ;; on undefined headers.
+ (setq headers (delq (assq elem headers)
+ headers))
+ (cons elem value)))))
+ (dolist (plot (cdr (assq :plots data)))
+ (let ((headers (assq :headers plot)))
+ (if headers
+ (nconc headers plot-headers)
+ (nconc plot (list (list :headers plot-headers)))))))
+ (append data headers))
+
+(defun eplot-eval-and-update ()
+ "Helper command when developing."
+ (interactive nil emacs-lisp-mode)
+ (save-some-buffers t)
+ (elisp-eval-buffer)
+ (eval-defun nil)
+ (eplot-update-view-buffer))
+
+;;; Parsing buffers.
+
+(defun eplot-update (&rest _ignore)
+ "Update the plot in the current buffer."
+ (interactive)
+ (unless eplot--data-buffer
+ (user-error "No data buffer associated with this eplot view buffer"))
+ (let ((data (with-current-buffer eplot--data-buffer
+ (eplot--parse-buffer)))
+ (eplot--user-defaults (with-current-buffer eplot--data-buffer
+ (eplot--settings-table)))
+ (inhibit-read-only t))
+ (erase-buffer)
+ (let ((chart (eplot--render data)))
+ (with-current-buffer eplot--data-buffer
+ (setq-local eplot--current-chart chart)))
+ (insert "\n\n")))
+
+(defun eplot--parse-buffer ()
+ (if (eq major-mode 'org-mode)
+ (eplot--parse-org-buffer)
+ (eplot--parse-eplot-buffer)))
+
+(defun eplot--parse-eplot-buffer ()
+ (if (eplot--csv-buffer-p)
+ (eplot--parse-csv-buffer)
+ (let ((buf (current-buffer)))
+ (with-temp-buffer
+ (insert-buffer-substring buf)
+ ;; Remove comments first.
+ (goto-char (point-min))
+ (while (re-search-forward "^[ \t]*#" nil t)
+ (delete-line))
+ (goto-char (point-min))
+ ;; First headers.
+ (let* ((data (eplot--parse-headers))
+ (plot-headers
+ ;; It's OK not to separate the plot headers from the chart
+ ;; headers. Collect them here, if any.
+ (cl-loop for elem in (mapcar #'car eplot--plot-headers)
+ for value = (eplot--vs elem data)
+ when value
+ collect (progn
+ ;; Remove these headers from the data
+ ;; headers so that we don't get errors
+ ;; on undefined headers.
+ (setq data (delq (assq elem data) data))
+ (cons elem value))))
+ plots)
+ ;; Then the values.
+ (while-let ((plot (eplot--parse-values nil plot-headers)))
+ (setq plot-headers nil)
+ (push plot plots))
+ (when plots
+ (push (cons :plots (nreverse plots)) data))
+ data)))))
+
+(defun eplot--parse-headers ()
+ (let ((data nil)
+ type value)
+ (while (looking-at "\\([^\n\t :]+\\):\\(.*\\)")
+ (setq type (intern (downcase (match-string 1)))
+ value (substring-no-properties (string-trim (match-string 2))))
+ (forward-line 1)
+ ;; Get continuation lines.
+ (while (looking-at "[ \t]+\\(.*\\)")
+ (setq value (concat value " " (string-trim (match-string 1))))
+ (forward-line 1))
+ (if (eq type 'header-file)
+ (setq data (nconc data
+ (with-temp-buffer
+ (insert-file-contents value)
+ (eplot--parse-headers))))
+ ;; We don't use `push' here because we want to preserve order
+ ;; also when inserting headers from other files.
+ (setq data (nconc data (list (cons type value))))))
+ data))
+
+(defun eplot--parse-values (&optional in-headers data-headers)
+ ;; Skip past separator lines.
+ (while (looking-at "[ \t]*\n")
+ (forward-line 1))
+ (let* ((values nil)
+ ;; We may have plot-specific headers.
+ (headers (nconc (eplot--parse-headers) data-headers))
+ (data-format (or (eplot--vyl 'data-format headers)
+ (eplot--vyl 'data-format in-headers)))
+ (two-values (memq 'two-values data-format))
+ (xy (or (memq 'year data-format)
+ (memq 'date data-format)
+ (memq 'time data-format)
+ (memq 'xy data-format)))
+ (data-column (or (eplot--vn 'data-column headers)
+ (eplot--vn 'data-column in-headers))))
+ (if-let ((data-file (eplot--vs 'data-file headers)))
+ (with-temp-buffer
+ (insert-file-contents data-file)
+ (setq values (cdr (assq :values (eplot--parse-values headers)))
+ headers (delq (assq 'data headers) headers)))
+ ;; Now we come to the data. The data is typically either just a
+ ;; number, or two numbers (in which case the first number is a
+ ;; date or a time). Labels ans settings can be introduced with
+ ;; a # char.
+ (while (looking-at "\\([-0-9. \t]+\\)\\([ \t]*#\\(.*\\)\\)?")
+ (let ((numbers (match-string 1))
+ (settings (eplot--parse-settings (match-string 3)))
+ this)
+ (setq numbers (mapcar #'string-to-number
+ (split-string (string-trim numbers))))
+ ;; If we're reading two dimensionalish data, the first
+ ;; number is the date/time/x.
+ (when xy
+ (setq this (list :x (pop numbers))))
+ ;; Chop off all the numbers until we read the column(s)
+ ;; we're using.
+ (when data-column
+ (setq numbers (nthcdr (1- data-column) numbers)))
+ (when numbers
+ (setq this (nconc this (list :value (pop numbers)))))
+ (when two-values
+ (setq this (nconc this (list :extra-value (pop numbers)))))
+ (when settings
+ (setq this (nconc this (list :settings settings))))
+ (when (plist-get this :value)
+ (push this values)))
+ (forward-line 1))
+ (setq values (nreverse values)))
+ (and values
+ `((:headers . ,headers) (:values . ,values)))))
+
+(defun eplot--parse-settings (string)
+ (when string
+ (with-temp-buffer
+ (insert (string-trim string) "\n")
+ (goto-char (point-min))
+ (while (re-search-forward "\\(.\\)," nil t)
+ (if (equal (match-string 1) "\\")
+ (replace-match "," t t)
+ (delete-char -1)
+ (insert "\n")
+ (when (looking-at "[ \t]+")
+ (replace-match ""))))
+ (goto-char (point-min))
+ (eplot--parse-headers))))
+
+;;; Accessing data.
+
+(defun eplot--vn (type data &optional default)
+ (if-let ((value (cdr (assq type data))))
+ (string-to-number value)
+ default))
+
+(defun eplot--vs (type data &optional default)
+ (or (cdr (assq type data)) default))
+
+(defun eplot--vy (type data &optional default)
+ (if-let ((value (cdr (assq type data))))
+ (intern (downcase value))
+ default))
+
+(defun eplot--vyl (type data &optional default)
+ (if-let ((value (cdr (assq type data))))
+ (mapcar #'intern (split-string (downcase value)))
+ default))
+
+(defmacro eplot-def (args doc-string)
+ (declare (indent defun))
+ `(eplot--def ',(nth 0 args) ',(nth 1 args) ',(nth 2 args) ',(nth 3 args)
+ ,doc-string))
+
+(defun eplot--def (name type default valid doc)
+ (setq eplot--chart-headers (delq (assq name eplot--chart-headers)
+ eplot--chart-headers))
+ (push (list name
+ :type type
+ :default default
+ :doc doc
+ :valid valid)
+ eplot--chart-headers))
+
+(eplot-def (width number)
+ "The width of the entire chart.")
+
+(eplot-def (height number)
+ "The height of the entire chart.")
+
+(eplot-def (format symbol normal (normal bar-chart horizontal-bar-chart))
+ "The overall format of the chart.")
+
+(eplot-def (layout symbol nil (normal compact))
+ "The general layout of the chart.")
+
+(eplot-def (mode symbol light (dark light))
+ "Dark/light mode.")
+
+(eplot-def (margin-left number 70)
+ "The left margin.")
+
+(eplot-def (margin-right number 20)
+ "The right margin.")
+
+(eplot-def (margin-top number 40)
+ "The top margin.")
+
+(eplot-def (margin-bottom number 60)
+ "The bottom margin.")
+
+(eplot-def (x-axis-title-space number 5)
+ "The space between the X axis and the label.")
+
+(eplot-def (font string "sans-serif")
+ "The font to use in titles, labels and legends.")
+
+(eplot-def (font-size number 12)
+ "The font size.")
+
+(eplot-def (font-weight symbol bold (bold normal))
+ "The font weight.")
+
+(eplot-def (label-font string (spec font))
+ "The font to use for axes labels.")
+
+(eplot-def (label-font-size number (spec font-size))
+ "The font size to use for axes labels.")
+
+(eplot-def (horizontal-label-font-size number (spec label-font-size))
+ "The font size to use for horizontal labels.")
+
+(eplot-def (bar-font string (spec font))
+ "The font to use for bar chart labels.")
+
+(eplot-def (bar-font-size number (spec font-size))
+ "The font size to use for bar chart labels.")
+
+(eplot-def (bar-font-weight symbol (spec font-weight) (bold normal))
+ "The font weight to use for bar chart labels.")
+
+(eplot-def (chart-color string "black")
+ "The foreground color to use in plots, axes, legends, etc.
+This is used as the default, but can be overridden per thing.")
+
+(eplot-def (background-color string "white")
+ "The background color.
+If you want a chart with a transparent background, use the color
+\"none\".")
+
+(eplot-def (background-gradient string)
+ "Use this to get a gradient color in the background.")
+
+(eplot-def (axes-color string (spec chart-color))
+ "The color of the axes.")
+
+(eplot-def (grid-color string "#e0e0e0")
+ "The color of the grid.")
+
+(eplot-def (grid symbol xy (xy x y off))
+ "What grid axes to do.")
+
+(eplot-def (grid-opacity number)
+ "The opacity of the grid.
+This should either be nil or a value between 0 and 1, where 0 is
+fully transparent.")
+
+(eplot-def (grid-position symbol bottom (bottom top))
+ "Whether to put the grid on top or under the plot.")
+
+(eplot-def (legend symbol nil (true nil))
+ "Whether to do a legend.")
+
+(eplot-def (legend-color string (spec chart-color))
+ "The color of legends (if any).")
+
+(eplot-def (legend-border-color string (spec chart-color))
+ "The border color of legends (if any).")
+
+(eplot-def (legend-background-color string (spec background-color))
+ "The background color of legends (if any).")
+
+(eplot-def (label-color string (spec axes-color))
+ "The color of labels on the axes.")
+
+(eplot-def (surround-color string)
+ "The color between the plot area and the edges of the chart.")
+
+(eplot-def (border-color string)
+ "The color of the border of the chart, if any.")
+
+(eplot-def (border-width number)
+ "The width of the border of the chart, if any.")
+
+(eplot-def (frame-color string)
+ "The color of the frame of the plot, if any.")
+
+(eplot-def (frame-width number)
+ "The width of the frame of the plot, if any.")
+
+(eplot-def (min number)
+ "The minimum value in the chart.
+This is normally computed automatically, but can be overridden
+ with this spec.")
+
+(eplot-def (max number)
+ "The maximum value in the chart.
+This is normally computed automatically, but can be overridden
+ with this spec.")
+
+(eplot-def (title string)
+ "The title of the chart, if any.")
+
+(eplot-def (title-color string (spec chart-color))
+ "The color of the title.")
+
+(eplot-def (x-title string)
+ "The title of the X axis, if any.")
+
+(eplot-def (y-title string)
+ "The title of the X axis, if any.")
+
+(eplot-def (x-label-format string)
+ "Format string for the X labels.
+This is a `format' string.")
+
+(eplot-def (y-label-format string)
+ "Format string for the Y labels.
+This is a `format' string.")
+
+(eplot-def (horizontal-label-left number)
+ "Position of the horizontal labels.")
+
+(eplot-def (x-label-orientation symbol horizontal (horizontal vertical))
+ "Orientation of the X labels.")
+
+(eplot-def (background-image-file string)
+ "Use an image as the background.")
+
+(eplot-def (background-image-opacity number 1)
+ "The opacity of the background image.")
+
+(eplot-def (background-image-cover symbol all (all plot frame))
+ "Position of the background image.
+Valid values are `all' (the entire image), `plot' (the plot area)
+and `frame' (the surrounding area).")
+
+(eplot-def (header-file string)
+ "File where the headers are.")
+
+(defvar eplot-compact-defaults
+ '((margin-left 30)
+ (margin-right 10)
+ (margin-top 20)
+ (margin-bottom 21)
+ (font-size 12)
+ (x-axis-title-space 3)))
+
+(defvar eplot-dark-defaults
+ '((chart-color "#c0c0c0")
+ (axes-color "#c0c0c0")
+ (grid-color "#404040")
+ (background-color "#101010")
+ (label-color "#c0c0c0")
+ (legend-color "#c0c0c0")
+ (legend-background-color "#000000")
+ (title-color "#c0c0c0")))
+
+(defvar eplot-bar-chart-defaults
+ '((grid-position top)
+ (grid y)
+ (grid-opacity 0.2)
+ (min 0)))
+
+(defvar eplot-horizontal-bar-chart-defaults
+ '((grid-position top)
+ (grid-opacity 0.2)
+ (min 0)))
+
+(defclass eplot-chart ()
+ (
+ (plots :initarg :plots)
+ (data :initarg :data)
+ (xs)
+ (ys)
+ (x-values :initform nil)
+ (x-type :initform nil)
+ (x-min)
+ (x-max)
+ (x-ticks)
+ (y-ticks)
+ (y-labels)
+ (x-labels)
+ (print-format)
+ (x-tick-step)
+ (x-label-step)
+ (x-step-map :initform nil)
+ (y-tick-step)
+ (y-label-step)
+ (inhibit-compute-x-step :initform nil)
+ ;; ---- CUT HERE ----
+ (axes-color :initarg :axes-color :initform nil)
+ (background-color :initarg :background-color :initform nil)
+ (background-gradient :initarg :background-gradient :initform nil)
+ (background-image-cover :initarg :background-image-cover :initform nil)
+ (background-image-file :initarg :background-image-file :initform nil)
+ (background-image-opacity :initarg :background-image-opacity :initform nil)
+ (bar-font :initarg :bar-font :initform nil)
+ (bar-font-size :initarg :bar-font-size :initform nil)
+ (bar-font-weight :initarg :bar-font-weight :initform nil)
+ (border-color :initarg :border-color :initform nil)
+ (border-width :initarg :border-width :initform nil)
+ (chart-color :initarg :chart-color :initform nil)
+ (font :initarg :font :initform nil)
+ (font-size :initarg :font-size :initform nil)
+ (font-weight :initarg :font-weight :initform nil)
+ (format :initarg :format :initform nil)
+ (frame-color :initarg :frame-color :initform nil)
+ (frame-width :initarg :frame-width :initform nil)
+ (grid :initarg :grid :initform nil)
+ (grid-color :initarg :grid-color :initform nil)
+ (grid-opacity :initarg :grid-opacity :initform nil)
+ (grid-position :initarg :grid-position :initform nil)
+ (header-file :initarg :header-file :initform nil)
+ (height :initarg :height :initform nil)
+ (horizontal-label-font-size :initarg :horizontal-label-font-size :initform nil)
+ (label-color :initarg :label-color :initform nil)
+ (label-font :initarg :label-font :initform nil)
+ (label-font-size :initarg :label-font-size :initform nil)
+ (layout :initarg :layout :initform nil)
+ (legend :initarg :legend :initform nil)
+ (legend-background-color :initarg :legend-background-color :initform nil)
+ (legend-border-color :initarg :legend-border-color :initform nil)
+ (legend-color :initarg :legend-color :initform nil)
+ (margin-bottom :initarg :margin-bottom :initform nil)
+ (margin-left :initarg :margin-left :initform nil)
+ (margin-right :initarg :margin-right :initform nil)
+ (margin-top :initarg :margin-top :initform nil)
+ (max :initarg :max :initform nil)
+ (min :initarg :min :initform nil)
+ (mode :initarg :mode :initform nil)
+ (surround-color :initarg :surround-color :initform nil)
+ (title :initarg :title :initform nil)
+ (title-color :initarg :title-color :initform nil)
+ (width :initarg :width :initform nil)
+ (x-axis-title-space :initarg :x-axis-title-space :initform nil)
+ (x-title :initarg :x-title :initform nil)
+ (y-title :initarg :y-title :initform nil)
+ (x-label-format :initarg :x-label-format :initform nil)
+ (x-label-orientation :initarg :x-label-orientation :initform nil)
+ (y-label-format :initarg :y-label-format :initform nil)
+ (horizontal-label-left :initarg :horizontal-label-left :initform nil)
+ ;; ---- CUT HERE ----
+ ))
+
+;;; Parameters that are plot specific.
+
+(defmacro eplot-pdef (args doc-string)
+ (declare (indent defun))
+ `(eplot--pdef ',(nth 0 args) ',(nth 1 args) ',(nth 2 args) ',(nth 3 args)
+ ,doc-string))
+
+(defun eplot--pdef (name type default valid doc)
+ (setq eplot--plot-headers (delq (assq name eplot--plot-headers)
+ eplot--plot-headers))
+ (push (list name
+ :type type
+ :default default
+ :valid valid
+ :doc doc)
+ eplot--plot-headers))
+
+(eplot-pdef (smoothing symbol nil (moving-average nil))
+ "Smoothing algorithm to apply to the data, if any.
+Valid values are `moving-average' and, er, probably more to come.")
+
+(eplot-pdef (gradient string)
+ "Gradient to apply to the plot.
+The syntax is:
+
+ from-color to-color direction position
+
+The last two parameters are optional.
+
+direction is either `top-down' (the default), `bottom-up',
+`left-right' or `right-left').
+
+position is either `below' or `above'.
+
+to-color can be either a color name, or a string that defines
+stops and colors:
+
+ Gradient: black 25-purple-50-white-75-purple-black
+
+In that case, the second element specifies the percentage points
+of where each color ends, so the above starts with black, then at
+25% it's purple, then at 50% it's white, then it's back to purple
+again at 75%, before ending up at black at a 100% (but you don't
+have to include the 100% here -- it's understood).")
+
+(eplot-pdef (style symbol line ( line impulse point square circle cross
+ triangle rectangle curve))
+ "Style the plot should be drawn in.
+Valid values are listed below. Some styles take additional
+optional parameters.
+
+line
+ Straight lines between values.
+
+curve
+ Curved lines between values.
+
+impulse
+ size: width of the impulse
+
+point
+
+square
+
+circle
+ size: diameter of the circle
+ fill-color: color to fill the center
+
+cross
+ size: length of the lines in the cross
+
+triangle
+ size: length of the sides of the triangle
+ fill-color: color to fill the center
+
+rectangle
+ size: length of the sides of the rectangle
+ fill-color: color to fill the center")
+
+(eplot-pdef (fill-color string)
+ "Color to use to fill the plot styles that are closed shapes.
+I.e., circle, triangle and rectangle.")
+
+(eplot-pdef (color string (spec chart-color))
+ "Color to draw the plot.")
+
+(eplot-pdef (data-format symbol single (single date time xy))
+ "Format of the data.
+By default, eplot assumes that each line has a single data point.
+This can also be `date', `time' and `xy'.
+
+date: The first column is a date on ISO8601 format (i.e., YYYYMMDD).
+
+time: The first column is a clock (i.e., HHMMSS).
+
+xy: The first column is the X position.")
+
+(eplot-pdef (data-column number 1)
+ "Column where the data is.")
+
+(eplot-pdef (fill-border-color string)
+ "Border around the fill area when using a fill/gradient style.")
+
+(eplot-pdef (size number)
+ "Size of elements in styles that have meaningful sizes.")
+
+(eplot-pdef (size-factor number)
+ "Multiply the size of the elements by the value.")
+
+(eplot-pdef (data-file string)
+ "File where the data is.")
+
+(eplot-pdef (data-format symbol-list nil (nil two-values date time))
+ "List of symbols to describe the data format.
+Elements allowed are `two-values', `date' and `time'.")
+
+(eplot-pdef (name string)
+ "Name of the plot, which will be displayed if legends are switched on.")
+
+(eplot-pdef (legend-color string (spec chart-color))
+ "Color for the name to be displayed in the legend.")
+
+(eplot-pdef (bezier-factor number 0.1)
+ "The Bezier factor to apply to curve plots.")
+
+(eplot-pdef (bar-max-width number)
+ "Max width of bars in bar plots.")
+
+(defclass eplot-plot ()
+ (
+ (values :initarg :values)
+ ;; ---- CUT HERE ----
+ (bar-max-width :initarg :bar-max-width :initform nil)
+ (bezier-factor :initarg :bezier-factor :initform nil)
+ (color :initarg :color :initform nil)
+ (data-column :initarg :data-column :initform nil)
+ (data-file :initarg :data-file :initform nil)
+ (data-format :initarg :data-format :initform nil)
+ (fill-border-color :initarg :fill-border-color :initform nil)
+ (fill-color :initarg :fill-color :initform nil)
+ (gradient :initarg :gradient :initform nil)
+ (legend-color :initarg :legend-color :initform nil)
+ (name :initarg :name :initform nil)
+ (size :initarg :size :initform nil)
+ (size-factor :initarg :size-factor :initform nil)
+ (smoothing :initarg :smoothing :initform nil)
+ (style :initarg :style :initform nil)
+ ;; ---- CUT HERE ----
+ ))
+
+(defun eplot--make-plot (data)
+ "Make an `eplot-plot' object and initialize based on DATA."
+ (let ((plot (make-instance 'eplot-plot
+ :values (cdr (assq :values data)))))
+ ;; Get the program-defined defaults.
+ (eplot--object-defaults plot eplot--plot-headers)
+ ;; One special case. I don't think this hack is quite right...
+ (when (or (eq (eplot--vs 'mode data) 'dark)
+ (eq (cdr (assq 'mode eplot--user-defaults)) 'dark))
+ (setf (slot-value plot 'color) "#c0c0c0"))
+ ;; Use the headers.
+ (eplot--object-values plot (cdr (assq :headers data)) eplot--plot-headers)
+ plot))
+
+(defun eplot--make-chart (data)
+ "Make an `eplot-chart' object and initialize based on DATA."
+ (let ((chart (make-instance 'eplot-chart
+ :plots (mapcar #'eplot--make-plot
+ (eplot--vs :plots data))
+ :data data)))
+ ;; First get the program-defined defaults.
+ (eplot--object-defaults chart eplot--chart-headers)
+ ;; Then do the "meta" variables.
+ (eplot--meta chart data 'mode 'dark eplot-dark-defaults)
+ (eplot--meta chart data 'layout 'compact eplot-compact-defaults)
+ (eplot--meta chart data 'format 'bar-chart eplot-bar-chart-defaults)
+ (eplot--meta chart data 'format 'horizontal-bar-chart
+ eplot-horizontal-bar-chart-defaults)
+ ;; Set defaults from user settings/transients.
+ (cl-loop for (name . value) in eplot--user-defaults
+ when (assq name eplot--chart-headers)
+ do
+ (setf (slot-value chart name) value)
+ (eplot--set-dependent-values chart name value))
+ ;; Finally, use the data from the chart.
+ (eplot--object-values chart data eplot--chart-headers)
+ ;; If not set, recompute the margins based on the font sizes (if
+ ;; the font size has been changed from defaults).
+ (when (or (assq 'font-size eplot--user-defaults)
+ (assq 'font-size data))
+ (with-slots ( title x-title y-title
+ margin-top margin-bottom margin-left
+ font-size font font-weight)
+ chart
+ (when (or title x-title y-title)
+ (let ((text-height
+ (eplot--text-height (concat title x-title y-title)
+ font font-size font-weight)))
+ (when (and title
+ (and (not (assq 'margin-top eplot--user-defaults))
+ (not (assq 'margin-top data))))
+ (cl-incf margin-top (* text-height 1.4)))
+ (when (and x-title
+ (and (not (assq 'margin-bottom eplot--user-defaults))
+ (not (assq 'margin-bottom data))))
+ (cl-incf margin-bottom (* text-height 1.4)))
+ (when (and y-title
+ (and (not (assq 'margin-left eplot--user-defaults))
+ (not (assq 'margin-left data))))
+ (cl-incf margin-left (* text-height 1.4)))))))
+ chart))
+
+(defun eplot--meta (chart data slot value defaults)
+ (when (or (eq (cdr (assq slot eplot--user-defaults)) value)
+ (eq (eplot--vy slot data) value))
+ (eplot--set-theme chart defaults)))
+
+(defun eplot--object-defaults (object headers)
+ (dolist (header headers)
+ (when-let ((default (plist-get (cdr header) :default)))
+ (setf (slot-value object (car header))
+ ;; Allow overrides via `eplot-set'.
+ (or (cdr (assq (car header) eplot--user-defaults))
+ (if (and (consp default)
+ (eq (car default) 'spec))
+ ;; Chase dependencies.
+ (eplot--default (cadr default))
+ default))))))
+
+(defun eplot--object-values (object data headers)
+ (cl-loop for (name . value) in data
+ do (unless (eq name :plots)
+ (let ((spec (cdr (assq name headers))))
+ (if (not spec)
+ (error "%s is not a valid spec" name)
+ (let ((value
+ (cl-case (plist-get spec :type)
+ (number
+ (string-to-number value))
+ (symbol
+ (intern (downcase value)))
+ (symbol-list
+ (mapcar #'intern (split-string (downcase value))))
+ (t
+ value))))
+ (setf (slot-value object name) value)
+ (eplot--set-dependent-values object name value)))))))
+
+(defun eplot--set-dependent-values (object name value)
+ (dolist (slot (gethash name (eplot--dependecy-graph)))
+ (setf (slot-value object slot) value)
+ (eplot--set-dependent-values object slot value)))
+
+(defun eplot--set-theme (chart map)
+ (cl-loop for (slot value) in map
+ do (setf (slot-value chart slot) value)))
+
+(defun eplot--default (slot)
+ "Find the default value for SLOT, chasing dependencies."
+ (let ((spec (cdr (assq slot eplot--chart-headers))))
+ (unless spec
+ (error "Invalid slot %s" slot))
+ (let ((default (plist-get spec :default)))
+ (if (and (consp default)
+ (eq (car default) 'spec))
+ (eplot--default (cadr default))
+ (or (cdr (assq slot eplot--user-defaults)) default)))))
+
+(defun eplot--dependecy-graph ()
+ (let ((table (make-hash-table)))
+ (dolist (elem eplot--chart-headers)
+ (let ((default (plist-get (cdr elem) :default)))
+ (when (and (consp default)
+ (eq (car default) 'spec))
+ (push (car elem) (gethash (cadr default) table)))))
+ table))
+
+(defun eplot--render (data &optional return-image)
+ "Create the chart and display it.
+If RETURN-IMAGE is non-nil, return it instead of displaying it."
+ (let* ((chart (eplot--make-chart data))
+ svg)
+ (with-slots ( width height xs ys
+ margin-left margin-right margin-top margin-bottom
+ grid-position plots x-min format
+ x-label-orientation)
+ chart
+ ;; Set the size of the chart based on the window it's going to
+ ;; be displayed in. It uses the *eplot* window by default, or
+ ;; the current one if that isn't displayed.
+ (let ((factor (image-compute-scaling-factor image-scaling-factor)))
+ (unless width
+ (setq width (truncate
+ (/ (* (window-pixel-width
+ (get-buffer-window "*eplot*" t))
+ 0.9)
+ factor))))
+ (unless height
+ (setq height (truncate
+ (/ (* (window-pixel-height
+ (get-buffer-window "*eplot*" t))
+ 0.9)
+ factor)))))
+ (setq svg (svg-create width height)
+ xs (- width margin-left margin-right)
+ ys (- height margin-top margin-bottom))
+ ;; Protect against being called in an empty buffer.
+ (if (not (and plots
+ ;; Sanity check against the user choosing dimensions
+ ;; that leave no space for the plot.
+ (> ys 0) (> xs 0)))
+ ;; Just draw the basics.
+ (eplot--draw-basics svg chart)
+
+ ;; Horizontal bar charts are special.
+ (when (eq format 'horizontal-bar-chart)
+ (eplot--adjust-horizontal-bar-chart chart data))
+ ;; Compute min/max based on all plots, and also compute x-ticks
+ ;; etc.
+ (eplot--compute-chart-dimensions chart)
+ (when (and (eq x-label-orientation 'vertical)
+ (eplot--default-p 'margin-bottom (slot-value chart 'data)))
+ (eplot--adjust-vertical-x-labels chart))
+ ;; Analyze values and adjust values accordingly.
+ (eplot--adjust-chart chart)
+ ;; Compute the Y labels -- this may adjust `margin-left'.
+ (eplot--compute-y-labels chart)
+ ;; Compute the X labels -- this may adjust `margin-bottom'.
+ (eplot--compute-x-labels chart)
+ ;; Draw background/borders/titles/etc.
+ (eplot--draw-basics svg chart)
+
+ (when (eq grid-position 'top)
+ (eplot--draw-plots svg chart))
+
+ (eplot--draw-x-ticks svg chart)
+ (unless (eq format 'horizontal-bar-chart)
+ (eplot--draw-y-ticks svg chart))
+
+ ;; Draw axes.
+ (with-slots ( margin-left margin-right margin-margin-top
+ margin-bottom axes-color)
+ chart
+ (svg-line svg margin-left margin-top margin-left
+ (+ (- height margin-bottom) 5)
+ :stroke axes-color)
+ (svg-line svg (- margin-left 5) (- height margin-bottom)
+ (- width margin-right) (- height margin-bottom)
+ :stroke axes-color))
+
+ (when (eq grid-position 'bottom)
+ (eplot--draw-plots svg chart)))
+
+ (with-slots (frame-color frame-width) chart
+ (when (or frame-color frame-width)
+ (svg-rectangle svg margin-left margin-top xs ys
+ :stroke-width frame-width
+ :fill "none"
+ :stroke-color frame-color)))
+ (eplot--draw-legend svg chart))
+
+ (if return-image
+ svg
+ (svg-insert-image svg)
+ chart)))
+
+(defun eplot--adjust-horizontal-bar-chart (chart data)
+ (with-slots ( plots bar-font bar-font-size bar-font-weight margin-left
+ width margin-right xs)
+ chart
+ (dolist (plot plots)
+ (with-slots ( data-format values) plot
+ (push 'xy data-format)
+ ;; Flip the values -- we want the values to be on the X
+ ;; axis instead.
+ (setf values
+ (cl-loop for value in values
+ for i from 1
+ collect (list :value i
+ :x (plist-get value :value)
+ :settings
+ (plist-get value :settings))))
+ (when (eplot--default-p 'margin-left data)
+ (setf margin-left
+ (+ (cl-loop for value in values
+ maximize
+ (eplot--text-width
+ (eplot--vs 'label (plist-get value :settings))
+ bar-font bar-font-size bar-font-weight))
+ 20)
+ xs (- width margin-left margin-right)))))))
+
+(defun eplot--draw-basics (svg chart)
+ (with-slots ( width height
+ chart-color font font-size font-weight
+ margin-left margin-right margin-top margin-bottom
+ background-color label-color
+ xs ys)
+ chart
+ ;; Add background.
+ (eplot--draw-background chart svg 0 0 width height)
+ (with-slots ( background-image-file background-image-opacity
+ background-image-cover)
+ chart
+ (when (and background-image-file
+ ;; Sanity checks to avoid erroring out later.
+ (file-exists-p background-image-file)
+ (file-regular-p background-image-file))
+ (apply #'svg-embed svg background-image-file "image/jpeg" nil
+ :opacity background-image-opacity
+ :preserveAspectRatio "xMidYMid slice"
+ (if (memq background-image-cover '(all frame))
+ `(:x 0 :y 0 :width ,width :height ,height)
+ `(:x ,margin-left :y ,margin-top :width ,xs :height ,ys)))
+ (when (eq background-image-cover 'frame)
+ (eplot--draw-background chart svg margin-left margin-right xs ys))))
+ ;; Area between plot and edges.
+ (with-slots (surround-color) chart
+ (when surround-color
+ (svg-rectangle svg 0 0 width height
+ :fill surround-color)
+ (svg-rectangle svg margin-left margin-top
+ xs ys
+ :fill background-color)))
+ ;; Border around the entire chart.
+ (with-slots (border-width border-color) chart
+ (when (or border-width border-color)
+ (svg-rectangle svg 0 0 width height
+ :stroke-width (or border-width 1)
+ :fill "none"
+ :stroke-color (or border-color chart-color))))
+ ;; Frame around the plot.
+ (with-slots (frame-width frame-color) chart
+ (when (or frame-width frame-color)
+ (svg-rectangle svg margin-left margin-top xs ys
+ :stroke-width (or frame-width 1)
+ :fill "none"
+ :stroke-color (or frame-color chart-color))))
+ ;; Title and legends.
+ (with-slots (title title-color) chart
+ (when title
+ (svg-text svg title
+ :font-family font
+ :text-anchor "middle"
+ :font-weight font-weight
+ :font-size font-size
+ :fill title-color
+ :x (+ margin-left (/ (- width margin-left margin-right) 2))
+ :y (+ 3 (/ margin-top 2)))))
+ (with-slots (x-title) chart
+ (when x-title
+ (svg-text svg x-title
+ :font-family font
+ :text-anchor "middle"
+ :font-weight font-weight
+ :font-size font-size
+ :fill label-color
+ :x (+ margin-left (/ (- width margin-left margin-right) 2))
+ :y (- height (/ margin-bottom 4)))))
+ (with-slots (y-title) chart
+ (when y-title
+ (let ((text-height
+ (eplot--text-height y-title font font-size font-weight)))
+ (svg-text svg y-title
+ :font-family font
+ :text-anchor "middle"
+ :font-weight font-weight
+ :font-size font-size
+ :fill label-color
+ :transform
+ (format "translate(%s,%s) rotate(-90)"
+ (- (/ margin-left 2) (/ text-height 2) 4)
+ (+ margin-top
+ (/ (- height margin-bottom margin-top) 2)))))))))
+
+(defun eplot--draw-background (chart svg left top width height)
+ (with-slots (background-gradient background-color) chart
+ (let ((gradient (eplot--parse-gradient background-gradient))
+ id)
+ (when gradient
+ (setq id (format "gradient-%s" (make-temp-name "grad")))
+ (eplot--gradient svg id 'linear
+ (eplot--stops (eplot--vs 'from gradient)
+ (eplot--vs 'to gradient))
+ (eplot--vs 'direction gradient)))
+ (apply #'svg-rectangle svg left top width height
+ (if gradient
+ `(:gradient ,id)
+ `(:fill ,background-color))))))
+
+(defun eplot--compute-chart-dimensions (chart)
+ (with-slots ( min max plots x-values x-min x-max x-ticks
+ print-format font-size
+ xs
+ inhibit-compute-x-step x-type x-step-map format
+ x-tick-step x-label-step
+ label-font label-font-size x-label-format)
+ chart
+ (let ((set-min min)
+ (set-max max))
+ (dolist (plot plots)
+ (with-slots (values data-format) plot
+ (let* ((vals (nconc (seq-map (lambda (v) (plist-get v :value)) values)
+ (and (memq 'two-values data-format)
+ (seq-map
+ (lambda (v) (plist-get v :extra-value))
+ values)))))
+ ;; Set the x-values based on the first plot.
+ (unless x-values
+ (setq print-format (cond
+ ((memq 'year data-format) 'year)
+ ((memq 'date data-format) 'date)
+ ((memq 'time data-format) 'time)
+ (t 'number)))
+ (cond
+ ((or (memq 'xy data-format)
+ (memq 'year data-format))
+ (setq x-values (cl-loop for val in values
+ collect (plist-get val :x))
+ x-min (if (eq format 'horizontal-bar-chart)
+ 0
+ (seq-min x-values))
+ x-max (seq-max x-values)
+ x-ticks (eplot--get-ticks x-min x-max xs))
+ (when (memq 'year data-format)
+ (setq print-format 'literal-year)))
+ ((memq 'date data-format)
+ (setq x-values
+ (cl-loop for val in values
+ collect
+ (time-to-days
+ (encode-time
+ (decoded-time-set-defaults
+ (iso8601-parse-date
+ (format "%d" (plist-get val :x)))))))
+ x-min (seq-min x-values)
+ x-max (seq-max x-values)
+ inhibit-compute-x-step t)
+ (let ((xs (eplot--get-date-ticks
+ x-min x-max xs
+ label-font label-font-size x-label-format)))
+ (setq x-ticks (car xs)
+ print-format (cadr xs)
+ x-tick-step 1
+ x-label-step 1
+ x-step-map (nth 2 xs))))
+ ((memq 'time data-format)
+ (setq x-values
+ (cl-loop for val in values
+ collect
+ (time-convert
+ (encode-time
+ (decoded-time-set-defaults
+ (iso8601-parse-time
+ (format "%06d" (plist-get val :x)))))
+ 'integer))
+ x-min (car x-values)
+ x-max (car (last x-values))
+ inhibit-compute-x-step t)
+ (let ((xs (eplot--get-time-ticks
+ x-min x-max xs label-font label-font-size
+ x-label-format)))
+ (setq x-ticks (car xs)
+ print-format (cadr xs)
+ x-tick-step 1
+ x-label-step 1
+ x-step-map (nth 2 xs))))
+ (t
+ ;; This is a one-dimensional plot -- we don't have X
+ ;; values, really, so we just do zero to (1- (length
+ ;; values)).
+ (setq x-type 'one-dimensional
+ x-values (cl-loop for i from 0
+ repeat (length values)
+ collect i)
+ x-min (car x-values)
+ x-max (car (last x-values))
+ x-ticks x-values))))
+ (unless set-min
+ (setq min (min (or min 1.0e+INF) (seq-min vals))))
+ (unless set-max
+ (setq max (max (or max -1.0e+INF) (seq-max vals))))))))))
+
+(defun eplot--adjust-chart (chart)
+ (with-slots ( x-tick-step x-label-step y-tick-step y-label-step
+ min max ys format inhibit-compute-x-step
+ y-ticks xs x-values print-format
+ x-label-format label-font label-font-size data
+ x-ticks)
+ chart
+ (setq y-ticks (and max
+ (eplot--get-ticks
+ min
+ ;; We get 5% more ticks to check whether we
+ ;; should extend max.
+ (if (eplot--default-p 'max data)
+ (* max 1.05)
+ max)
+ ys)))
+ (when (eplot--default-p 'max data)
+ (setq max (max max (car (last y-ticks)))))
+ (if (eq format 'bar-chart)
+ (setq x-tick-step 1
+ x-label-step 1)
+ (unless inhibit-compute-x-step
+ (let ((xt (eplot--compute-x-ticks
+ xs x-ticks print-format
+ x-label-format label-font label-font-size)))
+ (setq x-tick-step (car xt)
+ x-label-step (cadr xt)))))
+ (when max
+ (let ((yt (eplot--compute-y-ticks
+ ys y-ticks
+ (eplot--text-height "100" label-font label-font-size))))
+ (setq y-tick-step (car yt)
+ y-label-step (cadr yt))))
+ ;; If max is less than 2% off from a pleasant number, then
+ ;; increase max.
+ (when (eplot--default-p 'max data)
+ (cl-loop for tick in (reverse y-ticks)
+ when (and (< max tick)
+ (< (e/ (- tick max) (- max min)) 0.02))
+ return (progn
+ (setq max tick)
+ ;; Chop off any further ticks.
+ (setcdr (member tick y-ticks) nil))))
+
+ (when y-ticks
+ (if (and (eplot--default-p 'min data)
+ (< (car y-ticks) min))
+ (setq min (car y-ticks))
+ ;; We may be extending the bottom of the chart to get pleasing
+ ;; numbers. We don't want to be drawing the chart on top of the
+ ;; X axis, because the chart won't be visible there.
+ (when (and nil
+ (<= min (car y-ticks))
+ ;; But not if we start at origo, because that just
+ ;; looks confusing.
+ (not (zerop min)))
+ (setq min (- (car y-ticks)
+ ;; 2% of the value range.
+ (* 0.02 (- (car (last y-ticks)) (car y-ticks))))))))))
+
+(defun eplot--adjust-vertical-x-labels (chart)
+ (with-slots ( x-step-map x-ticks format plots
+ print-format x-label-format label-font
+ label-font-size margin-bottom
+ bar-font bar-font-size bar-font-weight)
+ chart
+ ;; Make X ticks.
+ (let ((width
+ (cl-loop
+ for xv in (or x-step-map x-ticks)
+ for x = (if (consp xv) (car xv) xv)
+ for i from 0
+ for value = (and (equal format 'bar-chart)
+ (elt (slot-value (car plots) 'values) i))
+ for label = (if (equal format 'bar-chart)
+ (eplot--vs 'label
+ (plist-get value :settings)
+ ;; When we're doing bar charts, we
+ ;; want default labeling to start with
+ ;; 1 and not zero.
+ (format "%s" (1+ x)))
+ (eplot--format-value x print-format x-label-format))
+ maximize (if (equal format 'bar-chart)
+ (eplot--text-width
+ label bar-font bar-font-size bar-font-weight)
+ (eplot--text-width
+ label label-font label-font-size)))))
+ ;; Ensure that we have enough room to display the X labels
+ ;; (unless overridden).
+ (with-slots ( height margin-top ys
+ y-ticks y-tick-step y-label-step min max)
+ chart
+ (setq margin-bottom (max margin-bottom (+ width 40))
+ ys (- height margin-top margin-bottom))))))
+
+(defun eplot--compute-x-labels (chart)
+ (with-slots ( x-step-map x-ticks
+ format plots print-format x-label-format x-labels
+ x-tick-step x-label-step
+ x-label-orientation margin-bottom)
+ chart
+ ;; Make X ticks.
+ (setf x-labels
+ (cl-loop
+ for xv in (or x-step-map x-ticks)
+ for x = (if (consp xv) (car xv) xv)
+ for do-tick = (if (consp xv)
+ (nth 1 xv)
+ (zerop (e% x x-tick-step)))
+ for do-label = (if (consp xv)
+ (nth 2 xv)
+ (zerop (e% x x-label-step)))
+ for i from 0
+ for value = (and (equal format 'bar-chart)
+ (elt (slot-value (car plots) 'values) i))
+ collect (list
+ (if (equal format 'bar-chart)
+ (eplot--vs 'label
+ (plist-get value :settings)
+ ;; When we're doing bar charts, we
+ ;; want default labeling to start with
+ ;; 1 and not zero.
+ (format "%s" (1+ x)))
+ (eplot--format-value x print-format x-label-format))
+ do-tick
+ do-label)))))
+
+(defun eplot--draw-x-ticks (svg chart)
+ (with-slots ( x-step-map x-ticks format layout print-format
+ margin-left margin-right margin-top margin-bottom
+ x-min x-max xs
+ width height
+ axes-color label-color
+ grid grid-opacity grid-color
+ font x-tick-step x-label-step x-label-format x-label-orientation
+ label-font label-font-size
+ plots x-labels
+ x-values
+ bar-font bar-font-size bar-font-weight
+ plots)
+ chart
+ (let ((font label-font)
+ (font-size label-font-size)
+ (font-weight 'normal)
+ (label-settings nil))
+ (when (equal format 'bar-chart)
+ (setq font bar-font
+ font-size bar-font-size
+ font-weight bar-font-weight
+ label-settings (mapcar (lambda (e)
+ (plist-get e :settings))
+ (slot-value (car plots) 'values))))
+ ;; Make X ticks.
+ (cl-loop with label-height
+ for xv in (or x-step-map x-ticks)
+ for x = (if (consp xv) (car xv) xv)
+ for i from 0
+ for (label do-tick do-label) in x-labels
+ for px = (if (equal format 'bar-chart)
+ (+ margin-left
+ (/ (/ xs (length x-values)) 2)
+ (* (e/ i (length x-values))
+ xs))
+ (+ margin-left
+ (* (/ (- (* 1.0 x) x-min) (- x-max x-min))
+ xs)))
+ for this-font-weight =
+ (if (equal format 'bar-chart)
+ (or (cdr (assq 'label-font-weight (nth i label-settings)))
+ font-weight)
+ font-weight)
+ ;; We might have one extra stride outside the area -- don't
+ ;; draw it.
+ when (<= px (- width margin-right))
+ do
+ (when do-tick
+ ;; Draw little tick.
+ (unless (equal format 'bar-chart)
+ (svg-line svg
+ px (- height margin-bottom)
+ px (+ (- height margin-bottom)
+ (if do-label
+ 4
+ 2))
+ :stroke axes-color))
+ (when (or (eq grid 'xy) (eq grid 'x))
+ (svg-line svg px margin-top
+ px (- height margin-bottom)
+ :opacity grid-opacity
+ :stroke grid-color)))
+ (when (and do-label
+ ;; We want to skip marking the first X value
+ ;; unless we're a bar chart or we're a one
+ ;; dimensional chart.
+ (or (equal format 'bar-chart)
+ t
+ (not (= x-min (car x-values)))
+ (eq x-type 'one-dimensional)
+ (and (not (zerop x)) (not (zerop i)))))
+ (if (eq x-label-orientation 'vertical)
+ (progn
+ (unless label-height
+ ;; The X position we're putting the label at is
+ ;; based on the bottom of the lower-case
+ ;; characters. So we want to ignore descenders
+ ;; etc, so we use "xx" to determine the height
+ ;; to be able to center the text.
+ (setq label-height
+ (eplot--text-height
+ ;; If the labels are numerical, we need
+ ;; to center them using the height of
+ ;; numbers.
+ (if (string-match "^[0-9]+$" label)
+ "10"
+ ;; Otherwise center them on the baseline.
+ "xx")
+ font font-size this-font-weight)))
+ (svg-text svg label
+ :font-family font
+ :text-anchor "end"
+ :font-size font-size
+ :font-weight this-font-weight
+ :fill label-color
+ :transform
+ (format "translate(%s,%s) rotate(-90)"
+ (+ px (/ label-height 2))
+ (- height margin-bottom -10))))
+ (svg-text svg label
+ :font-family font
+ :text-anchor "middle"
+ :font-size font-size
+ :font-weight this-font-weight
+ :fill label-color
+ :x px
+ :y (+ (- height margin-bottom)
+ font-size
+ (if (equal format 'bar-chart)
+ (if (equal layout 'compact) 3 5)
+ 2)))))))))
+
+(defun eplot--stride (chart values)
+ (with-slots (xs x-type format) chart
+ (if (eq x-type 'one-dimensional)
+ (e/ xs
+ ;; Fenceposting bar-chart vs everything else.
+ (if (eq format 'bar-chart)
+ (length values)
+ (1- (length values))))
+ (e/ xs (length values)))))
+
+(defun eplot--default-p (slot data)
+ "Return non-nil if SLOT is at the default value."
+ (and (not (assq slot eplot--user-defaults))
+ (not (assq slot data))))
+
+(defun eplot--compute-y-labels (chart)
+ (with-slots ( y-ticks y-labels
+ width height min max xs ys
+ margin-top margin-bottom margin-left margin-right
+ y-tick-step y-label-step y-label-format)
+ chart
+ ;; First collect all the labels we're thinking about outputting.
+ (setq y-labels
+ (cl-loop for y in y-ticks
+ for py = (- (- height margin-bottom)
+ (* (/ (- (* 1.0 y) min) (- max min))
+ ys))
+ when (and (<= margin-top py (- height margin-bottom))
+ (zerop (e% y y-tick-step))
+ (zerop (e% y y-label-step)))
+ collect (eplot--format-y
+ y (- (cadr y-ticks) (car y-ticks)) nil
+ y-label-format)))
+ ;; Check the labels to see whether we have too many digits for
+ ;; what we're actually going to display. Man, this is a lot of
+ ;; back-and-forth and should be rewritten to be less insanely
+ ;; inefficient.
+ (when (= (seq-count (lambda (label)
+ (string-match "\\." label))
+ y-labels)
+ (length y-labels))
+ (setq y-labels
+ (cl-loop with max = (cl-loop for label in y-labels
+ maximize (eplot--decimal-digits
+ (string-to-number label)))
+ for label in y-labels
+ collect (format (if (zerop max)
+ "%d"
+ (format "%%.%df" max))
+ (string-to-number label)))))
+ (setq y-labels (cl-coerce y-labels 'vector))
+ ;; Ensure that we have enough room to display the Y labels
+ ;; (unless overridden).
+ (when (eplot--default-p 'margin-left (slot-value chart 'data))
+ (with-slots (label-font label-font-size) chart
+ (setq margin-left (max margin-left
+ (+ (eplot--text-width
+ (elt y-labels (1- (length y-labels)))
+ label-font label-font-size)
+ 10))
+ xs (- width margin-left margin-right))))))
+
+(defun eplot--draw-y-ticks (svg chart)
+ (with-slots ( y-ticks y-labels y-tick-step y-label-step label-color
+ label-font label-font-size
+ width height min max ys
+ margin-top margin-bottom margin-left margin-right
+ axes-color
+ grid grid-opacity grid-color)
+ chart
+ ;; Make Y ticks.
+ (cl-loop with lnum = 0
+ with text-height = (eplot--text-height
+ "012" label-font label-font-size)
+ for y in y-ticks
+ for i from 0
+ for py = (- (- height margin-bottom)
+ (* (/ (- (* 1.0 y) min) (- max min))
+ ys))
+ do
+ (when (and (<= margin-top py (- height margin-bottom))
+ (zerop (e% y y-tick-step)))
+ (svg-line svg margin-left py
+ (- margin-left 3) py
+ :stroke-color axes-color)
+ (when (or (eq grid 'xy) (eq grid 'y))
+ (svg-line svg margin-left py
+ (- width margin-right) py
+ :opacity grid-opacity
+ :stroke-color grid-color))
+ (when (zerop (e% y y-label-step))
+ (svg-text svg (elt y-labels lnum)
+ :font-family label-font
+ :text-anchor "end"
+ :font-size label-font-size
+ :fill label-color
+ :x (- margin-left 6)
+ :y (+ py (/ text-height 2) -1))
+ (cl-incf lnum))))))
+
+(defun eplot--text-width (text font font-size &optional font-weight)
+ (string-pixel-width
+ (propertize text 'face
+ (list :font (font-spec :family font
+ :weight (or font-weight 'normal)
+ :size font-size)))))
+
+(defvar eplot--text-size-cache (make-hash-table :test #'equal))
+
+(defun eplot--text-height (text font font-size &optional font-weight)
+ (cdr (eplot--text-size text font font-size font-weight)))
+
+(defun eplot--text-size (text font font-size font-weight)
+ (let ((key (list text font font-size font-weight)))
+ (or (gethash key eplot--text-size-cache)
+ (let ((size (eplot--text-size-1 text font font-size font-weight)))
+ (setf (gethash key eplot--text-size-cache) size)
+ size))))
+
+(defun eplot--text-size-1 (text font font-size font-weight)
+ (if (not (executable-find "convert"))
+ ;; This "default" text size is kinda bogus.
+ (cons (* (length text) font-size) font-size)
+ (let* ((size (* font-size 10))
+ (svg (svg-create size size))
+ text-size)
+ (svg-rectangle svg 0 0 size size :fill "black")
+ (svg-text svg text
+ :font-family font
+ :text-anchor "middle"
+ :font-size font-size
+ :font-weight (or font-weight 'normal)
+ :fill "white"
+ :x (/ size 2)
+ :y (/ size 2))
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (svg-print svg)
+ (let* ((file (make-temp-file "eplot" nil ".svg"))
+ (png (file-name-with-extension file ".png")))
+ (unwind-protect
+ (progn
+ (write-region (point-min) (point-max) file nil 'silent)
+ ;; rsvg-convert is 5x faster than convert when doing SVG, so
+ ;; if we have it, we use it.
+ (when (executable-find "rsvg-convert")
+ (unwind-protect
+ (call-process "rsvg-convert" nil nil nil
+ (format "--output=%s" png) file)
+ (when (file-exists-p png)
+ (delete-file file)
+ (setq file png))))
+ (erase-buffer)
+ (when (zerop (call-process "convert" nil t nil
+ "-trim" "+repage" file "info:-"))
+ (goto-char (point-min))
+ (when (re-search-forward " \\([0-9]+\\)x\\([0-9]+\\)" nil t)
+ (setq text-size
+ (cons (string-to-number (match-string 1))
+ (string-to-number (match-string 2)))))))
+ (when (file-exists-p file)
+ (delete-file file)))))
+ (or text-size
+ ;; This "default" text size is kinda bogus.
+ (cons (* (length text) font-size) font-size)))))
+
+(defun eplot--draw-legend (svg chart)
+ (with-slots ( legend plots
+ margin-left margin-top
+ font font-size font-weight
+ background-color axes-color
+ legend-color legend-background-color legend-border-color)
+ chart
+ (when (eq legend 'true)
+ (when-let ((names
+ (cl-loop for plot in plots
+ for name = (slot-value plot 'name)
+ when name
+ collect
+ (cons name (slot-value plot 'color)))))
+ (svg-rectangle svg (+ margin-left 20) (+ margin-top 20)
+ (format "%dpx"
+ (+ 10
+ (seq-max
+ (mapcar (lambda (name)
+ (eplot--text-width (car name)
+ font font-size))
+ names))))
+ (* font-size (+ (length names) 2))
+ :font-size font-size
+ :fill-color legend-background-color
+ :stroke-color legend-border-color)
+ (cl-loop for name in names
+ for i from 0
+ do (svg-text svg (car name)
+ :font-family font
+ :text-anchor "front"
+ :font-size font-size
+ :font-weight font-weight
+ :fill (or (cdr name) legend-color)
+ :x (+ margin-left 25)
+ :y (+ margin-top 40 (* i font-size))))))))
+
+(defun eplot--format-y (y spacing whole format-string)
+ (format (or format-string "%s")
+ (cond
+ ((or (= (round (* spacing 100)) 10) (= (round (* spacing 100)) 20))
+ (format "%.1f" y))
+ ((< spacing 0.01)
+ (format "%.3f" y))
+ ((< spacing 1)
+ (format "%.2f" y))
+ ((and (< spacing 1) (not (zerop (mod (* spacing 10) 1))))
+ (format "%.1f" y))
+ ((zerop (% spacing 1000000000))
+ (format "%dG" (/ y 1000000000)))
+ ((zerop (% spacing 1000000))
+ (format "%dM" (/ y 1000000)))
+ ((zerop (% spacing 1000))
+ (format "%dk" (/ y 1000)))
+ ((>= spacing 1)
+ (format "%s" y))
+ ((not whole)
+ (format "%.1f" y))
+ (t
+ (format "%s" y)))))
+
+(defun eplot--format-value (value print-format label-format)
+ (replace-regexp-in-string
+ ;; Texts in SVG collapse multiple spaces into one. So do it here,
+ ;; too, so that width calculations are correct.
+ " +" " "
+ (cond
+ ((eq print-format 'date)
+ (format-time-string
+ (or label-format "%Y-%m-%d") (eplot--days-to-time value)))
+ ((eq print-format 'year)
+ (format-time-string (or label-format "%Y") (eplot--days-to-time value)))
+ ((eq print-format 'time)
+ (format-time-string (or label-format "%H:%M:%S") value))
+ ((eq print-format 'minute)
+ (format-time-string (or label-format "%H:%M") value))
+ ((eq print-format 'hour)
+ (format-time-string (or label-format "%H") value))
+ (t
+ (format (or label-format "%s") value)))))
+
+(defun eplot--compute-x-ticks (xs x-values print-format x-label-format
+ label-font label-font-size)
+ (let* ((min (seq-min x-values))
+ (max (seq-max x-values))
+ (count (length x-values))
+ (max-print (eplot--format-value max print-format x-label-format))
+ ;; We want each label to be spaced at least as long apart as
+ ;; the length of the longest label, with room for two blanks
+ ;; in between.
+ (min-spacing (* 1.2 (eplot--text-width max-print label-font
+ label-font-size)))
+ (digits (eplot--decimal-digits (- (cadr x-values) (car x-values))))
+ (every (e/ 1 (expt 10 digits))))
+ (cond
+ ;; We have room for every X value.
+ ((< (* count min-spacing) xs)
+ (list every every))
+ ;; We have to prune X labels, but not grid lines. (We shouldn't
+ ;; have a grid line more than every 10 pixels.)
+ ((< (* count 10) xs)
+ (list every
+ (let ((label-step every))
+ (while (> (/ (- max min) label-step) (/ xs min-spacing))
+ (setq label-step (eplot--next-weed label-step)))
+ label-step)))
+ ;; We have to reduce both grid lines and labels.
+ (t
+ (let ((tick-step every))
+ (while (> (/ (- max min) tick-step) (/ xs 10))
+ (setq tick-step (eplot--next-weed tick-step)))
+ (list tick-step
+ (let ((label-step tick-step))
+ (while (> (/ (- max min) label-step) (/ xs min-spacing))
+ (setq label-step (eplot--next-weed label-step))
+ (while (not (zerop (% label-step tick-step)))
+ (setq label-step (eplot--next-weed label-step))))
+ label-step)))))))
+
+(defun eplot--compute-y-ticks (ys y-values text-height)
+ (let* ((min (car y-values))
+ (max (car (last y-values)))
+ (count (length y-values))
+ ;; We want each label to be spaced at least as long apart as
+ ;; the height of the label.
+ (min-spacing (+ text-height 10))
+ (digits (eplot--decimal-digits (- (cadr y-values) (car y-values))))
+ (every (e/ 1 (expt 10 digits))))
+ (cond
+ ;; We have room for every X value.
+ ((< (* count min-spacing) ys)
+ (list every every))
+ ;; We have to prune Y labels, but not grid lines. (We shouldn't
+ ;; have a grid line more than every 10 pixels.)
+ ((< (* count 10) ys)
+ (list every
+ (let ((label-step every))
+ (while (> (/ (- max min) label-step) (/ ys min-spacing))
+ (setq label-step (eplot--next-weed label-step)))
+ label-step)))
+ ;; We have to reduce both grid lines and labels.
+ (t
+ (let ((tick-step 1))
+ (while (> (/ count tick-step) (/ ys 10))
+ (setq tick-step (eplot--next-weed tick-step)))
+ (list tick-step
+ (let ((label-step tick-step))
+ (while (> (/ (- max min) label-step) (/ ys min-spacing))
+ (setq label-step (eplot--next-weed label-step)))
+ label-step)))))))
+
+(defvar eplot--pleasing-numbers '(1 2 5 10))
+
+(defun eplot--next-weed (weed)
+ (let (digits series)
+ (if (>= weed 1)
+ (setq digits (truncate (log weed 10))
+ series (/ weed (expt 10 digits)))
+ (setq digits (eplot--decimal-digits weed)
+ series (truncate (* weed (expt 10 digits)))))
+ (let ((next (cadr (memq series eplot--pleasing-numbers))))
+ (unless next
+ (error "Invalid weed: %s" weed))
+ (if (>= weed 1)
+ (* next (expt 10 digits))
+ (e/ next (expt 10 digits))))))
+
+(defun eplot--parse-gradient (string)
+ (when string
+ (let ((bits (split-string string)))
+ (list
+ (cons 'from (nth 0 bits))
+ (cons 'to (nth 1 bits))
+ (cons 'direction (intern (or (nth 2 bits) "top-down")))
+ (cons 'position (intern (or (nth 3 bits) "below")))))))
+
+(defun eplot--smooth (values algo xs)
+ (if (not algo)
+ values
+ (let* ((vals (cl-coerce values 'vector))
+ (max (1- (length vals)))
+ (period (* 4 (ceiling (/ max xs)))))
+ (cl-case algo
+ (moving-average
+ (cl-loop for i from 0 upto max
+ collect (e/ (cl-loop for ii from 0 upto (1- period)
+ sum (elt vals (min (+ i ii) max)))
+ period)))))))
+
+(defun eplot--vary-color (color n)
+ (let ((colors ["#e6194b" "#3cb44b" "#ffe119" "#4363d8" "#f58231" "#911eb4"
+ "#46f0f0" "#f032e6" "#bcf60c" "#fabebe" "#008080" "#e6beff"
+ "#9a6324" "#fffac8" "#800000" "#aaffc3" "#808000" "#ffd8b1"
+ "#000075" "#808080" "#ffffff" "#000000"]))
+ (unless (equal color "vary")
+ (setq colors
+ (if (string-search " " color)
+ (split-string color)
+ (list color))))
+ (elt colors (mod n (length colors)))))
+
+(defun eplot--pv (plot slot &optional default)
+ (let ((user (cdr (assq slot eplot--user-defaults))))
+ (when (and (stringp user) (zerop (length user)))
+ (setq user nil))
+ (or user (slot-value plot slot) default)))
+
+(defun eplot--draw-plots (svg chart)
+ (if (eq (slot-value chart 'format) 'horizontal-bar-chart)
+ (eplot--draw-horizontal-bar-chart svg chart)
+ (eplot--draw-normal-plots svg chart)))
+
+(defun eplot--draw-normal-plots (svg chart)
+ (with-slots ( plots chart-color height format
+ margin-bottom margin-left
+ min max xs ys
+ margin-top
+ x-values x-min x-max
+ label-font label-font-size)
+ chart
+ ;; Draw all the plots.
+ (cl-loop for plot in (reverse plots)
+ for plot-number from 0
+ for values = (slot-value plot 'values)
+ for vals = (eplot--smooth
+ (seq-map (lambda (v) (plist-get v :value)) values)
+ (slot-value plot 'smoothing)
+ xs)
+ for polygon = nil
+ for gradient = (eplot--parse-gradient (eplot--pv plot 'gradient))
+ for lpy = nil
+ for lpx = nil
+ for style = (if (eq format 'bar-chart)
+ 'bar
+ (slot-value plot 'style))
+ for bar-max-width = (eplot--pv plot 'bar-max-width)
+ for bar-width = (and (eq style 'bar)
+ (min (or bar-max-width most-positive-fixnum)
+ (/ xs (length x-values))))
+ for bar-gap = (and (eq style 'bar)
+ (if (< bar-width
+ (or bar-max-width most-positive-fixnum))
+ (* bar-width 0.1)
+ 0))
+ for clip-id = (format "url(#clip-%d)" plot-number)
+ do
+ (svg--append
+ svg
+ (dom-node 'clipPath
+ `((id . ,(format "clip-%d" plot-number)))
+ (dom-node 'rect
+ `((x . ,margin-left)
+ (y . , margin-top)
+ (width . ,xs)
+ (height . ,ys)))))
+ (unless gradient
+ (when-let ((fill (slot-value plot 'fill-color)))
+ (setq gradient `((from . ,fill) (to . ,fill)
+ (direction . top-down) (position . below)))))
+ (when gradient
+ (if (eq (eplot--vs 'position gradient) 'above)
+ (push (cons margin-left margin-top) polygon)
+ (push (cons margin-left (- height margin-bottom)) polygon)))
+ (cl-loop
+ for val in vals
+ for value in values
+ for x in x-values
+ for i from 0
+ for settings = (plist-get value :settings)
+ for color = (eplot--vary-color
+ (eplot--vs 'color settings (slot-value plot 'color))
+ i)
+ for py = (- (- height margin-bottom)
+ (* (/ (- (* 1.0 val) min) (- max min))
+ ys))
+ for px = (if (eq style 'bar)
+ (+ margin-left
+ (/ (/ xs (length x-values)) 2)
+ (* (e/ i (length x-values))
+ xs))
+ (+ margin-left
+ (* (e/ (- x x-min) (- x-max x-min))
+ xs)))
+ do
+ ;; Some data points may have texts.
+ (when-let ((text (eplot--vs 'text settings)))
+ (svg-text svg text
+ :font-family label-font
+ :text-anchor "middle"
+ :font-size label-font-size
+ :font-weight 'normal
+ :fill color
+ :x px
+ :y (- py (eplot--text-height
+ text label-font label-font-size)
+ -5)))
+ ;; You may mark certain points.
+ (when-let ((mark (eplot--vy 'mark settings)))
+ (cl-case mark
+ (cross
+ (let ((s (eplot--element-size val plot settings 3)))
+ (svg-line svg (- px s) (- py s)
+ (+ px s) (+ py s)
+ :clip-path clip-id
+ :stroke color)
+ (svg-line svg (+ px s) (- py s)
+ (- px s) (+ py s)
+ :clip-path clip-id
+ :stroke color)))
+ (otherwise
+ (svg-circle svg px py 3
+ :fill color))))
+ (cl-case style
+ (bar
+ (if (not gradient)
+ (svg-rectangle
+ svg
+ (+ (- px (e/ bar-width 2)) (e/ bar-gap 2))
+ py
+ (- bar-width bar-gap)
+ (- height margin-bottom py)
+ :clip-path clip-id
+ :fill color)
+ (let ((id (format "gradient-%s" (make-temp-name "grad"))))
+ (eplot--gradient svg id 'linear
+ (eplot--stops (eplot--vs 'from gradient)
+ (eplot--vs 'to gradient))
+ (eplot--vs 'direction gradient))
+ (svg-rectangle
+ svg
+ (+ (- px (e/ bar-width 2)) (e/ bar-gap 2))
+ py
+ (- bar-width bar-gap)
+ (- height margin-bottom py)
+ :clip-path clip-id
+ :gradient id))))
+ (impulse
+ (let ((width (eplot--element-size val plot settings 1)))
+ (if (= width 1)
+ (svg-line svg
+ px py
+ px (- height margin-bottom)
+ :clip-path clip-id
+ :stroke color)
+ (svg-rectangle svg
+ (- px (e/ width 2)) py
+ width (- height py margin-bottom)
+ :clip-path clip-id
+ :fill color))))
+ (point
+ (svg-line svg px py (1+ px) (1+ py)
+ :clip-path clip-id
+ :stroke color))
+ (line
+ ;; If we're doing a gradient, we're just collecting
+ ;; points and will draw the polygon later.
+ (if gradient
+ (push (cons px py) polygon)
+ (when lpx
+ (svg-line svg lpx lpy px py
+ :stroke-width (eplot--pv plot 'size 1)
+ :clip-path clip-id
+ :stroke color))))
+ (curve
+ (push (cons px py) polygon))
+ (square
+ (if gradient
+ (progn
+ (when lpx
+ (push (cons lpx py) polygon))
+ (push (cons px py) polygon))
+ (when lpx
+ (svg-line svg lpx lpy px lpy
+ :clip-path clip-id
+ :stroke color)
+ (svg-line svg px lpy px py
+ :clip-path clip-id
+ :stroke color))))
+ (circle
+ (svg-circle svg px py
+ (eplot--element-size val plot settings 3)
+ :clip-path clip-id
+ :stroke color
+ :fill (eplot--vary-color
+ (eplot--vs
+ 'fill-color settings
+ (or (slot-value plot 'fill-color) "none"))
+ i)))
+ (cross
+ (let ((s (eplot--element-size val plot settings 3)))
+ (svg-line svg (- px s) (- py s)
+ (+ px s) (+ py s)
+ :clip-path clip-id
+ :stroke color)
+ (svg-line svg (+ px s) (- py s)
+ (- px s) (+ py s)
+ :clip-path clip-id
+ :stroke color)))
+ (triangle
+ (let ((s (eplot--element-size val plot settings 5)))
+ (svg-polygon svg
+ (list
+ (cons (- px (e/ s 2)) (+ py (e/ s 2)))
+ (cons px (- py (e/ s 2)))
+ (cons (+ px (e/ s 2)) (+ py (e/ s 2))))
+ :clip-path clip-id
+ :stroke color
+ :fill-color
+ (or (slot-value plot 'fill-color) "none"))))
+ (rectangle
+ (let ((s (eplot--element-size val plot settings 3)))
+ (svg-rectangle svg (- px (e/ s 2)) (- py (e/ s 2))
+ s s
+ :clip-path clip-id
+ :stroke color
+ :fill-color
+ (or (slot-value plot 'fill-color) "none")))))
+ (setq lpy py
+ lpx px))
+
+ ;; We're doing a gradient of some kind (or a curve), so
+ ;; draw it now when we've collected the polygon.
+ (when polygon
+ ;; We have a "between" chart, so collect the data points
+ ;; from the "extra" values, too.
+ (when (memq 'two-values (slot-value plot 'data-format))
+ (cl-loop
+ for val in (nreverse
+ (seq-map (lambda (v) (plist-get v :extra-value))
+ values))
+ for x from (1- (length vals)) downto 0
+ for py = (- (- height margin-bottom)
+ (* (/ (- (* 1.0 val) min) (- max min))
+ ys))
+ for px = (+ margin-left
+ (* (e/ (- x x-min) (- x-max x-min))
+ xs))
+ do
+ (cl-case style
+ (line
+ (push (cons px py) polygon))
+ (square
+ (when lpx
+ (push (cons lpx py) polygon))
+ (push (cons px py) polygon)))
+ (setq lpx px lpy py)))
+ (when gradient
+ (if (eq (eplot--vs 'position gradient) 'above)
+ (push (cons lpx margin-top) polygon)
+ (push (cons lpx (- height margin-bottom)) polygon)))
+ (let ((id (format "gradient-%d" plot-number)))
+ (when gradient
+ (eplot--gradient svg id 'linear
+ (eplot--stops (eplot--vs 'from gradient)
+ (eplot--vs 'to gradient))
+ (eplot--vs 'direction gradient)))
+ (if (eq style 'curve)
+ (apply #'svg-path svg
+ (nconc
+ (cl-loop
+ with points = (cl-coerce
+ (nreverse polygon) 'vector)
+ for i from 0 upto (1- (length points))
+ collect
+ (cond
+ ((zerop i)
+ `(moveto ((,(car (elt points 0)) .
+ ,(cdr (elt points 0))))))
+ (t
+ `(curveto
+ (,(eplot--bezier
+ (eplot--pv plot 'bezier-factor)
+ i points))))))
+ (and gradient '((closepath))))
+ `( :clip-path ,clip-id
+ :stroke-width ,(eplot--pv plot 'size 1)
+ :stroke ,(slot-value plot 'color)
+ ,@(if gradient
+ `(:gradient ,id)
+ `(:fill "none"))))
+ (svg-polygon
+ svg (nreverse polygon)
+ :clip-path clip-id
+ :gradient id
+ :stroke (slot-value plot 'fill-border-color))))))))
+
+(defun eplot--element-size (value plot settings default)
+ (eplot--vn 'size settings
+ (if (slot-value plot 'size-factor)
+ (* value (slot-value plot 'size-factor))
+ (or (slot-value plot 'size) default))))
+
+(defun eplot--draw-horizontal-bar-chart (svg chart)
+ (with-slots ( plots chart-color height format
+ margin-bottom margin-left
+ min max xs ys
+ margin-top
+ x-values x-min x-max
+ label-font label-font-size label-color
+ horizontal-label-left horizontal-label-font-size)
+ chart
+ (dolist (plot plots)
+ (cl-loop with values = (slot-value plot 'values)
+ with stride = (e/ ys (length values))
+ with label-height = (eplot--text-height "xx" label-font
+ label-font-size)
+ with bar-gap = (* stride 0.1)
+ for i from 0
+ for value in values
+ for settings = (plist-get value :settings)
+ for bar-max-width = (eplot--pv plot 'bar-max-width)
+ for py = (+ margin-top (* i stride))
+ for px = (* (e/ (plist-get value :x) x-max) xs)
+ for color = (eplot--vary-color
+ (eplot--vs 'color settings (slot-value plot 'color))
+ i)
+ do
+ (svg-rectangle svg
+ margin-left
+ (+ py (e/ bar-gap 2)
+ (if (and bar-max-width
+ (< bar-max-width (- stride bar-gap)))
+ (- (/ stride 2) (/ bar-max-width 2))
+ 0))
+ px
+ (if bar-max-width
+ (min bar-max-width (- stride bar-gap))
+ (- stride bar-gap))
+ :fill color)
+ (svg-text svg (eplot--vs 'label settings)
+ :font-family label-font
+ :text-anchor "left"
+ :font-size horizontal-label-font-size
+ :font-weight 'normal
+ :fill label-color
+ :x (or horizontal-label-left 5)
+ :y (+ py label-height
+ (/ stride 2)))))))
+
+(defun eplot--stops (from to)
+ (append `((0 . ,from))
+ (cl-loop for (pct col) on (split-string to "-") by #'cddr
+ collect (if col
+ (cons (string-to-number pct) col)
+ (cons 100 pct)))))
+
+(defun eplot--gradient (svg id type stops &optional direction)
+ "Add a gradient with ID to SVG.
+TYPE is `linear' or `radial'.
+
+STOPS is a list of percentage/color pairs.
+
+DIRECTION is one of `top-down', `bottom-up', `left-right' or `right-left'.
+nil means `top-down'."
+ (svg--def
+ svg
+ (apply
+ #'dom-node
+ (if (eq type 'linear)
+ 'linearGradient
+ 'radialGradient)
+ `((id . ,id)
+ (x1 . ,(if (eq direction 'left-right) 1 0))
+ (x2 . ,(if (eq direction 'right-left) 1 0))
+ (y1 . ,(if (eq direction 'bottom-up) 1 0))
+ (y2 . ,(if (eq direction 'top-down) 1 0)))
+ (mapcar
+ (lambda (stop)
+ (dom-node 'stop `((offset . ,(format "%s%%" (car stop)))
+ (stop-color . ,(cdr stop)))))
+ stops))))
+
+(defun e% (num1 num2)
+ (let ((factor (max (expt 10 (eplot--decimal-digits num1))
+ (expt 10 (eplot--decimal-digits num2)))))
+ (% (truncate (* num1 factor)) (truncate (* num2 factor)))))
+
+(defun eplot--decimal-digits (number)
+ (- (length (replace-regexp-in-string
+ "0+\\'" ""
+ (format "%.10f" (- number (truncate number)))))
+ 2))
+
+(defun e/ (&rest numbers)
+ (if (cl-every #'integerp numbers)
+ (let ((int (apply #'/ numbers))
+ (float (apply #'/ (* 1.0 (car numbers)) (cdr numbers))))
+ (if (= int float)
+ int
+ float))
+ (apply #'/ numbers)))
+
+(defun eplot--get-ticks (min max height &optional whole)
+ (let* ((diff (abs (- min max)))
+ (even (eplot--pleasing-numbers (* (e/ diff height) 10)))
+ (factor (max (expt 10 (eplot--decimal-digits even))
+ (expt 10 (eplot--decimal-digits diff))))
+ (fmin (truncate (* min factor)))
+ (feven (truncate (* factor even)))
+ start)
+ (when whole
+ (setq even 1
+ feven factor))
+
+ (setq start
+ (cond
+ ((< min 0)
+ (+ (floor fmin)
+ feven
+ (- (% (floor fmin) feven))
+ (- feven)))
+ (t
+ (- fmin (% fmin feven)))))
+ (cl-loop for x from start upto (* max factor) by feven
+ collect (e/ x factor))))
+
+(defun eplot--days-to-time (days)
+ (days-to-time (- days (time-to-days 0))))
+
+(defun eplot--get-date-ticks (start end xs label-font label-font-size
+ x-label-format &optional skip-until)
+ (let* ((duration (- end start))
+ (limits
+ (list
+ (list (/ 368 16) 'date
+ (lambda (_d) t))
+ (list (/ 368 4) 'date
+ ;; Collect Mondays.
+ (lambda (decoded)
+ (= (decoded-time-weekday decoded) 1)))
+ (list (/ 368 2) 'date
+ ;; Collect 1st and 15th.
+ (lambda (decoded)
+ (or (= (decoded-time-day decoded) 1)
+ (= (decoded-time-day decoded) 15))))
+ (list (* 368 2) 'date
+ ;; Collect 1st of every month.
+ (lambda (decoded)
+ (= (decoded-time-day decoded) 1)))
+ (list (* 368 4) 'date
+ ;; Collect every quarter.
+ (lambda (decoded)
+ (and (= (decoded-time-day decoded) 1)
+ (memq (decoded-time-month decoded) '(1 4 7 10)))))
+ (list (* 368 8) 'date
+ ;; Collect every half year.
+ (lambda (decoded)
+ (and (= (decoded-time-day decoded) 1)
+ (memq (decoded-time-month decoded) '(1 7)))))
+ (list 1.0e+INF 'year
+ ;; Collect every Jan 1st.
+ (lambda (decoded)
+ (and (= (decoded-time-day decoded) 1)
+ (= (decoded-time-month decoded) 1)))))))
+ ;; First we collect the potential ticks.
+ (while (or (>= duration (caar limits))
+ (and skip-until (>= skip-until (caar limits))))
+ (pop limits))
+ (let* ((x-ticks (cl-loop for day from start upto end
+ for time = (eplot--days-to-time day)
+ for decoded = (decode-time time)
+ when (funcall (nth 2 (car limits)) decoded)
+ collect day))
+ (count (length x-ticks))
+ (print-format (nth 1 (car limits)))
+ (max-print (eplot--format-value (car x-ticks) print-format
+ x-label-format))
+ (min-spacing (* 1.2 (eplot--text-width max-print label-font
+ label-font-size))))
+ (cond
+ ;; We have room for every X value.
+ ((< (* count min-spacing) xs)
+ (list x-ticks print-format))
+ ;; We have to prune X labels, but not grid lines. (We shouldn't
+ ;; have a grid line more than every 10 pixels.)
+ ((< (* count 10) xs)
+ (cond
+ ((not (cdr limits))
+ (eplot--year-ticks
+ x-ticks xs label-font label-font-size x-label-format))
+ ;; The Mondays grid is special, because it doesn't resolve
+ ;; into any of the bigger limits evenly.
+ ((= (caar limits) (/ 368 4))
+ (let* ((max-print (eplot--format-value
+ (car x-ticks) print-format x-label-format))
+ (min-spacing (* 1.2 (eplot--text-width
+ max-print label-font label-font-size)))
+ (weed-factor 2))
+ (while (> (* (/ (length x-ticks) weed-factor) min-spacing) xs)
+ (setq weed-factor (* weed-factor 2)))
+ (list x-ticks 'date
+ (cl-loop for val in x-ticks
+ for i from 0
+ collect (list val t (zerop (% i weed-factor)))))))
+ (t
+ (pop limits)
+ (catch 'found
+ (while limits
+ (let ((candidate
+ (cl-loop for day in x-ticks
+ for time = (eplot--days-to-time day)
+ for decoded = (decode-time time)
+ collect (list day t
+ (not (not
+ (funcall (nth 2 (car limits))
+ decoded)))))))
+ (setq print-format (nth 1 (car limits)))
+ (let* ((max-print (eplot--format-value
+ (car x-ticks) print-format x-label-format))
+ (min-spacing (* 1.2 (eplot--text-width
+ max-print label-font
+ label-font-size)))
+ (num-labels (seq-count (lambda (v) (nth 2 v))
+ candidate)))
+ (when (and (not (zerop num-labels))
+ (< (* num-labels min-spacing) xs))
+ (throw 'found (list x-ticks print-format candidate)))))
+ (pop limits))
+ (eplot--year-ticks
+ x-ticks xs label-font label-font-size x-label-format)))))
+ ;; We have to reduce both grid lines and labels.
+ (t
+ (eplot--get-date-ticks start end xs label-font label-font-size
+ x-label-format (caar limits)))))))
+
+(defun eplot--year-ticks (x-ticks xs label-font label-font-size x-label-format)
+ (let* ((year-ticks (mapcar (lambda (day)
+ (decoded-time-year
+ (decode-time (eplot--days-to-time day))))
+ x-ticks))
+ (xv (eplot--compute-x-ticks
+ xs year-ticks 'year x-label-format label-font label-font-size)))
+ (let ((tick-step (car xv))
+ (label-step (cadr xv)))
+ (list x-ticks 'year
+ (cl-loop for year in year-ticks
+ for val in x-ticks
+ collect (list val
+ (zerop (% year tick-step))
+ (zerop (% year label-step))))))))
+
+(defun eplot--get-time-ticks (start end xs label-font label-font-size
+ x-label-format
+ &optional skip-until)
+ (let* ((duration (- end start))
+ (limits
+ (list
+ (list (* 2 60) 'time
+ (lambda (_d) t))
+ (list (* 2 60 60) 'time
+ ;; Collect whole minutes.
+ (lambda (decoded)
+ (zerop (decoded-time-second decoded))))
+ (list (* 3 60 60) 'minute
+ ;; Collect five minutes.
+ (lambda (decoded)
+ (zerop (% (decoded-time-minute decoded) 5))))
+ (list (* 4 60 60) 'minute
+ ;; Collect fifteen minutes.
+ (lambda (decoded)
+ (and (zerop (decoded-time-second decoded))
+ (memq (decoded-time-minute decoded) '(0 15 30 45)))))
+ (list (* 8 60 60) 'minute
+ ;; Collect half hours.
+ (lambda (decoded)
+ (and (zerop (decoded-time-second decoded))
+ (memq (decoded-time-minute decoded) '(0 30)))))
+ (list 1.0e+INF 'hour
+ ;; Collect whole hours.
+ (lambda (decoded)
+ (and (zerop (decoded-time-second decoded))
+ (zerop (decoded-time-minute decoded))))))))
+ ;; First we collect the potential ticks.
+ (while (or (>= duration (caar limits))
+ (and skip-until (>= skip-until (caar limits))))
+ (pop limits))
+ (let* ((x-ticks (cl-loop for time from start upto end
+ for decoded = (decode-time time)
+ when (funcall (nth 2 (car limits)) decoded)
+ collect time))
+ (count (length x-ticks))
+ (print-format (nth 1 (car limits)))
+ (max-print (eplot--format-value (car x-ticks) print-format
+ x-label-format))
+ (min-spacing (* (+ (length max-print) 2) (e/ label-font-size 2))))
+ (cond
+ ;; We have room for every X value.
+ ((< (* count min-spacing) xs)
+ (list x-ticks print-format))
+ ;; We have to prune X labels, but not grid lines. (We shouldn't
+ ;; have a grid line more than every 10 pixels.)
+ ;; If we're plotting just seconds, then just weed out some seconds.
+ ((and (< (* count 10) xs)
+ (= (caar limits) (* 2 60)))
+ (let ((xv (eplot--compute-x-ticks
+ xs x-ticks 'time x-label-format label-font label-font-size)))
+ (let ((tick-step (car xv))
+ (label-step (cadr xv)))
+ (list x-ticks 'time
+ (cl-loop for val in x-ticks
+ collect (list val
+ (zerop (% val tick-step))
+ (zerop (% val label-step))))))))
+ ;; Normal case for pruning labels, but not grid lines.
+ ((< (* count 10) xs)
+ (if (not (cdr limits))
+ (eplot--hour-ticks x-ticks xs label-font label-font-size
+ x-label-format)
+ (pop limits)
+ (catch 'found
+ (while limits
+ (let ((candidate
+ (cl-loop for val in x-ticks
+ for decoded = (decode-time val)
+ collect (list val t
+ (not (not
+ (funcall (nth 2 (car limits))
+ decoded)))))))
+ (setq print-format (nth 1 (car limits)))
+ (let ((min-spacing (* (+ (length max-print) 2)
+ (e/ label-font-size 2))))
+ (when (< (* (seq-count (lambda (v) (nth 2 v)) candidate)
+ min-spacing)
+ xs)
+ (throw 'found (list x-ticks print-format candidate)))))
+ (pop limits))
+ (eplot--hour-ticks x-ticks xs label-font label-font-size
+ x-label-format))))
+ ;; We have to reduce both grid lines and labels.
+ (t
+ (eplot--get-time-ticks start end xs label-font label-font-size
+ x-label-format (caar limits)))))))
+
+(defun eplot--hour-ticks (x-ticks xs label-font label-font-size
+ x-label-format)
+ (let* ((eplot--pleasing-numbers '(1 3 6 12))
+ (hour-ticks (mapcar (lambda (time)
+ (decoded-time-hour (decode-time time)))
+ x-ticks))
+ (xv (eplot--compute-x-ticks
+ xs hour-ticks 'year x-label-format label-font label-font-size)))
+ (let ((tick-step (car xv))
+ (label-step (cadr xv)))
+ (list x-ticks 'hour
+ (cl-loop for hour in hour-ticks
+ for val in x-ticks
+ collect (list val
+ (zerop (% hour tick-step))
+ (zerop (% hour label-step))))))))
+
+(defun eplot--int (number)
+ (cond
+ ((integerp number)
+ number)
+ ((= number (truncate number))
+ (truncate number))
+ (t
+ number)))
+
+(defun eplot--pleasing-numbers (number)
+ (let* ((digits (eplot--decimal-digits number))
+ (one (e/ 1 (expt 10 digits)))
+ (two (e/ 2 (expt 10 digits)))
+ (five (e/ 5 (expt 10 digits))))
+ (catch 'found
+ (while t
+ (when (< number one)
+ (throw 'found one))
+ (setq one (* one 10))
+ (when (< number two)
+ (throw 'found two))
+ (setq two (* two 10))
+ (when (< number five)
+ (throw 'found five))
+ (setq five (* five 10))))))
+
+(defun eplot-parse-and-insert (file)
+ "Parse and insert a file in the current buffer."
+ (interactive "fEplot file: ")
+ (let ((default-directory (file-name-directory file)))
+ (setq-local eplot--current-chart
+ (eplot--render (with-temp-buffer
+ (insert-file-contents file)
+ (eplot--parse-buffer))))))
+
+(defun eplot-list-chart-headers ()
+ "Pop to a buffer showing all chart parameters."
+ (interactive)
+ (pop-to-buffer "*eplot help*")
+ (let ((inhibit-read-only t))
+ (special-mode)
+ (erase-buffer)
+ (insert "The following headers influence the overall\nlook of the chart:\n\n")
+ (eplot--list-headers eplot--chart-headers)
+ (ensure-empty-lines 2)
+ (insert "The following headers are per plot:\n\n")
+ (eplot--list-headers eplot--plot-headers)
+ (goto-char (point-min))))
+
+(defun eplot--list-headers (headers)
+ (dolist (header (sort (copy-sequence headers)
+ (lambda (e1 e2)
+ (string< (car e1) (car e2)))))
+ (insert (propertize (capitalize (symbol-name (car header))) 'face 'bold)
+ "\n")
+ (let ((start (point)))
+ (insert (plist-get (cdr header) :doc) "\n")
+ (when-let ((valid (plist-get (cdr header) :valid)))
+ (insert "Possible values are: "
+ (mapconcat (lambda (v) (format "`%s'" v)) valid ", ")
+ ".\n"))
+ (indent-rigidly start (point) 2))
+ (ensure-empty-lines 1)))
+
+(defvar eplot--transients
+ '((("Size"
+ ("sw" "Width")
+ ("sh" "Height")
+ ("sl" "Margin-Left")
+ ("st" "Margin-Top")
+ ("sr" "Margin-Right")
+ ("sb" "Margin-Bottom"))
+ ("Colors"
+ ("ca" "Axes-Color")
+ ("cb" "Border-Color")
+ ("cc" "Chart-Color")
+ ("cf" "Frame-Color")
+ ("cs" "Surround-Color")
+ ("ct" "Title-Color"))
+ ("Background"
+ ("bc" "Background-Color")
+ ("bg" "Background-Gradient")
+ ("bi" "Background-Image-File")
+ ("bv" "Background-Image-Cover")
+ ("bo" "Background-Image-Opacity")))
+ (("General"
+ ("gt" "Title")
+ ("gf" "Font")
+ ("gs" "Font-Size")
+ ("ge" "Font-Weight")
+ ("go" "Format")
+ ("gw" "Frame-Width")
+ ("gh" "Header-File")
+ ("gi" "Min")
+ ("ga" "Max")
+ ("gm" "Mode")
+ ("gr" "Reset" eplot--reset-transient)
+ ("gv" "Save" eplot--save-transient))
+ ("Axes, Grid & Legend"
+ ("xx" "X-Title")
+ ("xy" "Y-Title")
+ ("xf" "Label-Font")
+ ("xz" "Label-Font-Size")
+ ("xs" "X-Axis-Title-Space")
+ ("xl" "X-Label-Format")
+ ("xa" "Y-Label-Format")
+ ("il" "Grid-Color")
+ ("io" "Grid-Opacity")
+ ("ip" "Grid-Position")
+ ("ll" "Legend")
+ ("lb" "Legend-Background-Color")
+ ("lo" "Legend-Border-Color")
+ ("lc" "Legend-Color"))
+ ("Plot"
+ ("ps" "Style")
+ ("pc" "Color")
+ ("po" "Data-Column")
+ ("pr" "Data-format")
+ ("pn" "Fill-Border-Color")
+ ("pi" "Fill-Color")
+ ("pg" "Gradient")
+ ("pz" "Size")
+ ("pm" "Smoothing")
+ ("pb" "Bezier-Factor")))))
+
+(defun eplot--define-transients ()
+ (cl-loop for row in eplot--transients
+ collect (cl-coerce
+ (cl-loop for column in row
+ collect
+ (cl-coerce
+ (cons (pop column)
+ (mapcar #'eplot--define-transient column))
+ 'vector))
+ 'vector)))
+
+(defun eplot--define-transient (action)
+ (list (nth 0 action)
+ (nth 1 action)
+ ;; Allow explicit commands.
+ (or (nth 2 action)
+ ;; Make a command for altering a setting.
+ (lambda ()
+ (interactive)
+ (eplot--execute-transient (nth 1 action))))))
+
+(defun eplot--execute-transient (action)
+ (with-current-buffer (or eplot--data-buffer (current-buffer))
+ (unless eplot--transient-settings
+ (setq-local eplot--transient-settings nil))
+ (let* ((name (intern (downcase action)))
+ (spec (assq name (append eplot--chart-headers eplot--plot-headers)))
+ (type (plist-get (cdr spec) :type)))
+ ;; Sanity check.
+ (unless spec
+ (error "No such header type: %s" name))
+ (setq eplot--transient-settings
+ (append
+ eplot--transient-settings
+ (list
+ (cons
+ name
+ (cond
+ ((eq type 'number)
+ (read-number (format "Value for %s (%s): " action type)))
+ ((string-match "color" (downcase action))
+ (eplot--read-color (format "Value for %s (color): " action)))
+ ((string-match "font" (downcase action))
+ (eplot--read-font-family
+ (format "Value for %s (font family): " action)))
+ ((string-match "gradient" (downcase action))
+ (eplot--read-gradient action))
+ ((string-match "file" (downcase action))
+ (read-file-name (format "File for %s: " action)))
+ ((eq type 'symbol)
+ (intern
+ (completing-read (format "Value for %s: " action)
+ (plist-get (cdr spec) :valid)
+ nil t)))
+ (t
+ (read-string (format "Value for %s (string): " action))))))))
+ (eplot-update-view-buffer))))
+
+(defun eplot--read-gradient (action)
+ (format "%s %s %s %s"
+ (eplot--read-color (format "%s from color: " action))
+ (eplot--read-color (format "%s to color: " action))
+ (completing-read (format "%s direction: " action)
+ '(top-down bottom-up left-right right-left)
+ nil t)
+ (completing-read (format "%s position: " action)
+ '(below above)
+ nil t)))
+
+(defun eplot--reset-transient ()
+ (interactive)
+ (with-current-buffer (or eplot--data-buffer (current-buffer))
+ (setq-local eplot--transient-settings nil)
+ (eplot-update-view-buffer)))
+
+(defun eplot--save-transient (file)
+ (interactive "FSave parameters to file: ")
+ (when (and (file-exists-p file)
+ (not (yes-or-no-p "File exists; overwrite? ")))
+ (user-error "Exiting"))
+ (let ((settings (with-current-buffer (or eplot--data-buffer (current-buffer))
+ eplot--transient-settings)))
+ (with-temp-buffer
+ (cl-loop for (name . value) in settings
+ do (insert (capitalize (symbol-name name)) ": "
+ (format "%s" value) "\n"))
+ (write-region (point-min) (point-max) file))))
+
+(defvar-keymap eplot-control-mode-map
+ "RET" #'eplot-control-update
+ "TAB" #'eplot-control-next-field
+ "C-<tab>" #'eplot-control-next-field
+ "<backtab>" #'eplot-control-prev-field)
+
+(define-derived-mode eplot-control-mode special-mode "eplot control"
+ (setq-local completion-at-point-functions
+ (cons 'eplot--complete-control completion-at-point-functions))
+ (add-hook 'before-change-functions #'eplot--process-text-input-before nil t)
+ (add-hook 'after-change-functions #'eplot--process-text-value nil t)
+ (add-hook 'after-change-functions #'eplot--process-text-input nil t)
+ (setq-local nobreak-char-display nil)
+ (setq truncate-lines t))
+
+(defun eplot--complete-control ()
+ ;; Complete headers names.
+ (when-let* ((input (get-text-property (point) 'input))
+ (name (plist-get input :name))
+ (spec (cdr (assq name (append eplot--plot-headers
+ eplot--chart-headers))))
+ (start (plist-get input :start))
+ (end (- (plist-get input :end) 2))
+ (completion-ignore-case t))
+ (skip-chars-backward " " start)
+ (or
+ (and (eq (plist-get spec :type) 'symbol)
+ (lambda ()
+ (let ((valid (plist-get spec :valid)))
+ (completion-in-region
+ (save-excursion
+ (skip-chars-backward "^ " start)
+ (point))
+ end
+ (mapcar #'symbol-name valid))
+ 'completion-attempted)))
+ (and (string-match "color" (symbol-name name))
+ (lambda ()
+ (completion-in-region
+ (save-excursion
+ (skip-chars-backward "^ " start)
+ (point))
+ end eplot--colors)
+ 'completion-attempted))
+ (and (string-match "\\bfile\\b" (symbol-name name))
+ (lambda ()
+ (completion-in-region
+ (save-excursion
+ (skip-chars-backward "^ " start)
+ (point))
+ end (directory-files "."))
+ 'completion-attempted))
+ (and (string-match "\\bfont\\b" (symbol-name name))
+ (lambda ()
+ (completion-in-region
+ (save-excursion
+ (skip-chars-backward "^ " start)
+ (point))
+ end
+ (eplot--font-families))
+ 'completion-attempted)))))
+
+(defun eplot--read-font-family (prompt)
+ "Prompt for a font family, possibly offering autocomplete."
+ (let ((families (eplot--font-families)))
+ (if families
+ (completing-read prompt families)
+ (read-string prompt))))
+
+(defun eplot--font-families ()
+ (when (executable-find "fc-list")
+ (let ((fonts nil))
+ (with-temp-buffer
+ (call-process "fc-list" nil t nil ":" "family")
+ (goto-char (point-min))
+ (while (re-search-forward "^\\([^,\n]+\\)" nil t)
+ (push (downcase (match-string 1)) fonts)))
+ (seq-uniq (sort fonts #'string<)))))
+
+(defun eplot-control-next-input ()
+ "Go to the next input field."
+ (interactive)
+ (when-let ((match (text-property-search-forward 'input)))
+ (goto-char (prop-match-beginning match))))
+
+(defun eplot-control-update ()
+ "Update the chart based on the current settings."
+ (interactive)
+ (let ((settings nil))
+ (save-excursion
+ (goto-char (point-min))
+ (while-let ((match (text-property-search-forward 'input)))
+ (when (equal (get-text-property (prop-match-beginning match) 'face)
+ 'eplot--input-changed)
+ (let* ((name (plist-get (prop-match-value match) :name))
+ (spec (cdr (assq name (append eplot--plot-headers
+ eplot--chart-headers))))
+ (value
+ (or (plist-get (prop-match-value match) :value)
+ (plist-get (prop-match-value match) :original-value))))
+ (setq value (string-trim (string-replace "\u00A0" " " value)))
+ (push (cons name
+ (cl-case (plist-get spec :type)
+ (number
+ (string-to-number value))
+ (symbol
+ (intern (downcase value)))
+ (symbol-list
+ (mapcar #'intern (split-string (downcase value))))
+ (t
+ value)))
+ settings)))))
+ (with-current-buffer eplot--data-buffer
+ (setq-local eplot--transient-settings (nreverse settings))
+ (eplot-update-view-buffer))))
+
+(defvar eplot--column-width nil)
+
+(defun eplot-create-controls ()
+ "Pop to a buffer that lists all parameters and allows editing."
+ (interactive)
+ (with-current-buffer (or eplot--data-buffer (current-buffer))
+ (let ((settings eplot--transient-settings)
+ (data-buffer (current-buffer))
+ (chart eplot--current-chart)
+ ;; Find the max width of all the different names.
+ (width (seq-max
+ (mapcar (lambda (e)
+ (length (cadr e)))
+ (apply #'append
+ (mapcar #'cdr
+ (apply #'append eplot--transients))))))
+ (transients (mapcar #'copy-sequence
+ (copy-sequence eplot--transients))))
+ (unless chart
+ (user-error "Must be called from an eplot buffer that has rendered a chart"))
+ ;; Rearrange the transients a bit for better display.
+ (let ((size (caar transients)))
+ (setcar (car transients) (caadr transients))
+ (setcar (cadr transients) size))
+ (pop-to-buffer "*eplot controls*")
+ (unless (eq major-mode 'eplot-control-mode)
+ (eplot-control-mode))
+ (setq-local eplot--data-buffer data-buffer
+ eplot--column-width (+ width 12 2))
+ (let ((inhibit-read-only t)
+ (before-change-functions nil)
+ (after-change-functions nil))
+ (erase-buffer)
+ (cl-loop for column in transients
+ for cn from 0
+ do
+ (goto-char (point-min))
+ (end-of-line)
+ (cl-loop
+ for row in column
+ do
+ (if (zerop cn)
+ (when (not (bobp))
+ (insert (format (format "%%-%ds" (+ width 14)) "")
+ "\n"))
+ (unless (= (count-lines (point-min) (point)) 1)
+ (if (eobp)
+ (progn
+ (insert (format (format "%%-%ds" (+ width 14)) "")
+ "\n")
+ (insert (format (format "%%-%ds" (+ width 14)) "")
+ "\n")
+ (forward-line -1)
+ (end-of-line))
+ (forward-line 1)
+ (end-of-line))))
+ ;; If we have a too-long input in the first column,
+ ;; then go to the next line.
+ (when (and (> cn 0)
+ (> (- (point) (pos-bol))
+ (+ width 12 2)))
+ (forward-line 1)
+ (end-of-line))
+ (insert (format (format "%%-%ds" (+ width 14))
+ (propertize (pop row) 'face 'bold)))
+ (if (looking-at "\n")
+ (forward-line 1)
+ (insert "\n"))
+ (cl-loop
+ for elem in row
+ for name = (cadr elem)
+ for slot = (intern (downcase name))
+ when (null (nth 2 elem))
+ do
+ (let* ((object (if (assq slot eplot--chart-headers)
+ chart
+ (car (slot-value chart 'plots))))
+ (value (format "%s"
+ (or (cdr (assq slot settings))
+ (if (not (slot-boundp object slot))
+ ""
+ (or (slot-value object slot)
+ ""))))))
+ (end-of-line)
+ ;; If we have a too-long input in the first column,
+ ;; then go to the next line.
+ (when (and (> cn 0)
+ (> (- (point) (pos-bol))
+ (+ width 12 2)))
+ (forward-line 1)
+ (end-of-line))
+ (when (and (> cn 0)
+ (bolp))
+ (insert (format (format "%%-%ds" (+ width 14)) "") "\n")
+ (forward-line -1)
+ (end-of-line))
+ (insert (format (format "%%-%ds" (1+ width)) name))
+ (eplot--input slot value
+ (if (cdr (assq slot settings))
+ 'eplot--input-changed
+ 'eplot--input-default))
+ (if (looking-at "\n")
+ (forward-line 1)
+ (insert "\n")))))))
+ (goto-char (point-min)))))
+
+(defface eplot--input-default
+ '((t :background "#505050"
+ :foreground "#a0a0a0"
+ :box (:line-width 1)))
+ "Face for eplot default inputs.")
+
+(defface eplot--input-changed
+ '((t :background "#505050"
+ :foreground "white"
+ :box (:line-width 1)))
+ "Face for eplot changed inputs.")
+
+(defvar-keymap eplot--input-map
+ :full t :parent text-mode-map
+ "RET" #'eplot-control-update
+ "TAB" #'eplot-input-complete
+ "C-a" #'eplot-move-beginning-of-input
+ "C-e" #'eplot-move-end-of-input
+ "C-k" #'eplot-kill-input
+ "C-<tab>" #'eplot-control-next-field
+ "<backtab>" #'eplot-control-prev-field)
+
+(defun eplot-input-complete ()
+ "Complete values in inputs."
+ (interactive)
+ (cond
+ ((let ((completion-fail-discreetly t))
+ (completion-at-point))
+ ;; Completion was performed; nothing else to do.
+ nil)
+ ((not (get-text-property (point) 'input))
+ (eplot-control-next-input))
+ (t
+ (user-error "No completion in this field"))))
+
+(defun eplot-move-beginning-of-input ()
+ "Move to the start of the current input field."
+ (interactive)
+ (if (= (point) (eplot--beginning-of-field))
+ (goto-char (pos-bol))
+ (goto-char (eplot--beginning-of-field))))
+
+(defun eplot-move-end-of-input ()
+ "Move to the end of the current input field."
+ (interactive)
+ (let ((input (get-text-property (point) 'input)))
+ (if (or (not input)
+ (= (point) (1- (plist-get input :end))))
+ (goto-char (pos-eol))
+ (goto-char (1+ (eplot--end-of-field))))))
+
+(defun eplot-control-next-field ()
+ "Move to the beginning of the next field."
+ (interactive)
+ (let ((input (get-text-property (point) 'input))
+ (start (point)))
+ (when input
+ (goto-char (plist-get input :end)))
+ (let ((match (text-property-search-forward 'input)))
+ (if match
+ (goto-char (prop-match-beginning match))
+ (goto-char start)
+ (user-error "No next field")))))
+
+(defun eplot-control-prev-field ()
+ "Move to the beginning of the previous field."
+ (interactive)
+ (let ((input (get-text-property (point) 'input))
+ (start (point)))
+ (when input
+ (goto-char (plist-get input :start))
+ (unless (bobp)
+ (forward-char -1)))
+ (let ((match (text-property-search-backward 'input)))
+ (unless match
+ (goto-char start)
+ (user-error "No previous field")))))
+
+(defun eplot-kill-input ()
+ "Remove the part of the input after point."
+ (interactive)
+ (let ((end (1+ (eplot--end-of-field))))
+ (kill-new (string-trim (buffer-substring (point) end)))
+ (delete-region (point) end)))
+
+(defun eplot--input (name value face)
+ (let ((start (point))
+ input)
+ (insert value)
+ (when (< (length value) 11)
+ (insert (make-string (- 11 (length value)) ?\u00A0)))
+ (put-text-property start (point) 'face face)
+ (put-text-property start (point) 'inhibit-read-only t)
+ (put-text-property start (point) 'input
+ (setq input
+ (list :name name
+ :size 11
+ :is-default (eq face 'eplot--input-default)
+ :original-value value
+ :original-face face
+ :start (set-marker (make-marker) start)
+ :value value)))
+ (put-text-property start (point) 'local-map eplot--input-map)
+ ;; This seems like a NOOP, but redoing the properties like this
+ ;; somehow makes `delete-region' work better.
+ (set-text-properties start (point) (text-properties-at start))
+ (insert (propertize " " 'face face
+ 'input input
+ 'inhibit-read-only t
+ 'local-map eplot--input-map))
+ (plist-put input :end (point-marker))
+ (insert " ")))
+
+(defun eplot--end-of-field ()
+ (- (plist-get (get-text-property (point) 'input) :end) 2))
+
+(defun eplot--beginning-of-field ()
+ (plist-get (get-text-property (point) 'input) :start))
+
+(defvar eplot--prev-deletion nil)
+
+(defun eplot--process-text-input-before (beg end)
+ (message "Before: %s %s" beg end)
+ (cond
+ ((= beg end)
+ (setq eplot--prev-deletion nil))
+ ((> end beg)
+ (setq eplot--prev-deletion (buffer-substring beg end)))))
+
+(defun eplot--process-text-input (beg end _replace-length)
+ ;;(message "After: %s %s %s %s" beg end replace-length eplot--prev-deletion)
+ (when-let ((props (if eplot--prev-deletion
+ (text-properties-at 0 eplot--prev-deletion)
+ (if (get-text-property end 'input)
+ (text-properties-at end)
+ (text-properties-at beg))))
+ (input (plist-get props 'input)))
+ ;; The action concerns something in the input field.
+ (let ((buffer-undo-list t)
+ (inhibit-read-only t)
+ (size (plist-get input :size)))
+ (save-excursion
+ (set-text-properties beg (- (plist-get input :end) 2) props)
+ (goto-char (1- (plist-get input :end)))
+ (let* ((remains (- (point) (plist-get input :start) 1))
+ (trim (- size remains 1)))
+ (if (< remains size)
+ ;; We need to add some padding.
+ (insert (apply #'propertize (make-string trim ?\u00A0)
+ props))
+ ;; We need to delete some padding, but only delete
+ ;; spaces at the end.
+ (setq trim (abs trim))
+ (while (and (> trim 0)
+ (eql (char-after (1- (point))) ?\u00A0))
+ (delete-region (1- (point)) (point))
+ (cl-decf trim))
+ (when (> trim 0)
+ (eplot--possibly-open-column)))))
+ ;; We re-set the properties so that they are continguous. This
+ ;; somehow makes the machinery that decides whether we can kill
+ ;; a word work better.
+ (set-text-properties (plist-get input :start)
+ (1- (plist-get input :end)) props)
+ ;; Compute what the value is now.
+ (let ((value (buffer-substring-no-properties
+ (plist-get input :start)
+ (plist-get input :end))))
+ (when (string-match "\u00A0+\\'" value)
+ (setq value (substring value 0 (match-beginning 0))))
+ (plist-put input :value value)))))
+
+(defun eplot--possibly-open-column ()
+ (save-excursion
+ (when-let ((input (get-text-property (point) 'input)))
+ (goto-char (plist-get input :end)))
+ (unless (looking-at " *\n")
+ (skip-chars-forward " ")
+ (while (not (eobp))
+ (let ((text (buffer-substring (point) (pos-eol))))
+ (delete-region (point) (pos-eol))
+ (forward-line 1)
+ (if (eobp)
+ (insert (make-string eplot--column-width ?\s) text "\n")
+ (forward-char eplot--column-width)
+ (if (get-text-property (point) 'input)
+ (forward-line 1)
+ (insert text)
+ ;; We have to fix up the markers.
+ (save-excursion
+ (let* ((match (text-property-search-backward 'input))
+ (input (prop-match-value match)))
+ (plist-put input :start
+ (set-marker (plist-get input :start)
+ (prop-match-beginning match)))
+ (plist-put input :end
+ (set-marker (plist-get input :end)
+ (+ (prop-match-end match) 1))))))))))))
+
+(defun eplot--process-text-value (beg _end _replace-length)
+ (when-let* ((input (get-text-property beg 'input)))
+ (let ((inhibit-read-only t))
+ (when (plist-get input :is-default)
+ (put-text-property (plist-get input :start)
+ (plist-get input :end)
+ 'face
+ (if (equal (plist-get input :original-value)
+ (plist-get input :value))
+ 'eplot--input-default
+ 'eplot--input-changed))))))
+
+(defun eplot--read-color (prompt)
+ "Read an SVG color."
+ (completing-read prompt eplot--colors))
+
+(eval `(transient-define-prefix eplot-customize ()
+ "Customize Chart"
+ ,@(eplot--define-transients)))
+
+(defun eplot--bezier (factor i points)
+ (cl-labels ((padd (p1 p2)
+ (cons (+ (car p1) (car p2)) (+ (cdr p1) (cdr p2))))
+ (psub (p1 p2)
+ (cons (- (car p1) (car p2)) (- (cdr p1) (cdr p2))))
+ (pscale (factor point)
+ (cons (* factor (car point)) (* factor (cdr point)))))
+ (let* ((start (elt points (1- i)))
+ (end (elt points i))
+ (prev (if (< (- i 2) 0)
+ start
+ (elt points (- i 2))))
+ (next (if (> (1+ i) (1- (length points)))
+ end
+ (elt points (1+ i))))
+ (start-control-point
+ (padd start (pscale factor (psub end prev))))
+ (end-control-point
+ (padd end (pscale factor (psub start next)))))
+ (list (car start-control-point)
+ (cdr start-control-point)
+ (car end-control-point)
+ (cdr end-control-point)
+ (car end)
+ (cdr end)))))
+
+;;; CSV Parsing Stuff.
+
+(defun eplot--csv-buffer-p ()
+ (save-excursion
+ (goto-char (point-min))
+ (let ((min 1.0e+INF)
+ (max -1.0e+INF)
+ (total 0)
+ (lines 0))
+ (while (not (eobp))
+ (let ((this 0))
+ (while (search-forward "," (pos-eol) t)
+ (cl-incf total)
+ (cl-incf this))
+ (forward-line 1)
+ (cl-incf lines)
+ (setq min (min min this)
+ max (max max this))))
+ (let ((mid (e/ total lines)))
+ ;; If we have a comma on each line, and it's fairly evenly
+ ;; distributed, it's a CSV buffer.
+ (and (>= min 1)
+ (< (* mid 0.9) min)
+ (> (* mid 1.1) max))))))
+
+(defun eplot--numericalp (value)
+ (string-match-p "\\`[-.0-9]*\\'" value))
+
+(defun eplot--numberish (value)
+ (if (or (zerop (length value))
+ (not (eplot--numericalp value)))
+ value
+ (string-to-number value)))
+
+(defun eplot--parse-csv-buffer ()
+ (unless (fboundp 'pcsv-parse-buffer)
+ (user-error "You need to install the pcsv package to parse CSV files"))
+ (let ((csv (and (fboundp 'pcsv-parse-buffer)
+ ;; This repeated check is just to silence the byte
+ ;; compiler.
+ (pcsv-parse-buffer)))
+ names)
+ ;; Check whether the first line looks like a header.
+ (when (and (length> csv 1)
+ ;; The second line is all numbers...
+ (cl-every #'eplot--numericalp (nth 1 csv))
+ ;; .. and the first line isn't.
+ (not (cl-every #'eplot--numericalp (nth 0 csv))))
+ (setq names (pop csv)))
+ (list
+ (cons 'legend (and names "true"))
+ (cons :plots
+ (cl-loop
+ for column from 1 upto (1- (length (car csv)))
+ collect
+ (list (cons :headers
+ (list
+ (cons 'name (elt names column))
+ (cons 'data-format
+ (cond
+ ((cl-every (lambda (e) (<= (length e) 4))
+ (mapcar #'car csv))
+ "year")
+ ((cl-every (lambda (e) (= (length e) 8))
+ (mapcar #'car csv))
+ "date")
+ (t
+ "number")))
+ (cons 'color (eplot--vary-color "vary" (1- column)))))
+ (cons
+ :values
+ (cl-loop for line in csv
+ collect (list :x (eplot--numberish (car line))
+ :value (eplot--numberish
+ (elt line column)))))))))))
+
+(declare-function org-element-parse-buffer "org-element")
+
+(defun eplot--parse-org-buffer ()
+ (require 'org-element)
+ (let* ((table (nth 2 (nth 2 (org-element-parse-buffer))))
+ (columns (cl-loop for cell in (nthcdr 2 (nth 2 table))
+ collect (substring-no-properties (nth 2 cell))))
+ (value-column (or (seq-position columns "value") 0))
+ (date-column (seq-position columns "date")))
+ `((:plots
+ ((:headers
+ ,@(and date-column '((data-format . "date"))))
+ (:values
+ ,@(cl-loop for row in (nthcdr 4 table)
+ collect
+ (let ((cells (cl-loop for cell in (nthcdr 2 row)
+ collect (substring-no-properties
+ (nth 2 cell)))))
+ (list :value (string-to-number (elt cells value-column))
+ :x (string-to-number
+ (replace-regexp-in-string
+ "[^0-9]" "" (elt cells date-column)))
+ )))))))))
+
+(provide 'eplot)
+
+;;; eplot.el ends here
diff --git a/custom/gptel-prompts.el b/custom/gptel-prompts.el
new file mode 100644
index 00000000..a2b266f2
--- /dev/null
+++ b/custom/gptel-prompts.el
@@ -0,0 +1,418 @@
+;;; gptel-prompts.el --- GPTel directive management using files -*- lexical-binding: t -*-
+
+;; Copyright (C) 2025 John Wiegley
+
+;; Author: John Wiegley <johnw@gnu.org>
+;; Created: 19 May 2025
+;; Version: 1.0
+;; Keywords: ai gptel prompts
+;; X-URL: https://github.com/jwiegley/dot-emacs
+;; Package-Requires: ((emacs "24.1"))
+
+;; This file is NOT part of GNU Emacs.
+
+;;; License:
+
+;; 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 2, 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 GNU Emacs; see the file COPYING. If not, write to the
+;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+;; Boston, MA 02111-1307, USA.
+
+;;; Commentary:
+
+;; This package provides enhanced prompt management capabilities for GPTel,
+;; allowing you to organize and dynamically load AI prompts from external
+;; files rather than hardcoding them in your Emacs configuration.
+
+;; Key Features:
+;;
+;; * Multi-format prompt support: Load prompts from .txt, .md, .org, .json,
+;; .eld (Emacs Lisp data), .el (Emacs Lisp functions), and .poet/.jinja
+;; (Prompt Poet/Jinja2 templates)
+;;
+;; * Template interpolation: Use Jinja2-style {{variable}} syntax with
+;; customizable variables and dynamic functions
+;;
+;; * File watching: Automatically reload prompts when files change
+;;
+;; * Project-aware prompts: Automatically load project-specific conventions
+;; from CONVENTIONS.md or CLAUDE.md files
+;;
+;; * Conversation format support: Handle multi-turn conversations with
+;; system/user/assistant roles
+
+;; Setup:
+;;
+;; (use-package gptel-prompts
+;; :after (gptel)
+;; :custom
+;; (gptel-prompts-directory "~/my-prompts")
+;; :config
+;; (gptel-prompts-update)
+;; ;; Optional: auto-reload on file changes
+;; (gptel-prompts-add-update-watchers))
+
+;; File Formats:
+;;
+;; * Plain text (.txt, .md, .org): Used as-is for system prompts
+;; * JSON (.json): Array of {role: "system/user/assistant", content: "..."}
+;; * Emacs Lisp data (.eld): List format for conversations
+;; * Emacs Lisp code (.el): Lambda functions for dynamic prompts
+;; * Prompt Poet (.poet, .j2, .jinja, .jinja2): YAML + Jinja2 templates
+
+;; Template Variables:
+;;
+;; Use {{variable_name}} in your prompts. Variables can be defined in
+;; `gptel-prompts-template-variables' or generated dynamically by functions
+;; in `gptel-prompts-template-functions'.
+
+;; Project Integration:
+;;
+;; Add `gptel-prompts-project-conventions' to `gptel-directives' to
+;; automatically load project-specific prompts from CONVENTIONS.md or
+;; CLAUDE.md files in your project root.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'cl-macs)
+(require 'rx)
+(require 'filenotify)
+(require 'gptel)
+
+(defgroup gptel-prompts nil
+ "Helper library for managing GPTel prompts (aka directives)."
+ :group 'gptel)
+
+(defcustom gptel-prompts-directory "~/.emacs.d/prompts"
+ "*Directory where GPTel prompts are defined, one per file.
+
+Note that files can be of different types, which will cause them to be
+represented as directives differently. See `gptel-prompts-file-regexp'
+for more information."
+ :type 'file
+ :group 'gptel-prompts)
+
+(defcustom gptel-prompts-file-regexp
+ (rx "." (group
+ (or "txt"
+ "md"
+ "org"
+ "eld"
+ "el"
+ (seq "j" (optional "inja") (optional "2"))
+ "poet"
+ "json"))
+ string-end)
+ "*Directory where GPTel prompts are defined, one per file.
+
+Note that files can be of different types, which will cause them
+to be represented as directives differently:
+
+ .txt, .md, .org Purely textual prompts that are used as-is
+ .eld Must be a Lisp list represent a conversation:
+ SYSTEM, USER, ASSISTANT, [USER, ASSISTANT, ...]
+ .el Must evaluate to a Lisp function
+ .poet See https://github.com/character-ai/prompt-poet
+ .json JSON list of role-assigned prompts"
+ :type 'regexp
+ :group 'gptel-prompts)
+
+(defcustom gptel-prompts-template-variables nil
+ "*An alist of names to strings used during template expansion.
+
+Example:
+ ((\"name\" . \"John\")
+ (\"hobbies\" . \"Emacs\"))
+
+These would referred to using {{ name }} and {{ hobbies }} in the
+prompt template."
+ :type '(alist :key-type string :value-type string)
+ :group 'gptel-prompts)
+
+(defcustom gptel-prompts-template-functions
+ '(gptel-prompts-add-current-time)
+ "*Set of functions run when a template prompt is used.
+
+These are called when the template is going to be used by
+`gptel-request'. Each function receives the name of the template file,
+and must return either nil or an alist of variable values to prepend to
+`gptel-prompts-template-variables'. See that variable's documentation
+for the expected format."
+ :type '(list function)
+ :group 'gptel-prompts)
+
+(defun gptel-prompts-process-prompts (prompts)
+ "Convert from a list of PROMPTS in dialog format, to GPTel.
+
+For example:
+
+ (((role . \"system\")
+ (content . \"Sample\")
+ (name . \"system instructions\"))
+ ((role . \"system\")
+ (content . \"Sample\")
+ (name . \"further system instructions\"))
+ ((role . \"user\")
+ (content . \"Sample\")
+ (name . \"User message\"))
+ ((role . \"assistant\")
+ (content . \"Sample\")
+ (name . \"Model response\"))
+ ((role . \"user\")
+ (content . \"Sample\")
+ (name . \"Second user message\")))
+
+Becomes:
+
+ (\"system instructions\nfurther system instructions\"
+ (prompt \"User message\")
+ (response \"Model response\")
+ (prompt \"Second user message\"))"
+ (let ((system "") result)
+ (dolist (prompt prompts)
+ (let ((content (alist-get 'content prompt))
+ (role (alist-get 'role prompt)))
+ (cond
+ ((string= role "system")
+ (setq system (if (string-empty-p system)
+ content
+ (concat system "\n" content))))
+ ((string= role "user")
+ (setq result (cons (list 'prompt content) result)))
+ ((string= role "assistant")
+ (setq result (cons (list 'response content) result)))
+ ((string= role "tool")
+ (error "Tools not yet supported in Poet prompts"))
+ (t
+ (error "Role not recognized in prompt: %s"
+ (pp-to-string prompt))))))
+ (cons system (nreverse result))))
+
+(defun gptel-prompts-interpolate (prompt &optional file)
+ "Expand Jinja-style references to `gptel-prompts-template-variables'.
+The references are expected in the string PROMPT, possibly from FILE.
+`gptel-prompts-template-functions' are called to add to this list as
+well, so some variables can be dynamic in nature."
+ (require 'templatel)
+ (let ((vars (apply #'append
+ (mapcar #'(lambda (f) (funcall f file))
+ gptel-prompts-template-functions))))
+ (templatel-render-string
+ prompt
+ (cl-remove-duplicates
+ (append vars gptel-prompts-template-variables)
+ :test #'string= :from-end t :key #'car))))
+
+(defun gptel-prompts-interpolate-buffer ()
+ "Expand Jinja-style references to `gptel-prompts-template-variables'.
+See `gptel-prompts-interpolate'.
+This function can be added to `gptel-prompt-transform-functions'."
+ (let ((replacement (gptel-prompts-interpolate (buffer-string))))
+ (delete-region (point-min) (point-max))
+ (insert replacement)))
+
+(defun gptel-prompts-poet (file)
+ "Read Yaml + Jinja FILE in prompt-poet format."
+ (require 'yaml)
+ (gptel-prompts-process-prompts
+ (mapcar #'yaml--hash-table-to-alist
+ (yaml-parse-string
+ (gptel-prompts-interpolate
+ (with-temp-buffer
+ (insert-file-contents file)
+ (buffer-string))
+ file)))))
+
+(defun gptel-prompts-process-file (file)
+ "Process FILE and return appropriate content.
+
+FILE is a string path to the file to be processed.
+
+Handles different file types based on extension:
+- .eld files: Read as Emacs Lisp data, must evaluate to a list
+- .el files: Read as Emacs Lisp code, must evaluate to a function/lambda
+- .json files: Parse as JSON array and process as prompts via
+ `gptel-prompts-process-prompts'
+- .j2/.jinja/.jinja2/.poet files: Return lambda that calls
+ `gptel-prompts-poet' with FILE
+- Other files: Return trimmed file contents as plain text string
+
+Returns the processed content in the appropriate format for each file
+type. Signals an error if the file content doesn't match expected format
+for typed files."
+ (cond ((string-match "\\.eld\\'" file)
+ (with-temp-buffer
+ (insert-file-contents file)
+ (goto-char (point-min))
+ (let ((lst (read (current-buffer))))
+ (if (listp lst)
+ lst
+ (error "Emacs Lisp data prompts must evaluate to a list")))))
+ ((string-match "\\.el\\'" file)
+ (with-temp-buffer
+ (insert-file-contents file)
+ (goto-char (point-min))
+ (let ((func (read (current-buffer))))
+ (if (and (functionp func)
+ (listp func)
+ (eq 'lambda (car func)))
+ func
+ (error "Emacs Lisp prompts must evaluate to a function/lambda")))))
+ ((string-match "\\.json\\'" file)
+ (with-temp-buffer
+ (insert-file-contents file)
+ (goto-char (point-min))
+ (let ((conversation (json-read)))
+ (if (vectorp conversation)
+ (gptel-prompts-process-prompts (seq-into conversation 'list))
+ (error "Emacs Lisp prompts must evaluate to a list")))))
+ ((string-match "\\.\\(j\\(inja\\)?2?\\|poet\\)\\'" file)
+ `(lambda () (gptel-prompts-poet ,file)))
+ (t
+ (with-temp-buffer
+ (insert-file-contents file)
+ (string-trim (buffer-string))))))
+
+(defun gptel-prompts-read-directory (dir)
+ "Read prompts from directory DIR and establish them in `gptel-directives'."
+ (cl-loop for file in (directory-files dir t gptel-prompts-file-regexp)
+ collect (cons (intern (file-name-sans-extension
+ (file-name-nondirectory file)))
+ (gptel-prompts-process-file file))))
+
+(defun gptel-prompts-update ()
+ "Update `gptel-directives' from files in `gptel-prompts-directory'."
+ (interactive)
+ (dolist (prompt (gptel-prompts-read-directory gptel-prompts-directory))
+ (setq gptel-directives
+ (cl-delete-if #'(lambda (x) (eq (car x) (car prompt)))
+ gptel-directives))
+ (add-to-list 'gptel-directives prompt)))
+
+(defun gptel-prompts-add-current-time (_file)
+ "Add the current time as a variable for Poet interpolation."
+ `(("current_time" . ,(format-time-string "%F %T"))))
+
+(defun gptel-prompts-add-update-watchers ()
+ "Watch all files in DIR and run CALLBACK when any is modified."
+ (let ((watches (list (file-notify-add-watch
+ gptel-prompts-directory '(change)
+ #'(lambda (&rest _events)
+ (gptel-prompts-update))))))
+ (dolist (file (directory-files gptel-prompts-directory
+ t gptel-prompts-file-regexp))
+ (when (file-regular-p file)
+ (push (file-notify-add-watch file '(change)
+ #'(lambda (&rest _events)
+ (gptel-prompts-update)))
+ watches)))
+ watches))
+
+(defvar gptel-prompts--project-conventions-alist nil
+ "Alist mapping projects to project conventions for LLMs.")
+
+(defcustom gptel-prompts-project-files
+ '("CONVENTIONS.md"
+ "CLAUDE.md"
+ "AGENTS.md"
+ (".github" . "copilot-instructions\\.md")
+ (".instructions.d" . "^.*\\.md$")
+ ".instructions.md")
+ "A list of files or directories with prompts for the current project.
+Entries can be strings (file/directory names) or cons cells where the
+CAR is a directory path and the CDR is either a regexp string or a
+filter function for selecting which files in that directory should be
+chosen.
+
+The first matching rule in the list for a given project is used, with
+the rest ignored.
+
+If a directory is specified without a filter (as a plain string), all
+markdown files within it will be aggregated into a single prompt."
+ :type '(repeat (choice file directory
+ (cons directory (choice regexp function))))
+ :group 'gptel-prompts)
+
+(defun gptel-prompts--read-directory-filtered (dir regexp-or-function)
+ "Read files from DIR for which REGEXP-OR-FUNCTION is a match."
+ (when (and (file-directory-p dir)
+ (file-readable-p dir))
+ (let ((files
+ (cl-remove-if-not
+ (cond
+ ((functionp regexp-or-function)
+ (lambda (f)
+ (funcall regexp-or-function (file-name-nondirectory f))))
+ ((stringp regexp-or-function)
+ (lambda (f)
+ (string-match-p regexp-or-function (file-name-nondirectory f))))
+ (t (error "Invalid filter: %s" regexp-or-function)))
+ (directory-files dir t "^[^.].*" t))))
+ (unless (null files)
+ (mapconcat
+ (lambda (file)
+ (when (and (file-regular-p file)
+ (file-readable-p file))
+ (with-temp-buffer
+ (insert-file-contents file)
+ (buffer-string))))
+ files "\n\n")))))
+
+(defun gptel-prompts--read-directory (dir)
+ "Read all Markdown files from DIR, concated together."
+ (let ((contents
+ (mapconcat
+ (lambda (file)
+ (when (and (file-regular-p file)
+ (file-readable-p file))
+ (with-temp-buffer
+ (insert-file-contents file)
+ (buffer-string))))
+ (directory-files dir t "^[^.].*\\.md$" t)
+ "\n\n")))
+ (unless (string-empty-p contents)
+ contents)))
+
+(defun gptel-prompts-project-conventions ()
+ "System prompt is obtained from project CONVENTIONS.
+This function should be added to `gptel-directives'. To replace
+the default directive, use:
+
+ (setf (alist-get \\'default gptel-directives)
+ #\\'gptel-project-conventions)"
+ (when-let* ((project (project-current))
+ (root (project-root project)))
+ (with-memoization
+ (alist-get root gptel-prompts--project-conventions-alist
+ nil nil #'equal)
+ (or (cl-loop
+ for item in gptel-prompts-project-files
+ for path = (expand-file-name
+ (if (consp item) (car item) item)
+ root)
+ when (file-readable-p path)
+ return (cond
+ ((consp item)
+ (gptel-prompts--read-directory-filtered (car item) (cdr item)))
+ ((file-directory-p path)
+ (gptel-prompts--read-directory path))
+ (t
+ (with-temp-buffer
+ (insert-file-contents path)
+ (buffer-string)))))
+ "You are a helpful assistant. Respond concisely."))))
+
+(provide 'gptel-prompts)
+
+;;; gptel-prompts.el ends here
diff --git a/custom/org-checklist.el b/custom/org-checklist.el
new file mode 100644
index 00000000..e7d9b468
--- /dev/null
+++ b/custom/org-checklist.el
@@ -0,0 +1,153 @@
+;;; org-checklist.el --- org functions for checklist handling -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2008-2014, 2021 James TD Smith
+
+;; Author: James TD Smith (@ ahktenzero (. mohorovi cc))
+;; Version: 1.0
+;; Keywords: org, checklists
+;;
+;; This file is not part of GNU 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, 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 GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file provides some functions for handing repeated tasks which involve
+;; checking off a list of items. By setting the RESET_CHECK_BOXES property in an
+;; item, when the TODO state is set to done all checkboxes under that item are
+;; cleared. If the LIST_EXPORT_BASENAME property is set, a file will be created
+;; using the value of that property plus a timestamp, containing all the items
+;; in the list which are not checked. Additionally the user will be prompted to
+;; print the list.
+;;
+;; I use this for to keep track of stores of various things (food stores,
+;; components etc) which I check periodically and use the exported list of items
+;; which are not present as a shopping list.
+;;
+;;; Usage:
+;; (require 'org-checklist)
+;;
+;; Set the RESET_CHECK_BOXES and LIST_EXPORT_BASENAME properties in items as
+;; needed.
+;;
+;; https://orgmode.org/worg/org-contrib/org-checklist.html
+;; https://git.sr.ht/~bzg/org-contrib/blob/master/lisp/org-checklist.el
+;;
+;;; Code:
+(require 'org)
+(defvar org-state)
+;; FIXME: This library requires
+;; https://git.savannah.gnu.org/cgit/a2ps.git/tree/contrib/emacs/a2ps-print.el file
+;; It is a part of a2ps distribution.
+(load "a2ps-print" 'no-error)
+(defvar a2ps-switches)
+(declare-function a2ps-buffer "a2ps-print" (argp))
+
+(setq org-default-properties (cons "RESET_CHECK_BOXES" (cons "LIST_EXPORT_BASENAME" org-default-properties)))
+
+(defgroup org-checklist nil
+ "Extended checklist handling for org"
+ :tag "Org-checklist"
+ :group 'org)
+
+(defcustom org-checklist-export-time-format "%Y%m%d%H%M"
+ "The format of timestamp appended to LIST_EXPORT_BASENAME to
+ make the name of the export file."
+ :link '(function-link format-time-string)
+ :group 'org-checklist
+ :type 'string)
+
+(defcustom org-checklist-export-function 'org-export-as-ascii
+ "function used to prepare the export file for printing"
+ :group 'org-checklist
+ :type '(radio (function-item :tag "ascii text" org-export-as-ascii)
+ (function-item :tag "HTML" org-export-as-html)
+ (function-item :tag "LaTeX" :value org-export-as-latex)
+ (function-item :tag "XOXO" :value org-export-as-xoxo)))
+
+(defcustom org-checklist-export-params nil
+ "options for the export function file for printing"
+ :group 'org-checklist
+ :type '(repeat string))
+
+(defcustom org-checklist-a2ps-params nil
+ "options for a2ps for printing"
+ :group 'org-checklist
+ :type '(repeat string))
+
+(defun org-reset-checkbox-state-maybe ()
+ "Reset all checkboxes in an entry if the `RESET_CHECK_BOXES' property is set"
+ (interactive "*")
+ (if (org-entry-get (point) "RESET_CHECK_BOXES")
+ (org-reset-checkbox-state-subtree)))
+
+
+(defun org-make-checklist-export ()
+ "Produce a checklist containing all unchecked items from a list
+of checkbox items"
+ (interactive "*")
+ (when (org-entry-get (point) "LIST_EXPORT_BASENAME")
+ (let* ((export-file (concat (org-entry-get (point) "LIST_EXPORT_BASENAME" nil)
+ "-" (format-time-string
+ org-checklist-export-time-format)
+ ".org"))
+ (print (pcase (org-entry-get (point) "PRINT_EXPORT" nil)
+ (`(or "" "nil" nil) nil)
+ (`nil (y-or-n-p "Print list? "))
+ (_ t)))
+ exported-lines
+ (title "Checklist export"))
+ (save-restriction
+ (save-excursion
+ (org-narrow-to-subtree)
+ (org-update-checkbox-count-maybe)
+ (if (fboundp 'org-fold-show-subtree)
+ (org-fold-show-subtree)
+ (with-no-warnings (org-show-subtree)))
+ (goto-char (point-min))
+ (when (looking-at org-complex-heading-regexp)
+ (setq title (match-string 4)))
+ (goto-char (point-min))
+ (let ((end (point-max)))
+ (while (< (point) end)
+ (when (and (org-at-item-checkbox-p)
+ (or (string= (match-string 0) "[ ]")
+ (string= (match-string 0) "[-]")))
+ (setq exported-lines
+ (nconc exported-lines (list (thing-at-point 'line)))))
+ (beginning-of-line 2)))
+ (set-buffer (get-buffer-create export-file))
+ (org-insert-heading)
+ (insert (or title export-file) "\n")
+ (dolist (entry exported-lines) (insert entry))
+ (org-update-checkbox-count-maybe)
+ (write-file export-file)
+ (when print
+ (funcall org-checklist-export-function
+ org-checklist-export-params)
+ (let* ((current-a2ps-switches a2ps-switches)
+ (a2ps-switches (append current-a2ps-switches
+ org-checklist-a2ps-params)))
+ (a2ps-buffer nil))))))))
+
+(defun org-checklist ()
+ (when (member org-state org-done-keywords) ;; org-state dynamically bound in org.el/org-todo
+ (org-make-checklist-export)
+ (org-reset-checkbox-state-maybe)))
+
+(add-hook 'org-after-todo-state-change-hook 'org-checklist)
+
+(provide 'org-checklist)
+
+;;; org-checklist.el ends here
diff --git a/custom/pdf-continuous-scroll-mode-latest.el b/custom/pdf-continuous-scroll-mode-latest.el
new file mode 100644
index 00000000..b05890c4
--- /dev/null
+++ b/custom/pdf-continuous-scroll-mode-latest.el
@@ -0,0 +1,1046 @@
+;;; pdf-continuous-scroll-mode.el --- Continuous scroll for pdf-tools -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020 Daniel Laurens Nicolai
+
+;; Author: Daniel Laurens Nicolai <dalanicolai@gmail.com>
+;; Version: 0
+;; Keywords: pdf-tools,
+;; Package-Requires: ((emacs "27.1") (pdf-tools "1.0"))
+;; URL: https://github.com/dalanicolai/pdf-continuous-scroll-mode.el
+
+
+;; 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:
+
+;; Usage:
+
+;;; Code:
+(require 'pdf-tools)
+(require 'pdf-annot)
+(require 'image-mode)
+(require 'svg)
+(require 'cl-lib)
+
+(defgroup book nil
+ "Bookroll customizations.")
+
+(defcustom book-scroll-fraction 32
+ "The scroll step size in 1/fraction of page."
+ :type 'integer)
+
+(defcustom book-page-vertical-margin 5
+ "The size of the vertical margins around a page."
+ :type 'integer)
+
+(defcustom book-reverse-scrolling nil
+ "Reverse default scrolling direction"
+ :group 'pdf-continuous-scroll
+ :type 'boolean)
+
+(defvar-local book-number-of-pages 0)
+(defvar-local book-contents-end-pos 0)
+
+;; (defmacro book-current-page (&optional win)
+;; `(image-mode-window-get 'page ,win))
+(defmacro book-overlays (&optional window) `(image-mode-window-get 'overlays ,window))
+(defmacro book-image-sizes (&optional window) `(image-mode-window-get 'image-sizes ,window))
+(defmacro book-image-positions (&optional window) `(image-mode-window-get 'image-positions ,window))
+(defmacro book-currently-displayed-pages (&optional window) `(image-mode-window-get 'displayed-pages ,window))
+
+;; the following function only exists for backward compatibility
+(defun pdf-continuous-scroll-mode ()
+ (print "This is a new version of pdf-continuous-scroll-mode.el
+Despite the name, the `pdf-continuous-scroll-mode' itself has
+been removed. You should make sure that you remove the function
+from the pdf-view-mode-hook. If you prefer to use the previous
+'2-buffer' version, then you can download and load the file from
+the previous commit at
+https://github.com/dalanicolai/pdf-continuous-scroll-mode.el/tree/615dcfbf7a9b2ff602a39da189e5eb766600047f."))
+
+(make-obsolete 'pdf-continuous-scroll-mode nil "3 February 2022")
+
+(defun image-mode-winprops (&optional window cleanup)
+ "Return winprops of WINDOW.
+A winprops object has the shape (WINDOW . ALIST).
+WINDOW defaults to `selected-window' if it displays the current buffer, and
+otherwise it defaults to t, used for times when the buffer is not displayed."
+ (cond ((null window)
+ (setq window
+ (if (eq (current-buffer) (window-buffer)) (selected-window) t)))
+ ((eq window t))
+ ((not (windowp window))
+ (error "Not a window: %s" window)))
+ (when-let (o (nth 272 (assq 'overlays (assq window image-mode-winprops-alist))))
+ (message "%s" (image-property (overlay-get o 'display) :type)))
+ (when cleanup
+ (setq image-mode-winprops-alist
+ (delq nil (mapcar (lambda (winprop)
+ (let ((w (car-safe winprop)))
+ (if (or (not (windowp w)) (window-live-p w))
+ winprop)))
+ image-mode-winprops-alist))))
+ (let ((winprops (assq window image-mode-winprops-alist)))
+ ;; For new windows, set defaults from the latest.
+ (if winprops
+ ;; Move window to front.
+ (setq image-mode-winprops-alist
+ (cons winprops (delq winprops image-mode-winprops-alist)))
+ (setq winprops (cons window
+ (copy-alist (cdar image-mode-winprops-alist))))
+ ;; Add winprops before running the hook, to avoid inf-loops if the hook
+ ;; triggers window-configuration-change-hook.
+ (setq image-mode-winprops-alist
+ (cons winprops image-mode-winprops-alist))
+ (run-hook-with-args 'image-mode-new-window-functions winprops))
+ winprops))
+
+;; We overwrite the following image-mode function to make it also
+;; reapply winprops when the overlay has the 'invisible property
+(defun image-get-display-property ()
+ (or (get-char-property (point-min) 'display
+ ;; There might be different images for different displays.
+ (if (eq (window-buffer) (current-buffer))
+ (selected-window)))
+ (get-char-property (point-min) 'invisible
+ ;; There might be different images for different displays.
+ (if (eq (window-buffer) (current-buffer))
+ (selected-window)))))
+
+(defun image-set-window-vscroll (vscroll)
+ (setf (image-mode-window-get 'vscroll) vscroll
+ (image-mode-window-get 'relative-vscroll) (/ (float vscroll)
+ (car (last (book-image-positions)))))
+ (set-window-vscroll (selected-window) vscroll t))
+
+(defun image-mode-reapply-winprops ()
+ ;; When set-window-buffer, set hscroll and vscroll to what they were
+ ;; last time the image was displayed in this window.
+ (when (listp image-mode-winprops-alist)
+ ;; Beware: this call to image-mode-winprops can't be optimized away,
+ ;; because it not only gets the winprops data but sets it up if needed
+ ;; (e.g. it's used by doc-view to display the image in a new window).
+ (let* ((winprops (image-mode-winprops nil t))
+ (hscroll (image-mode-window-get 'hscroll winprops))
+ (vscroll (round (* (image-mode-window-get 'relative-vscroll winprops)
+ (car (last (book-image-positions)))))))
+ (when (image-get-display-property) ;Only do it if we display an image!
+ (if hscroll (set-window-hscroll (selected-window) hscroll))
+ (if vscroll (set-window-vscroll (selected-window) vscroll t))))))
+
+(defun book-create-image-positions (image-sizes)
+ (let ((sum 0)
+ (positions (list 0)))
+ (dolist (s image-sizes)
+ ;; the margin is added on both sides
+ (setq sum (+ sum (cdr s) (* 2 (or book-page-vertical-margin pdf-view-image-relief))))
+ (push sum positions))
+ (nreverse positions)))
+
+(defun book-create-overlays-list (winprops)
+ "Create list of overlays spread out over the buffer contents.
+Pass non-nil value for include-first when the buffer text starts with a match."
+ ;; first overlay starts at 1
+ ;; (setq book-contents-end-pos (goto-char (point-max)))
+ (goto-char book-contents-end-pos)
+ (let ((eobp (eobp))
+ overlays)
+ (if (eobp)
+ (insert " ")
+ (forward-char))
+ (push (make-overlay (1- (point)) (point)) overlays)
+ (let ((overlays-list (dotimes (_ (1- (length (book-image-sizes))) (nreverse overlays))
+ (if eobp
+ (insert "\n ")
+ (forward-char 2))
+ (push (make-overlay (1- (point)) (point)) overlays))))
+ ;; all windows require their overlays to apply to the window only because
+ ;; the windows and therefore bookroll's might have different sizes (see
+ ;; `overlay-properties')
+ (mapc (lambda (o) (overlay-put o 'window (car winprops))) overlays-list)
+ (image-mode-window-put 'overlays overlays-list winprops)))
+ (goto-char (point-min))
+ (set-buffer-modified-p nil))
+
+(defun book-create-empty-page (size)
+ (pcase-let* ((`(,w . ,h) size))
+ (svg-image (svg-create w h)
+ :margin (cons 0 book-page-vertical-margin))))
+
+(defun book-create-placeholders ()
+ (let* ((constant-size (cl-every #'eql (book-image-sizes) (cdr (book-image-sizes))))
+ (ph (when constant-size (book-create-empty-page (car (book-image-sizes))))))
+ (dotimes (i (length (book-image-sizes)))
+ ;; (let ((p (1+ i)));; shift by 1 to match with page numbers
+ ;; (overlay-put (nth i overlays-list) 'display (or ph (book-create-empty-page (nth i (book-image-sizes))))))))
+ (overlay-put (nth i (book-overlays)) 'display (or ph (book-create-empty-page (nth i (book-image-sizes))))))))
+
+(defun book-current-page ()
+ (interactive)
+ (let ((i 0)
+ (cur-pos (window-vscroll nil t)))
+ (while (<= (nth (1+ i) (book-image-positions)) (+ cur-pos (/ (window-pixel-height) 2)))
+ (setq i (1+ i)))
+ (1+ i)))
+
+(defun book-page-triplet (page)
+ ;; first handle the cases when the doc has only one or two pages
+ (pcase (pdf-info-number-of-pages)
+ (1 '(1))
+ (2 '(1 2))
+ (_ (pcase page
+ (1 '(1 2))
+ ((pred (= book-number-of-pages)) (list page (- page 1)))
+ (p (list (- p 1) p (+ p 1)))))))
+
+(defun book-remove-page-image (page)
+ ;; remove page image means insert back empty image (placeholder)
+ (overlay-put (nth (1- page) (book-overlays))
+ 'display
+ (book-create-empty-page (nth (1- page) (book-image-sizes)))))
+
+
+(defun book-scroll-to-page (page)
+ (interactive "n")
+ ;; (book-update-page-triplet page)
+ (let* ((elt (1- page)))
+ (set-window-vscroll nil
+ (+ (nth elt (book-image-positions))
+ (or book-page-vertical-margin pdf-view-image-relief))
+ t)))
+
+(defvar pdf-continuous-suppress-introduction nil)
+
+(define-derived-mode pdf-view-mode special-mode "PDFView"
+ "Major mode in PDF buffers.
+
+PDFView Mode is an Emacs PDF viewer. It displays PDF files as
+PNG images in Emacs buffers."
+ :group 'pdf-view
+ :abbrev-table nil
+ :syntax-table nil
+ ;; Setup a local copy for remote files.
+ (when (and (or jka-compr-really-do-compress
+ (let ((file-name-handler-alist nil))
+ (not (and buffer-file-name
+ (file-readable-p buffer-file-name)))))
+ (pdf-tools-pdf-buffer-p))
+ (let ((tempfile (pdf-util-make-temp-file)))
+ (write-region nil nil tempfile nil 'no-message)
+ (setq-local pdf-view--buffer-file-name tempfile)))
+ ;; Decryption needs to be done before any other function calls into
+ ;; pdf-info.el (e.g. from the mode-line during redisplay during
+ ;; waiting for process output).
+ (pdf-view-decrypt-document)
+
+ ;; Setup scroll functions
+ (if (boundp 'mwheel-scroll-up-function) ; not --without-x build
+ (setq-local mwheel-scroll-up-function
+ #'pdf-view-scroll-up-or-next-page))
+ (if (boundp 'mwheel-scroll-down-function)
+ (setq-local mwheel-scroll-down-function
+ #'pdf-view-scroll-down-or-previous-page))
+
+ ;; Clearing overlays
+ (add-hook 'change-major-mode-hook
+ (lambda ()
+ (remove-overlays (point-min) (point-max) 'pdf-view t))
+ nil t)
+ (remove-overlays (point-min) (point-max) 'pdf-view t) ;Just in case.
+
+ ;; Setup other local variables.
+ (setq-local mode-line-position
+ '(" P" (:eval (number-to-string (pdf-view-current-page)))
+ ;; Avoid errors during redisplay.
+ "/" (:eval (or (ignore-errors
+ (number-to-string (pdf-cache-number-of-pages)))
+ "???"))))
+ (setq-local auto-hscroll-mode nil)
+ (setq-local pdf-view--server-file-name (pdf-view-buffer-file-name))
+ ;; High values of scroll-conservatively seem to trigger
+ ;; some display bug in xdisp.c:try_scrolling .
+ (setq-local scroll-conservatively 0)
+ (setq-local cursor-type nil)
+ (setq-local buffer-read-only t)
+ (setq-local view-read-only nil)
+ (setq-local bookmark-make-record-function
+ 'pdf-view-bookmark-make-record)
+ (setq-local revert-buffer-function #'pdf-view-revert-buffer)
+ ;; No auto-save at the moment.
+ (setq-local buffer-auto-save-file-name nil)
+ ;; Disable image rescaling.
+ (when (boundp 'image-scaling-factor)
+ (setq-local image-scaling-factor 1))
+ ;; No undo at the moment.
+ (unless buffer-undo-list
+ (buffer-disable-undo))
+ ;; Enable transient-mark-mode, so region deactivation when quitting
+ ;; will work.
+ (setq-local transient-mark-mode t)
+ ;; In Emacs >= 24.4, `cua-copy-region' should have been advised when
+ ;; loading pdf-view.el so as to make it work with
+ ;; pdf-view-mode. Disable cua-mode if that is not the case.
+ ;; FIXME: cua-mode is a global minor-mode, but setting cua-mode to
+ ;; nil seems to do the trick.
+ (when (and (bound-and-true-p cua-mode)
+ (version< emacs-version "24.4"))
+ (setq-local cua-mode nil))
+
+ (setq-local book-contents-end-pos (point-max))
+ (setq-local book-number-of-pages (pdf-cache-number-of-pages))
+
+ (add-hook 'window-configuration-change-hook
+ 'pdf-view-redisplay-some-windows nil t)
+ (add-hook 'deactivate-mark-hook 'pdf-view-deactivate-region nil t)
+ (add-hook 'write-contents-functions
+ 'pdf-view--write-contents-function nil t)
+ (add-hook 'kill-buffer-hook 'pdf-view-close-document nil t)
+ (pdf-view-add-hotspot-function
+ 'pdf-view-text-regions-hotspots-function -9)
+
+ ;; Keep track of display info
+ (add-hook 'image-mode-new-window-functions
+ 'pdf-view-new-window-function nil t)
+ (image-mode-setup-winprops)
+
+ (unless pdf-continuous-suppress-introduction
+ (pdf-continuous-introduce))
+
+ ;; Issue a warning in the future about incompatible modes.
+ (run-with-timer 1 nil (lambda (buffer)
+ (when (buffer-live-p buffer)
+ (pdf-view-check-incompatible-modes buffer)
+
+ (unless pdf-continuous-suppress-introduction
+ (switch-to-buffer "*pdf-continuous-introduction*"))))
+ (current-buffer)))
+
+(defun pdf-continuous-toggle-message ()
+ (interactive)
+ (setq pdf-continuous-suppress-introduction
+ (if pdf-continuous-suppress-introduction
+ nil
+ (message "pdf-continuous message suppressed")
+ t)))
+
+(defun pdf-continuous-introduce ()
+ (with-current-buffer (get-buffer-create "*pdf-continuous-introduction*")
+ (insert "NEW PDF CONTINUOUS SCROLL: INTRODUCTION 3 February 2022
+
+Welcome to the new pdf-continuous-scroll-mode, now finally
+providing continuous scroll in a single buffer. 🎉🍾
+
+Despite the name, the `pdf-continuous-scroll-mode' itself has
+been removed. You should make sure that you remove the function
+from the pdf-view-mode-hook. If you prefer to use the previous
+'2-buffer' version, then you can download and load the file from
+the previous commit at
+https://github.com/dalanicolai/pdf-continuous-scroll-mode.el/tree/615dcfbf7a9b2ff602a39da189e5eb766600047f.
+
+My apologies for this rude interruption, however this behavior is
+only temporary (until this functionality gets merged into
+pdf-tools in May or so).
+
+There are two reasons for this interruption:
+
+- second, to inform you about how to obtain or set indicators
+ allowing you to differentiate between the pages. The default
+ design uses the customizable `book-page-vertical-margin'
+ variable, which sets vertical margins for the page images. If
+ your Emacs theme has a different background color than your
+ books page color, this will nicely indicate the page
+ 'transitions'. However, if the background color and the page
+ color are the same, then you can set the
+ `book-page-vertical-margin' to 0 and instead set
+ `pdf-view-image-relief' to some non negative number to help you
+ differentiate between the pages.
+
+ Also, redisplay (e.g. splitting buffers), does not work
+ flawlessly yet, but you can simply split and start scrolling,
+ or use `M-x pdf-view-goto-page', in the buffers and the display
+ problem will 'fix itself'. (To fix the root of the problem, I
+ probably have to make `image-mode-reapply-winprops' use
+ 'relative' instead of `absolute' vscroll. Also, I have noticed,
+ that, while in Spacemacs redisplay works almost fine, in
+ vanilla Emacs the vscroll does not get restored despite
+ `image-mode-reapply-winprops' getting called.)
+
+- first, I would like to inform you that I would be very happy
+ with any small donation if you can afford it (I guess most
+ Emacs PDF-tools users are students). Despite the low number of
+ lines of code here (of which a large part is adapted from
+ pdf-view.el), creating this package has cost me a lot of
+ effort. I think writing the code has only cost me about 0.001
+ percent of the time, while most of the time has been invested
+ in investigating pdf-tools and doc-view and their very opaque
+ and non-trivial display mechanisms.
+
+ You can find donate buttons on the
+ pdf-continuous-scroll-mode.el github page
+ (https://github.com/dalanicolai/pdf-continuous-scroll-mode.el).
+
+ With the help of donations I could work on fixing bugs and on
+ improving pdf-tools and image-mode documentation (which are
+ really great packages, but they lack documentation. If about 50
+ lines of documentation had been available, it would probably
+ have saved me two/three weeks of work).
+
+ Besides that, I have also written an alternative pdf
+ server (https://github.com/vedang/pdf-tools/pull/61) using
+ python with the excellent pymupdf
+ package (https://pymupdf.readthedocs.io/en/latest/). Again in
+ that case, writing the code was only a tiny fraction of the
+ total work. This alternative server provides new kinds of
+ annotation functionality like line, arrow and free text
+ annotations. Furthermore, it offers the possibility to send
+ python code directly to the server, so that it is possible to
+ use the full features provided by pymupdf.
+
+ The combined work has cost me almost two months of almost full
+ time investigating, debugging, iterating.
+
+ To save you a long interesting story, I just mention that
+ currently I am dependent on others for paying my rent and my
+ food. Otherwise, I would have been even more happy by
+ sharing/donating this package without asking for any donations.
+
+ I have created a few more packages that can be found and are
+ described at my github profile
+ page (https://github.com/dalanicolai).
+
+You can toggle off this message by doing `M-x
+pdf-continuous-toggle-message' or setting
+`pdf-continuous-suppress-introduction' to non-nil in your
+dotfile. Or you can simply close this buffer and start reading.
+
+Thank you.
+Daniel
+
+Happy scrolling!"
+)
+ (goto-char (point-min))))
+
+
+;; Despite what its docstring says, this function does not go to page in all
+;; `pdf-view' windows when WINDOW is non-nil.
+(defun pdf-view-goto-page (page &optional window)
+ "Go to PAGE in PDF.
+
+If optional parameter WINDOW, go to PAGE in all `pdf-view'
+windows."
+ (interactive
+ (list (if current-prefix-arg
+ (prefix-numeric-value current-prefix-arg)
+ (read-number "Page: "))))
+ (unless (and (>= page 1)
+ (<= page (pdf-cache-number-of-pages)))
+ (error "No such page: %d" page))
+ (unless window
+ (setq window
+ (if (pdf-util-pdf-window-p)
+ (selected-window)
+ t)))
+ (save-selected-window
+ ;; Select the window for the hooks below.
+ (when (window-live-p window)
+ (select-window window 'norecord))
+ (let ((changing-p
+ (not (eq page (pdf-view-current-page window)))))
+ (when changing-p
+ (run-hooks 'pdf-view-before-change-page-hook)
+ (setf (pdf-view-current-page window) page)
+ (run-hooks 'pdf-view-change-page-hook))
+ (when (window-live-p window)
+ (pdf-view-redisplay window))
+ (when changing-p
+ (pdf-view-deactivate-region)
+ (force-mode-line-update)
+ (run-hooks 'pdf-view-after-change-page-hook))))
+ (image-set-window-vscroll (+ (nth (1- page) (book-image-positions))
+ (or book-page-vertical-margin pdf-view-image-relief)))
+ nil)
+
+(defun pdf-continuous-scroll-forward (&optional pixels)
+ ;; (defun pdf-view-next-line-or-next-page ()
+ (interactive)
+ ;; because pages could have different heights, we calculate the step size on each scroll
+ ;; TODO define constant scroll size if doc has single page height
+ (let* ((scroll-step-size (/ (cdr (pdf-view-image-size)) book-scroll-fraction))
+ (page-end (nth (pdf-view-current-page) (book-image-positions)))
+ (vscroll (window-vscroll nil t))
+ (new-vscroll (image-set-window-vscroll (if (< vscroll (- (car (last (book-image-positions)))
+ (window-pixel-height)))
+ (+ vscroll (or pixels scroll-step-size))
+ (message "End of book")
+ vscroll))))
+ (when (> (+ new-vscroll (/ (window-pixel-height) 2)) page-end)
+ (let ((old-page (pdf-view-current-page))
+ (new-page (alist-get 'page (cl-incf (pdf-view-current-page)))))
+ (when (> old-page 1)
+ (book-remove-page-image (1- old-page))
+ (setf (book-currently-displayed-pages) (delete (1- old-page) (book-currently-displayed-pages))))
+ (when (< new-page (pdf-info-number-of-pages))
+ (pdf-view-display-triplet new-page)))))
+ ;; :width doc-view-image-width
+ ;; :pointer 'arrow
+ ;; :margin (cons 0 book-page-vertical-margin))))))
+ (sit-for 0))
+
+(defun pdf-continuous-scroll-backward (&optional pixels)
+ ;; (defun pdf-view-next-line-or-next-page ()
+ (interactive)
+ ;; because pages could have different heights, we calculate the step size on each scroll
+ ;; TODO define constant scroll size if doc has single page height
+ (let* ((scroll-step-size (/ (cdr (pdf-view-image-size)) book-scroll-fraction))
+ (page-beg (nth (1- (pdf-view-current-page)) (book-image-positions)))
+ (new-vscroll (image-set-window-vscroll (- (window-vscroll nil t) (or pixels scroll-step-size)))))
+ (when (< (+ new-vscroll (/ (window-pixel-height) 2)) page-beg)
+ (let ((old-page (pdf-view-current-page))
+ (new-page (alist-get 'page (cl-decf (pdf-view-current-page)))))
+ (when (< old-page (pdf-info-number-of-pages))
+ (book-remove-page-image (1+ old-page))
+ (setf (book-currently-displayed-pages) (delete (1+ old-page) (book-currently-displayed-pages))))
+ (when (> new-page 1)
+ (pdf-view-display-triplet new-page)))))
+ ;; :width doc-view-image-width
+ ;; :pointer 'arrow
+ ;; :margin (cons 0 book-page-vertical-margin))))))
+ (sit-for 0))
+
+(defun pdf-cs-mouse-scroll-forward ()
+ (interactive)
+ (with-selected-window
+ (or (caadr last-input-event) (selected-window))
+ (if book-reverse-scrolling
+ (pdf-continuous-scroll-backward nil)
+ (pdf-continuous-scroll-forward nil))))
+
+(defun pdf-cs-mouse-scroll-backward ()
+ (interactive)
+ (with-selected-window
+ (or (caadr last-input-event) (selected-window))
+ (if book-reverse-scrolling
+ (pdf-continuous-scroll-forward nil)
+ (pdf-continuous-scroll-backward nil))))
+
+(defun pdf-continuous-next-page ()
+ (interactive)
+ (pdf-continuous-scroll-forward (+ (cdr (nth (1- (book-current-page)) (book-image-sizes)))
+ (* 2 (or book-page-vertical-margin pdf-view-image-relief)))))
+
+(defun pdf-continuous-previous-page ()
+ (interactive)
+ (pdf-continuous-scroll-backward (+ (cdr (nth (1- (book-current-page)) (book-image-sizes)))
+ (* 2 (or book-page-vertical-margin pdf-view-image-relief)))))
+
+(defun pdf-cscroll-first-page ()
+ (interactive)
+ (pdf-view-goto-page 1))
+
+(defun pdf-cscroll-last-page ()
+ (interactive)
+ (pdf-view-goto-page (pdf-cache-number-of-pages)))
+
+(defun pdf-view-create-page (page &optional window)
+ "Create an image of PAGE for display on WINDOW."
+ (let* ((size (pdf-view-desired-image-size page window))
+ (data (pdf-cache-renderpage
+ page (car size)
+ (if (not pdf-view-use-scaling)
+ (car size)
+ (* 2 (car size)))))
+ (hotspots (pdf-view-apply-hotspot-functions
+ window page size)))
+ (pdf-view-create-image data
+ :width (car size)
+ :margin (cons 0 book-page-vertical-margin)
+ :map hotspots
+ :pointer 'arrow)))
+
+(defun pdf-view-image-size (&optional displayed-p window)
+ ;; TODO: add WINDOW to docstring.
+ "Return the size in pixel of the current image.
+
+If DISPLAYED-P is non-nil, return the size of the displayed
+image. These values may be different, if slicing is used."
+ ;; (if displayed-p
+ ;; (with-selected-window (or window (selected-window))
+ ;; (image-display-size
+ ;; (image-get-display-property) t))
+ (image-size (pdf-view-current-image window) t))
+
+(defun pdf-view-display-page (page &optional window)
+ "Display page PAGE in WINDOW."
+ (with-selected-window window
+
+ (let* ((image-sizes (let (s)
+ (dotimes (i (pdf-info-number-of-pages) (nreverse s))
+ (push (pdf-view-desired-image-size (1+ i)) s))))
+ (image-positions (book-create-image-positions image-sizes)))
+ (image-mode-window-put 'image-sizes image-sizes)
+ (image-mode-window-put 'image-positions image-positions))
+
+ (let ((inhibit-read-only t))
+ (book-create-placeholders)))
+
+ (setf (pdf-view-window-needs-redisplay window) nil)
+ ;; (setf (pdf-view-current-page window) page)
+
+ (pdf-view-display-triplet page window))
+
+(defun pdf-view-display-triplet (page &optional window inhibit-slice-p)
+ ;; TODO: write documentation!
+ (let ((ol (pdf-view-current-overlay window))
+ (display-pages (book-page-triplet page)))
+ (when (window-live-p (overlay-get ol 'window))
+ (dolist (p (book-currently-displayed-pages window))
+ (unless (member p display-pages)
+ (book-remove-page-image p)))
+ (dolist (p display-pages)
+ (let* ((image (pdf-view-create-page p window))
+ (size (image-size image t))
+ (slice (if (not inhibit-slice-p)
+ (pdf-view-current-slice window)))
+ (displayed-width (floor
+ (if slice
+ (* (nth 2 slice)
+ (car (image-size image)))
+ (car (image-size image))))))
+ (when (= p page)
+ (setf (pdf-view-current-image window) image))
+ ;; In case the window is wider than the image, center the image
+ ;; horizontally.
+ (overlay-put (nth (1- p) (book-overlays window)) 'before-string
+ (when (> (window-width window)
+ displayed-width)
+ (propertize " " 'display
+ `(space :align-to
+ ,(/ (- (window-width window)
+ displayed-width) 2)))))
+ ;; (message "%s %s" p window)
+ ;; (print (image-property image :type))
+ (overlay-put (nth (1- p) (book-overlays window)) 'display
+ (if slice
+ (list (cons 'slice
+ (pdf-util-scale slice size 'round))
+ image)
+ image))
+ (cl-pushnew p (book-currently-displayed-pages window))))
+ (image-property (overlay-get (nth (1- page) (book-overlays window)) 'display) :type)
+ (let* ((win (overlay-get ol 'window))
+ (hscroll (image-mode-window-get 'hscroll win))
+ (vscroll (if-let (vs (image-mode-window-get 'relative-vscroll (image-mode-winprops)))
+ (round (* vs
+ (car (last (book-image-positions window)))))
+ (image-mode-window-get 'vscroll win))))
+ ;; Reset scroll settings, in case they were changed.
+ (if hscroll (set-window-hscroll win hscroll))
+ (if vscroll (set-window-vscroll
+ win vscroll pdf-view-have-image-mode-pixel-vscroll)))
+ ;; (setq test-overlay (overlay-properties (nth (1- page) (book-overlays (print window)))))
+ (setq currently-displayed-pages display-pages))))
+
+(defun pdf-view-display-image (image &optional window inhibit-slice-p)
+ ;; TODO: write documentation!
+ (let ((ol (pdf-view-current-overlay window)))
+ (when (window-live-p (overlay-get ol 'window))
+ (let* ((size (image-size image t))
+ (slice (if (not inhibit-slice-p)
+ (pdf-view-current-slice window)))
+ (displayed-width (floor
+ (if slice
+ (* (nth 2 slice)
+ (car (image-size image)))
+ (car (image-size image)))))
+ (p (pdf-view-current-page)))
+ (setf (pdf-view-current-image window) image)
+ ;; (move-overlay ol (point-min) (point-max))
+ ;; In case the window is wider than the image, center the image
+ ;; horizontally.
+ (overlay-put (nth (1- p) (book-overlays)) 'before-string
+ (when (> (window-width window)
+ displayed-width)
+ (propertize " " 'display
+ `(space :align-to
+ ,(/ (- (window-width window)
+ displayed-width) 2)))))
+ (overlay-put (nth (1- p) (book-overlays)) 'display
+ (if slice
+ (list (cons 'slice
+ (pdf-util-scale slice size 'round))
+ image)
+ image))
+ (let* ((win (overlay-get ol 'window))
+ (hscroll (image-mode-window-get 'hscroll win))
+ (vscroll (image-mode-window-get 'vscroll win)))
+ ;; Reset scroll settings, in case they were changed.
+ (if hscroll (set-window-hscroll win hscroll))
+ (if vscroll (set-window-vscroll
+ win vscroll pdf-view-have-image-mode-pixel-vscroll)))))))
+
+(defun pdf-view-redisplay (&optional window)
+ "Redisplay page in WINDOW.
+
+If WINDOW is t, redisplay pages in all windows."
+ (unless pdf-view-inhibit-redisplay
+ (if (not (eq t window))
+ (pdf-view-display-page
+ (pdf-view-current-page window)
+ window)
+ (print "hello")
+ (dolist (win (get-buffer-window-list nil nil t))
+ (pdf-view-display-page
+ (pdf-view-current-page win)
+ win))
+ (when (consp image-mode-winprops-alist)
+ (dolist (window (mapcar #'car image-mode-winprops-alist))
+ (unless (or (not (window-live-p window))
+ (eq (current-buffer)
+ (window-buffer window)))
+ (setf (pdf-view-window-needs-redisplay window) t)))))
+ (force-mode-line-update)))
+
+(defun pdf-view-redisplay-some-windows ()
+ (pdf-view-maybe-redisplay-resized-windows)
+ (dolist (window (get-buffer-window-list nil nil t))
+ (when (pdf-view-window-needs-redisplay window)
+ (pdf-view-redisplay window))))
+
+(defun pdf-view-new-window-function (winprops)
+ ;; TODO: write documentation!
+ ;; (message "New window %s for buf %s" (car winprops) (current-buffer))
+ (cl-assert (or (eq t (car winprops))
+ (eq (window-buffer (car winprops)) (current-buffer))))
+ (let ((ol (image-mode-window-get 'overlay winprops)))
+ (if ol
+ (progn
+ (setq ol (copy-overlay ol))
+ ;; `ol' might actually be dead.
+ (move-overlay ol (point-min) book-contents-end-pos))
+ (setq ol (make-overlay (point-min) book-contents-end-pos nil t))
+ (overlay-put ol 'pdf-view t))
+ (overlay-put ol 'window (car winprops))
+ (unless (windowp (car winprops))
+ ;; It's a pseudo entry. Let's make sure it's not displayed (the
+ ;; `window' property is only effective if its value is a window).
+ (cl-assert (eq t (car winprops)))
+ (delete-overlay ol))
+ (image-mode-window-put 'overlay ol winprops)
+ ;; Clean up some overlays.
+ (dolist (ov (overlays-in (point-min) (point-max)))
+ (when (and (windowp (overlay-get ov 'window))
+ (not (window-live-p (overlay-get ov 'window))))
+ (delete-overlay ov)))
+ (when (windowp (car winprops))
+ (overlay-put ol 'invisible t)
+ (let* ((image-sizes (let (s)
+ (dotimes (i (pdf-info-number-of-pages) (nreverse s))
+ (push (pdf-view-desired-image-size (1+ i)) s))))
+ (image-positions (book-create-image-positions image-sizes)))
+ (image-mode-window-put 'image-sizes image-sizes winprops)
+ (image-mode-window-put 'image-positions image-positions winprops))
+ (let ((inhibit-read-only t))
+ (book-create-overlays-list winprops)
+ (book-create-placeholders))
+ ;; We're not displaying an image yet, so let's do so. This
+ ;; happens when the buffer is displayed for the first time.
+ ;; (when (null (print (image-mode-window-get 'image winprops)))
+ (with-selected-window (car winprops)
+ (pdf-view-goto-page
+ (or (image-mode-window-get 'page t) 1))))))
+
+(defun pdf-view-mouse-set-region (event &optional allow-extend-p
+ rectangle-p)
+ "Select a region of text using the mouse with mouse event EVENT.
+
+Allow for stacking of regions, if ALLOW-EXTEND-P is non-nil.
+
+Create a rectangular region, if RECTANGLE-P is non-nil.
+
+Stores the region in `pdf-view-active-region'."
+ (interactive "@e")
+ (setq pdf-view--have-rectangle-region rectangle-p)
+ (unless (and (eventp event)
+ (mouse-event-p event))
+ (signal 'wrong-type-argument (list 'mouse-event-p event)))
+ (unless (and allow-extend-p
+ (or (null (get this-command 'pdf-view-region-window))
+ (equal (get this-command 'pdf-view-region-window)
+ (selected-window))))
+ (pdf-view-deactivate-region))
+ (put this-command 'pdf-view-region-window
+ (selected-window))
+ (let* ((window (selected-window))
+ (pos (event-start event))
+ (begin-inside-image-p t)
+ (begin (if (posn-image pos)
+ (posn-object-x-y pos)
+ (setq begin-inside-image-p nil)
+ (posn-x-y pos)))
+ (abs-begin (posn-x-y pos))
+ pdf-view-continuous
+ region)
+ (when (pdf-util-track-mouse-dragging (event 0.05)
+ (message "1 %s" (window-vscroll nil t))
+ (let* ((pos (event-start event))
+ (end (posn-object-x-y pos))
+ (end-inside-image-p
+ (and (eq window (posn-window pos))
+ (posn-image pos))))
+ (when (or end-inside-image-p
+ begin-inside-image-p)
+ (cond
+ ((and end-inside-image-p
+ (not begin-inside-image-p))
+ ;; Started selection outside the image, setup begin.
+ (let* ((xy (posn-x-y pos))
+ (dxy (cons (- (car xy) (car begin))
+ (- (cdr xy) (cdr begin))))
+ (size (pdf-view-image-size t)))
+ (setq begin (cons (max 0 (min (car size)
+ (- (car end) (car dxy))))
+ (max 0 (min (cdr size)
+ (- (cdr end) (cdr dxy)))))
+ ;; Store absolute position for later.
+ abs-begin (cons (- (car xy)
+ (- (car end)
+ (car begin)))
+ (- (cdr xy)
+ (- (cdr end)
+ (cdr begin))))
+ begin-inside-image-p t)))
+ ((and begin-inside-image-p
+ (not end-inside-image-p))
+ ;; Moved outside the image, setup end.
+ (let* ((xy (posn-x-y pos))
+ (dxy (cons (- (car xy) (car abs-begin))
+ (- (cdr xy) (cdr abs-begin))))
+ (size (pdf-view-image-size t)))
+ (setq end (cons (max 0 (min (car size)
+ (+ (car begin) (car dxy))))
+ (max 0 (min (cdr size)
+ (+ (cdr begin) (cdr dxy)))))))))
+ (let ((iregion (if rectangle-p
+ (list (min (car begin) (car end))
+ (min (cdr begin) (cdr end))
+ (max (car begin) (car end))
+ (max (cdr begin) (cdr end)))
+ (list (car begin) (cdr begin)
+ (car end) (cdr end)))))
+ (setq region
+ (pdf-util-scale-pixel-to-relative iregion))
+ (message "2 %s" (window-vscroll nil t))
+ (pdf-view-display-region
+ (cons region pdf-view-active-region)
+ rectangle-p)
+ ;; the following somehow messes up activating regions
+ ;; (pdf-util-scroll-to-edges iregion)
+ ))))
+ (setq pdf-view-active-region
+ (append pdf-view-active-region
+ (list region)))
+ (pdf-view--push-mark))))
+
+
+;;; Fix jump to link (from outline)
+
+(defun pdf-links-action-perform (link)
+ "Follow LINK, depending on its type.
+
+This may turn to another page, switch to another PDF buffer or
+invoke `pdf-links-browse-uri-function'.
+
+Interactively, link is read via `pdf-links-read-link-action'.
+This function displays characters around the links in the current
+page and starts reading characters (ignoring case). After a
+sufficient number of characters have been read, the corresponding
+link's link is invoked. Additionally, SPC may be used to
+scroll the current page."
+ (interactive
+ (list (or (pdf-links-read-link-action "Activate link (SPC scrolls): ")
+ (error "No link selected"))))
+ (let-alist link
+ (cl-case .type
+ ((goto-dest goto-remote)
+ (let ((window (selected-window)))
+ (cl-case .type
+ (goto-dest
+ (unless (> .page 0)
+ (error "Link points to nowhere")))
+ (goto-remote
+ (unless (and .filename (file-exists-p .filename))
+ (error "Link points to nonexistent file %s" .filename))
+ (setq window (display-buffer
+ (or (find-buffer-visiting .filename)
+ (find-file-noselect .filename))))))
+ (with-selected-window window
+ (when (derived-mode-p 'pdf-view-mode)
+ (when (> .page 0)
+ (pdf-view-goto-page .page))
+ (when .top
+ ;; Showing the tooltip delays displaying the page for
+ ;; some reason (sit-for/redisplay don't help), do it
+ ;; later.
+
+ ;; TODO fix lambda/pdf-util-tooltip-arrow for compatibility with
+ ;; continuous scroll
+ ;; (run-with-idle-timer 0.001 nil
+ ;; (lambda ()
+ ;; (when (window-live-p window)
+ ;; (with-selected-window window
+ ;; (when (derived-mode-p 'pdf-view-mode)
+ ;; (pdf-util-tooltip-arrow .top))))))
+ )
+ ))))
+ (uri
+ (funcall pdf-links-browse-uri-function .uri))
+ (t
+ (error "Unrecognized link type: %s" .type)))
+ nil))
+
+;; (defun pdf-isearch-search-function (string &rest _)
+;; "Search for STRING in the current PDF buffer.
+
+;; This is a Isearch interface function."
+;; (when (> (length string) 0)
+;; (let ((same-search-p (pdf-isearch-same-search-p))
+;; (oldpage pdf-isearch-current-page)
+;; (matches (pdf-isearch-search-page string))
+;; next-match)
+;; ;; matches is a list of list of edges ((x0 y1 x1 y2) ...),
+;; ;; sorted top to bottom ,left to right. Coordinates are in image
+;; ;; space.
+;; (unless isearch-forward
+;; (setq matches (reverse matches)))
+;; (when pdf-isearch-filter-matches-function
+;; (setq matches (funcall pdf-isearch-filter-matches-function matches)))
+;; ;; Where to go next ?
+;; (setq pdf-isearch-current-page (pdf-view-current-page)
+;; pdf-isearch-current-matches matches
+;; next-match
+;; (pdf-isearch-next-match
+;; oldpage pdf-isearch-current-page
+;; pdf-isearch-current-match matches
+;; same-search-p
+;; isearch-forward)
+;; pdf-isearch-current-parameter
+;; (list string isearch-regexp
+;; isearch-case-fold-search isearch-word))
+;; (cond
+;; (next-match
+;; (setq pdf-isearch-current-match next-match)
+;; (pdf-isearch-hl-matches next-match matches)
+;; (pdf-isearch-focus-match next-match)
+;; ;; Don't get off track.
+;; (when (or (and (bobp) (not isearch-forward))
+;; (and (eobp) isearch-forward))
+;; (goto-char (1+ (/ (buffer-size) 2))))
+;; ;; Signal success to isearch.
+;; (if isearch-forward
+;; (re-search-forward ".")
+;; (re-search-backward ".")))
+;; ((and (not pdf-isearch-narrow-to-page)
+;; (not (pdf-isearch-empty-match-p matches)))
+;; (let ((next-page (pdf-isearch-find-next-matching-page
+;; string pdf-isearch-current-page t)))
+;; (when next-page
+;; (pdf-view-goto-page next-page)
+;; (pdf-isearch-search-function string))))))))
+
+;;; Fix occur (TODO fix isearch and remove this function)
+
+(defun pdf-occur-goto-occurrence (&optional no-select-window-p)
+ "Go to the occurrence at point.
+
+If EVENT is nil, use occurrence at current line. Select the
+PDF's window, unless NO-SELECT-WINDOW-P is non-nil.
+
+FIXME: EVENT not used at the moment."
+ (interactive)
+ (let ((item (tabulated-list-get-id)))
+ (when item
+ (let* ((doc (plist-get item :document))
+ (page (plist-get item :page))
+ (match (plist-get item :match-edges))
+ (buffer (if (bufferp doc)
+ doc
+ (or (find-buffer-visiting doc)
+ (find-file-noselect doc))))
+ window)
+ (if no-select-window-p
+ (setq window (display-buffer buffer))
+ (pop-to-buffer buffer)
+ (setq window (selected-window)))
+ (with-selected-window window
+ (when page
+ (pdf-view-goto-page page))
+ ;; Abuse isearch.
+ ;; (when match
+ ;; (let ((pixel-match
+ ;; (pdf-util-scale-relative-to-pixel match))
+ ;; (pdf-isearch-batch-mode t))
+ ;; (pdf-isearch-hl-matches pixel-match nil t)
+ ;; (pdf-isearch-focus-match-batch pixel-match)))
+ )))))
+
+
+(define-key pdf-view-mode-map (kbd "C-n") #'pdf-continuous-scroll-forward)
+(define-key pdf-view-mode-map (kbd "<down>") #'pdf-continuous-scroll-forward)
+(define-key pdf-view-mode-map (kbd "<wheel-down>") #'pdf-cs-mouse-scroll-forward)
+(define-key pdf-view-mode-map (kbd "<mouse-5>") #'pdf-cs-mouse-scroll-forward)
+(define-key pdf-view-mode-map (kbd "C-p") #'pdf-continuous-scroll-backward)
+(define-key pdf-view-mode-map (kbd "<up>") #'pdf-continuous-scroll-backward)
+(define-key pdf-view-mode-map (kbd "<wheel-up>") #'pdf-cs-mouse-scroll-backward)
+(define-key pdf-view-mode-map (kbd "<mouse-4>") #'pdf-cs-mouse-scroll-backward)
+(define-key pdf-view-mode-map "n" #'pdf-continuous-next-page)
+(define-key pdf-view-mode-map "p" #'pdf-continuous-previous-page)
+(define-key pdf-view-mode-map (kbd "<prior>") 'pdf-continuous-previous-page)
+(define-key pdf-view-mode-map (kbd "<next>") 'pdf-continuous-next-page)
+;; (define-key pdf-view-mode-map (kbd "M-<") #'pdf-cscroll-view-goto-page)
+(define-key pdf-view-mode-map (kbd "M-g g") #'pdf-cscroll-view-goto-page)
+(define-key pdf-view-mode-map (kbd "M-g M-g") #'pdf-cscroll-view-goto-page)
+(define-key pdf-view-mode-map (kbd "M-<") #'pdf-cscroll-first-page)
+(define-key pdf-view-mode-map (kbd "M->") #'pdf-cscroll-last-page)
+(define-key pdf-view-mode-map [remap forward-char] #'pdf-cscroll-image-forward-hscroll)
+(define-key pdf-view-mode-map [remap right-char] #'pdf-cscroll-image-forward-hscroll)
+(define-key pdf-view-mode-map [remap backward-char] #'pdf-cscroll-image-backward-hscroll)
+(define-key pdf-view-mode-map [remap left-char] #'pdf-cscroll-image-backward-hscroll)
+(define-key pdf-view-mode-map "T" #'pdf-cscroll-toggle-mode-line)
+(define-key pdf-view-mode-map "M" #'pdf-cscroll-toggle-narrow-mode-line)
+(define-key pdf-view-mode-map (kbd "q") '(lambda () (interactive) (pdf-continuous-scroll-mode -1)))
+(define-key pdf-view-mode-map "Q" #'pdf-cscroll-kill-buffer-and-windows)
+(define-key pdf-view-mode-map (kbd "C-c C-a l") #'pdf-cscroll-annot-list-annotations)
+
+(when (boundp 'spacemacs-version)
+ (evil-define-key 'evilified pdf-view-mode-map
+ "j" #'pdf-continuous-scroll-forward
+ (kbd "<mouse-5>") #'pdf-continuous-scroll-forward
+ "k" #'pdf-continuous-scroll-backward
+ (kbd "<mouse-4>") #'pdf-continuous-scroll-backward
+ "J" #'pdf-continuous-next-page
+ "K" #'pdf-continuous-previous-page
+ ;; (kbd "C-j") #'pdf-view-scroll-up-or-next-page
+ ;; (kbd "C-k") #'pdf-view-scroll-down-or-previous-page
+ (kbd "g t") #'pdf-view-goto-page
+ (kbd "g g") #'pdf-cscroll-first-page
+ "G" #'pdf-cscroll-last-page
+ ;; "M" #'pdf-cscroll-toggle-mode-line
+ ;; "q" #'pdf-cscroll-kill-buffer-and-windows
+ ;; "l" #'pdf-cscroll-image-forward-hscroll
+ ;; "h" #'pdf-cscroll-image-backward-hscroll
+ ))
+
+(provide 'pdf-continuous-scroll-mode)
+
+;;; pdf-continuous-scroll-mode.el ends here
diff --git a/custom/pdf-continuous-scroll-mode.el b/custom/pdf-continuous-scroll-mode.el
new file mode 100644
index 00000000..7748149d
--- /dev/null
+++ b/custom/pdf-continuous-scroll-mode.el
@@ -0,0 +1,434 @@
+;;; pdf-continuous-scroll-mode.el --- Continuous scroll minor mode for pdf-tools -*- lexical-binding: t; -*-
+;; Copyright (C) 2020 Daniel Laurens Nicolai
+
+;; Author: Daniel Laurens Nicolai <dalanicolai@gmail.com>
+;; with modifications by: Craig Jennings <c@ecjennings.net>
+;; Version: 0
+;; Keywords: files
+;; Package-Requires: ((emacs "27.1"))
+;; URL: https://github.com/dalanicolai/pdf-continuous-scroll-mode.el
+
+;; 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:
+
+;; Usage:
+
+;;; Code:
+(eval-when-compile
+ (require 'pdf-view))
+(require 'pdf-annot)
+
+(defvar pdf-cscroll-mode-line-format)
+(defvar pdf-cscroll-mode-line-original-face)
+
+(defcustom pdf-continuous-scroll-step-size 4
+ "Step size in lines (integer) for continuous scrolling."
+ :group 'pdf-continuous-scroll
+ :type 'integer)
+
+(defcustom pdf-continuous-scroll-reverse-scrolling nil
+ "Reverse default scrolling direction."
+ :group 'pdf-continuous-scroll
+ :type 'boolean)
+
+(defcustom pdf-continuous-scroll-custom-min-height nil
+ "Reverse default scrolling direction."
+ :group 'pdf-continuous-scroll
+ :type 'number)
+
+(defun pdf-continuous-scroll-window-dual-p ()
+ "Return t if current scroll window status is dual, else nil."
+ (or (equal 'upper (alist-get 'pdf-scroll-window-status (window-parameters)))
+ (equal 'lower (alist-get 'pdf-scroll-window-status (window-parameters)))))
+
+(defun pdf-continuous-scroll-close-window-when-dual ()
+ (when (pdf-continuous-scroll-window-dual-p)
+ (let ((window-status (alist-get 'pdf-scroll-window-status (window-parameters))))
+ (save-excursion
+ (if (equal window-status 'upper)
+ (windmove-down)
+ (windmove-up))
+ (delete-window)
+ (set-window-parameter nil 'pdf-scroll-window-status 'single)))))
+
+(defun pdf-continuous-scroll-forward-line (&optional arg)
+ "Scroll upward by ARG lines if possible, else go to the next page.
+This function is an adapted version of
+`pdf-view-next-line-or-next-page'. Although the ARG is kept here,
+this function generally works best without ARG is 1. To increase
+the step size for scrolling use the ARG in
+`pdf-continuous-scroll-forward'"
+ (if pdf-continuous-scroll-mode
+ (let ((current-file buffer-file-name)
+ (hscroll (window-hscroll))
+ (cur-page (pdf-view-current-page)))
+ (print (format
+ "%s: window-total-height %s, frame-height %s\n
+next line: vscroll value, second next line: output value (image-next-line)"
+ (alist-get 'pdf-scroll-window-status (window-parameters))
+ (window-total-height)
+ (frame-height))
+ (get-buffer-create "*pdf-scroll-log*"))
+ (if (not (equal (alist-get 'pdf-scroll-window-status (window-parameters)) 'lower))
+ (when (= (print
+ (window-vscroll nil pdf-view-have-image-mode-pixel-vscroll)
+ (get-buffer-create "*pdf-scroll-log*"))
+ (print (image-next-line arg) (get-buffer-create "*pdf-scroll-log*")))
+ (cond
+ ((not (window-full-height-p))
+ (condition-case nil
+ (window-resize (get-buffer-window) -1 nil t)
+ (error (delete-window)
+ (pop-to-buffer (get-file-buffer current-file))
+ (set-window-parameter nil 'pdf-scroll-window-status 'single)))
+ (image-next-line 1))
+ (t
+ (if (= (pdf-view-current-page) (pdf-cache-number-of-pages))
+ (message "No such page: %s" (+ (pdf-view-current-page) 1))
+ (display-buffer
+ (current-buffer)
+ '((display-buffer-below-selected)
+ (inhibit-same-window . t)
+ (window-height . 1)))
+ (set-window-parameter nil 'pdf-scroll-window-status 'upper)
+ (windmove-down)
+ (set-window-parameter nil 'pdf-scroll-window-status 'lower)
+ (pdf-view-goto-page cur-page)
+ (pdf-view-next-page)
+ (when (/= cur-page (pdf-view-current-page))
+ (image-bob)
+ (image-bol 1))
+ (image-set-window-hscroll hscroll)
+ (windmove-up)
+ (image-next-line 1)))))
+ (condition-case nil
+ (window-resize (get-buffer-window) +1 nil t)
+ (error (windmove-up)
+ (delete-window)
+ (pop-to-buffer (get-file-buffer current-file))
+ (set-window-parameter nil 'pdf-scroll-window-status 'single)))
+ (windmove-up)
+ (image-next-line 1)
+ (windmove-down)))
+ (message "pdf-continuous-scroll-mode not activated")))
+
+(defun pdf-continuous-scroll-forward (arg)
+ (interactive "P")
+ (let ((arg (or arg pdf-continuous-scroll-step-size)))
+ (dotimes (_ arg) (pdf-continuous-scroll-forward-line 1))))
+
+(defun pdf-continuous-scroll-mouse-scroll-forward ()
+ (interactive)
+ (with-selected-window
+ (or (caadr last-input-event) (selected-window))
+ (if pdf-continuous-scroll-reverse-scrolling
+ (pdf-continuous-scroll-backward nil)
+ (pdf-continuous-scroll-forward nil))))
+
+(defun pdf-continuous-scroll-backward-line (&optional arg)
+ "Scroll down by ARG lines if possible, else go to the previous page.
+This function is an adapted version of
+`pdf-view-previous-line-or-previous-page'. Although the ARG is
+kept here, this function generally works best without ARG is 1.
+To increase the step size for scrolling use the ARG in
+`pdf-continuous-scroll-backward'"
+ (if pdf-continuous-scroll-mode
+ (let ((hscroll (window-hscroll))
+ (cur-page (pdf-view-current-page))
+ (window-min-height (or pdf-continuous-scroll-custom-min-height
+ window-min-height)))
+ (print
+ (format
+ "%s: window-total-height %s, frame-height %s\n
+next line: vscroll value, second next line: output value (image-previous-line)"
+ (alist-get 'pdf-scroll-window-status (window-parameters))
+ (window-total-height)
+ (frame-height))
+ (get-buffer-create "*pdf-scroll-log*"))
+ (cond ((equal (alist-get 'pdf-scroll-window-status (window-parameters)) 'lower)
+ (cond ((= (window-total-height) window-min-height)
+ (delete-window)
+ (set-window-parameter nil 'pdf-scroll-window-status 'single)
+ (image-next-line 1))
+ (t (condition-case nil
+ (window-resize (get-buffer-window) -1 nil t)
+ (error nil))
+ (windmove-up)
+ (image-next-line 1)
+ (windmove-down))))
+ (t (when (= (print
+ (window-vscroll nil pdf-view-have-image-mode-pixel-vscroll)
+ (get-buffer-create "*pdf-scroll-log*"))
+ (print
+ (image-previous-line arg)
+ (get-buffer-create "*pdf-scroll-log*")))
+ (if (= (pdf-view-current-page) 1)
+ (message "No such page: 0")
+ (display-buffer-in-direction
+ (current-buffer)
+ (cons '(direction . above) '((window-height . 1))))
+ (set-window-parameter nil 'pdf-scroll-window-status 'lower)
+ (windmove-up)
+ (set-window-parameter nil 'pdf-scroll-window-status 'upper)
+ (pdf-view-goto-page cur-page)
+ (pdf-view-previous-page)
+ (when (/= cur-page (pdf-view-current-page))
+ (image-eob)
+ (image-bol 1))
+ (image-set-window-hscroll hscroll)
+ (window-resize (get-buffer-window) 1 nil t)))
+ (cond ((< (window-total-height) (- (frame-height) window-min-height))
+ (condition-case nil
+ (window-resize (get-buffer-window) 1 nil t)
+ (error nil)))
+ ((= (window-total-height) (- (frame-height) window-min-height))
+ (set-window-parameter nil 'pdf-scroll-window-status 'single)
+ (windmove-down)
+ (delete-window))))))
+ (message "pdf-continuous-scroll-mode not activated")))
+
+(defun pdf-continuous-scroll-backward (arg)
+ (interactive "P")
+ (let ((arg (or arg pdf-continuous-scroll-step-size)))
+ (dotimes (_ arg) (pdf-continuous-scroll-backward-line 1))))
+
+(defun pdf-continuous-scroll-mouse-scroll-backwards ()
+ (interactive)
+ (with-selected-window
+ (or (caadr last-input-event) (selected-window))
+ (if pdf-continuous-scroll-reverse-scrolling
+ (pdf-continuous-scroll-forward nil)
+ (pdf-continuous-scroll-backward nil))))
+
+(defun pdf-continuous-next-page (arg)
+ (declare (interactive-only pdf-view-previous-page))
+ (interactive "p")
+ (if pdf-continuous-scroll-mode
+ (let ((window-status (alist-get 'pdf-scroll-window-status (window-parameters))))
+ (let ((document-length (pdf-cache-number-of-pages)))
+ (if (if (equal window-status 'upper)
+ (= (pdf-view-current-page) (- document-length 1))
+ (= (pdf-view-current-page) document-length))
+ (message "No such page: %s" (+ document-length 1))
+ (cond ((equal window-status 'upper)
+ (windmove-down)
+ (with-no-warnings
+ (pdf-view-next-page arg))
+ (windmove-up)
+ (with-no-warnings
+ (pdf-view-next-page arg)))
+ ((equal window-status 'lower)
+ (windmove-up)
+ (with-no-warnings
+ (pdf-view-next-page arg))
+ (windmove-down)
+ (with-no-warnings
+ (pdf-view-next-page arg)))
+ (t (pdf-view-next-page))))))))
+
+(defun pdf-continuous-previous-page (arg)
+ (declare (interactive-only pdf-view-previous-page))
+ (interactive "p")
+ (if pdf-continuous-scroll-mode
+ (let ((window-status (alist-get 'pdf-scroll-window-status (window-parameters))))
+ (if (if (equal window-status 'lower)
+ (= (pdf-view-current-page) 2)
+ (= (pdf-view-current-page) 1))
+ (message "No such page: 0")
+ (cond ((equal window-status 'upper)
+ (windmove-down)
+ (with-no-warnings
+ (pdf-view-previous-page arg))
+ (windmove-up)
+ (with-no-warnings
+ (pdf-view-previous-page arg)))
+ ((equal window-status 'lower)
+ (windmove-up)
+ (with-no-warnings
+ (pdf-view-previous-page arg))
+ (windmove-down)
+ (with-no-warnings
+ (pdf-view-previous-page arg)))
+ (t (pdf-view-previous-page)))))))
+
+(defun pdf-cscroll-view-goto-page (page &optional window)
+ "Go to PAGE in PDF.
+
+If optional parameter WINDOW, go to PAGE in all `pdf-view'
+windows."
+ (interactive
+ (list (if current-prefix-arg
+ (prefix-numeric-value current-prefix-arg)
+ (read-number "Page: "))))
+ (unless (and (>= page 1)
+ (<= page (pdf-cache-number-of-pages)))
+ (error "No such page: %d" page))
+ (pdf-continuous-scroll-close-window-when-dual)
+ (pdf-view-goto-page page window))
+
+(defun pdf-cscroll-first-page ()
+ (interactive)
+ (pdf-continuous-scroll-close-window-when-dual)
+ (pdf-view-goto-page 1))
+
+(defun pdf-cscroll-last-page ()
+ (interactive)
+ (pdf-continuous-scroll-close-window-when-dual)
+ (pdf-view-goto-page (pdf-cache-number-of-pages)))
+
+(defun pdf-cscroll-kill-buffer-and-windows ()
+ (interactive)
+ (pdf-continuous-scroll-close-window-when-dual)
+ (kill-this-buffer))
+
+(defun pdf-cscroll-image-forward-hscroll (&optional n)
+ (interactive "p")
+ (let ((window-status (alist-get 'pdf-scroll-window-status (window-parameters))))
+ (cond ((equal window-status 'upper)
+ (windmove-down)
+ (image-forward-hscroll n)
+ (windmove-up)
+ (image-forward-hscroll n))
+ ((equal window-status 'lower)
+ (windmove-up)
+ (image-forward-hscroll n)
+ (windmove-down)
+ (image-forward-hscroll n))
+ (t (image-forward-hscroll n)))))
+
+(defun pdf-cscroll-image-backward-hscroll (&optional n)
+ (interactive "p")
+ (let ((window-status (alist-get 'pdf-scroll-window-status (window-parameters))))
+ (cond ((equal window-status 'upper)
+ (windmove-down)
+ (image-forward-hscroll (- n))
+ (windmove-up)
+ (image-forward-hscroll (- n)))
+ ((equal window-status 'lower)
+ (windmove-up)
+ (image-forward-hscroll (- n))
+ (windmove-down)
+ (image-forward-hscroll (- n)))
+ (t (image-forward-hscroll (- n))))))
+
+(defun pdf-cscroll-toggle-mode-line ()
+ (interactive)
+ (if (not mode-line-format)
+ (setq mode-line-format pdf-cscroll-mode-line-format)
+ (setq pdf-cscroll-mode-line-format mode-line-format)
+ (setq mode-line-format nil)))
+
+(defun pdf-cscroll-toggle-narrow-mode-line ()
+ (interactive)
+ (if (plist-get (custom-face-attributes-get 'mode-line (selected-frame)) :height)
+ (custom-set-faces
+ (list 'mode-line
+ (list
+ (list t pdf-cscroll-mode-line-original-face))))
+ (setq pdf-cscroll-mode-line-original-face
+ (custom-face-attributes-get 'mode-line (selected-frame)))
+ (custom-set-faces
+ ;; custom-set-faces was added by Custom.
+ ;; If you edit it by hand, you could mess it up, so be careful.
+ ;; Your init file should contain only one such instance.
+ ;; If there is more than one, they won't work right.
+ '(mode-line ((t (:background "black" :height 0.1)))))
+ ))
+
+(defun pdf-cscroll-imenu ()
+ (interactive)
+ (pdf-continuous-scroll-close-window-when-dual)
+ (cond ((fboundp 'counsel-imenu) (counsel-imenu))
+ ((fboundp 'helm-imenu) (helm-imenu))
+ (t (imenu (list (imenu-choose-buffer-index))))))
+
+(defun pdf-cscroll-annot-list-annotations ()
+ (interactive)
+ (pdf-continuous-scroll-close-window-when-dual)
+ (pdf-annot-list-annotations))
+
+
+(setq pdf-continuous-scroll-mode-map (make-sparse-keymap))
+(define-key pdf-continuous-scroll-mode-map (kbd "C-n") #'pdf-continuous-scroll-forward)
+(define-key pdf-continuous-scroll-mode-map (kbd "<down>") #'pdf-continuous-scroll-forward)
+(define-key pdf-continuous-scroll-mode-map (kbd "<wheel-down>") #'pdf-continuous-scroll-mouse-scroll-forward)
+(define-key pdf-continuous-scroll-mode-map (kbd "<mouse-5>") #'pdf-continuous-scroll-mouse-scroll-forward)
+(define-key pdf-continuous-scroll-mode-map (kbd "C-p") #'pdf-continuous-scroll-backward)
+(define-key pdf-continuous-scroll-mode-map (kbd "<up>") #'pdf-continuous-scroll-backward)
+(define-key pdf-continuous-scroll-mode-map (kbd "<wheel-up>") #'pdf-continuous-scroll-mouse-scroll-backwards)
+(define-key pdf-continuous-scroll-mode-map (kbd "<mouse-4>") #'pdf-continuous-scroll-mouse-scroll-backwards)
+(define-key pdf-continuous-scroll-mode-map "n" #'pdf-continuous-next-page)
+(define-key pdf-continuous-scroll-mode-map "p" #'pdf-continuous-previous-page)
+(define-key pdf-continuous-scroll-mode-map (kbd "<prior>") 'pdf-continuous-previous-page)
+(define-key pdf-continuous-scroll-mode-map (kbd "<next>") 'pdf-continuous-next-page)
+;; (define-key pdf-continuous-scroll-mode-map (kbd "M-<") #'pdf-cscroll-view-goto-page)
+(define-key pdf-continuous-scroll-mode-map (kbd "M-g g") #'pdf-cscroll-view-goto-page)
+(define-key pdf-continuous-scroll-mode-map (kbd "M-g M-g") #'pdf-cscroll-view-goto-page)
+(define-key pdf-continuous-scroll-mode-map (kbd "M-<") #'pdf-cscroll-first-page)
+(define-key pdf-continuous-scroll-mode-map (kbd "M->") #'pdf-cscroll-last-page)
+(define-key pdf-continuous-scroll-mode-map [remap forward-char] #'pdf-cscroll-image-forward-hscroll)
+(define-key pdf-continuous-scroll-mode-map [remap right-char] #'pdf-cscroll-image-forward-hscroll)
+(define-key pdf-continuous-scroll-mode-map [remap backward-char] #'pdf-cscroll-image-backward-hscroll)
+(define-key pdf-continuous-scroll-mode-map [remap left-char] #'pdf-cscroll-image-backward-hscroll)
+(define-key pdf-continuous-scroll-mode-map "T" #'pdf-cscroll-toggle-mode-line)
+(define-key pdf-continuous-scroll-mode-map "h" #'pdf-cscroll-toggle-narrow-mode-line)
+(define-key pdf-continuous-scroll-mode-map (kbd "q") #'(lambda () (interactive) (pdf-continuous-scroll-mode -1)))
+(define-key pdf-continuous-scroll-mode-map "Q" #'pdf-cscroll-kill-buffer-and-windows)
+(define-key pdf-continuous-scroll-mode-map (kbd "C-c C-a l") #'pdf-cscroll-annot-list-annotations)
+
+;;;###autoload
+(with-eval-after-load 'pdf-view
+ (define-key pdf-view-mode-map "c" #'pdf-continuous-scroll-mode))
+
+(when (boundp 'spacemacs-version)
+ (evil-define-minor-mode-key 'evilified 'pdf-continuous-scroll-mode
+ "j" #'pdf-continuous-scroll-forward
+ (kbd "<mouse-5>") #'pdf-continuous-scroll-mouse-scroll-forward
+ "k" #'pdf-continuous-scroll-backward
+ (kbd "<mouse-4>") #'pdf-continuous-scroll-mouse-scroll-backwards
+ "J" #'pdf-continuous-next-page
+ "K" #'pdf-continuous-previous-page
+ ;; (kbd "C-j") #'pdf-view-scroll-up-or-next-page
+ ;; (kbd "C-k") #'pdf-view-scroll-down-or-previous-page
+ (kbd "g t") #'pdf-cscroll-view-goto-page
+ (kbd "g g") #'pdf-cscroll-first-page
+ "G" #'pdf-cscroll-last-page
+ "M" #'pdf-cscroll-toggle-mode-line
+ "q" #'pdf-cscroll-kill-buffer-and-windows
+ "l" #'pdf-cscroll-image-forward-hscroll
+ "h" #'pdf-cscroll-image-backward-hscroll)
+ (spacemacs/set-leader-keys-for-minor-mode
+ 'pdf-continuous-scroll-mode
+ (kbd "a l") #'pdf-cscroll-annot-list-annotations))
+
+;;;###autoload
+(define-minor-mode pdf-continuous-scroll-mode
+ "Emulate continuous scroll with two synchronized buffers"
+
+ :init-value nil
+ :lighter " Continuous"
+ :keymap pdf-continuous-scroll-mode-map
+ (unless pdf-continuous-scroll-mode
+ (pdf-continuous-scroll-close-window-when-dual))
+ (set-window-parameter nil 'pdf-scroll-window-status 'single)
+ (defun pdf-outline-imenu-activate-link (&rest args)
+ ;; bug #14029
+ (pdf-continuous-scroll-close-window-when-dual)
+ (when (eq (nth 2 args) 'pdf-outline-imenu-activate-link)
+ (setq args (cdr args)))
+ (pdf-links-action-perform (nth 2 args))))
+
+(provide 'pdf-continuous-scroll-mode)
+;;; pdf-continuous-scroll-mode.el ends here
diff --git a/custom/profile-dotemacs.el b/custom/profile-dotemacs.el
new file mode 100644
index 00000000..f16e8652
--- /dev/null
+++ b/custom/profile-dotemacs.el
@@ -0,0 +1,200 @@
+;;; profile-dotemacs.el --- Profile your Emacs init file
+
+;; Copyright (C) 2010, 2012 David Engster
+
+;; Author: David Engster <dengste@eml.cc>
+
+;; This file is NOT part of GNU 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 2
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This is to easily profile your Emacs init file (or any other
+;; script-like Emacs Lisp file, for that matter).
+
+;; It will go over all sexp's (balanced expressions) in the file and
+;; run them through `benchmark-run'. It will then show the file with
+;; overlays applied in a way that let you easily find out which sexp's
+;; take the most time. Since time is relative, it's not the absolute
+;; value that counts but the percentage of the total running time.
+;;
+;; * All other sexp's with a percentage greater than
+;; `profile-dotemacs-low-percentage' will be preceded by a
+;; highlighted line, showing the results from `benchmark-run'.
+;; Also, the more 'reddish' the background of the sexp, the more
+;; time it needs.
+
+;; * All other sexp's will be grayed out to indicate that their
+;; running time is miniscule. You can still see the benchmark
+;; results in the minibuffer by hovering over the sexp with the
+;; mouse.
+
+;; You can only benchmark full sexp's, so if you wrapped large parts
+;; of your init file in some conditional clause, you'll have to remove
+;; that for getting finer granularity.
+
+;;; Usage:
+
+;; Start emacs as follows:
+;;
+;; emacs -Q -l <PATH>/profile-dotemacs.el -f profile-dotemacs
+;;
+;; with <PATH> being the path to where this file resides.
+
+;;; Caveats (thanks to Raffaele Ricciardi for reporting those):
+
+;; - The usual `--debug-init' for debugging your init file won't work
+;; with profile-dotemacs, so you'll have to call
+;; `toggle-debug-on-error', either on the commandline or at the
+;; beginning of your init file.
+;; - `load-file-name' is nil when the init file is being loaded
+;; by the profiler. This might matter if you perform the
+;; bulk of initializations in a different file.
+;; - Starting external shells like IELM or eshell in your init file
+;; might mess with overlay creation, so this must not be done.
+
+;;; Download:
+
+;; You can always get the latest version from
+;; http://randomsample.de/profile-dotemacs.el
+
+;;; Code:
+
+(require 'thingatpt)
+(require 'benchmark)
+
+;; User variables
+
+(defvar profile-dotemacs-file "~/.emacs.d/init.el"
+ "File to be profiled.")
+
+(defvar profile-dotemacs-low-percentage 3
+ "Percentage which should be considered low.
+All sexp's with a running time below this percentage will be
+grayed out.")
+
+(defface profile-dotemacs-time-face
+ '((((background dark)) (:background "OrangeRed1"))
+ (t (:background "red3")))
+ "Background color to indicate percentage of total time.")
+
+(defface profile-dotemacs-low-percentage-face
+ '((((background dark)) (:foreground "gray25"))
+ (t (:foreground "gray75")))
+ "Face for sexps below `profile-dotemacs-low-percentage'.")
+
+(defface profile-dotemacs-highlight-face
+ '((((background dark)) (:background "blue"))
+ (t (:background "yellow")))
+ "Highlight face for benchmark results.")
+
+;; Main function
+
+(defun profile-dotemacs ()
+ "Load `profile-dotemacs-file' and benchmark its sexps."
+ (interactive)
+ (with-current-buffer (find-file-noselect profile-dotemacs-file t)
+ (setq buffer-read-only t) ;; just to be sure
+ (goto-char (point-min))
+ (let (start end results)
+ (while
+ (< (point)
+ (setq end (progn
+ (forward-sexp 1)
+ (point))))
+ (forward-sexp -1)
+ (setq start (point))
+ (add-to-list
+ 'results
+ `(,start ,end
+ ,(benchmark-run
+ (eval (sexp-at-point)))))
+ (goto-char end))
+ (profile-dotemacs-show-results results)
+ (switch-to-buffer (current-buffer)))))
+
+;; Helper functions
+
+(defun profile-dotemacs-show-results (results)
+ "Show timings from RESULTS in current buffer."
+ (let ((totaltime (profile-dotemacs-totaltime results))
+ current percentage ov)
+ (while results
+ (let* ((current (pop results))
+ (ov (make-overlay (car current) (cadr current)))
+ (current (car (last current)))
+ (percentage (/ (+ (car current) (nth 2 current))
+ totaltime))
+ col benchstr lowface)
+ (setq col
+ (profile-dotemacs-percentage-color
+ percentage
+ (face-background 'default)
+ (face-background 'profile-dotemacs-time-face)))
+ (setq percentage (round (* 100 percentage)))
+ (setq benchstr (profile-dotemacs-make-benchstr current))
+ (overlay-put ov 'help-echo benchstr)
+ (if (and (numberp profile-dotemacs-low-percentage)
+ (< percentage profile-dotemacs-low-percentage))
+ (overlay-put ov 'face 'profile-dotemacs-low-percentage-face)
+ (overlay-put ov 'before-string
+ (propertize benchstr
+ 'face 'profile-dotemacs-highlight-face))
+ (overlay-put ov 'face
+ `(:background ,col)))))
+ (setq ov (make-overlay (1- (point-max)) (point-max)))
+ (overlay-put ov 'after-string
+ (propertize
+ (format "\n-----------------\nTotal time: %.2fs\n"
+ totaltime)
+ 'face 'profile-dotemacs-highlight-face))))
+
+(defun profile-dotemacs-totaltime (results)
+ "Calculate total time of RESULTS."
+ (let ((totaltime 0))
+ (mapc (lambda (x)
+ (let ((cur (car (last x))))
+ (setq totaltime (+ totaltime (car cur) (nth 2 cur)))))
+ results)
+ totaltime))
+
+(defun profile-dotemacs-percentage-color (percent col-begin col-end)
+ "Calculate color according to PERCENT between COL-BEGIN and COL-END."
+ (let* ((col1 (color-values col-begin))
+ (col2 (color-values col-end))
+ (col
+ (mapcar (lambda (c)
+ (round
+ (+ (* (- 1 percent) (nth c col1))
+ (* percent (nth c col2)))))
+ '(0 1 2))))
+ (format "RGB:%04x/%04x/%04x"
+ (car col)
+ (nth 1 col)
+ (nth 2 col))))
+
+(defun profile-dotemacs-make-benchstr (timings)
+ "Create descriptive benchmark string from TIMINGS."
+ (format
+ (concat
+ "<Percentage: %d ; "
+ "Time: %.2f ; "
+ "Number of GC: %d ; "
+ "Time for GC: %.2f>\n")
+ percentage
+ (car timings) (nth 1 timings) (nth 2 timings)))
+
+
+;; profile-dotemacs.el ends here
diff --git a/custom/titlecase-data.el b/custom/titlecase-data.el
new file mode 100644
index 00000000..a6468586
--- /dev/null
+++ b/custom/titlecase-data.el
@@ -0,0 +1,721 @@
+;;; titlecase-data.el --- Data for titlecase.el -*- lexical-binding: t; -*-
+
+;; Author: Case Duckworth <acdw@acdw.net>
+;; Maintainer: Case Duckworth <acdw@acdw.net>
+;; URL: https://github.com/duckwork/titlecase.el
+
+;; 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:
+
+;; Since the `titlecase' package requires a lot of data, that data lives here so
+;; as to not clog up the main package.
+
+;; Since [[https://github.com/duckwork/titlecase.el/issues/23][Issue #23]] makes
+;; a good point that I should like, make more sense in the commentary and README
+;; of this repository. At the same time, those couple of comments I wrote in
+;; there I don't want to just /delete/, so until I write this up in a proper
+;; blog post, I've included it here, in the data file, because this is where
+;; these implementation notes will be of most interest.
+
+;; The only setting you really should need to set is =titlecase-style=, which
+;; see. Each of these styles has a different set of rules regarding which words
+;; to capitalize in a title. After you've set =titlecase-style=, you can bind
+;; the command =titlecase-dwim= to a key, or call it using M-x, and it will
+;; either title-case your region (if it's active) or the current line.
+
+;; The tricky part is figuring out what words to capitalize in the title.
+
+;; Articles (~a~, ~an~, ~the~) are downcased.
+
+;; The first word of a title and all "important words" (generally nouns,
+;; pronouns, adjectives, verbs, and adverbs) are capitalized. The last word of
+;; a title is always capitalized, but only in Chicago, AP, Bluebook, AMA, NY
+;; Times, and Wikipedia.
+
+;; /All/ prepositions are downcased in Chicago, MLA, AP, NY Times, and
+;; Wikipedia, regardless of length; for APA, Bluebook, AMA, and Wikipedia, only
+;; prepositions shorter than 5 letters are (presumably, capitalize those longer
+;; than 5 letters, however only Wikipedia was clear on that point).
+
+;; Coordinating conjunctions are capitalized in Chicago and APA (presumably),
+;; but downcased in MLA, AP, Bluebook, AMA, NY Times, and Wikipedia.
+
+;; Hyphenated words are tricky: I could possibly figure out a way to have lookup
+;; tables to determine when to capitalize the second part of a hyphenated word,
+;; but I haven't implemented them yet. At any rate, the rules tend to be vague
+;; enough that it's hard to program anyway: For example, Chicago, APA, MLA, and
+;; AP lowercase the second word "after a hyphenated prefix (e.g., Mid-, Anti-,
+;; Super, etc.) in compound modifiers," but MLA and APA capitalize the second
+;; part of "hyphenated major words (e.g., Self-Report not Self-report).
+
+;; Perhaps unsurprisingly, the AMA (American Medical Association, used in the
+;; scientific community) has the most comprehensive capitalization rules around
+;; hyphenated words. I'll just copy-paste the bullet points here:
+
+;; - Lowercase the second word in a hyphenated compound when it is
+;; a prefix or suffix (e.g., "Anti-itch","world-wide") or part of a single word.
+;; - Capitalize the second word in a hyphenated compound if both words are equal
+;; and not suffices or prefixes (e.g., "Cost-Benefit")
+;; - Capitalize the first non-Greek letter after a lowercase Greek letter (e.g.,
+;; "ω-Bromohexanoic")
+;; - Lowercase the first non-Greek letter after a capital Greek letter (e.g.,
+;; "Δ-9-tetrahydrocannabinol") #+end_quote
+
+;; (The AMA also has a rule about capitilizing the genus but not species
+;; epithet, but the lookup on that would be wild as hell, so I trust yall to
+;; know on that one.)
+
+;; ~To~ as an infinitive is downcased in all /except/ AP. This is a rule I
+;; simply cannot implement without knowing whether the /next/ word is a verb,
+;; which would require expensive lookups, which even then wouldn't be foolproof.
+
+;; Now that I'm thinking about it, most styles count phrasal verbs (like "play
+;; with") as important enough to capitalize, when "with" would usually /not/ be
+;; capitalized, but again, open categories like phrasal verbs simply do not work
+;; in a package like this.
+
+;; ALL OF THIS IS TO SAY that titlecase offers a best-effort attempt to
+;; titlecase a line or region of text, but you should absolutely
+;; double-triple-check against the style guide you're writing for if you're
+;; trying for publication or something like that.
+
+;; SEE ALSO:
+
+;; Prior art:
+
+;; - https://emacs.stackexchange.com/questions/66361/#66362
+;; - https://github.com/novoid/title-capitalization.el
+;; - https://hungyi.net/posts/programmers-way-to-title-case/
+
+;; Rules:
+
+;; - https://capitalizemytitle.com/#capitalizationrules
+;; - https://titlecaseconverter.com/rules/
+
+;;; Code:
+
+(require 'seq)
+
+(defvar titlecase-prepositions
+ ;; This was pulled from Wikipedia, and so is somewhat weird.
+ ;; https://en.wikipedia.org/wiki/List_of_English_prepositions
+ '("'thout" "'tween" "aboard" "about" "above"
+ "abreast" "absent" "abt." "across" "after" "against" "ago" "aloft" "along"
+ "alongside" "amid" "amidst" "among" "amongst" "anti" "apart" "apropos"
+ "around" "as" "aside" "aslant" "astride" "at" "atop" "away" "before"
+ "behind" "below" "beneath" "beside" "besides" "between" "beyond" "but" "by"
+ "c." "ca." "circa" "come" "concerning" "contra" "counting" "cum" "despite"
+ "down" "during" "effective" "ere" "except" "excepting" "excluding" "failing"
+ "following" "for" "from" "hence" "in" "including" "inside" "into" "less"
+ "like" "mid" "midst" "minus" "mod" "modulo" "near" "nearer" "nearest"
+ "neath" "next" "notwithstanding" "o'" "o'er" "of" "off" "on"
+ "onto" "ontop" "opposite" "out" "outside" "over" "pace" "past" "pending"
+ "per" "plus" "post" "pre" "pro" "qua" "re" "regarding" "respecting" "round"
+ "sans" "save" "saving" "short" "since" "sub" "t'" "than" "through"
+ "throughout" "thru" "thruout" "till" "times" "to" "toward" "towards" "under"
+ "underneath" "unlike" "until" "unto" "up" "upon" "v." "versus" "via"
+ "vis-à-vis" "vs." "w." "w/" "w/i" "w/o" "wanting" "with" "within"
+ "without")
+ "List of prepositions in English.
+This list is, by necessity, incomplete, even though prepositions
+are a closed lexical group in the English language. This list
+was pulled and culled from
+https://en.wikipedia.org/wiki/List_of_English_prepositions.")
+
+(defvar titlecase-phrasal-verbs
+ (let (verbs)
+ (dolist
+ (phrase
+ '(("account" "for") ("ache" "for") ("act" "on")
+ ("act" "out") ("act" "up") ("add" "on") ("add" "up")
+ ("add" "up" "to") ("aim" "at") ("allow" "for") ("angle" "for")
+ ("answer" "back") ("answer" "for") ("argue" "out") ("ask" "after")
+ ("ask" "around") ("ask" "for") ("ask" "in") ("ask" "out")
+ ("ask" "over") ("ask" "round") ("auction" "off") ("back" "away")
+ ("back" "down") ("back" "off") ("back" "out") ("back" "out" "of")
+ ("back" "up") ("bag" "out") ("bail" "out") ("bail" "out" "on")
+ ("bail" "up") ("ball" "up") ("balls" "up") ("bang" "about")
+ ("bang" "around") ("bang" "on" "about") ("bang" "out") ("bang" "up")
+ ("bank" "on") ("barge" "in") ("barge" "into") ("bash" "about")
+ ("bash" "in") ("bash" "out") ("bash" "up") ("bawl" "out")
+ ("be" "after") ("be" "along") ("be" "away") ("be" "cut" "out" "for")
+ ("be" "cut" "up") ("be" "down") ("be" "down" "with")
+ ("be" "fed" "up") ("be" "in") ("be" "in" "on") ("be" "not" "on")
+ ("be" "off") ("be" "on") ("be" "on" "about") ("be" "onto")
+ ("be" "out") ("be" "out" "of") ("be" "out" "to")
+ ("be" "snowed" "under") ("be" "taken" "aback") ("be" "taken" "with")
+ ("be" "up") ("be" "up" "to") ("bear" "down" "on") ("bear" "on")
+ ("bear" "out") ("bear" "up") ("bear" "up" "under") ("bear" "with")
+ ("beat" "down") ("beat" "up") ("beaver" "away")
+ ("beaver" "away" "at") ("bed" "down") ("bed" "out") ("beef" "up")
+ ("belt" "out") ("belt" "up") ("bend" "down") ("bend" "over")
+ ("bend" "over" "backwards") ("black" "out") ("blank" "out")
+ ("blare" "out") ("blast" "off") ("blaze" "away") ("bliss" "out")
+ ("block" "in") ("block" "off") ("block" "out") ("block" "up")
+ ("blow" "away") ("blow" "down") ("blow" "off") ("blow" "out")
+ ("blow" "over") ("blow" "up") ("blurt" "out") ("bog" "down")
+ ("bog" "in") ("bog" "into") ("bog" "off") ("boil" "down")
+ ("boil" "down" "to") ("boil" "over") ("boil" "up")
+ ("bone" "up" "on") ("book" "in") ("book" "into") ("book" "up")
+ ("boot" "up") ("border" "on") ("boss" "about") ("boss" "around")
+ ("botch" "up") ("bottle" "away") ("bottle" "out") ("bottle" "up")
+ ("bottom" "out") ("bounce" "into") ("bounce" "off") ("bowl" "over")
+ ("box" "in") ("box" "up") ("branch" "out") ("break" "away")
+ ("break" "down") ("break" "in") ("break" "off") ("break" "out" "of")
+ ("break" "through") ("break" "up") ("breeze" "along")
+ ("breeze" "in") ("breeze" "into") ("breeze" "through")
+ ("brighten" "up") ("bring" "about") ("bring" "along")
+ ("bring" "around") ("bring" "back") ("bring" "down")
+ ("bring" "forth") ("bring" "forward") ("bring" "in") ("bring" "on")
+ ("bring" "out") ("bring" "out" "in") ("bring" "round")
+ ("bring" "up") ("brush" "off") ("brush" "up") ("bubble" "over")
+ ("bucket" "down") ("budge" "up") ("buff" "up") ("buff" "up" "on")
+ ("bug" "off") ("bug" "out") ("build" "up") ("bulk" "out")
+ ("bump" "into") ("bump" "off") ("bump" "up") ("bundle" "off")
+ ("bundle" "out") ("bundle" "up") ("bunk" "off") ("buoy" "up")
+ ("burn" "down") ("burn" "off") ("burn" "out") ("burst" "into")
+ ("butt" "in") ("butt" "out") ("butter" "up") ("buy" "in")
+ ("buy" "into") ("buy" "off") ("buy" "out") ("buy" "up")
+ ("buzz" "around") ("buzz" "off") ("buzz" "off") ("call" "after")
+ ("call" "around") ("call" "back") ("call" "for") ("call" "forth")
+ ("call" "in") ("call" "off") ("call" "on") ("call" "round")
+ ("call" "up") ("calm" "down") ("cancel" "out") ("care" "for")
+ ("carried" "away") ("carry" "forward") ("carry" "off")
+ ("carry" "on") ("carry" "on" "with") ("carry" "out")
+ ("carry" "over") ("carry" "through") ("cart" "off") ("cash" "in")
+ ("cash" "in" "on") ("cash" "out") ("cash" "up") ("catch" "at")
+ ("catch" "on") ("catch" "out") ("catch" "up") ("catch" "up" "on")
+ ("catch" "up" "with") ("cater" "for") ("cater" "to") ("cave" "in")
+ ("chalk" "out") ("chalk" "up") ("chalk" "up" "to") ("chance" "upon")
+ ("change" "over") ("charge" "up") ("charge" "with") ("chase" "down")
+ ("chase" "off") ("chase" "up") ("chat" "up") ("cheat" "on")
+ ("cheat" "out" "of") ("check" "by") ("check" "in") ("check" "into")
+ ("check" "off") ("check" "out") ("check" "out" "of")
+ ("check" "over") ("cheer" "on") ("cheer" "up") ("chew" "on")
+ ("chew" "out") ("chew" "over") ("chew" "up") ("chicken" "out")
+ ("chill" "out") ("chime" "in") ("chip" "away" "at") ("chip" "in")
+ ("choose" "up") ("chop" "down") ("chop" "up") ("chuck" "away")
+ ("chuck" "in") ("chuck" "out") ("chuck" "up") ("churn" "out")
+ ("clag" "up") ("clam" "up") ("clamp" "down" "on") ("claw" "back")
+ ("clean" "out") ("clean" "up") ("clear" "away") ("clear" "off")
+ ("clear" "out") ("clear" "up") ("click" "through") ("climb" "down")
+ ("clog" "up") ("close" "down") ("close" "in") ("close" "in" "on")
+ ("close" "in" "upon") ("close" "off") ("close" "on") ("close" "out")
+ ("close" "up") ("cloud" "over") ("clown" "about") ("clown" "around")
+ ("cock" "up") ("color" "up") ("colour" "up") ("come" "about")
+ ("come" "across") ("come" "apart") ("come" "before") ("come" "by")
+ ("come" "down") ("come" "down" "on") ("come" "down" "with")
+ ("come" "forth") ("come" "forth" "with") ("come" "from")
+ ("come" "in") ("come" "into") ("come" "into" "use") ("come" "off")
+ ("come" "off" "it") ("come" "on") ("come" "out") ("come" "out" "in")
+ ("come" "out" "with") ("come" "over") ("come" "round")
+ ("come" "through") ("come" "through" "with") ("come" "to")
+ ("come" "up") ("come" "up" "against") ("come" "up" "with")
+ ("come" "upon") ("conjure" "up") ("conk" "out") ("contract" "in")
+ ("contract" "out") ("contract" "out" "of") ("cool" "down")
+ ("coop" "up") ("cop" "it") ("cop" "off") ("cop" "out")
+ ("cotton" "on") ("could" "do" "with") ("count" "in") ("count" "on")
+ ("count" "out") ("count" "up") ("cozy" "up") ("cozy" "up" "to")
+ ("crack" "down" "on") ("crack" "on") ("crack" "up") ("crank" "out")
+ ("crank" "up") ("crash" "out") ("creep" "in") ("creep" "into")
+ ("creep" "out") ("creep" "out" "on") ("creep" "over")
+ ("creep" "up" "on") ("crop" "up") ("cross" "off") ("cross" "out")
+ ("crumb" "down") ("cry" "off") ("cry" "out") ("cut" "across")
+ ("cut" "back") ("cut" "back" "on") ("cut" "down")
+ ("cut" "down" "on") ("cut" "in") ("cut" "it" "out") ("cut" "off")
+ ("cut" "out") ("cut" "out" "on") ("cut" "up") ("dash" "off")
+ ("dawn" "on") ("die" "away") ("die" "down") ("die" "for")
+ ("die" "off") ("die" "out") ("dig" "in") ("dig" "into") ("dig" "up")
+ ("dine" "out") ("dine" "out" "on") ("dip" "in") ("dip" "into")
+ ("dip" "out") ("disagree" "with") ("dish" "out") ("dish" "up")
+ ("dive" "in") ("dive" "into") ("divvy" "out") ("divvy" "up")
+ ("do" "away" "with") ("do" "out" "of") ("do" "up") ("do" "without")
+ ("doss" "about") ("doss" "around") ("doss" "down") ("doze" "off")
+ ("drag" "on") ("draw" "back") ("draw" "down") ("draw" "in")
+ ("draw" "into") ("draw" "on") ("draw" "out") ("draw" "up")
+ ("dream" "of") ("dream" "up") ("dredge" "up") ("dress" "down")
+ ("dress" "up") ("drift" "apart") ("drift" "off") ("drill" "down")
+ ("drill" "down" "through") ("drill" "into") ("drink" "up")
+ ("drive" "away") ("drive" "back") ("drive" "by") ("drive" "off")
+ ("drive" "out") ("drive" "up") ("drone" "on") ("drop" "around")
+ ("drop" "away") ("drop" "back") ("drop" "by") ("drop" "in")
+ ("drop" "off") ("drop" "out") ("drop" "over") ("drop" "round")
+ ("drop" "someone" "in" "it") ("drop" "through") ("drown" "in")
+ ("drown" "out") ("drum" "into") ("drum" "out") ("drum" "up")
+ ("duck" "out" "of") ("duff" "up") ("dumb" "down") ("dwell" "on")
+ ("dwell" "upon") ("ease" "off") ("ease" "up") ("eat" "away")
+ ("eat" "in") ("eat" "into") ("eat" "out") ("eat" "up")
+ ("ebb" "away") ("edge" "out") ("edge" "up") ("egg" "on")
+ ("eke" "out") ("embark" "on") ("embark" "upon") ("empty" "out")
+ ("end" "in") ("end" "up") ("end" "up" "with") ("enter" "for")
+ ("enter" "into") ("eye" "up") ("face" "off") ("face" "up" "to")
+ ("faff" "about") ("faff" "around") ("fall" "about") ("fall" "apart")
+ ("fall" "back") ("fall" "back" "on") ("fall" "behind")
+ ("fall" "down") ("fall" "for") ("fall" "in") ("fall" "into")
+ ("fall" "off") ("fall" "out") ("fall" "over") ("fall" "through")
+ ("fall" "under") ("farm" "out") ("fart" "about") ("fart" "around")
+ ("fasten" "down") ("fasten" "on") ("fasten" "onto") ("fasten" "up")
+ ("fathom" "out") ("fatten" "up") ("fawn" "on") ("fawn" "over")
+ ("feed" "off") ("feed" "on") ("feed" "up") ("feel" "up")
+ ("feel" "up" "to") ("fend" "for") ("fend" "off") ("ferret" "out")
+ ("fess" "up") ("fess" "up" "to") ("fiddle" "about")
+ ("fiddle" "around") ("fiddle" "away") ("fight" "back")
+ ("fight" "it" "out") ("fight" "off") ("figure" "on")
+ ("figure" "out") ("file" "away") ("fill" "in") ("fill" "in" "for")
+ ("fill" "in" "on") ("fill" "out") ("fill" "up") ("filter" "in")
+ ("filter" "out") ("find" "out") ("finish" "off") ("fink" "on")
+ ("fink" "out") ("fire" "away") ("fire" "off") ("fire" "up")
+ ("firm" "up") ("fish" "for") ("fish" "out") ("fit" "in")
+ ("fit" "into") ("fit" "up") ("fix" "up") ("fizzle" "out")
+ ("flag" "down") ("flag" "up") ("flake" "out") ("flame" "out")
+ ("flame" "up") ("flare" "out") ("flare" "up") ("flesh" "out")
+ ("flick" "over") ("flick" "through") ("flip" "off") ("flip" "out")
+ ("flip" "through") ("flog" "off") ("floor" "it") ("flounce" "off")
+ ("flounce" "out") ("fly" "about") ("fly" "around") ("fly" "at")
+ ("fly" "by") ("fly" "into") ("fob" "off") ("fob" "off" "on")
+ ("fob" "off" "onto") ("fob" "off" "with") ("focus" "on")
+ ("fold" "up") ("fool" "around") ("forge" "ahead") ("freak" "out")
+ ("freeze" "out") ("freeze" "over") ("freeze" "up") ("freshen" "up")
+ ("front" "for") ("front" "off") ("front" "onto") ("front" "out")
+ ("front" "up") ("frown" "on") ("gad" "about") ("gad" "around")
+ ("gag" "for") ("gang" "up") ("gang" "up" "on") ("gear" "up")
+ ("geek" "out") ("get" "about") ("get" "above") ("get" "across")
+ ("get" "across" "to") ("get" "after") ("get" "ahead")
+ ("get" "ahead" "of") ("get" "along") ("get" "along" "in")
+ ("get" "along" "with") ("get" "around") ("get" "around")
+ ("get" "around" "to") ("get" "around" "to") ("get" "at")
+ ("get" "away") ("get" "away" "from") ("get" "away" "with")
+ ("get" "back") ("get" "back" "at") ("get" "back" "into")
+ ("get" "back" "to") ("get" "back" "together") ("get" "behind")
+ ("get" "behind" "with") ("get" "by") ("get" "by" "on")
+ ("get" "by" "with") ("get" "down") ("get" "down" "on")
+ ("get" "down" "to") ("get" "in") ("get" "in" "on")
+ ("get" "in" "with") ("get" "into") ("get" "it") ("get" "it" "off")
+ ("get" "it" "off" "with") ("get" "it" "on") ("get" "it" "on" "with")
+ ("get" "it" "together") ("get" "it" "up") ("get" "off")
+ ("get" "off" "it") ("get" "off" "on") ("get" "off" "with")
+ ("get" "on") ("get" "on" "at") ("get" "on" "for") ("get" "on" "to")
+ ("get" "on" "with") ("get" "onto") ("get" "out") ("get" "out" "of")
+ ("get" "over") ("get" "over" "with") ("get" "round") ("get" "round")
+ ("get" "round" "to") ("get" "through") ("get" "through" "to")
+ ("get" "to") ("get" "together") ("get" "up") ("get" "up" "to")
+ ("give") ("give" "away") ("give" "back") ("give" "in")
+ ("give" "in" "to") ("give" "it" "to") ("give" "it" "up" "for")
+ ("give" "it" "up" "to") ("give" "of") ("give" "off") ("give" "onto")
+ ("give" "out") ("give" "over") ("give" "over" "to") ("give" "up")
+ ("give" "up") ("give" "up" "on") ("give" "up" "to") ("give" "way")
+ ("give" "way" "to") ("give" "yourself" "up")
+ ("give" "yourself" "up" "to") ("gloss" "over") ("gnaw" "at")
+ ("gnaw" "away" "at") ("go" "about") ("go" "across") ("go" "after")
+ ("go" "against") ("go" "ahead") ("go" "ahead" "with")
+ ("go" "along" "with") ("go" "around") ("go" "at") ("go" "away")
+ ("go" "back") ("go" "back" "on") ("go" "before") ("go" "below")
+ ("go" "by") ("go" "down") ("go" "down" "on") ("go" "down" "to")
+ ("go" "down" "with") ("go" "for") ("go" "forth") ("go" "forward")
+ ("go" "in") ("go" "in" "for") ("go" "in" "with") ("go" "into")
+ ("go" "it") ("go" "it" "alone") ("go" "off") ("go" "off" "with")
+ ("go" "on") ("go" "on" "about") ("go" "on" "at") ("go" "on" "to")
+ ("go" "on" "with") ("go" "out") ("go" "out" "for") ("go" "out" "to")
+ ("go" "out" "with") ("go" "over") ("go" "over" "to") ("go" "past")
+ ("go" "round") ("go" "through") ("go" "through" "with")
+ ("go" "together") ("go" "towards") ("go" "under") ("go" "up")
+ ("go" "up") ("go" "up" "to") ("go" "with") ("go" "without")
+ ("goof" "around") ("goof" "off") ("goof" "up") ("grass" "on")
+ ("grass" "up") ("grey" "out") ("grind" "away") ("grind" "down")
+ ("grind" "into") ("grind" "on") ("grind" "out") ("grind" "up")
+ ("grow" "apart") ("grow" "away" "from") ("grow" "back")
+ ("grow" "from") ("grow" "into") ("grow" "on") ("grow" "out")
+ ("grow" "out" "of") ("grow" "to") ("grow" "together") ("grow" "up")
+ ("grow" "up" "on") ("grow" "upon") ("gun" "for") ("hack" "around")
+ ("hack" "into") ("hack" "off") ("ham" "up") ("hammer" "away" "at")
+ ("hammer" "into") ("hammer" "out") ("hand" "back") ("hand" "down")
+ ("hand" "in") ("hand" "on") ("hand" "out") ("hand" "over")
+ ("hang" "about") ("hang" "about") ("hang" "around") ("hang" "back")
+ ("hang" "back" "from") ("hang" "in" "there") ("hang" "on")
+ ("hang" "onto") ("hang" "out") ("hang" "out" "for") ("hang" "over")
+ ("hang" "together") ("hang" "up") ("hang" "up" "on") ("hang" "with")
+ ("hanker" "after") ("hanker" "for") ("harp" "on") ("have" "against")
+ ("have" "around") ("have" "down" "as") ("have" "in")
+ ("have" "it" "away") ("have" "it" "in" "for") ("have" "it" "off")
+ ("have" "it" "out" "with") ("have" "off") ("have" "on")
+ ("have" "over") ("have" "round") ("have" "up") ("head" "for")
+ ("head" "off") ("head" "out") ("head" "up") ("heat" "up")
+ ("help" "out") ("hit" "back") ("hit" "for") ("hit" "it" "off")
+ ("hit" "it" "off" "with") ("hit" "on") ("hit" "out" "at")
+ ("hit" "up") ("hit" "up" "on") ("hit" "upon") ("hit" "with")
+ ("hold" "against") ("hold" "back") ("hold" "back" "from")
+ ("hold" "down") ("hold" "forth") ("hold" "off") ("hold" "on")
+ ("hold" "on" "to") ("hold" "onto") ("hold" "out")
+ ("hold" "out" "against") ("hold" "out" "for") ("hold" "out" "on")
+ ("hold" "over") ("hold" "together") ("hold" "up") ("hold" "with")
+ ("home" "in" "on") ("hone" "in" "on") ("hook" "up")
+ ("hook" "up" "to") ("hoon" "around") ("horse" "around")
+ ("hound" "out") ("hunker" "down") ("hunt" "down") ("hunt" "out")
+ ("hunt" "up") ("hush" "up") ("iron" "out") ("issue" "forth")
+ ("jack" "around") ("jack" "in") ("jack" "up") ("jam" "on")
+ ("jaw" "away") ("jazz" "up") ("joke" "around") ("jot" "down")
+ ("juice" "up") ("jump" "at") ("jump" "in") ("jump" "on")
+ ("keel" "over") ("keep" "around") ("keep" "at") ("keep" "away")
+ ("keep" "back") ("keep" "down") ("keep" "from") ("keep" "in")
+ ("keep" "off") ("keep" "on") ("keep" "out") ("keep" "to")
+ ("keep" "up") ("keep" "up" "at") ("keep" "up" "with") ("key" "in")
+ ("kick" "about") ("kick" "around") ("kick" "around" "with")
+ ("kick" "back") ("kick" "down") ("kick" "in") ("kick" "off")
+ ("kick" "out") ("kick" "up") ("kiss" "off") ("kiss" "up" "to")
+ ("knock" "about") ("knock" "around") ("knock" "back")
+ ("knock" "down") ("knock" "it" "off") ("knock" "off")
+ ("knock" "out") ("knock" "together") ("knock" "up")
+ ("knuckle" "down") ("knuckle" "under") ("lap" "up")
+ ("large" "it" "up") ("lark" "about") ("lark" "around")
+ ("lark" "it" "up") ("lash" "down") ("lash" "into") ("lash" "out")
+ ("lash" "out" "against") ("lash" "out" "at") ("lash" "out" "on")
+ ("latch" "on") ("latch" "on" "to") ("latch" "onto") ("lay" "down")
+ ("lay" "into") ("lay" "off") ("lay" "on") ("lay" "out")
+ ("lead" "to") ("leak" "out") ("lean" "on") ("leave" "on")
+ ("leave" "out") ("let" "down") ("let" "in") ("let" "off")
+ ("let" "on") ("let" "out") ("lie" "down") ("lie" "with")
+ ("lift" "off") ("light" "up") ("lighten" "up") ("limber" "up")
+ ("limber" "up" "for") ("line" "up") ("link" "up")
+ ("link" "up" "with") ("listen" "out" "for") ("live" "by")
+ ("live" "down") ("live" "for") ("live" "in") ("live" "it" "up")
+ ("live" "off") ("live" "out") ("live" "through") ("live" "together")
+ ("live" "up" "to") ("live" "with") ("load" "down") ("load" "up")
+ ("load" "up" "on") ("lock" "away") ("lock" "down") ("lock" "in")
+ ("lock" "onto") ("lock" "out") ("lock" "up")
+ ("lock" "yourself" "away") ("log" "in") ("log" "into") ("log" "off")
+ ("log" "on") ("log" "out") ("look" "after") ("look" "back")
+ ("look" "down" "on") ("look" "for") ("look" "forward" "to")
+ ("look" "in") ("look" "in" "on") ("look" "into") ("look" "like")
+ ("look" "on") ("look" "on" "as") ("look" "out") ("look" "over")
+ ("look" "round") ("look" "to") ("look" "up") ("look" "up" "to")
+ ("look" "upon" "as") ("lord" "it" "over") ("lose" "out")
+ ("lose" "out" "on") ("lose" "out" "to") ("luck" "out")
+ ("magic" "away") ("make" "do" "with") ("make" "for") ("make" "into")
+ ("make" "it") ("make" "it" "up" "to") ("make" "off" "with")
+ ("make" "out") ("make" "over") ("make" "up") ("make" "up" "for")
+ ("make" "up" "to") ("make" "with") ("mark" "down")
+ ("mark" "down" "as") ("mark" "off") ("mark" "out")
+ ("mark" "out" "for") ("mark" "out" "from") ("mark" "up")
+ ("marry" "in") ("marry" "out") ("mash" "up") ("max" "out")
+ ("measure" "against") ("measure" "off") ("measure" "out")
+ ("measure" "up") ("meet" "with") ("mess" "about")
+ ("mess" "about" "with") ("mess" "around") ("mess" "around" "with")
+ ("mess" "over") ("mess" "up") ("mess" "with") ("mill" "around")
+ ("miss" "out") ("miss" "out" "on") ("mix" "up") ("monkey" "around")
+ ("mooch" "about") ("mooch" "around") ("mop" "up") ("mope" "about")
+ ("mope" "around") ("mount" "up") ("mouth" "off") ("move" "on")
+ ("mug" "up") ("mug" "up" "on") ("mull" "over") ("muscle" "in")
+ ("muscle" "in" "on") ("muscle" "into") ("muscle" "out")
+ ("naff" "off") ("nag" "at") ("nail" "down") ("name" "after")
+ ("nip" "off") ("nip" "out") ("nod" "off") ("nose" "about")
+ ("nose" "around") ("note" "down") ("nut" "out") ("occur" "to")
+ ("open" "up") ("opt" "for") ("opt" "out") ("owe" "to") ("own" "up")
+ ("pack" "away") ("pack" "in") ("pack" "it" "in") ("pack" "off")
+ ("pack" "out") ("pack" "up") ("pad" "down") ("pad" "out")
+ ("pair" "off") ("pair" "off" "with") ("pair" "up") ("palm" "off")
+ ("pan" "out") ("paper" "over") ("pare" "back") ("pare" "down")
+ ("pass" "around") ("pass" "as") ("pass" "away") ("pass" "by")
+ ("pass" "for") ("pass" "off") ("pass" "on") ("pass" "out")
+ ("pass" "over") ("pass" "through") ("pass" "to") ("pass" "up")
+ ("patch" "up") ("pay" "back") ("pay" "for") ("pay" "into")
+ ("pay" "off") ("peck" "at") ("peg" "away") ("peg" "down")
+ ("peg" "it") ("peg" "out") ("pencil" "in") ("perk" "up")
+ ("peter" "out") ("phase" "in") ("phase" "out") ("phrasal" "verb")
+ ("phrasal" "verbs") ("pick" "at") ("pick" "off") ("pick" "on")
+ ("pick" "out") ("pick" "through") ("pick" "up")
+ ("pick" "up" "after") ("pick" "up" "on") ("pick" "yourself" "up")
+ ("pig" "off") ("pig" "out") ("pile" "up") ("pin" "down")
+ ("pin" "on") ("pin" "up") ("pine" "away") ("pipe" "down")
+ ("pipe" "up") ("pit" "against") ("pit" "out") ("pitch" "for")
+ ("pitch" "in") ("pitch" "into") ("play" "along") ("play" "around")
+ ("play" "at") ("play" "away") ("play" "back") ("play" "down")
+ ("play" "off") ("play" "on") ("play" "out") ("play" "up")
+ ("play" "up" "to") ("play" "upon") ("play" "with") ("plead" "out")
+ ("plough" "back") ("plough" "into") ("plough" "on")
+ ("plough" "through") ("plough" "up") ("plow" "back") ("plow" "into")
+ ("plow" "on") ("plow" "through") ("plow" "up") ("plug" "in")
+ ("plump" "for") ("point" "out") ("polish" "off") ("polish" "up")
+ ("pony" "up") ("poop" "out") ("poop" "out" "on") ("pop" "in")
+ ("pop" "off") ("pop" "out") ("pop" "up") ("potter" "about")
+ ("potter" "around") ("pour" "down") ("pour" "forth")
+ ("prattle" "on") ("press" "ahead") ("press" "on") ("price" "up")
+ ("print" "out") ("prop" "up") ("psych" "out") ("psych" "up")
+ ("pull" "ahead") ("pull" "apart") ("pull" "away") ("pull" "back")
+ ("pull" "down") ("pull" "for") ("pull" "in") ("pull" "off")
+ ("pull" "on") ("pull" "out") ("pull" "over") ("pull" "through")
+ ("pull" "to") ("pull" "together") ("pull" "up")
+ ("pull" "yourself" "together") ("push" "in") ("put" "across")
+ ("put" "away") ("put" "back") ("put" "by") ("put" "down")
+ ("put" "down" "for") ("put" "down" "to") ("put" "in")
+ ("put" "in" "for") ("put" "off") ("put" "on") ("put" "out")
+ ("put" "through") ("put" "towards") ("put" "up") ("put" "up" "with")
+ ("quieten" "down") ("rack" "up") ("rain" "down" "on")
+ ("rake" "it" "in") ("rake" "up") ("ramp" "up") ("rat" "on")
+ ("rat" "out") ("rat" "through") ("ratchet" "up") ("rattle" "off")
+ ("reach" "out") ("reach" "out" "for") ("reach" "out" "to")
+ ("read" "off") ("read" "out") ("read" "up" "on") ("reckon" "on")
+ ("reel" "in") ("reel" "off") ("reel" "out") ("rein" "in")
+ ("ride" "off") ("ride" "on") ("ride" "out") ("ride" "up")
+ ("ring" "back") ("ring" "off") ("ring" "up") ("rip" "off")
+ ("rode" "off") ("roll" "back") ("roll" "in") ("roll" "on")
+ ("roll" "out") ("roll" "up") ("romp" "in") ("romp" "through")
+ ("room" "in") ("root" "about") ("root" "around") ("root" "for")
+ ("root" "out") ("root" "up") ("rope" "in") ("rough" "up")
+ ("round" "off") ("row" "back") ("rub" "along") ("rub" "down")
+ ("rub" "in") ("rub" "it" "in") ("rub" "off" "on") ("rub" "out")
+ ("rub" "up" "against") ("rub" "up" "on") ("rule" "out")
+ ("run" "across") ("run" "away") ("run" "down") ("run" "for")
+ ("run" "in") ("run" "into") ("run" "off") ("run" "on")
+ ("run" "out" "of") ("run" "over") ("run" "through") ("run" "to")
+ ("run" "up") ("run" "up" "against") ("run" "up" "on") ("run" "with")
+ ("rush" "into") ("sag" "off") ("sail" "into") ("sail" "through")
+ ("sally" "forth") ("sally" "out") ("salt" "away") ("save" "on")
+ ("save" "up") ("scare" "away") ("scare" "off") ("scout" "about")
+ ("scout" "around") ("scout" "out") ("scout" "round") ("scout" "up")
+ ("scrape" "along") ("scrape" "by") ("scrape" "in") ("scrape" "into")
+ ("scrape" "through") ("scrape" "together") ("scrape" "up")
+ ("screen" "off") ("screen" "out") ("screw" "around") ("screw" "up")
+ ("see" "about") ("see" "into") ("see" "off") ("see" "out")
+ ("see" "through") ("see" "to") ("sell" "off") ("sell" "on")
+ ("sell" "out") ("sell" "up") ("send" "back") ("send" "for")
+ ("send" "in") ("send" "off") ("send" "off" "for") ("send" "out")
+ ("send" "out" "for") ("send" "up") ("set" "about") ("set" "aside")
+ ("set" "back") ("set" "forth") ("set" "in") ("set" "off")
+ ("set" "out") ("set" "up") ("settle" "down") ("settle" "for")
+ ("settle" "in") ("settle" "on") ("settle" "up") ("sex" "up")
+ ("shack" "up") ("shake" "down") ("shake" "off") ("shape" "up")
+ ("shave" "off") ("shell" "out") ("ship" "off") ("ship" "out")
+ ("shoot" "away") ("shoot" "back") ("shoot" "off") ("shoot" "out")
+ ("shoot" "up") ("shop" "around") ("show" "around") ("show" "in")
+ ("show" "off") ("show" "out") ("show" "over") ("show" "round")
+ ("show" "through") ("show" "up") ("shrug" "off") ("shut" "away")
+ ("shut" "down") ("shut" "in") ("shut" "off") ("shut" "out")
+ ("shut" "out" "of") ("shut" "up") ("shut" "yourself" "away")
+ ("shy" "away" "from") ("side" "with") ("sift" "through")
+ ("sign" "away") ("sign" "for") ("sign" "in") ("sign" "into")
+ ("sign" "off") ("sign" "on") ("sign" "on" "with") ("sign" "out")
+ ("sign" "out" "of") ("sign" "up") ("sign" "with") ("simmer" "down")
+ ("sink" "in") ("sit" "about") ("sit" "around") ("sit" "back")
+ ("sit" "by") ("sit" "down") ("sit" "for") ("sit" "in")
+ ("sit" "in" "for") ("sit" "in" "on") ("sit" "on") ("sit" "out")
+ ("sit" "over") ("sit" "through") ("sit" "with") ("size" "up")
+ ("skive" "off") ("slack" "off") ("slag" "off") ("sleep" "off")
+ ("sleep" "on") ("sleep" "over") ("sleep" "through") ("slip" "out")
+ ("slip" "up") ("slob" "about") ("slob" "around") ("slope" "off")
+ ("slow" "down") ("slow" "up") ("smack" "of") ("smash" "down")
+ ("smash" "in") ("smash" "up") ("snap" "off") ("snap" "out" "of")
+ ("snap" "to" "it") ("snap" "up") ("sniff" "around") ("sniff" "at")
+ ("sniff" "out") ("sober" "up") ("soldier" "on") ("sort" "out")
+ ("sound" "off") ("sound" "out") ("spark" "off") ("spark" "up")
+ ("speak" "out") ("speak" "up") ("spell" "out") ("spit" "it" "out")
+ ("spit" "out") ("split" "up") ("spoil" "for") ("spur" "on")
+ ("square" "away") ("square" "off") ("square" "off" "against")
+ ("square" "up") ("square" "up" "to") ("square" "with")
+ ("squeeze" "up") ("stack" "up") ("stack" "up" "against")
+ ("staff" "up") ("stamp" "out") ("stand" "about") ("stand" "around")
+ ("stand" "aside") ("stand" "back") ("stand" "by") ("stand" "down")
+ ("stand" "for") ("stand" "in" "for") ("stand" "out") ("stand" "up")
+ ("stand" "up" "for") ("stand" "up" "to") ("start" "off")
+ ("start" "off" "on") ("start" "on") ("start" "on" "at")
+ ("start" "out") ("start" "out" "as") ("start" "out" "to")
+ ("start" "over") ("start" "up") ("stash" "away") ("stay" "away")
+ ("stay" "away" "from") ("stay" "in") ("stay" "on") ("stay" "out")
+ ("stay" "over") ("stay" "up") ("steer" "clear" "of") ("stem" "from")
+ ("step" "aside") ("step" "back") ("step" "down") ("step" "forward")
+ ("step" "in") ("step" "on" "it") ("step" "out") ("step" "to")
+ ("step" "up") ("stick" "around") ("stick" "at") ("stick" "by")
+ ("stick" "down") ("stick" "it" "to") ("stick" "out")
+ ("stick" "out" "for") ("stick" "to") ("stick" "together")
+ ("stick" "up") ("stick" "up" "for") ("stick" "with") ("stir" "up")
+ ("stitch" "up") ("stop" "around") ("stop" "back") ("stop" "behind")
+ ("stop" "by") ("stop" "doing") ("stop" "in") ("stop" "off")
+ ("stop" "out") ("stop" "over") ("stop" "round") ("stop" "up")
+ ("storm" "off") ("storm" "out") ("stow" "away") ("straighten" "out")
+ ("straighten" "up") ("strike" "back") ("strike" "down")
+ ("strike" "off") ("strike" "on") ("strike" "out") ("strike" "up")
+ ("strike" "upon") ("string" "along") ("string" "out")
+ ("string" "together") ("string" "up") ("stub" "out")
+ ("stumble" "across") ("stumble" "upon") ("stump" "up") ("suck" "in")
+ ("suck" "into") ("suck" "up" "to") ("sum" "up") ("summon" "up")
+ ("suss" "out") ("swan" "about") ("swan" "around") ("swan" "in")
+ ("swan" "off") ("swear" "by") ("swear" "down") ("sweep" "through")
+ ("swing" "around") ("swing" "at") ("swing" "by") ("swing" "round")
+ ("syphon" "off") ("tack" "on") ("tack" "onto") ("tag" "along")
+ ("tag" "on") ("tag" "onto") ("tag" "with") ("tail" "away")
+ ("tail" "back") ("tail" "off") ("take" "after") ("take" "apart")
+ ("take" "aside") ("take" "away") ("take" "back") ("take" "down")
+ ("take" "in") ("take" "it") ("take" "it" "out" "on")
+ ("take" "it" "upon" "yourself") ("take" "off") ("take" "on")
+ ("take" "out") ("take" "over") ("take" "to") ("take" "up")
+ ("talk" "back") ("talk" "down") ("talk" "down" "to") ("talk" "into")
+ ("talk" "out" "of") ("talk" "over") ("talk" "through") ("talk" "up")
+ ("tap" "for") ("tap" "into") ("tap" "off" "with") ("tap" "out")
+ ("tap" "up") ("team" "up") ("tear" "apart") ("tear" "at")
+ ("tear" "away") ("tear" "down") ("tear" "into") ("tear" "off")
+ ("tear" "up") ("tee" "off") ("tee" "off" "on") ("tee" "up")
+ ("tell" "apart") ("tell" "off") ("tell" "on") ("think" "over")
+ ("think" "through") ("think" "up") ("throw" "away") ("throw" "out")
+ ("throw" "up") ("tick" "away") ("tick" "by") ("tick" "off")
+ ("tick" "over") ("tide" "over") ("tidy" "up") ("tie" "back")
+ ("tie" "down") ("tie" "in") ("tie" "in" "with") ("tie" "up")
+ ("tighten" "up") ("tip" "off") ("tip" "over") ("tire" "of")
+ ("tire" "out") ("toddle" "off") ("tone" "down") ("tool" "up")
+ ("top" "off") ("top" "out") ("top" "up") ("touch" "down")
+ ("touch" "for") ("touch" "off") ("touch" "on") ("touch" "up")
+ ("touch" "upon") ("toy" "at") ("toy" "over") ("toy" "with")
+ ("track" "down") ("trade" "down") ("trade" "in") ("trade" "off")
+ ("trade" "on") ("trade" "up") ("trade" "upon") ("trickle" "down")
+ ("trip" "over") ("trip" "up") ("trot" "out") ("trump" "up")
+ ("try" "back") ("try" "for") ("try" "it" "on") ("try" "on")
+ ("try" "out") ("try" "out" "for") ("tuck" "away") ("tuck" "in")
+ ("tuck" "into") ("tuck" "up") ("tune" "in") ("tune" "in" "to")
+ ("tune" "out") ("tune" "up") ("turn" "against") ("turn" "away")
+ ("turn" "down") ("turn" "in") ("turn" "into") ("turn" "off")
+ ("turn" "on") ("turn" "out") ("turn" "over") ("turn" "to")
+ ("turn" "up") ("type" "up") ("use" "up") ("veg" "out")
+ ("venture" "forth") ("wade" "in") ("wade" "into") ("wade" "through")
+ ("wait" "about") ("wait" "around") ("wait" "behind") ("wait" "in")
+ ("wait" "on") ("wait" "out") ("wait" "up") ("wait" "upon")
+ ("wake" "up") ("walk" "away" "from") ("walk" "away" "with")
+ ("walk" "in" "on") ("walk" "into") ("walk" "off")
+ ("walk" "off" "with") ("walk" "on") ("walk" "out")
+ ("walk" "out" "on") ("walk" "up") ("want" "out") ("warm" "up")
+ ("wash" "away") ("wash" "down") ("wash" "out") ("wash" "over")
+ ("wash" "up") ("watch" "out") ("watch" "out" "for") ("watch" "over")
+ ("water" "down") ("wean" "off") ("wear" "away") ("wear" "down")
+ ("wear" "off") ("wear" "out") ("weed" "out") ("weigh" "in")
+ ("weigh" "up") ("went" "up") ("while" "away") ("whip" "out")
+ ("whip" "through") ("whip" "up") ("whisk" "away") ("white" "out")
+ ("wig" "out") ("wimp" "out") ("wind" "down") ("wind" "on")
+ ("wind" "up") ("winkle" "out") ("wipe" "out") ("wire" "up")
+ ("wise" "up") ("word" "up") ("work" "off") ("work" "on")
+ ("work" "out") ("wrap" "up") ("wriggle" "out" "of") ("write" "down")
+ ("write" "in") ("write" "off") ("write" "out") ("write" "up")
+ ("yack" "on") ("yammer" "on") ("yield" "to") ("zero" "in" "on")
+ ("zero" "out") ("zip" "around") ("zip" "by") ("zip" "up")
+ ("zone" "in") ("zone" "in" "on") ("zone" "out") ("zonk" "out")
+ ("zoom" "in") ("zoom" "in" "on") ("zoom" "off") ("zoom" "out")))
+ (setf (alist-get (car phrase) verbs nil nil 'string-equal)
+ (cons (cdr phrase)
+ (alist-get (car phrase) verbs nil nil 'string-equal))))
+ verbs)
+ "List of phrasal verbs in English.
+This list is (a) very long and (b) probably incomplete. It's
+been pulled from
+https://capitalizemytitle.com/what-is-a-phrasal-verb/.")
+
+(defvar titlecase-articles '("a" "an" "the")
+ "List of articles in English.")
+
+(defvar titlecase-coordinating-conjunctions '("for" "and" "nor" "but" "or"
+ "yet" "so")
+ "List of coordinating conjunctions in English.")
+
+
+(defvar titlecase-styles-capitalize-phrasal-verbs
+ '(chicago apa mla ap bluebook ama nyt wikipedia)
+ "Styles in which to capitalize phrasal verbs.")
+
+(defvar titlecase-styles-capitalize-last-word
+ '(chicago ap bluebook ama nyt wikipedia)
+ "Styles in which to capitalize the last word of a title.")
+
+(defvar titlecase-styles-capitalize-non-short-words
+ '(ap)
+ "Styles in which to capitalize all non-short words.
+See `titlecase-short-word-length'.")
+
+(defvar titlecase-short-word-length 3
+ "Maximum length of a short word.
+See `titlecase-styles-capitalize-non-short-words'.")
+
+(defvar titlecase-lowercase-chicago (append titlecase-articles
+ titlecase-prepositions
+ '("as" "and" "but" "for" "nor"
+ "or"))
+ "Words to lowercase in Chicago Style.
+Include: articles, and, but, for, nor, or, prepositions, and
+\"to\" in an infinitive (though that's caught as a preposition).")
+
+(defvar titlecase-lowercase-apa (append titlecase-articles
+ (seq-filter (lambda (p)
+ (< (length p) 4))
+ titlecase-prepositions)
+ '("as" "if"))
+ "Words to lowercase in APA Style.")
+
+(defvar titlecase-lowercase-mla (append titlecase-articles
+ titlecase-prepositions
+ titlecase-coordinating-conjunctions)
+ "Words to lowercase in MLA Style.")
+
+(defvar titlecase-lowercase-ap (append titlecase-articles
+ (seq-filter (lambda (p)
+ (< (length p) 4))
+ titlecase-prepositions)
+ (seq-filter
+ (lambda (p)
+ (< (length p) 4))
+ titlecase-coordinating-conjunctions)
+ '("as" "if"))
+ "Words to lowercase in AP Style.")
+
+(defvar titlecase-lowercase-bluebook (append titlecase-articles
+ titlecase-coordinating-conjunctions
+ (seq-filter
+ (lambda (p)
+ (< (length p) 4))
+ titlecase-prepositions))
+ "Words to lowercase in Bluebook Style.")
+
+(defvar titlecase-lowercase-ama (append titlecase-articles
+ titlecase-coordinating-conjunctions
+ (seq-filter (lambda (p)
+ (< (length p) 4))
+ titlecase-prepositions))
+ "Words to lowercase in AMA Style.")
+
+(defvar titlecase-lowercase-nyt '("as" "and" "as" "at" "but" "by" "en" "for"
+ "in" "if" "of" "on" "or" "the" "to" "v." "vs."
+ "via")
+ "Words to lowercase in New York Times Style.
+NYT is unique in explicitly listing all words to lowercase.
+Thanks, NYT! Of course, these should be capitalized when used as
+adverbs, and \"for\" should be capitalized \"if it takes the
+place of a verb meaning 'support' or 'advocate.'\" In addition,
+\"in\" and \"on\" are generally capitalized when used as
+adjectives in actual NYT headlines.")
+
+(defvar titlecase-lowercase-imdb '("a" "an" "and" "as" "at" "by" "for"
+ "from" "in" "of" "on" "or" "the" "to"
+ "with")
+ "Words to lowercase in IMDB style.
+IMDB is another style guide that's explicit in which words to
+lowercase. Shout-out to IMDB!")
+
+(defvar titlecase-lowercase-wikipedia
+ (append titlecase-articles
+ (seq-filter (lambda (p) (< (length p) 5)) titlecase-prepositions)
+ titlecase-coordinating-conjunctions)
+ "Words to lowercase in Wikipedia Style.")
+
+(defvar titlecase-lowercase-sentence nil
+ "Words to titlecase in Sentence style.
+This is nil, since `titlecase--region-with-style-impl' deals with
+the logic here (and besides, this list would be /quite/ long when
+enumerated!)")
+
+(provide 'titlecase-data)
+;;; titlecase-data.el ends here
diff --git a/custom/titlecase.el b/custom/titlecase.el
new file mode 100644
index 00000000..43947822
--- /dev/null
+++ b/custom/titlecase.el
@@ -0,0 +1,396 @@
+;;; titlecase.el --- Title-case phrases -*- lexical-binding: t; -*-
+
+;; Author: Case Duckworth <acdw@acdw.net>
+;; Maintainer: Case Duckworth <acdw@acdw.net>
+;; Version: 0.3.0
+;; URL: https://codeberg.org/acdw/titlecase.el
+;; Package-Requires: ((emacs "25.1"))
+
+;; 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:
+
+;; This library strives to be the most accurate possible with title-casing
+;; sentences, lines, and regions of text in English prose according to a number
+;; of styles guides' capitalization rules. It is necessarily a best-effort; due
+;; to the vaguaries of written English it's impossible to completely correctly
+;; capitalize aribtrary titles. So be sure to proofread and copy-edit your
+;; titles before sending them off to be published, and never trust a computer.
+
+;; INSTALLATION and USE:
+
+;; Make sure both titlecase.el and titlecase-data.el are in your `load-path',
+;; and `require' titlecase. You should then be able to call the interactive
+;; functions defined in this file.
+
+;;; CUSTOMIZATION:
+
+;; Only two customization options are probably going to be of any interest:
+;; `titlecase-style' (the style to use for capitalizing titles), and
+;; `titlecase-dwim-non-region-function', which determines what to do when
+;; `titlecase-dwim' isn't acting on a region.
+
+;; If you want to use your own title-casing code, or a third party, you can
+;; customize `titlecase-command' to something other than its default. One
+;; possibility is titlecase.pl, written John Gruber and Aristotle Pagaltzis:
+;; https://github.com/ap/titlecase.
+
+;;; Code:
+
+(require 'browse-url) ; `browse-url-button-regexp'
+(require 'cl-lib) ; `cl-loop'
+(require 'seq) ; `seq-some'
+(require 'thingatpt) ; `bounds-of-thing-at-point'
+(require 'titlecase-data)
+
+(defgroup titlecase nil
+ "Customization for title-casing phrases."
+ :prefix "titlecase-"
+ :group 'text)
+
+(defvar titlecase-styles '((chicago . "Chicago Style")
+ (apa . "APA Style")
+ (mla . "MLA Style")
+ (ap . "AP Style")
+ (bluebook . "Bluebook Style")
+ (ama . "AMA Style")
+ (nyt . "New York Times Style")
+ (wikipedia . "Wikipedia Style")
+ (imdb . "IMDB Style")
+ (sentence . "Sentence style"))
+ "Available styles for title-casing.")
+
+(defvar titlecase-default-case-function #'capitalize-word
+ "What to do to a word when a style doesn't specify what to do.")
+
+(defcustom titlecase-normalize-functions '(titlecase--lowercase-all-caps)
+ "List of functions for normalizing input before title-casing.
+Each function will be passed 3 arguments: the beginning and
+ending points of the region to operate on, as well as the
+title-casing style. They are called one after another in order
+in a `save-excursion' block."
+ :type '(repeat function))
+
+(defcustom titlecase-skip-words-regexps (list "\\b[[:upper:]]+\\b"
+ browse-url-button-regexp)
+ "Regexps of words to skip when titlecasing.
+Each regexp in this list will be tested on each word considered
+for title-casing, and if the regexp matches the entire word, the
+word will be skipped.
+
+NOTE: These regexps will be matched against the title-cased
+region /after/ normalizing it, which means that, by default, if
+the region is in all-caps before calling `titlecase-region', it
+will be downcased before title-casing. Thus, some of these
+regexps might not match when expected. This behavior is a
+trade-off between possible user expectations. To change this
+behavior, customize `titlecase-normalize-functions'."
+ :type '(repeat regexp))
+
+(defcustom titlecase-style 'wikipedia
+ "Which style to use when title-casing."
+ :type (cons 'choice (cl-loop
+ for style in titlecase-styles
+ collect (list 'const :tag (cdr style) (car style))
+ into choices
+ finally return choices)))
+
+(defcustom titlecase-force-cap-after-punc "[.?!\\/;:\n\r]"
+ "Regexp to force the next word capitalized."
+ :type 'regexp)
+
+(defcustom titlecase-dwim-non-region-function #'titlecase-line
+ "What to do with `titlecase-dwim' when region isn't active.
+Recommended: `titlecase-line' or `titlecase-sentence'."
+ :type 'function)
+
+(defcustom titlecase-command #'titlecase--region-with-style
+ "Command to use for titlecasing titles.
+This option can be one of two things:
+
+A string value, or a list of string values, is interpreted as a
+system command to run using `call-process-region' on a temp
+buffer containing the text to titlecase. Just a string is
+interpreted as the command to run, with no arguments. A list of
+strings will pass those strings as aruguments to the command-line
+program. In that list, the symbol `style' will be replaced with
+the the string of the title-casing style that's passed.
+
+A function value is interpreted as the function to call on the
+region. The function will be called with three arguments: the
+beginning and end of the region, and the style (see
+`titlecase-style') to capitalize it in.")
+
+(defcustom titlecase-downcase-sentences nil
+ "Whether to downcase words after the first in \"sentence\" style.
+If nil, titlecasing using the \"sentence\" style will leave all
+words as-is. If t, \"sentence\"-style titlecasing will downcase
+words that don't begin a sentence."
+ :type 'boolean)
+
+(defun titlecase--region-with-style-impl (begin end style)
+ "Title-case implementation.
+`titlecase-force-cap-after-punc' must be handled by the caller.
+This is expected to have run in a block that uses `save-excursion' and
+`save-match-data'. See documentation for `titlecase--region-with-style'
+for docs on BEGIN, END and STYLE."
+ (let ( ;; Constants during this function's runtime.
+ (case-fold-search nil)
+ (downcase-word-list (symbol-value
+ (intern (format "titlecase-lowercase-%s"
+ style)))))
+
+ ;; Normalize the text in region by calling `titlecase-normalize-functions'
+ ;; in order.
+ (dolist (fn titlecase-normalize-functions)
+ (save-excursion
+ (funcall fn begin end style)))
+
+ ;; Skip blank lines & white-space (where `current-word' would return nil).
+ ;; It's important this uses the same logic that `current-word' uses to scan
+ ;; for words, or this may be nil when it's not expected. See #11.
+ (goto-char begin)
+ (skip-syntax-forward "^w" end)
+ (setq begin (point))
+
+ ;; And loop over the rest.
+ (catch :done
+ (while (< (point) end)
+ (let ((this-word (current-word)))
+ (cond
+ ;; Skip words matching `titlecase-skip-words-regexps'.
+ ((looking-at (format "%s"
+ (mapconcat #'identity
+ titlecase-skip-words-regexps
+ "\\|")))
+ (goto-char (match-end 0))
+ ;; TODO: Document what this does (it's late)
+ (when (>= (point) end)
+ (throw :done 'skipped)))
+ ;; Phrasal verbs!
+ ((and (memq style titlecase-styles-capitalize-phrasal-verbs)
+ (member (downcase this-word)
+ (mapcar #'car titlecase-phrasal-verbs)))
+ ;; We need to do a little state machine thingy here.
+ (let ((next-words (assoc this-word titlecase-phrasal-verbs))
+ (bail-pt (point)))
+ ;; Take care of the first word --- this is inelegant.
+ (capitalize-word 1)
+ (skip-syntax-forward "^w" end)
+ (setq this-word (current-word))
+ ;; Loop through the rest
+ (while (and this-word
+ (member (downcase this-word)
+ (mapcar #'car-safe next-words)))
+ (capitalize-word 1)
+ (skip-syntax-forward "^w" end)
+ (setq this-word (current-word)
+ next-words (mapcar #'cdr-safe next-words)))
+ (unless (seq-some #'null next-words)
+ ;; If it's not a phrasal verb, bail --- but still
+ ;; capitalize the first word!
+ (downcase-region bail-pt (point))
+ (goto-char bail-pt)
+ (capitalize-word 1))))
+ ;; Force capitalization if this is the first word.
+ ((eq begin (point))
+ (capitalize-word 1))
+ ;; AP capitalizes /all/ words longer than 3 letters.
+ ((and (memq style titlecase-styles-capitalize-non-short-words)
+ (> (length this-word) titlecase-short-word-length))
+ (capitalize-word 1))
+ ;; Sentence style just capitalizes the first word. Since we can't be
+ ;; sure how the user has already capitalized anything, we just skip
+ ;; the current word. HOWEVER, there are times when downcasing the
+ ;; rest of the sentence is warranted. --- NOTE 2022-05-09: Now I'm
+ ;; thinking about it, does `sentence' style need to do anything
+ ;; whatsoever? Maybe I just need to include a test toward the top of
+ ;; the enclosing function to make `titlecase-default-case-function'
+ ;; be `downcase-word' if `titlecase-downcase-sentences' is true... or
+ ;; something of that nature. I might be over-engineering this, is
+ ;; what I'm saying. Curious, isn't it?
+ ((eq style 'sentence)
+ (funcall (if titlecase-downcase-sentences
+ #'downcase-word
+ #'forward-word)
+ 1))
+ ;; Skip the next word if:
+ ((or
+ ;; None of the styles require a capital letter after an
+ ;; apostrophe.
+ (memq (char-before (point)) '(?' ?’))
+ ;; FIXME: Hyphens are a completely different story with
+ ;; capitalization.
+ (eq (char-before (point)) ?-))
+ (forward-word 1))
+ ;; Down-case words that should be.
+ ((member (downcase this-word) downcase-word-list)
+ (downcase-word 1))
+ ;; Otherwise, do the default function on the word.
+ (t
+ (funcall titlecase-default-case-function 1))))
+
+ ;; Step over the loop.
+ (unless (= end (point))
+ (skip-syntax-forward "^w" end)))
+ ;; Capitalize the last word, only in some styles and some conditions.
+ (when (and (memq style titlecase-styles-capitalize-last-word))
+ (save-excursion
+ (backward-word 1)
+ (when (and (>= (point) begin)
+ (not (seq-some (lambda (r) (looking-at r))
+ titlecase-skip-words-regexps)))
+ (capitalize-word 1)))))))
+
+(defun titlecase--region-with-style (begin end style)
+ "Title-case the region of English text from BEGIN to END, using STYLE."
+ ;; It doesn't makes sense for this function to be interactive;
+ ;; `titlecase-region' can now specify a style interactively.
+ (save-match-data
+ (while (< begin end)
+ (goto-char begin)
+ (let ((end-step
+ (if (re-search-forward titlecase-force-cap-after-punc
+ end :noerror)
+ (point)
+ end)))
+ (if (memq (titlecase--region-with-style-impl begin end-step style)
+ '(skipped))
+ (setq begin (point))
+ (setq begin end-step))))))
+
+(defun titlecase--read-style ()
+ "Read which title-case style to use from the minibuffer."
+ (let ((choice (completing-read
+ "Title-case style: "
+ (mapcar #'cdr titlecase-styles)
+ nil t nil nil
+ (alist-get titlecase-style titlecase-styles))))
+ (cl-loop for (s . n) in titlecase-styles
+ if (equal n choice) return s)))
+
+(defun titlecase--arg (style interactivep)
+ "Process arguments to titlecase functions.
+If STYLE is passed to a function in any way, use it; otherwise,
+if INTERACTIVEP, prompt the user for a style to use. As a
+fall-back, use `titlecase-style'."
+ (or style
+ (and interactivep (titlecase--read-style))
+ titlecase-style))
+
+(defun titlecase--string (str style)
+ "Run `titlecase-command' on STR with STYLE and return the result.
+See the docstring for `titlecase-command' for its possible
+values."
+ (let (;; Remember the existing newlines
+ (str-ending-newlines (replace-regexp-in-string
+ "\\`\\([^z-a]*?\\)\n*\\'" "" str nil nil 1)))
+ (with-temp-buffer
+ (insert str)
+ (cond
+ ((stringp titlecase-command)
+ (call-process-region (point-min) (point-max) titlecase-command t t nil))
+ ((listp titlecase-command)
+ (apply #'call-process-region (point-min) (point-max)
+ (car titlecase-command) t t nil
+ (mapcar (lambda (s)
+ (format "%s" (if (or (null s)
+ (eq s 'style))
+ titlecase-style
+ s)))
+ (cdr titlecase-command))))
+ ((functionp titlecase-command)
+ (funcall titlecase-command (point-min) (point-max)
+ (or style titlecase-style))))
+ ;; Ensure that the string has no extra trailing whitespace.
+ (goto-char (point-max)) ; Go to the end of the buffer
+ (newline) ; Ensure at least one newline
+ (delete-blank-lines) ; Delete all but the last newline
+ (insert str-ending-newlines) ; Replace the pre-existing newlines
+ ;; Delete the extra newline and return the buffer as a string
+ (buffer-substring (point-min) (1- (point-max))))))
+
+(defun titlecase--lowercase-all-caps (begin end _style)
+ "If the text from BEGIN to END is all-caps, downcase it."
+ (goto-char begin)
+ (unless (re-search-forward "[[:lower:]]" end :noerror)
+ (downcase-region begin end)))
+
+;;;###autoload
+(defun titlecase-region (begin end &optional style interactivep)
+ "Title-case the region of English text from BEGIN to END.
+Uses the style provided in `titlecase-style', unless optional
+STYLE is provided.
+
+When called interactively , or when INTERACTIVEP is non-nil,
+\\[universal-argument] \\[titlecase-region] will prompt the user
+for the style to use."
+ (interactive "*r\ni\nP")
+ (atomic-change-group
+ (save-excursion ; `replace-region-contents'
+ (save-restriction
+ (narrow-to-region begin end)
+ (insert (titlecase--string (delete-and-extract-region begin end)
+ style))))))
+
+;;;###autoload
+(defun titlecase-line (&optional point style interactivep)
+ "Title-case the line at POINT.
+Uses the style provided in `titlecase-style', unless optional
+STYLE is provided.
+
+When called interactively , or when INTERACTIVEP is non-nil,
+POINT is the current point, and calling with
+\\[universal-argument] \\[titlecase-line] will prompt the user
+for the style to use."
+ (interactive "d\ni\nP")
+ (goto-char (or point (point)))
+ (let ((style (titlecase--arg style interactivep))
+ (thing (bounds-of-thing-at-point 'line)))
+ (titlecase-region (car thing) (cdr thing) style)))
+
+;;;###autoload
+(defun titlecase-sentence (&optional point style interactivep)
+ "Title-case the sentence at POINT.
+Uses the style provided in `titlecase-style', unless optional
+STYLE is provided.
+
+When called interactively , or when INTERACTIVEP is non-nil,
+POINT is the current point, and calling with
+\\[universal-argument] \\[titlecase-sentence] will prompt the
+user for the style to use."
+ (interactive "d\ni\nP")
+ (goto-char (or point (point)))
+ (let ((style (titlecase--arg style interactivep))
+ (thing (bounds-of-thing-at-point 'sentence)))
+ (titlecase-region (car thing) (cdr thing) style)
+ (goto-char (cdr thing))))
+
+;;;###autoload
+(defun titlecase-dwim (&optional style interactivep)
+ "Title-case either the region, if active, or the current line.
+Uses the style provided in `titlecase-style', unless optional
+STYLE is provided.
+
+When called interactively with \\[universal-argument]
+\\[titlecase-dwim], or when INTERACTIVEP is non-nil, prompt the
+user for the style to use."
+ (interactive "i\nP")
+ (let ((style (titlecase--arg style interactivep)))
+ (if (region-active-p)
+ (titlecase-region (region-beginning) (region-end) style)
+ (funcall titlecase-dwim-non-region-function (point) style))))
+
+(provide 'titlecase)
+;;; titlecase.el ends here
diff --git a/custom/utilities/vcf-conversion-helpers.el b/custom/utilities/vcf-conversion-helpers.el
new file mode 100644
index 00000000..334edc4e
--- /dev/null
+++ b/custom/utilities/vcf-conversion-helpers.el
@@ -0,0 +1,388 @@
+;;; vcf-conversion-helpers.el --- vcf conversion utility functions -*- coding: utf-8; lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(defun cj/clean-vcf-for-import (input-vcf output-vcf)
+ "Clean and prepare VCF file for import to org-contacts."
+ (interactive "fInput VCF file: \nFOutput VCF file: ")
+ (with-temp-buffer
+ (insert-file-contents input-vcf)
+ (goto-char (point-min))
+
+ ;; First, clean up multi-line fields (unfold them) BEFORE processing
+ ;; This ensures PHOTO and other multi-line fields are on single lines
+ (goto-char (point-min))
+ (while (re-search-forward "\n[ \t]+" nil t)
+ (replace-match " " t t))
+
+ ;; Handle birthdays while the structure is intact
+ (goto-char (point-min))
+ (while (re-search-forward "^BDAY:\\(.*\\)$" nil t)
+ (let* ((bday-value (match-string 1))
+ (full-match (match-string 0))
+ (match-start (match-beginning 0))
+ (match-end (match-end 0))
+ (cleaned-bday
+ (cond
+ ;; Full date format: 19700805 -> 1970-08-05
+ ((string-match "^\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)$" bday-value)
+ (format "BDAY:%s-%s-%s"
+ (match-string 1 bday-value)
+ (match-string 2 bday-value)
+ (match-string 3 bday-value)))
+ ;; Month-day only: --0714 -> use current year or set to 1900
+ ((string-match "^--\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)$" bday-value)
+ (format "BDAY:1900-%s-%s"
+ (match-string 1 bday-value)
+ (match-string 2 bday-value)))
+ ;; Text format incomplete: convert to note
+ ((string-match "VALUE=text" bday-value)
+ nil) ; We'll add as note later
+ ;; Keep as is if already in good format
+ (t full-match))))
+ (when cleaned-bday
+ (goto-char match-start)
+ (delete-region match-start match-end)
+ (insert cleaned-bday)
+ (goto-char match-end))))
+
+ ;; Convert item-prefixed TEL and EMAIL fields to standard format
+ (goto-char (point-min))
+ (while (re-search-forward "^item[0-9]+\\.\\(TEL\\|EMAIL\\)\\(.*?\\):\\(.+\\)$" nil t)
+ (let ((field-type (match-string 1))
+ (field-params (match-string 2))
+ (field-value (match-string 3)))
+ (replace-match (format "%s%s:%s" field-type field-params field-value) t t)))
+
+ ;; NOW remove unwanted fields (but not the converted TEL/EMAIL fields)
+ (let ((remove-patterns
+ '("^PHOTO:.*$"
+ "^X-ABRELATEDNAMES:.*$"
+ "^X-ABLabel:.*$"
+ "^X-FILE-AS:.*$"
+ "^VERSION:.*$"
+ "^item[0-9]+\\.X-.*$" ; Only remove item-prefixed X- fields
+ "^CATEGORIES:.*Imported on.*$")))
+ (dolist (pattern remove-patterns)
+ (goto-char (point-min))
+ (while (re-search-forward pattern nil t)
+ (delete-region (line-beginning-position)
+ (min (1+ (line-end-position)) (point-max))))))
+
+ ;; Process each VCARD to ensure it has an FN field or remove if no identifying info
+ (goto-char (point-min))
+ (let ((vcards-to-remove '()))
+ (while (re-search-forward "^BEGIN:VCARD" nil t)
+ (let ((vcard-start (match-beginning 0))
+ (has-fn nil)
+ (has-n nil)
+ (has-org nil)
+ (fn-value nil)
+ (n-value nil)
+ (org-value nil))
+ (when (re-search-forward "^END:VCARD" nil t)
+ (let ((vcard-end (match-end 0)))
+ (save-excursion
+ (save-restriction
+ (narrow-to-region vcard-start vcard-end)
+
+ ;; Check for FN field
+ (goto-char (point-min))
+ (when (re-search-forward "^FN:\\(.+\\)$" nil t)
+ (setq has-fn t)
+ (setq fn-value (match-string 1)))
+
+ ;; Check for N field (structured name)
+ (goto-char (point-min))
+ (when (re-search-forward "^N:\\(.+\\)$" nil t)
+ (setq has-n t)
+ (setq n-value (match-string 1)))
+
+ ;; Check for ORG field
+ (goto-char (point-min))
+ (when (re-search-forward "^ORG:\\(.+\\)$" nil t)
+ (setq has-org t)
+ (setq org-value (match-string 1)))
+
+ ;; If no FN but has N or ORG, synthesize FN
+ (when (and (not has-fn) (or has-n has-org))
+ (goto-char (point-min))
+ (if (re-search-forward "^BEGIN:VCARD$" nil t)
+ (let ((synthesized-fn
+ (cond
+ ;; Try to build from N field first
+ (has-n
+ (let* ((n-parts (split-string n-value ";"))
+ (last-name (nth 0 n-parts))
+ (first-name (nth 1 n-parts))
+ (middle-name (nth 2 n-parts))
+ (prefix (nth 3 n-parts))
+ (suffix (nth 4 n-parts))
+ (name-parts '()))
+ ;; Build name in "First Middle Last" order
+ (when (and prefix (not (string-empty-p prefix)))
+ (push prefix name-parts))
+ (when (and first-name (not (string-empty-p first-name)))
+ (push first-name name-parts))
+ (when (and middle-name (not (string-empty-p middle-name)))
+ (push middle-name name-parts))
+ (when (and last-name (not (string-empty-p last-name)))
+ (push last-name name-parts))
+ (when (and suffix (not (string-empty-p suffix)))
+ (push suffix name-parts))
+ (if name-parts
+ (string-join (reverse name-parts) " ")
+ ;; If N field exists but is empty, fall back to ORG
+ (when has-org
+ (replace-regexp-in-string ";" ", " org-value)))))
+ ;; Use ORG field if no N field
+ (has-org
+ (replace-regexp-in-string ";" ", " org-value))
+ (t nil))))
+ (when synthesized-fn
+ (end-of-line)
+ (insert (format "\nFN:%s" synthesized-fn))
+ (setq has-fn t)))))
+
+ ;; Mark for removal if no identifying information
+ (when (not (or has-fn has-n has-org))
+ (push (cons vcard-start vcard-end) vcards-to-remove))
+
+ (widen)))))))
+
+ ;; Remove VCARDs with no identifying information (in reverse order to preserve positions)
+ (dolist (vcard-range (reverse vcards-to-remove))
+ (delete-region (car vcard-range) (cdr vcard-range))
+ (message "Removed VCARD with no identifying information")))
+
+ ;; Remove empty lines within VCARDs
+ (goto-char (point-min))
+ (while (re-search-forward "^BEGIN:VCARD" nil t)
+ (let ((start (point)))
+ (when (re-search-forward "^END:VCARD" nil t)
+ (save-restriction
+ (narrow-to-region start (match-beginning 0))
+ (goto-char (point-min))
+ (delete-matching-lines "^$")
+ (widen)))))
+
+ ;; Add back VERSION field (required for valid VCF)
+ (goto-char (point-min))
+ (while (re-search-forward "^BEGIN:VCARD$" nil t)
+ (end-of-line)
+ (insert "\nVERSION:3.0"))
+
+ ;; Save cleaned file
+ (write-region (point-min) (point-max) output-vcf)
+ (message "Cleaned VCF saved to %s" output-vcf)))
+
+
+(defun split-vcf-file (vcf-file output-dir)
+ "Split a combined VCF file into individual contact files."
+ (interactive "fVCF file to split: \nDOutput directory: ")
+ (mkdir output-dir t)
+ (with-temp-buffer
+ (insert-file-contents vcf-file)
+ (goto-char (point-min))
+ (let ((count 0)
+ (contact-name ""))
+ (while (re-search-forward "^BEGIN:VCARD" nil t)
+ (let ((start (match-beginning 0)))
+ (when (re-search-forward "^END:VCARD" nil t)
+ (let* ((end (match-end 0))
+ (vcard (buffer-substring start end)))
+ ;; Try to extract name for better filename
+ (with-temp-buffer
+ (insert vcard)
+ (goto-char (point-min))
+ (if (re-search-forward "^FN:\\(.+\\)$" nil t)
+ (setq contact-name
+ (replace-regexp-in-string
+ "[^A-Za-z0-9-_]" "_"
+ (match-string 1)))
+ (setq contact-name (format "contact-%04d" count))))
+ ;; Write individual VCF file
+ (let ((filename (format "%s/%s.vcf" output-dir contact-name)))
+ (with-temp-file filename
+ (insert vcard))
+ (cl-incf count))))))
+ (message "Split into %d contact files in %s" count output-dir))))
+
+
+(defun cj/convert-cleaned-vcf-to-org-contacts (vcf-file org-file)
+ "Convert cleaned VCF file to org-contacts format."
+ (interactive "fCleaned VCF file: \nFOrg file to create: ")
+ (with-temp-buffer
+ (insert-file-contents vcf-file)
+ (let ((contacts '())
+ (contact-count 0)) ; Add a counter for the final message
+ (goto-char (point-min))
+ (while (re-search-forward "^BEGIN:VCARD" nil t)
+ (let ((vcard-start (point))
+ (contact (make-hash-table :test 'equal)))
+ (when (re-search-forward "^END:VCARD" nil t)
+ (let ((vcard-end (match-beginning 0)))
+ (save-restriction
+ (narrow-to-region vcard-start vcard-end)
+ (goto-char (point-min))
+
+ ;; Extract FN (Full Name)
+ (when (re-search-forward "^FN:\\(.+\\)$" nil t)
+ (puthash "name" (match-string 1) contact))
+
+ ;; Extract EMAIL (can have multiple)
+ (goto-char (point-min))
+ (let ((emails '()))
+ (while (re-search-forward "^EMAIL[^:]*:\\(.+\\)$" nil t)
+ (push (match-string 1) emails))
+ (when emails
+ (puthash "email" (string-join (reverse emails) " ") contact)))
+
+ ;; Extract PHONE (can have multiple)
+ (goto-char (point-min))
+ (let ((phones '()))
+ (while (re-search-forward "^TEL[^:]*:\\(.+\\)$" nil t)
+ (let ((phone (match-string 1)))
+ ;; Clean up phone numbers slightly
+ (setq phone (replace-regexp-in-string "^\\+1 " "" phone))
+ (push phone phones)))
+ (when phones
+ (puthash "phone" (string-join (reverse phones) ", ") contact)))
+
+ ;; Extract ORG
+ (goto-char (point-min))
+ (when (re-search-forward "^ORG:\\(.+\\)$" nil t)
+ (let ((org-value (match-string 1)))
+ ;; Handle semicolon-separated org values
+ (setq org-value (replace-regexp-in-string ";" ", " org-value))
+ (puthash "org" org-value contact)))
+
+ ;; Extract TITLE
+ (goto-char (point-min))
+ (when (re-search-forward "^TITLE:\\(.+\\)$" nil t)
+ (puthash "title" (match-string 1) contact))
+
+ ;; Extract ADDRESS
+ (goto-char (point-min))
+ (when (re-search-forward "^ADR[^:]*:\\(.+\\)$" nil t)
+ (let ((addr (match-string 1)))
+ ;; VCF address format: ;;Street;City;State;Zip;Country
+ ;; Clean up the semicolons
+ (setq addr (replace-regexp-in-string ";" " " addr))
+ (setq addr (replace-regexp-in-string " +" " " addr))
+ (puthash "address" addr contact)))
+
+ ;; Extract NOTE
+ (goto-char (point-min))
+ (when (re-search-forward "^NOTE:\\(.+\\)$" nil t)
+ (let ((note (match-string 1)))
+ ;; Unescape VCF note formatting
+ (setq note (replace-regexp-in-string "\\\\n" "\n " note))
+ (setq note (replace-regexp-in-string "\\\\:" ":" note))
+ (puthash "note" note contact)))
+
+ ;; Extract BDAY (should be cleaned format now)
+ (goto-char (point-min))
+ (when (re-search-forward "^BDAY:\\(.+\\)$" nil t)
+ (let ((bday (match-string 1)))
+ ;; Convert to org format
+ (when (string-match "\\([0-9]\\{4\\}\\)-\\([0-9]\\{2\\}\\)-\\([0-9]\\{2\\}\\)" bday)
+ (let ((year (match-string 1 bday))
+ (month (match-string 2 bday))
+ (day (match-string 3 bday)))
+ (if (string= year "1900") ; Our placeholder for unknown years
+ (puthash "birthday"
+ (format "<%%(diary-anniversary %d %d)>"
+ (string-to-number month)
+ (string-to-number day))
+ contact)
+ (puthash "birthday" (format "<%s-%s-%s>" year month day) contact))))))
+
+ ;; Extract URL
+ (goto-char (point-min))
+ (when (re-search-forward "^URL:\\(.+\\)$" nil t)
+ (puthash "url" (match-string 1) contact))
+
+ ;; Extract NICKNAME
+ (goto-char (point-min))
+ (when (re-search-forward "^NICKNAME:\\(.+\\)$" nil t)
+ (puthash "nickname" (match-string 1) contact))
+
+ (widen))))
+
+ ;; Only add contact if it has a name
+ (when (gethash "name" contact)
+ (push contact contacts)
+ (setq contact-count (1+ contact-count)))))
+
+ ;; Write to org file
+ (with-temp-file org-file
+ (insert "#+TITLE: Contacts\n")
+ (insert "#+STARTUP: overview\n")
+ (insert "#+CATEGORY: contacts\n\n")
+
+ ;; Sort contacts by name
+ (setq contacts (sort (reverse contacts)
+ (lambda (a b)
+ (string< (or (gethash "name" a) "")
+ (or (gethash "name" b) "")))))
+
+ (dolist (contact contacts)
+ (insert (format "* %s\n" (gethash "name" contact "")))
+ (insert " :PROPERTIES:\n")
+
+ ;; Add all properties
+ (when-let ((email (gethash "email" contact)))
+ (insert (format " :EMAIL: %s\n" email)))
+ (when-let ((phone (gethash "phone" contact)))
+ (insert (format " :PHONE: %s\n" phone)))
+ (when-let ((org-name (gethash "org" contact)))
+ (insert (format " :COMPANY: %s\n" org-name)))
+ (when-let ((title (gethash "title" contact)))
+ (insert (format " :TITLE: %s\n" title)))
+ (when-let ((addr (gethash "address" contact)))
+ (insert (format " :ADDRESS: %s\n" addr)))
+ (when-let ((bday (gethash "birthday" contact)))
+ (insert (format " :BIRTHDAY: %s\n" bday)))
+ (when-let ((url (gethash "url" contact)))
+ (insert (format " :URL: %s\n" url)))
+ (when-let ((nick (gethash "nickname" contact)))
+ (insert (format " :NICKNAME: %s\n" nick)))
+
+ (insert " :END:\n")
+
+ ;; Add note as body text
+ (when-let ((note (gethash "note" contact)))
+ (insert (format "\n %s\n" note)))
+
+ (insert "\n")))
+
+ ;; Message is now inside the let block where contact-count is accessible
+ (message "Converted %d contacts to %s" contact-count org-file))))
+
+
+(defun cj/delete-contact-files ()
+ "Delete contact conversion files without confirmation."
+ (interactive)
+ (let ((files '("~/downloads/contacts-clean.vcf"
+ "~/sync/org/contacts.org")))
+ (dolist (file files)
+ (let ((expanded-file (expand-file-name file)))
+ (when (file-exists-p expanded-file)
+ (delete-file expanded-file)
+ (message "Deleted: %s" expanded-file))))))
+
+(cj/delete-contact-files)
+
+(cj/clean-vcf-for-import "~/downloads/contacts.vcf"
+ "~/downloads/contacts-clean.vcf")
+
+;; convert it to org contacts
+(cj/convert-cleaned-vcf-to-org-contacts "~/downloads/contacts-clean.vcf"
+ "~/sync/org/contacts.org")
+
+(provide 'vcf-conversion-helpers)
+;;; vcf-conversion-helpers.el ends here.