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