2 -- SPDX-License-Identifier: BSD-2-Clause-FreeBSD
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
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")
42 local return_menu_entry = {
43 entry_type = core.MENU_RETURN,
44 name = "Back to main menu" .. color.highlight(" [Backspace]"),
47 local function OnOff(str, value)
49 return str .. color.escapefg(color.GREEN) .. "On" ..
50 color.escapefg(color.WHITE)
52 return str .. color.escapefg(color.RED) .. "off" ..
53 color.escapefg(color.WHITE)
57 local function bootenvSet(env)
58 loader.setenv("vfs.root.mountfrom", env)
59 loader.setenv("currdev", env .. ":")
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)
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
83 caridx = (caridx % #choices) + 1
84 config.setCarouselIndex(carid, caridx)
85 entry.func(caridx, choices[caridx], choices)
88 [core.MENU_SUBMENU] = function(_, entry)
89 menu.process(entry.submenu)
91 [core.MENU_RETURN] = function(_, entry)
92 -- allow entry to have a function/side effect
93 if entry.func ~= nil then
99 -- loader menu tree is rooted at menu.welcome
101 menu.boot_environments = {
103 -- return to welcome menu
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
114 local is_default = (idx == 1)
115 local bootenv_name = ""
118 name_color = color.escapefg(color.GREEN)
120 name_color = color.escapefg(color.BLUE)
122 bootenv_name = bootenv_name .. name_color ..
123 choice .. color.resetfg()
124 return color.highlight("A").."ctive: " ..
125 bootenv_name .. " (" .. idx .. " of " ..
128 func = function(_, choice, _)
134 entry_type = core.MENU_ENTRY,
136 return color.highlight("b") .. "ootfs: " ..
137 core.bootenvDefault()
140 -- Reset active boot environment to the default
141 config.setCarouselIndex("be_active", 1)
142 bootenvSet(core.bootenvDefault())
149 menu.boot_options = {
151 -- return to welcome menu
155 entry_type = core.MENU_ENTRY,
156 name = "Load System " .. color.highlight("D") ..
158 func = core.setDefaults,
162 entry_type = core.MENU_SEPARATOR,
165 entry_type = core.MENU_SEPARATOR,
166 name = "Boot Options:",
170 entry_type = core.MENU_ENTRY,
171 visible = core.isSystem386,
173 return OnOff(color.highlight("A") ..
181 entry_type = core.MENU_ENTRY,
183 return OnOff("Safe " .. color.highlight("M") ..
186 func = core.setSafeMode,
191 entry_type = core.MENU_ENTRY,
193 return OnOff(color.highlight("S") ..
194 "ingle user:", core.su)
196 func = core.setSingleUser,
201 entry_type = core.MENU_ENTRY,
203 return OnOff(color.highlight("V") ..
204 "erbose :", core.verbose)
206 func = core.setVerbose,
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
221 -- Shallow copy the table
222 menu_entries = core.deepCopyTable(menu_entries)
224 -- Swap the first two menu entries
225 menu_entries[1], menu_entries[2] =
226 menu_entries[2], menu_entries[1]
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
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") ..
246 core.setSingleUser(false)
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]"),
259 core.setSingleUser(true)
264 -- escape to interpreter
266 entry_type = core.MENU_RETURN,
267 name = color.highlight("Esc") .. "ape to loader prompt",
269 loader.setenv("autoboot_delay", "NO")
271 alias = {core.KEYSTR_ESCAPE},
275 entry_type = core.MENU_ENTRY,
276 name = color.highlight("R") .. "eboot",
278 loader.perform("reboot")
283 entry_type = core.MENU_SEPARATOR,
286 entry_type = core.MENU_SEPARATOR,
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
299 local is_default = (idx == 1)
300 local kernel_name = ""
303 name_color = color.escapefg(color.GREEN)
304 kernel_name = "default/"
306 name_color = color.escapefg(color.BLUE)
308 kernel_name = kernel_name .. name_color ..
309 choice .. color.resetfg()
310 return color.highlight("K") .. "ernel: " ..
311 kernel_name .. " (" .. idx .. " of " ..
314 func = function(_, choice, _)
315 if loader.getenv("kernelname") ~= nil then
316 loader.perform("unload")
318 config.selectKernel(choice)
324 entry_type = core.MENU_SUBMENU,
325 name = "Boot " .. color.highlight("O") .. "ptions",
326 submenu = menu.boot_options,
331 entry_type = core.MENU_SUBMENU,
333 return core.isZFSBoot() and
334 #core.bootenvList() > 1
336 name = "Boot " .. color.highlight("E") .. "nvironments",
337 submenu = menu.boot_environments,
342 entry_type = core.MENU_ENTRY,
344 return 'Chain' .. color.highlight("L") ..
345 "oad " .. loader.getenv('chain_disk')
348 loader.perform("chain " ..
349 loader.getenv('chain_disk'))
352 return loader.getenv('chain_disk') ~= nil
359 menu.default = menu.welcome
360 -- current_alias_table will be used to keep our alias table consistent across
361 -- screen redraws, instead of relying on whatever triggered the redraw to update
362 -- the local alias_table in menu.process.
363 menu.current_alias_table = {}
365 function menu.draw(menudef)
366 -- Clear the screen, reset the cursor, then draw
368 menu.current_alias_table = drawer.drawscreen(menudef)
373 -- 'keypress' allows the caller to indicate that a key has been pressed that we
374 -- should process as our initial input.
375 function menu.process(menudef, keypress)
376 assert(menudef ~= nil)
378 if drawn_menu ~= menudef then
383 local key = keypress or io.getchar()
386 -- Special key behaviors
387 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
388 menudef ~= menu.default then
390 elseif key == core.KEY_ENTER then
392 -- Should not return. If it does, escape menu handling
393 -- and drop to loader prompt.
397 key = string.char(key)
398 -- check to see if key is an alias
399 local sel_entry = nil
400 for k, v in pairs(menu.current_alias_table) do
407 -- if we have an alias do the assigned action:
408 if sel_entry ~= nil then
409 local handler = menu.handlers[sel_entry.entry_type]
410 assert(handler ~= nil)
411 -- The handler's return value indicates if we
412 -- need to exit this menu. An omitted or true
413 -- return value means to continue.
414 if handler(menudef, sel_entry) == false then
417 -- If we got an alias key the screen is out of date...
426 local delay = loader.getenv("autoboot_delay")
428 if delay ~= nil and delay:lower() == "no" then
431 delay = tonumber(delay) or 10
439 menu.draw(menu.default)
442 autoboot_key = menu.autoboot(delay)
444 -- autoboot_key should return the key pressed. It will only
445 -- return nil if we hit the timeout and executed the timeout
446 -- command. Bail out.
447 if autoboot_key == nil then
452 menu.process(menu.default, autoboot_key)
456 print("Exiting menu!")
459 function menu.autoboot(delay)
460 local x = loader.getenv("loader_menu_timeout_x") or 4
461 local y = loader.getenv("loader_menu_timeout_y") or 23
462 local endtime = loader.time() + delay
466 time = endtime - loader.time()
467 if last == nil or last ~= time then
469 screen.setcursor(x, y)
470 print("Autoboot in " .. time ..
471 " seconds, hit [Enter] to boot" ..
472 " or any other key to stop ")
476 local ch = io.getchar()
477 if ch == core.KEY_ENTER then
480 -- erase autoboot msg
481 screen.setcursor(0, y)
482 print(string.rep(" ", 80))
491 local cmd = loader.getenv("menu_timeout_command") or "boot"
492 cli_execute_unparsed(cmd)