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