emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/repology fa5dcd3 4/4: Fix compilation issues plus a few


From: Stefan Monnier
Subject: [elpa] externals/repology fa5dcd3 4/4: Fix compilation issues plus a few more changes
Date: Sat, 16 Jan 2021 16:01:14 -0500 (EST)

branch: externals/repology
commit fa5dcd3f6f6e6592658d6517da427c7750cbcb0a
Author: Nicolas Goaziou <mail@nicolasgoaziou.fr>
Commit: Stefan Monnier <monnier@iro.umontreal.ca>

    Fix compilation issues plus a few more changes
    
    * repology-license.el: Build list of gentoo free licenses dynamically.
    (repology--license-identifiers-url:gentoo)
    (repology--license-categories:gentoo): New vars.
    (repology--license-identifiers:gentoo): Default to nil.
    (repology--license-get-identifiers:gentoo): New function.
    (repology--license-gentoo:peek): Remove unuse arg.
    (repology--license-check:gentoo): Handle absence of license info.
    (repology--license-check): Rename from `repology--license-free-p`.
    
    * repology.el: Shuffle the code so it's better organized.
    Add a few temporary messages to keep the user entertained.
    (repology-request): Rename from `repology--request`.
    (repology--check-freedom): New command.
    (repology--main-prompt): New const.
    (repology--select-key): New function.
    (repology): Use them.
---
 repology-license.el |  202 +++++-----
 repology.el         | 1054 ++++++++++++++++++++++++++-------------------------
 2 files changed, 643 insertions(+), 613 deletions(-)

diff --git a/repology-license.el b/repology-license.el
index 5e76f63..d3bae5c 100644
--- a/repology-license.el
+++ b/repology-license.el
@@ -29,6 +29,15 @@
 ;; In order to see the results of each vote, and possibly debug the
 ;; process, you can set `repology-license-debug' to a non-nil value.
 
+;;; Code:
+
+(declare-function repology-request "repology" (url &optional extra-headers))
+(declare-function repology-package-field "repology" (package field))
+(declare-function repology-project-name "repology" (project))
+(declare-function repology-package-p "repology" (object))
+(declare-function repology-project-p "repology" (object))
+(declare-function repology-project-packages "repology" (project))
+
 
 ;;; Constants
 (defconst repology-license-reference-repositories
@@ -53,10 +62,13 @@ This is a list of triplets (REPO SUBREPO PREDICATE) where:
  SUBREPO is a regexp matching a sub-repository or nil;
  PREDICATE is either a boolean or a function called with one string argument.
 
-When PREDICATE is a function, it must return a non-nil value if the argument
-is a free license according to the repository.  If PREDICATE is t, we trust
-the repository to provide only free software.  Conversely, PREDICATE is nil
-when the repository is known to reference only non-free software.
+When PREDICATE is a function, a return value of t means the argument is a free
+license according to the repository, whereas nil means it is non-free.  Any
+other value means the repository cannot decide, and pass.
+
+If PREDICATE is t, we trust the repository to provide only free software.
+Conversely, PREDICATE is nil when the repository is known to reference only
+non-free software.
 
 A repository with a PREDICATE function is expected to have the following
 properties:
@@ -68,13 +80,22 @@ properties:
 (defconst repology-license-poll-threshold 0.5
   "Ratio of votes above which a package is declared to be free.")
 
+(defconst repology--license-identifiers-url:gentoo
+  "https://gitweb.gentoo.org/repo/gentoo.git/plain/profiles/license_groups";
+  "URL referencing Gentoo free license identifiers.")
+
+(defconst repology--license-categories:gentoo
+  '("GPL-COMPATIBLE" "FSF-APPROVED" "OSI-APPROVED" "MISC-FREE"
+    "FSF-APPROVED-OTHER" "MISC-FREE-DOCS")
+  "List of free license categories according to Gentoo.")
+
 
 ;;; Tools
 (defun repology--license-interpret-vote (free votes)
   "Return freedom vote result as a boolean.
 FREE is the number of \"Free\" votes.  VOTES is the total number of votes."
   (and (> votes 0)
-       (>= (/ (float free) votes) repology-license-poll-threshold)))
+       (> (/ (float free) votes) repology-license-poll-threshold)))
 
 
 ;;; Reference Repository: Fedora
@@ -83,82 +104,43 @@ FREE is the number of \"Free\" votes.  VOTES is the total 
number of votes."
 See URL \
 
`https://docs.fedoraproject.org/en-US/packaging-guidelines/LicensingGuidelines/'"
   (let ((case-fold-search t)
-        ;; Anything in Fedora is free, unless its license contains the
-        ;; following.
+        ;; Anything in Fedora is considered to be free, unless its
+        ;; license contains the following.
         (non-free-license-re
          (rx word-start "Redistributable, no modification permitted" 
word-end)))
     (not (string-match non-free-license-re license))))
 
 
 ;;; Reference Repository: Gentoo
