]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - stand/lua/menu.lua
bsddialog: import version 0.0.1
[FreeBSD/FreeBSD.git] / stand / lua / menu.lua
1 --
2 -- SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3 --
4 -- Copyright (c) 2015 Pedro Souza <pedrosouza@freebsd.org>
5 -- Copyright (c) 2018 Kyle Evans <kevans@FreeBSD.org>
6 -- All rights reserved.
7 --
8 -- Redistribution and use in source and binary forms, with or without
9 -- modification, are permitted provided that the following conditions
10 -- are met:
11 -- 1. Redistributions of source code must retain the above copyright
12 --    notice, this list of conditions and the following disclaimer.
13 -- 2. Redistributions in binary form must reproduce the above copyright
14 --    notice, this list of conditions and the following disclaimer in the
15 --    documentation and/or other materials provided with the distribution.
16 --
17 -- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18 -- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 -- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 -- ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21 -- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 -- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 -- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 -- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 -- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 -- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 -- SUCH DAMAGE.
28 --
29 -- $FreeBSD$
30 --
31
32 local cli = require("cli")
33 local core = require("core")
34 local color = require("color")
35 local config = require("config")
36 local screen = require("screen")
37 local drawer = require("drawer")
38
39 local menu = {}
40
41 local drawn_menu
42 local return_menu_entry = {
43         entry_type = core.MENU_RETURN,
44         name = "Back to main menu" .. color.highlight(" [Backspace]"),
45 }
46
47 local function OnOff(str, value)
48         if value then
49                 return str .. color.escapefg(color.GREEN) .. "On" ..
50                     color.resetfg()
51         else
52                 return str .. color.escapefg(color.RED) .. "off" ..
53                     color.resetfg()
54         end
55 end
56
57 local function bootenvSet(env)
58         loader.setenv("vfs.root.mountfrom", env)
59         loader.setenv("currdev", env .. ":")
60         config.reload()
61 end
62
63 -- Module exports
64 menu.handlers = {
65         -- Menu handlers take the current menu and selected entry as parameters,
66         -- and should return a boolean indicating whether execution should
67         -- continue or not. The return value may be omitted if this entry should
68         -- have no bearing on whether we continue or not, indicating that we
69         -- should just continue after execution.
70         [core.MENU_ENTRY] = function(_, entry)
71                 -- run function
72                 entry.func()
73         end,
74         [core.MENU_CAROUSEL_ENTRY] = function(_, entry)
75                 -- carousel (rotating) functionality
76                 local carid = entry.carousel_id
77                 local caridx = config.getCarouselIndex(carid)
78                 local choices = entry.items
79                 if type(choices) == "function" then
80                         choices = choices()
81                 end
82                 if #choices > 0 then
83                         caridx = (caridx % #choices) + 1
84                         config.setCarouselIndex(carid, caridx)
85                         entry.func(caridx, choices[caridx], choices)
86                 end
87         end,
88         [core.MENU_SUBMENU] = function(_, entry)
89                 menu.process(entry.submenu)
90         end,
91         [core.MENU_RETURN] = function(_, entry)
92                 -- allow entry to have a function/side effect
93                 if entry.func ~= nil then
94                         entry.func()
95                 end
96                 return false
97         end,
98 }
99 -- loader menu tree is rooted at menu.welcome
100
101 menu.boot_environments = {
102         entries = {
103                 -- return to welcome menu
104                 return_menu_entry,
105                 {
106                         entry_type = core.MENU_CAROUSEL_ENTRY,
107                         carousel_id = "be_active",
108                         items = core.bootenvList,
109                         name = function(idx, choice, all_choices)
110                                 if #all_choices == 0 then
111                                         return "Active: "
112                                 end
113
114                                 local is_default = (idx == 1)
115                                 local bootenv_name = ""
116                                 local name_color
117                                 if is_default then
118                                         name_color = color.escapefg(color.GREEN)
119                                 else
120                                         name_color = color.escapefg(color.BLUE)
121                                 end
122                                 bootenv_name = bootenv_name .. name_color ..
123                                     choice .. color.resetfg()
124                                 return color.highlight("A").."ctive: " ..
125                                     bootenv_name .. " (" .. idx .. " of " ..
126                                     #all_choices .. ")"
127                         end,
128                         func = function(_, choice, _)
129                                 bootenvSet(choice)
130                         end,
131                         alias = {"a", "A"},
132                 },
133                 {
134                         entry_type = core.MENU_ENTRY,
135                         visible = function()
136                                 return core.isRewinded() == false
137                         end,
138                         name = function()
139                                 return color.highlight("b") .. "ootfs: " ..
140                                     core.bootenvDefault()
141                         end,
142                         func = function()
143                                 -- Reset active boot environment to the default
144                                 config.setCarouselIndex("be_active", 1)
145                                 bootenvSet(core.bootenvDefault())
146                         end,
147                         alias = {"b", "B"},
148                 },
149         },
150 }
151
152 menu.boot_options = {
153         entries = {
154                 -- return to welcome menu
155                 return_menu_entry,
156                 -- load defaults
157                 {
158                         entry_type = core.MENU_ENTRY,
159                         name = "Load System " .. color.highlight("D") ..
160                             "efaults",
161                         func = core.setDefaults,
162                         alias = {"d", "D"},
163                 },
164                 {
165                         entry_type = core.MENU_SEPARATOR,
166                 },
167                 {
168                         entry_type = core.MENU_SEPARATOR,
169                         name = "Boot Options:",
170                 },
171                 -- acpi
172                 {
173                         entry_type = core.MENU_ENTRY,
174                         visible = core.isSystem386,
175                         name = function()
176                                 return OnOff(color.highlight("A") ..
177                                     "CPI       :", core.acpi)
178                         end,
179                         func = core.setACPI,
180                         alias = {"a", "A"},
181                 },
182                 -- safe mode
183                 {
184                         entry_type = core.MENU_ENTRY,
185                         name = function()
186                                 return OnOff("Safe " .. color.highlight("M") ..
187                                     "ode  :", core.sm)
188                         end,
189                         func = core.setSafeMode,
190                         alias = {"m", "M"},
191                 },
192                 -- single user
193                 {
194                         entry_type = core.MENU_ENTRY,
195                         name = function()
196                                 return OnOff(color.highlight("S") ..
197                                     "ingle user:", core.su)
198                         end,
199                         func = core.setSingleUser,
200                         alias = {"s", "S"},
201                 },
202                 -- verbose boot
203                 {
204                         entry_type = core.MENU_ENTRY,
205                         name = function()
206                                 return OnOff(color.highlight("V") ..
207                                     "erbose    :", core.verbose)
208                         end,
209                         func = core.setVerbose,
210                         alias = {"v", "V"},
211                 },
212         },
213 }
214
215 menu.welcome = {
216         entries = function()
217                 local menu_entries = menu.welcome.all_entries
218                 local multi_user = menu_entries.multi_user
219                 local single_user = menu_entries.single_user
220                 local boot_entry_1, boot_entry_2
221                 if core.isSingleUserBoot() then
222                         -- Swap the first two menu items on single user boot.
223                         -- We'll cache the alternate entries for performance.
224                         local alts = menu_entries.alts
225                         if alts == nil then
226                                 single_user = core.deepCopyTable(single_user)
227                                 multi_user = core.deepCopyTable(multi_user)
228                                 single_user.name = single_user.alternate_name
229                                 multi_user.name = multi_user.alternate_name
230                                 menu_entries.alts = {
231                                         single_user = single_user,
232                                         multi_user = multi_user,
233                                 }
234                         else
235                                 single_user = alts.single_user
236                                 multi_user = alts.multi_user
237                         end
238                         boot_entry_1, boot_entry_2 = single_user, multi_user
239                 else
240                         boot_entry_1, boot_entry_2 = multi_user, single_user
241                 end
242                 return {
243                         boot_entry_1,
244                         boot_entry_2,
245                         menu_entries.prompt,
246                         menu_entries.reboot,
247                         menu_entries.console,
248                         {
249                                 entry_type = core.MENU_SEPARATOR,
250                         },
251                         {
252                                 entry_type = core.MENU_SEPARATOR,
253                                 name = "Options:",
254                         },
255                         menu_entries.kernel_options,
256                         menu_entries.boot_options,
257                         menu_entries.zpool_checkpoints,
258                         menu_entries.boot_envs,
259                         menu_entries.chainload,
260                         menu_entries.vendor,
261                 }
262         end,
263         all_entries = {
264                 multi_user = {
265                         entry_type = core.MENU_ENTRY,
266                         name = color.highlight("B") .. "oot Multi user " ..
267                             color.highlight("[Enter]"),
268                         -- Not a standard menu entry function!
269                         alternate_name = color.highlight("B") ..
270                             "oot Multi user",
271                         func = function()
272                                 core.setSingleUser(false)
273                                 core.boot()
274                         end,
275                         alias = {"b", "B"},
276                 },
277                 single_user = {
278                         entry_type = core.MENU_ENTRY,
279                         name = "Boot " .. color.highlight("S") .. "ingle user",
280                         -- Not a standard menu entry function!
281                         alternate_name = "Boot " .. color.highlight("S") ..
282                             "ingle user " .. color.highlight("[Enter]"),
283                         func = function()
284                                 core.setSingleUser(true)
285                                 core.boot()
286                         end,
287                         alias = {"s", "S"},
288                 },
289                 console = {
290                         entry_type = core.MENU_ENTRY,
291                         name = function()
292                                 return color.highlight("C") .. "ons: " .. core.getConsoleName()
293                         end,
294                         func = function()
295                                 core.nextConsoleChoice()
296                         end,
297                         alias = {"c", "C"},
298                 },
299                 prompt = {
300                         entry_type = core.MENU_RETURN,
301                         name = color.highlight("Esc") .. "ape to loader prompt",
302                         func = function()
303                                 loader.setenv("autoboot_delay", "NO")
304                         end,
305                         alias = {core.KEYSTR_ESCAPE},
306                 },
307                 reboot = {
308                         entry_type = core.MENU_ENTRY,
309                         name = color.highlight("R") .. "eboot",
310                         func = function()
311                                 loader.perform("reboot")
312                         end,
313                         alias = {"r", "R"},
314                 },
315                 kernel_options = {
316                         entry_type = core.MENU_CAROUSEL_ENTRY,
317                         carousel_id = "kernel",
318                         items = core.kernelList,
319                         name = function(idx, choice, all_choices)
320                                 if #all_choices == 0 then
321                                         return "Kernel: "
322                                 end
323
324                                 local is_default = (idx == 1)
325                                 local kernel_name = ""
326                                 local name_color
327                                 if is_default then
328                                         name_color = color.escapefg(color.GREEN)
329                                         kernel_name = "default/"
330                                 else
331                                         name_color = color.escapefg(color.BLUE)
332                                 end
333                                 kernel_name = kernel_name .. name_color ..
334                                     choice .. color.resetfg()
335                                 return color.highlight("K") .. "ernel: " ..
336                                     kernel_name .. " (" .. idx .. " of " ..
337                                     #all_choices .. ")"
338                         end,
339                         func = function(_, choice, _)
340                                 if loader.getenv("kernelname") ~= nil then
341                                         loader.perform("unload")
342                                 end
343                                 config.selectKernel(choice)
344                         end,
345                         alias = {"k", "K"},
346                 },
347                 boot_options = {
348                         entry_type = core.MENU_SUBMENU,
349                         name = "Boot " .. color.highlight("O") .. "ptions",
350                         submenu = menu.boot_options,
351                         alias = {"o", "O"},
352                 },
353                 zpool_checkpoints = {
354                         entry_type = core.MENU_ENTRY,
355                         name = function()
356                                 local rewind = "No"
357                                 if core.isRewinded() then
358                                         rewind = "Yes"
359                                 end
360                                 return "Rewind ZFS " .. color.highlight("C") ..
361                                         "heckpoint: " .. rewind
362                         end,
363                         func = function()
364                                 core.changeRewindCheckpoint()
365                                 if core.isRewinded() then
366                                         bootenvSet(
367                                             core.bootenvDefaultRewinded())
368                                 else
369                                         bootenvSet(core.bootenvDefault())
370                                 end
371                                 config.setCarouselIndex("be_active", 1)
372                         end,
373                         visible = function()
374                                 return core.isZFSBoot() and
375                                     core.isCheckpointed()
376                         end,
377                         alias = {"c", "C"},
378                 },
379                 boot_envs = {
380                         entry_type = core.MENU_SUBMENU,
381                         visible = function()
382                                 return core.isZFSBoot() and
383                                     #core.bootenvList() > 1
384                         end,
385                         name = "Boot " .. color.highlight("E") .. "nvironments",
386                         submenu = menu.boot_environments,
387                         alias = {"e", "E"},
388                 },
389                 chainload = {
390                         entry_type = core.MENU_ENTRY,
391                         name = function()
392                                 return 'Chain' .. color.highlight("L") ..
393                                     "oad " .. loader.getenv('chain_disk')
394                         end,
395                         func = function()
396                                 loader.perform("chain " ..
397                                     loader.getenv('chain_disk'))
398                         end,
399                         visible = function()
400                                 return loader.getenv('chain_disk') ~= nil
401                         end,
402                         alias = {"l", "L"},
403                 },
404                 vendor = {
405                         entry_type = core.MENU_ENTRY,
406                         visible = function()
407                                 return false
408                         end
409                 },
410         },
411 }
412
413 menu.default = menu.welcome
414 -- current_alias_table will be used to keep our alias table consistent across
415 -- screen redraws, instead of relying on whatever triggered the redraw to update
416 -- the local alias_table in menu.process.
417 menu.current_alias_table = {}
418
419 function menu.draw(menudef)
420         -- Clear the screen, reset the cursor, then draw
421         screen.clear()
422         menu.current_alias_table = drawer.drawscreen(menudef)
423         drawn_menu = menudef
424         screen.defcursor()
425 end
426
427 -- 'keypress' allows the caller to indicate that a key has been pressed that we
428 -- should process as our initial input.
429 function menu.process(menudef, keypress)
430         assert(menudef ~= nil)
431
432         if drawn_menu ~= menudef then
433                 menu.draw(menudef)
434         end
435
436         while true do
437                 local key = keypress or io.getchar()
438                 keypress = nil
439
440                 -- Special key behaviors
441                 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
442                     menudef ~= menu.default then
443                         break
444                 elseif key == core.KEY_ENTER then
445                         core.boot()
446                         -- Should not return.  If it does, escape menu handling
447                         -- and drop to loader prompt.
448                         return false
449                 end
450
451                 key = string.char(key)
452                 -- check to see if key is an alias
453                 local sel_entry = nil
454                 for k, v in pairs(menu.current_alias_table) do
455                         if key == k then
456                                 sel_entry = v
457                                 break
458                         end
459                 end
460
461                 -- if we have an alias do the assigned action:
462                 if sel_entry ~= nil then
463                         local handler = menu.handlers[sel_entry.entry_type]
464                         assert(handler ~= nil)
465                         -- The handler's return value indicates if we
466                         -- need to exit this menu.  An omitted or true
467                         -- return value means to continue.
468                         if handler(menudef, sel_entry) == false then
469                                 return
470                         end
471                         -- If we got an alias key the screen is out of date...
472                         -- redraw it.
473                         menu.draw(menudef)
474                 end
475         end
476 end
477
478 function menu.run()
479         local autoboot_key
480         local delay = loader.getenv("autoboot_delay")
481
482         if delay ~= nil and delay:lower() == "no" then
483                 delay = nil
484         else
485                 delay = tonumber(delay) or 10
486         end
487
488         if delay == -1 then
489                 core.boot()
490                 return
491         end
492
493         menu.draw(menu.default)
494
495         if delay ~= nil then
496                 autoboot_key = menu.autoboot(delay)
497
498                 -- autoboot_key should return the key pressed.  It will only
499                 -- return nil if we hit the timeout and executed the timeout
500                 -- command.  Bail out.
501                 if autoboot_key == nil then
502                         return
503                 end
504         end
505
506         menu.process(menu.default, autoboot_key)
507         drawn_menu = nil
508
509         screen.defcursor()
510         print("Exiting menu!")
511 end
512
513 function menu.autoboot(delay)
514         local x = loader.getenv("loader_menu_timeout_x") or 4
515         local y = loader.getenv("loader_menu_timeout_y") or 23
516         local endtime = loader.time() + delay
517         local time
518         local last
519         repeat
520                 time = endtime - loader.time()
521                 if last == nil or last ~= time then
522                         last = time
523                         screen.setcursor(x, y)
524                         print("Autoboot in " .. time ..
525                             " seconds. [Space] to pause ")
526                         screen.defcursor()
527                 end
528                 if io.ischar() then
529                         local ch = io.getchar()
530                         if ch == core.KEY_ENTER then
531                                 break
532                         else
533                                 -- erase autoboot msg
534                                 screen.setcursor(0, y)
535                                 print(string.rep(" ", 80))
536                                 screen.defcursor()
537                                 return ch
538                         end
539                 end
540
541                 loader.delay(50000)
542         until time <= 0
543
544         local cmd = loader.getenv("menu_timeout_command") or "boot"
545         cli_execute_unparsed(cmd)
546         return nil
547 end
548
549 -- CLI commands
550 function cli.menu()
551         menu.run()
552 end
553
554 return menu