gnu: emacs-telega: Properly install alists.
[jackhill/guix/guix.git] / guix / ui.scm
index 4be31db..540671f 100644 (file)
   #:use-module (guix derivations)
   #:use-module (guix build-system)
   #:use-module (guix serialization)
-  #:use-module ((guix licenses) #:select (license? license-name))
+  #:use-module ((guix licenses)
+                #:select (license? license-name license-uri))
   #:use-module ((guix build syscalls)
-                #:select (free-disk-space terminal-columns
-                                          terminal-rows))
+                #:select (free-disk-space terminal-columns terminal-rows
+                          with-file-lock/no-wait))
   #:use-module ((guix build utils)
                 ;; XXX: All we need are the bindings related to
                 ;; '&invoke-error'.  However, to work around the bug described
@@ -69,6 +70,7 @@
   #:autoload   (system base compile) (compile-file)
   #:autoload   (system repl repl)  (start-repl)
   #:autoload   (system repl debug) (make-debug stack->vector)
+  #:autoload   (web uri) (encode-and-join-uri-path)
   #:use-module (texinfo)
   #:use-module (texinfo plain-text)
   #:use-module (texinfo string-utils)
             package->recutils
             package-specification->name+version+output
 
+            supports-hyperlinks?
+            hyperlink
+            file-hyperlink
+            location->hyperlink
+
             relevance
             package-relevance
             display-search-results
 
+            with-profile-lock
             string->generations
             string->duration
             matching-generations
             roll-back*
             switch-to-generation*
             delete-generation*
+
+            %default-message-language
+            current-message-language
+
             run-guix-command
             run-guix
             guix-main))
