b57c656f72
* gnu/home/services/xdg.scm (ensure-xdg-base-dirs-on-activation): Skip mkdir XDG_RUNTIME_DIR in activation script. Signed-off-by: Ludovic Courtès <ludo@gnu.org>
489 lines
18 KiB
Scheme
489 lines
18 KiB
Scheme
;;; GNU Guix --- Functional package management for GNU
|
||
;;; Copyright © 2021 Andrew Tropin <andrew@trop.in>
|
||
;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz>
|
||
;;;
|
||
;;; This file is part of GNU Guix.
|
||
;;;
|
||
;;; GNU Guix is free software; you can redistribute it and/or modify it
|
||
;;; under the terms of the GNU General Public License as published by
|
||
;;; the Free Software Foundation; either version 3 of the License, or (at
|
||
;;; your option) any later version.
|
||
;;;
|
||
;;; GNU Guix is distributed in the hope that it will be useful, but
|
||
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
;;; GNU General Public License for more details.
|
||
;;;
|
||
;;; You should have received a copy of the GNU General Public License
|
||
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
(define-module (gnu home services xdg)
|
||
#:use-module (gnu services configuration)
|
||
#:use-module (gnu home services)
|
||
#:use-module (gnu packages freedesktop)
|
||
#:use-module (gnu home services utils)
|
||
#:use-module (guix gexp)
|
||
#:use-module (guix modules)
|
||
#:use-module (guix records)
|
||
#:use-module (guix i18n)
|
||
#:use-module (guix diagnostics)
|
||
|
||
#:use-module (ice-9 match)
|
||
#:use-module (srfi srfi-1)
|
||
#:use-module (rnrs enums)
|
||
|
||
#:export (home-xdg-base-directories-service-type
|
||
home-xdg-base-directories-configuration
|
||
home-xdg-base-directories-configuration?
|
||
|
||
home-xdg-user-directories-service-type
|
||
home-xdg-user-directories-configuration
|
||
home-xdg-user-directories-configuration?
|
||
|
||
xdg-desktop-action
|
||
xdg-desktop-entry
|
||
home-xdg-mime-applications-service-type
|
||
home-xdg-mime-applications-configuration))
|
||
|
||
;;; Commentary:
|
||
;;
|
||
;; This module contains services related to XDG directories and
|
||
;; applications.
|
||
;;
|
||
;; - XDG base directories
|
||
;; - XDG user directories
|
||
;; - XDG MIME applications
|
||
;;
|
||
;;; Code:
|
||
|
||
|
||
;;;
|
||
;;; XDG base directories.
|
||
;;;
|
||
|
||
(define (serialize-path field-name val) "")
|
||
(define path? string?)
|
||
|
||
(define-configuration home-xdg-base-directories-configuration
|
||
(cache-home
|
||
(path "$HOME/.cache")
|
||
"Base directory for programs to store user-specific non-essential
|
||
(cached) data. Files in this directory can be deleted anytime without
|
||
loss of important data.")
|
||
(config-home
|
||
(path "$HOME/.config")
|
||
"Base directory for programs to store configuration files.
|
||
Some programs store here log or state files, but it's not desired,
|
||
this directory should contain static configurations.")
|
||
(data-home
|
||
(path "$HOME/.local/share")
|
||
"Base directory for programs to store architecture independent
|
||
read-only shared data, analogus to @file{/usr/share}, but for user.")
|
||
(runtime-dir
|
||
(path "${XDG_RUNTIME_DIR:-/run/user/$UID}")
|
||
"Base directory for programs to store user-specific runtime files,
|
||
like sockets.")
|
||
(log-home
|
||
(path "$HOME/.local/var/log")
|
||
"Base directory for programs to store log files, analogus to
|
||
@file{/var/log}, but for user. It is not a part of XDG Base Directory
|
||
Specification, but helps to make implementation of home services more
|
||
consistent.")
|
||
(state-home
|
||
(path "$HOME/.local/var/lib")
|
||
"Base directory for programs to store state files, like databases,
|
||
analogus to @file{/var/lib}, but for user. It is not a part of XDG
|
||
Base Directory Specification, but helps to make implementation of home
|
||
services more consistent."))
|
||
|
||
(define (home-xdg-base-directories-environment-variables-service config)
|
||
(map
|
||
(lambda (field)
|
||
(cons (format
|
||
#f "XDG_~a"
|
||
(object->snake-case-string (configuration-field-name field) 'upper))
|
||
((configuration-field-getter field) config)))
|
||
home-xdg-base-directories-configuration-fields))
|
||
|
||
(define (ensure-xdg-base-dirs-on-activation config)
|
||
#~(map (lambda (xdg-base-dir-variable)
|
||
((@ (guix build utils) mkdir-p)
|
||
(getenv
|
||
xdg-base-dir-variable)))
|
||
'#$(filter-map
|
||
(lambda (field)
|
||
(let ((variable
|
||
(string-append
|
||
"XDG_"
|
||
(object->snake-case-string
|
||
(configuration-field-name field) 'upper))))
|
||
;; XDG_RUNTIME_DIR shouldn't be created during activation
|
||
;; and will be provided by elogind or other service.
|
||
(and (not (string=? "XDG_RUNTIME_DIR" variable))
|
||
variable)))
|
||
home-xdg-base-directories-configuration-fields)))
|
||
|
||
(define (last-extension-or-cfg config extensions)
|
||
"Picks configuration value from last provided extension. If there
|
||
are no extensions use configuration instead."
|
||
(or (and (not (null? extensions)) (last extensions)) config))
|
||
|
||
(define home-xdg-base-directories-service-type
|
||
(service-type (name 'home-xdg-base-directories)
|
||
(extensions
|
||
(list (service-extension
|
||
home-environment-variables-service-type
|
||
home-xdg-base-directories-environment-variables-service)
|
||
(service-extension
|
||
home-activation-service-type
|
||
ensure-xdg-base-dirs-on-activation)))
|
||
(default-value (home-xdg-base-directories-configuration))
|
||
(compose identity)
|
||
(extend last-extension-or-cfg)
|
||
(description "Configure XDG base directories. This
|
||
service introduces two additional variables @env{XDG_STATE_HOME},
|
||
@env{XDG_LOG_HOME}. They are not a part of XDG specification, at
|
||
least yet, but are convenient to have, it improves the consistency
|
||
between different home services. The services of this service-type is
|
||
instantiated by default, to provide non-default value, extend the
|
||
service-type (using @code{simple-service} for example).")))
|
||
|
||
(define (generate-home-xdg-base-directories-documentation)
|
||
(generate-documentation
|
||
`((home-xdg-base-directories-configuration
|
||
,home-xdg-base-directories-configuration-fields))
|
||
'home-xdg-base-directories-configuration))
|
||
|
||
|
||
;;;
|
||
;;; XDG user directories.
|
||
;;;
|
||
|
||
(define (serialize-string field-name val)
|
||
;; The path has to be quoted
|
||
(format #f "XDG_~a_DIR=\"~a\"\n"
|
||
(object->snake-case-string field-name 'upper) val))
|
||
|
||
(define-configuration home-xdg-user-directories-configuration
|
||
(desktop
|
||
(string "$HOME/Desktop")
|
||
"Default ``desktop'' directory, this is what you see on your
|
||
desktop when using a desktop environment,
|
||
e.g. GNOME (@pxref{XWindow,,,guix.info}).")
|
||
(documents
|
||
(string "$HOME/Documents")
|
||
"Default directory to put documents like PDFs.")
|
||
(download
|
||
(string "$HOME/Downloads")
|
||
"Default directory downloaded files, this is where your Web-broser
|
||
will put downloaded files in.")
|
||
(music
|
||
(string "$HOME/Music")
|
||
"Default directory for audio files.")
|
||
(pictures
|
||
(string "$HOME/Pictures")
|
||
"Default directory for pictures and images.")
|
||
(publicshare
|
||
(string "$HOME/Public")
|
||
"Default directory for shared files, which can be accessed by other
|
||
users on local machine or via network.")
|
||
(templates
|
||
(string "$HOME/Templates")
|
||
"Default directory for templates. They can be used by graphical
|
||
file manager or other apps for creating new files with some
|
||
pre-populated content.")
|
||
(videos
|
||
(string "$HOME/Videos")
|
||
"Default directory for videos."))
|
||
|
||
(define (home-xdg-user-directories-files-service config)
|
||
`(("user-dirs.conf"
|
||
,(mixed-text-file
|
||
"user-dirs.conf"
|
||
"enabled=False\n"))
|
||
("user-dirs.dirs"
|
||
,(mixed-text-file
|
||
"user-dirs.dirs"
|
||
(serialize-configuration
|
||
config
|
||
home-xdg-user-directories-configuration-fields)))))
|
||
|
||
(define (home-xdg-user-directories-activation-service config)
|
||
(let ((dirs (map (lambda (field)
|
||
((configuration-field-getter field) config))
|
||
home-xdg-user-directories-configuration-fields)))
|
||
#~(let ((ensure-dir
|
||
(lambda (path)
|
||
((@ (guix build utils) mkdir-p)
|
||
((@ (ice-9 string-fun) string-replace-substring)
|
||
path "$HOME" (getenv "HOME"))))))
|
||
(display "Creating XDG user directories...")
|
||
(map ensure-dir '#$dirs)
|
||
(display " done\n"))))
|
||
|
||
(define home-xdg-user-directories-service-type
|
||
(service-type (name 'home-xdg-user-directories)
|
||
(extensions
|
||
(list (service-extension
|
||
home-xdg-configuration-files-service-type
|
||
home-xdg-user-directories-files-service)
|
||
(service-extension
|
||
home-activation-service-type
|
||
home-xdg-user-directories-activation-service)))
|
||
(default-value (home-xdg-user-directories-configuration))
|
||
(description "Configure XDG user directories. To
|
||
disable a directory, point it to the $HOME.")))
|
||
|
||
(define (generate-home-xdg-user-directories-documentation)
|
||
(generate-documentation
|
||
`((home-xdg-user-directories-configuration
|
||
,home-xdg-user-directories-configuration-fields))
|
||
'home-xdg-user-directories-configuration))
|
||
|
||
|
||
;;;
|
||
;;; XDG MIME applications.
|
||
;;;
|
||
|
||
;; Example config
|
||
;;
|
||
;; (home-xdg-mime-applications-configuration
|
||
;; (added '((x-scheme-handler/magnet . torrent.desktop)))
|
||
;; (default '((inode/directory . file.desktop)))
|
||
;; (removed '((inode/directory . thunar.desktop)))
|
||
;; (desktop-entries
|
||
;; (list (xdg-desktop-entry
|
||
;; (file "file")
|
||
;; (name "File manager")
|
||
;; (type 'application)
|
||
;; (config
|
||
;; '((exec . "emacsclient -c -a emacs %u"))))
|
||
;; (xdg-desktop-entry
|
||
;; (file "text")
|
||
;; (name "Text editor")
|
||
;; (type 'application)
|
||
;; (config
|
||
;; '((exec . "emacsclient -c -a emacs %u")))
|
||
;; (actions
|
||
;; (list (xdg-desktop-action
|
||
;; (action 'create)
|
||
;; (name "Create an action")
|
||
;; (config
|
||
;; '((exec . "echo hi"))))))))))
|
||
|
||
;; See
|
||
;; <https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html>
|
||
;; <https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html>
|
||
|
||
(define (serialize-alist field-name val)
|
||
(define (serialize-mimelist-entry key val)
|
||
(let ((val (cond
|
||
((list? val)
|
||
(string-join (map maybe-object->string val) ";"))
|
||
((or (string? val) (symbol? val))
|
||
val)
|
||
(else (raise (formatted-message
|
||
(G_ "\
|
||
The value of an XDG MIME entry must be a list, string or symbol, was given ~a")
|
||
val))))))
|
||
(format #f "~a=~a\n" key val)))
|
||
|
||
(define (merge-duplicates alist acc)
|
||
"Merge values that have the same key.
|
||
|
||
@example
|
||
(merge-duplicates '((key1 . value1)
|
||
(key2 . value2)
|
||
(key1 . value3)
|
||
(key1 . value4)) '())
|
||
|
||
@result{} ((key1 . (value4 value3 value1)) (key2 . value2))
|
||
@end example"
|
||
(cond
|
||
((null? alist) acc)
|
||
(else (let* ((head (first alist))
|
||
(tail (cdr alist))
|
||
(key (first head))
|
||
(value (cdr head))
|
||
(duplicate? (assoc key acc))
|
||
(ensure-list (lambda (x)
|
||
(if (list? x) x (list x)))))
|
||
(if duplicate?
|
||
;; XXX: This will change the order of things,
|
||
;; though, it shouldn't be a problem for XDG MIME.
|
||
(merge-duplicates
|
||
tail
|
||
(alist-cons key
|
||
(cons value (ensure-list (cdr duplicate?)))
|
||
(alist-delete key acc)))
|
||
(merge-duplicates tail (cons head acc)))))))
|
||
|
||
(string-append (if (equal? field-name 'default)
|
||
"\n[Default Applications]\n"
|
||
(format #f "\n[~a Associations]\n"
|
||
(string-capitalize (symbol->string field-name))))
|
||
(generic-serialize-alist string-append
|
||
serialize-mimelist-entry
|
||
(merge-duplicates val '()))))
|
||
|
||
(define xdg-desktop-types (make-enumeration
|
||
'(application
|
||
link
|
||
directory)))
|
||
|
||
(define (xdg-desktop-type? type)
|
||
(unless (enum-set-member? type xdg-desktop-types)
|
||
(raise (formatted-message
|
||
(G_ "XDG desktop type must be of of ~a, was given: ~a")
|
||
(list->human-readable-list (enum-set->list xdg-desktop-types))
|
||
type))))
|
||
|
||
;; TODO: Add proper docs for this
|
||
;; XXX: 'define-configuration' require that fields have a default
|
||
;; value.
|
||
(define-record-type* <xdg-desktop-action>
|
||
xdg-desktop-action make-xdg-desktop-action
|
||
xdg-desktop-action?
|
||
(action xdg-desktop-action-action) ; symbol
|
||
(name xdg-desktop-action-name) ; string
|
||
(config xdg-desktop-action-config ; alist
|
||
(default '())))
|
||
|
||
(define-record-type* <xdg-desktop-entry>
|
||
xdg-desktop-entry make-xdg-desktop-entry
|
||
xdg-desktop-entry?
|
||
;; ".desktop" will automatically be added
|
||
(file xdg-desktop-entry-file) ; string
|
||
(name xdg-desktop-entry-name) ; string
|
||
(type xdg-desktop-entry-type) ; xdg-desktop-type
|
||
(config xdg-desktop-entry-config ; alist
|
||
(default '()))
|
||
(actions xdg-desktop-entry-actions ; list of <xdg-desktop-action>
|
||
(default '())))
|
||
|
||
(define desktop-entries? (list-of xdg-desktop-entry?))
|
||
(define (serialize-desktop-entries field-name val) "")
|
||
|
||
(define (serialize-xdg-desktop-entry entry)
|
||
"Return a tuple of the file name for ENTRY and the serialized
|
||
configuration."
|
||
(define (format-config key val)
|
||
(let ((val (cond
|
||
((list? val)
|
||
(string-join (map maybe-object->string val) ";"))
|
||
((boolean? val)
|
||
(if val "true" "false"))
|
||
(else val)))
|
||
(key (string-capitalize (maybe-object->string key))))
|
||
(list (if (string-suffix? key "?")
|
||
(string-drop-right key (- (string-length key) 1))
|
||
key)
|
||
"=" val "\n")))
|
||
|
||
(define (serialize-alist config)
|
||
(generic-serialize-alist append format-config config))
|
||
|
||
(define (serialize-xdg-desktop-action action)
|
||
(match action
|
||
(($ <xdg-desktop-action> action name config)
|
||
`(,(format #f "[Desktop Action ~a]\n"
|
||
(string-capitalize (maybe-object->string action)))
|
||
,(format #f "Name=~a\n" name)
|
||
,@(serialize-alist config)))))
|
||
|
||
(match entry
|
||
(($ <xdg-desktop-entry> file name type config actions)
|
||
(list (if (string-suffix? file ".desktop")
|
||
file
|
||
(string-append file ".desktop"))
|
||
`("[Desktop Entry]\n"
|
||
,(format #f "Name=~a\n" name)
|
||
,(format #f "Type=~a\n"
|
||
(string-capitalize (symbol->string type)))
|
||
,@(serialize-alist config)
|
||
,@(append-map serialize-xdg-desktop-action actions))))))
|
||
|
||
(define-configuration home-xdg-mime-applications-configuration
|
||
(added
|
||
(alist '())
|
||
"An association list of MIME types and desktop entries which indicate
|
||
that the application should used to open the specified MIME type. The
|
||
value has to be string, symbol, or list of strings or symbols, this
|
||
applies to the `@code{default}', and `@code{removed}' fields as well.")
|
||
(default
|
||
(alist '())
|
||
"An association list of MIME types and desktop entries which indicate
|
||
that the application should be the default for opening the specified
|
||
MIME type.")
|
||
(removed
|
||
(alist '())
|
||
"An association list of MIME types and desktop entries which indicate
|
||
that the application cannot open the specified MIME type.")
|
||
(desktop-entries
|
||
(desktop-entries '())
|
||
"A list of XDG desktop entries to create. See
|
||
@code{xdg-desktop-entry}."))
|
||
|
||
(define (home-xdg-mime-applications-files config)
|
||
(define (add-xdg-desktop-entry-file entry)
|
||
(let ((file (first entry))
|
||
(config (second entry)))
|
||
;; TODO: Use xdg-data-files instead of home-files here
|
||
(list (format #f "applications/~a" file)
|
||
(apply mixed-text-file
|
||
(format #f "xdg-desktop-~a-entry" file)
|
||
config))))
|
||
(map (compose add-xdg-desktop-entry-file serialize-xdg-desktop-entry)
|
||
(home-xdg-mime-applications-configuration-desktop-entries config)))
|
||
|
||
(define (home-xdg-mime-applications-xdg-files config)
|
||
`(("mimeapps.list"
|
||
,(mixed-text-file
|
||
"xdg-mime-appplications"
|
||
(serialize-configuration
|
||
config
|
||
home-xdg-mime-applications-configuration-fields)))))
|
||
|
||
(define (home-xdg-mime-applications-extension old-config extension-configs)
|
||
(define (extract-fields config)
|
||
;; return '(added default removed desktop-entries)
|
||
(list (home-xdg-mime-applications-configuration-added config)
|
||
(home-xdg-mime-applications-configuration-default config)
|
||
(home-xdg-mime-applications-configuration-removed config)
|
||
(home-xdg-mime-applications-configuration-desktop-entries config)))
|
||
|
||
(define (append-configs elem acc)
|
||
(list (append (first elem) (first acc))
|
||
(append (second elem) (second acc))
|
||
(append (third elem) (third acc))
|
||
(append (fourth elem) (fourth acc))))
|
||
|
||
;; TODO: Implement procedure to check for duplicates without
|
||
;; sacrificing performance.
|
||
;;
|
||
;; Combine all the alists from 'added', 'default' and 'removed'
|
||
;; into one big alist.
|
||
(let ((folded-configs (fold append-configs
|
||
(extract-fields old-config)
|
||
(map extract-fields extension-configs))))
|
||
(home-xdg-mime-applications-configuration
|
||
(added (first folded-configs))
|
||
(default (second folded-configs))
|
||
(removed (third folded-configs))
|
||
(desktop-entries (fourth folded-configs)))))
|
||
|
||
(define home-xdg-mime-applications-service-type
|
||
(service-type (name 'home-xdg-mime-applications)
|
||
(extensions
|
||
(list (service-extension
|
||
home-xdg-data-files-service-type
|
||
home-xdg-mime-applications-files)
|
||
(service-extension
|
||
home-xdg-configuration-files-service-type
|
||
home-xdg-mime-applications-xdg-files)))
|
||
(compose identity)
|
||
(extend home-xdg-mime-applications-extension)
|
||
(default-value (home-xdg-mime-applications-configuration))
|
||
(description
|
||
"Configure XDG MIME applications, and XDG desktop entries.")))
|