page.scm 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2018 Mathieu Othacehe <m.othacehe@gmail.com>
  3. ;;;
  4. ;;; This file is part of GNU Guix.
  5. ;;;
  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.
  10. ;;;
  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.
  15. ;;;
  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/>.
  18. (define-module (gnu installer newt page)
  19. #:use-module (gnu installer utils)
  20. #:use-module (gnu installer newt utils)
  21. #:use-module (guix i18n)
  22. #:use-module (ice-9 match)
  23. #:use-module (ice-9 receive)
  24. #:use-module (srfi srfi-1)
  25. #:use-module (srfi srfi-26)
  26. #:use-module (newt)
  27. #:export (draw-info-page
  28. draw-connecting-page
  29. run-input-page
  30. run-error-page
  31. run-listbox-selection-page
  32. run-scale-page
  33. run-checkbox-tree-page
  34. run-file-textbox-page))
  35. ;;; Commentary:
  36. ;;;
  37. ;;; Some helpers around guile-newt to draw or run generic pages. The
  38. ;;; difference between 'draw' and 'run' terms comes from newt library. A page
  39. ;;; is drawn when the form it contains does not expect any user
  40. ;;; interaction. In that case, it is necessary to call (newt-refresh) to force
  41. ;;; the page to be displayed. When a form is 'run', it is blocked waiting for
  42. ;;; any action from the user (press a button, input some text, ...).
  43. ;;;
  44. ;;; Code:
  45. (define (draw-info-page text title)
  46. "Draw an informative page with the given TEXT as content. Set the title of
  47. this page to TITLE."
  48. (let* ((text-box
  49. (make-reflowed-textbox -1 -1 text 40
  50. #:flags FLAG-BORDER))
  51. (grid (make-grid 1 1))
  52. (form (make-form)))
  53. (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
  54. (add-component-to-form form text-box)
  55. (make-wrapped-grid-window grid title)
  56. (draw-form form)
  57. ;; This call is imperative, otherwise the form won't be displayed. See the
  58. ;; explanation in the above commentary.
  59. (newt-refresh)
  60. form))
  61. (define (draw-connecting-page service-name)
  62. "Draw a page to indicate a connection in in progress."
  63. (draw-info-page
  64. (format #f (G_ "Connecting to ~a, please wait.") service-name)
  65. (G_ "Connection in progress")))
  66. (define* (run-input-page text title
  67. #:key
  68. (allow-empty-input? #f)
  69. (default-text #f)
  70. (input-field-width 40))
  71. "Run a page to prompt user for an input. The given TEXT will be displayed
  72. above the input field. The page title is set to TITLE. Unless
  73. allow-empty-input? is set to #t, an error page will be displayed if the user
  74. enters an empty input."
  75. (let* ((text-box
  76. (make-reflowed-textbox -1 -1 text
  77. input-field-width
  78. #:flags FLAG-BORDER))
  79. (grid (make-grid 1 3))
  80. (input-entry (make-entry -1 -1 20))
  81. (ok-button (make-button -1 -1 (G_ "OK")))
  82. (form (make-form)))
  83. (when default-text
  84. (set-entry-text input-entry default-text))
  85. (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
  86. (set-grid-field grid 0 1 GRID-ELEMENT-COMPONENT input-entry
  87. #:pad-top 1)
  88. (set-grid-field grid 0 2 GRID-ELEMENT-COMPONENT ok-button
  89. #:pad-top 1)
  90. (add-components-to-form form text-box input-entry ok-button)
  91. (make-wrapped-grid-window grid title)
  92. (let ((error-page (lambda ()
  93. (run-error-page (G_ "Please enter a non empty input.")
  94. (G_ "Empty input")))))
  95. (let loop ()
  96. (receive (exit-reason argument)
  97. (run-form form)
  98. (let ((input (entry-value input-entry)))
  99. (if (and (not allow-empty-input?)
  100. (eq? exit-reason 'exit-component)
  101. (string=? input ""))
  102. (begin
  103. ;; Display the error page.
  104. (error-page)
  105. ;; Set the focus back to the input input field.
  106. (set-current-component form input-entry)
  107. (loop))
  108. (begin
  109. (destroy-form-and-pop form)
  110. input))))))))
  111. (define (run-error-page text title)
  112. "Run a page to inform the user of an error. The page contains the given TEXT
  113. to explain the error and an \"OK\" button to acknowledge the error. The title
  114. of the page is set to TITLE."
  115. (let* ((text-box
  116. (make-reflowed-textbox -1 -1 text 40
  117. #:flags FLAG-BORDER))
  118. (grid (make-grid 1 2))
  119. (ok-button (make-button -1 -1 "OK"))
  120. (form (make-form)))
  121. (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
  122. (set-grid-field grid 0 1 GRID-ELEMENT-COMPONENT ok-button
  123. #:pad-top 1)
  124. ;; Set the background color to red to indicate something went wrong.
  125. (newt-set-color COLORSET-ROOT "white" "red")
  126. (add-components-to-form form text-box ok-button)
  127. (make-wrapped-grid-window grid title)
  128. (run-form form)
  129. ;; Restore the background to its original color.
  130. (newt-set-color COLORSET-ROOT "white" "blue")
  131. (destroy-form-and-pop form)))
  132. (define* (run-listbox-selection-page #:key
  133. info-text
  134. title
  135. (info-textbox-width 50)
  136. listbox-items
  137. listbox-item->text
  138. (listbox-height 20)
  139. (listbox-default-item #f)
  140. (listbox-allow-multiple? #f)
  141. (sort-listbox-items? #t)
  142. (allow-delete? #f)
  143. (skip-item-procedure?
  144. (const #f))
  145. button-text
  146. (button-callback-procedure
  147. (const #t))
  148. (button2-text #f)
  149. (button2-callback-procedure
  150. (const #t))
  151. (listbox-callback-procedure
  152. identity)
  153. (hotkey-callback-procedure
  154. (const #t)))
  155. "Run a page asking the user to select an item in a listbox. The page
  156. contains, stacked vertically from the top to the bottom, an informative text
  157. set to INFO-TEXT, a listbox and a button. The listbox will be filled with
  158. LISTBOX-ITEMS converted to text by applying the procedure LISTBOX-ITEM->TEXT
  159. on every item. The selected item from LISTBOX-ITEMS is returned. The button
  160. text is set to BUTTON-TEXT and the procedure BUTTON-CALLBACK-PROCEDURE called
  161. when it is pressed. The procedure LISTBOX-CALLBACK-PROCEDURE is called when an
  162. item from the listbox is selected (by pressing the <ENTER> key).
  163. INFO-TEXTBOX-WIDTH is the width of the textbox where INFO-TEXT will be
  164. displayed. LISTBOX-HEIGHT is the height of the listbox.
  165. If LISTBOX-DEFAULT-ITEM is set to the value of one of the items in
  166. LISTBOX-ITEMS, it will be selected by default. Otherwise, the first element of
  167. the listbox is selected.
  168. If LISTBOX-ALLOW-MULTIPLE? is set to #t, multiple items from the listbox can
  169. be selected (using the <SPACE> key). It that case, a list containing the
  170. selected items will be returned.
  171. If SORT-LISTBOX-ITEMS? is set to #t, the listbox items are sorted using
  172. 'string<=' procedure (after being converted to text).
  173. If ALLOW-DELETE? is #t, the form will return if the <DELETE> key is pressed,
  174. otherwise nothing will happen.
  175. Each time the listbox current item changes, call SKIP-ITEM-PROCEDURE? with the
  176. current listbox item as argument. If it returns #t, skip the element and jump
  177. to the next/previous one depending on the previous item, otherwise do
  178. nothing."
  179. (define (fill-listbox listbox items)
  180. "Append the given ITEMS to LISTBOX, once they have been converted to text
  181. with LISTBOX-ITEM->TEXT. Each item appended to the LISTBOX is given a key by
  182. newt. Save this key by returning an association list under the form:
  183. ((NEWT-LISTBOX-KEY . ITEM) ...)
  184. where NEWT-LISTBOX-KEY is the key returned by APPEND-ENTRY-TO-LISTBOX, when
  185. ITEM was inserted into LISTBOX."
  186. (map (lambda (item)
  187. (let* ((text (listbox-item->text item))
  188. (key (append-entry-to-listbox listbox text)))
  189. (cons key item)))
  190. items))
  191. (define (sort-listbox-items listbox-items)
  192. "Return LISTBOX-ITEMS sorted using the 'string<=' procedure on the text
  193. corresponding to each item in the list."
  194. (let* ((items (map (lambda (item)
  195. (cons item (listbox-item->text item)))
  196. listbox-items))
  197. (sorted-items
  198. (sort items (lambda (a b)
  199. (let ((text-a (cdr a))
  200. (text-b (cdr b)))
  201. (string<= text-a text-b))))))
  202. (map car sorted-items)))
  203. ;; Store the last selected listbox item's key.
  204. (define last-listbox-key (make-parameter #f))
  205. (define (previous-key keys key)
  206. (let ((index (list-index (cut eq? key <>) keys)))
  207. (and index
  208. (> index 0)
  209. (list-ref keys (- index 1)))))
  210. (define (next-key keys key)
  211. (let ((index (list-index (cut eq? key <>) keys)))
  212. (and index
  213. (< index (- (length keys) 1))
  214. (list-ref keys (+ index 1)))))
  215. (define (set-default-item listbox listbox-keys default-item)
  216. "Set the default item of LISTBOX to DEFAULT-ITEM. LISTBOX-KEYS is the
  217. association list returned by the FILL-LISTBOX procedure. It is used because
  218. the current listbox item has to be selected by key."
  219. (for-each (match-lambda
  220. ((key . item)
  221. (when (equal? item default-item)
  222. (set-current-listbox-entry-by-key listbox key))))
  223. listbox-keys))
  224. (let* ((listbox (make-listbox
  225. -1 -1
  226. listbox-height
  227. (logior FLAG-SCROLL FLAG-BORDER FLAG-RETURNEXIT
  228. (if listbox-allow-multiple?
  229. FLAG-MULTIPLE
  230. 0))))
  231. (form (make-form))
  232. (info-textbox
  233. (make-reflowed-textbox -1 -1 info-text
  234. info-textbox-width
  235. #:flags FLAG-BORDER))
  236. (button (make-button -1 -1 button-text))
  237. (button2 (and button2-text
  238. (make-button -1 -1 button2-text)))
  239. (grid (vertically-stacked-grid
  240. GRID-ELEMENT-COMPONENT info-textbox
  241. GRID-ELEMENT-COMPONENT listbox
  242. GRID-ELEMENT-SUBGRID
  243. (apply
  244. horizontal-stacked-grid
  245. GRID-ELEMENT-COMPONENT button
  246. `(,@(if button2
  247. (list GRID-ELEMENT-COMPONENT button2)
  248. '())))))
  249. (sorted-items (if sort-listbox-items?
  250. (sort-listbox-items listbox-items)
  251. listbox-items))
  252. (keys (fill-listbox listbox sorted-items)))
  253. ;; On every listbox element change, check if we need to skip it. If yes,
  254. ;; depending on the 'last-listbox-key', jump forward or backward. If no,
  255. ;; do nothing.
  256. (add-component-callback
  257. listbox
  258. (lambda (component)
  259. (let* ((current-key (current-listbox-entry listbox))
  260. (listbox-keys (map car keys))
  261. (last-key (last-listbox-key))
  262. (item (assoc-ref keys current-key))
  263. (prev-key (previous-key listbox-keys current-key))
  264. (next-key (next-key listbox-keys current-key)))
  265. ;; Update last-listbox-key before a potential call to
  266. ;; set-current-listbox-entry-by-key, because it will immediately
  267. ;; cause this callback to be called for the new entry.
  268. (last-listbox-key current-key)
  269. (when (skip-item-procedure? item)
  270. (when (eq? prev-key last-key)
  271. (if next-key
  272. (set-current-listbox-entry-by-key listbox next-key)
  273. (set-current-listbox-entry-by-key listbox prev-key)))
  274. (when (eq? next-key last-key)
  275. (if prev-key
  276. (set-current-listbox-entry-by-key listbox prev-key)
  277. (set-current-listbox-entry-by-key listbox next-key)))))))
  278. (when listbox-default-item
  279. (set-default-item listbox keys listbox-default-item))
  280. (when allow-delete?
  281. (form-add-hotkey form KEY-DELETE))
  282. (add-form-to-grid grid form #t)
  283. (make-wrapped-grid-window grid title)
  284. (receive (exit-reason argument)
  285. (run-form form)
  286. (dynamic-wind
  287. (const #t)
  288. (lambda ()
  289. (case exit-reason
  290. ((exit-component)
  291. (cond
  292. ((components=? argument button)
  293. (button-callback-procedure))
  294. ((and button2
  295. (components=? argument button2))
  296. (button2-callback-procedure))
  297. ((components=? argument listbox)
  298. (if listbox-allow-multiple?
  299. (let* ((entries (listbox-selection listbox))
  300. (items (map (lambda (entry)
  301. (assoc-ref keys entry))
  302. entries)))
  303. (listbox-callback-procedure items))
  304. (let* ((entry (current-listbox-entry listbox))
  305. (item (assoc-ref keys entry)))
  306. (listbox-callback-procedure item))))))
  307. ((exit-hotkey)
  308. (let* ((entry (current-listbox-entry listbox))
  309. (item (assoc-ref keys entry)))
  310. (hotkey-callback-procedure argument item)))))
  311. (lambda ()
  312. (destroy-form-and-pop form))))))
  313. (define* (run-scale-page #:key
  314. title
  315. info-text
  316. (info-textbox-width 50)
  317. (scale-width 40)
  318. (scale-full-value 100)
  319. scale-update-proc
  320. (max-scale-update 5))
  321. "Run a page with a progress bar (called 'scale' in newt). The given
  322. INFO-TEXT is displayed in a textbox above the scale. The width of the textbox
  323. is set to INFO-TEXTBOX-WIDTH. The width of the scale is set to
  324. SCALE-WIDTH. SCALE-FULL-VALUE indicates the value that correspond to 100% of
  325. the scale.
  326. The procedure SCALE-UPDATE-PROC shall return a new scale
  327. value. SCALE-UPDATE-PROC will be called until the returned value is superior
  328. or equal to SCALE-FULL-VALUE, but no more than MAX-SCALE-UPDATE times. An
  329. error is raised if the MAX-SCALE-UPDATE limit is reached."
  330. (let* ((info-textbox
  331. (make-reflowed-textbox -1 -1 info-text
  332. info-textbox-width
  333. #:flags FLAG-BORDER))
  334. (scale (make-scale -1 -1 scale-width scale-full-value))
  335. (grid (vertically-stacked-grid
  336. GRID-ELEMENT-COMPONENT info-textbox
  337. GRID-ELEMENT-COMPONENT scale))
  338. (form (make-form)))
  339. (add-form-to-grid grid form #t)
  340. (make-wrapped-grid-window grid title)
  341. (draw-form form)
  342. ;; This call is imperative, otherwise the form won't be displayed. See the
  343. ;; explanation in the above commentary.
  344. (newt-refresh)
  345. (dynamic-wind
  346. (const #t)
  347. (lambda ()
  348. (let loop ((i max-scale-update)
  349. (last-value 0))
  350. (let ((value (scale-update-proc last-value)))
  351. (set-scale-value scale value)
  352. ;; Same as above.
  353. (newt-refresh)
  354. (unless (>= value scale-full-value)
  355. (if (> i 0)
  356. (loop (- i 1) value)
  357. (error "Max scale updates reached."))))))
  358. (lambda ()
  359. (destroy-form-and-pop form)))))
  360. (define* (run-checkbox-tree-page #:key
  361. info-text
  362. title
  363. items
  364. item->text
  365. (info-textbox-width 50)
  366. (checkbox-tree-height 10)
  367. (ok-button-callback-procedure
  368. (const #t))
  369. (exit-button-callback-procedure
  370. (const #t)))
  371. "Run a page allowing the user to select one or multiple items among ITEMS in
  372. a checkbox list. The page contains vertically stacked from the top to the
  373. bottom, an informative text set to INFO-TEXT, the checkbox list and two
  374. buttons, 'Ok' and 'Exit'. The page title's is set to TITLE. ITEMS are
  375. converted to text using ITEM->TEXT before being displayed in the checkbox
  376. list.
  377. INFO-TEXTBOX-WIDTH is the width of the textbox where INFO-TEXT will be
  378. displayed. CHECKBOX-TREE-HEIGHT is the height of the checkbox list.
  379. OK-BUTTON-CALLBACK-PROCEDURE is called when the 'Ok' button is pressed.
  380. EXIT-BUTTON-CALLBACK-PROCEDURE is called when the 'Exit' button is
  381. pressed.
  382. This procedure returns the list of checked items in the checkbox list among
  383. ITEMS when 'Ok' is pressed."
  384. (define (fill-checkbox-tree checkbox-tree items)
  385. (map
  386. (lambda (item)
  387. (let* ((item-text (item->text item))
  388. (key (add-entry-to-checkboxtree checkbox-tree item-text 0)))
  389. (cons key item)))
  390. items))
  391. (let* ((checkbox-tree
  392. (make-checkboxtree -1 -1
  393. checkbox-tree-height
  394. FLAG-BORDER))
  395. (info-textbox
  396. (make-reflowed-textbox -1 -1 info-text
  397. info-textbox-width
  398. #:flags FLAG-BORDER))
  399. (ok-button (make-button -1 -1 (G_ "OK")))
  400. (exit-button (make-button -1 -1 (G_ "Exit")))
  401. (grid (vertically-stacked-grid
  402. GRID-ELEMENT-COMPONENT info-textbox
  403. GRID-ELEMENT-COMPONENT checkbox-tree
  404. GRID-ELEMENT-SUBGRID
  405. (horizontal-stacked-grid
  406. GRID-ELEMENT-COMPONENT ok-button
  407. GRID-ELEMENT-COMPONENT exit-button)))
  408. (keys (fill-checkbox-tree checkbox-tree items))
  409. (form (make-form)))
  410. (add-form-to-grid grid form #t)
  411. (make-wrapped-grid-window grid title)
  412. (receive (exit-reason argument)
  413. (run-form form)
  414. (dynamic-wind
  415. (const #t)
  416. (lambda ()
  417. (case exit-reason
  418. ((exit-component)
  419. (cond
  420. ((components=? argument ok-button)
  421. (let* ((entries (current-checkbox-selection checkbox-tree))
  422. (current-items (map (lambda (entry)
  423. (assoc-ref keys entry))
  424. entries)))
  425. (ok-button-callback-procedure)
  426. current-items))
  427. ((components=? argument exit-button)
  428. (exit-button-callback-procedure))))))
  429. (lambda ()
  430. (destroy-form-and-pop form))))))
  431. (define* (run-file-textbox-page #:key
  432. info-text
  433. title
  434. file
  435. (info-textbox-width 50)
  436. (file-textbox-width 50)
  437. (file-textbox-height 30)
  438. (exit-button? #t)
  439. (ok-button-callback-procedure
  440. (const #t))
  441. (exit-button-callback-procedure
  442. (const #t)))
  443. (let* ((info-textbox
  444. (make-reflowed-textbox -1 -1 info-text
  445. info-textbox-width
  446. #:flags FLAG-BORDER))
  447. (file-text (read-all file))
  448. (file-textbox
  449. (make-textbox -1 -1
  450. file-textbox-width
  451. file-textbox-height
  452. (logior FLAG-SCROLL FLAG-BORDER)))
  453. (ok-button (make-button -1 -1 (G_ "OK")))
  454. (exit-button (make-button -1 -1 (G_ "Exit")))
  455. (grid (vertically-stacked-grid
  456. GRID-ELEMENT-COMPONENT info-textbox
  457. GRID-ELEMENT-COMPONENT file-textbox
  458. GRID-ELEMENT-SUBGRID
  459. (apply
  460. horizontal-stacked-grid
  461. GRID-ELEMENT-COMPONENT ok-button
  462. `(,@(if exit-button?
  463. (list GRID-ELEMENT-COMPONENT exit-button)
  464. '())))))
  465. (form (make-form)))
  466. (set-textbox-text file-textbox file-text)
  467. (add-form-to-grid grid form #t)
  468. (make-wrapped-grid-window grid title)
  469. (receive (exit-reason argument)
  470. (run-form form)
  471. (dynamic-wind
  472. (const #t)
  473. (lambda ()
  474. (case exit-reason
  475. ((exit-component)
  476. (cond
  477. ((components=? argument ok-button)
  478. (ok-button-callback-procedure))
  479. ((and exit-button?
  480. (components=? argument exit-button))
  481. (exit-button-callback-procedure))))))
  482. (lambda ()
  483. (destroy-form-and-pop form))))))