code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018, 2020 Mathieu Othacehe <m.othacehe@gmail.com>
;;; Copyright © 2019 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2020 Florian Pelz <pelzflorian@pelzflorian.de>
;;;
;;; 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 installer newt keymap)
  #:use-module (gnu installer keymap)
  #:use-module (gnu installer steps)
  #:use-module (gnu installer newt page)
  #:use-module (guix i18n)
  #:use-module (guix records)
  #:use-module (newt)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (ice-9 i18n)
  #:use-module (ice-9 match)
  #:export (run-keymap-page
            keyboard-layout->configuration))

(define (run-layout-page layouts layout->text context)
  (let ((title (G_ "Layout")))
    (run-listbox-selection-page
     #:title title
     #:info-text
     (case context
       ((param) (G_ "Please choose your keyboard layout. \
It will only be used during the installation process. \
Non-Latin layouts can be toggled with Alt+Shift."))
       (else (G_ "Please choose your keyboard layout. \
It will be used during the install process, and for the installed system. \
Non-Latin layouts can be toggled with Alt+Shift. You can switch to a \
different layout at any time from the parameters menu.")))
     #:listbox-items layouts
     #:listbox-item->text layout->text
     #:sort-listbox-items? #f
     #:button-text
     (case context
       ((param) (G_ "Continue"))
       (else (G_ "Exit")))
     #:button-callback-procedure
     (case context
       ((param) (const #f))
       (else
        (lambda _
          (raise
           (condition
            (&installer-step-abort)))))))))

(define (run-variant-page variants variant->text)
  (let ((title (G_ "Variant")))
    (run-listbox-selection-page
     #:title title
     #:info-text (G_ "Please choose a variant for your keyboard layout.")
     #:listbox-items variants
     #:listbox-item->text variant->text
     #:sort-listbox-items? #f
     #:button-text (G_ "Back")
     #:button-callback-procedure
     (lambda _
       (raise
        (condition
         (&installer-step-abort)))))))

(define (sort-layouts layouts)
  "Sort LAYOUTS list by putting the US layout ahead and return it."
  (define (layout<? layout1 layout2)
    (let ((text1 (x11-keymap-layout-description layout1))
          (text2 (x11-keymap-layout-description layout2)))
      ;; XXX: We're calling 'gettext' more than once per item.
      (string-locale<? (gettext text1 "xkeyboard-config")
                       (gettext text2 "xkeyboard-config"))))

  (define preferred
    ;; Two-letter language tag for the preferred keyboard layout.
    (or (getenv "LANGUAGE") "us"))

  (call-with-values
      (lambda ()
        (partition
         (lambda (layout)
           ;; The 'synopsis' field is usually a language code (e.g., "en")
           ;; while the 'name' field is a country code (e.g., "us").
           (or (string=? (x11-keymap-layout-name layout) preferred)
               (string=? (x11-keymap-layout-synopsis layout) preferred)))
         layouts))
    (lambda (main others)
      (append (sort main layout<?)
              (sort others layout<?)))))

(define (add-empty-variant variants)
  "Prepend #f to VARIANTS so the user has the option to select no variant.
The resulting layout may be different from all other variants (e.g. for
Azerbaijani)."
  (cons #f variants))

(define (sort-variants variants)
  "Sort VARIANTS list by putting the international variant ahead and return it."
  (call-with-values
      (lambda ()
        (partition
         (lambda (variant)
           (and variant
                (let ((name (x11-keymap-variant-name variant)))
                  (string=? name "altgr-intl"))))
         variants))
    (cut append <> <>)))

(define %non-latin-layouts
  ;; List of keyboard layouts marked as $nonlatin in xkeyboard-config.
  ;; See comments in xkeyboard-config file /share/X11/xkb/rules/base.
  ;; We ignore layouts that support Latin input: "kr"
  '("am" "ara" "ben" "bd" "bg" "bt" "by" "cs" "deva" "ge" "gh"
    "gr" "guj" "guru" "il" "in" "ir" "iku" "jp" "kan" "kh"
    "la" "lao" "lk" "mk" "mm" "mn" "mv" "mal" "olck" "ori" "pk"
    "ru" "scc" "sy" "syr" "tel" "th" "tj" "tam" "ua" "uz"
    ;; The list from xkeyboard-config is incomplete.  Add more layouts when
    ;; noticed:
    "et" "kz"))

(define %non-latin-variants
  '("cyrillic"))

(define %latin-layout+variants
  ;; These layout+variant combinations are Latin after all.
  '(("ir" "ku")))

(define (toggleable-latin-layout layout variant)
  "If LAYOUT is a non-Latin layout, return a new combined layout,
a variant, and options that allow the user to switch between the
non-Latin and the Latin layout.  Otherwise, return LAYOUT, VARIANT,
and #f."
  (if (and (not (equal? variant "latin"))
           (not (member (list layout variant) %latin-layout+variants))
           (or (member layout %non-latin-layouts)
               (member variant %non-latin-variants)))
      (let ((latin-layout (if (equal? variant "azerty") "fr" "us")))
        (list
         (string-append layout "," latin-layout)
         ;; Comma to use variant only for non-Latin:
         (and variant (string-append variant ","))
         "grp:alt_shift_toggle"))
      (list layout variant #f)))

(define* (run-keymap-page layouts #:key (context #f))
  "Run a page asking the user to select a keyboard layout and variant. LAYOUTS
is a list of supported X11-KEYMAP-LAYOUT.  For non-Latin keyboard layouts, a
second layout and toggle options will be added automatically.  Return a list
of three elements, the names of the selected keyboard layout, variant and
options."
  (define keymap-steps
    (list
     (installer-step
      (id 'layout)
      (compute
       (lambda _
         (run-layout-page
          (sort-layouts layouts)
          (lambda (layout)
            (gettext (x11-keymap-layout-description layout)
                     "xkeyboard-config"))
          context))))
     ;; Propose the user to select a variant among those supported by the
     ;; previously selected layout.
     (installer-step
      (id 'variant)
      (compute
       (lambda (result _)
         (let* ((layout (result-step result 'layout))
                (variants (if layout
                              (x11-keymap-layout-variants layout)
                              '())))
           ;; Return #f if the layout does not have any variant.
           (and (not (null? variants))
                (run-variant-page
                 (sort-variants (add-empty-variant variants))
                 (lambda (variant)
                   (if variant
                       (gettext (x11-keymap-variant-description variant)
                                "xkeyboard-config")
                       ;; Text to opt for no variant at all:
                       (gettext (x11-keymap-layout-description layout)
                                "xkeyboard-config")))))))))))

  (define (format-result layout variant)
    (let ((layout (x11-keymap-layout-name layout))
          (variant (and=> variant
                          (lambda (variant)
                            (gettext (x11-keymap-variant-name variant)
                                     "xkeyboard-config")))))
      (toggleable-latin-layout layout variant)))

  (let* ((result (run-installer-steps #:steps keymap-steps))
         (layout (result-step result 'layout))
         (variant (result-step result 'variant)))
    (and layout
         (format-result layout variant))))

(define (keyboard-layout->configuration keymap)
  "Return the operating system configuration snippet to install KEYMAP."
  (match keymap
    ((name #f "grp:alt_shift_toggle")
     `((keyboard-layout (keyboard-layout ,name
                                         #:options '("grp:alt_shift_toggle")))))
    ((name #f _)
     `((keyboard-layout (keyboard-layout ,name))))
    ((name variant "grp:alt_shift_toggle")
     `((keyboard-layout (keyboard-layout ,name ,variant
                                         #:options '("grp:alt_shift_toggle")))))
    ((name variant _)
     `((keyboard-layout (keyboard-layout ,name ,variant))))))


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Mathieu Othacehe <m.othacehe@gmail.com>
;;; Copyright © 2019 Ludovic Courtès <ludo@gnu.org>
;;;
;;; 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 installer newt locale)
  #:use-module (gnu installer locale)
  #:use-module (gnu installer steps)
  #:use-module (gnu installer newt page)
  #:use-module (guix i18n)
  #:use-module (newt)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (ice-9 match)
  #:export (run-locale-page))

(define (run-language-page languages language->text)
  (define result
    (run-listbox-selection-page
     #:title (G_ "Locale language")
     #:info-text (G_ "Choose the language to use for the \
installation process and for the installed system.")
     #:info-textbox-width 70
     #:listbox-items languages
     #:listbox-item->text language->text
     #:sort-listbox-items? #f
     #:button-text (G_ "Exit")
     #:button-callback-procedure
     (lambda _
       (raise
        (condition
         (&installer-step-abort))))))

  ;; Immediately install the chosen language so that the territory page that
  ;; comes after (optionally) is displayed in the chosen language.
  (setenv "LANGUAGE" result)

  result)

(define (run-territory-page territories territory->text)
  (let ((title (G_ "Locale location")))
    (run-listbox-selection-page
     #:title title
     #:info-text (G_ "Choose a territory for this language.")
     #:listbox-items territories
     #:listbox-item->text territory->text
     #:button-text (G_ "Back")
     #:button-callback-procedure
     (lambda _
       (raise
        (condition
         (&installer-step-abort)))))))

(define (run-codeset-page codesets)
  (let ((title (G_ "Locale codeset")))
    (run-listbox-selection-page
     #:title title
     #:info-text (G_ "Choose the locale encoding.")
     #:listbox-items codesets
     #:listbox-item->text identity
     #:listbox-default-item "UTF-8"
     #:button-text (G_ "Back")
     #:button-callback-procedure
     (lambda _
       (raise
        (condition
         (&installer-step-abort)))))))

(define (run-modifier-page modifiers modifier->text)
  (let ((title (G_ "Locale modifier")))
    (run-listbox-selection-page
     #:title title
     #:info-text (G_ "Choose your locale's modifier. The most frequent \
modifier is euro. It indicates that you want to use Euro as the currency \
symbol.")
     #:listbox-items modifiers
     #:listbox-item->text modifier->text
     #:button-text (G_ "Back")
     #:button-callback-procedure
     (lambda _
       (raise
        (condition
         (&installer-step-abort)))))))

(define* (run-locale-page #:key
                          supported-locales
                          iso639-languages
                          iso3166-territories)
  "Run a page asking the user to select a locale language and possibly
territory, codeset and modifier. Use SUPPORTED-LOCALES as the list of glibc
available locales. ISO639-LANGUAGES is an association list associating a
locale code to a locale name. ISO3166-TERRITORIES is an association list
associating a territory code with a territory name. The formatted locale, under
glibc format is returned."

  (define (break-on-locale-found locales)
    "Raise the &installer-step-break condition if LOCALES contains exactly one
element."
    (and (= (length locales) 1)
         (raise
          (condition (&installer-step-break)))))

  (define (filter-locales locales result)
    "Filter the list of locale records LOCALES using the RESULT returned by
the installer-steps defined below."
    (filter
     (lambda (locale)
       (and-map identity
                `(,(string=? (locale-language locale)
                             (result-step result 'language))
                  ,@(if (result-step-done? result 'territory)
                        (list (equal? (locale-territory locale)
                                      (result-step result 'territory)))
                        '())
                  ,@(if (result-step-done? result 'codeset)
                        (list (equal? (locale-codeset locale)
                                      (result-step result 'codeset)))
                        '())
                  ,@(if (result-step-done? result 'modifier)
                        (list (equal? (locale-modifier locale)
                                      (result-step result 'modifier)))
                        '()))))
     locales))

  (define (result->locale-string locales result)
    "Supposing that LOCALES contains exactly one locale record, turn it into a
glibc locale string and return it."
    (match (filter-locales locales result)
      ((locale)
       (locale->locale-string locale))))

  (define (sort-languages languages)
    "Extract some languages from LANGUAGES list and place them ahead."
    (let* ((first-languages '("en"))
           (other-languages (lset-difference equal?
                                             languages
                                             first-languages)))
      `(,@first-languages ,@other-languages)))

  (define locale-steps
    (list
     (installer-step
      (id 'language)
      (compute
       (lambda _
         (run-language-page
          (sort-languages
           (delete-duplicates (map locale-language supported-locales)))
          (lambda (language)
            (let ((english (language-code->language-name iso639-languages
                                                         language)))
              (setenv "LANGUAGE" language)
              (let ((native (gettext english "iso_639-3")))
                (unsetenv "LANGUAGE")
                native)))))))
     (installer-step
      (id 'territory)
      (compute
       (lambda (result _)
         (let ((locales (filter-locales supported-locales result)))
           ;; Stop the process if the language returned by the previous step
           ;; is matching one and only one supported locale.
           (break-on-locale-found locales)

           ;; Otherwise, ask the user to select a territory among those
           ;; supported by the previously selected language.
           (run-territory-page
            (delete-duplicates (map locale-territory locales))
            (lambda (territory)
              (if territory
                  (let ((english (territory-code->territory-name
                                  iso3166-territories territory)))
                    (gettext english "iso_3166-1"))
                  (G_ "No location"))))))))
     (installer-step
      (id 'codeset)
      (compute
       (lambda (result _)
         (let ((locales (filter-locales supported-locales result)))
           ;; Same as above but we now have a language and a territory to
           ;; narrow down the search of a locale.
           (break-on-locale-found locales)

           ;; Otherwise, choose a codeset.
           (let ((codesets (delete-duplicates (map locale-codeset locales))))
             (if (member "UTF-8" codesets)
                 "UTF-8"                          ;don't even ask
                 (run-codeset-page codesets)))))))
     (installer-step
      (id 'modifier)
      (compute
       (lambda (result _)
         (let ((locales (filter-locales supported-locales result)))
           ;; Same thing with a language, a territory and a codeset this time.
           (break-on-locale-found locales)

           ;; Otherwise, ask for a modifier.
           (run-modifier-page
            (delete-duplicates (map locale-modifier locales))
            (lambda (modifier)
              (or modifier (G_ "No modifier"))))))))))

  ;; If run-installer-steps returns locally, it means that the user had to go
  ;; through all steps (language, territory, codeset and modifier) to select a
  ;; locale. In that case, like if we exited by raising &installer-step-break
  ;; condition, turn the result into a glibc locale string and return it.
  (result->locale-string
   supported-locales
   (run-installer-steps #:steps locale-steps)))

repo_name: maximed/guix

license: mit


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Mathieu Othacehe <m.othacehe@gmail.com>
;;;
;;; 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 installer newt menu)
  #:use-module (gnu installer steps)
  #:use-module (gnu installer newt page)
  #:use-module (guix i18n)
  #:use-module (newt)
  #:export (run-menu-page))

(define (run-menu-page steps)
  "Run a menu page, asking the user to select where to resume the install
process from."
  (define (steps->items steps)
    (filter (lambda (step)
              (installer-step-description step))
            steps))

  (run-listbox-selection-page
   #:info-text (G_ "Choose where you want to resume the install.  \
You can also abort the installation by pressing the Abort button.")
   #:title (G_ "Installation menu")
   #:listbox-items (steps->items steps)
   #:listbox-item->text installer-step-description
   #:sort-listbox-items? #f
   #:button-text (G_ "Abort")
   #:button-callback-procedure (lambda ()
                                 (newt-finish)
                                 (primitive-exit 1))))


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Mathieu Othacehe <m.othacehe@gmail.com>
;;; Copyright © 2019, 2020 Ludovic Courtès <ludo@gnu.org>
;;;
;;; 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 installer newt network)
  #:use-module (gnu installer connman)
  #:use-module (gnu installer steps)
  #:use-module (gnu installer utils)
  #:use-module (gnu installer newt ethernet)
  #:use-module (gnu installer newt page)
  #:use-module (gnu installer newt wifi)
  #:use-module (guix i18n)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-11)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (ice-9 match)
  #:use-module (newt)
  #:export (run-network-page))

;; Maximum length of a technology name.
(define technology-name-max-length (make-parameter 20))

(define (technology->text technology)
  "Return a string describing the given TECHNOLOGY."
  (let* ((name (technology-name technology))
         (padded-name (string-pad-right name
                                        (technology-name-max-length))))
    (format #f "~a~%" padded-name)))

(define (run-technology-page)
  "Run a page to ask the user which technology shall be used to access
Internet and return the selected technology. For now, only technologies with
\"ethernet\" or \"wifi\" types are supported."
  (define (technology-items)
    (filter (lambda (technology)
              (let ((type (technology-type technology)))
                (or
                 (string=? type "ethernet")
                 (string=? type "wifi"))))
            (connman-technologies)))

  (match (technology-items)
    (()
     (case (choice-window
            (G_ "Internet access")
            (G_ "Continue")
            (G_ "Exit")
            (G_ "The install process requires Internet access but no \
network devices were found. Do you want to continue anyway?"))
       ((1) (raise
             (condition
              (&installer-step-break))))
       ((2) (raise
             (condition
              (&installer-step-abort))))))
    ((technology)
     ;; Since there's only one technology available, skip the selection
     ;; screen.
     technology)
    ((items ...)
     (run-listbox-selection-page
      #:info-text (G_ "The install process requires Internet access.\
 Please select a network device.")
      #:title (G_ "Internet access")
      #:listbox-items items
      #:listbox-item->text technology->text
      #:listbox-height (min (+ (length items) 2) 5)
      #:button-text (G_ "Exit")
      #:button-callback-procedure
      (lambda _
        (raise
         (condition
          (&installer-step-abort))))))))

(define (find-technology-by-type technologies type)
  "Find and return a technology with the given TYPE in TECHNOLOGIES list."
  (find (lambda (technology)
          (string=? (technology-type technology)
                    type))
        technologies))

(define (wait-technology-powered technology)
  "Wait and display a progress bar until the given TECHNOLOGY is powered."
  (let ((name (technology-name technology))
        (full-value 5))
    (run-scale-page
     #:title (G_ "Powering technology")
     #:info-text (format #f (G_ "Waiting for technology ~a to be powered.")
                         name)
     #:scale-full-value full-value
     #:scale-update-proc
     (lambda (value)
       (let* ((technologies (connman-technologies))
              (type (technology-type technology))
              (updated-technology
               (find-technology-by-type technologies type))
              (technology-powered? updated-technology))
         (sleep 1)
         (if technology-powered?
             full-value
             (+ value 1)))))))

(define (wait-service-online)
  "Display a newt scale until connman detects an Internet access. Do
FULL-VALUE tentatives, spaced by 1 second."
  (define (online?)
    (or (connman-online?)
        (file-exists? "/tmp/installer-assume-online")))

  (let* ((full-value 5))
    (run-scale-page
     #:title (G_ "Checking connectivity")
     #:info-text (G_ "Waiting for Internet access establishment...")
     #:scale-full-value full-value
     #:scale-update-proc
     (lambda (value)
       (sleep 1)
       (if (online?)
           full-value
           (+ value 1))))
    (unless (online?)
      (run-error-page
       (G_ "The selected network does not provide access to the \
Internet, please try again.")
       (G_ "Connection error"))
      (raise
       (condition
        (&installer-step-abort))))))

(define (run-network-page)
  "Run a page to allow the user to configure connman so that it can access the
Internet."
  (define network-steps
    (list
     ;; Ask the user to choose between ethernet and wifi technologies.
     (installer-step
      (id 'select-technology)
      (compute
       (lambda _
         (run-technology-page))))
     ;; Enable the previously selected technology.
     (installer-step
      (id 'power-technology)
      (compute
       (lambda (result _)
         (let ((technology (result-step result 'select-technology)))
           (connman-enable-technology technology)
           (wait-technology-powered technology)))))
     ;; Propose the user to connect to one of the service available for the
     ;; previously selected technology.
     (installer-step
      (id 'connect-service)
      (compute
       (lambda (result _)
         (let* ((technology (result-step result 'select-technology))
                (type (technology-type technology)))
           (cond
            ((string=? "wifi" type)
             (run-wifi-page))
            ((string=? "ethernet" type)
             (run-ethernet-page)))))))
     ;; Wait for connman status to switch to 'online, which means it can
     ;; access Internet.
     (installer-step
      (id 'wait-online)
      (compute (lambda _
                 (wait-service-online))))))
  (run-installer-steps
   #:steps network-steps
   #:rewind-strategy 'start))


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Mathieu Othacehe <m.othacehe@gmail.com>
;;; Copyright © 2019, 2020 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2019 Tobias Geerinckx-Rice <me@tobias.gr>
;;;
;;; 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 installer newt page)
  #:use-module (gnu installer steps)
  #:use-module (gnu installer utils)
  #:use-module (gnu installer newt utils)
  #:use-module (guix i18n)
  #:use-module (ice-9 i18n)
  #:use-module (ice-9 match)
  #:use-module (ice-9 receive)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-11)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (newt)
  #:export (default-listbox-height

            draw-info-page
            draw-connecting-page
            run-input-page
            run-error-page
            run-confirmation-page
            run-listbox-selection-page
            run-scale-page
            run-checkbox-tree-page
            run-file-textbox-page

            run-form-with-clients))

;;; Commentary:
;;;
;;; Some helpers around guile-newt to draw or run generic pages. The
;;; difference between 'draw' and 'run' terms comes from newt library. A page
;;; is drawn when the form it contains does not expect any user
;;; interaction. In that case, it is necessary to call (newt-refresh) to force
;;; the page to be displayed. When a form is 'run', it is blocked waiting for
;;; any action from the user (press a button, input some text, ...).
;;;
;;; Code:

(define* (watch-clients! form #:optional (clients (current-clients)))
  "Have FORM watch the file descriptors corresponding to current client
connections.  Consequently, FORM may exit with the 'exit-fd-ready' reason."
  (when (current-server-socket)
    (form-watch-fd form (fileno (current-server-socket))
                   FD-READ))

  (for-each (lambda (client)
              (form-watch-fd form (fileno client)
                             (logior FD-READ FD-EXCEPT)))
            clients))

(define close-port-and-reuse-fd
  (let ((bit-bucket #f))
    (lambda (port)
      "Close PORT and redirect its underlying FD to point to a valid open file
descriptor."
      (let ((fd (fileno port)))
        (unless bit-bucket
          (set! bit-bucket (car (pipe))))
        (close-port port)

        ;; FIXME: We're leaking FD.
        (dup2 (fileno bit-bucket) fd)))))

(define* (run-form-with-clients form exp)
  "Run FORM such as it watches the file descriptors beneath CLIENTS after
sending EXP to all the clients.

Automatically restart the form when it exits with 'exit-fd-ready but without
an actual client reply--e.g., it got a connection request or a client
disconnect.

Like 'run-form', return two values: the exit reason, and an \"argument\"."
  (define* (discard-client! port #:optional errno)
    (if errno
        (syslog "removing client ~d due to ~s~%"
                (fileno port) (strerror errno))
        (syslog "removing client ~d due to EOF~%"
                (fileno port)))

    ;; XXX: Watch out!  There's no 'form-unwatch-fd' procedure in Newt so we
    ;; cheat: we keep PORT's file descriptor open, but make it a duplicate of
    ;; a valid but inactive FD.  Failing to do that, 'run-form' would
    ;; select(2) on the now-closed port and keep spinning as select(2) returns
    ;; EBADF.
    (close-port-and-reuse-fd port)

    (current-clients (delq port (current-clients)))
    (close-port port))

  (define title
    ;; Title of FORM.
    (match exp
      (((? symbol? tag) alist ...)
       (match (assq 'title alist)
         ((_ title) title)
         (_         tag)))
      (((? symbol? tag) _ ...)
       tag)
      (_
       'unknown)))

  ;; Send EXP to all the currently-connected clients.
  (send-to-clients exp)

  (let loop ()
    (syslog "running form ~s (~s) with ~d clients~%"
            form title (length (current-clients)))

    ;; Call 'watch-clients!' within the loop because there might be new
    ;; clients.
    (watch-clients! form)

    (let-values (((reason argument) (run-form form)))
      (match reason
        ('exit-fd-ready
         (match (fdes->ports argument)
           ((port _ ...)
            (if (memq port (current-clients))

                ;; Read a reply from a client or handle its departure.
                (catch 'system-error
                  (lambda ()
                    (match (read port)
                      ((? eof-object? eof)
                       (discard-client! port)
                       (loop))
                      (obj
                       (syslog "form ~s (~s): client ~d replied ~s~%"
                               form title (fileno port) obj)
                       (values 'exit-fd-ready obj))))
                  (lambda args
                    (discard-client! port (system-error-errno args))
                    (loop)))

                ;; Accept a new client and send it EXP.
                (match (accept port)
                  ((client . _)
                   (syslog "accepting new client ~d while on form ~s~%"
                           (fileno client) form)
                   (catch 'system-error
                     (lambda ()
                       (write exp client)
                       (newline client)
                       (force-output client)
                       (current-clients (cons client (current-clients))))
                     (lambda _
                       (close-port client)))
                   (loop)))))))
        (_
         (values reason argument))))))

(define (default-listbox-height)
  "Return the default listbox height."
  (max 5 (- (screen-rows) 20)))

(define (draw-info-page text title)
  "Draw an informative page with the given TEXT as content.  Set the title of
this page to TITLE."
  (send-to-clients `(info (title ,title) (text ,text)))
  (let* ((text-box
          (make-reflowed-textbox -1 -1 text 40
                                 #:flags FLAG-BORDER))
         (grid (make-grid 1 1))
         (form (make-form)))
    (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
    (add-component-to-form form text-box)
    (make-wrapped-grid-window grid title)
    (draw-form form)
    ;; This call is imperative, otherwise the form won't be displayed. See the
    ;; explanation in the above commentary.
    (newt-refresh)
    form))

(define (draw-connecting-page service-name)
  "Draw a page to indicate a connection in in progress."
  (draw-info-page
   (format #f (G_ "Connecting to ~a, please wait.") service-name)
   (G_ "Connection in progress")))

(define* (run-input-page text title
                         #:key
                         (allow-empty-input? #f)
                         (default-text #f)
                         (input-visibility-checkbox? #f)
                         (input-field-width 40)
                         (input-flags 0))
  "Run a page to prompt user for an input. The given TEXT will be displayed
above the input field. The page title is set to TITLE. Unless
allow-empty-input? is set to #t, an error page will be displayed if the user
enters an empty input.  INPUT-FLAGS is a bitwise-or'd set of flags for the
input box, such as FLAG-PASSWORD."
  (let* ((text-box
          (make-reflowed-textbox -1 -1 text
                                 input-field-width
                                 #:flags FLAG-BORDER))
         (input-visible-cb
          (make-checkbox -1 -1 (G_ "Show") #\space "x "))
         (input-flags* (if input-visibility-checkbox?
                           (logior FLAG-PASSWORD FLAG-SCROLL
                                   input-flags)
                           input-flags))
         (input-entry (make-entry -1 -1 20
                                  #:flags input-flags*))
         (ok-button (make-button -1 -1 (G_ "OK")))
         (grid (vertically-stacked-grid
                GRID-ELEMENT-COMPONENT text-box
                GRID-ELEMENT-SUBGRID
                (apply
                 horizontal-stacked-grid
                 GRID-ELEMENT-COMPONENT input-entry
                 `(,@(if input-visibility-checkbox?
                         (list GRID-ELEMENT-COMPONENT input-visible-cb)
                         '())))
                GRID-ELEMENT-COMPONENT ok-button))
         (form (make-form #:flags FLAG-NOF12)))

    (add-component-callback
     input-visible-cb
     (lambda ()
       (set-entry-flags input-entry
                        FLAG-PASSWORD
                        FLAG-ROLE-TOGGLE)))

    (when default-text
      (set-entry-text input-entry default-text))

    (add-form-to-grid grid form #t)
    (make-wrapped-grid-window grid title)
    (let ((error-page (lambda ()
                        (run-error-page (G_ "Please enter a non empty input.")
                                        (G_ "Empty input")))))
      (let loop ()
        (receive (exit-reason argument)
            (run-form-with-clients form
                                   `(input (title ,title) (text ,text)
                                           (default ,default-text)))
          (let ((input (if (eq? exit-reason 'exit-fd-ready)
                           argument
                           (entry-value input-entry))))
            (cond ((not input)                 ;client disconnect or something
                   (loop))
                  ((and (not allow-empty-input?)
                        (eq? exit-reason 'exit-component)
                        (string=? input ""))
                   ;; Display the error page.
                   (error-page)
                   ;; Set the focus back to the input input field.
                   (set-current-component form input-entry)
                   (loop))
                  (else
                   (destroy-form-and-pop form)
                   input))))))))

(define (run-error-page text title)
  "Run a page to inform the user of an error. The page contains the given TEXT
to explain the error and an \"OK\" button to acknowledge the error. The title
of the page is set to TITLE."
  (let* ((text-box
          (make-reflowed-textbox -1 -1 text 40
                                 #:flags FLAG-BORDER))
         (grid (make-grid 1 2))
         (ok-button (make-button -1 -1 "OK"))
         (form (make-form)))

    (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
    (set-grid-field grid 0 1 GRID-ELEMENT-COMPONENT ok-button
                    #:pad-top 1)

    ;; Set the background color to red to indicate something went wrong.
    (newt-set-color COLORSET-ROOT "white" "red")
    (add-components-to-form form text-box ok-button)
    (make-wrapped-grid-window grid title)
    (run-form-with-clients form
                           `(error (title ,title) (text ,text)))
    ;; Restore the background to its original color.
    (newt-set-color COLORSET-ROOT "white" "blue")
    (destroy-form-and-pop form)))

(define* (run-confirmation-page text title
                                #:key (exit-button-procedure (const #f)))
  "Run a page to inform the user of an error. The page contains the given TEXT
to explain the error and an \"OK\" button to acknowledge the error. The title
of the page is set to TITLE."
  (let* ((text-box
          (make-reflowed-textbox -1 -1 text 40
                                 #:flags FLAG-BORDER))
         (ok-button (make-button -1 -1 (G_ "Continue")))
         (exit-button (make-button -1 -1 (G_ "Exit")))
         (grid (vertically-stacked-grid
                GRID-ELEMENT-COMPONENT text-box
                GRID-ELEMENT-SUBGRID
                (horizontal-stacked-grid
                 GRID-ELEMENT-COMPONENT ok-button
                 GRID-ELEMENT-COMPONENT exit-button)))
         (form (make-form #:flags FLAG-NOF12)))

    (add-form-to-grid grid form #t)
    (make-wrapped-grid-window grid title)

    (receive (exit-reason argument)
        (run-form-with-clients form
                               `(confirmation (title ,title)
                                              (text ,text)))
      (dynamic-wind
        (const #t)
        (lambda ()
          (match exit-reason
            ('exit-component
             (cond
              ((components=? argument ok-button)
               #t)
              ((components=? argument exit-button)
               (exit-button-procedure))))
            ('exit-fd-ready
             (if argument
                 #t
                 (exit-button-procedure)))))
        (lambda ()
          (destroy-form-and-pop form))))))

(define* (run-listbox-selection-page #:key
                                     info-text
                                     title
                                     (info-textbox-width 50)
                                     listbox-items
                                     listbox-item->text
                                     (listbox-height
                                      (default-listbox-height))
                                     (listbox-default-item #f)
                                     (listbox-allow-multiple? #f)
                                     (sort-listbox-items? #t)
                                     (allow-delete? #f)
                                     (skip-item-procedure?
                                      (const #f))
                                     button-text
                                     (button-callback-procedure
                                      (const #t))
                                     (button2-text #f)
                                     (button2-callback-procedure
                                      (const #t))
                                     (listbox-callback-procedure
                                      identity)
                                     (client-callback-procedure
                                      listbox-callback-procedure)
                                     (hotkey-callback-procedure
                                      (const #t)))
  "Run a page asking the user to select an item in a listbox. The page
contains, stacked vertically from the top to the bottom, an informative text
set to INFO-TEXT, a listbox and a button. The listbox will be filled with
LISTBOX-ITEMS converted to text by applying the procedure LISTBOX-ITEM->TEXT
on every item. The selected item from LISTBOX-ITEMS is returned. The button
text is set to BUTTON-TEXT and the procedure BUTTON-CALLBACK-PROCEDURE called
when it is pressed. The procedure LISTBOX-CALLBACK-PROCEDURE is called when an
item from the listbox is selected (by pressing the <ENTER> key).

INFO-TEXTBOX-WIDTH is the width of the textbox where INFO-TEXT will be
displayed. LISTBOX-HEIGHT is the height of the listbox.

If LISTBOX-DEFAULT-ITEM is set to the value of one of the items in
LISTBOX-ITEMS, it will be selected by default. Otherwise, the first element of
the listbox is selected.

If LISTBOX-ALLOW-MULTIPLE? is set to #t, multiple items from the listbox can
be selected (using the <SPACE> key). It that case, a list containing the
selected items will be returned.

If SORT-LISTBOX-ITEMS? is set to #t, the listbox items are sorted using
'string-locale<?' procedure (after being converted to text).

If ALLOW-DELETE? is #t, the form will return if the <DELETE> key is pressed,
otherwise nothing will happen.

Each time the listbox current item changes, call SKIP-ITEM-PROCEDURE? with the
current listbox item as argument. If it returns #t, skip the element and jump
to the next/previous one depending on the previous item, otherwise do
nothing."
  (let loop ()
    (define (fill-listbox listbox items)
      "Append the given ITEMS to LISTBOX, once they have been converted to text
with LISTBOX-ITEM->TEXT. Each item appended to the LISTBOX is given a key by
newt. Save this key by returning an association list under the form:

	((NEWT-LISTBOX-KEY . ITEM) ...)

where NEWT-LISTBOX-KEY is the key returned by APPEND-ENTRY-TO-LISTBOX, when
ITEM was inserted into LISTBOX."
      (map (lambda (item)
             (let* ((text (listbox-item->text item))
                    (key (append-entry-to-listbox listbox text)))
               (cons key item)))
           items))

    (define (sort-listbox-items listbox-items)
      "Return LISTBOX-ITEMS sorted using the 'string-locale<?' procedure on the text
corresponding to each item in the list."
      (let* ((items (map (lambda (item)
                           (cons item (listbox-item->text item)))
                         listbox-items))
             (sorted-items
              (sort items (lambda (a b)
                            (let ((text-a (cdr a))
                                  (text-b (cdr b)))
                              (string-locale<? text-a text-b))))))
        (map car sorted-items)))

    ;; Store the last selected listbox item's key.
    (define last-listbox-key (make-parameter #f))

    (define (previous-key keys key)
      (let ((index (list-index (cut eq? key <>) keys)))
        (and index
             (> index 0)
             (list-ref keys (- index 1)))))

    (define (next-key keys key)
      (let ((index (list-index (cut eq? key <>) keys)))
        (and index
             (< index (- (length keys) 1))
             (list-ref keys (+ index 1)))))

    (define (set-default-item listbox listbox-keys default-item)
      "Set the default item of LISTBOX to DEFAULT-ITEM. LISTBOX-KEYS is the
association list returned by the FILL-LISTBOX procedure. It is used because
the current listbox item has to be selected by key."
      (for-each (match-lambda
                  ((key . item)
                   (when (equal? item default-item)
                     (set-current-listbox-entry-by-key listbox key))))
                listbox-keys))

    (let* ((listbox (make-listbox
                     -1 -1
                     listbox-height
                     (logior FLAG-SCROLL FLAG-BORDER FLAG-RETURNEXIT
                             (if listbox-allow-multiple?
                                 FLAG-MULTIPLE
                                 0))))
           (form (make-form #:flags FLAG-NOF12))
           (info-textbox
            (make-reflowed-textbox -1 -1 info-text
                                   info-textbox-width
                                   #:flags FLAG-BORDER))
           (button (make-button -1 -1 button-text))
           (button2 (and button2-text
                         (make-button -1 -1 button2-text)))
           (grid (vertically-stacked-grid
                  GRID-ELEMENT-COMPONENT info-textbox
                  GRID-ELEMENT-COMPONENT listbox
                  GRID-ELEMENT-SUBGRID
                  (apply
                   horizontal-stacked-grid
                   GRID-ELEMENT-COMPONENT button
                   `(,@(if button2
                           (list GRID-ELEMENT-COMPONENT button2)
                           '())))))
           (sorted-items (if sort-listbox-items?
                             (sort-listbox-items listbox-items)
                             listbox-items))
           (keys (fill-listbox listbox sorted-items)))

      (define (choice->item str)
        ;; Return the item that corresponds to STR.
        (match (find (match-lambda
                       ((key . item)
                        (string=? str (listbox-item->text item))))
                     keys)
          ((key . item) item)
          (#f (raise (condition (&installer-step-abort))))))

      ;; On every listbox element change, check if we need to skip it. If yes,
      ;; depending on the 'last-listbox-key', jump forward or backward. If no,
      ;; do nothing.
      (add-component-callback
       listbox
       (lambda ()
         (let* ((current-key (current-listbox-entry listbox))
                (listbox-keys (map car keys))
                (last-key (last-listbox-key))
                (item (assoc-ref keys current-key))
                (prev-key (previous-key listbox-keys current-key))
                (next-key (next-key listbox-keys current-key)))
           ;; Update last-listbox-key before a potential call to
           ;; set-current-listbox-entry-by-key, because it will immediately
           ;; cause this callback to be called for the new entry.
           (last-listbox-key current-key)
           (when (skip-item-procedure? item)
             (when (eq? prev-key last-key)
               (if next-key
                   (set-current-listbox-entry-by-key listbox next-key)
                   (set-current-listbox-entry-by-key listbox prev-key)))
             (when (eq? next-key last-key)
               (if prev-key
                   (set-current-listbox-entry-by-key listbox prev-key)
                   (set-current-listbox-entry-by-key listbox next-key)))))))

      (when listbox-default-item
        (set-default-item listbox keys listbox-default-item))

      (when allow-delete?
        (form-add-hotkey form KEY-DELETE))

      (add-form-to-grid grid form #t)
      (make-wrapped-grid-window grid title)

      (receive (exit-reason argument)
          (run-form-with-clients form
                                 `(list-selection (title ,title)
                                                  (multiple-choices?
                                                   ,listbox-allow-multiple?)
                                                  (items
                                                   ,(map listbox-item->text
                                                         listbox-items))))
        (dynamic-wind
          (const #t)
          (lambda ()
            (match exit-reason
              ('exit-component
               (cond
                ((components=? argument button)
                 (button-callback-procedure))
                ((and button2
                      (components=? argument button2))
                 (button2-callback-procedure))
                ((components=? argument listbox)
                 (if listbox-allow-multiple?
                     (let* ((entries (listbox-selection listbox))
                            (items (map (lambda (entry)
                                          (assoc-ref keys entry))
                                        entries)))
                       (listbox-callback-procedure items))
                     (let* ((entry (current-listbox-entry listbox))
                            (item (assoc-ref keys entry)))
                       (listbox-callback-procedure item))))))
              ('exit-fd-ready
               (let* ((choice argument)
                      (item   (if listbox-allow-multiple?
                                  (map choice->item choice)
                                  (choice->item choice))))
                 (client-callback-procedure item)))
              ('exit-hotkey
               (let* ((entry (current-listbox-entry listbox))
                      (item (assoc-ref keys entry)))
                 (hotkey-callback-procedure argument item)))))
          (lambda ()
            (destroy-form-and-pop form)))))))

(define* (run-scale-page #:key
                         title
                         info-text
                         (info-textbox-width 50)
                         (scale-width 40)
                         (scale-full-value 100)
                         scale-update-proc
                         (max-scale-update 5))
  "Run a page with a progress bar (called 'scale' in newt). The given
INFO-TEXT is displayed in a textbox above the scale. The width of the textbox
is set to INFO-TEXTBOX-WIDTH. The width of the scale is set to
SCALE-WIDTH. SCALE-FULL-VALUE indicates the value that correspond to 100% of
the scale.

The procedure SCALE-UPDATE-PROC shall return a new scale
value. SCALE-UPDATE-PROC will be called until the returned value is superior
or equal to SCALE-FULL-VALUE, but no more than MAX-SCALE-UPDATE times. An
error is raised if the MAX-SCALE-UPDATE limit is reached."
  (let* ((info-textbox
          (make-reflowed-textbox -1 -1 info-text
                                 info-textbox-width
                                 #:flags FLAG-BORDER))
         (scale (make-scale -1 -1 scale-width scale-full-value))
         (grid (vertically-stacked-grid
                GRID-ELEMENT-COMPONENT info-textbox
                GRID-ELEMENT-COMPONENT scale))
         (form (make-form)))

    (add-form-to-grid grid form #t)
    (make-wrapped-grid-window grid title)

    (draw-form form)
    ;; This call is imperative, otherwise the form won't be displayed. See the
    ;; explanation in the above commentary.
    (newt-refresh)

    (dynamic-wind
      (const #t)
      (lambda ()
        (let loop ((i max-scale-update)
                   (last-value 0))
          (let ((value (scale-update-proc last-value)))
            (set-scale-value scale value)
            ;; Same as above.
            (newt-refresh)
            (unless (>= value scale-full-value)
              (if (> i 0)
                  (loop (- i 1) value)
                  (error "Max scale updates reached."))))))
      (lambda ()
        (destroy-form-and-pop form)))))

(define %none-selected
  (circular-list #f))

(define* (run-checkbox-tree-page #:key
                                 info-text
                                 title
                                 items
                                 (selection %none-selected)
                                 item->text
                                 (info-textbox-width 50)
                                 (checkbox-tree-height 10)
                                 (ok-button-callback-procedure
                                  (const #t))
                                 (exit-button-callback-procedure
                                  (const #t)))
  "Run a page allowing the user to select one or multiple items among ITEMS in
a checkbox list. The page contains vertically stacked from the top to the
bottom, an informative text set to INFO-TEXT, the checkbox list and two
buttons, 'Ok' and 'Exit'. The page title's is set to TITLE. ITEMS are
converted to text using ITEM->TEXT before being displayed in the checkbox
list.  SELECTION is a list of Booleans of the same length as ITEMS that
specifies which items are initially checked.

INFO-TEXTBOX-WIDTH is the width of the textbox where INFO-TEXT will be
displayed. CHECKBOX-TREE-HEIGHT is the height of the checkbox list.

OK-BUTTON-CALLBACK-PROCEDURE is called when the 'Ok' button is pressed.
EXIT-BUTTON-CALLBACK-PROCEDURE is called when the 'Exit' button is
pressed.

This procedure returns the list of checked items in the checkbox list among
ITEMS when 'Ok' is pressed."
  (define (fill-checkbox-tree checkbox-tree items)
    (map (lambda (item selected?)
           (let* ((item-text (item->text item))
                  (key (add-entry-to-checkboxtree checkbox-tree item-text
                                                  (if selected?
                                                      FLAG-SELECTED
                                                      0))))
             (cons key item)))
         items
         selection))

  (let loop ()
    (let* ((checkbox-tree
            (make-checkboxtree -1 -1
                               checkbox-tree-height
                               FLAG-BORDER))
           (info-textbox
            (make-reflowed-textbox -1 -1 info-text
                                   info-textbox-width
                                   #:flags FLAG-BORDER))
           (ok-button (make-button -1 -1 (G_ "OK")))
           (exit-button (make-button -1 -1 (G_ "Exit")))
           (grid (vertically-stacked-grid
                  GRID-ELEMENT-COMPONENT info-textbox
                  GRID-ELEMENT-COMPONENT checkbox-tree
                  GRID-ELEMENT-SUBGRID
                  (horizontal-stacked-grid
                   GRID-ELEMENT-COMPONENT ok-button
                   GRID-ELEMENT-COMPONENT exit-button)))
           (keys (fill-checkbox-tree checkbox-tree items))
           (form (make-form #:flags FLAG-NOF12)))

      (define (choice->item str)
        ;; Return the item that corresponds to STR.
        (match (find (match-lambda
                       ((key . item)
                        (string=? str (item->text item))))
                     keys)
          ((key . item) item)
          (#f (raise (condition (&installer-step-abort))))))

      (add-form-to-grid grid form #t)
      (make-wrapped-grid-window grid title)

      (receive (exit-reason argument)
          (run-form-with-clients form
                                 `(checkbox-list (title ,title)
                                                 (text ,info-text)
                                                 (items
                                                  ,(map item->text items))))
        (dynamic-wind
          (const #t)

          (lambda ()
            (match exit-reason
              ('exit-component
               (cond
                ((components=? argument ok-button)
                 (let* ((entries (current-checkbox-selection checkbox-tree))
                        (current-items (map (lambda (entry)
                                              (assoc-ref keys entry))
                                            entries)))
                   (ok-button-callback-procedure)
                   current-items))
                ((components=? argument exit-button)
                 (exit-button-callback-procedure))))
              ('exit-fd-ready
               (map choice->item argument))))
          (lambda ()
            (destroy-form-and-pop form)))))))

(define* (edit-file file #:key locale)
  "Spawn an editor for FILE."
  (clear-screen)
  (newt-suspend)
  ;; Use Nano because it syntax-highlights Scheme by default.
  ;; TODO: Add a menu to choose an editor?
  (run-command (list "/run/current-system/profile/bin/nano" file)
               #:locale locale)
  (newt-resume))

(define* (run-file-textbox-page #:key
                                info-text
                                title
                                file
                                (info-textbox-width 50)
                                (file-textbox-width 50)
                                (file-textbox-height 30)
                                (exit-button? #t)
                                (edit-button? #f)
                                (editor-locale #f)
                                (ok-button-callback-procedure
                                 (const #t))
                                (exit-button-callback-procedure
                                 (const #t)))
  (let loop ()
    (let* ((info-textbox
            (make-reflowed-textbox -1 -1 info-text
                                   info-textbox-width
                                   #:flags FLAG-BORDER))
           (file-textbox
            (make-textbox -1 -1
                          file-textbox-width
                          file-textbox-height
                          (logior FLAG-SCROLL FLAG-BORDER)))
           (ok-button (make-button -1 -1 (G_ "OK")))
           (exit-button (make-button -1 -1 (G_ "Exit")))
           (edit-button (and edit-button?
                             (make-button -1 -1 (G_ "Edit"))))
           (grid (vertically-stacked-grid
                  GRID-ELEMENT-COMPONENT info-textbox
                  GRID-ELEMENT-COMPONENT file-textbox
                  GRID-ELEMENT-SUBGRID
                  (apply
                   horizontal-stacked-grid
                   GRID-ELEMENT-COMPONENT ok-button
                   `(,@(if edit-button?
                           (list GRID-ELEMENT-COMPONENT edit-button)
                           '())
                     ,@(if exit-button?
                           (list GRID-ELEMENT-COMPONENT exit-button)
                           '())))))
           (form (make-form #:flags FLAG-NOF12)))

      (add-form-to-grid grid form #t)
      (make-wrapped-grid-window grid title)

      (set-textbox-text file-textbox
                        (receive (_w _h text)
                            (reflow-text (read-all file)
                                         file-textbox-width
                                         0 0)
                          text))

      (receive (exit-reason argument)
          (run-form-with-clients form
                                 `(file-dialog (title ,title)
                                               (text ,info-text)
                                               (file ,file)))
        (define result
          (dynamic-wind
            (const #t)
            (lambda ()
              (match exit-reason
                ('exit-component
                 (cond
                  ((components=? argument ok-button)
                   (ok-button-callback-procedure))
                  ((and exit-button?
                        (components=? argument exit-button))
                   (exit-button-callback-procedure))
                  ((and edit-button?
                        (components=? argument edit-button))
                   (edit-file file))))
                ('exit-fd-ready
                 (if argument
                     (ok-button-callback-procedure)
                     (exit-button-callback-procedure)))))
            (lambda ()
              (destroy-form-and-pop form))))

        (if (and (eq? exit-reason 'exit-component)
                 (components=? argument edit-button))
            (loop)                                ;recurse in tail position
            result)))))


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2020 Mathieu Othacehe <m.othacehe@gmail.com>
;;;
;;; 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 installer newt parameters)
  #:use-module (gnu installer proxy)
  #:use-module (gnu installer steps)
  #:use-module (gnu installer newt page)
  #:use-module (guix build syscalls)
  #:use-module (guix i18n)
  #:use-module (ice-9 match)
  #:use-module (newt)
  #:export (run-parameters-page))

(define (run-proxy-page)
  (define proxy
    (run-input-page (G_ "Please enter the HTTP proxy URL. If you enter an \
empty string, proxy usage will be disabled.")
                    (G_ "HTTP proxy configuration")
                    #:allow-empty-input? #t))
  (if (string=? proxy "")
      (clear-http-proxy)
      (set-http-proxy proxy)))

(define (run-parameters-page keyboard-layout-selection)
  "Run a parameters page allowing to change the keyboard layout"
  (let* ((items
          (list
           (cons (G_ "Change keyboard layout") keyboard-layout-selection)
           (cons (G_ "Configure HTTP proxy") run-proxy-page)
           (cons (G_ "Reboot") reboot)))
         (result
          (run-listbox-selection-page
           #:info-text (G_ "Please choose one of the following parameters or \
press ‘Back’ to go back to the installation process.")
           #:title (G_ "Installation parameters")
           #:listbox-items items
           #:listbox-item->text car
           #:sort-listbox-items? #f
           #:listbox-height 6
           #:button-text (G_ "Back"))))
    (match result
      ((_ . proc)
       (proc))
      (_ #f))))


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018, 2019 Mathieu Othacehe <m.othacehe@gmail.com>
;;; Copyright © 2019, 2020 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2020 Tobias Geerinckx-Rice <me@tobias.gr>
;;;
;;; 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 installer newt partition)
  #:use-module (gnu installer parted)
  #:use-module (gnu installer steps)
  #:use-module (gnu installer utils)
  #:use-module (gnu installer newt page)
  #:use-module (gnu installer newt utils)
  #:use-module (guix i18n)
  #:use-module (ice-9 format)
  #:use-module (ice-9 match)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (newt)
  #:use-module (parted)
  #:export (run-partitioning-page))

(define (button-exit-action)
  "Raise the &installer-step-abort condition."
  (raise
   (condition
    (&installer-step-abort))))

(define (run-scheme-page)
  "Run a page asking the user for a partitioning scheme."
  (let* ((items
          `((root . ,(G_ "Everything is one partition"))
            (root-home . ,(G_ "Separate /home partition"))))
         (result (run-listbox-selection-page
                  #:info-text (G_ "Please select a partitioning scheme.")
                  #:title (G_ "Partition scheme")
                  #:listbox-items items
                  #:listbox-item->text cdr
                  #:listbox-height 4
                  #:sort-listbox-items? #f       ;keep the 'root' option first
                  #:button-text (G_ "Exit")
                  #:button-callback-procedure button-exit-action)))
    (car result)))

(define (draw-formatting-page partitions)
  "Draw a page asking for confirmation, and then indicating that partitions
are being formatted."
  ;; TRANSLATORS: The ~{ and ~} format specifiers are used to iterate the list
  ;; of device names of the user partitions that will be formatted.
  (run-confirmation-page (format #f (G_ "We are about to write the configured \
partition table to the disk and format the partitions listed below.  Their \
data will be lost.  Do you wish to continue?~%~%~{ - ~a~%~}")
                                 (map user-partition-file-name
                                      (filter user-partition-need-formatting?
                                              partitions)))
                         (G_ "Format disk?")
                         #:exit-button-procedure button-exit-action)
  (draw-info-page
   (format #f (G_ "Partition formatting is in progress, please wait."))
   (G_ "Preparing partitions")))

(define (run-device-page devices)
  "Run a page asking the user to select a device among those in the given
DEVICES list."
  (define (device-items)
    (map (lambda (device)
           `(,device . ,(device-description device)))
         devices))

  (let* ((result (run-listbox-selection-page
                  #:info-text (G_ "Please select a disk.")
                  #:title (G_ "Disk")
                  #:listbox-items (device-items)
                  #:listbox-item->text cdr
                  #:listbox-height 10
                  #:button-text (G_ "Exit")
                  #:button-callback-procedure button-exit-action))
         (device (car result)))
    device))

(define (run-label-page button-text button-callback)
  "Run a page asking the user to select a partition table label."
  ;; Force the GPT label if UEFI is supported.
  (if (efi-installation?)
      "gpt"
      (run-listbox-selection-page
       #:info-text (G_ "Select a new partition table type. \
Be careful, all data on the disk will be lost.")
       #:title (G_ "Partition table")
       #:listbox-items '("msdos" "gpt")
       #:listbox-item->text identity
       #:button-text button-text
       #:button-callback-procedure button-callback)))

(define (run-type-page partition)
  "Run a page asking the user to select a partition type."
  (let* ((disk (partition-disk partition))
         (partitions (disk-partitions disk))
         (other-extended-partitions?
          (any extended-partition? partitions))
         (items
          `(normal ,@(if other-extended-partitions?
                         '()
                         '(extended)))))
    (run-listbox-selection-page
     #:info-text (G_ "Please select a partition type.")
     #:title (G_ "Partition type")
     #:listbox-items items
     #:listbox-item->text symbol->string
     #:sort-listbox-items? #f
     #:button-text (G_ "Exit")
     #:button-callback-procedure button-exit-action)))

(define (run-fs-type-page)
  "Run a page asking the user to select a file-system type."
  (run-listbox-selection-page
   #:info-text (G_ "Please select the file-system type for this partition.")
   #:title (G_ "File-system type")
   #:listbox-items '(ext4 btrfs fat16 fat32 jfs ntfs swap)
   #:listbox-item->text user-fs-type-name
   #:sort-listbox-items? #f
   #:button-text (G_ "Exit")
   #:button-callback-procedure button-exit-action))

(define (inform-can-create-partition? user-partition)
  "Return #t if it is possible to create USER-PARTITION. This is determined by
calling CAN-CREATE-PARTITION? procedure. If an exception is raised, catch it
an inform the user with an appropriate error-page and return #f."
  (guard (c ((max-primary-exceeded? c)
            (run-error-page
             (G_ "Primary partitions count exceeded.")
             (G_ "Creation error"))
            #f)
           ((extended-creation-error? c)
            (run-error-page
             (G_ "Extended partition creation error.")
             (G_ "Creation error"))
            #f)
           ((logical-creation-error? c)
            (run-error-page
             (G_ "Logical partition creation error.")
             (G_ "Creation error"))
            #f))
    (can-create-partition? user-partition)))

(define (prompt-luks-passwords user-partitions)
  "Prompt for the luks passwords of the encrypted partitions in
USER-PARTITIONS list. Return this list with password fields filled-in."
  (map (lambda (user-part)
         (let* ((crypt-label (user-partition-crypt-label user-part))
                (file-name (user-partition-file-name user-part))
                (password-page
                 (lambda ()
                   (run-input-page
                    (format #f (G_ "Please enter the password for the \
encryption of partition ~a (label: ~a).") file-name crypt-label)
                    (G_ "Password required")
                    #:input-visibility-checkbox? #t)))
                (password-confirm-page
                 (lambda ()
                   (run-input-page
                    (format #f (G_ "Please confirm the password for the \
encryption of partition ~a (label: ~a).") file-name crypt-label)
                    (G_ "Password confirmation required")
                    #:input-visibility-checkbox? #t))))
           (if crypt-label
               (let loop ()
                 (let ((password (password-page))
                       (confirmation (password-confirm-page)))
                   (if (string=? password confirmation)
                       (user-partition
                        (inherit user-part)
                        (crypt-password password))
                       (begin
                         (run-error-page
                          (G_ "Password mismatch, please try again.")
                          (G_ "Password error"))
                         (loop)))))
               user-part)))
       user-partitions))

(define* (run-partition-page target-user-partition
                             #:key
                             (default-item #f))
  "Run a page allowing the user to edit the given TARGET-USER-PARTITION
record. If the argument DEFAULT-ITEM is passed, use it to select the current
listbox item. This is used to avoid the focus to switch back to the first
listbox entry while calling this procedure recursively."

  (define (numeric-size device size)
    "Parse the given SIZE on DEVICE and return it."
    (call-with-values
        (lambda ()
          (unit-parse size device))
      (lambda (value range)
        value)))

  (define (numeric-size-range device size)
    "Parse the given SIZE on DEVICE and return the associated RANGE."
    (call-with-values
        (lambda ()
          (unit-parse size device))
      (lambda (value range)
        range)))

  (define* (fill-user-partition-geom user-part
                                     #:key
                                     device (size #f) start end)
    "Return the given USER-PART with the START, END and SIZE fields set to the
eponym arguments. Use UNIT-FORMAT-CUSTOM to format START and END arguments as
sectors on DEVICE."
    (user-partition
     (inherit user-part)
     (size size)
     (start (unit-format-custom device start UNIT-SECTOR))
     (end (unit-format-custom device end UNIT-SECTOR))))

  (define (apply-user-partition-changes user-part)
    "Set the name, file-system type and boot flag on the partition specified
by USER-PART, if it is applicable for the partition type."
    (let* ((partition (user-partition-parted-object user-part))
           (disk (partition-disk partition))
           (disk-type (disk-disk-type disk))
           (device (disk-device disk))
           (has-name? (disk-type-check-feature
                       disk-type
                       DISK-TYPE-FEATURE-PARTITION-NAME))
           (name (user-partition-name user-part))
           (fs-type (filesystem-type-get
                     (user-fs-type-name
                      (user-partition-fs-type user-part))))
           (bootable? (user-partition-bootable? user-part))
           (esp? (user-partition-esp? user-part))
           (flag-bootable?
            (partition-is-flag-available? partition PARTITION-FLAG-BOOT))
           (flag-esp?
            (partition-is-flag-available? partition PARTITION-FLAG-ESP)))
      (when (and has-name? name)
        (partition-set-name partition name))
      (partition-set-system partition fs-type)
      (when flag-bootable?
        (partition-set-flag partition
                            PARTITION-FLAG-BOOT
                            (if bootable? 1 0)))
      (when flag-esp?
        (partition-set-flag partition
                            PARTITION-FLAG-ESP
                            (if esp? 1 0)))
      #t))

  (define (listbox-action listbox-item)
    (let* ((item (car listbox-item))
           (partition (user-partition-parted-object
                       target-user-partition))
           (disk (partition-disk partition))
           (device (disk-device disk)))
      (list
       item
       (case item
         ((name)
          (let* ((old-name (user-partition-name target-user-partition))
                 (name
                  (run-input-page (G_ "Please enter the partition gpt name.")
                                  (G_ "Partition name")
                                  #:default-text old-name)))
            (user-partition
             (inherit target-user-partition)
             (name name))))
         ((type)
          (let ((new-type (run-type-page partition)))
            (user-partition
             (inherit target-user-partition)
             (type new-type))))
         ((bootable)
          (user-partition
           (inherit target-user-partition)
           (bootable? (not (user-partition-bootable?
                            target-user-partition)))))
         ((esp?)
          (let ((new-esp? (not (user-partition-esp?
                                target-user-partition))))
            (user-partition
             (inherit target-user-partition)
             (esp? new-esp?)
             (mount-point (if new-esp?
                              (default-esp-mount-point)
                              "")))))
         ((crypt-label)
          (let* ((label (user-partition-crypt-label
                         target-user-partition))
                 (new-label
                  (and (not label)
                       (run-input-page
                        (G_ "Please enter the encrypted label")
                        (G_ "Encryption label")))))
            (user-partition
             (inherit target-user-partition)
             (need-formatting? #t)
             (crypt-label new-label))))
         ((need-formatting?)
          (user-partition
           (inherit target-user-partition)
           (need-formatting?
            (not (user-partition-need-formatting?
                  target-user-partition)))))
         ((size)
          (let* ((old-size (user-partition-size target-user-partition))
                 (max-size-value (partition-length partition))
                 (max-size (unit-format device max-size-value))
                 (start (partition-start partition))
                 (size (run-input-page
                        (format #f (G_ "Please enter the size of the partition.\
 The maximum size is ~a.") max-size)
                        (G_ "Partition size")
                        #:default-text (or old-size max-size)))
                 (size-percentage (read-percentage size))
                 (size-value (if size-percentage
                                 (nearest-exact-integer
                                  (/ (* max-size-value size-percentage)
                                     100))
                                 (numeric-size device size)))
                 (end (and size-value
                           (+ start size-value)))
                 (size-range (numeric-size-range device size))
                 (size-range-ok? (and size-range
                                      (< (+ start
                                            (geometry-start size-range))
                                         (partition-end partition)))))
            (cond
             ((and size-percentage (> size-percentage 100))
              (run-error-page
               (G_ "The percentage can not be superior to 100.")
               (G_ "Size error"))
              target-user-partition)
             ((not size-value)
              (run-error-page
               (G_ "The requested size is incorrectly formatted, or too large.")
               (G_ "Size error"))
              target-user-partition)
             ((not (or size-percentage size-range-ok?))
              (run-error-page
               (G_ "The request size is superior to the maximum size.")
               (G_ "Size error"))
              target-user-partition)
             (else
              (fill-user-partition-geom target-user-partition
                                        #:device device
                                        #:size size
                                        #:start start
                                        #:end end)))))
         ((fs-type)
          (let ((fs-type (run-fs-type-page)))
            (user-partition
             (inherit target-user-partition)
             (fs-type fs-type))))
         ((mount-point)
          (let* ((old-mount (or (user-partition-mount-point
                                 target-user-partition)
                                ""))
                 (mount
                  (run-input-page
                   (G_ "Please enter the desired mounting point for this \
partition. Leave this field empty if you don't want to set a mounting point.")
                   (G_ "Mounting point")
                   #:default-text old-mount
                   #:allow-empty-input? #t)))
            (user-partition
             (inherit target-user-partition)
             (mount-point (and (not (string=? mount ""))
                               mount)))))))))

  (define (button-action)
    (let* ((partition (user-partition-parted-object
                       target-user-partition))
           (prev-part (partition-prev partition))
           (disk (partition-disk partition))
           (device (disk-device disk))
           (creation? (freespace-partition? partition))
           (start (partition-start partition))
           (end (partition-end partition))
           (new-user-partition
            (if (user-partition-start target-user-partition)
                target-user-partition
                (fill-user-partition-geom target-user-partition
                                          #:device device
                                          #:start start
                                          #:end end))))
      ;; It the backend PARTITION has free-space type, it means we are
      ;; creating a new partition, otherwise, we are editing an already
      ;; existing PARTITION.
      (if creation?
          (let* ((ok-create-partition?
                  (inform-can-create-partition? new-user-partition))
                 (new-partition
                  (and ok-create-partition?
                       (mkpart disk
                               new-user-partition
                               #:previous-partition prev-part))))
            (and new-partition
                 (user-partition
                  (inherit new-user-partition)
                  (need-formatting? #t)
                  (file-name (partition-get-path new-partition))
                  (disk-file-name (device-path device))
                  (parted-object new-partition))))
          (and (apply-user-partition-changes new-user-partition)
               new-user-partition))))

  (let* ((items (user-partition-description target-user-partition))
         (partition (user-partition-parted-object
                     target-user-partition))
         (disk (partition-disk partition))
         (device (disk-device disk))
         (file-name (device-path device))
         (number-str (partition-print-number partition))
         (type (user-partition-type target-user-partition))
         (type-str (symbol->string type))
         (start (unit-format device (partition-start partition)))
         (creation? (freespace-partition? partition))
         (default-item (and default-item
                            (find (lambda (item)
                                    (eq? (car item) default-item))
                                  items)))
         (result
          (run-listbox-selection-page
           #:info-text
           (if creation?
               (format #f (G_ "Creating ~a partition starting at ~a of ~a.")
                       type-str start file-name)
               (format #f (G_ "You are currently editing partition ~a.")
                       number-str))
           #:title (if creation?
                       (G_ "Partition creation")
                       (G_ "Partition edit"))
           #:listbox-items items
           #:listbox-item->text cdr
           #:sort-listbox-items? #f
           #:listbox-default-item default-item
           #:button-text (G_ "OK")
           #:listbox-callback-procedure listbox-action
           #:button-callback-procedure button-action)))
    (match result
      ((item new-user-partition)
       (run-partition-page new-user-partition
                           #:default-item item))
      (else result))))

(define* (run-disk-page disks
                        #:optional (user-partitions '())
                        #:key (guided? #f))
  "Run a page allowing to edit the partition tables of the given DISKS. If
specified, USER-PARTITIONS is a list of <user-partition> records associated to
the partitions on DISKS."

  (define (other-logical-partitions? partitions)
    "Return #t if at least one of the partition in PARTITIONS list is a
logical partition, return #f otherwise."
    (any logical-partition? partitions))

  (define (other-non-logical-partitions? partitions)
    "Return #t is at least one of the partitions in PARTITIONS list is not a
logical partition, return #f otherwise."
    (let ((non-logical-partitions
           (remove logical-partition? partitions)))
      (or (any normal-partition? non-logical-partitions)
          (any freespace-partition? non-logical-partitions))))

  (define (add-tree-symbols partitions descriptions)
    "Concatenate tree symbols to the given DESCRIPTIONS list and return
it. The PARTITIONS list is the list of partitions described in
DESCRIPTIONS. The tree symbols are used to indicate the partition's disk and
for logical partitions, the extended partition which includes them."
    (match descriptions
      (() '())
      ((description . rest-descriptions)
       (match partitions
         ((partition . rest-partitions)
          (if (null? rest-descriptions)
              (list (if (logical-partition? partition)
                        (string-append " ┗━ " description)
                        (string-append "┗━  " description)))
              (cons (cond
                     ((extended-partition? partition)
                      (if (other-non-logical-partitions? rest-partitions)
                          (string-append "┣┳  " description)
                          (string-append "┗┳  " description)))
                     ((logical-partition? partition)
                      (if (other-logical-partitions? rest-partitions)
                          (if (other-non-logical-partitions? rest-partitions)
                              (string-append "┃┣━ " description)
                              (string-append " ┣━ " description))
                          (if (other-non-logical-partitions? rest-partitions)
                              (string-append "┃┗━ " description)
                              (string-append " ┗━ " description))))
                     (else
                      (string-append "┣━  " description)))
                    (add-tree-symbols rest-partitions
                                      rest-descriptions))))))))

  (define (skip-item? item)
    (eq? (car item) 'skip))

  (define (disk-items)
    "Return the list of strings describing DISKS."
    (let loop ((disks disks))
      (match disks
        (() '())
        ((disk . rest)
         (let* ((device (disk-device disk))
                (partitions (disk-partitions disk))
                (partitions*
                 (filter-map
                  (lambda (partition)
                    (and (not (metadata-partition? partition))
                         (not (small-freespace-partition? device
                                                          partition))
                         partition))
                  partitions))
                (descriptions (add-tree-symbols
                               partitions*
                               (partitions-descriptions partitions*
                                                        user-partitions)))
                (partition-items (map cons partitions* descriptions)))
           (append
            `((,disk . ,(device-description device disk))
              ,@partition-items
              ,@(if (null? rest)
                    '()
                    '((skip . ""))))
            (loop rest)))))))

  (define (remove-user-partition-by-partition user-partitions partition)
    "Return the USER-PARTITIONS list with the record with the given PARTITION
object removed. If PARTITION is an extended partition, also remove all logical
partitions from USER-PARTITIONS."
    (remove (lambda (p)
              (let ((cur-partition (user-partition-parted-object p)))
                (or (equal? cur-partition partition)
                    (and (extended-partition? partition)
                         (logical-partition? cur-partition)))))
            user-partitions))

  (define (remove-user-partition-by-disk user-partitions disk)
    "Return the USER-PARTITIONS list with the <user-partition> records located
on given DISK removed."
    (remove (lambda (p)
              (let* ((partition (user-partition-parted-object p))
                     (cur-disk (partition-disk partition)))
                (equal? cur-disk disk)))
            user-partitions))

  (define (update-user-partitions user-partitions new-user-partition)
    "Update or insert NEW-USER-PARTITION record in USER-PARTITIONS list
depending if one of the <user-partition> record in USER-PARTITIONS has the
same PARTITION object as NEW-USER-PARTITION."
    (let* ((partition (user-partition-parted-object new-user-partition))
           (user-partitions*
            (remove-user-partition-by-partition user-partitions
                                                partition)))
      (cons new-user-partition user-partitions*)))

  (define (button-ok-action)
    "Commit the modifications to all DISKS and return #t."
    (for-each (lambda (disk)
                (disk-commit disk))
              disks)
    #t)

  (define (listbox-action listbox-item)
    "A disk or a partition has been selected. If it's a disk, ask for a label
to create a new partition table. If it is a partition, propose the user to
edit it."
    (let ((item (car listbox-item)))
      (cond
       ((disk? item)
        (let ((label (run-label-page (G_ "Back") (const #f))))
          (if label
              (let* ((device (disk-device item))
                     (new-disk (mklabel device label))
                     (commit-new-disk (disk-commit new-disk))
                     (other-disks (remove (lambda (disk)
                                            (equal? disk item))
                                          disks))
                     (new-user-partitions
                      (remove-user-partition-by-disk user-partitions item)))
                `((disks . ,(cons new-disk other-disks))
                  (user-partitions . ,new-user-partitions)))
              `((disks . ,disks)
                (user-partitions . ,user-partitions)))))
       ((partition? item)
        (let* ((partition item)
               (disk (partition-disk partition))
               (device (disk-device disk))
               (existing-user-partition
                (find-user-partition-by-parted-object user-partitions
                                                      partition))
               (edit-user-partition
                (or existing-user-partition
                    (partition->user-partition partition))))
          `((disks . ,disks)
            (user-partitions . ,user-partitions)
            (edit-user-partition . ,edit-user-partition)))))))

  (define (hotkey-action key listbox-item)
    "The DELETE key has been pressed on a disk or a partition item."
    (let ((item (car listbox-item))
          (default-result
            `((disks . ,disks)
              (user-partitions . ,user-partitions))))
      (cond
       ((disk? item)
        (let* ((device (disk-device item))
               (file-name (device-path device))
               (info-text
                (format #f (G_ "Are you sure you want to delete everything on disk ~a?")
                        file-name))
               (result (choice-window (G_ "Delete disk")
                                      (G_ "OK")
                                      (G_ "Exit")
                                      info-text)))
          (case result
            ((1)
             (disk-remove-all-partitions item)
             `((disks . ,disks)
               (user-partitions
                . ,(remove-user-partition-by-disk user-partitions item))))
            (else
             default-result))))
       ((partition? item)
        (if (freespace-partition? item)
            (begin
              (run-error-page (G_ "You cannot delete a free space area.")
                              (G_ "Delete partition"))
              default-result)
            (let* ((disk (partition-disk item))
                   (number-str (partition-print-number item))
                   (info-text
                    (format #f (G_ "Are you sure you want to delete partition ~a?")
                            number-str))
                   (result (choice-window (G_ "Delete partition")
                                          (G_ "OK")
                                          (G_ "Exit")
                                          info-text)))
              (case result
                ((1)
                 (let ((new-user-partitions
                        (remove-user-partition-by-partition user-partitions
                                                            item)))
                   (disk-remove-partition* disk item)
                   `((disks . ,disks)
                     (user-partitions . ,new-user-partitions))))
                (else
                 default-result))))))))

  (let* ((info-text (G_ "You can change a disk's partition table by \
selecting it and pressing ENTER. You can also edit a partition by selecting it \
and pressing ENTER, or remove it by pressing DELETE. To create a new \
partition, select a free space area and press ENTER.

At least one partition must have its mounting point set to '/'."))
         (guided-info-text (format #f (G_ "This is the proposed \
partitioning. It is still possible to edit it or to go back to install menu \
by pressing the Exit button.~%~%")))
         (result
          (run-listbox-selection-page
           #:info-text (if guided?
                           (string-append guided-info-text info-text)
                           info-text)

          #:title (if guided?
                      (G_ "Guided partitioning")
                      (G_ "Manual partitioning"))
          #:info-textbox-width 76         ;we need a lot of room for INFO-TEXT
          #:listbox-height (max 5 (- (screen-rows) 30))
          #:listbox-items (disk-items)
          #:listbox-item->text cdr
          #:sort-listbox-items? #f
          #:skip-item-procedure? skip-item?
          #:allow-delete? #t
          #:button-text (G_ "OK")
          #:button-callback-procedure button-ok-action

          ;; Consider client replies equivalent to hitting the "OK" button.
          ;; XXX: In practice this means that clients cannot do anything but
          ;; approve the predefined list of partitions.
          #:client-callback-procedure (lambda (_) (button-ok-action))

          #:button2-text (G_ "Exit")
          #:button2-callback-procedure button-exit-action
          #:listbox-callback-procedure listbox-action
          #:hotkey-callback-procedure hotkey-action)))
    (if (eq? result #t)
        (let ((user-partitions-ok?
               (guard
                   (c ((no-root-mount-point? c)
                       (run-error-page
                        (G_ "No root mount point found.")
                        (G_ "Missing mount point"))
                       #f)
                      ((cannot-read-uuid? c)
                       (run-error-page
                        (format #f (G_ "Cannot read the ~a partition UUID.\
 You may need to format it.")
                                (cannot-read-uuid-partition c))
                        (G_ "Wrong partition format"))
                       #f))
                 (check-user-partitions user-partitions))))
          (if user-partitions-ok?
              user-partitions
              (run-disk-page disks user-partitions
                             #:guided? guided?)))
        (let* ((result-disks (assoc-ref result 'disks))
               (result-user-partitions (assoc-ref result
                                                  'user-partitions))
               (edit-user-partition (assoc-ref result
                                               'edit-user-partition))
               (can-create-partition?
                (and edit-user-partition
                     (inform-can-create-partition? edit-user-partition)))
               (new-user-partition (and edit-user-partition
                                        can-create-partition?
                                        (run-partition-page
                                         edit-user-partition)))
               (new-user-partitions
                (if new-user-partition
                    (update-user-partitions result-user-partitions
                                            new-user-partition)
                    result-user-partitions)))
          (run-disk-page result-disks new-user-partitions
                         #:guided? guided?)))))

(define (run-partitioning-page)
  "Run a page asking the user for a partitioning method."
  (define (run-page devices)
    (let* ((items
            `((entire . ,(G_ "Guided - using the entire disk"))
              (entire-encrypted . ,(G_ "Guided - using the entire disk with encryption"))
              (manual . ,(G_ "Manual"))))
           (result (run-listbox-selection-page
                    #:info-text (G_ "Please select a partitioning method.")
                    #:title (G_ "Partitioning method")
                    #:listbox-height (+ (length items) 2)
                    #:listbox-items items
                    #:listbox-item->text cdr
                    #:sort-listbox-items? #f
                    #:button-text (G_ "Exit")
                    #:button-callback-procedure button-exit-action))
           (method (car result)))
      (cond
       ((or (eq? method 'entire)
            (eq? method 'entire-encrypted))
         (let* ((device (run-device-page devices))
                (disk-type (disk-probe device))
                (disk (if disk-type
                          (disk-new device)
                          (let* ((label (run-label-page
                                         (G_ "Exit")
                                         button-exit-action))
                                 (disk (mklabel device label)))
                            (disk-commit disk)
                            disk)))
                (scheme (symbol-append method '- (run-scheme-page)))
                (user-partitions (auto-partition! disk #:scheme scheme)))
           (run-disk-page (list disk) user-partitions
                          #:guided? #t)))
       ((eq? method 'manual)
         (let* ((disks (filter-map disk-new devices))
                (user-partitions (append-map
                                  create-special-user-partitions
                                  (map disk-partitions disks)))
                (result-user-partitions (run-disk-page disks
                                                       user-partitions)))
           result-user-partitions)))))

  (init-parted)
  (let* ((non-install-devices (non-install-devices))
         (user-partitions (run-page non-install-devices))
         (user-partitions-with-pass (prompt-luks-passwords
                                     user-partitions))
         (form (draw-formatting-page user-partitions)))
    ;; Make sure the disks are not in use before proceeding to formatting.
    (free-parted non-install-devices)
    (format-user-partitions user-partitions-with-pass)
    (syslog "formatted ~a user partitions~%"
            (length user-partitions-with-pass))
    (syslog "user-partitions: ~a~%" user-partitions)

    (destroy-form-and-pop form)
    user-partitions))