;;; helm-system-packages.el --- Helm UI wrapper for system package managers. -*- lexical-binding: t -*-

;; Copyright (C) 2017 ~ 2020 Pierre Neidhardt <mail@ambrevar.xyz>
;;               2020 ~ 2026 Thierry Volpiatto <thievol@posteo.net>

;; Author: Pierre Neidhardt <mail@ambrevar.xyz>
;; URL: https://github.com/emacs-helm/helm-system-packages
;; Package-Version: 20260411.501
;; Package-Revision: ca29a21bb917
;; Package-Requires: ((emacs "24.4") (helm "2.8.7") (seq "1.8"))
;; Keywords: helm, packages

;; 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 <http://www.gnu.org/licenses/>.

;;; Commentary:
;; Helm UI wrapper for system package managers.

;;; Code:
;;
;; Since package lists can be memory consuming, we use
;; `helm-build-in-buffer-source' to make Helm search faster.
;;
;; The core of the mechanism evolves around the two buffer caches:
;; - the "name" cache with one name per line,
;; - the "description" cache with one "name <whitespace> description" per line.
;;
;; `helm-system-packages-toggle-descriptions' switches from one to the
;; other.
;;
;; The cache is generated by `helm-system-packages-refresh'.
;; How the cache is generated depends on the package manager.  Smart commandline
;; expressions and parsing are crucial to guarantee a swift startup.
;;
;; The Helm transformer filters and fontifies the candidates.
;; `helm-system-packages-refresh' fills "display-lists"
;; with the categories each package belongs to.
;; Categories are really just faces, such as `helm-system-packages-explicit'.
;; No category means the package is not installed.
;;
;; Actions each have their own functions.  helm-system-packages.el provides a
;; lot of helpers.  Key functions include:
;; - `helm-system-packages-categorize' which turns a list of packages (e.g. the
;;   selected candidates) into an alist where keys are categories (without the
;;   "helm-system-packages-" prefix, e.g. "explicit").
;;
;; - `helm-system-packages-mapalist' which allows for chaining function calls
;;   over the aforementioned category alists.

(require 'helm)
(require 'cl-lib)
(require 'ansi-color)

;; Shut up byte compiler
(declare-function eshell-interactive-process "esh-cmd.el")
(declare-function eshell-send-input "esh-mode.el")
(declare-function helm-comp-read "helm-mode.el")
(declare-function org-sort-entries "org.el")
(declare-function outline-show-all "outline.el")

(defvar eshell-buffer-name)
(defvar helm-ff-transformer-show-only-basename)


;; Internal vars
(defvar helm-system-packages-shell-buffer-name "helm-system-packages-eshell")
(defvar helm-system-packages-eshell-buffer
  (concat "*" helm-system-packages-shell-buffer-name "*"))
(make-obsolete-variable 'helm-system-packages-eshell-buffer
                        'helm-system-packages-shell-buffer-name "1.9.0")
(defvar helm-system-packages-buffer "*helm-system-packages-output*")

(defvar helm-system-packages--show-uninstalled-p t)
(defvar helm-system-packages--show-explicit-p t)
(defvar helm-system-packages--show-dependencies-p t)
(defvar helm-system-packages--show-orphans-p t)
(defvar helm-system-packages--show-locals-p t)
(defvar helm-system-packages--show-groups-p t)
(defvar helm-system-packages--show-pinned-p t)

;; Some package managers do not have an executable bearing the same name,
;; hence the pair (EXECUTABLE . PACKAGE-MANAGER).
(defconst helm-system-packages--managers
  '(("emerge" . "portage") ("dnf" . "dnf")
    ("dpkg" . "dpkg") ("pacman" . "pacman")
    ("xbps-query" . "xbps") ("brew" . "brew")
    ;; Keep "guix" last because it can be installed
    ;; beside other package managers and we want to
    ;; give priority to the original package
    ;; manager.
    ("guix" . "guix")))

;; Store previous used manager here.
(defvar helm-system-packages--last-manager nil)

;; TODO: Replace `mapcar' by `mapcan' when possible.

