|
| 1 | +;;; composer-list.el --- Interface for viewing and manipulating composer packages -*- lexical-binding: t -*- |
| 2 | + |
| 3 | +;; Copyright (C) 2024 Friends of Emacs-PHP development |
| 4 | + |
| 5 | +;; Author: USAMI Kenta <[email protected]> |
| 6 | +;; Created: 15 June 2024 |
| 7 | +;; Version: 0.2.0 |
| 8 | +;; Keywords: tools php dependency manager |
| 9 | +;; Homepage: https://github.com/zonuexe/composer.el |
| 10 | +;; Package-Requires: ((emacs "25.1") (seq "1.9") (php-runtime "0.1.0")) |
| 11 | +;; License: GPL-3.0-or-later |
| 12 | + |
| 13 | +;; This file is NOT part of GNU Emacs. |
| 14 | + |
| 15 | +;; This program is free software; you can redistribute it and/or modify |
| 16 | +;; it under the terms of the GNU General Public License as published by |
| 17 | +;; the Free Software Foundation, either version 3 of the License, or |
| 18 | +;; (at your option) any later version. |
| 19 | + |
| 20 | +;; This program is distributed in the hope that it will be useful, |
| 21 | +;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 22 | +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 23 | +;; GNU General Public License for more details. |
| 24 | + |
| 25 | +;; You should have received a copy of the GNU General Public License |
| 26 | +;; along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 27 | + |
| 28 | +;;; Commentary: |
| 29 | + |
| 30 | +;; Interface for viewing and manipulating composer packages. |
| 31 | + |
| 32 | +;;; Code: |
| 33 | +(require 'tabulated-list) |
| 34 | +(require 'composer) |
| 35 | +(require 'php-project nil t) |
| 36 | +(require 'package) |
| 37 | + |
| 38 | +(defgroup composer-list nil |
| 39 | + "Interface to PHP Composer." |
| 40 | + :group 'composer-list |
| 41 | + :tag "Composer List" |
| 42 | + :prefix "composer-list-") |
| 43 | + |
| 44 | +(defcustom composer-list-name-column-width 30 |
| 45 | + "Column width for the Package name in the composer list." |
| 46 | + :type 'natnum |
| 47 | + :group 'composer-list) |
| 48 | + |
| 49 | +(defcustom composer-list-version-column-width 14 |
| 50 | + "Column width for the Package version in the composer list." |
| 51 | + :type 'natnum |
| 52 | + :group 'composer-list) |
| 53 | + |
| 54 | +(defcustom composer-list-status-column-width 12 |
| 55 | + "Column width for the Package status in the composer list." |
| 56 | + :type 'natnum |
| 57 | + :group 'composer-list) |
| 58 | + |
| 59 | +(define-derived-mode composer-list-mode tabulated-list-mode "Composer packages" |
| 60 | + "." |
| 61 | + :interactive nil |
| 62 | + (setq-local buffer-stale-function |
| 63 | + (lambda (&optional _noconfirm) 'fast)) |
| 64 | + (setq tabulated-list-format |
| 65 | + `[("Package" ,composer-list-name-column-width t) |
| 66 | + ("Version" ,composer-list-version-column-width t) |
| 67 | + ("Latest" ,composer-list-version-column-width t) |
| 68 | + ("Status" ,composer-list-status-column-width t) |
| 69 | + ("Description" 0 package-menu--description-predicate)]) |
| 70 | + (setq tabulated-list-padding 2) |
| 71 | + (add-hook 'tabulated-list-revert-hook #'composer-list--refresh-packages nil t) |
| 72 | + (tabulated-list-init-header) |
| 73 | + (composer-list--refresh-packages) |
| 74 | + (tabulated-list-print)) |
| 75 | + |
| 76 | +(defun composer-list--load-packages () |
| 77 | + "List `composer' sub commands." |
| 78 | + (let ((output (composer--command-execute "show" "--latest" "--all" "--format=json"))) |
| 79 | + (cdr-safe (assq 'locked (composer--parse-json-string output))))) |
| 80 | + |
| 81 | +(defun composer-list--load-outdated () |
| 82 | + "List `composer' sub commands." |
| 83 | + (let ((output (composer--command-execute "outdated" "--format=json"))) |
| 84 | + (cdr-safe (assq 'installed (composer--parse-json-string output))))) |
| 85 | + |
| 86 | +(defun composer-list--ensure-bool (value) |
| 87 | + "Ensure that the provided VALUE is a boolean. |
| 88 | +If VALUE is :false or nil, return nil. |
| 89 | +If VALUE is :true, return t. |
| 90 | +Otherwise, if VALUE is already a boolean, return it as is." |
| 91 | + (cond |
| 92 | + ((or (eq :false value) (null value)) nil) |
| 93 | + ((eq :true value) t) |
| 94 | + ((booleanp value) value))) |
| 95 | + |
| 96 | +(defun composer-list--ensure-string (value) |
| 97 | + "Ensure that the provided VALUE is a string. |
| 98 | +If VALUE is :null or NIL, return an empty string \"\". |
| 99 | +If VALUE is already a string, return it as is. |
| 100 | +If VALUE is a number, convert it to a string using `number-to-string'." |
| 101 | + (cond |
| 102 | + ((or (eq :null value) (null value)) "") |
| 103 | + ((stringp value) value) |
| 104 | + ((numberp value) (number-to-string value)))) |
| 105 | + |
| 106 | +(defun composer-list--print-info (pkg) |
| 107 | + "Return a PKG package entry suitable for `tabulated-list-entries'." |
| 108 | + (let* ((latest-status (alist-get 'latest-status pkg)) |
| 109 | + (face (pcase latest-status |
| 110 | + ("update-possible" 'error) |
| 111 | + ("semver-safe-update" 'warning) |
| 112 | + ("up-to-date" 'success))) |
| 113 | + (name (alist-get 'name pkg)) |
| 114 | + (version (alist-get 'version pkg)) |
| 115 | + (direct (composer-list--ensure-bool (alist-get 'direct-dependency pkg))) |
| 116 | + (latest (alist-get 'latest pkg)) |
| 117 | + (warning (composer-list--ensure-bool (alist-get 'warning pkg))) |
| 118 | + (abandoned (alist-get 'warning pkg)) |
| 119 | + (status (cond (abandoned "abandoned") |
| 120 | + ((not direct) "dependency") |
| 121 | + ("installed"))) |
| 122 | + (desc (composer-list--ensure-string (alist-get 'description pkg))) |
| 123 | + (entry `[(,name |
| 124 | + face link |
| 125 | + font-lock-face link |
| 126 | + follow-link t |
| 127 | + package-desc ,name |
| 128 | + action composer-list-describe-package) |
| 129 | + ,version |
| 130 | + ,(if face (propertize (or latest "") 'font-lock-face face) latest) |
| 131 | + ,(if warning (propertize status 'font-lock-face 'error) status) |
| 132 | + ,desc])) |
| 133 | + (list name entry))) |
| 134 | + |
| 135 | +(defun composer-list--refresh-packages () |
| 136 | + "Setup for `tabulated-list-format'." |
| 137 | + (let ((packages (composer-list--load-packages))) |
| 138 | + (tabulated-list-init-header) |
| 139 | + (setq tabulated-list-entries (seq-map #'composer-list--print-info packages)))) |
| 140 | + |
| 141 | +(define-derived-mode composer-list-describe-mode text-mode "Composer-pkg" |
| 142 | + "Major mode for viewing PsySH Doc." |
| 143 | + (setq show-trailing-whitespace nil) |
| 144 | + (goto-address-mode +1) |
| 145 | + (read-only-mode +1)) |
| 146 | + |
| 147 | +(defun composer-list-describe-package (package) |
| 148 | + "Display the full information of PACKAGE." |
| 149 | + (interactive (list (or (tabulated-list-get-id) |
| 150 | + (completing-read |
| 151 | + "Composer package: " |
| 152 | + (let ((composer-use-ansi-color nil)) |
| 153 | + (split-string (composer--command-execute "show" "--name-only"))))))) |
| 154 | + (let* ((buf (get-buffer-create "*Composer-pkg*")) |
| 155 | + (composer-use-ansi-color t) |
| 156 | + (command (composer--make-command-string "show" (list package "--ansi")))) |
| 157 | + (with-current-buffer buf |
| 158 | + (composer-list-describe-mode) |
| 159 | + (let ((default-directory (composer--find-composer-root default-directory)) |
| 160 | + (buffer-read-only nil) |
| 161 | + (composer--quote-shell-argument t) |
| 162 | + pos) |
| 163 | + (erase-buffer) |
| 164 | + (insert command "\n\n") |
| 165 | + (setq pos (point)) |
| 166 | + (shell-command command (current-buffer)) |
| 167 | + (ansi-color-apply-on-region pos (point-max)))) |
| 168 | + (pop-to-buffer-same-window buf))) |
| 169 | + |
| 170 | +;;;###autoload |
| 171 | +(defun composer-list-packages (directory) |
| 172 | + "Display a list of packages in DIRECTORY." |
| 173 | + (interactive |
| 174 | + (list (read-directory-name "Composer Directory: " (composer--find-composer-root default-directory)))) |
| 175 | + (let* ((default-directory (composer--find-composer-root directory)) |
| 176 | + (buf (get-buffer-create (format "*Composer: %s*" default-directory)))) |
| 177 | + (with-current-buffer buf |
| 178 | + (composer-list-mode)) |
| 179 | + (pop-to-buffer-same-window buf))) |
| 180 | + |
| 181 | +(provide 'composer-list) |
| 182 | +;;; composer-list.el ends here |
0 commit comments