|
|
|
|
;; SPDX-FileCopyrightText: 2021 Xinglu Chen <public@yoctocell.xyz>
|
|
|
|
|
;; SPDX-FileCopyrightText: 2021 Andrew Tropin <andrew@trop.in>
|
|
|
|
|
;;
|
|
|
|
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
|
|
|
|
(define-module (rosenthal utils home-services-utils)
|
|
|
|
|
#:use-module (gnu services configuration)
|
|
|
|
|
#:use-module (gnu home services utils)
|
|
|
|
|
#:use-module (guix ui)
|
|
|
|
|
#:use-module (guix diagnostics)
|
|
|
|
|
#:use-module (guix gexp)
|
|
|
|
|
#:use-module (guix monads)
|
|
|
|
|
#:use-module (guix i18n)
|
|
|
|
|
#:use-module (guix profiles)
|
|
|
|
|
#:use-module (guix packages)
|
|
|
|
|
#:use-module (guix build-system trivial)
|
|
|
|
|
#:use-module (guix deprecation)
|
|
|
|
|
|
|
|
|
|
#:use-module (ice-9 curried-definitions)
|
|
|
|
|
#:use-module (ice-9 match)
|
|
|
|
|
#:use-module (ice-9 string-fun)
|
|
|
|
|
#:use-module (srfi srfi-1)
|
|
|
|
|
#:use-module (srfi srfi-9)
|
|
|
|
|
#:use-module (srfi srfi-171)
|
|
|
|
|
#:use-module (srfi srfi-26)
|
|
|
|
|
#:use-module (srfi srfi-43)
|
|
|
|
|
|
|
|
|
|
#:re-export (filter-configuration-fields
|
|
|
|
|
|
|
|
|
|
list-of
|
|
|
|
|
|
|
|
|
|
list-of-strings?
|
|
|
|
|
alist?
|
|
|
|
|
text-config?
|
|
|
|
|
serialize-text-config
|
|
|
|
|
generic-serialize-alist-entry
|
|
|
|
|
generic-serialize-alist
|
|
|
|
|
|
|
|
|
|
maybe-object->string
|
|
|
|
|
object->snake-case-string
|
|
|
|
|
object->camel-case-string)
|
|
|
|
|
|
|
|
|
|
#:export (slurp-file-gexp
|
|
|
|
|
|
|
|
|
|
alist-entry->mixed-text
|
|
|
|
|
boolean->yes-or-no
|
|
|
|
|
boolean->true-or-false
|
|
|
|
|
list->human-readable-list
|
|
|
|
|
|
|
|
|
|
ini-config?
|
|
|
|
|
generic-serialize-ini-config
|
|
|
|
|
generic-serialize-git-ini-config
|
|
|
|
|
|
|
|
|
|
yaml-config?
|
|
|
|
|
serialize-yaml-config
|
|
|
|
|
|
|
|
|
|
string-or-gexp?
|
|
|
|
|
serialize-string-or-gexp
|
|
|
|
|
|
|
|
|
|
gexp-text-config?
|
|
|
|
|
serialize-gexp-text-config
|
|
|
|
|
|
|
|
|
|
rest
|
|
|
|
|
maybe-list
|
|
|
|
|
optional
|
|
|
|
|
wrap-package
|
|
|
|
|
|
|
|
|
|
define-enum
|
|
|
|
|
enum-name
|
|
|
|
|
enum-value))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;;
|
|
|
|
|
;;; User's utils.
|
|
|
|
|
;;;
|
|
|
|
|
|
|
|
|
|
(define* (slurp-file-gexp file #:key (encoding "UTF-8"))
|
|
|
|
|
"Returns a gexp, which reads all the content of the FILE and returns
|
|
|
|
|
it as a string. FILE must be a file-like object."
|
|
|
|
|
(when (not (file-like? file))
|
|
|
|
|
(raise (formatted-message
|
|
|
|
|
(G_ "~a is not a file-like object.")
|
|
|
|
|
file)))
|
|
|
|
|
#~(call-with-input-file #$file
|
|
|
|
|
(@ (ice-9 textual-ports) get-string-all)
|
|
|
|
|
#:encoding #$encoding))
|
|
|
|
|
|
|
|
|
|
(define-deprecated/alias slurp-file-gexp slurp-file-like)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;;
|
|
|
|
|
;;; Configuration related helpers.
|
|
|
|
|
;;;
|
|
|
|
|
|
|
|
|
|
(define* ((alist-entry->mixed-text prefix sep #:optional (suffix "\n"))
|
|
|
|
|
alist-entry)
|
|
|
|
|
"Create a list from ALIST-ENTRY, which can be used with
|
|
|
|
|
@code{mixed-text-file} for example to create key-value configuration
|
|
|
|
|
file or shell script.
|
|
|
|
|
|
|
|
|
|
PREFIX is the string to prefix the key-value pair with. For example,
|
|
|
|
|
@code{\"export\"} will return @code{'(\"export\" KEY SEP VALUE)},
|
|
|
|
|
where KEY is the first element of ALIST-ENTRY, and VALUE is the second
|
|
|
|
|
element of ALIST-ENTRY.
|
|
|
|
|
|
|
|
|
|
SEP is the separator between the key and the value.
|
|
|
|
|
|
|
|
|
|
SUFFIX is the optional argument, default to newline.
|
|
|
|
|
|
|
|
|
|
Different things will happen depending on the value of VALUE:
|
|
|
|
|
@itemize @bullet
|
|
|
|
|
@item If VALUE is #f, ignore everything in the entry and just return
|
|
|
|
|
an empty list.
|
|
|
|
|
|
|
|
|
|
@item If VALUE is #t or not provided (empty list), ignore the VALUE
|
|
|
|
|
and SEP and just return a list of PREFIX and KEY followed by a
|
|
|
|
|
SUFFIX.
|
|
|
|
|
|
|
|
|
|
@item If VALUE is a flat list, it will get added to the resulting
|
|
|
|
|
list. If not flat the exception will be raised.
|
|
|
|
|
|
|
|
|
|
@item If VALUE is not a list (string, file-like object, etc), return a
|
|
|
|
|
list of PREFIX, KEY, SEP and VALUE followed by a SUFFIX.
|
|
|
|
|
|
|
|
|
|
The following code
|
|
|
|
|
@lisp
|
|
|
|
|
((alist-entry->mixed-text \"export \" \"=\") '(\"EDITOR\" . \"emacsclient\"))
|
|
|
|
|
((alist-entry->mixed-text \"export \" \"=\") '(\"EDITOR\" . #t))
|
|
|
|
|
((alist-entry->mixed-text \"export \" \"=\") '(\"EDITOR\"))
|
|
|
|
|
((alist-entry->mixed-text \"export \" \"=\") '(\"EDITOR\" . #f))
|
|
|
|
|
((alist-entry->mixed-text \"export \" \"=\") '(\"EDITOR\" . (\"emacsclient\" \"vim\")))
|
|
|
|
|
@end lisp
|
|
|
|
|
|
|
|
|
|
would yield
|
|
|
|
|
|
|
|
|
|
@example
|
|
|
|
|
(\"export \" \"EDITOR\" \"=\" \"emacsclient\" \"\n\")
|
|
|
|
|
(\"export \" \"EDITOR\" \"\n\")
|
|
|
|
|
(\"export \" \"EDITOR\" \"\n\")
|
|
|
|
|
()
|
|
|
|
|
(\"export \" \"EDITOR\" \"=\" \"emacsclient\" \"vim\" \"\n\")
|
|
|
|
|
@end example"
|
|
|
|
|
(define (not-alist-entry-error)
|
|
|
|
|
(raise (formatted-message
|
|
|
|
|
(G_ "~a has to be an association list entry")
|
|
|
|
|
alist-entry)))
|
|
|
|
|
(match alist-entry
|
|
|
|
|
((key . value)
|
|
|
|
|
(let* ((values (cond
|
|
|
|
|
((eq? value #f)
|
|
|
|
|
#f)
|
|
|
|
|
((or (eq? value #t) (null? value))
|
|
|
|
|
'(""))
|
|
|
|
|
((list? value)
|
|
|
|
|
(if (any list? value)
|
|
|
|
|
(raise (formatted-message
|
|
|
|
|
(G_ "~a is not a flat list")
|
|
|
|
|
value))
|
|
|
|
|
value))
|
|
|
|
|
(else
|
|
|
|
|
(list value))))
|
|
|
|
|
(sep (if (eq? values '(""))
|
|
|
|
|
""
|
|
|
|
|
sep)))
|
|
|
|
|
(if values
|
|
|
|
|
`(,prefix ,key ,sep ,@values ,suffix)
|
|
|
|
|
'())))
|
|
|
|
|
(_ (not-alist-entry-error))))
|
|
|
|
|
|
|
|
|
|
(define* (boolean->yes-or-no bool #:optional (capitalize? #f))
|
|
|
|
|
"Convert a boolean BOOL to \"yes\" or \"no\".
|
|
|
|
|
Setting CAPITALIZE? to @code{#t} will capitalize the word, it is set to
|
|
|
|
|
@code{#f} by default."
|
|
|
|
|
(let ((word (if (eq? bool #t) "yes" "no")))
|
|
|
|
|
(if capitalize?
|
|
|
|
|
(string-capitalize word)
|
|
|
|
|
word)))
|
|
|
|
|
|
|
|
|
|
(define* (boolean->true-or-false bool #:optional (capitalize? #f))
|
|
|
|
|
"Convert a boolean BOOL to \"true\" or \"false\".
|
|
|
|
|
Setting CAPITALIZE? to @code{#t} will capitalize the word, it is set to
|
|
|
|
|
@code{#f} by default."
|
|
|
|
|
(let ((word (if bool "true" "false")))
|
|
|
|
|
(if capitalize?
|
|
|
|
|
(string-capitalize word)
|
|
|
|
|
word)))
|
|
|
|
|
|
|
|
|
|
;; TODO: Remove once upstreamed
|
|
|
|
|
(define* (list->human-readable-list lst
|
|
|
|
|
#:key
|
|
|
|
|
(cumulative? #f)
|
|
|
|
|
(proc identity))
|
|
|
|
|
"Turn a list LST into a sequence of terms readable by humans.
|
|
|
|
|
If CUMULATIVE? is @code{#t}, use ``and'', otherwise use ``or'' before
|
|
|
|
|
the last term.
|
|
|
|
|
|
|
|
|
|
PROC is a procedure to apply to each of the elements of a list before
|
|
|
|
|
turning them into a single human readable string.
|
|
|
|
|
|
|
|
|
|
@example
|
|
|
|
|
(list->human-readable-list '(1 4 9) #:cumulative? #t #:proc sqrt)
|
|
|
|
|
@result{} \"1, 2, and 3\"
|
|
|
|
|
@end example
|
|
|
|
|
|
|
|
|
|
yields:"
|
|
|
|
|
(let* ((word (if cumulative? "and " "or "))
|
|
|
|
|
(init (append (drop-right lst 1))))
|
|
|
|
|
(format #f "~a" (string-append
|
|
|
|
|
(string-join
|
|
|
|
|
(map (compose maybe-object->string proc) init)
|
|
|
|
|
", " 'suffix)
|
|
|
|
|
word
|
|
|
|
|
(maybe-object->string (proc (last lst)))))))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;;
|
|
|
|
|
;;; Serializers.
|
|
|
|
|
;;;
|
|
|
|
|
|
|
|
|
|
(define ini-config? list?)
|
|
|
|
|
(define (generic-serialize-ini-config-section section proc)
|
|
|
|
|
"Format a section from SECTION for an INI configuration.
|
|
|
|
|
Apply the procedure PROC on SECTION after it has been converted to a string"
|
|
|
|
|
(format #f "[~a]\n" (proc section)))
|
|
|
|
|
|
|
|
|
|
(define default-ini-format-section
|
|
|
|
|
(match-lambda
|
|
|
|
|
((section subsection)
|
|
|
|
|
(string-append (maybe-object->string section) " "
|
|
|
|
|
(maybe-object->string subsection)))
|
|
|
|
|
(section
|
|
|
|
|
(maybe-object->string section))))
|
|
|
|
|
|
|
|
|
|
(define* (generic-serialize-ini-config
|
|
|
|
|
#:key
|
|
|
|
|
(combine-ini string-join)
|
|
|
|
|
(combine-alist string-append)
|
|
|
|
|
(combine-section-alist string-append)
|
|
|
|
|
(format-section default-ini-format-section)
|
|
|
|
|
serialize-field
|
|
|
|
|
fields)
|
|
|
|
|
"Create an INI configuration from nested lists FIELDS. This uses
|
|
|
|
|
@code{generic-serialize-ini-config-section} and @{generic-serialize-alist} to
|
|
|
|
|
serialize the section and the association lists, respectively.
|
|
|
|
|
|
|
|
|
|
@example
|
|
|
|
|
(generic-serialize-ini-config
|
|
|
|
|
#:serialize-field (lambda (a b) (format #f \"~a = ~a\n\" a b))
|
|
|
|
|
#:format-section (compose string-capitalize symbol->string)
|
|
|
|
|
#:fields '((application ((key . value)))))
|
|
|
|
|
@end example
|
|
|
|
|
|
|
|
|
|
@result{} \"[Application]\nkey = value\n\""
|
|
|
|
|
(combine-ini
|
|
|
|
|
(map (match-lambda
|
|
|
|
|
((section alist)
|
|
|
|
|
(combine-section-alist
|
|
|
|
|
(generic-serialize-ini-config-section section format-section)
|
|
|
|
|
(generic-serialize-alist combine-alist serialize-field alist))))
|
|
|
|
|
fields)
|
|
|
|
|
"\n"))
|
|
|
|
|
|
|
|
|
|
(define* (generic-serialize-git-ini-config
|
|
|
|
|
#:key
|
|
|
|
|
(combine-ini string-join)
|
|
|
|
|
(combine-alist string-append)
|
|
|
|
|
(combine-section-alist string-append)
|
|
|
|
|
(format-section default-ini-format-section)
|
|
|
|
|
serialize-field
|
|
|
|
|
fields)
|
|
|
|
|
"Like @code{generic-serialize-ini-config}, but the section can also
|
|
|
|
|
have a @dfn{subsection}. FORMAT-SECTION will take a list of two
|
|
|
|
|
elements: the section and the subsection."
|
|
|
|
|
(combine-ini
|
|
|
|
|
(map (match-lambda
|
|
|
|
|
((section subsection alist)
|
|
|
|
|
(combine-section-alist
|
|
|
|
|
(generic-serialize-ini-config-section
|
|
|
|
|
(list section subsection) format-section)
|
|
|
|
|
(generic-serialize-alist combine-alist serialize-field alist)))
|
|
|
|
|
((section alist)
|
|
|
|
|
(combine-section-alist
|
|
|
|
|
(generic-serialize-ini-config-section section format-section)
|
|
|
|
|
(generic-serialize-alist combine-alist serialize-field alist))))
|
|
|
|
|
fields)
|
|
|
|
|
"\n"))
|
|
|
|
|
|
|
|
|
|
(define yaml-config? list?)
|
|
|
|
|
(define (make-yaml-indent depth)
|
|
|
|
|
(make-string (* 2 depth) #\space))
|
|
|
|
|
|
|
|
|
|
(define ((serialize-yaml-value depth) value)
|
|
|
|
|
(let* ((tab (make-yaml-indent depth)))
|
|
|
|
|
(cond
|
|
|
|
|
((string? value)
|
|
|
|
|
(list (format #f "'~a'" value)))
|
|
|
|
|
((boolean? value)
|
|
|
|
|
(list (format #f "~a" (if value "true" "false"))))
|
|
|
|
|
((file-like? value)
|
|
|
|
|
(list value))
|
|
|
|
|
((alist? value)
|
|
|
|
|
(serialize-yaml-alist value #:depth (1+ depth)))
|
|
|
|
|
((vector? value)
|
|
|
|
|
(serialize-yaml-vector value #:depth depth))
|
|
|
|
|
(else (list (format #f "~a" value))))))
|
|
|
|
|
|
|
|
|
|
(define ((serialize-yaml-key depth) key)
|
|
|
|
|
(when (vector? key)
|
|
|
|
|
(raise (formatted-message
|
|
|
|
|
(G_ "Vector as key value are not supported by serializer, \
|
|
|
|
|
try to avoid them. ~a") key)))
|
|
|
|
|
((serialize-yaml-value depth) key))
|
|
|
|
|
|
|
|
|
|
(define ((serialize-yaml-key-value depth) key value)
|
|
|
|
|
(let ((tab (make-yaml-indent depth)))
|
|
|
|
|
`("\n"
|
|
|
|
|
,tab
|
|
|
|
|
,@((serialize-yaml-key depth) key) ": "
|
|
|
|
|
,@((serialize-yaml-value depth) value))))
|
|
|
|
|
|
|
|
|
|
(define ((serialize-yaml-vector-elem depth) elem)
|
|
|
|
|
(let ((tab (make-yaml-indent (1+ depth))))
|
|
|
|
|
(cons*
|
|
|
|
|
"\n" tab "- "
|
|
|
|
|
((serialize-yaml-value (1+ depth)) elem))))
|
|
|
|
|
|
|
|
|
|
(define* (serialize-yaml-vector vec #:key (depth 0))
|
|
|
|
|
(append-map (serialize-yaml-vector-elem depth) (vector->list vec)))
|
|
|
|
|
|
|
|
|
|
(define* (serialize-yaml-alist lst #:key (depth 0))
|
|
|
|
|
(generic-serialize-alist append (serialize-yaml-key-value depth) lst))
|
|
|
|
|
|
|
|
|
|
(define (serialize-yaml-config config)
|
|
|
|
|
"Simplified yaml serializer, which supports only a subset of yaml, use
|
|
|
|
|
it with caution."
|
|
|
|
|
(serialize-yaml-alist config))
|
|
|
|
|
|
|
|
|
|
(define (string-or-gexp? sg) (or (string? sg) (gexp? sg)))
|
|
|
|
|
(define (serialize-string-or-gexp field-name val) "")
|
|
|
|
|
|
|
|
|
|
;; Guix proper has a different version of text-config.
|
|
|
|
|
(define (gexp-text-config? config)
|
|
|
|
|
(and (list? config) (every string-or-gexp? config)))
|
|
|
|
|
(define (serialize-gexp-text-config field-name val)
|
|
|
|
|
#~(string-append #$@(interpose val "\n" 'suffix)))
|
|
|
|
|
|
|
|
|
|
;;;
|
|
|
|
|
;;; Miscellaneous.
|
|
|
|
|
;;;
|
|
|
|
|
|
|
|
|
|
(define rest cdr)
|
|
|
|
|
|
|
|
|
|
;; Confusing with maybe-list type.
|
|
|
|
|
(define (maybe-list a)
|
|
|
|
|
"If A is a list, return it, otherwise return a singleton list with A."
|
|
|
|
|
(if (list? a)
|
|
|
|
|
a
|
|
|
|
|
(list a)))
|
|
|
|
|
|
|
|
|
|
;; If EXPR1 evaluates to a non-@code{#f} value and EXPR2 is specified,
|
|
|
|
|
;; return EXPR2; if it isn't specified, return EXPR1. Otherwise, return
|
|
|
|
|
;; an empty list @code{'()}.
|
|
|
|
|
(define-syntax optional
|
|
|
|
|
(syntax-rules ()
|
|
|
|
|
((_ expr1)
|
|
|
|
|
(if expr1 expr1 '()))
|
|
|
|
|
((_ expr1 expr2)
|
|
|
|
|
(if expr1 expr2 '()))))
|
|
|
|
|
|
|
|
|
|
(define (wrap-package pkg executable-name gexp)
|
|
|
|
|
"Create a @code{<package>} object that is a wrapper for PACKAGE, and
|
|
|
|
|
runs GEXP. NAME is the name of the executable that will be put in the store."
|
|
|
|
|
(let* ((wrapper-name (string-append executable-name "-wrapper"))
|
|
|
|
|
(wrapper (program-file wrapper-name gexp)))
|
|
|
|
|
(package
|
|
|
|
|
(inherit pkg)
|
|
|
|
|
(name wrapper-name)
|
|
|
|
|
(source wrapper)
|
|
|
|
|
(propagated-inputs `((,(package-name pkg) ,pkg)))
|
|
|
|
|
(build-system trivial-build-system)
|
|
|
|
|
(arguments
|
|
|
|
|
`(#:modules
|
|
|
|
|
((guix build utils))
|
|
|
|
|
#:builder
|
|
|
|
|
(begin
|
|
|
|
|
(use-modules (guix build utils)
|
|
|
|
|
(srfi srfi-1))
|
|
|
|
|
(let* ((bin (string-append %output "/bin"))
|
|
|
|
|
(wrapper (assoc-ref %build-inputs "source")))
|
|
|
|
|
(mkdir-p bin)
|
|
|
|
|
(copy-file wrapper (string-append bin "/" ,executable-name)))))))))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;;
|
|
|
|
|
;;; Enums.
|
|
|
|
|
;;;
|
|
|
|
|
|
|
|
|
|
(define-record-type <enum>
|
|
|
|
|
(make-enum name value)
|
|
|
|
|
enum?
|
|
|
|
|
(name enum-name)
|
|
|
|
|
(value enum-value))
|
|
|
|
|
|
|
|
|
|
;; Copied from (gnu services configuration)
|
|
|
|
|
(define-syntax-rule (id ctx parts ...)
|
|
|
|
|
"Assemble PARTS into a raw (unhygienic) identifier."
|
|
|
|
|
(datum->syntax ctx (symbol-append (syntax->datum parts) ...)))
|
|
|
|
|
|
|
|
|
|
;; (define-enum pinentry-flavor
|
|
|
|
|
;; '(emacs gtk qt ncurses tty))
|
|
|
|
|
;;
|
|
|
|
|
;; (pinentry-flavor? 'gtk)
|
|
|
|
|
;; => #t
|
|
|
|
|
;;
|
|
|
|
|
;; (enum-value pinentry-flavor)
|
|
|
|
|
;; => '(emacs gtk qt ncurses tty)
|
|
|
|
|
;;
|
|
|
|
|
;; (pinentry-flavor? 'vim)
|
|
|
|
|
;; exception: `pinetry-flavor' must be one of `emacs', `gtk', `qt',
|
|
|
|
|
;; `ncurses', or `tty', was given `vim'
|
|
|
|
|
|
|
|
|
|
(define-syntax define-enum
|
|
|
|
|
(lambda (x)
|
|
|
|
|
(syntax-case x ()
|
|
|
|
|
((_ stem value)
|
|
|
|
|
(with-syntax ((stem? (id #'stem #'stem #'?))
|
|
|
|
|
(msg (list->human-readable-list
|
|
|
|
|
(second (syntax->datum #'value))
|
|
|
|
|
#:proc (cut format #f "`~a'" <>))))
|
|
|
|
|
#'(begin
|
|
|
|
|
(define stem (make-enum (quote stem) value))
|
|
|
|
|
|
|
|
|
|
(define (stem? val)
|
|
|
|
|
(if (member val value)
|
|
|
|
|
#t
|
|
|
|
|
(raise (formatted-message
|
|
|
|
|
(G_ "`~a' must of ~a, was given: ~s")
|
|
|
|
|
(enum-name stem)
|
|
|
|
|
(syntax->datum msg)
|
|
|
|
|
val))))))))))
|