]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - stand/lua/menu.lua
lualoader: Always return a proper dictionary for blacklist
[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.escapefg(color.WHITE)
51         else
52                 return str .. color.escapefg(color.RED) .. "off" ..
53                     color.escapefg(color.WHITE)
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                 -- Swap the first two menu items on single user boot
216                 if core.isSingleUserBoot() then
217                         -- We'll cache the swapped menu, for performance
218                         if menu.welcome.swapped_menu ~= nil then
219                                 return menu.welcome.swapped_menu
220                         end
221                         -- Shallow copy the table
222                         menu_entries = core.deepCopyTable(menu_entries)
223
224                         -- Swap the first two menu entries
225                         menu_entries[1], menu_entries[2] =
226                             menu_entries[2], menu_entries[1]
227
228                         -- Then set their names to their alternate names
229                         menu_entries[1].name, menu_entries[2].name =
230                             menu_entries[1].alternate_name,
231                             menu_entries[2].alternate_name
232                         menu.welcome.swapped_menu = menu_entries
233                 end
234                 return menu_entries
235         end,
236         all_entries = {
237                 -- boot multi user
238                 {
239                         entry_type = core.MENU_ENTRY,
240                         name = color.highlight("B") .. "oot Multi user " ..
241                             color.highlight("[Enter]"),
242                         -- Not a standard menu entry function!
243                         alternate_name = color.highlight("B") ..
244                             "oot Multi user",
245                         func = function()
246                                 core.setSingleUser(false)
247                                 core.boot()
248                         end,
249                         alias = {"b", "B"},
250                 },
251                 -- boot single user
252                 {
253                         entry_type = core.MENU_ENTRY,
254                         name = "Boot " .. color.highlight("S") .. "ingle user",
255                         -- Not a standard menu entry function!
256                         alternate_name = "Boot " .. color.highlight("S") ..
257                             "ingle user " .. color.highlight("[Enter]"),
258                         func = function()
259                                 core.setSingleUser(true)
260                                 core.boot()
261                         end,
262                         alias = {"s", "S"},
263                 },
264                 -- escape to interpreter
265                 {
266                         entry_type = core.MENU_RETURN,
267                         name = color.highlight("Esc") .. "ape to loader prompt",
268                         func = function()
269                                 loader.setenv("autoboot_delay", "NO")
270                         end,
271                         alias = {core.KEYSTR_ESCAPE},
272                 },
273                 -- reboot
274                 {
275                         entry_type = core.MENU_ENTRY,
276                         name = color.highlight("R") .. "eboot",
277                         func = function()
278                                 loader.perform("reboot")
279                         end,
280                         alias = {"r", "R"},
281                 },
282                 {
283                         entry_type = core.MENU_SEPARATOR,
284                 },
285                 {
286                         entry_type = core.MENU_SEPARATOR,
287                         name = "Options:",
288                 },
289                 -- kernel options
290                 {
291                         entry_type = core.MENU_CAROUSEL_ENTRY,
292                         carousel_id = "kernel",
293                         items = core.kernelList,
294                         name = function(idx, choice, all_choices)
295                                 if #all_choices == 0 then
296                                         return "Kernel: "
297                                 end
298
299                                 local is_default = (idx == 1)
300                                 local kernel_name = ""
301                                 local name_color
302                                 if is_default then
303                                         name_color = color.escapefg(color.GREEN)
304                                         kernel_name = "default/"
305                                 else
306                                         name_color = color.escapefg(color.BLUE)
307                                 end
308                                 kernel_name = kernel_name .. name_color ..
309                                     choice .. color.resetfg()
310                                 return color.highlight("K") .. "ernel: " ..
311                                     kernel_name .. " (" .. idx .. " of " ..
312                                     #all_choices .. ")"
313                         end,
314                         func = function(_, choice, _)
315                                 if loader.getenv("kernelname") ~= nil then
316                                         loader.perform("unload")
317                                 end
318                                 config.selectKernel(choice)
319                         end,
320                         alias = {"k", "K"},
321                 },
322                 -- boot options
323                 {
324                         entry_type = core.MENU_SUBMENU,
325                         name = "Boot " .. color.highlight("O") .. "ptions",
326                         submenu = menu.boot_options,
327                         alias = {"o", "O"},
328                 },
329                 -- boot environments
330                 {
331                         entry_type = core.MENU_SUBMENU,
332                         visible = function()
333                                 return core.isZFSBoot() and
334                                     #core.bootenvList() > 1
335                         end,
336                         name = "Boot " .. color.highlight("E") .. "nvironments",
337                         submenu = menu.boot_environments,
338                         alias = {"e", "E"},
339                 },
340         },
341 }
342
343 menu.default = menu.welcome
344 -- current_alias_table will be used to keep our alias table consistent across
345 -- screen redraws, instead of relying on whatever triggered the redraw to update
346 -- the local alias_table in menu.process.
347 menu.current_alias_table = {}
348
349 function menu.draw(menudef)
350         -- Clear the screen, reset the cursor, then draw
351         screen.clear()
352         menu.current_alias_table = drawer.drawscreen(menudef)
353         drawn_menu = menudef
354         screen.defcursor()
355 end
356
357 -- 'keypress' allows the caller to indicate that a key has been pressed that we
358 -- should process as our initial input.
359 function menu.process(menudef, keypress)
360         assert(menudef ~= nil)
361
362         if drawn_menu ~= menudef then
363                 menu.draw(menudef)
364         end
365
366         while true do
367                 local key = keypress or io.getchar()
368                 keypress = nil
369
370                 -- Special key behaviors
371                 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
372                     menudef ~= menu.default then
373                         break
374                 elseif key == core.KEY_ENTER then
375                         core.boot()
376                         -- Should not return.  If it does, escape menu handling
377                         -- and drop to loader prompt.
378                         return false
379                 end
380
381                 key = string.char(key)
382                 -- check to see if key is an alias
383                 local sel_entry = nil
384                 for k, v in pairs(menu.current_alias_table) do
385                         if key == k then
386                                 sel_entry = v
387                                 break
388                         end
389                 end
390
391                 -- if we have an alias do the assigned action:
392                 if sel_entry ~= nil then
393                         local handler = menu.handlers[sel_entry.entry_type]
394                         assert(handler ~= nil)
395                         -- The handler's return value indicates if we
396                         -- need to exit this menu.  An omitted or true
397                         -- return value means to continue.
398                         if handler(menudef, sel_entry) == false then
399                                 return
400                         end
401                         -- If we got an alias key the screen is out of date...
402                         -- redraw it.
403                         menu.draw(menudef)
404                 end
405         end
406 end
407
408 function menu.run()
409         local autoboot_key
410         local delay = loader.getenv("autoboot_delay")
411
412         if delay ~= nil and delay:lower() == "no" then
413                 delay = nil
414         else
415                 delay = tonumber(delay) or 10
416         end
417
418         if delay == -1 then
419                 core.boot()
420                 return
421         end
422
423         menu.draw(menu.default)
424
425         if delay ~= nil then
426                 autoboot_key = menu.autoboot(delay)
427
428                 -- autoboot_key should return the key pressed.  It will only
429                 -- return nil if we hit the timeout and executed the timeout
430                 -- command.  Bail out.
431                 if autoboot_key == nil then
432                         return
433                 end
434         end
435
436         menu.process(menu.default, autoboot_key)
437         drawn_menu = nil
438
439         screen.defcursor()
440         print("Exiting menu!")
441 end
442
443 function menu.autoboot(delay)
444         local x = loader.getenv("loader_menu_timeout_x") or 4
445         local y = loader.getenv("loader_menu_timeout_y") or 23
446         local endtime = loader.time() + delay
447         local time
448         local last
449         repeat
450                 time = endtime - loader.time()
451                 if last == nil or last ~= time then
452                         last = time
453                         screen.setcursor(x, y)
454                         print("Autoboot in " .. time ..
455                             " seconds, hit [Enter] to boot" ..
456                             " or any other key to stop     ")
457                         screen.defcursor()
458                 end
459                 if io.ischar() then
460                         local ch = io.getchar()
461                         if ch == core.KEY_ENTER then
462                                 break
463                         else
464                                 -- erase autoboot msg
465                                 screen.setcursor(0, y)
466                                 print(string.rep(" ", 80))
467                                 screen.defcursor()
468                                 return ch
469                         end
470                 end
471
472                 loader.delay(50000)
473         until time <= 0
474
475         local cmd = loader.getenv("menu_timeout_command") or "boot"
476         cli_execute_unparsed(cmd)
477         return nil
478 end
479
480 -- CLI commands
481 function cli.menu(...)
482         menu.run()
483 end
484
485 return menu