2 -- SPDX-License-Identifier: BSD-2-Clause
4 -- Copyright (c) 2015 Pedro Souza <pedrosouza@freebsd.org>
5 -- Copyright (c) 2018 Kyle Evans <kevans@FreeBSD.org>
6 -- All rights reserved.
8 -- Redistribution and use in source and binary forms, with or without
9 -- modification, are permitted provided that the following conditions
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.
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
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")
40 local return_menu_entry = {
41 entry_type = core.MENU_RETURN,
42 name = "Back to main menu" .. color.highlight(" [Backspace]"),
45 local function OnOff(str, value)
47 return str .. color.escapefg(color.GREEN) .. "On" ..
50 return str .. color.escapefg(color.RED) .. "off" ..
55 local function bootenvSet(env)
56 loader.setenv("vfs.root.mountfrom", env)
57 loader.setenv("currdev", env .. ":")
59 if loader.getenv("kernelname") ~= nil then
60 loader.perform("unload")
64 local function multiUserPrompt()
65 return loader.getenv("loader_menu_multi_user_prompt") or "Multi user"
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)
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
88 caridx = (caridx % #choices) + 1
89 config.setCarouselIndex(carid, caridx)
90 entry.func(caridx, choices[caridx], choices)
93 [core.MENU_SUBMENU] = function(_, entry)
94 menu.process(entry.submenu)
96 [core.MENU_RETURN] = function(_, entry)
97 -- allow entry to have a function/side effect
98 if entry.func ~= nil then
104 -- loader menu tree is rooted at menu.welcome
106 menu.boot_environments = {
108 -- return to welcome menu
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
119 local is_default = (idx == 1)
120 local bootenv_name = ""
123 name_color = color.escapefg(color.GREEN)
125 name_color = color.escapefg(color.BLUE)
127 bootenv_name = bootenv_name .. name_color ..
128 choice .. color.resetfg()
129 return color.highlight("A").."ctive: " ..
130 bootenv_name .. " (" .. idx .. " of " ..
133 func = function(_, choice, _)
139 entry_type = core.MENU_ENTRY,
141 return core.isRewinded() == false
144 return color.highlight("b") .. "ootfs: " ..
145 core.bootenvDefault()
148 -- Reset active boot environment to the default
149 config.setCarouselIndex("be_active", 1)
150 bootenvSet(core.bootenvDefault())
157 menu.boot_options = {
159 -- return to welcome menu
163 entry_type = core.MENU_ENTRY,
164 name = "Load System " .. color.highlight("D") ..
166 func = core.setDefaults,
170 entry_type = core.MENU_SEPARATOR,
173 entry_type = core.MENU_SEPARATOR,
174 name = "Boot Options:",
178 entry_type = core.MENU_ENTRY,
179 visible = core.hasACPI,
181 return OnOff(color.highlight("A") ..
189 entry_type = core.MENU_ENTRY,
191 return OnOff("Safe " .. color.highlight("M") ..
194 func = core.setSafeMode,
199 entry_type = core.MENU_ENTRY,
201 return OnOff(color.highlight("S") ..
202 "ingle user:", core.su)
204 func = core.setSingleUser,
209 entry_type = core.MENU_ENTRY,
211 return OnOff(color.highlight("V") ..
212 "erbose :", core.verbose)
214 func = core.setVerbose,
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
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,
240 single_user = alts.single_user
241 multi_user = alts.multi_user
243 boot_entry_1, boot_entry_2 = single_user, multi_user
245 boot_entry_1, boot_entry_2 = multi_user, single_user
252 menu_entries.console,
254 entry_type = core.MENU_SEPARATOR,
257 entry_type = core.MENU_SEPARATOR,
260 menu_entries.kernel_options,
261 menu_entries.boot_options,
262 menu_entries.zpool_checkpoints,
263 menu_entries.boot_envs,
264 menu_entries.chainload,
270 entry_type = core.MENU_ENTRY,
272 return color.highlight("B") .. "oot " ..
273 multiUserPrompt() .. " " ..
274 color.highlight("[Enter]")
276 -- Not a standard menu entry function!
277 alternate_name = function()
278 return color.highlight("B") .. "oot " ..
282 core.setSingleUser(false)
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]"),
294 core.setSingleUser(true)
300 entry_type = core.MENU_ENTRY,
302 return color.highlight("C") .. "ons: " .. core.getConsoleName()
305 core.nextConsoleChoice()
310 entry_type = core.MENU_RETURN,
311 name = color.highlight("Esc") .. "ape to loader prompt",
313 loader.setenv("autoboot_delay", "NO")
315 alias = {core.KEYSTR_ESCAPE},
318 entry_type = core.MENU_ENTRY,
319 name = color.highlight("R") .. "eboot",
321 loader.perform("reboot")
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
334 local is_default = (idx == 1)
335 local kernel_name = ""
338 name_color = color.escapefg(color.GREEN)
339 kernel_name = "default/"
341 name_color = color.escapefg(color.BLUE)
343 kernel_name = kernel_name .. name_color ..
344 choice .. color.resetfg()
345 return color.highlight("K") .. "ernel: " ..
346 kernel_name .. " (" .. idx .. " of " ..
349 func = function(_, choice, _)
350 if loader.getenv("kernelname") ~= nil then
351 loader.perform("unload")
353 config.selectKernel(choice)
358 entry_type = core.MENU_SUBMENU,
359 name = "Boot " .. color.highlight("O") .. "ptions",
360 submenu = menu.boot_options,
363 zpool_checkpoints = {
364 entry_type = core.MENU_ENTRY,
367 if core.isRewinded() then
370 return "Rewind ZFS " .. color.highlight("C") ..
371 "heckpoint: " .. rewind
374 core.changeRewindCheckpoint()
375 if core.isRewinded() then
377 core.bootenvDefaultRewinded())
379 bootenvSet(core.bootenvDefault())
381 config.setCarouselIndex("be_active", 1)
384 return core.isZFSBoot() and
385 core.isCheckpointed()
390 entry_type = core.MENU_SUBMENU,
392 return core.isZFSBoot() and
393 #core.bootenvList() > 1
395 name = "Boot " .. color.highlight("E") .. "nvironments",
396 submenu = menu.boot_environments,
400 entry_type = core.MENU_ENTRY,
402 return 'Chain' .. color.highlight("L") ..
403 "oad " .. loader.getenv('chain_disk')
406 loader.perform("chain " ..
407 loader.getenv('chain_disk'))
410 return loader.getenv('chain_disk') ~= nil
415 entry_type = core.MENU_ENTRY,
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 = {}
429 function menu.draw(menudef)
430 -- Clear the screen, reset the cursor, then draw
432 menu.current_alias_table = drawer.drawscreen(menudef)
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)
442 if drawn_menu ~= menudef then
447 local key = keypress or io.getchar()
450 -- Special key behaviors
451 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
452 menudef ~= menu.default then
454 elseif key == core.KEY_ENTER then
456 -- Should not return. If it does, escape menu handling
457 -- and drop to loader prompt.
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
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
481 -- If we got an alias key the screen is out of date...
490 local delay = loader.getenv("autoboot_delay")
492 if delay ~= nil and delay:lower() == "no" then
495 delay = tonumber(delay) or 10
503 menu.draw(menu.default)
506 autoboot_key = menu.autoboot(delay)
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
516 menu.process(menu.default, autoboot_key)
520 print("Exiting menu!")
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
530 time = endtime - loader.time()
531 if last == nil or last ~= time then
533 screen.setcursor(x, y)
534 print("Autoboot in " .. time ..
535 " seconds. [Space] to pause ")
539 local ch = io.getchar()
540 if ch == core.KEY_ENTER then
543 -- erase autoboot msg
544 screen.setcursor(0, y)
545 print(string.rep(" ", 80))
554 local cmd = loader.getenv("menu_timeout_command") or "boot"
555 cli_execute_unparsed(cmd)