ui: Rename '_' to 'G_'.
[jackhill/guix/guix.git] / guix / ui.scm
index fb8121c..e551d48 100644 (file)
@@ -1,12 +1,14 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012, 2013, 2014, 2015 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2012, 2013, 2014, 2015, 2016, 2017 Ludovic Courtès <ludo@gnu.org>
 ;;; Copyright © 2013 Mark H Weaver <mhw@netris.org>
 ;;; Copyright © 2013 Nikita Karetnikov <nikita@karetnikov.org>
 ;;; Copyright © 2014 Cyril Roelandt <tipecaml@gmail.com>
 ;;; Copyright © 2014 Cyrill Schenkel <cyrill.schenkel@gmail.com>
-;;; Copyright © 2014, 2015 Alex Kost <alezost@gmail.com>
+;;; Copyright © 2014, 2015, 2017 Alex Kost <alezost@gmail.com>
 ;;; Copyright © 2015 David Thompson <davet@gnu.org>
-;;; Copyright © 2015 Mathieu Lirzin <mthl@openmailbox.org>
+;;; Copyright © 2015, 2016 Mathieu Lirzin <mthl@gnu.org>
+;;; Copyright © 2016 Roel Janssen <roel@gnu.org>
+;;; Copyright © 2016 Benz Schenk <benz.schenk@uzh.ch>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
 ;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
 
 (define-module (guix ui)
+  #:use-module (guix gexp)
   #:use-module (guix utils)
   #:use-module (guix store)
   #:use-module (guix config)
   #:use-module (guix packages)
   #:use-module (guix profiles)
   #:use-module (guix derivations)
+  #:use-module (guix combinators)
   #:use-module (guix build-system)
   #:use-module (guix serialization)
   #:use-module ((guix build utils) #:select (mkdir-p))
   #:use-module ((guix licenses) #:select (license? license-name))
+  #:use-module ((guix build syscalls) #:select (terminal-columns))
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-11)
   #:use-module (srfi srfi-19)
@@ -50,7 +55,7 @@
   #:use-module (texinfo)
   #:use-module (texinfo plain-text)
   #:use-module (texinfo string-utils)
-  #:export (_
+  #:export (G_
             N_
             P_
             report-error
@@ -60,6 +65,7 @@
             warn-about-load-error
             show-version-and-exit
             show-bug-report-information
+            make-regexp*
             string->number*
             size->number
             show-derivation-outputs
             read/eval
             read/eval-package-expression
             location->string
-            switch-symlinks
             config-directory
             fill-paragraph
             texi->plain-text
             package-description-string
+            package-synopsis-string
             string->recutils
             package->recutils
             package-specification->name+version+output
             string->generations
             string->duration
+            matching-generations
+            display-generation
+            display-profile-content
+            display-profile-content-diff
+            roll-back*
+            switch-to-generation*
+            delete-generation*
             run-guix-command
             run-guix
             program-name
             guix-warning-port
             warning
+            info
             guix-main))
 
 ;;; Commentary:
   ;; Text domain for package synopses and descriptions.
   "guix-packages")
 
