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.escapefg(color.WHITE) 51 else 52 return str .. color.escapefg(color.RED) .. "off" .. 53 color.escapefg(color.WHITE) 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 -- 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 220 end 221 -- Shallow copy the table 222 menu_entries = core.deepCopyTable(menu_entries) 223 224 -- Swap the first two menu entries 225 menu_entries[1], menu_entries[2] = 226 menu_entries[2], menu_entries[1] 227 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 233 end 234 return menu_entries 235 end, 236 all_entries = { 237 -- boot multi user 238 { 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") .. 244 "oot Multi user", 245 func = function() 246 core.setSingleUser(false) 247 core.boot() 248 end, 249 alias = {"b", "B"}, 250 }, 251 -- boot single user 252 { 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]"), 258 func = function() 259 core.setSingleUser(true) 260 core.boot() 261 end, 262 alias = {"s", "S"}, 263 }, 264 -- escape to interpreter 265 { 266 entry_type = core.MENU_RETURN, 267 name = color.highlight("Esc") .. "ape to loader prompt", 268 func = function() 269 loader.setenv("autoboot_delay", "NO") 270 end, 271 alias = {core.KEYSTR_ESCAPE}, 272 }, 273 -- reboot 274 { 275 entry_type = core.MENU_ENTRY, 276 name = color.highlight("R") .. "eboot", 277 func = function() 278 loader.perform("reboot") 279 end, 280 alias = {"r", "R"}, 281 }, 282 { 283 entry_type = core.MENU_SEPARATOR, 284 }, 285 { 286 entry_type = core.MENU_SEPARATOR, 287 name = "Options:", 288 }, 289 -- kernel options 290 { 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 296 return "Kernel: " 297 end 298 299 local is_default = (idx == 1) 300 local kernel_name = "" 301 local name_color 302 if is_default then 303 name_color = color.escapefg(color.GREEN) 304 kernel_name = "default/" 305 else 306 name_color = color.escapefg(color.BLUE) 307 end 308 kernel_name = kernel_name .. name_color .. 309 choice .. color.resetfg() 310 return color.highlight("K") .. "ernel: " .. 311 kernel_name .. " (" .. idx .. " of " .. 312 #all_choices .. ")" 313 end, 314 func = function(_, choice, _) 315 if loader.getenv("kernelname") ~= nil then 316 loader.perform("unload") 317 end 318 config.selectKernel(choice) 319 end, 320 alias = {"k", "K"}, 321 }, 322 -- boot options 323 { 324 entry_type = core.MENU_SUBMENU, 325 name = "Boot " .. color.highlight("O") .. "ptions", 326 submenu = menu.boot_options, 327 alias = {"o", "O"}, 328 }, 329 -- boot environments 330 { 331 entry_type = core.MENU_SUBMENU, 332 visible = function() 333 return core.isZFSBoot() and 334 #core.bootenvList() > 1 335 end, 336 name = "Boot " .. color.highlight("E") .. "nvironments", 337 submenu = menu.boot_environments, 338 alias = {"e", "E"}, 339 }, 340 -- chainload 341 { 342 entry_type = core.MENU_ENTRY, 343 name = function() 344 return 'Chain' .. color.highlight("L") .. 345 "oad " .. loader.getenv('chain_disk') 346 end, 347 func = function() 348 loader.perform("chain " .. 349 loader.getenv('chain_disk')) 350 end, 351 visible = function() 352 return loader.getenv('chain_disk') ~= nil 353 end, 354 alias = {"l", "L"}, 355 }, 356 }, 357} 358 359menu.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. 363menu.current_alias_table = {} 364 365function menu.draw(menudef) 366 -- Clear the screen, reset the cursor, then draw 367 screen.clear() 368 menu.current_alias_table = drawer.drawscreen(menudef) 369 drawn_menu = menudef 370 screen.defcursor() 371end 372 373-- 'keypress' allows the caller to indicate that a key has been pressed that we 374-- should process as our initial input. 375function menu.process(menudef, keypress) 376 assert(menudef ~= nil) 377 378 if drawn_menu ~= menudef then 379 menu.draw(menudef) 380 end 381 382 while true do 383 local key = keypress or io.getchar() 384 keypress = nil 385 386 -- Special key behaviors 387 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and 388 menudef ~= menu.default then 389 break 390 elseif key == core.KEY_ENTER then 391 core.boot() 392 -- Should not return. If it does, escape menu handling 393 -- and drop to loader prompt. 394 return false 395 end 396 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 401 if key == k then 402 sel_entry = v 403 break 404 end 405 end 406 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 415 return 416 end 417 -- If we got an alias key the screen is out of date... 418 -- redraw it. 419 menu.draw(menudef) 420 end 421 end 422end 423 424function menu.run() 425 local autoboot_key 426 local delay = loader.getenv("autoboot_delay") 427 428 if delay ~= nil and delay:lower() == "no" then 429 delay = nil 430 else 431 delay = tonumber(delay) or 10 432 end 433 434 if delay == -1 then 435 core.boot() 436 return 437 end 438 439 menu.draw(menu.default) 440 441 if delay ~= nil then 442 autoboot_key = menu.autoboot(delay) 443 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 448 return 449 end 450 end 451 452 menu.process(menu.default, autoboot_key) 453 drawn_menu = nil 454 455 screen.defcursor() 456 print("Exiting menu!") 457end 458 459function 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 463 local time 464 local last 465 repeat 466 time = endtime - loader.time() 467 if last == nil or last ~= time then 468 last = time 469 screen.setcursor(x, y) 470 print("Autoboot in " .. time .. 471 " seconds, hit [Enter] to boot" .. 472 " or any other key to stop ") 473 screen.defcursor() 474 end 475 if io.ischar() then 476 local ch = io.getchar() 477 if ch == core.KEY_ENTER then 478 break 479 else 480 -- erase autoboot msg 481 screen.setcursor(0, y) 482 print(string.rep(" ", 80)) 483 screen.defcursor() 484 return ch 485 end 486 end 487 488 loader.delay(50000) 489 until time <= 0 490 491 local cmd = loader.getenv("menu_timeout_command") or "boot" 492 cli_execute_unparsed(cmd) 493 return nil 494end 495 496-- CLI commands 497function cli.menu(...) 498 menu.run() 499end 500 501return menu 502