bui-list.el 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. ;;; bui-list.el --- 'List' buffer interface for displaying data -*- lexical-binding: t -*-
  2. ;; Copyright © 2014-2016 Alex Kost <alezost@gmail.com>
  3. ;; This program is free software; you can redistribute it and/or modify
  4. ;; it under the terms of the GNU General Public License as published by
  5. ;; the Free Software Foundation, either version 3 of the License, or
  6. ;; (at your option) any later version.
  7. ;;
  8. ;; This program is distributed in the hope that it will be useful,
  9. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;; GNU General Public License for more details.
  12. ;;
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;; This file provides 'list' buffer interface for displaying an arbitrary
  17. ;; data.
  18. ;;; Code:
  19. (require 'cl-lib)
  20. (require 'dash)
  21. (require 'tabulated-list)
  22. (require 'bui-core)
  23. (require 'bui-button)
  24. (require 'bui-entry)
  25. (require 'bui-utils)
  26. (bui-define-groups bui-list)
  27. ;;; General 'list' variables
  28. (defvar bui-list-format nil
  29. "List of methods to get values of the displayed columns.
  30. Each element of the list has a form:
  31. (PARAM VALUE-FUN WIDTH SORT . PROPS)
  32. PARAM is a name of an entry parameter.
  33. VALUE-FUN may be either nil or a function returning a value that
  34. will be inserted. The function is called with 2 arguments: the
  35. first one is the value of the parameter; the second one is an
  36. entry (alist of parameter names and values).
  37. For the meaning of WIDTH, SORT and PROPS, see
  38. `tabulated-list-format'.")
  39. (defcustom bui-list-sort-key nil
  40. "Default sort key for 'list' buffer.
  41. Should be nil (no sort) or have a form:
  42. (PARAM . FLIP)
  43. PARAM is the name of an entry parameter. For the meaning of
  44. FLIP, see `tabulated-list-sort-key'."
  45. :type '(choice (const :tag "No sort" nil)
  46. (cons symbol boolean))
  47. :group 'bui-list)
  48. (defvar bui-list-additional-marks nil
  49. "Alist of additional marks for 'list' buffer.
  50. Marks from this list are used along with `bui-list-default-marks'.")
  51. (defcustom bui-list-show-single nil
  52. "If non-nil, list an entry even if it is the only matching result.
  53. If nil, show a single entry in the 'info' buffer instead."
  54. :type 'boolean
  55. :group 'bui-list)
  56. (defcustom bui-list-describe-warning-count 10
  57. "The maximum number of entries to describe without a warning.
  58. If you want to describe more than this number of marked entries,
  59. you will be prompted for confirmation. See also
  60. `bui-list-describe'."
  61. :type 'integer
  62. :group 'bui-list)
  63. (defvar bui-list-describe-function nil
  64. "Function used to describe entries.
  65. It is applied to the entries IDs as the rest arguments.")
  66. (defconst bui-list-symbol-specifications
  67. '((:describe-function describe-function)
  68. (:describe-count describe-warning-count t)
  69. (:format format t)
  70. (:list-single? show-single t)
  71. (:marks additional-marks)
  72. (:sort-key sort-key t))
  73. "Specifications for generating 'list' variables.
  74. See `bui-symbol-specifications' for details.")
  75. ;;; Displaying 'info' buffer
  76. (defun bui-list-describe (&rest mark-names)
  77. "Describe entries marked with MARK-NAMES.
  78. 'Describe' means display entries in 'info' buffer.
  79. If no entries are marked, describe the current entry.
  80. Available MARK-NAMES are symbols from `bui-list-marks'.
  81. Interactively, describe entries marked with a general mark. With
  82. prefix argument, describe entries marked with any mark."
  83. (interactive (unless current-prefix-arg '(general)))
  84. (let* ((ids (or (apply #'bui-list-get-marked-id-list mark-names)
  85. (list (bui-list-current-id))))
  86. (count (length ids))
  87. (entry-type (bui-current-entry-type)))
  88. (when (or (<= count bui-list-describe-warning-count)
  89. (y-or-n-p (format "Do you really want to describe %d entries? "
  90. count)))
  91. (bui-list-describe-entries entry-type ids))))
  92. ;;; Wrappers for 'list' variables
  93. (defun bui-list-symbol (entry-type symbol)
  94. "Return symbol for ENTRY-TYPE and 'list' buffer type."
  95. (bui-symbol entry-type 'list symbol))
  96. (defun bui-list-symbol-value (entry-type symbol)
  97. "Return SYMBOL's value for ENTRY-TYPE and 'list' buffer type."
  98. (bui-symbol-value entry-type 'list symbol))
  99. (defun bui-list-param-title (entry-type param)
  100. "Return column title of an ENTRY-TYPE parameter PARAM."
  101. (bui-param-title entry-type 'list param))
  102. (defun bui-list-format (entry-type)
  103. "Return column format for ENTRY-TYPE."
  104. (bui-list-symbol-value entry-type 'format))
  105. (defun bui-list-displayed-params (entry-type)
  106. "Return a list of ENTRY-TYPE parameters that should be displayed."
  107. (mapcar #'car (bui-list-format entry-type)))
  108. (defun bui-list-sort-key (entry-type)
  109. "Return sort key for ENTRY-TYPE."
  110. (bui-list-symbol-value entry-type 'sort-key))
  111. (defun bui-list-show-single-entry? (entry-type)
  112. "Return non-nil, if a single entry of ENTRY-TYPE should be listed."
  113. (bui-list-symbol-value entry-type 'show-single))
  114. (defun bui-list-describe-entries (entry-type ids)
  115. "Describe ENTRY-TYPE entries with IDS in 'info' buffer."
  116. (--if-let (bui-list-symbol-value entry-type 'describe-function)
  117. (apply it ids)
  118. (error "Can't describe %s: '%S' is unset or undefined"
  119. (if (null (cdr ids)) "this entry" "these entries")
  120. (bui-list-symbol entry-type 'describe-function))))
  121. ;;; Tabulated list internals
  122. (defun bui-list-sort-numerically (column a b)
  123. "Compare COLUMN of tabulated entries A and B numerically.
  124. This function is used for sort predicates for `tabulated-list-format'.
  125. Return non-nil, if B is bigger than A."
  126. (cl-flet ((num (entry)
  127. (string-to-number (aref (cadr entry) column))))
  128. (> (num b) (num a))))
  129. (defmacro bui-list-define-numerical-sorter (column)
  130. "Define numerical sort predicate for COLUMN.
  131. See `bui-list-sort-numerically' for details."
  132. (let ((name (intern (format "bui-list-sort-numerically-%d" column)))
  133. (doc (format "\
  134. Predicate to sort tabulated list by column %d numerically.
  135. See `bui-list-sort-numerically' for details."
  136. column)))
  137. `(defun ,name (a b)
  138. ,doc
  139. (bui-list-sort-numerically ,column a b))))
  140. (defmacro bui-list-define-numerical-sorters (n)
  141. "Define numerical sort predicates for columns from 0 to N.
  142. See `bui-list-define-numerical-sorter' for details."
  143. `(progn
  144. ,@(mapcar (lambda (i)
  145. `(bui-list-define-numerical-sorter ,i))
  146. (number-sequence 0 n))))
  147. (bui-list-define-numerical-sorters 9)
  148. (defun bui-list-tabulated-sort-key (entry-type)
  149. "Return ENTRY-TYPE sort key for `tabulated-list-sort-key'."
  150. (let ((sort-key (bui-list-sort-key entry-type)))
  151. (and sort-key
  152. (cons (bui-list-param-title entry-type (car sort-key))
  153. (cdr sort-key)))))
  154. (defun bui-list-tabulated-vector (entry-type fun)
  155. "Call FUN on each column specification for ENTRY-TYPE.
  156. FUN is applied to column specification as arguments (see
  157. `bui-list-format').
  158. Return a vector made of values of FUN calls."
  159. (apply #'vector
  160. (mapcar (lambda (col-spec)
  161. (apply fun col-spec))
  162. (bui-list-format entry-type))))
  163. (defun bui-list-tabulated-format (entry-type)
  164. "Return ENTRY-TYPE list specification for `tabulated-list-format'."
  165. (bui-list-tabulated-vector
  166. entry-type
  167. (lambda (param _ &rest rest-spec)
  168. (cons (bui-list-param-title entry-type param)
  169. rest-spec))))
  170. (defun bui-list-tabulated-entries (entries entry-type)
  171. "Return a list of ENTRY-TYPE values for `tabulated-list-entries'."
  172. (mapcar (lambda (entry)
  173. (list (bui-entry-id entry)
  174. (bui-list-tabulated-entry entry entry-type)))
  175. entries))
  176. (defun bui-list-tabulated-entry (entry entry-type)
  177. "Return array of values for `tabulated-list-entries'.
  178. Parameters are taken from ENTRY-TYPE ENTRY."
  179. (bui-list-tabulated-vector
  180. entry-type
  181. (lambda (param fun &rest _)
  182. (let ((value (bui-entry-value entry param)))
  183. (cond
  184. ((bui-void-value? value) bui-empty-string)
  185. (fun (funcall fun value entry))
  186. ((and (null value)
  187. (bui-boolean-param? entry-type 'list param))
  188. bui-false-string)
  189. (t (bui-get-string value)))))))
  190. ;;; Displaying entries
  191. (defun bui-list-get-display-entries (entry-type &rest args)
  192. "Search for entries and show them in a 'list' buffer preferably."
  193. (let ((entries (bui-get-entries entry-type 'list args)))
  194. (if (or (null entries) ; = 0
  195. (cdr entries) ; > 1
  196. (bui-list-show-single-entry? entry-type)
  197. (not (bui-interface-defined? entry-type 'info)))
  198. (bui-display-entries entries entry-type 'list args)
  199. (if (equal (bui-symbol-value entry-type 'info 'get-entries-function)
  200. (bui-symbol-value entry-type 'list 'get-entries-function))
  201. (bui-display-entries entries entry-type 'info args)
  202. (bui-get-display-entries entry-type 'info args)))))
  203. (defun bui-list-insert-entries (entries entry-type)
  204. "Print ENTRY-TYPE ENTRIES in the current buffer."
  205. (setq tabulated-list-entries
  206. (bui-list-tabulated-entries entries entry-type))
  207. (tabulated-list-print))
  208. (defun bui-list-get-one-line (value &optional _)
  209. "Return one-line string from a multi-line string VALUE.
  210. VALUE may be nil."
  211. (bui-get-non-nil value
  212. (bui-get-one-line value)))
  213. (defun bui-list-get-time (time &optional _)
  214. "Return formatted time string from TIME.
  215. TIME may be nil or another value supported by `bui-get-time-string'."
  216. (bui-get-non-nil time
  217. (bui-get-string (bui-get-time-string time)
  218. 'bui-time)))
  219. (defun bui-list-get-file-name (file-name &optional _)
  220. "Return FILE-NAME button specification for `tabulated-list-entries'.
  221. FILE-NAME may be nil."
  222. (bui-get-non-nil file-name
  223. (list file-name
  224. :type 'bui-file
  225. 'file file-name)))
  226. (defun bui-list-get-url (url &optional _)
  227. "Return URL button specification for `tabulated-list-entries'.
  228. URL may be nil."
  229. (bui-get-non-nil url
  230. (list url
  231. :type 'bui-url
  232. 'url url)))
  233. ;;; 'List' lines
  234. (defun bui-list-current-id ()
  235. "Return ID of the entry at point."
  236. (or (tabulated-list-get-id)
  237. (user-error "No entry here")))
  238. (defun bui-list-current-entry ()
  239. "Return entry at point."
  240. (bui-entry-by-id (bui-current-entries)
  241. (bui-list-current-id)))
  242. (defun bui-list-for-each-line (fun &rest args)
  243. "Call FUN with ARGS for each entry line."
  244. (or (derived-mode-p 'bui-list-mode)
  245. (error "The current buffer is not in `bui-list-mode'"))
  246. (save-excursion
  247. (goto-char (point-min))
  248. (while (not (eobp))
  249. (apply fun args)
  250. (forward-line))))
  251. (defun bui-list-fold-lines (fun init)
  252. "Fold over entry lines in the current list buffer.
  253. Call FUN with RESULT as argument for each line, using INIT as
  254. the initial value of RESULT. Return the final result."
  255. (let ((res init))
  256. (bui-list-for-each-line
  257. (lambda () (setq res (funcall fun res))))
  258. res))
  259. ;;; Marking and sorting
  260. (defvar-local bui-list-marked nil
  261. "List of the marked entries.
  262. Each element of the list has a form:
  263. (ID MARK-NAME . ARGS)
  264. ID is an entry ID.
  265. MARK-NAME is a symbol from `bui-list-marks'.
  266. ARGS is a list of additional values.")
  267. (defvar-local bui-list-marks nil
  268. "Alist of available mark names and mark characters.")
  269. (defvar bui-list-default-marks
  270. '((empty . ?\s)
  271. (general . ?*))
  272. "Alist of default mark names and mark characters.")
  273. (defun bui-list-get-mark (name)
  274. "Return mark character by its NAME."
  275. (or (bui-assq-value bui-list-marks name)
  276. (error "Mark '%S' not found" name)))
  277. (defun bui-list-get-mark-string (name)
  278. "Return mark string by its NAME."
  279. (string (bui-list-get-mark name)))
  280. (defun bui-list-current-mark ()
  281. "Return mark character of the current line."
  282. (char-after (line-beginning-position)))
  283. (defun bui-list-get-marked (&rest mark-names)
  284. "Return list of specs of entries marked with any mark from MARK-NAMES.
  285. Entry specs are elements from `bui-list-marked' list.
  286. If MARK-NAMES are not specified, use all marks from
  287. `bui-list-marks' except the `empty' one."
  288. (or mark-names
  289. (setq mark-names
  290. (delq 'empty (mapcar #'car bui-list-marks))))
  291. (-filter (-lambda ((_id name . _))
  292. (memq name mark-names))
  293. bui-list-marked))
  294. (defun bui-list-get-marked-args (mark-name)
  295. "Return list of (ID . ARGS) elements from lines marked with MARK-NAME.
  296. See `bui-list-marked' for the meaning of ARGS."
  297. (mapcar (-lambda ((id _name . args))
  298. (cons id args))
  299. (bui-list-get-marked mark-name)))
  300. (defun bui-list-get-marked-id-list (&rest mark-names)
  301. "Return list of IDs of entries marked with any mark from MARK-NAMES.
  302. See `bui-list-get-marked' for details."
  303. (mapcar #'car (apply #'bui-list-get-marked mark-names)))
  304. (defun bui-list--mark (mark-name &optional advance &rest args)
  305. "Put a mark on the current line.
  306. Also add the current entry to `bui-list-marked' using its ID and ARGS.
  307. MARK-NAME is a symbol from `bui-list-marks'.
  308. If ADVANCE is non-nil, move forward by one line after marking."
  309. (let ((id (bui-list-current-id)))
  310. (if (eq mark-name 'empty)
  311. (setq bui-list-marked (assq-delete-all id bui-list-marked))
  312. (let ((assoc (assq id bui-list-marked))
  313. (val (cons mark-name args)))
  314. (if assoc
  315. (setcdr assoc val)
  316. (push (cons id val) bui-list-marked)))))
  317. (tabulated-list-put-tag (bui-list-get-mark-string mark-name)
  318. advance))
  319. (defun bui-list-mark (&optional arg)
  320. "Mark the current line and move to the next line.
  321. With ARG, mark all lines."
  322. (interactive "P")
  323. (if arg
  324. (bui-list-mark-all)
  325. (bui-list--mark 'general t)))
  326. (defun bui-list-mark-all (&optional mark-name)
  327. "Mark all lines with MARK-NAME mark.
  328. MARK-NAME is a symbol from `bui-list-marks'.
  329. Interactively, put a general mark on all lines."
  330. (interactive)
  331. (or mark-name (setq mark-name 'general))
  332. (bui-list-for-each-line #'bui-list--mark mark-name))
  333. (defun bui-list-unmark (&optional arg)
  334. "Unmark the current line and move to the next line.
  335. With ARG, unmark all lines."
  336. (interactive "P")
  337. (if arg
  338. (bui-list-unmark-all)
  339. (bui-list--mark 'empty t)))
  340. (defun bui-list-unmark-backward ()
  341. "Move up one line and unmark it."
  342. (interactive)
  343. (forward-line -1)
  344. (bui-list--mark 'empty))
  345. (defun bui-list-unmark-all ()
  346. "Unmark all lines."
  347. (interactive)
  348. (bui-list-mark-all 'empty))
  349. (defun bui-list-restore-marks ()
  350. "Put marks according to `bui-list-marked'."
  351. (bui-list-for-each-line
  352. (lambda ()
  353. (let ((mark-name (car (bui-assq-value bui-list-marked
  354. (bui-list-current-id)))))
  355. (tabulated-list-put-tag
  356. (bui-list-get-mark-string (or mark-name 'empty)))))))
  357. (defun bui-list-sort (&optional n)
  358. "Sort list entries by the column at point.
  359. With a numeric prefix argument N, sort the Nth column.
  360. Same as `tabulated-list-sort', but also restore marks after sorting."
  361. (interactive "P")
  362. (tabulated-list-sort n)
  363. (bui-list-restore-marks))
  364. ;;; Major mode
  365. (defvar bui-list-mode-map
  366. (let ((map (make-sparse-keymap)))
  367. (set-keymap-parent
  368. map (make-composed-keymap bui-map
  369. tabulated-list-mode-map))
  370. (define-key map (kbd "RET") 'bui-list-describe)
  371. (define-key map (kbd "i") 'bui-list-describe)
  372. (define-key map (kbd "m") 'bui-list-mark)
  373. (define-key map (kbd "*") 'bui-list-mark)
  374. (define-key map (kbd "M") 'bui-list-mark-all)
  375. (define-key map (kbd "u") 'bui-list-unmark)
  376. (define-key map (kbd "DEL") 'bui-list-unmark-backward)
  377. (define-key map (kbd "U") 'bui-list-unmark-all)
  378. (define-key map (kbd "s") 'bui-list-sort)
  379. (define-key map [remap tabulated-list-sort] 'bui-list-sort)
  380. map)
  381. "Keymap for `bui-list-mode' buffers.")
  382. (define-derived-mode bui-list-mode tabulated-list-mode "BUI-List"
  383. "Parent mode for displaying data in 'list' form.")
  384. (defun bui-list-initialize (entry-type)
  385. "Set up the current 'list' buffer to display ENTRY-TYPE entries."
  386. (setq tabulated-list-padding 2
  387. tabulated-list-format (bui-list-tabulated-format entry-type)
  388. tabulated-list-sort-key (bui-list-tabulated-sort-key entry-type))
  389. (bui-set-local-variables entry-type 'list
  390. (mapcar #'bui-symbol-specification-suffix
  391. bui-list-symbol-specifications))
  392. (setq-local bui-list-marks (append bui-list-default-marks
  393. bui-list-additional-marks))
  394. (tabulated-list-init-header))
  395. (provide 'bui-list)
  396. ;;; bui-list.el ends here