-(define _ (cut gettext <> %gettext-domain))
+(define G_ (cut gettext <> %gettext-domain))
 (define N_ (cut ngettext <> <> <> %gettext-domain))
 
 (define (P_ msgid)
@@ -126,7 +140,7 @@ messages."
       (syntax-case x ()
         ((name (underscore fmt) args (... ...))
          (and (string? (syntax->datum #'fmt))
-              (free-identifier=? #'underscore #'_))
+              (free-identifier=? #'underscore #'G_))
          (with-syntax ((fmt*   (augmented-format-string #'fmt))
                        (prefix (datum->syntax x prefix)))
            #'(format (guix-warning-port) (gettext fmt*)
@@ -145,6 +159,7 @@ messages."
                      args (... ...))))))))
 
 (define-diagnostic warning "warning: ") ; emit a warning
+(define-diagnostic info "")
 
 (define-diagnostic report-error "error: ")
 (define-syntax-rule (leave args ...)
@@ -223,7 +238,7 @@ messages."
            (case on-error
              ((debug)
               (newline)
-              (display (_ "entering debugger; type ',bt' for a backtrace\n"))
+              (display (G_ "entering debugger; type ',bt' for a backtrace\n"))
               (start-repl #:debug (make-debug (stack->vector stack) 0
                                               (error-string frame args)
                                               #f)))
@@ -237,35 +252,43 @@ messages."
   "Report the failure to load FILE, a user-provided Scheme file.
 ARGS is the list of arguments received by the 'throw' handler."
   (match args
-    (('system-error . _)
+    (('system-error . rest)
      (let ((err (system-error-errno args)))
-       (report-error (_ "failed to load '~a': ~a~%") file (strerror err))))
+       (report-error (G_ "failed to load '~a': ~a~%") file (strerror err))))
     (('syntax-error proc message properties form . rest)
      (let ((loc (source-properties->location properties)))
-       (format (current-error-port) (_ "~a: error: ~a~%")
+       (format (current-error-port) (G_ "~a: error: ~a~%")
                (location->string loc) message)))
     (('srfi-34 obj)
-     (report-error (_ "exception thrown: ~s~%") obj))
+     (if (message-condition? obj)
+         (report-error (G_ "~a~%")
+                       (gettext (condition-message obj)
+                                %gettext-domain))
+         (report-error (G_ "exception thrown: ~s~%") obj)))
     ((error args ...)
-     (report-error (_ "failed to load '~a':~%") file)
+     (report-error (G_ "failed to load '~a':~%") file)
      (apply display-error frame (current-error-port) args))))
 
 (define (warn-about-load-error file args)         ;FIXME: factorize with ↑
   "Report the failure to load FILE, a user-provided Scheme file, without
 exiting.  ARGS is the list of arguments received by the 'throw' handler."
   (match args
-    (('system-error . _)
+    (('system-error . rest)
      (let ((err (system-error-errno args)))
-       (warning (_ "failed to load '~a': ~a~%") file (strerror err))))
+       (warning (G_ "failed to load '~a': ~a~%") file (strerror err))))
     (('syntax-error proc message properties form . rest)
      (let ((loc (source-properties->location properties)))
-       (format (current-error-port) (_ "~a: warning: ~a~%")
+       (format (current-error-port) (G_ "~a: warning: ~a~%")
                (location->string loc) message)))
     (('srfi-34 obj)
-     (warning (_ "failed to load '~a': exception thrown: ~s~%")
-              file obj))
+     (if (message-condition? obj)
+         (warning (G_ "failed to load '~a': ~a~%")
+                  file
+                  (gettext (condition-message obj) %gettext-domain))
+         (warning (G_ "failed to load '~a': exception thrown: ~s~%")
+                  file obj)))
     ((error args ...)
-     (warning (_ "failed to load '~a':~%") file)
+     (warning (G_ "failed to load '~a':~%") file)
      (apply display-error #f (current-error-port) args))))
 
 (define (install-locale)
@@ -274,7 +297,7 @@ exiting.  ARGS is the list of arguments received by the 'throw' handler."
     (lambda _
       (setlocale LC_ALL ""))
     (lambda args
-      (warning (_ "failed to install locale: ~a~%")
+      (warning (G_ "failed to install locale: ~a~%")
                (strerror (system-error-errno args))))))
 
 (define (initialize-guix)
@@ -293,7 +316,13 @@ exiting.  ARGS is the list of arguments received by the 'throw' handler."
   "Display version information for COMMAND and `(exit 0)'."
   (simple-format #t "~a (~a) ~a~%"
                  command %guix-package-name %guix-version)
-  (display (_ "Copyright (C) 2015 the Guix authors
+  (format #t "Copyright ~a 2017 ~a"
+          ;; TRANSLATORS: Translate "(C)" to the copyright symbol
+          ;; (C-in-a-circle), if this symbol is available in the user's
+          ;; locale.  Otherwise, do not translate "(C)"; leave it as-is.  */
+          (G_ "(C)")
+          (G_ "the Guix authors\n"))
+  (display (G_"\
 License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
 This is free software: you are free to change and redistribute it.
 There is NO WARRANTY, to the extent permitted by law.
@@ -301,52 +330,66 @@ There is NO WARRANTY, to the extent permitted by law.
   (exit 0))
 
 (define (show-bug-report-information)
-  (format #t (_ "
+  ;; TRANSLATORS: The placeholder indicates the bug-reporting address for this
+  ;; package.  Please add another line saying "Report translation bugs to
+  ;; ...\n" with the address for translation bugs (typically your translation
+  ;; team's web or email address).
+  (format #t (G_ "
 Report bugs to: ~a.") %guix-bug-report-address)
-  (format #t (_ "
+  (format #t (G_ "
 ~a home page: <~a>") %guix-package-name %guix-home-page-url)
-  (display (_ "
+  (display (G_ "
 General help using GNU software: <http://www.gnu.org/gethelp/>"))
   (newline))
 
+(define (augmented-system-error-handler file)
+  "Return a 'system-error' handler that mentions FILE in its message."
+  (lambda (key proc fmt args errno)
+    ;; Augment the FMT and ARGS with information about TARGET (this
+    ;; information is missing as of Guile 2.0.11, making the exception
+    ;; uninformative.)
+    (apply throw key proc "~A: ~S"
+           (list (strerror (car errno)) file)
+           (list errno))))
+
+(define-syntax-rule (error-reporting-wrapper proc (args ...) file)
+  "Wrap PROC such that its 'system-error' exceptions are augmented to mention
+FILE."
+  (let ((real-proc (@ (guile) proc)))
+    (lambda (args ...)
+      (catch 'system-error
+        (lambda ()
+          (real-proc args ...))
+        (augmented-system-error-handler file)))))
+
 (set! symlink
   ;; We 'set!' the global binding because (gnu build ...) modules and similar
   ;; typically don't use (guix ui).
-  (let ((real-symlink (@ (guile) symlink)))
-    (lambda (target link)
-      "This is a 'symlink' replacement that provides proper error reporting."
-      (catch 'system-error
-        (lambda ()
-          (real-symlink target link))
-        (lambda (key proc fmt args errno)
-          ;; Augment the FMT and ARGS with information about LINK (this
-          ;; information is missing as of Guile 2.0.11, making the exception
-          ;; uninformative.)
-          (apply throw key proc "~A: ~S"
-                 (list (strerror (car errno)) link)
-                 (list errno)))))))
+  (error-reporting-wrapper symlink (source target) target))
 
 (set! copy-file
   ;; Note: here we use 'set!', not #:replace, because UIs typically use
   ;; 'copy-recursively', which doesn't use (guix ui).
-  (let ((real-copy-file (@ (guile) copy-file)))
-    (lambda (source target)
-      "This is a 'copy-file' replacement that provides proper error reporting."
-      (catch 'system-error
-        (lambda ()
-          (real-copy-file source target))
-        (lambda (key proc fmt args errno)
-          ;; Augment the FMT and ARGS with information about TARGET (this
-          ;; information is missing as of Guile 2.0.11, making the exception
-          ;; uninformative.)
-          (apply throw key proc "~A: ~S"
-                 (list (strerror (car errno)) target)
-                 (list errno)))))))
+  (error-reporting-wrapper copy-file (source target) target))
+
+(set! canonicalize-path
+  (error-reporting-wrapper canonicalize-path (file) file))
+
+
+(define (make-regexp* regexp . flags)
+  "Like 'make-regexp' but error out if REGEXP is invalid, reporting the error
+nicely."
+  (catch 'regular-expression-syntax
+    (lambda ()
+      (apply make-regexp regexp flags))
+    (lambda (key proc message . rest)
+      (leave (G_ "'~a' is not a valid regular expression: ~a~%")
+             regexp message))))
 
 (define (string->number* str)
   "Like `string->number', but error out with an error message on failure."
   (or (string->number str)
-      (leave (_ "~a: invalid number~%") str)))
+      (leave (G_ "~a: invalid number~%") str)))
 
 (define (size->number str)
   "Convert STR, a storage measurement representation such as \"1024\" or
@@ -363,7 +406,7 @@ interpreted."
                      str))
          (num    (string->number numstr)))
     (unless num
-      (leave (_ "invalid number: ~a~%") numstr))
+      (leave (G_ "invalid number: ~a~%") numstr))
 
     ((compose inexact->exact round)
      (* num
@@ -385,11 +428,17 @@ interpreted."
           ("ZB"  (expt 10 21))
           ("YB"  (expt 10 24))
           (""    1)
-          (_
-           (leave (_ "unknown unit: ~a~%") unit)))))))
+          (x
+           (leave (G_ "unknown unit: ~a~%") unit)))))))
 
 (define (call-with-error-handling thunk)
   "Call THUNK within a user-friendly error handler."
+  (define (port-filename* port)
+    ;; 'port-filename' returns #f for non-file ports, but it raises an
+    ;; exception for file ports that are closed.  Work around that.
+    (and (not (port-closed? port))
+         (port-filename port)))
+
   (guard (c ((package-input-error? c)
              (let* ((package  (package-error-package c))
                     (input    (package-error-invalid-input c))
@@ -397,53 +446,62 @@ interpreted."
                     (file     (location-file location))
                     (line     (location-line location))
                     (column   (location-column location)))
-               (leave (_ "~a:~a:~a: package `~a' has an invalid input: ~s~%")
+               (leave (G_ "~a:~a:~a: package `~a' has an invalid input: ~s~%")
                       file line column
                       (package-full-name package) input)))
             ((package-cross-build-system-error? c)
              (let* ((package (package-error-package c))
                     (loc     (package-location package))
                     (system  (package-build-system package)))
-               (leave (_ "~a: ~a: build system `~a' does not support cross builds~%")
+               (leave (G_ "~a: ~a: build system `~a' does not support cross builds~%")
                       (location->string loc)
                       (package-full-name package)
                       (build-system-name system))))
+            ((gexp-input-error? c)
+             (let ((input (package-error-invalid-input c)))
+               (leave (G_ "~s: invalid G-expression input~%")
+                      (gexp-error-invalid-input c))))
             ((profile-not-found-error? c)
-             (leave (_ "profile '~a' does not exist~%")
+             (leave (G_ "profile '~a' does not exist~%")
                     (profile-error-profile c)))
             ((missing-generation-error? c)
-             (leave (_ "generation ~a of profile '~a' does not exist~%")
+             (leave (G_ "generation ~a of profile '~a' does not exist~%")
                     (missing-generation-error-generation c)
                     (profile-error-profile c)))
             ((nar-error? c)
              (let ((file (nar-error-file c))
                    (port (nar-error-port c)))
                (if file
-                   (leave (_ "corrupt input while restoring '~a' from ~s~%")
-                          file (or (port-filename port) port))
-                   (leave (_ "corrupt input while restoring archive from ~s~%")
-                          (or (port-filename port) port)))))
+                   (leave (G_ "corrupt input while restoring '~a' from ~s~%")
+                          file (or (port-filename* port) port))
+                   (leave (G_ "corrupt input while restoring archive from ~s~%")
+                          (or (port-filename* port) port)))))
             ((nix-connection-error? c)
-             (leave (_ "failed to connect to `~a': ~a~%")
+             (leave (G_ "failed to connect to `~a': ~a~%")
                     (nix-connection-error-file c)
                     (strerror (nix-connection-error-code c))))
             ((nix-protocol-error? c)
              ;; FIXME: Server-provided error messages aren't i18n'd.
-             (leave (_ "build failed: ~a~%")
+             (leave (G_ "build failed: ~a~%")
                     (nix-protocol-error-message c)))
             ((derivation-missing-output-error? c)
-             (leave (_ "reference to invalid output '~a' of derivation '~a'~%")
+             (leave (G_ "reference to invalid output '~a' of derivation '~a'~%")
                     (derivation-missing-output c)
                     (derivation-file-name (derivation-error-derivation c))))
+            ((file-search-error? c)
+             (leave (G_ "file '~a' could not be found in these \
+directories:~{ ~a~}~%")
+                    (file-search-error-file-name c)
+                    (file-search-error-search-path c)))
             ((message-condition? c)
              ;; Normally '&message' error conditions have an i18n'd message.
-             (leave (_ "~a~%")
+             (leave (G_ "~a~%")
                     (gettext (condition-message c) %gettext-domain))))
     ;; Catch EPIPE and the likes.
     (catch 'system-error
       thunk
       (lambda (key proc format-string format-args . rest)
-        (leave (_ "~a: ~a~%") proc
+        (leave (G_ "~a: ~a~%") proc
                (apply format #f format-string format-args))))))
 
 (define-syntax-rule (leave-on-EPIPE exp ...)
@@ -478,18 +536,22 @@ similar."
                (lambda ()
                  (call-with-input-string str read))
                (lambda args
-                 (leave (_ "failed to read expression ~s: ~s~%")
+                 (leave (G_ "failed to read expression ~s: ~s~%")
                         str args)))))
     (catch #t
       (lambda ()
         (eval exp (force %guix-user-module)))
       (lambda args
-        (report-error (_ "failed to evaluate expression '~a':~%") exp)
+        (report-error (G_ "failed to evaluate expression '~a':~%") exp)
         (match args
           (('syntax-error proc message properties form . rest)
-           (report-error (_ "syntax error: ~a~%") message))
+           (report-error (G_ "syntax error: ~a~%") message))
           (('srfi-34 obj)
-           (report-error (_ "exception thrown: ~s~%") obj))
+           (if (message-condition? obj)
+               (report-error (G_ "~a~%")
+                             (gettext (condition-message obj)
+                                      %gettext-domain))
+               (report-error (G_ "exception thrown: ~s~%") obj)))
           ((error args ...)
            (apply display-error #f (current-error-port) args))
           (what? #f))
@@ -500,8 +562,8 @@ similar."
 error."
   (match (read/eval str)
     ((? package? p) p)
-    (_
-     (leave (_ "expression ~s does not evaluate to a package~%")
+    (x
+     (leave (G_ "expression ~s does not evaluate to a package~%")
             str))))
 
 (define (show-derivation-outputs derivation)
@@ -513,17 +575,18 @@ error."
                (derivation-outputs derivation))))
 
 (define* (show-what-to-build store drv
-                             #:key dry-run? (use-substitutes? #t))
+                             #:key dry-run? (use-substitutes? #t)
+                             (mode (build-mode normal)))
   "Show what will or would (depending on DRY-RUN?) be built in realizing the
-derivations listed in DRV.  Return #t if there's something to build, #f
-otherwise.  When USE-SUBSTITUTES?, check and report what is prerequisites are
-available for download."
+derivations listed in DRV using MODE, a 'build-mode' value.  Return #t if
+there's something to build, #f otherwise.  When USE-SUBSTITUTES?, check and
+report what is prerequisites are available for download."
   (define substitutable?
     ;; Call 'substitutation-oracle' upfront so we don't end up launching the
     ;; substituter many times.  This makes a big difference, especially when
     ;; DRV is a long list as is the case with 'guix environment'.
     (if use-substitutes?
-        (substitution-oracle store drv)
+        (substitution-oracle store drv #:mode mode)
         (const #f)))
 
   (define (built-or-substitutable? drv)
@@ -537,6 +600,7 @@ available for download."
                           (let-values (((b d)
                                         (derivation-prerequisites-to-build
                                          store drv
+                                         #:mode mode
                                          #:substitutable? substitutable?)))
                             (values (append b build)
                                     (append d download))))
@@ -589,16 +653,17 @@ available for download."
 (define (right-arrow port)
   "Return either a string containing the 'RIGHT ARROW' character, or an ASCII
 replacement if PORT is not Unicode-capable."
-  (with-fluids ((%default-port-encoding (port-encoding port)))
-    (let ((arrow "→"))
-      (catch 'encoding-error
-        (lambda ()
-          (call-with-output-string
-            (lambda (port)
-              (set-port-conversion-strategy! port 'error)
-              (display arrow port))))
-        (lambda (key . args)
-          "->")))))
+  (let ((encoding (port-encoding port))
+        (arrow "→"))
+    (catch 'encoding-error
+      (lambda ()
+        (call-with-output-string
+          (lambda (port)
+            (set-port-encoding! port encoding)
+            (set-port-conversion-strategy! port 'error)
+            (display arrow port))))
+      (lambda (key . args)
+        "->"))))
 
 (define* (show-manifest-transaction store manifest transaction
                                     #:key dry-run?)
@@ -641,7 +706,7 @@ replacement if PORT is not Unicode-capable."
                          "The following packages will be removed:~%~{~a~%~}~%"
                          len)
                      remove))))
-      (_ #f))
+      (x #f))
     (match downgrade
       (((($ <manifest-entry> name old-version)
          . ($ <manifest-entry> _ new-version output item)) ..1)
@@ -659,7 +724,7 @@ replacement if PORT is not Unicode-capable."
                          "The following packages will be downgraded:~%~{~a~%~}~%"
                          len)
                      downgrade))))
-      (_ #f))
+      (x #f))
     (match upgrade
       (((($ <manifest-entry> name old-version)
          . ($ <manifest-entry> _ new-version output item)) ..1)
@@ -677,7 +742,7 @@ replacement if PORT is not Unicode-capable."
                          "The following packages will be upgraded:~%~{~a~%~}~%"
                          len)
                      upgrade))))
-      (_ #f))
+      (x #f))
     (match install
       ((($ <manifest-entry> name version output item _) ..1)
        (let ((len     (length name))
@@ -693,7 +758,7 @@ replacement if PORT is not Unicode-capable."
                          "The following packages will be installed:~%~{~a~%~}~%"
                          len)
                      install))))
-      (_ #f))))
+      (x #f))))
 
 (define-syntax with-error-handling
   (syntax-rules ()
@@ -706,17 +771,10 @@ replacement if PORT is not Unicode-capable."
 (define (location->string loc)
   "Return a human-friendly, GNU-standard representation of LOC."
   (match loc
-    (#f (_ "<unknown location>"))
+    (#f (G_ "<unknown location>"))
     (($ <location> file line column)
      (format #f "~a:~a:~a" file line column))))
 
-(define (switch-symlinks link target)
-  "Atomically switch LINK, a symbolic link, to point to TARGET.  Works
-both when LINK already exists and when it does not."
-  (let ((pivot (string-append link ".new")))
-    (symlink target pivot)
-    (rename-file pivot link)))
-
 (define (config-directory)
   "Return the name of the configuration directory, after making sure that it
 exists.  Honor the XDG specs,
@@ -732,7 +790,7 @@ exists.  Honor the XDG specs,
       (lambda args
         (let ((err (system-error-errno args)))
           ;; ERR is necessarily different from EEXIST.
-          (leave (_ "failed to create configuration directory `~a': ~a~%")
+          (leave (G_ "failed to create configuration directory `~a': ~a~%")
                  dir (strerror err)))))))
 
 (define* (fill-paragraph str width #:optional (column 0))
@@ -781,7 +839,7 @@ converted to a space; sequences of more than one line break are preserved."
   (match (string-fold maybe-break
                       `(,column 0 ())
                       str)
-    ((_ _ chars)
+    ((column newlines chars)
      (list->string (reverse chars)))))
 
 \f
@@ -790,8 +848,7 @@ converted to a space; sequences of more than one line break are preserved."
 ;;;
 
 (define %text-width
-  (make-parameter (or (and=> (getenv "WIDTH") string->number)
-                      80)))
+  (make-parameter (terminal-columns)))
 
 (set! (@@ (texinfo plain-text) wrap*)
       ;; XXX: Monkey patch this private procedure to let 'package->recutils'
@@ -809,10 +866,18 @@ converted to a space; sequences of more than one line break are preserved."
   (with-fluids ((%default-port-encoding "UTF-8"))
     (stexi->plain-text (texi-fragment->stexi str))))
 
+(define (package-field-string package field-accessor)
+  "Return a plain-text representation of PACKAGE field."
+  (and=> (field-accessor package)
+         (compose texi->plain-text P_)))
+
 (define (package-description-string package)
   "Return a plain-text representation of PACKAGE description field."
-  (and=> (package-description package)
-         (compose texi->plain-text P_)))
+  (package-field-string package package-description))
+
+(define (package-synopsis-string package)
+  "Return a plain-text representation of PACKAGE synopsis field."
+  (package-field-string package package-synopsis))
 
 (define (string->recutils str)
   "Return a version of STR where newlines have been replaced by newlines
@@ -829,11 +894,16 @@ followed by \"+ \", which makes for a valid multi-line field value in the
 (define* (package->recutils p port #:optional (width (%text-width)))
   "Write to PORT a `recutils' record of package P, arranging to fit within
 WIDTH columns."
+  (define width*
+    ;; The available number of columns once we've taken into account space for
+    ;; the initial "+ " prefix.
+    (if (> width 2) (- width 2) width))
+
   (define (dependencies->recutils packages)
     (let ((list (string-join (map package-full-name
                                   (sort packages package<?)) " ")))
       (string->recutils
-       (fill-paragraph list width
+       (fill-paragraph list width*
                        (string-length "dependencies: ")))))
 
   (define (package<? p1 p2)
@@ -851,7 +921,7 @@ WIDTH columns."
              (dependencies->recutils (filter package? inputs)))))
   (format port "location: ~a~%"
           (or (and=> (package-location p) location->string)
-              (_ "unknown")))
+              (G_ "unknown")))
 
   ;; Note: Starting from version 1.6 or recutils, hyphens are not allowed in
   ;; field identifiers.
@@ -865,17 +935,17 @@ WIDTH columns."
             ((? license? license)
              (license-name license))
             (x
-             (_ "unknown"))))
+             (G_ "unknown"))))
   (format port "synopsis: ~a~%"
           (string-map (match-lambda
                        (#\newline #\space)
                        (chr       chr))
-                      (or (and=> (package-synopsis p) P_)
+                      (or (and=> (package-synopsis-string p) P_)
                           "")))
   (format port "~a~2%"
           (string->recutils
            (string-trim-right
-            (parameterize ((%text-width width))
+            (parameterize ((%text-width width*))
               (texi->plain-text
                (string-append "description: "
                               (or (and=> (package-description p) P_)
@@ -932,7 +1002,15 @@ following patterns: \"1d\", \"1w\", \"1m\"."
     (make-time time-duration 0
                (* 3600 hours (string->number (match:substring match 1)))))
 
-  (cond ((string-match "^([0-9]+)d$" str)
+  (cond ((string-match "^([0-9]+)s$" str)
+         =>
+         (lambda (match)
+           (make-time time-duration 0
+                      (string->number (match:substring match 1)))))
+        ((string-match "^([0-9]+)h$" str)
+         (lambda (match)
+           (hours->duration 1 match)))
+        ((string-match "^([0-9]+)d$" str)
          =>
          (lambda (match)
            (hours->duration 24 match)))
@@ -946,6 +1024,148 @@ following patterns: \"1d\", \"1w\", \"1m\"."
            (hours->duration (* 24 30) match)))
         (else #f)))
 
+(define* (matching-generations str profile
+                               #:key (duration-relation <=))
+  "Return the list of available generations matching a pattern in STR.  See
+'string->generations' and 'string->duration' for the list of valid patterns.
+When STR is a duration pattern, return all the generations whose ctime has
+DURATION-RELATION with the current time."
+  (define (valid-generations lst)
+    (define (valid-generation? n)
+      (any (cut = n <>) (generation-numbers profile)))
+
+    (fold-right (lambda (x acc)
+                  (if (valid-generation? x)
+                      (cons x acc)
+                      acc))
+                '()
+                lst))
+
+  (define (filter-generations generations)
+    (match generations
+      (() '())
+      (('>= n)
+       (drop-while (cut > n <>)
+                   (generation-numbers profile)))
+      (('<= n)
+       (valid-generations (iota n 1)))
+      ((lst ..1)
+       (valid-generations lst))
+      (x #f)))
+
+  (define (filter-by-duration duration)
+    (define (time-at-midnight time)
+      ;; Return TIME at midnight by setting nanoseconds, seconds, minutes, and
+      ;; hours to zeros.
+      (let ((d (time-utc->date time)))
+         (date->time-utc
+          (make-date 0 0 0 0
+                     (date-day d) (date-month d)
+                     (date-year d) (date-zone-offset d)))))
+
+    (define generation-ctime-alist
+      (map (lambda (number)
+             (cons number
+                   (time-second
+                    (time-at-midnight
+                     (generation-time profile number)))))
+           (generation-numbers profile)))
+
+    (match duration
+      (#f #f)
+      (res
+       (let ((s (time-second
+                 (subtract-duration (time-at-midnight (current-time))
+                                    duration))))
+         (delete #f (map (lambda (x)
+                           (and (duration-relation s (cdr x))
+                                (first x)))
+                         generation-ctime-alist))))))
+
+  (cond ((string->generations str)
+         =>
+         filter-generations)
+        ((string->duration str)
+         =>
+         filter-by-duration)
+        (else #f)))
+
+(define (display-generation profile number)
+  "Display a one-line summary of generation NUMBER of PROFILE."
+  (unless (zero? number)
+    (let ((header (format #f (G_ "Generation ~a\t~a") number
+                          (date->string
+                           (time-utc->date
+                            (generation-time profile number))
+                           "~b ~d ~Y ~T")))
+          (current (generation-number profile)))
+      (if (= number current)
+          ;; TRANSLATORS: The word "current" here is an adjective for
+          ;; "Generation", as in "current generation".  Use the appropriate
+          ;; gender where applicable.
+          (format #t (G_ "~a\t(current)~%") header)
+          (format #t "~a~%" header)))))
+
+(define (display-profile-content-diff profile gen1 gen2)
+  "Display the changed packages in PROFILE GEN2 compared to generation GEN2."
+
+  (define (equal-entry? first second)
+    (string= (manifest-entry-item first) (manifest-entry-item second)))
+
+  (define (display-entry entry prefix)
+    (match entry
+      (($ <manifest-entry> name version output location _)
+       (format #t " ~a ~a\t~a\t~a\t~a~%" prefix name version output location))))
+
+  (define (list-entries number)
+    (manifest-entries (profile-manifest (generation-file-name profile number))))
+
+  (define (display-diff profile old new)
+    (display-generation profile new)
+    (let ((added (lset-difference
+                  equal-entry? (list-entries new) (list-entries old)))
+          (removed (lset-difference
+                    equal-entry? (list-entries old) (list-entries new))))
+      (for-each (cut display-entry <> "+") added)
+      (for-each (cut display-entry <> "-") removed)
+      (newline)))
+
+  (display-diff profile gen1 gen2))
+
+(define (display-profile-content profile number)
+  "Display the packages in PROFILE, generation NUMBER, in a human-readable
+way."
+  (for-each (match-lambda
+              (($ <manifest-entry> name version output location _)
+               (format #t "  ~a\t~a\t~a\t~a~%"
+                       name version output location)))
+
+            ;; Show most recently installed packages last.
+            (reverse
+             (manifest-entries
+              (profile-manifest (generation-file-name profile number))))))
+
+(define (display-generation-change previous current)
+  (format #t (G_ "switched from generation ~a to ~a~%") previous current))
+
+(define (roll-back* store profile)
+  "Like 'roll-back', but display what is happening."
+  (call-with-values
+      (lambda ()
+        (roll-back store profile))
+    display-generation-change))
+
+(define (switch-to-generation* profile number)
+  "Like 'switch-generation', but display what is happening."
+  (let ((previous (switch-to-generation profile number)))
+    (display-generation-change previous number)))
+
+(define (delete-generation* store profile generation)
+  "Like 'delete-generation', but display what is going on."
+  (format #t (G_ "deleting ~a~%")
+          (generation-file-name profile generation))
+  (delete-generation store profile generation))
+
 (define* (package-specification->name+version+output spec
                                                      #:optional (output "out"))
   "Parse package specification SPEC and return three value: the specified
@@ -953,9 +1173,9 @@ package name, version number (or #f), and output name (or OUTPUT).  SPEC may
 optionally contain a version number and an output name, as in these examples:
 
   guile
-  guile-2.0.9
+  guile@2.0.9
   guile:debug
-  guile-2.0.9:debug
+  guile@2.0.9:debug
 "
   (let*-values (((name sub-drv)
                  (match (string-rindex spec #\:)
@@ -973,7 +1193,7 @@ optionally contain a version number and an output name, as in these examples:
 
 (define (show-guix-usage)
   (format (current-error-port)
-          (_ "Try `guix --help' for more information.~%"))
+          (G_ "Try `guix --help' for more information.~%"))
   (exit 1))
 
 (define (command-files)
@@ -998,12 +1218,13 @@ optionally contain a version number and an output name, as in these examples:
 
 (define (show-guix-help)
   (define (internal? command)
-    (member command '("substitute" "authenticate" "offload")))
+    (member command '("substitute" "authenticate" "offload"
+                      "perform-download")))
 
-  (format #t (_ "Usage: guix COMMAND ARGS...
+  (format #t (G_ "Usage: guix COMMAND ARGS...
 Run COMMAND with ARGS.\n"))
   (newline)
-  (format #t (_ "COMMAND must be one of the sub-commands listed below:\n"))
+  (format #t (G_ "COMMAND must be one of the sub-commands listed below:\n"))
   (newline)
   ;; TODO: Display a synopsis of each command.
   (format #t "~{   ~a~%~}" (sort (remove internal? (commands))
@@ -1023,13 +1244,15 @@ found."
         (resolve-interface `(guix scripts ,command)))
       (lambda -
         (format (current-error-port)
-                (_ "guix: ~a: command not found~%") command)
+                (G_ "guix: ~a: command not found~%") command)
         (show-guix-usage))))
 
   (let ((command-main (module-ref module
                                   (symbol-append 'guix- command))))
     (parameterize ((program-name command))
-      (apply command-main args))))
+      ;; Disable canonicalization so we don't don't stat unreasonably.
+      (with-fluids ((%file-port-name-canonicalization #f))
+        (apply command-main args)))))
 
 (define (run-guix . args)
   "Run the 'guix' command defined by command line ARGS.
@@ -1040,7 +1263,7 @@ and signal handling has already been set up."
   (match args
     (()
      (format (current-error-port)
-             (_ "guix: missing command name~%"))
+             (G_ "guix: missing command name~%"))
      (show-guix-usage))
     ((or ("-h") ("--help"))
      (show-guix-help))
@@ -1048,8 +1271,11 @@ and signal handling has already been set up."
      (show-version-and-exit "guix"))
     (((? option? o) args ...)
      (format (current-error-port)
-             (_ "guix: unrecognized option '~a'~%") o)
+             (G_ "guix: unrecognized option '~a'~%") o)
      (show-guix-usage))
+    (("help" command)
+     (apply run-guix-command (string->symbol command)
+            '("--help")))
     (("help" args ...)
      (show-guix-help))
     ((command args ...)