page.scm 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2018 Mathieu Othacehe <m.othacehe@gmail.com>
  3. ;;; Copyright © 2019, 2020 Ludovic Courtès <ludo@gnu.org>
  4. ;;; Copyright © 2019 Tobias Geerinckx-Rice <me@tobias.gr>
  5. ;;;
  6. ;;; This file is part of GNU Guix.
  7. ;;;
  8. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  9. ;;; under the terms of the GNU General Public License as published by
  10. ;;; the Free Software Foundation; either version 3 of the License, or (at
  11. ;;; your option) any later version.
  12. ;;;
  13. ;;; GNU Guix is distributed in the hope that it will be useful, but
  14. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;;; GNU General Public License for more details.
  17. ;;;
  18. ;;; You should have received a copy of the GNU General Public License
  19. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  20. (define-module (gnu installer newt page)
  21. #:use-module (gnu installer steps)
  22. #:use-module (gnu installer utils)
  23. #:use-module (gnu installer newt utils)
  24. #:use-module (guix i18n)
  25. #:use-module (ice-9 i18n)
  26. #:use-module (ice-9 match)
  27. #:use-module (ice-9 receive)
  28. #:use-module (srfi srfi-1)
  29. #:use-module (srfi srfi-11)
  30. #:use-module (srfi srfi-26)
  31. #:use-module (srfi srfi-34)
  32. #:use-module (srfi srfi-35)
  33. #:use-module (newt)
  34. #:export (default-listbox-height
  35. draw-info-page
  36. draw-connecting-page
  37. run-input-page
  38. run-error-page
  39. run-confirmation-page
  40. run-listbox-selection-page
  41. run-scale-page
  42. run-checkbox-tree-page
  43. run-file-textbox-page
  44. run-form-with-clients))
  45. ;;; Commentary:
  46. ;;;
  47. ;;; Some helpers around guile-newt to draw or run generic pages. The
  48. ;;; difference between 'draw' and 'run' terms comes from newt library. A page
  49. ;;; is drawn when the form it contains does not expect any user
  50. ;;; interaction. In that case, it is necessary to call (newt-refresh) to force
  51. ;;; the page to be displayed. When a form is 'run', it is blocked waiting for
  52. ;;; any action from the user (press a button, input some text, ...).
  53. ;;;
  54. ;;; Code:
  55. (define* (watch-clients! form #:optional (clients (current-clients)))
  56. "Have FORM watch the file descriptors corresponding to current client
  57. connections. Consequently, FORM may exit with the 'exit-fd-ready' reason."
  58. (when (current-server-socket)
  59. (form-watch-fd form (fileno (current-server-socket))
  60. FD-READ))
  61. (for-each (lambda (client)
  62. (form-watch-fd form (fileno client)
  63. (logior FD-READ FD-EXCEPT)))
  64. clients))
  65. (define close-port-and-reuse-fd
  66. (let ((bit-bucket #f))
  67. (lambda (port)
  68. "Close PORT and redirect its underlying FD to point to a valid open file
  69. descriptor."
  70. (let ((fd (fileno port)))
  71. (unless bit-bucket
  72. (set! bit-bucket (car (pipe))))
  73. (close-port port)
  74. ;; FIXME: We're leaking FD.
  75. (dup2 (fileno bit-bucket) fd)))))
  76. (define* (run-form-with-clients form exp)
  77. "Run FORM such as it watches the file descriptors beneath CLIENTS after
  78. sending EXP to all the clients.
  79. Automatically restart the form when it exits with 'exit-fd-ready but without
  80. an actual client reply--e.g., it got a connection request or a client
  81. disconnect.
  82. Like 'run-form', return two values: the exit reason, and an \"argument\"."
  83. (define* (discard-client! port #:optional errno)
  84. (if errno
  85. (syslog "removing client ~d due to ~s~%"
  86. (fileno port) (strerror errno))
  87. (syslog "removing client ~d due to EOF~%"
  88. (fileno port)))
  89. ;; XXX: Watch out! There's no 'form-unwatch-fd' procedure in Newt so we
  90. ;; cheat: we keep PORT's file descriptor open, but make it a duplicate of
  91. ;; a valid but inactive FD. Failing to do that, 'run-form' would
  92. ;; select(2) on the now-closed port and keep spinning as select(2) returns
  93. ;; EBADF.
  94. (close-port-and-reuse-fd port)
  95. (current-clients (delq port (current-clients)))
  96. (close-port port))
  97. (define title
  98. ;; Title of FORM.
  99. (match exp
  100. (((? symbol? tag) alist ...)
  101. (match (assq 'title alist)
  102. ((_ title) title)
  103. (_ tag)))
  104. (((? symbol? tag) _ ...)
  105. tag)
  106. (_
  107. 'unknown)))
  108. ;; Send EXP to all the currently-connected clients.
  109. (send-to-clients exp)
  110. (let loop ()
  111. (syslog "running form ~s (~s) with ~d clients~%"
  112. form title (length (current-clients)))
  113. ;; Call 'watch-clients!' within the loop because there might be new
  114. ;; clients.
  115. (watch-clients! form)
  116. (let-values (((reason argument) (run-form form)))
  117. (match reason
  118. ('exit-fd-ready
  119. (match (fdes->ports argument)
  120. ((port _ ...)
  121. (if (memq port (current-clients))
  122. ;; Read a reply from a client or handle its departure.
  123. (catch 'system-error
  124. (lambda ()
  125. (match (read port)
  126. ((? eof-object? eof)
  127. (discard-client! port)
  128. (loop))
  129. (obj
  130. (syslog "form ~s (~s): client ~d replied ~s~%"
  131. form title (fileno port) obj)
  132. (values 'exit-fd-ready obj))))
  133. (lambda args
  134. (discard-client! port (system-error-errno args))
  135. (loop)))
  136. ;; Accept a new client and send it EXP.
  137. (match (accept port)
  138. ((client . _)
  139. (syslog "accepting new client ~d while on form ~s~%"
  140. (fileno client) form)
  141. (catch 'system-error
  142. (lambda ()
  143. (write exp client)
  144. (newline client)
  145. (force-output client)
  146. (current-clients (cons client (current-clients))))
  147. (lambda _
  148. (close-port client)))
  149. (loop)))))))
  150. (_
  151. (values reason argument))))))
  152. (define (default-listbox-height)
  153. "Return the default listbox height."
  154. (max 5 (- (screen-rows) 20)))
  155. (define (draw-info-page text title)
  156. "Draw an informative page with the given TEXT as content. Set the title of
  157. this page to TITLE."
  158. (send-to-clients `(info (title ,title) (text ,text)))
  159. (let* ((text-box
  160. (make-reflowed-textbox -1 -1 text 40
  161. #:flags FLAG-BORDER))
  162. (grid (make-grid 1 1))
  163. (form (make-form)))
  164. (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
  165. (add-component-to-form form text-box)
  166. (make-wrapped-grid-window grid title)
  167. (draw-form form)
  168. ;; This call is imperative, otherwise the form won't be displayed. See the
  169. ;; explanation in the above commentary.
  170. (newt-refresh)
  171. form))
  172. (define (draw-connecting-page service-name)
  173. "Draw a page to indicate a connection in in progress."
  174. (draw-info-page
  175. (format #f (G_ "Connecting to ~a, please wait.") service-name)
  176. (G_ "Connection in progress")))
  177. (define* (run-input-page text title
  178. #:key
  179. (allow-empty-input? #f)
  180. (default-text #f)
  181. (input-visibility-checkbox? #f)
  182. (input-field-width 40)
  183. (input-flags 0))
  184. "Run a page to prompt user for an input. The given TEXT will be displayed
  185. above the input field. The page title is set to TITLE. Unless
  186. allow-empty-input? is set to #t, an error page will be displayed if the user
  187. enters an empty input. INPUT-FLAGS is a bitwise-or'd set of flags for the
  188. input box, such as FLAG-PASSWORD."
  189. (let* ((text-box
  190. (make-reflowed-textbox -1 -1 text
  191. input-field-width
  192. #:flags FLAG-BORDER))
  193. (input-visible-cb
  194. (make-checkbox -1 -1 (G_ "Show") #\space "x "))
  195. (input-flags* (if input-visibility-checkbox?
  196. (logior FLAG-PASSWORD FLAG-SCROLL
  197. input-flags)
  198. input-flags))
  199. (input-entry (make-entry -1 -1 20
  200. #:flags input-flags*))
  201. (ok-button (make-button -1 -1 (G_ "OK")))
  202. (grid (vertically-stacked-grid
  203. GRID-ELEMENT-COMPONENT text-box
  204. GRID-ELEMENT-SUBGRID
  205. (apply
  206. horizontal-stacked-grid
  207. GRID-ELEMENT-COMPONENT input-entry
  208. `(,@(if input-visibility-checkbox?
  209. (list GRID-ELEMENT-COMPONENT input-visible-cb)
  210. '())))
  211. GRID-ELEMENT-COMPONENT ok-button))
  212. (form (make-form #:flags FLAG-NOF12)))
  213. (add-component-callback
  214. input-visible-cb
  215. (lambda ()
  216. (set-entry-flags input-entry
  217. FLAG-PASSWORD
  218. FLAG-ROLE-TOGGLE)))
  219. (when default-text
  220. (set-entry-text input-entry default-text))
  221. (add-form-to-grid grid form #t)
  222. (make-wrapped-grid-window grid title)
  223. (let ((error-page (lambda ()
  224. (run-error-page (G_ "Please enter a non empty input.")
  225. (G_ "Empty input")))))
  226. (let loop ()
  227. (receive (exit-reason argument)
  228. (run-form-with-clients form
  229. `(input (title ,title) (text ,text)
  230. (default ,default-text)))
  231. (let ((input (if (eq? exit-reason 'exit-fd-ready)
  232. argument
  233. (entry-value input-entry))))
  234. (cond ((not input) ;client disconnect or something
  235. (loop))
  236. ((and (not allow-empty-input?)
  237. (eq? exit-reason 'exit-component)
  238. (string=? input ""))
  239. ;; Display the error page.
  240. (error-page)
  241. ;; Set the focus back to the input input field.
  242. (set-current-component form input-entry)
  243. (loop))
  244. (else
  245. (destroy-form-and-pop form)
  246. input))))))))
  247. (define (run-error-page text title)
  248. "Run a page to inform the user of an error. The page contains the given TEXT
  249. to explain the error and an \"OK\" button to acknowledge the error. The title
  250. of the page is set to TITLE."
  251. (let* ((text-box
  252. (make-reflowed-textbox -1 -1 text 40
  253. #:flags FLAG-BORDER))
  254. (grid (make-grid 1 2))
  255. (ok-button (make-button -1 -1 "OK"))
  256. (form (make-form)))
  257. (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
  258. (set-grid-field grid 0 1 GRID-ELEMENT-COMPONENT ok-button
  259. #:pad-top 1)
  260. ;; Set the background color to red to indicate something went wrong.
  261. (newt-set-color COLORSET-ROOT "white" "red")
  262. (add-components-to-form form text-box ok-button)
  263. (make-wrapped-grid-window grid title)
  264. (run-form-with-clients form
  265. `(error (title ,title) (text ,text)))
  266. ;; Restore the background to its original color.
  267. (newt-set-color COLORSET-ROOT "white" "blue")
  268. (destroy-form-and-pop form)))
  269. (define* (run-confirmation-page text title
  270. #:key (exit-button-procedure (const #f)))
  271. "Run a page to inform the user of an error. The page contains the given TEXT
  272. to explain the error and an \"OK\" button to acknowledge the error. The title
  273. of the page is set to TITLE."
  274. (let* ((text-box
  275. (make-reflowed-textbox -1 -1 text 40
  276. #:flags FLAG-BORDER))
  277. (ok-button (make-button -1 -1 (G_ "Continue")))
  278. (exit-button (make-button -1 -1 (G_ "Exit")))
  279. (grid (vertically-stacked-grid
  280. GRID-ELEMENT-COMPONENT text-box
  281. GRID-ELEMENT-SUBGRID
  282. (horizontal-stacked-grid
  283. GRID-ELEMENT-COMPONENT ok-button
  284. GRID-ELEMENT-COMPONENT exit-button)))
  285. (form (make-form #:flags FLAG-NOF12)))
  286. (add-form-to-grid grid form #t)
  287. (make-wrapped-grid-window grid title)
  288. (receive (exit-reason argument)
  289. (run-form-with-clients form
  290. `(confirmation (title ,title)
  291. (text ,text)))
  292. (dynamic-wind
  293. (const #t)
  294. (lambda ()
  295. (match exit-reason
  296. ('exit-component
  297. (cond
  298. ((components=? argument ok-button)
  299. #t)
  300. ((components=? argument exit-button)
  301. (exit-button-procedure))))
  302. ('exit-fd-ready
  303. (if argument
  304. #t
  305. (exit-button-procedure)))))
  306. (lambda ()
  307. (destroy-form-and-pop form))))))
  308. (define* (run-listbox-selection-page #:key
  309. info-text
  310. title
  311. (info-textbox-width 50)
  312. listbox-items
  313. listbox-item->text
  314. (listbox-height
  315. (default-listbox-height))
  316. (listbox-default-item #f)
  317. (listbox-allow-multiple? #f)
  318. (sort-listbox-items? #t)
  319. (allow-delete? #f)
  320. (skip-item-procedure?
  321. (const #f))
  322. button-text
  323. (button-callback-procedure
  324. (const #t))
  325. (button2-text #f)
  326. (button2-callback-procedure
  327. (const #t))
  328. (listbox-callback-procedure
  329. identity)
  330. (client-callback-procedure
  331. listbox-callback-procedure)
  332. (hotkey-callback-procedure
  333. (const #t)))
  334. "Run a page asking the user to select an item in a listbox. The page
  335. contains, stacked vertically from the top to the bottom, an informative text
  336. set to INFO-TEXT, a listbox and a button. The listbox will be filled with
  337. LISTBOX-ITEMS converted to text by applying the procedure LISTBOX-ITEM->TEXT
  338. on every item. The selected item from LISTBOX-ITEMS is returned. The button
  339. text is set to BUTTON-TEXT and the procedure BUTTON-CALLBACK-PROCEDURE called
  340. when it is pressed. The procedure LISTBOX-CALLBACK-PROCEDURE is called when an
  341. item from the listbox is selected (by pressing the <ENTER> key).
  342. INFO-TEXTBOX-WIDTH is the width of the textbox where INFO-TEXT will be
  343. displayed. LISTBOX-HEIGHT is the height of the listbox.
  344. If LISTBOX-DEFAULT-ITEM is set to the value of one of the items in
  345. LISTBOX-ITEMS, it will be selected by default. Otherwise, the first element of
  346. the listbox is selected.
  347. If LISTBOX-ALLOW-MULTIPLE? is set to #t, multiple items from the listbox can
  348. be selected (using the <SPACE> key). It that case, a list containing the
  349. selected items will be returned.
  350. If SORT-LISTBOX-ITEMS? is set to #t, the listbox items are sorted using
  351. 'string-locale<?' procedure (after being converted to text).
  352. If ALLOW-DELETE? is #t, the form will return if the <DELETE> key is pressed,
  353. otherwise nothing will happen.
  354. Each time the listbox current item changes, call SKIP-ITEM-PROCEDURE? with the
  355. current listbox item as argument. If it returns #t, skip the element and jump
  356. to the next/previous one depending on the previous item, otherwise do
  357. nothing."
  358. (let loop ()
  359. (define (fill-listbox listbox items)
  360. "Append the given ITEMS to LISTBOX, once they have been converted to text
  361. with LISTBOX-ITEM->TEXT. Each item appended to the LISTBOX is given a key by
  362. newt. Save this key by returning an association list under the form:
  363. ((NEWT-LISTBOX-KEY . ITEM) ...)
  364. where NEWT-LISTBOX-KEY is the key returned by APPEND-ENTRY-TO-LISTBOX, when
  365. ITEM was inserted into LISTBOX."
  366. (map (lambda (item)
  367. (let* ((text (listbox-item->text item))
  368. (key (append-entry-to-listbox listbox text)))
  369. (cons key item)))
  370. items))
  371. (define (sort-listbox-items listbox-items)
  372. "Return LISTBOX-ITEMS sorted using the 'string-locale<?' procedure on the text
  373. corresponding to each item in the list."
  374. (let* ((items (map (lambda (item)
  375. (cons item (listbox-item->text item)))
  376. listbox-items))
  377. (sorted-items
  378. (sort items (lambda (a b)
  379. (let ((text-a (cdr a))
  380. (text-b (cdr b)))
  381. (string-locale<? text-a text-b))))))
  382. (map car sorted-items)))
  383. ;; Store the last selected listbox item's key.
  384. (define last-listbox-key (make-parameter #f))
  385. (define (previous-key keys key)
  386. (let ((index (list-index (cut eq? key <>) keys)))
  387. (and index
  388. (> index 0)
  389. (list-ref keys (- index 1)))))
  390. (define (next-key keys key)
  391. (let ((index (list-index (cut eq? key <>) keys)))
  392. (and index
  393. (< index (- (length keys) 1))
  394. (list-ref keys (+ index 1)))))
  395. (define (set-default-item listbox listbox-keys default-item)
  396. "Set the default item of LISTBOX to DEFAULT-ITEM. LISTBOX-KEYS is the
  397. association list returned by the FILL-LISTBOX procedure. It is used because
  398. the current listbox item has to be selected by key."
  399. (for-each (match-lambda
  400. ((key . item)
  401. (when (equal? item default-item)
  402. (set-current-listbox-entry-by-key listbox key))))
  403. listbox-keys))
  404. (let* ((listbox (make-listbox
  405. -1 -1
  406. listbox-height
  407. (logior FLAG-SCROLL FLAG-BORDER FLAG-RETURNEXIT
  408. (if listbox-allow-multiple?
  409. FLAG-MULTIPLE
  410. 0))))
  411. (form (make-form #:flags FLAG-NOF12))
  412. (info-textbox
  413. (make-reflowed-textbox -1 -1 info-text
  414. info-textbox-width
  415. #:flags FLAG-BORDER))
  416. (button (make-button -1 -1 button-text))
  417. (button2 (and button2-text
  418. (make-button -1 -1 button2-text)))
  419. (grid (vertically-stacked-grid
  420. GRID-ELEMENT-COMPONENT info-textbox
  421. GRID-ELEMENT-COMPONENT listbox
  422. GRID-ELEMENT-SUBGRID
  423. (apply
  424. horizontal-stacked-grid
  425. GRID-ELEMENT-COMPONENT button
  426. `(,@(if button2
  427. (list GRID-ELEMENT-COMPONENT button2)
  428. '())))))
  429. (sorted-items (if sort-listbox-items?
  430. (sort-listbox-items listbox-items)
  431. listbox-items))
  432. (keys (fill-listbox listbox sorted-items)))
  433. (define (choice->item str)
  434. ;; Return the item that corresponds to STR.
  435. (match (find (match-lambda
  436. ((key . item)
  437. (string=? str (listbox-item->text item))))
  438. keys)
  439. ((key . item) item)
  440. (#f (raise (condition (&installer-step-abort))))))
  441. ;; On every listbox element change, check if we need to skip it. If yes,
  442. ;; depending on the 'last-listbox-key', jump forward or backward. If no,
  443. ;; do nothing.
  444. (add-component-callback
  445. listbox
  446. (lambda ()
  447. (let* ((current-key (current-listbox-entry listbox))
  448. (listbox-keys (map car keys))
  449. (last-key (last-listbox-key))
  450. (item (assoc-ref keys current-key))
  451. (prev-key (previous-key listbox-keys current-key))
  452. (next-key (next-key listbox-keys current-key)))
  453. ;; Update last-listbox-key before a potential call to
  454. ;; set-current-listbox-entry-by-key, because it will immediately
  455. ;; cause this callback to be called for the new entry.
  456. (last-listbox-key current-key)
  457. (when (skip-item-procedure? item)
  458. (when (eq? prev-key last-key)
  459. (if next-key
  460. (set-current-listbox-entry-by-key listbox next-key)
  461. (set-current-listbox-entry-by-key listbox prev-key)))
  462. (when (eq? next-key last-key)
  463. (if prev-key
  464. (set-current-listbox-entry-by-key listbox prev-key)
  465. (set-current-listbox-entry-by-key listbox next-key)))))))
  466. (when listbox-default-item
  467. (set-default-item listbox keys listbox-default-item))
  468. (when allow-delete?
  469. (form-add-hotkey form KEY-DELETE))
  470. (add-form-to-grid grid form #t)
  471. (make-wrapped-grid-window grid title)
  472. (receive (exit-reason argument)
  473. (run-form-with-clients form
  474. `(list-selection (title ,title)
  475. (multiple-choices?
  476. ,listbox-allow-multiple?)
  477. (items
  478. ,(map listbox-item->text
  479. listbox-items))))
  480. (dynamic-wind
  481. (const #t)
  482. (lambda ()
  483. (match exit-reason
  484. ('exit-component
  485. (cond
  486. ((components=? argument button)
  487. (button-callback-procedure))
  488. ((and button2
  489. (components=? argument button2))
  490. (button2-callback-procedure))
  491. ((components=? argument listbox)
  492. (if listbox-allow-multiple?
  493. (let* ((entries (listbox-selection listbox))
  494. (items (map (lambda (entry)
  495. (assoc-ref keys entry))
  496. entries)))
  497. (listbox-callback-procedure items))
  498. (let* ((entry (current-listbox-entry listbox))
  499. (item (assoc-ref keys entry)))
  500. (listbox-callback-procedure item))))))
  501. ('exit-fd-ready
  502. (let* ((choice argument)
  503. (item (if listbox-allow-multiple?
  504. (map choice->item choice)
  505. (choice->item choice))))
  506. (client-callback-procedure item)))
  507. ('exit-hotkey
  508. (let* ((entry (current-listbox-entry listbox))
  509. (item (assoc-ref keys entry)))
  510. (hotkey-callback-procedure argument item)))))
  511. (lambda ()
  512. (destroy-form-and-pop form)))))))
  513. (define* (run-scale-page #:key
  514. title
  515. info-text
  516. (info-textbox-width 50)
  517. (scale-width 40)
  518. (scale-full-value 100)
  519. scale-update-proc
  520. (max-scale-update 5))
  521. "Run a page with a progress bar (called 'scale' in newt). The given
  522. INFO-TEXT is displayed in a textbox above the scale. The width of the textbox
  523. is set to INFO-TEXTBOX-WIDTH. The width of the scale is set to
  524. SCALE-WIDTH. SCALE-FULL-VALUE indicates the value that correspond to 100% of
  525. the scale.
  526. The procedure SCALE-UPDATE-PROC shall return a new scale
  527. value. SCALE-UPDATE-PROC will be called until the returned value is superior
  528. or equal to SCALE-FULL-VALUE, but no more than MAX-SCALE-UPDATE times. An
  529. error is raised if the MAX-SCALE-UPDATE limit is reached."
  530. (let* ((info-textbox
  531. (make-reflowed-textbox -1 -1 info-text
  532. info-textbox-width
  533. #:flags FLAG-BORDER))
  534. (scale (make-scale -1 -1 scale-width scale-full-value))
  535. (grid (vertically-stacked-grid
  536. GRID-ELEMENT-COMPONENT info-textbox
  537. GRID-ELEMENT-COMPONENT scale))
  538. (form (make-form)))
  539. (add-form-to-grid grid form #t)
  540. (make-wrapped-grid-window grid title)
  541. (draw-form form)
  542. ;; This call is imperative, otherwise the form won't be displayed. See the
  543. ;; explanation in the above commentary.
  544. (newt-refresh)
  545. (dynamic-wind
  546. (const #t)
  547. (lambda ()
  548. (let loop ((i max-scale-update)
  549. (last-value 0))
  550. (let ((value (scale-update-proc last-value)))
  551. (set-scale-value scale value)
  552. ;; Same as above.
  553. (newt-refresh)
  554. (unless (>= value scale-full-value)
  555. (if (> i 0)
  556. (loop (- i 1) value)
  557. (error "Max scale updates reached."))))))
  558. (lambda ()
  559. (destroy-form-and-pop form)))))
  560. (define %none-selected
  561. (circular-list #f))
  562. (define* (run-checkbox-tree-page #:key
  563. info-text
  564. title
  565. items
  566. (selection %none-selected)
  567. item->text
  568. (info-textbox-width 50)
  569. (checkbox-tree-height 10)
  570. (ok-button-callback-procedure
  571. (const #t))
  572. (exit-button-callback-procedure
  573. (const #t)))
  574. "Run a page allowing the user to select one or multiple items among ITEMS in
  575. a checkbox list. The page contains vertically stacked from the top to the
  576. bottom, an informative text set to INFO-TEXT, the checkbox list and two
  577. buttons, 'Ok' and 'Exit'. The page title's is set to TITLE. ITEMS are
  578. converted to text using ITEM->TEXT before being displayed in the checkbox
  579. list. SELECTION is a list of Booleans of the same length as ITEMS that
  580. specifies which items are initially checked.
  581. INFO-TEXTBOX-WIDTH is the width of the textbox where INFO-TEXT will be
  582. displayed. CHECKBOX-TREE-HEIGHT is the height of the checkbox list.
  583. OK-BUTTON-CALLBACK-PROCEDURE is called when the 'Ok' button is pressed.
  584. EXIT-BUTTON-CALLBACK-PROCEDURE is called when the 'Exit' button is
  585. pressed.
  586. This procedure returns the list of checked items in the checkbox list among
  587. ITEMS when 'Ok' is pressed."
  588. (define (fill-checkbox-tree checkbox-tree items)
  589. (map (lambda (item selected?)
  590. (let* ((item-text (item->text item))
  591. (key (add-entry-to-checkboxtree checkbox-tree item-text
  592. (if selected?
  593. FLAG-SELECTED
  594. 0))))
  595. (cons key item)))
  596. items
  597. selection))
  598. (let loop ()
  599. (let* ((checkbox-tree
  600. (make-checkboxtree -1 -1
  601. checkbox-tree-height
  602. FLAG-BORDER))
  603. (info-textbox
  604. (make-reflowed-textbox -1 -1 info-text
  605. info-textbox-width
  606. #:flags FLAG-BORDER))
  607. (ok-button (make-button -1 -1 (G_ "OK")))
  608. (exit-button (make-button -1 -1 (G_ "Exit")))
  609. (grid (vertically-stacked-grid
  610. GRID-ELEMENT-COMPONENT info-textbox
  611. GRID-ELEMENT-COMPONENT checkbox-tree
  612. GRID-ELEMENT-SUBGRID
  613. (horizontal-stacked-grid
  614. GRID-ELEMENT-COMPONENT ok-button
  615. GRID-ELEMENT-COMPONENT exit-button)))
  616. (keys (fill-checkbox-tree checkbox-tree items))
  617. (form (make-form #:flags FLAG-NOF12)))
  618. (define (choice->item str)
  619. ;; Return the item that corresponds to STR.
  620. (match (find (match-lambda
  621. ((key . item)
  622. (string=? str (item->text item))))
  623. keys)
  624. ((key . item) item)
  625. (#f (raise (condition (&installer-step-abort))))))
  626. (add-form-to-grid grid form #t)
  627. (make-wrapped-grid-window grid title)
  628. (receive (exit-reason argument)
  629. (run-form-with-clients form
  630. `(checkbox-list (title ,title)
  631. (text ,info-text)
  632. (items
  633. ,(map item->text items))))
  634. (dynamic-wind
  635. (const #t)
  636. (lambda ()
  637. (match exit-reason
  638. ('exit-component
  639. (cond
  640. ((components=? argument ok-button)
  641. (let* ((entries (current-checkbox-selection checkbox-tree))
  642. (current-items (map (lambda (entry)
  643. (assoc-ref keys entry))
  644. entries)))
  645. (ok-button-callback-procedure)
  646. current-items))
  647. ((components=? argument exit-button)
  648. (exit-button-callback-procedure))))
  649. ('exit-fd-ready
  650. (map choice->item argument))))
  651. (lambda ()
  652. (destroy-form-and-pop form)))))))
  653. (define* (edit-file file #:key locale)
  654. "Spawn an editor for FILE."
  655. (clear-screen)
  656. (newt-suspend)
  657. ;; Use Nano because it syntax-highlights Scheme by default.
  658. ;; TODO: Add a menu to choose an editor?
  659. (run-command (list "/run/current-system/profile/bin/nano" file)
  660. #:locale locale)
  661. (newt-resume))
  662. (define* (run-file-textbox-page #:key
  663. info-text
  664. title
  665. file
  666. (info-textbox-width 50)
  667. (file-textbox-width 50)
  668. (file-textbox-height 30)
  669. (exit-button? #t)
  670. (edit-button? #f)
  671. (editor-locale #f)
  672. (ok-button-callback-procedure
  673. (const #t))
  674. (exit-button-callback-procedure
  675. (const #t)))
  676. (let loop ()
  677. (let* ((info-textbox
  678. (make-reflowed-textbox -1 -1 info-text
  679. info-textbox-width
  680. #:flags FLAG-BORDER))
  681. (file-textbox
  682. (make-textbox -1 -1
  683. file-textbox-width
  684. file-textbox-height
  685. (logior FLAG-SCROLL FLAG-BORDER)))
  686. (ok-button (make-button -1 -1 (G_ "OK")))
  687. (exit-button (make-button -1 -1 (G_ "Exit")))
  688. (edit-button (and edit-button?
  689. (make-button -1 -1 (G_ "Edit"))))
  690. (grid (vertically-stacked-grid
  691. GRID-ELEMENT-COMPONENT info-textbox
  692. GRID-ELEMENT-COMPONENT file-textbox
  693. GRID-ELEMENT-SUBGRID
  694. (apply
  695. horizontal-stacked-grid
  696. GRID-ELEMENT-COMPONENT ok-button
  697. `(,@(if edit-button?
  698. (list GRID-ELEMENT-COMPONENT edit-button)
  699. '())
  700. ,@(if exit-button?
  701. (list GRID-ELEMENT-COMPONENT exit-button)
  702. '())))))
  703. (form (make-form #:flags FLAG-NOF12)))
  704. (add-form-to-grid grid form #t)
  705. (make-wrapped-grid-window grid title)
  706. (set-textbox-text file-textbox
  707. (receive (_w _h text)
  708. (reflow-text (read-all file)
  709. file-textbox-width
  710. 0 0)
  711. text))
  712. (receive (exit-reason argument)
  713. (run-form-with-clients form
  714. `(file-dialog (title ,title)
  715. (text ,info-text)
  716. (file ,file)))
  717. (define result
  718. (dynamic-wind
  719. (const #t)
  720. (lambda ()
  721. (match exit-reason
  722. ('exit-component
  723. (cond
  724. ((components=? argument ok-button)
  725. (ok-button-callback-procedure))
  726. ((and exit-button?
  727. (components=? argument exit-button))
  728. (exit-button-callback-procedure))
  729. ((and edit-button?
  730. (components=? argument edit-button))
  731. (edit-file file))))
  732. ('exit-fd-ready
  733. (if argument
  734. (ok-button-callback-procedure)
  735. (exit-button-callback-procedure)))))
  736. (lambda ()
  737. (destroy-form-and-pop form))))
  738. (if (and (eq? exit-reason 'exit-component)
  739. (components=? argument edit-button))
  740. (loop) ;recurse in tail position
  741. result)))))