bui-list.el 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. ;;; bui-list.el --- 'List' buffer interface for displaying data -*- lexical-binding: t -*-
  2. ;; Copyright © 2014–2018 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. (put 'bui-list-format 'permanent-local t)
  40. (defcustom bui-list-sort-key nil
  41. "Default sort key for 'list' buffer.
  42. Should be nil (no sort) or have a form:
  43. (PARAM . FLIP)
  44. PARAM is the name of an entry parameter. For the meaning of
  45. FLIP, see `tabulated-list-sort-key'."
  46. :type '(choice (const :tag "No sort" nil)
  47. (cons symbol boolean))
  48. :group 'bui-list)
  49. (put 'bui-list-sort-key 'permanent-local t)
  50. (defvar bui-list-additional-marks nil
  51. "Alist of additional marks for 'list' buffer.
  52. Marks from this list are used along with `bui-list-default-marks'.")
  53. (put 'bui-list-additional-marks 'permanent-local t)
  54. (defcustom bui-list-show-single nil
  55. "If non-nil, list an entry even if it is the only matching result.
  56. If nil, show a single entry in the 'info' buffer instead."
  57. :type 'boolean
  58. :group 'bui-list)
  59. (put 'bui-list-show-single 'permanent-local t)
  60. (defcustom bui-list-describe-warning-count 10
  61. "The maximum number of entries to describe without a warning.
  62. If you want to describe more than this number of marked entries,
  63. you will be prompted for confirmation. See also
  64. `bui-list-describe'."
  65. :type 'integer
  66. :group 'bui-list)
  67. (put 'bui-list-describe-warning-count 'permanent-local t)
  68. (defvar bui-list-describe-function nil
  69. "Function used by `bui-list-describe'.
  70. It is applied to the entries IDs as the rest arguments.
  71. If nil, 'describing' is not performed (it usually means that
  72. 'info' interface is not defined).")
  73. (put 'bui-list-describe-function 'permanent-local t)
  74. (defconst bui-list-symbol-specifications
  75. '((:describe-function describe-function t)
  76. (:describe-count describe-warning-count t)
  77. (:format format t)
  78. (:list-single? show-single t)
  79. (:marks additional-marks)
  80. (:sort-key sort-key t))
  81. "Specifications for generating 'list' variables.
  82. See `bui-symbol-specifications' for details.")
  83. ;;; Displaying 'info' buffer
  84. (defun bui-list-describe (&rest mark-names)
  85. "Describe entries marked with MARK-NAMES.
  86. 'Describe' means display entries in 'info' buffer.
  87. If no entries are marked, describe the current entry.
  88. Available MARK-NAMES are symbols from `bui-list-marks'.
  89. Interactively, describe entries marked with a general mark. With
  90. prefix argument, describe entries marked with any mark."
  91. (interactive (unless current-prefix-arg '(general)))
  92. (or bui-list-describe-function
  93. (error "Can't display 'info' buffer: '%S' is unset"
  94. (bui-list-symbol (bui-current-entry-type)
  95. 'describe-function)))
  96. (let* ((ids (or (apply #'bui-list-get-marked-id-list mark-names)
  97. (list (bui-list-current-id))))
  98. (count (length ids)))
  99. (when (or (<= count bui-list-describe-warning-count)
  100. (y-or-n-p (format "Do you really want to describe %d entries? "
  101. count)))
  102. (apply bui-list-describe-function ids))))
  103. ;;; Wrappers for 'list' variables
  104. (defun bui-list-symbol (entry-type symbol)
  105. "Return symbol for ENTRY-TYPE and 'list' buffer type."
  106. (bui-symbol entry-type 'list symbol))
  107. (defun bui-list-symbol-value (entry-type symbol)
  108. "Return SYMBOL's value for ENTRY-TYPE and 'list' buffer type."
  109. (bui-symbol-value entry-type 'list symbol))
  110. (defun bui-list-param-title (entry-type param)
  111. "Return column title of an ENTRY-TYPE parameter PARAM."
  112. (bui-param-title entry-type 'list param))
  113. (defun bui-list-format (entry-type)
  114. "Return column format for ENTRY-TYPE."
  115. (bui-list-symbol-value entry-type 'format))
  116. (defun bui-list-displayed-params (entry-type)
  117. "Return a list of ENTRY-TYPE parameters that should be displayed."
  118. (mapcar #'car (bui-list-format entry-type)))
  119. (defun bui-list-show-single-entry? (entry-type)
  120. "Return non-nil, if a single entry of ENTRY-TYPE should be listed."
  121. (or (bui-list-symbol-value entry-type 'show-single)
  122. bui-list-show-single))
  123. ;;; Tabulated list internals
  124. (defun bui-list-sort-numerically (column a b)
  125. "Compare COLUMN of tabulated entries A and B numerically.
  126. This function is used for sort predicates for `tabulated-list-format'.
  127. Return non-nil, if B is bigger than A."
  128. (cl-flet ((num (entry)
  129. (string-to-number (aref (cadr entry) column))))
  130. (> (num b) (num a))))
  131. (defmacro bui-list-define-numerical-sorter (column)
  132. "Define numerical sort predicate for COLUMN.
  133. See `bui-list-sort-numerically' for details."
  134. (let ((name (intern (format "bui-list-sort-numerically-%d" column)))
  135. (doc (format "\
  136. Predicate to sort tabulated list by column %d numerically.
  137. See `bui-list-sort-numerically' for details."
  138. column)))
  139. `(defun ,name (a b)
  140. ,doc
  141. (bui-list-sort-numerically ,column a b))))
  142. (defmacro bui-list-define-numerical-sorters (n)
  143. "Define numerical sort predicates for columns from 0 to N.
  144. See `bui-list-define-numerical-sorter' for details."
  145. `(progn
  146. ,@(mapcar (lambda (i)
  147. `(bui-list-define-numerical-sorter ,i))
  148. (number-sequence 0 n))))
  149. (bui-list-define-numerical-sorters 9)
  150. (defun bui-list-tabulated-sort-key ()
  151. "Return sort key for `tabulated-list-sort-key'."
  152. (and bui-list-sort-key
  153. (cons (bui-current-param-title (car bui-list-sort-key))
  154. (cdr bui-list-sort-key))))
  155. (defun bui-list-tabulated-vector (fun)
  156. "Call FUN on each column specification.
  157. FUN is applied to column specification as arguments (see
  158. `bui-list-format').
  159. Return a vector made of values of FUN calls."
  160. (apply #'vector
  161. (mapcar (lambda (col-spec)
  162. (apply fun col-spec))
  163. bui-list-format)))
  164. (defun bui-list-tabulated-format ()
  165. "Return list specification for `tabulated-list-format'."
  166. (bui-list-tabulated-vector
  167. (lambda (param _ &rest rest-spec)
  168. (cons (bui-current-param-title 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. (lambda (param fun &rest _)
  181. (let ((value (bui-entry-value entry param)))
  182. (cond
  183. ;; If function is specified, then it should probably return
  184. ;; something, even if VALUE is void, so give it the precedence.
  185. (fun (funcall fun (bui-entry-non-void-value entry param) entry))
  186. ((bui-void-value? value) bui-empty-string)
  187. ((and (null value)
  188. (bui-boolean-param? entry-type 'list param))
  189. bui-false-string)
  190. (t (bui-get-string value)))))))
  191. ;;; Displaying entries
  192. (defun bui-list-get-display-entries (entry-type &rest args)
  193. "Search for entries and show them in a 'list' buffer preferably."
  194. (let ((entries (bui-get-entries entry-type 'list args)))
  195. (if (or (null entries) ; = 0
  196. (cdr entries) ; > 1
  197. (bui-list-show-single-entry? entry-type)
  198. (not (bui-interface-defined? entry-type 'info)))
  199. (bui-display-entries entries entry-type 'list args)
  200. (if (equal (bui-symbol-value entry-type 'info 'get-entries-function)
  201. (bui-symbol-value entry-type 'list 'get-entries-function))
  202. (bui-display-entries entries entry-type 'info args)
  203. (bui-get-display-entries entry-type 'info args)))))
  204. (defun bui-list-insert-entries (entries entry-type)
  205. "Print ENTRY-TYPE ENTRIES in the current buffer."
  206. (setq tabulated-list-entries
  207. (bui-list-tabulated-entries entries entry-type))
  208. (tabulated-list-print))
  209. (defun bui-list-get-one-line (value &optional _)
  210. "Return one-line string from a multi-line string VALUE.
  211. VALUE may be nil."
  212. (bui-get-non-nil value
  213. (bui-get-one-line value)))
  214. (defun bui-list-get-time (time &optional _)
  215. "Return formatted time string from TIME.
  216. TIME may be nil or another value supported by `bui-get-time-string'."
  217. (bui-get-non-nil time
  218. (bui-get-string (bui-get-time-string time)
  219. 'bui-time)))
  220. (defun bui-list-get-file-name (file-name &optional _)
  221. "Return FILE-NAME button specification for `tabulated-list-entries'.
  222. FILE-NAME may be nil."
  223. (bui-get-non-nil file-name
  224. (list file-name
  225. :type 'bui-file
  226. 'file file-name)))
  227. (defun bui-list-get-url (url &optional _)
  228. "Return URL button specification for `tabulated-list-entries'.
  229. URL may be nil."
  230. (bui-get-non-nil url
  231. (list url
  232. :type 'bui-url
  233. 'url url)))
  234. ;;; 'List' lines
  235. (defun bui-list-current-id ()
  236. "Return ID of the entry at point."
  237. (or (tabulated-list-get-id)
  238. (user-error "No entry here")))
  239. (defun bui-list-current-entry ()
  240. "Return entry at point."
  241. (bui-entry-by-id (bui-current-entries)
  242. (bui-list-current-id)))
  243. (defun bui-list-for-each-line (fun &rest args)
  244. "Call FUN with ARGS for each entry line."
  245. (or (derived-mode-p 'bui-list-mode)
  246. (error "The current buffer is not in `bui-list-mode'"))
  247. (save-excursion
  248. (goto-char (point-min))
  249. (while (not (eobp))
  250. (apply fun args)
  251. (forward-line))))
  252. (defun bui-list-fold-lines (fun init)
  253. "Fold over entry lines in the current list buffer.
  254. Call FUN with RESULT as argument for each line, using INIT as
  255. the initial value of RESULT. Return the final result."
  256. (let ((res init))
  257. (bui-list-for-each-line
  258. (lambda () (setq res (funcall fun res))))
  259. res))
  260. ;;; Marking and sorting
  261. (defvar-local bui-list-marked nil
  262. "List of the marked entries.
  263. Each element of the list has a form:
  264. (ID MARK-NAME . ARGS)
  265. ID is an entry ID.
  266. MARK-NAME is a symbol from `bui-list-marks'.
  267. ARGS is a list of additional values.")
  268. (defvar-local bui-list-marks nil
  269. "Alist of available mark names and mark characters.")
  270. (defvar bui-list-default-marks
  271. '((empty . ?\s)
  272. (general . ?*))
  273. "Alist of default mark names and mark characters.")
  274. (defun bui-list-get-mark (name)
  275. "Return mark character by its NAME."
  276. (or (bui-assq-value bui-list-marks name)
  277. (error "Mark '%S' not found" name)))
  278. (defun bui-list-get-mark-string (name)
  279. "Return mark string by its NAME."
  280. (string (bui-list-get-mark name)))
  281. (defun bui-list-current-mark ()
  282. "Return mark character of the current line."
  283. (char-after (line-beginning-position)))
  284. (defun bui-list-current-mark-name ()
  285. "Return name of the mark on the current line."
  286. (or (car (bui-assq-value bui-list-marked (bui-list-current-id)))
  287. 'empty))
  288. (defun bui-list-get-marked (&rest mark-names)
  289. "Return list of specs of entries marked with any mark from MARK-NAMES.
  290. Entry specs are elements from `bui-list-marked' list.
  291. If MARK-NAMES are not specified, use all marks from
  292. `bui-list-marks' except the `empty' one."
  293. (or mark-names
  294. (setq mark-names
  295. (delq 'empty (mapcar #'car bui-list-marks))))
  296. (-filter (-lambda ((_id name . _))
  297. (memq name mark-names))
  298. bui-list-marked))
  299. (defun bui-list-get-marked-args (mark-name)
  300. "Return list of (ID . ARGS) elements from lines marked with MARK-NAME.
  301. See `bui-list-marked' for the meaning of ARGS."
  302. (mapcar (-lambda ((id _name . args))
  303. (cons id args))
  304. (bui-list-get-marked mark-name)))
  305. (defun bui-list-get-marked-id-list (&rest mark-names)
  306. "Return list of IDs of entries marked with any mark from MARK-NAMES.
  307. See `bui-list-get-marked' for details."
  308. (mapcar #'car (apply #'bui-list-get-marked mark-names)))
  309. (defun bui-list-marked-or-current (&rest mark-names)
  310. "Return a list of IDs of the marked entries.
  311. If nothing is marked, return a list with ID of the current entry.
  312. See `bui-list-get-marked' for the meaning of MARK-NAMES."
  313. (or (apply #'bui-list-get-marked-id-list mark-names)
  314. (list (bui-list-current-id))))
  315. (defun bui-list-map-marked (function &rest mark-names)
  316. "Apply FUNCTION to each element of the marked entries.
  317. If nothing is marked, call FUNCTION on the current entry.
  318. See `bui-list-get-marked' for the meaning of MARK-NAMES."
  319. (mapcar function
  320. (apply #'bui-list-marked-or-current mark-names)))
  321. (defun bui-list--mark (mark-name &optional advance &rest args)
  322. "Put a mark on the current line.
  323. Also add the current entry to `bui-list-marked' using its ID and ARGS.
  324. MARK-NAME is a symbol from `bui-list-marks'.
  325. If ADVANCE is non-nil, move forward by one line after marking."
  326. (let ((id (bui-list-current-id)))
  327. (if (eq mark-name 'empty)
  328. (setq bui-list-marked (assq-delete-all id bui-list-marked))
  329. (let ((assoc (assq id bui-list-marked))
  330. (val (cons mark-name args)))
  331. (if assoc
  332. (setcdr assoc val)
  333. (push (cons id val) bui-list-marked)))))
  334. (tabulated-list-put-tag (bui-list-get-mark-string mark-name)
  335. advance))
  336. (defun bui-list-mark (&optional arg)
  337. "Mark the current line and move to the next line.
  338. With ARG, mark all lines."
  339. (interactive "P")
  340. (if arg
  341. (bui-list-mark-all)
  342. (bui-list--mark 'general t)))
  343. (defun bui-list-mark-all (&optional mark-name)
  344. "Mark all lines with MARK-NAME mark.
  345. MARK-NAME is a symbol from `bui-list-marks'.
  346. Interactively, put a general mark on all lines."
  347. (interactive)
  348. (or mark-name (setq mark-name 'general))
  349. (setq bui-list-marked
  350. (if (eq mark-name 'empty)
  351. nil
  352. (mapcar (lambda (entry)
  353. (list (bui-entry-id entry) mark-name))
  354. (bui-current-entries))))
  355. (bui-list-for-each-line #'tabulated-list-put-tag
  356. (bui-list-get-mark-string mark-name)))
  357. (defun bui-list-unmark (&optional arg)
  358. "Unmark the current line and move to the next line.
  359. With ARG, unmark all lines."
  360. (interactive "P")
  361. (if arg
  362. (bui-list-unmark-all)
  363. (bui-list--mark 'empty t)))
  364. (defun bui-list-unmark-backward ()
  365. "Move up one line and unmark it."
  366. (interactive)
  367. (forward-line -1)
  368. (bui-list--mark 'empty))
  369. (defun bui-list-unmark-all ()
  370. "Unmark all lines."
  371. (interactive)
  372. (bui-list-mark-all 'empty))
  373. (defun bui-list-restore-marks ()
  374. "Put marks according to `bui-list-marked'."
  375. (bui-list-for-each-line
  376. (lambda ()
  377. (let ((mark-name (bui-list-current-mark-name)))
  378. (unless (eq mark-name 'empty)
  379. (tabulated-list-put-tag
  380. (bui-list-get-mark-string mark-name)))))))
  381. (defun bui-list-sort (&optional n)
  382. "Sort list entries by the column at point.
  383. With a numeric prefix argument N, sort the Nth column.
  384. Same as `tabulated-list-sort', but also restore marks after sorting."
  385. (interactive "P")
  386. (tabulated-list-sort n)
  387. (bui-list-restore-marks))
  388. ;;; Major mode
  389. (defvar bui-list-mode-map
  390. (let ((map (make-sparse-keymap)))
  391. (set-keymap-parent
  392. map (make-composed-keymap bui-map
  393. tabulated-list-mode-map))
  394. (define-key map (kbd "i") 'bui-list-describe)
  395. (define-key map (kbd "RET") 'bui-list-describe)
  396. (define-key map (kbd "*") 'bui-list-mark)
  397. (define-key map (kbd "m") 'bui-list-mark)
  398. (define-key map (kbd "M") 'bui-list-mark-all)
  399. (define-key map (kbd "u") 'bui-list-unmark)
  400. (define-key map (kbd "DEL") 'bui-list-unmark-backward)
  401. (define-key map (kbd "U") 'bui-list-unmark-all)
  402. (define-key map (kbd "s") 'bui-list-sort)
  403. (define-key map [remap tabulated-list-sort] 'bui-list-sort)
  404. map)
  405. "Keymap for `bui-list-mode' buffers.")
  406. (defvar bui-list-mark-hint
  407. '(("\\[bui-list-mark]") " mark; "
  408. ("\\[bui-list-unmark]") " unmark; "
  409. ("\\[bui-list-unmark-backward]") " unmark backward;\n")
  410. "Hint with 'mark' keys for 'list' buffer.
  411. See `bui-hint' for details.")
  412. (defvar bui-list-sort-hint
  413. '(("\\[bui-list-sort]") " sort by column;\n")
  414. "Hint with 'sort' keys for 'list' buffer.
  415. See `bui-hint' for details.")
  416. (defvar bui-list-info-hint
  417. '(("\\[bui-list-describe]") " show 'info' buffer;\n")
  418. "Hint for 'list' buffer used only when 'info' interface is defined.
  419. See `bui-hint' for details.")
  420. (defun bui-list-hint ()
  421. "Return hint structure for the current 'list' buffer."
  422. (bui-format-hints
  423. bui-list-mark-hint
  424. (and (bui-interface-defined? (bui-current-entry-type) 'info)
  425. bui-list-info-hint)
  426. bui-list-sort-hint))
  427. (define-derived-mode bui-list-mode tabulated-list-mode "BUI-List"
  428. "Parent mode for displaying data in 'list' form."
  429. (bui-list-initialize))
  430. (defun bui-list-initialize ()
  431. "Set up the current 'list' buffer."
  432. (setq tabulated-list-padding 2
  433. tabulated-list-format (bui-list-tabulated-format)
  434. tabulated-list-sort-key (bui-list-tabulated-sort-key))
  435. (setq-local bui-list-marks (append bui-list-default-marks
  436. bui-list-additional-marks))
  437. (tabulated-list-init-header))
  438. (provide 'bui-list)
  439. ;;; bui-list.el ends here