1-- 2-- Copyright (c) 2015 Pedro Souza <pedrosouza@freebsd.org> 3-- Copyright (C) 2018 Kyle Evans <kevans@FreeBSD.org> 4-- All rights reserved. 5-- 6-- Redistribution and use in source and binary forms, with or without 7-- modification, are permitted provided that the following conditions 8-- are met: 9-- 1. Redistributions of source code must retain the above copyright 10-- notice, this list of conditions and the following disclaimer. 11-- 2. Redistributions in binary form must reproduce the above copyright 12-- notice, this list of conditions and the following disclaimer in the 13-- documentation and/or other materials provided with the distribution. 14-- 15-- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 16-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18-- ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 19-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 21-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 23-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 24-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25-- SUCH DAMAGE. 26-- 27-- $FreeBSD$ 28-- 29 30 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 OnOff = function(str, b) 40 if b then 41 return str .. color.escapef(color.GREEN) .. "On" .. 42 color.escapef(color.WHITE) 43 else 44 return str .. color.escapef(color.RED) .. "off" .. 45 color.escapef(color.WHITE) 46 end 47end 48 49local bootenvSet = function(env) 50 loader.setenv("vfs.root.mountfrom", env) 51 loader.setenv("currdev", env .. ":") 52 config.reload() 53end 54 55-- Module exports 56menu.handlers = { 57 -- Menu handlers take the current menu and selected entry as parameters, 58 -- and should return a boolean indicating whether execution should 59 -- continue or not. The return value may be omitted if this entry should 60 -- have no bearing on whether we continue or not, indicating that we 61 -- should just continue after execution. 62 [core.MENU_ENTRY] = function(current_menu, entry) 63 -- run function 64 entry.func() 65 end, 66 [core.MENU_CAROUSEL_ENTRY] = function(current_menu, entry) 67 -- carousel (rotating) functionality 68 local carid = entry.carousel_id 69 local caridx = config.getCarouselIndex(carid) 70 local choices = entry.items 71 if type(choices) == "function" then 72 choices = choices() 73 end 74 if #choices > 0 then 75 caridx = (caridx % #choices) + 1 76 config.setCarouselIndex(carid, caridx) 77 entry.func(caridx, choices[caridx], choices) 78 end 79 end, 80 [core.MENU_SUBMENU] = function(current_menu, entry) 81 -- recurse 82 return menu.run(entry.submenu) 83 end, 84 [core.MENU_RETURN] = function(current_menu, entry) 85 -- allow entry to have a function/side effect 86 if entry.func ~= nil then 87 entry.func() 88 end 89 return false 90 end, 91} 92-- loader menu tree is rooted at menu.welcome 93 94menu.boot_environments = { 95 entries = { 96 -- return to welcome menu 97 { 98 entry_type = core.MENU_RETURN, 99 name = "Back to main menu" .. 100 color.highlight(" [Backspace]"), 101 }, 102 { 103 entry_type = core.MENU_CAROUSEL_ENTRY, 104 carousel_id = "be_active", 105 items = core.bootenvList, 106 name = function(idx, choice, all_choices) 107 if #all_choices == 0 then 108 return "Active: " 109 end 110 111 local is_default = (idx == 1) 112 local bootenv_name = "" 113 local name_color 114 if is_default then 115 name_color = color.escapef(color.GREEN) 116 else 117 name_color = color.escapef(color.BLUE) 118 end 119 bootenv_name = bootenv_name .. name_color .. 120 choice .. color.default() 121 return color.highlight("A").."ctive: " .. 122 bootenv_name .. " (" .. idx .. " of " .. 123 #all_choices .. ")" 124 end, 125 func = function(idx, choice, all_choices) 126 bootenvSet(choice) 127 end, 128 alias = {"a", "A"}, 129 }, 130 { 131 entry_type = core.MENU_ENTRY, 132 name = function() 133 return color.highlight("b") .. "ootfs: " .. 134 core.bootenvDefault() 135 end, 136 func = function() 137 -- Reset active boot environment to the default 138 config.setCarouselIndex("be_active", 1) 139 bootenvSet(core.bootenvDefault()) 140 end, 141 alias = {"b", "B"}, 142 }, 143 }, 144} 145 146menu.boot_options = { 147 entries = { 148 -- return to welcome menu 149 { 150 entry_type = core.MENU_RETURN, 151 name = "Back to main menu" .. 152 color.highlight(" [Backspace]"), 153 }, 154 -- load defaults 155 { 156 entry_type = core.MENU_ENTRY, 157 name = "Load System " .. color.highlight("D") .. 158 "efaults", 159 func = core.setDefaults, 160 alias = {"d", "D"} 161 }, 162 { 163 entry_type = core.MENU_SEPARATOR, 164 }, 165 { 166 entry_type = core.MENU_SEPARATOR, 167 name = "Boot Options:", 168 }, 169 -- acpi 170 { 171 entry_type = core.MENU_ENTRY, 172 visible = core.isSystem386, 173 name = function() 174 return OnOff(color.highlight("A") .. 175 "CPI :", core.acpi) 176 end, 177 func = core.setACPI, 178 alias = {"a", "A"} 179 }, 180 -- safe mode 181 { 182 entry_type = core.MENU_ENTRY, 183 name = function() 184 return OnOff("Safe " .. color.highlight("M") .. 185 "ode :", core.sm) 186 end, 187 func = core.setSafeMode, 188 alias = {"m", "M"} 189 }, 190 -- single user 191 { 192 entry_type = core.MENU_ENTRY, 193 name = function() 194 return OnOff(color.highlight("S") .. 195 "ingle user:", core.su) 196 end, 197 func = core.setSingleUser, 198 alias = {"s", "S"} 199 }, 200 -- verbose boot 201 { 202 entry_type = core.MENU_ENTRY, 203 name = function() 204 return OnOff(color.highlight("V") .. 205 "erbose :", core.verbose) 206 end, 207 func = core.setVerbose, 208 alias = {"v", "V"} 209 }, 210 }, 211} 212 213menu.welcome = { 214 entries = function() 215 local menu_entries = menu.welcome.all_entries 216 -- Swap the first two menu items on single user boot 217 if core.isSingleUserBoot() then 218 -- We'll cache the swapped menu, for performance 219 if menu.welcome.swapped_menu ~= nil then 220 return menu.welcome.swapped_menu 221 end 222 -- Shallow copy the table 223 menu_entries = core.shallowCopyTable(menu_entries) 224 225 -- Swap the first two menu entries 226 menu_entries[1], menu_entries[2] = 227 menu_entries[2], menu_entries[1] 228 229 -- Then set their names to their alternate names 230 menu_entries[1].name, menu_entries[2].name = 231 menu_entries[1].alternate_name, 232 menu_entries[2].alternate_name 233 menu.welcome.swapped_menu = menu_entries 234 end 235 return menu_entries 236 end, 237 all_entries = { 238 -- boot multi user 239 { 240 entry_type = core.MENU_ENTRY, 241 name = color.highlight("B") .. "oot Multi user " .. 242 color.highlight("[Enter]"), 243 -- Not a standard menu entry function! 244 alternate_name = color.highlight("B") .. 245 "oot Multi user", 246 func = function() 247 core.setSingleUser(false) 248 core.boot() 249 end, 250 alias = {"b", "B"} 251 }, 252 -- boot single user 253 { 254 entry_type = core.MENU_ENTRY, 255 name = "Boot " .. color.highlight("S") .. "ingle user", 256 -- Not a standard menu entry function! 257 alternate_name = "Boot " .. color.highlight("S") .. 258 "ingle user " .. color.highlight("[Enter]"), 259 func = function() 260 core.setSingleUser(true) 261 core.boot() 262 end, 263 alias = {"s", "S"} 264 }, 265 -- escape to interpreter 266 { 267 entry_type = core.MENU_RETURN, 268 name = color.highlight("Esc") .. "ape to loader prompt", 269 func = function() 270 loader.setenv("autoboot_delay", "NO") 271 end, 272 alias = {core.KEYSTR_ESCAPE} 273 }, 274 -- reboot 275 { 276 entry_type = core.MENU_ENTRY, 277 name = color.highlight("R") .. "eboot", 278 func = function() 279 loader.perform("reboot") 280 end, 281 alias = {"r", "R"} 282 }, 283 { 284 entry_type = core.MENU_SEPARATOR, 285 }, 286 { 287 entry_type = core.MENU_SEPARATOR, 288 name = "Options:", 289 }, 290 -- kernel options 291 { 292 entry_type = core.MENU_CAROUSEL_ENTRY, 293 carousel_id = "kernel", 294 items = core.kernelList, 295 name = function(idx, choice, all_choices) 296 if #all_choices == 0 then 297 return "Kernel: " 298 end 299 300 local is_default = (idx == 1) 301 local kernel_name = "" 302 local name_color 303 if is_default then 304 name_color = color.escapef(color.GREEN) 305 kernel_name = "default/" 306 else 307 name_color = color.escapef(color.BLUE) 308 end 309 kernel_name = kernel_name .. name_color .. 310 choice .. color.default() 311 return color.highlight("K") .. "ernel: " .. 312 kernel_name .. " (" .. idx .. " of " .. 313 #all_choices .. ")" 314 end, 315 func = function(idx, choice, all_choices) 316 config.selectkernel(choice) 317 end, 318 alias = {"k", "K"} 319 }, 320 -- boot options 321 { 322 entry_type = core.MENU_SUBMENU, 323 name = "Boot " .. color.highlight("O") .. "ptions", 324 submenu = menu.boot_options, 325 alias = {"o", "O"} 326 }, 327 -- boot environments 328 { 329 entry_type = core.MENU_SUBMENU, 330 visible = function() 331 return core.isZFSBoot() and 332 #core.bootenvList() > 1 333 end, 334 name = "Boot " .. color.highlight("E") .. "nvironments", 335 submenu = menu.boot_environments, 336 alias = {"e", "E"}, 337 }, 338 }, 339} 340 341menu.default = menu.welcome 342 343function menu.run(m) 344 345 if menu.skip() then 346 core.autoboot() 347 return false 348 end 349 350 if m == nil then 351 m = menu.default 352 end 353 354 -- redraw screen 355 screen.clear() 356 screen.defcursor() 357 local alias_table = drawer.drawscreen(m) 358 359 -- Might return nil, that's ok 360 local autoboot_key; 361 if m == menu.default then 362 autoboot_key = menu.autoboot() 363 end 364 cont = true 365 while cont do 366 local key = autoboot_key or io.getchar() 367 autoboot_key = nil 368 369 -- Special key behaviors 370 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and 371 m ~= menu.default then 372 break 373 elseif key == core.KEY_ENTER then 374 core.boot() 375 -- Should not return 376 end 377 378 key = string.char(key) 379 -- check to see if key is an alias 380 local sel_entry = nil 381 for k, v in pairs(alias_table) do 382 if key == k then 383 sel_entry = v 384 end 385 end 386 387 -- if we have an alias do the assigned action: 388 if sel_entry ~= nil then 389 -- Get menu handler 390 local handler = menu.handlers[sel_entry.entry_type] 391 if handler ~= nil then 392 -- The handler's return value indicates whether 393 -- we need to exit this menu. An omitted return 394 -- value means "continue" by default. 395 cont = handler(m, sel_entry) 396 if cont == nil then 397 cont = true 398 end 399 end 400 -- if we got an alias key the screen is out of date: 401 screen.clear() 402 screen.defcursor() 403 alias_table = drawer.drawscreen(m) 404 end 405 end 406 407 if m == menu.default then 408 screen.defcursor() 409 print("Exiting menu!") 410 return false 411 end 412 413 return true 414end 415 416function menu.skip() 417 if core.isSerialBoot() then 418 return true 419 end 420 local c = string.lower(loader.getenv("console") or "") 421 if c:match("^efi[ ;]") ~= nil or c:match("[ ;]efi[ ;]") ~= nil then 422 return true 423 end 424 425 c = string.lower(loader.getenv("beastie_disable") or "") 426 print("beastie_disable", c) 427 return c == "yes" 428end 429 430function menu.autoboot() 431 local ab = loader.getenv("autoboot_delay") 432 if ab ~= nil and ab:lower() == "no" then 433 return nil 434 elseif tonumber(ab) == -1 then 435 core.boot() 436 end 437 ab = tonumber(ab) or 10 438 439 local x = loader.getenv("loader_menu_timeout_x") or 5 440 local y = loader.getenv("loader_menu_timeout_y") or 22 441 442 local endtime = loader.time() + ab 443 local time 444 445 repeat 446 time = endtime - loader.time() 447 screen.setcursor(x, y) 448 print("Autoboot in " .. time .. 449 " seconds, hit [Enter] to boot" .. 450 " or any other key to stop ") 451 screen.defcursor() 452 if io.ischar() then 453 local ch = io.getchar() 454 if ch == core.KEY_ENTER then 455 break 456 else 457 -- erase autoboot msg 458 screen.setcursor(0, y) 459 print(" " 460 .. " ") 461 screen.defcursor() 462 return ch 463 end 464 end 465 466 loader.delay(50000) 467 until time <= 0 468 core.boot() 469 470end 471 472return menu 473