summaryrefslogtreecommitdiffhomepage
path: root/lib
diff options
context:
space:
mode:
authorjao <jao@gnu.org>2021-02-02 05:16:17 +0000
committerjao <jao@gnu.org>2021-02-02 05:16:17 +0000
commit771abb84830678455de4625ac7f082d8100f0ea0 (patch)
tree0d303c2cb0861b949ca73a9705954f6a69c4f877 /lib
parent81eceb5507aa0659e9f0c9761e54e9102085c4ac (diff)
downloadelibs-771abb84830678455de4625ac7f082d8100f0ea0.tar.gz
elibs-771abb84830678455de4625ac7f082d8100f0ea0.tar.bz2
libs -> lib/
Diffstat (limited to 'lib')
-rw-r--r--lib/bmk/bmk-mgr-w3.el58
-rw-r--r--lib/bmk/bmk-mgr-w3m.el84
-rw-r--r--lib/bmk/bmk-mgr.el1478
-rw-r--r--lib/bmk/dot-emacs.el42
-rw-r--r--lib/bmk/folder-closed.xpm31
-rw-r--r--lib/bmk/folder-open.xpm39
-rw-r--r--lib/bmk/url-alt.xpm31
-rw-r--r--lib/bmk/url.xpm39
-rw-r--r--lib/doc/jao-counsel-recoll.el60
-rw-r--r--lib/doc/jao-doc-view.el153
-rw-r--r--lib/doc/jao-recoll.el82
-rw-r--r--lib/eos/jao-afio.el212
-rw-r--r--lib/eos/jao-ednc.el148
-rw-r--r--lib/eos/jao-embark-targets.el97
-rw-r--r--lib/eos/jao-minibuffer.el138
-rw-r--r--lib/eos/jao-notify.el33
-rw-r--r--lib/eos/jao-osd.el55
-rw-r--r--lib/eos/jao-sleep.el58
-rw-r--r--lib/media/espotify.org627
-rw-r--r--lib/media/jao-emms-info-track.el212
-rw-r--r--lib/media/jao-emms-lyrics.el41
-rw-r--r--lib/media/jao-emms-random-album.el118
-rw-r--r--lib/media/jao-emms.el27
-rw-r--r--lib/media/jao-lyrics.el152
-rw-r--r--lib/media/jao-mpris.el139
-rw-r--r--lib/media/jao-random-album.el101
-rwxr-xr-xlib/media/leoslyrics.py84
-rwxr-xr-xlib/media/lyricwiki.rb52
-rw-r--r--lib/net/jao-frm.el222
-rw-r--r--lib/net/jao-maildir.el155
-rw-r--r--lib/net/jao-proton-utils.el131
-rw-r--r--lib/net/randomsig.el724
-rw-r--r--lib/net/signel.org546
-rw-r--r--lib/org/jao-org-gnus.el72
-rw-r--r--lib/org/jao-org-links.el147
-rw-r--r--lib/org/jao-org-notes.el79
-rw-r--r--lib/org/jao-org-popup.el31
-rw-r--r--lib/org/jao-org-utils.el43
-rw-r--r--lib/prog/jao-compilation.el118
-rw-r--r--lib/prog/jao-sloc.el33
-rw-r--r--lib/prog/jao-vterm-repl.el130
-rw-r--r--lib/readme.org19
-rw-r--r--lib/themes/jao-dark-blue-theme.el100
-rw-r--r--lib/themes/jao-dark-forest-theme.el131
-rw-r--r--lib/themes/jao-dark-theme.el77
-rw-r--r--lib/themes/jao-doom-theme.el57
-rw-r--r--lib/themes/jao-doomish-theme.el152
-rw-r--r--lib/themes/jao-greenish-theme.el114
-rw-r--r--lib/themes/jao-light-theme.el111
-rw-r--r--lib/themes/jao-mono-dark-theme.el98
-rw-r--r--lib/themes/jao-themes.el1099
-rw-r--r--lib/themes/jao-zenburn-theme.el132
52 files changed, 8912 insertions, 0 deletions
diff --git a/lib/bmk/bmk-mgr-w3.el b/lib/bmk/bmk-mgr-w3.el
new file mode 100644
index 0000000..c22700f
--- /dev/null
+++ b/lib/bmk/bmk-mgr-w3.el
@@ -0,0 +1,58 @@
+;;; bmk-mgr-w3.el --- w3 specific code for bmk-mgr
+
+;; Copyright (C) 2007, 2008 Jose Antonio Ortega Ruiz.
+;;
+;; Author: Robert D. Crawford
+;; Author: Jose A Ortega Ruiz <jao@gnu.org>
+;; Keywords: hypermedia
+
+;; 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 file 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:
+;;
+;; Set up bmk-mgr for w3.
+
+;;; Code:
+
+;;;; Dependencies:
+
+(require 'bmk-mgr)
+(require 'w3)
+
+(defun bmk-mgr-w3-current-url ()
+ "Returns the current document url
+without the string properties."
+ (interactive)
+ (substring-no-properties (url-view-url)))
+
+(defun bmk-mgr-w3-document-title-fixed ()
+ "Removes the newline in long titles that
+seems to have cropped up in current versions of w3."
+ (replace-regexp-in-string "\n" " " (buffer-name)))
+
+(add-hook 'w3-mode-hook
+ (lambda ()
+ (setq bmk-mgr-document-title
+ 'bmk-mgr-w3-document-title-fixed)
+ (setq bmk-mgr-url-at-point 'w3-view-this-url)
+ (setq bmk-mgr-current-url 'bmk-mgr-w3-current-url)))
+;; (setq bmk-mgr-document-title 'buffer-name)
+(provide 'bmk-mgr-w3)
+
+;; Local variables **
+;; indent-tabs-mode: nil **
+;; end **
+;;; bmk-mgr-w3.el ends here
diff --git a/lib/bmk/bmk-mgr-w3m.el b/lib/bmk/bmk-mgr-w3m.el
new file mode 100644
index 0000000..cc53d41
--- /dev/null
+++ b/lib/bmk/bmk-mgr-w3m.el
@@ -0,0 +1,84 @@
+;;; bmk-mgr-w3m.el --- w3m specific code for bmk-mgr
+
+;; Copyright (C) 2007 Jose Antonio Ortega Ruiz.
+;;
+;; Author: Robert D. Crawford
+;; Author: Jose A Ortega Ruiz <jao@gnu.org>
+;; Keywords: hypermedia
+
+;; 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 2, or (at your option)
+;; any later version.
+
+;; This file 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:
+;;
+;; Set up bmk-mgr for w3m.
+
+;;; Code:
+
+;;;; Dependencies:
+
+(require 'bmk-mgr)
+(require 'w3m)
+
+(defsubst bmk-mgr-w3m-url-at-point ()
+ "Return the url at point in w3m."
+ (or (w3m-anchor (point)) (w3m-image (point))))
+
+(defsubst bmk-mgr-w3m-current-url ()
+ "Returns the value of w3m-current-url."
+ w3m-current-url)
+
+(add-hook 'w3m-fontify-after-hook
+ (lambda ()
+ (setq bmk-mgr-document-title 'w3m-current-title)
+ (setq bmk-mgr-url-at-point 'bmk-mgr-w3m-url-at-point)
+ (setq bmk-mgr-current-url 'bmk-mgr-w3m-current-url)))
+
+(bmk-mgr-import-add-formatter "w3m" 'bmk-mgr-w3m-import)
+
+(defun bmk-mgr-w3m-import (file name)
+ (if (not (file-readable-p file)) (error "Cannot read file"))
+ (with-temp-buffer
+ (let ((result (bmk-mgr-node-folder-new (or name "w3m")))
+ (coding-system-for-read
+ (if (boundp 'w3m-bookmark-file-coding-system)
+ w3m-bookmark-file-coding-system
+ coding-system-for-read))
+ (sec-delim (if (boundp 'w3m-bookmark-section-delimiter)
+ w3m-bookmark-section-delimiter
+ "<!--End of section (do not delete this comment)-->\n")))
+ (insert-file-contents file)
+ (goto-char 1)
+ (while (re-search-forward "<h2>\\([^<]+\\)</h2>\n<ul>\n" nil t)
+ (let* ((folder
+ (bmk-mgr-node-folder-new (match-string 1) t))
+ (limit
+ (save-excursion
+ (and (search-forward sec-delim nil t) (point)))))
+ (while (search-forward "<li><a href=\"" limit t)
+ (if (re-search-forward "\\([^\"]+\\)\">\\([^<]+\\)</a>\n" nil t)
+ (bmk-mgr-node-add-child
+ folder
+ (bmk-mgr-node-url-new (match-string 2) (match-string 1)))))
+ (bmk-mgr-node-add-child result folder)))
+ result)))
+
+(provide 'bmk-mgr-w3m)
+
+;; Local variables **
+;; indent-tabs-mode: nil **
+;; end **
+
+;;; bmk-mgr-w3m.el ends here
diff --git a/lib/bmk/bmk-mgr.el b/lib/bmk/bmk-mgr.el
new file mode 100644
index 0000000..eab1844
--- /dev/null
+++ b/lib/bmk/bmk-mgr.el
@@ -0,0 +1,1478 @@
+;;; bmk-mgr.el --- Bookmark manager:
+
+;; Copyright (C) 2003, 2004, 2006, 2007, 2020 Jose Antonio Ortega Ruiz.
+;;
+
+(defconst bmk-mgr-version "0.1.2")
+
+;; Author: Jose A Ortega Ruiz <jao@gnu.org>
+;; Keywords: hypermedia
+
+;; 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 2, or (at your option)
+;; any later version.
+
+;; This file 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:
+
+;;;; INTRODUCTION:
+;;;; Emacs Bookmark Manager.
+;;;;
+;;;; INSTALLATION:
+;;;;
+;;;; CUSTOMIZATION:
+;;;;
+;;;; HISTORY:
+;;;; - 0.1.1 (May 2006). XBEL importing corrected.
+;;;;
+;;;; TODO:
+;;;; - Export: xbel, HTML, bmk
+;;;; - Add menu: display bookmarks as a menu
+;;;;
+;;;; THANKS:
+;;;; - David Magill, for lots of help in debugging.
+;;;;
+
+;;; Code:
+
+;;;; Dependencies:
+
+(require 'cl)
+(require 'outline)
+(require 'browse-url)
+
+;;;; Compatibility:
+(if (< emacs-major-version 22)
+ (progn
+ (defun substring-no-properties (x) x)
+ (defsubst bmk-string-to-int (x) (string-to-int x)))
+ (progn
+ (defsubst bmk-string-to-int (x) (string-to-number x))))
+
+;;;; Customization:
+
+;;;;; Customization buffer:
+(defgroup bmk-mgr nil
+ "Bookmark manager"
+ :group 'hypermedia
+ :prefix "bmk-mgr-")
+
+(defcustom bmk-mgr-bookmark-file "~/.emacs.bookmarks"
+ "The file where bookmarks are stored."
+ :group 'bmk-mgr
+ :type 'file)
+
+(defcustom bmk-mgr-autosave t
+ "If on, save bookmarks whenever they are modified."
+ :group 'bmk-mgr
+ :type 'boolean)
+
+(defcustom bmk-mgr-indent-width 2
+ "The amount of indentation for evey new subfolder level."
+ :group 'bmk-mgr
+ :type 'number)
+
+(defcustom bmk-mgr-link-mark ""
+ "The string used to prefix link names."
+ :group 'bmk-mgr
+ :type 'string)
+
+(defcustom bmk-mgr-open-mark "- "
+ "The string used to prefix open folder names."
+ :group 'bmk-mgr
+ :type 'string)
+
+(defcustom bmk-mgr-closed-mark "+ "
+ "The string used to prefix closed folder names."
+ :group 'bmk-mgr
+ :type 'string)
+
+(defconst bmk-mgr-available-browsers
+ '(choice
+ (function-item :tag "Default" :value nil)
+ (function-item :tag "Emacs W3" :value browse-url-w3)
+ (function-item :tag "W3 in another Emacs via `gnudoit'"
+ :value browse-url-w3-gnudoit)
+ (function-item :tag "Mozilla" :value browse-url-mozilla)
+ (function-item :tag "Galeon" :value browse-url-galeon)
+ (function-item :tag "Netscape" :value browse-url-netscape)
+ (function-item :tag "Mosaic" :value browse-url-mosaic)
+ (function-item :tag "Mosaic using CCI" :value browse-url-cci)
+ (function-item :tag "IXI Mosaic" :value browse-url-iximosaic)
+ (function-item :tag "Lynx in an xterm window"
+ :value browse-url-lynx-xterm)
+ (function-item :tag "Lynx in an Emacs window"
+ :value browse-url-lynx-emacs)
+ (function-item :tag "Grail" :value browse-url-grail)
+ (function-item :tag "MMM" :value browse-url-mmm)
+ (function-item :tag "KDE" :value browse-url-kde)
+ (function-item :tag "Specified by `Browse Url Generic Program'"
+ :value browse-url-generic)
+ (function-item :tag "Default Windows browser"
+ :value browse-url-default-windows-browser)
+ (function-item :tag "GNOME invoking Mozilla"
+ :value browse-url-gnome-moz)
+ (function-item :tag "Default browser"
+ :value browse-url-default-browser)
+ (function :tag "Your own function")
+ (alist :tag "Regexp/function association list"
+ :key-type regexp :value-type function)))
+
+(defcustom bmk-mgr-browser-function nil
+ "*Function to display the current bookmark in a WWW browser.
+
+This has the same semantics as `browse-url''s `browse-url-browser-function'.
+If you set this variable to nil, the latter will be used. Otherwise,
+if the value is not a function it should be a list of pairs
+\(REGEXP . FUNCTION). In this case the function called will be the one
+associated with the first REGEXP which matches the current URL. The
+function is passed the URL and any other args of `browse-url'. The last
+regexp should probably be \".\" to specify a default browser."
+ :type bmk-mgr-available-browsers
+ :group 'bmk-mgr)
+
+(defcustom bmk-mgr-alt-browser-function nil
+ "Alternative function to display the current bookmark in a WWW browser.
+
+This has the same semantics as `bmk-mgr-browser-function'. You can use
+it to have a second browsing function available (activated by pressing
+`shift-return' instead of just `return'). A typical application is to
+have one to display the bookmark in the current tab, and another to
+display the bookmark in a new tab."
+ :type bmk-mgr-available-browsers
+ :group 'bmk-mgr)
+
+
+(defcustom bmk-mgr-inhibit-welcome-message nil
+ "When on, do not display a welcome message in the minibuffer upon
+entering the bookmark manager."
+ :group 'bmk-mgr
+ :type 'boolean)
+
+(defcustom bmk-mgr-inhibit-minibuffer nil
+ "When on, do not automatically display info about the current folder
+or bookmark in the minibuffer."
+ :group 'bmk-mgr
+ :type 'boolean)
+
+(defcustom bmk-mgr-ignore-fold-state nil
+ "Turn this variable on to display the initial tree with all
+subfolders closed, instead of using their last state."
+ :group 'bmk-mgr
+ :type 'boolean)
+
+(defcustom bmk-mgr-use-images nil
+ "If on, images are used by default."
+ :type 'boolean
+ :group 'bmk-mgr)
+
+(defcustom bmk-mgr-folder-open-image "folder-open.xpm"
+ "Image to use for representing open folders."
+ :type 'file
+ :group 'bmk-mgr)
+
+(defcustom bmk-mgr-folder-closed-image "folder-closed.xpm"
+ "Image to use for representing closed folders."
+ :type 'file
+ :group 'bmk-mgr)
+
+(defcustom bmk-mgr-bookmark-image "url.xpm"
+ "Image to use for representing bookmarks."
+ :type 'file
+ :group 'bmk-mgr)
+
+(defcustom bmk-mgr-use-own-frame nil
+ "Whether the bookmars buffer should be displayed on its own frame."
+ :type 'boolean
+ :group 'bmk-mgr)
+
+(defcustom bmk-mgr-frame-parameters '((width . 60))
+ "Parameters of the bookmars buffer frame, when
+`bmk-mgr-use-own-frame' has been set to non-nil"
+ :type '(repeat (sexp :tag "Parameter:"))
+ :group 'bmk-mgr)
+
+(defface bmk-mgr-folder-face '((t (:bold t :foreground nil :weight bold)))
+ "Face for folder names."
+ :group 'bmk-mgr)
+
+(defface bmk-mgr-sel-folder-face
+ '((t (:bold t :foreground "IndianRed" :weight bold)))
+ "Face for selected folder names."
+ :group 'bmk-mgr)
+
+(defface bmk-mgr-bookmark-face '((t ()))
+ "Face for bookmark names."
+ :group 'bmk-mgr)
+
+(defface bmk-mgr-sel-bookmark-face '((t (:foreground "IndianRed")))
+ "Face for selected bookmark names."
+ :group 'bmk-mgr)
+
+;;;;; Other variables:
+
+(defvar bmk-mgr-bookmark-buffer-name "*Bookmarks*"
+ "*Name of the bookmarks buffer.")
+
+(defvar bmk-mgr-kill-ring-size 50
+ "*Maximum number of killed bookmarks to be remembered.")
+
+(defvar bmk-mgr-line-spacing 2
+ "*Additional space to put between lines when displaying the
+bookmarks buffer.
+
+The space is measured in pixels, and put below lines on window
+systems.")
+
+(defvar bmk-mgr-document-title nil
+ "Function variable returning the current document title.")
+
+(defvar bmk-mgr-url-at-point nil
+ "Function variable returning the value of the url under point.")
+
+(defvar bmk-mgr-current-url nil
+ "Function variable returning the value of the current document url.")
+
+(make-variable-buffer-local 'bmk-mgr-document-title)
+(make-variable-buffer-local 'bmk-mgr-url-at-point)
+(make-variable-buffer-local 'bmk-mgr-current-url)
+
+;;;; User interactive functions:
+
+(defun bmk-mgr-create-bookmark-buffer ()
+ (let ((tree (bmk-mgr-read-from-file bmk-mgr-bookmark-file)))
+ (when tree
+ (when bmk-mgr-use-own-frame
+ (select-frame (make-frame bmk-mgr-frame-parameters)))
+ (switch-to-buffer
+ (get-buffer-create bmk-mgr-bookmark-buffer-name))
+ (bmk-mgr-mode tree)
+ (current-buffer))))
+
+(defsubst bmk-mgr-get-bookmark-buffer ()
+ (or (get-buffer bmk-mgr-bookmark-buffer-name)
+ (bmk-mgr-create-bookmark-buffer)))
+
+(defun bmk-mgr-show-bookmarks ()
+ "Display the bookmarks buffer."
+ (interactive)
+ (let ((display-buffer-reuse-frames bmk-mgr-use-own-frame)
+ (pop-up-frames bmk-mgr-use-own-frame))
+ (switch-to-buffer (bmk-mgr-get-bookmark-buffer))))
+
+(defun bmk-mgr-show-bookmarks-other-window ()
+ "Display the bookmarks buffer in other window"
+ (interactive)
+ (let ((display-buffer-reuse-frames nil)
+ (pop-up-frames nil))
+ (split-window-horizontally (/ (* 2 (window-width)) 3))
+ (other-window 1)
+ (switch-to-buffer (bmk-mgr-get-bookmark-buffer))))
+
+(defun bmk-mgr-add-url-at-point ()
+ "Add URL at point to the bookmarks collection.
+If there is no URL at point, this command asks for it."
+ (interactive)
+ (if bmk-mgr-url-at-point
+ (bmk-mgr-add-bookmark-at-folder (funcall bmk-mgr-url-at-point))
+ (progn
+ (require 'ffap)
+ (bmk-mgr-add-bookmark-at-folder (ffap-url-at-point)))))
+
+;; the following 2 functions need to be combined and generalized
+(defun bmk-mgr-add-current-page ()
+ "Adds the current page to the bookmark list."
+ (interactive)
+ (unless bmk-mgr-current-url
+ (error "Current buffer has no associated URL."))
+ ;; please leave these here, as I will need them later -- rdc
+ ;; (message "bmk-mgr-current-url value as function is %s"
+ ;; bmk-mgr-current-url)
+ ;; (message "bmk-mgr-current-url value as variable is %s"
+ ;; (funcall bmk-mgr-current-url))
+ ;; (message "bmk-mgr-document-title value as function is %s"
+ ;; bmk-mgr-document-title)
+ ;; (message "bmk-mgr-document-title value as variable is %s"
+ ;; (funcall bmk-mgr-document-title))
+ (bmk-mgr-add-bookmark-at-folder
+ (funcall bmk-mgr-current-url)
+ (funcall bmk-mgr-document-title)))
+
+;;;; Bookmark mode:
+
+;;;;; Variables:
+
+(defvar bmk-mgr-kill-ring nil "Killed nodes list")
+
+(defmacro bmk-mgr-folder-or-url (ffun ufun)
+ `(lambda ()
+ (interactive)
+ (if (bmk-mgr-node-folder-p (bmk-mgr-get-node-at-point))
+ (funcall ',ffun)
+ (funcall ',ufun))))
+
+(defvar bmk-mgr-mode-map
+ (let ((map (make-keymap)))
+ (suppress-keymap map)
+ (define-key map [(control ?a)] 'beginning-of-line)
+ (define-key map [(control ?e)] 'end-of-line)
+ (define-key map [(control ?k)] 'bmk-mgr-kill-bookmark)
+ (define-key map [(control ?n)] 'bmk-mgr-next-line)
+ (define-key map [(control ?p)] 'bmk-mgr-previous-line)
+ (define-key map [(control ?y)] 'bmk-mgr-yank-bookmark)
+ (define-key map [??] 'describe-mode)
+ (define-key map [?A] 'bmk-mgr-add-folder)
+ (define-key map [?I] 'bmk-mgr-toggle-images)
+ (define-key map [?N] 'bmk-mgr-next-folder)
+ (define-key map [?P] 'bmk-mgr-previous-folder)
+ (define-key map [?Q] 'bmk-mgr-quit)
+ (define-key map [?V] 'bmk-mgr-version)
+ (define-key map [?a] 'bmk-mgr-add-bookmark)
+ (define-key map [?c] 'bmk-mgr-close-children)
+ (define-key map [?d] 'bmk-mgr-move-bookmark-down)
+ (define-key map [?e] 'bmk-mgr-edit-bookmark)
+ (define-key map [?f] 'bmk-mgr-find-folder)
+ (define-key map [?h] 'describe-mode)
+ (define-key map [?i] 'bmk-mgr-import)
+ (define-key map [?n] 'bmk-mgr-next-line)
+ (define-key map [?p] 'bmk-mgr-previous-line)
+ (define-key map [?q] 'bmk-mgr-quit-ask)
+ (define-key map [?s] 'bmk-mgr-save-bookmarks)
+ (define-key map [?u] 'bmk-mgr-move-bookmark-up)
+ (define-key map [?v] 'bmk-mgr-bookmark-info)
+ (define-key map [?y] 'bmk-mgr-copy-url)
+ (define-key map (kbd "<up>") 'bmk-mgr-previous-line)
+ (define-key map (kbd "<down>") 'bmk-mgr-next-line)
+ (define-key map (kbd "<left>") 'beginning-of-line)
+ (define-key map (kbd "<right>") 'end-of-line)
+ (define-key map (kbd "<mouse-1>") 'bmk-mgr-mouse-click)
+ (define-key map (kbd "<mouse-2>") 'bmk-mgr-mouse-click-alt)
+ (define-key map (kbd "<S-return>") 'bmk-mgr-browse-url-alt)
+ (define-key map (kbd "M-RET") 'bmk-mgr-browse-url-alt)
+ (define-key map (kbd "RET") 'bmk-mgr-browse-url)
+ (define-key map (kbd "TAB") 'bmk-mgr-toggle-folder)
+ map)
+ "Keymap for `bmk-mgr-mode'.")
+
+(defvar bmk-mgr-mode-syntax-table
+ (let ((st (make-syntax-table)))
+ st)
+ "Syntax table for `bmk-mgr-mode'.")
+
+;; regexps used by bmk-mgr-mode and other functions
+(defvar bmk-mgr-outline-regexp nil)
+
+;; images
+(defvar bmk-mgr-url-img)
+(defvar bmk-mgr-fopen-img)
+(defvar bmk-mgr-fclosed-img)
+
+;;;;; Mode definition:
+
+;;;###autoload
+(defun bmk-mgr-mode (&optional tree)
+ "\\<bmk-mgr-mode-map>
+ Major mode for displaying bookmark files.
+
+Commands:
+
+<DIGIT>+<key>\tRepeat command denoted by <key> the number of times
+ \tpreviously typed. Commands accepting a prefix count are
+ \tmarked with (*) below.
+
+\\[bmk-mgr-next-line]\tGo to next visible line (*).
+\\[bmk-mgr-previous-line]\tGo to previous visible line (*).
+\\[bmk-mgr-next-folder]\tGo to next visible folder (*).
+\\[bmk-mgr-previous-folder]\tGo to previous visible folder (*).
+\\[beginning-of-line]\tGo to the beginning of text in current line.
+\\[end-of-line]\tGo to the end of text in current line.
+\\[bmk-mgr-toggle-folder]\tOpens or closes current folder.
+\\[bmk-mgr-close-children]\tCloses all subfolders of current folder.
+\\[bmk-mgr-bookmark-info]\tDisplay info about current bookmark or folder.
+\\[bmk-mgr-copy-url]\tPut the current URL (if any) in the kill ring.
+\\[bmk-mgr-find-folder]\tFind bookmarks folder.
+
+\\[bmk-mgr-mouse-click]\tDisplay or toggle the clicked URL or folder.
+\\[bmk-mgr-mouse-click-alt]\tDisplay or toggle the clicked URL or folder,
+ \tusing the alternate browser.
+\\[bmk-mgr-browse-url]\tDisplay current URL in browser.
+\\[bmk-mgr-browse-url-alt]\tDisplay current URL in alternate browser.
+
+\\[bmk-mgr-move-bookmark-up]\tMoves current bookmark one line up (*).
+\\[bmk-mgr-move-bookmark-down]\tMoves current bookmark one line down (*).
+\\[bmk-mgr-edit-bookmark]\tEdit bookmark or folder in current line.
+\\[bmk-mgr-add-bookmark]\tAdd a new bookmark (asks for its name and URL).
+\\[bmk-mgr-add-folder]\tAdd a new bookmark folder (asks for its path).
+\\[bmk-mgr-kill-bookmark]\tKills current bookmark or folder, putting it in the kill ring (*).
+\\[bmk-mgr-yank-bookmark]\tYanks a previously killed bookmark or folder (*).
+
+\\[bmk-mgr-import]\tImports an external bookmarks file (xbel, w3m, bmk).
+
+\\[bmk-mgr-toggle-images]\tToggle display of images.
+
+\\[bmk-mgr-save-bookmarks]\tSave current bookmarks.
+\\[bmk-mgr-quit]\tQuit Bookmark Manager.
+\\[bmk-mgr-quit-ask]\tQuit Bookmark Manager asking for confirmation.
+
+\\[bmk-mgr-version]\tShow version.
+\\[describe-mode]\tShows this help page.
+"
+ (kill-all-local-variables)
+ (buffer-disable-undo)
+ (setq major-mode 'bmk-mgr-mode)
+ (setq mode-name "bmk")
+ (use-local-map bmk-mgr-mode-map)
+
+ (let ((prefix (make-string bmk-mgr-indent-width 32)))
+ (setq bmk-mgr-outline-regexp (concat "\\(" prefix "\\)*.")))
+ (set (make-local-variable 'outline-regexp) bmk-mgr-outline-regexp)
+ (set (make-local-variable 'truncate-lines) t)
+ (set (make-local-variable 'automatic-hscrolling) t)
+ (set (make-local-variable 'line-spacing) bmk-mgr-line-spacing)
+ (set (make-local-variable 'kill-whole-line) t)
+ (set (make-local-variable 'next-line-add-newlines) t)
+ (goto-char 1)
+ (bmk-mgr-refresh tree)
+ (toggle-read-only 1)
+ (unless bmk-mgr-inhibit-welcome-message
+ (message
+ "Emacs Bookmark Manager, version %s. Type `h' for help." bmk-mgr-version)))
+
+;;;;; Functions:
+
+;;;;;; Helper macros:
+(defmacro bmk-mgr-with-bookmarks-buffer (&rest body)
+ `(with-current-buffer (bmk-mgr-get-bookmark-buffer)
+ (unwind-protect
+ (prog1
+ (let ((inhibit-read-only t))
+ (bmk-mgr-unmark-current)
+ ,@body)
+ (if (not bmk-mgr-inhibit-minibuffer) (bmk-mgr-bookmark-info)))
+ (bmk-mgr-mark-current))))
+
+(defmacro bmk-mgr-with-current-node (&rest body)
+ `(bmk-mgr-with-bookmarks-buffer
+ (beginning-of-line)
+ (let ((bmk-node (bmk-mgr-get-node-at-point))
+ (bmk-path (bmk-mgr-get-path-at-point)))
+ ,@body)))
+
+(defmacro bmk-mgr-with-current-node-save (&rest body)
+ `(bmk-mgr-with-current-node
+ (prog1
+ (progn ,@body)
+ (if bmk-mgr-autosave
+ (progn
+ (bmk-mgr-save-current-tree)
+ (set-buffer-modified-p nil))))))
+
+(defmacro bmk-mgr-repeat (&rest body)
+ `(let ((count bmk-mgr-repeat-count))
+ (while (> count 0)
+ (decf count)
+ ,@body)))
+
+;;;;;; Helper functions:
+(defun bmk-mgr-outline-level ()
+ (save-excursion
+ (beginning-of-line)
+ (if (looking-at bmk-mgr-outline-regexp)
+ (length (match-string 0))
+ 0)))
+
+(defun bmk-mgr-mark-current ()
+ (let* ((inhibit-read-only 1)
+ (node (bmk-mgr-get-node-at-point))
+ (face (if (bmk-mgr-node-folder-p node) 'bmk-mgr-sel-folder-face
+ 'bmk-mgr-sel-bookmark-face)))
+ (beginning-of-line)
+ (save-excursion
+ (add-text-properties (progn (bmk-mgr-beginning) (point))
+ (progn (end-of-line) (point))
+ `(face ,face)))))
+
+(defun bmk-mgr-unmark-current ()
+ (let* ((inhibit-read-only 1)
+ (node (bmk-mgr-get-node-at-point))
+ (face (if (bmk-mgr-node-folder-p node) 'bmk-mgr-folder-face
+ 'bmk-mgr-bookmark-face)))
+ (add-text-properties (progn (bmk-mgr-beginning) (point))
+ (save-excursion (end-of-line) (point))
+ `(face ,face))))
+
+(defun bmk-mgr-unmark-all ()
+ (save-excursion
+ (goto-char 1)
+ (while (not (eobp))
+ (bmk-mgr-unmark-current)
+ (next-line 1))))
+
+(defun bmk-mgr-push-to-kill-ring (node)
+ (push (copy-list node) bmk-mgr-kill-ring)
+ (when (> (length bmk-mgr-kill-ring) bmk-mgr-kill-ring-size)
+ (setcdr (nthcdr (1- bmk-mgr-kill-ring-size) bmk-mgr-kill-ring) nil)))
+
+(defsubst bmk-mgr-pop-kill-ring () (pop bmk-mgr-kill-ring))
+
+(defsubst bmk-mgr-url-at-point ()
+ "Get the URL of the current bookmark, if any."
+ (bmk-mgr-node-url (bmk-mgr-get-node-at-point)))
+
+(defun bmk-mgr-save-current-tree ()
+ (bmk-mgr-save-to-file (bmk-mgr-get-root-node-in-buffer)
+ bmk-mgr-bookmark-file))
+
+(defun bmk-mgr-ask-path (prompt &optional path)
+ (let ((PC-word-delimiters ".")
+ (bmk-mgr-inhibit-minibuffer t)
+ (path (or path (list (bmk-mgr-node-name
+ (bmk-mgr-get-root-node-in-buffer))))))
+ (bmk-mgr-string-to-path
+ (completing-read prompt 'bmk-mgr-complete-path nil nil
+ (concat (bmk-mgr-path-to-string path) "/")))))
+
+(defun bmk-mgr-complete-path (pstr fun flag)
+ (bmk-mgr-with-bookmarks-buffer
+ (let* ((root (bmk-mgr-get-root-node-in-buffer))
+ (partial (not (string-match "/$" pstr)))
+ (pc (split-string pstr "/"))
+ (path (or pc (list (bmk-mgr-node-name root))))
+ (ppath (if partial (bmk-mgr-path-parent path) path))
+ (partstr (concat "^" (regexp-quote (bmk-mgr-path-leaf path))))
+ (str (concat (bmk-mgr-path-to-string ppath) "/"))
+ (children (bmk-mgr-node-child-folders root ppath))
+ (comp (mapcar (lambda (x) (concat str (bmk-mgr-node-name x) "/"))
+ (if partial
+ (remove-if-not
+ (lambda (x)
+ (string-match partstr (bmk-mgr-node-name x)))
+ children)
+ children)))
+ (len (length comp)))
+ (case flag
+ ((nil) (cond
+ ((and (not partial) (zerop len)) t)
+ ((= len 1) (car comp))
+ ((zerop len) nil)
+ (t pstr)))
+ ((lambda) (not partial))
+ (t comp)))))
+
+;;;;;; Mode functions:
+
+(defun bmk-mgr-version ()
+ "Display version."
+ (interactive)
+ (message "Emacs Bookmark Manager, version %s" bmk-mgr-version))
+
+(defun bmk-mgr-toggle-images ()
+ "Toggle image display."
+ (interactive)
+ (setq bmk-mgr-use-images (not bmk-mgr-use-images))
+ (bmk-mgr-refresh))
+
+(defun bmk-mgr-refresh (&optional tree)
+ "Refresh the bookmarks buffer."
+ (interactive)
+ (message "Redisplaying bookmarks...")
+ (bmk-mgr-with-bookmarks-buffer
+ (if window-system
+ (progn
+ (clear-image-cache
+ (window-frame (get-buffer-window (current-buffer))))
+ (setq bmk-mgr-url-img
+ (find-image
+ `((:file ,bmk-mgr-bookmark-image :type xpm :ascent center))))
+ (setq bmk-mgr-fopen-img
+ (find-image
+ `((:file ,bmk-mgr-folder-open-image :type xpm :ascent 95))))
+ (setq bmk-mgr-fclosed-img
+ (find-image
+ `((:file ,bmk-mgr-folder-closed-image :type xpm :ascent 95)))))
+ (setq bmk-mgr-url-img nil bmk-mgr-fopen-img nil bmk-mgr-fclosed-img nil
+ bmk-mgr-use-images nil))
+ (let ((tree (or tree (bmk-mgr-get-root-node-in-buffer))))
+ (save-excursion
+ (erase-buffer)
+ (if bmk-mgr-ignore-fold-state (bmk-mgr-node-close-all-children tree))
+ (bmk-mgr-print-tree tree)
+ (goto-char 1)
+ (bmk-mgr-unmark-all)
+ (bmk-mgr-refresh-open-close)))
+ (message "Redisplaying bookmarks... done.")))
+
+(defsubst bmk-mgr-beginning ()
+ "Go to beginning of current bookmark."
+ (interactive)
+ (beginning-of-line)
+ (re-search-forward "^ *"))
+
+(defun bmk-mgr-next-line (arg)
+ "Go to next visible bookmark line."
+ (interactive "P")
+ (bmk-mgr-with-bookmarks-buffer
+ (outline-next-visible-heading (if arg (prefix-numeric-value arg) 1))
+ (if (eobp) (outline-previous-visible-heading 1))))
+
+(defun bmk-mgr-previous-line (arg)
+ "Go to previous visible bookmark line."
+ (interactive "P")
+ (bmk-mgr-with-bookmarks-buffer
+ (outline-previous-visible-heading (if arg (prefix-numeric-value arg) 1))))
+
+(defun bmk-mgr-bookmark-info ()
+ "Show info about current bookmark or folder."
+ (interactive)
+ (let ((node (bmk-mgr-get-node-at-point)))
+ (if node
+ (if (bmk-mgr-node-url-p node)
+ (let ((url (bmk-mgr-node-url node)))
+ (and url (message "%s" url)))
+ (let ((children (bmk-mgr-node-child-folders node)))
+ (if children
+ (message
+ "%s"
+ (concat "Subfolders: "
+ (mapconcat 'bmk-mgr-node-name children ", ")))))))))
+
+(defun bmk-mgr-copy-url ()
+ "Put current URL in the kill ring."
+ (interactive)
+ (bmk-mgr-with-current-node
+ (let ((url (bmk-mgr-node-url bmk-node)))
+ (when url
+ (kill-new url)
+ (message "%s copied" url)))))
+
+(defun bmk-mgr-next-folder (arg)
+ "Go to next visible bookmark folder."
+ (interactive "P")
+ (bmk-mgr-with-bookmarks-buffer
+ (let ((count (if arg (prefix-numeric-value arg) 1))
+ (test (lambda () (bmk-mgr-node-url-p (bmk-mgr-get-node-at-point)))))
+ (while (> count 0)
+ (decf count)
+ (if (not (funcall test)) (outline-next-visible-heading 1))
+ (while (funcall test)
+ (outline-next-visible-heading 1))))
+ (if (eobp) (outline-previous-visible-heading 1))
+ (bmk-mgr-beginning)))
+
+(defun bmk-mgr-previous-folder (arg)
+ "Go to previous visible bookmark folder."
+ (interactive "P")
+ (bmk-mgr-with-bookmarks-buffer
+ (let ((count (if arg (prefix-numeric-value arg) 1))
+ (test (lambda () (bmk-mgr-node-url-p (bmk-mgr-get-node-at-point)))))
+ (while (> count 0)
+ (decf count)
+ (if (not (funcall test)) (outline-previous-visible-heading 1))
+ (while (funcall test)
+ (outline-previous-visible-heading 1))))
+ (bmk-mgr-beginning)))
+
+(defun bmk-mgr-browse-url ()
+ "Display current bookmark in browser."
+ (interactive)
+ (let ((browse-url-browser-function
+ (or bmk-mgr-browser-function browse-url-browser-function))
+ (url (bmk-mgr-node-url (bmk-mgr-get-node-at-point))))
+ (if url (browse-url url) (bmk-mgr-toggle-folder))))
+
+(defun bmk-mgr-browse-url-alt ()
+ "Display current bookmark in alternate browser."
+ (interactive)
+ (bmk-mgr-with-current-node
+ (let ((browse-url-browser-function
+ (or bmk-mgr-alt-browser-function browse-url-browser-function))
+ (url (bmk-mgr-node-url (bmk-mgr-get-node-at-point))))
+ (if url (browse-url url) (bmk-mgr-toggle-folder)))))
+
+(defun bmk-mgr-mouse-click (event)
+ "Visit the clicked bookmark or toogle the folder state."
+ (interactive "e")
+ (set-buffer (bmk-mgr-get-bookmark-buffer))
+ (goto-char (posn-point (event-start event)))
+ (let ((node (bmk-mgr-get-node-at-point)))
+ (if (bmk-mgr-node-url-p node)
+ (bmk-mgr-browse-url)
+ (if (bmk-mgr-node-folder-p node)
+ (bmk-mgr-toggle-folder)))))
+
+(defun bmk-mgr-mouse-click-alt (event)
+ "Visit the clicked bookmark or toogle the folder state."
+ (interactive "e")
+ (set-buffer (bmk-mgr-get-bookmark-buffer))
+ (goto-char (posn-point (event-start event)))
+ (let ((node (bmk-mgr-get-node-at-point)))
+ (if (bmk-mgr-node-url-p node)
+ (bmk-mgr-browse-url-alt)
+ (if (bmk-mgr-node-folder-p node)
+ (bmk-mgr-toggle-folder)))))
+
+(defun bmk-mgr-toggle-folder ()
+ "Toggle the open/closed status of folder at point, if any."
+ (interactive)
+ (bmk-mgr-with-current-node
+ (when (bmk-mgr-node-folder-p bmk-node)
+ (bmk-mgr-node-toggle-open-closed bmk-node)
+ (bmk-mgr-redraw-node-at-point))))
+
+(defun bmk-mgr-close-children ()
+ "Close all subfolders of folder at point, if any."
+ (interactive)
+ (bmk-mgr-with-current-node
+ (when (bmk-mgr-node-folder-p bmk-node)
+ (bmk-mgr-node-close-all-children bmk-node)
+ (bmk-mgr-update-tree-at-point)
+ (bmk-mgr-refresh-open-close))))
+
+(defun bmk-mgr-find-folder ()
+ "Find a bookmarks folder."
+ (interactive)
+ (bmk-mgr-with-bookmarks-buffer
+ (bmk-mgr-find-path-in-buffer (bmk-mgr-ask-path "Find folder: ") t)))
+
+(defun bmk-mgr-save-bookmarks ()
+ "Save current bookmars."
+ (interactive)
+ (when (y-or-n-p "Save current bookmarks? ")
+ (with-current-buffer (bmk-mgr-get-bookmark-buffer)
+ (bmk-mgr-save-current-tree)
+ (set-buffer-modified-p nil))))
+
+(defun bmk-mgr-edit-bookmark ()
+ "Edit the current bookmark."
+ (interactive)
+ (bmk-mgr-with-current-node-save
+ (when bmk-node
+ (let ((newtitle (read-string "Name: " (bmk-mgr-node-title bmk-node))))
+ (if (> (length newtitle) 0) (bmk-mgr-node-set-name bmk-node newtitle))
+ (if (bmk-mgr-node-url-p bmk-node)
+ (let ((newurl (read-string "URL: " (bmk-mgr-node-url bmk-node))))
+ (if (> (length newurl) 0) (bmk-mgr-node-set-url bmk-node newurl))))
+ (bmk-mgr-redraw-node-at-point
+ (append (bmk-mgr-path-parent bmk-path) (list newtitle)))
+ (if (bmk-mgr-node-folder-p bmk-node) ; update children paths
+ (save-excursion
+ (let ((cl (bmk-mgr-outline-level))
+ (pos (length (bmk-mgr-path-parent bmk-path))))
+ (forward-line 1)
+ (while (> (bmk-mgr-outline-level) cl)
+ (setf (nth pos (bmk-mgr-get-path-at-point)) newtitle)
+ (forward-line 1)))))
+ (beginning-of-line)))))
+
+(defun bmk-mgr-add-bookmark-at-folder (&optional url title)
+ (let ((path
+ (bmk-mgr-with-current-node
+ (bmk-mgr-ask-path "Add bookmark to folder: "
+ (if (bmk-mgr-node-folder-p bmk-node) bmk-path
+ (bmk-mgr-path-parent bmk-path))))))
+ (bmk-mgr-add-bookmark path nil url title t)))
+
+
+(defun bmk-mgr-add-bookmark (&optional path node url title after)
+ "Insert bookmark at a given path or current point."
+ (interactive)
+ (bmk-mgr-with-current-node-save
+ (let* ((title (or title
+ (and node (bmk-mgr-node-name node))
+ (read-string "Name of new bookmark: ")))
+ (url (or (and node "") url (read-string "URL: ")))
+ (node (or node (bmk-mgr-node-url-new title url))))
+ (if (and path (not (bmk-mgr-find-path-in-buffer path t)))
+ (error "Folder %s does not exist"
+ (bmk-mgr-path-to-string path)))
+ (message "adding with path %S (%S)" path after)
+ (if (not (or path bmk-path)) (outline-previous-visible-heading 1))
+ (if (and (bmk-mgr-node-folder-p (bmk-mgr-get-node-at-point))
+ (bmk-mgr-node-open-p (bmk-mgr-get-node-at-point)))
+ (bmk-mgr-insert-child-at-point node (not after))
+ (bmk-mgr-insert-sibling-at-point node nil))
+ (while (not (or (eobp) (eq node (bmk-mgr-get-node-at-point))))
+ (outline-next-visible-heading 1)))))
+
+(defun bmk-mgr-add-folder ()
+ "Insert new bookmarks folder."
+ (interactive)
+ (bmk-mgr-with-current-node-save
+ (let* ((fpath (if (bmk-mgr-node-folder-p bmk-node) bmk-path
+ (bmk-mgr-path-parent bmk-path)))
+ (npath (bmk-mgr-ask-path "New folder: " fpath))
+ (pnpath (bmk-mgr-path-parent npath))
+ (sibling (and (not (equal bmk-path fpath)) ; inserting besides a url
+ (equal fpath pnpath)))) ; in the same folder
+ (if (and (not (equal fpath pnpath))
+ (not (bmk-mgr-find-path-in-buffer pnpath t)))
+ (error "Folder %s does not exist" (bmk-mgr-path-to-string pnpath)))
+ (let ((node (bmk-mgr-node-folder-new (bmk-mgr-path-leaf npath))))
+ (if sibling
+ (bmk-mgr-insert-sibling-at-point node nil)
+ (bmk-mgr-insert-child-at-point node t))
+ (bmk-mgr-goto-node-around node)))))
+
+(defun bmk-mgr-yank-bookmark (arg)
+ "Yank last killed bookmark at point."
+ (interactive "P")
+ (let ((count (if arg (prefix-numeric-value arg) 1)))
+ (while (> count 0)
+ (decf count)
+ (bmk-mgr-with-current-node-save
+ (bmk-mgr-add-bookmark nil (bmk-mgr-pop-kill-ring))))))
+
+(defun bmk-mgr-delete-node-at-point ()
+ (let ((path (bmk-mgr-get-path-at-point)))
+ (beginning-of-line)
+ (hide-subtree)
+ (let ((a (point))
+ (b (save-excursion (outline-next-visible-heading 1) (point))))
+ (if bmk-mgr-use-images (remove-images a b))
+ (delete-region a b)
+ (if (eobp) (outline-previous-visible-heading 1)))
+ (bmk-mgr-delete-node (bmk-mgr-get-root-node-in-buffer) path)))
+
+(defun bmk-mgr-kill-bookmark (arg)
+ "Delete bookmark at point."
+ (interactive "P")
+ (let ((count (if arg (prefix-numeric-value arg) 1)))
+ (while (> count 0)
+ (decf count)
+ (bmk-mgr-with-current-node-save
+ (if (not (bmk-mgr-path-parent bmk-path))
+ (error "Cannot kill root node"))
+ (if (and (bmk-mgr-node-folder-p bmk-node)
+ (not (null (bmk-mgr-node-children bmk-node)))
+ (not (y-or-n-p
+ (format
+ "Killing `%s' and all its contents. Are you sure? "
+ (bmk-mgr-path-leaf bmk-path)))))
+ (error "Cancelled"))
+ (bmk-mgr-push-to-kill-ring bmk-node)
+ (bmk-mgr-delete-node-at-point)))))
+
+(defun bmk-mgr-transpose-lines (node path count &optional up)
+ (beginning-of-line)
+ (outline-next-visible-heading (if up count (* -1 count)))
+ (let ((eol (save-excursion (end-of-line) (point))))
+ (if bmk-mgr-use-images (remove-images (point) eol))
+ (delete-region (point) (1+ eol))
+ (outline-next-visible-heading (if up (* -1 count) count))
+ (bmk-mgr-print-single-node-at-point node path t)))
+
+(defun bmk-mgr-goto-node-around (node &optional width)
+ (let ((width (or width 2)))
+ (outline-previous-visible-heading (1+ width))
+ (do ((max (1+ (* 2 width))) (n 0 (incf n)))
+ ((or (> n max) (eq node (bmk-mgr-get-node-at-point))))
+ (outline-next-visible-heading 1))))
+
+(defun bmk-mgr-move-bookmark-up (arg)
+ "Move bookmark at point one line up."
+ (interactive "P")
+ (bmk-mgr-with-current-node-save
+ (let ((ppath (bmk-mgr-path-parent bmk-path))
+ (count (if arg (prefix-numeric-value arg) 1)))
+ (when (and (> count 0)
+ (bmk-mgr-node-url-p bmk-node)
+ (> (length bmk-path) 1))
+ (beginning-of-line)
+ (let ((p (point)))
+ (outline-previous-visible-heading count)
+ (if (= (bmk-mgr-outline-level) 1)
+ (progn
+ (outline-next-visible-heading 1)
+ (when (not (equal bmk-node (bmk-mgr-get-node-at-point)))
+ (goto-char p)
+ (bmk-mgr-delete-node-at-point)
+ (goto-char 1)
+ (bmk-mgr-insert-child-at-point bmk-node t)))
+ (let* ((current (bmk-mgr-get-node-at-point))
+ (iscl (bmk-mgr-node-closed-p current))
+ (isurl (bmk-mgr-node-url-p current))
+ (cpath (bmk-mgr-get-path-at-point))
+ (cppath (bmk-mgr-path-parent cpath)))
+ (cond
+ ((and (equal ppath cppath) (or isurl iscl))
+ (bmk-mgr-node-swap-children-at-path
+ (bmk-mgr-get-root-node-in-buffer) ppath bmk-node current)
+ (bmk-mgr-transpose-lines bmk-node bmk-path count t))
+ (t
+ (outline-next-visible-heading count)
+ (bmk-mgr-delete-node-at-point)
+ (outline-previous-visible-heading count)
+ (if (or isurl iscl (equal cpath ppath))
+ (bmk-mgr-insert-sibling-at-point bmk-node
+ (equal cpath ppath))
+ (bmk-mgr-insert-child-at-point bmk-node nil)))))))
+ (bmk-mgr-goto-node-around bmk-node)))))
+
+(defun bmk-mgr-move-bookmark-down (arg)
+ "Move bookmark at point one line down."
+ (interactive "P")
+ (bmk-mgr-with-current-node-save
+ (let ((ppath (bmk-mgr-path-parent bmk-path))
+ (count (if arg (prefix-numeric-value arg) 1)))
+ (when (and (> count 0)
+ (bmk-mgr-node-url-p bmk-node)
+ (> (length bmk-path) 1))
+ (beginning-of-line)
+ (let ((p (point)))
+ (outline-next-visible-heading count)
+ (if (null (bmk-mgr-get-node-at-point))
+ (progn
+ (outline-previous-visible-heading 1)
+ (when (not (equal bmk-node (bmk-mgr-get-node-at-point)))
+ (goto-char p)
+ (bmk-mgr-delete-node-at-point)
+ (goto-char 1)
+ (bmk-mgr-insert-child-at-point bmk-node nil)
+ (goto-char (point-max))))
+ (let* ((current (bmk-mgr-get-node-at-point))
+ (iscl (bmk-mgr-node-closed-p current))
+ (isurl (bmk-mgr-node-url-p current))
+ (cpath (bmk-mgr-get-path-at-point))
+ (isout (< (length cpath) (length bmk-path)))
+ (cppath (bmk-mgr-path-parent cpath)))
+ (cond
+ ((and (equal ppath cppath) (or isurl iscl))
+ (bmk-mgr-node-swap-children-at-path
+ (bmk-mgr-get-root-node-in-buffer) ppath bmk-node current)
+ (bmk-mgr-transpose-lines bmk-node bmk-path count nil))
+ (t
+ (outline-previous-visible-heading count)
+ (bmk-mgr-delete-node-at-point)
+ (outline-next-visible-heading (1- count))
+ (if (or isurl iscl isout)
+ (bmk-mgr-insert-sibling-at-point bmk-node isout)
+ (bmk-mgr-insert-child-at-point bmk-node t)))))))
+ (bmk-mgr-goto-node-around bmk-node)))))
+
+(defvar bmk-mgr-import-formats '(("xbel" . bmk-mgr-import-xbel)
+ ("bmk" . bmk-mgr-import-bmk)))
+
+(defun bmk-mgr-import-add-formatter (name fun)
+ (add-to-list 'bmk-mgr-import-formats `(,name . ,fun)))
+
+(defun bmk-mgr-import ()
+ "Import bookmarks file."
+ (interactive)
+ (let* ((formats bmk-mgr-import-formats)
+ (names (mapcar 'car formats))
+ (prompt (concat "Format (" (mapconcat 'identity names ", ") "): "))
+ (sel (completing-read prompt formats nil 1))
+ (fun (cdr (assoc sel formats))))
+ (if fun
+ (bmk-mgr-with-bookmarks-buffer
+ (let* ((file (read-file-name "File: " nil nil t))
+ (folder (bmk-mgr-ask-path "Import to folder: "))
+ (ign (message "Reading %s..." file))
+ (node (funcall fun file (bmk-mgr-path-leaf folder))))
+ (when node
+ (message "Importing bookmarks...")
+ (if (bmk-mgr-find-path-in-buffer folder t)
+ (let ((parent (bmk-mgr-get-node-at-point))
+ (children (bmk-mgr-node-children node)))
+ (if (bmk-mgr-node-folder-p parent)
+ (progn
+ (mapc (lambda (x)
+ (bmk-mgr-node-add-child parent x)) children)
+ (bmk-mgr-update-tree-at-point)
+ (bmk-mgr-refresh-open-close)
+ (message nil))
+ (message "`%s' is not a correct insertion point"
+ (bmk-mgr-node-name parent))))
+ (if (bmk-mgr-find-path-in-buffer (bmk-mgr-path-parent folder) t)
+ (progn
+ (bmk-mgr-insert-child-at-point node nil)
+ (message nil))))))))))
+
+(defun bmk-mgr-quit-ask ()
+ "Quit bookmarks buffer, asking for confirmation."
+ (interactive)
+ (when (y-or-n-p "Close bookmarks browser? ") (bmk-mgr-quit)))
+
+(defun bmk-mgr-quit ()
+ "Quit bookmarks buffer."
+ (interactive)
+ (with-current-buffer (bmk-mgr-get-bookmark-buffer)
+ (bmk-mgr-save-current-tree)
+ (kill-buffer (current-buffer))))
+
+;;;; Import/export:
+
+;;;;; xbel:
+(defun bmk-mgr-xbel-get-title (node def)
+ (let* ((title-node (car (xml-get-children node 'title)))
+ (title-body (or (and title-node (xml-node-children title-node))
+ '())))
+ (bmk-mgr-filter-html
+ (or (and title-body (stringp (car title-body)) (car title-body)) def))))
+
+(defun bmk-mgr-xbel-to-bmk (xbel &optional name)
+ (when (listp xbel)
+ (case (xml-node-name xbel)
+ (xbel (bmk-mgr-node-folder-new
+ (or name "xbel") nil
+ (mapcar 'bmk-mgr-xbel-to-bmk
+ (append (xml-get-children xbel 'bookmark)
+ (xml-get-children xbel 'folder)))))
+ (folder (bmk-mgr-node-folder-new
+ (bmk-mgr-xbel-get-title xbel "folder")
+ (equal (xml-get-attribute xbel 'folded) "yes")
+ (mapcar 'bmk-mgr-xbel-to-bmk
+ (append (xml-get-children xbel 'bookmark)
+ (xml-get-children xbel 'folder)))))
+ (bookmark
+ (let* ((href (bmk-mgr-filter-html (xml-get-attribute xbel 'href)))
+ (title (bmk-mgr-xbel-get-title xbel href)))
+ (bmk-mgr-node-url-new title href))))))
+
+(defun bmk-mgr-import-xbel (file name)
+ (save-current-buffer
+ (if (not (file-readable-p file)) (error "Cannot read file"))
+ (require 'xml)
+ (message "Reading XBEL file...")
+ (bmk-mgr-xbel-to-bmk
+ (car (with-temp-buffer
+ (insert-buffer (find-file-noselect file))
+ (beginning-of-buffer)
+ (while (re-search-forward "\n" nil t) (replace-match ""))
+ (beginning-of-buffer)
+ (while (re-search-forward "\"\"" nil t) (replace-match "\"empty\""))
+ (beginning-of-buffer)
+ (while (re-search-forward "> +<" nil t) (replace-match "><"))
+ (xml-parse-region (point-min) (point-max))))
+ name)))
+
+;;;;; aux:
+(defconst bmk-mgr-html-scp "&#[0-9]+\\;")
+
+(defun bmk-mgr-filter-html (str)
+ (let* ((str (substring-no-properties str))
+ (result "")
+ (p0 0)
+ (p1 (string-match bmk-mgr-html-scp str)))
+ (while p1
+ (let* ((p2 (match-end 0))
+ (ch
+ (char-to-string (bmk-string-to-int (substring
+ str (+ 2 p1) (1- p2))))))
+ (setf result (concat result (substring str p0 p1) ch))
+ (setf p0 p2)
+ (setf p1 (string-match bmk-mgr-html-scp str p2))))
+ (concat result (substring str p0))))
+
+;;;; Bookmarks buffer:
+
+;;;;; Functions:
+(defun bmk-mgr-print-single-node-at-point (node path &optional insert)
+ (beginning-of-line)
+ (let ((kill-whole-line nil)
+ (inhibit-read-only t)
+ (depth (* (- (length path) 1) bmk-mgr-indent-width))
+ (txt-mark "")
+ (img))
+ (if (bmk-mgr-node-folder-p node)
+ (if (bmk-mgr-node-open-p node)
+ (setq txt-mark bmk-mgr-open-mark img bmk-mgr-fopen-img)
+ (setq txt-mark bmk-mgr-closed-mark img bmk-mgr-fclosed-img))
+ (if (> (length (bmk-mgr-node-url node)) 0)
+ (setq txt-mark bmk-mgr-link-mark img bmk-mgr-url-img)))
+ (if insert
+ (progn (newline)
+ (forward-line -1))
+ (kill-line))
+ (delete-trailing-whitespace)
+ (remove-images (point) (save-excursion (end-of-line) (point)))
+ (insert (make-string depth 32))
+ (if bmk-mgr-use-images
+ (if img ; no image for separators
+ (progn (put-image img (point)) (insert " ")))
+ (insert txt-mark))
+ (insert (bmk-mgr-node-title node))
+ (bmk-mgr-set-path-at-point path)
+ (bmk-mgr-set-node-at-point node)))
+
+(defun bmk-mgr-print-tree (tree &optional path level)
+ (let* ((kill-whole-line nil)
+ (inhibit-read-only t)
+ (next-line-add-newlines nil)
+ (insertp
+ (lambda (node path)
+ (not
+ (and (equal path (bmk-mgr-get-path-at-point))
+ (equal (bmk-mgr-node-type node)
+ (bmk-mgr-node-type (bmk-mgr-get-node-at-point)))))))
+ (pfun
+ (lambda (n w)
+ (beginning-of-line)
+ (let* ((title (bmk-mgr-node-title n))
+ (neww (append w (list title))))
+ (bmk-mgr-print-single-node-at-point
+ n neww (funcall insertp n neww))
+ (if (eobp) (newline))
+ (next-line 1)
+ (cons neww t)))))
+ (bmk-mgr-visit-tree tree pfun path)))
+
+(defsubst bmk-mgr-update-tree-at-point ()
+ (save-excursion
+ (bmk-mgr-print-tree (bmk-mgr-get-node-at-point)
+ (bmk-mgr-path-parent (bmk-mgr-get-path-at-point)))))
+
+(defsubst bmk-mgr-set-path-at-point (path &optional buffer)
+ (let ((inhibit-field-text-motion t)
+ (pos (save-excursion (end-of-line) (point))))
+ (save-excursion
+ (beginning-of-line)
+ (add-text-properties (point) pos (list 'bmk-mgr-path path) buffer))))
+
+(defsubst bmk-mgr-set-node-at-point (node &optional buffer)
+ (let ((inhibit-field-text-motion t)
+ (pos (save-excursion (end-of-line) (point))))
+ (save-excursion
+ (beginning-of-line)
+ (add-text-properties (point) pos (list 'bmk-mgr-node node) buffer)
+ (when (bmk-mgr-node-url-p node)
+ (bmk-mgr-beginning)
+ (add-text-properties (point) (1- pos)
+ (list 'mouse-face 'bmk-mgr-sel-bookmark-face)
+ buffer)))))
+
+(defsubst bmk-mgr-get-path-at-point (&optional buffer)
+ (get-text-property (point) 'bmk-mgr-path buffer))
+
+(defsubst bmk-mgr-get-node-at-point (&optional buffer)
+ (get-text-property (point) 'bmk-mgr-node buffer))
+
+(defun bmk-mgr-get-root-node-in-buffer (&optional buffer)
+ (save-current-buffer
+ (if buffer (set-buffer buffer))
+ (save-excursion
+ (goto-char (point-min))
+ (bmk-mgr-get-node-at-point))))
+
+(defun bmk-mgr-refresh-open-close ()
+ (save-excursion
+ (let* ((node (bmk-mgr-get-node-at-point))
+ (path (bmk-mgr-get-path-at-point))
+ (cl (length path)))
+ (unless (eobp)
+ (bmk-mgr-unmark-current)
+ (if (bmk-mgr-node-open-p node)
+ (progn
+ (show-children)
+ (outline-next-visible-heading 1)
+ (while (> (length (bmk-mgr-get-path-at-point)) cl)
+ (bmk-mgr-refresh-open-close)
+ (outline-next-visible-heading 1)))
+ (hide-subtree))))))
+
+(defun bmk-mgr-redraw-node-at-point (&optional path)
+ (save-excursion
+ (let ((node (bmk-mgr-get-node-at-point)))
+ (when node
+ (show-children)
+ (bmk-mgr-print-single-node-at-point
+ node (or path (bmk-mgr-get-path-at-point)))
+ (beginning-of-line)
+ (when (bmk-mgr-node-folder-p node)
+ (if (bmk-mgr-node-open-p node)
+ (bmk-mgr-refresh-open-close)
+ (hide-subtree)))))))
+
+(defun bmk-mgr-find-path-in-buffer (path &optional begin)
+ (beginning-of-line)
+ (let ((ip (point))
+ (ppos)
+ (found))
+ (if begin (goto-char (point-min)))
+ (while (not (or found (eobp)))
+ (let* ((cp (bmk-mgr-get-path-at-point))
+ (node (bmk-mgr-get-node-at-point))
+ (isf (and node (bmk-mgr-node-folder-p node)))
+ (isclf (and isf (bmk-mgr-node-closed-p node))))
+ (cond
+ ((equal path cp)
+ (save-excursion
+ (mapc (lambda (p)
+ (goto-char p)
+ (bmk-mgr-toggle-folder)
+ (bmk-mgr-unmark-current))
+ (reverse (if isclf (cons (point) ppos) ppos))))
+ (setf found t))
+ ((or (and isf (bmk-mgr-path-contains cp path))
+ (and (not isf) (equal (bmk-mgr-path-parent cp)
+ (bmk-mgr-path-parent path))))
+ (if isclf (setf ppos (cons (point) ppos)))
+ (forward-line 1))
+ (t (let ((cl (bmk-mgr-outline-level)))
+ (forward-line 1)
+ (while (and (not (eobp))
+ (< cl (bmk-mgr-outline-level)))
+ (forward-line 1)))))))
+ (if (not found) (goto-char ip)
+ (save-excursion (goto-char ip) (bmk-mgr-unmark-current)))
+ (and found (point))))
+
+(defun bmk-mgr-insert-sibling-at-point (node before)
+ (let ((bmk-node (bmk-mgr-get-node-at-point))
+ (bmk-path (bmk-mgr-get-path-at-point))
+ (pos (point)))
+ (save-excursion
+ (if (and node
+ (bmk-mgr-find-path-in-buffer (bmk-mgr-path-parent bmk-path) t))
+ (let* ((path (list (bmk-mgr-path-leaf (bmk-mgr-get-path-at-point))
+ (bmk-mgr-path-leaf bmk-path)))
+ (newtree (bmk-mgr-insert-node (bmk-mgr-get-node-at-point)
+ node path t before)))
+ (if newtree
+ (progn
+ (goto-char pos)
+ (when (not before)
+ (outline-next-visible-heading 1)
+ (if (eobp) (newline)))
+ (save-excursion
+ (bmk-mgr-print-tree node
+ (bmk-mgr-path-parent bmk-path)))
+ (bmk-mgr-refresh-open-close))
+ (error "Internal error")))
+ (error "Path to node not found")))))
+
+
+(defun bmk-mgr-insert-child-at-point (node before)
+ (let ((bmk-node (bmk-mgr-get-node-at-point))
+ (bmk-path (bmk-mgr-get-path-at-point)))
+ (if (bmk-mgr-node-url-p bmk-node)
+ (bmk-mgr-insert-sibling-at-point node before)
+ (when node
+ (let* ((path (list (bmk-mgr-path-leaf bmk-path)))
+ (newtree (bmk-mgr-insert-node bmk-node node path nil before)))
+ (if newtree
+ (progn
+ (if (bmk-mgr-node-closed-p bmk-node) (bmk-mgr-toggle-folder))
+ (forward-line 1)
+ (if (and (not before)
+ (> (length (bmk-mgr-node-children bmk-node)) 1))
+ (condition-case nil
+ (while (not (eobp)) (outline-forward-same-level 1))
+ (error (forward-line 1))))
+ (save-excursion
+ (bmk-mgr-print-tree node bmk-path))
+ (bmk-mgr-refresh-open-close))))))))
+
+;;;; Bookmark tree datatype:
+
+;;;;; paths:
+
+(defsubst bmk-mgr-path-parent (path) (and (listp path) (subseq path 0 -1)))
+(defsubst bmk-mgr-path-leaf (path) (and (listp path) (car (subseq path -1))))
+(defsubst bmk-mgr-path-to-string (path)
+ (mapconcat (lambda (x) (and (stringp x) x))
+ (delete-if (lambda (x) (string= x "")) path) "/"))
+(defsubst bmk-mgr-string-to-path (path)
+ (delete-if (lambda (x) (string= x ""))
+ (split-string path "/")))
+(defsubst bmk-mgr-path-contains (parent child)
+ (equal parent (subseq child 0 (length parent))))
+
+;;;;; constructors:
+(defsubst bmk-mgr-node-url-new (title url) (list title url))
+(defsubst bmk-mgr-node-folder-new (name &optional closed children)
+ (cons name (cons (if closed :closed :open) children)))
+
+;;;;; accessors:
+(defsubst bmk-mgr-node-children (n) (cddr n))
+(defsubst bmk-mgr-node-name (n) (nth 0 n))
+(defsubst bmk-mgr-node-folder-p (n) (and n (symbolp (nth 1 n))))
+(defsubst bmk-mgr-node-open-p (n) (equal :open (nth 1 n)))
+(defsubst bmk-mgr-node-closed-p (n) (equal :closed (nth 1 n)))
+(defsubst bmk-mgr-node-url-p (n) (stringp (nth 1 n)))
+(defsubst bmk-mgr-node-url (n) (and (stringp (nth 1 n)) (nth 1 n)))
+(defsubst bmk-mgr-node-title (n) (nth 0 n))
+(defsubst bmk-mgr-node-type (n) (if (bmk-mgr-node-url-p n) 'url 'folder))
+
+(defun bmk-mgr-node-child-folders (node &optional path)
+ (let ((node (or (and (null path) node)
+ (and node path (bmk-mgr-find-node node path)))))
+ (when node
+ (remove-if 'bmk-mgr-node-url-p
+ (bmk-mgr-node-children node)))))
+
+(defun bmk-mgr-find-node (tree path)
+ (let* ((node nil)
+ (ffun (lambda (n p)
+ (if (equal (car p) (bmk-mgr-node-name n))
+ (if (null (cdr p))
+ (progn
+ (setq node n)
+ (cons nil nil))
+ (cons (cdr p) t))
+ (cons nil nil)))))
+ (bmk-mgr-visit-tree tree ffun path)
+ node))
+
+(defun bmk-mgr-find-node-and-parent (tree path)
+ (let* ((parent tree)
+ (node nil)
+ (fnode (lambda (n p)
+ (if (equal (car p) (bmk-mgr-node-name n))
+ (if (null (cdr p))
+ (progn (setq node n)
+ (cons nil nil))
+ (progn (setq parent n)
+ (cons (cdr p) t)))
+ (cons nil nil)))))
+ (bmk-mgr-visit-tree tree fnode path)
+ (cons (and node parent) node)))
+
+;;;;; modifiers:
+(defsubst bmk-mgr-node-set-name (node name)
+ (when (stringp name) (setf (car node) name)))
+
+(defsubst bmk-mgr-node-set-url (node url)
+ (when (and (bmk-mgr-node-url-p node) (stringp url)) (setf (nth 1 node) url)))
+
+(defun bmk-mgr-node-toggle-open-closed (node)
+ (when (bmk-mgr-node-folder-p node)
+ (setf (nth 1 node) (if (bmk-mgr-node-closed-p node) :open :closed))))
+
+(defsubst bmk-mgr-node-close (node)
+ (when (bmk-mgr-node-folder-p node) (setf (nth 1 node) :closed)))
+
+(defsubst bmk-mgr-node-close-all (tree)
+ (when (bmk-mgr-node-folder-p tree)
+ (bmk-mgr-node-close tree)
+ (mapc #'bmk-mgr-node-close-all (bmk-mgr-node-children tree))))
+
+(defsubst bmk-mgr-node-close-all-children (tree)
+ (when (bmk-mgr-node-folder-p tree)
+ (mapc #'bmk-mgr-node-close-all (bmk-mgr-node-children tree))))
+
+(defun bmk-mgr-node-set-children (node children)
+ (when (bmk-mgr-node-folder-p node)
+ (setf (nthcdr 2 node) children)))
+
+(defun bmk-mgr-node-swap-children (node c0 c1)
+ "Swap the positions of C0 and C1, which are children of NODE.
+
+If C0 is null, C1 gets promoted to the top of the children list.
+Conversely, if C1 is null, C0 goes to the tail."
+ (when (and (bmk-mgr-node-folder-p node) (or c0 c1))
+ (let* ((children (bmk-mgr-node-children node))
+ (p0 (position c0 children))
+ (p1 (position c1 children)))
+ (when (and children (or p0 p1))
+ (if (and c1 p1) (setf (nth (or p0 2) children) c1))
+ (if (and c0 p0)
+ (setf (nth (or p1 (1- (length children))) children) c0))))))
+
+(defun bmk-mgr-node-swap-children-at-path (tree path c0 c1)
+ "Calls `bmk-mgr-node-swap-children' on the node of TREE denoted by
+path."
+ (when tree
+ (let ((node (bmk-mgr-find-node tree path)))
+ (if node (bmk-mgr-node-swap-children node c0 c1)))))
+
+(defun bmk-mgr-node-add-child (tree node &optional prev before)
+ "Add NODE as a new child of TREE, after (or before, if BEFORE is not
+null) node PREV if it exists.
+
+Returns the updated TREE if successful, nil otherwise."
+ (when (bmk-mgr-node-folder-p tree)
+ (let* ((pos (or (position prev tree) (if before 2)))
+ (insp (if (not pos) (length tree) (if before pos (1+ pos)))))
+ (setf (nthcdr insp tree) (cons node (nthcdr insp tree)))
+ tree)))
+
+(defun bmk-mgr-insert-node (tree node path &optional sibling before)
+ "Insert the NODE at the given PATH of TREE.
+
+If SIBLING is not null, the new node will be inserted as a sibling of
+the one denoted by PATH. Otherwise, PATH is the path of NODE's parent.
+If BEFORE is not null, NODE is inserter before or as the first child
+denoted by path.
+
+Returns the updated parent of NODE if successful, nil otherwise."
+ (let* ((np (bmk-mgr-find-node-and-parent tree path))
+ (parent (and np (car np)))
+ (found (and np (cdr np))))
+ (when found
+ (if (or sibling (bmk-mgr-node-url-p found))
+ (bmk-mgr-node-add-child parent node found before)
+ (bmk-mgr-node-add-child found node nil before)))))
+
+(defun bmk-mgr-delete-node (tree path)
+ (let* ((np (bmk-mgr-find-node-and-parent tree path))
+ (parent (and np (car np)))
+ (found (and np (cdr np)))
+ (children (and found (bmk-mgr-node-children parent))))
+ (when children
+ (bmk-mgr-node-set-children parent (remove found children)))))
+
+;;;;; input/output:
+(defun bmk-mgr-read-from-file (filename)
+ (let ((rfname (expand-file-name filename)))
+ (if (file-readable-p rfname)
+ (with-temp-buffer
+ (insert-file-contents rfname)
+ (goto-char (point-min))
+ (let ((sexp (read (current-buffer))))
+ (and (bmk-mgr-node-folder-p sexp) sexp)))
+ '("Bookmarks" :open
+ ("Emacs bookmark manager"
+ "http://www.emacswiki.org/cgi-bin/wiki/EmacsBmkMgr")))))
+
+(defun bmk-mgr-save-to-file (bmks filename)
+ (require 'pp)
+ (when (bmk-mgr-node-folder-p bmks)
+ (let ((rfname (expand-file-name filename))
+ (b (if bmk-mgr-ignore-fold-state (subst :closed :open bmks) bmks)))
+ (with-temp-buffer
+ (insert ";;; File automatically generated by Emacs Bookmark Manager"
+ "\n")
+ (if bmk-mgr-ignore-fold-state (bmk-mgr-node-toggle-open-closed b))
+ (pp b (current-buffer))
+ (insert "\n;;; End of " (file-name-nondirectory rfname) "\n")
+ (write-region (point-min) (point-max) rfname)))))
+
+
+;;;;; aux functions:
+(defun bmk-mgr-visit-tree (tree fun arg)
+ "Visit a bookmarks tree aplying FUN to its nodes."
+ (when tree
+ (let ((arg (funcall fun tree arg)))
+ (when (cdr arg)
+ (mapc (lambda (n) (bmk-mgr-visit-tree n fun (car arg)))
+ (bmk-mgr-node-children tree))))))
+
+
+
+(provide 'bmk-mgr)
+
+
+
+
+
+;;; Local stuff:
+;;;; Local Variables: ;;
+;;;; mode: emacs-lisp ;;
+;;;; mode: outline-minor ;;
+;;;; outline-regexp: ";;[;\f]+ " ;;
+;;;; outline-heading-end-regexp: ":\n" ;;
+;;;; indent-tabs-mode: nil ;;
+;;;; End: ;;
+
+;;; bmk-mgr.el ends here
diff --git a/lib/bmk/dot-emacs.el b/lib/bmk/dot-emacs.el
new file mode 100644
index 0000000..01f00d0
--- /dev/null
+++ b/lib/bmk/dot-emacs.el
@@ -0,0 +1,42 @@
+;;; sample initialisation file for bmk-mgr
+
+(if (require 'bmk-mgr nil t)
+ (progn
+ (setq bmk-mgr-bookmark-file "~/.emacs.d/bookmarks")
+ (setq bmk-mgr-inhibit-welcome-message nil)
+ (setq bmk-mgr-inhibit-minibuffer t)
+ (setq bmk-mgr-use-own-frame nil)
+ (setq bmk-mgr-use-images t)
+ (setq bmk-mgr-ignore-fold-state t)
+
+ (define-key bmk-mgr-mode-map "g" 'bmk-mgr-browse-url)
+ (define-key bmk-mgr-mode-map "G" 'bmk-mgr-browse-url-alt)
+ (global-set-key "\C-cB" 'bmk-mgr-show-bookmarks)
+ (global-set-key "\C-cA" 'bmk-mgr-add-url-at-point)
+
+ ;;;; integration with emacs-w3m (optional)
+ (when (require 'w3m nil t)
+ (require 'bmk-mgr-w3m)
+ (defun browse-bmk-w3m (url &rest ig)
+ (goto-w3m-buffer)
+ (w3m-goto-url url t))
+
+ (defun browse-bmk-new-tab-w3m (url &rest ig)
+ (goto-w3m-buffer)
+ (w3m-goto-url-new-session url t))
+
+ (defun goto-w3m-buffer () (interactive)
+ (let ((display-buffer-reuse-frames 1)
+ (pop-up-windows nil)
+ (buffer (w3m-alive-p)))
+ (if buffer (pop-to-buffer buffer))))
+
+ (define-key bmk-mgr-mode-map "w" 'goto-w3m-buffer)
+ (setq bmk-mgr-browser-function 'browse-bmk-w3m)
+ (setq bmk-mgr-alt-browser-function 'browse-bmk-new-tab-w3m))
+
+ ;; integration with w3
+ (require 'bmk-mgr-w3)))
+
+ (message "bookmark manager not available"))
+
diff --git a/lib/bmk/folder-closed.xpm b/lib/bmk/folder-closed.xpm
new file mode 100644
index 0000000..ece8a9e
--- /dev/null
+++ b/lib/bmk/folder-closed.xpm
@@ -0,0 +1,31 @@
+/* XPM */
+static char *folder-closed[] = {
+/* columns rows colors chars-per-pixel */
+"16 16 9 1",
+" c gray100",
+". c #FFFFCC",
+"X c #CCCCFF",
+"o c #C0C0C0",
+"O c #9999FF",
+"+ c #6666CC",
+"@ c #222222",
+"# c black",
+"$ c None",
+/* pixels */
+"$$$$$$$$$$$$$$$$",
+"$$$$$$$$$$$$$$$$",
+"$$$$$$$$$$$$$$$$",
+"$$$$$$$$$$$$$$$$",
+"$$++++O$$$$$$$$$",
+"$+ XXO.++++++$$$",
+"+ XXXXXXXXXXXO$$",
+"+ OXOXOXOXOXO+$$",
+"+ XOXOXOXOXOX+#$",
+"+ OXOXOXOXOXO+#$",
+"+ XOXOXOXOXOX+#$",
+"+ OXOXOXOXOXO+#$",
+"+ XOXOXOXOXOX+#$",
+"+XOXOXOXOXOXO+#$",
+"++++++++++++++#$",
+"$#@@###########$"
+};
diff --git a/lib/bmk/folder-open.xpm b/lib/bmk/folder-open.xpm
new file mode 100644
index 0000000..f03f65c
--- /dev/null
+++ b/lib/bmk/folder-open.xpm
@@ -0,0 +1,39 @@
+/* XPM */
+static char *folder-open[] = {
+/* columns rows colors chars-per-pixel */
+"16 16 17 1",
+" c gray100",
+". c #FFFFCC",
+"X c magenta",
+"o c #CCCCFF",
+"O c #C0C0C0",
+"+ c #9999FF",
+"@ c #6666CC",
+"# c #222222",
+"$ c black",
+"% c black",
+"& c black",
+"* c black",
+"= c black",
+"- c black",
+"; c black",
+": c black",
+"> c None",
+/* pixels */
+">>>>>>>>>>>>>>>>",
+">>>>>>>>>>>>>>>>",
+">>>>>>>>>>>>>>>>",
+">>>>>>>>>>>>>>>>",
+">>@@@@>>>>>>>>>>",
+">@ @@@@@@@@@>>",
+">@ +O.......#+@>",
+">@ O.......#+o@>",
+">+ .......#+o+@$",
+"@@@@@@@@@@#@+o@$",
+"@ O$o+@$",
+">@+O+O+O+O+O$o@$",
+">@O+O+O+O+O+#$@$",
+">>@O+O+O+O+O+#$$",
+">>@@@@@@@@@@@@@$",
+">>>$$$$$$$$$$$$$"
+};
diff --git a/lib/bmk/url-alt.xpm b/lib/bmk/url-alt.xpm
new file mode 100644
index 0000000..4cb2c14
--- /dev/null
+++ b/lib/bmk/url-alt.xpm
@@ -0,0 +1,31 @@
+/* XPM */
+static char *document[] = {
+/* columns rows colors chars-per-pixel */
+"16 16 9 1",
+" c gray100",
+". c #CECEBF",
+"X c gray80",
+"o c #272724",
+"O c black",
+"+ c black",
+"@ c black",
+"# c black",
+"$ c None",
+/* pixels */
+"$oooooooooo$$$$$",
+"$o........oO$$$$",
+"$o........oXO$$$",
+"$o........oooo$$",
+"$o...........o$$",
+"$o...........o$$",
+"$o..o.oo.oo..o$$",
+"$o...........o$$",
+"$o..oooo.oo..o$$",
+"$o...........o$$",
+"$o..oo.oo.o..o$$",
+"$o...........o$$",
+"$o...........o$$",
+"$o...........o$$",
+"$o...........o$$",
+"$ooooooooooooo$$"
+};
diff --git a/lib/bmk/url.xpm b/lib/bmk/url.xpm
new file mode 100644
index 0000000..60cad93
--- /dev/null
+++ b/lib/bmk/url.xpm
@@ -0,0 +1,39 @@
+/* XPM */
+static char *article[] = {
+/* columns rows colors chars-per-pixel */
+"16 16 17 1",
+" c black",
+". c #BF0000",
+"X c #00BF00",
+"o c #BFBF00",
+"O c #0000BF",
+"+ c #BF00BF",
+"@ c #00BFBF",
+"# c #C0C0C0",
+"$ c #808080",
+"% c red",
+"& c green",
+"* c yellow",
+"= c blue",
+"- c magenta",
+"; c cyan",
+": c gray100",
+"> c None",
+/* pixels */
+"OOOOOOOOOOO>>>>>",
+"O:::::::::O>>>>>",
+"O: #$ # #:OOO>>>",
+"O:$ # $ :O*O>>>",
+"O:::::::::O:O$>>",
+"O:#$:$$#$:O*O$>>",
+"O:::::::::O:O$>>",
+"O:$$:$#$$:O*O$>>",
+"O:::::::::O:O$>>",
+"O:#$:$$$#:O*O$>>",
+"O:::::::::O:O$>>",
+"OOOOOOOOOOO*O$>>",
+">>O:*:*:*:*:O$>>",
+">>OOOOOOOOOOO$>>",
+">>>>$$$$$$$$$$>>",
+">>>>>>>>>>>>>>>>"
+};
diff --git a/lib/doc/jao-counsel-recoll.el b/lib/doc/jao-counsel-recoll.el
new file mode 100644
index 0000000..adae881
--- /dev/null
+++ b/lib/doc/jao-counsel-recoll.el
@@ -0,0 +1,60 @@
+;;; jao-counsel-recoll.el --- counsel and recoll -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020 jao
+
+;; Author: jao <mail@jao.io>
+;; Keywords: docs
+
+;; 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:
+
+;; Helpers for using recoll with counsel
+
+;;; Code:
+
+(require 'jao-recoll)
+(require 'counsel)
+(require 'ivy)
+
+(defvar jao-counsel-recoll--history nil)
+(defun jao-counsel-recoll--function (str)
+ (let ((xs (counsel-recoll-function str)))
+ (cl-remove-if-not (lambda (x) (string-prefix-p "file://" x)) xs)))
+
+;;;###autoload
+(defun jao-counsel-recoll (&optional initial-input)
+ (interactive)
+ (counsel-require-program "recoll")
+ (ivy-read "recoll: " 'jao-counsel-recoll--function
+ :initial-input initial-input
+ :dynamic-collection t
+ :history 'jao-counsel-recoll--history
+ :action (lambda (x)
+ (when (string-match "file://\\(.*\\)\\'" x)
+ (let ((file-name (match-string 1 x)))
+ (if (string-match "pdf$" x)
+ (jao-open-doc file-name)
+ (find-file file-name)))))
+ :unwind #'counsel-delete-process
+ :caller 'jao-counsel-recoll))
+
+(defun jao-counsel-recoll--recoll (_s) (jao-recoll ivy-text))
+
+(ivy-set-actions 'jao-counsel-recoll
+ '(("x" jao-counsel-recoll--recoll "List in buffer")))
+
+
+(provide 'jao-counsel-recoll)
+;;; jao-counsel-recoll.el ends here
diff --git a/lib/doc/jao-doc-view.el b/lib/doc/jao-doc-view.el
new file mode 100644
index 0000000..5060452
--- /dev/null
+++ b/lib/doc/jao-doc-view.el
@@ -0,0 +1,153 @@
+;; jao-doc-view.el -- Remembering visited documents
+
+;; Copyright (c) 2013, 2015, 2017, 2018, 2019 Jose Antonio Ortega Ruiz
+
+;; 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 of the License, or
+;; (at your option) any later version.
+
+;; This file 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/>.
+
+;; Author: Jose Antonio Ortega Ruiz <jao@gnu.org>
+;; Start date: Fri Feb 15, 2013 01:21
+
+;;; Comentary:
+
+;; Some utilities to keep track of visited documents and the last
+;; visited page.
+
+;;; Code:
+
+(defvar jao-doc-view-bmk-file "~/.emacs.d/doc-view-bmk")
+(defvar jao-doc-view-session-file "~/.emacs.d/doc-view-session")
+(defvar jao-doc-view--current-bmks nil)
+
+(defun jao-doc-view--read-file (file)
+ (let ((buff (find-file-noselect file)))
+ (ignore-errors
+ (with-current-buffer buff
+ (goto-char (point-min)))
+ (read buff))))
+
+(defun jao-doc-view--save-to-file (file value)
+ (with-current-buffer (find-file-noselect file)
+ (erase-buffer)
+ (insert (format "%S" value))
+ (save-buffer)))
+
+(defun jao-doc-view--read-bmks ()
+ (let ((bmks (jao-doc-view--read-file jao-doc-view-bmk-file)))
+ (if (hash-table-p bmks) bmks (make-hash-table :test 'equal))))
+
+(defun jao-doc-view--current-bmks ()
+ (or jao-doc-view--current-bmks
+ (setq jao-doc-view--current-bmks (jao-doc-view--read-bmks))))
+
+(defun jao-doc-view-purge-bmks ()
+ (interactive)
+ (when jao-doc-view--current-bmks
+ (maphash (lambda (k v)
+ (when (or (not k) (= 1 v) (not (file-exists-p k)))
+ (remhash k jao-doc-view--current-bmks)))
+ jao-doc-view--current-bmks)))
+
+(defun jao-doc-view-goto-bmk ()
+ (interactive)
+ (when (eq major-mode 'pdf-view-mode)
+ (let* ((bmks (jao-doc-view--current-bmks))
+ (fname (buffer-file-name))
+ (p (when fname (gethash (expand-file-name fname) bmks 1))))
+ (when (and (numberp p) (> p 1))
+ (message "Found bookmark at page %d" p)
+ (ignore-errors (pdf-view-goto-page p))))))
+
+(defun jao-doc-view-open (file)
+ (let* ((buffs (buffer-list))
+ (b (catch 'done
+ (while buffs
+ (when (string-equal (buffer-file-name (car buffs)) file)
+ (throw 'done (car buffs)))
+ (setq buffs (cdr buffs))))))
+ (if b
+ (pop-to-buffer b)
+ (when (file-exists-p file) (find-file file)))))
+
+(defun jao-doc-view-session (&optional file)
+ (let ((file (or file jao-doc-view-session-file)))
+ (jao-doc-view--read-file file)))
+
+(defun jao-doc-view-load-session (&optional file)
+ (interactive)
+ (let ((docs (jao-doc-view-session file)))
+ (when (not (listp docs)) (error "Empty session"))
+ (dolist (d docs) (other-window 1) (jao-doc-view-open d))))
+
+(defun jao-doc-view--save-bmks ()
+ (jao-doc-view-purge-bmks)
+ (jao-doc-view--save-to-file jao-doc-view-bmk-file
+ (jao-doc-view--current-bmks)))
+
+(defun jao-doc-view--save-bmk (&rest ignored)
+ (when (eq major-mode 'pdf-view-mode)
+ (ignore-errors
+ (puthash (buffer-file-name)
+ (max (pdf-view-current-page) 1)
+ (jao-doc-view--current-bmks)))))
+
+(defun jao-doc-view-save-session (&optional skip-current)
+ (interactive)
+ (let ((docs '())
+ (cb (when skip-current (current-buffer))))
+ (dolist (b (buffer-list))
+ (with-current-buffer b
+ (when (and (equalp major-mode 'pdf-view-mode)
+ (not (equalp cb b)))
+ (jao-doc-view--save-bmk)
+ (add-to-list 'docs (buffer-file-name)))))
+ (jao-doc-view--save-bmks)
+ (when (> (length docs) 0)
+ (jao-doc-view--save-to-file jao-doc-view-session-file docs))))
+
+(defun jao-doc-view--save-session-1 ()
+ (when (equalp major-mode 'pdf-view-mode)
+ (jao-doc-view-purge-bmks)
+ (jao-doc-view-save-session t)))
+
+(defvar jao-doc-session-timer nil)
+(defvar jao-doc-session-timer-seconds 60)
+
+(defun jao-doc-view-stop-session-timer ()
+ (interactive)
+ (when jao-doc-session-timer
+ (cancel-timer jao-doc-session-timer)
+ (setq jao-doc-session-timer nil)))
+
+(defun jao-doc-view--save-session ()
+ (let ((inhibit-message t)
+ (message-log-max nil))
+ (jao-doc-view-save-session)))
+
+(defun jao-doc-view-start-session-timer ()
+ (interactive)
+ (setq jao-doc-session-timer
+ (run-with-idle-timer jao-doc-session-timer-seconds
+ t
+ 'jao-doc-view--save-session)))
+
+(defun jao-doc-view-install ()
+ (jao-doc-view--current-bmks)
+ (add-hook 'kill-buffer-hook 'jao-doc-view--save-bmk)
+ (add-hook 'kill-buffer-hook 'jao-doc-view--save-session-1 t)
+ (add-hook 'kill-emacs-hook 'jao-doc-view-save-session)
+ (jao-doc-view-start-session-timer))
+
+
+
+(provide 'jao-doc-view)
diff --git a/lib/doc/jao-recoll.el b/lib/doc/jao-recoll.el
new file mode 100644
index 0000000..28a1c1a
--- /dev/null
+++ b/lib/doc/jao-recoll.el
@@ -0,0 +1,82 @@
+;; jao-recoll.el -- Displaying recoll queries
+
+;; Copyright (c) 2017, 2020 Jose Antonio Ortega Ruiz
+
+;; 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 of the License, or
+;; (at your option) any later version.
+
+;; This file 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/>.
+
+;; Author: Jose Antonio Ortega Ruiz <jao@gnu.org>
+;; Start date: Wed Nov 01, 2017 18:14
+
+
+;;; Comentary:
+
+;; A simple interactive command to perform recoll queries and display
+;; its results using org-mode.
+
+;;; Code:
+
+
+(require 'org)
+
+(define-derived-mode recoll-mode org-mode "Recoll"
+ "Simple mode for showing recoll query results"
+ (read-only-mode 1))
+
+(defvar jao-recoll--file-regexp
+ "\\(\\w+/\\w+\\)\t+\\[\\([^]]+\\)\\]\t+\\[\\([^]]+\\)\\].+")
+
+(defvar jao-recoll-flags "-A")
+
+;;;###autoload
+(defun jao-recoll (keywords)
+ "Performs a query using recoll and shows the results in a
+buffer using org mode."
+ (interactive "sRecoll query string: ")
+ (with-current-buffer (get-buffer-create (format "* Recoll: '%s' *" keywords))
+ (read-only-mode -1)
+ (delete-region (point-min) (point-max))
+ (let ((c (format "recoll %s -t %s"
+ jao-recoll-flags (shell-quote-argument keywords))))
+ (shell-command c t))
+ (goto-char (point-min))
+ (when (looking-at-p "Recoll query:")
+ (let ((kill-whole-line t)) (kill-line))
+ (forward-line 1))
+ (open-line 1)
+ (while (search-forward-regexp jao-recoll--file-regexp nil t)
+ (replace-match "* [[\\2][\\3]] (\\1)")
+ (forward-line)
+ (beginning-of-line)
+ (let ((kill-whole-line nil)) (kill-line))
+ (forward-line)
+ (let ((p (point)))
+ (re-search-forward "/ABSTRACT")
+ (beginning-of-line)
+ (fill-region p (point))
+ (let ((kill-whole-line nil)) (kill-line))))
+ (recoll-mode)
+ (pop-to-buffer (current-buffer))
+ (goto-char (point-min))
+ (org-cycle '(4))
+ (org-next-visible-heading 1)))
+
+(define-key recoll-mode-map [?n] 'org-next-link)
+(define-key recoll-mode-map [?p] 'org-previous-link)
+(define-key recoll-mode-map [?q] 'bury-buffer)
+(define-key recoll-mode-map [?r] 'jao-recoll)
+
+
+
+(provide 'jao-recoll)
+;;; jao-recoll.el ends here
diff --git a/lib/eos/jao-afio.el b/lib/eos/jao-afio.el
new file mode 100644
index 0000000..10ca474
--- /dev/null
+++ b/lib/eos/jao-afio.el
@@ -0,0 +1,212 @@
+;;; jao-afio.el --- workspaces in just one frame -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020, 2021 jao
+
+;; Author: jao <mail@jao.io>
+;; Keywords: frames
+
+;; 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/>.
+
+;;; Code:
+
+(defvar jao-afio--configs '(?c ?w ?g ?p ?s))
+(defvar jao-afio--current-config (car jao-afio--configs))
+(defvar jao-afio--locker nil)
+(defvar jao-afio-fallback-fun nil)
+
+(defun jao-afio--check-frame-p ()
+ (assoc 'afio (frame-parameters)))
+
+(defun jao-afio--init (&optional f)
+ (interactive)
+ (when (and (frame-live-p jao-afio--locker)
+ (not (eql f jao-afio--locker)))
+ (if jao-afio-fallback-fun
+ (funcall jao-afio-fallback-fun)
+ (error "Another frame is using afio")))
+ (setq jao-afio--locker f)
+ (modify-frame-parameters f '((afio . t)))
+ (setq jao-afio--current-config ?c)
+ (mapc (lambda (r) (set-register r nil)) jao-afio--configs)
+ (window-configuration-to-register ?c))
+
+(defun jao-afio--steal ()
+ (interactive)
+ (setq jao-afio--locker nil)
+ (jao-afio--init (window-frame (get-buffer-window (current-buffer)))))
+
+(defun jao-afio--check-frame ()
+ (unless (jao-afio--check-frame-p)
+ (or (when jao-afio-fallback-fun
+ (funcall jao-afio-fallback-fun)
+ t)
+ (when (y-or-n-p "Another frame is using afio. Steal? ")
+ (jao-afio--steal)
+ t)
+ (error "Aborted"))))
+
+(defun jao-afio--next-frame ()
+ (interactive)
+ (jao-afio--check-frame)
+ (let* ((cur (member jao-afio--current-config jao-afio--configs))
+ (next (or (cadr cur) (car jao-afio--configs))))
+ (jao-afio--goto-frame next)))
+
+;;;###autoload
+(defun jao-afio-open-doc ()
+ (interactive)
+ (delete-other-windows)
+ (split-window-right)
+ (let ((docs (remove-if-not (lambda (b)
+ (eq (buffer-local-value 'major-mode b)
+ 'pdf-view-mode))
+ (buffer-list))))
+ (if (car docs)
+ (progn (switch-to-buffer (car docs))
+ (switch-to-buffer-other-window (or (cadr docs) (car docs))))
+ (when (and (jao-doc-view-session)
+ (y-or-n-p "Load saved session? "))
+ (dolist (doc (jao-doc-view-session))
+ (when (and (file-exists-p doc) (y-or-n-p (format "Open %s? " doc)))
+ (find-file doc)))))))
+
+;;;###autoload
+(defun jao-afio-open-w3m ()
+ (interactive)
+ (if (< (frame-width) 180)
+ (w3m)
+ (delete-other-windows)
+ (split-window-right)
+ (w3m)
+ (other-window 1)
+ (switch-to-buffer "*w3m*")
+ (ignore-errors (w3m-previous-buffer 2))))
+
+;;;###autoload
+(defun jao-afio-open-gnus ()
+ (interactive)
+ (delete-other-windows)
+ (org-agenda-list)
+ (calendar)
+ (find-file (expand-file-name "inbox.org" org-directory))
+ (gnus)
+ (jao-gnus--set-summary-line))
+
+;;;###autoload
+(defun jao-afio-open-mail (mail-func)
+ (interactive)
+ (delete-other-windows)
+ (funcall mail-func)
+ (jao-bisect)
+ (other-window 1)
+ (find-file (expand-file-name "inbox.org" org-directory))
+ (split-window-below (/ (window-height) 3))
+ (other-window 1)
+ (org-agenda-list)
+ (split-window-below -9)
+ (other-window 1)
+ (switch-to-buffer "*Calendar*")
+ (other-window 1))
+
+(defvar jao-afio-switch-hook nil)
+
+(defun jao-afio--goto-frame (next &optional reset)
+ (let ((next-cfg (when (not reset) (get-register next))))
+ (window-configuration-to-register jao-afio--current-config)
+ (setq jao-afio--current-config next)
+ (if next-cfg
+ (jump-to-register next)
+ (delete-other-windows)
+ (cl-case next
+ (?w (jao-afio-open-w3m))
+ (?g (jao-afio-open-gnus))
+ (?p (jao-afio-open-doc))
+ (?s (delete-other-windows))))
+ (run-hooks 'jao-afio-switch-hook)))
+
+(defun jao-afio--goto-main (&optional reset)
+ (interactive "P")
+ (jao-afio--check-frame)
+ (jao-afio--goto-frame ?c reset))
+
+(defun jao-afio--goto-scratch (&optional reset)
+ (interactive "P")
+ (jao-afio--check-frame)
+ (jao-afio--goto-frame ?s reset))
+
+(defun jao-afio--goto-gnus (&optional reset)
+ (interactive "P")
+ (jao-afio--check-frame)
+ (jao-afio--goto-frame ?g reset))
+
+(defun jao-afio--goto-docs (&optional reset)
+ (interactive "P")
+ (jao-afio--check-frame)
+ (jao-afio--goto-frame ?p reset))
+
+(defun jao-afio--goto-w3m (&optional reset)
+ (interactive "P")
+ (if (jao-afio--check-frame-p)
+ (jao-afio--goto-frame ?w reset)
+ (when (w3m-alive-p)
+ (pop-to-buffer (w3m-alive-p)))))
+
+(defun jao-afio--try-init (&optional f)
+ (ignore-errors (jao-afio--init f))
+ t)
+
+(defun jao-afio--goto-w3m-buffer (buf &rest _)
+ (jao-afio--goto-w3m)
+ (jao-first-window)
+ (switch-to-buffer buf nil t))
+
+(defun jao-afio--goto-pdf-buffer (buf &rest _)
+ (if (jao-afio--check-frame-p)
+ (progn (jao-afio--goto-docs)
+ (jao-first-window)
+ (switch-to-buffer buf nil t))
+ (pop-to-buffer buf)))
+
+(defun jao-afio-goto-scratch (&optional one-win)
+ (jao-afio--goto-scratch)
+ (when one-win (delete-other-windows)))
+
+(defun jao-afio-current-frame ()
+ (cl-case jao-afio--current-config
+ (?c "Main")
+ (?s "Scratch")
+ (?g "Gnus")
+ (?p "Docs")
+ (?w "Web")))
+
+(defun jao-afio-current-no ()
+ (cl-case jao-afio--current-config
+ (?c "1")
+ (?s "0")
+ (?g "2")
+ (?p "4")
+ (?w "3")))
+
+;;;###autoload
+(defun jao-afio-setup (&optional fallback-fun init-p)
+ (global-set-key "\C-cf" 'jao-afio--goto-main)
+ (global-set-key "\C-cg" 'jao-afio--goto-gnus)
+ (global-set-key "\C-cw" 'jao-afio--goto-w3m)
+ (global-set-key "\C-cz" 'jao-afio--goto-docs)
+ (setq jao-afio-fallback-fun fallback-fun)
+ (add-hook (if init-p 'after-init-hook 'after-make-frame-functions)
+ 'jao-afio--try-init))
+
+(provide 'jao-afio)
+;;; jao-afio.el ends here
diff --git a/lib/eos/jao-ednc.el b/lib/eos/jao-ednc.el
new file mode 100644
index 0000000..8e55a56
--- /dev/null
+++ b/lib/eos/jao-ednc.el
@@ -0,0 +1,148 @@
+;;; jao-ednc.el --- Minibuffer notifications using EDNC -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020, 2021 jao
+
+;; Author: jao <mail@jao.io>
+;; Keywords: tools, abbrev
+
+;; 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:
+
+;; Use the ednc package to provide a notification daemon that uses
+;; the minibuffer to display them.
+
+;;; Code:
+
+(require 'ednc)
+(require 'jao-minibuffer)
+
+(declare-function tracking-add-buffer "tracking")
+(declare-function tracking-remove-buffer "tracking")
+
+(defvar jao-ednc--count-format " {%d} ")
+(defvar jao-ednc--notifications ())
+(defvar jao-ednc--handlers ())
+
+(defvar jao-ednc-use-minibuffer-notifications nil)
+(defvar jao-ednc-use-tracking nil)
+
+(defface jao-ednc-tracking '((t :inherit warning))
+ "Tracking notifications face"
+ :group 'jao-ednc)
+
+(defun jao-ednc--last-notification () (car jao-ednc--notifications))
+
+(defun jao-ednc--format-last ()
+ (when (jao-ednc--last-notification)
+ (let ((s (ednc-format-notification (jao-ednc--last-notification) t)))
+ (replace-regexp-in-string "\n" " " (substring-no-properties s)))))
+
+(defun jao-ednc--count ()
+ (let ((no (length jao-ednc--notifications)))
+ (if (> no 0)
+ (propertize (format jao-ednc--count-format no) 'face 'warning)
+ "")))
+
+(defun jao-ednc-add-handler (app handler)
+ (add-to-list 'jao-ednc--handlers (cons app handler)))
+
+(defun jao-ednc-ignore-app (app)
+ (jao-ednc-add-handler app (lambda (not _) (ednc-dismiss-notification not))))
+
+(defun jao-ednc--pop-minibuffer ()
+ (if jao-ednc-use-minibuffer-notifications
+ (jao-minibuffer-pop-notification)
+ (jao-minibuffer-refresh)))
+
+(defun jao-ednc--clean (&optional notification)
+ (tracking-remove-buffer (get-buffer ednc-log-name))
+ (if notification
+ (remove notification jao-ednc--notifications)
+ (pop jao-ednc--notifications))
+ (jao-ednc--pop-minibuffer))
+
+(defun jao-ednc--show-last ()
+ (if jao-ednc-use-minibuffer-notifications
+ (jao-minibuffer-push-notification '(:eval (jao-ednc--format-last)))
+ (message "%s" (jao-ednc--format-last))))
+
+(defun jao-ednc--default-handler (notification newp)
+ (if (not newp)
+ (jao-ednc--clean notification)
+ (tracking-add-buffer (get-buffer ednc-log-name) '(jao-ednc-tracking))
+ (push notification jao-ednc--notifications)
+ (jao-ednc--show-last)))
+
+(defun jao-ednc--handler (notification)
+ (alist-get (ednc-notification-app-name notification)
+ jao-ednc--handlers
+ #'jao-ednc--default-handler
+ nil
+ 'string=))
+
+(defun jao-ednc--on-notify (old new)
+ (when old (funcall (jao-ednc--handler old) old nil))
+ (when new (funcall (jao-ednc--handler new) new t)))
+
+;;;###autoload
+(defun jao-ednc-setup (minibuffer-order)
+ (setq jao-notify-use-messages-p t)
+ (with-eval-after-load "tracking"
+ (when jao-ednc-use-tracking
+ (add-to-list 'tracking-faces-priorities 'jao-ednc-tracking)
+ (when (listp tracking-shorten-modes)
+ (add-to-list 'tracking-shorten-modes 'ednc-view-mode))))
+ (when minibuffer-order
+ (jao-minibuffer-add-variable '(jao-ednc--count) minibuffer-order))
+ (add-hook 'ednc-notification-presentation-functions #'jao-ednc--on-notify)
+ (ednc-mode))
+
+;;;###autoload
+(defun jao-ednc-pop ()
+ (interactive)
+ (pop-to-buffer-same-window ednc-log-name))
+
+;;;###autoload
+(defun jao-ednc-show ()
+ (interactive)
+ (if (not (jao-ednc--last-notification))
+ (jao-ednc-pop)
+ (jao-ednc--show-last)))
+
+;;;###autoload
+(defun jao-ednc-invoke-last-action ()
+ (interactive)
+ (if (jao-ednc--last-notification)
+ (ednc-invoke-action (jao-ednc--last-notification))
+ (message "No active notifications"))
+ (jao-ednc--clean))
+
+;;;###autoload
+(defun jao-ednc-dismiss ()
+ (interactive)
+ (when (jao-ednc--last-notification)
+ (ignore-errors
+ (with-current-buffer ednc-log-name
+ (ednc-dismiss-notification (jao-ednc--last-notification)))))
+ (jao-ednc--clean))
+
+;;;###autoload
+(defun jao-ednc-dismiss-all ()
+ (interactive)
+ (while (jao-ednc--last-notification)
+ (jao-ednc-dismiss)))
+
+(provide 'jao-ednc)
+;;; jao-ednc.el ends here
diff --git a/lib/eos/jao-embark-targets.el b/lib/eos/jao-embark-targets.el
new file mode 100644
index 0000000..1887b79
--- /dev/null
+++ b/lib/eos/jao-embark-targets.el
@@ -0,0 +1,97 @@
+;;; jao-embark-targets.el --- embark actions -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2021 jao
+
+;; Author: jao <mail@jao.io>
+;; Keywords: convenience
+
+;; 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:
+
+;; Embark targets and actions
+
+;;; Code:
+
+(require 'embark)
+
+(declare-function w3m-anchor "w3m")
+(declare-function org-link-any-re "ol")
+(declare-function org-open-link-from-string "ol")
+(declare-function org-in-regexp "org-macs")
+
+(defun jao-embark-targets--org-link ()
+ (when (derived-mode-p 'org-mode)
+ (when (org-in-regexp org-link-any-re)
+ (let ((lnk (match-string-no-properties 2)))
+ (if (string-match-p "http://.+" lnk)
+ (cons 'url lnk)
+ (cons 'org-link (match-string-no-properties 0)))))))
+
+(embark-define-keymap jao-embark-targets-org-link-map
+ "Actions for org links"
+ ((kbd "RET") org-open-link-from-string))
+
+(add-to-list 'embark-target-finders #'jao-embark-targets--org-link)
+(add-to-list 'embark-keymap-alist '(org-link . jao-embark-targets-org-link-map))
+
+(defvar jao-embark-targets-video-url-rx
+ (format "^https?://\\(?:www\\.\\)?%s/.+"
+ (regexp-opt '("youtu.be"
+ "youtube.com"
+ "blip.tv"
+ "vimeo.com"
+ "infoq.com")
+ t))
+ "A regular expression matching URLs that point to video streams")
+
+(defun jao-embark-targets--w3m-anchor ()
+ (when (not (region-active-p))
+ (when-let ((url (or (w3m-anchor) w3m-current-url)))
+ (cons 'url url))))
+
+(defun jao-embark-targets--refine-url (url)
+ (if (string-match-p jao-embark-targets-video-url-rx url)
+ (cons 'video-url url)
+ (cons 'url url)))
+
+(defun jao-embark-targets--play-video (player url)
+ (interactive "sURL: ")
+ (let ((cmd (format "%s %s" player (shell-quote-argument url))))
+ (start-process-shell-command player nil cmd)))
+
+(defun jao-embark-targets-mpv (&optional url)
+ "Play video stream with mpv"
+ (interactive "sURL: ")
+ (jao-embark-targets--play-video "mpv" url))
+
+(defun jao-embark-targets-vlc (&optional url)
+ "Play video stream with vlc"
+ (interactive "sURL: ")
+ (jao-embark-targets--play-video "vlc" url))
+
+(embark-define-keymap jao-embark-targets-video-url-map
+ "Actions on URLs pointing to remote video streams."
+ :parent embark-url-map
+ ("v" jao-embark-targets-vlc)
+ ("m" jao-embark-targets-mpv))
+
+(define-key embark-url-map (kbd "f") #'browse-url-firefox)
+
+(add-to-list 'embark-target-finders #'jao-embark-targets--w3m-anchor)
+(add-to-list 'embark-transformer-alist '(url . jao-embark-targets--refine-url))
+(add-to-list 'embark-keymap-alist '(video-url . jao-embark-targets-video-url-map))
+
+(provide 'jao-embark-targets)
+;;; jao-embark-targets.el ends here
diff --git a/lib/eos/jao-minibuffer.el b/lib/eos/jao-minibuffer.el
new file mode 100644
index 0000000..91662bf
--- /dev/null
+++ b/lib/eos/jao-minibuffer.el
@@ -0,0 +1,138 @@
+;;; jao-minibuffer.el --- using the minibuffer to report status -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020, 2021 jao
+
+;; Author: jao <mail@jao.io>
+;; Keywords: extensions
+
+;; 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:
+
+;; Simple asynchronous display of information in the minibuffer.
+
+;;; Code:
+
+(defvar jao-minibuffer-info ())
+(defvar jao-minibuffer-notification nil)
+(defvar jao-minibuffer-align-right-p t)
+(defvar jao-minibuffer-right-margin (if window-system 0 1))
+(defvar jao-minibuffer-maximized-frames-p nil)
+(defvar jao-minibuffer-frame-width nil)
+(defvar jao-minibuffer-notification-timeout 5)
+(defvar jao-minibuffer-enabled-p t)
+
+(defconst jao-minibuffer--name " *Minibuf-0*")
+
+(defun jao-minibuffer--trim (s w)
+ (if (<= (string-width (or s "")) w)
+ (format (format "%%%ds" (if jao-minibuffer-align-right-p w (- w))) s)
+ (substring s 0 w)))
+
+(defun jao-minibuffer--current ()
+ (with-current-buffer jao-minibuffer--name
+ (buffer-substring (point-min) (point-max))))
+
+(defun jao-minibuffer--width ()
+ (cond ((numberp jao-minibuffer-frame-width) jao-minibuffer-frame-width)
+ (jao-minibuffer-maximized-frames-p (frame-width))
+ (t (min (frame-width) (window-width (minibuffer-window))))))
+
+(defun jao-minibuffer--format-info ()
+ (mapconcat 'string-trim
+ (seq-filter (lambda (s) (not (string-blank-p s)))
+ (mapcar 'format-mode-line
+ (if jao-minibuffer-align-right-p
+ jao-minibuffer-info
+ (reverse jao-minibuffer-info))))
+ " "))
+
+(defun jao-minibuffer--aligned (&optional w currentp)
+ (let* ((msg (cond (currentp (jao-minibuffer--current))
+ (jao-minibuffer-notification
+ (format-mode-line jao-minibuffer-notification))
+ (t (jao-minibuffer--format-info))))
+ (msg (if jao-minibuffer-align-right-p
+ (string-trim msg)
+ (string-trim-left msg)))
+ (msg (propertize msg :minibuffer-message t)))
+ (when (not (string-empty-p msg))
+ (let* ((mw (jao-minibuffer--width))
+ (w (mod (or w (string-width (or (current-message) ""))) mw))
+ (w (- mw w jao-minibuffer-right-margin)))
+ (if (> w 0) (jao-minibuffer--trim msg w) "")))))
+
+(defun jao-minibuffer--set-message (msg)
+ (if current-minibuffer-command
+ msg
+ (let* ((msg (string-trim (replace-regexp-in-string "\n" " " msg)))
+ (msg (if (string-blank-p msg) msg (concat msg " "))))
+ (if jao-minibuffer-align-right-p
+ (concat msg (jao-minibuffer--aligned (string-width (or msg "")) t))
+ (concat (jao-minibuffer--aligned (+ 3 (string-width (or msg ""))) t)
+ " " msg)))))
+
+(defun jao-minibuffer--insert (msg)
+ (with-current-buffer jao-minibuffer--name
+ (erase-buffer)
+ (insert msg)))
+
+;;;###autoload
+(defun jao-minibuffer-refresh ()
+ (interactive)
+ (when jao-minibuffer-enabled-p
+ (jao-minibuffer--insert (or (jao-minibuffer--aligned) ""))))
+
+;;;###autoload
+(defun jao-minibuffer-add-variable (variable-name &optional order)
+ (add-to-ordered-list 'jao-minibuffer-info `(:eval ,variable-name) order))
+
+(defvar jao-minibuffer--notification-timer nil)
+
+(defun jao-minibuffer--start-notification-timer (timeout)
+ (interactive)
+ (when jao-minibuffer--notification-timer
+ (cancel-timer jao-minibuffer--notification-timer))
+ (setq jao-minibuffer--notification-timer
+ (run-with-idle-timer (or timeout jao-minibuffer-notification-timeout)
+ nil
+ 'jao-minibuffer-pop-notification)))
+
+;;;###autoload
+(defun jao-minibuffer-push-notification (msg &optional timeout)
+ (setq jao-minibuffer-notification msg)
+ (jao-minibuffer--start-notification-timer timeout)
+ (jao-minibuffer-refresh))
+
+;;;###autoload
+(defun jao-minibuffer-pop-notification ()
+ (interactive)
+ (setq jao-minibuffer-notification nil)
+ (jao-minibuffer-refresh))
+
+;;;###autoload
+(defun jao-minibuffer-toggle ()
+ (interactive)
+ (setq jao-minibuffer-enabled-p (not jao-minibuffer-enabled-p))
+ (if jao-minibuffer-enabled-p
+ (jao-minibuffer-refresh)
+ (jao-minibuffer--insert "")))
+
+(setq set-message-function #'jao-minibuffer--set-message)
+(setq clear-message-function #'jao-minibuffer-refresh)
+
+(setq resize-mini-windows nil)
+
+(provide 'jao-minibuffer)
+;;; jao-minibuffer.el ends here
diff --git a/lib/eos/jao-notify.el b/lib/eos/jao-notify.el
new file mode 100644
index 0000000..dc48ca4
--- /dev/null
+++ b/lib/eos/jao-notify.el
@@ -0,0 +1,33 @@
+;; jao-notify.el -- Interacting with notification daemon
+
+;; Copyright (c) 2017, 2019, 2020 Jose Antonio Ortega Ruiz
+
+;; Author: Jose Antonio Ortega Ruiz <jao@gnu.org>
+;; Start date: Sun Jan 08, 2017 20:24
+
+
+;;; Comentary:
+
+;; Simple notifications using echo or dbus notifications
+
+;;; Code:
+
+(defvar jao-notify-use-messages-p nil)
+(defvar jao-notify-timeout 5000)
+
+(declare-function notifications-notify "notifications")
+
+(defun jao-notify (msg &optional title icon)
+ (if jao-notify-use-messages-p
+ (message "%s%s%s" (or title "") (if title ": " "") (or msg ""))
+ (let* ((args `(:timeout ,jao-notify-timeout))
+ (args (append args
+ (if title `(:title ,title :body ,msg) `(:title ,msg))))
+ (args (if (and (stringp icon) (file-exists-p icon))
+ (append args `(:app-icon ,(format "%s" icon)))
+ args)))
+ (apply 'notifications-notify args))))
+
+
+(provide 'jao-notify)
+;;; jao-notify.el ends here
diff --git a/lib/eos/jao-osd.el b/lib/eos/jao-osd.el
new file mode 100644
index 0000000..acdc629
--- /dev/null
+++ b/lib/eos/jao-osd.el
@@ -0,0 +1,55 @@
+;; candy
+(defvar jao-osd-cat-color-fg "black")
+(defvar jao-osd-cat-color-bg "white")
+(defvar jao-osd-cat-font "Andika Basic 16")
+;; (setq jao-osd-cat-font "Inconsolata 20")
+(defun jao-osd-cat-font (&optional font)
+ (or font jao-osd-cat-font))
+
+(defun jao-osd-process-args (&optional font fg bg)
+ `("-n" ,(jao-osd-cat-font font)
+ "-R" ,(or bg jao-osd-cat-color-fg) "-B" ,(or fg jao-osd-cat-color-bg)
+ "-b" "200" "-r" "255"
+ "-e" "0" "-t" "2" "-d" "10" "-p" "0" "-x" "10" "-y" "10" "-u" "5000"))
+
+(setq jao-osd-processes (make-hash-table))
+
+(defsubst jao-osd--delete-process (name)
+ (remhash name jao-osd-processes))
+
+(defun jao-osd-process (name &optional font color)
+ (let ((proc (gethash name jao-osd-processes)))
+ (or (and proc (eq (process-status proc) 'run) proc)
+ (puthash name
+ (apply 'start-process
+ `("notifications"
+ ,(format "*notifications/%s*" name)
+ "aosd_cat"
+ ,@(jao-osd-process-args)))
+ jao-osd-processes))))
+
+(defun jao-osd-cat (name lines)
+ (let* ((proc (jao-osd-process name))
+ (lines (if (listp lines) lines (list lines)))
+ (trail (- 5 (length lines))))
+ (when proc
+ (dolist (line lines)
+ (send-string proc (format "%s\n" line))))))
+ ; (when (> trail 0) (send-string proc (make-string trail ?\n))))))
+
+(defun jao-osd--names ()
+ (let (names)
+ (maphash (lambda (n k) (push n names)) jao-osd-processes)
+ (reverse names)))
+
+(defun jao-osd-kill (name)
+ (let ((proc (gethash name jao-osd-processes)))
+ (when (processp proc)
+ (kill-process proc))))
+
+(defun jao-osd-kill-notifiers ()
+ (interactive)
+ (maphash (lambda (n p) (ignore-errors (kill-process p))) jao-osd-processes)
+ (clrhash jao-osd-processes))
+
+(provide 'jao-osd)
diff --git a/lib/eos/jao-sleep.el b/lib/eos/jao-sleep.el
new file mode 100644
index 0000000..93da0e7
--- /dev/null
+++ b/lib/eos/jao-sleep.el
@@ -0,0 +1,58 @@
+;;; jao-sleep.el --- Actions upon sleep/awake -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020 jao
+
+;; Author: jao <mail@jao.io>
+;; Keywords: hardware
+
+;; 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/>.
+
+;;; Code:
+
+(require 'dbus)
+
+(defvar jao-sleep-sleep-functions nil)
+(defvar jao-sleep-awake-functions nil)
+
+(defvar jao-sleep--dbus-registration-object nil)
+
+(defun jao-sleep--dbus-sleep-handler (sleep-start)
+ (condition-case nil
+ (if sleep-start
+ (progn (message "Running on sleep functions")
+ (run-hooks 'jao-sleep-sleep-functions))
+ (message "Running on awake functions")
+ (run-hooks 'jao-sleep-awake-functions))
+ (error (message "There was an error running %s" sleep-start))))
+
+;;;###autoload
+(defun jao-sleep-dbus-register (&optional session-dbus)
+ "Register actions to take on sleep and on awake, using the system D-BUS."
+ (when (featurep 'dbusbind)
+ (setq jao-sleep--dbus-sleep-registration-object
+ (dbus-register-signal (if session-dbus :session :system)
+ "org.freedesktop.login1"
+ "/org/freedesktop/login1"
+ "org.freedesktop.login1.Manager"
+ "PrepareForSleep"
+ #'jao-sleep--dbus-sleep-handler))))
+
+;;;###autoload
+(defun jao-sleep-dbus-unregister ()
+ (condition-case nil
+ (dbus-unregister-object jao-sleep--dbus-sleep-registration-object)
+ (wrong-type-argument nil)))
+
+(provide 'jao-sleep)
+;;; jao-sleep.el ends here
diff --git a/lib/media/espotify.org b/lib/media/espotify.org
new file mode 100644
index 0000000..93338a9
--- /dev/null
+++ b/lib/media/espotify.org
@@ -0,0 +1,627 @@
+#+title: consulting spotify
+#+date: <2021-01-08 04:02>
+#+filetags: emacs
+#+PROPERTY: header-args :tangle yes :comments no :results silent
+
+(/Note/: you can tangle this file (e.g., with =C-c C-v t= inside Emacs)
+into three elisp libraries, =espotify.el=, =espotify-consult.el,
+=espotify-embark=. and =espotify-counsel=)
+
+We have two kinds of interaction with Spotify: via its HTTP API to
+perform operations such as search, and via our local DBUS to talk to
+client players running in our computer, such as the official client,
+[[https://github.com/Spotifyd/spotifyd][spotifyd]] or [[https://mopidy.com/ext/spotify/][mopidy-spotify]]. Our goal is to obtain via the former a
+track or album identifier that we can send then to the latter to play,
+with emacs completion mechanisms (consult and friends in this case)
+providing the glue between both worlds.
+
+Let's start with an umbrella customization group:
+#+begin_src emacs-lisp
+ ;;; espotify.el - spotify search and play - -*- lexical-binding: t; -*-
+
+ (defgroup espotify nil
+ "Access to Spotify API and clients"
+ :group 'multimedia)
+#+end_src
+
+* Access to Spotify's API: authentication
+
+ I am stealing most of the ideas on how to establish authenticated
+ connections to the Spotify API and performing queries from
+ [[https://github.com/Lautaro-Garcia/counsel-spotify][counsel-spotify]], with many simplifications.
+
+ We start defining a couple of end-points:
+
+ #+begin_src emacs-lisp
+ (defvar espotify-spotify-api-url "https://api.spotify.com/v1")
+ (defvar espotify-spotify-api-authentication-url
+ "https://accounts.spotify.com/api/token")
+ #+end_src
+
+ And we're going to need as well a client id and secret for our
+ application, which i am again defining as variables since i expect
+ them to be set in some secure manner instead of via customize:
+
+ #+begin_src emacs-lisp
+ (defvar espotify-client-id nil "Spotify application client ID.")
+ (defvar espotify-client-secret nil "Spotify application client secret.")
+ #+end_src
+
+ To get valid values for them, one just needs to [[https://developer.spotify.com/my-applications][register a Spotify
+ application]]. From them we can derive a base64-encoded credentials
+ value:
+
+ #+begin_src emacs-lisp
+ (defun espotify--basic-auth-credentials ()
+ (let ((credential (concat espotify-client-id ":" espotify-client-secret)))
+ (concat "Basic " (base64-encode-string credential t))))
+ #+end_src
+
+ The return value of the function above is to be used as the
+ "Authorization" header of our requests to the authorization
+ end-point, which is going to answer with an authorization token
+ that we can then use to further requests. Let's define a function to
+ wrap that operation:
+
+ #+begin_src emacs-lisp
+ (defun espotify--with-auth-token (callback)
+ (let ((url-request-method "POST")
+ (url-request-data "&grant_type=client_credentials")
+ (url-request-extra-headers
+ `(("Content-Type" . "application/x-www-form-urlencoded")
+ ("Authorization" . ,(espotify--basic-auth-credentials)))))
+ (url-retrieve espotify-spotify-api-authentication-url
+ (lambda (_status)
+ (goto-char url-http-end-of-headers)
+ (funcall callback
+ (alist-get 'access_token (json-read)))))))
+ #+end_src
+
+ For instance:
+ #+begin_src emacs-lisp :load no :tangle no
+ (espotify--with-auth-token
+ (lambda (token) (message "Your token is: %s" token)))
+ #+end_src
+
+ obtains an auth token and prints it as a message. Note that ~body~
+ is evaluated asynchronously by ~url-retrieve~, so invocations to
+ ~espotify-with-auth-token~ evaluate to the request's buffer and are
+ usually discarded.
+
+* Search queries using the Spotify API
+
+ We are interested in performing a search for some ~term~, of items
+ of a given ~types~ (~:track~, ~:album~, ~:artist~, etc.), possibly with an
+ additional ~filter~. That's specified in a GET request's URL
+ as constructed by this function:
+
+ #+begin_src emacs-lisp
+ (defun espotify--make-search-url (term types &optional filter)
+ (when (null types)
+ (error "Must supply a non-empty list of types to search for"))
+ (let ((term (url-encode-url term)))
+ (format "%s/search?q=%s&type=%s&limit=50"
+ espotify-spotify-api-url
+ (if filter (format "%s:%s" filter term) term)
+ (mapconcat #'symbol-name types ","))))
+ #+end_src
+
+ For instance:
+
+ #+begin_src emacs-lisp :load no :tangle no :results replace
+ (espotify--make-search-url "dream blue turtles" '(album))
+ #+end_src
+
+ #+RESULTS:
+ : https://api.spotify.com/v1/search?q=dream%20blue%20turtles&type=album&limit=50
+
+ If we have an [[*Access to Spotify's API: authentication][authorisation token]] and a search URL in our hands,
+ we can use them as in the following helper function, which will
+ calls the given callback with the results of the query:
+
+ #+begin_src emacs-lisp
+ (defun espotify--with-query-results (token url callback)
+ (let ((url-request-extra-headers
+ `(("Authorization" . ,(concat "Bearer " token)))))
+ (url-retrieve url
+ (lambda (_status)
+ (goto-char url-http-end-of-headers)
+ (funcall callback
+ (let ((json-array-type 'list))
+ (thread-first
+ (buffer-substring (point) (point-max))
+ (decode-coding-string 'utf-8)
+ (json-read-from-string))))))))
+ #+end_src
+
+ So we can combine this macro with ~espotify--with-auth-token~ in a
+ single search function that takes a callback that will be applied
+ to a given query, specified as a triple of term, types and filter:
+
+ #+begin_src emacs-lisp
+ (defun espotify-get (callback url)
+ (espotify--with-auth-token
+ (lambda (token)
+ (espotify--with-query-results token url callback))))
+
+ (defun espotify-search (callback term types &optional filter)
+ (espotify-get callback (espotify--make-search-url term types filter)))
+ #+end_src
+
+ For instance:
+ #+begin_src emacs-lisp :load no :tangle no
+ (defvar espotify-query-result nil)
+ (espotify-search (lambda (res) (setq espotify-query-result res))
+ "dream blue turtles"
+ '(album artist))
+ (sit-for 0)
+ #+end_src
+
+ #+begin_src emacs-lisp :load no :tangle no :results replace
+ (mapcar 'car espotify-query-result)
+ #+end_src
+
+ #+RESULTS:
+ | albums | artists |
+
+ So Spotify is returning a results entry per type, which in turn,
+ contains an ~items~ with the list of actual results. So let's
+ provide an interface for a callback that takes as many lists of
+ items as types it asks for:
+
+ #+begin_src emacs-lisp
+ (defun espotify--type-items (res type)
+ (alist-get 'items (alist-get (intern (format "%ss" type)) res)))
+
+ (defun espotify-search* (callback term types &optional filter)
+ (let* ((types (if (listp types) types (list types)))
+ (cb (lambda (res)
+ (let ((its (mapcar (lambda (tp)
+ (espotify--type-items res tp))
+ types)))
+ (apply callback its)))))
+ (espotify-search cb term types filter)))
+ #+end_src
+
+ For example:
+
+ #+begin_src emacs-lisp :load no :tangle no
+ (defvar espotify-query-result nil)
+ (espotify-search* (lambda (al ar)
+ (message "Found %s albums, %s artists"
+ (length al) (length ar))
+ (setq espotify-query-result (cons al ar)))
+ "blue turtles"
+ '(album artist))
+ (sit-for 0)
+ (list (mapcar 'car (car (car espotify-query-result)))
+ (mapcar 'car (car (cdr espotify-query-result))))
+ #+end_src
+
+ #+RESULTS:
+ | album_type | artists | available_markets | external_urls | href | id | images | name | release_date | release_date_precision | total_tracks | type | uri |
+ | external_urls | followers | genres | href | id | images | name | popularity | type | uri | | | |
+
+ Another strategy would be to search for several types and pass to
+ our callback the concatenation of all items:
+
+ #+begin_src emacs-lisp
+ (defun espotify-search-all (callback term &optional types filter)
+ (let ((types (or types '(album track artist playlist))))
+ (espotify-search* (lambda (&rest items)
+ (funcall callback (apply 'append items)))
+ term
+ types
+ filter)))
+ #+end_src
+
+* Listing user resources in the Spotify API
+
+ It is also possible to obtain lists of items of a given type for the
+ current user, with a standard URL format:
+
+ #+begin_src emacs-lisp
+ (defun espotify--make-user-url (type)
+ (format "%s/me/%ss" espotify-spotify-api-url (symbol-name type)))
+ #+end_src
+
+ and we can then use ~espotify-get~ to offer access to our playlists,
+ albums, etc.:
+
+ #+begin_src emacs-lisp
+ (defun espotify-with-user-resources (callback type)
+ (espotify-get (lambda (res) (funcall callback (alist-get 'items res)))
+ (espotify--make-user-url type)))
+ #+end_src
+
+* Sending commands to local players
+
+ Once we now the URI we want to play (that ~uri~ entry in our items),
+ sending it to a local player via DBUS is fairly easy. Let's
+ define a couple of customizable variables pointing to the service
+ name and bus:
+
+ #+begin_src emacs-lisp
+ (defcustom espotify-service-name "mopidy"
+ "Name of the DBUS service used by the client we talk to.
+
+ The official Spotify client uses `spotify', but one can also use
+ alternative clients such as mopidy or spotifyd."
+ :type 'string)
+
+ (defcustom espotify-use-system-bus-p t
+ "Whether to access the spotify client using the system DBUS.")
+ #+end_src
+
+ and then using the Emacs DBUS API to send methods to it is a
+ breeze:
+
+ #+begin_src emacs-lisp
+ (defun espotify-call-spotify-via-dbus (method &rest args)
+ "Tell Spotify to execute METHOD with ARGS through DBUS."
+ (apply #'dbus-call-method `(,(if espotify-use-system-bus-p :system :session)
+ ,(format "org.mpris.MediaPlayer2.%s"
+ espotify-service-name)
+ "/org/mpris/MediaPlayer2"
+ "org.mpris.MediaPlayer2.Player"
+ ,method
+ ,@args)))
+
+ (defun espotify-play-uri (uri)
+ (espotify-call-spotify-via-dbus "OpenUri" uri))
+ #+end_src
+
+* Search front-end using consult
+ :PROPERTIES:
+ :header-args: :tangle espotify-consult.el
+ :END:
+
+ I am exploring [[https://github.com/minad/consult][consult.el]] (and friends) to replace ivy/counsel,
+ inspired in part by [[https://protesilaos.com/codelog/2021-01-06-emacs-default-completion/][Protesilaos Stavrou's musings]], and liking a
+ lot what i see. Up till now, everything i had with counsel is
+ supported, often in better ways, with one exception: completing
+ search of spotify albums using [[https://github.com/Lautaro-Garcia/counsel-spotify][counsel-spotify]]. So let's fix that
+ by defining an asynchronous consult function that does precisely
+ that!
+
+ The top-level command will have this form:
+
+ #+begin_src emacs-lisp
+ ;;; espotify-consult.el - consult support - -*- lexical-binding: t; -*-
+
+ (require 'espotify)
+ (require 'consult)
+
+ (defvar espotify-consult-history nil)
+
+ (defun espotify-consult-by (type &optional filter)
+ (let ((orderless-matching-styles '(orderless-literal)))
+ (consult--read (format "Search %ss: " type)
+ (espotify--search-generator type filter)
+ :lookup 'espotify--consult-lookup
+ :category 'espotify-search-item
+ :history 'espotify-consult-history
+ :initial consult-async-default-split
+ :require-match t)))
+ #+end_src
+
+ where we can write an asynchronous generator of search results
+ with the helper function:
+
+ #+begin_src emacs-lisp
+ (defun espotify--search-generator (type filter)
+ (thread-first (consult--async-sink)
+ (consult--async-refresh-immediate)
+ (consult--async-map #'espotify--format-item)
+ (espotify--async-search type filter)
+ (consult--async-throttle)
+ (consult--async-split)))
+ #+end_src
+
+ The above follows a generic consult pattern, where all functions
+ are pre-defined for us except ~espotify--async-search~, an
+ asynchronous dispatcher closure that must generate and handle a
+ list of candidates, responding to a set of action messages (init,
+ reset, get, flush, etc.) [fn:1] Here's its definition in our
+ case:
+
+ #+begin_src emacs-lisp
+ (defun espotify--async-search (next type filter)
+ (let ((current ""))
+ (lambda (action)
+ (pcase action
+ ((pred stringp)
+ (when-let (term (espotify-check-term current action))
+ (setq current term)
+ (espotify-search-all
+ (lambda (x)
+ (funcall next 'flush)
+ (funcall next x))
+ current
+ type
+ filter)))
+ (_ (funcall next action))))))
+ #+end_src
+
+ We have introduced the convention that we're only launching a search
+ when the input string ends in "=", to avoid piling on HTTP
+ requests, and also played a bit with Levenshtein distance, both via
+ the function =espotify-check-search-term=:
+
+ #+begin_src emacs-lisp :tangle espotify.el
+ (defvar espotify-search-suffix "="
+ "Suffix in the search string launching an actual Web query.")
+
+ (defvar espotify-search-threshold 8
+ "Threshold to automatically launch an actual Web query.")
+
+ (defun espotify-check-term (prev new)
+ (when (not (string-blank-p new))
+ (cond ((string-suffix-p espotify-search-suffix new)
+ (substring new 0 (- (length new) (length espotify-search-suffix))))
+ ((>= (string-distance prev new) espotify-search-threshold) new))))
+ #+end_src
+
+ In the consult case, a more natural choice for the search suffix is
+
+ #+begin_src emacs-lisp
+ (setq espotify-search-suffix consult-async-default-split)
+ #+end_src
+
+ When processing the results, we format them as a displayable
+ string, while hiding in a property the URI that will allow us to
+ play the item (and pass the formatter to ~consult-async--map~, in
+ ~espotify--search-generator~ above):
+
+ #+begin_src emacs-lisp :tangle espotify.el
+ (defun espotify--additional-info (x)
+ (mapconcat 'identity
+ (seq-filter 'identity
+ `(,(alist-get 'name (alist-get 'album x))
+ ,(alist-get 'name (car (alist-get 'artists x)))
+ ,(alist-get 'display_name (alist-get 'owner x))))
+ ", "))
+
+ (defun espotify--format-item (x)
+ (propertize (format "%s%s"
+ (alist-get 'name x)
+ (if-let ((info (espotify--additional-info x)))
+ (format " (%s)" info)
+ ""))
+ 'espotify-item x))
+
+ (defun espotify--item (cand)
+ (get-text-property 0 'espotify-item cand))
+
+ (defun espotify--uri (cand)
+ (alist-get 'uri (espotify--item cand)))
+ #+end_src
+
+ and then we make sure that we access that original string when
+ consult looks up for it using the ~:lookup~ function, which we can
+ simply define as:
+
+ #+begin_src emacs-lisp
+ (require 'seq)
+ (defun espotify--consult-lookup (_input cands cand)
+ (seq-find (lambda (x) (string= cand x)) cands))
+ #+end_src
+
+
+ With that, when we receive the final result from ~consult--read~,
+ we can play the selected URI right away:
+
+ #+begin_src emacs-lisp :tangle espotify.el
+ (defun espotify--maybe-play (cand)
+ (when-let (uri (when cand (espotify--uri cand)))
+ (espotify-play-uri uri)))
+ #+end_src
+
+ And here, finally, are our interactive command to search and play
+ albums using consult:
+
+ #+begin_src emacs-lisp
+ (defun espotify-consult-album (&optional filter)
+ (interactive)
+ (espotify--maybe-play (espotify-consult-by 'album filter)))
+ #+end_src
+
+ And likewise for playlists, artists and combinations thereof:
+
+ #+begin_src emacs-lisp
+ (defun espotify-consult-artist (&optional filter)
+ (interactive)
+ (espotify--maybe-play (espotify-consult-by 'artist filter)))
+
+ (defun espotify-consult-track (&optional filter)
+ (interactive)
+ (espotify--maybe-play (espotify-consult-by 'track filter)))
+
+ (defun espotify-consult-playlist (&optional filter)
+ (interactive)
+ (espotify--maybe-play (espotify-consult-by 'playlist filter)))
+ #+end_src
+
+* Adding metadata to candidates using Marginalia
+ :PROPERTIES:
+ :header-args: :tangle espotify-consult.el
+ :END:
+
+ Let's add metadata fields to our candidates, so that packages like
+ [[https://github.com/minad/marginalia][Marginalia]] can offer it to consult or selectrum.
+
+ #+begin_src emacs-lisp
+ (defun espotify-marginalia-annotate (cand)
+ (when-let (x (espotify--item cand))
+ (marginalia--fields
+ ((alist-get 'type x "") :face 'marginalia-mode :width 10)
+ ((if-let (d (alist-get 'duration_ms x))
+ (let ((secs (/ d 1000)))
+ (format "%02d:%02d" (/ secs 60) (mod secs 60)))
+ ""))
+ ((if-let (d (alist-get 'total_tracks x)) (format "%s tracks" d) "")
+ :face 'marginalia-size :width 12)
+ ((if-let (d (alist-get 'release_date (alist-get 'album x x)))
+ (format "%s" d)
+ "")
+ :face 'marginalia-date :width 10))))
+
+ (add-to-list 'marginalia-annotators-heavy
+ '(espotify-search-item . espotify-marginalia-annotate))
+ #+end_src
+
+* Embark actions
+ :PROPERTIES:
+ :header-args: :tangle espotify-embark.el
+ :END:
+
+ In addition to the default action (play the URI in the selected
+ candidate), we can use embark to define other operations. For
+ instance, we could print the full item alist in its own buffer, or
+ always look for an album to play:
+
+ #+begin_src emacs-lisp
+ (require 'espotify-consult)
+ (require 'embark)
+
+ (defvar espotify--current-item nil)
+
+ (defun espotify--show-info (name)
+ "Show low-level info (an alist) about selection."
+ (interactive "s")
+ (pop-to-buffer (get-buffer-create "*espotify info*"))
+ (read-only-mode -1)
+ (delete-region (point-min) (point-max))
+ (insert (propertize name 'face 'bold))
+ (newline)
+ (when espotify--current-item
+ (insert (pp-to-string espotify--current-item)))
+ (newline)
+ (goto-char (point-min))
+ (read-only-mode 1))
+
+ (defun espotify--play-album (ignored)
+ "Play album associated with selected item."
+ (interactive "i")
+ (if-let (album (if (string= "album"
+ (alist-get 'type espotify--current-item ""))
+ espotify--current-item
+ (alist-get 'album espotify--current-item)))
+ (espotify-play-uri (alist-get 'uri album))
+ (error "No album for %s" (alist-get 'nmae espotify--current-item))))
+
+ (embark-define-keymap espotify-item-keymap
+ "Actions for Spotify search results"
+ ("a" espotify--play-album)
+ ("h" espotify--show-info))
+
+ (defun espotify--annotate-item (cand)
+ (setq espotify--current-item (espotify--item cand))
+ (cons 'espotify-search-item cand))
+
+ (add-to-list 'embark-transformer-alist
+ '(espotify-search-item . espotify--annotate-item))
+
+ (add-to-list 'embark-keymap-alist
+ '(espotify-search-item . espotify-item-keymap))
+ #+end_src
+
+* Search fronted using ivy
+ :PROPERTIES:
+ :header-args: :tangle espotify-counsel.el
+ :END:
+
+ #+begin_src emacs-lisp
+ ;;; counsel-espotify.el - counsel and spotify - -*- lexical-binding: t; -*-
+ (require 'espotify)
+ (require 'ivy)
+ #+end_src
+
+ It is is also not too complicated to provide a counsel collection of
+ functions. Here, we use =ivy-read= to access the completion
+ interface, with the flag =dynamic-collection= set. Ivy will wait
+ until we call =ivy-candidate-updates= with our items.
+
+ #+begin_src emacs-lisp
+ (defun espotify-counsel--search-by (type filter)
+ (let ((current-term ""))
+ (lambda (term)
+ (when-let (term (espotify-check-term current-term term))
+ (espotify-search-all (lambda (its)
+ (let ((cs (mapcar #'espotify--format-item its)))
+ (ivy-update-candidates cs)))
+ (setq current-term term)
+ type
+ filter))
+ 0)))
+ #+end_src
+
+ With that, we can define our generic completing read:
+
+ #+begin_src emacs-lisp
+
+ (defun espotify-counsel--play-album (candidate)
+ "Play album associated with selected item."
+ (interactive "s")
+ (let ((item (espotify--item candidate)))
+ (if-let (album (if (string= "album" (alist-get 'type item ""))
+ item
+ (alist-get 'album item)))
+ (espotify-play-uri (alist-get 'uri album))
+ (error "No album for %s" (alist-get 'name item)))))
+
+ (defun espotify-search-by (type filter)
+ (ivy-read (format "Search %s: " type)
+ (espotify-counsel--search-by type filter)
+ :dynamic-collection t
+ :action `(1 ("a" espotify-counsel--play-album "Play album")
+ ("p" espotify--maybe-play ,(format "Play %s" type)))))
+ #+end_src
+
+ and our collection of searching commands:
+
+ #+begin_src emacs-lisp
+ (defun espotify-counsel-album (&optional filter)
+ (interactive)
+ (espotify-search-by 'album filter))
+
+ (defun espotify-counsel-artist (&optional filter)
+ (interactive)
+ (espotify-search-by 'artist filter))
+
+ (defun espotify-counsel-track (&optional filter)
+ (interactive)
+ (espotify-search-by 'track filter))
+
+ (defun espotify-counsel-playlist (&optional filter)
+ (interactive)
+ (espotify-search-by 'playlist filter))
+ #+end_src
+
+ Simpler than our initial consult, although it's true that we already
+ had part of the job done. The nice "split search" that counsult
+ offers out of the box, though, is much more difficult to get.
+
+* Postamble
+
+ #+begin_src emacs-lisp
+ (provide 'espotify)
+ #+end_src
+
+ #+begin_src emacs-lisp :tangle espotify-consult.el
+ (provide 'espotify-consult)
+ #+end_src
+
+ #+begin_src emacs-lisp :tangle espotify-embark.el
+ (provide 'espotify-embark)
+ #+end_src
+
+ #+begin_src emacs-lisp :tangle espotify-counsel.el
+ (provide 'espotify-counsel)
+ #+end_src
+
+* Footnotes
+
+[fn:1] This is an elegant strategy i first learnt about in SICP, many,
+many years ago, and i must say that it is very charming to find it
+around in the wild!
diff --git a/lib/media/jao-emms-info-track.el b/lib/media/jao-emms-info-track.el
new file mode 100644
index 0000000..839ef73
--- /dev/null
+++ b/lib/media/jao-emms-info-track.el
@@ -0,0 +1,212 @@
+;; jao-emms-info-track.el -- utilities to show tracks -*- lexical-binding:t; -*-
+
+;; Copyright (C) 2009, 2010, 2013, 2017, 2020, 2021 Jose Antonio Ortega Ruiz
+
+;; Author: Jose Antonio Ortega Ruiz <jao@gnu.org>
+;; Start date: Sat Jul 04, 2009 13:47
+
+;; 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 of the License, or
+;; (at your option) any later version.
+
+;; This file 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/>.
+
+;;; Code:
+
+(require 'emms)
+(require 'emms-tag-editor)
+(require 'emms-player-mpd)
+(require 'jao-osd)
+(require 'jao-emms)
+(require 'jao-minibuffer)
+
+(defgroup jao-emms-faces nil "Faces"
+ :group 'faces
+ :group 'jao-emms)
+
+(defface jao-emms-font-lock-album '((t (:foreground "lightgoldenrod2")))
+ "Album name in EMMS track message."
+ :group 'jao-emms-faces)
+
+(defface jao-emms-font-lock-track '((t (:bold t)))
+ "Track number in EMMS track message."
+ :group 'jao-emms-faces)
+
+(defface jao-emms-font-lock-title '((t (:foreground "dodgerblue2")))
+ "Track title in EMMS track message."
+ :group 'jao-emms-faces)
+
+(defface jao-emms-font-lock-artist '((t (:foreground "dodgerblue3")))
+ "Artist name in EMMS track message."
+ :group 'jao-emms-faces)
+
+(defcustom jao-emms-show-osd-p nil
+ "Whether to show osd notices on track change"
+ :group 'jao-emms)
+
+
+
+(defun jao-emms-info-track-stream (track)
+ "Return track info for streams"
+ (let ((name (emms-track-name track))
+ (title (or (emms-track-get track 'title nil)
+ (car (emms-track-get track 'metadata nil))
+ (car (split-string (shell-command-to-string "mpc status")
+ "\n")))))
+ (format "♪ %s (%s)" (or title "") (if title (emms-track-type track) name))))
+
+(defsubst jao--put-face (str face)
+ (put-text-property 0 (length str) 'face face str)
+ str)
+
+(defun jao-emms--to-number (x)
+ (or (and (numberp x) x)
+ (and (stringp x)
+ (string-match "\\`\\(:?[0-9]+\\)" x)
+ (string-to-number (match-string 1 x)))))
+
+(defun jao-emms--fmt-time (x suffix)
+ (if x (format "%02d:%02d%s" (/ x 60) (mod x 60) (or suffix "")) ""))
+
+(defun jao-emms--fmt-song-times (track lapsed pre post)
+ (if lapsed
+ (let ((time (when track (emms-track-get track 'info-playing-time))))
+ (format "%s%s%s%s"
+ (or pre "")
+ (jao-emms--fmt-time lapsed (when time "/"))
+ (jao-emms--fmt-time time "")
+ (or post "")))
+ ""))
+
+(defun jao-emms-info-track-file (track &optional lapsed plen titlesep)
+ "Return a description of the current track."
+ (let* ((no (jao-emms--to-number (emms-track-get track 'info-tracknumber "0")))
+ (time (emms-track-get track 'info-playing-time))
+ (year (emms-track-get track 'info-year))
+ (year (if year (format " (%s)" year) ""))
+ (artist (emms-track-get track 'info-artist ""))
+ (composer (emms-track-get track 'info-composer nil))
+ (title (emms-track-get track 'info-title ""))
+ (album (emms-track-get track 'info-album))
+ (last-played (or (emms-track-get track 'last-played) '(0 0 0)))
+ (play-count (or (emms-track-get track 'play-count) 0))
+ (playlength (if plen (format "/%02d" (string-to-number plen)) "")))
+ (if (or (not title) (not album))
+ (emms-track-simple-description track)
+ (format "🎵 %s%s%s%s%s%s%s"
+ (jao--put-face (if (zerop no) "" (format "%02d%s " no playlength))
+ 'jao-emms-font-lock-track)
+ (jao--put-face title
+ 'jao-emms-font-lock-title)
+ (or titlesep " ")
+ (jao-emms--fmt-song-times track lapsed "[" "] ")
+ (jao--put-face artist 'jao-emms-font-lock-artist)
+ (jao--put-face (if composer (format " [%s]" composer) "")
+ 'jao-emms-font-lock-artist)
+ (jao--put-face (if album
+ (format " (%s%s)" album year)
+ (format "%s *") year)
+ 'jao-emms-font-lock-album)))))
+
+;;;###autoload
+(defun jao-emms-info-track-description (track &optional lapsed plen tsep)
+ (if (memq (emms-track-type track) '(streamlist url))
+ (jao-emms-info-track-stream track)
+ (jao-emms-info-track-file track lapsed plen tsep)))
+
+;;;###autoload
+(defun jao-emms-toggle-osd ()
+ (interactive)
+ (setq jao-emms-show-osd-p (not jao-emms-show-osd-p))
+ (message "Emms OSD %s" (if jao-emms-show-osd-p "enabled" "disabled")))
+
+(defvar jao-emms-show-icon nil)
+
+(defun jao-emms--with-mpd-track (callback)
+ (emms-player-mpd-get-status
+ nil
+ (lambda (_ st)
+ (let* ((lapsed (jao-emms--to-number (cdr (assoc "time" st))))
+ (plen (cdr (assoc "playlistlength" st)))
+ (song (jao-emms--to-number (cdr (assoc "song" st))))
+ (track (emms-playlist-current-selected-track)))
+ (when (and track song)
+ (emms-track-set track 'info-tracknumber (format "%d" (1+ song))))
+ (funcall callback track lapsed plen)))))
+
+;;;###autoload
+(defun jao-emms-show-osd ()
+ (interactive)
+ (jao-emms--with-mpd-track
+ (lambda (track lapsed play-len)
+ (let* ((sep "~~~~~")
+ (s (jao-emms-info-track-description track lapsed play-len sep))
+ (s (substring-no-properties s 2))
+ (cs (split-string s sep)))
+ (jao-notify (car cs) (cadr cs) jao-emms-show-icon)))))
+
+(defun jao-emms-show-osd-hook ()
+ (interactive)
+ (when jao-emms-show-osd-p (jao-emms-show-osd)))
+
+(defun jao-emms-install-id3v2 ()
+ (add-to-list 'emms-tag-editor-tagfile-functions
+ '("mp3" "id3v2" ((info-artist . "-a")
+ (info-title . "-t")
+ (info-album . "-A")
+ (info-tracknumber . "-T")
+ (info-year . "-y")
+ (info-genre . "-g")
+ (info-composer . "--TCOM")
+ (info-note . "-c")))))
+
+(defvar jao-emms-echo-string "")
+
+(defun jao-emms--echo-string (v)
+ (setq jao-emms-echo-string v)
+ (jao-minibuffer-refresh))
+
+(defun jao-emms-update-echo-string (&optional existing-track)
+ (if emms-player-playing-p
+ (jao-emms--with-mpd-track
+ (lambda (track lapsed play-len)
+ (jao-emms--echo-string
+ (cond ((and emms-player-paused-p existing-track)
+ (format "(%s/%s)"
+ (emms-track-get existing-track 'info-tracknumber)
+ play-len))
+ (emms-player-paused-p "")
+ (t (jao-emms-info-track-description track nil play-len))))))
+ (jao-emms--echo-string "")))
+
+(defun jao-emms-enable-minibuffer (minibuffer-order)
+ (jao-minibuffer-add-variable 'jao-emms-echo-string minibuffer-order)
+ (dolist (h '(emms-track-updated-functions
+ emms-player-finished-hook
+ emms-player-stopped-hook
+ emms-player-started-hook
+ emms-player-paused-hook))
+ (add-hook h #'jao-emms-update-echo-string)))
+
+;;;###autoload
+(defun jao-emms-info-setup (&optional minibuffer show-osd show-echo-line id3)
+ (setq emms-track-description-function 'jao-emms-info-track-description)
+ (setq jao-emms-show-osd-p show-osd)
+ (add-hook 'emms-player-started-hook 'jao-emms-show-osd-hook)
+ (when minibuffer (jao-emms-enable-minibuffer minibuffer))
+ (unless show-echo-line
+ (eval-after-load 'emms-player-mpd
+ '(remove-hook 'emms-player-started-hook 'emms-player-mpd-show)))
+ (when id3 (jao-emms-install-id3v2))
+ (ignore-errors (emms-player-mpd-connect)))
+
+
+(provide 'jao-emms-info-track)
+;;; jao-emms-info-track.el ends here
diff --git a/lib/media/jao-emms-lyrics.el b/lib/media/jao-emms-lyrics.el
new file mode 100644
index 0000000..0ea52e0
--- /dev/null
+++ b/lib/media/jao-emms-lyrics.el
@@ -0,0 +1,41 @@
+;; jao-emms-lyrics.el -- simple show lyrics in emms
+
+;; Copyright (C) 2009, 2010, 2017, 2019, 2020 Jose Antonio Ortega Ruiz
+
+;; Author: Jose Antonio Ortega Ruiz <jao@gnu.org>
+;; Start date: Sat Jul 04, 2009 13:41
+
+;; 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 of the License, or
+;; (at your option) any later version.
+
+;; This file 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/>.
+
+;;; Code:
+
+(require 'emms)
+(require 'jao-lyrics)
+
+;;;###autoload
+(defun jao-emms-lyrics-track-data ()
+ (let ((track (or (emms-playlist-current-selected-track)
+ (error "No playing track"))))
+ (cons (or (emms-track-get track 'info-artist nil)
+ (error "No artist"))
+ (or (emms-track-get track 'info-title nil)
+ (error "No artist")))))
+
+;;;###autoload
+(defun jao-emms-show-lyrics (&optional force)
+ (let ((jao-lyrics-info-function 'jao-emms-lyrics-track-data))
+ (jao-show-lyrics force)))
+
+(provide 'jao-emms-lyrics)
+;;; jao-emms-lyrics.el ends here
diff --git a/lib/media/jao-emms-random-album.el b/lib/media/jao-emms-random-album.el
new file mode 100644
index 0000000..72e056b
--- /dev/null
+++ b/lib/media/jao-emms-random-album.el
@@ -0,0 +1,118 @@
+;; jao-emms-random-album.el -- play random albums in emms
+
+;; Copyright (C) 2009, 2010, 2017, 2018, 2020 Jose Antonio Ortega Ruiz
+
+;; Author: Jose Antonio Ortega Ruiz <jao@gnu.org>
+;; Start date: Sat Jul 04, 2009 13:06
+
+;; 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 of the License, or
+;; (at your option) any later version.
+
+;; This file 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/>.
+
+
+(require 'emms)
+(require 'jao-minibuffer)
+
+(defvar jao-emms-random-album-p t)
+(defvar jao-emms-random-lines nil)
+(defvar jao-emms-random-lines-file
+ (expand-file-name "~/.emacs.d/random-lines"))
+(defvar jao-emms-random-album-notify-p t)
+(defvar jao-emms-random-album-notify-icon nil)
+
+(defun jao-emms-random-lines ()
+ (or jao-emms-random-lines
+ (and (file-exists-p jao-emms-random-lines-file)
+ (with-current-buffer
+ (find-file-noselect jao-emms-random-lines-file)
+ (goto-char (point-min))
+ (setq jao-emms-random-lines (read (current-buffer)))))
+ (dotimes (n (1- (line-number-at-pos (point-max)))
+ jao-emms-random-lines)
+ (push (1+ n) jao-emms-random-lines))))
+
+(defun jao-emms-random-lines-save ()
+ (with-current-buffer (find-file-noselect jao-emms-random-lines-file)
+ (delete-region (point-min) (point-max))
+ (insert (format "%s\n" jao-emms-random-lines))
+ (save-buffer)))
+
+(defun jao-emms-goto-random-album ()
+ (let* ((pos (random (length (jao-emms-random-lines))))
+ (line (nth pos jao-emms-random-lines)))
+ (setq jao-emms-random-lines (remove line jao-emms-random-lines))
+ (jao-emms-random-lines-save)
+ (goto-line line)))
+
+(defun jao-emms-next-noerror ()
+ (interactive)
+ (when emms-player-playing-p
+ (error "A track is already being played"))
+ (cond (emms-repeat-track
+ (emms-start))
+ ((condition-case nil
+ (progn
+ (emms-playlist-current-select-next)
+ t)
+ (error nil))
+ (emms-start))
+ (t
+ (if jao-emms-random-album-p
+ (jao-emms-random-album-next)
+ (message "No next track in playlist")))))
+
+
+;; User interface
+;;;###autoload
+(defun jao-emms-random-album-start ()
+ (interactive)
+ (setq jao-emms-random-album-p t)
+ (jao-emms-random-album-next))
+
+;;;###autoload
+(defun jao-emms-random-album-stop ()
+ (interactive)
+ (setq jao-emms-random-album-p nil)
+ (emms-stop))
+
+;;;###autoload
+(defun jao-emms-random-album-toggle ()
+ (interactive)
+ (setq jao-emms-random-album-p (not jao-emms-random-album-p))
+ (message "Random album %s"
+ (if jao-emms-random-album-p "enabled" "disabled")))
+
+;;;###autoload
+(defun jao-emms-random-album-next ()
+ (interactive)
+ (save-excursion
+ (ignore-errors (emms-browser-clear-playlist))
+ (emms-browse-by-album)
+ (jao-emms-goto-random-album)
+ (let ((album (substring-no-properties (thing-at-point 'line) 0 -1)))
+ (emms-browser-add-tracks-and-play)
+ (when jao-emms-random-album-notify-p
+ (jao-notify album "Next album" jao-emms-random-album-notify-icon)))
+ (emms-browser-bury-buffer)
+ (jao-minibuffer-refresh)))
+
+;;;###autoload
+(defun jao-emms-random-album-reset ()
+ (interactive)
+ (setq jao-emms-random-lines nil)
+ (jao-emms-random-lines-save))
+
+(setq emms-player-next-function 'jao-emms-next-noerror)
+
+
+(provide 'jao-emms-random-album)
+;;; jao-emms-random-album.el ends here
diff --git a/lib/media/jao-emms.el b/lib/media/jao-emms.el
new file mode 100644
index 0000000..53b3513
--- /dev/null
+++ b/lib/media/jao-emms.el
@@ -0,0 +1,27 @@
+;; jao-emms.el -- shared bits
+
+;; Copyright (C) 2009, 2010 Jose Antonio Ortega Ruiz
+
+;; Author: Jose Antonio Ortega Ruiz <jao@gnu.org>
+;; Start date: Sat Jul 04, 2009 13:51
+
+;; 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 of the License, or
+;; (at your option) any later version.
+
+;; This file 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/>.
+
+;;; Code:
+
+(defgroup jao-emms nil "Emms extensions" :group 'emms)
+
+
+(provide 'jao-emms)
+;;; jao-emms.el ends here
diff --git a/lib/media/jao-lyrics.el b/lib/media/jao-lyrics.el
new file mode 100644
index 0000000..dd85da1
--- /dev/null
+++ b/lib/media/jao-lyrics.el
@@ -0,0 +1,152 @@
+;; jao-lyrics.el -- simple show lyrics using glyrc
+
+;; Copyright (C) 2009, 2010, 2017, 2019, 2020 Jose Antonio Ortega Ruiz
+
+;; Author: Jose Antonio Ortega Ruiz <jao@gnu.org>
+;; Start date: Sat Jul 04, 2009 13:41
+
+;; 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 of the License, or
+;; (at your option) any later version.
+
+;; This file 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/>.
+
+;;; Code:
+
+(defgroup jao-lyrics-faces nil "Faces"
+ :group 'faces)
+
+(defface jao-lyrics-font-lock-album '((t (:foreground "lightgoldenrod2")))
+ "Album name in lyrics."
+ :group 'jao-lyrics-faces)
+
+(defface jao-lyrics-font-lock-title '((t (:foreground "dodgerblue2")))
+ "Track title in lyrics."
+ :group 'jao-lyrics-faces)
+
+(defface jao-lyrics-font-lock-artist '((t (:foreground "dodgerblue3")))
+ "Artist name in lyrics."
+ :group 'jao-lyrics-faces)
+
+(defvar jao-lyrics-cache-dir "~/.lyrics")
+
+(defun jao-lyrics--filename (artist title)
+ (expand-file-name (format "%s - %s.txt" artist title)
+ jao-lyrics-cache-dir))
+
+(defun jao-lyrics--get-cached (artist title)
+ (let ((candidate (jao-lyrics--filename artist title)))
+ (and (file-exists-p candidate)
+ (with-current-buffer (find-file-noselect candidate)
+ (prog1
+ (buffer-string)
+ (kill-buffer))))))
+
+(defun jao-lyrics--cache (artist title lyrics)
+ (with-current-buffer
+ (find-file-noselect (jao-lyrics--filename artist title))
+ (delete-region (point-min) (point-max))
+ (insert lyrics)
+ (save-buffer)
+ (kill-buffer)))
+
+(make-variable-buffer-local
+ (defvar jao-lyrics--path nil))
+
+(defvar jao-lyrics-mode-map)
+(setq jao-lyrics-mode-map
+ (let ((map (make-keymap)))
+ (suppress-keymap map)
+ (define-key map [?q] 'bury-buffer)
+ (define-key map [?g] 'jao-show-lyrics)
+ (define-key map [?G] (lambda () (interactive) (jao-show-lyrics t)))
+ (define-key map [?e] 'jao-edit-lyrics)
+ map))
+
+(defun jao-lyrics-mode ()
+ (interactive)
+ (kill-all-local-variables)
+ (use-local-map jao-lyrics-mode-map)
+ (setq major-mode 'jao-lyrics-mode)
+ (setq mode-name "lyrics")
+ (toggle-read-only 1))
+
+(defun jao-lyrics-buffer ()
+ (or (get-buffer "*Lyrics*")
+ (with-current-buffer (get-buffer-create "*Lyrics*")
+ (jao-lyrics-mode)
+ (current-buffer))))
+
+(defun jao-edit-lyrics ()
+ (interactive)
+ (unless jao-lyrics--path
+ (error "No track data available."))
+ (find-file-other-window jao-lyrics--path))
+
+
+
+(defun jao-lyrics--clean-download (fn)
+ (with-current-buffer (find-file-noselect fn)
+ (goto-char (point-min))
+ (when (re-search-forward
+ "^\\(CreditsWritten by:\\|External linksNominate\\)" nil t)
+ (beginning-of-line)
+ (kill-region (point) (point-max)))
+ (replace-string " " "" nil (point-min) (point-max))
+ (replace-string "\\'" "'" nil (point-min) (point-max))
+ (save-buffer)))
+
+(defun jao-lyrics--download (artist title &optional noartist)
+ (message "Retrieving lyrics...")
+ (or (executable-find "glyrc")
+ (error "glyrc not installed"))
+ (let ((fn (jao-lyrics--filename (or noartist artist) title)))
+ (shell-command-to-string (format "glyrc lyrics -n 1-8 -Y -a %s -t %s -w %s"
+ (shell-quote-argument artist)
+ (shell-quote-argument title)
+ (shell-quote-argument fn)))
+ (jao-lyrics--clean-download fn)
+ (prog1 (jao-lyrics--get-cached artist title) (message nil))))
+
+(defvar jao-lyrics-info-function)
+(defvar-local jao-lyrics--info-function nil)
+
+;;;###autoload
+(defun jao-show-lyrics (&optional force info-function)
+ (interactive "P")
+ (let* ((a/t (funcall (or info-function
+ jao-lyrics--info-function
+ jao-lyrics-info-function)))
+ (artist (car a/t))
+ (title (cdr a/t))
+ (artist (if force (read-string "Artist: " artist) artist))
+ (title (if force (read-string "Title: " title) title))
+ (buffer (jao-lyrics-buffer))
+ (cached (and (not force) (jao-lyrics--get-cached artist title)))
+ (cached (and (not (zerop (length cached))) cached))
+ (lyrics (or cached
+ (jao-lyrics--download artist title)
+ (jao-lyrics--download "" title artist)))
+ (inhibit-read-only t))
+ (with-current-buffer buffer
+ (when info-function
+ (setq-local jao-lyrics--info-function info-function))
+ (delete-region (point-min) (point-max))
+ (insert (format "♪ %s - %s\n\n"
+ (propertize artist 'face 'jao-lyrics-font-lock-artist)
+ (propertize title 'face 'jao-lyrics-font-lock-title)))
+ (when lyrics (insert lyrics))
+ (goto-char (point-min))
+ (setq jao-lyrics--path (jao-lyrics--filename artist title)))
+ (pop-to-buffer buffer)))
+
+
+(provide 'jao-lyrics)
+;;; jao-lyrics.el ends here
diff --git a/lib/media/jao-mpris.el b/lib/media/jao-mpris.el
new file mode 100644
index 0000000..ad4b452
--- /dev/null
+++ b/lib/media/jao-mpris.el
@@ -0,0 +1,139 @@
+;;; jao-mpris.el --- mpris players control -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020, 2021 jao
+
+;; Author: jao <mail@jao.io>
+;; Keywords: multimedia
+
+;; 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:
+
+;; controlling and showing info on mpris players
+
+;;; Code:
+
+(require 'dbus)
+(require 'jao-minibuffer)
+(require 'jao-emms-info-track)
+
+(defun jao-mpris--playerctl (&rest args)
+ (shell-command-to-string (format "playerctl %s"
+ (mapconcat #'shell-quote-argument args " "))))
+
+(defmacro jao-playerctl--def (name &rest args)
+ `(defun ,name () (interactive) (jao-mpris--playerctl ,@args)))
+
+(jao-playerctl--def jao-mpris-play-pause "play-pause")
+(jao-playerctl--def jao-mpris-next "next")
+(jao-playerctl--def jao-mpris-previous "previous")
+
+(defun jao-playerctl--status (&optional sep)
+ (let* ((sep (or sep " ||| "))
+ (fmt (mapconcat 'identity
+ '("{{status}}"
+ "{{xesam:trackNumber}}"
+ "{{title}}"
+ "{{artist}}"
+ "{{album}}"
+ "{{duration(mpris:length)}}")
+ sep))
+ (m (jao-mpris--playerctl "metadata" "--format" fmt)))
+ (split-string (car (split-string m "\n")) sep)))
+
+;;;###autoload
+(defun jao-mpris-status-times ()
+ (interactive)
+ (let ((m (jao-mpris--playerctl "metadata" "--format"
+ (concat "{{duration(position)}}/"
+ "{{duration(mpris:length)}}"))))
+ (jao-notify (string-trim m) "Playing")))
+
+(defvar jao-mpris--current nil)
+(defvar jao-mpris-track-string "")
+
+(defun jao-mpris--get (k &optional l)
+ (alist-get k (or l jao-mpris--current)))
+
+(defun jao-mpris--format (&optional info)
+ (let* ((artist (jao-mpris--get 'artist info))
+ (title (jao-mpris--get 'title info))
+ (track (jao-mpris--get 'track info))
+ (album (jao-mpris--get 'album info))
+ (len (jao-mpris--get 'length info))
+ (duration (cond ((stringp len) len)
+ ((numberp len) (jao-emms--fmt-time (/ len 1e6) "")))))
+ (format "ï…„ %s %s %s%s%s"
+ (jao--put-face (format "%s" (or track "")) 'jao-emms-font-lock-track)
+ (jao--put-face title 'jao-emms-font-lock-title)
+ (jao--put-face artist 'jao-emms-font-lock-artist)
+ (jao--put-face (if album (format " (%s)" album) "")
+ 'jao-emms-font-lock-album)
+ (if duration (format " [%s]" duration) ""))))
+
+(defun jao-mpris--track (&optional info)
+ (let ((info (or info (jao-playerctl--status))))
+ (if (string= "Playing" (jao-mpris--get 'status info))
+ (setq jao-mpris-track-string (jao-mpris--format info))
+ (setq jao-mpris-track-string "")))
+ (jao-minibuffer-refresh))
+
+;;;###autoload
+(defun jao-mpris-artist-title ()
+ (when jao-mpris--current
+ (cons (jao-mpris--get 'artist) (jao-mpris--get 'title))))
+
+;;;###autoload
+(defun jao-mpris-show-osd ()
+ (interactive)
+ (when jao-mpris--current
+ (jao-notify (format "%s: %s" (jao-mpris--get 'status) (jao-mpris--format)))))
+
+(defun jao-mpris-minibuffer-order (order)
+ (jao-minibuffer-add-variable 'jao-mpris-track-string order))
+
+(defun jao-mpris--handler (iname properties &rest args)
+ (when properties
+ (let ((st (caadr (assoc "PlaybackStatus" properties)))
+ (md (caadr (assoc "Metadata" properties))))
+ (cond ((and st (not (string= "Playing" st)))
+ (setq jao-mpris-track-string "")
+ (setq jao-mpris--current
+ (cons (cons 'status st)
+ (assq-delete-all 'status jao-mpris--current)))
+ (jao-minibuffer-refresh)
+ (message "Music %s" st))
+ (md (let ((tno (caadr (assoc "xesam:trackNumber" md)))
+ (tlt (caadr (assoc "xesam:title" md)))
+ (art (caaadr (assoc "xesam:artist" md)))
+ (alb (caadr (assoc "xesam:album" md)))
+ (len (caadr (assoc "mpris:length" md))))
+ (setq jao-mpris--current
+ `((track . ,tno) (title . ,tlt)
+ (artist . ,art) (album . ,alb)
+ (length . ,len) (status . ,st)))
+ (jao-mpris--track jao-mpris--current)))))))
+
+;;;###autoload
+(defun jao-mpris-minibuffer-register (name &optional bus)
+ (dbus-register-signal (or bus :session)
+ name
+ "/org/mpris/MediaPlayer2"
+ "org.freedesktop.DBus.Properties"
+ "PropertiesChanged"
+ 'jao-mpris--handler))
+
+
+(provide 'jao-mpris)
+;;; jao-mpris.el ends here
diff --git a/lib/media/jao-random-album.el b/lib/media/jao-random-album.el
new file mode 100644
index 0000000..7158417
--- /dev/null
+++ b/lib/media/jao-random-album.el
@@ -0,0 +1,101 @@
+;; jao-random-album.el -- play random albums
+
+;; Copyright (C) 2009, 2010, 2017, 2018, 2019 Jose Antonio Ortega Ruiz
+
+;; Author: Jose Antonio Ortega Ruiz <jao@gnu.org>
+;; Start date: Sat Jul 04, 2009 13:06
+
+;; 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 of the License, or
+;; (at your option) any later version.
+
+;; This file 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/>.
+
+(require 'jao-notify)
+
+(defvar jao-random-album-p t)
+(defvar jao-random-lines nil)
+(defvar jao-random-lines-file (expand-file-name "~/.emacs.d/random-lines"))
+(defvar jao-random-album-notify-p t)
+(defvar jao-random-album-notify-icon nil)
+(defvar jao-random-album-skip-lines 2)
+
+(defun jao-random-lines ()
+ (or jao-random-lines
+ (and (file-exists-p jao-random-lines-file)
+ (with-current-buffer
+ (find-file-noselect jao-random-lines-file)
+ (goto-char (point-min))
+ (setq jao-random-lines (read (current-buffer)))))
+ (dotimes (n (1- (line-number-at-pos (point-max)))
+ jao-random-lines)
+ (when (> n jao-random-album-skip-lines)
+ (push (1+ n) jao-random-lines)))))
+
+(defun jao-random-lines-save ()
+ (with-current-buffer (find-file-noselect jao-random-lines-file)
+ (delete-region (point-min) (point-max))
+ (insert (format "%s\n" jao-random-lines))
+ (save-buffer)))
+
+(defun jao-goto-random-album ()
+ (let* ((pos (random (length (jao-random-lines))))
+ (line (nth pos jao-random-lines)))
+ (setq jao-random-lines (remove line jao-random-lines))
+ (jao-random-lines-save)
+ (goto-line line)))
+
+
+;; User interface
+(defvar jao-random-album-buffer)
+(defvar jao-random-album-add-tracks-and-play)
+(defvar jao-random-album-stop)
+
+(defun jao-random-album-start ()
+ (interactive)
+ (setq jao-random-album-p t)
+ (jao-random-album-next))
+
+(defun jao-random-album-stop ()
+ (interactive)
+ (setq jao-random-album-p nil)
+ (funcall jao-random-album-stop))
+
+(defun jao-random-album-toggle ()
+ (interactive)
+ (setq jao-random-album-p (not jao-random-album-p))
+ (message "Random album %s"
+ (if jao-random-album-p "enabled" "disabled")))
+
+(defun jao-random-album-next ()
+ (interactive)
+ (with-current-buffer (get-buffer (funcall jao-random-album-buffer))
+ (save-excursion
+ (jao-goto-random-album)
+ (let ((album (string-trim
+ (substring-no-properties (thing-at-point 'line) 0 -1))))
+ (funcall jao-random-album-add-tracks-and-play)
+ (when jao-random-album-notify-p
+ (jao-notify album "Next album" jao-random-album-notify-icon))))))
+
+(defun jao-random-album-reset ()
+ (interactive)
+ (setq jao-random-lines nil)
+ (jao-random-lines-save))
+
+(defun jao-random-album-setup (album-buffer add-and-play stop &optional icon)
+ (setq jao-random-album-buffer album-buffer
+ jao-random-album-add-tracks-and-play add-and-play
+ jao-random-album-stop stop
+ jao-random-album-notify-icon icon))
+
+
+(provide 'jao-random-album)
+;;; jao-random-album.el ends here
diff --git a/lib/media/leoslyrics.py b/lib/media/leoslyrics.py
new file mode 100755
index 0000000..5e4f8c8
--- /dev/null
+++ b/lib/media/leoslyrics.py
@@ -0,0 +1,84 @@
+#!/usr/bin/python
+#
+# (c) 2004-2008 The Music Player Daemon Project
+# http://www.musicpd.org/
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+#
+# Load lyrics from leoslyrics.com
+#
+
+from sys import argv, exit
+from urllib import urlencode, urlopen
+from xml.sax import make_parser, SAXException
+from xml.sax.handler import ContentHandler
+
+class SearchContentHandler(ContentHandler):
+ def __init__(self):
+ self.code = None
+ self.hid = None
+
+ def startElement(self, name, attrs):
+ if name == 'response':
+ self.code = int(attrs['code'])
+ elif name == 'result':
+ if self.hid is None or attrs['exactMatch'] == 'true':
+ self.hid = attrs['hid']
+
+def search(artist, title):
+ query = urlencode({'auth': 'ncmpc',
+ 'artist': artist,
+ 'songtitle': title})
+ url = "http://api.leoslyrics.com/api_search.php?" + query
+ f = urlopen(url)
+ handler = SearchContentHandler()
+ parser = make_parser()
+ parser.setContentHandler(handler)
+ parser.parse(f)
+ return handler.hid
+
+class LyricsContentHandler(ContentHandler):
+ def __init__(self):
+ self.code = None
+ self.is_text = False
+ self.text = None
+
+ def startElement(self, name, attrs):
+ if name == 'text':
+ self.text = ''
+ self.is_text = True
+ else:
+ self.is_text = False
+
+ def characters(self, chars):
+ if self.is_text:
+ self.text += chars
+
+def lyrics(hid):
+ query = urlencode({'auth': 'ncmpc',
+ 'hid': hid})
+ url = "http://api.leoslyrics.com/api_lyrics.php?" + query
+ f = urlopen(url)
+ handler = LyricsContentHandler()
+ parser = make_parser()
+ parser.setContentHandler(handler)
+ parser.parse(f)
+ return handler.text
+
+hid = search(argv[1], argv[2])
+if hid is None:
+ exit(2)
+print lyrics(hid).encode('utf-8')
diff --git a/lib/media/lyricwiki.rb b/lib/media/lyricwiki.rb
new file mode 100755
index 0000000..f163fa4
--- /dev/null
+++ b/lib/media/lyricwiki.rb
@@ -0,0 +1,52 @@
+#!/usr/bin/env ruby
+#
+# (c) 2004-2008 The Music Player Daemon Project
+# http://www.musicpd.org/
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+#
+# Load lyrics from lyrics.wikia.com, formerly lyricwiki.org
+#
+
+require 'uri'
+require 'net/http'
+require 'cgi'
+
+url = "http://lyrics.wikia.com/api.php?action=lyrics&fmt=xml&func=getSong" + \
+ "&artist=#{URI.escape(ARGV[0])}&song=#{URI.escape(ARGV[1])}"
+response = Net::HTTP.get(URI.parse(url))
+
+if not response =~ /<url>\s*(.*?)\s*<\/url>/im
+ $stderr.puts "No URL in response!"
+ exit(1)
+end
+
+url = $1
+exit(69) if url =~ /action=edit$/
+
+response = Net::HTTP.get(URI.parse(url))
+if not response =~ /<div class='lyricbox'>\s*(.*?)\s*<!--/im
+ $stderr.puts "No <div class='lyricbox'> in lyrics page!\n"
+ exit(1)
+end
+
+# if not $1 =~ /^.*<\/div>(.*?)$/im
+if not $1 =~ /^.*<\/script>(.*?)$/im
+ $stderr.puts "Couldn't remove leading XML tags in lyricbox!\n"
+ exit(1)
+end
+
+puts CGI::unescapeHTML($1.gsub(/<br \/>/, "\n"))
diff --git a/lib/net/jao-frm.el b/lib/net/jao-frm.el
new file mode 100644
index 0000000..2658687
--- /dev/null
+++ b/lib/net/jao-frm.el
@@ -0,0 +1,222 @@
+;;; jao-frm.el --- use frm to show mailbox
+
+;; Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2019, 2020
+
+;; Author: Jose Antonio Ortega Ruiz <jao@gnu.org>
+;; Keywords: mail
+
+;; 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 file 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:
+
+;; Little hack to see the contents of your mailbox using GNU mailutils'
+;; `frm' program.
+;;
+;; Just put (require 'jao-frm) in your .emacs, and M-x jao-frm will pop up a
+;; new window with your mailbox contents (from and subject) as
+;; printed by frm. In this buffer, use `n' and `p' to move, `q' to close
+;; the window. `g' will call Gnus.
+;;
+
+;;; Code:
+
+;;;; Customisation:
+
+(defgroup jao-frm nil
+ "Frm-base mailbox checker"
+ :group 'mail
+ :prefix "jao-frm-")
+
+(defcustom jao-frm-exec-path "frm"
+ "frm executable path"
+ :group 'jao-frm
+ :type 'file)
+
+(defcustom jao-frm-mail-command 'gnus
+ "Emacs function to invoke when `g' is pressed on an frm buffer."
+ :group 'jao-frm
+ :type 'symbol)
+
+(defcustom jao-frm-mailboxes nil
+ "List of mailboxes to check, or directory containing them."
+ :group 'jao-frm
+ :type '(choice directory (repeat file)))
+
+(defface jao-frm-mailno-face '((t (:foreground "dark slate grey")))
+ "Face for the mail number."
+ :group 'jao-frm)
+
+(defface jao-frm-from-face '((t (:foreground "slate grey")))
+ "Face for From: header."
+ :group 'jao-frm)
+
+(defface jao-frm-subject-face '((t (:foreground "slate blue")))
+ "Face for Subject: header."
+ :group 'jao-frm)
+
+(defface jao-frm-mailbox-face '((t (:bold t :weight bold)))
+ "Face for mailbox name."
+ :group 'jao-frm)
+
+;;;; Mode:
+
+(defvar jao-frm-mode-map
+ (let ((map (make-keymap)))
+ (suppress-keymap map)
+ (define-key map [?q] 'jao-frm-delete-window)
+ (define-key map [?n] 'next-line)
+ (define-key map [?p] 'previous-line)
+ (define-key map [?r] 'jao-frm)
+ (define-key map [?g] (lambda ()
+ (interactive)
+ (funcall jao-frm-mail-command)))
+ (define-key map [(control k)] 'jao-frm-delete-message)
+ map))
+
+(setq jao-frm-font-lock-keywords
+ '(("^[^ :]+:" . 'jao-frm-mailbox-face)
+ ("^\\([ 0-9]+\\):\t+\\([^\t]+\\)\t+\\([^\n]+$\\)"
+ (1 'jao-frm-mailno-face)
+ (2 'jao-frm-from-face)
+ (3 'jao-frm-subject-face))))
+
+(defvar jao-frm-mode-syntax-table
+ (let ((st (make-syntax-table)))
+ st))
+
+(defun jao-frm-mode ()
+ "Major mode for displaying frm output."
+ (interactive)
+ (kill-all-local-variables)
+ (buffer-disable-undo)
+ (use-local-map jao-frm-mode-map)
+ (set (make-local-variable 'font-lock-defaults)
+ '(jao-frm-font-lock-keywords))
+ (set (make-local-variable 'truncate-lines) t)
+ (set (make-local-variable 'kill-whole-line) t)
+ (set (make-local-variable 'next-line-add-newlines) nil)
+ (setq major-mode 'jao-frm-mode)
+ (setq mode-name "frm")
+ (read-only-mode 1)
+ (goto-char 1))
+
+;;;; Mode commands:
+(defvar jao-frm-last-config nil)
+
+(defun jao-frm-delete-window ()
+ "Delete frm window and restore last win config"
+ (interactive)
+ (if (and (consp jao-frm-last-config)
+ (window-configuration-p (car jao-frm-last-config)))
+ (progn
+ (set-window-configuration (car jao-frm-last-config))
+ (goto-char (cadr jao-frm-last-config))
+ (setq jao-frm-last-config nil))
+ (bury-buffer)))
+
+(defun jao-frm-delete-message ()
+ "Delete message at point"
+ (interactive)
+ (when (eq (current-buffer) (get-buffer "*frm*"))
+ (beginning-of-line)
+ (when (search-forward-regexp "^ +\\([0-9]+\\):" nil t)
+ (let ((mn (string-to-number (match-string 1))))
+ (when (y-or-n-p (format "Delete message number %d? " mn))
+ (read-only-mode -1)
+ (shell-command (format "echo 'd %d'|mail" mn) t)
+ (jao-frm)
+ (when (= (point-max) (point-min))
+ (jao-frm-delete-window)
+ (message "Mailbox is empty")))))))
+
+;;;; Activate frm:
+(defun jao-frm-mbox-mails (mbox)
+ (let ((no (ignore-errors
+ (substring
+ (shell-command-to-string (format "frm -s n %s|wc -l" mbox))
+ 0 -1))))
+ (if (stringp no) (string-to-number no) 0)))
+
+(defun jao-frm-mail-number ()
+ (let ((no 0))
+ (dolist (b (jao-frm-mboxes) no) (setq no (+ no (jao-frm-mbox-mails b))))))
+
+(defun jao-frm-default-count-formatter (m n)
+ (format "%s: %s" (file-name-sans-extension (file-name-nondirectory m)) n))
+
+(defun jao-frm-mail-counts (fmt)
+ (let ((fmt (or fmt 'jao-frm-default-count-formatter)))
+ (remove nil
+ (mapcar (lambda (m)
+ (let ((n (jao-frm-mbox-mails m)))
+ (unless (zerop n) (funcall fmt m n))))
+ (jao-frm-mboxes)))))
+
+(defun jao-frm-display-mailbox (mbox)
+ (when (not (zerop (jao-frm-mbox-mails mbox)))
+ (insert (or (file-name-nondirectory mbox) mbox) ":\n\n")
+ (apply 'call-process
+ `(,jao-frm-exec-path nil ,(current-buffer) nil
+ "-s" "n" "-n" "-t" ,@(and mbox (list mbox))))
+ (newline 2)))
+
+(defun jao-frm-mboxes ()
+ (cond ((null jao-frm-mailboxes) (list (getenv "MAIL")))
+ ((listp jao-frm-mailboxes) jao-frm-mailboxes)
+ ((stringp jao-frm-mailboxes)
+ (if (file-directory-p jao-frm-mailboxes)
+ (directory-files jao-frm-mailboxes t "^[^.]")
+ (list jao-frm-mailboxes)))
+ (t (error "Error in mbox specification. Check `jao-frm-mailboxes'"))))
+
+;;;###autoload
+(defun jao-frm ()
+ "Run frm."
+ (interactive)
+ (let ((fbuff (get-buffer-create "*frm*"))
+ (inhibit-read-only t))
+ (if (not (eq fbuff (current-buffer)))
+ (setq jao-frm-last-config
+ (list (current-window-configuration) (point-marker))))
+ (with-current-buffer fbuff
+ (delete-region (point-min) (point-max))
+ (mapc 'jao-frm-display-mailbox (jao-frm-mboxes))
+ (unless (eq major-mode 'jao-frm-mode)
+ (jao-frm-mode))
+ (goto-char (point-min))
+ (if (= (point-min) (point-max))
+ (message "Mailbox is empty.")
+ (pop-to-buffer fbuff))
+ (when (and (boundp 'display-time-mode) display-time-mode)
+ (display-time-update)))))
+
+;;;###autoload
+(defun jao-frm-show-mail-numbers (&optional fmt)
+ (interactive)
+ (let ((counts (jao-frm-mail-counts fmt)))
+ (message (if counts (mapconcat 'identity counts ", ") "No mail"))))
+
+;;;###autoload
+(defun jao-frm-mail-string ()
+ (let ((counts (jao-frm-mail-counts
+ (lambda (m n)
+ (let ((m (substring (file-name-nondirectory m) 0 1)))
+ (format "%s%s" (capitalize m) n))))))
+ (mapconcat 'identity counts " ")))
+
+(provide 'jao-frm)
+
+;;; jao-frm.el ends here
diff --git a/lib/net/jao-maildir.el b/lib/net/jao-maildir.el
new file mode 100644
index 0000000..76a9f9e
--- /dev/null
+++ b/lib/net/jao-maildir.el
@@ -0,0 +1,155 @@
+;; jao-maildir.el -- Utilities for reading maildirs -*- lexical-binding: t; -*-
+
+;; Copyright (c) 2019, 2020, 2021 jao
+
+;; Author: jao <mail@jao.io>
+;; Start date: Sun Dec 01, 2019 15:48
+;; Keywords: mail
+
+;; 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 file 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.
+
+;;; Comentary:
+
+;; Inspecting the contents of maildirs and reporting it.
+
+;;; Code:
+
+(require 'seq)
+(require 'jao-minibuffer)
+
+(defvar jao-maildir-debug-p nil)
+(defvar jao-maildir-echo-p t)
+(defvar jao-maildir-tracked-maildirs nil)
+(defvar jao-maildir-info-string "")
+
+(defgroup jao-maildir-faces nil "Faces"
+ :group 'faces)
+(defun jao-maildir--maildir-new (mbox) (expand-file-name "new" mbox))
+
+(defun jao-maildir--maildir-new-count (mbox)
+ (- (length (directory-files (jao-maildir--maildir-new mbox))) 2))
+
+(defface jao-maildir-emph '((t :inherit font-lock-keyword-face))
+ "Face used to highlihgt non-boring tracked maildirs"
+ :group 'jao-maildir-faces)
+
+(defvar jao-maildir--maildirs nil)
+(defvar jao-maildir--counts nil)
+(defvar jao-maildir--label-mboxes nil)
+(defvar jao-maildir--trackers nil)
+(defvar jao-maildir--track-strings ())
+
+(defun jao-maildir--update-counts ()
+ (dolist (mbox jao-maildir--maildirs)
+ (puthash mbox (jao-maildir--maildir-new-count mbox) jao-maildir--counts)))
+
+(defun jao-maildir--init-counts (maildirs)
+ (setq jao-maildir--counts (make-hash-table :test 'equal))
+ (setq jao-maildir--maildirs maildirs)
+ (jao-maildir--update-counts))
+
+(defun jao-maildir--set-trackers (maildirs tracked-maildirs)
+ (jao-maildir--init-counts maildirs)
+ (let* ((label-mboxes (make-hash-table :test 'equal))
+ (trackers (seq-map-indexed
+ (lambda (track idx)
+ (puthash (car track) () label-mboxes)
+ (let ((tr (seq-take track 2))
+ (l (elt track 2)))
+ (append tr
+ (cond ((eq l t) '(jao-maildir-emph))
+ ((null l) '(default))
+ (t (list l)))
+ (list (or (elt track 3) idx)))))
+ tracked-maildirs)))
+ (dolist (mbox maildirs)
+ (let ((lb (seq-find (lambda (lb) (when lb (string-match-p lb mbox)))
+ (hash-table-keys label-mboxes))))
+ (puthash lb (cons mbox (gethash lb label-mboxes)) label-mboxes)))
+ (setq jao-maildir--label-mboxes label-mboxes)
+ (setq jao-maildir--trackers trackers)))
+
+(defun jao-maildir--tracked-count (track)
+ (seq-reduce (lambda (c k) (+ c (gethash k jao-maildir--counts 0)))
+ (gethash (car track) jao-maildir--label-mboxes)
+ 0))
+
+(defun jao-maildir--update-track-string (mbox)
+ (when-let ((track (seq-find (lambda (td) (string-match-p (car td) mbox))
+ jao-maildir--trackers)))
+ (let* ((label (cadr track))
+ (other (assoc-delete-all label jao-maildir--track-strings))
+ (cnt (jao-maildir--tracked-count track)))
+ (if (> cnt 0)
+ (let* ((face (car (last (butlast track))))
+ (order (car (last track)))
+ (str (propertize (format "%s%s" label cnt) 'face face))
+ (str (cons label (cons order str))))
+ (setq jao-maildir--track-strings (cons str other)))
+ (setq jao-maildir--track-strings other)))))
+
+;;;###autoload
+(defun jao-maildir-update-info-string (&optional mbox)
+ (cond ((eq mbox t)
+ (seq-do 'jao-maildir--update-track-string jao-maildir--maildirs))
+ ((stringp mbox)
+ (puthash mbox (jao-maildir--maildir-new-count mbox) jao-maildir--counts)
+ (jao-maildir--update-track-string mbox)))
+ (let* ((s (seq-sort-by #'cadr #'< jao-maildir--track-strings))
+ (s (mapconcat 'identity (mapcar 'cddr s) " ")))
+ (setq jao-maildir-info-string (if (string-blank-p s) "" (concat s " "))))
+ (when jao-maildir-echo-p (jao-minibuffer-refresh)))
+
+(defvar jao-maildir--watches nil)
+
+(defun jao-maildir-cancel-watchers ()
+ (dolist (w jao-maildir--watches) (file-notify-rm-watch w))
+ (setq jao-maildir--watches nil))
+
+(defun jao-maildir--log-watch (mbox e)
+ (when jao-maildir-debug-p
+ (message "[%s] watch: %s: %s" (current-time-string) mbox e)))
+
+(defun jao-maildir--watcher (mbox cb)
+ (lambda (e)
+ (jao-maildir--log-watch e mbox)
+ (when (memq (cadr e) '(created deleted))
+ (jao-maildir-update-info-string mbox)
+ (when cb (funcall cb mbox)))))
+
+(defun jao-maildir--setup-watches (cb)
+ (jao-maildir-cancel-watchers)
+ (setq jao-maildir--watches
+ (mapcar (lambda (mbox)
+ (file-notify-add-watch (jao-maildir--maildir-new mbox)
+ '(change attribute-change)
+ (jao-maildir--watcher mbox cb)))
+ jao-maildir--maildirs)))
+
+;;;###autoload
+(defun jao-maildir-setup (maildirs trackers mode-line &optional cb)
+ (jao-maildir--set-trackers maildirs trackers)
+ (cond ((eq 'mode-line mode-line)
+ (add-to-list 'global-mode-string 'jao-maildir-info-string t))
+ ((numberp mode-line)
+ (jao-minibuffer-add-variable 'jao-maildir-info-string mode-line)
+ (jao-maildir-update-info-string t))
+ (t (error "Invalid mode-line value")))
+ (jao-maildir--setup-watches cb))
+
+
+(provide 'jao-maildir)
+;;; jao-maildir.el ends here
diff --git a/lib/net/jao-proton-utils.el b/lib/net/jao-proton-utils.el
new file mode 100644
index 0000000..012a2ff
--- /dev/null
+++ b/lib/net/jao-proton-utils.el
@@ -0,0 +1,131 @@
+;; jao-proton-utils.el -- simple interaction with Proton mail and vpn
+
+;; Copyright (c) 2018, 2019, 2020 Jose Antonio Ortega Ruiz
+
+;; 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 file 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.
+
+;; Author: Jose Antonio Ortega Ruiz <mail@jao.io>
+;; Start date: Fri Dec 21, 2018 23:56
+
+;;; Comentary:
+
+;; This is a very simple comint-derived mode to run the CLI version
+;; of PM's Bridge within the comfort of emacs.
+
+;;; Code:
+
+(define-derived-mode proton-bridge-mode comint-mode "proton-bridge"
+ "A very simple comint-based mode to run ProtonMail's bridge"
+ (setq comint-prompt-read-only t)
+ (setq comint-prompt-regexp "^>>> "))
+
+;;;###autoload
+(defun run-proton-bridge ()
+ "Run or switch to an existing bridge process, using its CLI"
+ (interactive)
+ (pop-to-buffer (make-comint "proton-bridge" "protonmail-bridge" nil "-c"))
+ (unless (eq major-mode 'proton-bridge-mode)
+ (proton-bridge-mode)))
+
+(defvar proton-vpn-mode-map)
+
+(defvar jao-proton-vpn-font-lock-keywords '("\\[.+\\]"))
+
+;;;###autoload
+(defun proton-vpn-mode ()
+ "A very simple mode to show the output of ProtonVPN commands"
+ (interactive)
+ (kill-all-local-variables)
+ (buffer-disable-undo)
+ (use-local-map proton-vpn-mode-map)
+ (setq-local font-lock-defaults '(jao-proton-vpn-font-lock-keywords))
+ (setq-local truncate-lines t)
+ (setq-local next-line-add-newlines nil)
+ (setq major-mode 'proton-vpn-mode)
+ (setq mode-name "proton-vpn")
+ (read-only-mode 1))
+
+(defvar jao-proton-vpn--buffer "*pvpn*")
+
+(defun jao-proton-vpn--do (things)
+ (let ((b (pop-to-buffer (get-buffer-create jao-proton-vpn--buffer))))
+ (let ((inhibit-read-only t)
+ (cmd (format "protonvpn-cli %s" things)))
+ (delete-region (point-min) (point-max))
+ (message "Running: %s ...." cmd)
+ (shell-command cmd b)
+ (message ""))
+ (proton-vpn-mode)))
+
+;;;###autoload
+(defun proton-vpn-status ()
+ (interactive)
+ (jao-proton-vpn--do "s"))
+
+(defun proton-vpn--get-status ()
+ (or (when-let ((b (get-buffer jao-proton-vpn--buffer)))
+ (with-current-buffer b
+ (goto-char (point-min))
+ (if (re-search-forward "^Status: *\\(.+\\)$" nil t)
+ (match-string-no-properties 1)
+ (when (re-search-forward "^Connected!$")
+ "Connected"))))
+ "Disconnected"))
+
+;;;###autoload
+(defun proton-vpn-connect (cc)
+ (interactive "P")
+ (let ((cc (when cc (read-string "Country code: "))))
+ (jao-proton-vpn--do (if cc (format "c --cc %s" cc) "c --sc"))
+ (proton-vpn-status)))
+
+(defun proton-vpn-reconnect ()
+ (interactive)
+ (jao-proton-vpn--do "r"))
+
+(setenv "PVPN_WAIT" "300")
+
+;;;###autoload
+(defun proton-vpn-maybe-reconnect ()
+ (interactive)
+ (when (string= "Connected" (proton-vpn--get-status))
+ (jao-proton-vpn--do "d")
+ (sit-for 5)
+ (jao-proton-vpn--do "r")))
+
+;;;###autoload
+(defun proton-vpn-disconnect ()
+ (interactive)
+ (jao-proton-vpn--do "d"))
+
+(setq proton-vpn-mode-map
+ (let ((map (make-keymap)))
+ (suppress-keymap map)
+ (define-key map [?q] 'bury-buffer)
+ (define-key map [?n] 'next-line)
+ (define-key map [?p] 'previous-line)
+ (define-key map [?g] 'proton-vpn-status)
+ (define-key map [?r] 'proton-vpn-reconnect)
+ (define-key map [?d] (lambda ()
+ (interactive)
+ (when (y-or-n-p "Disconnect?")
+ (proton-vpn-disconnect))))
+ (define-key map [?c] 'proton-vpn-connect)
+ map))
+
+
+(provide 'jao-proton-utils)
+;;; jao-proton.el ends here
diff --git a/lib/net/randomsig.el b/lib/net/randomsig.el
new file mode 100644
index 0000000..d07e676
--- /dev/null
+++ b/lib/net/randomsig.el
@@ -0,0 +1,724 @@
+;;; randomsig.el --- insert a randomly selected signature
+
+;; Copyright (C) 2001, 2002, 2013, 2020 Hans-Jürgen Ficker
+
+;; Emacs Lisp Archive Entry
+;; Author: Hans-Juergen Ficker <hj@backmes.de>
+;; Version: 0.7.0
+;; X-CVS-Version: $Id: randomsig.el,v 1.1.1.1 2003/09/17 22:49:45 jao Exp $
+;; Keywords: mail random signature
+
+;; This file is not currently 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, 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 ; 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 is yet another implementation to insert a randomly choosen
+;; signature into a mail.
+
+;; It is only tested with gnus.
+
+;; To make it work, put the following lines into your ~/.gnus:
+
+;; (require 'randomsig)
+;; (define-key message-mode-map (kbd "C-c s") 'randomsig-replace-sig)
+;; (define-key message-mode-map (kbd "C-c S") 'randomsig-select-sig)
+;; (require 'gnus-sum) ; probably required for `gnus-summary-save-map'
+;; (define-key gnus-summary-save-map "-" 'gnus/randomsig-summary-read-sig)
+;; (setq randomsig-dir "/some/directory")
+;; (setq randomsig-files '("some" "files"))
+;; ;; or (setq randomsig-files (randomsig-search-sigfiles))
+;; ;; or (setq randomsig-files 'randomsig-search-sigfiles)
+;; (setq message-signature 'randomsig-signature)
+
+;; This will also define the shortcut `C-c s' in message-mode to
+;; change the signature, `C-c S' in message-mode to interactively
+;; select the signature to replace the current signature, and `O -' in
+;; gnus-summary-mode to read the signature from the selected mail.
+
+;; `randomsig-files' must be a list of existing files, an existing
+;; file, or a function returning a list of existing files. If these
+;; don't have absolute paths, they are located in `randomsig-dir'.
+
+;; File format: Each file must contain at least one signature.
+;; Signatures are separated with `randomsig-delimiter-pattern'. If
+;; there is only one signature in the file, the delimiter can be
+;; omitted, so real .signature-files can be used.
+
+;; `randomsig-delimiter' is used when inserting new signatures with
+;; `randomsig-message-read-sig' into the signature file. So
+;; `randomsig-delimiter' should match `randomsig-delimiter-pattern'.
+
+;; `randomsig-static-string' is put in front of every random signature
+;; if non-`nil'.
+
+;; The *-read-sig functions read the signature of a message, or use
+;; the marked text, and write it to a signature-file, for which the
+;; name is asked. If the file does not exist, it will be generated.
+;; When called with any prefix, the signatures will be offered to edit
+;; before saving.
+
+;; if `randomsig-replace-sig' is called with any prefix, it will ask
+;; for a file to get the signature from.
+
+;; `randomsig-select-sig' will offer a list of signatures to select
+;; from in an extra buffer. n will jump to the next signature, p to
+;; the previous, RET will insert the selected signature, q will exit
+;; the selection buffer without replacing the current signature, R
+;; will reload the signature-files, and e will open a buffer for
+;; editing the signature at the point. When called with any prefix, it
+;; will ask for a file to get the signatures from
+
+;; `randomsig-search-sigfiles' will search for regular files in
+;; `randomsig-dir', which do not match `randomsig-search-unwanted'. A
+;; subdirectory of `randomsig-dir' can be given as optional argument.
+
+;; Completion will only work for files in `randomsig-files', though
+;; others files can be used, too.
+
+;;; Changelog:
+
+;; 2001/04/12 0.1
+;; * Initial release
+
+;; 2001/04/19 0.2
+;; * inserted `randomsig-delimiter' to add the capability to change
+;; the delimiter between the signatures (thanks to Andreas Büsching
+;; <crunchy@tzi.de>)
+
+;; 2001/04/25 0.3
+;; * new function `randomsig-search-sigfiles', to search all regular files
+;; in directory `randomsig-dir'
+;; * normal signatures only worked, when using only one signature. Fixed.
+
+;; 2001/04/25 0.3.1
+;; * Fixed a bug in `randomsig-search-sigfiles'
+
+;; 2001/04/26 0.3.2
+;; * replaced `point-at-eol' with `line-end-position' (Don't know where
+;; `point-at-eol' is defined)
+;; * require cl
+;; * require message in some functions
+
+;; 2001/07/09 0.3.3
+;; * don't (setq message-signature 'randomsig-signature) by default,
+;; the user can do this in his .gnus
+;; * remove unnecessary optional arguments to `find-file-noselect' to
+;; make it work with XEmacs
+;; (Thanks to Micha Wiedenmann <Micha.Wiedenmann@gmx.net> for both
+;; suggestions)
+;; * documentation updates
+
+;; 2001/07/12 0.3.4
+;; * more fixes for XEmacs
+;; * more documentation Updates
+
+;; 2001/07/20 0.4.0
+;; * new command `randomsig-select-sig' to interactively select a signature
+;; * new mode `randomsig-select-mode' (for `randomsig-select-sig')
+;; * `randomsig-files' can also be function returning a list of
+;; Signature files
+;; * `randomsig-replace-sig' does not remove old signature when interrupted
+
+;; 2001/07/22 0.4.1
+;; * (require 'message) only when needed
+
+;; 2001/08/13 0.5.0
+;; * doesn't require message anymore, so it should work without gnus
+
+;; 2001/08/20 0.5.1
+;; * add (random t) to initialize random seed (thanks to Evgeny
+;; Roubinchtein <evgenyr@cs.washington.edu> for pointing this out
+;; * insert a newline if it is missing at the end of a signature file
+
+;; 2001/09/17 0.5.2
+;; * new variable `randomsig-static-string' (thanks to Raymond Scholz
+;; <rscholz@zonix.de>)
+
+;; 2001/10/01 0.5.3
+;; * Documentation updates
+
+;; 2002/01/20 0.5.99
+;; * It is now possible to edit signatures before saving, or to edit
+;; single signatures from the selection buffer.
+;; * Mark many variables as user option
+;; * randomsig-files-to-list works recursive
+
+;; 2002/03/04 0.6.0
+;; * `randomsig-replace-signature-in-signature-files' should be safer now
+;; * `randomsig-files-to-list' did endless recursion when called
+;; with nil. Fixed.
+;; * Some error-handling for non-existing `randomsig-dir'.
+
+;; 2002/09/21 0.7.0
+;; * most variables customizable
+;; * `randomsig-static-string' works for `randomsig-select-sig', too
+;; (thanks to Mark Trettin <mtr-dev0@gmx.de> for pointing this out)
+;; * documentation updates
+
+(eval-when-compile
+ (require 'cl-lib))
+
+
+(defconst randomsig-version "0.7.0")
+
+
+(defvar randomsig-dir "~/.signatures"
+ "*Directory for signature-files. See also `randomsig-files'")
+
+
+(defgroup randomsig nil
+ "insert a randomly choosen signature into a mail."
+ :group 'mail
+ :group 'news)
+
+(defcustom randomsig-files '("default")
+ "*Files with random signatures.
+This variable may be a list of strings, a string, or a function returning a
+list of strings.
+The files are searched in `randomsig-dir', if they don't have absolute paths.
+The signatures have to be separated by lines matching
+`randomsig-delimiter-pattern' at the beginning."
+ :type '(choice
+ (repeat
+ :tag "List of filenames"
+ (string :tag "filename"))
+ (function
+ :tag "function returning the signature files"
+ :value randomsig-search-sigfiles))
+ :group 'randomsig)
+
+(defcustom randomsig-delimiter "-- "
+ "*delimiter used when adding new signatures in signature file.
+You have to change `randomsig-delimiter-pattern', too, if you change this."
+ :type '(string)
+ :group 'randomsig)
+
+
+(defcustom randomsig-delimiter-pattern
+ (concat "^" (regexp-quote randomsig-delimiter) "$")
+ "*Regular expression that matches the delimiters between signatures.
+`randomsig-delimiter' must match `randomsig-delimiter-pattern'."
+ :type '(regexp)
+ :group 'randomsig)
+
+
+(defcustom randomsig-search-unwanted "\\(/\\|^\\)\\(CVS\\|RCS\\|.*~\\)$"
+ "*Regular expression matching unwanted files when scanning with
+`randomsig-search-sigfiles'"
+ :type '(regexp)
+ :group 'randomsig)
+
+
+(defcustom randomsig-static-string nil
+ "*Static string to be inserted above every random signature.
+You probably want to have a newline at the end of it."
+ :type '(choice
+ (const :tag "none" nil)
+ (string))
+ :group 'randomsig)
+
+
+(defvar randomsig-buffer-name "*Signatures*"
+ "Name for the (temporary) buffer for the signatures")
+
+(defvar randomsig-edit-buffer-name "*Edit Signature*"
+ "Name for the (temporary) buffer for editing the signatures")
+
+(defvar randomsig-select-original-buffer nil)
+(defvar randomsig-select-original-position nil)
+
+(defvar randomsig-history nil)
+
+(defvar randomsig-buffer-file-pos-list nil)
+
+(defvar randomsig-select-edit-bufferpos nil)
+
+(defvar randomsig-loaded-files nil)
+
+;; definitions for XEmacs:
+(unless (fboundp 'line-end-position)
+ (defalias 'line-end-position 'point-at-eol))
+
+(defun randomsig-mark-active-p ()
+ mark-active) ;; jao: region-active-p is defined in GNU Emacs 23 with
+ ;; a different meaning
+;;; (if (boundp 'region-active-p)
+
+;;; (region-active-p) ; XEmacs
+
+;;; mark-active)) ; Gnu Emacs
+
+
+(require 'cl-lib)
+
+(random t) ; Initialize random seed
+
+;;; Helper Functions
+
+(defun randomsig-files-to-list (files)
+ ;; return a list of strings
+ (cond ((listp files) files)
+ ((and (symbolp files)
+ (fboundp files)) (randomsig-files-to-list (funcall files)))
+ ((and (symbolp files)
+ (boundp files)) (randomsig-files-to-list (symbol-value files)))
+ ((stringp files) (list files))
+ (t nil)))
+
+
+(defun randomsig-prompt (&optional prompt)
+ ;; Prompt for a signature file.
+ (let ((files (randomsig-files-to-list randomsig-files)))
+ (completing-read (if prompt prompt "signature: ")
+ (mapcar 'list files)
+ nil
+ nil
+ (unless (cdr files) (car files))
+ randomsig-history)))
+
+
+(defun randomsig-read-signatures-to-buffer (buffer-name &optional files)
+ ;; read the signatures into the signature buffer
+ ;; save possibly local variables `randomsig-files' and `randomsig-dir'
+ (let ((sigfiles randomsig-files) (sigdir randomsig-dir))
+ (if (get-buffer buffer-name)
+ (progn
+ (set-buffer buffer-name)
+ (setq buffer-read-only nil)
+ (delete-region (point-min) (point-max)))
+ (progn
+ (get-buffer-create buffer-name)
+ (set-buffer buffer-name)))
+ (set (make-local-variable 'randomsig-files) sigfiles)
+ (set (make-local-variable 'randomsig-dir) sigdir))
+
+ (setq randomsig-buffer-file-pos-list nil)
+
+ (unless files
+ (setq files randomsig-files))
+
+ (setq randomsig-loaded-files files)
+
+ ;; get a list with file names of signature files
+ (let ((sigfiles (randomsig-files-to-list files)))
+ ;; Insert all files into the newly created buffer
+ (mapcar
+ (lambda (fname)
+
+ (let ((pos (point-max)))
+ ;;(add-to-list 'randomsig-buffer-file-pos-list (cons fname pos) t)
+ ; this does not work with XEmacs
+ (goto-char pos)
+ (insert-file-contents (expand-file-name fname randomsig-dir))
+ ;; No delimiter at the beginning? Insert one.
+ (unless (string-match randomsig-delimiter-pattern
+ (buffer-substring (goto-char pos)
+ (line-end-position)))
+ (goto-char pos)
+ (insert randomsig-delimiter)
+ (insert "\n")
+ ;; Correct position...
+ (setq pos (+ pos (length randomsig-delimiter) 1)))
+
+ (setq randomsig-buffer-file-pos-list
+ (append randomsig-buffer-file-pos-list
+ (list (cons fname pos))))
+ (goto-char (point-max))
+ (unless (and (char-before)
+ (char-equal (char-before) ?\n)) ; Newline?
+ (insert "\n"))))
+ sigfiles)
+ (set-buffer-modified-p nil)
+ (setq buffer-read-only t)
+ (current-buffer)))
+
+
+
+(defun randomsig-insert-signature (sig)
+ ;; Insert SIG as signature in current buffer
+ (save-excursion
+ (goto-char (point-max))
+ (insert "\n-- \n" sig)))
+
+
+
+(defun randomsig-goto-signature ()
+;; This function is stolen fom message-goto signature.
+;; Go to beginnig of the signature, and return t.
+;; If there is no signature in current buffer, go to end of buffer,
+;; and return nil.
+ (goto-char (point-min))
+ (if (re-search-forward "^-- $" nil t)
+ (progn
+ (forward-line 1)
+ t)
+ (progn
+ (goto-char (point-max))
+ nil)))
+
+
+
+(defun randomsig-replace-signature (sig)
+ ;; Replace the current signature with SIG
+ (save-excursion
+ (when (randomsig-goto-signature)
+ (forward-line -1)
+ (backward-char)
+ (delete-region (point) (point-max)))
+
+ (randomsig-insert-signature sig)))
+
+
+(defun randomsig-signature (&optional files)
+ "Return a randomly choosen signature.
+If FILES is non-nil, a signature out of FILES will be choosen.
+Else a signature out of `randomsig-files' will be choosen."
+ (save-excursion
+
+ (randomsig-read-signatures-to-buffer randomsig-buffer-name files)
+
+ (goto-char (point-min))
+ (let '(count 0) 'selected
+
+ ;; Count number of signatures
+ (while (search-forward-regexp randomsig-delimiter-pattern nil t)
+ (setq count (1+ count)))
+
+ ;; Select random signature out out these
+ (setq selected (1+ (random count)))
+ (goto-char (point-min))
+ (if (search-forward-regexp randomsig-delimiter-pattern nil t selected)
+ (forward-char))
+
+ ;; Cut signature and return it
+ (let '(here (point)) 'signature-string
+
+ (if (not (search-forward-regexp randomsig-delimiter-pattern
+ nil t))
+ (goto-char (point-max))
+ (beginning-of-line))
+ (setq signature-string
+ (concat randomsig-static-string
+ (buffer-substring here (point))))
+ (kill-buffer randomsig-buffer-name)
+ signature-string))))
+
+
+(defun randomsig-replace-sig (arg)
+ "Replace the actual signature with a new one.
+When called with prefix, read the filename of the signature-file
+that should be used"
+ (interactive "P")
+ (save-excursion
+
+ (randomsig-replace-signature
+ (randomsig-signature
+ (if arg
+ (randomsig-prompt "read from signature-lib: ")
+ randomsig-files)))))
+
+
+
+(defun randomsig-message-read-sig (arg)
+ "Get the signature of current message and copy it to a file.
+If mark is active, get the marked region instead.
+When called with prefix, let the user edit the signature before saving"
+ (interactive "P")
+ (save-excursion
+ (let '(signature-string
+ (if (randomsig-mark-active-p)
+
+ (buffer-substring (point) (mark))
+
+ (progn
+ (if (randomsig-goto-signature)
+ (let `(here (point))
+ (goto-char (point-max))
+ (while (char-equal (char-before) 10)
+ (backward-char))
+ (buffer-substring here (point)))
+ nil))))
+ (when signature-string
+ (if arg
+ (progn
+ ;; make sure this is nil...
+ (setq randomsig-select-edit-bufferpos nil)
+ (randomsig-edit signature-string))
+ (randomsig-write-signature signature-string))))))
+
+
+(defun randomsig-write-signature (signature-string)
+ (set-buffer (find-file-noselect
+ (expand-file-name
+ (randomsig-prompt "Write to signature-lib: ")
+ randomsig-dir)))
+
+ (goto-char (point-max))
+ (insert (concat randomsig-delimiter "\n"))
+ (insert signature-string)
+ (insert "\n")
+ (save-buffer))
+
+
+(defun gnus/randomsig-summary-read-sig (arg)
+ "Get the signature of current message and copy it to a file"
+ (interactive "P")
+ (progn ;save-excursion
+ ;; FIXME: Doesn't return to summary buffer (save-excursion should do this)
+ (gnus-summary-select-article-buffer)
+ (randomsig-message-read-sig arg)))
+
+
+(defun randomsig-search-sigfiles (&optional file)
+ "Scan `randomsig-dir' and its subdirectories for regular files.
+If FILE is given, only FILE and its subdirectory will be searched."
+ (unless (file-exists-p randomsig-dir)
+ (error "\"%s\" does not exist" randomsig-dir))
+ (unless (file-directory-p randomsig-dir)
+ (error "\"%s\" is not a directory" randomsig-dir))
+ (unless file
+ (setq file ""))
+
+ (if (or (string-match "\\(/\\|^\\)\\(\\.\\|\\.\\.\\)$" file)
+ (string-match randomsig-search-unwanted file))
+ ;; unwanted...
+ nil
+
+ (let '(path (expand-file-name file randomsig-dir))
+ (if (file-directory-p path)
+ (mapcan (lambda (f)
+ (randomsig-search-sigfiles (if (string= file "")
+ f
+ (concat file "/" f))))
+ (directory-files path))
+ (if (file-regular-p path)
+ (list file)
+ nil)))))
+
+
+;;; Commands/Function for randomsig-edit-mode
+
+(defun randomsig-edit (signature)
+ (if (get-buffer randomsig-edit-buffer-name)
+ (kill-buffer randomsig-edit-buffer-name))
+ (switch-to-buffer (get-buffer-create randomsig-edit-buffer-name))
+ (insert signature)
+ (goto-char (point-min))
+ (set-buffer-modified-p t)
+ (setq buffer-read-only nil)
+ (randomsig-edit-mode))
+
+
+
+(defun randomsig-replace-signature-in-signature-files (signature)
+ (if (not randomsig-select-edit-bufferpos)
+ (error "Not in select buffer previously"))
+ (set-buffer randomsig-buffer-name)
+ (let* ((fname (randomsig-buffer-which-file))
+ (sig_end
+ ;; point in selection buffer, where signature ends
+ (progn
+ (if (search-forward-regexp randomsig-delimiter-pattern nil t)
+ (search-backward-regexp randomsig-delimiter-pattern nil nil))
+ (point)))
+ (sig_start
+ ;; point in selection buffer, where signature starts
+ (progn
+ (if (search-backward-regexp randomsig-delimiter-pattern nil t)
+ (progn
+ (search-forward-regexp randomsig-delimiter-pattern nil nil)
+ (forward-char)))
+ (point)))
+ (f_start
+ ;; point in selection buffer, where signature file starts
+ (- (cdr (assoc fname randomsig-buffer-file-pos-list))
+ (point-min)))
+ ;; point in file, where Signature starts/ends
+ (f_sig_start (- sig_start f_start))
+ (f_sig_end (- sig_end f_start))
+ ;; old signature
+ (old_sig (randomsig-signature-at-point)))
+ (set-buffer (find-file-noselect (expand-file-name fname randomsig-dir)))
+
+ (if (not (string= old_sig (buffer-substring f_sig_start f_sig_end)))
+ (error "Signature file has changed"))
+ (delete-region f_sig_start f_sig_end)
+ (goto-char f_sig_start)
+ (insert signature)
+ (save-buffer))
+ (randomsig-select-reload))
+
+
+(defun randomsig-edit-done ()
+ (interactive)
+ (let ((signature-string (buffer-string))
+ (edit-buffer (current-buffer)))
+ (if randomsig-select-edit-bufferpos
+ (randomsig-replace-signature-in-signature-files signature-string)
+ (randomsig-write-signature signature-string))
+ (kill-buffer edit-buffer)))
+
+
+(define-derived-mode randomsig-edit-mode text-mode
+ "Randomsig Edit"
+ "A major mode for editing signatures.
+You most likely do not want to call `randomsig-edit-mode' directly.
+
+\\{randomsig-edit-mode-map}"
+ (define-key randomsig-edit-mode-map
+ (kbd "C-c C-c") 'randomsig-edit-done))
+
+
+;;; Commands for randomsig-select-mode
+
+(defun randomsig-select-next ()
+ "Goto next signature."
+ (interactive)
+ (if (search-forward-regexp randomsig-delimiter-pattern nil t)
+ (forward-char)))
+
+
+(defun randomsig-select-prev ()
+ "Goto next signature."
+ (interactive)
+ (if (search-backward-regexp randomsig-delimiter-pattern nil t 2)
+ (forward-line)))
+
+
+(defun randomsig-signature-at-point()
+ ;; Return the signature at current cursor position
+ (save-excursion
+ (if (search-backward-regexp randomsig-delimiter-pattern nil t)
+ (forward-line))
+ (let ((beginning (point)))
+ (if (search-backward-regexp randomsig-delimiter-pattern nil t)
+ (forward-line))
+ (if (not (search-forward-regexp randomsig-delimiter-pattern
+ nil t))
+ (goto-char (point-max))
+ (beginning-of-line))
+ (buffer-substring beginning (point)))))
+
+
+(defun randomsig-select-replace ()
+ "Replace the signature in `randomsig-select-original-buffer'
+with the signature at the current position, and quit selection."
+ (interactive)
+ (let ((sig (randomsig-signature-at-point)))
+ (kill-buffer randomsig-buffer-name)
+ (switch-to-buffer randomsig-select-original-buffer)
+ (randomsig-replace-signature (concat randomsig-static-string sig))
+ (goto-char randomsig-select-original-position)))
+
+
+(defun randomsig-select-quit ()
+ "Quit the signature-buffer without selection of a signature."
+ (interactive)
+ (kill-buffer randomsig-buffer-name))
+
+
+(defun randomsig-select-abort ()
+ "Abort the selection from signature-buffer."
+ (interactive)
+ (ding)
+ (kill-buffer randomsig-buffer-name))
+
+
+(defun randomsig-select-reload ()
+ "Reload the current randomsig-buffer"
+ (interactive)
+ (set-buffer randomsig-buffer-name)
+ (let ((pos (point)))
+ (randomsig-read-signatures-to-buffer randomsig-buffer-name
+ randomsig-loaded-files)
+ (goto-char pos)))
+
+
+(defun randomsig-select-edit ()
+ "Edit the signature at point"
+ (interactive)
+ (setq randomsig-select-edit-bufferpos (point))
+ (randomsig-edit (randomsig-signature-at-point)))
+
+
+(defun randomsig-buffer-which-file ()
+ (let ((p 0)
+ (fname "")
+ (l randomsig-buffer-file-pos-list))
+ (while (progn
+ (setq fname (car (car l)))
+ (setq l (cdr l))
+ (setq p (cdr (car l)))
+ (and l (<= p (point)))))
+ fname))
+
+
+(define-derived-mode randomsig-select-mode fundamental-mode
+ "Randomsig Select"
+ "A major mode for selecting signatures.
+You most likely do not want to call `randomsig-select-mode' directly; use
+`randomsig-select-sig' instead.
+
+\\{randomsig-select-mode-map}"
+
+ (define-key randomsig-select-mode-map (kbd "n") 'randomsig-select-next)
+ (define-key randomsig-select-mode-map (kbd "p") 'randomsig-select-prev)
+ (define-key randomsig-select-mode-map (kbd "?") 'describe-mode)
+ (define-key randomsig-select-mode-map (kbd "h") 'describe-mode)
+ (define-key randomsig-select-mode-map (kbd "RET") 'randomsig-select-replace)
+ (define-key randomsig-select-mode-map (kbd "R") 'randomsig-select-reload)
+ (define-key randomsig-select-mode-map (kbd "e") 'randomsig-select-edit)
+ (define-key randomsig-select-mode-map (kbd "q") 'randomsig-select-quit)
+ (define-key randomsig-select-mode-map (kbd "C-g") 'randomsig-select-abort)
+
+ ;; Experimental: show the file
+ ;; FIXME: this does only work for Gnu Emacs 21
+ (and (not (boundp 'xemacs-codename))
+ (>= emacs-major-version 21)
+ (setq mode-line-buffer-identification
+ '(:eval (format "%-12s"
+ (concat "["
+ (randomsig-buffer-which-file)
+ "]"))))))
+
+(defun randomsig-select-sig (arg)
+ "Select a new signature from a list.
+If called with prefix argument, read the filename of the signature-file
+that should be used."
+ (interactive "P")
+
+ (setq randomsig-select-original-buffer (current-buffer))
+ (setq randomsig-select-original-position (point))
+
+
+ (switch-to-buffer
+ (randomsig-read-signatures-to-buffer
+ randomsig-buffer-name
+ (if arg
+ (randomsig-prompt "read from signature-lib: ")
+ randomsig-files)))
+ (goto-char 0)
+ (forward-line)
+ (randomsig-select-mode))
+
+
+
+(provide 'randomsig)
+
+
+;;; randomsig.el ends here
diff --git a/lib/net/signel.org b/lib/net/signel.org
new file mode 100644
index 0000000..25b7d25
--- /dev/null
+++ b/lib/net/signel.org
@@ -0,0 +1,546 @@
+#+title: signel, a barebones signal chat on top of signal-cli
+#+date: <2020-02-23 05:03>
+#+filetags: emacs
+#+PROPERTY: header-args :tangle yes :comments yes :results silent
+
+Unlike most chat systems in common use, [[https://signal.org][Signal]] lacks a decent emacs
+client. All i could find was [[https://github.com/mrkrd/signal-msg][signal-msg]], which is able only to send
+messages and has a readme that explicitly warns that its is /not/ a chat
+application. Skimming over signal-msg's code i learnt about
+[[https://github.com/AsamK/signal-cli][signal-cli]], a java-based daemon that knows how to send and receive
+signal messages, and how to link to a nearby phone, or register new
+users. And playing with it i saw that it can output its activities
+formatted as JSON, and that offers (when run in daemon mode) a DBUS
+service that can be used to send messages.
+
+Now, emacs knows how to run a process and capture its output handling
+it to a filter function, and comes equipped with a JSON parser and
+a set of built-in functions to talk to DBUS buses.
+
+So how about writing a simple Signal chat app for emacs? Let's call it
+/signel/, and write it as [[https://gitlab.com/jaor/elibs/-/blob/master/net/signel.org][a blog post in literate org-mode]].
+
+* Starting a process
+
+We are going to need a variable for our identity (telephone number),
+and a list of contact names (until i discover how to get them directly
+from signal-cli):
+
+#+begin_src emacs-lisp
+(require 'cl-lib)
+
+(defvar signel-cli-user "+44744xxxxxx")
+(defvar signel-contact-names '(("+447xxxxxxxx" . "john")
+ ("+346xxxxxxxx" . "anna")))
+#+end_src
+
+and a simple function to get a contact name given its telephone
+number:
+
+#+begin_src emacs-lisp
+(defun signel--contact-name (src)
+ (or (alist-get src signel-contact-names nil nil #'string-equal) src))
+#+end_src
+
+We are also going to need the path for our signal-cli executable
+
+#+begin_src emacs-lisp
+(defvar signel-cli-exec "signal-cli")
+#+end_src
+
+Starting the signal-cli process is easy: ~make-process~ provides all the
+necessary bits. What we need is essentially calling
+
+#+begin_src shell
+signal-cli -u +44744xxxxxx daemon --json
+#+end_src
+
+associating to the process a buffer selected by the function
+~signel--proc-buffer~ . While we are at it, we'll write also little
+helpers for users of our API.
+
+#+begin_src emacs-lisp
+(defun signel--proc-buffer ()
+ (get-buffer-create "*signal-cli*"))
+
+(defun signel-signal-cli-buffer ()
+ (get-buffer "*signal-cli*"))
+
+(defun signel-signal-cli-process ()
+ (when-let ((proc (get-buffer-process (signel-signal-cli-buffer))))
+ (and (process-live-p proc) proc)))
+#+end_src
+
+#+begin_src emacs-lisp
+(defun signel-start ()
+ "Start the underlying signal-cli process if needed."
+ (interactive)
+ (if (signel-signal-cli-process)
+ (message "signal-cli is already running!")
+ (let ((b (signel--proc-buffer)))
+ (make-process :name "signal-cli"
+ :buffer b
+ :command `(,signel-cli-exec
+ "-u"
+ ,signel-cli-user
+ "daemon" "--json")
+ :filter #'signel--filter)
+ (message "Listening to signals!"))))
+#+end_src
+
+* Parsing JSON
+
+We've told emacs to handle any ouput of the process to the function
+~signel--filter~, which we're going to write next. This function will
+receive the process object and its latest output as a string
+representing a JSON object. Here's an example of the kind of outputs
+that signal-cli emits:
+
+#+begin_src json :tangle no
+{
+ "envelope": {
+ "source": "+4473xxxxxxxx",
+ "sourceDevice": 1,
+ "relay": null,
+ "timestamp": 1582396178696,
+ "isReceipt": false,
+ "dataMessage": {
+ "timestamp": 1582396178696,
+ "message": "Hello there!",
+ "expiresInSeconds": 0,
+ "attachments": [],
+ "groupInfo": null
+ },
+ "syncMessage": null,
+ "callMessage": null,
+ "receiptMessage": null
+ }
+}
+#+end_src
+
+Everything seems to be always inside ~envelope~, which contains objects
+for the possible messages received. In the example above, we're
+receiving a message from a /source/ contact. We can also receive
+receipt messages, telling us whether our last message has been
+received or read; e.g.:
+
+#+begin_src json :tangle no
+{
+ "envelope": {
+ "source": "+4473xxxxxxxx",
+ "sourceDevice": 1,
+ "relay": null,
+ "timestamp": 1582397117584,
+ "isReceipt": false,
+ "dataMessage": null,
+ "syncMessage": null,
+ "callMessage": null,
+ "receiptMessage": {
+ "when": 1582397117584,
+ "isDelivery": true,
+ "isRead": false,
+ "timestamps": [
+ 1582397111524
+ ]
+ }
+ }
+}
+#+end_src
+
+A bit confusingly, that delivery notification has a ~receiptMessage~,
+but its ~isReceipt~ flag is set to ~false~. At other times, we get
+~isReceipt~ but no ~receiptMessage~:
+
+#+begin_src json :tangle no
+{
+ "envelope": {
+ "source": "+346xxxxxxxx",
+ "sourceDevice": 1,
+ "relay": null,
+ "timestamp": 1582476539281,
+ "isReceipt": true,
+ "dataMessage": null,
+ "syncMessage": null,
+ "callMessage": null,
+ "receiptMessage": null
+ }
+}
+#+end_src
+
+It is very easy to parse JSON in emacs and extract signal-cli's
+envelopes (and it's become faster in emacs 27, but the interface is a
+bit different):
+
+#+begin_src emacs-lisp
+(defun signel--parse-json (str)
+ (if (> emacs-major-version 26)
+ (json-parse-string str
+ :null-object nil
+ :false-object nil
+ :object-type 'alist
+ :array-type 'list)
+ (json-read-from-string str)))
+
+(defun signel--msg-contents (str)
+ (alist-get 'envelope (ignore-errors (signel--parse-json str))))
+#+end_src
+
+Here i am being old-school and opting to receive JSON dicitionaries as
+alists (rather than hash maps, the default), and arrays as lists
+rather than vectors just because lisps are lisps for a reason. I'm
+also going to do some mild [[https://lispcast.com/nil-punning/][nil punning]],
+hence the choice for null and false representations.
+
+Once the contents of the envelope is extracted, it's trivial (and
+boring) to get into its components:
+
+#+begin_src emacs-lisp
+(defun signel--msg-source (msg) (alist-get 'source msg))
+
+(defun signel--msg-data (msg)
+ (alist-get 'message (alist-get 'dataMessage msg)))
+
+(defun signel--msg-timestamp (msg)
+ (if-let (msecs (alist-get 'timestamp msg))
+ (format-time-string "%H:%M" (/ msecs 1000))
+ ""))
+
+;; emacs 26 compat
+(defun signel--not-false (x)
+ (and (not (eq :json-false x)) x))
+
+(defun signel--msg-receipt (msg)
+ (alist-get 'receiptMessage msg))
+
+(defun signel--msg-is-receipt (msg)
+ (signel--not-false (alist-get 'isReceipt msg)))
+
+(defun signel--msg-receipt-timestamp (msg)
+ (when-let (msecs (alist-get 'when (signel--msg-receipt msg)))
+ (format-time-string "%H:%M" (/ msecs 1000))))
+
+(defun signel--msg-is-delivery (msg)
+ (when-let ((receipt (signel--msg-receipt msg)))
+ (signel--not-false (alist-get 'isDelivery msg))))
+
+(defun signel--msg-is-read (msg)
+ (when-let ((receipt (signel--msg-receipt msg)))
+ (signel--not-false (alist-get 'isRead msg))))
+#+end_src
+
+* A process output filter
+
+We're almost ready to write our filter. It will:
+
+- For debugging purposes, insert the raw JSON string in the process
+ buffer.
+- Parse the received JSON string and extract its envelope contents.
+- Check wether it has a source and either message data or a receipt
+ timestamp.
+- Dispatch to a helper function that will insert the data or
+ notification in a chat buffer.
+
+Or, in elisp:
+
+#+begin_src emacs-lisp
+(defvar signel--line-buffer "")
+
+(defun signel--filter (proc str)
+ (signel--ordinary-insertion-filter proc str)
+ (let ((str (concat signel--line-buffer str)))
+ (if-let ((msg (signel--msg-contents str)))
+ (let ((source (signel--msg-source msg))
+ (stamp (signel--msg-timestamp msg))
+ (data (signel--msg-data msg))
+ (rec-stamp (signel--msg-receipt-timestamp msg)))
+ (setq signel--line-buffer "")
+ (when source
+ (signel--update-chat-buffer source data stamp rec-stamp msg)))
+ (setq signel--line-buffer
+ (if (string-match-p ".*\n$" str) "" str)))))
+#+end_src
+
+We've had to take care of the case when the filter receives input that
+is not a complete JSON expression: in the case of signal-cli, that
+only happens when we haven't seen yet an end of line.
+
+The function to insert the raw contents in the process buffer is
+surprisingly hard to get right, but the emacs manual spells out a
+reasonable implementation, which i just copied:
+
+#+begin_src emacs-lisp
+(defun signel--ordinary-insertion-filter (proc string)
+ (when (and proc (buffer-live-p (process-buffer proc)))
+ (with-current-buffer (process-buffer proc)
+ (let ((moving (= (point) (process-mark proc))))
+ (save-excursion
+ ;; Insert the text, advancing the process marker.
+ (goto-char (process-mark proc))
+ (insert string)
+ (set-marker (process-mark proc) (point)))
+ (if moving (goto-char (process-mark proc)))))))
+#+end_src
+
+* It's not an emacs app if it doesn't have a new mode
+
+With that out of the way, we just have to insert our data in an
+appropriate buffer. We are going to associate a separate buffer to
+each /source/, using for that its name:
+
+#+begin_src emacs-lisp
+(defvar-local signel-user nil)
+
+(defun signel--contact-buffer (source)
+ (let* ((name (format "*%s" (signel--contact-name source)))
+ (buffer (get-buffer name)))
+ (unless buffer
+ (setq buffer (get-buffer-create name))
+ (with-current-buffer buffer
+ (signel-chat-mode)
+ (setq-local signel-user source)
+ (insert signel-prompt)))
+ buffer))
+#+end_src
+
+where, as is often the case in emacs, we are going to have a dedicated
+major mode for chat buffers, called ~signel-chat-mode~. For now, let's
+keep it really simple (for the record, this is essentially a copy of
+what ERC does for its erc-mode):
+
+#+begin_src emacs-lisp
+(defvar signel-prompt ": ")
+
+(define-derived-mode signel-chat-mode fundamental-mode "Signal"
+ "Major mode for Signal chats."
+ (when (boundp 'next-line-add-newlines)
+ (set (make-local-variable 'next-line-add-newlines) nil))
+ (setq line-move-ignore-invisible t)
+ (set (make-local-variable 'paragraph-separate)
+ (concat "\C-l\\|\\(^" (regexp-quote signel-prompt) "\\)"))
+ (set (make-local-variable 'paragraph-start)
+ (concat "\\(" (regexp-quote signel-prompt) "\\)"))
+ (setq-local completion-ignore-case t))
+#+end_src
+
+Note how, in ~signel--contact-buffer~, we're storing the user identity
+associated with the buffer (its /source/) in a buffer-local variable
+named ~signel-user~ that is set /after/ enabling ~signel-chat-mode~: order
+here matters because the major mode activation cleans up the values of
+any local variables previously set (i always forget that!).
+
+* And a customization group
+
+We're going to need a couple of new faces for the different parts of
+inserted messages, so we'll take the chance to be tidy and introduce a
+customization group:
+
+#+begin_src emacs-lisp
+(defgroup signel nil "Signel")
+
+(defface signel-contact '((t :weight bold))
+ "Face for contact names."
+ :group 'signel)
+
+(defface signel-timestamp '((t :foreground "grey70"))
+ "Face for timestamp names."
+ :group 'signel)
+
+(defface signel-notice '((t :inherit signel-timestamp))
+ "Face for delivery notices."
+ :group 'signel)
+
+(defface signel-prompt '((t :weight bold))
+ "Face for the input prompt marker."
+ :group 'signel)
+
+(defface signel-user '((t :foreground "orangered"))
+ "Face for sent messages."
+ :group 'signel)
+
+(defface signel-notification '((t :foreground "burlywood"))
+ "Face for notifications shown by tracking, when available."
+ :group 'signel)
+
+#+end_src
+
+
+* Displaying incoming messages
+
+We have now almost all the ingredients to write
+~signel--update-chat-buffer~, the function that inserts the received
+message data into the chat buffer. Let's define a few little
+functions to format those parts:
+
+#+begin_src emacs-lisp
+(defun signel--contact (name)
+ (propertize name 'face 'signel-contact))
+
+(defun signel--timestamp (&rest p)
+ (propertize (apply #'concat p) 'face 'signel-timestamp))
+
+(defun signel--notice (notice)
+ (propertize notice 'face 'signel-notice))
+
+(defun signel--insert-prompt ()
+ (let ((inhibit-read-only t)
+ (p (point)))
+ (insert signel-prompt)
+ (set-text-properties p (- (point) 1)
+ '(face signel-prompt
+ read-only t front-sticky t rear-sticky t))))
+
+(defun signel--delete-prompt ()
+ (when (looking-at-p (regexp-quote signel-prompt))
+ (let ((inhibit-read-only t))
+ (delete-char (length signel-prompt)))))
+
+(defun signel--delete-last-prompt ()
+ (goto-char (point-max))
+ (when (re-search-backward (concat "^" (regexp-quote signel-prompt)))
+ (signel--delete-prompt)))
+
+#+end_src
+
+With that, we're finally ready to insert messages in our signel chat
+buffers:
+
+#+begin_src emacs-lisp
+(defcustom signel-report-deliveries nil
+ "Whether to show message delivery notices."
+ :group 'signel
+ :type 'boolean)
+
+(defcustom signel-report-read t
+ "Whether to show message read notices."
+ :group 'signel
+ :type 'boolean)
+
+(defun signel--prompt-and-notify ()
+ (signel--insert-prompt)
+ (when (fboundp 'tracking-add-buffer)
+ (tracking-add-buffer (current-buffer) '(signel-notification))))
+
+(defun signel--needs-insert-p (data stamp rec-stamp msg)
+ (or data
+ (and (or rec-stamp stamp)
+ (not (string= source signel-cli-user))
+ (or signel-report-deliveries
+ (and signel-report-read (signel--msg-is-read msg))))))
+
+(defun signel--update-chat-buffer (source data stamp rec-stamp msg)
+ (when (signel--needs-insert-p data stamp rec-stamp msg)
+ (when-let ((b (signel--contact-buffer source)))
+ (with-current-buffer b
+ (signel--delete-last-prompt)
+ (if data
+ (let ((p (point)))
+ (insert (signel--timestamp "[" stamp "] ")
+ (signel--contact (signel--contact-name source))
+ signel-prompt
+ data
+ "\n")
+ (fill-region p (point)))
+ (let ((is-read (signel--msg-is-read msg)))
+ (insert (signel--timestamp "*" (or rec-stamp stamp) "* ")
+ (signel--notice (if is-read "(read)" "(delivered)"))
+ "\n")))
+ (signel--prompt-and-notify)
+ (end-of-line)))))
+#+end_src
+
+There are some rough edges in the above implementation that must be
+polished should signel ever be released in the wild. For one, proper
+handling of timestamps and their formats. And of course notifications
+should be much more customizable (here i'm using [[https://github.com/jorgenschaefer/circe/blob/master/tracking.el][Circe's tracking.el]]
+if available).
+
+* Sending messages: the DBUS interface
+
+With that, we're going to receive and display messages and simple
+receipts, and i'm sure that we will feel the urge to answer some of
+them. As mentioned above, signal-cli let's us send messages via its
+[[https://github.com/AsamK/signal-cli/wiki/DBus-service][DBUS interface]].
+In a nutshell, if you want to send ~MESSAGETEXT~ to a
+~RECIPIENT~ you'd invoke something like:
+
+#+begin_src shell :tangle no
+dbus-send --session --type=method_call \
+ --dest="org.asamk.Signal" \
+ /org/asamk/Signal \
+ org.asamk.Signal.sendMessage \
+ string:MESSAGETEXT array:string: string:RECIPIENT
+#+end_src
+
+That is, call the method ~sendMessage~ of the corresponding service
+interface with three arguments (the second one empty). Using emacs'
+dbus libray one can write the above as:
+
+#+begin_src emacs-lisp
+(defun signel--send-message (user msg)
+ (dbus-call-method :session "org.asamk.Signal" "/org/asamk/Signal"
+ "org.asamk.Signal" "sendMessage"
+ :string msg
+ '(:array)
+ :string user))
+#+end_src
+
+The only complicated bit is being careful with the specification of
+the types of the method arguments: if one gets them wrong, DBUS will
+simply complain and say that the method is not defined, which was
+confusing me at first (but of course makes sense because DBUS allows
+overloading method names, so the full method spec must include its
+signature).
+
+We want to read whatever our user writes after the last prompt and
+send it via the little helper above. Here's our interactive command
+for that:
+
+#+begin_src emacs-lisp
+(defun signel-send ()
+ "Read text inserted in the current buffer after the last prompt and send it.
+
+The recipient of the message is looked up in a local variable set
+when the buffer was created."
+ (interactive)
+ (goto-char (point-max))
+ (beginning-of-line)
+ (let* ((p (point))
+ (plen (length signel-prompt))
+ (msg (buffer-substring (+ p plen) (point-max))))
+ (signel--delete-prompt)
+ (signel--send-message signel-user msg)
+ (insert (signel--timestamp (format-time-string "(%H:%M) ")))
+ (fill-region p (point-max))
+ (goto-char (point-max))
+ (set-text-properties p (point) '(face signel-user))
+ (insert "\n")
+ (signel--insert-prompt)))
+#+end_src
+
+and we can bind it to the return key in signal chat buffers:
+
+#+begin_src emacs-lisp
+(define-key signel-chat-mode-map "\C-m" #'signel-send)
+#+end_src
+
+And we are going sometimes to want to talk to contacts that don't have
+yet said anything and have, therefore, no associated chat buffer:
+
+#+begin_src emacs-lisp
+(defun signel-query (contact)
+ "Start a conversation with a signal contact."
+ (interactive (list (completing-read "Signal to: "
+ (mapcar #'cdr-safe signel-contact-names))))
+ (let ((phone (alist-get contact
+ (cl-pairlis (mapcar #'cdr signel-contact-names)
+ (mapcar #'car signel-contact-names))
+ nil nil #'string-equal)))
+ (when (not phone)
+ (error "Unknown contact %s" contact))
+ (pop-to-buffer (signel--contact-buffer phone))))
+#+end_src
+
+There are of course lots of rough edges and missing functionality in
+this incipient signel, but it's already usable and a nice
+demonstration of how easy it is to get the ball rolling in this lisp
+machine of ours!
diff --git a/lib/org/jao-org-gnus.el b/lib/org/jao-org-gnus.el
new file mode 100644
index 0000000..cdeec65
--- /dev/null
+++ b/lib/org/jao-org-gnus.el
@@ -0,0 +1,72 @@
+;; Support for saving Gnus messages by Message-ID
+(defun mde-org-gnus-save-by-mid ()
+ (when (memq major-mode '(gnus-summary-mode gnus-article-mode))
+ (when (eq major-mode 'gnus-article-mode)
+ (gnus-article-show-summary))
+ (let* ((group gnus-newsgroup-name)
+ (method (gnus-find-method-for-group group)))
+ (when (memq (car method) '(nnml nntp))
+ (let* ((article (gnus-summary-article-number))
+ (header (gnus-summary-article-header article))
+ (from (mail-header-from header))
+ (message-id
+ (save-match-data
+ (let ((mid (mail-header-id header)))
+ (if (string-match "<\\(.*\\)>" mid)
+ (match-string 1 mid)
+ (error "Malformed message ID header %s" mid)))))
+ (date (mail-header-date header))
+ (subject (gnus-summary-subject-string)))
+ (org-store-link-props :type "mid" :from from :subject subject
+ :message-id message-id :group group
+ :link (org-make-link "mid:" message-id))
+ (apply 'org-store-link-props
+ :description (org-email-link-description)
+ org-store-link-plist)
+ t)))))
+
+(defvar mde-mid-resolve-methods '()
+ "List of methods to try when resolving message ID's. For Gnus,
+it is a cons of 'gnus and the select (type and name).")
+(setq mde-mid-resolve-methods
+ '((gnus nnml "")))
+
+(defvar mde-org-gnus-open-level 1
+ "Level at which Gnus is started when opening a link")
+(defun mde-org-gnus-open-message-link (msgid)
+ "Open a message link with Gnus"
+ (require 'gnus)
+ (require 'org-table)
+ (catch 'method-found
+ (message "[MID linker] Resolving %s" msgid)
+ (dolist (method mde-mid-resolve-methods)
+ (cond
+ ((and (eq (car method) 'gnus)
+ (eq (cadr method) 'nnml))
+ (funcall (cdr (assq 'gnus org-link-frame-setup))
+ mde-org-gnus-open-level)
+ (when gnus-other-frame-object
+ (select-frame gnus-other-frame-object))
+ (let* ((msg-info (nnml-find-group-number
+ (concat "<" msgid ">")
+ (cdr method)))
+ (group (and msg-info (car msg-info)))
+ (message (and msg-info (cdr msg-info)))
+ (qname (and group
+ (if (gnus-methods-equal-p
+ (cdr method)
+ gnus-select-method)
+ group
+ (gnus-group-full-name group (cdr method))))))
+ (when msg-info
+ (gnus-summary-read-group qname nil t)
+ (gnus-summary-goto-article message nil t))
+ (throw 'method-found t)))
+ (t (error "Unknown link type"))))))
+
+(eval-after-load 'org-gnus
+ '(progn
+ (add-to-list 'org-store-link-functions 'mde-org-gnus-save-by-mid)
+ (org-add-link-type "mid" 'mde-org-gnus-open-message-link)))
+
+(provide 'jao-org-gnus)
diff --git a/lib/org/jao-org-links.el b/lib/org/jao-org-links.el
new file mode 100644
index 0000000..7d9cb55
--- /dev/null
+++ b/lib/org/jao-org-links.el
@@ -0,0 +1,147 @@
+(require 'jao-org-utils)
+(require 'pdf-info)
+
+(defvar jao-org--sink-dir "./")
+(defvar jao-org-open-pdf-fun 'jao-org--pdf-tools-open)
+
+(defun jao-org--pdf-tools-open (path page &optional height)
+ (org-open-file path 1)
+ (pdf-view-goto-page page)
+ (when height
+ (image-set-window-vscroll
+ (round (/ (* height (cdr (pdf-view-image-size))) (frame-char-height))))))
+
+(defun jao-org--pdf-open (path page &optional height)
+ (funcall (or jao-org-open-pdf-fun 'jao-org--pdf-tools-open) path page height))
+
+(defun jao-org--pdf-p (file) (string-match-p ".*\\.pdf$" file))
+
+(defun jao-org-links--open-pdf (link)
+ "Open LINK in pdf-view-mode."
+ (require 'pdf-tools)
+ (cond ((string-match "\\(.*\\)::\\([0-9]*\\)\\+\\+\\([[0-9]\\.*[0-9]*\\)" link)
+ (let* ((path (match-string 1 link))
+ (page (string-to-number (match-string 2 link)))
+ (height (string-to-number (match-string 3 link))))
+ (jao-org--pdf-open path page height)))
+ ((string-match "\\(.*\\)::\\([0-9]+\\)$" link)
+ (let* ((path (match-string 1 link))
+ (page (string-to-number (match-string 2 link))))
+ (jao-org--pdf-open path page)))
+ (t (org-open-file link 1))))
+
+(defun jao-org-links--follow-doc (link)
+ (let* ((full-link (concat org-directory "/doc/" link))
+ (dest-path (car (split-string full-link "::"))))
+ (when (not (file-exists-p dest-path))
+ (let* ((sink-file (expand-file-name link jao-org--sink-dir))
+ (real-file (if (file-exists-p sink-file) sink-file
+ (read-file-name "Import file: "
+ jao-org--sink-dir link link))))
+ (shell-command (format "mv %s %s" real-file dest-path))))
+ (if (jao-org--pdf-p dest-path)
+ (jao-org-links--open-pdf full-link)
+ (browse-url (format "file://%s" (expand-file-name dest-path))))))
+
+(defun jao-org-links--complete-doc (&optional arg)
+ (let ((default-directory jao-org--sink-dir))
+ (let ((f (replace-regexp-in-string "^file:" "doc:"
+ (org-file-complete-link arg))))
+ (if (jao-org--pdf-p f)
+ (concat f "::" (read-from-minibuffer "Page: " "1"))
+ f))))
+
+(defsubst jao-org--title->file (title)
+ (concat (mapconcat 'downcase (split-string title nil t) "-") ".pdf"))
+
+(defun jao-org--pdf-title (&optional fname)
+ (let ((base (file-name-base (or fname (pdf-view-buffer-file-name)))))
+ (capitalize (replace-regexp-in-string "-" " " base))))
+
+(defvar-local jao--pdf-outline nil)
+
+(defun jao-org--pdf-section-title (&optional page)
+ (when (not jao--pdf-outline)
+ (setq-local jao--pdf-outline (pdf-info-outline)))
+ (let ((page (or page (pdf-view-current-page)))
+ (outline jao--pdf-outline)
+ (cur-page 0)
+ (cur-title (jao-org--pdf-title)))
+ (while (and (car outline) (< cur-page page))
+ (setq cur-page (cdr (assoc 'page (car outline))))
+ (when (<= cur-page page)
+ (setq cur-title (cdr (assoc 'title (car outline)))))
+ (setq outline (cdr outline)))
+ (replace-regexp-in-string "[[:blank:]]+" " " cur-title)))
+
+;;;###autoload
+(defvar jao-org-links-pdf-store-fun nil)
+
+(defun jao-org-links--store-pdf-link ()
+ (or (when (fboundp jao-org-links-pdf-store-fun)
+ (funcall jao-org-links-pdf-store-fun))
+ (when (derived-mode-p 'pdf-view-mode)
+ (jao-org-links-store-pdf-link buffer-file-name
+ (pdf-view-current-page)
+ (jao-org--pdf-section-title)))))
+
+;;;###autoload
+(defun jao-org-links-store-pdf-link (path page title)
+ (org-store-link-props
+ :type "doc"
+ :link (format "doc:%s::%d" (file-name-nondirectory path) page)
+ :description (format "%s (p. %d)" title page)))
+
+;;;###autoload
+(defun jao-org-insert-doc (title)
+ (interactive "sDocument title: ")
+ (insert (format "[[doc:%s][%s]]" (jao-org--title->file title) title)))
+
+;;;###autoload
+(defun jao-org-links-setup (sink-dir)
+ (interactive)
+ (org-link-set-parameters "doc"
+ :follow #'jao-org-links--follow-doc
+ :complete #'jao-org-links--complete-doc
+ :store #'jao-org-links--store-pdf-link)
+ (setq jao-org--sink-dir (file-name-as-directory sink-dir)))
+
+;;;###autoload
+(defvar jao-org-doc-notes-dir "notes/books")
+
+;;;###autoload
+(defun jao-org-org-to-pdf-file ()
+ (replace-regexp-in-string (format "/%s/\\(.+\\)\\.org$" jao-org-doc-notes-dir)
+ "/doc/\\1.org"
+ buffer-file-name))
+
+;;;###autoload
+(defun jao-org-pdf-to-org-file (&optional file-name)
+ (replace-regexp-in-string "/doc/\\(.+\\)\\.pdf$"
+ (format "/%s/\\1.org" jao-org-doc-notes-dir)
+ (or file-name buffer-file-name)))
+
+;;;###autoload
+(defun jao-org-insert-doc-skeleton (&optional title)
+ (insert "#+title: " (or title (jao-org--pdf-title (buffer-file-name)))
+ "\n#+author:\n#+startup: latexpreview\n\n"))
+
+;;;###autoload
+(defun jao-org-pdf-goto-org (arg)
+ (interactive "P")
+ (when (jao-org--pdf-p buffer-file-name)
+ (let* ((file (jao-org-pdf-to-org-file))
+ (new (not (file-exists-p file)))
+ (title (jao-org--pdf-title)))
+ (when (or arg new) (org-store-link nil t))
+ (find-file-other-window file)
+ (when new
+ (jao-org-insert-doc-skeleton title)
+ (org-insert-link)))))
+
+;;;###autoload
+(defun jao-org-pdf-goto-org* ()
+ (interactive)
+ (jao-org-pdf-goto-org t))
+
+(provide 'jao-org-links)
diff --git a/lib/org/jao-org-notes.el b/lib/org/jao-org-notes.el
new file mode 100644
index 0000000..3e9abbb
--- /dev/null
+++ b/lib/org/jao-org-notes.el
@@ -0,0 +1,79 @@
+;;; jao-org-notes.el --- A simple system for org note taking -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020, 2021 jao
+
+;; Author: jao <mail@jao.io>
+;; Keywords: tools
+
+;; 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:
+
+;; A note per file
+
+;;; Code:
+
+(require 'org)
+
+(defvar jao-org-notes-dir (expand-file-name "notes" org-directory))
+
+(defun jao-org-notes--insert-title ()
+ (let ((title (read-string "Title: ")))
+ (when (not (string-empty-p title))
+ (let* ((base (replace-regexp-in-string " +" "-" (downcase title)))
+ (fname (expand-file-name (concat base ".org") jao-org-notes-dir))
+ (exists? (file-exists-p fname)))
+ (find-file fname)
+ (when (not exists?)
+ (insert "#+title: " title "\n")
+ t)))))
+
+(defun jao-org-notes--insert-tags ()
+ (let ((ts (completing-read-multiple "Tags: "
+ (org-global-tags-completion-table))))
+ (insert "#+filetags:" ":" (mapconcat 'identity ts ":") ":\n")))
+
+(defun jao-org-notes--insert-date ()
+ (insert "#+date: ")
+ (org-insert-time-stamp (current-time))
+ (insert "\n"))
+
+(defun jao-org-notes--template (k)
+ `(,k "Note" plain (file jao-org-notes-open) "* %a "))
+
+;;;###autoload
+(defun jao-org-notes-open ()
+ (interactive)
+ (when (jao-org-notes--insert-title)
+ (jao-org-notes--insert-date)
+ (jao-org-notes--insert-tags)
+ (insert "#+link: "))
+ (save-buffer)
+ (buffer-file-name))
+
+;;;###autoload
+(defun jao-org-notes-setup (mnemonic)
+ (setq org-capture-templates
+ (add-to-list 'org-capture-templates (jao-org-notes--template mnemonic)))
+ (add-to-list 'org-agenda-files jao-org-notes-dir)
+ (when (fboundp 'org-capture-upgrade-templates)
+ (org-capture-upgrade-templates org-capture-templates)))
+
+;;;###autoload
+(defun jao-org-notes-backlinks ()
+ (interactive)
+ (consult-ripgrep jao-org-notes-dir (regexp-quote (buffer-name))))
+
+(provide 'jao-org-notes)
+;;; jao-org-notes.el ends here
diff --git a/lib/org/jao-org-popup.el b/lib/org/jao-org-popup.el
new file mode 100644
index 0000000..eb5b24d
--- /dev/null
+++ b/lib/org/jao-org-popup.el
@@ -0,0 +1,31 @@
+;;; frame popups
+;; http://metajack.im/2008/12/30/gtd-capture-with-emacs-orgmode/
+(defsubst jao-remember--frame-p ()
+ (equal "*Remember*" (frame-parameter nil 'name)))
+
+(defadvice remember-finalize (after delete-remember-frame activate)
+ "Advise remember-finalize to close the frame if it is the remember frame"
+ (when (jao-remember--frame-p) (delete-frame)))
+
+(defadvice remember-destroy (after delete-remember-frame activate)
+ "Advise remember-destroy to close the frame if it is the remember frame"
+ (when (jao-remember--frame-p) (delete-frame)))
+
+;; make the frame contain a single window. by default org-remember
+;; splits the window.
+(defun jao-remember--delete-other-windows ()
+ (when (jao-remember--frame-p) (delete-other-windows)))
+
+(add-hook 'remember-mode-hook 'jao-remember--delete-other-windows)
+
+(defun make-remember-frame ()
+ "Create a new frame and run org-remember"
+ (interactive)
+ (make-frame-on-display (getenv "DISPLAY")
+ '((name . "*Remember*")
+ (width . 80)
+ (height . 10)))
+ (select-frame-by-name "*Remember*")
+ (org-remember nil ?x))
+
+(provide 'jao-org-popup) \ No newline at end of file
diff --git a/lib/org/jao-org-utils.el b/lib/org/jao-org-utils.el
new file mode 100644
index 0000000..8d65ed7
--- /dev/null
+++ b/lib/org/jao-org-utils.el
@@ -0,0 +1,43 @@
+(require 'org)
+
+;;; links
+(defun jao-org-link-at-point (&optional copy)
+ (when (thing-at-point-looking-at "\\[\\[\\([^]]+\\)\\]\\[[^]]+\\]\\]")
+ (when copy (kill-ring-save (match-beginning 1) (match-end 1)))
+ (match-string-no-properties 1)))
+
+(defun jao-org-copy-link-at-point ()
+ (interactive)
+ (message "%s" (or (jao-org-link-at-point t) "No link at point")))
+
+(defun jao-org-insert-link (url title)
+ (insert (format "[[%s][%s]]" url title)))
+
+;;; eldoc
+(defun jao-org-eldoc--hook ()
+ (set (make-local-variable 'eldoc-documentation-function)
+ 'jao-org-link-at-point)
+ (eldoc-mode))
+
+;;;###autoload
+(defun jao-org-utils-eldoc-setup ()
+ (add-hook 'org-mode-hook 'jao-org-eldoc--hook))
+
+;;; play fair with saveplace
+(defun jao-org--show-if-hidden ()
+ (when (outline-invisible-p)
+ (save-excursion
+ (outline-previous-visible-heading 1)
+ (org-show-subtree))))
+
+;;; verifying org refile targets
+(defun jao-org--refile-target-verify ()
+ (not (looking-at-p ".*\\[\\[.+$")))
+
+;;;###autoload
+(defun jao-org-utils-setup ()
+ (setq org-refile-target-verify-function 'jao-org--refile-target-verify)
+ (add-hook 'org-mode-hook 'jao-org--show-if-hidden t))
+
+
+(provide 'jao-org-utils)
diff --git a/lib/prog/jao-compilation.el b/lib/prog/jao-compilation.el
new file mode 100644
index 0000000..ef303ea
--- /dev/null
+++ b/lib/prog/jao-compilation.el
@@ -0,0 +1,118 @@
+;;; jao-compilation.el --- utilities to lauch compilations -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020, 2021 jao
+
+;; Author: jao <mail@jao.io>
+;; Keywords: convenience
+
+;; 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:
+
+;; Utilities to launch compilation processes from adequate root directories
+
+;;; Code:
+
+(defvar jao-compilation-dominating-files nil)
+(defvar jao-compilation-dominating-file-rxs '(".+\\.cabal"))
+(defvar jao-compilation-environment ())
+(defvar jao-compilation-dominating-rx "")
+
+(defun jao-compilation--environment ()
+ (let (result)
+ (dolist (v jao-compilation-environment result)
+ (let ((vv (getenv v)))
+ (when vv (add-to-list 'result (format "%s=%s" v vv)))))))
+
+;;;###autoload
+(defun jao-compilation-add-dominating (&rest fs)
+ (dolist (f fs) (add-to-list 'jao-compilation-dominating-files f))
+ (setq jao-compilation-dominating-rx
+ (concat "\\("
+ (regexp-opt jao-compilation-dominating-files)
+ "\\|"
+ (mapconcat 'identity
+ jao-compilation-dominating-file-rxs
+ "\\|")
+ "\\)$")))
+
+;;;###autoload
+(defun jao-path-relative-to (path base)
+ (let* ((path (file-name-directory path))
+ (base (file-name-directory base))
+ (blen (length base)))
+ (if (<= (length path) blen)
+ path
+ (if (string-equal base (substring path 0 blen))
+ (substring path blen)
+ path))))
+
+;;;###autoload
+(defun jao-compilation-find-root (file doms)
+ (when file
+ (locate-dominating-file file `(lambda (d)
+ (when (file-directory-p d)
+ (directory-files d nil ,doms))))))
+
+;;;###autoload
+(defun jao-compilation-root (&optional dir)
+ (when-let ((rfn (jao-compilation-find-root (or dir (buffer-file-name))
+ jao-compilation-dominating-rx)))
+ (let* ((default-directory (expand-file-name rfn))
+ (dir (file-name-directory rfn))
+ (rel-path (jao-path-relative-to dir default-directory)))
+ (if (and (file-directory-p "build")
+ (not (file-exists-p "build.xml"))
+ (not (file-exists-p "setup.py")))
+ (expand-file-name rel-path (expand-file-name "build/"))
+ default-directory))))
+
+;;;###autoload
+(defun jao-compilation-root-file ()
+ (when-let ((dir (jao-compilation-root)))
+ (car (directory-files dir nil jao-compilation-dominating-rx))))
+
+;;;###autoload
+(defun jao-find-compilation-root (dir)
+ (when (and (stringp dir) (file-exists-p dir))
+ (when-let ((root (jao-compilation-root dir)))
+ (cons 'transient root))))
+
+;;;###autoload
+(defun jao-compilation-env (v)
+ "Add new environment variables to the compilation environment
+ used by `jao-compile'"
+ (add-to-list 'jao-compilation-environment v))
+
+;;;###autoload
+(defun jao-compile ()
+ "Find the root of current file's project and issue a
+ compilation command"
+ (interactive)
+ (let ((default-directory (jao-compilation-root))
+ (compilation-environment (jao-compilation--environment))
+ (compilation-read-command 'compilation-read-command))
+ (call-interactively 'compile)))
+
+;;;###autoload
+(defun jao-compilation-setup ()
+ (jao-compilation-add-dominating
+ "Makefile" "makefile" "configure.ac" "bootstrap.sh" "aclocal.m4"
+ "project.clj" "build.xml" "pom.xml" "setup.py" "stack.yaml")
+ (with-eval-after-load "project"
+ (add-to-list 'project-find-functions #'jao-find-compilation-root)))
+
+
+(provide 'jao-compilation)
+;;; jao-compilation.el ends here
diff --git a/lib/prog/jao-sloc.el b/lib/prog/jao-sloc.el
new file mode 100644
index 0000000..1f0e9ab
--- /dev/null
+++ b/lib/prog/jao-sloc.el
@@ -0,0 +1,33 @@
+;; sloc.el -- LOC utilities
+
+;;;###autoload
+(defun count-sloc-region (beg end kind)
+ "Count source lines of code in region (or (narrowed part of)
+ the buffer when no region is active). SLOC means that empty
+ lines and comment-only lines are not taken into consideration.
+
+ (function by Stefan Monnier).
+ "
+ (interactive
+ (if (use-region-p)
+ (list (region-beginning) (region-end) 'region)
+ (list (point-min) (point-max) 'buffer)))
+ (save-excursion
+ (goto-char beg)
+ (let ((count 0))
+ (while (< (point) end)
+ (cond
+ ((nth 4 (syntax-ppss)) ;; BOL is already inside a comment.
+ (let ((pos (point)))
+ (goto-char (nth 8 (syntax-ppss)))
+ (forward-comment (point-max))
+ (if (< (point) pos) (goto-char pos)))) ;; Just paranoia
+ (t (forward-comment (point-max))))
+ (setq count (1+ count))
+ (forward-line))
+ (when kind
+ (message "SLOC in %s: %s." kind count)))))
+
+
+(provide 'jao-sloc)
+;;; sloc.el ends here
diff --git a/lib/prog/jao-vterm-repl.el b/lib/prog/jao-vterm-repl.el
new file mode 100644
index 0000000..699ff39
--- /dev/null
+++ b/lib/prog/jao-vterm-repl.el
@@ -0,0 +1,130 @@
+;;; jao-vterm-repl.el --- vterm-based repls -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020, 2021 jao
+
+;; Author: jao <mail@jao.io>
+;; Keywords: terminals
+
+;; 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:
+
+;; Helpers to launch reply things such as erlang shells inside a vterm.
+;; For instance, to declare an erl repl for rebar projects, one would call:
+;;
+;; (jao-vterm-repl-register "rebar.config" "rebar3 shell" "^[0-9]+> ")
+
+;;; Code:
+
+(require 'jao-compilation)
+
+(declare-function 'vterm-copy-mode "vterm")
+(declare-function 'vterm-send-string "vterm")
+(declare-function 'vterm-send-return "vterm")
+
+(defun jao-vterm-repl--buffer-name (&optional dir)
+ (format "*vterm -- repl - %s*" (or dir (jao-compilation-root))))
+
+(defvar jao-vterm-repl-repls nil)
+(defvar jao-vterm-repl-prompts nil)
+(defvar-local jao-vterm-repl--name nil)
+(defvar-local jao-vterm-repl--last-buffer nil)
+(defvar-local jao-vterm-repl--prompt-rx "^[0-9]+> ")
+
+(setq vterm-buffer-name-string nil)
+
+(defun jao-vterm-repl--exec (cmd &optional name)
+ (vterm name)
+ (when name
+ (vterm-send-string "unset PROMPT_COMMAND\n\n"))
+ (vterm-send-string cmd)
+ (vterm-send-return)
+ (when name (rename-buffer name t)))
+
+;;;###autoload
+(defun jao-vterm-repl-previous-prompt ()
+ (interactive)
+ (when (derived-mode-p 'vterm-mode)
+ (vterm-copy-mode 1)
+ (forward-line 0)
+ (when (re-search-backward jao-vterm-repl--prompt-rx nil t)
+ (goto-char (match-end 0)))))
+
+;;;###autoload
+(defun jao-vterm-repl-next-prompt ()
+ (interactive)
+ (when (derived-mode-p 'vterm-mode)
+ (vterm-copy-mode 1)
+ (or (re-search-forward jao-vterm-repl--prompt-rx nil t)
+ (vterm-copy-mode -1))
+ (unless (save-excursion
+ (re-search-forward jao-vterm-repl--prompt-rx nil t))
+ (vterm-copy-mode -1))))
+
+;;;###autoload
+(define-minor-mode jao-vterm-repl-mode "repl-aware vterm" nil nil
+ '(("\C-c\C-p" . jao-vterm-repl-previous-prompt)
+ ("\C-c\C-n" . jao-vterm-repl-next-prompt)
+ ("\C-c\C-z" . jao-vterm-repl-pop-to-src)))
+
+;;;###autoload
+(defun jao-vterm-repl ()
+ (let* ((dir (jao-compilation-root))
+ (vname (jao-vterm-repl--buffer-name dir))
+ (root-name (jao-compilation-root-file))
+ (buffer (seq-find `(lambda (b)
+ (string=
+ (buffer-local-value 'jao-vterm-repl--name
+ b)
+ ,vname))
+ (buffer-list))))
+ (or buffer
+ (let ((default-directory dir)
+ (prompt (cdr (assoc root-name jao-vterm-repl-prompts)))
+ (cmd (or (cdr (assoc root-name jao-vterm-repl-repls))
+ (read-string "REPL command: ")))
+ (bname (format "* vrepl - %s/%s *"
+ (file-name-base (string-remove-suffix "/" dir))
+ root-name)))
+ (jao-vterm-repl--exec cmd bname)
+ (jao-vterm-repl-mode)
+ (setq-local jao-vterm-repl--name vname)
+ (when prompt (setq-local jao-vterm-repl--prompt-rx prompt))
+ (current-buffer)))))
+
+;;;###autoload
+(defun jao-vterm-repl-register (build-file repl-cmd prompt-rx)
+ (jao-compilation-add-dominating build-file)
+ (add-to-list 'jao-vterm-repl-repls (cons build-file repl-cmd))
+ (add-to-list 'jao-vterm-repl-prompts (cons build-file prompt-rx)))
+
+;;;###autoload
+(defun jao-vterm-repl-pop-to-repl ()
+ (interactive)
+ (let ((bn (current-buffer)))
+ (pop-to-buffer (jao-vterm-repl))
+ (setq-local jao-vterm-repl--last-buffer bn)))
+
+;;;###autoload
+(defun jao-vterm-repl-pop-to-src ()
+ (interactive)
+ (when (buffer-live-p jao-vterm-repl--last-buffer)
+ (pop-to-buffer jao-vterm-repl--last-buffer)))
+
+;;;###autoload
+(defun jao-vterm-repl-send (cmd)
+ (with-current-buffer (jao-vterm-repl) (vterm-send-string cmd)))
+
+(provide 'jao-vterm-repl)
+;;; jao-vterm-repl.el ends here
diff --git a/lib/readme.org b/lib/readme.org
new file mode 100644
index 0000000..cf8013c
--- /dev/null
+++ b/lib/readme.org
@@ -0,0 +1,19 @@
+* Elisp libraries
+
+*** Literate Libraries
+
+ - [[file:net/signel.org][signel]] a simplistic Signal client, using the signal-cli java lib.
+ - [[file:media/espotify.org][espotify]] searching and playing Spotify using consult.
+
+*** Sections
+
+ - [[./eos][eos]] generic utilities for the emacs operating system
+ - [[./themes][themes]] color themes based on Emacs builtin custom themes
+ - [[org][org]] utilities for org-mode
+ - [[./doc][doc]] opening documents (pdfs, mostly)
+ - [[./media][media]] utilities for music players and the like
+ - [[./prog][prog]] utilities for compilation and programming modes
+ - [[./net][net]] utilities for networking (w3m, weather &c.)
+ - [[./bmk][bmk]] a web bookmark manager
+
+ See also my [[https://jao.io/cgit/emacs][emacs custom files]].
diff --git a/lib/themes/jao-dark-blue-theme.el b/lib/themes/jao-dark-blue-theme.el
new file mode 100644
index 0000000..800bc28
--- /dev/null
+++ b/lib/themes/jao-dark-blue-theme.el
@@ -0,0 +1,100 @@
+(jao-define-custom-theme jao-dark-blue
+ (:palette (fg unspecified "grey77")
+ ;; (bg unspecified "#3f3f3f")
+ ;; (bg unspecified "#0e1111")
+ (bg unspecified "#192021")
+ (box "color-237" "grey25")
+ (button ((c 240) nul)
+ ;; ((c "lightskyblue2" "#3f3f4f"))
+ ((c "lightskyblue2" "#333436") nul))
+ (hilite ((c nil "#303336")))
+ (strike-through ((c 237)) (st))
+ (italic ((c 137) it) (it (c "lightyellow3")))
+ ;; (link ((c 108) nul) ((c "#F0DFAF") nit nul))
+ ;; (visited-link ((c 36) nul) ((c "#E0CF9F") nul))
+ (link ((c "antiquewhite3") nit nul))
+ ;; (link ((c "lemonchiffon") nit nul))
+ (visited-link ((c "burlywood3") nit nul))
+ (tab-sel ((c 252 232) nbf))
+ (tab-unsel ((c 245 232)))
+ (comment ((c 102) it) ((c "lightsteelblue4") it))
+ ;; (keyword ((c 151) nbf nul nit) ((c "darkseagreen3")))
+ ;; (function ((c 115) nul nbf) ((c "palegreen3")))
+ (keyword ((c 151) nbf nul nit) ((c "lightblue3")))
+ ;; (function ((c 115) nul nbf) ((c "lightskyblue3")))
+ (function ((c 115) nul nbf) ((c "cadetblue3")))
+ (type ((c 72) nbf) ((c "honeydew3")))
+ (variable-name ((c nil)))
+ ;; (constant ((c 72)) ((c "lavenderblush4")))
+ (constant ((c 72)) ((c "slategray3") nbf nit nul))
+ ;; (string ((c 36)) ((c "thistle4")))
+ (string ((c 36)) ((c "cadetblue")))
+ (warning ((c 144)) ((c "#F0DFAF")))
+ (error ((c 95)) ((c "goldenrod3")))
+ ;; (dimm ((c 240)))
+ (dimm ((c 59)) ((c "#6f6f6f")))
+ (gnus-mail ((c "gray70" nil)))
+ (gnus-news ((c "gray70" nil)))
+ ;; (outline ((c "aquamarine3")))
+ (outline ((c nil)))
+ (f00 ((c 29)) ((c "slategray3")))
+ (f01 ((c 108)) ((c "cadetblue")))
+ (f02 ((c 102)) ((c "lightcyan4"))) ;; ((c "paleturquoise4"))
+ (f10 ((c "cornsilk3")))
+ (f11 ((c "lemonchiffon3")))
+ (f12 ((c "azure3"))))
+ (:faces (bold (c nil nil) nul)
+ (button (c 66))
+ (font-lock-doc-face (c 30))
+ (gnus-button (c nil) nul)
+ (gnus-header-subject (p f01))
+ (gnus-summary-selected (c 250))
+ ;; (gnus-summary-selected (c 66 nil) nul nbf)
+ (match ul)
+ (magit-log-tag-label (c 95 240) nbf)
+ (mm-uu-extract (c nil 234))
+ (mode-line (c 248 235) nbf nul)
+ (mode-line-inactive (c 243 235) nbf nul)
+ (org-hide (c 0 nil))
+ (rcirc-other-nick (c 108))
+ (vertical-border (c 59 nil) :inherit nil)
+ (w3m-image (c 144))
+ (w3m-tab-background (c 0 0) ul)
+ (w3m-tab-line (c 0 0) ul)
+ (widget-button (c 196))
+ (widget-field (c 143 236)))
+ (:x-faces (company-scrollbar-bg (c nil "#383941"))
+ (company-scrollbar-fg (c nil "#484951"))
+ (diff-hl-change (c "#3f3f3f" "darkseagreen4"))
+ (diff-hl-delete (c "#3f3f3f" "goldenrod4"))
+ (diff-hl-insert (c "#3f3f3f" "cadetblue4"))
+ (fill-column-indicator (c "#303030") :inherit nil)
+ (font-lock-doc-face (c "lightcyan3") it)
+ (fringe (p dimm))
+ (gnus-button (c "lightyellow3") nul)
+ (gnus-summary-cancelled (c "dark slate gray" nil) st)
+ (gnus-summary-selected (p warning) nul nbf)
+ (header-line (p hilite))
+ (mode-line (c "grey60" "#2f2f2f"))
+ (mode-line-inactive (c "grey50" "#3f3f3f"))
+ (org-hide (c 0 nil))
+ (show-paren-match (c "darkseagreen1" "#5f5f5f"))
+ (spaceline-read-only (c "lightgoldenrod2" "gray10") niv)
+ (spaceline-modified (c "burlywood3" "gray10") nbf nit)
+ (spaceline-unmodified (c "darkseagreen" "gray10") niv)
+ (variable-pitch (c nil nil))
+ (vertical-border (c "#3f3f3f") :inherit nil)
+ (w3m-image (c "lightcyan2"))
+ (w3m-tab-background (c nil nil))
+ (w3m-tab-line (c 0 0) ul)
+ (widget-button (c nil nil) nul))
+ (:x-colors "lemonchiffon"
+ "sienna3"
+ "darkseagreen3"
+ "lightgoldenrod3"
+ "cadetblue4"
+ "lightcyan4"
+ "cadetblue3"
+ "black"))
+
+(provide 'jao-dark-blue-theme)
diff --git a/lib/themes/jao-dark-forest-theme.el b/lib/themes/jao-dark-forest-theme.el
new file mode 100644
index 0000000..42aaaac
--- /dev/null
+++ b/lib/themes/jao-dark-forest-theme.el
@@ -0,0 +1,131 @@
+(jao-define-custom-theme jao-dark-forest
+ (:names (zenburn-fg-05 "#989890")
+ (zenburn-fg-1 "#656555")
+ (zenburn-fg-15 "#6f6f69")
+ (zenburn-fg-2 "#696969")
+ (zenburn-fg-3 "#595959")
+ (zenburn-yellow "#F0DFAF")
+ (zenburn-yellow-1 "#E0CF9F")
+ (zenburn-yellow-2 "#D0BF8F")
+ (zenburn-yellow-3 "#C0AF7F")
+ (zenburn-yellow-4 "#B09F6F")
+ (zenburn-green "#7F9F7F")
+ (zenburn-green+1 "#8FB28F")
+ (zenburn-green+2 "#9FC29F")
+ (zenburn-green-5 "#2F4F2F")
+ (zenburn-green-4 "#3F5F3F")
+ (zenburn-green-3 "#4F6F4F")
+ (zenburn-green-2 "#5F7F5F")
+ (zenburn-green-1 "#6F8F6F")
+ (zenburn-orange "#DFAF8F")
+ (zenburn-blue-5 "#366060")
+ (zenburn-red "#CC9393")
+ (zenburn-red-1 "#BC8383")
+ (zenburn-red-2 "#AC7373")
+ (zenburn-red-3 "#9C6363")
+ (spaceline-bg "#1F1F1F")
+ (dimm-line-fg "#3f3f3f")
+ (box-line-fg "#303030")
+ (comment-fg "honeydew4")
+ (zenburn-bg-05 "#212121"))
+ (:palette (fg unspecified "dark grey")
+ (bg unspecified "#1f1f1f")
+ (box zenburn-fg-05 "grey25")
+ (button ((c 240) nul) (bx nul))
+ (hilite ((c nil "#2a2b2c") ex))
+ (strike-through ((c 237)) (st))
+ (italic ((c 137) it) (it))
+ (link ((c zenburn-green) nit nul))
+ (visited-link ((c zenburn-green-2) nit nul))
+ (tab-sel ((c 252 232) nbf))
+ (tab-unsel ((c 245 232)))
+ (comment ((c 102) it) ((c comment-fg)))
+ (keyword ((c 151) nbf nul nit) ((c zenburn-green+1)))
+ (function ((c 115) nul nbf) ((c zenburn-green-1)))
+ (type ((c 72) nbf) ((c "honeydew3")))
+ (variable-name ((c nil)))
+ (constant ((c 72)) ((c zenburn-red-3) nbf nit nul))
+ (string ((c 36)) ((c "wheat3"))) ;; "slate gray" "medium aquamarine"
+ (error ((c 144)) ((c zenburn-red-1)))
+ (warning ((c 95)) ((c zenburn-orange)))
+ (success ((c zenburn-green+2)))
+ (dimm ((c 59)) ((c "#6f6f6f")))
+ (gnus-mail ((c zenburn-fg-05)))
+ (gnus-news ((c zenburn-fg-05)))
+ (outline ((c nil)))
+ (f00 ((c 29)) ((c "burlywood3")))
+ (f01 ((c 108)) ((c "burlywood4")))
+ (f02 ((c 102)) ((c "lemonchiffon4"))) ;; ((c "paleturquoise4"))
+ (f10 ((c "cornsilk3")))
+ (f11 ((c "lemonchiffon3")))
+ (f12 ((c "honeydew4"))))
+ (:faces (bold (c nil nil) nul)
+ (button (c 66))
+ (font-lock-doc-face (c 30))
+ (gnus-button (c nil) nul)
+ (gnus-header-subject (p f01))
+ (gnus-summary-selected (c 250))
+ (match ul)
+ (magit-log-tag-label (c 95 240) nbf)
+ (mm-uu-extract (c nil 234))
+ (mode-line (c 248 235) nbf nul)
+ (mode-line-inactive (c 243 235) nbf nul)
+ (org-hide (c 0 nil))
+ (rcirc-other-nick (c 108))
+ (vertical-border (c 59 nil) :inherit nil)
+ (w3m-image (c 144))
+ (w3m-tab-background (c 0 0) ul)
+ (w3m-tab-line (c 0 0) ul)
+ (widget-button (c 196))
+ (widget-field (c 143 236)))
+ (:x-faces (company-scrollbar-bg (c nil "#383941"))
+ (company-scrollbar-fg (c nil "#484951"))
+ (diff-hl-change (c dimm-line-fg "#313131"))
+ (diff-hl-delete (c dimm-line-fg zenburn-red-3))
+ (diff-hl-insert (c dimm-line-fg "lemonchiffon4"))
+ (fill-column-indicator (c box-line-fg) :inherit nil)
+ (font-lock-doc-face (~ font-lock-comment-face) it)
+ (fringe (p dimm))
+ (gnus-button (c "lightyellow3") nul)
+ (gnus-cite-1 (c zenburn-fg-05))
+ (gnus-cite-2 (c zenburn-fg-1))
+ (gnus-cite-3 (c zenburn-fg-1))
+ (gnus-cite-4 (c zenburn-fg-1))
+ (gnus-group-mail-3 (c nil nil)) ;; "#252525"
+ (gnus-group-mail-3-empty (c zenburn-fg-2))
+ (gnus-group-news-3 (~ gnus-group-mail-3))
+ (gnus-group-news-3-empty (~ gnus-group-mail-3-empty))
+ (gnus-summary-cancelled (c "dark slate gray" nil) st)
+ (gnus-summary-selected (p warning) nul nbf)
+ (header-line (p hilite))
+ (lui-track-bar (c nil "#303030") :height 0.1 ex)
+ (magit-diff-context-highlight (c nil "grey20") ex)
+ (mode-line (c "grey60" "#2b2b2b")
+ :box (:line-width 1 :color "grey28"))
+ (mode-line-inactive (c "grey35" zenburn-bg-05)
+ :box (:line-width 1 :color "grey20"))
+ (mode-line-buffer-id (c zenburn-green-2))
+ (org-hide (c 0 nil))
+ (scroll-bar (c box-line-fg))
+ (show-paren-match (c "darkseagreen1" "#5f5f5f"))
+ (spaceline-read-only (c "burlywood3" spaceline-bg) niv)
+ (spaceline-modified (c zenburn-orange spaceline-bg) nbf nit)
+ (spaceline-unmodified (c zenburn-green-3 spaceline-bg) niv)
+ (variable-pitch (c nil nil))
+ (vertical-border (c dimm-line-fg) :inherit nil)
+ (w3m-image (c zenburn-fg-05) bx it)
+ (w3m-tab-background (c nil nil))
+ (w3m-tab-line (c 0 0) ul)
+ (widget-button (c nil nil) nul))
+ (:x-colors "lemonchiffon"
+ "sienna3"
+ "darkseagreen3"
+ "lightgoldenrod3"
+ "cadetblue4"
+ "lightcyan4"
+ "cadetblue3"
+ "black"))
+
+;; (enable-theme 'jao-dark-forest)
+
+(provide 'jao-dark-forest-theme)
diff --git a/lib/themes/jao-dark-theme.el b/lib/themes/jao-dark-theme.el
new file mode 100644
index 0000000..1c2725e
--- /dev/null
+++ b/lib/themes/jao-dark-theme.el
@@ -0,0 +1,77 @@
+(jao-define-custom-theme jao-dark
+ (:palette (fg unspecified "grey60")
+ (bg unspecified "grey2")
+ (box "yellow" "grey30")
+ (button ((c 11) nul))
+ (hilite ((c nil 8)))
+ (strike-through ((c 8)))
+ (italic ((c 101) nul) (it :family "DejaVu Sans Mono" :height 100))
+ (link ((c 2) nul))
+ (visited-link ((c 2) nul))
+ (tab-sel ((c 9 8) nbf))
+ (tab-unsel ((c 15 6) bx))
+ (comment ((c 3)) ((c 3)))
+ (keyword ((c 12) nbf nul))
+;; (keyword ((c 151) nbf nul) (bf))
+ (type ((c 11) nbf))
+ (function ((c 108) nul nbf))
+;; (function ((c 13) nul bf))
+ (variable-name ((c nil)))
+ (constant ((c 4)))
+ (string ((c 2)))
+ (warning ((c 144)) ((c 1)))
+ (error ((c 9)))
+ (dimm ((c 3)))
+ (gnus-mail ((c 15 nil)))
+ (gnus-news ((c 15 nil)))
+ (outline ((c 7)))
+ (f00 ((c 11)))
+ (f01 ((c 10)))
+ (f02 ((c 23)) ((c "cadetblue4")))
+ (f10 ((p f00)))
+ (f11 ((p f01)))
+ (f12 ((p f02))))
+ (:faces (bold (c nil nil) nul)
+ (font-lock-doc-face (c 10))
+ (gnus-button (c nil nil) nul)
+ (gnus-summary-selected (c nil nil) ul nbf)
+ (mm-uu-extract (c nil 6))
+ (mode-line (c 7 8) nbf nul)
+ (mode-line-inactive (c 8 16) nbf nul)
+ (org-hide (c 0 nil))
+;; (rcirc-other-nick (c 4) nbf)
+ (vertical-border (c 8 nil) :inherit nil)
+ (w3m-image (c 1))
+ (w3m-tab-background (c 0 0))
+ (w3m-tab-line (c 0 0))
+ (widget-button (c nil nil) nul))
+ (:x-faces (gnus-button (c nil nil) nul)
+ (gnus-summary-selected (c "grey40" nil) ul)
+ (mode-line (c 14 8) nbf nul bx)
+ (mode-line-inactive (c 3 8) nbf nul bx)
+ (org-hide (c 0 nil))
+ (font-lock-doc-face (c 10))
+ (fringe (p dimm))
+ (rcirc-other-nick (c 5))
+ (vertical-border (c 8 nil) :inherit nil)
+ (w3m-image (c 9))
+ (w3m-tab-background (c 0 0))
+ (widget-button (c nil nil) nul))
+ (:x-colors "#050505"
+ "lightgoldenrod3"
+ "darkseagreen4"
+ "grey40"
+ "lightcyan4"
+ "paleturquoise4"
+ "grey7"
+ "grey60"
+ "grey20"
+ "sienna4"
+ "#44836e"
+ "#648f81"
+ "darkseagreen4"
+ "aquamarine4"
+ "azure4"
+ "grey60"))
+
+(provide 'jao-dark-theme)
diff --git a/lib/themes/jao-doom-theme.el b/lib/themes/jao-doom-theme.el
new file mode 100644
index 0000000..0f4b2df
--- /dev/null
+++ b/lib/themes/jao-doom-theme.el
@@ -0,0 +1,57 @@
+(deftheme jao-doom
+ "Created 2019-12-13.")
+
+(defun jao-doom-color (c &optional alt)
+ (let ((c (assoc c doom-themes--colors))) (if alt (caddr c) (cadr c))))
+(defun jao-doom-face (f)
+ (mapcar (lambda (x)
+ (cond ((symbolp x) (or (jao-doom-color x) x))
+ ((listp x) (jao-doom-face x))
+ (t x)))
+ (if (listp f) f (cdr (assoc f doom-themes--faces)))))
+
+;; (enable-theme 'jao-doom)
+
+(custom-theme-set-faces
+ 'jao-doom
+ '(Info-quoted ((t (:inherit font-lock-variable-name-face))))
+ '(custom-button ((t (:background "#282b33" :foreground "#819cd6" :box nil))))
+ '(dictionary-reference-face ((t (:inherit (font-lock-keyword-face)))))
+ '(dictionary-word-definition-face ((t (:inherit default))))
+ `(error ((t (:foreground ,(jao-doom-color 'orange)))))
+ `(diff-hl-change ((t (:inherit default :background "#313153"))))
+ '(diff-hl-delete ((t (:inherit default :background "#533133"))))
+ `(diff-hl-insert ((t (:inherit default :background "#315331"))))
+ '(fill-column-indicator ((t (:foreground "grey25"))))
+ `(gnus-summary-normal-read ((t (:foreground ,(jao-doom-color 'grey)))))
+ `(gnus-summary-normal-unread ((t ,(jao-doom-face 'default))))
+ `(highlight ((t (;; :underline ,(jao-doom-color 'green t)
+ :background ,(jao-doom-color 'bg-alt)))))
+ `(gnus-summary-selected ((t (:inherit highlight))))
+ '(lui-button-face ((t (:foreground "#7ebebd" :underline nil))))
+ `(link-visited ((t (:foreground ,(jao-doom-color 'green)))))
+ '(magit-diff-context-highlight ((t (:background "#333344"))))
+ `(magit-diff-hunk-heading-highlight ((t (,@(jao-doom-face 'default)
+ :overline nil :underline t :extend t))))
+ '(magit-diff-removed-highlight ((t (:foreground "tan" :bold nil))))
+ '(magit-diff-added-highlight ((t (:foreground "antiquewhite" :bold nil))))
+ `(mode-line ((t (:foreground "#999999" ;; ,(jao-doom-color 'modeline-fg-alt)
+ :background ,(jao-doom-color 'modeline-bg)
+ :box (:line-width 2 :color ,(jao-doom-color 'modeline-bg))))))
+ `(mode-line-inactive ((t (:foreground ,(jao-doom-color 'modeline-fg-alt t)
+ :background ,(jao-doom-color 'modeline-bg-inactive)
+ :box (:line-width 2 :color ,(jao-doom-color 'modeline-bg-inactive)))))) ;; "#3a3a4a"
+ '(mpdel-tablist-album-face ((t (:inherit font-lock-doc-face))))
+ '(mpdel-tablist-artist-face ((t (:inherit font-lock-keyword-face))))
+ '(org-block-begin-line ((t (:inherit font-lock-comment-face :extend nil))))
+ '(org-block-end-line ((t (:inherit org-block-begin-line :extend nil))))
+ `(scroll-bar ((t (:foreground ,(jao-doom-color 'modeline-bg)
+ :background ,(jao-doom-color 'bg)))))
+ '(variable-pitch ((t (:inherit default))))
+ '(w3m-form-button ((t (:inherit button)))))
+
+(custom-theme-set-variables
+ 'jao-doom
+ '(fci-rule-color "grey25"))
+
+(provide-theme 'jao-doom)
diff --git a/lib/themes/jao-doomish-theme.el b/lib/themes/jao-doomish-theme.el
new file mode 100644
index 0000000..5ac666c
--- /dev/null
+++ b/lib/themes/jao-doomish-theme.el
@@ -0,0 +1,152 @@
+(jao-define-custom-theme jao-doomish
+ (:names (bg "#282b33" nil nil)
+ (bg-alt "#1f2024" nil nil)
+ (blue "#819cd6")
+ (blue2 "#51afef")
+ (comments "#6e7899" "#5699AF" "cyan")
+ (constants "#a6c1e0" "magenta")
+ (cyan "#7289bc" "#46D9FF" "brightcyan")
+ (dark-blue "#616c96")
+ (dark-blue-1 "#2257A0" "blue")
+ (dark-cyan "#6e7899" "#5699AF" "cyan")
+ (dimm-line-fg "#3f3f3f")
+ (doc-comments "#9299b2" "#80b2c3" "cyan")
+ (error "#e1c1ee")
+ (error2 "#ff6655")
+ (fg "#c6c6c6")
+ (fg-0.5 "#a6a6a6")
+ (fg-1 "#868686")
+ (fg-2 "#666666")
+ (light-purple "#c9d9ff")
+ (functions "#7ebebd")
+ (functions2 "#44b9b1")
+ (green "#5b94ab")
+ (green2 "#99bb66")
+ (green3 "#44b9b1")
+ (grey "#515462" "#3f3f3f" "brightblack")
+ (hidden "#282b33" "black" "black")
+ (highlight "#819cd6" "#51afef" "brightblue")
+ (keywords "#819cd6")
+ (keywords2 "#51afef")
+ (light-blue "#90a6db" "#ECBE7B" "yellow")
+ (magenta "#a6c1e0" "#c678dd" "magenta")
+ (methods "#7289bc" "#46D9FF" "brightcyan")
+ (modeline-bg "#22242b" "black" "black")
+ (modeline-bg-inactive "#24262d" nil nil)
+ (modeline-bg-inactive-l "#282b33" "#1e1e1e" "brightblack")
+ (modeline-bg-l "#24262d" "black" "black")
+ (modeline-fg-alt "#888395" "#525252" "brightblack")
+ (numbers "#a6c1e0" "#c678dd" "magenta")
+ (operators "#819cd6" "#51afef" "brightblue")
+ (orange "#a6c1e0")
+ (orange2 "#dd8844")
+ (red "#e1c1ee" "#ff6655" "red")
+ (region "#41454b")
+ (region2 "#262626")
+ (selection "#616c96" "#2257A0" "blue")
+ (strings "#5b94ab" "#99bb66" "green")
+ (success "#5b94ab" "#99bb66" "green")
+ (teal "#7ebebd" "#44b9b1" "brightgreen")
+ (types "lightsteelblue4")
+ (type2 "#a9a1e1")
+ (vc-added "#5b94ab" "#99bb66" "green")
+ (vc-deleted "#e1c1ee" "#ff6655" "red")
+ (vc-modified "#a6c1e0" "#dd8844" "brightred")
+ (vertical-bar "#141519" "#0f0f0f" "brightblack")
+ (violet "#b0a2e7" "#a9a1e1" "brightmagenta")
+ (warning "#cfcf9c")
+ (warning2 "#ECBE7B")
+ (yellow "#cfcf9c" "#ECBE7B" "yellow")
+ (zenburn-green-2 "#5F7F5F")
+ (zenburn-orange "#DFAF8F")
+ (zenburn-red-1 "#BC8383"))
+ (:palette (fg unspecified "#c6c6c6")
+ (bg unspecified "#1f2024") ;; bg-alt
+ (box "#2d2d2d" "grey25")
+ (button ((p f01) bx nul))
+ (hilite ((c nil region) ex))
+ (strike-through (st))
+ (italic (it))
+ (link ((c blue) nit nul))
+ (visited-link ((c green) nit nul))
+ (tab-sel ((c 252 232) nbf))
+ (tab-unsel ((c 245 232)))
+ (comment ((c comments)))
+ (keyword ((c keywords)))
+ (function ((c functions)))
+ (type ((c types)))
+ (variable-name ((c nil)))
+ (constant ((c constants) nbf nit nul))
+ (string ((c strings))) ;; "wheat3" "slate gray" "medium aquamarine"
+ (error ((c warning2)))
+ (warning ((c warning)))
+ (success ((c green)))
+ (dimm ((c "#6f6f6f")))
+ (gnus-mail ((c nil)))
+ (gnus-news ((c nil)))
+ (outline-1 ((c keywords) bf))
+ (outline-2 ((c functions) bf))
+ (outline-3 ((c keywords2) bf))
+ (outline-4 ((c functions2) bf))
+ (outline-5 ((c nil)))
+ (f00 ((c "steelblue3")))
+ (f01 ((c "lightsteelblue3")))
+ (f02 ((c "skyblue4")))
+ (f10 ((c "slategray3")))
+ (f11 ((c "lightskyblue2")))
+ (f12 ((c "lightskyblue3"))))
+ (:faces (bold (c nil nil) nul)
+ (mode-line (c 248 235) nbf nul)
+ (mode-line-inactive (c 243 235) nbf nul))
+ (:x-faces (company-scrollbar-bg (c nil "#383941"))
+ (company-scrollbar-fg (c nil "#484951"))
+ (diff-hl-change (c dimm-line-fg green))
+ (diff-hl-delete (c dimm-line-fg orange2))
+ (diff-hl-insert (c dimm-line-fg dark-blue))
+ (fill-column-indicator (c "black") :inherit nil)
+ (font-lock-doc-face (~ font-lock-comment-face) it)
+ (fringe (p dimm))
+ (gnus-button (c "lightyellow3") nul)
+ (gnus-cite-1 (c fg-0.5))
+ (gnus-cite-2 (c fg-1))
+ (gnus-cite-3 (c fg-2))
+ (gnus-cite-4 (c fg-2))
+ (gnus-group-mail-3 (c base8)) ;; "#252525"
+ (gnus-group-mail-3-empty (c fg-0.5))
+ (gnus-group-news-3 (~ gnus-group-mail-3))
+ (gnus-group-news-3-empty (~ gnus-group-mail-3-empty))
+ (gnus-summary-cancelled (c "dark slate gray" nil) st)
+ (gnus-summary-selected (p warning) nul nbf)
+ (header-line (p hilite))
+ (magit-diff-context-highlight (p hilite) ex)
+ (mode-line (c "grey60" modeline-bg)
+ :box (:line-width 1 :color "#282b33")) ;; "#22242b"
+ (mode-line-inactive (c "grey35" modeline-bg-inactive)
+ :box (:line-width 1 :color "#282b33")) ;; "#24262d"
+ (mode-line-buffer-id (c nil) bf)
+ (org-hide (c 0 nil))
+ (org-code (c yellow))
+ (scroll-bar (c bg))
+ (term-color-blue (c nil nil) it)
+ (vterm-color-blue (c light-blue nil))
+ (show-paren-match (c "darkseagreen1" "#5f5f5f"))
+ (variable-pitch (c nil nil))
+ (vertical-border (c "black") :inherit nil)
+ (w3m-image (c green) bx it)
+ (w3m-tab-background (c nil nil))
+ (w3m-tab-line (c 0 0) ul)
+ (widget-button (c nil nil) nul)))
+
+;; (enable-theme 'jao-doomish)
+
+;; (base0 "#222228" "black" "black")
+;; (base1 "#282b33" "#1e1e1e" "brightblack")
+;; (base2 "#34373e" "#2e2e2e" "brightblack")
+;; (base3 "#41454b" "#262626" "brightblack")
+;; (base4 "#515462" "#3f3f3f" "brightblack")
+;; (base5 "#888395" "#525252" "brightblack")
+;; (base6 "#929292" "#6b6b6b" "brightblack")
+;; (base7 "#727269" "#979797" "brightblack")
+;; (base8 "#eceff4" "#dfdfdf" "white")
+
+(provide 'jao-doomish-theme)
diff --git a/lib/themes/jao-greenish-theme.el b/lib/themes/jao-greenish-theme.el
new file mode 100644
index 0000000..1bed7fb
--- /dev/null
+++ b/lib/themes/jao-greenish-theme.el
@@ -0,0 +1,114 @@
+(jao-define-custom-theme jao-greenish
+ (:names (bg "#282b33" nil nil)
+ (bg-alt "#1f2024" nil nil)
+ (fg "#c6c6c6")
+ (fg-0.5 "#a6a6a6")
+ (fg-1 "#868686")
+ (fg-2 "#666666")
+ (blue "#819cd6")
+ (dark-blue "#616c96")
+ (green "#5b94ab")
+ (yellow "antiquewhite3")
+ (dark-yellow "antiquewhite4")
+ (orange "#a6c1e0")
+ (orange2 "#dd8844")
+ (red "#e1c1ee" "#ff6655" "red")
+ (comments "#5699AF" "#6e7899")
+ (constants "#a6c1e0")
+ (dimm-line-fg "#3f3f3f")
+ (dimm-line "#6f6f6f")
+ (error "#e1c1ee")
+ (functions "#7ebebd")
+ (functions2 "#44b9b1")
+ (keywords "paleturquoise3")
+ (keywords2 "#51afef")
+ (modeline-bg "#22242b")
+ (modeline-bg-inactive "#24262d")
+ (region "#41454b")
+ (strings "azure3" "lightskyblue4")
+ (success "#5b94ab" "#99bb66")
+ (warning "#cfcf9c")
+ (warning2 "#ECBE7B"))
+ (:palette (fg "#c6c6c6")
+ (bg "#1f2024")
+ (box "grey25")
+ (button ((p f01) bx nul))
+ (hilite ((c nil region) ex))
+ (strike-through (st))
+ (italic (it))
+ (link ((c blue))) ;; (ul dimm-line)
+ (visited-link ((c dark-blue)))
+ (tab-sel ((c 252 232) nbf))
+ (tab-unsel ((c 245 232)))
+ (comment ((c fg-0.5) it))
+ (keyword ((c keywords)))
+ (function ((c functions)))
+ (type ((c strings)))
+ (variable-name ((c nil)))
+ (constant ((c blue)))
+ (string ((c strings)))
+ (error ((c warning2)))
+ (warning ((c warning)))
+ (success ((c green)))
+ (dimm ((c dimm-line)))
+ (gnus-mail ((c nil)))
+ (gnus-news ((c nil)))
+ (outline-1 ((c keywords) bf))
+ (outline-2 ((c functions) bf))
+ (outline-3 ((c keywords2) bf))
+ (outline-4 ((c functions2) bf))
+ (outline-5 ((c nil)))
+ (f00 ((c "lightcyan3")))
+ (f01 ((c "darkslategray3")))
+ (f02 ((c "lightblue3")))
+ (f10 ((c "cadetblue4")))
+ (f11 ((c "lightskyblue2")))
+ (f12 ((c "lightskyblue3"))))
+ (:faces (bold (c nil nil) nul)
+ (mode-line (c 248 235) nbf nul)
+ (mode-line-inactive (c 243 235) nbf nul))
+ (:x-faces (clojure-keyword-face (c "powder blue"))
+ (company-scrollbar-bg (c nil "#383941"))
+ (company-scrollbar-fg (c nil "#484951"))
+ (cursor (c warning2 warning2))
+ (diff-hl-change (c dimm-line-fg green))
+ (diff-hl-delete (c dimm-line-fg orange2))
+ (diff-hl-insert (c dimm-line-fg dark-blue))
+ (fill-column-indicator (c "black") :inherit nil)
+ (font-lock-doc-face (~ font-lock-comment-face) it)
+ (fringe (p dimm))
+ (gnus-button (c "lightyellow3"))
+ (gnus-cite-1 (c fg-0.5))
+ (gnus-cite-2 (c fg-1))
+ (gnus-cite-3 (c fg-2))
+ (gnus-cite-4 (c fg-2))
+ (gnus-group-mail-3 (c yellow))
+ (gnus-group-mail-3-empty (c fg-0.5))
+ (gnus-group-news-3 (~ gnus-group-mail-3))
+ (gnus-group-news-3-empty (~ gnus-group-mail-3-empty))
+ (gnus-group-mail-low (p f10))
+ (gnus-group-mail-low-empty (~ gnus-group-mail-3-empty))
+ (gnus-summary-cancelled (c "dark slate gray" nil) st)
+ (gnus-summary-selected (p warning) nul nbf)
+ (header-line (p hilite))
+ (magit-diff-context-highlight (p hilite) ex)
+ (mode-line (c "grey60" modeline-bg)
+ :box (:line-width 1 :color "#282b33")) ;; "#22242b"
+ (mode-line-inactive (c "grey35" modeline-bg-inactive)
+ :box (:line-width 1 :color "#282b33")) ;; "#24262d"
+ (mode-line-buffer-id (c yellow))
+ (org-hide (c 0 nil))
+ (org-code (c yellow))
+ (scroll-bar (c bg))
+ (term-color-blue (c nil nil) it)
+ (vterm-color-blue (c "steelblue4" nil))
+ (show-paren-match (c "darkseagreen1" "#5f5f5f"))
+ (variable-pitch (c nil nil))
+ (vertical-border (c "black") :inherit nil)
+ (w3m-image (c green) bx it)
+ (w3m-tab-background (c nil nil))
+ (w3m-tab-line (c 0 0) ul)))
+
+;; (enable-theme 'jao-greenish)
+
+(provide 'jao-greenish-theme)
diff --git a/lib/themes/jao-light-theme.el b/lib/themes/jao-light-theme.el
new file mode 100644
index 0000000..13a416f
--- /dev/null
+++ b/lib/themes/jao-light-theme.el
@@ -0,0 +1,111 @@
+(jao-define-custom-theme jao-light
+ (:names (dimm-background "#f4f4f4")
+ (dimm-background-2 "#f0f0f0")
+ (dimm-background-3 "#f6f6f6")
+ (dimm-background-4 "#fafafa")
+ (yellowish-background "#fffff8")
+ (link "#00552a")
+ (yellow "#fdf6e3")
+ (pale-yellow "#fff8e5")
+ (paler-yellow "#fffff8")
+ (green "#005555")
+ (light-green "darkolivegreen4")
+ (greyish "#626262")
+ ;; (blueish "midnightblue")
+ (blueish "deepskyblue4")
+ (blue "#819cd6")
+ (blue2 "#51afef")
+ (pale-blue "azure2")
+ (dark-blue "#616c96")
+ (dark-blue-1 "#2257A0")
+ (dark-blue-2 "#023770")
+ (keywords "lightsteelblue4")
+ (keywords2 "#2257A0")
+ (functions "#005555")
+ (red "salmon3")
+ (red2 "sienna4"))
+ (:palette (fg unspecified "black")
+ (bg unspecified "white")
+ ;; (bg unspecified "#fffff8")
+ (box "grey80" "antiquewhite3")
+ (button ((c link) nit))
+ (hilite ((c nil dimm-background)))
+ (strike-through ((c 1)) (st))
+ (italic (it))
+ (link ((c dark-blue-2) nul nbf))
+ (visited-link ((c dark-blue-1) nul nbf))
+ (tab-sel ((~ mode-line)))
+ (tab-unsel ((~ mode-line-inactive)))
+ (comment ((c greyish) it))
+ (keyword ((c dark-blue-2) nbf))
+ (type ((c blueish) nbf))
+ (function ((c green nil) nbf))
+ (variable-name ((c "black")))
+ (constant ((c 23)))
+ (string ((c link)))
+ (warning ((c red2)))
+ (error ((c red)))
+ (dimm ((c "lemonchiffon4")))
+ (gnus-mail ((c "black")))
+ (gnus-news ((c "black")))
+ (outline ((c "black") nbf))
+ (outline-1 ((c dark-blue-1) it bf))
+ (outline-2 ((c functions) it nbf))
+ (outline-3 ((c link) it nbf))
+ (outline-4 ((c nil) it nbf))
+ (outline-5 ((c nil)))
+ (f00 ((c green)))
+ (f01 ((c dark-blue-1)))
+ (f02 ((c light-green)))
+ (f10 ((p f00)))
+ (f11 ((p f01)))
+ (f12 ((p f02))))
+ (:faces (mode-line (c nil dimm-background) ;; "ghost white"
+ :box (:line-width 1 :color "grey80"))
+ (mode-line-inactive (c "grey40" dimm-background-2)
+ :box (:line-width 1 :color "grey85"))
+ (mode-line-buffer-id (~ mode-line) nit)
+ (mode-line-emphasis it)
+ (mode-line-highlight (c green nil)))
+ (:x-faces (bold bf)
+ (compilation-info (c "#223142" nil) nbf)
+ (company-scrollbar-bg (c nil "grey95"))
+ (company-scrollbar-fg (c nil "grey90"))
+ (cursor (c "sienna3" "sienna3"))
+ (diary (p error) nbf)
+ (diff-hl-change (c "white" pale-blue))
+ (diff-hl-insert (c "white" "honeydew2"))
+ (diff-hl-delete (c "white" "wheat1"))
+ (fill-column-indicator (c "grey80"))
+ (fringe (c "grey70" nil))
+ (gnus-button (p link))
+ (gnus-summary-selected (c green) nbf)
+ (gnus-summary-cancelled (c "sienna3") st)
+ (header-line (c nil "#efebe7"))
+ (ivy-highlight-face (c nil pale-yellow))
+ (ivy-current-match (c nil pale-yellow))
+ (lui-track-bar (p dimm) :height 0.2 nul nil ex)
+ (magit-diff-context-highlight (c nil yellow) ex)
+ (magit-diff-hunk-heading-highlight (c nil yellow) it bf)
+ (mode-line (c "grey30" dimm-background-3) ;; "ghost white"
+ :box (:line-width 1 :color "grey90"))
+ (mode-line-inactive (c "grey40" dimm-background-4)
+ :height 1
+ :box (:line-width 1 :color "grey90"))
+ (mode-line-buffer-id (~ mode-line) (c dark-blue-2) nit)
+ (mode-line-emphasis (c green nil))
+ (mode-line-highlight (c green nil))
+ (org-link (p link) ul)
+ (scroll-bar (c "grey80"))
+ (success (p f00))
+ (vertical-border (c "grey70" nil))
+ (warning (c "burlywood4"))
+ (w3m-image (c "midnightblue" "azure2"))
+ (w3m-bold (c "darkslategray") bf)
+ (w3m-tab-selected (c "orangered4" "white") bf)
+ (w3m-tab-selected-retrieving (~ w3m-tab-selected) (c 1))
+ (w3m-tab-background (c "white" "white") nul)))
+
+;; (enable-theme 'jao-light)
+
+(provide 'jao-light-theme)
diff --git a/lib/themes/jao-mono-dark-theme.el b/lib/themes/jao-mono-dark-theme.el
new file mode 100644
index 0000000..a5cf532
--- /dev/null
+++ b/lib/themes/jao-mono-dark-theme.el
@@ -0,0 +1,98 @@
+(jao-define-custom-theme jao-mono-dark
+ (:palette (fg unspecified "grey77")
+ ;; (bg unspecified "#3f3f3f")
+ ;; (bg unspecified "#0e1111")
+ (bg unspecified "#192021")
+ (box "color-237" "grey25")
+ (button ((c 240) nul)
+ ;; ((c "lightskyblue2" "#3f3f4f"))
+ ((c "lightskyblue2" "#333436") nul))
+ (hilite ((c nil "#303336")))
+ (strike-through ((c 237)) (st))
+ (italic ((c 137) it) (it (c "lightyellow3")))
+ ;; (link ((c 108) nul) ((c "#F0DFAF") nit nul))
+ ;; (visited-link ((c 36) nul) ((c "#E0CF9F") nul))
+ (link ((c "antiquewhite3") nit nul))
+ ;; (link ((c "lemonchiffon") nit nul))
+ (visited-link ((c "burlywood3") nit nul))
+ (tab-sel ((c 252 232) nbf))
+ (tab-unsel ((c 245 232)))
+ (comment ((c 102) it) ((c "darkslategray4") it))
+ ;; (keyword ((c 151) nbf nul nit) ((c "darkseagreen3")))
+ ;; (function ((c 115) nul nbf) ((c "palegreen3")))
+ (keyword ((c 151) nbf nul nit) ((c "lightblue3")))
+ (function ((c 115) nul nbf) ((c "lightskyblue3")))
+ (type ((c 72) nbf) ((c "honeydew3")))
+ (variable-name ((c nil)))
+ (constant ((c 72)) ((c "mediumaquamarine") nbf nit nul))
+ ;; (constant ((c 72)) ((c "lightblue3")))
+ (string ((c 36)) ((c "darkslategray3")))
+ ;; (string ((c 36)) ((c "light sea green")))
+ (warning ((c 144)) ((c "#F0DFAF")))
+ (error ((c 95)) ((c "goldenrod3")))
+ ;; (dimm ((c 240)))
+ (dimm ((c 59)) ((c "#6f6f6f")))
+ (gnus-mail ((c "gray70" nil)))
+ (gnus-news ((c "gray70" nil)))
+ ;; (outline ((c "aquamarine3")))
+ (outline ((c nil)))
+ (f00 ((c 29)) ((c "darkseagreen")))
+ (f01 ((c 108)) ((c "darkseagreen2")))
+ (f02 ((c 102)) ((c "lightcyan4"))) ;; ((c "paleturquoise4"))
+ (f10 ((c "cornsilk3")))
+ (f11 ((c "lemonchiffon3")))
+ (f12 ((c "azure3"))))
+ (:faces (bold (c nil nil) nul)
+ (button (c 66))
+ (font-lock-doc-face (c 30))
+ (gnus-button (c nil) nul)
+ (gnus-header-subject (p f01))
+ (gnus-summary-selected (c 250))
+ ;; (gnus-summary-selected (c 66 nil) nul nbf)
+ (match ul)
+ (magit-log-tag-label (c 95 240) nbf)
+ (mm-uu-extract (c nil 234))
+ (mode-line (c 248 235) nbf nul)
+ (mode-line-inactive (c 243 235) nbf nul)
+ (org-hide (c 0 nil))
+ (rcirc-other-nick (c 108))
+ (vertical-border (c 59 nil) :inherit nil)
+ (w3m-image (c 144))
+ (w3m-tab-background (c 0 0) ul)
+ (w3m-tab-line (c 0 0) ul)
+ (widget-button (c 196))
+ (widget-field (c 143 236)))
+ (:x-faces (company-scrollbar-bg (c nil "#383941"))
+ (company-scrollbar-fg (c nil "#484951"))
+ (diff-hl-change (c "#3f3f3f" "darkseagreen4"))
+ (diff-hl-delete (c "#3f3f3f" "goldenrod4"))
+ (diff-hl-insert (c "#3f3f3f" "cadetblue4"))
+ (font-lock-doc-face (c "lightcyan3") it)
+ (fringe (p dimm))
+ (gnus-button (c "lightyellow3") nul)
+ (gnus-summary-cancelled (c "dark slate gray" nil) st)
+ (gnus-summary-selected (p warning) nul nbf)
+ (header-line (p hilite))
+ (mode-line (c "grey60" "#2f2f2f"))
+ (mode-line-inactive (c "grey50" "#3f3f3f"))
+ (org-hide (c 0 nil))
+ (show-paren-match (c "darkseagreen1" "#5f5f5f"))
+ (spaceline-read-only (c "lightgoldenrod2" "gray10") niv)
+ (spaceline-modified (c "burlywood3" "gray10") nbf nit)
+ (spaceline-unmodified (c "darkseagreen" "gray10") niv)
+ (variable-pitch (c nil nil))
+ (vertical-border (c "#3f3f3f") :inherit nil)
+ (w3m-image (c "lightcyan2"))
+ (w3m-tab-background (c nil nil))
+ (w3m-tab-line (c 0 0) ul)
+ (widget-button (c nil nil) nul))
+ (:x-colors "lemonchiffon"
+ "sienna3"
+ "darkseagreen3"
+ "lightgoldenrod3"
+ "cadetblue4"
+ "lightcyan4"
+ "cadetblue3"
+ "black"))
+
+(provide 'jao-mono-dark-theme)
diff --git a/lib/themes/jao-themes.el b/lib/themes/jao-themes.el
new file mode 100644
index 0000000..d3f110d
--- /dev/null
+++ b/lib/themes/jao-themes.el
@@ -0,0 +1,1099 @@
+;;; palette
+(defvar jao-themes--face-family "Inconsolata")
+(defvar jao-themes--fg "black")
+(defvar jao-themes--bg "white")
+(defvar jao-themes--box "grey75")
+(defvar jao-themes--hilite nil)
+(defvar jao-themes--italic '(it))
+(defvar jao-themes--button '(ul))
+(defvar jao-themes--strike-through '(:strike-through t))
+(defvar jao-themes--outline '((c "darkslategrey")))
+(defvar jao-themes--outline-1 '((p outline)))
+(defvar jao-themes--outline-2 '((p outline-1)))
+(defvar jao-themes--outline-3 '((p outline-2)))
+(defvar jao-themes--outline-4 '((p outline-3)))
+(defvar jao-themes--outline-5 '((p outline-4)))
+(defvar jao-themes--outline-6 '((p outline-5)))
+(defvar jao-themes--outline-7 '((p outline-6)))
+(defvar jao-themes--outline-8 '((p outline-7)))
+(defvar jao-themes--link '((c "darkgoldenrod4")))
+(defvar jao-themes--visited-link '((c "darkolivegreen4") nul))
+(defvar jao-themes--gnus-mail '(dfg))
+(defvar jao-themes--gnus-news '(dfg))
+(defvar jao-themes--tab-sel '((c nil "grey90") bx))
+(defvar jao-themes--tab-unsel '((c "grey30" "grey85") nbf bx))
+(defvar jao-themes--comment '((c "grey30")))
+(defvar jao-themes--warning '((c "indianred3") nbf))
+(defvar jao-themes--error '((c "indianred3") bf))
+(defvar jao-themes--constant '((c "darkolivegreen") nbf))
+(defvar jao-themes--function '((c "darkolivegreen") nbf))
+(defvar jao-themes--keyword '((c "darkslategrey") nbf))
+(defvar jao-themes--string '((c "skyblue4")))
+(defvar jao-themes--type '((c "darkslategrey")))
+(defvar jao-themes--variable-name '((c "DodgerBlue4")))
+(defvar jao-themes--dimm '((c "grey30") nbf))
+(defvar jao-themes--f00 '((c "dodgerblue4")))
+(defvar jao-themes--f01 '((c "cadetblue4")))
+(defvar jao-themes--f02 '((c "darkslategrey")))
+(defvar jao-themes--f10 '((c "dodgerblue4")))
+(defvar jao-themes--f11 '((c "cadetblue4")))
+(defvar jao-themes--f12 '((c "darkslategrey")))
+
+(defface jao-themes-hilite '((t :inherit default)) "")
+(defface jao-themes-italic '((t :inherit default)) "")
+(defface jao-themes-button '((t :inherit default)) "")
+(defface jao-themes-strike-through '((t :inherite default)) "")
+(defface jao-themes-outline '((t :inherite default)) "")
+(defface jao-themes-outline-1 '((t :inherite default)) "")
+(defface jao-themes-outline-2 '((t :inherite default)) "")
+(defface jao-themes-outline-3 '((t :inherite default)) "")
+(defface jao-themes-outline-4 '((t :inherite default)) "")
+(defface jao-themes-outline-5 '((t :inherite default)) "")
+(defface jao-themes-outline-6 '((t :inherite default)) "")
+(defface jao-themes-outline-7 '((t :inherite default)) "")
+(defface jao-themes-outline-8 '((t :inherite default)) "")
+(defface jao-themes-link '((t :inherite default)) "")
+(defface jao-themes-visited-link '((t :inherite default)) "")
+(defface jao-themes-gnus-mail '((t :inherite default)) "")
+(defface jao-themes-gnus-news '((t :inherite default)) "")
+(defface jao-themes-tab-sel '((t :inherite default)) "")
+(defface jao-themes-tab-unsel '((t :inherite default)) "")
+(defface jao-themes-comment '((t :inherite default)) "")
+(defface jao-themes-warning '((t :inherite default)) "")
+(defface jao-themes-error '((t :inherite default)) "")
+(defface jao-themes-constant '((t :inherite default)) "")
+(defface jao-themes-function '((t :inherite default)) "")
+(defface jao-themes-keyword '((t :inherite default)) "")
+(defface jao-themes-string '((t :inherite default)) "")
+(defface jao-themes-type '((t :inherite default)) "")
+(defface jao-themes-variable-name '((t :inherite default)) "")
+(defface jao-themes-dimm '((t :inherite default)) "")
+(defface jao-themes-f00 '((t :inherite default)) "")
+(defface jao-themes-f01 '((t :inherite default)) "")
+(defface jao-themes-f02 '((t :inherite default)) "")
+(defface jao-themes-f10 '((t :inherite default)) "")
+(defface jao-themes-f11 '((t :inherite default)) "")
+(defface jao-themes-f12 '((t :inherite default)) "")
+
+(defsubst jao-themes--palette-face (face)
+ (intern (format "jao-themes--%s" face)))
+
+(defun jao-themes--normalize-body (body)
+ (dolist (p '(:inverse-video :underline :inherit) body)
+ (unless (member p body)
+ (setq body (append body (list p nil))))))
+
+(defun jao-themes--parse-face-body (f)
+ (cond ((null f) nil)
+ ((listp f)
+ (jao-themes--normalize-body
+ (apply 'append (mapcar 'jao-themes--parse-face-sym f))))))
+
+(defvar jao-themes--default-cidxs
+ '("#000000" "#cd0000" "#00cd00" "#cdcd00"
+ "#0000cd" "#cd00cd" "#00cdcd" "#e5e5e5"
+ "#4d4d4d" "#ff0000" "#00ff00" "#ffff00"
+ "#0000ff" "#ff00ff" "#00ffff" "#ffffff"
+ "#000000" "#00002a" "#000055" "#000080"
+ "#0000aa" "#0000d4" "#002a00" "#002a2a"
+ "#002a55" "#002a80" "#002aaa" "#002ad4"
+ "#005500" "#00552a" "#005555" "#005580"
+ "#0055aa" "#0055d4" "#008000" "#00802a"
+ "#008055" "#008080" "#0080aa" "#0080d4"
+ "#00aa00" "#00aa2a" "#00aa55" "#00aa80"
+ "#00aaaa" "#00aad4" "#00d400" "#00d42a"
+ "#00d455" "#00d480" "#00d4aa" "#00d4d4"
+ "#2a0000" "#2a002a" "#2a0055" "#2a0080"
+ "#2a00aa" "#2a00d4" "#2a2a00" "#2a2a2a"
+ "#2a2a55" "#2a2a80" "#2a2aaa" "#2a2ad4"
+ "#2a5500" "#2a552a" "#2a5555" "#2a5580"
+ "#2a55aa" "#2a55d4" "#2a8000" "#2a802a"
+ "#2a8055" "#2a8080" "#2a80aa" "#2a80d4"
+ "#2aaa00" "#2aaa2a" "#2aaa55" "#2aaa80"
+ "#2aaaaa" "#2aaad4" "#2ad400" "#2ad42a"
+ "#2ad455" "#2ad480" "#2ad4aa" "#2ad4d4"
+ "#550000" "#55002a" "#550055" "#550080"
+ "#5500aa" "#5500d4" "#552a00" "#552a2a"
+ "#552a55" "#552a80" "#552aaa" "#552ad4"
+ "#555500" "#55552a" "#555555" "#555580"
+ "#5555aa" "#5555d4" "#558000" "#55802a"
+ "#558055" "#558080" "#5580aa" "#5580d4"
+ "#55aa00" "#55aa2a" "#55aa55" "#55aa80"
+ "#55aaaa" "#55aad4" "#55d400" "#55d42a"
+ "#55d455" "#55d480" "#55d4aa" "#55d4d4"
+ "#800000" "#80002a" "#800055" "#800080"
+ "#8000aa" "#8000d4" "#802a00" "#802a2a"
+ "#802a55" "#802a80" "#802aaa" "#802ad4"
+ "#805500" "#80552a" "#805555" "#805580"
+ "#8055aa" "#8055d4" "#808000" "#80802a"
+ "#808055" "#808080" "#8080aa" "#8080d4"
+ "#80aa00" "#80aa2a" "#80aa55" "#80aa80"
+ "#80aaaa" "#80aad4" "#80d400" "#80d42a"
+ "#80d455" "#80d480" "#80d4aa" "#80d4d4"
+ "#aa0000" "#aa002a" "#aa0055" "#aa0080"
+ "#aa00aa" "#aa00d4" "#aa2a00" "#aa2a2a"
+ "#aa2a55" "#aa2a80" "#aa2aaa" "#aa2ad4"
+ "#aa5500" "#aa552a" "#aa5555" "#aa5580"
+ "#aa55aa" "#aa55d4" "#aa8000" "#aa802a"
+ "#aa8055" "#aa8080" "#aa80aa" "#aa80d4"
+ "#aaaa00" "#aaaa2a" "#aaaa55" "#aaaa80"
+ "#aaaaaa" "#aaaad4" "#aad400" "#aad42a"
+ "#aad455" "#aad480" "#aad4aa" "#aad4d4"
+ "#d40000" "#d4002a" "#d40055" "#d40080"
+ "#d400aa" "#d400d4" "#d42a00" "#d42a2a"
+ "#d42a55" "#d42a80" "#d42aaa" "#d42ad4"
+ "#d45500" "#d4552a" "#d45555" "#d45580"
+ "#d455aa" "#d455d4" "#d48000" "#d4802a"
+ "#d48055" "#d48080" "#d480aa" "#d480d4"
+ "#d4aa00" "#d4aa2a" "#d4aa55" "#d4aa80"
+ "#d4aaaa" "#d4aad4" "#d4d400" "#d4d42a"
+ "#d4d455" "#d4d480" "#d4d4aa" "#d4d4d4"
+ "#080808" "#121212" "#1c1c1c" "#262626"
+ "#303030" "#3a3a3a" "#444444" "#4e4e4e"
+ "#585858" "#626262" "#6c6c6c" "#767676"
+ "#808080" "#8a8a8a" "#949494" "#9e9e9e"
+ "#a8a8a8" "#b2b2b2" "#bcbcbc" "#c6c6c6"
+ "#d0d0d0" "#dadada" "#e4e4e4" "#eeeeee"))
+
+(defvar jao-themes--cidxs nil)
+(defvar jao-themes--x-colors nil)
+
+(defvar *jao-themes--color-names* nil)
+(defvar *jao--parsed-faces* nil)
+
+(defun jao-themes--color (clr)
+ (cond ((stringp clr) clr)
+ ((numberp clr) (or (nth clr jao-themes--cidxs)
+ (nth clr jao-themes--default-cidxs)
+ (format "color-%s" clr)))
+ ((symbolp clr) (or (cadr (assoc clr *jao-themes--color-names*))
+ 'unspecified))
+ (t 'unspecified)))
+
+(defun jao-themes--parse-face-sym (s)
+ (cond ((listp s)
+ (cl-case (car s)
+ (c `(:foreground ,(jao-themes--color (cadr s))
+ :background ,(jao-themes--color (caddr s))))
+ (p (let ((var (jao-themes--palette-face (cadr s))))
+ (when (boundp var)
+ (let ((val (symbol-value var)))
+ (if (listp val)
+ (jao-themes--parse-face-body val)
+ val)))))
+ (ul `(:underline ,(jao-themes--color (cadr s))))
+ (~ (cdr (assq (cadr s) *jao--parsed-faces*)))
+ (t (list s))))
+ ((atom s)
+ (cl-case s
+ (~ '(:inherit))
+ (dbg `(:background ,jao-themes--bg))
+ (dfg `(:foreground ,jao-themes--fg))
+ (link (jao-themes--parse-face-body jao-themes--link))
+ (vlink (jao-themes--parse-face-body jao-themes--visited-link))
+ (bf '(:bold t :weight bold))
+ (nbf '(:bold t :weight normal))
+ (it '(:italic t :slant italic))
+ (nit '(:italic nil :slant normal))
+ (iv '(:inverse-video t))
+ (niv '(:inverse-video nil))
+ (ul '(:underline t))
+ (nul '(:underline nil))
+ (st '(:strike-through t))
+ (ex '(:extend t))
+ (nex '(:extend nil))
+ (bx `(:box (:line-width -1 :color ,jao-themes--box)))
+ (t (list s))))))
+
+(defun jao-themes--make-faces (fs &optional cidxs)
+ (let ((*jao--parsed-faces* nil)
+ (jao-themes--cidxs (or cidxs jao-themes--default-cidxs))
+ (result nil))
+ (dolist (f (sort (jao-themes--dfs fs) 'jao--cmp-faces) (reverse result))
+ (let ((body (jao-themes--parse-face-body (cdr f))))
+ (push (cons (car f) body) *jao--parsed-faces*)
+ (push (list (car f) body) result)))))
+
+(defun jao-themes-parse-face (f)
+ `(,(car f) ((t ,(jao-themes--parse-face-body (cdr f))))))
+
+(defun jao-themes-parse-faces (fs)
+ (let ((*jao--parsed-faces* nil))
+ (mapcar (lambda (f)
+ (let ((fp (jao-themes--parse-face-body (cdr f))))
+ (push (cons (car f) fp) *jao--parsed-faces*)
+ `(,(car f) ((t ,fp)))))
+ fs)))
+
+
+(defun jao--cmp-faces (a b)
+ (let ((ai (cadr (assq '~ a)))
+ (bi (cadr (assq '~ b))))
+ (cond ((and ai (not bi)) nil)
+ ((and bi (not ai)) t)
+ ((eq (car a) bi) t)
+ ((eq (car b) ai) nil)
+ (t (string< (symbol-name (car a))
+ (symbol-name (car b)))))))
+
+(defun jao-themes--dfs (fs)
+ (let ((dfs
+ (append
+ `((jao-themes-hilite (p hilite))
+ (jao-themes-italic (p italic))
+ (jao-themes-button (p button))
+ (jao-themes-strike-through (p strike-through))
+ (jao-themes-outline (p outline))
+ (jao-themes-outline-1 (p outline-1))
+ (jao-themes-outline-2 (p outline-2))
+ (jao-themes-outline-3 (p outline-3))
+ (jao-themes-outline-4 (p outline-4))
+ (jao-themes-outline-5 (p outline-5))
+ (jao-themes-outline-6 (p outline-6))
+ (jao-themes-outline-7 (p outline-7))
+ (jao-themes-outline-8 (p outline-8))
+ (jao-themes-link (p link))
+ (jao-themes-visited-link (p visited-link))
+ (jao-themes-gnus-mail (p gnus-mail))
+ (jao-themes-gnus-news (p gnus-news))
+ (jao-themes-tab-sel (p tab-sel))
+ (jao-themes-tab-unsel (p tab-unsel))
+ (jao-themes-comment (p comment))
+ (jao-themes-warning (p warning))
+ (jao-themes-error (p error))
+ (jao-themes-constant (p constant))
+ (jao-themes-function (p function))
+ (jao-themes-keyword (p keyword))
+ (jao-themes-string (p string))
+ (jao-themes-type (p type))
+ (jao-themes-variable-name (p variable-name))
+ (jao-themes-dimm (p dimm))
+ (jao-themes-f00 (p f00))
+ (jao-themes-f01 (p f01))
+ (jao-themes-f02 (p f02))
+ (jao-themes-f10 (p f10))
+ (jao-themes-f11 (p f11))
+ (jao-themes-f12 (p f12)))
+ `((aw-background-face (p dimm))
+ (aw-leading-char-face (~ error) :height 1.5)
+ (awesome-tray-module-awesome-tab-face (p f00))
+ (awesome-tray-module-battery-face (p f00))
+ (awesome-tray-module-battery-face (p f00))
+ (awesome-tray-module-buffer-name-face (p f11))
+ (awesome-tray-module-circe-face (p f00))
+ (awesome-tray-module-date-face (p f00))
+ (awesome-tray-module-date-face (p f01))
+ (awesome-tray-module-evil-face (p f00))
+ (awesome-tray-module-file-path-face (p f00))
+ (awesome-tray-module-git-face (p f10))
+ (awesome-tray-module-last-command-face (p f00))
+ (awesome-tray-module-location-face (p f00))
+ (awesome-tray-module-mode-name-face (p f00))
+ (awesome-tray-module-parent-dir-face (p f00))
+ (awesome-tray-module-rvm-face (p f00)))
+ `((bbdb-company)
+ (bbdb-field-name bf)
+ (bbdb-field-value nil)
+ (bbdb-name ul)
+ (bmk-mgr-bookmark-face nil)
+ (bmk-mgr-folder-face bf)
+ (bmk-mgr-sel-bookmark-face link)
+ (bmk-mgr-sel-folder-face bf)
+ (bold bf)
+ (bold-italic bf)
+ (border (c nil nil))
+ (buffer-menu-buffer bf)
+ (button (p button)))
+ `((calendar-holiday-marker (p f00))
+ (circe-highlight-nick-face (p warning))
+ (circe-originator-face (p f00))
+ (circe-my-message-face (p f10))
+ (circe-server-face (p dimm))
+ (clojure-test-failure-face (p warning) ul)
+ (clojure-test-error-face (p error) ul)
+ (cursor (p error)))
+ `((diredp-compressed-file-suffix (~ diredp-file-suffix))
+ (diredp-date-time (p f01))
+ (diredp-deletion (p error))
+ (diredp-deletion-file-name (~ diredp-deletion))
+ (diredp-dir-heading bf dfg dbg)
+ (diredp-dir-name (p f10))
+ (diredp-dir-priv dfg dbg bf)
+ (diredp-display-msg (p f00))
+ (diredp-exec-priv dfg dbg bf)
+ (diredp-executable-tag (p error))
+ (diredp-file-name dfg dbg)
+ (diredp-file-suffix (~ diredp-file-name))
+ (diredp-flag-mark (p f00) bf)
+ (diredp-flag-mark-line (p hilite))
+ (diredp-ignored-file-name (p dimm))
+ (diredp-link-priv (~ diredp-symlink))
+ (diredp-no-priv (~ diredp-read-priv))
+ (diredp-other-priv dfg dbg)
+ (diredp-rare-priv dfg dfg)
+ (diredp-read-priv dfg dbg bf)
+ (diredp-symlink (p warning))
+ (diredp-write-priv dfg dbg bf))
+ `((change-log-acknowledgement (p f02))
+ (change-log-conditionals (p f02))
+ (change-log-date (p f01))
+ (change-log-email (p f00))
+ (change-log-file (p f10))
+ (change-log-function (p function))
+ (change-log-list (p f11))
+ (change-log-name (p keyword))
+ (cider-stacktrace-face (~ highlight) ex)
+ (cider-test-error-face (p warning) ex)
+ (cider-test-failure-face (p error))
+ (cider-test-success-face (p f01))
+ (comint-highlight-input (p f01) nbf)
+ (comint-highlight-prompt (p f00))
+ (clojure-keyword-face (p keyword))
+ (company-echo-common (p warning))
+ (company-scrollbar-bg (p hilite))
+ (company-scrollbar-fg (p warning))
+ (company-tooltip (~ highlight))
+ (company-tooltip-annotation (~ company-tooltip) it)
+ (company-tooltip-annotation-selection (~ company-tooltip) it ul)
+ (company-tooltip-selection (~ company-tooltip) ul)
+ (company-tooltip-common (~ company-tooltip) bf)
+ (company-tooltip-common-selection
+ (~ company-tooltip-selection) bf)
+ (company-preview (~ highlight))
+ (company-preview-common (~ company-preview) bf)
+ (compilation-column-number (p f00) nul)
+ (compilation-error nbf (p error) nul)
+ (compilation-info nbf (p f02) nul)
+ (compilation-line-number (p f01) nul)
+ (compilation-mode-line-fail (p error))
+ (compilation-mode-line-exit (p f01) nbf)
+ (compilation-mode-line-run (p warning))
+ (compilation-warning nbf (p warning) nul)
+ (completions-common-part nbf :width normal)
+ (completions-first-difference bf dfg dbg)
+ (cursor dfg dbg)
+ (custom-button (~ button))
+ (custom-button-mouse (~ button))
+ (custom-button-pressed (~ button))
+ (custom-button-pressed-unraised (~ button))
+ (custom-button-unraised (~ button))
+ (custom-changed (p warning))
+ (custom-comment (p string))
+ (custom-comment-tag (p keyword))
+ (custom-documentation (p string))
+ (custom-face-tag nbf)
+ (custom-group-tag bf (p f00) :height 11)
+ (custom-group-tag-1 bf :family ,jao-themes--face-family
+ (p f00) :height 11)
+ (custom-invalid (p error))
+ (custom-link link)
+ (custom-modified (p f10))
+ (custom-rogue (p error))
+ (custom-saved ul)
+ (custom-set (p f11))
+ (custom-state (p f12))
+ (custom-themed (p f00))
+ (custom-variable-button (~ button))
+ (custom-variable-tag (p variable-name) bf)
+ (cvs-handled (p dimm)))
+ `((darcsum-change-line-face (p warning))
+ (darcsum-filename-face (p f00))
+ (darcsum-header-face (p f01))
+ (darcsum-marked-face (p f00) bf)
+ (darcsum-need-action-face (p warning))
+ (darcsum-need-action-marked-face bf (p warning))
+ (diary (p f02))
+ (dictionary-button-face (p link))
+ (dictionary-reference-face (p f11))
+ (dictionary-word-definition-face nil)
+ (dictionary-word-entry-face (p f10))
+ (diff-added (p warning) ex)
+ (diff-changed (p f02) nul)
+ (diff-context (p dimm))
+ (diff-file-header dfg dbg nbf)
+ (diff-function (p function))
+ (diff-header nbf dfg dbg)
+ (diff-hl-change (p dimm))
+ (diff-hl-insert (p dimm))
+ (diff-hl-delete (p warn))
+ (diff-hunk-header (~ diff-file-header))
+ (diff-index bf dfg dbg)
+ (diff-indicator-added (~ diff-added))
+ (diff-indicator-changed (~ diff-changed))
+ (diff-indicator-removed (~ diff-removed))
+ (diff-nonexistent bf (p error))
+ (diff-refine-added (~ diff-added))
+ (diff-refine-change (~ diff-changed))
+ (diff-refine-removed (~ diff-removed))
+ (diff-removed (p error) ex)
+ (dired-directory (p f02))
+ (dired-flagged bf)
+ (dired-header (p f01))
+ (dired-ignored (p dimm))
+ (dired-mark (p f00) bf)
+ (dired-marked bf (p f00))
+ (dired-symlink (p f11))
+ (dired-warn-writable (p warning))
+ (dired-warning (p warning))
+ (diredp-number (p f11)))
+ `((ediff-current-diff-A (~ diff-added) ex)
+ (ediff-current-diff-Ancestor (c nil ,jao-themes--box))
+ (ediff-current-diff-B (~ ediff-current-diff-A) ex)
+ (ediff-current-diff-C (~ ediff-current-diff-A) ex)
+ (ediff-even-diff-A (~ diff-added) bf ex)
+ (ediff-even-diff-Ancestor (c nil ,jao-themes--box) ex)
+ (ediff-even-diff-B (~ ediff-even-diff-A))
+ (ediff-even-diff-C (~ ediff-even-diff-A))
+ (ediff-fine-diff-A (~ ediff-current-diff-A) nbf ul)
+ (ediff-fine-diff-Ancestor (c nil ,jao-themes--box) ex)
+ (ediff-fine-diff-B (~ ediff-fine-diff-A))
+ (ediff-fine-diff-C (~ ediff-fine-diff-A))
+ (ediff-odd-diff-A (~ ediff-even-diff-A))
+ (ediff-odd-diff-Ancestor (~ ediff-odd-diff-A) nbf)
+ (ediff-odd-diff-B (~ ediff-odd-diff-A))
+ (ediff-odd-diff-C (~ ediff-odd-diff-A))
+ (emms-browser-album-face (p f00) :height 1.0)
+ (emms-browser-artist-face (p f01) :height 1.0)
+ (emms-browser-composer-face (p f02) :height 1.0)
+ (emms-browser-track-face (p f10) :height 1.0)
+ (emms-browser-year/genre-face (p f11) :height 1.0)
+ (emms-metaplaylist-mode-current-face (p f00) bf)
+ (emms-metaplaylist-mode-face (p f00))
+ (emms-playlist-selected-face (p f00) bf)
+ (emms-playlist-track-face (p f00) nbf)
+ (emms-stream-name-face (p f00))
+ (emms-stream-url-face link)
+ (enwc-connected (p warning))
+ (epa-field-body)
+ (epa-field-name bf)
+ (epa-mark bf (p f00))
+ (epa-string (p f01))
+ (epa-validity-disabled)
+ (epa-validity-high bf)
+ (epa-validity-low)
+ (epa-validity-medium)
+ (erc-action-face (p f02))
+ (erc-button (p link))
+ (erc-current-nick-face (p error))
+ (erc-direct-msg-face (p warning))
+ (erc-error-face (p error))
+ (erc-header-line (~ header))
+ (erc-input-face (p f01))
+ (erc-my-nick-face (p warning))
+ (erc-nick-default-face (p f00))
+ (erc-nick-msg-face (p warning))
+ (erc-notice-face (p dimm))
+ (erc-pal-face (p warning))
+ (erc-prompt-face (f 01))
+ (erc-timestamp-face (p dimm))
+ (error (p error))
+ (escape-glyph (p dimm))
+ (eshell-ls-archive (p f12))
+ (eshell-ls-backup (p dimm))
+ (eshell-ls-clutter (p dimm))
+ (eshell-ls-directory (p f02))
+ (eshell-ls-executable (p warning))
+ (eshell-ls-missing (p dimm))
+ (eshell-ls-product (p f01))
+ (eshell-ls-readonly (p f01) bf)
+ (eshell-ls-special bf (p f10))
+ (eshell-ls-symlink bf (p f11))
+ (eshell-ls-unreadable (p dimm))
+ (eshell-prompt (p f00)))
+ `((factor-font-lock-comment (~ font-lock-comment-face))
+ (factor-font-lock-constructor (~ font-lock-function-name-face))
+ (factor-font-lock-declaration (~ font-lock-type-face))
+ (factor-font-lock-getter-word (~ font-lock-function-name-face))
+ (factor-font-lock-parsing-word (~ font-lock-keyword-face))
+ (factor-font-lock-setter-word (~ font-lock-function-name-face))
+ (factor-font-lock-stack-effect (~ font-lock-comment-face))
+ (factor-font-lock-string (~ font-lock-string-face))
+ (factor-font-lock-symbol (~ font-lock-keyword-face))
+ (factor-font-lock-symbol-definition (~ font-lock-builtin-face))
+ (factor-font-lock-type-definition (~ font-lock-type-face))
+ (factor-font-lock-type-name (~ font-lock-type-face))
+ (factor-font-lock-vocabulary-name (~ font-lock-constant-face))
+ (factor-font-lock-word (~ font-lock-function-name-face))
+ (ffap)
+ (file-name-shadow (p dimm))
+ (fill-column-indicator (p dimm))
+ (fixed-pitch :family ,jao-themes--face-family)
+ (flyspell-duplicate nbf (p warning))
+ (flyspell-incorrect nbf (p error))
+ (font-latex-sectioning-1-face (~ outline-1))
+ (font-latex-sectioning-2-face (~ outline-2))
+ (font-latex-sectioning-3-face (~ outline-3))
+ (font-latex-sectioning-4-face (~ outline-4))
+ (font-latex-sectioning-5-face (~ outline-5))
+ (font-latex-slide-title-face (p f11))
+ (font-latex-warning-face (p warning))
+ (font-lock-builtin-face (p keyword))
+ (font-lock-comment-delimiter-face (p comment))
+ (font-lock-comment-face (p comment))
+ (font-lock-constant-face (p constant))
+ (font-lock-doc-face (p comment))
+ (font-lock-function-name-face (p function))
+ (font-lock-keyword-face (p keyword))
+ (font-lock-negation-char-face nil)
+ (font-lock-preprocessor-face (p constant))
+ (font-lock-regexp-grouping-backslash bf)
+ (font-lock-regexp-grouping-construct bf)
+ (font-lock-string-face (p string))
+ (font-lock-type-face (p type))
+ (font-lock-variable-name-face (p variable-name))
+ (font-lock-warning-face (p warning))
+ (fringe (p dimm))
+ (fuel-font-lock-debug-error (p error) nul)
+ (fuel-font-lock-debug-info (p f01) nul)
+ (fuel-font-lock-stack-region (p hilite))
+ (fuel-font-lock-xref-link link nul)
+ (fuel-font-lock-xref-vocab italic nul)
+ (fuel-font-lock-markup-link link)
+ (fuel-font-lock-markup-title (~ outline-1))
+ (fuel-font-lock-markup-emphasis (~ italic))
+ (fuel-font-lock-markup-heading (~ outline-1))
+ (fuel-font-lock-markup-strong (~ bold)))
+ `((geiser-font-lock-autodoc-current-arg (~ highlight))
+ (geiser-font-lock-autodoc-identifier
+ (~ font-lock-function-name-face))
+ (geiser-font-lock-doc-button (~ button))
+ (geiser-font-lock-doc-link link)
+ (geiser-font-lock-doc-title bf)
+ (geiser-font-lock-xref-header bf)
+ (geiser-font-lock-xref-link link nul)
+ (git-commit-summary-face (p f10))
+ (git-gutter-fr:added (~ fringe) nbf)
+ (git-gutter-fr:deleted (~ fringe) nbf)
+ (git-gutter-fr:modified (~ fringe) nbf)
+ (gnus-button (~ button))
+ (gnus-cite-attribution nil)
+ (gnus-cite-1 (p f10))
+ (gnus-cite-2 (p f11))
+ (gnus-cite-3 (p f12))
+ (gnus-cite-4 (p dimm))
+ (gnus-cite-5 (p dimm))
+ (gnus-cite-6 (p dimm))
+ (gnus-cite-7 (p dimm))
+ (gnus-cite-8 (p dimm))
+ (gnus-cite-9 (p dimm))
+ (gnus-cite-10 (p dimm))
+ (gnus-cite-11 (p dimm))
+ (gnus-emphasis-bold bf)
+ (gnus-emphasis-bold-italic bf)
+ (gnus-emphasis-highlight-words (p hilite))
+ (gnus-emphasis-italic nil)
+ (gnus-emphasis-strikethru st)
+ (gnus-emphasis-underline ul)
+ (gnus-emphasis-underline-bold bf ul)
+ (gnus-emphasis-underline-bold-italic bf ul)
+ (gnus-emphasis-underline-italic ul)
+ (gnus-group-mail-1 (p gnus-mail) bf)
+ (gnus-group-mail-1-empty (p gnus-mail) nbf)
+ (gnus-group-mail-2 (~ gnus-group-mail-1))
+ (gnus-group-mail-2-empty (~ gnus-group-mail-1-empty))
+ (gnus-group-mail-3 (~ gnus-group-mail-1))
+ (gnus-group-mail-3-empty (~ gnus-group-mail-1-empty))
+ (gnus-group-mail-4 (~ gnus-group-mail-1))
+ (gnus-group-mail-4-empty (~ gnus-group-mail-1-empty))
+ (gnus-group-mail-5 (p f00) bf)
+ (gnus-group-mail-5-empty (p f00))
+ (gnus-group-mail-6 (p dimm) bf)
+ (gnus-group-mail-6-empty (p dimm))
+ (gnus-group-mail-low bf (p dimm))
+ (gnus-group-mail-low-empty (p dimm))
+ (gnus-group-news-low bf (p dimm))
+ (gnus-group-news-low-empty (p dimm))
+ (gnus-group-news-1 (p gnus-news) bf)
+ (gnus-group-news-1-empty (p gnus-news) nbf)
+ (gnus-group-news-2 (~ gnus-group-news-1))
+ (gnus-group-news-2-empty (~ gnus-group-news-1-empty))
+ (gnus-group-news-3 (~ gnus-group-news-1))
+ (gnus-group-news-3-empty (~ gnus-group-news-1-empty))
+ (gnus-group-news-4 (~ gnus-group-news-1))
+ (gnus-group-news-4-empty (~ gnus-group-news-1-empty))
+ (gnus-group-news-5 (p f00) bf)
+ (gnus-group-news-5-empty (p f00))
+ (gnus-group-news-6 (p dimm) bf)
+ (gnus-group-news-6-empty (p dimm))
+ (gnus-header-content (p f02))
+ (gnus-header-from (p f01))
+ (gnus-header-name nbf (p f02))
+ (gnus-header-newsgroups (p dimm))
+ (gnus-header-subject (p f00) nbf)
+ (gnus-mouse-face nil)
+ (gnus-server-agent nbf)
+ (gnus-server-closed (p warning))
+ (gnus-server-denied bf (p error))
+ (gnus-server-offline (p dimm))
+ (gnus-server-opened bf)
+ (gnus-signature nit (p f10))
+ (gnus-splash dfg dbg)
+ (gnus-summary-high-undownloaded bf nit dfg dbg)
+ (gnus-summary-cancelled (p strike-through))
+ (gnus-summary-high-unread bf nit)
+ (gnus-summary-normal-ancient (p dimm))
+ (gnus-summary-normal-read (p dimm))
+ (gnus-summary-high-ticked bf nit dfg dbg)
+ (gnus-summary-low-ancient (p dimm))
+ (gnus-summary-low-read (p dimm) st)
+ (gnus-summary-low-ticked (p dimm))
+ (gnus-summary-low-unread (p dimm))
+ (gnus-summary-low-undownloaded (p dimm))
+ (gnus-summary-normal-ancient (p dimm))
+ (gnus-summary-normal-read (p dimm))
+ (gnus-summary-normal-ticked (p f10) nbf)
+ (gnus-summary-normal-undownloaded bf dfg dbg)
+ (gnus-summary-normal-unread dfg dbg)
+ (gnus-summary-selected (p hilite))
+ (gnus-x-face)
+ (google-translate-listen-button-face (~ button))
+ (google-translate-phonetic-face (~ default))
+ (google-translate-suggestion-face (p f00))
+ (google-translate-suggestion-label-face (p f01))
+ (google-translate-text-face (~ default))
+ (google-translate-translation-face bf)
+ (gui-button-face (~ button))
+ (gui-element (~ gui-button-face)))
+ `((header-line (~ mode-line-inactive))
+ (help-argument-name)
+ (helm-selection (p hilite))
+ (helm-separator (p dimm))
+ (helm-source-header (~ outline-1))
+ (helm-ls-git-added-copied-face dfg dbg)
+ (helm-ls-git-conflict-face (p error))
+ (helm-ls-git-deleted-and-staged-face (p dimm))
+ (helm-ls-git-deleted-not-staged-face dfg dbg)
+ (helm-ls-git-modified-and-staged-face (p f10))
+ (helm-ls-git-modified-not-staged-face (p warning))
+ (helm-ls-git-renamed-modified-face (p warning))
+ (helm-ls-git-untracked-face (p error))
+ (highlight (p hilite))
+ (hydra-face-blue (p f00))
+ (hydra-face-red (p error)))
+ `((ido-first-match (p warning))
+ (ido-first-match-face (p warning))
+ (ido-incomplete-regexp (p error))
+ (ido-indicator (p error) nbf)
+ (ido-only-match (p error))
+ (ido-subdir (p f01))
+ (info-header-node bf dfg)
+ (info-header-xref dfg)
+ (info-menu-header bf)
+ (info-menu-star bf dfg)
+ (info-node (p f00))
+ (info-title-1 (~ outline-1) bf)
+ (info-title-2 (~ outline-2) bf)
+ (info-title-3 (~ outline-3) bf)
+ (info-title-4 (~ outline-4) bf)
+ (Info-quoted (p f01))
+ (info-xref link)
+ (info-xref-visited vlink)
+ (isearch bf (p hilite))
+ (isearch-fail (p error))
+ (italic (p italic))
+ (ivy-confirm (p f01))
+ (ivy-current-match (p hilite) ex)
+ (ivy-grep-info (p f00))
+ (ivy-highlight-face (p hilite))
+ (ivy-match-required-face (p warning))
+ (ivy-minibuffer-match-highlight (c nil nil) ul)
+ (ivy-minibuffer-match-face-1 (p f00))
+ (ivy-minibuffer-match-face-2 (p f10))
+ (ivy-minibuffer-match-face-3 (p f01))
+ (ivy-minibuffer-match-face-4 (p f01))
+ (ivy-modified-buffer it)
+ (ivy-subdir (p f02))
+ (ivy-virtual (~ default) it))
+ `((jabber-activity-face dbg dfg nbf)
+ (jabber-activity-personal-face (p warning) nbf)
+ (jabber-chat-error (p error))
+ (jabber-chat-prompt-foreign (p f00) nbf)
+ (jabber-chat-prompt-local (p f01) nbf)
+ (jabber-chat-prompt-system (p f02) nbf)
+ (jabber-rare-time-face (p dimm))
+ (jabber-roster-user-away (p dimm))
+ (jabber-roster-user-dnd (p dimm))
+ (jabber-roster-user-chatty (p warning) nbf)
+ (jabber-roster-user-offline (p dimm))
+ (jabber-roster-user-online (p f01) nbf)
+ (jabber-roster-user-xa (p dimm))
+ (jabber-title-large (~ default) bf)
+ (jabber-title-medium bf)
+ (jabber-title-roster bf (p warning))
+ (jao-emms-font-lock-album (p f01))
+ (jao-emms-font-lock-artist (p f02))
+ (jao-emms-font-lock-title (p f01))
+ (jao-emms-font-lock-track dfg dbg)
+ (jao-frm-from-face (p f00))
+ (jao-frm-mailbox-face bf)
+ (jao-frm-subject-face (p f01))
+ (jao-frm-mailno-face bf)
+ (jao-gnus-face-tree (p dimm))
+ (jde-java-font-lock-constant-face (~ font-lock-constant-face))
+ (jde-java-font-lock-doc-tag-face (p f02))
+ (jde-java-font-lock-package-face (p f02))
+ (jde-java-font-lock-link-face (p link))
+ (jde-java-font-lock-number-face (~ font-lock-constant-face))
+ (jde-java-font-lock-public-face (~ font-lock-keyword-face))
+ (jde-java-font-lock-private-face (~ font-lock-keyword-face))
+ (jde-java-font-lock-protected-face (~ font-lock-keyword-face))
+ (jde-java-font-lock-modifier-face (~ font-lock-keyword-face)))
+ `((lazy-highlight (p hilite))
+ (line-number (p dimm))
+ (line-number-current-line (p hilite))
+ (link link nul)
+ (link-visited vlink nul)
+ (lui-button-face (p link))
+ (lui-highlight-face (p warning))
+ (lui-time-stamp-face (p dimm))
+ (lui-track-bar (p hilite) :height 0.1))
+ `((magit-branch (p f00))
+ (magit-cherry-equivalent (p warning))
+ (magit-diff-add (~ diff-added))
+ (magit-diff-context-highlight (p hilite) ex)
+ (magit-diff-del (~ diff-removed))
+ (magit-diff-file-heading (p keyword))
+ (magit-diff-file-header (~ diff-file-header))
+ (magit-diff-hunk-header (~ diff-hunk-header))
+ (magit-diff-none (p dimm))
+ (magit-hash (p f12))
+ (magit-item-highlight (~ mm-uu-extract) ex)
+ (magit-item-mark (p warning))
+ (magit-log-head-label (p keyword) bf)
+ (magit-log-head-label-head (p keyword) nbf ul)
+ (magit-log-head-label-default (p keyword) nbf)
+ (magit-log-head-label-local (p keyword) nbf)
+ (magit-log-head-label-remote (p function) bf)
+ (magit-log-head-label-tags (p warning) nbf)
+ (magit-log-graph (p f11))
+ (magit-log-tag-label (p keyword))
+ (magit-section-highlight (p hilite) ex)
+ (magit-section-heading (~ outline-2))
+ (magit-section-title (~ outline-2))
+ (Man-underline ul)
+ (match (p hilite))
+ (markdown-pre-face (~ font-lock-constant-face))
+ (markdown-code-face (p keyword))
+ (markdown-inline-code-face (p function))
+ (markdown-italic-face (~ italic))
+ (menu nil)
+ (message-cited-text (p f01) nbf)
+ (message-header-cc (p f00) nbf)
+ (message-header-name (p f01) nbf)
+ (message-header-newsgroups (p dimm) nbf)
+ (message-header-other (p f00) nbf)
+ (message-header-subject (p f00) nbf)
+ (message-header-to (p f00) nbf)
+ (message-header-xheader (p f00) nbf)
+ (message-mml (p warning) nbf)
+ (message-separator (p warning) nbf)
+ (mm-uu-extract (p hilite) ex)
+ (minibuffer-line (p f00))
+ (minibuffer-prompt (p f00))
+ (mode-line-buffer-id nbf (c nil nil))
+ (mode-line-emphasis (p warning))
+ (mode-line-highlight (~ mode-line))
+ ;; (modeline-mousable (~ mode-line-active))
+ ;; (modeline-mousable-minor-mode (~ modeline-mousable))
+ (moinmoin-table-pi (p f02))
+ (mouse dfg dbg ul)
+ (mpdel-playlist-current-song-face (p hilite) ex)
+ (mpdel-tablist-song-name-face (p f00))
+ (mpdel-tablist-track-face (~ default))
+ (mpdel-tablist-album-face (p f01))
+ (mpdel-tablist-disk-face (~ default))
+ (mpdel-tablist-date-face (~ default))
+ (mpdel-tablist-artist-face (p f01))
+ (muse-bad-link (p warning))
+ (muse-header-1 (~ outline-1))
+ (muse-header-2 (~ outline-2))
+ (muse-header-3 (~ outline-3))
+ (muse-header-4 (~ outline-4))
+ (muse-header-5 (~ outline-5))
+ (muse-link link)
+ (muse-verbatim (p f02)))
+ `((next-error (p hilite))
+ (nobreak-space dbg dfg ul)
+ (nrepl-error-face (p error))
+ (nrepl-input-face (p f01))
+ (nrepl-output-face (p f02))
+ (nrepl-prompt-face (p f00))
+ (nrepl-result-face nil))
+ `((org-agenda-date-today (p hilite) nul)
+ (org-agenda-date-weekend (p dimm))
+ (org-agenda-done (p dimm))
+ (org-agenda-restriction-lock (~ default))
+ (org-agenda-structure (p f00))
+ (org-archived (p dimm))
+ (org-code (p f11))
+ (org-column dfg dbg :height 1.0)
+ (org-date (p f02) nul)
+ (org-document-info nul)
+ (org-document-title bf)
+ (org-done (p dimm) nbf niv)
+ (org-drawer (p f02))
+ (org-ellipsis (p dimm))
+ (org-formula (p f02))
+ (org-headline-done (p dimm))
+ (org-hide (c ,jao-themes--bg))
+ (org-latex-and-export-specials (~ default))
+ (org-level-1 (~ outline-1))
+ (org-level-2 (~ outline-2))
+ (org-level-3 (~ outline-3))
+ (org-level-4 (~ outline-4))
+ (org-level-5 (~ outline-5))
+ (org-level-6 (~ outline-6))
+ (org-level-7 (~ outline-7))
+ (org-level-8 (~ outline-8))
+ (org-link link)
+ (org-noter-notes-exist-face it)
+ (org-property-value nil)
+ (org-roam-link (~ org-link) it)
+ (org-scheduled (p f01))
+ (org-scheduled-previously (p f00) nbf)
+ (org-scheduled-today (p f01))
+ (org-sexp-date (p f01))
+ (org-special-keyword (p keyword))
+ (org-table (p f01))
+ (org-tag (p dimm) nbf)
+ (org-target ul)
+ (org-time-grid dfg dbg)
+ (org-todo nbf niv (p error))
+ (org-upcoming-deadline (p f02))
+ (org-verbatim (p hilite))
+ (org-warning bf (p warning))
+ (outline-1 bf (p outline-1))
+ (outline-2 bf (p outline-2))
+ (outline-3 bf (p outline-3))
+ (outline-4 bf (p outline-4))
+ (outline-5 nbf ul (p outline-5))
+ (outline-6 nbf ul (p outline-6))
+ (outline-7 nbf ul (p outline-7))
+ (outline-8 nbf ul (p outline-8)))
+ `((powerline-active1 (~ mode-line))
+ (powerline-active2 (~ mode-line-inactive))
+ (powerline-inactive1 (~ mode-line-inactive))
+ (powerline-inactive2 (~ mode-line)))
+ `((query-replace bf (p hilite)))
+ `((rcirc-bright-nick (p hilite))
+ (rcirc-my-nick (p warning))
+ (rcirc-nick-in-message (p warning))
+ (rcirc-nick-in-message-full-line (~ rcirc-nick-in-message))
+ (rcirc-other-nick (p keyword))
+ (rcirc-prompt bf)
+ (rcirc-server (p dimm))
+ (rcirc-timestamp (p dimm))
+ (rcirc-track-keyword (p warning))
+ (rcirc-track-nick (~ rcirc-my-nick) niv)
+ (rcirc-url nbf link)
+ (reb-match-0 (p hilite))
+ (reb-match-1 (~ secondary-selection))
+ (reb-match-2 (~ secondary-selection) bf)
+ (reb-match-3 (~ secondary-selection) ul)
+ (region (p hilite) ex)
+ (rst-level-1-face (~ outline-1))
+ (rst-level-2-face (~ outline-2))
+ (rst-level-3-face (~ outline-3))
+ (rst-level-4-face (~ outline-4))
+ (rst-level-5-face (~ outline-5))
+ (rst-level-6-face (~ outline-6))
+ (rst-level-7-face (~ outline-7))
+ (rst-level-8-face (~ outline-8)))
+ `((secondary-selection (p hilite) ex)
+ (sh-quoted-exec (p f00))
+ (show-paren-match (p hilite))
+ (show-paren-mismatch (p error))
+ (sieve-control-commands (~ font-lock-builtin-face))
+ (sieve-tagged-arguments (~ font-lock-constant-face))
+ (sieve-test-commands (~ font-lock-keyword-face))
+ (sieve-action-commands (~ font-lock-keyword-face))
+ (signel-contact-face (p f11))
+ (signel-notice (p dimm))
+ (signel-notification (p warning))
+ (signel-prompt it)
+ (signel-timestamp (p dimm))
+ (signel-user (p f00))
+ (slack-channel-button-face (~ link))
+ (slack-message-action-face (~ link))
+ (slack-message-mention-face (p f01))
+ (slack-message-mention-keyword-face (p f01))
+ (slack-message-mention-me-face (p error))
+ (slack-message-output-header (p f00) it)
+ (slack-message-output-text nil)
+ (slack-new-message-marker-face (p warning))
+ (slack-preview-face (p f11))
+ (slack-search-result-message-header-face it)
+ (slack-user-profile-header-face (p f01))
+ (slack-user-profile-property-name-face bf)
+ (sldb-frame-line-face (p f00))
+ (sldb-frame-label-face (p f01))
+ (sldb-condition-face (p f02))
+ (slime-repl-prompt-face (p f00))
+ (slime-repl-input-face (p f00) bf)
+ (slime-repl-inputed-output-face (p f02))
+ (slime-repl-output-face (p string))
+ (sp-show-pair-enclosing nil)
+ (sp-show-pair-match-face (p hilite))
+ (sp-show-pair-mismatch-face (p error))
+ (spaceline-highlight-face-default (p f00))
+ (spaceline-highlight-face-modified (p f01))
+ (spaceline-highlight-face (p f02))
+ (spaceline-modified (p f10) iv)
+ (spaceline-unmodified (p f11) iv)
+ (spaceline-read-only (p f12) iv)
+ (speedbar-directory-face (~ diredp-dir-heading))
+ (speedbar-file-face (~ diredp-file-name))
+ (speedbar-highlight-face (p hilite))
+ (speedbar-selected-face ul)
+ (speedbar-separator-face (p f00))
+ (scroll-bar nil)
+ (shadow nil)
+ (success (p success))
+ (sunshine-forecast-date-face nil)
+ (sunshine-forecast-day-divider-face (p dimm))
+ (sunshine-forecast-headline-face (~ header-line)))
+ `((telega-button (~ button))
+ (telega-button-active (~ button))
+ (telega-msg-heading (p f00))
+ (telega-root-heading (p hilite))
+ (term nil)
+ (tool-bar nil)
+ (tooltip :family ,jao-themes--face-family (c nil "lightyellow"))
+ (trailing-whitespace (p error))
+ (treemacs-root-face nul bf :scale 1.1)
+ (twittering-timeline-footer-face (~ header-line))
+ (twittering-timeline-header-face (~ header-line))
+ (twittering-uri-face (~ link))
+ (twittering-username-face (p f01)))
+ `((underline ul))
+ `((variable-pitch :family ,jao-themes--face-family :height 110)
+ (vertical-border (c ,jao-themes--box nil) :inherit default))
+ `((w3m-anchor link)
+ (w3m-arrived-anchor vlink)
+ (w3m-bold bf dbg dfg)
+ (w3m-current-anchor nbf ul)
+ (w3m-form dfg dbg ul)
+ (w3m-form-button (~ button))
+ (w3m-form-button-mouse (~ custom-button-mouse))
+ (w3m-form-button-pressed (~ custom-button-pressed))
+ (w3m-header-line-location-content
+ :box (:line-width 3 :color ,jao-themes--bg) dfg dbg)
+ (w3m-header-line-location-title
+ :box (:line-width 3 :color ,jao-themes--bg) dfg dbg)
+ (w3m-header-line-content
+ :box (:line-width 3 :color ,jao-themes--bg) dfg dbg)
+ (w3m-header-line-title
+ :box (:line-width 3 :color ,jao-themes--bg) dfg dbg)
+ (w3m-history-current-url (c nil nil) ul)
+ (w3m-image (p f10))
+ (w3m-image-anchor (c nil nil))
+ (w3m-insert (p f12))
+ (w3m-italic (~ italic))
+ (w3m-linknum-match (p warning))
+ (w3m-linknum-minibuffer-prompt (~ minibuffer-prompt))
+ (w3m-session-select (p f10))
+ (w3m-session-selected bf nul (p f10))
+ (w3m-strike-through st)
+ (w3m-tab-background nul (c nil nil))
+ (w3m-tab-mouse nil)
+ (w3m-tab-selected (p tab-sel))
+ (w3m-tab-selected-background nil)
+ (w3m-tab-selected-retrieving (p tab-sel) it)
+ (w3m-tab-unselected (p tab-unsel))
+ (w3m-tab-unselected-retrieving (p tab-unsel) it)
+ (w3m-tab-unselected-unseen (p tab-unsel))
+ (w3m-underline ul)
+ (warning (p warning))
+ (wg-brace-face nil)
+ (wg-command-face (p f00))
+ (wg-current-workgroup-face (p f11) bf)
+ (wg-divider-face nil)
+ (wg-filename-face nil)
+ (wg-frame-face nil)
+ (wg-message-face (p string))
+ (wg-mode-line-face nil)
+ (wg-previous-workgroup-face (p f00))
+ (wgrep-delete-face st)
+ (wgrep-done-face (p f00))
+ (wgrep-face (p f10) ul)
+ (wgrep-file-face (p f01))
+ (wgrep-reject-face (p error) ul)
+ (widget-button (~ button))
+ (widget-button-pressed nbf (~ custom-button-pressed))
+ (widget-button-face (~ button))
+ (widget-button-pressed-face (~ button))
+ (widget-documentation (p dimm))
+ (widget-field (p hilite) bx)
+ (widget-inactive (p dimm))
+ (Widget-single-line-field (~ widget-field))
+ (woman-bold (p f00) bf)
+ (woman-italic (p f01) nul nit)
+ (woman-italic-no-ul (p f01) nul nit)))))
+ (dolist (df dfs fs)
+ (when (not (assq (car df) fs))
+ (push df fs)))))
+
+(defsubst jao-themes--let-palette (palette xp)
+ (mapcar (lambda (f)
+ `(,(jao-themes--palette-face (car f))
+ ',(or (and xp (caddr f)) (cadr f))))
+ palette))
+
+(defun jao-themes--extract-faces (t-faces x-faces)
+ (let ((result))
+ (dolist (f t-faces (reverse result))
+ (let ((xfb (cdr (assq (car f) x-faces))))
+ (push `(,(car f) ((((type x pgtk ns)) ,@xfb)
+ (t ,@(cdr f)))) result)))))
+
+(defun jao-themes--set-fbg (kind)
+ (let* ((kvs (cdr (assoc kind window-system-default-frame-alist)))
+ (f-alist (assq-delete-all 'background-color kvs))
+ (f-alist (assq-delete-all 'foreground-color f-alist)))
+ (when jao-themes--fg
+ (push (cons 'foreground-color jao-themes--fg) f-alist))
+ (when jao-themes--bg
+ (push (cons 'background-color jao-themes--bg) f-alist))
+ (setq window-system-default-frame-alist
+ (cons
+ (cons kind f-alist)
+ (assq-delete-all kind window-system-default-frame-alist)))))
+
+(defmacro jao-define-custom-theme (name &rest args)
+ (let* ((t-faces (make-symbol "t-faces"))
+ (xfaces (make-symbol "xfaces"))
+ (tx-faces (make-symbol "tx-faces"))
+ (palette (cdr (assoc :palette args)))
+ (faces (or (cdr (assoc :faces args)) (list)))
+ (x-faces (cdr (assoc :x-faces args)))
+ (x-colors (cdr (assoc :x-colors args)))
+ (a-colors (cdr (assoc :ansi-colors args)))
+ (ansi-colors (when a-colors
+ (apply 'vector (butlast a-colors
+ (- (length a-colors) 8))))))
+ `(progn
+ (setq ansi-color-names-vector ,ansi-colors)
+ (ansi-color-map-update 'ansi-color-names-vector ,ansi-colors)
+ (custom-make-theme-feature ',name)
+ (deftheme ,name)
+ (let ((*jao-themes--color-names* ',(cdr (assoc :names args))))
+ (let* ,(jao-themes--let-palette palette nil)
+ (jao-themes--set-fbg nil)
+ (let ((,t-faces (jao-themes--make-faces ',faces)))
+ (let* ,(jao-themes--let-palette palette t)
+ (jao-themes--set-fbg 'x)
+ (jao-themes--set-fbg 'pgtk)
+ (let* ((,xfaces (jao-themes--make-faces ',x-faces ',x-colors))
+ (,tx-faces (jao-themes--extract-faces ,t-faces ,xfaces)))
+ (put ',name 'theme-immediate t)
+ (apply 'custom-theme-set-faces (cons ',name ,tx-faces)))))
+ (provide-theme ',name))))))
+
+(put 'jao-define-custom-theme 'lisp-indent-function 1)
+
+(when load-file-name
+ (add-to-list 'custom-theme-load-path (file-name-directory load-file-name)))
+
+
+
+(provide 'jao-themes)
diff --git a/lib/themes/jao-zenburn-theme.el b/lib/themes/jao-zenburn-theme.el
new file mode 100644
index 0000000..a866d03
--- /dev/null
+++ b/lib/themes/jao-zenburn-theme.el
@@ -0,0 +1,132 @@
+(require 'jao-themes)
+
+(setq zenburn-override-colors-alist
+ `(("zenburn-magenta" . "thistle")
+ ("zenburn-cyan" . "LightSteelBlue1")
+ ("zenburn-blue+1" . "LemonChiffon")
+ ("zenburn-blue" . "LemonChiffon1")
+ ("zenburn-blue-1" . "LemonChiffon2")
+ ("zenburn-blue-2" . "LemonChiffon3")
+ ("zenburn-blue-3" . "LemonChiffon4")
+ ("zenburn-blue-4" . "cadet blue")
+ ("zenburn-blue-5" . "dark cyan")))
+
+(use-package zenburn-theme :ensure t)
+
+;; (setq zenburn-colors-alist
+;; (append zenburn-default-colors-alist
+;; zenburn-override-colors-alist))
+
+(load-theme 'zenburn t)
+
+(zenburn-with-color-variables
+ (let* ((box '(:box (:line-width 1 :color "grey35")))
+ (f (jao-themes-parse-faces
+ `((circe-my-message-face (c "gray70"))
+ (circe-originator-face (c ,zenburn-yellow-1))
+ (compilation-info (c ,zenburn-yellow) nul)
+ (compilation-error (c ,zenburn-red+1) nul)
+ (custom-button ,@box it)
+ (dictionary-word-definition-face nil)
+ (diff-hl-change (c nil ,zenburn-blue-3))
+ (diff-hl-delete (c nil ,zenburn-red-1))
+ (diff-hl-insert (c nil ,zenburn-green-1))
+ (diredp-date-time (c ,zenburn-yellow))
+ (diredp-dir-name (c ,zenburn-blue-2) bf)
+ (diredp-exec-priv (c ,zenburn-yellow-2))
+ (diredp-write-priv (c ,zenburn-yellow-2))
+ (emms-browser-artist-face (c ,zenburn-yellow-1))
+ (emms-browser-composer-face (~ emms-browser-artist-face))
+ (emms-browser-performer-face (~ emms-browser-artist-face))
+ (emms-browser-year-face (~ emms-browser-artist-face))
+ (emms-browser-year/genre-face (~ emms-browser-artist-face))
+ (fill-column-indicator (c ,zenburn-bg+1))
+ (font-lock-function-name-face (c ,zenburn-yellow) nbf)
+ (fringe (c ,zenburn-fg-05 nil))
+ (gnus-cite-1 (c "#b8b8b0"))
+ (gnus-cite-2 (c ,zenburn-fg-05))
+ (gnus-cite-3 (c ,zenburn-fg-05))
+ (gnus-cite-4 (c ,zenburn-fg-05))
+ (gnus-group-mail-1 (c ,zenburn-yellow))
+ (gnus-group-mail-2 (c ,zenburn-yellow))
+ (gnus-group-mail-3 (c ,zenburn-yellow))
+ (gnus-group-mail-4 (c ,zenburn-yellow))
+ (gnus-group-mail-5 (c ,zenburn-yellow))
+ (gnus-group-mail-6 (c ,zenburn-yellow))
+ (gnus-group-news-1 (c ,zenburn-yellow))
+ (gnus-group-news-2 (c ,zenburn-yellow))
+ (gnus-group-news-3 (c ,zenburn-yellow))
+ (gnus-group-news-4 (c ,zenburn-yellow))
+ (gnus-group-news-5 (c ,zenburn-yellow))
+ (gnus-group-news-6 (c ,zenburn-yellow))
+ (gnus-group-news-1-empty (c ,zenburn-fg-05))
+ (gnus-group-news-2-empty (c ,zenburn-fg-05))
+ (gnus-group-news-3-empty (c ,zenburn-fg-05))
+ (gnus-group-news-4-empty (c ,zenburn-fg-05))
+ (gnus-group-news-5-empty (c ,zenburn-fg-05))
+ (gnus-group-news-6-empty (c ,zenburn-fg-05))
+ (gnus-summary-cancelled (c ,zenburn-red) st)
+ (gnus-summary-normal-ancient (c ,zenburn-fg-05))
+ (header-line (c ,zenburn-fg ,zenburn-bg+1))
+ (isearch (c nil ,zenburn-bg+1))
+ (ivy-confirm (c ,zenburn-blue))
+ (ivy-current-match (c ,zenburn-orange))
+ (ivy-highlight-face (c ,zenburn-bg-08))
+ (ivy-match-required-face (c ,zenburn-orange))
+ (ivy-minibuffer-match-highlight (c nil nil))
+ (ivy-minibuffer-match-face-1 (c ,zenburn-yellow-2) ul)
+ (ivy-minibuffer-match-face-2 (c ,zenburn-yellow-2) ul)
+ (ivy-minibuffer-match-face-3 (c ,zenburn-yellow-2) ul)
+ (ivy-minibuffer-match-face-4 (c ,zenburn-yellow-2) ul)
+ (ivy-modified-buffer it)
+ (ivy-subdir (c ,zenburn-green+2))
+ (link (c ,zenburn-yellow) nbf nul)
+ (link-visited (c ,zenburn-yellow-2) nbf nul)
+ (lui-button-face (c ,zenburn-green+2))
+ (lui-time-stamp-face (c ,zenburn-bg+3))
+ (magit-diff-added-highlight (c ,zenburn-fg+1 ,zenburn-green))
+ (magit-hash (c ,zenburn-green))
+ (match (c ,zenburn-orange) nbf)
+ (mm-uu-extract (c nil ,zenburn-bg+1))
+ (mode-line (c ,zenburn-fg ,zenburn-bg+1) ,@box)
+ (mode-line-buffer-id (c ,zenburn-yellow nil) bf)
+ (mode-line-buffer-id-inactive (c ,zenburn-fg-1 nil) nbf)
+ (mode-line-inactive (~ header-line) ,@box)
+ (org-block nil)
+ (org-ellipsis (c ,zenburn-yellow) nul bf)
+ (powerline-active1 (c nil ,zenburn-bg+1))
+ (powerline-active2 (c nil ,zenburn-bg+3))
+ (powerline-inactive1 (c nil ,zenburn-bg+1))
+ (powerline-inactive2 (c nil ,zenburn-bg+2))
+ (rcirc-track-nick (c ,zenburn-orange))
+ (spaceline-read-only (c "black" ,zenburn-blue-3))
+ (spaceline-modified (c "black" ,zenburn-blue-2))
+ (spaceline-unmodified (c nil ,zenburn-green-1))
+ (slack-channel-button-face (~ link))
+ (slack-message-mention-face (p f01))
+ (slack-message-mention-keyword-face (p f01))
+ (slack-message-mention-me-face (p error))
+ (slack-message-output-header (c ,zenburn-yellow) it)
+ (slack-message-output-text nil)
+ (slack-new-message-marker-face (p warning))
+ (slack-preview-face (c ,zenburn-green))
+ (slack-search-result-message-header-face it)
+ (slack-user-profile-header-face (p f01))
+ (slack-user-profile-property-name-face bf)
+ (TeX-error-description-error (c ,zenburn-red))
+ (vertical-border (c ,zenburn-bg+2))
+ (w3m-anchor (~ link))
+ (w3m-arrived-anchor (~ visited-link))
+ (w3m-form-button (c ,zenburn-green+2 ,zenburn-bg+1))
+ (w3m-header-line-location-content (c ,zenburn-yellow))
+ (w3m-header-line-location-title nil)
+ (w3m-image-anchor (~ w3m-anchor) (c nil ,zenburn-bg+2))
+ (w3m-tab-background (~ mode-line))
+ (w3m-tab-selected (c ,zenburn-red+1 ,zenburn-bg) bf bx)
+ (w3m-tab-unselected (c ,zenburn-fg "grey30") bx)
+ (w3m-tab-selected-background (~ w3m-tab-selected))
+ (w3m-tab-unselected-unseen (~ w3m-tab-unselected))))))
+ (apply 'custom-theme-set-faces (cons 'zenburn f))
+ (custom-theme-set-variables 'zenburn `(fci-rule-color ,zenburn-bg+1))))
+
+(provide 'jao-zenburn-theme)