xdg.scm 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2021 Andrew Tropin <andrew@trop.in>
  3. ;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz>
  4. ;;;
  5. ;;; This file is part of GNU Guix.
  6. ;;;
  7. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  8. ;;; under the terms of the GNU General Public License as published by
  9. ;;; the Free Software Foundation; either version 3 of the License, or (at
  10. ;;; your option) any later version.
  11. ;;;
  12. ;;; GNU Guix is distributed in the hope that it will be useful, but
  13. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;;; GNU General Public License for more details.
  16. ;;;
  17. ;;; You should have received a copy of the GNU General Public License
  18. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  19. (define-module (gnu home services xdg)
  20. #:use-module (gnu services configuration)
  21. #:use-module (gnu home services)
  22. #:use-module (gnu packages freedesktop)
  23. #:use-module (gnu home services utils)
  24. #:use-module (guix gexp)
  25. #:use-module (guix records)
  26. #:use-module (guix i18n)
  27. #:use-module (guix diagnostics)
  28. #:use-module (ice-9 match)
  29. #:use-module (srfi srfi-1)
  30. #:use-module (rnrs enums)
  31. #:export (home-xdg-base-directories-service-type
  32. home-xdg-base-directories-configuration
  33. home-xdg-base-directories-configuration?
  34. home-xdg-user-directories-service-type
  35. home-xdg-user-directories-configuration
  36. home-xdg-user-directories-configuration?
  37. xdg-desktop-action
  38. xdg-desktop-entry
  39. home-xdg-mime-applications-service-type
  40. home-xdg-mime-applications-configuration))
  41. ;;; Commentary:
  42. ;;
  43. ;; This module contains services related to XDG directories and
  44. ;; applications.
  45. ;;
  46. ;; - XDG base directories
  47. ;; - XDG user directories
  48. ;; - XDG MIME applications
  49. ;;
  50. ;;; Code:
  51. ;;;
  52. ;;; XDG base directories.
  53. ;;;
  54. (define (serialize-path field-name val) "")
  55. (define path? string?)
  56. (define-configuration home-xdg-base-directories-configuration
  57. (cache-home
  58. (path "$HOME/.cache")
  59. "Base directory for programs to store user-specific non-essential
  60. (cached) data. Files in this directory can be deleted anytime without
  61. loss of important data.")
  62. (config-home
  63. (path "$HOME/.config")
  64. "Base directory for programs to store configuration files.
  65. Some programs store here log or state files, but it's not desired,
  66. this directory should contain static configurations.")
  67. (data-home
  68. (path "$HOME/.local/share")
  69. "Base directory for programs to store architecture independent
  70. read-only shared data, analogus to @file{/usr/share}, but for user.")
  71. (runtime-dir
  72. (path "${XDG_RUNTIME_DIR:-/run/user/$UID}")
  73. "Base directory for programs to store user-specific runtime files,
  74. like sockets.")
  75. (log-home
  76. (path "$HOME/.local/var/log")
  77. "Base directory for programs to store log files, analogus to
  78. @file{/var/log}, but for user. It is not a part of XDG Base Directory
  79. Specification, but helps to make implementation of home services more
  80. consistent.")
  81. (state-home
  82. (path "$HOME/.local/var/lib")
  83. "Base directory for programs to store state files, like databases,
  84. analogus to @file{/var/lib}, but for user. It is not a part of XDG
  85. Base Directory Specification, but helps to make implementation of home
  86. services more consistent."))
  87. (define (home-xdg-base-directories-environment-variables-service config)
  88. (map
  89. (lambda (field)
  90. (cons (format
  91. #f "XDG_~a"
  92. (object->snake-case-string (configuration-field-name field) 'upper))
  93. ((configuration-field-getter field) config)))
  94. home-xdg-base-directories-configuration-fields))
  95. (define (ensure-xdg-base-dirs-on-activation config)
  96. #~(map (lambda (xdg-base-dir-variable)
  97. ((@@ (guix build utils) mkdir-p)
  98. (getenv
  99. xdg-base-dir-variable)))
  100. '#$(map (lambda (field)
  101. (format
  102. #f "XDG_~a"
  103. (object->snake-case-string
  104. (configuration-field-name field) 'upper)))
  105. home-xdg-base-directories-configuration-fields)))
  106. (define (last-extension-or-cfg config extensions)
  107. "Picks configuration value from last provided extension. If there
  108. are no extensions use configuration instead."
  109. (or (and (not (null? extensions)) (last extensions)) config))
  110. (define home-xdg-base-directories-service-type
  111. (service-type (name 'home-xdg-base-directories)
  112. (extensions
  113. (list (service-extension
  114. home-environment-variables-service-type
  115. home-xdg-base-directories-environment-variables-service)
  116. (service-extension
  117. home-activation-service-type
  118. ensure-xdg-base-dirs-on-activation)))
  119. (default-value (home-xdg-base-directories-configuration))
  120. (compose identity)
  121. (extend last-extension-or-cfg)
  122. (description "Configure XDG base directories. This
  123. service introduces two additional variables @env{XDG_STATE_HOME},
  124. @env{XDG_LOG_HOME}. They are not a part of XDG specification, at
  125. least yet, but are convinient to have, it improves the consistency
  126. between different home services. The services of this service-type is
  127. instantiated by default, to provide non-default value, extend the
  128. service-type (using @code{simple-service} for example).")))
  129. (define (generate-home-xdg-base-directories-documentation)
  130. (generate-documentation
  131. `((home-xdg-base-directories-configuration
  132. ,home-xdg-base-directories-configuration-fields))
  133. 'home-xdg-base-directories-configuration))
  134. ;;;
  135. ;;; XDG user directories.
  136. ;;;
  137. (define (serialize-string field-name val)
  138. ;; The path has to be quoted
  139. (format #f "XDG_~a_DIR=\"~a\"\n"
  140. (object->snake-case-string field-name 'upper) val))
  141. (define-configuration home-xdg-user-directories-configuration
  142. (desktop
  143. (string "$HOME/Desktop")
  144. "Default ``desktop'' directory, this is what you see on your
  145. desktop when using a desktop environment,
  146. e.g. GNOME (@pxref{XWindow,,,guix.info}).")
  147. (documents
  148. (string "$HOME/Documents")
  149. "Default directory to put documents like PDFs.")
  150. (download
  151. (string "$HOME/Downloads")
  152. "Default directory downloaded files, this is where your Web-broser
  153. will put downloaded files in.")
  154. (music
  155. (string "$HOME/Music")
  156. "Default directory for audio files.")
  157. (pictures
  158. (string "$HOME/Pictures")
  159. "Default directory for pictures and images.")
  160. (publicshare
  161. (string "$HOME/Public")
  162. "Default directory for shared files, which can be accessed by other
  163. users on local machine or via network.")
  164. (templates
  165. (string "$HOME/Templates")
  166. "Default directory for templates. They can be used by graphical
  167. file manager or other apps for creating new files with some
  168. pre-populated content.")
  169. (videos
  170. (string "$HOME/Videos")
  171. "Default directory for videos."))
  172. (define (home-xdg-user-directories-files-service config)
  173. `(("config/user-dirs.conf"
  174. ,(mixed-text-file
  175. "user-dirs.conf"
  176. "enabled=False\n"))
  177. ("config/user-dirs.dirs"
  178. ,(mixed-text-file
  179. "user-dirs.dirs"
  180. (serialize-configuration
  181. config
  182. home-xdg-user-directories-configuration-fields)))))
  183. (define (home-xdg-user-directories-activation-service config)
  184. (let ((dirs (map (lambda (field)
  185. ((configuration-field-getter field) config))
  186. home-xdg-user-directories-configuration-fields)))
  187. #~(let ((ensure-dir
  188. (lambda (path)
  189. (mkdir-p
  190. ((@@ (ice-9 string-fun) string-replace-substring)
  191. path "$HOME" (getenv "HOME"))))))
  192. (display "Creating XDG user directories...")
  193. (map ensure-dir '#$dirs)
  194. (display " done\n"))))
  195. (define home-xdg-user-directories-service-type
  196. (service-type (name 'home-xdg-user-directories)
  197. (extensions
  198. (list (service-extension
  199. home-files-service-type
  200. home-xdg-user-directories-files-service)
  201. (service-extension
  202. home-activation-service-type
  203. home-xdg-user-directories-activation-service)))
  204. (default-value (home-xdg-user-directories-configuration))
  205. (description "Configure XDG user directories. To
  206. disable a directory, point it to the $HOME.")))
  207. (define (generate-home-xdg-user-directories-documentation)
  208. (generate-documentation
  209. `((home-xdg-user-directories-configuration
  210. ,home-xdg-user-directories-configuration-fields))
  211. 'home-xdg-user-directories-configuration))
  212. ;;;
  213. ;;; XDG MIME applications.
  214. ;;;
  215. ;; Example config
  216. ;;
  217. ;; (home-xdg-mime-applications-configuration
  218. ;; (added '((x-scheme-handler/magnet . torrent.desktop)))
  219. ;; (default '((inode/directory . file.desktop)))
  220. ;; (removed '((inode/directory . thunar.desktop)))
  221. ;; (desktop-entries
  222. ;; (list (xdg-desktop-entry
  223. ;; (file "file")
  224. ;; (name "File manager")
  225. ;; (type 'application)
  226. ;; (config
  227. ;; '((exec . "emacsclient -c -a emacs %u"))))
  228. ;; (xdg-desktop-entry
  229. ;; (file "text")
  230. ;; (name "Text editor")
  231. ;; (type 'application)
  232. ;; (config
  233. ;; '((exec . "emacsclient -c -a emacs %u")))
  234. ;; (actions
  235. ;; (list (xdg-desktop-action
  236. ;; (action 'create)
  237. ;; (name "Create an action")
  238. ;; (config
  239. ;; '((exec . "echo hi"))))))))))
  240. ;; See
  241. ;; <https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html>
  242. ;; <https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html>
  243. (define (serialize-alist field-name val)
  244. (define (serialize-mimelist-entry key val)
  245. (let ((val (cond
  246. ((list? val)
  247. (string-join (map maybe-object->string val) ";"))
  248. ((or (string? val) (symbol? val))
  249. val)
  250. (else (raise (formatted-message
  251. (G_ "\
  252. The value of an XDG MIME entry must be a list, string or symbol, was given ~a")
  253. val))))))
  254. (format #f "~a=~a\n" key val)))
  255. (define (merge-duplicates alist acc)
  256. "Merge values that have the same key.
  257. @example
  258. (merge-duplicates '((key1 . value1)
  259. (key2 . value2)
  260. (key1 . value3)
  261. (key1 . value4)) '())
  262. @result{} ((key1 . (value4 value3 value1)) (key2 . value2))
  263. @end example"
  264. (cond
  265. ((null? alist) acc)
  266. (else (let* ((head (first alist))
  267. (tail (cdr alist))
  268. (key (first head))
  269. (value (cdr head))
  270. (duplicate? (assoc key acc))
  271. (ensure-list (lambda (x)
  272. (if (list? x) x (list x)))))
  273. (if duplicate?
  274. ;; XXX: This will change the order of things,
  275. ;; though, it shouldn't be a problem for XDG MIME.
  276. (merge-duplicates
  277. tail
  278. (alist-cons key
  279. (cons value (ensure-list (cdr duplicate?)))
  280. (alist-delete key acc)))
  281. (merge-duplicates tail (cons head acc)))))))
  282. (string-append (if (equal? field-name 'default)
  283. "\n[Default Applications]\n"
  284. (format #f "\n[~a Associations]\n"
  285. (string-capitalize (symbol->string field-name))))
  286. (generic-serialize-alist string-append
  287. serialize-mimelist-entry
  288. (merge-duplicates val '()))))
  289. (define xdg-desktop-types (make-enumeration
  290. '(application
  291. link
  292. directory)))
  293. (define (xdg-desktop-type? type)
  294. (unless (enum-set-member? type xdg-desktop-types)
  295. (raise (formatted-message
  296. (G_ "XDG desktop type must be of of ~a, was given: ~a")
  297. (list->human-readable-list (enum-set->list xdg-desktop-types))
  298. type))))
  299. ;; TODO: Add proper docs for this
  300. ;; XXX: 'define-configuration' require that fields have a default
  301. ;; value.
  302. (define-record-type* <xdg-desktop-action>
  303. xdg-desktop-action make-xdg-desktop-action
  304. xdg-desktop-action?
  305. (action xdg-desktop-action-action) ; symbol
  306. (name xdg-desktop-action-name) ; string
  307. (config xdg-desktop-action-config ; alist
  308. (default '())))
  309. (define-record-type* <xdg-desktop-entry>
  310. xdg-desktop-entry make-xdg-desktop-entry
  311. xdg-desktop-entry?
  312. ;; ".desktop" will automatically be added
  313. (file xdg-desktop-entry-file) ; string
  314. (name xdg-desktop-entry-name) ; string
  315. (type xdg-desktop-entry-type) ; xdg-desktop-type
  316. (config xdg-desktop-entry-config ; alist
  317. (default '()))
  318. (actions xdg-desktop-entry-actions ; list of <xdg-desktop-action>
  319. (default '())))
  320. (define desktop-entries? (list-of xdg-desktop-entry?))
  321. (define (serialize-desktop-entries field-name val) "")
  322. (define (serialize-xdg-desktop-entry entry)
  323. "Return a tuple of the file name for ENTRY and the serialized
  324. configuration."
  325. (define (format-config key val)
  326. (let ((val (cond
  327. ((list? val)
  328. (string-join (map maybe-object->string val) ";"))
  329. ((boolean? val)
  330. (if val "true" "false"))
  331. (else val)))
  332. (key (string-capitalize (maybe-object->string key))))
  333. (list (if (string-suffix? key "?")
  334. (string-drop-right key (- (string-length key) 1))
  335. key)
  336. "=" val "\n")))
  337. (define (serialize-alist config)
  338. (generic-serialize-alist identity format-config config))
  339. (define (serialize-xdg-desktop-action action)
  340. (match action
  341. (($ <xdg-desktop-action> action name config)
  342. `(,(format #f "[Desktop Action ~a]\n"
  343. (string-capitalize (maybe-object->string action)))
  344. ,(format #f "Name=~a\n" name)
  345. ,@(serialize-alist config)))))
  346. (match entry
  347. (($ <xdg-desktop-entry> file name type config actions)
  348. (list (if (string-suffix? file ".desktop")
  349. file
  350. (string-append file ".desktop"))
  351. `("[Desktop Entry]\n"
  352. ,(format #f "Name=~a\n" name)
  353. ,(format #f "Type=~a\n"
  354. (string-capitalize (symbol->string type)))
  355. ,@(serialize-alist config)
  356. ,@(append-map serialize-xdg-desktop-action actions))))))
  357. (define-configuration home-xdg-mime-applications-configuration
  358. (added
  359. (alist '())
  360. "An association list of MIME types and desktop entries which indicate
  361. that the application should used to open the specified MIME type. The
  362. value has to be string, symbol, or list of strings or symbols, this
  363. applies to the `@code{default}', and `@code{removed}' fields as well.")
  364. (default
  365. (alist '())
  366. "An association list of MIME types and desktop entries which indicate
  367. that the application should be the default for opening the specified
  368. MIME type.")
  369. (removed
  370. (alist '())
  371. "An association list of MIME types and desktop entries which indicate
  372. that the application cannot open the specified MIME type.")
  373. (desktop-entries
  374. (desktop-entries '())
  375. "A list of XDG desktop entries to create. See
  376. @code{xdg-desktop-entry}."))
  377. (define (home-xdg-mime-applications-files-service config)
  378. (define (add-xdg-desktop-entry-file entry)
  379. (let ((file (first entry))
  380. (config (second entry)))
  381. (list (format #f "local/share/applications/~a" file)
  382. (apply mixed-text-file
  383. (format #f "xdg-desktop-~a-entry" file)
  384. config))))
  385. (append
  386. `(("config/mimeapps.list"
  387. ,(mixed-text-file
  388. "xdg-mime-appplications"
  389. (serialize-configuration
  390. config
  391. home-xdg-mime-applications-configuration-fields))))
  392. (map (compose add-xdg-desktop-entry-file serialize-xdg-desktop-entry)
  393. (home-xdg-mime-applications-configuration-desktop-entries config))))
  394. (define (home-xdg-mime-applications-extension old-config extension-configs)
  395. (define (extract-fields config)
  396. ;; return '(added default removed desktop-entries)
  397. (list (home-xdg-mime-applications-configuration-added config)
  398. (home-xdg-mime-applications-configuration-default config)
  399. (home-xdg-mime-applications-configuration-removed config)
  400. (home-xdg-mime-applications-configuration-desktop-entries config)))
  401. (define (append-configs elem acc)
  402. (list (append (first elem) (first acc))
  403. (append (second elem) (second acc))
  404. (append (third elem) (third acc))
  405. (append (fourth elem) (fourth acc))))
  406. ;; TODO: Implement procedure to check for duplicates without
  407. ;; sacrificing performance.
  408. ;;
  409. ;; Combine all the alists from 'added', 'default' and 'removed'
  410. ;; into one big alist.
  411. (let ((folded-configs (fold append-configs
  412. (extract-fields old-config)
  413. (map extract-fields extension-configs))))
  414. (home-xdg-mime-applications-configuration
  415. (added (first folded-configs))
  416. (default (second folded-configs))
  417. (removed (third folded-configs))
  418. (desktop-entries (fourth folded-configs)))))
  419. (define home-xdg-mime-applications-service-type
  420. (service-type (name 'home-xdg-mime-applications)
  421. (extensions
  422. (list (service-extension
  423. home-files-service-type
  424. home-xdg-mime-applications-files-service)))
  425. (compose identity)
  426. (extend home-xdg-mime-applications-extension)
  427. (default-value (home-xdg-mime-applications-configuration))
  428. (description
  429. "Configure XDG MIME applications, and XDG desktop entries.")))