]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - stand/lua/menu.lua
MFC r360199
[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                         name = function()
136                                 return color.highlight("b") .. "ootfs: " ..
137                                     core.bootenvDefault()
138                         end,
139                         func = function()
140                                 -- Reset active boot environment to the default
141                                 config.setCarouselIndex("be_active", 1)
142                                 bootenvSet(core.bootenvDefault())
143                         end,
144                         alias = {"b", "B"},
145                 },
146         },
147 }
148
149 menu.boot_options = {
150         entries = {
151                 -- return to welcome menu
152                 return_menu_entry,
153                 -- load defaults
154                 {
155                         entry_type = core.MENU_ENTRY,
156                         name = "Load System " .. color.highlight("D") ..
157                             "efaults",
158                         func = core.setDefaults,
159                         alias = {"d", "D"},
160                 },
161                 {
162                         entry_type = core.MENU_SEPARATOR,
163                 },
164                 {
165                         entry_type = core.MENU_SEPARATOR,
166                         name = "Boot Options:",
167                 },
168                 -- acpi
169                 {
170                         entry_type = core.MENU_ENTRY,
171                         visible = core.isSystem386,
172                         name = function()
173                                 return OnOff(color.highlight("A") ..
174                                     "CPI       :", core.acpi)
175                         end,
176                         func = core.setACPI,
177                         alias = {"a", "A"},
178                 },
179                 -- safe mode
180                 {
181                         entry_type = core.MENU_ENTRY,
182                         name = function()
183                                 return OnOff("Safe " .. color.highlight("M") ..
184                                     "ode  :", core.sm)
185                         end,
186                         func = core.setSafeMode,
187                         alias = {"m", "M"},
188                 },
189                 -- single user
190                 {
191                         entry_type = core.MENU_ENTRY,
192                         name = function()
193                                 return OnOff(color.highlight("S") ..
194                                     "ingle user:", core.su)
195                         end,
196                         func = core.setSingleUser,
197                         alias = {"s", "S"},
198                 },
199                 -- verbose boot
200                 {
201                         entry_type = core.MENU_ENTRY,
202                         name = function()
203                                 return OnOff(color.highlight("V") ..
204                                     "erbose    :", core.verbose)
205                         end,
206                         func = core.setVerbose,
207                         alias = {"v", "V"},
208                 },
209         },
210 }
211
212 menu.welcome = {
213         entries = function()
214                 local menu_entries = menu.welcome.all_entries
215                 local multi_user = menu_entries.multi_user
216                 local single_user = menu_entries.single_user
217                 local boot_entry_1, boot_entry_2
218                 if core.isSingleUserBoot() then
219                         -- Swap the first two menu items on single user boot.
220                         -- We'll cache the alternate entries for performance.
221                         local alts = menu_entries.alts
222                         if alts == nil then
223                                 single_user = core.deepCopyTable(single_user)
224                                 multi_user = core.deepCopyTable(multi_user)
225                                 single_user.name = single_user.alternate_name
226                                 multi_user.name = multi_user.alternate_name
227                                 menu_entries.alts = {
228                                         single_user = single_user,
229                                         multi_user = multi_user,
230                                 }
231                         else
232                                 single_user = alts.single_user 
233                                 multi_user = alts.multi_user
234                         end
235                         boot_entry_1, boot_entry_2 = single_user, multi_user
236                 else
237                         boot_entry_1, boot_entry_2 = multi_user, single_user
238                 end
239                 return {
240                         boot_entry_1,
241                         boot_entry_2,
242                         menu_entries.prompt,
243                         menu_entries.reboot,
244                         {
245                                 entry_type = core.MENU_SEPARATOR,
246                         },
247                         {
248                                 entry_type = core.MENU_SEPARATOR,
249                                 name = "Options:",
250                         },
251                         menu_entries.kernel_options,
252                         menu_entries.boot_options,
253                         menu_entries.boot_envs,
254                         menu_entries.chainload,
255                 }
256         end,
257         all_entries = {
258                 multi_user = {
259                         entry_type = core.MENU_ENTRY,
260                         name = color.highlight("B") .. "oot Multi user " ..
261                             color.highlight("[Enter]"),
262                         -- Not a standard menu entry function!
263                         alternate_name = color.highlight("B") ..
264                             "oot Multi user",
265                         func = function()
266                                 core.setSingleUser(false)
267                                 core.boot()
268                         end,
269                         alias = {"b", "B"},
270                 },
271                 single_user = {
272                         entry_type = core.MENU_ENTRY,
273                         name = "Boot " .. color.highlight("S") .. "ingle user",
274                         -- Not a standard menu entry function!
275                         alternate_name = "Boot " .. color.highlight("S") ..
276                             "ingle user " .. color.highlight("[Enter]"),
277                         func = function()
278                                 core.setSingleUser(true)
279                                 core.boot()
280                         end,
281                         alias = {"s", "S"},
282                 },
283                 prompt = {
284                         entry_type = core.MENU_RETURN,
285                         name = color.highlight("Esc") .. "ape to loader prompt",
286                         func = function()
287                                 loader.setenv("autoboot_delay", "NO")
288                         end,
289                         alias = {core.KEYSTR_ESCAPE},
290                 },
291                 reboot = {
292                         entry_type = core.MENU_ENTRY,
293                         name = color.highlight("R") .. "eboot",
294                         func = function()
295                                 loader.perform("reboot")
296                         end,
297                         alias = {"r", "R"},
298                 },
299                 kernel_options = {
300                         entry_type = core.MENU_CAROUSEL_ENTRY,
301                         carousel_id = "kernel",
302                         items = core.kernelList,
303                         name = function(idx, choice, all_choices)
304                                 if #all_choices == 0 then
305                                         return "Kernel: "
306                                 end
307
308                                 local is_default = (idx == 1)
309                                 local kernel_name = ""
310                                 local name_color
311                                 if is_default then
312                                         name_color = color.escapefg(color.GREEN)
313                                         kernel_name = "default/"
314                                 else
315                                         name_color = color.escapefg(color.BLUE)
316                                 end
317                                 kernel_name = kernel_name .. name_color ..
318                                     choice .. color.resetfg()
319                                 return color.highlight("K") .. "ernel: " ..
320                                     kernel_name .. " (" .. idx .. " of " ..
321                                     #all_choices .. ")"
322                         end,
323                         func = function(_, choice, _)
324                                 if loader.getenv("kernelname") ~= nil then
325                                         loader.perform("unload")
326                                 end
327                                 config.selectKernel(choice)
328                         end,
329                         alias = {"k", "K"},
330                 },
331                 boot_options = {
332                         entry_type = core.MENU_SUBMENU,
333                         name = "Boot " .. color.highlight("O") .. "ptions",
334                         submenu = menu.boot_options,
335                         alias = {"o", "O"},
336                 },
337                 boot_envs = {
338                         entry_type = core.MENU_SUBMENU,
339                         visible = function()
340                                 return core.isZFSBoot() and
341                                     #core.bootenvList() > 1
342                         end,
343                         name = "Boot " .. color.highlight("E") .. "nvironments",
344                         submenu = menu.boot_environments,
345                         alias = {"e", "E"},
346                 },
347                 chainload = {
348                         entry_type = core.MENU_ENTRY,
349                         name = function()
350                                 return 'Chain' .. color.highlight("L") ..
351                                     "oad " .. loader.getenv('chain_disk')
352                         end,
353                         func = function()
354                                 loader.perform("chain " ..
355                                     loader.getenv('chain_disk'))
356                         end,
357                         visible = function()
358                                 return loader.getenv('chain_disk') ~= nil
359                         end,
360                         alias = {"l", "L"},
361                 },
362         },
363 }
364
365 menu.default = menu.welcome
366 -- current_alias_table will be used to keep our alias table consistent across
367 -- screen redraws, instead of relying on whatever triggered the redraw to update
368 -- the local alias_table in menu.process.
369 menu.current_alias_table = {}
370
371 function menu.draw(menudef)
372         -- Clear the screen, reset the cursor, then draw
373         screen.clear()
374         menu.current_alias_table = drawer.drawscreen(menudef)
375         drawn_menu = menudef
376         screen.defcursor()
377 end
378
379 -- 'keypress' allows the caller to indicate that a key has been pressed that we
380 -- should process as our initial input.
381 function menu.process(menudef, keypress)
382         assert(menudef ~= nil)
383
384         if drawn_menu ~= menudef then
385                 menu.draw(menudef)
386         end
387
388         while true do
389                 local key = keypress or io.getchar()
390                 keypress = nil
391
392                 -- Special key behaviors
393                 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
394                     menudef ~= menu.default then
395                         break
396                 elseif key == core.KEY_ENTER then
397                         core.boot()
398                         -- Should not return.  If it does, escape menu handling
399                         -- and drop to loader prompt.
400                         return false
401                 end
402
403                 key = string.char(key)
404                 -- check to see if key is an alias
405                 local sel_entry = nil
406                 for k, v in pairs(menu.current_alias_table) do
407                         if key == k then
408                                 sel_entry = v
409                                 break
410                         end
411                 end
412
413                 -- if we have an alias do the assigned action:
414                 if sel_entry ~= nil then
415                         local handler = menu.handlers[sel_entry.entry_type]
416                         assert(handler ~= nil)
417                         -- The handler's return value indicates if we
418                         -- need to exit this menu.  An omitted or true
419                         -- return value means to continue.
420                         if handler(menudef, sel_entry) == false then
421                                 return
422                         end
423                         -- If we got an alias key the screen is out of date...
424                         -- redraw it.
425                         menu.draw(menudef)
426                 end
427         end
428 end
429
430 function menu.run()
431         local autoboot_key
432         local delay = loader.getenv("autoboot_delay")
433
434         if delay ~= nil and delay:lower() == "no" then
435                 delay = nil
436         else
437                 delay = tonumber(delay) or 10
438         end
439
440         if delay == -1 then
441                 core.boot()
442                 return
443         end
444
445         menu.draw(menu.default)
446
447         if delay ~= nil then
448                 autoboot_key = menu.autoboot(delay)
449
450                 -- autoboot_key should return the key pressed.  It will only
451                 -- return nil if we hit the timeout and executed the timeout
452                 -- command.  Bail out.
453                 if autoboot_key == nil then
454                         return
455                 end
456         end
457
458         menu.process(menu.default, autoboot_key)
459         drawn_menu = nil
460
461         screen.defcursor()
462         print("Exiting menu!")
463 end
464
465 function menu.autoboot(delay)
466         local x = loader.getenv("loader_menu_timeout_x") or 4
467         local y = loader.getenv("loader_menu_timeout_y") or 23
468         local endtime = loader.time() + delay
469         local time
470         local last
471         repeat
472                 time = endtime - loader.time()
473                 if last == nil or last ~= time then
474                         last = time
475                         screen.setcursor(x, y)
476                         print("Autoboot in " .. time ..
477                             " seconds, hit [Enter] to boot" ..
478                             " or any other key to stop     ")
479                         screen.defcursor()
480                 end
481                 if io.ischar() then
482                         local ch = io.getchar()
483                         if ch == core.KEY_ENTER then
484                                 break
485                         else
486                                 -- erase autoboot msg
487                                 screen.setcursor(0, y)
488                                 print(string.rep(" ", 80))
489                                 screen.defcursor()
490                                 return ch
491                         end
492                 end
493
494                 loader.delay(50000)
495         until time <= 0
496
497         local cmd = loader.getenv("menu_timeout_command") or "boot"
498         cli_execute_unparsed(cmd)
499         return nil
500 end
501
502 -- CLI commands
503 function cli.menu()
504         menu.run()
505 end
506
507 return menu