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 32local cli = require("cli") 33local core = require("core") 34local color = require("color") 35local config = require("config") 36local screen = require("screen") 37local drawer = require("drawer") 38 39local menu = {} 40 41local drawn_menu 42local return_menu_entry = { 43 entry_type = core.MENU_RETURN, 44 name = "Back to main menu" .. color.highlight(" [Backspace]"), 45} 46 47local 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 55end 56 57local function bootenvSet(env) 58 loader.setenv("vfs.root.mountfrom", env) 59 loader.setenv("currdev", env .. ":") 60 config.reload() 61end 62 63-- Module exports 64menu.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 101menu.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 149menu.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 212menu.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 365menu.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. 369menu.current_alias_table = {} 370 371function 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() 377end 378 379-- 'keypress' allows the caller to indicate that a key has been pressed that we 380-- should process as our initial input. 381function 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 428end 429 430function 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!") 463end 464 465function 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 500end 501 502-- CLI commands 503function cli.menu() 504 menu.run() 505end 506 507return menu 508