[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[nongnu] elpa/popon 240fb286d8 01/15: Working implementation
From: |
ELPA Syncer |
Subject: |
[nongnu] elpa/popon 240fb286d8 01/15: Working implementation |
Date: |
Sun, 22 May 2022 12:59:18 -0400 (EDT) |
branch: elpa/popon
commit 240fb286d8e8f8333c5d20da5fe8b32c277cd6eb
Author: Akib Azmain Turja <akib@disroot.org>
Commit: Akib Azmain Turja <akib@disroot.org>
Working implementation
---
README.org | 18 +++
popon.el | 408 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 426 insertions(+)
diff --git a/README.org b/README.org
new file mode 100644
index 0000000000..c08bb8a16d
--- /dev/null
+++ b/README.org
@@ -0,0 +1,18 @@
+#+title: Popon - "Pop" floating text "on" a window
+
+Popon allows you to pop text on a window, what we call a popon. Popons are
+window-local and sticky, they don't move while scrolling, and they even
+don't go away when switching buffer, but you can bind a popon to a specific
+buffer to only show on that buffer.
+
+If some popons annoying you and you can't kill them, do
+=M-x popon-kill-all= to kill all popons.
+
+* Usage
+
+The main entry point is ~popon-create~, which creates a popon and returns
+that. Use ~popon-kill~ to kill it. Popons are immutable, you can't modify
+them. Most of time you'll want to place the popon at certain point of
+buffer; call ~popon-x-y-at-pos~ with the point and use the return value as
+the coordinates. Be sure see the docstring of each function, they describe
+the best.
diff --git a/popon.el b/popon.el
new file mode 100644
index 0000000000..cf8b022b28
--- /dev/null
+++ b/popon.el
@@ -0,0 +1,408 @@
+;;; popon.el --- "Pop" floating text "on" a window -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Akib Azmain Turja.
+
+;; Author: Akib Azmain Turja <akib@disroot.org>
+;; Created: 2022-04-11
+;; Version: 0.1
+;; Package-Requires: ((emacs "27.1"))
+;; Keywords: lisp extensions frames
+;; Homepage: https://codeberg.org/akib/emacs-popon
+
+;; This file is not part of GNU Emacs.
+
+;; This file 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.
+
+;; For a full copy of the GNU General Public License
+;; see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Popon allows you to pop text on a window, what we call a popon. Popons
+;; are window-local and sticky, they don't move while scrolling, and they
+;; even don't go away when switching buffer, but you can bind a popon to a
+;; specific buffer to only show on that buffer.
+
+;; If some popons annoying you and you can't kill them, do M-x
+;; popon-kill-all to kill all popons.
+
+;;; Code:
+
+(require 'subr-x)
+(require 'cl-lib)
+
+(defun popon--render-lines (framebuffer x y lines width)
+ "Place LINES on top of FRAMEBUFFER.
+Place LINES on top of text at line X and column Y on FRAMEBUFFER and return
+FRAMEBUFFER. LINES is a list of list as string. FRAMEBUFFER is a list,
+each element is of form: (LINE MODIFIED OTHERS...), where LINE is the line
+as string and MODIFIED is t when LINE is modified. OTHERS is not modified
+in any way. Each line in LINES is assumed to occupy WIDTH character.
+FRAMEBUFFER and LINES shouldn't contain newlines. Example:
+
+\(`popon--render-lines'
+ '((\"GNU Emacs is “free software”; this means\" nil)
+ (\"that everyone is free to use it and free\" nil)
+ (\"to redistribute it under certain\" nil)
+ (\"conditions. GNU Emacs is not in the\" nil)
+ (\"public domain; it is copyrighted and\" nil)
+ (\"there are restrictions on its\" nil)
+ (\"distribution, but these restrictions are\" nil)
+ (\"designed to permit everything that a\" nil foo)
+ (\"good cooperating citizen would want to\" nil bar baz)
+ (\"do. What is not allowed is to try to\" nil)
+ (\"prevent others from further sharing any\" nil)
+ (\"version of GNU Emacs that they might get\" nil)
+ (\"from you. The precise conditions are\" nil)
+ (\"found in the GNU General Public License\" nil)
+ (\"that comes with Emacs and also appears\" nil)
+ (\"in this manual(1). See Copying.\" t))
+ 11 1 '(\"+--^^^^^^^^^^^^^-------------------------+\"
+ \"|Free software is a type of software that|\"
+ \"|respects user freedom. Think free as in|\"
+ \"|free speech, not as in free beer. |\"
+ \"+----------------------------------------+\")
+ 42)
+=> ((\"GNU Emacs is “free software”; this means\" nil)
+ (\"that everyo+--^^^^^^^^^^^^^-------------------------+\" t)
+ (\"to redistri|Free software is a type of software that|\" t)
+ (\"conditions.|respects user freedom. Think free as in|\" t)
+ (\"public doma|free speech, not as in free beer. |\" t)
+ (\"there are r+----------------------------------------+\" t)
+ (\"distribution, but these restrictions are\" nil)
+ (\"designed to permit everything that a\" nil foo)
+ (\"good cooperating citizen would want to\" nil bar baz)
+ (\"do. What is not allowed is to try to\" nil)
+ (\"prevent others from further sharing any\" nil)
+ (\"version of GNU Emacs that they might get\" nil)
+ (\"from you. The precise conditions are\" nil)
+ (\"found in the GNU General Public License\" nil)
+ (\"that comes with Emacs and also appears\" nil)
+ (\"in this manual(1). See Copying.\" t))"
+ (let ((tab-size tab-width))
+ (with-temp-buffer
+ (setq-local tab-width tab-size) ; Preseve tab width.
+ (dotimes (i (length lines))
+ (when (< (+ y i) (length framebuffer))
+ (erase-buffer)
+ (insert (car (nth (+ y i) framebuffer)))
+ (move-to-column x t)
+ (let ((mark (point)))
+ (move-to-column (+ x width) t)
+ (setf (car (nth (+ y i) framebuffer))
+ (concat (buffer-substring (point-min) mark)
+ (nth i lines)
+ (buffer-substring (point) (point-max))))
+ (setf (cadr (nth (+ y i) framebuffer)) t))))
+ framebuffer)))
+
+(defun poponp (object)
+ "Return t if OBJECT is a popon."
+ (and (proper-list-p object)
+ (eq (car-safe object) 'popon)))
+
+(defun popon-live-p (object)
+ "Return t if OBJECT is a popon and not killed."
+ (and (poponp object)
+ (plist-get (cdr object) :live)
+ (and (plist-get (cdr object) :window)
+ (window-live-p (plist-get (cdr object) :window)))
+ (or (not (plist-get (cdr object) :buffer))
+ (buffer-live-p (plist-get (cdr object) :buffer)))
+ t))
+
+(defun popon-get (popon prop)
+ "Get the PROP property of popon POPON."
+ (plist-get (plist-get (cdr popon) :plist) prop))
+
+(defun popon-put (popon prop value)
+ "Set the PROP property of popon POPON to VALUE."
+ (setcdr popon (plist-put (cdr popon)
+ :plist (plist-put (plist-get (cdr popon) :plist)
+ prop value))))
+
+(defun popon-properties (popon)
+ "Return a copy the property list of popon POPON."
+ (copy-sequence (plist-get (cdr popon) :plist)))
+
+(defun popon-position (popon)
+ "Return the position of popon POPON as a cons (X, Y).
+
+When popon POPON is killed, return nil."
+ (when (popon-live-p popon)
+ (cons (plist-get (cdr popon) :x)
+ (plist-get (cdr popon) :y))))
+
+(defun popon-size (popon)
+ "Return the size of popon POPON as a cons (WIDTH . HEIGHT).
+
+When popon POPON is killed, return nil."
+ (when (popon-live-p popon)
+ (cons (plist-get (cdr popon) :width)
+ (length (plist-get (cdr popon) :lines)))))
+
+(defun popon-window (popon)
+ "Return the window popon POPON belongs to.
+
+Return nil if popon POPON is killed."
+ (when (popon-live-p popon)
+ (plist-get (cdr popon) :window)))
+
+(defun popon-buffer (popon)
+ "Return the buffer popon POPON belongs to.
+
+Return nil if popon POPON is killed."
+ (when (popon-live-p popon)
+ (plist-get (cdr popon) :buffer)))
+
+(defun popon-text (popon)
+ "Return the text popon POPON is displaying.
+
+POPON may be a killed popon. Return nil if POPON isn't a popon at all."
+ (when (poponp popon)
+ (mapconcat #'identity (plist-get (cdr popon) :lines) "\n")))
+
+(defun popon--render (popon framebuffer offset)
+ "Render POPON in FRAMEBUFFER at vertical offset OFFSET."
+ (popon--render-lines framebuffer
+ (+ (plist-get (cdr popon) :x) offset)
+ (plist-get (cdr popon) :y)
+ (plist-get (cdr popon) :lines)
+ (plist-get (cdr popon) :width)))
+
+(defun popon-create (text pos &optional window buffer)
+ "Create a popon showing TEXT at POS of WINDOW.
+
+Display popon only if WINDOW is displaying BUFFER.
+
+POS is a cons (X, Y), where X is column and Y is line in WINDOW. TEXT
+should be a string or a cons cell of form (STR . WIDTH). When TEXT is a
+string, each line of it should be of same length (i.e `string-width' should
+return the same length for every line). When TEXT is a cons cell, STR is
+used as the text to display and each line of it should be of visual length
+width."
+ (let* ((lines (split-string (if (consp text) (car text) text) "\n"))
+ (popon `(popon :live t
+ :x ,(car pos)
+ :y ,(cdr pos)
+ :lines ,lines
+ :width ,(or (and (consp text) (cdr text))
+ (string-width (car lines)))
+ :window ,(or window (selected-window))
+ :buffer ,buffer
+ :plist nil)))
+ (push popon (window-parameter window 'popon-list))
+ (popon-update)
+ popon))
+
+(defun popon-kill (popon)
+ "Kill popon POPON.
+
+Do nothing if POPON isn't a live popon. Return nil."
+ (when (popon-live-p popon)
+ (let ((window (popon-window popon)))
+ (setf (window-parameter window 'popon-list)
+ (delete popon (window-parameter window 'popon-list))))
+ (setcdr popon (plist-put (cdr popon) :live nil))
+ (popon-update)
+ nil))
+
+(defvar-local popon--line-beginnings nil
+ "List of line beginning of current buffer.
+
+The value is of form (TICK . LINE-BEGINNINGS), where LINE-BEGINNINGS is the
+sorted list of beginning of lines and TICK is the value of tick counter
+when LINE-BEGINNINGS was calculated.")
+
+(defun popon--make-framebuffer ()
+ "Create a framebuffer for current window and buffer."
+ (let ((framebuffer nil)
+ (line-boundaries (let ((pair popon--line-beginnings)
+ (boundaries nil))
+ (when (eq (car pair) (buffer-modified-tick))
+ (setq pair (cdr pair))
+ (while pair
+ (when (and (integerp (car pair))
+ (integerp (cadr pair)))
+ (push (cons (car pair) (cadr pair))
+ boundaries))
+ (setq pair (cdr pair))))
+ boundaries)))
+ (save-excursion
+ (goto-char (window-start))
+ (let ((mark (point))
+ (point-to-line nil))
+ (dotimes (i (floor (window-screen-lines)))
+ (if-let ((next (alist-get (point) line-boundaries)))
+ (goto-char next)
+ (if truncate-lines
+ (forward-line 1)
+ (vertical-motion 1)))
+ (let ((line (alist-get mark point-to-line)))
+ (unless line
+ (setq line i)
+ (setf (alist-get mark point-to-line) line))
+ (push (list (string-trim-right (buffer-substring mark (point))
+ "\n")
+ nil line mark (point))
+ framebuffer))
+ (push (cons mark (point)) line-boundaries)
+ (setq mark (point)))))
+ (let ((line-beginnings nil))
+ (dolist (pair (sort (cl-delete-duplicates line-boundaries
+ :test #'equal)
+ #'car-less-than-car))
+ (unless (eq (car line-beginnings) (car pair))
+ (when (car line-beginnings)
+ (push nil line-beginnings))
+ (push (car pair) line-beginnings))
+ (push (cdr pair) line-beginnings))
+ (push nil line-beginnings)
+ (setq popon--line-beginnings (cons (buffer-modified-tick)
+ (nreverse line-beginnings))))
+ (nreverse framebuffer)))
+
+(defun popon--make-overlays (framebuffer)
+ "Make overlays to display FRAMEBUFFER on window."
+ (let ((line-map nil))
+ (let ((i 0))
+ (dolist (line framebuffer)
+ (when (nth 1 line)
+ (let* ((key (cons (nth 3 line) (nth 4 line)))
+ (pair (assoc key line-map)))
+ (unless pair
+ (setq pair (cons key nil))
+ (push pair line-map))
+ (push (cons (- i (nth 2 line)) (car line)) (cdr pair))))
+ (setq i (1+ i))))
+ (let ((newline-at-display t))
+ (dolist (block line-map)
+ (let ((ov (make-overlay (caar block) (cdar block))))
+ (push ov (window-parameter nil 'popon-overlays))
+ (overlay-put ov 'display (if newline-at-display "\n" ""))
+ (overlay-put
+ ov 'before-string
+ (let ((text "")
+ (current-offset 0))
+ (dolist (line (sort (cdr block) #'car-less-than-car))
+ (setq text (concat text
+ (make-string (- (car line)
+ current-offset)
+ ?\n)
+ (cdr line)))
+ (setq current-offset (car line)))
+ (add-face-text-property 0 (length text) 'default 'append text)
+ (concat text (unless newline-at-display "\n"))))
+ (overlay-put ov 'window (selected-window))
+ (setq newline-at-display (not newline-at-display)))))))
+
+(defun popon--redisplay-1 (force)
+ "Redisplay popon overlays.
+
+When FORCE is non-nil, update all overlays."
+ (let ((popon-available-p nil)
+ (any-popon-visible-p nil))
+ (dolist (frame (frame-list))
+ (dolist (window (window-list frame))
+ (set-window-parameter
+ window 'popon-list
+ (cl-remove-if-not #'popon-live-p
+ (window-parameter window 'popon-list)))
+ (when (window-parameter window 'popon-list)
+ (setq popon-available-p t))
+ (let ((popons
+ (cl-remove-if-not
+ (lambda (popon)
+ (and (or (null (popon-buffer popon))
+ (eq (popon-buffer popon) (window-buffer)))
+ (< (cdr (popon-position popon))
+ (with-selected-window window
+ (floor (window-screen-lines))))
+ (< (car (popon-position popon))
+ (- (window-width window)
+ (with-selected-window window
+ (line-number-display-width))))))
+ (window-parameter window 'popon-list))))
+ (when (or force
+ (not
+ (and
+ (null (cl-set-difference
+ popons
+ (window-parameter window
+ 'popon-visible-popons)))
+ (null (cl-set-difference
+ (window-parameter window
+ 'popon-visible-popons)
+ popons))
+ (eq (window-parameter window 'popon-window-start)
+ (window-start window))
+ (eq (window-parameter window 'popon-window-hscroll)
+ (window-hscroll window))
+ (eq (window-parameter window 'popon-window-buffer)
+ (window-buffer window)))))
+ (while (window-parameter window 'popon-overlays)
+ (delete-overlay (pop (window-parameter window
+ 'popon-overlays))))
+ (with-selected-window window
+ (let* ((framebuffer (popon--make-framebuffer)))
+ (dolist (popon popons)
+ (popon--render popon framebuffer (window-hscroll)))
+ (popon--make-overlays framebuffer)))
+ (set-window-parameter window 'popon-visible-popons popons)
+ (set-window-parameter window 'popon-window-start
+ (window-start window))
+ (set-window-parameter window 'popon-window-hscroll
+ (window-hscroll window))
+ (set-window-parameter window 'popon-window-buffer
+ (window-buffer window))))
+ (when (window-parameter window 'popon-visible-popons)
+ (setq any-popon-visible-p t))))
+ (if any-popon-visible-p
+ (add-hook 'pre-redisplay-functions #'popon--pre-redisplay)
+ (remove-hook 'pre-redisplay-functions #'popon--pre-redisplay))
+ (if popon-available-p
+ (add-hook 'window-configuration-change-hook #'popon-update)
+ (remove-hook 'window-configuration-change-hook #'popon-update))))
+
+(defun popon-redisplay ()
+ "Redisplay popon overlays."
+ (popon--redisplay-1 t))
+
+(defun popon-update ()
+ "Update popons if needed."
+ (popon--redisplay-1 nil))
+
+(defun popon--pre-redisplay (_)
+ "Update popons."
+ (popon-update))
+
+(defun popon-x-y-at-pos (point)
+ "Return the (X, Y) coodinate of POINT in selected window as a cons cell.
+
+Return nil if POINT is not in visible text area.
+
+NOTE: This uses `posn-at-point', which is slow. So try to minimize calls
+to this function."
+ (let ((window-start-x-y (posn-col-row (posn-at-point (window-start))))
+ (point-x-y (posn-col-row (posn-at-point point))))
+ (cons (- (car point-x-y) (car window-start-x-y))
+ (- (cdr point-x-y) (cdr window-start-x-y)))))
+
+(defun popon-kill-all ()
+ "Kill all popons."
+ (interactive)
+ (dolist (frame (frame-list))
+ (dolist (window (window-list frame))
+ (while (window-parameter window 'popon-list)
+ (popon-kill (pop (window-parameter window 'popon-list)))))))
+
+(provide 'popon)
+;;; popon.el ends here
- [nongnu] branch elpa/popon created (now aed8ff598d), ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon cbcfb8d978 03/15: Fix problem with buffer without final newline, ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon 6d7ee4f32b 07/15: Don't mess up line numbers, ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon 651b648420 06/15: Fix popon breaking on buffers without final newline, ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon 95c1a57d08 08/15: Fix #1 newline background at end of line, ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon 88d2d3ca2a 09/15: Revert "Fix #1 newline background at end of line", ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon e4d9123e0b 13/15: Fix popon beyond EOB appearing at wrong position, ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon 7e6a5d6ec6 02/15: Add COPYING, ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon 7a359994ff 05/15: Add autoload cookies, ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon 240fb286d8 01/15: Working implementation,
ELPA Syncer <=
- [nongnu] elpa/popon e6e2e8dff0 10/15: Revert "Revert "Fix #1 newline background at end of line"", ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon aed8ff598d 15/15: Take the fringe width into account, ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon c34dd56fbd 04/15: Add installation instruction, ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon 6c1311600a 11/15: Lower required Emacs version, ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon 22e76f812c 14/15: Fix Eshell popon error, ELPA Syncer, 2022/05/22
- [nongnu] elpa/popon 61b54bea8c 12/15: Fix popon breaking when long lines aren't truncated, ELPA Syncer, 2022/05/22