Rosenthal/rosenthal/utils/home-services-utils.scm
Hilton Chain b61194a46b
Import home-services-utils from rde.
* rosenthal/utils/home-services-utils.scm: New file imported from rde.
<https://git.sr.ht/~abcdw/rde/tree/master/item/gnu/home-services-utils.scm>
2022-11-25 19:14:59 +08:00

442 lines
14 KiB
Scheme
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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