-(defconst repology--license-identifiers:gentoo
-  (list
-   ;; GPL-COMPATIBLE
-   "AGPL-3" "AGPL-3+" "Apache-2.0" "Apache-2.0-with-LLVM-exceptions"
-   "Artistic-2" "Boost-1.0" "BSD" "BSD-2" "CC0-1.0" "CeCILL-2"
-   "Clarified-Artistic" "Clear-BSD"  "ECL-2.0" "FTL"
-   "gcc-runtime-library-exception-3.1" "GPL-1" "GPL-1+" "GPL-2" "GPL-2+" 
"GPL-3"
-   "GPL-3+" "GPL-2-with-classpath-exception" "GPL-2-with-exceptions"
-   "GPL-2-with-font-exception" "GPL-2-with-linking-exception"
-   "GPL-2-with-MySQL-FLOSS-exception" "GPL-2+-with-openssl-exception"
-   "GPL-3+-with-cuda-exception" "GPL-3+-with-cuda-openssl-exception"
-   "GPL-3-with-font-exception" "GPL-3+-with-opencl-exception"
-   "GPL-3+-with-opencl-openssl-exception" "GPL-3-with-openssl-exception"
-   "Transmission-OpenSSL-exception" "UPX-exception" "HPND" "IJG" "ISC" "LGPL-2"
-   "LGPL-2+" "LGPL-2.1" "LGPL-2.1+" "LGPL-3" "LGPL-3+"
-   "LGPL-2-with-linking-exception" "LGPL-2.1-with-linking-exception"
-   "LGPL-3-with-linking-exception" "Nokia-Qt-LGPL-Exception-1.1" "libgcc"
-   "libstdc++" "metapackage" "MIT" "MPL-2.0" "OPENLDAP" "PSF-2" "PSF-2.2"
-   "PSF-2.3" "PSF-2.4" "public-domain" "PYTHON" "qwt" "Ruby" "Ruby-BSD"
-   "SGI-B-2.0" "Sleepycat" "tanuki-community" "unicode" "Unlicense" "UoI-NCSA"
-   "vim" "W3C" "WTFPL-2" "wxWinLL-3.1" "ZLIB" "ZPL"
-   ;; FSF-APPROVED
-   "AFL-2.1" "AFL-3.0" "Apache-1.0" "Apache-1.1" "APSL-2" "BSD-4" "CDDL" "CNRI"
-   "CPAL-1.0" "CPL-1.0" "EPL-1.0" "EPL-2.0" "EUPL-1.1" "gnuplot" "IBM"
-   "LPPL-1.2" "MPL-1.0" "MPL-1.1" "Ms-PL" "NPL-1.1" "openssl" "OSL-1.1"
-   "OSL-2.0" "OSL-2.1" "PHP-3.01" "QPL" "QPL-1.0" "Zend-2.0"
-   ;; OSI-APPROVED
-   "AFL-3.0" "AGPL-3" "AGPL-3" "Apache-1.1" "Apache-2.0" "APL-1.0" "APSL-2"
-   "Artistic" "Artistic-2" "Boost-1.0" "BSD" "BSD-2" "CDDL" "CNRI" "CPAL-1.0"
-   "CPL-1.0" "ECL-2.0" "EPL-1.0" "EPL-2.0" "EUPL-1.1" "GPL-1" "GPL-2" "GPL-2"
-   "GPL-3" "GPL-3" "HPND" "IBM" "IPAfont" "ISC" "LGPL-2" "LGPL-2.1" "LGPL-2.1"
-   "LGPL-3" "LGPL-3" "LPPL-1.3c" "MIT" "MPL-1.0" "MPL-1.1" "MPL-2.0" "Ms-PL"
-   "nethack" "NOSA" "OFL-1.1"  "OSL-2.1" "PHP-3" "PHP-3.01" "POSTGRESQL" 
"PSF-2"
-   "QPL" "Sleepycat" "UoI-NCSA" "W3C" "Watcom-1.0" "wxWinLL-3" "ZLIB" "ZPL"
-   ;; MISC-FREE
-   "Allegro" "alternate" "AMPAS" "bea.ri.jsr173" "BEER-WARE" "boehm-gc" "BSD-1"
-   "BSD-with-attribution" "BSD-with-disclosure" "buddy" "bufexplorer.vim"
-   "BZIP2" "canfep" "CAOSL" "CDDL-Schily" "CeCILL-C" "CLX" "CMake" "CPL-0.5"
-   "CRACKLIB" "Crypt-IDEA" "DES" "docbook" "dom4j" "DUMB-0.9.3"
-   "eGenixPublic-1.1" "ElementTree" "Emacs" "ErlPL-1.1" "FastCGI" "feh"
-   "File-MMagic" "Flashpix" "FLEX" "flexmock" "FLTK" "freetts" "FVWM" "gd"
-   "gsm" "HTML-Tidy" "htmlc" "iASL" "icu" "IDPL" "imagemagick" "Info-ZIP"
-   "inner-net" "Interbase-1.0" "ipadic" "ipx-utils" "Ispell" "JasPer2.0" "JDOM"
-   "JNIC" "JOVE" "Khronos-CLHPP" "LambdaMOO" "LIBGLOSS" "libmng" "libpng"
-   "libpng2" "libtiff" "LLVM-Grant" "LPPL-1.3" "LPPL-1.3b" "lsof"
-   "Mail-Sendmail" "mapm-4.9.5" "matplotlib" "Mini-XML" "minpack"
-   "MIT-with-advertising" "mm" "mpich2" "NCSA-HDF" "netcat" "NEWLIB" "ngrep"
-   "Old-MIT" "openafs-krb5-a" "Openwall" "otter" "PCRE" "perforce" "photopc"
-   "PHP-2.02" "pngcrush" "pngnq" "Princeton" "psutils" "qmail-nelson" "rc"
-   "rdisc" "regexp-UofT" "repoze" "RSA" "rwpng" "scanlogd" "Sendmail"
-   "Sendmail-Open-Source" "shrimp" "SMAIL" "Snd" "SNIA" "SSLeay" "Subversion"
-   "SVFL" "symlinks" "tablelist" "tcltk" "tcp_wrappers_license" "TeX"
-   "TeX-other-free" "the-Click-license" "Time-Format" "Time-modules" "tm-align"
-   "torque-2.5" "totd" "Toyoda" "UCAR-Unidata" "URT" "VTK" "w3m" "x2x" "xbatt"
-   "xboing" "XC" "Xdebug" "xtrs" "xvt" "YaTeX" "yuuji" "ZSH"
-   ;; FSF-APPROVED-OTHER.
-   "Arphic" "CC-BY-2.0" "CC-BY-2.5" "CC-BY-3.0" "CC-BY-4.0" "CC-BY-SA-2.0"
-   "CC-BY-SA-2.5" "CC-BY-SA-3.0" "CC-BY-SA-4.0" "FDL-1.1" "FDL-1.1+" "FDL-1.2"
-   "FDL-1.2+" "FDL-1.3" "FDL-1.3+" "FreeArt" "GPL-1" "GPL-1+" "GPL-2" "GPL-2+"
-   "GPL-3" "GPL-3+" "IPAfont" "OFL" "OFL-1.1" "OPL"
-   ;; MISC-FREE-DOCS.
-   "BitstreamVera" "CC-PD" "CC-BY-SA-1.0" "CC-SA-1.0" "LDP-1" "LDP-1a"
-   "man-pages" "man-pages-posix" "man-pages-posix-2013" "MaxMind2" 
"mplus-fonts"
-   "myspell-en_CA-KevinAtkinson" "quake1-textures" "Texinfo-manual"
-   "UbuntuFontLicense-1.0" "Unicode_Fonts_for_Ancient_Scripts" "vlgothic"
-   "wxWinFDL-3")
-  "List of identifiers considered as free licenses by Gentoo
-See URL `https://wiki.gentoo.org/wiki/License_groups'.")
+(defvar repology--license-identifiers:gentoo nil
+  "List of identifiers considered as free licenses by Gentoo.
+See URL `https://wiki.gentoo.org/wiki/License_groups'.
+This list is populated with `repology--license-get-identifiers:gentoo'.")
+
+(defun repology--license-get-identifiers:gentoo ()
+  "Return list of free license identifiers according to Gentoo."
+  (unless repology--license-identifiers:gentoo
+    (with-temp-message "Repology: Fetching license identifiers for Gentoo..."
+      (let ((request
+              (repology-request repology--license-identifiers-url:gentoo)))
+        (pcase (plist-get request :reason)
+          ("OK"
+           (let ((identifiers nil))
+             (with-temp-buffer
+               (insert (plist-get request :body))
+               (dolist (category repology--license-categories:gentoo)
+                 (goto-char 1)
+                 (when (re-search-forward (concat "^" category " +"))
+                   (let ((line (buffer-substring (point) (line-end-position))))
+                     (setq identifiers
+                           (nconc (split-string line) identifiers)))))
+               (dolist (category repology--license-categories:gentoo)
+                 (setq identifiers (delete (concat "@" category) 
identifiers))))
+             (setq repology--license-identifiers:gentoo identifiers)))
+          (_
+           (message
+            "Repology: Cannot fetch Gentoo licenses.  \
+Ignoring repository")))))))
 
 (defun repology--license-gentoo:skip-whitespace ()
   "Skip past the whitespace at point."
@@ -172,8 +154,8 @@ See URL `https://wiki.gentoo.org/wiki/License_groups'.")
   "Advance N characters forward."
   (forward-char n))
 
