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 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 function OnOff(str, b) 42 if b then 43 return str .. color.escapef(color.GREEN) .. "On" .. 44 color.escapef(color.WHITE) 45 else 46 return str .. color.escapef(color.RED) .. "off" .. 47 color.escapef(color.WHITE) 48 end 49end 50 51local function bootenvSet(env) 52 loader.setenv("vfs.root.mountfrom", env) 53 loader.setenv("currdev", env .. ":") 54 config.reload() 55end 56 57-- Module exports 58menu.handlers = { 59 -- Menu handlers take the current menu and selected entry as parameters, 60 -- and should return a boolean indicating whether execution should 61 -- continue or not. The return value may be omitted if this entry should 62 -- have no bearing on whether we continue or not, indicating that we 63 -- should just continue after execution. 64 [core.MENU_ENTRY] = function(_, entry) 65 -- run function 66 entry.func() 67 end, 68 [core.MENU_CAROUSEL_ENTRY] = function(_, entry) 69 -- carousel (rotating) functionality 70 local carid = entry.carousel_id 71 local caridx = config.getCarouselIndex(carid) 72 local choices = entry.items 73 if type(choices) == "function" then 74 choices = choices() 75 end 76 if #choices > 0 then 77 caridx = (caridx % #choices) + 1 78 config.setCarouselIndex(carid, caridx) 79 entry.func(caridx, choices[caridx], choices) 80 end 81 end, 82 [core.MENU_SUBMENU] = function(_, entry) 83 -- recurse 84 return menu.run(entry.submenu) 85 end, 86 [core.MENU_RETURN] = function(_, entry) 87 -- allow entry to have a function/side effect 88 if entry.func ~= nil then 89 entry.func() 90 end 91 return false 92 end, 93} 94-- loader menu tree is rooted at menu.welcome 95 96menu.boot_environments = { 97 entries = { 98 -- return to welcome menu 99 { 100 entry_type = core.MENU_RETURN, 101 name = "Back to main menu" .. 102 color.highlight(" [Backspace]"), 103 }, 104 { 105 entry_type = core.MENU_CAROUSEL_ENTRY, 106 carousel_id = "be_active", 107 items = core.bootenvList, 108 name = function(idx, choice, all_choices) 109 if #all_choices == 0 then 110 return "Active: " 111 end 112 113 local is_default = (idx == 1) 114 local bootenv_name = "" 115 local name_color 116 if is_default then 117 name_color = color.escapef(color.GREEN) 118 else 119 name_color = color.escapef(color.BLUE) 120 end 121 bootenv_name = bootenv_name .. name_color .. 122 choice .. color.default() 123 return color.highlight("A").."ctive: " .. 124 bootenv_name .. " (" .. idx .. " of " .. 125 #all_choices .. ")" 126 end, 127 func = function(_, choice, _) 128 bootenvSet(choice) 129 end, 130 alias = {"a", "A"}, 131 }, 132 { 133 entry_type = core.MENU_ENTRY, 134 name = function() 135 return color.highlight("b") .. "ootfs: " .. 136 core.bootenvDefault() 137 end, 138 func = function() 139 -- Reset active boot environment to the default 140 config.setCarouselIndex("be_active", 1) 141 bootenvSet(core.bootenvDefault()) 142 end, 143 alias = {"b", "B"}, 144 }, 145 }, 146} 147 148menu.boot_options = { 149 entries = { 150 -- return to welcome menu 151 { 152 entry_type = core.MENU_RETURN, 153 name = "Back to main menu" .. 154 color.highlight(" [Backspace]"), 155 }, 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.isSystem386, 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 -- Swap the first two menu items on single user boot 219 if core.isSingleUserBoot() then 220 -- We'll cache the swapped menu, for performance 221 if menu.welcome.swapped_menu ~= nil then 222 return menu.welcome.swapped_menu 223 end 224 -- Shallow copy the table 225 menu_entries = core.deepCopyTable(menu_entries) 226 227 -- Swap the first two menu entries 228 menu_entries[1], menu_entries[2] = 229 menu_entries[2], menu_entries[1] 230 231 -- Then set their names to their alternate names 232 menu_entries[1].name, menu_entries[2].name = 233 menu_entries[1].alternate_name, 234 menu_entries[2].alternate_name 235 menu.welcome.swapped_menu = menu_entries 236 end 237 return menu_entries 238 end, 239 all_entries = { 240 -- boot multi user 241 { 242 entry_type = core.MENU_ENTRY, 243 name = color.highlight("B") .. "oot Multi user " .. 244 color.highlight("[Enter]"), 245 -- Not a standard menu entry function! 246 alternate_name = color.highlight("B") .. 247 "oot Multi user", 248 func = function() 249 core.setSingleUser(false) 250 core.boot() 251 end, 252 alias = {"b", "B"} 253 }, 254 -- boot single user 255 { 256 entry_type = core.MENU_ENTRY, 257 name = "Boot " .. color.highlight("S") .. "ingle user", 258 -- Not a standard menu entry function! 259 alternate_name = "Boot " .. color.highlight("S") .. 260 "ingle user " .. color.highlight("[Enter]"), 261 func = function() 262 core.setSingleUser(true) 263 core.boot() 264 end, 265 alias = {"s", "S"} 266 }, 267 -- escape to interpreter 268 { 269 entry_type = core.MENU_RETURN, 270 name = color.highlight("Esc") .. "ape to loader prompt", 271 func = function() 272 loader.setenv("autoboot_delay", "NO") 273 end, 274 alias = {core.KEYSTR_ESCAPE} 275 }, 276 -- reboot 277 { 278 entry_type = core.MENU_ENTRY, 279 name = color.highlight("R") .. "eboot", 280 func = function() 281 loader.perform("reboot") 282 end, 283 alias = {"r", "R"} 284 }, 285 { 286 entry_type = core.MENU_SEPARATOR, 287 }, 288 { 289 entry_type = core.MENU_SEPARATOR, 290 name = "Options:", 291 }, 292 -- kernel options 293 { 294 entry_type = core.MENU_CAROUSEL_ENTRY, 295 carousel_id = "kernel", 296 items = core.kernelList, 297 name = function(idx, choice, all_choices) 298 if #all_choices == 0 then 299 return "Kernel: " 300 end 301 302 local is_default = (idx == 1) 303 local kernel_name = "" 304 local name_color 305 if is_default then 306 name_color = color.escapef(color.GREEN) 307 kernel_name = "default/" 308 else 309 name_color = color.escapef(color.BLUE) 310 end 311 kernel_name = kernel_name .. name_color .. 312 choice .. color.default() 313 return color.highlight("K") .. "ernel: " .. 314 kernel_name .. " (" .. idx .. " of " .. 315 #all_choices .. ")" 316 end, 317 func = function(_, choice, _) 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 }, 341} 342 343menu.default = menu.welcome 344 345function menu.run(m) 346 347 if menu.skip() then 348 core.autoboot() 349 return false 350 end 351 352 if m == nil then 353 m = menu.default 354 end 355 356 -- redraw screen 357 screen.clear() 358 screen.defcursor() 359 local alias_table = drawer.drawscreen(m) 360 361 -- Might return nil, that's ok 362 local autoboot_key; 363 if m == menu.default then 364 autoboot_key = menu.autoboot() 365 end 366 local cont = true 367 while cont do 368 local key = autoboot_key or io.getchar() 369 autoboot_key = nil 370 371 -- Special key behaviors 372 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and 373 m ~= menu.default then 374 break 375 elseif key == core.KEY_ENTER then 376 core.boot() 377 -- Should not return 378 end 379 380 key = string.char(key) 381 -- check to see if key is an alias 382 local sel_entry = nil 383 for k, v in pairs(alias_table) do 384 if key == k then 385 sel_entry = v 386 end 387 end 388 389 -- if we have an alias do the assigned action: 390 if sel_entry ~= nil then 391 -- Get menu handler 392 local handler = menu.handlers[sel_entry.entry_type] 393 if handler ~= nil then 394 -- The handler's return value indicates whether 395 -- we need to exit this menu. An omitted return 396 -- value means "continue" by default. 397 cont = handler(m, sel_entry) 398 if cont == nil then 399 cont = true 400 end 401 end 402 -- if we got an alias key the screen is out of date: 403 screen.clear() 404 screen.defcursor() 405 alias_table = drawer.drawscreen(m) 406 end 407 end 408 409 if m == menu.default then 410 screen.defcursor() 411 print("Exiting menu!") 412 return false 413 end 414 415 return true 416end 417 418function menu.skip() 419 if core.isSerialBoot() then 420 return true 421 end 422 local c = string.lower(loader.getenv("console") or "") 423 if c:match("^efi[ ;]") ~= nil or c:match("[ ;]efi[ ;]") ~= nil then 424 return true 425 end 426 427 c = string.lower(loader.getenv("beastie_disable") or "") 428 print("beastie_disable", c) 429 return c == "yes" 430end 431 432function menu.autoboot() 433 local ab = loader.getenv("autoboot_delay") 434 if ab ~= nil and ab:lower() == "no" then 435 return nil 436 elseif tonumber(ab) == -1 then 437 core.boot() 438 end 439 ab = tonumber(ab) or 10 440 441 local x = loader.getenv("loader_menu_timeout_x") or 5 442 local y = loader.getenv("loader_menu_timeout_y") or 22 443 444 local endtime = loader.time() + ab 445 local time 446 447 repeat 448 time = endtime - loader.time() 449 screen.setcursor(x, y) 450 print("Autoboot in " .. time .. 451 " seconds, hit [Enter] to boot" .. 452 " or any other key to stop ") 453 screen.defcursor() 454 if io.ischar() then 455 local ch = io.getchar() 456 if ch == core.KEY_ENTER then 457 break 458 else 459 -- erase autoboot msg 460 screen.setcursor(0, y) 461 print(string.rep(" ", 80)) 462 screen.defcursor() 463 return ch 464 end 465 end 466 467 loader.delay(50000) 468 until time <= 0 469 core.boot() 470 471end 472 473return menu 474