@@ -363,7 +375,7 @@ ARGS is the list of arguments received by the 'throw' handler."
        (report-error loc (G_ "~a~%") message)))
     (('unbound-variable _ ...)
      (report-unbound-variable-error args #:frame frame))
-    (('srfi-34 obj)
+    (((or 'srfi-34 '%exception) obj)
      (if (message-condition? obj)
          (report-error (and (error-location? obj)
                             (error-location obj))
@@ -395,7 +407,7 @@ exiting.  ARGS is the list of arguments received by the 'throw' handler."
        (warning loc (G_ "~a~%") message)))
     (('unbound-variable _ ...)
      (report-unbound-variable-error args))
-    (('srfi-34 obj)
+    (((or 'srfi-34 '%exception) obj)
      (if (message-condition? obj)
          (warning (G_ "failed to load '~a': ~a~%")
                   file
@@ -428,6 +440,20 @@ exiting.  ARGS is the list of arguments received by the 'throw' handler."
 report them in a user-friendly way."
   (call-with-unbound-variable-handling (lambda () exp ...)))
 
+(define %default-message-language
+  ;; Default language to use for messages.
+  (make-parameter "en"))
+
+(define (current-message-language)
+  "Return the language used for messages according to the current locale.
+Return %DEFAULT-MESSAGE-LANGUAGE if that information could not be obtained.  The
+result is an ISO-639-2 language code such as \"ar\", without the territory
+part."
+  (let ((locale (setlocale LC_MESSAGES)))
+    (match (string-index locale #\_)
+      (#f    locale)
+      (index (string-take locale index)))))
+
 (define (install-locale)
   "Install the current locale settings."
   (catch 'system-error
@@ -790,7 +816,7 @@ similar."
         (match args
           (('syntax-error proc message properties form . rest)
            (report-error (G_ "syntax error: ~a~%") message))
-          (('srfi-34 obj)
+          (((or 'srfi-34 '%exception) obj)
            (if (message-condition? obj)
                (report-error (G_ "~a~%")
                              (gettext (condition-message obj)
@@ -849,6 +875,17 @@ warning."
     ('profile-hook #t)
     (_ #f)))
 
+(define (colorize-store-file-name file)
+  "Colorize FILE, a store file name, such that the hash part is less prominent
+that the rest."
+  (let ((len    (string-length file))
+        (prefix (+ (string-length (%store-prefix)) 32 2)))
+    (if (< len prefix)
+        file
+        (string-append (colorize-string (string-take file prefix)
+                                        (color DARK))
+                       (string-drop file prefix)))))
+
 (define* (show-what-to-build store drv
                              #:key dry-run? (use-substitutes? #t)
                              (mode (build-mode normal)))
@@ -872,6 +909,11 @@ check and report what is prerequisites are available for download."
         (substitution-oracle store inputs #:mode mode)
         (const #f)))
 
+  (define colorized-store-item
+    (if (color-output? (current-error-port))
+        colorize-store-file-name
+        identity))
+
   (let*-values (((build download)
                  (derivation-build-plan store inputs
                                         #:mode mode
@@ -917,7 +959,7 @@ check and report what is prerequisites are available for download."
                   (N_ "~:[The following derivation would be built:~%~{   ~a~%~}~;~]"
                       "~:[The following derivations would be built:~%~{   ~a~%~}~;~]"
                       (length build))
-                  (null? build) build)
+                  (null? build) (map colorized-store-item build))
           (if display-download-size?
               (format (current-error-port)
                       ;; TRANSLATORS: "MB" is for "megabyte"; it should be
@@ -925,29 +967,31 @@ check and report what is prerequisites are available for download."
                       (G_ "~:[~,1h MB would be downloaded:~%~{   ~a~%~}~;~]")
                       (null? download)
                       download-size
-                      (map substitutable-path download))
+                      (map (compose colorized-store-item substitutable-path)
+                           download))
               (format (current-error-port)
                       (N_ "~:[The following file would be downloaded:~%~{   ~a~%~}~;~]"
                           "~:[The following files would be downloaded:~%~{   ~a~%~}~;~]"
                           (length download))
                       (null? download)
-                      (map substitutable-path download)))
+                      (map (compose colorized-store-item substitutable-path)
+                           download)))
           (format (current-error-port)
                   (N_ "~:[The following graft would be made:~%~{   ~a~%~}~;~]"
                       "~:[The following grafts would be made:~%~{   ~a~%~}~;~]"
                       (length graft))
-                  (null? graft) graft)
+                  (null? graft) (map colorized-store-item graft))
           (format (current-error-port)
                   (N_ "~:[The following profile hook would be built:~%~{   ~a~%~}~;~]"
                       "~:[The following profile hooks would be built:~%~{   ~a~%~}~;~]"
                       (length hook))
-                  (null? hook) hook))
+                  (null? hook) (map colorized-store-item hook)))
         (begin
           (format (current-error-port)
                   (N_ "~:[The following derivation will be built:~%~{   ~a~%~}~;~]"
                       "~:[The following derivations will be built:~%~{   ~a~%~}~;~]"
                       (length build))
-                  (null? build) build)
+                  (null? build) (map colorized-store-item build))
           (if display-download-size?
               (format (current-error-port)
                       ;; TRANSLATORS: "MB" is for "megabyte"; it should be
@@ -955,23 +999,25 @@ check and report what is prerequisites are available for download."
                       (G_ "~:[~,1h MB will be downloaded:~%~{   ~a~%~}~;~]")
                       (null? download)
                       download-size
-                      (map substitutable-path download))
+                      (map (compose colorized-store-item substitutable-path)
+                           download))
               (format (current-error-port)
                       (N_ "~:[The following file will be downloaded:~%~{   ~a~%~}~;~]"
                           "~:[The following files will be downloaded:~%~{   ~a~%~}~;~]"
                           (length download))
                       (null? download)
-                      (map substitutable-path download)))
+                      (map (compose colorized-store-item substitutable-path)
+                           download)))
           (format (current-error-port)
                   (N_ "~:[The following graft will be made:~%~{   ~a~%~}~;~]"
                       "~:[The following grafts will be made:~%~{   ~a~%~}~;~]"
                       (length graft))
-                  (null? graft) graft)
+                  (null? graft) (map colorized-store-item graft))
           (format (current-error-port)
                   (N_ "~:[The following profile hook will be built:~%~{   ~a~%~}~;~]"
                       "~:[The following profile hooks will be built:~%~{   ~a~%~}~;~]"
                       (length hook))
-                  (null? hook) hook)))
+                  (null? hook) (map colorized-store-item hook))))
 
     (check-available-space installed-size)
 
@@ -1196,10 +1242,46 @@ followed by \"+ \", which makes for a valid multi-line field value in the
                       '()
                       str)))
 
+(define (hyperlink uri text)
+  "Return a string that denotes a hyperlink using an OSC escape sequence as
+documented at
+<https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>."
+  (string-append "\x1b]8;;" uri "\x1b\\"
+                 text "\x1b]8;;\x1b\\"))
+
+(define* (supports-hyperlinks? #:optional (port (current-output-port)))
+  "Return true if PORT is a terminal that supports hyperlink escapes."
+  ;; Note that terminals are supposed to ignore OSC escapes they don't
+  ;; understand (this is the case of xterm as of version 349, for instance.)
+  ;; However, Emacs comint as of 26.3 does not ignore it and instead lets it
+  ;; through, hence the 'INSIDE_EMACS' special case below.
+  (and (isatty?* port)
+       (not (getenv "INSIDE_EMACS"))))
+
+(define* (file-hyperlink file #:optional (text file))
+  "Return TEXT with escapes for a hyperlink to FILE."
+  (hyperlink (string-append "file://" (gethostname)
+                            (encode-and-join-uri-path
+                             (string-split file #\/)))
+             text))
+
+(define (location->hyperlink location)
+  "Return a string corresponding to LOCATION, with escapes for a hyperlink."
+  (let ((str  (location->string location))
+        (file (if (string-prefix? "/" (location-file location))
+                  (location-file location)
+                  (search-path %load-path (location-file location)))))
+    (if file
+        (file-hyperlink file str)
+        str)))
+
 (define* (package->recutils p port #:optional (width (%text-width))
-                            #:key (extra-fields '()))
+                            #:key
+                            (hyperlinks? (supports-hyperlinks? port))
+                            (extra-fields '()))
   "Write to PORT a `recutils' record of package P, arranging to fit within
-WIDTH columns.  EXTRA-FIELDS is a list of symbol/value pairs to emit."
+WIDTH columns.  EXTRA-FIELDS is a list of symbol/value pairs to emit.  When
+HYPERLINKS? is true, emit hyperlink escape sequences when appropriate."
   (define width*
     ;; The available number of columns once we've taken into account space for
     ;; the initial "+ " prefix.
@@ -1227,7 +1309,8 @@ WIDTH columns.  EXTRA-FIELDS is a list of symbol/value pairs to emit."
             (((labels inputs . _) ...)
              (dependencies->recutils (filter package? inputs)))))
   (format port "location: ~a~%"
-          (or (and=> (package-location p) location->string)
+          (or (and=> (package-location p)
+                     (if hyperlinks? location->hyperlink location->string))
               (G_ "unknown")))
 
   ;; Note: Starting from version 1.6 or recutils, hyphens are not allowed in
@@ -1240,7 +1323,11 @@ WIDTH columns.  EXTRA-FIELDS is a list of symbol/value pairs to emit."
              (string-join (map license-name licenses)
                           ", "))
             ((? license? license)
-             (license-name license))
+             (let ((text (license-name license))
+                   (uri  (license-uri license)))
+               (if (and hyperlinks? uri (string-prefix? "http" uri))
+                   (hyperlink uri text)
+                   text)))
             (x
              (G_ "unknown"))))
   (format port "synopsis: ~a~%"
@@ -1360,11 +1447,13 @@ them.  If PORT is a terminal, print at most a full screen of results."
   (let loop ((matches matches))
     (match matches
       (((package . score) rest ...)
-       (let ((text (call-with-output-string
-                     (lambda (port)
-                       (print package port
-                              #:extra-fields
-                              `((relevance . ,score)))))))
+       (let* ((links? (supports-hyperlinks? port))
+              (text   (call-with-output-string
+                        (lambda (port)
+                          (print package port
+                                 #:hyperlinks? links?
+                                 #:extra-fields
+                                 `((relevance . ,score)))))))
          (if (and max-rows
                   (> (port-line port) first-line) ;print at least one result
                   (> (+ 4 (line-count text) (port-line port))
@@ -1526,17 +1615,22 @@ DURATION-RELATION with the current time."
 (define (display-generation profile number)
   "Display a one-line summary of generation NUMBER of PROFILE."
   (unless (zero? number)
-    (let ((header (format #f (highlight (G_ "Generation ~a\t~a")) number
-                          (date->string
-                           (time-utc->date
-                            (generation-time profile number))
-                           ;; TRANSLATORS: This is a format-string for date->string.
-                           ;; Please choose a format that corresponds to the
-                           ;; usual way of presenting dates in your locale.
-                           ;; See https://www.gnu.org/software/guile/manual/html_node/SRFI_002d19-Date-to-string.html
-                           ;; for details.
-                           (G_ "~b ~d ~Y ~T"))))
-          (current (generation-number profile)))
+    (let* ((file   (generation-file-name profile number))
+           (link   (if (supports-hyperlinks?)
+                       (cut file-hyperlink file <>)
+                       identity))
+           (header (format #f (link (highlight (G_ "Generation ~a\t~a")))
+                           number
+                           (date->string
+                            (time-utc->date
+                             (generation-time profile number))
+                            ;; TRANSLATORS: This is a format-string for date->string.
+                            ;; Please choose a format that corresponds to the
+                            ;; usual way of presenting dates in your locale.
+                            ;; See https://www.gnu.org/software/guile/manual/html_node/SRFI_002d19-Date-to-string.html
+                            ;; for details.
+                            (G_ "~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
@@ -1570,6 +1664,26 @@ DURATION-RELATION with the current time."
 
   (display-diff profile gen1 gen2))
 
+(define (profile-lock-handler profile errno . _)
+  "Handle failure to acquire PROFILE's lock."
+  ;; NFS mounts can return ENOLCK.  When that happens, there's not much that
+  ;; can be done, so warn the user and keep going.
+  (if (= errno ENOLCK)
+      (warning (G_ "cannot lock profile ~a: ~a~%")
+               profile (strerror errno))
+      (leave (G_ "profile ~a is locked by another process~%")
+             profile)))
+
+(define profile-lock-file
+  (cut string-append <> ".lock"))
+
+(define-syntax-rule (with-profile-lock profile exp ...)
+  "Grab PROFILE's lock and evaluate EXP...  Call 'leave' if the lock is
+already taken."
+  (with-file-lock/no-wait (profile-lock-file profile)
+    (cut profile-lock-handler profile <...>)
+    exp ...))
+
 (define (display-profile-content profile number)
   "Display the packages in PROFILE, generation NUMBER, in a human-readable
 way."