1-- 2-- Copyright (c) 2015 Pedro Souza <[email protected]> 3-- Copyright (C) 2018 Kyle Evans <[email protected]> 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 skip 40local run 41local autoboot 42 43local OnOff = function(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 bootenvSet = function(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(current_menu, entry) 67 -- run function 68 entry.func() 69 end, 70 [core.MENU_CAROUSEL_ENTRY] = function(current_menu, 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(current_menu, entry) 85 -- recurse 86 return menu.run(entry.submenu) 87 end, 88 [core.MENU_RETURN] = function(current_menu, 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(idx, choice, all_choices) 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.shallowCopyTable(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(idx, choice, all_choices) 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 347function menu.run(m) 348 349 if menu.skip() then 350 core.autoboot() 351 return false 352 end 353 354 if m == nil then 355 m = menu.default 356 end 357 358 -- redraw screen 359 screen.clear() 360 screen.defcursor() 361 local alias_table = drawer.drawscreen(m) 362 363 -- Might return nil, that's ok 364 local autoboot_key; 365 if m == menu.default then 366 autoboot_key = menu.autoboot() 367 end 368 cont = true 369 while cont do 370 local key = autoboot_key or io.getchar() 371 autoboot_key = nil 372 373 -- Special key behaviors 374 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and 375 m ~= menu.default then 376 break 377 elseif key == core.KEY_ENTER then 378 core.boot() 379 -- Should not return 380 end 381 382 key = string.char(key) 383 -- check to see if key is an alias 384 local sel_entry = nil 385 for k, v in pairs(alias_table) do 386 if key == k then 387 sel_entry = v 388 end 389 end 390 391 -- if we have an alias do the assigned action: 392 if sel_entry ~= nil then 393 -- Get menu handler 394 local handler = menu.handlers[sel_entry.entry_type] 395 if handler ~= nil then 396 -- The handler's return value indicates whether 397 -- we need to exit this menu. An omitted return 398 -- value means "continue" by default. 399 cont = handler(m, sel_entry) 400 if cont == nil then 401 cont = true 402 end 403 end 404 -- if we got an alias key the screen is out of date: 405 screen.clear() 406 screen.defcursor() 407 alias_table = drawer.drawscreen(m) 408 end 409 end 410 411 if m == menu.default then 412 screen.defcursor() 413 print("Exiting menu!") 414 return false 415 end 416 417 return true 418end 419 420function menu.skip() 421 if core.isSerialBoot() then 422 return true 423 end 424 local c = string.lower(loader.getenv("console") or "") 425 if c:match("^efi[ ;]") ~= nil or c:match("[ ;]efi[ ;]") ~= nil then 426 return true 427 end 428 429 c = string.lower(loader.getenv("beastie_disable") or "") 430 print("beastie_disable", c) 431 return c == "yes" 432end 433 434function menu.autoboot() 435 local ab = loader.getenv("autoboot_delay") 436 if ab ~= nil and ab:lower() == "no" then 437 return nil 438 elseif tonumber(ab) == -1 then 439 core.boot() 440 end 441 ab = tonumber(ab) or 10 442 443 local x = loader.getenv("loader_menu_timeout_x") or 5 444 local y = loader.getenv("loader_menu_timeout_y") or 22 445 446 local endtime = loader.time() + ab 447 local time 448 449 repeat 450 time = endtime - loader.time() 451 screen.setcursor(x, y) 452 print("Autoboot in " .. time .. 453 " seconds, hit [Enter] to boot" .. 454 " or any other key to stop ") 455 screen.defcursor() 456 if io.ischar() then 457 local ch = io.getchar() 458 if ch == core.KEY_ENTER then 459 break 460 else 461 -- erase autoboot msg 462 screen.setcursor(0, y) 463 print(" " 464 .. " ") 465 screen.defcursor() 466 return ch 467 end 468 end 469 470 loader.delay(50000) 471 until time <= 0 472 core.boot() 473 474end 475 476return menu 477