1-- 2-- SPDX-License-Identifier: BSD-2-Clause 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 30local cli = require("cli") 31local core = require("core") 32local color = require("color") 33local config = require("config") 34local screen = require("screen") 35local drawer = require("drawer") 36 37local menu = {} 38 39local drawn_menu 40local return_menu_entry = { 41 entry_type = core.MENU_RETURN, 42 name = "Back to main menu" .. color.highlight(" [Backspace]"), 43} 44 45local function OnOff(str, value) 46 if value then 47 return str .. color.escapefg(color.GREEN) .. "On" .. 48 color.resetfg() 49 else 50 return str .. color.escapefg(color.RED) .. "off" .. 51 color.resetfg() 52 end 53end 54 55local function bootenvSet(env) 56 core.switchBE(env) 57end 58 59local function multiUserPrompt() 60 return loader.getenv("loader_menu_multi_user_prompt") or "Multi user" 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.CYAN) 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 visible = function() 136 return core.isRewinded() == false 137 end, 138 name = function() 139 return color.highlight("b") .. "ootfs: " .. 140 core.bootenvDefault() 141 end, 142 func = function() 143 -- Reset active boot environment to the default 144 config.setCarouselIndex("be_active", 1) 145 bootenvSet(core.bootenvDefault()) 146 end, 147 alias = {"b", "B"}, 148 }, 149 }, 150} 151 152menu.boot_options = { 153 entries = { 154 -- return to welcome menu 155 return_menu_entry, 156 -- load defaults 157 { 158 entry_type = core.MENU_ENTRY, 159 name = "Load System " .. color.highlight("D") .. 160 "efaults", 161 func = core.setDefaults, 162 alias = {"d", "D"}, 163 }, 164 { 165 entry_type = core.MENU_SEPARATOR, 166 }, 167 { 168 entry_type = core.MENU_SEPARATOR, 169 name = "Boot Options:", 170 }, 171 -- acpi 172 { 173 entry_type = core.MENU_ENTRY, 174 visible = core.hasACPI, 175 name = function() 176 return OnOff(color.highlight("A") .. 177 "CPI :", core.acpi) 178 end, 179 func = core.setACPI, 180 alias = {"a", "A"}, 181 }, 182 -- safe mode 183 { 184 entry_type = core.MENU_ENTRY, 185 name = function() 186 return OnOff("Safe " .. color.highlight("M") .. 187 "ode :", core.sm) 188 end, 189 func = core.setSafeMode, 190 alias = {"m", "M"}, 191 }, 192 -- single user 193 { 194 entry_type = core.MENU_ENTRY, 195 name = function() 196 return OnOff(color.highlight("S") .. 197 "ingle user:", core.su) 198 end, 199 func = core.setSingleUser, 200 alias = {"s", "S"}, 201 }, 202 -- verbose boot 203 { 204 entry_type = core.MENU_ENTRY, 205 name = function() 206 return OnOff(color.highlight("V") .. 207 "erbose :", core.verbose) 208 end, 209 func = core.setVerbose, 210 alias = {"v", "V"}, 211 }, 212 }, 213} 214 215menu.welcome = { 216 entries = function() 217 local menu_entries = menu.welcome.all_entries 218 local multi_user = menu_entries.multi_user 219 local single_user = menu_entries.single_user 220 local boot_entry_1, boot_entry_2 221 if core.isSingleUserBoot() then 222 -- Swap the first two menu items on single user boot. 223 -- We'll cache the alternate entries for performance. 224 local alts = menu_entries.alts 225 if alts == nil then 226 single_user = core.deepCopyTable(single_user) 227 multi_user = core.deepCopyTable(multi_user) 228 single_user.name = single_user.alternate_name 229 multi_user.name = multi_user.alternate_name 230 menu_entries.alts = { 231 single_user = single_user, 232 multi_user = multi_user, 233 } 234 else 235 single_user = alts.single_user 236 multi_user = alts.multi_user 237 end 238 boot_entry_1, boot_entry_2 = single_user, multi_user 239 else 240 boot_entry_1, boot_entry_2 = multi_user, single_user 241 end 242 return { 243 boot_entry_1, 244 boot_entry_2, 245 menu_entries.prompt, 246 menu_entries.reboot, 247 menu_entries.console, 248 { 249 entry_type = core.MENU_SEPARATOR, 250 }, 251 { 252 entry_type = core.MENU_SEPARATOR, 253 name = "Kernel:", 254 }, 255 menu_entries.kernel_options, 256 { 257 entry_type = core.MENU_SEPARATOR, 258 }, 259 { 260 entry_type = core.MENU_SEPARATOR, 261 name = "Options:", 262 }, 263 menu_entries.boot_options, 264 menu_entries.zpool_checkpoints, 265 menu_entries.boot_envs, 266 menu_entries.chainload, 267 menu_entries.vendor, 268 { 269 entry_type = core.MENU_SEPARATOR, 270 }, 271 menu_entries.loader_needs_upgrade, 272 } 273 end, 274 all_entries = { 275 multi_user = { 276 entry_type = core.MENU_ENTRY, 277 name = function() 278 return color.highlight("B") .. "oot " .. 279 multiUserPrompt() .. " " .. 280 color.highlight("[Enter]") 281 end, 282 -- Not a standard menu entry function! 283 alternate_name = function() 284 return color.highlight("B") .. "oot " .. 285 multiUserPrompt() 286 end, 287 func = function() 288 core.setSingleUser(false) 289 core.boot() 290 end, 291 alias = {"b", "B"}, 292 }, 293 single_user = { 294 entry_type = core.MENU_ENTRY, 295 name = "Boot " .. color.highlight("S") .. "ingle user", 296 -- Not a standard menu entry function! 297 alternate_name = "Boot " .. color.highlight("S") .. 298 "ingle user " .. color.highlight("[Enter]"), 299 func = function() 300 core.setSingleUser(true) 301 core.boot() 302 end, 303 alias = {"s", "S"}, 304 }, 305 console = { 306 entry_type = core.MENU_ENTRY, 307 name = function() 308 return color.highlight("C") .. "ons: " .. core.getConsoleName() 309 end, 310 func = function() 311 core.nextConsoleChoice() 312 end, 313 alias = {"c", "C"}, 314 }, 315 prompt = { 316 entry_type = core.MENU_RETURN, 317 name = color.highlight("Esc") .. "ape to loader prompt", 318 func = function() 319 loader.setenv("autoboot_delay", "NO") 320 end, 321 alias = {core.KEYSTR_ESCAPE}, 322 }, 323 reboot = { 324 entry_type = core.MENU_ENTRY, 325 name = color.highlight("R") .. "eboot", 326 func = function() 327 loader.perform("reboot") 328 end, 329 alias = {"r", "R"}, 330 }, 331 kernel_options = { 332 entry_type = core.MENU_CAROUSEL_ENTRY, 333 carousel_id = "kernel", 334 items = core.kernelList, 335 name = function(idx, choice, all_choices) 336 if #all_choices == 0 then 337 return "" 338 end 339 340 local kernel_name 341 local name_color 342 if idx == 1 then 343 name_color = color.escapefg(color.GREEN) 344 else 345 name_color = color.escapefg(color.CYAN) 346 end 347 kernel_name = name_color .. choice .. 348 color.resetfg() 349 return kernel_name .. " (" .. idx .. " of " .. 350 #all_choices .. ")" 351 end, 352 func = function(_, choice, _) 353 if loader.getenv("kernelname") ~= nil then 354 loader.perform("unload") 355 end 356 config.selectKernel(choice) 357 end, 358 alias = {"k", "K"}, 359 }, 360 boot_options = { 361 entry_type = core.MENU_SUBMENU, 362 name = "Boot " .. color.highlight("O") .. "ptions", 363 submenu = menu.boot_options, 364 alias = {"o", "O"}, 365 }, 366 zpool_checkpoints = { 367 entry_type = core.MENU_ENTRY, 368 name = function() 369 local rewind = "No" 370 if core.isRewinded() then 371 rewind = "Yes" 372 end 373 return "Rewind ZFS " .. color.highlight("C") .. 374 "heckpoint: " .. rewind 375 end, 376 func = function() 377 core.changeRewindCheckpoint() 378 if core.isRewinded() then 379 bootenvSet( 380 core.bootenvDefaultRewinded()) 381 else 382 bootenvSet(core.bootenvDefault()) 383 end 384 config.setCarouselIndex("be_active", 1) 385 end, 386 visible = function() 387 return core.isZFSBoot() and 388 core.isCheckpointed() 389 end, 390 alias = {"c", "C"}, 391 }, 392 boot_envs = { 393 entry_type = core.MENU_SUBMENU, 394 visible = function() 395 return core.isZFSBoot() and 396 #core.bootenvList() > 1 397 end, 398 name = "Boot " .. color.highlight("E") .. "nvironments", 399 submenu = menu.boot_environments, 400 alias = {"e", "E"}, 401 }, 402 chainload = { 403 entry_type = core.MENU_ENTRY, 404 name = function() 405 return 'Chain' .. color.highlight("L") .. 406 "oad " .. loader.getenv('chain_disk') 407 end, 408 func = function() 409 loader.perform("chain " .. 410 loader.getenv('chain_disk')) 411 end, 412 visible = function() 413 return loader.getenv('chain_disk') ~= nil 414 end, 415 alias = {"l", "L"}, 416 }, 417 loader_needs_upgrade = { 418 entry_type = core.MENU_SEPARATOR, 419 name = function() 420 return color.highlight("Loader needs to be updated") 421 end, 422 visible = function() 423 return core.loaderTooOld() 424 end 425 }, 426 vendor = { 427 entry_type = core.MENU_ENTRY, 428 visible = function() 429 return false 430 end 431 }, 432 }, 433} 434 435menu.default = menu.welcome 436-- current_alias_table will be used to keep our alias table consistent across 437-- screen redraws, instead of relying on whatever triggered the redraw to update 438-- the local alias_table in menu.process. 439menu.current_alias_table = {} 440 441function menu.draw(menudef) 442 -- Clear the screen, reset the cursor, then draw 443 screen.clear() 444 menu.current_alias_table = drawer.drawscreen(menudef) 445 drawn_menu = menudef 446 screen.defcursor() 447end 448 449-- 'keypress' allows the caller to indicate that a key has been pressed that we 450-- should process as our initial input. 451function menu.process(menudef, keypress) 452 assert(menudef ~= nil) 453 454 if drawn_menu ~= menudef then 455 menu.draw(menudef) 456 end 457 458 while true do 459 local key = keypress or io.getchar() 460 keypress = nil 461 462 -- Special key behaviors 463 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and 464 menudef ~= menu.default then 465 break 466 elseif key == core.KEY_ENTER then 467 core.boot() 468 -- Should not return. If it does, escape menu handling 469 -- and drop to loader prompt. 470 return false 471 end 472 473 key = string.char(key) 474 -- check to see if key is an alias 475 local sel_entry = nil 476 for k, v in pairs(menu.current_alias_table) do 477 if key == k then 478 sel_entry = v 479 break 480 end 481 end 482 483 -- if we have an alias do the assigned action: 484 if sel_entry ~= nil then 485 local handler = menu.handlers[sel_entry.entry_type] 486 assert(handler ~= nil) 487 -- The handler's return value indicates if we 488 -- need to exit this menu. An omitted or true 489 -- return value means to continue. 490 if handler(menudef, sel_entry) == false then 491 return 492 end 493 -- If we got an alias key the screen is out of date... 494 -- redraw it. 495 menu.draw(menudef) 496 end 497 end 498end 499 500function menu.run() 501 local autoboot_key 502 local delay = loader.getenv("autoboot_delay") 503 504 if delay ~= nil and delay:lower() == "no" then 505 delay = nil 506 else 507 delay = tonumber(delay) or 10 508 end 509 510 if delay == -1 then 511 core.boot() 512 return 513 end 514 515 menu.draw(menu.default) 516 517 if delay ~= nil then 518 autoboot_key = menu.autoboot(delay) 519 520 -- autoboot_key should return the key pressed. It will only 521 -- return nil if we hit the timeout and executed the timeout 522 -- command. Bail out. 523 if autoboot_key == nil then 524 return 525 end 526 end 527 528 menu.process(menu.default, autoboot_key) 529 drawn_menu = nil 530 531 screen.defcursor() 532 -- We explicitly want the newline print adds 533 print("Exiting menu!") 534end 535 536function menu.autoboot(delay) 537 local x = loader.getenv("loader_menu_timeout_x") or 4 538 local y = loader.getenv("loader_menu_timeout_y") or 24 539 local autoboot_show = loader.getenv("loader_autoboot_show") or "yes" 540 local endtime = loader.time() + delay 541 local time 542 local last 543 repeat 544 time = endtime - loader.time() 545 if last == nil or last ~= time then 546 last = time 547 if autoboot_show == "yes" then 548 screen.setcursor(x, y) 549 printc("Autoboot in " .. time .. 550 " seconds. [Space] to pause ") 551 screen.defcursor() 552 end 553 end 554 if io.ischar() then 555 local ch = io.getchar() 556 if ch == core.KEY_ENTER then 557 break 558 else 559 -- Erase autoboot msg. While real VT100s 560 -- wouldn't scroll when receiving a char with 561 -- the cursor at (79, 24), bad emulators do. 562 -- Avoid the issue by stopping at 79. 563 screen.setcursor(1, y) 564 printc(string.rep(" ", 79)) 565 screen.defcursor() 566 return ch 567 end 568 end 569 570 loader.delay(50000) 571 until time <= 0 572 573 local cmd = loader.getenv("menu_timeout_command") or "boot" 574 cli_execute_unparsed(cmd) 575 return nil 576end 577 578-- CLI commands 579function cli.menu() 580 menu.run() 581end 582 583return menu 584