diff options
author | jao <jao@gnu.org> | 2021-02-02 05:16:17 +0000 |
---|---|---|
committer | jao <jao@gnu.org> | 2021-02-02 05:16:17 +0000 |
commit | 771abb84830678455de4625ac7f082d8100f0ea0 (patch) | |
tree | 0d303c2cb0861b949ca73a9705954f6a69c4f877 /lib | |
parent | 81eceb5507aa0659e9f0c9761e54e9102085c4ac (diff) | |
download | elibs-771abb84830678455de4625ac7f082d8100f0ea0.tar.gz elibs-771abb84830678455de4625ac7f082d8100f0ea0.tar.bz2 |
libs -> lib/
Diffstat (limited to 'lib')
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) |