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 drawn_menu 42 43local function OnOff(str, value) 44 if value 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 menu.process(entry.submenu) 86 end, 87 [core.MENU_RETURN] = function(_, entry) 88 -- allow entry to have a function/side effect 89 if entry.func ~= nil then 90 entry.func() 91 end 92 return false 93 end, 94} 95-- loader menu tree is rooted at menu.welcome 96 97menu.boot_environments = { 98 entries = { 99 -- return to welcome menu 100 { 101 entry_type = core.MENU_RETURN, 102 name = "Back to main menu" .. 103 color.highlight(" [Backspace]"), 104 }, 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.escapef(color.GREEN) 119 else 120 name_color = color.escapef(color.BLUE) 121 end 122 bootenv_name = bootenv_name .. name_color .. 123 choice .. color.default() 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 { 153 entry_type = core.MENU_RETURN, 154 name = "Back to main menu" .. 155 color.highlight(" [Backspace]"), 156 }, 157 -- load defaults 158 { 159 entry_type = core.MENU_ENTRY, 160 name = "Load System " .. color.highlight("D") .. 161 "efaults", 162 func = core.setDefaults, 163 alias = {"d", "D"}, 164 }, 165 { 166 entry_type = core.MENU_SEPARATOR, 167 }, 168 { 169 entry_type = core.MENU_SEPARATOR, 170 name = "Boot Options:", 171 }, 172 -- acpi 173 { 174 entry_type = core.MENU_ENTRY, 175 visible = core.isSystem386, 176 name = function() 177 return OnOff(color.highlight("A") .. 178 "CPI :", core.acpi) 179 end, 180 func = core.setACPI, 181 alias = {"a", "A"}, 182 }, 183 -- safe mode 184 { 185 entry_type = core.MENU_ENTRY, 186 name = function() 187 return OnOff("Safe " .. color.highlight("M") .. 188 "ode :", core.sm) 189 end, 190 func = core.setSafeMode, 191 alias = {"m", "M"}, 192 }, 193 -- single user 194 { 195 entry_type = core.MENU_ENTRY, 196 name = function() 197 return OnOff(color.highlight("S") .. 198 "ingle user:", core.su) 199 end, 200 func = core.setSingleUser, 201 alias = {"s", "S"}, 202 }, 203 -- verbose boot 204 { 205 entry_type = core.MENU_ENTRY, 206 name = function() 207 return OnOff(color.highlight("V") .. 208 "erbose :", core.verbose) 209 end, 210 func = core.setVerbose, 211 alias = {"v", "V"}, 212 }, 213 }, 214} 215 216menu.welcome = { 217 entries = function() 218 local menu_entries = menu.welcome.all_entries 219 -- Swap the first two menu items on single user boot 220 if core.isSingleUserBoot() then 221 -- We'll cache the swapped menu, for performance 222 if menu.welcome.swapped_menu ~= nil then 223 return menu.welcome.swapped_menu 224 end 225 -- Shallow copy the table 226 menu_entries = core.deepCopyTable(menu_entries) 227 228 -- Swap the first two menu entries 229 menu_entries[1], menu_entries[2] = 230 menu_entries[2], menu_entries[1] 231 232 -- Then set their names to their alternate names 233 menu_entries[1].name, menu_entries[2].name = 234 menu_entries[1].alternate_name, 235 menu_entries[2].alternate_name 236 menu.welcome.swapped_menu = menu_entries 237 end 238 return menu_entries 239 end, 240 all_entries = { 241 -- boot multi user 242 { 243 entry_type = core.MENU_ENTRY, 244 name = color.highlight("B") .. "oot Multi user " .. 245 color.highlight("[Enter]"), 246 -- Not a standard menu entry function! 247 alternate_name = color.highlight("B") .. 248 "oot Multi user", 249 func = function() 250 core.setSingleUser(false) 251 core.boot() 252 end, 253 alias = {"b", "B"}, 254 }, 255 -- boot single user 256 { 257 entry_type = core.MENU_ENTRY, 258 name = "Boot " .. color.highlight("S") .. "ingle user", 259 -- Not a standard menu entry function! 260 alternate_name = "Boot " .. color.highlight("S") .. 261 "ingle user " .. color.highlight("[Enter]"), 262 func = function() 263 core.setSingleUser(true) 264 core.boot() 265 end, 266 alias = {"s", "S"}, 267 }, 268 -- escape to interpreter 269 { 270 entry_type = core.MENU_RETURN, 271 name = color.highlight("Esc") .. "ape to loader prompt", 272 func = function() 273 loader.setenv("autoboot_delay", "NO") 274 end, 275 alias = {core.KEYSTR_ESCAPE}, 276 }, 277 -- reboot 278 { 279 entry_type = core.MENU_ENTRY, 280 name = color.highlight("R") .. "eboot", 281 func = function() 282 loader.perform("reboot") 283 end, 284 alias = {"r", "R"}, 285 }, 286 { 287 entry_type = core.MENU_SEPARATOR, 288 }, 289 { 290 entry_type = core.MENU_SEPARATOR, 291 name = "Options:", 292 }, 293 -- kernel options 294 { 295 entry_type = core.MENU_CAROUSEL_ENTRY, 296 carousel_id = "kernel", 297 items = core.kernelList, 298 name = function(idx, choice, all_choices) 299 if #all_choices == 0 then 300 return "Kernel: " 301 end 302 303 local is_default = (idx == 1) 304 local kernel_name = "" 305 local name_color 306 if is_default then 307 name_color = color.escapef(color.GREEN) 308 kernel_name = "default/" 309 else 310 name_color = color.escapef(color.BLUE) 311 end 312 kernel_name = kernel_name .. name_color .. 313 choice .. color.default() 314 return color.highlight("K") .. "ernel: " .. 315 kernel_name .. " (" .. idx .. " of " .. 316 #all_choices .. ")" 317 end, 318 func = function(_, choice, _) 319 config.selectKernel(choice) 320 end, 321 alias = {"k", "K"}, 322 }, 323 -- boot options 324 { 325 entry_type = core.MENU_SUBMENU, 326 name = "Boot " .. color.highlight("O") .. "ptions", 327 submenu = menu.boot_options, 328 alias = {"o", "O"}, 329 }, 330 -- boot environments 331 { 332 entry_type = core.MENU_SUBMENU, 333 visible = function() 334 return core.isZFSBoot() and 335 #core.bootenvList() > 1 336 end, 337 name = "Boot " .. color.highlight("E") .. "nvironments", 338 submenu = menu.boot_environments, 339 alias = {"e", "E"}, 340 }, 341 }, 342} 343 344menu.default = menu.welcome 345-- current_alias_table will be used to keep our alias table consistent across 346-- screen redraws, instead of relying on whatever triggered the redraw to update 347-- the local alias_table in menu.process. 348menu.current_alias_table = {} 349 350function menu.draw(menudef) 351 -- Clear the screen, reset the cursor, then draw 352 screen.clear() 353 screen.defcursor() 354 menu.current_alias_table = drawer.drawscreen(menudef) 355 drawn_menu = menudef 356end 357 358-- 'keypress' allows the caller to indicate that a key has been pressed that we 359-- should process as our initial input. 360function menu.process(menudef, keypress) 361 assert(menudef ~= nil) 362 363 if drawn_menu ~= menudef then 364 menu.draw(menudef) 365 end 366 367 while true do 368 local key = keypress or io.getchar() 369 keypress = nil 370 371 -- Special key behaviors 372 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and 373 menudef ~= 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(menu.current_alias_table) do 384 if key == k then 385 sel_entry = v 386 break 387 end 388 end 389 390 -- if we have an alias do the assigned action: 391 if sel_entry ~= nil then 392 local handler = menu.handlers[sel_entry.entry_type] 393 assert(handler ~= nil) 394 -- The handler's return value indicates if we 395 -- need to exit this menu. An omitted or true 396 -- return value means to continue. 397 if handler(menudef, sel_entry) == false then 398 return 399 end 400 -- If we got an alias key the screen is out of date... 401 -- redraw it. 402 menu.draw(menudef) 403 end 404 end 405end 406 407function menu.run() 408 menu.draw(menu.default) 409 local autoboot_key = menu.autoboot() 410 411 menu.process(menu.default, autoboot_key) 412 drawn_menu = nil 413 414 screen.defcursor() 415 print("Exiting menu!") 416end 417 418function menu.autoboot() 419 local ab = loader.getenv("autoboot_delay") 420 if ab ~= nil and ab:lower() == "no" then 421 return nil 422 elseif tonumber(ab) == -1 then 423 core.boot() 424 end 425 ab = tonumber(ab) or 10 426 427 local x = loader.getenv("loader_menu_timeout_x") or 5 428 local y = loader.getenv("loader_menu_timeout_y") or 22 429 430 local endtime = loader.time() + ab 431 local time 432 433 repeat 434 time = endtime - loader.time() 435 screen.setcursor(x, y) 436 print("Autoboot in " .. time .. 437 " seconds, hit [Enter] to boot" .. 438 " or any other key to stop ") 439 screen.defcursor() 440 if io.ischar() then 441 local ch = io.getchar() 442 if ch == core.KEY_ENTER then 443 break 444 else 445 -- erase autoboot msg 446 screen.setcursor(0, y) 447 print(string.rep(" ", 80)) 448 screen.defcursor() 449 return ch 450 end 451 end 452 453 loader.delay(50000) 454 until time <= 0 455 core.boot() 456 457end 458 459return menu 460