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