1-- 2-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD 3-- 4-- Copyright (c) 2015 Pedro Souza <[email protected]> 5-- Copyright (C) 2018 Kyle Evans <[email protected]> 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 32 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 screen_invalid = true 42 43local function OnOff(str, b) 44 if b then 45 return str .. color.escapef(color.GREEN) .. "On" .. 46 color.escapef(color.WHITE) 47 else 48 return str .. color.escapef(color.RED) .. "off" .. 49 color.escapef(color.WHITE) 50 end 51end 52 53local function bootenvSet(env) 54 loader.setenv("vfs.root.mountfrom", env) 55 loader.setenv("currdev", env .. ":") 56 config.reload() 57end 58 59-- Module exports 60menu.handlers = { 61 -- Menu handlers take the current menu and selected entry as parameters, 62 -- and should return a boolean indicating whether execution should 63 -- continue or not. The return value may be omitted if this entry should 64 -- have no bearing on whether we continue or not, indicating that we 65 -- should just continue after execution. 66 [core.MENU_ENTRY] = function(_, entry) 67 -- run function 68 entry.func() 69 end, 70 [core.MENU_CAROUSEL_ENTRY] = function(_, entry) 71 -- carousel (rotating) functionality 72 local carid = entry.carousel_id 73 local caridx = config.getCarouselIndex(carid) 74 local choices = entry.items 75 if type(choices) == "function" then 76 choices = choices() 77 end 78 if #choices > 0 then 79 caridx = (caridx % #choices) + 1 80 config.setCarouselIndex(carid, caridx) 81 entry.func(caridx, choices[caridx], choices) 82 end 83 end, 84 [core.MENU_SUBMENU] = function(_, entry) 85 screen_invalid = true 86 menu.process(entry.submenu) 87 end, 88 [core.MENU_RETURN] = function(_, entry) 89 -- allow entry to have a function/side effect 90 if entry.func ~= nil then 91 entry.func() 92 end 93 return false 94 end, 95} 96-- loader menu tree is rooted at menu.welcome 97 98menu.boot_environments = { 99 entries = { 100 -- return to welcome menu 101 { 102 entry_type = core.MENU_RETURN, 103 name = "Back to main menu" .. 104 color.highlight(" [Backspace]"), 105 }, 106 { 107 entry_type = core.MENU_CAROUSEL_ENTRY, 108 carousel_id = "be_active", 109 items = core.bootenvList, 110 name = function(idx, choice, all_choices) 111 if #all_choices == 0 then 112 return "Active: " 113 end 114 115 local is_default = (idx == 1) 116 local bootenv_name = "" 117 local name_color 118 if is_default then 119 name_color = color.escapef(color.GREEN) 120 else 121 name_color = color.escapef(color.BLUE) 122 end 123 bootenv_name = bootenv_name .. name_color .. 124 choice .. color.default() 125 return color.highlight("A").."ctive: " .. 126 bootenv_name .. " (" .. idx .. " of " .. 127 #all_choices .. ")" 128 end, 129 func = function(_, choice, _) 130 bootenvSet(choice) 131 end, 132 alias = {"a", "A"}, 133 }, 134 { 135 entry_type = core.MENU_ENTRY, 136 name = function() 137 return color.highlight("b") .. "ootfs: " .. 138 core.bootenvDefault() 139 end, 140 func = function() 141 -- Reset active boot environment to the default 142 config.setCarouselIndex("be_active", 1) 143 bootenvSet(core.bootenvDefault()) 144 end, 145 alias = {"b", "B"}, 146 }, 147 }, 148} 149 150menu.boot_options = { 151 entries = { 152 -- return to welcome menu 153 { 154 entry_type = core.MENU_RETURN, 155 name = "Back to main menu" .. 156 color.highlight(" [Backspace]"), 157 }, 158 -- load defaults 159 { 160 entry_type = core.MENU_ENTRY, 161 name = "Load System " .. color.highlight("D") .. 162 "efaults", 163 func = core.setDefaults, 164 alias = {"d", "D"}, 165 }, 166 { 167 entry_type = core.MENU_SEPARATOR, 168 }, 169 { 170 entry_type = core.MENU_SEPARATOR, 171 name = "Boot Options:", 172 }, 173 -- acpi 174 { 175 entry_type = core.MENU_ENTRY, 176 visible = core.isSystem386, 177 name = function() 178 return OnOff(color.highlight("A") .. 179 "CPI :", core.acpi) 180 end, 181 func = core.setACPI, 182 alias = {"a", "A"}, 183 }, 184 -- safe mode 185 { 186 entry_type = core.MENU_ENTRY, 187 name = function() 188 return OnOff("Safe " .. color.highlight("M") .. 189 "ode :", core.sm) 190 end, 191 func = core.setSafeMode, 192 alias = {"m", "M"}, 193 }, 194 -- single user 195 { 196 entry_type = core.MENU_ENTRY, 197 name = function() 198 return OnOff(color.highlight("S") .. 199 "ingle user:", core.su) 200 end, 201 func = core.setSingleUser, 202 alias = {"s", "S"}, 203 }, 204 -- verbose boot 205 { 206 entry_type = core.MENU_ENTRY, 207 name = function() 208 return OnOff(color.highlight("V") .. 209 "erbose :", core.verbose) 210 end, 211 func = core.setVerbose, 212 alias = {"v", "V"}, 213 }, 214 }, 215} 216 217menu.welcome = { 218 entries = function() 219 local menu_entries = menu.welcome.all_entries 220 -- Swap the first two menu items on single user boot 221 if core.isSingleUserBoot() then 222 -- We'll cache the swapped menu, for performance 223 if menu.welcome.swapped_menu ~= nil then 224 return menu.welcome.swapped_menu 225 end 226 -- Shallow copy the table 227 menu_entries = core.deepCopyTable(menu_entries) 228 229 -- Swap the first two menu entries 230 menu_entries[1], menu_entries[2] = 231 menu_entries[2], menu_entries[1] 232 233 -- Then set their names to their alternate names 234 menu_entries[1].name, menu_entries[2].name = 235 menu_entries[1].alternate_name, 236 menu_entries[2].alternate_name 237 menu.welcome.swapped_menu = menu_entries 238 end 239 return menu_entries 240 end, 241 all_entries = { 242 -- boot multi user 243 { 244 entry_type = core.MENU_ENTRY, 245 name = color.highlight("B") .. "oot Multi user " .. 246 color.highlight("[Enter]"), 247 -- Not a standard menu entry function! 248 alternate_name = color.highlight("B") .. 249 "oot Multi user", 250 func = function() 251 core.setSingleUser(false) 252 core.boot() 253 end, 254 alias = {"b", "B"}, 255 }, 256 -- boot single user 257 { 258 entry_type = core.MENU_ENTRY, 259 name = "Boot " .. color.highlight("S") .. "ingle user", 260 -- Not a standard menu entry function! 261 alternate_name = "Boot " .. color.highlight("S") .. 262 "ingle user " .. color.highlight("[Enter]"), 263 func = function() 264 core.setSingleUser(true) 265 core.boot() 266 end, 267 alias = {"s", "S"}, 268 }, 269 -- escape to interpreter 270 { 271 entry_type = core.MENU_RETURN, 272 name = color.highlight("Esc") .. "ape to loader prompt", 273 func = function() 274 loader.setenv("autoboot_delay", "NO") 275 end, 276 alias = {core.KEYSTR_ESCAPE}, 277 }, 278 -- reboot 279 { 280 entry_type = core.MENU_ENTRY, 281 name = color.highlight("R") .. "eboot", 282 func = function() 283 loader.perform("reboot") 284 end, 285 alias = {"r", "R"}, 286 }, 287 { 288 entry_type = core.MENU_SEPARATOR, 289 }, 290 { 291 entry_type = core.MENU_SEPARATOR, 292 name = "Options:", 293 }, 294 -- kernel options 295 { 296 entry_type = core.MENU_CAROUSEL_ENTRY, 297 carousel_id = "kernel", 298 items = core.kernelList, 299 name = function(idx, choice, all_choices) 300 if #all_choices == 0 then 301 return "Kernel: " 302 end 303 304 local is_default = (idx == 1) 305 local kernel_name = "" 306 local name_color 307 if is_default then 308 name_color = color.escapef(color.GREEN) 309 kernel_name = "default/" 310 else 311 name_color = color.escapef(color.BLUE) 312 end 313 kernel_name = kernel_name .. name_color .. 314 choice .. color.default() 315 return color.highlight("K") .. "ernel: " .. 316 kernel_name .. " (" .. idx .. " of " .. 317 #all_choices .. ")" 318 end, 319 func = function(_, choice, _) 320 config.selectKernel(choice) 321 end, 322 alias = {"k", "K"}, 323 }, 324 -- boot options 325 { 326 entry_type = core.MENU_SUBMENU, 327 name = "Boot " .. color.highlight("O") .. "ptions", 328 submenu = menu.boot_options, 329 alias = {"o", "O"}, 330 }, 331 -- boot environments 332 { 333 entry_type = core.MENU_SUBMENU, 334 visible = function() 335 return core.isZFSBoot() and 336 #core.bootenvList() > 1 337 end, 338 name = "Boot " .. color.highlight("E") .. "nvironments", 339 submenu = menu.boot_environments, 340 alias = {"e", "E"}, 341 }, 342 }, 343} 344 345menu.default = menu.welcome 346-- current_alias_table will be used to keep our alias table consistent across 347-- screen redraws, instead of relying on whatever triggered the redraw to update 348-- the local alias_table in menu.process. 349menu.current_alias_table = {} 350 351function menu.redraw(m) 352 -- redraw screen 353 screen.clear() 354 screen.defcursor() 355 menu.current_alias_table = drawer.drawscreen(m) 356 screen_invalid = false 357end 358 359-- 'keypress' allows the caller to indicate that a key has been pressed that we 360-- should process as our initial input. 361function menu.process(m, keypress) 362 assert(m ~= nil) 363 364 -- Trigger a redraw if we've been invalidated. Otherwise, we assume 365 -- that this menu has already been drawn. 366 if screen_invalid then 367 menu.redraw(m) 368 end 369 370 while true do 371 local key = keypress or io.getchar() 372 keypress = nil 373 374 -- Special key behaviors 375 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and 376 m ~= menu.default then 377 break 378 elseif key == core.KEY_ENTER then 379 core.boot() 380 -- Should not return 381 end 382 383 key = string.char(key) 384 -- check to see if key is an alias 385 local sel_entry = nil 386 for k, v in pairs(menu.current_alias_table) do 387 if key == k then 388 sel_entry = v 389 break 390 end 391 end 392 393 -- if we have an alias do the assigned action: 394 if sel_entry ~= nil then 395 -- Get menu handler 396 local handler = menu.handlers[sel_entry.entry_type] 397 if handler ~= nil then 398 -- The handler's return value indicates if we 399 -- need to exit this menu. An omitted or true 400 -- return value means to continue. 401 if handler(m, sel_entry) == false then 402 return 403 end 404 end 405 -- If we got an alias key the screen is out of date... 406 -- redraw it. 407 menu.redraw(m) 408 end 409 end 410end 411 412function menu.run() 413 if menu.skip() then 414 core.autoboot() 415 return 416 end 417 418 menu.redraw(menu.default) 419 local autoboot_key = menu.autoboot() 420 421 menu.process(menu.default, autoboot_key) 422 423 screen.defcursor() 424 print("Exiting menu!") 425end 426 427function menu.skip() 428 if core.isSerialBoot() then 429 return true 430 end 431 local c = string.lower(loader.getenv("console") or "") 432 if c:match("^efi[ ;]") ~= nil or c:match("[ ;]efi[ ;]") ~= nil then 433 return true 434 end 435 436 c = string.lower(loader.getenv("beastie_disable") or "") 437 print("beastie_disable", c) 438 return c == "yes" 439end 440 441function menu.autoboot() 442 local ab = loader.getenv("autoboot_delay") 443 if ab ~= nil and ab:lower() == "no" then 444 return nil 445 elseif tonumber(ab) == -1 then 446 core.boot() 447 end 448 ab = tonumber(ab) or 10 449 450 local x = loader.getenv("loader_menu_timeout_x") or 5 451 local y = loader.getenv("loader_menu_timeout_y") or 22 452 453 local endtime = loader.time() + ab 454 local time 455 456 repeat 457 time = endtime - loader.time() 458 screen.setcursor(x, y) 459 print("Autoboot in " .. time .. 460 " seconds, hit [Enter] to boot" .. 461 " or any other key to stop ") 462 screen.defcursor() 463 if io.ischar() then 464 local ch = io.getchar() 465 if ch == core.KEY_ENTER then 466 break 467 else 468 -- erase autoboot msg 469 screen.setcursor(0, y) 470 print(string.rep(" ", 80)) 471 screen.defcursor() 472 return ch 473 end 474 end 475 476 loader.delay(50000) 477 until time <= 0 478 core.boot() 479 480end 481 482return menu 483