summaryrefslogtreecommitdiff
path: root/lisp/erc/erc-stamp.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/erc/erc-stamp.el')
-rw-r--r--lisp/erc/erc-stamp.el760
1 files changed, 678 insertions, 82 deletions
diff --git a/lisp/erc/erc-stamp.el b/lisp/erc/erc-stamp.el
index ebee40364da..bcb9b4aafef 100644
--- a/lisp/erc/erc-stamp.el
+++ b/lisp/erc/erc-stamp.el
@@ -56,19 +56,24 @@ If nil, timestamping is turned off."
(string)))
(defcustom erc-timestamp-format-left "\n[%a %b %e %Y]\n"
- "If set to a string, messages will be timestamped.
-This string is processed using `format-time-string'.
-Good examples are \"%T\" and \"%H:%M\".
-
-This timestamp is used for timestamps on the left side of the
-screen when `erc-insert-timestamp-function' is set to
-`erc-insert-timestamp-left-and-right'.
-
-If nil, timestamping is turned off."
- :type '(choice (const nil)
- (string)))
+ "Format recognized by `format-time-string' for date stamps.
+Only considered when `erc-insert-timestamp-function' is set to
+`erc-insert-timestamp-left-and-right'. Used for displaying date
+stamps on their own line, between messages. ERC inserts this
+flavor of stamp as a separate \"pseudo message\", so a final
+newline isn't necessary. For compatibility, only additional
+trailing newlines beyond the first become empty lines. For
+example, the default value results in an empty line after the
+previous message, followed by the timestamp on its own line,
+followed immediately by the next message on the next line. ERC
+expects to display these stamps less frequently, so the
+formatting specifiers should reflect that. To omit these stamps
+entirely, use a different `erc-insert-timestamp-function', such
+as `erc-timestamp-format-right'. Note that changing this value
+during an ERC session requires cycling `erc-stamp-mode'."
+ :type 'string)
-(defcustom erc-timestamp-format-right " [%H:%M]"
+(defcustom erc-timestamp-format-right nil
"If set to a string, messages will be timestamped.
This string is processed using `format-time-string'.
Good examples are \"%T\" and \"%H:%M\".
@@ -77,9 +82,14 @@ This timestamp is used for timestamps on the right side of the
screen when `erc-insert-timestamp-function' is set to
`erc-insert-timestamp-left-and-right'.
-If nil, timestamping is turned off."
+Unlike `erc-timestamp-format' and `erc-timestamp-format-left', if
+the value of this option is nil, it falls back to using the value
+of `erc-timestamp-format'."
+ :package-version '(ERC . "5.6")
:type '(choice (const nil)
(string)))
+(make-obsolete-variable 'erc-timestamp-format-right
+ 'erc-timestamp-format "30.1")
(defcustom erc-insert-timestamp-function 'erc-insert-timestamp-left-and-right
"Function to use to insert timestamps.
@@ -128,14 +138,28 @@ hidden, they will still be present in the logs."
"If non-nil, print timestamp in the minibuffer when point is moved.
Using this variable, you can turn off normal timestamping,
and simply move point to an irc message to see its timestamp
-printed in the minibuffer."
+printed in the minibuffer. When attempting to enable this option
+after `erc-stamp-mode' is already active, you may need to run the
+command `erc-show-timestamps' (or `erc-hide-timestamps') in the
+appropriate ERC buffer before the change will take effect."
:type 'boolean)
(defcustom erc-echo-timestamp-format "Timestamped %A, %H:%M:%S"
"Format string to be used when `erc-echo-timestamps' is non-nil.
This string specifies the format of the timestamp being echoed in
the minibuffer."
- :type 'string)
+ :type '(choice (const :tag "Timestamped Monday, 15:04:05"
+ "Timestamped %A, %H:%M:%S")
+ (const :tag "2006-01-02 15:04:05 MST" "%F %T %Z")
+ string))
+
+(defcustom erc-echo-timestamp-zone nil
+ "Default timezone for the option `erc-echo-timestamps'.
+Also affects the command `erc-echo-timestamp' (singular). See
+the ZONE parameter of `format-time-string' for a description of
+acceptable value types."
+ :type '(choice boolean number (const wall) (list number string))
+ :package-version '(ERC . "5.6"))
(defcustom erc-timestamp-intangible nil
"Whether the timestamps should be intangible, i.e. prevent the point
@@ -147,39 +171,107 @@ from entering them and instead jump over them."
"ERC timestamp face."
:group 'erc-faces)
+;; New libraries should only autoload the minor mode for a module's
+;; preferred name (rather than its alias).
+
+;;;###autoload(put 'timestamp 'erc--module 'stamp)
;;;###autoload(autoload 'erc-timestamp-mode "erc-stamp" nil t)
(define-erc-module stamp timestamp
"This mode timestamps messages in the channel buffers."
- ((add-hook 'erc-mode-hook #'erc-munge-invisibility-spec)
- (add-hook 'erc-insert-modify-hook #'erc-add-timestamp t)
- (add-hook 'erc-send-modify-hook #'erc-add-timestamp t))
- ((remove-hook 'erc-mode-hook #'erc-munge-invisibility-spec)
+ ((add-hook 'erc-mode-hook #'erc-stamp--setup)
+ (add-hook 'erc-insert-modify-hook #'erc-add-timestamp 70)
+ (add-hook 'erc-send-modify-hook #'erc-add-timestamp 70)
+ (add-hook 'erc-mode-hook #'erc-stamp--recover-on-reconnect)
+ (add-hook 'erc--pre-clear-functions #'erc-stamp--reset-on-clear 40)
+ (unless erc--updating-modules-p (erc-buffer-do #'erc-stamp--setup)))
+ ((remove-hook 'erc-mode-hook #'erc-stamp--setup)
(remove-hook 'erc-insert-modify-hook #'erc-add-timestamp)
- (remove-hook 'erc-send-modify-hook #'erc-add-timestamp)))
+ (remove-hook 'erc-send-modify-hook #'erc-add-timestamp)
+ (remove-hook 'erc-mode-hook #'erc-stamp--recover-on-reconnect)
+ (remove-hook 'erc--pre-clear-functions #'erc-stamp--reset-on-clear)
+ (erc-buffer-do #'erc-stamp--setup)))
+
+(defvar erc-stamp--invisible-property nil
+ "Existing `invisible' property value and/or symbol `timestamp'.")
+
+(defvar erc-stamp--skip-when-invisible nil
+ "Escape hatch for omitting stamps when first char is invisible.")
+
+(defun erc-stamp--recover-on-reconnect ()
+ "Attempt to restore \"last-inserted\" snapshots from prior session."
+ (when-let ((priors (or erc--server-reconnecting erc--target-priors)))
+ (dolist (var '(erc-timestamp-last-inserted
+ erc-timestamp-last-inserted-left
+ erc-timestamp-last-inserted-right))
+ (when-let (existing (alist-get var priors))
+ (set var existing)))))
+
+(defvar erc-stamp--current-time nil
+ "The current time when calling `erc-insert-timestamp-function'.
+Specifically, this is the same lisp time object used to create
+the stamp passed to `erc-insert-timestamp-function'.")
+
+(cl-defgeneric erc-stamp--current-time ()
+ "Return a lisp time object to associate with an IRC message.
+This becomes the message's `erc--ts' text property."
+ (erc-compat--current-lisp-time))
+
+(cl-defmethod erc-stamp--current-time :around ()
+ (or erc-stamp--current-time (cl-call-next-method)))
+
+(defvar erc-stamp--skip nil
+ "Non-nil means inhibit `erc-add-timestamp' completely.")
+
+(defvar erc-stamp--allow-unmanaged nil
+ "Non-nil means run `erc-add-timestamp' almost unconditionally.
+This is an unofficial escape hatch for code wanting to use
+lower-level message-insertion functions, like `erc-insert-line',
+directly. Third parties needing such functionality should
+petition for it via \\[erc-bug].")
+
+(defvar erc-stamp--permanent-cursor-sensor-functions nil
+ "Non-nil means add `cursor-sensor-functions' unconditionally.
+This is an unofficial escape hatch for code wanting the text
+property `cursor-sensor-functions' to always be present,
+regardless of the option `erc-echo-timestamps'. Third parties
+needing such pre-5.6 behavior to stick around should make that
+known via \\[erc-bug].")
(defun erc-add-timestamp ()
"Add timestamp and text-properties to message.
This function is meant to be called from `erc-insert-modify-hook'
or `erc-send-modify-hook'."
- (unless (get-text-property (point) 'invisible)
- (let ((ct (current-time)))
- (if (fboundp erc-insert-timestamp-function)
- (funcall erc-insert-timestamp-function
- (erc-format-timestamp ct erc-timestamp-format))
- (error "Timestamp function unbound"))
- (when (and (fboundp erc-insert-away-timestamp-function)
- erc-away-timestamp-format
- (erc-away-time)
- (not erc-timestamp-format))
+ (unless (or erc-stamp--skip (and (not erc-stamp--allow-unmanaged)
+ (null erc--msg-props)))
+ (let* ((ct (erc-stamp--current-time))
+ (invisible (get-text-property (point-min) 'invisible))
+ (erc-stamp--invisible-property
+ ;; FIXME on major version bump, make this `erc-' prefixed.
+ (if invisible `(timestamp ,@(ensure-list invisible)) 'timestamp))
+ (skipp (or (and erc-stamp--skip-when-invisible invisible)
+ (erc--check-msg-prop 'erc--ephemeral)))
+ (erc-stamp--current-time ct))
+ (when erc--msg-props
+ (puthash 'erc--ts ct erc--msg-props))
+ (unless skipp
+ (funcall erc-insert-timestamp-function
+ (erc-format-timestamp ct erc-timestamp-format)))
+ ;; Check `erc-insert-away-timestamp-function' for historical
+ ;; reasons even though its Custom :type only allows functions.
+ (when (and (not (or skipp erc-timestamp-format))
+ erc-away-timestamp-format
+ (functionp erc-insert-away-timestamp-function)
+ (erc-away-time))
(funcall erc-insert-away-timestamp-function
(erc-format-timestamp ct erc-away-timestamp-format)))
- (add-text-properties (point-min) (point-max)
+ (when erc-stamp--permanent-cursor-sensor-functions
+ (add-text-properties (point-min) (max (point-min) (1- (point-max)))
;; It's important for the function to
;; be different on different entries (bug#22700).
(list 'cursor-sensor-functions
- (list (lambda (_window _before dir)
- (erc-echo-timestamp dir ct))))))))
+ ;; Regions are no longer contiguous ^
+ '(erc--echo-ts-csf) 'erc--ts ct))))))
(defvar-local erc-timestamp-last-window-width nil
"The width of the last window that showed the current buffer.
@@ -190,9 +282,11 @@ buffer is not shown in any window.")
"Last timestamp inserted into the buffer.")
(defvar-local erc-timestamp-last-inserted-left nil
- "Last timestamp inserted into the left side of the buffer.
-This is used when `erc-insert-timestamp-function' is set to
-`erc-timestamp-left-and-right'")
+ "Last \"date stamp\" inserted into the left side of the buffer.
+Used when `erc-insert-timestamp-function' is set to
+`erc-timestamp-left-and-right'. If the format string specified
+by `erc-timestamp-format-left' includes trailing newlines, this
+value omits the last one.")
(defvar-local erc-timestamp-last-inserted-right nil
"Last timestamp inserted into the right side of the buffer.
@@ -217,17 +311,213 @@ the correct column."
(integer :tag "Column number")
(const :tag "Unspecified" nil)))
-(defcustom erc-timestamp-use-align-to (eq window-system 'x)
+(defcustom erc-timestamp-use-align-to (and (display-graphic-p) t)
"If non-nil, use the :align-to display property to align the stamp.
This gives better results when variable-width characters (like
Asian language characters and math symbols) precede a timestamp.
-A side effect of enabling this is that there will only be one
-space before a right timestamp in any saved logs."
- :type 'boolean)
+This option only matters when `erc-insert-timestamp-function' is
+set to `erc-insert-timestamp-right' or that option's default,
+`erc-insert-timestamp-left-and-right'. If the value is a
+positive integer, alignment occurs that many columns from the
+right edge.
+
+Enabling this option produces a side effect in that stamps aren't
+indented in saved logs. When its value is an integer, this
+option adds a space after the end of a message if the stamp
+doesn't already start with one. And when its value is t, it adds
+a single space, unconditionally."
+ :type '(choice boolean integer)
+ :package-version '(ERC . "5.6"))
+
+(defvar-local erc-stamp--margin-width nil
+ "Width in columns of margin for `erc-stamp--display-margin-mode'.
+Only consulted when resetting or initializing margin.")
+
+(defvar-local erc-stamp--margin-left-p nil
+ "Whether `erc-stamp--display-margin-mode' uses the left margin.
+During initialization, the mode respects this variable's existing
+value if it already has a local binding. Otherwise, modules can
+bind this to any value while enabling the mode. If it's nil, ERC
+will check to see if `erc-insert-timestamp-function' is
+`erc-insert-timestamp-left', interpreting the latter as a non-nil
+value. It'll then coerce any non-nil value to t.")
+
+(defun erc-stamp--init-margins-on-connect (&rest _)
+ (let ((existing (if erc-stamp--margin-left-p
+ left-margin-width
+ right-margin-width)))
+ (erc-stamp--adjust-margin existing 'resetp)))
+
+(defun erc-stamp--adjust-margin (cols &optional resetp)
+ "Adjust managed margin by increment COLS.
+With RESETP, set margin's width to COLS. However, if COLS is
+zero, set the width to a non-nil `erc-stamp--margin-width'.
+Otherwise, go with the `string-width' of `erc-timestamp-format'.
+However, when `erc-stamp--margin-left-p' is non-nil and the
+prompt is wider, use its width instead."
+ (let* ((leftp erc-stamp--margin-left-p)
+ (width
+ (if resetp
+ (or (and (not (zerop cols)) cols)
+ erc-stamp--margin-width
+ (max (if leftp
+ (cond ((fboundp 'erc-fill--wrap-measure)
+ (let* ((b erc-insert-marker)
+ (e (1- erc-input-marker))
+ (w (erc-fill--wrap-measure b e)))
+ (/ (if (consp w) (car w) w)
+ (frame-char-width))))
+ ((fboundp 'string-pixel-width)
+ (/ (string-pixel-width (erc-prompt))
+ (frame-char-width)))
+ (t (string-width (erc-prompt))))
+ 0)
+ (1+ (string-width
+ (or (if leftp
+ erc-timestamp-last-inserted
+ erc-timestamp-last-inserted-right)
+ (erc-format-timestamp
+ (current-time) erc-timestamp-format))))))
+ (+ (if leftp left-margin-width right-margin-width) cols))))
+ (set (if leftp 'left-margin-width 'right-margin-width) width)
+ (when (eq (current-buffer) (window-buffer))
+ (set-window-margins nil
+ (if leftp width left-margin-width)
+ (if leftp right-margin-width width)))))
+
+;;;###autoload
+(defun erc-stamp-prefix-log-filter (text)
+ "Prefix every message in the buffer with a stamp.
+Remove trailing stamps as well. For now, hard code the format to
+\"ZNC\"-log style, which is [HH:MM:SS]. Expect to be used as a
+`erc-log-filter-function' when `erc-timestamp-use-align-to' is
+non-nil."
+ (insert text)
+ (goto-char (point-min))
+ (while
+ (progn
+ (when-let (((< (point) (pos-eol)))
+ (end (1- (pos-eol)))
+ ((eq 'erc-timestamp (field-at-pos end)))
+ (beg (field-beginning end))
+ ;; Skip a line that's just a timestamp.
+ ((> beg (point))))
+ (delete-region beg (1+ end)))
+ (when-let (time (erc--get-inserted-msg-prop 'erc--ts))
+ (insert (format-time-string "[%H:%M:%S] " time)))
+ (zerop (forward-line))))
+ "")
+
+;; These are currently extended manually, but we could also bind
+;; `text-property-default-nonsticky' and call `insert-and-inherit'
+;; instead of `insert', but we'd have to pair the props with differing
+;; boolean values for left and right stamps. Also, since this hook
+;; runs last, we can't expect overriding sticky props to be absent,
+;; even though, as of 5.6, `front-sticky' is only added by the
+;; `readonly' module after hooks run.
+(defvar erc-stamp--inherited-props '(line-prefix wrap-prefix)
+ "Extant properties at the start of a message inherited by the stamp.")
+
+(defvar-local erc-stamp--skip-left-margin-prompt-p nil
+ "Don't display prompt in left margin.")
+
+(declare-function erc--remove-text-properties "erc" (string))
+
+;; Currently, `erc-insert-timestamp-right' hard codes its display
+;; property to use `right-margin', and `erc-insert-timestamp-left'
+;; does the same for `left-margin'. However, there's no reason a
+;; trailing stamp couldn't be displayed on the left and vice versa.
+(define-minor-mode erc-stamp--display-margin-mode
+ "Internal minor mode for built-in modules integrating with `stamp'.
+Arranges for displaying stamps in a single margin, with the
+variable `erc-stamp--margin-left-p' controlling which one.
+Provides `erc-stamp--margin-width' and `erc-stamp--adjust-margin'
+to help manage the chosen margin's width. Also removes `display'
+properties in killed text to reveal stamps. The invoking module
+should set controlling variables, like `erc-stamp--margin-width'
+and `erc-stamp--margin-left-p', before activating the mode."
+ :interactive nil
+ (if erc-stamp--display-margin-mode
+ (progn
+ (setq fringes-outside-margins t)
+ (when (eq (current-buffer) (window-buffer))
+ (set-window-buffer (selected-window) (current-buffer)))
+ (setq erc-stamp--margin-left-p (and erc-stamp--margin-left-p t))
+ (if (or erc-server-connected (not (functionp erc-prompt)))
+ (erc-stamp--init-margins-on-connect)
+ (add-hook 'erc-after-connect
+ #'erc-stamp--init-margins-on-connect nil t))
+ (add-function :filter-return (local 'filter-buffer-substring-function)
+ #'erc--remove-text-properties)
+ (add-hook 'erc--setup-buffer-hook
+ #'erc-stamp--refresh-left-margin-prompt nil t)
+ (when (and erc-stamp--margin-left-p
+ (not erc-stamp--skip-left-margin-prompt-p))
+ (add-hook 'erc--refresh-prompt-hook
+ #'erc-stamp--display-prompt-in-left-margin nil t)))
+ (remove-function (local 'filter-buffer-substring-function)
+ #'erc--remove-text-properties)
+ (remove-hook 'erc-after-connect
+ #'erc-stamp--init-margins-on-connect t)
+ (remove-hook 'erc--refresh-prompt-hook
+ #'erc-stamp--display-prompt-in-left-margin t)
+ (remove-hook 'erc--setup-buffer-hook
+ #'erc-stamp--refresh-left-margin-prompt t)
+ (kill-local-variable (if erc-stamp--margin-left-p
+ 'left-margin-width
+ 'right-margin-width))
+ (kill-local-variable 'erc-stamp--skip-left-margin-prompt-p)
+ (kill-local-variable 'fringes-outside-margins)
+ (kill-local-variable 'erc-stamp--margin-left-p)
+ (kill-local-variable 'erc-stamp--margin-width)
+ (when (eq (current-buffer) (window-buffer))
+ (set-window-margins nil left-margin-width nil)
+ (set-window-buffer (selected-window) (current-buffer)))))
+
+(defvar-local erc-stamp--last-prompt nil)
+
+(defun erc-stamp--display-prompt-in-left-margin ()
+ "Show prompt in the left margin with padding."
+ (when (or (not erc-stamp--last-prompt) (functionp erc-prompt)
+ (> (string-width erc-stamp--last-prompt) left-margin-width))
+ (let ((s (buffer-substring erc-insert-marker (1- erc-input-marker))))
+ ;; Prevent #("abc" n m (display ((...) #("abc" p q (display...))))
+ (remove-text-properties 0 (length s) '(display nil) s)
+ (when (and erc-stamp--last-prompt
+ (>= (string-width erc-stamp--last-prompt) left-margin-width))
+ (let ((sm (truncate-string-to-width s (1- left-margin-width) 0 nil t)))
+ ;; This papers over a subtle off-by-1 bug here.
+ (unless (equal sm s)
+ (setq s (concat sm (substring s -1))))))
+ (setq erc-stamp--last-prompt (string-pad s left-margin-width nil t))))
+ (put-text-property erc-insert-marker (1- erc-input-marker)
+ 'display `((margin left-margin) ,erc-stamp--last-prompt))
+ erc-stamp--last-prompt)
+
+(defun erc-stamp--refresh-left-margin-prompt ()
+ "Forcefully-recompute display property of prompt in left margin."
+ (with-silent-modifications
+ (unless (functionp erc-prompt)
+ (setq erc-stamp--last-prompt nil))
+ (erc--refresh-prompt)))
+
+(cl-defmethod erc--conceal-prompt
+ (&context (erc-stamp--display-margin-mode (eql t))
+ (erc-stamp--margin-left-p (eql t))
+ (erc-stamp--skip-left-margin-prompt-p null))
+ (when-let (((null erc--hidden-prompt-overlay))
+ (prompt (string-pad erc-prompt-hidden left-margin-width nil 'start))
+ (ov (make-overlay erc-insert-marker (1- erc-input-marker)
+ nil 'front-advance)))
+ (overlay-put ov 'display `((margin left-margin) ,prompt))
+ (setq erc--hidden-prompt-overlay ov)))
(defun erc-insert-timestamp-left (string)
"Insert timestamps at the beginning of the line."
+ (erc--insert-timestamp-left string))
+
+(cl-defmethod erc--insert-timestamp-left (string)
(goto-char (point-min))
(let* ((ignore-p (and erc-timestamp-only-if-changed-flag
(string-equal string erc-timestamp-last-inserted)))
@@ -235,14 +525,30 @@ space before a right timestamp in any saved logs."
(s (if ignore-p (make-string len ? ) string)))
(unless ignore-p (setq erc-timestamp-last-inserted string))
(erc-put-text-property 0 len 'field 'erc-timestamp s)
- (erc-put-text-property 0 len 'invisible 'timestamp s)
+ (erc-put-text-property 0 len 'invisible erc-stamp--invisible-property s)
(insert s)))
+(cl-defmethod erc--insert-timestamp-left
+ (string &context (erc-stamp--display-margin-mode (eql t)))
+ (unless (and erc-timestamp-only-if-changed-flag
+ (string-equal string erc-timestamp-last-inserted))
+ (goto-char (point-min))
+ (insert-and-inherit (setq erc-timestamp-last-inserted string))
+ (dolist (p erc-stamp--inherited-props)
+ (when-let ((v (get-text-property (point) p)))
+ (put-text-property (point-min) (point) p v)))
+ (erc-put-text-property (point-min) (point) 'invisible
+ erc-stamp--invisible-property)
+ (put-text-property (point-min) (point) 'field 'erc-timestamp)
+ (put-text-property (point-min) (point)
+ 'display `((margin left-margin) ,string))))
+
(defun erc-insert-aligned (string pos)
"Insert STRING at the POSth column.
If `erc-timestamp-use-align-to' is t, use the :align-to display
property to get to the POSth column."
+ (declare (obsolete "inlined and removed from client code path" "30.1"))
(if (not erc-timestamp-use-align-to)
(indent-to pos)
(insert " ")
@@ -253,6 +559,12 @@ property to get to the POSth column."
;; Silence byte-compiler
(defvar erc-fill-column)
+(defvar erc-stamp--omit-properties-on-folded-lines nil
+ "Skip properties before right stamps occupying their own line.
+This escape hatch restores pre-5.6 behavior that left leading
+white space alone (unpropertized) for right-sided stamps folded
+onto their own line.")
+
(defun erc-insert-timestamp-right (string)
"Insert timestamp on the right side of the screen.
STRING is the timestamp to insert. This function is a possible
@@ -280,6 +592,7 @@ printed just after each line's text (no alignment)."
(goto-char (point-max))
(forward-char -1) ; before the last newline
(let* ((str-width (string-width string))
+ (buffer-invisibility-spec nil) ; `current-column' > 0
window ; used in computation of `pos' only
(pos (cond
(erc-timestamp-right-column erc-timestamp-right-column)
@@ -304,31 +617,200 @@ printed just after each line's text (no alignment)."
;; some margin of error if what is displayed on the line differs
;; from the number of characters on the line.
(setq col (+ col (ceiling (/ (- col (- (point) (line-beginning-position))) 1.6))))
- (if (< col pos)
- (erc-insert-aligned string pos)
- (newline)
- (indent-to pos)
- (setq from (point))
- (insert string))
+ ;; For compatibility reasons, the `erc-timestamp' field includes
+ ;; intervening white space unless a hard break is warranted.
+ (pcase erc-timestamp-use-align-to
+ ((guard erc-stamp--display-margin-mode)
+ (let ((s (propertize (substring-no-properties string)
+ 'invisible erc-stamp--invisible-property)))
+ (insert " ")
+ (put-text-property 0 (length string) 'display
+ `((margin right-margin) ,s)
+ string)))
+ ((and 't (guard (< col pos)))
+ (insert " ")
+ (put-text-property from (point) 'display `(space :align-to ,pos)))
+ ((pred integerp) ; (cl-type (integer 0 *))
+ (insert " ")
+ (when (eq ?\s (aref string 0))
+ (setq string (substring string 1)))
+ (let ((s (+ erc-timestamp-use-align-to (string-width string))))
+ (put-text-property from (point) 'display
+ `(space :align-to (- right ,s)))))
+ ((guard (>= col pos)) (newline) (indent-to pos)
+ (when erc-stamp--omit-properties-on-folded-lines (setq from (point))))
+ (_ (indent-to pos)))
+ (insert string)
+ (dolist (p erc-stamp--inherited-props)
+ (when-let ((v (get-text-property (1- from) p)))
+ (put-text-property from (point) p v)))
(erc-put-text-property from (point) 'field 'erc-timestamp)
(erc-put-text-property from (point) 'rear-nonsticky t)
+ (erc-put-text-property from (point) 'invisible
+ erc-stamp--invisible-property)
(when erc-timestamp-intangible
(erc-put-text-property from (1+ (point)) 'cursor-intangible t)))))
-(defun erc-insert-timestamp-left-and-right (_string)
- "This is another function that can be used with `erc-insert-timestamp-function'.
-If the date is changed, it will print a blank line, the date, and
-another blank line. If the time is changed, it will then print
-it off to the right."
- (let* ((ct (current-time))
- (ts-left (erc-format-timestamp ct erc-timestamp-format-left))
- (ts-right (erc-format-timestamp ct erc-timestamp-format-right)))
- ;; insert left timestamp
- (unless (string-equal ts-left erc-timestamp-last-inserted-left)
+(defvar erc-stamp--insert-date-hook nil
+ "Functions appended to send and modify hooks when inserting date stamp.")
+
+(defvar-local erc-stamp--date-format-end nil
+ "Tristate value indicating how and whether date stamps have been set up.
+A non-nil value means the buffer has been initialized to use date
+stamps. An integer marks the `substring' TO parameter for
+truncating `erc-timestamp-format-left' prior to rendering. A
+value of t means the option's value doesn't require trimming.")
+
+(defun erc-stamp--propertize-left-date-stamp ()
+ (add-text-properties (point-min) (1- (point-max)) '(field erc-timestamp))
+ (erc--hide-message 'timestamp)
+ (run-hooks 'erc-stamp--insert-date-hook))
+
+(defun erc-stamp--format-date-stamp (ct)
+ "Format left date stamp with `erc-timestamp-format-left'."
+ (unless erc-stamp--date-format-end
+ ;; Don't add text properties to the trailing newline.
+ (setq erc-stamp--date-format-end
+ (if (string-suffix-p "\n" erc-timestamp-format-left) -1 t)))
+ ;; Ignore existing `invisible' prop value because date stamps should
+ ;; never be hideable except via `timestamp'.
+ (let (erc-stamp--invisible-property)
+ (erc-format-timestamp ct (if (numberp erc-stamp--date-format-end)
+ (substring erc-timestamp-format-left
+ 0 erc-stamp--date-format-end)
+ erc-timestamp-format-left))))
+
+(defun erc-stamp-inserting-date-stamp-p ()
+ "Return non-nil if the narrowed buffer contains a date stamp.
+Expect to be called by members of `erc-insert-modify-hook' and
+`erc-insert-post-hook' to detect whether the message being
+inserted is a date stamp."
+ (erc--check-msg-prop 'erc--msg 'datestamp))
+
+;; Calling `erc-display-message' from within a hook it's currently
+;; running is roundabout, but it's a definite means of ensuring hooks
+;; can act on the date stamp as a standalone message to do things like
+;; adjust invisibility props.
+(defun erc-stamp--insert-date-stamp-as-phony-message (string)
+ (cl-assert (string-empty-p string))
+ (setq string erc-timestamp-last-inserted-left)
+ (let ((erc-stamp--skip t)
+ (erc-insert-modify-hook `(,@erc-insert-modify-hook
+ erc-stamp--propertize-left-date-stamp))
+ (erc--insert-line-function #'insert-before-markers)
+ ;; Don't run hooks that aren't expecting a narrowed buffer.
+ (erc-insert-pre-hook nil)
+ (erc-insert-done-hook nil))
+ (erc-display-message nil nil (current-buffer) string)))
+
+(defun erc-stamp--lr-date-on-pre-modify (_)
+ (when-let (((not erc-stamp--skip))
+ (ct (erc-stamp--current-time))
+ (rendered (erc-stamp--format-date-stamp ct))
+ ((not (string-equal rendered erc-timestamp-last-inserted-left)))
+ (erc-insert-timestamp-function
+ #'erc-stamp--insert-date-stamp-as-phony-message))
+ (save-excursion
+ (save-restriction
+ (narrow-to-region (or erc--insert-marker erc-insert-marker)
+ (or erc--insert-marker erc-insert-marker))
+ ;; Ensure all hooks, like `erc-stamp--insert-date-hook', only
+ ;; see the let-bound value below during `erc-add-timestamp'.
+ (setq erc-timestamp-last-inserted-left nil)
+ (let* ((aligned (erc-stamp--time-as-day ct))
+ (erc-stamp--current-time aligned)
+ ;; Forget current `erc--cmd', etc.
+ (erc--msg-props (map-into `((erc--msg . datestamp))
+ 'hash-table))
+ (erc-timestamp-last-inserted-left rendered)
+ erc-timestamp-format erc-away-timestamp-format)
+ (erc-add-timestamp))
+ (setq erc-timestamp-last-inserted-left rendered)))))
+
+;; This minor mode is just a placeholder and currently unhelpful for
+;; managing complexity. A useful version would leave a marker during
+;; post-modify hooks and then perform insertions (before markers)
+;; during "done" hooks. This would enable completely decoupling from
+;; and possibly deprecating `erc-insert-timestamp-left-and-right'.
+;; However, doing this would require expanding the internal API to
+;; include insertion and deletion handlers for twiddling and massaging
+;; text properties based on context immediately after modifying text
+;; earlier in a buffer (away from `erc-insert-marker'). Without such
+;; handlers, things like "merged" `fill-wrap' speakers and invisible
+;; messages may be damaged by buffer modifications.
+(define-minor-mode erc-stamp--date-mode
+ "Insert date stamps as standalone messages."
+ :interactive nil
+ (if erc-stamp--date-mode
+ (progn (add-hook 'erc-insert-pre-hook
+ #'erc-stamp--lr-date-on-pre-modify 10 t)
+ (add-hook 'erc-pre-send-functions
+ #'erc-stamp--lr-date-on-pre-modify 10 t))
+ (kill-local-variable 'erc-timestamp-last-inserted-left)
+ (remove-hook 'erc-insert-pre-hook
+ #'erc-stamp--lr-date-on-pre-modify t)
+ (remove-hook 'erc-pre-send-functions
+ #'erc-stamp--lr-date-on-pre-modify t)))
+
+(defvar erc-stamp-prepend-date-stamps-p nil
+ "When non-nil, date stamps are not independent messages.
+This flag restores pre-5.6 behavior in which date stamps formed
+the leading portion of affected messages. Beware that enabling
+this degrades the user experience by causing 5.6+ features, like
+`fill-wrap', dynamic invisibility, etc., to malfunction. When
+non-nil, none of the newline twiddling mentioned in the doc
+string for `erc-timestamp-format-left' occurs. That is, ERC does
+not append or remove trailing newlines.")
+(make-obsolete-variable 'erc-stamp-prepend-date-stamps-p
+ "unsupported legacy behavior" "30.1")
+
+(defun erc-insert-timestamp-left-and-right (string)
+ "Insert a stamp on either side when it changes.
+When the deprecated option `erc-timestamp-format-right' is nil,
+use STRING, which originates from `erc-timestamp-format', for the
+right-hand stamp. Use `erc-timestamp-format-left' for formatting
+the left-sided \"date stamp,\" and expect it to change less
+frequently. Include all but the final trailing newline present
+in the latter (if any) as part of the `erc-timestamp' field.
+Allow the stamp's `invisible' property to span that same interval
+but also cover the previous newline, in order to satisfy folding
+requirements related to `erc-legacy-invisible-bounds-p'.
+Additionally, ensure every date stamp is identifiable as such so
+that internal modules can easily distinguish between other
+left-sided stamps and date stamps inserted by this function."
+ (unless (or erc-stamp--date-format-end erc-stamp-prepend-date-stamps-p
+ (and (or (null erc-timestamp-format-left)
+ (string-empty-p ; compat
+ (string-trim erc-timestamp-format-left "\n")))
+ (always (erc-stamp--date-mode -1))
+ (setq erc-stamp-prepend-date-stamps-p t)))
+ (erc-stamp--date-mode +1)
+ ;; Hooks used by ^ are the preferred means of inserting date
+ ;; stamps. But they'll never see this inaugural message, so it
+ ;; must be handled specially.
+ (let ((erc--insert-marker (point-min-marker))
+ (end-marker (point-max-marker)))
+ (set-marker-insertion-type erc--insert-marker t)
+ (erc-stamp--lr-date-on-pre-modify nil)
+ (narrow-to-region erc--insert-marker end-marker)
+ (set-marker end-marker nil)
+ (set-marker erc--insert-marker nil)))
+ (let* ((ct (erc-stamp--current-time))
+ (ts-right (with-suppressed-warnings
+ ((obsolete erc-timestamp-format-right))
+ (if erc-timestamp-format-right
+ (erc-format-timestamp ct erc-timestamp-format-right)
+ string))))
+ ;; We should arguably be ensuring a trailing newline on legacy
+ ;; "prepended" date stamps as well. However, since this is a
+ ;; compatibility oriented code path, and pre-5.6 did no such
+ ;; thing, better to punt.
+ (when-let ((erc-stamp-prepend-date-stamps-p)
+ (ts-left (erc-format-timestamp ct erc-timestamp-format-left))
+ ((not (string= ts-left erc-timestamp-last-inserted-left))))
(goto-char (point-min))
(erc-put-text-property 0 (length ts-left) 'field 'erc-timestamp ts-left)
- (insert ts-left)
- (setq erc-timestamp-last-inserted-left ts-left))
+ (insert (setq erc-timestamp-last-inserted-left ts-left)))
;; insert right timestamp
(let ((erc-timestamp-only-if-changed-flag t)
(erc-timestamp-last-inserted erc-timestamp-last-inserted-right))
@@ -336,17 +818,36 @@ it off to the right."
(setq erc-timestamp-last-inserted-right ts-right))))
;; for testing: (setq erc-timestamp-only-if-changed-flag nil)
+(defvar erc-stamp--tz nil)
+
+;; Unfortunately, cursory measurements show that this function is 10x
+;; slower than `erc-format-timestamp', which is perhaps
+;; counterintuitive. Thus, we use the latter for our cache, and
+;; perform day alignments via this function only when needed.
+(defun erc-stamp--time-as-day (current-time)
+ "Discard hour, minute, and second info from timestamp CURRENT-TIME."
+ (defvar current-time-list) ; <=28
+ (let* ((current-time-list) ; flag
+ (decoded (decode-time current-time erc-stamp--tz)))
+ (setf (decoded-time-second decoded) 0
+ (decoded-time-minute decoded) 0
+ (decoded-time-hour decoded) 0
+ (decoded-time-dst decoded) -1
+ (decoded-time-weekday decoded) nil
+ (decoded-time-zone decoded)
+ (and erc-stamp--tz (car (current-time-zone nil erc-stamp--tz))))
+ (encode-time decoded))) ; may return an integer
(defun erc-format-timestamp (time format)
"Return TIME formatted as string according to FORMAT.
Return the empty string if FORMAT is nil."
(if format
- (let ((ts (format-time-string format time)))
+ (let ((ts (format-time-string format time erc-stamp--tz)))
(erc-put-text-property 0 (length ts)
'font-lock-face 'erc-timestamp-face ts)
- (erc-put-text-property 0 (length ts) 'invisible 'timestamp ts)
- (erc-put-text-property 0 (length ts)
- 'isearch-open-invisible 'timestamp ts)
+ (when erc-stamp--invisible-property
+ (erc-put-text-property 0 (length ts) 'invisible
+ erc-stamp--invisible-property ts))
;; N.B. Later use categories instead of this harmless, but
;; inelegant, hack. -- BPT
(and erc-timestamp-intangible
@@ -355,25 +856,81 @@ Return the empty string if FORMAT is nil."
ts)
""))
-;; This function is used to munge `buffer-invisibility-spec' to an
-;; appropriate value. Currently, it only handles timestamps, thus its
-;; location. If you add other features which affect invisibility,
-;; please modify this function and move it to a more appropriate
-;; location.
-(defun erc-munge-invisibility-spec ()
- (and erc-timestamp-intangible (not (bound-and-true-p cursor-intangible-mode))
- (cursor-intangible-mode 1))
- (and erc-echo-timestamps (not (bound-and-true-p cursor-sensor-mode))
- (cursor-sensor-mode 1))
+(defvar-local erc-stamp--csf-props-updated-p nil)
+
+(define-obsolete-function-alias 'erc-munge-invisibility-spec
+ #'erc-stamp--manage-local-options-state "30.1"
+ "Perform setup and teardown of `stamp'-owned options.
+
+Note that this function's role in practice has long defied its
+stated mandate as claimed in a now deleted comment, which
+envisioned it as evolving into a central toggle for modifying
+`buffer-invisibility-spec' on behalf of options and features
+ERC-wide.")
+(defun erc-stamp--manage-local-options-state ()
+ "Perform local setup and teardown for `stamp'-owned options.
+For `erc-timestamp-intangible', toggle `cursor-intangible-mode'.
+For `erc-echo-timestamps', integrate with `cursor-sensor-mode'.
+For `erc-hide-timestamps, modify `buffer-invisibility-spec'."
+ (if erc-timestamp-intangible
+ (cursor-intangible-mode +1) ; idempotent
+ (when (bound-and-true-p cursor-intangible-mode)
+ (cursor-intangible-mode -1)))
+ (if erc-echo-timestamps
+ (progn
+ (unless erc-stamp--permanent-cursor-sensor-functions
+ (dolist (hook '(erc-insert-post-hook erc-send-post-hook))
+ (add-hook hook #'erc-stamp--add-csf-on-post-modify nil t))
+ (setq erc-stamp--csf-props-updated-p
+ (alist-get 'erc-stamp--csf-props-updated-p
+ (or erc--server-reconnecting erc--target-priors)))
+ (unless erc-stamp--csf-props-updated-p
+ (setq erc-stamp--csf-props-updated-p t)
+ ;; Spoof `erc--ts' as being non-nil.
+ (let ((erc--msg-props (map-into '((erc--ts . t)) 'hash-table)))
+ (with-silent-modifications
+ (erc--traverse-inserted
+ (point-min) erc-insert-marker
+ #'erc-stamp--add-csf-on-post-modify)))))
+ (cursor-sensor-mode +1) ; idempotent
+ (when (>= emacs-major-version 29)
+ (add-function :before-until (local 'clear-message-function)
+ #'erc-stamp--on-clear-message)))
+ (dolist (hook '(erc-insert-post-hook erc-send-post-hook))
+ (remove-hook hook #'erc-stamp--add-csf-on-post-modify t))
+ (kill-local-variable 'erc-stamp--csf-props-updated-p)
+ (when (bound-and-true-p cursor-sensor-mode)
+ (cursor-sensor-mode -1))
+ (remove-function (local 'clear-message-function)
+ #'erc-stamp--on-clear-message))
(if erc-hide-timestamps
(add-to-invisibility-spec 'timestamp)
(remove-from-invisibility-spec 'timestamp)))
+(defun erc-stamp--add-csf-on-post-modify ()
+ "Add `cursor-sensor-functions' to narrowed buffer."
+ (when (erc--check-msg-prop 'erc--ts)
+ (put-text-property (point-min) (1- (point-max))
+ 'cursor-sensor-functions '(erc--echo-ts-csf))))
+
+(defun erc-stamp--setup ()
+ "Enable or disable buffer-local `erc-stamp-mode' modifications."
+ (if erc-stamp-mode
+ (erc-stamp--manage-local-options-state)
+ (let (erc-echo-timestamps erc-hide-timestamps erc-timestamp-intangible)
+ (erc-stamp--manage-local-options-state))
+ ;; Undo local mods from `erc-insert-timestamp-left-and-right'.
+ (erc-stamp--date-mode -1) ; kills `erc-timestamp-last-inserted-left'
+ (kill-local-variable 'erc-stamp--last-stamp)
+ (kill-local-variable 'erc-timestamp-last-inserted)
+ (kill-local-variable 'erc-timestamp-last-inserted-right)
+ (kill-local-variable 'erc-stamp--date-format-end)))
+
(defun erc-hide-timestamps ()
"Hide timestamp information from display."
(interactive)
(setq erc-hide-timestamps t)
- (erc-munge-invisibility-spec))
+ (erc-stamp--manage-local-options-state))
(defun erc-show-timestamps ()
"Show timestamp information on display.
@@ -381,7 +938,7 @@ This function only works if `erc-timestamp-format' was previously
set, and timestamping is already active."
(interactive)
(setq erc-hide-timestamps nil)
- (erc-munge-invisibility-spec))
+ (erc-stamp--manage-local-options-state))
(defun erc-toggle-timestamps ()
"Hide or show timestamps in ERC buffers.
@@ -395,15 +952,54 @@ enabled when the message was inserted."
(setq erc-hide-timestamps t))
(mapc (lambda (buffer)
(with-current-buffer buffer
- (erc-munge-invisibility-spec)))
+ (erc-stamp--manage-local-options-state)))
(erc-buffer-list)))
-(defun erc-echo-timestamp (dir stamp)
- "Print timestamp text-property of an IRC message."
- (when (and erc-echo-timestamps (eq 'entered dir))
- (when stamp
- (message "%s" (format-time-string erc-echo-timestamp-format
- stamp)))))
+(defvar-local erc-stamp--last-stamp nil)
+
+(defun erc-stamp--on-clear-message (&rest _)
+ "Return `dont-clear-message' when operating inside the same stamp."
+ (and erc-stamp--last-stamp erc-echo-timestamps
+ (eq (erc--get-inserted-msg-prop 'erc--ts) erc-stamp--last-stamp)
+ 'dont-clear-message))
+
+(defun erc-echo-timestamp (dir stamp &optional zone)
+ "Display timestamp of message at point in echo area.
+Interactively, interpret a numeric prefix as a ZONE offset in
+hours (or seconds, if its abs value is larger than 14), and
+interpret a \"raw\" prefix as UTC. To specify a zone for use
+with the option `erc-echo-timestamps', see the companion option
+`erc-echo-timestamp-zone'."
+ (interactive (list nil (erc--get-inserted-msg-prop 'erc--ts)
+ (pcase current-prefix-arg
+ ((and (pred numberp) v)
+ (if (<= (abs v) 14) (* v 3600) v))
+ (`(,_) t))))
+ (if (and stamp (or (null dir) (and erc-echo-timestamps (eq 'entered dir))))
+ (progn
+ (setq erc-stamp--last-stamp stamp)
+ (message (format-time-string erc-echo-timestamp-format
+ stamp (or zone erc-echo-timestamp-zone))))
+ (when (and erc-echo-timestamps (eq 'left dir))
+ (setq erc-stamp--last-stamp nil))))
+
+(defun erc--echo-ts-csf (_window _before dir)
+ (erc-echo-timestamp dir (erc--get-inserted-msg-prop 'erc--ts)))
+
+(defun erc-stamp--update-saved-position (&rest _)
+ (remove-hook 'erc-stamp--insert-date-hook
+ #'erc-stamp--update-saved-position t)
+ (move-marker erc-last-saved-position (1- (point-max))))
+
+(defun erc-stamp--reset-on-clear (pos)
+ "Forget last-inserted stamps when POS is at insert marker."
+ (when (= pos (1- erc-insert-marker))
+ (when erc-stamp--date-mode
+ (add-hook 'erc-stamp--insert-date-hook
+ #'erc-stamp--update-saved-position 0 t))
+ (setq erc-timestamp-last-inserted nil
+ erc-timestamp-last-inserted-left nil
+ erc-timestamp-last-inserted-right nil)))
(provide 'erc-stamp)