;; TODO: Possible optimization: turn into hash table, notably the display list.
(defvar helm-system-packages--cache nil
  "Cache of all package names and descriptions.
It's an alist indexed by hostnames.
The values are in the form

  (:names STRING-BUFFER
   :descriptions STRING-BUFFER
   :filtered LIST
   :title STRING ...)

:filtered is a list of (package . (faces...))
These are special packages to highlight like installed,
orphans etc...

Optional :title is usually the package manager.")

(defvar helm-system-packages--cache-current nil
  "Current host to use from cache.
If nil, use host linked with `default-directory'.")

(defvar helm-system-packages--virtual-list nil
  "List of virtual packages.
This is only used for dependency display.")

(defvar helm-system-packages--refresh nil
  "Store function in charge to refresh the package list.
It is called:
- on each session start as :init function.
- whenever a shell command completes.")

;; User vars and faces
(defgroup helm-system-packages nil
  "Predefined configurations for `helm-system-packages'."
  :group 'helm)

(defcustom helm-system-packages-column-width 40
  "Column at which descriptions are aligned, excluding a double-space gap."
  :type 'integer)

(defcustom helm-system-packages-show-descriptions-p t
  "Always show descriptions in package list when non-nil."
  :type 'boolean)

(defcustom helm-system-packages-auto-send-commandline-p t
  "When a transaction commandline is inserted into a shell buffer, "
  :type 'boolean)

(defcustom helm-system-packages-candidate-limit 1000
  "Maximum number of candidates to display at once.
0 means display all."
  :type 'integer)

(defcustom helm-system-packages-use-symbol-at-point-p nil
  "Whether to use symbol at point as a default search entry."
  :type 'boolean)

(defcustom helm-system-packages-editable-info-p nil
  "Whether info buffer is editable.
If nil, it is displayed in view-mode, which means \"q\" (default binding) quits
the window."
  :type 'boolean)

(defface helm-system-packages-descriptions
    '((t (:inherit font-lock-warning-face)))
  "Face for package descriptions.")

(defface helm-system-packages-explicit
    '((t (:inherit font-lock-type-face)))
  "Face for explicitly installed packages.")

(defface helm-system-packages-dependencies
    '((t (:inherit font-lock-comment-face :slant italic)))
  "Face for packages installed as dependencies.")

(defface helm-system-packages-orphans
    '((t (:inherit font-lock-string-face :slant italic)))
  "Face for orphan packages (unrequired dependencies).")

(defface helm-system-packages-locals
    '((t (:weight bold)))
  "Face for local packages.")

(defface helm-system-packages-groups
    '((t (:inherit font-lock-doc-face)))
  "Face for package groups.")

(defface helm-system-packages-pinned
    '((t (:weight bold)))
  "Face for pinned packages.")

(defface helm-system-packages-virtual
    '((t (:slant italic)))
  "Face for virtual packages.")

(defface helm-system-packages-residuals
    '((t (:slant italic)))
  "Face for packages with left-over configuration files.")


(defun helm-system-packages-toggle-explicit ()
  (interactive)
  (with-helm-alive-p
    (setq helm-system-packages--show-explicit-p
          (not helm-system-packages--show-explicit-p))
    (helm-update)))
(put 'helm-system-packages-toggle-explicit 'helm-only t)

(defun helm-system-packages-toggle-uninstalled ()
  (interactive)
  (with-helm-alive-p
    (setq helm-system-packages--show-uninstalled-p
          (not helm-system-packages--show-uninstalled-p))
    (helm-update)))
(put 'helm-system-packages-toggle-uninstalled 'helm-only t)

(defun helm-system-packages-toggle-dependencies ()
  (interactive)
  (with-helm-alive-p
    (setq helm-system-packages--show-dependencies-p
          (not helm-system-packages--show-dependencies-p))
    (helm-update)))
(put 'helm-system-packages-toggle-dependencies 'helm-only t)

(defun helm-system-packages-toggle-orphans ()
  (interactive)
  (with-helm-alive-p
    (setq helm-system-packages--show-orphans-p
          (not helm-system-packages--show-orphans-p))
    (helm-update)))
(put 'helm-system-packages-toggle-orphans 'helm-only t)

(defun helm-system-packages-toggle-locals ()
  (interactive)
  (with-helm-alive-p
    (setq helm-system-packages--show-locals-p
          (not helm-system-packages--show-locals-p))
    (helm-update)))
(put 'helm-system-packages-toggle-locals 'helm-only t)

(defun helm-system-packages-toggle-groups ()
  (interactive)
  (with-helm-alive-p
    (setq helm-system-packages--show-groups-p
          (not helm-system-packages--show-groups-p))
    (helm-update)))
(put 'helm-system-packages-toggle-groups 'helm-only t)

(defun helm-system-packages-toggle-pinned ()
  (interactive)
  (with-helm-alive-p
    (setq helm-system-packages--show-pinned-p
          (not helm-system-packages--show-pinned-p))
    (helm-update)))
(put 'helm-system-packages-toggle-pinned 'helm-only t)

(defun helm-system-packages--cache-get ()
  "Get current cache entry.
See `helm-system-packages--cache-current'."
  (let ((host (or helm-system-packages--cache-current
                  (file-remote-p default-directory 'host)
                  "")))
    (cdr (assoc host helm-system-packages--cache))))

(defun helm-system-packages--cache-set (names descriptions display-list
                                        &optional title &rest extra)
  "Set current cache entry.
NAMES and DESCRIPTIONS are strings.
TITLE is a string, usually the name of the package manager.

DISPLAY-LIST is a list of (PACKAGE . (FACES...)).
It associates packages with the list of faces used for display.  A face
corresponds to a category.  A package can belong to multiple
categories (e.g. both \"orphan\" and \"installed\").

EXTRA is an arbitrary prop-val sequence appended to the resulting plist."
  (let ((host (or (file-remote-p default-directory 'host) ""))
        (val (append  (list :names names :descriptions descriptions
                            :filtered display-list :title title)
                      extra)))
    (if (assoc host helm-system-packages--cache)
        (setcdr (assoc host helm-system-packages--cache) val)
      (push (cons host val) helm-system-packages--cache))))

(defun helm-system-packages-refresh (&optional verbose)
  "Refresh package list for current manager.
Current manager is known only after the init function run and the
manager refresh function to use is stored in
`helm-system-packages--refresh'."
  (when helm-system-packages--refresh
    (if verbose
        (let ((time (current-time)))
          (message "Helm-system-packages updating cache...")
          (funcall helm-system-packages--refresh)
          (message "Helm-system-packages updating cache done in %.2f secs"
                   (float-time (time-subtract (current-time) time))))
      (funcall helm-system-packages--refresh))
    (when (and (boundp 'eshell-post-command-hook)
               (memq 'helm-system-packages-refresh
                     eshell-post-command-hook))
      (remove-hook 'eshell-post-command-hook
                   'helm-system-packages-refresh t))))

(defun helm-system-packages--make-init (manager)
  "Return the init function for MANAGER as a lambda.

MANAGER is a symbol like `helm-system-packages-<MANAGER>' which is
build in `helm-system-packages'.
The init function is in charge of caching package lists and creating
Helm candidate buffer."
  (lambda ()
    (let ((val (helm-system-packages--cache-get)))
      (unless val
        (setq helm-system-packages--refresh
              (helm-system-packages-manager-refresh-function manager))
        (helm-system-packages-refresh t)
        (setq val (helm-system-packages--cache-get)))
      ;; TODO: We should only create the buffer if it does not already exist.
      ;; On the other hand, we need to be able to override the package list.
      ;; (unless (helm-candidate-buffer) ...
      (helm-init-candidates-in-buffer
          'global
        (if helm-system-packages-show-descriptions-p
            (plist-get val :descriptions)
          (plist-get val :names))))))

(defun helm-system-packages-mapalist (fun-alist alist)
  "Apply each function of FUN-ALIST to the list with the same key in ALIST.
Return the alist of the results.
Keys must be symbols.

The special key `all' matches all members in ALIST.
Only the first match is applied.
If a member of ALIST does not have a matching function, it is dropped.

To explicitly drop an element, use the `ignore' function.
To explicitly keep an element, use the `identity' function."
  (cl-loop for (key . val) in alist
           for fun = (or (car (alist-get key fun-alist))
                         (car (alist-get 'all fun-alist)))
           for result = (and fun (funcall fun val))
           when result collect (cons key result)))

(defun helm-system-packages-categorize (packages)
  "Return an alist of PACKAGES indexed by category.
PACKAGES must be a list.
Order is preserved within categories.
Categories are infered from the display list: it's the last word of the first
associated symbol.
If not found, category is `uninstalled'."
  (let ((result '()))
    (dolist (p packages result)
      (let* ((display (helm-aand (helm-system-packages--cache-get)
                                 (plist-get it :filtered)
                                 (assoc p it)))
             (category (or (helm-aand (cadr display)
                                      (symbol-name it)
                                      (replace-regexp-in-string
                                       ".*\\W\\(\\w+\\)$" "\\1" it)
                                      (intern it))
                           'uninstalled))
             (cell (assoc category result)))
        (if cell
            (setcdr cell (nconc (cdr cell) (list p)))
          (push (list category p) result))))))

(defun helm-system-packages-toggle-descriptions ()
  "Toggle description column."
  (interactive)
  (with-helm-alive-p
    (setq helm-system-packages-show-descriptions-p
          (not helm-system-packages-show-descriptions-p))
    (helm-force-update)))
(put 'helm-system-packages-toggle-descriptions 'helm-only t)

;; TODO: Possible optimization: turn into macro.
(defun helm-system-packages-extract-name (package)
  "Extract package name from the candidate.
This is useful because the Helm session runs over a buffer
source, so there is only a REAL value which might contain
additional display information such as the package description."
  (if helm-system-packages-show-descriptions-p
      (car (split-string package))
    package))

(defun helm-system-packages-show-information (desc-alist)
  "Show package information contained in DESC-ALIST.
DESC-ALIST's keys are ignored, the values are in the form

    ((package-name . package-desc)...)"
  (cond
   ((not desc-alist)
    (message "No information for package(s) %s"
             (mapconcat 'identity (helm-marked-candidates) " ")))
   ;; TODO: Sort buffer output? Or keep the mark order?
   (helm-current-prefix-arg
    (mapc 'insert (mapcar 'cadr desc-alist)))
   (t (switch-to-buffer helm-system-packages-buffer)
      (set-buffer helm-system-packages-buffer)
      (erase-buffer)
      (dolist (desc (sort
                     (apply 'append (mapcar 'cdr desc-alist))
                     (lambda (a b) (string< (car a) (car b)))))
        (insert "* " (car desc) "\n" (replace-regexp-in-string
                                      "^* " "- " (cdr desc))
                "\n"))
      (goto-char (point-min))
      (org-mode)
      (outline-show-all)
      (unless (or helm-current-prefix-arg helm-system-packages-editable-info-p)
        (read-only-mode 1)
        (local-set-key (kbd "q") 'quit-window)))))

(defun helm-system-packages-call (command &optional args &rest options)
  "COMMAND to run with OPTIONS over the ARGS list.
OPTIONS are insert before ARGS.
Return the result as a string."
  (with-temp-buffer
    ;; We discard errors.
    (apply #'process-file command nil '(t nil) nil (append options args))
    (buffer-string)))

(defun helm-system-packages-run (command &rest args)
  "COMMAND to run over `helm-marked-candidates'."
  (helm-system-packages-call command (helm-marked-candidates) args))

(defun helm-system-packages-print (command &rest args)
  "Print output of COMMAND executed with args on marked candidates.

With prefix argument, print at point, otherwise display in
`helm-system-packages-buffer'."
  (let ((res (apply 'helm-system-packages-run (cons command args))))
    (if (string= res "")
        (message "No result")
      (unless helm-current-prefix-arg
        (switch-to-buffer helm-system-packages-buffer)
        (view-mode 0)
        (erase-buffer)
        (org-mode)
        ;; This is does not work for pacman which needs a specialized function.
        (setq res (replace-regexp-in-string "\\`.*: " "* " res))
        (setq res (replace-regexp-in-string "\n\n.*: " "\n* " res))
        (setq res (ansi-color-apply res)))
      (save-excursion (insert res))
      (unless (or helm-current-prefix-arg helm-system-packages-editable-info-p)
        (view-mode 1)))))
(make-obsolete 'helm-system-packages-print
               'helm-system-packages-show-information "1.9.0")

(defun helm-system-packages-prefix-remote (file)
  (or (file-remote-p file) file))

(defun helm-system-packages-build-file-source (package files)
  "Build Helm file source for PACKAGE with FILES candidates.
PACKAGES is a string and FILES is a list of strings."
  (require 'helm-files)
  (helm-build-sync-source (concat package " files")
    :candidates files
    :display-to-real 'helm-system-packages-prefix-remote
    :candidate-number-limit 'helm-ff-candidate-number-limit
    :persistent-action-if 'helm-find-files-persistent-action-if
    :keymap 'helm-find-files-map
    :action 'helm-find-files-actions))

(defun helm-system-packages-find-files (files)
  "Run a `helm-find-files' over files in FILES.
FILES are either a hash table whose keys are the package names and the
values the list of files, or a single list of files.

In case of a hash table, one Helm source per package will be created."
  (if (= (hash-table-count files) 0)
      (message "No file list for package(s) %s"
               (mapconcat 'identity (helm-marked-candidates) " "))
    (require 'helm-files)
    (if (hash-table-p files)
        (let (sources)
          (maphash
           (lambda (package files)
             (push (helm-system-packages-build-file-source package files)
                   sources))
           files)
          (helm :sources sources
                :buffer "*helm system package files*"))
      (helm :sources
            (helm-system-packages-build-file-source "Package files" files)
            :buffer "*helm system package files*"))))

(defalias 'helm-system-packages-files 'helm-system-packages-find-files)
(make-obsolete 'helm-system-packages-files
               'helm-system-packages-find-files "1.9.0")

(defun helm-system-packages-shell-name ()
  "Return the name of the shell buffer associated with `default-directory'.
The basename is defined by `helm-system-packages-shell-buffer-name'."
  (concat "*"
          helm-system-packages-shell-buffer-name
          (helm-aif (cl-loop for id in '(user host)
                             when (file-remote-p default-directory id)
                             collect it)
              (concat " " (car it) "@" (cadr it)))
          "*"))

(defun helm-system-packages-call-as-root (command args packages)
  "Call COMMAND ARGS PACKAGES as root.
ARGS and PACKAGES must be lists.
COMMAND will be run in the Eshell buffer named by
`helm-system-packages-shell-name'."
  (require 'esh-mode)
  (if (not packages)
      (message "No suitable package selected")
    (let ((arg-list (append args packages))
          (eshell-buffer-name (helm-system-packages-shell-name)))
      ;; Refresh package list after command has completed.
      (eshell)
      (if (eshell-interactive-process)
          (message "A process is already running")
        (push command arg-list)
        (push "sudo" arg-list)
        (add-hook 'eshell-post-command-hook 'helm-system-packages-refresh nil t)
        (goto-char (point-max))
        (insert (mapconcat 'identity arg-list " "))
        (when helm-system-packages-auto-send-commandline-p
          (eshell-send-input))))))

(defun helm-system-packages-run-as-root (command &rest args)
  "COMMAND to run over `helm-marked-candidates'.
COMMAND will be run in the Eshell buffer `helm-system-packages-eshell-buffer'."
  (helm-system-packages-call-as-root command args (helm-marked-candidates)))

(defun helm-system-packages-run-as-root-over-installed (command &rest args)
  "COMMAND to run over installed packages among `helm-marked-candidates'.
COMMAND will be run in the Eshell buffer `helm-system-packages-eshell-buffer'."
  (helm-system-packages-call-as-root
   command args
   (cl-loop for p in (helm-marked-candidates)
            for alist = (plist-get (helm-system-packages--cache-get) :filtered)
            when (assoc p alist) collect p)))

;; TODO: When all entries are filtered out by the transformer, it seems that
;; bindings don't work (e.g. M-N to re-enable uninstalled packages).  Helm bug?
(defun helm-system-packages-show-packages (pkg-alist &optional title)
  "Run a Helm session over the packages in PACKAGE-ALIST.
The key of the alist is ignored and the package lists are considered as one
single list.  This may change in the future.
The value is a string buffer, like the cache.
TITLE is the name of the Helm session."
  (if (not pkg-alist)
      ;; TODO: Do not quit Helm session.
      (message "No dependency list for package(s) %s"
               (mapconcat 'identity (helm-marked-candidates) " "))
    ;; TODO: Possible optimization: split-string + sort + del-dups +
    ;; mapconcat instead of working on buffer.
    (let (desc-res
          (descriptions (plist-get (helm-system-packages--cache-get)
                                   :descriptions))
          (buf (with-temp-buffer
                 (mapc 'insert (mapcar 'cdr pkg-alist))
                 (sort-lines nil (point-min) (point-max))
                 (delete-duplicate-lines (point-min) (point-max))
                 (buffer-string))))
      (dolist (name (split-string buf "\n" t))
        (if (string-match (concat "^" name "  .*$") descriptions)
            (setq desc-res (concat desc-res (match-string 0 descriptions) "\n"))
          (push name helm-system-packages--virtual-list)
          (setq desc-res
                (concat desc-res
                        name
                        (make-string (- helm-system-packages-column-width
                                        (length name))
                                     ? )
                        "  <virtual package>"
                        "\n"))))
      (let ((helm-system-packages--cache-current 'dependencies)
            (ass (assq 'dependencies helm-system-packages--cache))
            (val (list :names buf :descriptions desc-res :title title)))
        (if ass
            (setcdr ass val)
          (push (cons 'dependencies val) helm-system-packages--cache))
        (helm-system-packages)))))

(defun helm-system-packages-browse-url (urls)
  "Browse homepage URLs of `helm-marked-candidates'."
  (if (not urls)
      (message "No result")
    (mapc 'browse-url (helm-comp-read
                       "URL: "
                       (helm-fast-remove-dups urls :test #'equal)
                       :must-match t
                       :exec-when-only-one t
                       :marked-candidates t))))

(defun helm-system-packages-missing-dependencies-p (&rest deps)
  "Return non-nil if some DEPS are missing."
  (let ((missing-deps (cl-loop for p in deps
                               unless (executable-find p t) collect p)))
    (when missing-deps
      (message "Dependencies are missing (%s), please install them"
               (mapconcat 'identity missing-deps ", ")))))

;; WARN: Each time we modify the slots in this structure (add or
;; remove) we have to recompile all other files otherwise we endup
;; with an args-out-of-range error.
(cl-defstruct (helm-system-packages-manager
               (:constructor nil)
               (:copier nil)
               (:constructor helm-system-packages-manager-create))
  "Package manager interface.

DEPENDENCIES is a list of strings of external executables
required by the package manager.

HELP-MESSAGE, KEYMAP, TRANSFORMER, FC-TRANSFORMER and ACTIONS are as
specified by `helm-build-in-buffer-source'.
Use TRANSFORMER for a candidate-transformer and FC-TRANSFORMER for a
filtered-candidate-transformer.
EXTRA-SOURCE allows adding a second source to helm :sources keyword."
  name
  refresh-function
  dependencies
  ;; Helm source parameters follow:
  help-message
  keymap
  transformer
  fc-transformer
  actions
  extra-source)

(defun helm-system-packages-build-source (manager)
  "Build Helm source for MANAGER."
  (let ((title (or (plist-get (helm-system-packages--cache-get) :title)
                   "package manager")))
    (helm-build-in-buffer-source title
      :init (helm-system-packages--make-init manager)
      :candidate-transformer (helm-system-packages-manager-transformer manager)
      :filtered-candidate-transformer
      (helm-system-packages-manager-fc-transformer manager)
      :candidate-number-limit helm-system-packages-candidate-limit
      :display-to-real 'helm-system-packages-extract-name
      :keymap (helm-system-packages-manager-keymap manager)
      :help-message (helm-system-packages-manager-help-message manager)
      :persistent-help "Show package description"
      :action (helm-system-packages-manager-actions manager))))

(defun helm-system-packages-1 (pkg)
  "Call helm on PKG manager.
PKG should handle the variable `helm-system-packages-<manager>'.
PKG contains the needed infos to build helm source see
`helm-system-packages-manager' defstruct."
  (let ((current-manager
         (symbol-value pkg)))
    (unless (apply 'helm-system-packages-missing-dependencies-p
                   (helm-system-packages-manager-dependencies
                    current-manager))
      (helm :sources (list (helm-system-packages-build-source current-manager)
                           (helm-system-packages-manager-extra-source
                            current-manager))
            :buffer (format "*helm %s*" (helm-system-packages-manager-name
                                         current-manager))
            :truncate-lines t
            :input (when helm-system-packages-use-symbol-at-point-p
                     (substring-no-properties
                      (or (thing-at-point 'pkg) "")))))))

;;;###autoload
(defun helm-system-packages (&optional arg)
  "Helm user interface for system packages.
By default choose the package manager dedicated to this system, with one
prefix arg use Guix which is the only one that can be used aside
system manager.  With two prefix args only refresh cache using the
default system manager"
  (interactive "P")
  (let* ((manager (if (and (equal arg '(4)) (executable-find "guix" t)
                           (y-or-n-p "Really use Guix as package manager? "))
                      "guix"
                    (cl-loop for (exe . mng) in helm-system-packages--managers
                             thereis (and (executable-find exe t) mng))))
         (pkg (and manager
                   (intern (concat "helm-system-packages-" manager)))))
    (cl-assert manager nil "No supported package manager was found")
    (when (or arg (and helm-system-packages--last-manager
                       (not (eq helm-system-packages--last-manager
                                pkg))))
      (setq helm-system-packages--cache nil
            helm-system-packages--virtual-list nil
            helm-system-packages--cache-current nil))
    (setq helm-system-packages--last-manager pkg)
    (require pkg)
    (cl-assert (boundp pkg) nil (format "Undefined variable %s" pkg))
    (helm-system-packages-1 pkg)))

(provide 'helm-system-packages)

;; Local Variables:
;; byte-compile-warnings: (not cl-functions obsolete)
;; indent-tabs-mode: nil
;; End:

;;; helm-system-packages.el ends here