-(defun repology--license-gentoo:peek (&optional n)
-  "Advance N characters forward."
+(defun repology--license-gentoo:peek ()
+  "Return the character at point."
   (following-char))
 
 (defun repology--license-gentoo:and ()
@@ -234,15 +216,17 @@ See URL `https://wiki.gentoo.org/wiki/License_groups'.")
 
 (defun repology--license-check:gentoo (license)
   "Return a non-nil value if LICENSE is free, according to Gentoo."
-  (with-temp-buffer
-    (insert license)
-    (goto-char 1)
-    (repology--license-gentoo:skip-whitespace)
-    (let ((value (not (eobp))))         ;blank string check
-      (while (and value (/= (repology--license-gentoo:peek) 0))
-        (unless (repology--license-gentoo:read-next)
-          (setq value nil)))
-      value)))
+  (if (null (repology--license-get-identifiers:gentoo))
+      'pass                             ;no license to check
+    (with-temp-buffer
+      (insert license)
+      (goto-char 1)
+      (repology--license-gentoo:skip-whitespace)
+      (let ((value (not (eobp))))       ;blank string check
+        (while (and value (/= (repology--license-gentoo:peek) 0))
+          (unless (repology--license-gentoo:read-next)
+            (setq value nil)))
+        value))))
 
 
 ;;; Reference Repository: OpenSUSE (OSS)
@@ -250,8 +234,8 @@ See URL `https://wiki.gentoo.org/wiki/License_groups'.")
   "Return a non-nil value if LICENSE is free, according to OpenSUSE (OSS).
 See URL `https://en.opensuse.org/openSUSE:Packaging_guidelines#Licensing'."
   (let ((case-fold-search t)
-        ;; Anything in Fedora is free, unless its license contains the
-        ;; following.
+        ;; Anything in OSS sub-repository from OpenSUSE is considered
+        ;; to be free, unless its license contains the following.
         (non-free-license-re
          (rx word-start "SUSE-Firmware" word-end)))
     (not (string-match non-free-license-re license))))
@@ -295,7 +279,7 @@ from reference repositories in PROJECT."
 ;;; Main Function
 (defun repology--license-find-reference-repository (package)
   "Return the reference repository containing PACKAGE, or nil.
-Return value is a triplet per `repology-license-reference-repositories'."
+Return value is a triplet from `repology-license-reference-repositories'."
   (let ((repo (repology-package-field package 'repo))
         (subrepo (repology-package-field package 'subrepo)))
     (seq-find (pcase-lambda (`(,r ,s ,_))
@@ -304,33 +288,38 @@ Return value is a triplet per 
`repology-license-reference-repositories'."
                          (and subrepo (string-match s subrepo)))))
               repology-license-reference-repositories)))
 
-(defun repology--license-free-p (package &optional repository)
-  "Return a non-nil value when PACKAGE is free.
-A package is free when any reference repository can attest it uses only free
-licenses.  When optional argument REPOSITORY is non-nil, use it as a 
reference."
+(defun repology--license-check (package repository)
+  "Check if PACKAGE is free according to REPOSITORY.
+REPOSITORY is an element from `repology-license-reference-repositories'.
+PACKAGE is free when REPOSITORY can attest it uses only free licenses."
   (pcase (or repository (repology--license-find-reference-repository package))
-    ('nil nil)
     (`(,_ ,_ ,(and (pred functionp) p))
      (seq-every-p p (repology-package-field package 'licenses)))
     (`(,_ ,_ ,boolean) boolean)
     (other (error "Wrong repository definition: %S" other))))
 
 (defun repology-free-p (datum)
-  "Return a non-nil value when DATUM is free.
-
-DATUM is a project or a package.
+  "Return t when project or package DATUM is free.
 
 A package is free when any reference repository can attest it uses only free
 licenses.  See `repology-license-reference-repositories' for a list of such
-repositories.
+repositories.  If the package does not belong to any of these repositories,
+or if there is not enough information to decide, return `unknown'.  Otherwise,
+return nil.
 
 A project is free if the ratio of free packages among the packages from
 reference repositories is above `repology-license-poll-threshold'.
-A project without any package from these repositories is declared as non-free.
+In any other case, return nil.  In particular, a project without any package
+from reference repositories is declared non-free.
 
-Of course, it is not a legal statement, merely an indicator."
+Of course, it is not a legal statement, merely an indication."
   (pcase datum
-    ((pred repology-package-p) (repology--license-free-p datum))
+    ((pred repology-package-p)
+     (pcase (repology--license-find-reference-repository datum)
+       ('nil 'unknown)
+       (repository
+        (let ((decision (repology--license-check datum repository)))
+          (if (booleanp decision) decision 'unknown)))))
     ((pred repology-project-p)
      (let ((votes 0)
            (yes 0)
@@ -343,11 +332,12 @@ Of course, it is not a legal statement, merely an 
indicator."
             (unless (member repository voters)
               (cl-incf votes)
               (push repository voters)  ;a repository votes only once
-              (let ((free (repology--license-free-p package repository)))
-                (when free (cl-incf yes))
-                (when repology-license-debug
-                  (push (repology--license-debug-line package free)
-                        reports)))))))
+              (let ((free (repology--license-check package repository)))
+                (when (booleanp free)   ;has repository an opinion?
+                  (when free (cl-incf yes))
+                  (when repology-license-debug
+                    (push (repology--license-debug-line package free)
+                          reports))))))))
        ;; Maybe display vote reports as debugging information.
        (when repology-license-debug
          (repology--license-debug-display datum reports yes votes))
diff --git a/repology.el b/repology.el
index 1df64e4..30e0930 100644
--- a/repology.el
+++ b/repology.el
@@ -34,9 +34,10 @@
 ;; Projects-related requests are limited to `repology-projects-limit'.
 ;; All requests are cached during `repology-cache-duration' seconds.
 ;;
-;; By default, projects including packages with a known non-free license
-;; are not included in the search results.  You can control this behavior
-;; with the variable `repology-free-only-projects'.
+;; By default, only projects recognized as free are included in the search
+;; results.  You can control this behavior with the variable
+;; `repology-free-only-projects'.  The function `repology-free-p' is 
responsible
+;; for guessing if a project, or a package, is free or not.
 
 ;; You can then access data from those various objects using dedicated
 ;; accessors.  See, for example, `repology-project-name',
@@ -51,7 +52,7 @@
 ;; `repology-display-packages-columns',`repology-display-projects-columns',
 ;; and `repology-display-problems-columns').  When projects or packages
 ;; are displayed, pressing <RET> gives you more information about the item
-;; at point.
+;; at point, whereas pressing <F> reports their "freedom" status.
 
 ;; For example, the following expression displays all outdated projects
 ;; named after "emacs" and containing a package in GNU Guix repository
@@ -105,12 +106,12 @@ objects of the column."
          (funcall ,predicate s1 s2)))))
 
 
-;;; Constants
+;;; Upstream Constants
 (defconst repology-base-url "https://repology.org/api/v1/";
   "Base URL for Repology API.")
 
 (defconst repology-statistics-url 
"https://repology.org/repositories/statistics";
-  "Base URL for \"Statistics\" page in Repology website.
+  "URL for \"Statistics\" page in Repology website.
 It is used as a source for all known repositories.")
 
 (defconst repology-package-all-fields
@@ -127,37 +128,6 @@ It is used as a source for all known repositories.")
   "Maximum number of projects Repology API can return.
 See URL `https://repology.org/api'.")
 
-(defconst repology-project-filters-parameters
-  `((:search          "Name search (e.g. emacs): " nil)
-    (:maintainer      "Maintainer (e.g. foo@bar.com): " nil)
-    (:category        "Category (e.g. games): " nil)
-    (:inrepo          "In repository: " repology--query-repository)
-    (:notinrepo       "Not in repository: " repology--query-repository)
-    (:repos           "Repositories (e.g. 1 or 2- or 3-5): " nil)
-    (:families        "Families (e.g. 1 or 2- or 3-5): " nil)
-    (:repos_newest    "Repositories newest (e.g. 1 or 2- or 3-5): " nil)
-    (:families_newest "Families newest (e.g. 1 or 2- or 3-5): " nil)
-    (:newest          "Newest? " repology--query-y-or-n-p)
-    (:outdated        "Outdated? " repology--query-y-or-n-p)
-    (:problematic     "Problematic? " repology--query-y-or-n-p)
-    (:vulnerable      "Potentially vulnerable? " repology--query-y-or-n-p)
-    (:has_related     "Has related? " repology--query-y-or-n-p))
-  "Association list between project filters and query data.
-Each entry is a triplet (FILTER PROMPT QUERY) where FILTER is a keyword, PROMPT
-is a string, and QUERY is a function used to prompt the user, or nil.
-When setting the value of FILTER interactively, QUERY is called with
-two arguments, PROMPT and an initial value.  It must return a string.  If QUERY
-is nil, `read-string' is used.")
-
-(defconst repology-version-zero-component '(1 . 0)
-  "Version component representing 0 or any missing component.")
-
-(defconst repology-version-pre-keywords '("alpha" "beta" "rc" "pre")
-  "List of pre-release keywords in version strings.")
-
-(defconst repology-version-post-keywords '("patch" "post" "pl" "errata")
-  "List of post-release keywords in version strings.")
-
 
 ;;; Configuration
 (defgroup repology nil
@@ -297,401 +267,51 @@ predicates like `repology-compare-texts', 
`repology-compare-numbers', or
 `repology-compare-versions' in order to build SORT values."
   :type '(choice
           (repeat
-           (list :tag "Column definition"
-                 (string :tag "Column name")
-                 function
-                 (integer :tag "Width")
-                 (choice (const :tag "Do not sort" nil)
-                         (const :tag "Sort" t)
-                         (function :tag "Custom sort predicate"))))
-          (function :tag "Function describing columns")))
-
-
-;;; Internal variables
-(defconst repology--project-filters
-  (mapcar #'car repology-project-filters-parameters)
-  "List of known filters for projects.
-Other keywords are ignored when building the query string.")
-
-(defvar repology--cache (make-hash-table :test #'equal)
-  "Hash table used to cache request to Repology API.
-Keys are triplets of arguments for `repology--get'.  Values are
-cons cells like (TIME . REQUEST-RESULT).")
-
-(defvar repology--repositories nil
-  "List of repositories known to Repology.
-The list is populated by `repology-list-repositories'.  Call that function
-instead of using this variable.")
-
-
-;;; Internal functions
-(defun repology--cache-key (action value start)
-  "Return a cache key for current query.
-See `repology--get' for precision about ACTION, VALUE, and START."
-  (list action
-        (if (not (eq action 'projects)) value
-          ;; VALUE is a p-list.  Sort it in a fixed order so p-lists
-          ;; sorted differently are cached the same way.  Also ignore
-          ;; unknown filters.
-          (let ((normalized nil))
-            (dolist (prop repology--project-filters)
-              (when (plist-member value prop)
-                (setq normalized
-                      (plist-put normalized prop (plist-get value prop)))))
-            normalized))
-        start))
-
-(defun repology--cache-get (key)
-  "Return cached value associated to KEY, or nil.
-If the cached value is too old according to `repology-cache-duration',
-reset the cache and return nil."
-  (pcase (gethash key repology--cache)
-    (`(,time . ,value)
-     ;; Check if cached value is still valid.
-     (if (> repology-cache-duration (time-to-seconds (time-since time)))
-         value
-       ;; Time is over: reset cache and return nil.
-       (remhash key repology--cache)))
-    (_ nil)))
-
-(defun repology--cache-put (key value)
-  "Cache KEY with VALUE."
-  (puthash key (cons (current-time) value) repology--cache))
-
-(defun repology--parse-json (json-string)
-  "Parse a JSON string and returns an object.
-JSON objects become alists and JSON arrays become lists."
-  (if (null json-string)
-      nil
-    (let ((json-object-type 'alist)
-          (json-array-type 'list))
-      (condition-case err
-          (json-read-from-string json-string)
-        (json-readtable-error
-         (message "%s: Could not parse string into an object.  See %S"
-                  (error-message-string err)
-                  json-string))))))
-
-(defun repology--build-query-string (filters)
-  "Build a filter string from a given FILTERS plist."
-  (let ((query nil))
-    (dolist (keyword repology--project-filters)
-      (let ((value (plist-get filters keyword)))
-        (when value
-          (let ((key (substring (symbol-name keyword) 1)))
-            (push (format "%s=%s"
-                          (url-hexify-string key)
-                          (url-hexify-string value))
-                  query)))))
-    (if (null query) ""
-      (concat "?" (mapconcat #'identity query "&")))))
-
-(defun repology--build-url (action value start)
-  "Build a URL from an ACTION symbol.
-Value is a plist if ACTION is `projects', or a string otherwise."
-  (concat repology-base-url
-          (symbol-name action)
-          "/"
-          (pcase action
-            ('project value)
-            ('repository (concat value "/problems"))
-            ('projects
-             (concat (and start (concat start "/"))
-                     (repology--build-query-string value)))
-            (_ (error "Unknown action: %S" action)))))
-
-(defun repology--request (url &optional extra-headers)
-  "Perform a raw HTTP request on URL.
-EXTRA-HEADERS is an assoc list of headers/contents to send with
-the request."
-  (let* ((url-request-method "GET")
-         (url-request-extra-headers extra-headers)
-         (process-buffer (url-retrieve-synchronously url)))
-    (unwind-protect
-        (with-current-buffer process-buffer
-          (goto-char (point-min))
-          (let* ((status-line-regexp
-                  (rx bol
-                      (one-or-more (not (any " "))) " "
-                      (group (in "1-5") (= 2 digit)) " "
-                      (group (one-or-more (in "A-Z" "a-z" " ")))
-                      eol))
-                 (status
-                  (and (looking-at status-line-regexp)
-                       (list :code (string-to-number (match-string 1))
-                             :reason (match-string 2))))
-                 (header nil)
-                 (body nil))
-            (forward-line)
-            (while (looking-at "^\\([^:]+\\): \\(.*\\)")
-              (push (match-string 1) header)
-              (push (match-string 2) header)
-              (forward-line))
-            (forward-line)
-            (unless (eobp)
-              (setq body (buffer-substring (point) (point-max))))
-            (append status (list :header (nreverse header) :body body))))
-      (kill-buffer process-buffer))))
-
-(defun repology--get (action value start)
-  "Perform an HTTP GET request to Repology.
-
-ACTION is a symbol.  If it is `projects', VALUE is a plist and START a string.
-Otherwise, VALUE is a string, and START is nil.
-
-Information is returned as parsed JSON."
-  (let ((key (repology--cache-key action value start)))
-    (or (repology--cache-get key)
-        (let ((request (repology--request
-                        (repology--build-url action value start)
-                        '(("Content-Type" . "application/json")))))
-          (pcase (plist-get request :reason)
-            ("OK"
-             (let ((body (repology--parse-json (plist-get request :body))))
-               (repology--cache-put key body)
-               ;; Information from `projects' is a list of projects,
-               ;; so, we can also cache each of them for a future
-               ;; project lookup.
-               (when (eq action 'projects)
-                 (dolist (project body)
-                   (let ((key (repology--cache-key
-                               'project (repology-project-name project) nil))
-                         (packages (repology-project-packages project)))
-                     (repology--cache-put key packages))))
-               ;; Return information.
-               body))
-            (status
-             (error "Cannot retrieve information: %S" status)))))))
-
-(defun repology--value-to-string (value)
-  "Change VALUE object into a string suitable for display."
-  (pcase value
-    (`nil "-")
-    ((pred listp)
-     (mapconcat (lambda (e) (format "%s" e))
-                (seq-uniq value)
-                " "))
-    (_
-     (format "%s" value))))
-
-(defun repology--package-status-face (package)
-  "Return face associated to status from PACKAGE."
-  (let ((status (repology-package-field package 'status)))
-    (alist-get status repology-status-faces 'default nil #'equal)))
-
-(defun repology--make-display (data buffer-name mode format-descriptors)
-  "Display DATA in a buffer named after BUFFER-NAME string.
-DATA is displayed in a major mode derived from `tabulated-list-mode', and set
-by function MODE.  Each entry is identified by the element from DATA, and
-formatted according to FORMAT-DESCRIPTORS function.  This function is called
-with one argument: an element from DATA."
-  (let ((buffer (get-buffer-create buffer-name)))
-    (with-current-buffer buffer
-      (funcall mode)
-      (setq tabulated-list-entries
-            (mapcar (lambda (datum)
-                      (list datum
-                            (apply #'vector
-                                   (funcall format-descriptors datum))))
-                    data))
-      (tabulated-list-print))
-    (pop-to-buffer buffer)))
-
-(defun repology--show-current-package ()
-  "Display packages associated to project at point."
-  (interactive)
-  (repology-display-package (tabulated-list-get-id)))
-
-(defun repology--show-current-project ()
-  "Display packages associated to project at point."
-  (interactive)
-  (repology-display-packages
-   (repology-project-packages (tabulated-list-get-id))))
-
-(defvar repology--display-projects-mode-map
-  (let ((map (make-sparse-keymap)))
-    (set-keymap-parent map tabulated-list-mode-map)
-    (define-key map (kbd "RET") 'repology--show-current-project)
-    map)
-  "Local keymap for `repology--display-projects-mode' buffers.")
-
-(defvar repology--display-packages-mode-map
-  (let ((map (make-sparse-keymap)))
-    (set-keymap-parent map tabulated-list-mode-map)
-    (define-key map (kbd "RET") 'repology--show-current-package)
-    map)
-  "Local keymap for `repology--display-packages-mode' buffers.")
-
-(defun repology--columns-to-header (specs)
-  "Return vector of column names according to SPECS.
-SPECS is an association list.  Each entry has the form (NAME _ WIDTH SORT)
-where NAME, WIDTH and SORT are of the expected type in 
`tabulated-list-format'."
-  (apply #'vector
-         (mapcar (lambda (format)
-                   (pcase format
-                     (`(,name ,_ ,width ,sort) (list name width sort))
-                     (other
-                      (user-error "Invalid package column format: %S" other))))
-                 specs)))
-
-(define-derived-mode repology--display-package-mode tabulated-list-mode
-  "Repology/Package"
-  "Major mode used to display packages returned by Repology API.
-\\{tabulated-list-mode-map}"
-  (setq tabulated-list-format [("Field" 15 t) ("Value" 0 t)])
-  (tabulated-list-init-header))
-
-(define-derived-mode repology--display-packages-mode tabulated-list-mode
-  "Repology/Packages"
-  "Major mode used to display packages returned by Repology API.
-\\{repology--display-packages-mode-map}"
-  (setq tabulated-list-format
-        (repology--columns-to-header repology-display-packages-columns))
-  (tabulated-list-init-header))
-
-(define-derived-mode repology--display-projects-mode tabulated-list-mode
-  "Repology/Project"
-  "Major mode used to display projects returned by Repology API.
-\\{repology--display-projects-mode-map}"
-  (setq tabulated-list-format
-        (repology--columns-to-header repology-display-projects-columns))
-  (tabulated-list-init-header))
-
-(define-derived-mode repology--display-problems-mode tabulated-list-mode
-  "Repology/Problems"
-  "Major mode used to display problems returned by Repology API.
-\\{tabulated-list-mode-map}"
-  (setq tabulated-list-format
-        (repology--columns-to-header repology-display-problems-columns))
-  (tabulated-list-init-header))
-
-(defun repology--column-to-descriptor (datum specs &optional symbol-handler)
-  "Return list of descriptors for DATUM according to SPECS.
-
-DATUM is a package, a problem, or a project.  SPECS is an association
-list.  Each entry has the form (_ VALUE _ _).
-
-VALUE is a function called with DATUM as its sole argument.  When VALUE is
-a symbol, and optional argument SYMBOL-HANDLER is a function, SYMBOL-HANDLER
-is called with two arguments: DATUM and VALUE.  In any case, the return value
-is then turned into a string and displayed."
-  (mapcar (lambda (spec)
-            (pcase spec
-              ;; Contents as a function.
-              (`(,_ ,(and (pred functionp) f) ,_ ,_)
-               (repology--value-to-string (funcall f datum)))
-              ;; Contents as a symbol.
-              ((and (guard symbol-handler)
-                    `(,_ ,(and (pred symbolp) field) ,_ ,_))
-               (repology--value-to-string (funcall symbol-handler datum 
field)))
-              ;; Invalid contents.
-              (other (user-error "Invalid format type: %S" other))))
-          specs))
-
-(defun repology--format-field-descriptors (field)
-  "Format an entry from FIELD.
-Format follows `repology-display-packages-columns' specifications.
-Return a list of descriptors."
-  (pcase field
-    (`(,name . ,value)
-     (list (symbol-name name)
-           (repology--value-to-string value) ))
-    (_
-     (error "Invalid field: %S" field))))
-
-(defun repology--format-package-descriptors (package)
-  "Format an entry from PACKAGE.
-Format follows `repology-display-packages-columns' specifications.
-Return a list of descriptors."
-  (repology--column-to-descriptor package
-                                  repology-display-packages-columns
-                                  #'repology-package-field))
-
-(defun repology--format-project-descriptors (project)
-  "Format an entry for PROJECT.
-Format follows `repology-display-packages-columns' specifications.
-Return a list of descriptors."
-  (repology--column-to-descriptor project repology-display-projects-columns))
-
-
-(defun repology--format-problem-descriptors (problem)
-  "Format an entry from PROBLEM.
-Format follows `repology-display-problems-columns' specifications.
-Return a list of descriptors."
-  (repology--column-to-descriptor problem
-                                  repology-display-problems-columns
-                                  #'repology-problem-field))
-
-(defun repology--query-y-or-n-p (prompt _)
-  "Ask user a \"y or n\" question, displaying PROMPT.
-Return \"on\" or \"off\"."
-  (if (y-or-n-p prompt) "on" "off"))
-
-(defun repology--query-repository (prompt initial)
-  "Ask user an existing repository by its full name, displaying PROMPT.
-INITIAL is the initial input.  Return a repository internal name."
-  (repology-repository-name
-   (completing-read prompt (repology-list-repositories t) nil t initial)))
-
-(defun repology--query-filter-value (filter initial)
-  "Ask user for FILTER value.
-FILTER is a project filter, as a keyword.  INITIAL is a string inserted as
-a first suggestion, or nil.  Return the answer as a string."
-  (pcase (assq filter repology-project-filters-parameters)
-    (`nil
-     (error "Unknown filter: %S" filter))
-    (`(,_ ,prompt nil)
-     (read-string prompt initial))
-    (`(,_ ,prompt ,(and (pred functionp) collection))
-     (funcall collection prompt initial))
-    (other
-     (error "Invalid value: %S" other))))
+           (list :tag "Column definition"
+                 (string :tag "Column name")
+                 function
+                 (integer :tag "Width")
+                 (choice (const :tag "Do not sort" nil)
+                         (const :tag "Sort" t)
+                         (function :tag "Custom sort predicate"))))
+          (function :tag "Function describing columns")))
 
-(defun repology--string-to-version (s)
-  "Return version associated to string S.
-Version is a list of components (RANK . VALUE) suitable for comparison, with
-the function `repology-compare-versions'."
-  (let ((split nil))
-    ;; Explode string into numeric and alphabetic components.
-    ;; Intermediate SPLIT result is in reverse order.
-    (let ((regexp (rx (or (group (one-or-more digit)) (one-or-more alpha))))
-          (start 0))
-      (while (string-match regexp s start)
-        (let ((component (match-string 0 s)))
-          (push (if (match-beginning 1) ;numeric component?
-                    (string-to-number component)
-                  ;; Version comparison ignores case.
-                  (downcase component))
-                split))
-        (setq start (match-end 0))))
-    ;; Attach ranks to components.  NUMERIC-FLAG is used to catch
-    ;; trailing alphabetic components, which get a special rank.
-    ;; However, if there is no numeric component, no alphabetic
-    ;; component ever gets this rank, hence the initial value.
-    (let ((numeric-flag (seq-every-p #'stringp split))
-          (result nil))
-      (dolist (component split)
-        (let ((rank
-               (cond
-                ;; 0 gets "zero" (1) rank.
-                ((equal 0 component) 1)
-                ;; Other numeric components get "nonzero" (3) rank.
-                ((wholenump component) 3)
-                ;; Pre-release keywords get "pre_release" (0) rank.
-                ((member component repology-version-pre-keywords) 0)
-                ;; Post-release keywords get "post_release" (2) rank.
-                ((member component repology-version-post-keywords) 2)
-                ;; Alphabetic components after the last numeric
-                ;; component get the "letter_suffix" (4) rank.
-                ((not numeric-flag) 4)
-                ;; Any other alphabetic component is "pre_release".
-                (t 0))))
-          (when (wholenump component) (setq numeric-flag t))
-          (push (cons rank component) result)))
-      result)))
+
+;;; Global Internal Variables
+(defconst repology-project-filters-parameters
+  `((:search          "Name search (e.g. emacs): " nil)
+    (:maintainer      "Maintainer (e.g. foo@bar.com): " nil)
+    (:category        "Category (e.g. games): " nil)
+    (:inrepo          "In repository: " repology--query-repository)
+    (:notinrepo       "Not in repository: " repology--query-repository)
+    (:repos           "Repositories (e.g. 1 or 2- or 3-5): " nil)
+    (:families        "Families (e.g. 1 or 2- or 3-5): " nil)
+    (:repos_newest    "Repositories newest (e.g. 1 or 2- or 3-5): " nil)
+    (:families_newest "Families newest (e.g. 1 or 2- or 3-5): " nil)
+    (:newest          "Newest? " repology--query-y-or-n-p)
+    (:outdated        "Outdated? " repology--query-y-or-n-p)
+    (:problematic     "Problematic? " repology--query-y-or-n-p)
+    (:vulnerable      "Potentially vulnerable? " repology--query-y-or-n-p)
+    (:has_related     "Has related? " repology--query-y-or-n-p))
+  "Association list between project filters and query data.
+Each entry is a triplet (FILTER PROMPT QUERY) where FILTER is a keyword, PROMPT
+is a string, and QUERY is a function used to prompt the user, or nil.
+When setting the value of FILTER interactively, QUERY is called with
+two arguments, PROMPT and an initial value.  It must return a string.  If QUERY
+is nil, `read-string' is used.")
+
+(defconst repology--project-filters
+  (mapcar #'car repology-project-filters-parameters)
+  "List of known filters for projects.
+Other keywords are ignored when building the query string.")
 
 
 ;;; Utilities
+(defvar repology--repositories nil
+  "List of repositories known to Repology.
+The list is populated by `repology-list-repositories'.  Call that function
+instead of using this variable.")
+
 (defun repology-package-p (object)
   "Return t if OBJECT is a package."
   (and (consp object)
@@ -854,28 +474,29 @@ following ones:
 Return a list of strings.  When option argument FULL-NAME is non-nil, list
 the repositories with their full name instead of their internal name."
   (unless repology--repositories
-    (let ((request (repology--request repology-statistics-url)))
-      (pcase (plist-get request :reason)
-        ("OK"
-         (let ((body (plist-get request :body))
-               (repositories nil)
-               (start 0))
-           (while (string-match "id=\"\\(.+?\\)\"" body start)
-             (setq start (match-end 0))
-             (let* ((repo (match-string 1 body))
-                    (regexp
-                     (rx "href=\"/repository/"
-                         (+? anychar)
-                         "\">"
-                         (group (+? anychar))
-                         "<"))
-                    (true-name
-                     (and (string-match regexp body start)
-                          (match-string 1 body))))
-               (push (cons repo true-name) repositories)))
-           (setq repology--repositories (nreverse repositories))))
-        (status
-         (error "Cannot retrieve information: %S" status)))))
+    (with-temp-message "Repology: Fetching list of repositories..."
+      (let ((request (repology-request repology-statistics-url)))
+       (pcase (plist-get request :reason)
+         ("OK"
+          (let ((body (plist-get request :body))
+                (repositories nil)
+                (start 0))
+            (while (string-match "id=\"\\(.+?\\)\"" body start)
+              (setq start (match-end 0))
+              (let* ((repo (match-string 1 body))
+                     (regexp
+                      (rx "href=\"/repository/"
+                          (+? anychar)
+                          "\">"
+                          (group (+? anychar))
+                          "<"))
+                     (true-name
+                      (and (string-match regexp body start)
+                           (match-string 1 body))))
+                (push (cons repo true-name) repositories)))
+            (setq repology--repositories (nreverse repositories))))
+         (status
+          (error "Cannot retrieve information: %S" status))))))
   (mapcar (if full-name #'cdr #'car) repology--repositories))
 
 (defun repology-refresh-repositories ()
@@ -910,40 +531,240 @@ Return t if S1 is less than S2.  Case is ignored."
 Return t if S1 is less than S2."
   (< (string-to-number s1) (string-to-number s2)))
 
-(defun repology-compare-versions (s1 s2)
-  "Compare package versions associated to strings S1 and S2.
+
+;;; Version Comparison
+(defconst repology-version-zero-component '(1 . 0)
+  "Version component representing 0 or any missing component.")
+
+(defconst repology-version-pre-keywords '("alpha" "beta" "rc" "pre")
+  "List of pre-release keywords in version strings.")
+
+(defconst repology-version-post-keywords '("patch" "post" "pl" "errata")
+  "List of post-release keywords in version strings.")
+
+(defun repology--string-to-version (s)
+  "Return version associated to string S.
+Version is a list of components (RANK . VALUE) suitable for comparison, with
+the function `repology-compare-versions'."
+  (let ((split nil))
+    ;; Explode string into numeric and alphabetic components.
+    ;; Intermediate SPLIT result is in reverse order.
+    (let ((regexp (rx (or (group (one-or-more digit)) (one-or-more alpha))))
+          (start 0))
+      (while (string-match regexp s start)
+        (let ((component (match-string 0 s)))
+          (push (if (match-beginning 1) ;numeric component?
+                    (string-to-number component)
+                  ;; Version comparison ignores case.
+                  (downcase component))
+                split))
+        (setq start (match-end 0))))
+    ;; Attach ranks to components.  NUMERIC-FLAG is used to catch
+    ;; trailing alphabetic components, which get a special rank.
+    ;; However, if there is no numeric component, no alphabetic
+    ;; component ever gets this rank, hence the initial value.
+    (let ((numeric-flag (seq-every-p #'stringp split))
+          (result nil))
+      (dolist (component split)
+        (let ((rank
+               (cond
+                ;; 0 gets "zero" (1) rank.
+                ((equal 0 component) 1)
+                ;; Other numeric components get "nonzero" (3) rank.
+                ((wholenump component) 3)
+                ;; Pre-release keywords get "pre_release" (0) rank.
+                ((member component repology-version-pre-keywords) 0)
+                ;; Post-release keywords get "post_release" (2) rank.
+                ((member component repology-version-post-keywords) 2)
+                ;; Alphabetic components after the last numeric
+                ;; component get the "letter_suffix" (4) rank.
+                ((not numeric-flag) 4)
+                ;; Any other alphabetic component is "pre_release".
+                (t 0))))
+          (when (wholenump component) (setq numeric-flag t))
+          (push (cons rank component) result)))
+      result)))
+
+(defun repology-compare-versions (s1 s2)
+  "Compare package versions associated to strings S1 and S2.
+
+Return t if version S1 is lower than version S2.
+
+See URL `https://github.com/repology/libversion/blob/master/doc/ALGORITHM.md'."
+  (let ((v1 (repology--string-to-version s1))
+        (v2 (repology--string-to-version s2)))
+    (catch :less?
+      (while (or v1 v2)
+        (pcase-let ((`(,r1 . ,v1)
+                     (or (pop v1) repology-version-zero-component))
+                    (`(,r2 . ,v2)
+                     (or (pop v2) repology-version-zero-component)))
+          (cond
+           ;; First compare ranks, then values.
+           ((/= r1 r2) (throw :less? (< r1 r2)))
+           ;; Components are equal.  Try next component.
+           ((equal v1 v2) nil)
+           ;; Numeric components are compared... numerically.
+           ((= r1 3) (throw :less? (< v1 v2)))
+           ;; Alphabetic components are compared by case insensitively
+           ;; comparing their first letters.
+           (t (throw :less?
+                     (string-lessp (substring v1 0 1) (substring v2 0 1)))))))
+      ;; Strings S1 and S2 represent equal versions.
+      nil)))
+
+
+;;; Search functions
+(defvar repology--cache (make-hash-table :test #'equal)
+  "Hash table used to cache requests to Repology API.
+Keys are triplets of arguments for `repology--get'.  Values are
+cons cells like (TIME . REQUEST-RESULT).")
+
+(defun repology--cache-key (action value start)
+  "Return a cache key for current query.
+See `repology--get' for precision about ACTION, VALUE, and START."
+  (list action
+        (if (not (eq action 'projects)) value
+          ;; VALUE is a p-list.  Sort it in a fixed order so p-lists
+          ;; sorted differently are cached the same way.  Also ignore
+          ;; unknown filters.
+          (let ((normalized nil))
+            (dolist (prop repology--project-filters)
+              (when (plist-member value prop)
+                (setq normalized
+                      (plist-put normalized prop (plist-get value prop)))))
+            normalized))
+        start))
+
+(defun repology--cache-get (key)
+  "Return cached value associated to KEY, or nil.
+If the cached value is too old according to `repology-cache-duration',
+reset the cache and return nil."
+  (pcase (gethash key repology--cache)
+    (`(,time . ,value)
+     ;; Check if cached value is still valid.
+     (if (> repology-cache-duration (time-to-seconds (time-since time)))
+         value
+       ;; Time is over: reset cache and return nil.
+       (remhash key repology--cache)))
+    (_ nil)))
+
+(defun repology--cache-put (key value)
+  "Cache KEY with VALUE."
+  (puthash key (cons (current-time) value) repology--cache))
+
+(defun repology--parse-json (json-string)
+  "Parse a JSON string and returns an object.
+JSON objects become alists and JSON arrays become lists."
+  (if (null json-string)
+      nil
+    (let ((json-object-type 'alist)
+          (json-array-type 'list))
+      (condition-case err
+          (json-read-from-string json-string)
+        (json-readtable-error
+         (message "%s: Could not parse string into an object.  See %S"
+                  (error-message-string err)
+                  json-string))))))
+
+(defun repology--build-query-string (filters)
+  "Build a filter string from a given FILTERS plist."
+  (let ((query nil))
+    (dolist (keyword repology--project-filters)
+      (let ((value (plist-get filters keyword)))
+        (when value
+          (let ((key (substring (symbol-name keyword) 1)))
+            (push (format "%s=%s"
+                          (url-hexify-string key)
+                          (url-hexify-string value))
+                  query)))))
+    (if (null query) ""
+      (concat "?" (mapconcat #'identity query "&")))))
+
+(defun repology--build-url (action value start)
+  "Build a URL from an ACTION symbol.
+Value is a plist if ACTION is `projects', or a string otherwise."
+  (concat repology-base-url
+          (symbol-name action)
+          "/"
+          (pcase action
+            ('project value)
+            ('repository (concat value "/problems"))
+            ('projects
+             (concat (and start (concat start "/"))
+                     (repology--build-query-string value)))
+            (_ (error "Unknown action: %S" action)))))
+
+(defun repology-request (url &optional extra-headers)
+  "Perform a raw HTTP request on URL.
+EXTRA-HEADERS is an assoc list of headers/contents to send with
+the request."
+  (let* ((url-request-method "GET")
+         (url-request-extra-headers extra-headers)
+         (process-buffer (url-retrieve-synchronously url t)))
+    (unwind-protect
+        (with-current-buffer process-buffer
+          (goto-char (point-min))
+          (let* ((status-line-regexp
+                  (rx bol
+                      (one-or-more (not (any " "))) " "
+                      (group (in "1-5") (= 2 digit)) " "
+                      (group (one-or-more (in "A-Z" "a-z" " ")))
+                      eol))
+                 (status
+                  (and (looking-at status-line-regexp)
+                       (list :code (string-to-number (match-string 1))
+                             :reason (match-string 2))))
+                 (header nil)
+                 (body nil))
+            (forward-line)
+            (while (looking-at "^\\([^:]+\\): \\(.*\\)")
+              (push (match-string 1) header)
+              (push (match-string 2) header)
+              (forward-line))
+            (forward-line)
+            (unless (eobp)
+              (setq body (buffer-substring (point) (point-max))))
+            (append status (list :header (nreverse header) :body body))))
+      (kill-buffer process-buffer))))
+
+(defun repology--get (action value start)
+  "Perform an HTTP GET request to Repology API.
 
-Return t if version S1 is lower than version S2.
+ACTION is a symbol.  If it is `projects', VALUE is a plist and START a string.
+Otherwise, VALUE is a string, and START is nil.
 
-See URL `https://github.com/repology/libversion/blob/master/doc/ALGORITHM.md'."
-  (let ((v1 (repology--string-to-version s1))
-        (v2 (repology--string-to-version s2)))
-    (catch :less?
-      (while (or v1 v2)
-        (pcase-let ((`(,r1 . ,v1)
-                     (or (pop v1) repology-version-zero-component))
-                    (`(,r2 . ,v2)
-                     (or (pop v2) repology-version-zero-component)))
-          (cond
-           ;; First compare ranks, then values.
-           ((/= r1 r2) (throw :less? (< r1 r2)))
-           ;; Components are equal.  Try next component.
-           ((equal v1 v2) nil)
-           ;; Numeric components are compared... numerically.
-           ((= r1 3) (throw :less? (< v1 v2)))
-           ;; Alphabetic components are compared by case insensitively
-           ;; comparing their first letters.
-           (t (throw :less?
-                     (string-lessp (substring v1 0 1) (substring v2 0 1)))))))
-      ;; Strings S1 and S2 represent equal versions.
-      nil)))
+Information is returned as parsed JSON."
+  (let ((key (repology--cache-key action value start)))
+    (or (repology--cache-get key)
+        (let ((request
+                (repology-request
+                 (repology--build-url action value start)
+                 '(("Content-Type" . "application/json")))))
+          (pcase (plist-get request :reason)
+            ("OK"
+             (let ((body (repology--parse-json (plist-get request :body))))
+               (repology--cache-put key body)
+               ;; Information from `projects' is a list of projects,
+               ;; so, we can also cache each of them for a future
+               ;; project lookup.
+               (when (eq action 'projects)
+                 (dolist (project body)
+                   (let ((key (repology--cache-key
+                               'project (repology-project-name project) nil))
+                         (packages (repology-project-packages project)))
+                     (repology--cache-put key packages))))
+               ;; Return information.
+               body))
+            (status
+             (error "Cannot retrieve information: %S" status)))))))
 
-
-;;; Search functions
 (defun repology-lookup-project (name)
   "List packages for project NAME.
 NAME is a string.  Return a list of packages."
-  (repology--get 'project name nil))
+  (with-temp-message
+      (format-message "Repology: Requesting information about `%s'..." name)
+    (repology--get 'project name nil)))
 
 (defun repology-search-projects (&rest filters)
   "Retrieve results of an advanced search in Repology.
@@ -1001,41 +822,217 @@ Return a list of projects.  Projects with a known 
non-free license are removed
 from output, unless `repology-free-only-projects' is nil."
   (let ((result nil)
         (name nil))
-    (catch :exit
-      (while t
-        (let ((request (repology--get 'projects filters name)))
-          (setq result (append result (cdr request)))
-          (cond
-           ;; Too many matches: drop those above limit and exit.
-           ((<= repology-projects-limit (length result))
-            (setq result (seq-subseq result 0 repology-projects-limit))
-            (throw :exit nil))
-           ;; Matches exhausted: exit and return result.
-           ((> repology-projects-hard-limit (length request))
-            (throw :exit result))
-           ;; Resume search starting from an imaginary project located
-           ;; right after the last project found, alphabetically.
-           (t
-            (setq name
-                  (pcase (last request)
-                    (`(,(and (pred repology-project-p) project))
-                     (concat (repology-project-name project) "-"))
-                    (other (error "Invalid request result: %S" other)))))))))
-    ;; Trim non-free projects.
-    (if (not repology-free-only-projects)
-        result
-      (seq-filter (lambda (project) (repology-free-p project))
-                  result))))
+    (with-temp-message "Repology: Querying API..."
+      (catch :exit
+        (while t
+          (let ((request (repology--get 'projects filters name)))
+            (setq result (append result request))
+            (cond
+             ;; Too many matches: drop those above limit and exit.
+             ((<= repology-projects-limit (length result))
+              (setq result (seq-subseq result 0 repology-projects-limit))
+              (throw :exit nil))
+             ;; Matches exhausted: exit and return result.
+             ((> repology-projects-hard-limit (length request))
+              (throw :exit result))
+             ;; Resume search starting from an imaginary project
+             ;; located right after the last project found,
+             ;; alphabetically. This is done by appending an hyphen to
+             ;; the name of the last project found.
+             (t
+              (setq name
+                    (pcase (last request)
+                      (`(,(and (pred repology-project-p) project))
+                       (concat (repology-project-name project) "-"))
+                      (other (error "Invalid request result: %S" 
other))))))))))
+    ;; Possibly keep only non projects.
+    (if repology-free-only-projects
+        (with-temp-message "Repology: Filtering out non-free projects..."
+          (seq-filter (lambda (project) (repology-free-p project))
+                      result))
+      result)))
 
 (defun repology-report-problems (repository)
   "List problems related to REPOSITORY.
 REPOSITORY is a string.  Return a list of problems."
   (unless (member repository (repology-list-repositories))
     (user-error "Unknown repository: %S" repository))
-  (repology--get 'repository repository nil))
+  (with-temp-message
+      (message "Repology: Fetching problems reports about %s"
+               (repology-repository-full-name repository))
+    (repology--get 'repository repository nil)))
 
 
 ;;; Display functions
+(defvar repology--display-projects-mode-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map tabulated-list-mode-map)
+    (define-key map (kbd "RET") 'repology--show-current-project)
+    (define-key map (kbd "F") 'repology--check-freedom)
+    map)
+  "Local keymap for `repology--display-projects-mode' buffers.")
+
+(defvar repology--display-packages-mode-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map tabulated-list-mode-map)
+    (define-key map (kbd "RET") 'repology--show-current-package)
+    (define-key map (kbd "F") 'repology--check-freedom)
+    map)
+  "Local keymap for `repology--display-packages-mode' buffers.")
+
+(defun repology--show-current-package ()
+  "Display packages associated to package at point."
+  (interactive)
+  (repology-display-package (tabulated-list-get-id)))
+
+(defun repology--check-freedom ()
+  "Check if package or project at point is free."
+  (interactive)
+  (message "Freedom status: %s"
+           (pcase (repology-free-p (tabulated-list-get-id))
+             ('unknown (propertize "Unknown" 'face 'shadow))
+             ('nil (propertize "Non-Free" 'face 'warning))
+             (_ (propertize "Free" 'face 'highlight)))))
+
+(defun repology--show-current-project ()
+  "Display packages associated to project at point."
+  (interactive)
+  (repology-display-packages
+   (repology-project-packages (tabulated-list-get-id))))
+
+(define-derived-mode repology--display-package-mode tabulated-list-mode
+  "Repology/Package"
+  "Major mode used to display packages returned by Repology API.
+\\{tabulated-list-mode-map}"
+  (setq tabulated-list-format [("Field" 15 t) ("Value" 0 t)])
+  (tabulated-list-init-header))
+
+(define-derived-mode repology--display-packages-mode tabulated-list-mode
+  "Repology/Packages"
+  "Major mode used to display packages returned by Repology API.
+\\{repology--display-packages-mode-map}"
+  (setq tabulated-list-format
+        (repology--columns-to-header repology-display-packages-columns))
+  (tabulated-list-init-header))
+
+(define-derived-mode repology--display-projects-mode tabulated-list-mode
+  "Repology/Projects"
+  "Major mode used to display projects returned by Repology API.
+\\{repology--display-projects-mode-map}"
+  (setq tabulated-list-format
+        (repology--columns-to-header repology-display-projects-columns))
+  (tabulated-list-init-header))
+
+(define-derived-mode repology--display-problems-mode tabulated-list-mode
+  "Repology/Problems"
+  "Major mode used to display problems returned by Repology API.
+\\{tabulated-list-mode-map}"
+  (setq tabulated-list-format
+        (repology--columns-to-header repology-display-problems-columns))
+  (tabulated-list-init-header))
+
+(defun repology--value-to-string (value)
+  "Change VALUE object into a string suitable for display."
+  (pcase value
+    (`nil "-")
+    ((pred listp)
+     (mapconcat (lambda (e) (format "%s" e))
+                (seq-uniq value)
+                " "))
+    (_
+     (format "%s" value))))
+
+(defun repology--package-status-face (package)
+  "Return face associated to status from PACKAGE."
+  (let ((status (repology-package-field package 'status)))
+    (alist-get status repology-status-faces 'default nil #'equal)))
+
+(defun repology--make-display (data buffer-name mode format-descriptors)
+  "Display DATA in a buffer named after BUFFER-NAME string.
+DATA is displayed in a major mode derived from `tabulated-list-mode', and set
+by function MODE.  Each entry is identified by the element from DATA, and
+formatted according to FORMAT-DESCRIPTORS function.  This function is called
+with one argument: an element from DATA."
+  (let ((buffer (get-buffer-create buffer-name)))
+    (with-current-buffer buffer
+      (funcall mode)
+      (setq tabulated-list-entries
+            (mapcar (lambda (datum)
+                      (list datum
+                            (apply #'vector
+                                   (funcall format-descriptors datum))))
+                    data))
+      (tabulated-list-print))
+    (pop-to-buffer buffer)))
+
+(defun repology--columns-to-header (specs)
+  "Return vector of column names according to SPECS.
+SPECS is an association list.  Each entry has the form (NAME _ WIDTH SORT)
+where NAME, WIDTH and SORT are of the expected type in 
`tabulated-list-format'."
+  (apply #'vector
+         (mapcar (lambda (format)
+                   (pcase format
+                     (`(,name ,_ ,width ,sort) (list name width sort))
+                     (other
+                      (user-error "Invalid package column format: %S" other))))
+                 specs)))
+
+(defun repology--column-to-descriptor (datum specs &optional symbol-handler)
+  "Return list of descriptors for DATUM according to SPECS.
+
+DATUM is a package, a problem, or a project.  SPECS is an association
+list.  Each entry has the form (_ VALUE _ _).
+
+VALUE is a function called with DATUM as its sole argument.  When VALUE is
+a symbol, and optional argument SYMBOL-HANDLER is a function, SYMBOL-HANDLER
+is called with two arguments: DATUM and VALUE.  In any case, the return value
+is then turned into a string and displayed."
+  (mapcar (lambda (spec)
+            (pcase spec
+              ;; Contents as a function.
+              (`(,_ ,(and (pred functionp) f) ,_ ,_)
+               (repology--value-to-string (funcall f datum)))
+              ;; Contents as a symbol.
+              ((and (guard symbol-handler)
+                    `(,_ ,(and (pred symbolp) field) ,_ ,_))
+               (repology--value-to-string (funcall symbol-handler datum 
field)))
+              ;; Invalid contents.
+              (other (user-error "Invalid format type: %S" other))))
+          specs))
+
+(defun repology--format-field-descriptors (field)
+  "Format an entry from FIELD.
+Format follows `repology-display-packages-columns' specifications.
+Return a list of descriptors."
+  (pcase field
+    (`(,name . ,value)
+     (list (symbol-name name)
+           (repology--value-to-string value) ))
+    (_
+     (error "Invalid field: %S" field))))
+
+(defun repology--format-package-descriptors (package)
+  "Format an entry from PACKAGE.
+Format follows `repology-display-packages-columns' specifications.
+Return a list of descriptors."
+  (repology--column-to-descriptor package
+                                  repology-display-packages-columns
+                                  #'repology-package-field))
+
+(defun repology--format-project-descriptors (project)
+  "Format an entry for PROJECT.
+Format follows `repology-display-packages-columns' specifications.
+Return a list of descriptors."
+  (repology--column-to-descriptor project repology-display-projects-columns))
+
+(defun repology--format-problem-descriptors (problem)
+  "Format an entry from PROBLEM.
+Format follows `repology-display-problems-columns' specifications.
+Return a list of descriptors."
+  (repology--column-to-descriptor problem
+                                  repology-display-problems-columns
+                                  #'repology-problem-field))
+
 (defun repology-display-projects-default (_ selected)
   "Return columns format rules appropriate for projects display.
 SELECTED is a selected repository, i.e., the value of `:inrepo' filter,
@@ -1109,6 +1106,47 @@ Columns are displayed according to 
`repology-display-problems-columns'."
 
 
 ;;; Interactive query
+(defconst repology--main-prompt
+  (format-message
+   "Action: [S]earch projects  [L]ookup project  \
+\[R]eport repository problems    (`q' to quit)")
+  "Main prompt used if `repology' UI.")
+
+(defun repology--select-key (allowed-keys msg)
+  "Keep requesting user to press a key until it belongs to ALLOWED-KEYS.
+ALLOWED-KEYS is a list of characters.  MSG is the message used as the prompt."
+  (let ((key (read-char msg)))
+    (while (not (memq key allowed-keys))
+      (message "Invalid key")
+      (sit-for 0.5)
+      (setq key (read-char msg)))
+    key))
+
+(defun repology--query-y-or-n-p (prompt _)
+  "Ask user a \"y or n\" question, displaying PROMPT.
+Return \"on\" or \"off\"."
+  (if (y-or-n-p prompt) "on" "off"))
+
+(defun repology--query-repository (prompt initial)
+  "Ask user an existing repository by its full name, displaying PROMPT.
+INITIAL is the initial input.  Return a repository internal name."
+  (repology-repository-name
+   (completing-read prompt (repology-list-repositories t) nil t initial)))
+
+(defun repology--query-filter-value (filter initial)
+  "Ask user for FILTER value.
+FILTER is a project filter, as a keyword.  INITIAL is a string inserted as
+a first suggestion, or nil.  Return the answer as a string."
+  (pcase (assq filter repology-project-filters-parameters)
+    (`nil
+     (error "Unknown filter: %S" filter))
+    (`(,_ ,prompt nil)
+     (read-string prompt initial))
+    (`(,_ ,prompt ,(and (pred functionp) collection))
+     (funcall collection prompt initial))
+    (other
+     (error "Invalid value: %S" other))))
+
 ;;;###autoload
 (defun repology ()
   "Query Repology interactively.
@@ -1130,8 +1168,7 @@ This function interacts with Repology API in three ways.  
You can:
    displayed by selecting \"limit\" from the list of properties.  The default
    value is `repology-projects-limit'."
   (interactive)
-  (pcase (read-char "Action: [S]earch projects  [L]ookup project  \
-\[R]eport repository problems")
+  (pcase (repology--select-key '(?s ?S ?l ?L ?r ?R ?q ?Q) 
repology--main-prompt)
     ((or ?r ?R)
      (repology-display-problems
       (repology-report-problems
@@ -1176,7 +1213,10 @@ This function interacts with Repology API in three ways. 
 You can:
                                     (apply #'repology-search-projects query))
                                   ;; Selected repository, or nil.
                                   (plist-get query :inrepo))))
-    (_ (user-error "Unknown answer.  Aborting"))))
+    ((or ?q ?Q)
+     (message "Repology: Quitting"))
+    (_
+     (error "This should not happen"))))
 
 
 (provide 'repology)



reply via email to

[Prev in Thread] Current Thread [Next in Thread]