Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

951 lines
38 KiB

  1. ;;; paradox-menu.el --- defining the Packages menu -*- lexical-binding:t -*-
  2. ;; Copyright (C) 2014-2015 Artur Malabarba <bruce.connor.am@gmail.com>
  3. ;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
  4. ;; Prefix: paradox
  5. ;; Separator: -
  6. ;;; License:
  7. ;;
  8. ;; This file is NOT part of GNU Emacs.
  9. ;;
  10. ;; This program is free software; you can redistribute it and/or
  11. ;; modify it under the terms of the GNU General Public License
  12. ;; as published by the Free Software Foundation; either version 2
  13. ;; of the License, or (at your option) any later version.
  14. ;;
  15. ;; This program is distributed in the hope that it will be useful,
  16. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. ;; GNU General Public License for more details.
  19. ;;
  20. ;;; Code:
  21. (require 'cl-lib)
  22. (require 'cus-edit)
  23. (require 'package)
  24. (require 'subr-x)
  25. (require 'hydra)
  26. (require 'paradox-core)
  27. (require 'paradox-github)
  28. (require 'paradox-commit-list)
  29. (require 'paradox-execute)
  30. (defgroup paradox-menu nil
  31. "Paradox Packages Menu configurations."
  32. :prefix "paradox-"
  33. :package-version '(paradox . "2.0")
  34. :group 'paradox)
  35. ;;; Customization Variables
  36. (defcustom paradox-column-width-package 18
  37. "Width of the \"Package\" column."
  38. :type 'integer
  39. :group 'paradox-menu
  40. :package-version '(paradox . "0.1"))
  41. (defcustom paradox-column-width-version 9
  42. "Width of the \"Version\" column."
  43. :type 'integer
  44. :group 'paradox-menu
  45. :package-version '(paradox . "0.1"))
  46. (defcustom paradox-column-width-status 10
  47. "Width of the \"Status\" column."
  48. :type 'integer
  49. :group 'paradox-menu
  50. :package-version '(paradox . "0.1"))
  51. (defcustom paradox-column-width-star 4
  52. "Width of the \"Star\" column."
  53. :type 'integer
  54. :group 'paradox-menu
  55. :package-version '(paradox . "0.1"))
  56. (defcustom paradox-column-width-download 4
  57. "Width of the \"Download Count\" column."
  58. :type 'integer
  59. :group 'paradox-menu
  60. :package-version '(paradox . "1.1"))
  61. (defcustom paradox-display-star-count t
  62. "If non-nil, adds a \"Star\" column to the Package Menu."
  63. :type 'boolean
  64. :group 'paradox-menu
  65. :package-version '(paradox . "1.1"))
  66. (defcustom paradox-display-download-count nil
  67. "If non-nil, adds a \"Download\" column to the Package Menu."
  68. :type 'boolean
  69. :group 'paradox-menu
  70. :package-version '(paradox . "1.2.3"))
  71. (defface paradox-mode-line-face
  72. '((t :inherit (font-lock-keyword-face mode-line-buffer-id)
  73. :weight normal))
  74. "Face used on mode line statuses."
  75. :group 'paradox)
  76. (defface paradox-name-face
  77. '((t :inherit link))
  78. "Face used on the package's name."
  79. :group 'paradox)
  80. (defface paradox-homepage-button-face
  81. '((t :underline t :inherit font-lock-comment-face))
  82. "Face used on the homepage button."
  83. :group 'paradox)
  84. ;; (defface paradox-version-face
  85. ;; '((t :inherit default))
  86. ;; "Face used on the version column."
  87. ;; :group 'paradox)
  88. (defface paradox-archive-face
  89. '((t :inherit paradox-comment-face))
  90. "Face used on the archive column."
  91. :group 'paradox)
  92. (defface paradox-star-face
  93. '((t :inherit font-lock-string-face))
  94. "Face used on the star column, for packages you haven't starred."
  95. :group 'paradox)
  96. (defface paradox-starred-face
  97. '((t :inherit font-lock-variable-name-face))
  98. "Face used on the star column, for packages you have starred."
  99. :group 'paradox)
  100. (defface paradox-download-face
  101. '((t :inherit font-lock-keyword-face))
  102. "Face used on the Downloads column."
  103. :group 'paradox)
  104. (defface paradox-description-face
  105. '((t :inherit default))
  106. "Face used on the description column.
  107. If `paradox-lines-per-entry' > 1, the face
  108. `paradox-description-face-multiline' is used instead."
  109. :group 'paradox)
  110. (defface paradox-description-face-multiline
  111. '((t :inherit font-lock-doc-face))
  112. "Face used on the description column when `paradox-lines-per-entry' > 1.
  113. If `paradox-lines-per-entry' = 1, the face
  114. `paradox-description-face' is used instead."
  115. :group 'paradox)
  116. (defcustom paradox-status-face-alist
  117. '(("built-in" . font-lock-builtin-face)
  118. ("available" . default)
  119. ("new" . bold)
  120. ("held" . font-lock-constant-face)
  121. ("disabled" . font-lock-warning-face)
  122. ("avail-obso" . font-lock-comment-face)
  123. ("installed" . font-lock-comment-face)
  124. ("dependency" . font-lock-comment-face)
  125. ("incompat" . font-lock-comment-face)
  126. ("deleted" . font-lock-comment-face)
  127. ("unsigned" . font-lock-warning-face))
  128. "List of (\"STATUS\" . FACE) cons cells.
  129. When displaying the package menu, FACE will be used to paint the
  130. Version, Status, and Description columns of each package whose
  131. status is STATUS."
  132. :type '(repeat (cons string face))
  133. :group 'paradox-menu
  134. :package-version '(paradox . "2.0"))
  135. (defcustom paradox-homepage-button-string "h"
  136. "String used to for the link that takes you to a package's homepage."
  137. :type 'string
  138. :group 'paradox-menu
  139. :package-version '(paradox . "0.10"))
  140. (defcustom paradox-use-homepage-buttons t
  141. "If non-nil a button will be added after the name of each package.
  142. This button takes you to the package's homepage."
  143. :type 'boolean
  144. :group 'paradox-menu
  145. :package-version '(paradox . "0.10"))
  146. (defcustom paradox-lines-per-entry 1
  147. "Number of lines used to display each entry in the Package Menu.
  148. 1 Gives you the regular package menu.
  149. 2 Displays the description on a separate line below the entry.
  150. 3+ Adds empty lines separating the entries."
  151. :type 'integer
  152. :group 'paradox-menu
  153. :package-version '(paradox . "0.10"))
  154. ;;; Internal
  155. (defvar-local paradox--current-filter nil)
  156. (defvar paradox--column-name-star
  157. (if (char-displayable-p ?\x2605) "\x2605" "*"))
  158. (defvar paradox--column-name-download
  159. (if (char-displayable-p ?\x2193) "\x2193" "DC"))
  160. (defvar paradox--upgradeable-packages nil)
  161. (defvar paradox--upgradeable-packages-number nil)
  162. (defvar paradox--upgradeable-packages-any? nil)
  163. (defvar paradox--column-index-star nil)
  164. (defvar paradox--column-index-download nil)
  165. (defvar paradox--desc-suffix nil)
  166. (defvar paradox--desc-prefix nil)
  167. (defvar paradox--commit-list-buffer "*Package Commit List*")
  168. ;;; Building the packages buffer.
  169. (defun paradox-refresh-upgradeable-packages ()
  170. "Refresh the list of upgradeable packages."
  171. (interactive)
  172. (setq paradox--upgradeable-packages (package-menu--find-upgrades))
  173. (setq paradox--upgradeable-packages-number
  174. (length paradox--upgradeable-packages))
  175. (setq paradox--upgradeable-packages-any?
  176. (> paradox--upgradeable-packages-number 0)))
  177. (defun paradox--print-info (pkg)
  178. "Return a package entry suitable for `tabulated-list-entries'.
  179. PKG has the form (PKG-DESC . STATUS).
  180. Return (PKG-DESC [STAR NAME VERSION STATUS DOC])."
  181. (let* ((pkg-desc (if (consp pkg) (car pkg) pkg))
  182. (status (if (consp pkg) (cdr pkg) (package-desc-status pkg)))
  183. (face (or (cdr (assoc-string status paradox-status-face-alist))
  184. 'font-lock-warning-face))
  185. (url (paradox--package-homepage pkg-desc))
  186. (name (symbol-name (package-desc-name pkg-desc)))
  187. (name-length (length name))
  188. (counts (paradox--count-print (package-desc-name pkg-desc)))
  189. (button-length (if paradox-use-homepage-buttons (length paradox-homepage-button-string) 0)))
  190. (paradox--incf status)
  191. (let ((cell (assq :stars (package-desc-extras pkg-desc))))
  192. (if cell
  193. (setcdr cell counts)
  194. (push (cons :stars counts) (package-desc-extras pkg-desc))))
  195. (list pkg-desc
  196. `[,(concat
  197. (truncate-string-to-width
  198. (propertize name
  199. 'font-lock-face 'paradox-name-face
  200. 'button t
  201. 'follow-link t
  202. 'help-echo (format "Package: %s" name)
  203. 'package-desc pkg-desc
  204. 'action 'package-menu-describe-package)
  205. (- paradox-column-width-package button-length) 0 nil t)
  206. (when (and paradox-use-homepage-buttons url)
  207. (make-string (max 0 (- paradox-column-width-package name-length button-length)) ?\s))
  208. (when (and paradox-use-homepage-buttons url)
  209. (propertize paradox-homepage-button-string
  210. 'font-lock-face 'paradox-homepage-button-face
  211. 'mouse-face 'custom-button-mouse
  212. 'help-echo (format "Visit %s" url)
  213. 'button t
  214. 'follow-link t
  215. 'keymap '(keymap (mouse-2 . push-button))
  216. 'action #'paradox-menu-visit-homepage)))
  217. ,(propertize (package-version-join
  218. (package-desc-version pkg-desc))
  219. 'font-lock-face face)
  220. ,(propertize status 'font-lock-face face)
  221. ,@(if (cdr package-archives)
  222. (list (propertize (or (package-desc-archive pkg-desc) "")
  223. 'font-lock-face 'paradox-archive-face)))
  224. ,@counts
  225. ,(propertize
  226. (concat (propertize " " 'display paradox--desc-prefix)
  227. (package-desc-summary pkg-desc)
  228. (propertize " " 'display paradox--desc-suffix)) ;└╰
  229. 'font-lock-face
  230. (if (> paradox-lines-per-entry 1)
  231. 'paradox-description-face-multiline
  232. 'paradox-description-face))])))
  233. (defun paradox--count-print (pkg)
  234. "Return counts of PKG as a package-desc list."
  235. (append
  236. (when (and paradox-display-star-count (hash-table-p paradox--star-count))
  237. (list (paradox--package-star-count pkg)))
  238. (when (and paradox-display-download-count (hash-table-p paradox--download-count))
  239. (list (paradox--package-download-count pkg)))))
  240. (defun paradox--package-download-count (pkg)
  241. "Return propertized string with the download count of PKG."
  242. (let ((c (gethash pkg paradox--download-count nil)))
  243. (propertize
  244. (if (numberp c)
  245. (if (> c 999) (format "%sK" (truncate c 1000)) (format "%s" c))
  246. " ")
  247. 'font-lock-face 'paradox-download-face
  248. 'value (or c 0))))
  249. (defun paradox--package-homepage (pkg)
  250. "PKG can be the package-name symbol or a package-desc object."
  251. (let* ((object (if (symbolp pkg) (cadr (assoc pkg package-archive-contents)) pkg))
  252. (name (if (symbolp pkg) pkg (package-desc-name pkg)))
  253. (extras (package-desc-extras object))
  254. (homepage (and (listp extras) (cdr-safe (assoc :url extras)))))
  255. (or homepage
  256. (and (setq extras (gethash name paradox--package-repo-list))
  257. (format "https://github.com/%s" extras)))))
  258. (defun paradox--get-or-return-package (pkg)
  259. "Take a marker or package name PKG and return a package name."
  260. (if (or (markerp pkg) (null pkg))
  261. (if (derived-mode-p 'package-menu-mode)
  262. (package-desc-name (tabulated-list-get-id))
  263. (error "Not in Package Menu"))
  264. pkg))
  265. (defun paradox--incf (status)
  266. "Increment the count for STATUS on `paradox--package-count'.
  267. Also increments the count for \"total\"."
  268. (paradox--inc-count status)
  269. (unless (member status '("obsolete" "avail-obso" "incompat"))
  270. (paradox--inc-count "total")))
  271. (defun paradox--inc-count (string)
  272. "Increment the cdr of (assoc-string STRING paradox--package-count)."
  273. (let ((cons (assoc-string string paradox--package-count)))
  274. (setcdr cons (1+ (cdr cons)))))
  275. (defun paradox--entry-star-count (entry)
  276. "Get the star count of the package in ENTRY."
  277. (paradox--package-star-count
  278. ;; The package symbol should be in the ID field, but that's not mandatory,
  279. (or (ignore-errors (elt (car entry) 1))
  280. ;; So we also try interning the package name.
  281. (intern (car (elt (cadr entry) 0))))))
  282. (defun paradox--handle-failed-download (&rest _)
  283. "Handle the case when Emacs fails to download Github data."
  284. (paradox--update-downloads-in-progress 'paradox--data)
  285. (unless (hash-table-p paradox--download-count)
  286. (setq paradox--download-count (make-hash-table)))
  287. (unless (hash-table-p paradox--package-repo-list)
  288. (setq paradox--package-repo-list (make-hash-table)))
  289. (unless (hash-table-p paradox--star-count)
  290. (setq paradox--star-count (make-hash-table)))
  291. (unless (hash-table-p paradox--wiki-packages)
  292. (setq paradox--wiki-packages (make-hash-table)))
  293. (message "[Paradox] Error downloading Github data"))
  294. (defmacro paradox--with-work-buffer (location file &rest body)
  295. "Run BODY in a buffer containing the contents of FILE at LOCATION.
  296. This is the same as `package--with-work-buffer-async', except it
  297. automatically decides whether to download asynchronously based on
  298. `package-menu-async'."
  299. (declare (indent 2) (debug t))
  300. (require 'package)
  301. (if (fboundp 'package--with-response-buffer)
  302. `(package--with-response-buffer
  303. ,location :file ,file
  304. :async package-menu-async
  305. :error-form (paradox--handle-failed-download)
  306. ,@body
  307. (paradox--update-downloads-in-progress 'paradox--data))
  308. `(package--with-work-buffer ,location ,file ,@body)))
  309. (defun paradox--refresh-remote-data ()
  310. "Download metadata and populate the respective variables."
  311. (interactive)
  312. (when (boundp 'package--downloads-in-progress)
  313. (add-to-list 'package--downloads-in-progress 'paradox--data))
  314. (condition-case-unless-debug nil
  315. (paradox--with-work-buffer paradox--data-url "data-hashtables"
  316. (setq paradox--star-count (read (current-buffer)))
  317. (setq paradox--package-repo-list (read (current-buffer)))
  318. (setq paradox--download-count (read (current-buffer)))
  319. (setq paradox--wiki-packages (read (current-buffer))))
  320. (error (paradox--handle-failed-download))))
  321. (defun paradox--package-star-count (package)
  322. "Get the star count of PACKAGE."
  323. (let ((count (gethash package paradox--star-count nil))
  324. (repo (gethash package paradox--package-repo-list nil)))
  325. (propertize
  326. (format "%s" (or count ""))
  327. 'font-lock-face
  328. (if (and repo (paradox--starred-repo-p repo))
  329. 'paradox-starred-face
  330. 'paradox-star-face))))
  331. (defun paradox--star-predicate (A B)
  332. "Non-nil t if star count of A is larger than B."
  333. (> (string-to-number (elt (cadr A) paradox--column-index-star))
  334. (string-to-number (elt (cadr B) paradox--column-index-star))))
  335. (defun paradox--download-predicate (A B)
  336. "Non-nil t if download count of A is larger than B."
  337. (> (get-text-property 0 'value (elt (cadr A) paradox--column-index-download))
  338. (get-text-property 0 'value (elt (cadr B) paradox--column-index-download))))
  339. (defun paradox--generate-menu (remember-pos packages &optional keywords)
  340. "Populate the Package Menu, without hacking into the header-format.
  341. If REMEMBER-POS is non-nil, keep point on the same entry.
  342. PACKAGES should be t, which means to display all known packages,
  343. or a list of package names (symbols) to display.
  344. With KEYWORDS given, only packages with those keywords are
  345. shown."
  346. (paradox-menu--refresh packages keywords)
  347. (setq paradox--current-filter
  348. (if keywords (mapconcat 'identity keywords ",")
  349. nil))
  350. (let ((idx (paradox--column-index "Package")))
  351. (when (integerp idx)
  352. (setcar (elt tabulated-list-format idx)
  353. (if keywords
  354. (concat "Package[" paradox--current-filter "]")
  355. "Package"))))
  356. (tabulated-list-print remember-pos)
  357. (tabulated-list-init-header)
  358. (paradox--update-mode-line))
  359. (defcustom paradox-hide-wiki-packages nil
  360. "If non-nil, don't display packages from the emacswiki."
  361. :type 'boolean)
  362. (defun paradox--maybe-remove-wiki-packages (pkgs)
  363. "Remove wiki packages from PKGS.
  364. If `paradox-hide-wiki-packages' is nil, just return PKGS."
  365. (if (not paradox-hide-wiki-packages)
  366. pkgs
  367. (remq nil
  368. (mapcar
  369. (lambda (entry)
  370. (let ((name (or (car-safe entry) entry)))
  371. (unless (gethash name paradox--wiki-packages)
  372. name)))
  373. (if (or (not pkgs) (eq t pkgs))
  374. package-archive-contents
  375. pkgs)))))
  376. (defun paradox-menu--refresh (&optional packages keywords)
  377. "Call `package-menu--refresh' retaining current filter.
  378. PACKAGES and KEYWORDS are passed to `package-menu--refresh'. If
  379. KEYWORDS is nil and `paradox--current-filter' is non-nil, it is
  380. used to define keywords."
  381. (mapc (lambda (x) (setf (cdr x) 0)) paradox--package-count)
  382. (let ((paradox--desc-prefix (if (> paradox-lines-per-entry 1) " \n " ""))
  383. (paradox--desc-suffix (make-string (max 0 (- paradox-lines-per-entry 2)) ?\n)))
  384. (cond
  385. ((or packages keywords (not paradox--current-filter))
  386. (package-menu--refresh
  387. (paradox--maybe-remove-wiki-packages packages)
  388. keywords)
  389. (paradox-refresh-upgradeable-packages))
  390. ((string= paradox--current-filter "Upgradable")
  391. (paradox-refresh-upgradeable-packages)
  392. (paradox-filter-upgrades))
  393. ((string= paradox--current-filter "Starred")
  394. (paradox-filter-stars)
  395. (paradox-refresh-upgradeable-packages))
  396. ((string-match "\\`Regexp:\\(.*\\)\\'" paradox--current-filter)
  397. (paradox-filter-regexp (match-string 1 paradox--current-filter))
  398. (paradox-refresh-upgradeable-packages))
  399. (t
  400. (paradox-menu--refresh
  401. packages (split-string paradox--current-filter ","))))))
  402. (defun paradox--column-index (regexp)
  403. "Find the index of the column that matches REGEXP."
  404. (cl-position (format "\\`%s\\'" (regexp-quote regexp)) tabulated-list-format
  405. :test (lambda (x y) (string-match x (or (car-safe y) "")))))
  406. (defun paradox--count-format ()
  407. "List of star/download counts to be used as part of the entry."
  408. (remove
  409. nil
  410. (list
  411. (when paradox-display-star-count
  412. (list paradox--column-name-star paradox-column-width-star
  413. 'paradox--star-predicate :right-align t))
  414. (when paradox-display-download-count
  415. (list paradox--column-name-download paradox-column-width-download
  416. 'paradox--download-predicate :right-align t)))))
  417. (defun paradox--archive-format ()
  418. "List containing archive to be used as part of the entry."
  419. (when (cdr package-archives)
  420. (list (list "Archive"
  421. (apply 'max (mapcar 'length (mapcar 'car package-archives)))
  422. 'package-menu--archive-predicate))))
  423. (add-hook 'paradox-menu-mode-hook 'paradox-refresh-upgradeable-packages)
  424. ;;; Mode Definition
  425. (define-derived-mode paradox-menu-mode tabulated-list-mode "Paradox Menu"
  426. "Major mode for browsing a list of packages.
  427. Letters do not insert themselves; instead, they are commands.
  428. \\<paradox-menu-mode-map>
  429. \\{paradox-menu-mode-map}"
  430. (hl-line-mode 1)
  431. (when (boundp 'package--post-download-archives-hook)
  432. (add-hook 'package--post-download-archives-hook
  433. #'paradox--stop-spinner))
  434. (if (boundp 'package--downloads-in-progress)
  435. (setq mode-line-process
  436. '("" (package--downloads-in-progress
  437. (":Loading "
  438. (paradox--spinner
  439. (:eval (spinner-print paradox--spinner))
  440. (:eval (paradox--start-spinner))))
  441. (paradox--spinner
  442. (":Executing " (:eval (spinner-print paradox--spinner)))))))
  443. (setq mode-line-process
  444. '(paradox--spinner
  445. (":Executing " (:eval (spinner-print paradox--spinner))))))
  446. (paradox--update-mode-line)
  447. (setq tabulated-list-format
  448. `[("Package" ,paradox-column-width-package package-menu--name-predicate)
  449. ("Version" ,paradox-column-width-version nil)
  450. ("Status" ,paradox-column-width-status package-menu--status-predicate)
  451. ,@(paradox--archive-format)
  452. ,@(paradox--count-format)
  453. ("Description" 0 nil)])
  454. (setq paradox--column-index-star
  455. (paradox--column-index paradox--column-name-star))
  456. (setq paradox--column-index-download
  457. (paradox--column-index paradox--column-name-download))
  458. (setq tabulated-list-padding 2)
  459. (setq tabulated-list-sort-key (cons "Status" nil))
  460. (add-hook 'tabulated-list-revert-hook #'paradox-menu--refresh nil t)
  461. (add-hook 'tabulated-list-revert-hook #'paradox-refresh-upgradeable-packages nil t)
  462. ;; (add-hook 'tabulated-list-revert-hook #'paradox--refresh-remote-data nil t)
  463. (add-hook 'tabulated-list-revert-hook #'paradox--update-mode-line 'append t)
  464. (tabulated-list-init-header)
  465. ;; We need package-menu-mode to be our parent, otherwise some
  466. ;; commands throw errors. But we can't actually derive from it,
  467. ;; otherwise its initialization will screw up the header-format. So
  468. ;; we "patch" it like this.
  469. (put 'paradox-menu-mode 'derived-mode-parent 'package-menu-mode)
  470. (run-hooks 'package-menu-mode-hook))
  471. (put 'paradox-menu-mode 'derived-mode-parent 'package-menu-mode)
  472. (defun paradox--define-sort (name &optional key)
  473. "Define sorting by column NAME and bind it to KEY.
  474. Defines a function called paradox-sort-by-NAME."
  475. (let ((symb (intern (format "paradox-sort-by-%s" (downcase name))))
  476. (key (or key (substring name 0 1))))
  477. (eval
  478. `(progn
  479. (defun ,symb
  480. (invert)
  481. ,(format "Sort Package Menu by the %s column." name)
  482. (interactive "P")
  483. (when invert
  484. (setq tabulated-list-sort-key (cons ,name nil)))
  485. (tabulated-list--sort-by-column-name ,name))
  486. (define-key paradox-menu-mode-map ,(concat "S" (upcase key)) ',symb)
  487. (define-key paradox-menu-mode-map ,(concat "S" (downcase key)) ',symb)))))
  488. (paradox--define-sort "Package")
  489. (paradox--define-sort "Status")
  490. (paradox--define-sort paradox--column-name-star "*")
  491. (declare-function paradox-sort-by-package "paradox-menu")
  492. (defalias 'paradox-filter-clear #'package-show-package-list
  493. "Clear current Package filter.
  494. Redisplay the Packages buffer listing all packages, without
  495. fetching the list.")
  496. (defmacro paradox--apply-filter (name packages &optional nil-message)
  497. "Apply filter called NAME (a string) listing only PACKAGES.
  498. PACKAGES should be a list of symbols (the names of packages to
  499. display) or a list of cons cells whose `car's are symbols.
  500. NIL-MESSAGE is the message to show if PACKAGES is nil, and
  501. defaults to: \"No %s packages\"."
  502. (declare (debug t)
  503. (indent 1))
  504. (let* ((n (format "%s" name))
  505. (cn (capitalize n))
  506. (dn (downcase n)))
  507. (macroexp-let2 macroexp-copyable-p pl packages
  508. `(if (null ,pl)
  509. (user-error ,(or nil-message (format "No %s packages." dn)))
  510. (package-show-package-list
  511. (mapcar (lambda (p) (or (car-safe p) p)) ,pl))
  512. (setq paradox--current-filter ,cn)))))
  513. (defun paradox-filter-upgrades ()
  514. "Show only upgradable packages."
  515. (interactive)
  516. (paradox--apply-filter Upgradable
  517. paradox--upgradeable-packages)
  518. (paradox-sort-by-package nil))
  519. (defun paradox-filter-stars ()
  520. "Show only starred packages."
  521. (interactive)
  522. (let ((list))
  523. (maphash (lambda (pkg repo)
  524. (when (paradox--starred-repo-p repo)
  525. (push pkg list)))
  526. paradox--package-repo-list)
  527. (paradox--apply-filter Starred list)))
  528. (defun paradox-filter-regexp (regexp)
  529. "Show only packages matching REGEXP.
  530. Test match against name and summary."
  531. (interactive (list (read-regexp "Enter Regular Expression: ")))
  532. (paradox--apply-filter Regexp
  533. (cl-remove-if-not
  534. (lambda (package)
  535. (or (string-match-p regexp (symbol-name (car package)))
  536. (string-match-p regexp (package-desc-summary (cadr package)))))
  537. package-archive-contents)
  538. "No packages match this regexp.")
  539. (setq paradox--current-filter (concat "Regexp:" regexp)))
  540. (set-keymap-parent paradox-menu-mode-map package-menu-mode-map)
  541. (define-key paradox-menu-mode-map "q" #'paradox-quit-and-close)
  542. (define-key paradox-menu-mode-map "p" #'paradox-previous-entry)
  543. (define-key paradox-menu-mode-map "n" #'paradox-next-entry)
  544. (define-key paradox-menu-mode-map "k" #'paradox-previous-describe)
  545. (define-key paradox-menu-mode-map "j" #'paradox-next-describe)
  546. (define-key paradox-menu-mode-map "s" #'paradox-menu-mark-star-unstar)
  547. (define-key paradox-menu-mode-map "h" #'paradox-menu-quick-help)
  548. (define-key paradox-menu-mode-map "v" #'paradox-menu-visit-homepage)
  549. (define-key paradox-menu-mode-map "w" #'paradox-menu-copy-homepage-as-kill)
  550. (define-key paradox-menu-mode-map "l" #'paradox-menu-view-commit-list)
  551. (define-key paradox-menu-mode-map "x" #'paradox-menu-execute)
  552. (define-key paradox-menu-mode-map "\r" #'paradox-push-button)
  553. (define-key paradox-menu-mode-map "F" 'package-menu-filter)
  554. (if (version< emacs-version "25")
  555. (defhydra hydra-paradox-filter (:color blue :hint nil)
  556. "
  557. Filter by:
  558. _u_pgrades _r_egexp _k_eyword _s_tarred _c_lear
  559. "
  560. ("f" package-menu-filter)
  561. ("k" package-menu-filter)
  562. ("r" paradox-filter-regexp)
  563. ("u" paradox-filter-upgrades)
  564. ("s" paradox-filter-stars)
  565. ("c" paradox-filter-clear)
  566. ("g" paradox-filter-clear)
  567. ("q" nil "cancel" :color blue))
  568. (defhydra hydra-paradox-filter (:color blue :hint nil)
  569. "
  570. Filter by:
  571. _u_pgrades _r_egexp _k_eyword _s_tarred _c_lear
  572. Archive: g_n_u _o_ther
  573. Status: _i_nstalled _a_vailable _d_ependency _b_uilt-in
  574. "
  575. ("f" package-menu-filter)
  576. ("k" package-menu-filter)
  577. ("n" (package-menu-filter "arc:gnu"))
  578. ("o" (package-menu-filter
  579. (remove "arc:gnu"
  580. (mapcar (lambda (e) (concat "arc:" (car e)))
  581. package-archives))))
  582. ("r" paradox-filter-regexp)
  583. ("u" paradox-filter-upgrades)
  584. ("s" paradox-filter-stars)
  585. ("i" (package-menu-filter "status:installed"))
  586. ("a" (package-menu-filter "status:available"))
  587. ("b" (package-menu-filter "status:built-in"))
  588. ("d" (package-menu-filter "status:dependency"))
  589. ("c" paradox-filter-clear)
  590. ("g" paradox-filter-clear)
  591. ("q" nil "cancel" :color blue)))
  592. (define-key paradox-menu-mode-map "f" #'hydra-paradox-filter/body)
  593. ;;; for those who don't want a hydra
  594. (defvar paradox--filter-map)
  595. (define-prefix-command 'paradox--filter-map)
  596. (define-key paradox--filter-map "k" #'package-menu-filter)
  597. (define-key paradox--filter-map "f" #'package-menu-filter)
  598. (define-key paradox--filter-map "r" #'paradox-filter-regexp)
  599. (define-key paradox--filter-map "u" #'paradox-filter-upgrades)
  600. (define-key paradox--filter-map "s" #'paradox-filter-stars)
  601. (define-key paradox--filter-map "c" #'paradox-filter-clear)
  602. (easy-menu-define paradox-menu-mode-menu paradox-menu-mode-map
  603. "Menu for `paradox-menu-mode'."
  604. `("Paradox"
  605. ["Describe Package" package-menu-describe-package :help "Display information about this package"]
  606. ["Help" paradox-menu-quick-help :help "Show short key binding help for package-menu-mode"]
  607. "--"
  608. ["Refresh Package List" package-menu-refresh
  609. :help "Redownload the ELPA archive"
  610. :active (not package--downloads-in-progress)]
  611. ["Execute Marked Actions" paradox-menu-execute :help "Perform all the marked actions"]
  612. ["Mark All Available Upgrades" package-menu-mark-upgrades
  613. :help "Mark packages that have a newer version for upgrading"
  614. :active (not package--downloads-in-progress)]
  615. ("Other Mark Actions"
  616. ["Mark All Obsolete for Deletion" package-menu-mark-obsolete-for-deletion :help "Mark all obsolete packages for deletion"]
  617. ["Mark for Install" package-menu-mark-install :help "Mark a package for installation and move to the next line"]
  618. ["Mark for Deletion" package-menu-mark-delete :help "Mark a package for deletion and move to the next line"]
  619. ["Unmark" package-menu-mark-unmark :help "Clear any marks on a package and move to the next line"])
  620. "--"
  621. ("Github" :visible (stringp paradox-github-token)
  622. ["Star or unstar this package" paradox-menu-mark-star-unstar]
  623. ["Star all installed packages" paradox-star-all-installed-packages]
  624. ["Star packages when installing" (customize-save-variable 'paradox-automatically-star (not paradox-automatically-star))
  625. :help "Automatically star packages that you install (and unstar packages you delete)"
  626. :style toggle :selected paradox-automatically-star])
  627. ["Configure Github Inegration" (paradox--check-github-token) :visible (not paradox-github-token)]
  628. ["View Changelog" paradox-menu-view-commit-list :help "Show a package's commit list on Github"]
  629. ["Visit Homepage" paradox-menu-visit-homepage :help "Visit a package's Homepage on a browser"]
  630. "--"
  631. ("Filter Package List"
  632. ["Clear filter" paradox-filter-clear :help "Go back to unfiltered list"]
  633. ["By Keyword" package-menu-filter :help "Filter by package keyword"]
  634. ["By Upgrades" paradox-filter-upgrades :help "List only upgradeable packages"]
  635. ["By Regexp" paradox-filter-regexp :help "Filter packages matching a regexp"]
  636. ["By Starred" paradox-filter-stars :help "List only packages starred by the user"])
  637. ("Sort Package List"
  638. ["By Package Name" paradox-sort-by-package]
  639. ["By Status (default)" paradox-sort-by-status]
  640. ["By Number of Stars" paradox-sort-by-])
  641. ["Hide by Regexp" package-menu-hide-package :help "Permanently hide all packages matching a regexp"]
  642. ["Display Older Versions" package-menu-toggle-hiding
  643. :style toggle :selected (not package-menu--hide-packages)
  644. :help "Display package even if a newer version is already installed"]
  645. "--"
  646. ["Quit" quit-window :help "Quit package selection"]
  647. ["Customize" (customize-group 'package)]))
  648. ;;; Menu Mode Commands
  649. (defun paradox-previous-entry (&optional n)
  650. "Move to previous entry, which might not be the previous line.
  651. With prefix N, move to the N-th previous entry."
  652. (interactive "p")
  653. (paradox-next-entry (- n))
  654. (forward-line 0)
  655. (forward-button 1))
  656. (defun paradox-next-entry (&optional n)
  657. "Move to next entry, which might not be the next line.
  658. With prefix N, move to the N-th next entry."
  659. (interactive "p")
  660. (dotimes (_ (abs n))
  661. (let ((d (cl-signum n)))
  662. (forward-line (if (> n 0) 1 0))
  663. (if (eobp) (forward-line -1))
  664. (forward-button d))))
  665. (defun paradox-next-describe (&optional n)
  666. "Move to the next package and describe it.
  667. With prefix N, move to the N-th next package instead."
  668. (interactive "p")
  669. (paradox-next-entry n)
  670. (call-interactively 'package-menu-describe-package))
  671. (defun paradox-previous-describe (&optional n)
  672. "Move to the previous package and describe it.
  673. With prefix N, move to the N-th previous package instead."
  674. (interactive "p")
  675. (paradox-previous-entry n)
  676. (call-interactively 'package-menu-describe-package))
  677. (defun paradox-push-button ()
  678. "Push button under point, or describe package."
  679. (interactive)
  680. (if (get-text-property (point) 'action)
  681. (call-interactively 'push-button)
  682. (call-interactively 'package-menu-describe-package)))
  683. (defvar paradox--key-descriptors
  684. '(("next," "previous," "install," "delete," ("execute," . 1) "refresh," "help")
  685. ("star," "visit homepage," "unmark," ("mark Upgrades," . 5) "~delete obsolete")
  686. ("list commits")
  687. ("filter by" "+" "upgrades" "regexp" "keyword" "starred" "clear")
  688. ("Sort by" "+" "Package name" "Status" "*(star)")))
  689. (defun paradox-menu-quick-help ()
  690. "Show short key binding help for `paradox-menu-mode'.
  691. The full list of keys can be viewed with \\[describe-mode]."
  692. (interactive)
  693. (message (mapconcat 'paradox--prettify-key-descriptor
  694. paradox--key-descriptors "\n")))
  695. (defun paradox-quit-and-close (kill)
  696. "Bury this buffer and close the window.
  697. With prefix KILL, kill the buffer instead of burying."
  698. (interactive "P")
  699. (let ((log (get-buffer-window paradox--commit-list-buffer)))
  700. (when (window-live-p log)
  701. (quit-window kill log))
  702. (quit-window kill)))
  703. (defun paradox-menu-visit-homepage (pkg)
  704. "Visit the homepage of package named PKG.
  705. PKG is a symbol. Interactively it is the package under point."
  706. (interactive '(nil))
  707. (let ((url (paradox--package-homepage
  708. (paradox--get-or-return-package pkg))))
  709. (if (stringp url)
  710. (browse-url url)
  711. (message "Package %s has no homepage."
  712. (propertize (symbol-name pkg)
  713. 'face 'font-lock-keyword-face)))))
  714. (defun paradox-menu-copy-homepage-as-kill (pkg)
  715. "Save the homepage of package named PKG as kill.
  716. PKG is a symbol. Interactively it is the package under point."
  717. (interactive '(nil))
  718. (let ((url (paradox--package-homepage
  719. (paradox--get-or-return-package pkg))))
  720. (if (stringp url)
  721. (progn (kill-new url)
  722. (message "copied \"%s\"" url))
  723. (message "Package %s has no homepage."
  724. (propertize (symbol-name pkg)
  725. 'face 'font-lock-keyword-face)))))
  726. (defun paradox-menu-mark-star-unstar ()
  727. "Star or unstar a package and move to the next line."
  728. (interactive)
  729. (paradox--enforce-github-token
  730. (unless paradox--user-starred-repos
  731. (paradox--refresh-user-starred-list))
  732. ;; Get package name
  733. (let* ((pkg (paradox--get-or-return-package nil))
  734. (repo (gethash pkg paradox--package-repo-list))
  735. will-delete)
  736. (unless pkg (error "Couldn't find package-name for this entry"))
  737. ;; (Un)Star repo
  738. (if (not repo)
  739. (message "This package is not a GitHub repo.")
  740. (setq will-delete (paradox--starred-repo-p repo))
  741. (paradox--star-repo repo will-delete)
  742. (cl-incf (gethash pkg paradox--star-count 0)
  743. (if will-delete -1 1))
  744. (tabulated-list-set-col paradox--column-name-star
  745. (paradox--package-star-count pkg)))))
  746. (forward-line 1))
  747. (defun paradox-menu-view-commit-list (pkg)
  748. "Visit the commit list of package named PKG.
  749. PKG is a symbol. Interactively it is the package under point."
  750. (interactive '(nil))
  751. (let* ((name (paradox--get-or-return-package pkg))
  752. (repo (gethash name paradox--package-repo-list)))
  753. (if repo
  754. (with-selected-window
  755. (display-buffer (get-buffer-create paradox--commit-list-buffer))
  756. (paradox-commit-list-mode)
  757. (setq paradox--package-repo repo)
  758. (setq paradox--package-name name)
  759. (setq paradox--package-version
  760. (paradox--get-installed-version name))
  761. (setq paradox--package-tag-commit-alist
  762. (paradox--get-tag-commit-alist repo))
  763. (paradox--commit-list-update-entries)
  764. (tabulated-list-print))
  765. (message "Package %s is not a GitHub repo." pkg))))
  766. ;;; Mode-line Construction
  767. (defcustom paradox-local-variables
  768. '(mode-line-mule-info
  769. mode-line-client
  770. mode-line-remote mode-line-position
  771. column-number-mode size-indication-mode)
  772. "Variables which will take special values on the Packages buffer.
  773. This is a list, where each element is either SYMBOL or (SYMBOL . VALUE).
  774. Each SYMBOL (if it is bound) will be locally set to VALUE (or
  775. nil) on the Packages buffer."
  776. :type '(repeat (choice symbol (cons symbol sexp)))
  777. :group 'paradox-menu
  778. :package-version '(paradox . "0.1"))
  779. (defcustom paradox-display-buffer-name nil
  780. "If nil, *Packages* buffer name won't be displayed in the mode-line."
  781. :type 'boolean
  782. :group 'paradox-menu
  783. :package-version '(paradox . "0.2"))
  784. (defun paradox--build-buffer-id (st n)
  785. "Return a list that propertizes ST and N for the mode-line."
  786. `((:propertize ,st
  787. face paradox-mode-line-face)
  788. (:propertize ,(int-to-string n)
  789. face mode-line-buffer-id)))
  790. (defun paradox--update-mode-line ()
  791. "Update `mode-line-format'."
  792. (mapc #'paradox--set-local-value paradox-local-variables)
  793. (let ((total-lines (int-to-string (length tabulated-list-entries))))
  794. (paradox--update-mode-line-front-space total-lines)
  795. (paradox--update-mode-line-buffer-identification total-lines)))
  796. (defun paradox--update-mode-line-buffer-identification (_total-lines)
  797. "Update `mode-line-buffer-identification'.
  798. TOTAL-LINES is currently unused."
  799. (require 'spinner)
  800. (setq mode-line-buffer-identification
  801. `((paradox-display-buffer-name
  802. ,(propertized-buffer-identification
  803. (format "%%%sb" (length (buffer-name)))))
  804. (paradox--current-filter (:propertize ("[" paradox--current-filter "]") face paradox-mode-line-face))
  805. (paradox--upgradeable-packages-any?
  806. (:eval (paradox--build-buffer-id " Upgrade:" paradox--upgradeable-packages-number)))
  807. (package-menu--new-package-list
  808. (:eval (paradox--build-buffer-id " New:" (paradox--cas "new"))))
  809. ,(paradox--build-buffer-id " Installed:" (+ (paradox--cas "installed")
  810. (paradox--cas "dependency")
  811. (paradox--cas "unsigned")))
  812. (paradox--current-filter
  813. "" ,(paradox--build-buffer-id " Total:" (length package-archive-contents))))))
  814. (defvar sml/col-number)
  815. (defvar sml/numbers-separator)
  816. (defvar sml/col-number-format)
  817. (defvar sml/line-number-format)
  818. (defvar sml/position-construct)
  819. (declare-function sml/compile-position-construct "sml")
  820. (defvar sml/post-id-separator)
  821. (defun paradox--update-mode-line-front-space (total-lines)
  822. "Update `mode-line-front-space'.
  823. TOTAL-LINES is the number of lines in the buffer."
  824. (if (memq 'sml/post-id-separator mode-line-format)
  825. (progn
  826. (add-to-list (make-local-variable 'mode-line-front-space)
  827. (propertize " (" 'face 'sml/col-number))
  828. (setq column-number-mode line-number-mode)
  829. (set (make-local-variable 'sml/numbers-separator) "")
  830. (set (make-local-variable 'sml/col-number-format)
  831. (format "/%s)" total-lines))
  832. (set (make-local-variable 'sml/line-number-format)
  833. (format "%%%sl" (length total-lines)))
  834. (make-local-variable 'sml/position-construct)
  835. (sml/compile-position-construct))
  836. (set (make-local-variable 'mode-line-front-space)
  837. `(line-number-mode
  838. ("(" (:propertize ,(format "%%%sl" (length total-lines)) face mode-line-buffer-id) "/"
  839. ,total-lines ")")))
  840. (set (make-local-variable 'mode-line-modified) nil)))
  841. (defun paradox--set-local-value (x)
  842. "Locally set value of (car X) to (cdr X)."
  843. (let ((sym (or (car-safe x) x)))
  844. (when (boundp sym)
  845. (set (make-local-variable sym) (cdr-safe x)))))
  846. (defun paradox--prettify-key-descriptor (desc)
  847. "Prettify DESC to be displayed as a help menu."
  848. (if (listp desc)
  849. (if (listp (cdr desc))
  850. (mapconcat 'paradox--prettify-key-descriptor desc " ")
  851. (let ((place (cdr desc))
  852. (out (car desc)))
  853. (setq out (propertize out 'face 'paradox-comment-face))
  854. (add-text-properties place (1+ place) '(face paradox-highlight-face) out)
  855. out))
  856. (paradox--prettify-key-descriptor (cons desc 0))))
  857. (provide 'paradox-menu)
  858. ;;; paradox-menu.el ends here