]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - stand/lua/menu.lua
stand/powerpc: Only build loader.kboot for powerpc64
[FreeBSD/FreeBSD.git] / stand / lua / menu.lua
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 local cli = require("cli")
33 local core = require("core")
34 local color = require("color")
35 local config = require("config")
36 local screen = require("screen")
37 local drawer = require("drawer")
38
39 local menu = {}
40
41 local drawn_menu
42 local return_menu_entry = {
43         entry_type = core.MENU_RETURN,
44         name = "Back to main menu" .. color.highlight(" [Backspace]"),
45 }
46
47 local 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
55 end
56
57 local function bootenvSet(env)
58         loader.setenv("vfs.root.mountfrom", env)
59         loader.setenv("currdev", env .. ":")
60         config.reload()
61 end
62
63 -- Module exports
64 menu.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
101 menu.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
149 menu.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
212 menu.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                                 if loader.getenv("kernelname") ~= nil then
316                                         loader.perform("unload")
317                                 end
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                 -- chainload
341                 {
342                         entry_type = core.MENU_ENTRY,
343                         name = function()
344                                 return 'Chain' .. color.highlight("L") ..
345                                     "oad " .. loader.getenv('chain_disk')
346                         end,
347                         func = function()
348                                 loader.perform("chain " ..
349                                     loader.getenv('chain_disk'))
350                         end,
351                         visible = function()
352                                 return loader.getenv('chain_disk') ~= nil
353                         end,
354                         alias = {"l", "L"},
355                 },
356         },
357 }
358
359 menu.default = menu.welcome
360 -- current_alias_table will be used to keep our alias table consistent across
361 -- screen redraws, instead of relying on whatever triggered the redraw to update
362 -- the local alias_table in menu.process.
363 menu.current_alias_table = {}
364
365 function menu.draw(menudef)
366         -- Clear the screen, reset the cursor, then draw
367         screen.clear()
368         menu.current_alias_table = drawer.drawscreen(menudef)
369         drawn_menu = menudef
370         screen.defcursor()
371 end
372
373 -- 'keypress' allows the caller to indicate that a key has been pressed that we
374 -- should process as our initial input.
375 function menu.process(menudef, keypress)
376         assert(menudef ~= nil)
377
378         if drawn_menu ~= menudef then
379                 menu.draw(menudef)
380         end
381
382         while true do
383                 local key = keypress or io.getchar()
384                 keypress = nil
385
386                 -- Special key behaviors
387                 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
388                     menudef ~= menu.default then
389                         break
390                 elseif key == core.KEY_ENTER then
391                         core.boot()
392                         -- Should not return.  If it does, escape menu handling
393                         -- and drop to loader prompt.
394                         return false
395                 end
396
397                 key = string.char(key)
398                 -- check to see if key is an alias
399                 local sel_entry = nil
400                 for k, v in pairs(menu.current_alias_table) do
401                         if key == k then
402                                 sel_entry = v
403                                 break
404                         end
405                 end
406
407                 -- if we have an alias do the assigned action:
408                 if sel_entry ~= nil then
409                         local handler = menu.handlers[sel_entry.entry_type]
410                         assert(handler ~= nil)
411                         -- The handler's return value indicates if we
412                         -- need to exit this menu.  An omitted or true
413                         -- return value means to continue.
414                         if handler(menudef, sel_entry) == false then
415                                 return
416                         end
417                         -- If we got an alias key the screen is out of date...
418                         -- redraw it.
419                         menu.draw(menudef)
420                 end
421         end
422 end
423
424 function menu.run()
425         local autoboot_key
426         local delay = loader.getenv("autoboot_delay")
427
428         if delay ~= nil and delay:lower() == "no" then
429                 delay = nil
430         else
431                 delay = tonumber(delay) or 10
432         end
433
434         if delay == -1 then
435                 core.boot()
436                 return
437         end
438
439         menu.draw(menu.default)
440
441         if delay ~= nil then
442                 autoboot_key = menu.autoboot(delay)
443
444                 -- autoboot_key should return the key pressed.  It will only
445                 -- return nil if we hit the timeout and executed the timeout
446                 -- command.  Bail out.
447                 if autoboot_key == nil then
448                         return
449                 end
450         end
451
452         menu.process(menu.default, autoboot_key)
453         drawn_menu = nil
454
455         screen.defcursor()
456         print("Exiting menu!")
457 end
458
459 function menu.autoboot(delay)
460         local x = loader.getenv("loader_menu_timeout_x") or 4
461         local y = loader.getenv("loader_menu_timeout_y") or 23
462         local endtime = loader.time() + delay
463         local time
464         local last
465         repeat
466                 time = endtime - loader.time()
467                 if last == nil or last ~= time then
468                         last = time
469                         screen.setcursor(x, y)
470                         print("Autoboot in " .. time ..
471                             " seconds, hit [Enter] to boot" ..
472                             " or any other key to stop     ")
473                         screen.defcursor()
474                 end
475                 if io.ischar() then
476                         local ch = io.getchar()
477                         if ch == core.KEY_ENTER then
478                                 break
479                         else
480                                 -- erase autoboot msg
481                                 screen.setcursor(0, y)
482                                 print(string.rep(" ", 80))
483                                 screen.defcursor()
484                                 return ch
485                         end
486                 end
487
488                 loader.delay(50000)
489         until time <= 0
490
491         local cmd = loader.getenv("menu_timeout_command") or "boot"
492         cli_execute_unparsed(cmd)
493         return nil
494 end
495
496 -- CLI commands
497 function cli.menu()
498         menu.run()
499 end
500
501 return menu