1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2018 Mathieu Othacehe <m.othacehe@gmail.com>
4 ;;; This file is part of GNU Guix.
6 ;;; GNU Guix is free software; you can redistribute it and/or modify it
7 ;;; under the terms of the GNU General Public License as published by
8 ;;; the Free Software Foundation; either version 3 of the License, or (at
9 ;;; your option) any later version.
11 ;;; GNU Guix is distributed in the hope that it will be useful, but
12 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ;;; GNU General Public License for more details.
16 ;;; You should have received a copy of the GNU General Public License
17 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19 (define-module (gnu installer newt page)
20 #:use-module (gnu installer utils)
21 #:use-module (gnu installer newt utils)
22 #:use-module (guix i18n)
23 #:use-module (ice-9 match)
24 #:use-module (ice-9 receive)
25 #:use-module (srfi srfi-1)
26 #:use-module (srfi srfi-26)
28 #:export (draw-info-page
32 run-listbox-selection-page
34 run-checkbox-tree-page
35 run-file-textbox-page))
39 ;;; Some helpers around guile-newt to draw or run generic pages. The
40 ;;; difference between 'draw' and 'run' terms comes from newt library. A page
41 ;;; is drawn when the form it contains does not expect any user
42 ;;; interaction. In that case, it is necessary to call (newt-refresh) to force
43 ;;; the page to be displayed. When a form is 'run', it is blocked waiting for
44 ;;; any action from the user (press a button, input some text, ...).
48 (define (draw-info-page text title)
49 "Draw an informative page with the given TEXT as content. Set the title of
52 (make-reflowed-textbox -1 -1 text 40
54 (grid (make-grid 1 1))
56 (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
57 (add-component-to-form form text-box)
58 (make-wrapped-grid-window grid title)
60 ;; This call is imperative, otherwise the form won't be displayed. See the
61 ;; explanation in the above commentary.
65 (define (draw-connecting-page service-name)
66 "Draw a page to indicate a connection in in progress."
68 (format #f (G_ "Connecting to ~a, please wait.") service-name)
69 (G_ "Connection in progress")))
71 (define* (run-input-page text title
73 (allow-empty-input? #f)
75 (input-field-width 40))
76 "Run a page to prompt user for an input. The given TEXT will be displayed
77 above the input field. The page title is set to TITLE. Unless
78 allow-empty-input? is set to #t, an error page will be displayed if the user
79 enters an empty input."
81 (make-reflowed-textbox -1 -1 text
84 (grid (make-grid 1 3))
85 (input-entry (make-entry -1 -1 20))
86 (ok-button (make-button -1 -1 (G_ "OK")))
90 (set-entry-text input-entry default-text))
92 (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
93 (set-grid-field grid 0 1 GRID-ELEMENT-COMPONENT input-entry
95 (set-grid-field grid 0 2 GRID-ELEMENT-COMPONENT ok-button
98 (add-components-to-form form text-box input-entry ok-button)
99 (make-wrapped-grid-window grid title)
100 (let ((error-page (lambda ()
101 (run-error-page (G_ "Please enter a non empty input.")
102 (G_ "Empty input")))))
104 (receive (exit-reason argument)
106 (let ((input (entry-value input-entry)))
107 (if (and (not allow-empty-input?)
108 (eq? exit-reason 'exit-component)
111 ;; Display the error page.
113 ;; Set the focus back to the input input field.
114 (set-current-component form input-entry)
117 (destroy-form-and-pop form)
120 (define (run-error-page text title)
121 "Run a page to inform the user of an error. The page contains the given TEXT
122 to explain the error and an \"OK\" button to acknowledge the error. The title
123 of the page is set to TITLE."
125 (make-reflowed-textbox -1 -1 text 40
126 #:flags FLAG-BORDER))
127 (grid (make-grid 1 2))
128 (ok-button (make-button -1 -1 "OK"))
131 (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
132 (set-grid-field grid 0 1 GRID-ELEMENT-COMPONENT ok-button
135 ;; Set the background color to red to indicate something went wrong.
136 (newt-set-color COLORSET-ROOT "white" "red")
137 (add-components-to-form form text-box ok-button)
138 (make-wrapped-grid-window grid title)
140 ;; Restore the background to its original color.
141 (newt-set-color COLORSET-ROOT "white" "blue")
142 (destroy-form-and-pop form)))
144 (define* (run-listbox-selection-page #:key
147 (info-textbox-width 50)
151 (listbox-default-item #f)
152 (listbox-allow-multiple? #f)
153 (sort-listbox-items? #t)
155 (skip-item-procedure?
158 (button-callback-procedure
161 (button2-callback-procedure
163 (listbox-callback-procedure
165 (hotkey-callback-procedure
167 "Run a page asking the user to select an item in a listbox. The page
168 contains, stacked vertically from the top to the bottom, an informative text
169 set to INFO-TEXT, a listbox and a button. The listbox will be filled with
170 LISTBOX-ITEMS converted to text by applying the procedure LISTBOX-ITEM->TEXT
171 on every item. The selected item from LISTBOX-ITEMS is returned. The button
172 text is set to BUTTON-TEXT and the procedure BUTTON-CALLBACK-PROCEDURE called
173 when it is pressed. The procedure LISTBOX-CALLBACK-PROCEDURE is called when an
174 item from the listbox is selected (by pressing the <ENTER> key).
176 INFO-TEXTBOX-WIDTH is the width of the textbox where INFO-TEXT will be
177 displayed. LISTBOX-HEIGHT is the height of the listbox.
179 If LISTBOX-DEFAULT-ITEM is set to the value of one of the items in
180 LISTBOX-ITEMS, it will be selected by default. Otherwise, the first element of
181 the listbox is selected.
183 If LISTBOX-ALLOW-MULTIPLE? is set to #t, multiple items from the listbox can
184 be selected (using the <SPACE> key). It that case, a list containing the
185 selected items will be returned.
187 If SORT-LISTBOX-ITEMS? is set to #t, the listbox items are sorted using
188 'string<=' procedure (after being converted to text).
190 If ALLOW-DELETE? is #t, the form will return if the <DELETE> key is pressed,
191 otherwise nothing will happen.
193 Each time the listbox current item changes, call SKIP-ITEM-PROCEDURE? with the
194 current listbox item as argument. If it returns #t, skip the element and jump
195 to the next/previous one depending on the previous item, otherwise do
198 (define (fill-listbox listbox items)
199 "Append the given ITEMS to LISTBOX, once they have been converted to text
200 with LISTBOX-ITEM->TEXT. Each item appended to the LISTBOX is given a key by
201 newt. Save this key by returning an association list under the form:
203 ((NEWT-LISTBOX-KEY . ITEM) ...)
205 where NEWT-LISTBOX-KEY is the key returned by APPEND-ENTRY-TO-LISTBOX, when
206 ITEM was inserted into LISTBOX."
208 (let* ((text (listbox-item->text item))
209 (key (append-entry-to-listbox listbox text)))
213 (define (sort-listbox-items listbox-items)
214 "Return LISTBOX-ITEMS sorted using the 'string<=' procedure on the text
215 corresponding to each item in the list."
216 (let* ((items (map (lambda (item)
217 (cons item (listbox-item->text item)))
220 (sort items (lambda (a b)
221 (let ((text-a (cdr a))
223 (string<= text-a text-b))))))
224 (map car sorted-items)))
226 ;; Store the last selected listbox item's key.
227 (define last-listbox-key (make-parameter #f))
229 (define (previous-key keys key)
230 (let ((index (list-index (cut eq? key <>) keys)))
233 (list-ref keys (- index 1)))))
235 (define (next-key keys key)
236 (let ((index (list-index (cut eq? key <>) keys)))
238 (< index (- (length keys) 1))
239 (list-ref keys (+ index 1)))))
241 (define (set-default-item listbox listbox-keys default-item)
242 "Set the default item of LISTBOX to DEFAULT-ITEM. LISTBOX-KEYS is the
243 association list returned by the FILL-LISTBOX procedure. It is used because
244 the current listbox item has to be selected by key."
245 (for-each (match-lambda
247 (when (equal? item default-item)
248 (set-current-listbox-entry-by-key listbox key))))
251 (let* ((listbox (make-listbox
254 (logior FLAG-SCROLL FLAG-BORDER FLAG-RETURNEXIT
255 (if listbox-allow-multiple?
260 (make-reflowed-textbox -1 -1 info-text
262 #:flags FLAG-BORDER))
263 (button (make-button -1 -1 button-text))
264 (button2 (and button2-text
265 (make-button -1 -1 button2-text)))
266 (grid (vertically-stacked-grid
267 GRID-ELEMENT-COMPONENT info-textbox
268 GRID-ELEMENT-COMPONENT listbox
271 horizontal-stacked-grid
272 GRID-ELEMENT-COMPONENT button
274 (list GRID-ELEMENT-COMPONENT button2)
276 (sorted-items (if sort-listbox-items?
277 (sort-listbox-items listbox-items)
279 (keys (fill-listbox listbox sorted-items)))
281 ;; On every listbox element change, check if we need to skip it. If yes,
282 ;; depending on the 'last-listbox-key', jump forward or backward. If no,
284 (add-component-callback
287 (let* ((current-key (current-listbox-entry listbox))
288 (listbox-keys (map car keys))
289 (last-key (last-listbox-key))
290 (item (assoc-ref keys current-key))
291 (prev-key (previous-key listbox-keys current-key))
292 (next-key (next-key listbox-keys current-key)))
293 ;; Update last-listbox-key before a potential call to
294 ;; set-current-listbox-entry-by-key, because it will immediately
295 ;; cause this callback to be called for the new entry.
296 (last-listbox-key current-key)
297 (when (skip-item-procedure? item)
298 (when (eq? prev-key last-key)
300 (set-current-listbox-entry-by-key listbox next-key)
301 (set-current-listbox-entry-by-key listbox prev-key)))
302 (when (eq? next-key last-key)
304 (set-current-listbox-entry-by-key listbox prev-key)
305 (set-current-listbox-entry-by-key listbox next-key)))))))
307 (when listbox-default-item
308 (set-default-item listbox keys listbox-default-item))
311 (form-add-hotkey form KEY-DELETE))
313 (add-form-to-grid grid form #t)
314 (make-wrapped-grid-window grid title)
316 (receive (exit-reason argument)
324 ((components=? argument button)
325 (button-callback-procedure))
327 (components=? argument button2))
328 (button2-callback-procedure))
329 ((components=? argument listbox)
330 (if listbox-allow-multiple?
331 (let* ((entries (listbox-selection listbox))
332 (items (map (lambda (entry)
333 (assoc-ref keys entry))
335 (listbox-callback-procedure items))
336 (let* ((entry (current-listbox-entry listbox))
337 (item (assoc-ref keys entry)))
338 (listbox-callback-procedure item))))))
340 (let* ((entry (current-listbox-entry listbox))
341 (item (assoc-ref keys entry)))
342 (hotkey-callback-procedure argument item)))))
344 (destroy-form-and-pop form))))))
346 (define* (run-scale-page #:key
349 (info-textbox-width 50)
351 (scale-full-value 100)
353 (max-scale-update 5))
354 "Run a page with a progress bar (called 'scale' in newt). The given
355 INFO-TEXT is displayed in a textbox above the scale. The width of the textbox
356 is set to INFO-TEXTBOX-WIDTH. The width of the scale is set to
357 SCALE-WIDTH. SCALE-FULL-VALUE indicates the value that correspond to 100% of
360 The procedure SCALE-UPDATE-PROC shall return a new scale
361 value. SCALE-UPDATE-PROC will be called until the returned value is superior
362 or equal to SCALE-FULL-VALUE, but no more than MAX-SCALE-UPDATE times. An
363 error is raised if the MAX-SCALE-UPDATE limit is reached."
365 (make-reflowed-textbox -1 -1 info-text
367 #:flags FLAG-BORDER))
368 (scale (make-scale -1 -1 scale-width scale-full-value))
369 (grid (vertically-stacked-grid
370 GRID-ELEMENT-COMPONENT info-textbox
371 GRID-ELEMENT-COMPONENT scale))
374 (add-form-to-grid grid form #t)
375 (make-wrapped-grid-window grid title)
378 ;; This call is imperative, otherwise the form won't be displayed. See the
379 ;; explanation in the above commentary.
385 (let loop ((i max-scale-update)
387 (let ((value (scale-update-proc last-value)))
388 (set-scale-value scale value)
391 (unless (>= value scale-full-value)
394 (error "Max scale updates reached."))))))
396 (destroy-form-and-pop form)))))
398 (define* (run-checkbox-tree-page #:key
403 (info-textbox-width 50)
404 (checkbox-tree-height 10)
405 (ok-button-callback-procedure
407 (exit-button-callback-procedure
409 "Run a page allowing the user to select one or multiple items among ITEMS in
410 a checkbox list. The page contains vertically stacked from the top to the
411 bottom, an informative text set to INFO-TEXT, the checkbox list and two
412 buttons, 'Ok' and 'Exit'. The page title's is set to TITLE. ITEMS are
413 converted to text using ITEM->TEXT before being displayed in the checkbox
416 INFO-TEXTBOX-WIDTH is the width of the textbox where INFO-TEXT will be
417 displayed. CHECKBOX-TREE-HEIGHT is the height of the checkbox list.
419 OK-BUTTON-CALLBACK-PROCEDURE is called when the 'Ok' button is pressed.
420 EXIT-BUTTON-CALLBACK-PROCEDURE is called when the 'Exit' button is
423 This procedure returns the list of checked items in the checkbox list among
424 ITEMS when 'Ok' is pressed."
425 (define (fill-checkbox-tree checkbox-tree items)
428 (let* ((item-text (item->text item))
429 (key (add-entry-to-checkboxtree checkbox-tree item-text 0)))
433 (let* ((checkbox-tree
434 (make-checkboxtree -1 -1
438 (make-reflowed-textbox -1 -1 info-text
440 #:flags FLAG-BORDER))
441 (ok-button (make-button -1 -1 (G_ "OK")))
442 (exit-button (make-button -1 -1 (G_ "Exit")))
443 (grid (vertically-stacked-grid
444 GRID-ELEMENT-COMPONENT info-textbox
445 GRID-ELEMENT-COMPONENT checkbox-tree
447 (horizontal-stacked-grid
448 GRID-ELEMENT-COMPONENT ok-button
449 GRID-ELEMENT-COMPONENT exit-button)))
450 (keys (fill-checkbox-tree checkbox-tree items))
453 (add-form-to-grid grid form #t)
454 (make-wrapped-grid-window grid title)
456 (receive (exit-reason argument)
464 ((components=? argument ok-button)
465 (let* ((entries (current-checkbox-selection checkbox-tree))
466 (current-items (map (lambda (entry)
467 (assoc-ref keys entry))
469 (ok-button-callback-procedure)
471 ((components=? argument exit-button)
472 (exit-button-callback-procedure))))))
474 (destroy-form-and-pop form))))))
476 (define* (run-file-textbox-page #:key
480 (info-textbox-width 50)
481 (file-textbox-width 50)
482 (file-textbox-height 30)
484 (ok-button-callback-procedure
486 (exit-button-callback-procedure
489 (make-reflowed-textbox -1 -1 info-text
491 #:flags FLAG-BORDER))
492 (file-text (read-all file))
497 (logior FLAG-SCROLL FLAG-BORDER)))
498 (ok-button (make-button -1 -1 (G_ "OK")))
499 (exit-button (make-button -1 -1 (G_ "Exit")))
500 (grid (vertically-stacked-grid
501 GRID-ELEMENT-COMPONENT info-textbox
502 GRID-ELEMENT-COMPONENT file-textbox
505 horizontal-stacked-grid
506 GRID-ELEMENT-COMPONENT ok-button
508 (list GRID-ELEMENT-COMPONENT exit-button)
512 (set-textbox-text file-textbox file-text)
513 (add-form-to-grid grid form #t)
514 (make-wrapped-grid-window grid title)
516 (receive (exit-reason argument)
524 ((components=? argument ok-button)
525 (ok-button-callback-procedure))
527 (components=? argument exit-button))
528 (exit-button-callback-procedure))))))
530 (destroy-form-and-pop form))))))