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: stable/12/stand/lua/menu.lua 370544 2021-09-12 05:44:02Z kevans $ 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 menu_entries.console, 245 { 246 entry_type = core.MENU_SEPARATOR, 247 }, 248 { 249 entry_type = core.MENU_SEPARATOR, 250 name = "Options:", 251 }, 252 menu_entries.kernel_options, 253 menu_entries.boot_options, 254 menu_entries.boot_envs, 255 menu_entries.chainload, 256 } 257 end, 258 all_entries = { 259 multi_user = { 260 entry_type = core.MENU_ENTRY, 261 name = color.highlight("B") .. "oot Multi user " .. 262 color.highlight("[Enter]"), 263 -- Not a standard menu entry function! 264 alternate_name = color.highlight("B") .. 265 "oot Multi user", 266 func = function() 267 core.setSingleUser(false) 268 core.boot() 269 end, 270 alias = {"b", "B"}, 271 }, 272 single_user = { 273 entry_type = core.MENU_ENTRY, 274 name = "Boot " .. color.highlight("S") .. "ingle user", 275 -- Not a standard menu entry function! 276 alternate_name = "Boot " .. color.highlight("S") .. 277 "ingle user " .. color.highlight("[Enter]"), 278 func = function() 279 core.setSingleUser(true) 280 core.boot() 281 end, 282 alias = {"s", "S"}, 283 }, 284 console = { 285 entry_type = core.MENU_ENTRY, 286 name = function() 287 return color.highlight("C") .. "ons: " .. core.getConsoleName() 288 end, 289 func = function() 290 core.nextConsoleChoice() 291 end, 292 alias = {"c", "C"}, 293 }, 294 prompt = { 295 entry_type = core.MENU_RETURN, 296 name = color.highlight("Esc") .. "ape to loader prompt", 297 func = function() 298 loader.setenv("autoboot_delay", "NO") 299 end, 300 alias = {core.KEYSTR_ESCAPE}, 301 }, 302 reboot = { 303 entry_type = core.MENU_ENTRY, 304 name = color.highlight("R") .. "eboot", 305 func = function() 306 loader.perform("reboot") 307 end, 308 alias = {"r", "R"}, 309 }, 310 kernel_options = { 311 entry_type = core.MENU_CAROUSEL_ENTRY, 312 carousel_id = "kernel", 313 items = core.kernelList, 314 name = function(idx, choice, all_choices) 315 if #all_choices == 0 then 316 return "Kernel: " 317 end 318 319 local is_default = (idx == 1) 320 local kernel_name = "" 321 local name_color 322 if is_default then 323 name_color = color.escapefg(color.GREEN) 324 kernel_name = "default/" 325 else 326 name_color = color.escapefg(color.BLUE) 327 end 328 kernel_name = kernel_name .. name_color .. 329 choice .. color.resetfg() 330 return color.highlight("K") .. "ernel: " .. 331 kernel_name .. " (" .. idx .. " of " .. 332 #all_choices .. ")" 333 end, 334 func = function(_, choice, _) 335 if loader.getenv("kernelname") ~= nil then 336 loader.perform("unload") 337 end 338 config.selectKernel(choice) 339 end, 340 alias = {"k", "K"}, 341 }, 342 boot_options = { 343 entry_type = core.MENU_SUBMENU, 344 name = "Boot " .. color.highlight("O") .. "ptions", 345 submenu = menu.boot_options, 346 alias = {"o", "O"}, 347 }, 348 boot_envs = { 349 entry_type = core.MENU_SUBMENU, 350 visible = function() 351 return core.isZFSBoot() and 352 #core.bootenvList() > 1 353 end, 354 name = "Boot " .. color.highlight("E") .. "nvironments", 355 submenu = menu.boot_environments, 356 alias = {"e", "E"}, 357 }, 358 chainload = { 359 entry_type = core.MENU_ENTRY, 360 name = function() 361 return 'Chain' .. color.highlight("L") .. 362 "oad " .. loader.getenv('chain_disk') 363 end, 364 func = function() 365 loader.perform("chain " .. 366 loader.getenv('chain_disk')) 367 end, 368 visible = function() 369 return loader.getenv('chain_disk') ~= nil 370 end, 371 alias = {"l", "L"}, 372 }, 373 }, 374} 375 376menu.default = menu.welcome 377-- current_alias_table will be used to keep our alias table consistent across 378-- screen redraws, instead of relying on whatever triggered the redraw to update 379-- the local alias_table in menu.process. 380menu.current_alias_table = {} 381 382function menu.draw(menudef) 383 -- Clear the screen, reset the cursor, then draw 384 screen.clear() 385 menu.current_alias_table = drawer.drawscreen(menudef) 386 drawn_menu = menudef 387 screen.defcursor() 388end 389 390-- 'keypress' allows the caller to indicate that a key has been pressed that we 391-- should process as our initial input. 392function menu.process(menudef, keypress) 393 assert(menudef ~= nil) 394 395 if drawn_menu ~= menudef then 396 menu.draw(menudef) 397 end 398 399 while true do 400 local key = keypress or io.getchar() 401 keypress = nil 402 403 -- Special key behaviors 404 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and 405 menudef ~= menu.default then 406 break 407 elseif key == core.KEY_ENTER then 408 core.boot() 409 -- Should not return. If it does, escape menu handling 410 -- and drop to loader prompt. 411 return false 412 end 413 414 key = string.char(key) 415 -- check to see if key is an alias 416 local sel_entry = nil 417 for k, v in pairs(menu.current_alias_table) do 418 if key == k then 419 sel_entry = v 420 break 421 end 422 end 423 424 -- if we have an alias do the assigned action: 425 if sel_entry ~= nil then 426 local handler = menu.handlers[sel_entry.entry_type] 427 assert(handler ~= nil) 428 -- The handler's return value indicates if we 429 -- need to exit this menu. An omitted or true 430 -- return value means to continue. 431 if handler(menudef, sel_entry) == false then 432 return 433 end 434 -- If we got an alias key the screen is out of date... 435 -- redraw it. 436 menu.draw(menudef) 437 end 438 end 439end 440 441function menu.run() 442 local autoboot_key 443 local delay = loader.getenv("autoboot_delay") 444 445 if delay ~= nil and delay:lower() == "no" then 446 delay = nil 447 else 448 delay = tonumber(delay) or 10 449 end 450 451 if delay == -1 then 452 core.boot() 453 return 454 end 455 456 menu.draw(menu.default) 457 458 if delay ~= nil then 459 autoboot_key = menu.autoboot(delay) 460 461 -- autoboot_key should return the key pressed. It will only 462 -- return nil if we hit the timeout and executed the timeout 463 -- command. Bail out. 464 if autoboot_key == nil then 465 return 466 end 467 end 468 469 menu.process(menu.default, autoboot_key) 470 drawn_menu = nil 471 472 screen.defcursor() 473 print("Exiting menu!") 474end 475 476function menu.autoboot(delay) 477 local x = loader.getenv("loader_menu_timeout_x") or 4 478 local y = loader.getenv("loader_menu_timeout_y") or 23 479 local endtime = loader.time() + delay 480 local time 481 local last 482 repeat 483 time = endtime - loader.time() 484 if last == nil or last ~= time then 485 last = time 486 screen.setcursor(x, y) 487 print("Autoboot in " .. time .. 488 " seconds. [Space] to pause") 489 screen.defcursor() 490 end 491 if io.ischar() then 492 local ch = io.getchar() 493 if ch == core.KEY_ENTER then 494 break 495 else 496 -- erase autoboot msg 497 screen.setcursor(0, y) 498 print(string.rep(" ", 80)) 499 screen.defcursor() 500 return ch 501 end 502 end 503 504 loader.delay(50000) 505 until time <= 0 506 507 local cmd = loader.getenv("menu_timeout_command") or "boot" 508 cli_execute_unparsed(cmd) 509 return nil 510end 511 512-- CLI commands 513function cli.menu() 514 menu.run() 515end 516 517return menu 518