You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

441 lines
14 KiB

;; 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))))))))))